From a0b742b4e2a7ce546f7c5a2a708639bde6e5d88c Mon Sep 17 00:00:00 2001 From: Oli Date: Fri, 4 Sep 2026 18:15:17 +0200 Subject: [PATCH] plan: add and fix unit tests for corrected sizes Fixes the witness size and scriptSig size after updating the corresponding code. --- src/plan.rs | 99 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 87 insertions(+), 12 deletions(-) diff --git a/src/plan.rs b/src/plan.rs index 5177ae82d..3acdc4f73 100644 --- a/src/plan.rs +++ b/src/plan.rs @@ -28,7 +28,7 @@ use crate::descriptor::{self, Descriptor, DescriptorType, KeyMap}; use crate::miniscript::hash256; use crate::miniscript::satisfy::{Placeholder, Satisfier, SchnorrSigType}; use crate::prelude::*; -use crate::util::witness_size; +use crate::util::{varint_len, witness_size}; use crate::{DefiniteDescriptorKey, DescriptorPublicKey, Error, MiniscriptKey, ToPublicKey}; /// Trait describing a present/missing lookup table for constructing witness templates @@ -792,10 +792,11 @@ mod test { let desc = format!("wsh(t:or_c(pk({}),v:pkh({})))", keys[0], keys[1]); // expected weight: 4 (scriptSig len) + 1 (witness len) + 73 (sig) + // + 1 (witness script len) + 63 (witness script) let tests = vec![ (vec![], vec![], None, None, None), - (vec![0], vec![], None, None, Some(4 + 1 + 73)), - (vec![0, 1], vec![], None, None, Some(4 + 1 + 73)), + (vec![0], vec![], None, None, Some(4 + 1 + 73 + 1 + 63)), + (vec![0, 1], vec![], None, None, Some(4 + 1 + 73 + 1 + 63)), ]; test_inner(&desc, keys, hashes, tests); @@ -817,10 +818,11 @@ mod test { let desc = format!("wsh(and_v(v:pk({}),pk({})))", keys[0], keys[1]); // expected weight: 4 (scriptSig len) + 1 (witness len) + 73 (sig) * 2 + // + 1 (witness script len) + 70 (witness script) let tests = vec![ (vec![], vec![], None, None, None), (vec![0], vec![], None, None, None), - (vec![0, 1], vec![], None, None, Some(4 + 1 + 73 * 2)), + (vec![0, 1], vec![], None, None, Some(4 + 1 + 73 * 2 + 1 + 70)), ]; test_inner(&desc, keys, hashes, tests); @@ -853,7 +855,8 @@ mod test { (vec![], vec![], None, None, None), (vec![0, 1], vec![], None, None, None), // expected weight: 4 (scriptSig len) + 1 (witness len) + 73 (sig) * 3 + 1 (dummy push) - (vec![0, 1, 3], vec![], None, None, Some(4 + 1 + 73 * 3 + 1)), + // + 1 (witness script len) + 139 (witness script) + (vec![0, 1, 3], vec![], None, None, Some(4 + 1 + 73 * 3 + 1 + 1 + 139)), ]; test_inner(&desc, keys, hashes, tests); @@ -887,24 +890,28 @@ mod test { ), (vec![0], vec![], None, None, None), // expected weight: 4 (scriptSig len) + 1 (witness len) + 73 (sig) + 1 (OP_0) + 1 (OP_ZERO) + // + 1 (witness script len) + 85 (witness script) ( vec![0], vec![], Some(Sequence(1000).to_relative_lock_time().unwrap()), None, - Some(80), + Some(166), ), // expected weight: 4 (scriptSig len) + 1 (witness len) + 73 (sig) * 2 + 2 (OP_PUSHBYTE_1 0x01) - (vec![0, 1], vec![], None, None, Some(153)), + // + 1 (witness script len) + 85 (witness script) + (vec![0, 1], vec![], None, None, Some(239)), // expected weight: 4 (scriptSig len) + 1 (witness len) + 73 (sig) + 1 (OP_0) + 1 (OP_ZERO) + // + 1 (witness script len) + 85 (witness script) ( vec![0, 1], vec![], Some(Sequence(1000).to_relative_lock_time().unwrap()), None, - Some(80), + Some(166), ), // expected weight: 4 (scriptSig len) + 1 (witness len) + 73 (sig) * 2 + 2 (OP_PUSHBYTE_1 0x01) + // + 1 (witness script len) + 85 (witness script) ( vec![0, 1], vec![], @@ -914,7 +921,7 @@ mod test { .unwrap(), ), None, - Some(153), + Some(239), ), // incompatible timelock ]; @@ -924,20 +931,22 @@ mod test { let tests = vec![ // expected weight: 4 (scriptSig len) + 1 (witness len) + 73 (sig) + 1 (OP_0) + 1 (OP_ZERO) + // + 1 (witness script len) + 85 (witness script) ( vec![0], vec![], None, Some(absolute::LockTime::from_height(1000).unwrap()), - Some(80), + Some(166), ), // expected weight: 4 (scriptSig len) + 1 (witness len) + 73 (sig) * 2 + 2 (OP_PUSHBYTE_1 0x01) + // + 1 (witness script len) + 85 (witness script) ( vec![0, 1], vec![], None, Some(absolute::LockTime::from_time(500_001_000).unwrap()), - Some(153), + Some(239), ), // incompatible timelock ]; @@ -1081,7 +1090,8 @@ mod test { (vec![], vec![0], None, None, None), // Key + hash // expected weight: 4 (scriptSig len) + 1 (witness len) + 73 (sig) + 1 (OP_PUSH) + 32 (preimage) - (vec![0], vec![0], None, None, Some(111)), + // + 1 (witness script len) + 62 (witness script) + (vec![0], vec![0], None, None, Some(174)), ]; test_inner(&desc, keys, hashes, tests); @@ -1260,4 +1270,69 @@ mod test { assert!(desc.into_plan(&assets).is_err()); } + + /// A [`Satisfier`] that knows hash preimages only. + /// + /// A plan sizes a signature at its 73-byte maximum while a real one varies + /// in length, so a spend path that carries no signature is the one whose + /// witness has a size a plan can be held to byte for byte. + struct Preimages(BTreeMap); + + impl Satisfier for Preimages { + fn lookup_sha256(&self, h: &sha256::Hash) -> Option<[u8; 32]> { self.0.get(h).copied() } + } + + #[test] + fn sizes_match_satisfaction() { + use bitcoin::hashes::Hash as _; + + let preimages = [[1u8; 32], [2u8; 32]]; + let hashes = preimages.map(|p| sha256::Hash::hash(&p)); + let satisfier = Preimages(hashes.iter().copied().zip(preimages).collect()); + let assets = Assets::new().add(hashes[0]).add(hashes[1]); + + // Sigless, so it has to go around the sanity checks of `from_str`. + let ms = Miniscript::::from_str_insane(&format!( + "and_v(v:sha256({}),sha256({}))", + hashes[0], hashes[1] + )) + .unwrap(); + + for desc in [ + Descriptor::new_wsh(ms.clone()).unwrap(), + Descriptor::new_sh_wsh(ms).unwrap(), + ] { + let max_weight = desc.max_weight_to_satisfy().unwrap().to_wu() as usize; + let plan = desc.into_plan(&assets).unwrap(); + let (witness, script_sig) = plan.satisfy(&satisfier).unwrap(); + + // The witness as it goes on the wire: an element count followed by + // length-prefixed elements, the witness script among them. + let witness = bitcoin::Witness::from_slice(&witness); + assert_eq!(plan.witness_size(), bitcoin::consensus::serialize(&witness).len()); + assert_eq!(plan.scriptsig_size(), varint_len(script_sig.len()) + script_sig.len()); + + // `max_weight_to_satisfy` is the weight that satisfying adds to an + // input, so it leaves out the scriptSig length byte (4 WU) and the + // witness count byte (1 WU) that an unsatisfied input carries too. + // The plan reports the absolute size, so it is larger by exactly those. + assert_eq!(plan.satisfaction_weight(), max_weight + 4 + 1); + } + + // sh(wpkh) needs a signature, whose real size varies, so only what a plan + // can be held to exactly is checked: the scriptSig it returns, and the + // relation to `max_weight_to_satisfy`, which sizes a signature the way the + // plan does. + let key = DescriptorPublicKey::from_str( + "02c2fd50ceae468857bb7eb32ae9cd4083e6c7e42fbbec179d81134b3e3830586c", + ) + .unwrap(); + let desc = + Descriptor::::from_str(&format!("sh(wpkh({}))", key)).unwrap(); + let max_weight = desc.max_weight_to_satisfy().unwrap().to_wu() as usize; + let script_sig = desc.unsigned_script_sig(); + let plan = desc.into_plan(&Assets::new().add(key)).unwrap(); + assert_eq!(plan.scriptsig_size(), varint_len(script_sig.len()) + script_sig.len()); + assert_eq!(plan.satisfaction_weight(), max_weight + 4 + 1); + } }