Skip to content

Commit 3a7398e

Browse files
evanlinjinclaude
andcommitted
plan: Append witness script for P2WSH in Plan::satisfy()
Fix Plan::satisfy() to correctly append the witnessScript as the final witness stack element for P2WSH descriptor types (Wsh, WshSortedMulti, ShWsh, ShWshSortedMulti). Previously, these types were incorrectly grouped with Wpkh and Tr, which don't require a trailing witness script. This caused transactions built using Plan::satisfy() to fail validation with "Witness program hash mismatch" when broadcast. The fix separates the descriptor type handling: - Wpkh/Tr: return stack as-is (no witness script needed) - Wsh/WshSortedMulti: append witness script, empty script_sig - ShWpkh: return stack with unsigned_script_sig (no witness script) - ShWsh/ShWshSortedMulti: append witness script and unsigned_script_sig Closes #896 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 3ddad52 commit 3a7398e

1 file changed

Lines changed: 96 additions & 4 deletions

File tree

src/plan.rs

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -292,11 +292,18 @@ impl Plan {
292292
})
293293
.into_script(),
294294
),
295-
DescriptorType::Wpkh
296-
| DescriptorType::Wsh
295+
DescriptorType::Wpkh | DescriptorType::Tr => (stack, ScriptBuf::new()),
296+
DescriptorType::ShWpkh => (stack, self.descriptor.unsigned_script_sig()),
297+
DescriptorType::Wsh
297298
| DescriptorType::WshSortedMulti
298-
| DescriptorType::Tr => (stack, ScriptBuf::new()),
299-
DescriptorType::ShWsh | DescriptorType::ShWshSortedMulti | DescriptorType::ShWpkh => {
299+
| DescriptorType::ShWsh
300+
| DescriptorType::ShWshSortedMulti => {
301+
let mut stack = stack;
302+
let witness_script = self
303+
.descriptor
304+
.explicit_script()
305+
.expect("wsh descriptors have explicit script");
306+
stack.push(witness_script.into_bytes());
300307
(stack, self.descriptor.unsigned_script_sig())
301308
}
302309
})
@@ -1154,4 +1161,89 @@ mod test {
11541161
assert!(psbt_input.redeem_script.is_none(), "Redeem script present");
11551162
assert_eq!(psbt_input.bip32_derivation.len(), 2, "Unexpected number of bip32_derivation");
11561163
}
1164+
1165+
#[test]
1166+
fn test_plan_satisfy_wsh() {
1167+
use std::collections::BTreeMap;
1168+
1169+
use bitcoin::secp256k1::{self, Secp256k1};
1170+
1171+
let secp = Secp256k1::new();
1172+
1173+
let sk =
1174+
secp256k1::SecretKey::from_slice(&b"sally was a secret key, she said"[..]).unwrap();
1175+
let pk = bitcoin::PublicKey::new(secp256k1::PublicKey::from_secret_key(&secp, &sk));
1176+
1177+
let desc =
1178+
Descriptor::<DefiniteDescriptorKey>::from_str(&format!("wsh(pk({}))", pk)).unwrap();
1179+
1180+
let sighash =
1181+
secp256k1::Message::from_digest_slice(&b"michael was a message, amusingly"[..])
1182+
.expect("32 bytes");
1183+
let ecdsa_sig = bitcoin::ecdsa::Signature {
1184+
signature: secp.sign_ecdsa(&sighash, &sk),
1185+
sighash_type: bitcoin::sighash::EcdsaSighashType::All,
1186+
};
1187+
1188+
// This witness script should exist in the witness stack returned by `Plan::satisfy`.
1189+
let exp_witness_script = desc.explicit_script().expect("wsh has explicit script");
1190+
1191+
let mut satisfier = BTreeMap::<DefiniteDescriptorKey, bitcoin::ecdsa::Signature>::new();
1192+
satisfier.insert(DefiniteDescriptorKey::from_str(&pk.to_string()).unwrap(), ecdsa_sig);
1193+
1194+
let assets = Assets::new().add(DescriptorPublicKey::from_str(&pk.to_string()).unwrap());
1195+
let plan = desc.plan(&assets).expect("plan should succeed");
1196+
1197+
let (witness, script_sig) = plan.satisfy(&satisfier).expect("satisfy should succeed");
1198+
1199+
// For native P2WSH:
1200+
// - script_sig should be empty
1201+
// - witness should contain [signature, witness_script]
1202+
assert_eq!(script_sig, ScriptBuf::new());
1203+
assert_eq!(witness.len(), 2);
1204+
assert_eq!(witness.last().unwrap(), &exp_witness_script.into_bytes());
1205+
}
1206+
1207+
#[test]
1208+
fn test_plan_satisfy_sh_wsh() {
1209+
use std::collections::BTreeMap;
1210+
1211+
use bitcoin::secp256k1::{self, Secp256k1};
1212+
1213+
let secp = Secp256k1::new();
1214+
let sk =
1215+
secp256k1::SecretKey::from_slice(&b"sally was a secret key, she said"[..]).unwrap();
1216+
let pk = bitcoin::PublicKey::new(secp256k1::PublicKey::from_secret_key(&secp, &sk));
1217+
1218+
let desc =
1219+
Descriptor::<DefiniteDescriptorKey>::from_str(&format!("sh(wsh(pk({})))", pk)).unwrap();
1220+
1221+
let sighash =
1222+
secp256k1::Message::from_digest_slice(&b"michael was a message, amusingly"[..])
1223+
.expect("32 bytes");
1224+
let ecdsa_sig = bitcoin::ecdsa::Signature {
1225+
signature: secp.sign_ecdsa(&sighash, &sk),
1226+
sighash_type: bitcoin::sighash::EcdsaSighashType::All,
1227+
};
1228+
1229+
// Get expected values before plan() consumes the descriptor.
1230+
let exp_witness_script = desc.explicit_script().expect("sh-wsh has explicit script");
1231+
let exp_script_sig = desc.unsigned_script_sig();
1232+
1233+
let mut satisfier: BTreeMap<DefiniteDescriptorKey, bitcoin::ecdsa::Signature> =
1234+
BTreeMap::new();
1235+
satisfier.insert(DefiniteDescriptorKey::from_str(&pk.to_string()).unwrap(), ecdsa_sig);
1236+
1237+
let assets = Assets::new().add(DescriptorPublicKey::from_str(&pk.to_string()).unwrap());
1238+
let plan = desc.plan(&assets).expect("plan should succeed");
1239+
1240+
let (witness, script_sig) = plan.satisfy(&satisfier).expect("satisfy should succeed");
1241+
1242+
// For P2SH-P2WSH:
1243+
// - script_sig should be the unsigned_script_sig (pushes the P2WSH redeemScript)
1244+
// - witness should contain [signature, witness_script]
1245+
assert_eq!(script_sig, exp_script_sig);
1246+
assert_eq!(witness.len(), 2);
1247+
assert_eq!(witness.last().unwrap(), &exp_witness_script.into_bytes());
1248+
}
11571249
}

0 commit comments

Comments
 (0)