Fix: Phoenixd incorrectly marks payment as partial (#7325)
What changed, and why it matters
This commit updates several software libraries and changes how BTCPay Server reads Bitcoin wallet descriptors (the strings that define which addresses belong to a wallet). The title says it fixes a bug where Phoenixd lightning payments were wrongly marked as partial, but the actual code changes mostly replace an older output-descriptor parser with a simpler one and update documentation examples. The security relevance is unclear from the diff alone: it could be a routine bug fix, or the library updates could include security patches. There is no direct evidence in the commit of an exploit or a disclosed vulnerability.
Treat this as a maintenance/bug-fix commit with possible indirect security implications from dependency updates. Review the changelogs for NBitcoin 10.0.1, NBitcoin.Altcoins 6.0.3, BTCPayServer.Lightning.All 1.6.15, and MailKit 4.16.0 to identify any security fixes. Validate that the simplified descriptor parser still rejects malformed or unsupported descriptors correctly, because the removal of explicit type-specific checks could broaden accepted input. No immediate emergency response is warranted based solely on this diff.
Security signals we found
Dependency version bumps in NBitcoin, NBitcoin.Altcoins, BTCPayServer.Lightning.All, and MailKit
Replacement of custom output-descriptor parsing with a simpler path-conversion wrapper
Removal of explicit handling for many descriptor types (pkh, sh, wpkh, wsh, tr, multi) in legacy parser
Test case changed from single-path `/0/*` to multi-path `/<0;1>/*` descriptor
UI documentation updated to recommend `/**` descriptors with master fingerprints
Evidence from the diff
The commit bumps NBitcoin 9.0.5→10.0.1, NBitcoin.Altcoins 5.0.1→6.0.3, BTCPayServer.Lightning.All 1.6.14→1.6.15, and MailKit 4.15.1→4.16.0. It replaces the custom ParseLegacyOutputDescriptor implementation in DerivationSchemeParser.cs with a much shorter wrapper that converts /0/* to /**, strips checksums, and delegates to ParseMiniscript. Test and UI examples are updated to use /** descriptors with master fingerprints. The stated purpose is to fix Phoenixd incorrectly marking payments as partial, but the diff does not show any Phoenixd-specific logic; the fix likely comes from the BTCPayServer.Lightning.All bump. No CVE, advisory, or security explanation is present in the commit materials.
Changed components
BTCPayServer/DerivationSchemeParser.csBTCPayServer.Client/BTCPayServer.Client.csprojBTCPayServer.Data/BTCPayServer.Data.csprojBTCPayServer.Rating/BTCPayServer.Rating.csprojBTCPayServer/BTCPayServer.csprojBTCPayServer.Tests/FastTests.csBTCPayServer/Views/UIStores/ImportWallet/Xpub.cshtmlInspect captured patch +22 / −97
diff --git a/BTCPayServer.Client/BTCPayServer.Client.csproj b/BTCPayServer.Client/BTCPayServer.Client.csproj
index 08aecfb..bb31376 100644
--- a/BTCPayServer.Client/BTCPayServer.Client.csproj
+++ b/BTCPayServer.Client/BTCPayServer.Client.csproj
@@ -31,7 +31,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.5.2" />
- <PackageReference Include="NBitcoin" Version="9.0.5" />
+ <PackageReference Include="NBitcoin" Version="10.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
</ItemGroup>
<ItemGroup>
diff --git a/BTCPayServer.Data/BTCPayServer.Data.csproj b/BTCPayServer.Data/BTCPayServer.Data.csproj
index 39f63e1..79ae4e8 100644
--- a/BTCPayServer.Data/BTCPayServer.Data.csproj
+++ b/BTCPayServer.Data/BTCPayServer.Data.csproj
@@ -8,7 +8,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.4" />
- <PackageReference Include="NBitcoin.Altcoins" Version="5.0.1" />
+ <PackageReference Include="NBitcoin.Altcoins" Version="6.0.3" />
<PackageReference Include="Dapper" Version="2.1.72" />
</ItemGroup>
<ItemGroup>
diff --git a/BTCPayServer.Rating/BTCPayServer.Rating.csproj b/BTCPayServer.Rating/BTCPayServer.Rating.csproj
index 4740cb7..0c1cbef 100644
--- a/BTCPayServer.Rating/BTCPayServer.Rating.csproj
+++ b/BTCPayServer.Rating/BTCPayServer.Rating.csproj
@@ -6,7 +6,7 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
- <PackageReference Include="NBitcoin" Version="9.0.5" />
+ <PackageReference Include="NBitcoin" Version="10.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.2.1" />
</ItemGroup>
diff --git a/BTCPayServer.Tests/FastTests.cs b/BTCPayServer.Tests/FastTests.cs
index a6b9173..70f9d4e 100644
--- a/BTCPayServer.Tests/FastTests.cs
+++ b/BTCPayServer.Tests/FastTests.cs
@@ -2192,9 +2192,8 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
//a master fingerprint must always be present if youre providing rooted path
Assert.ThrowsAny<FormatException>(() => mainnetParser.ParseOD("pkh([44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)"));
-
parsedDescriptor = mainnetParser.ParseOD(
- "pkh(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)");
+ "pkh([d34db33f]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/<0;1>/*)");
Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]", parsedDescriptor.AccountDerivation.ToString());
//but a different deriv path from standard (0/*) is not supported
diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj
index cc5c625..29b2ae1 100644
--- a/BTCPayServer/BTCPayServer.csproj
+++ b/BTCPayServer/BTCPayServer.csproj
@@ -34,16 +34,16 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="NBitcoin" Version="9.0.5" />
+ <PackageReference Include="NBitcoin" Version="10.0.1" />
<PackageReference Include="YamlDotNet" Version="16.3.0" />
<PackageReference Include="BIP78.Sender" Version="0.2.5" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.6" />
- <PackageReference Include="BTCPayServer.Lightning.All" Version="1.6.14" />
+ <PackageReference Include="BTCPayServer.Lightning.All" Version="1.6.15" />
<PackageReference Include="CsvHelper" Version="33.1.0" />
<PackageReference Include="Fido2" Version="4.0.0" />
<PackageReference Include="Fido2.AspNet" Version="4.0.0" />
<PackageReference Include="LNURL" Version="0.0.36" />
- <PackageReference Include="MailKit" Version="4.15.1" />
+ <PackageReference Include="MailKit" Version="4.16.0" />
<PackageReference Include="QRCoder" Version="1.7.0" />
<PackageReference Include="NBitpayClient" Version="1.0.0.39" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
diff --git a/BTCPayServer/DerivationSchemeParser.cs b/BTCPayServer/DerivationSchemeParser.cs
index cd7c3a2..8d399d5 100644
--- a/BTCPayServer/DerivationSchemeParser.cs
+++ b/BTCPayServer/DerivationSchemeParser.cs
@@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.RegularExpressions;
using NBitcoin;
-using NBitcoin.Scripting;
using NBitcoin.WalletPolicies;
using NBXplorer.DerivationStrategy;
using static NBitcoin.WalletPolicies.MiniscriptNode;
@@ -243,81 +242,17 @@ namespace BTCPayServer
}
+ // Parse output descriptor with bitcoin core format (eg. p2sh(xpub/0/*))
private (DerivationStrategyBase, RootedKeyPath[]) ParseLegacyOutputDescriptor(string str)
{
- (DerivationStrategyBase, RootedKeyPath[]) ExtractFromPkProvider(PubKeyProvider pubKeyProvider,
- string suffix = "")
- {
- switch (pubKeyProvider)
- {
- case PubKeyProvider.Const _:
- throw new FormatException("Only HD output descriptors are supported.");
- case PubKeyProvider.HD hd:
- if (hd.Path != null && hd.Path.ToString() != "0")
- {
- throw new FormatException("Custom change paths are not supported.");
- }
- return (Parse($"{hd.Extkey}{suffix}"), null);
- case PubKeyProvider.Origin origin:
- var innerResult = ExtractFromPkProvider(origin.Inner, suffix);
- return (innerResult.Item1, new[] { origin.KeyOriginInfo });
- default:
- throw new ArgumentOutOfRangeException();
- }
- }
-
- (DerivationStrategyBase, RootedKeyPath[]) ExtractFromMulti(OutputDescriptor.Multi multi)
- {
- var xpubs = multi.PkProviders.Select(provider => ExtractFromPkProvider(provider));
- return (
- Parse(
- $"{multi.Threshold}-of-{(string.Join('-', xpubs.Select(tuple => tuple.Item1.ToString())))}{(multi.IsSorted ? "" : "-[keeporder]")}"),
- xpubs.SelectMany(tuple => tuple.Item2).ToArray());
- }
-
- ArgumentNullException.ThrowIfNull(str);
- str = str.Trim();
-
- //nbitcoin output descriptor does not support taproot, so let's check if it is a taproot descriptor and fake until it is supported
- var outputDescriptor = OutputDescriptor.Parse(str, Network);
- switch (outputDescriptor)
- {
- case OutputDescriptor.PK _:
- case OutputDescriptor.Raw _:
- case OutputDescriptor.Addr _:
- throw new FormatException("Only HD output descriptors are supported.");
- case OutputDescriptor.Combo _:
- throw new FormatException("Only output descriptors of one format are supported.");
- case OutputDescriptor.Multi multi:
- return ExtractFromMulti(multi);
- case OutputDescriptor.PKH pkh:
- return ExtractFromPkProvider(pkh.PkProvider, "-[legacy]");
- case OutputDescriptor.SH sh:
- var suffix = "-[p2sh]";
- if (sh.Inner is OutputDescriptor.Multi)
- {
- //non segwit
- suffix = "-[legacy]";
- }
-
- if (sh.Inner is OutputDescriptor.Multi || sh.Inner is OutputDescriptor.WPKH ||
- sh.Inner is OutputDescriptor.WSH)
- {
- var ds = ParseLegacyOutputDescriptor(sh.Inner.ToString());
- return (Parse(ds.Item1 + suffix), ds.Item2);
- };
- throw new FormatException("sh descriptors are only supported with multsig(legacy or p2wsh) and segwit(p2wpkh)");
- case OutputDescriptor.Tr tr:
- return ExtractFromPkProvider(tr.InnerPubkey, "-[taproot]");
- case OutputDescriptor.WPKH wpkh:
- return ExtractFromPkProvider(wpkh.PkProvider);
- case OutputDescriptor.WSH { Inner: OutputDescriptor.Multi multi }:
- return ExtractFromMulti(multi);
- case OutputDescriptor.WSH:
- throw new FormatException("wsh descriptors are only supported with multisig");
- default:
- throw new ArgumentOutOfRangeException(nameof(outputDescriptor));
- }
+ if (!str.Contains("0/*"))
+ throw new FormatException("Invalid output descriptor");
+ str = str.Replace("0/*", "**");
+ // Strip everything after "#" character
+ var hashIndex = str.IndexOf('#');
+ if (hashIndex >= 0)
+ str = str.Substring(0, hashIndex);
+ return ParseMiniscript(str);
}
public DerivationStrategyBase Parse(string str)
diff --git a/BTCPayServer/Views/UIStores/ImportWallet/Xpub.cshtml b/BTCPayServer/Views/UIStores/ImportWallet/Xpub.cshtml
index 185d598..89e037c 100644
--- a/BTCPayServer/Views/UIStores/ImportWallet/Xpub.cshtml
+++ b/BTCPayServer/Views/UIStores/ImportWallet/Xpub.cshtml
@@ -55,10 +55,7 @@
<td class="font-monospace">zpub…</td>
</tr>
<tr>
- <td class="font-monospace">wpkh(xpub…/0/*)</td>
- </tr>
- <tr>
- <td class="font-monospace">wpkh([…/84'/0'/0']xpub…/0/*)</td>
+ <td class="font-monospace">wpkh([…/84'/0'/0']xpub…/**)</td>
</tr>
<tr>
<td rowspan="4">P2SH-P2WPKH</td>
@@ -68,10 +65,7 @@
<td class="font-monospace">ypub…</td>
</tr>
<tr>
- <td class="font-monospace">sh(wpkh(xpub…/0/*)</td>
- </tr>
- <tr>
- <td class="font-monospace">sh(wpkh([…/49'/0'/0']xpub…/0/*)</td>
+ <td class="font-monospace">sh(wpkh([89abcdef/49'/0'/0']xpub…/**))</td>
</tr>
}
<tr>
@@ -79,10 +73,7 @@
<td class="font-monospace">xpub…-[legacy]</td>
</tr>
<tr>
- <td class="font-monospace">pkh([…/44'/0'/0']xpub…/0/*)</td>
- </tr>
- <tr>
- <td class="font-monospace">pkh(xpub…/0/*)</td>
+ <td class="font-monospace">pkh([89abcdef/44'/0'/0']xpub…/**)</td>
</tr>
@if (Model.SupportTaproot)
{
@@ -98,21 +89,21 @@
<td class="font-monospace">2-of-xpub1…-xpub2…</td>
</tr>
<tr>
- <td class="font-monospace">wsh(multi(2,<br>[…/48'/0'/0'/2']xpub…/0/*,<br>[…/48'/0'/0'/2']xpub…/0/*))</td>
+ <td class="font-monospace">wsh(multi(2,<br>[89abcdef/48'/0'/0'/2']xpub…/**,<br>[89abcdef/48'/0'/0'/2']xpub…/**))</td>
</tr>
<tr>
<td class="text-nowrap" rowspan="2">Multi-sig P2SH-P2WSH</td>
<td class="font-monospace">2-of-xpub1…-xpub2…-[p2sh]</td>
</tr>
<tr>
- <td class="font-monospace">sh(wsh(multi(2,<br>[…/48'/0'/0'/1']xpub…/0/*,<br>[…/48'/0'/0'/1']xpub…/0/*)))</td>
+ <td class="font-monospace">sh(wsh(multi(2,<br>[89abcdef/48'/0'/0'/1']xpub…/**,<br>[89abcdef/48'/0'/0'/1']xpub…/**)))</td>
</tr>
<tr>
<td class="text-nowrap" rowspan="2">Multi-sig P2SH</td>
<td class="font-monospace">2-of-xpub1…-xpub2…-[legacy]</td>
</tr>
<tr>
- <td class="font-monospace">sh(multi(2,<br>[…/45'/0]xpub…/0/*,<br>[…/45'/0]xpub…/0/*))</td>
+ <td class="font-monospace">sh(multi(2,<br>[89abcdef/45'/0]xpub…/**,<br>[89abcdef/45'/0]xpub…/**))</td>
</tr>
</tbody>
</table>
Why this scored 34/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.