What changed, and why it matters
This commit changes how BTCPay Server decides whether a Bitcoin transaction input is a SegWit (witness) type when building PSBTs (Partially Signed Bitcoin Transactions). Previously the code only recognized three specific SegWit formats: native SegWit pay-to-pubkey-hash, native SegWit pay-to-script-hash, and Taproot. The new code uses a broader check that covers any script type classified as a witness script, including possible future SegWit versions. It also adds calls to fill in the 'witness UTXO' field in two wallet controller flows that previously did not do so. The practical effect is to make PSBTs more complete and compatible with future Bitcoin upgrades, and to reduce the chance that a wallet or signer will fall back to less secure non-witness data handling.
Treat as a hardening/compatibility fix. Review whether other PSBT creation paths in the codebase also need AddWitnessUtxoToSegwitInputs(), and verify that downstream signers/hardware wallets handle generic ScriptType.Witness inputs correctly. No immediate incident response is indicated, but the change should be included in release notes as a robustness improvement.
Security signals we found
Broadens SegWit detection from a fixed allow-list to any ScriptType.Witness, mitigating future-version compatibility issues
Adds missing WitnessUtxo population in two wallet PSBT creation paths
Witness UTXO data reduces reliance on NonWitnessUtxo (full previous transaction) for SegWit inputs, which is a known PSBT hardening recommendation
Test coverage added for a future witness version (OP_2 witness program)
Evidence from the diff
PSBTExtensions.cs replaces a hand-rolled IsSegwit() helper that only matched PayToWitPubKeyHashTemplate, PayToWitScriptHashTemplate, and PayToTaprootTemplate with ScriptPubKey.IsScriptType(ScriptType.Witness). This generic check covers all witness v0+ scriptPubKeys, including future/unknown witness versions such as the new test case (PayToWitTemplate.Instance.GenerateScriptPubKey(OpcodeType.OP_2, new byte[32])). UIWalletsController.cs now invokes AddWitnessUtxoToSegwitInputs() after CreatePSBTAsync in two places (the wallet send and wallet pull payment flows, based on line context), ensuring witness UTXOs are populated for SegWit inputs. The unit test is updated to assert that inputs 0-4 receive WitnessUtxo, while the non-SegWit P2SH input remains null.
Changed components
BTCPayServer/Extensions/PSBTExtensions.csBTCPayServer/Plugins/Wallets/Controllers/UIWalletsController.csBTCPayServer.Tests/PSBTTests.csInspect captured patch +10 / −13
diff --git a/BTCPayServer.Tests/PSBTTests.cs b/BTCPayServer.Tests/PSBTTests.cs
index 68ef078..c29df0c 100644
--- a/BTCPayServer.Tests/PSBTTests.cs
+++ b/BTCPayServer.Tests/PSBTTests.cs
@@ -20,6 +20,7 @@ namespace BTCPayServer.Tests
var nestedP2wshRedeem = witnessScript.WitHash.ScriptPubKey;
var nestedP2wsh = nestedP2wshRedeem.Hash.ScriptPubKey;
var taproot = new Key().PubKey.GetScriptPubKey(ScriptPubKeyType.TaprootBIP86);
+ var futureWitness = PayToWitTemplate.Instance.GenerateScriptPubKey(OpcodeType.OP_2, new byte[32]);
var legacyP2sh = witnessScript.Hash.ScriptPubKey;
var previousTransaction = network.CreateTransaction();
@@ -27,7 +28,8 @@ namespace BTCPayServer.Tests
previousTransaction.Outputs.Add(Money.Coins(1.1m), nativeP2wpkh);
previousTransaction.Outputs.Add(Money.Coins(1.2m), nestedP2wsh);
previousTransaction.Outputs.Add(Money.Coins(1.3m), taproot);
- previousTransaction.Outputs.Add(Money.Coins(1.4m), legacyP2sh);
+ previousTransaction.Outputs.Add(Money.Coins(1.4m), futureWitness);
+ previousTransaction.Outputs.Add(Money.Coins(1.5m), legacyP2sh);
var spendingTransaction = network.CreateTransaction();
for (var i = 0; i < previousTransaction.Outputs.Count; i++)
@@ -40,18 +42,18 @@ namespace BTCPayServer.Tests
psbt.Inputs[0].WitnessScript = witnessScript;
psbt.Inputs[2].RedeemScript = nestedP2wshRedeem;
psbt.Inputs[2].WitnessScript = witnessScript;
- psbt.Inputs[4].RedeemScript = witnessScript;
+ psbt.Inputs[5].RedeemScript = witnessScript;
Assert.All(psbt.Inputs, input => Assert.Null(input.WitnessUtxo));
psbt.AddWitnessUtxoToSegwitInputs();
- for (var i = 0; i < 4; i++)
+ for (var i = 0; i < 5; i++)
{
Assert.NotNull(psbt.Inputs[i].WitnessUtxo);
Assert.Equal(previousTransaction.Outputs[i], psbt.Inputs[i].WitnessUtxo);
}
- Assert.Null(psbt.Inputs[4].WitnessUtxo);
+ Assert.Null(psbt.Inputs[5].WitnessUtxo);
Assert.All(psbt.Inputs, input => Assert.Same(previousTransaction, input.NonWitnessUtxo));
}
diff --git a/BTCPayServer/Extensions/PSBTExtensions.cs b/BTCPayServer/Extensions/PSBTExtensions.cs
index c487a0d..6217150 100644
--- a/BTCPayServer/Extensions/PSBTExtensions.cs
+++ b/BTCPayServer/Extensions/PSBTExtensions.cs
@@ -26,9 +26,9 @@ namespace BTCPayServer
var previousOutput = nonWitnessUtxo.Outputs[(int)input.PrevOut.N];
var redeemScript = input.RedeemScript;
- if (IsSegwit(previousOutput.ScriptPubKey) ||
+ if (previousOutput.ScriptPubKey.IsScriptType(ScriptType.Witness) ||
redeemScript is not null &&
- IsSegwit(redeemScript) &&
+ redeemScript.IsScriptType(ScriptType.Witness) &&
redeemScript.Hash.ScriptPubKey == previousOutput.ScriptPubKey)
{
input.WitnessUtxo = previousOutput;
@@ -37,12 +37,5 @@ namespace BTCPayServer
return psbt;
}
-
- private static bool IsSegwit(Script scriptPubKey)
- {
- return PayToWitPubKeyHashTemplate.Instance.CheckScriptPubKey(scriptPubKey) ||
- PayToWitScriptHashTemplate.Instance.CheckScriptPubKey(scriptPubKey) ||
- PayToTaprootTemplate.Instance.CheckScriptPubKey(scriptPubKey);
- }
}
}
diff --git a/BTCPayServer/Plugins/Wallets/Controllers/UIWalletsController.cs b/BTCPayServer/Plugins/Wallets/Controllers/UIWalletsController.cs
index 395d55c..1a70f01 100644
--- a/BTCPayServer/Plugins/Wallets/Controllers/UIWalletsController.cs
+++ b/BTCPayServer/Plugins/Wallets/Controllers/UIWalletsController.cs
@@ -304,6 +304,7 @@ namespace BTCPayServer.Controllers
try
{
var psbtResponse = await explorer.CreatePSBTAsync(paymentMethod.AccountDerivation, createPSBT, cancellationToken);
+ psbtResponse.PSBT.AddWitnessUtxoToSegwitInputs();
signingContext = new SigningContextModel
{
@@ -384,6 +385,7 @@ namespace BTCPayServer.Controllers
try
{
var psbtResponse = await explorer.CreatePSBTAsync(paymentMethod.AccountDerivation, createPSBT, cancellationToken);
+ psbtResponse.PSBT.AddWitnessUtxoToSegwitInputs();
signingContext = new SigningContextModel
{
Why this scored 57/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.