psbt: Fix tapscript witness signature ordering
What changed, and why it matters
This commit fixes example code and a test helper that builds Bitcoin Taproot script-path witness data. The old code placed digital signatures in the wrong order on the transaction witness stack, following the order of an internal sorted map instead of the reverse order required by the Bitcoin Taproot specification (BIP-0342). A transaction finalized with the old ordering would be rejected by the Bitcoin network, so no funds could be stolen, but users copying the example could produce invalid transactions. The fix reads public keys from the script in their actual script order, then pushes the matching signatures in reverse.
Review any local copies or forks of the taproot-psbt example and psbt-sign-taproot test helper that finalize PSBTs for Taproot script-path spends; ensure signatures are pushed in reverse script-key order. The library itself is not patched here, so downstream projects using rust-bitcoin's PSBT APIs should verify their own finalization logic. No emergency upgrade is required solely because of this commit.
Security signals we found
Incorrect witness ordering in Taproot script-path spend construction
Violation of BIP-0342 stack ordering requirement
Fix located in example/test code rather than production library
No cryptographic weakness introduced; change corrects a protocol-formatting bug
Evidence from the diff
BIP-0342 script-path spends require witness stack signatures in reverse of the public-key order in the leaf script, because the stack is LIFO. The previous code iterated input.tap_script_sigs, a BTreeMap keyed by (x-only pubkey, leaf_hash), so signatures were pushed in sorted-pubkey order. The corrected code parses each leaf script, collects valid 32-byte x-only pubkeys in script order, computes the leaf hash, then pushes signatures from input.tap_script_sigs in reverse pubkey order, followed by the script and control block. The change only affects bitcoin/examples/taproot-psbt.rs and bitcoin/tests/psbt-sign-taproot.rs; no library finalizer logic is changed.
Changed components
bitcoin/examples/taproot-psbt.rsbitcoin/tests/psbt-sign-taproot.rsInspect captured patch +49 / −7
diff --git a/bitcoin/examples/taproot-psbt.rs b/bitcoin/examples/taproot-psbt.rs
index 2b5ef494..7633985e 100644
--- a/bitcoin/examples/taproot-psbt.rs
+++ b/bitcoin/examples/taproot-psbt.rs
@@ -677,10 +677,31 @@ impl BeneficiaryWallet {
// FINALIZER
psbt.inputs.iter_mut().for_each(|input| {
let mut script_witness: Witness = Witness::new();
- for (_, signature) in input.tap_script_sigs.iter() {
- script_witness.push(signature.to_vec());
- }
for (control_block, (script, _)) in input.tap_scripts.iter() {
+ // Extract 32-byte script pushes that validate as pubkeys, preserving script order
+ let mut pubkeys_in_order = Vec::new();
+ for instruction in script.instructions().flatten() {
+ if let script::Instruction::PushBytes(push_bytes) = instruction {
+ if push_bytes.len() == 32 {
+ let candidate_bytes: [u8; 32] =
+ push_bytes.as_bytes().try_into().expect("length checked above");
+ if let Ok(pubkey) = XOnlyPublicKey::from_byte_array(&candidate_bytes) {
+ pubkeys_in_order.push(pubkey);
+ }
+ }
+ }
+ }
+
+ let leaf_hash = script.tapscript_leaf_hash();
+
+ // Push signatures in reverse order
+ for pubkey in pubkeys_in_order.iter().rev() {
+ if let Some(sig) = input.tap_script_sigs.get(&(*pubkey, leaf_hash)) {
+ script_witness.push(sig.to_vec());
+ }
+ }
+
+ // Push script and control block
script_witness.push(script.to_vec());
script_witness.push(control_block.serialize());
}
diff --git a/bitcoin/tests/psbt-sign-taproot.rs b/bitcoin/tests/psbt-sign-taproot.rs
index 294d9276..f9a9a0a0 100644
--- a/bitcoin/tests/psbt-sign-taproot.rs
+++ b/bitcoin/tests/psbt-sign-taproot.rs
@@ -8,7 +8,7 @@ use bitcoin::bip32::{DerivationPath, Fingerprint};
use bitcoin::consensus::encode::serialize_hex;
use bitcoin::opcodes::all::OP_CHECKSIG;
use bitcoin::psbt::{GetKey, Input, KeyRequest, PsbtSighashType, SignError};
-use bitcoin::script::TapScriptExt as _;
+use bitcoin::script::{ScriptExt, TapScriptExt as _};
use bitcoin::taproot::{LeafVersion, TaprootBuilder, TaprootSpendInfo};
use bitcoin::transaction::Version;
use bitcoin::{
@@ -331,10 +331,31 @@ fn create_psbt_for_taproot_script_path_spend<K: Into<XOnlyPublicKey>>(
fn finalize_psbt_for_script_path_spend(mut psbt: Psbt) -> Psbt {
psbt.inputs.iter_mut().for_each(|input| {
let mut script_witness: Witness = Witness::new();
- for (_, signature) in input.tap_script_sigs.iter() {
- script_witness.push(signature.to_vec());
- }
for (control_block, (script, _)) in input.tap_scripts.iter() {
+ // Extract 32-byte script pushes that validate as pubkeys, preserving script order
+ let mut pubkeys_in_order = Vec::new();
+ for instruction in script.instructions().flatten() {
+ if let script::Instruction::PushBytes(push_bytes) = instruction {
+ if push_bytes.len() == 32 {
+ let candidate_bytes: [u8; 32] =
+ push_bytes.as_bytes().try_into().expect("length checked above");
+ if let Ok(pubkey) = XOnlyPublicKey::from_byte_array(&candidate_bytes) {
+ pubkeys_in_order.push(pubkey);
+ }
+ }
+ }
+ }
+
+ let leaf_hash = script.tapscript_leaf_hash();
+
+ // Push signatures in reverse order
+ for pubkey in pubkeys_in_order.iter().rev() {
+ if let Some(sig) = input.tap_script_sigs.get(&(*pubkey, leaf_hash)) {
+ script_witness.push(sig.to_vec());
+ }
+ }
+
+ // Push script and control block
script_witness.push(script.to_vec());
script_witness.push(control_block.serialize());
}
Why this scored 45/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.