Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions bitcoind-tests/tests/setup/test_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ use bitcoin::hex::DisplayHex;
use bitcoin::secp256k1;
use miniscript::descriptor::{SinglePub, SinglePubKey};
use miniscript::{
bitcoin, hash256, Descriptor, DescriptorPublicKey, Error, Miniscript, ScriptContext,
Translator,
bitcoin, hash256, Descriptor, DescriptorPublicKey, Error, Miniscript, ScriptContext, Translator,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: If you feel like it can you throw a patch at the front of this PR that runs the formatter in bitcoind-tests please mate so that these fromatting changes aren't mixed in with your changes.

};
use rand::RngCore;
use secp256k1::XOnlyPublicKey;
Expand Down
153 changes: 148 additions & 5 deletions bitcoind-tests/tests/test_desc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//!

use std::collections::BTreeMap;
use std::str::FromStr;
use std::{error, fmt};

use actual_rand as rand;
Expand Down Expand Up @@ -168,16 +169,19 @@ pub fn test_desc_satisfy(

if let Some(internal_keypair) = internal_keypair {
// ---------------------- Tr key spend --------------------
let internal_keypair = internal_keypair
.tap_tweak(&secp, tr.spend_info().merkle_root());
let internal_keypair =
internal_keypair.tap_tweak(&secp, tr.spend_info().merkle_root());
let sighash_msg = sighash_cache
.taproot_key_spend_signature_hash(0, &prevouts, sighash_type)
.unwrap();
let msg = secp256k1::Message::from_digest(sighash_msg.to_byte_array());
let mut aux_rand = [0u8; 32];
rand::thread_rng().fill_bytes(&mut aux_rand);
let schnorr_sig =
secp.sign_schnorr_with_aux_rand(&msg, &internal_keypair.to_keypair(), &aux_rand);
let schnorr_sig = secp.sign_schnorr_with_aux_rand(
&msg,
&internal_keypair.to_keypair(),
&aux_rand,
);
psbt.inputs[0].tap_key_sig =
Some(taproot::Signature { signature: schnorr_sig, sighash_type });
} else {
Expand All @@ -187,7 +191,8 @@ pub fn test_desc_satisfy(
let x_only_keypairs_reqd: Vec<(secp256k1::Keypair, TapLeafHash)> = tr
.leaves()
.flat_map(|leaf| {
let leaf_hash = TapLeafHash::from_script(&leaf.compute_script(), LeafVersion::TapScript);
let leaf_hash =
TapLeafHash::from_script(&leaf.compute_script(), LeafVersion::TapScript);
leaf.miniscript().iter_pk().filter_map(move |pk| {
let i = x_only_pks.iter().position(|&x| x.to_public_key() == pk);
i.map(|idx| (xonly_keypairs[idx], leaf_hash))
Expand Down Expand Up @@ -419,3 +424,141 @@ fn test_satisfy() {
let cl = &setup::setup().client;
test_descs(cl, &testdata);
}

fn test_plan_satisfy(
cl: &Client,
testdata: &TestData,
descriptor: &str,
) -> Result<Witness, DescError> {
use std::collections::BTreeMap;

use miniscript::plan::Assets;
use miniscript::DefiniteDescriptorKey;

let secp = secp256k1::Secp256k1::new();
let sks = &testdata.secretdata.sks;
let pks = &testdata.pubdata.pks;

let blocks = cl
.generate_to_address(1, &cl.new_address().unwrap())
.unwrap();
assert_eq!(blocks.0.len(), 1);

let definite_desc = test_util::parse_test_desc(descriptor, &testdata.pubdata)
.map_err(|_| DescError::DescParseError)?
.at_derivation_index(0)
.unwrap();

let derived_desc = definite_desc.derived_descriptor(&secp);
let desc_address = derived_desc
.address(bitcoin::Network::Regtest)
.map_err(|_| DescError::AddressComputationError)?;

let txid = cl
.send_to_address(&desc_address, btc(1))
.expect("rpc call failed")
.txid()
.expect("conversion to model failed");

let blocks = cl
.generate_to_address(2, &cl.new_address().unwrap())
.unwrap();
assert_eq!(blocks.0.len(), 2);

let (outpoint, witness_utxo) = get_vout(cl, txid, btc(1.0), derived_desc.script_pubkey());

let mut assets = Assets::new();
for pk in pks.iter() {
let dpk = miniscript::DescriptorPublicKey::Single(miniscript::descriptor::SinglePub {
origin: None,
key: miniscript::descriptor::SinglePubKey::FullKey(*pk),
});
assets = assets.add(dpk);
}

let plan = definite_desc
.clone()
.plan(&assets)
.expect("plan creation failed");

let mut unsigned_tx = Transaction {
version: transaction::Version::TWO,
lock_time: absolute::LockTime::from_time(1_603_866_330).expect("valid timestamp"),
input: vec![TxIn {
previous_output: outpoint,
sequence: Sequence::from_height(1),
..Default::default()
}],
output: vec![TxOut {
value: Amount::from_sat(99_997_000),
script_pubkey: cl
.new_address_with_type(AddressType::Bech32)
.unwrap()
.script_pubkey(),
}],
};

let mut sighash_cache = SighashCache::new(&unsigned_tx);

use miniscript::descriptor::DescriptorType;
let sighash_type = sighash::EcdsaSighashType::All;
let desc_type = derived_desc.desc_type();
let sighash_msg = match desc_type {
DescriptorType::Wsh
| DescriptorType::WshSortedMulti
| DescriptorType::ShWsh
| DescriptorType::ShWshSortedMulti => {
let script_code = derived_desc.script_code().expect("has script_code");
sighash_cache
.p2wsh_signature_hash(0, &script_code, witness_utxo.value, sighash_type)
.expect("sighash")
}
_ => panic!("test is only for wsh descriptors, got {:?}", desc_type),
};

let msg = secp256k1::Message::from_digest(sighash_msg.to_byte_array());

let mut sig_map: BTreeMap<DefiniteDescriptorKey, ecdsa::Signature> = BTreeMap::new();
for (i, pk) in pks.iter().enumerate() {
let signature = secp.sign_ecdsa(&msg, &sks[i]);
let dpk = DefiniteDescriptorKey::from_str(&pk.to_string()).unwrap();
sig_map.insert(dpk, ecdsa::Signature { signature, sighash_type });
}

let (witness_stack, script_sig) = plan.satisfy(&sig_map).expect("satisfaction failed");

unsigned_tx.input[0].witness = Witness::from_slice(&witness_stack);
unsigned_tx.input[0].script_sig = script_sig;

let txid = cl
.send_raw_transaction(&unsigned_tx)
.unwrap_or_else(|e| panic!("send tx failed for desc {}: {:?}", definite_desc, e))
.txid()
.expect("conversion to model failed");

let _blocks = cl
.generate_to_address(1, &cl.new_address().unwrap())
.unwrap();
let num_conf = cl.get_transaction(txid).unwrap().confirmations;
assert!(num_conf > 0);

Ok(unsigned_tx.input[0].witness.clone())
}
Copy link
Member

@tcharding tcharding Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This repo is not my baby so defering to @apoelstra.

Do we want to have code like this in the repo? It may do whats needed but its pretty far from clean code.

(Not a dig at you @evanlinjin more a philosophical / high level comment on do we want to merge LLM generated test code since users will look at it and copy it and think we wrote it.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea of this is to test what we have against Bitcoin core - can we broadcast a tx with wsh spend(s)?

In terms of "cleanliness", I would argue it's quite easy to read as it is? Could you expand on this please, thanks.


#[test]
fn test_plan_satisfy_wsh() {
let testdata = TestData::new_fixed_data(50);
let cl = &setup::setup().client;

test_plan_satisfy(cl, &testdata, "wsh(pk(K))").unwrap();
test_plan_satisfy(cl, &testdata, "wsh(multi(2,K1,K2,K3))").unwrap();
}

#[test]
fn test_plan_satisfy_sh_wsh() {
let testdata = TestData::new_fixed_data(50);
let cl = &setup::setup().client;

test_plan_satisfy(cl, &testdata, "sh(wsh(pk(K)))").unwrap();
test_plan_satisfy(cl, &testdata, "sh(wsh(multi(2,K1,K2,K3)))").unwrap();
}
100 changes: 96 additions & 4 deletions src/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,11 +292,18 @@ impl Plan {
})
.into_script(),
),
DescriptorType::Wpkh
| DescriptorType::Wsh
DescriptorType::Wpkh | DescriptorType::Tr => (stack, ScriptBuf::new()),
DescriptorType::ShWpkh => (stack, self.descriptor.unsigned_script_sig()),
DescriptorType::Wsh
| DescriptorType::WshSortedMulti
| DescriptorType::Tr => (stack, ScriptBuf::new()),
DescriptorType::ShWsh | DescriptorType::ShWshSortedMulti | DescriptorType::ShWpkh => {
| DescriptorType::ShWsh
| DescriptorType::ShWshSortedMulti => {
let mut stack = stack;
let witness_script = self
.descriptor
.explicit_script()
.expect("wsh descriptors have explicit script");
stack.push(witness_script.into_bytes());
(stack, self.descriptor.unsigned_script_sig())
}
Comment on lines 300 to 308
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| DescriptorType::ShWshSortedMulti => {
let mut stack = stack;
let witness_script = self
.descriptor
.explicit_script()
.expect("wsh descriptors have explicit script");
stack.push(witness_script.into_bytes());
let script_sig = self.descriptor.unsigned_script_sig();
(stack, script_sig)
}
| DescriptorType::ShWshSortedMulti => {
let mut stack = stack;
// We need to append the witness script as per BIP-141
let witness_script = self
.descriptor
.explicit_script()
.expect("wsh descriptors have explicit script");
stack.push(witness_script.into_bytes());
(stack, self.descriptor.unsigned_script_sig())
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't hack on this repo much so am not super familiar with the style but I'd write it like that. Specifically making the return statement mirror the other one that is the same while highlighting the difference.

})
Expand Down Expand Up @@ -1154,4 +1161,89 @@ mod test {
assert!(psbt_input.redeem_script.is_none(), "Redeem script present");
assert_eq!(psbt_input.bip32_derivation.len(), 2, "Unexpected number of bip32_derivation");
}

#[test]
fn test_plan_satisfy_wsh() {
use std::collections::BTreeMap;

use bitcoin::secp256k1::{self, Secp256k1};

let secp = Secp256k1::new();

let sk =
secp256k1::SecretKey::from_slice(&b"sally was a secret key, she said"[..]).unwrap();
let pk = bitcoin::PublicKey::new(secp256k1::PublicKey::from_secret_key(&secp, &sk));

let desc =
Descriptor::<DefiniteDescriptorKey>::from_str(&format!("wsh(pk({}))", pk)).unwrap();

let sighash =
secp256k1::Message::from_digest_slice(&b"michael was a message, amusingly"[..])
.expect("32 bytes");
let ecdsa_sig = bitcoin::ecdsa::Signature {
signature: secp.sign_ecdsa(&sighash, &sk),
sighash_type: bitcoin::sighash::EcdsaSighashType::All,
};

// This witness script should exist in the witness stack returned by `Plan::satisfy`.
let exp_witness_script = desc.explicit_script().expect("wsh has explicit script");

let mut satisfier = BTreeMap::<DefiniteDescriptorKey, bitcoin::ecdsa::Signature>::new();
satisfier.insert(DefiniteDescriptorKey::from_str(&pk.to_string()).unwrap(), ecdsa_sig);

let assets = Assets::new().add(DescriptorPublicKey::from_str(&pk.to_string()).unwrap());
let plan = desc.plan(&assets).expect("plan should succeed");

let (witness, script_sig) = plan.satisfy(&satisfier).expect("satisfy should succeed");

// For native P2WSH:
// - script_sig should be empty
// - witness should contain [signature, witness_script]
assert_eq!(script_sig, ScriptBuf::new());
assert_eq!(witness.len(), 2);
assert_eq!(witness.last().unwrap(), &exp_witness_script.into_bytes());
}

#[test]
fn test_plan_satisfy_sh_wsh() {
use std::collections::BTreeMap;

use bitcoin::secp256k1::{self, Secp256k1};

let secp = Secp256k1::new();
let sk =
secp256k1::SecretKey::from_slice(&b"sally was a secret key, she said"[..]).unwrap();
let pk = bitcoin::PublicKey::new(secp256k1::PublicKey::from_secret_key(&secp, &sk));

let desc =
Descriptor::<DefiniteDescriptorKey>::from_str(&format!("sh(wsh(pk({})))", pk)).unwrap();

let sighash =
secp256k1::Message::from_digest_slice(&b"michael was a message, amusingly"[..])
.expect("32 bytes");
let ecdsa_sig = bitcoin::ecdsa::Signature {
signature: secp.sign_ecdsa(&sighash, &sk),
sighash_type: bitcoin::sighash::EcdsaSighashType::All,
};

// Get expected values before plan() consumes the descriptor.
let exp_witness_script = desc.explicit_script().expect("sh-wsh has explicit script");
let exp_script_sig = desc.unsigned_script_sig();

let mut satisfier: BTreeMap<DefiniteDescriptorKey, bitcoin::ecdsa::Signature> =
BTreeMap::new();
satisfier.insert(DefiniteDescriptorKey::from_str(&pk.to_string()).unwrap(), ecdsa_sig);

let assets = Assets::new().add(DescriptorPublicKey::from_str(&pk.to_string()).unwrap());
let plan = desc.plan(&assets).expect("plan should succeed");

let (witness, script_sig) = plan.satisfy(&satisfier).expect("satisfy should succeed");

// For P2SH-P2WSH:
// - script_sig should be the unsigned_script_sig (pushes the P2WSH redeemScript)
// - witness should contain [signature, witness_script]
assert_eq!(script_sig, exp_script_sig);
assert_eq!(witness.len(), 2);
assert_eq!(witness.last().unwrap(), &exp_witness_script.into_bytes());
}
}
Loading