Skip to content

Commit 8d9a61a

Browse files
evanlinjinclaude
andcommitted
bitcoind-tests: Add integration test for Plan::satisfy() with P2WSH
Add test_plan_satisfy() which verifies that Plan::satisfy() correctly constructs witness and script_sig for P2WSH descriptors by: - Creating a Plan from the descriptor - Calling plan.satisfy() directly (not using PSBT finalization) - Manually setting witness/script_sig on the transaction - Broadcasting to Bitcoin Core and verifying it gets mined This is a regression test for #896 that exercises the exact code path that was fixed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent c369c52 commit 8d9a61a

File tree

2 files changed

+168
-7
lines changed

2 files changed

+168
-7
lines changed

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: 167 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,160 @@ fn test_satisfy() {
419424
let cl = &setup::setup().client;
420425
test_descs(cl, &testdata);
421426
}
427+
428+
/// Test that Plan::satisfy() correctly constructs witness for P2WSH descriptors.
429+
/// This is a regression test for https://github.com/rust-bitcoin/rust-miniscript/issues/896
430+
pub fn test_plan_satisfy_wsh(
431+
cl: &Client,
432+
testdata: &TestData,
433+
descriptor: &str,
434+
) -> Result<Witness, DescError> {
435+
use std::collections::BTreeMap;
436+
437+
use miniscript::plan::Assets;
438+
use miniscript::DefiniteDescriptorKey;
439+
440+
let secp = secp256k1::Secp256k1::new();
441+
let sks = &testdata.secretdata.sks;
442+
let pks = &testdata.pubdata.pks;
443+
444+
// Generate some blocks
445+
let blocks = cl
446+
.generate_to_address(1, &cl.new_address().unwrap())
447+
.unwrap();
448+
assert_eq!(blocks.0.len(), 1);
449+
450+
let definite_desc = test_util::parse_test_desc(descriptor, &testdata.pubdata)
451+
.map_err(|_| DescError::DescParseError)?
452+
.at_derivation_index(0)
453+
.unwrap();
454+
455+
let derived_desc = definite_desc.derived_descriptor(&secp);
456+
let desc_address = derived_desc
457+
.address(bitcoin::Network::Regtest)
458+
.map_err(|_| DescError::AddressComputationError)?;
459+
460+
// Send some btc to the descriptor address
461+
let txid = cl
462+
.send_to_address(&desc_address, btc(1))
463+
.expect("rpc call failed")
464+
.txid()
465+
.expect("conversion to model failed");
466+
467+
// Wait for the funds to mature
468+
let blocks = cl
469+
.generate_to_address(2, &cl.new_address().unwrap())
470+
.unwrap();
471+
assert_eq!(blocks.0.len(), 2);
472+
473+
// Get the UTXO
474+
let (outpoint, witness_utxo) = get_vout(cl, txid, btc(1.0), derived_desc.script_pubkey());
475+
476+
// Build assets from known keys
477+
let mut assets = Assets::new();
478+
for pk in pks.iter() {
479+
let dpk = miniscript::DescriptorPublicKey::Single(miniscript::descriptor::SinglePub {
480+
origin: None,
481+
key: miniscript::descriptor::SinglePubKey::FullKey(*pk),
482+
});
483+
assets = assets.add(dpk);
484+
}
485+
486+
// Get a plan (clone since plan() takes ownership)
487+
let plan = definite_desc
488+
.clone()
489+
.plan(&assets)
490+
.expect("Failed to create plan");
491+
492+
// Create the sighash for signing
493+
let mut unsigned_tx = Transaction {
494+
version: transaction::Version::TWO,
495+
lock_time: absolute::LockTime::from_time(1_603_866_330).expect("valid timestamp"),
496+
input: vec![TxIn {
497+
previous_output: outpoint,
498+
sequence: Sequence::from_height(1),
499+
..Default::default()
500+
}],
501+
output: vec![TxOut {
502+
value: Amount::from_sat(99_997_000),
503+
script_pubkey: cl
504+
.new_address_with_type(AddressType::Bech32)
505+
.unwrap()
506+
.script_pubkey(),
507+
}],
508+
};
509+
510+
let mut sighash_cache = SighashCache::new(&unsigned_tx);
511+
512+
// Compute the sighash based on descriptor type
513+
use miniscript::descriptor::DescriptorType;
514+
let sighash_type = sighash::EcdsaSighashType::All;
515+
let desc_type = derived_desc.desc_type();
516+
let sighash_msg = match desc_type {
517+
DescriptorType::Wsh
518+
| DescriptorType::WshSortedMulti
519+
| DescriptorType::ShWsh
520+
| DescriptorType::ShWshSortedMulti => {
521+
let script_code = derived_desc.script_code().expect("has script_code");
522+
sighash_cache
523+
.p2wsh_signature_hash(0, &script_code, witness_utxo.value, sighash_type)
524+
.expect("sighash")
525+
}
526+
_ => panic!("This test is only for WSH descriptors, got {:?}", desc_type),
527+
};
528+
529+
let msg = secp256k1::Message::from_digest(sighash_msg.to_byte_array());
530+
531+
// Sign with all required keys and build a satisfier map
532+
let mut sig_map: BTreeMap<DefiniteDescriptorKey, ecdsa::Signature> = BTreeMap::new();
533+
for (i, pk) in pks.iter().enumerate() {
534+
let signature = secp.sign_ecdsa(&msg, &sks[i]);
535+
let dpk = DefiniteDescriptorKey::from_str(&pk.to_string()).unwrap();
536+
sig_map.insert(dpk, ecdsa::Signature { signature, sighash_type });
537+
}
538+
539+
// Use Plan::satisfy() to construct witness and script_sig
540+
let (witness_stack, script_sig) = plan
541+
.satisfy(&sig_map)
542+
.expect("Plan::satisfy() should succeed");
543+
544+
// Set the witness and script_sig on the transaction
545+
unsigned_tx.input[0].witness = Witness::from_slice(&witness_stack);
546+
unsigned_tx.input[0].script_sig = script_sig;
547+
548+
// Broadcast the transaction
549+
let txid = cl
550+
.send_raw_transaction(&unsigned_tx)
551+
.unwrap_or_else(|e| panic!("send tx failed for desc {}: {:?}", definite_desc, e))
552+
.txid()
553+
.expect("conversion to model failed");
554+
555+
// Mine a block and verify confirmation
556+
let _blocks = cl
557+
.generate_to_address(1, &cl.new_address().unwrap())
558+
.unwrap();
559+
let num_conf = cl.get_transaction(txid).unwrap().confirmations;
560+
assert!(num_conf > 0);
561+
562+
Ok(unsigned_tx.input[0].witness.clone())
563+
}
564+
565+
#[test]
566+
fn test_plan_satisfy() {
567+
let testdata = TestData::new_fixed_data(50);
568+
let cl = &setup::setup().client;
569+
570+
// Test native P2WSH with Plan::satisfy()
571+
println!("Testing wsh(pk(K)) with Plan::satisfy()");
572+
test_plan_satisfy_wsh(cl, &testdata, "wsh(pk(K))").unwrap();
573+
574+
println!("Testing wsh(multi(2,K1,K2,K3)) with Plan::satisfy()");
575+
test_plan_satisfy_wsh(cl, &testdata, "wsh(multi(2,K1,K2,K3))").unwrap();
576+
577+
// Test P2SH-wrapped P2WSH with Plan::satisfy()
578+
println!("Testing sh(wsh(pk(K))) with Plan::satisfy()");
579+
test_plan_satisfy_wsh(cl, &testdata, "sh(wsh(pk(K)))").unwrap();
580+
581+
println!("Testing sh(wsh(multi(2,K1,K2,K3))) with Plan::satisfy()");
582+
test_plan_satisfy_wsh(cl, &testdata, "sh(wsh(multi(2,K1,K2,K3)))").unwrap();
583+
}

0 commit comments

Comments
 (0)