Skip to content

Commit 4d63e62

Browse files
committed
Merge #897: Append witness script for P2WSH in Plan::satisfy()
871e953 bitcoind-tests: Add integration test for Plan::satisfy() with P2WSH (志宇) 3a7398e plan: Append witness script for P2WSH in Plan::satisfy() (志宇) 3ddad52 bitcoind-tests: Run cargo fmt (志宇) Pull request description: 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) ACKs for top commit: tcharding: ACK 871e953 apoelstra: ACK 871e953; successfully ran local tests Tree-SHA512: a45be25cf7460c26590f60a5ec9470cd19c70dae85f55d7da77ed1699936b57bc5fe85ccc09fbdac663098c8c8b72725de218eb6ee63adb5cf3b3f2c339209f2
2 parents 411b651 + 871e953 commit 4d63e62

3 files changed

Lines changed: 245 additions & 11 deletions

File tree

bitcoind-tests/tests/setup/test_util.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ use bitcoin::hex::DisplayHex;
2525
use bitcoin::secp256k1;
2626
use miniscript::descriptor::{SinglePub, SinglePubKey};
2727
use miniscript::{
28-
bitcoin, hash256, Descriptor, DescriptorPublicKey, Error, Miniscript, ScriptContext,
29-
Translator,
28+
bitcoin, hash256, Descriptor, DescriptorPublicKey, Error, Miniscript, ScriptContext, Translator,
3029
};
3130
use rand::RngCore;
3231
use secp256k1::XOnlyPublicKey;

bitcoind-tests/tests/test_desc.rs

Lines changed: 148 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//!
66
77
use std::collections::BTreeMap;
8+
use std::str::FromStr;
89
use std::{error, fmt};
910

1011
use actual_rand as rand;
@@ -168,16 +169,19 @@ pub fn test_desc_satisfy(
168169

169170
if let Some(internal_keypair) = internal_keypair {
170171
// ---------------------- Tr key spend --------------------
171-
let internal_keypair = internal_keypair
172-
.tap_tweak(&secp, tr.spend_info().merkle_root());
172+
let internal_keypair =
173+
internal_keypair.tap_tweak(&secp, tr.spend_info().merkle_root());
173174
let sighash_msg = sighash_cache
174175
.taproot_key_spend_signature_hash(0, &prevouts, sighash_type)
175176
.unwrap();
176177
let msg = secp256k1::Message::from_digest(sighash_msg.to_byte_array());
177178
let mut aux_rand = [0u8; 32];
178179
rand::thread_rng().fill_bytes(&mut aux_rand);
179-
let schnorr_sig =
180-
secp.sign_schnorr_with_aux_rand(&msg, &internal_keypair.to_keypair(), &aux_rand);
180+
let schnorr_sig = secp.sign_schnorr_with_aux_rand(
181+
&msg,
182+
&internal_keypair.to_keypair(),
183+
&aux_rand,
184+
);
181185
psbt.inputs[0].tap_key_sig =
182186
Some(taproot::Signature { signature: schnorr_sig, sighash_type });
183187
} else {
@@ -187,7 +191,8 @@ pub fn test_desc_satisfy(
187191
let x_only_keypairs_reqd: Vec<(secp256k1::Keypair, TapLeafHash)> = tr
188192
.leaves()
189193
.flat_map(|leaf| {
190-
let leaf_hash = TapLeafHash::from_script(&leaf.compute_script(), LeafVersion::TapScript);
194+
let leaf_hash =
195+
TapLeafHash::from_script(&leaf.compute_script(), LeafVersion::TapScript);
191196
leaf.miniscript().iter_pk().filter_map(move |pk| {
192197
let i = x_only_pks.iter().position(|&x| x.to_public_key() == pk);
193198
i.map(|idx| (xonly_keypairs[idx], leaf_hash))
@@ -419,3 +424,141 @@ fn test_satisfy() {
419424
let cl = &setup::setup().client;
420425
test_descs(cl, &testdata);
421426
}
427+
428+
fn test_plan_satisfy(
429+
cl: &Client,
430+
testdata: &TestData,
431+
descriptor: &str,
432+
) -> Result<Witness, DescError> {
433+
use std::collections::BTreeMap;
434+
435+
use miniscript::plan::Assets;
436+
use miniscript::DefiniteDescriptorKey;
437+
438+
let secp = secp256k1::Secp256k1::new();
439+
let sks = &testdata.secretdata.sks;
440+
let pks = &testdata.pubdata.pks;
441+
442+
let blocks = cl
443+
.generate_to_address(1, &cl.new_address().unwrap())
444+
.unwrap();
445+
assert_eq!(blocks.0.len(), 1);
446+
447+
let definite_desc = test_util::parse_test_desc(descriptor, &testdata.pubdata)
448+
.map_err(|_| DescError::DescParseError)?
449+
.at_derivation_index(0)
450+
.unwrap();
451+
452+
let derived_desc = definite_desc.derived_descriptor(&secp);
453+
let desc_address = derived_desc
454+
.address(bitcoin::Network::Regtest)
455+
.map_err(|_| DescError::AddressComputationError)?;
456+
457+
let txid = cl
458+
.send_to_address(&desc_address, btc(1))
459+
.expect("rpc call failed")
460+
.txid()
461+
.expect("conversion to model failed");
462+
463+
let blocks = cl
464+
.generate_to_address(2, &cl.new_address().unwrap())
465+
.unwrap();
466+
assert_eq!(blocks.0.len(), 2);
467+
468+
let (outpoint, witness_utxo) = get_vout(cl, txid, btc(1.0), derived_desc.script_pubkey());
469+
470+
let mut assets = Assets::new();
471+
for pk in pks.iter() {
472+
let dpk = miniscript::DescriptorPublicKey::Single(miniscript::descriptor::SinglePub {
473+
origin: None,
474+
key: miniscript::descriptor::SinglePubKey::FullKey(*pk),
475+
});
476+
assets = assets.add(dpk);
477+
}
478+
479+
let plan = definite_desc
480+
.clone()
481+
.plan(&assets)
482+
.expect("plan creation failed");
483+
484+
let mut unsigned_tx = Transaction {
485+
version: transaction::Version::TWO,
486+
lock_time: absolute::LockTime::from_time(1_603_866_330).expect("valid timestamp"),
487+
input: vec![TxIn {
488+
previous_output: outpoint,
489+
sequence: Sequence::from_height(1),
490+
..Default::default()
491+
}],
492+
output: vec![TxOut {
493+
value: Amount::from_sat(99_997_000),
494+
script_pubkey: cl
495+
.new_address_with_type(AddressType::Bech32)
496+
.unwrap()
497+
.script_pubkey(),
498+
}],
499+
};
500+
501+
let mut sighash_cache = SighashCache::new(&unsigned_tx);
502+
503+
use miniscript::descriptor::DescriptorType;
504+
let sighash_type = sighash::EcdsaSighashType::All;
505+
let desc_type = derived_desc.desc_type();
506+
let sighash_msg = match desc_type {
507+
DescriptorType::Wsh
508+
| DescriptorType::WshSortedMulti
509+
| DescriptorType::ShWsh
510+
| DescriptorType::ShWshSortedMulti => {
511+
let script_code = derived_desc.script_code().expect("has script_code");
512+
sighash_cache
513+
.p2wsh_signature_hash(0, &script_code, witness_utxo.value, sighash_type)
514+
.expect("sighash")
515+
}
516+
_ => panic!("test is only for wsh descriptors, got {:?}", desc_type),
517+
};
518+
519+
let msg = secp256k1::Message::from_digest(sighash_msg.to_byte_array());
520+
521+
let mut sig_map: BTreeMap<DefiniteDescriptorKey, ecdsa::Signature> = BTreeMap::new();
522+
for (i, pk) in pks.iter().enumerate() {
523+
let signature = secp.sign_ecdsa(&msg, &sks[i]);
524+
let dpk = DefiniteDescriptorKey::from_str(&pk.to_string()).unwrap();
525+
sig_map.insert(dpk, ecdsa::Signature { signature, sighash_type });
526+
}
527+
528+
let (witness_stack, script_sig) = plan.satisfy(&sig_map).expect("satisfaction failed");
529+
530+
unsigned_tx.input[0].witness = Witness::from_slice(&witness_stack);
531+
unsigned_tx.input[0].script_sig = script_sig;
532+
533+
let txid = cl
534+
.send_raw_transaction(&unsigned_tx)
535+
.unwrap_or_else(|e| panic!("send tx failed for desc {}: {:?}", definite_desc, e))
536+
.txid()
537+
.expect("conversion to model failed");
538+
539+
let _blocks = cl
540+
.generate_to_address(1, &cl.new_address().unwrap())
541+
.unwrap();
542+
let num_conf = cl.get_transaction(txid).unwrap().confirmations;
543+
assert!(num_conf > 0);
544+
545+
Ok(unsigned_tx.input[0].witness.clone())
546+
}
547+
548+
#[test]
549+
fn test_plan_satisfy_wsh() {
550+
let testdata = TestData::new_fixed_data(50);
551+
let cl = &setup::setup().client;
552+
553+
test_plan_satisfy(cl, &testdata, "wsh(pk(K))").unwrap();
554+
test_plan_satisfy(cl, &testdata, "wsh(multi(2,K1,K2,K3))").unwrap();
555+
}
556+
557+
#[test]
558+
fn test_plan_satisfy_sh_wsh() {
559+
let testdata = TestData::new_fixed_data(50);
560+
let cl = &setup::setup().client;
561+
562+
test_plan_satisfy(cl, &testdata, "sh(wsh(pk(K)))").unwrap();
563+
test_plan_satisfy(cl, &testdata, "sh(wsh(multi(2,K1,K2,K3)))").unwrap();
564+
}

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)