From 3d9acd2231a4a7560dcd871d1476831b2e6b0531 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Fri, 11 Oct 2024 02:47:00 +0300 Subject: [PATCH 01/13] Document an error case for at_derivation_index() --- src/descriptor/key.rs | 1 + src/descriptor/mod.rs | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/descriptor/key.rs b/src/descriptor/key.rs index 7e034ef99..bafe4c2d6 100644 --- a/src/descriptor/key.rs +++ b/src/descriptor/key.rs @@ -627,6 +627,7 @@ impl DescriptorPublicKey { /// # Errors /// /// - If `index` is hardened. + /// - If the key contains multi-path derivations pub fn at_derivation_index(self, index: u32) -> Result { let definite = match self { DescriptorPublicKey::Single(_) => self, diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 487bbcc45..862fc58c6 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -596,6 +596,7 @@ impl Descriptor { /// /// # Errors /// - If index ≥ 2^31 + /// - If the descriptor contains multi-path derivations pub fn at_derivation_index( &self, index: u32, @@ -650,7 +651,8 @@ impl Descriptor { /// /// # Errors /// - /// This function will return an error if hardened derivation is attempted. + /// This function will return an error for multi-path descriptors + /// or if hardened derivation is attempted, pub fn derived_descriptor( &self, secp: &secp256k1::Secp256k1, From 851a2203801042b84b65fc8e98d4602b1b1aaf64 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Mon, 14 Oct 2024 04:46:59 +0300 Subject: [PATCH 02/13] Fix decoding of WIF with BIP32 origin --- src/descriptor/key.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/descriptor/key.rs b/src/descriptor/key.rs index bafe4c2d6..bcb74cdf6 100644 --- a/src/descriptor/key.rs +++ b/src/descriptor/key.rs @@ -703,7 +703,7 @@ impl FromStr for DescriptorSecretKey { if key_part.len() <= 52 { let sk = bitcoin::PrivateKey::from_str(key_part) .map_err(|_| DescriptorKeyParseError("Error while parsing a WIF private key"))?; - Ok(DescriptorSecretKey::Single(SinglePriv { key: sk, origin: None })) + Ok(DescriptorSecretKey::Single(SinglePriv { key: sk, origin })) } else { let (xpriv, derivation_paths, wildcard) = parse_xkey_deriv::(key_part)?; if derivation_paths.len() > 1 { @@ -1489,6 +1489,27 @@ mod test { DescriptorPublicKey::from_str("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/4/<0;1;>").unwrap_err(); } + #[test] + fn test_parse_wif() { + let secret_key = "[0dd03d09/0'/1/2']5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ" + .parse() + .unwrap(); + if let DescriptorSecretKey::Single(single) = secret_key { + assert_eq!( + single.key.inner, + "0C28FCA386C7A227600B2FE50B7CAE11EC86D3BF1FBE471BE89827E19D72AA1D" + .parse() + .unwrap() + ); + assert_eq!( + single.origin, + Some(("0dd03d09".parse().unwrap(), "m/0'/1/2'".parse().unwrap())) + ); + } else { + panic!("expected a DescriptorSecretKey::Single"); + } + } + #[test] #[cfg(feature = "serde")] fn test_descriptor_public_key_serde() { From 361d0191f206e614748511e344210d6fd3089b06 Mon Sep 17 00:00:00 2001 From: ChrisCho-H Date: Thu, 17 Oct 2024 00:12:52 +0900 Subject: [PATCH 03/13] feat: add MaxBareScriptSizeExceeded err for bare consensus check --- src/miniscript/context.rs | 11 ++++++++++- src/miniscript/mod.rs | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/miniscript/context.rs b/src/miniscript/context.rs index bb53ed67a..45b4a03a2 100644 --- a/src/miniscript/context.rs +++ b/src/miniscript/context.rs @@ -52,6 +52,9 @@ pub enum ScriptContextError { /// The Miniscript (under p2sh context) corresponding Script would be /// larger than `MAX_SCRIPT_ELEMENT_SIZE` bytes. MaxRedeemScriptSizeExceeded, + /// The Miniscript(under bare context) corresponding + /// Script would be larger than `MAX_SCRIPT_SIZE` bytes. + MaxBareScriptSizeExceeded, /// The policy rules of bitcoin core only permit Script size upto 1650 bytes MaxScriptSigSizeExceeded, /// Impossible to satisfy the miniscript under the current context @@ -80,6 +83,7 @@ impl error::Error for ScriptContextError { | MaxOpCountExceeded | MaxWitnessScriptSizeExceeded | MaxRedeemScriptSizeExceeded + | MaxBareScriptSizeExceeded | MaxScriptSigSizeExceeded | ImpossibleSatisfaction | TaprootMultiDisabled @@ -127,6 +131,11 @@ impl fmt::Display for ScriptContextError { "The Miniscript corresponding Script would be larger than \ MAX_SCRIPT_ELEMENT_SIZE bytes." ), + ScriptContextError::MaxBareScriptSizeExceeded => write!( + f, + "The Miniscript corresponding Script would be larger than \ + MAX_SCRIPT_SIZE bytes." + ), ScriptContextError::MaxScriptSigSizeExceeded => write!( f, "At least one satisfaction in Miniscript would be larger than \ @@ -740,7 +749,7 @@ impl ScriptContext for BareCtx { match node_checked { Ok(_) => { if ms.ext.pk_cost > MAX_SCRIPT_SIZE { - Err(ScriptContextError::MaxWitnessScriptSizeExceeded) + Err(ScriptContextError::MaxBareScriptSizeExceeded) } else { Ok(()) } diff --git a/src/miniscript/mod.rs b/src/miniscript/mod.rs index 02829ac5f..79aa0a682 100644 --- a/src/miniscript/mod.rs +++ b/src/miniscript/mod.rs @@ -1694,7 +1694,7 @@ mod tests { ); assert_eq!( bare_multi_ms.unwrap_err().to_string(), - "The Miniscript corresponding Script would be larger than MAX_STANDARD_P2WSH_SCRIPT_SIZE bytes." + "The Miniscript corresponding Script would be larger than MAX_SCRIPT_SIZE bytes." ); } } From adcc4c64e2bd9aa9ad9a13f3cf386041961bbe83 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Thu, 17 Oct 2024 03:14:21 +0300 Subject: [PATCH 04/13] Support conversion of multi-Xprivs into multi-Xpubs Possible when all hardened derivation steps are shared among all paths (or if there are none). Errors otherwise. --- src/descriptor/key.rs | 108 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 7 deletions(-) diff --git a/src/descriptor/key.rs b/src/descriptor/key.rs index bcb74cdf6..7e317f3a4 100644 --- a/src/descriptor/key.rs +++ b/src/descriptor/key.rs @@ -252,6 +252,89 @@ impl DescriptorXKey { } } +impl DescriptorMultiXKey { + /// Returns the public version of this multi-key, applying all the hardened derivation steps that + /// are shared among all derivation paths before turning it into a public key. + /// + /// Errors if there are hardened derivation steps that are not shared among all paths. + fn to_public( + &self, + secp: &Secp256k1, + ) -> Result, DescriptorKeyParseError> { + let deriv_paths = self.derivation_paths.paths(); + + let shared_prefix: Vec<_> = deriv_paths[0] + .into_iter() + .enumerate() + .take_while(|(index, child_num)| { + deriv_paths[1..].iter().all(|other_path| { + other_path.len() > *index && other_path[*index] == **child_num + }) + }) + .map(|(_, child_num)| *child_num) + .collect(); + + let suffixes: Vec> = deriv_paths + .iter() + .map(|path| { + path.into_iter() + .skip(shared_prefix.len()) + .map(|child_num| { + if child_num.is_normal() { + Ok(*child_num) + } else { + Err(DescriptorKeyParseError("Can't make a multi-xpriv with hardened derivation steps that are not shared among all paths into a public key.")) + } + }) + .collect() + }) + .collect::>()?; + + let unhardened = shared_prefix + .iter() + .rev() + .take_while(|c| c.is_normal()) + .count(); + let last_hardened_idx = shared_prefix.len() - unhardened; + let hardened_path = &shared_prefix[..last_hardened_idx]; + let unhardened_path = &shared_prefix[last_hardened_idx..]; + + let xprv = self + .xkey + .derive_priv(secp, &hardened_path) + .map_err(|_| DescriptorKeyParseError("Unable to derive the hardened steps"))?; + let xpub = bip32::Xpub::from_priv(secp, &xprv); + + let origin = match &self.origin { + Some((fingerprint, path)) => Some(( + *fingerprint, + path.into_iter() + .chain(hardened_path.iter()) + .copied() + .collect(), + )), + None if !hardened_path.is_empty() => { + Some((self.xkey.fingerprint(secp), hardened_path.into())) + } + None => None, + }; + let new_deriv_paths = suffixes + .into_iter() + .map(|suffix| { + let path = unhardened_path.iter().copied().chain(suffix); + path.collect::>().into() + }) + .collect(); + + Ok(DescriptorMultiXKey { + origin, + xkey: xpub, + derivation_paths: DerivPaths::new(new_deriv_paths).expect("not empty"), + wildcard: self.wildcard, + }) + } +} + /// Descriptor Key parsing errors // FIXME: replace with error enums #[derive(Debug, PartialEq, Clone, Copy)] @@ -309,9 +392,8 @@ impl DescriptorSecretKey { /// If the key is an "XPrv", the hardened derivation steps will be applied /// before converting it to a public key. /// - /// It will return an error if the key is a "multi-xpriv", as we wouldn't - /// always be able to apply hardened derivation steps if there are multiple - /// paths. + /// It will return an error if the key is a "multi-xpriv" that includes + /// hardened derivation steps not shared for all paths. pub fn to_public( &self, secp: &Secp256k1, @@ -319,10 +401,8 @@ impl DescriptorSecretKey { let pk = match self { DescriptorSecretKey::Single(prv) => DescriptorPublicKey::Single(prv.to_public(secp)), DescriptorSecretKey::XPrv(xprv) => DescriptorPublicKey::XPub(xprv.to_public(secp)?), - DescriptorSecretKey::MultiXPrv(_) => { - return Err(DescriptorKeyParseError( - "Can't make an extended private key with multiple paths into a public key.", - )) + DescriptorSecretKey::MultiXPrv(xprv) => { + DescriptorPublicKey::MultiXPub(xprv.to_public(secp)?) } }; @@ -1489,6 +1569,20 @@ mod test { DescriptorPublicKey::from_str("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/4/<0;1;>").unwrap_err(); } + #[test] + fn test_multixprv_to_public() { + let secp = secp256k1::Secp256k1::signing_only(); + + // Works if all hardended derivation steps are part of the shared path + let xprv = get_multipath_xprv("[01020304/5]tprv8ZgxMBicQKsPcwcD4gSnMti126ZiETsuX7qwrtMypr6FBwAP65puFn4v6c3jrN9VwtMRMph6nyT63NrfUL4C3nBzPcduzVSuHD7zbX2JKVc/1'/2'/3/<4;5>/6"); + let xpub = DescriptorPublicKey::MultiXPub(xprv.to_public(&secp).unwrap()); // wrap in a DescriptorPublicKey to have Display + assert_eq!(xpub.to_string(), "[01020304/5/1'/2']tpubDBTRkEMEFkUbk3WTz6CFSULyswkTPpPr38AWibf5TVkB5GxuBxbSbmdFGr3jmswwemknyYxAGoX7BJnKfyPy4WXaHmcrxZhfzFwoUFvFtm5/3/<4;5>/6"); + + // Fails if they're part of the multi-path specifier or following it + get_multipath_xprv("tprv8ZgxMBicQKsPcwcD4gSnMti126ZiETsuX7qwrtMypr6FBwAP65puFn4v6c3jrN9VwtMRMph6nyT63NrfUL4C3nBzPcduzVSuHD7zbX2JKVc/1/2/<3';4'>/5").to_public(&secp).unwrap_err(); + get_multipath_xprv("tprv8ZgxMBicQKsPcwcD4gSnMti126ZiETsuX7qwrtMypr6FBwAP65puFn4v6c3jrN9VwtMRMph6nyT63NrfUL4C3nBzPcduzVSuHD7zbX2JKVc/1/2/<3;4>/5/6'").to_public(&secp).unwrap_err(); + } + #[test] fn test_parse_wif() { let secret_key = "[0dd03d09/0'/1/2']5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ" From 9a53288bfe58c68e05db4169af4fb2c1e9b02537 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Thu, 17 Oct 2024 20:33:04 +0300 Subject: [PATCH 05/13] Some minor formatting changes --- src/descriptor/key.rs | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/src/descriptor/key.rs b/src/descriptor/key.rs index 7e317f3a4..c2e1fa1cc 100644 --- a/src/descriptor/key.rs +++ b/src/descriptor/key.rs @@ -227,20 +227,13 @@ impl DescriptorXKey { let xpub = bip32::Xpub::from_priv(secp, &xprv); let origin = match &self.origin { - Some((fingerprint, path)) => Some(( - *fingerprint, - path.into_iter() - .chain(hardened_path.iter()) - .cloned() - .collect(), - )), - None => { - if hardened_path.is_empty() { - None - } else { - Some((self.xkey.fingerprint(secp), hardened_path.into())) - } + Some((fingerprint, path)) => { + Some((*fingerprint, path.into_iter().chain(hardened_path).copied().collect())) } + None if !hardened_path.is_empty() => { + Some((self.xkey.fingerprint(secp), hardened_path.into())) + } + None => None, }; Ok(DescriptorXKey { @@ -306,13 +299,9 @@ impl DescriptorMultiXKey { let xpub = bip32::Xpub::from_priv(secp, &xprv); let origin = match &self.origin { - Some((fingerprint, path)) => Some(( - *fingerprint, - path.into_iter() - .chain(hardened_path.iter()) - .copied() - .collect(), - )), + Some((fingerprint, path)) => { + Some((*fingerprint, path.into_iter().chain(hardened_path).copied().collect())) + } None if !hardened_path.is_empty() => { Some((self.xkey.fingerprint(secp), hardened_path.into())) } From 7df81af7714dd97d452f35ec759800c9ac1efcbf Mon Sep 17 00:00:00 2001 From: ChrisCho-H Date: Fri, 18 Oct 2024 14:02:07 +0900 Subject: [PATCH 06/13] fix: fix typo for MaxWitnessItemsExceeded --- src/miniscript/context.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/miniscript/context.rs b/src/miniscript/context.rs index 45b4a03a2..d394b116a 100644 --- a/src/miniscript/context.rs +++ b/src/miniscript/context.rs @@ -42,7 +42,7 @@ pub enum ScriptContextError { UncompressedKeysNotAllowed, /// At least one satisfaction path in the Miniscript fragment has more than /// `MAX_STANDARD_P2WSH_STACK_ITEMS` (100) witness elements. - MaxWitnessItemssExceeded { actual: usize, limit: usize }, + MaxWitnessItemsExceeded { actual: usize, limit: usize }, /// At least one satisfaction path in the Miniscript fragment contains more /// than `MAX_OPS_PER_SCRIPT`(201) opcodes. MaxOpCountExceeded, @@ -79,7 +79,7 @@ impl error::Error for ScriptContextError { | CompressedOnly(_) | XOnlyKeysNotAllowed(_, _) | UncompressedKeysNotAllowed - | MaxWitnessItemssExceeded { .. } + | MaxWitnessItemsExceeded { .. } | MaxOpCountExceeded | MaxWitnessScriptSizeExceeded | MaxRedeemScriptSizeExceeded @@ -110,7 +110,7 @@ impl fmt::Display for ScriptContextError { ScriptContextError::UncompressedKeysNotAllowed => { write!(f, "uncompressed keys cannot be used in Taproot descriptors.") } - ScriptContextError::MaxWitnessItemssExceeded { actual, limit } => write!( + ScriptContextError::MaxWitnessItemsExceeded { actual, limit } => write!( f, "At least one spending path in the Miniscript fragment has {} more \ witness items than limit {}.", @@ -498,7 +498,7 @@ impl ScriptContext for Segwitv0 { fn check_witness(witness: &[Vec]) -> Result<(), ScriptContextError> { if witness.len() > MAX_STANDARD_P2WSH_STACK_ITEMS { - return Err(ScriptContextError::MaxWitnessItemssExceeded { + return Err(ScriptContextError::MaxWitnessItemsExceeded { actual: witness.len(), limit: MAX_STANDARD_P2WSH_STACK_ITEMS, }); @@ -565,7 +565,7 @@ impl ScriptContext for Segwitv0 { // No possible satisfactions Err(_e) => Err(ScriptContextError::ImpossibleSatisfaction), Ok(max_witness_items) if max_witness_items > MAX_STANDARD_P2WSH_STACK_ITEMS => { - Err(ScriptContextError::MaxWitnessItemssExceeded { + Err(ScriptContextError::MaxWitnessItemsExceeded { actual: max_witness_items, limit: MAX_STANDARD_P2WSH_STACK_ITEMS, }) @@ -612,7 +612,7 @@ impl ScriptContext for Tap { fn check_witness(witness: &[Vec]) -> Result<(), ScriptContextError> { // Note that tapscript has a 1000 limit compared to 100 of segwitv0 if witness.len() > MAX_STACK_SIZE { - return Err(ScriptContextError::MaxWitnessItemssExceeded { + return Err(ScriptContextError::MaxWitnessItemsExceeded { actual: witness.len(), limit: MAX_STACK_SIZE, }); From 0d96f1755d6c0ae12444e003f6a69b5deb1dcd7b Mon Sep 17 00:00:00 2001 From: ChrisCho-H Date: Fri, 18 Oct 2024 13:39:59 +0900 Subject: [PATCH 07/13] fix: differentiate max witness script size upon context --- src/miniscript/context.rs | 29 ++++++++++++++++++++--------- src/miniscript/mod.rs | 2 +- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/miniscript/context.rs b/src/miniscript/context.rs index d394b116a..ad810da1f 100644 --- a/src/miniscript/context.rs +++ b/src/miniscript/context.rs @@ -47,8 +47,9 @@ pub enum ScriptContextError { /// than `MAX_OPS_PER_SCRIPT`(201) opcodes. MaxOpCountExceeded, /// The Miniscript(under segwit context) corresponding - /// Script would be larger than `MAX_STANDARD_P2WSH_SCRIPT_SIZE` bytes. - MaxWitnessScriptSizeExceeded, + /// Script would be larger than `MAX_STANDARD_P2WSH_SCRIPT_SIZE`, + /// `MAX_SCRIPT_SIZE` or `MAX_BLOCK`(`Tap`) bytes. + MaxWitnessScriptSizeExceeded { max: usize, got: usize }, /// The Miniscript (under p2sh context) corresponding Script would be /// larger than `MAX_SCRIPT_ELEMENT_SIZE` bytes. MaxRedeemScriptSizeExceeded, @@ -81,7 +82,7 @@ impl error::Error for ScriptContextError { | UncompressedKeysNotAllowed | MaxWitnessItemsExceeded { .. } | MaxOpCountExceeded - | MaxWitnessScriptSizeExceeded + | MaxWitnessScriptSizeExceeded { .. } | MaxRedeemScriptSizeExceeded | MaxBareScriptSizeExceeded | MaxScriptSigSizeExceeded @@ -121,10 +122,11 @@ impl fmt::Display for ScriptContextError { "At least one satisfaction path in the Miniscript fragment contains \ more than MAX_OPS_PER_SCRIPT opcodes." ), - ScriptContextError::MaxWitnessScriptSizeExceeded => write!( + ScriptContextError::MaxWitnessScriptSizeExceeded { max, got } => write!( f, - "The Miniscript corresponding Script would be larger than \ - MAX_STANDARD_P2WSH_SCRIPT_SIZE bytes." + "The Miniscript corresponding Script cannot be larger than \ + {} bytes, but got {} bytes.", + max, got ), ScriptContextError::MaxRedeemScriptSizeExceeded => write!( f, @@ -525,7 +527,10 @@ impl ScriptContext for Segwitv0 { match node_checked { Ok(_) => { if ms.ext.pk_cost > MAX_SCRIPT_SIZE { - Err(ScriptContextError::MaxWitnessScriptSizeExceeded) + Err(ScriptContextError::MaxWitnessScriptSizeExceeded { + max: MAX_SCRIPT_SIZE, + got: ms.ext.pk_cost, + }) } else { Ok(()) } @@ -550,7 +555,10 @@ impl ScriptContext for Segwitv0 { ms: &Miniscript, ) -> Result<(), ScriptContextError> { if ms.ext.pk_cost > MAX_STANDARD_P2WSH_SCRIPT_SIZE { - return Err(ScriptContextError::MaxWitnessScriptSizeExceeded); + return Err(ScriptContextError::MaxWitnessScriptSizeExceeded { + max: MAX_STANDARD_P2WSH_SCRIPT_SIZE, + got: ms.ext.pk_cost, + }); } Ok(()) } @@ -644,7 +652,10 @@ impl ScriptContext for Tap { // some guarantees are not easy to satisfy because of knapsack // constraints if ms.ext.pk_cost as u64 > Weight::MAX_BLOCK.to_wu() { - Err(ScriptContextError::MaxWitnessScriptSizeExceeded) + Err(ScriptContextError::MaxWitnessScriptSizeExceeded { + max: Weight::MAX_BLOCK.to_wu() as usize, + got: ms.ext.pk_cost, + }) } else { Ok(()) } diff --git a/src/miniscript/mod.rs b/src/miniscript/mod.rs index 79aa0a682..c2da2855f 100644 --- a/src/miniscript/mod.rs +++ b/src/miniscript/mod.rs @@ -1690,7 +1690,7 @@ mod tests { ); assert_eq!( segwit_multi_ms.unwrap_err().to_string(), - "The Miniscript corresponding Script would be larger than MAX_STANDARD_P2WSH_SCRIPT_SIZE bytes." + "The Miniscript corresponding Script cannot be larger than 3600 bytes, but got 4110 bytes." ); assert_eq!( bare_multi_ms.unwrap_err().to_string(), From 68143b8282535d3d2df224dc5e0a02dd945b5120 Mon Sep 17 00:00:00 2001 From: ChrisCho-H Date: Sat, 19 Oct 2024 20:34:31 +0900 Subject: [PATCH 08/13] fix: use ImpossibleSatisfaction when op_count is None --- src/miniscript/context.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/miniscript/context.rs b/src/miniscript/context.rs index ad810da1f..980b20b58 100644 --- a/src/miniscript/context.rs +++ b/src/miniscript/context.rs @@ -434,7 +434,7 @@ impl ScriptContext for Legacy { ms: &Miniscript, ) -> Result<(), ScriptContextError> { match ms.ext.ops.op_count() { - None => Err(ScriptContextError::MaxOpCountExceeded), + None => Err(ScriptContextError::ImpossibleSatisfaction), Some(op_count) if op_count > MAX_OPS_PER_SCRIPT => { Err(ScriptContextError::MaxOpCountExceeded) } @@ -543,7 +543,7 @@ impl ScriptContext for Segwitv0 { ms: &Miniscript, ) -> Result<(), ScriptContextError> { match ms.ext.ops.op_count() { - None => Err(ScriptContextError::MaxOpCountExceeded), + None => Err(ScriptContextError::ImpossibleSatisfaction), Some(op_count) if op_count > MAX_OPS_PER_SCRIPT => { Err(ScriptContextError::MaxOpCountExceeded) } @@ -773,7 +773,7 @@ impl ScriptContext for BareCtx { ms: &Miniscript, ) -> Result<(), ScriptContextError> { match ms.ext.ops.op_count() { - None => Err(ScriptContextError::MaxOpCountExceeded), + None => Err(ScriptContextError::ImpossibleSatisfaction), Some(op_count) if op_count > MAX_OPS_PER_SCRIPT => { Err(ScriptContextError::MaxOpCountExceeded) } From 3426670fddc2f0bcfeee32b5f1a11a19aaea066d Mon Sep 17 00:00:00 2001 From: ChrisCho-H Date: Sun, 20 Oct 2024 10:21:27 +0900 Subject: [PATCH 09/13] feat: unify err msg to contain helpful info --- src/miniscript/context.rs | 88 +++++++++++++++++++++++++-------------- src/miniscript/mod.rs | 4 +- 2 files changed, 59 insertions(+), 33 deletions(-) diff --git a/src/miniscript/context.rs b/src/miniscript/context.rs index 980b20b58..9ddb5322e 100644 --- a/src/miniscript/context.rs +++ b/src/miniscript/context.rs @@ -45,19 +45,19 @@ pub enum ScriptContextError { MaxWitnessItemsExceeded { actual: usize, limit: usize }, /// At least one satisfaction path in the Miniscript fragment contains more /// than `MAX_OPS_PER_SCRIPT`(201) opcodes. - MaxOpCountExceeded, + MaxOpCountExceeded { actual: usize, limit: usize }, /// The Miniscript(under segwit context) corresponding /// Script would be larger than `MAX_STANDARD_P2WSH_SCRIPT_SIZE`, /// `MAX_SCRIPT_SIZE` or `MAX_BLOCK`(`Tap`) bytes. MaxWitnessScriptSizeExceeded { max: usize, got: usize }, /// The Miniscript (under p2sh context) corresponding Script would be /// larger than `MAX_SCRIPT_ELEMENT_SIZE` bytes. - MaxRedeemScriptSizeExceeded, + MaxRedeemScriptSizeExceeded { max: usize, got: usize }, /// The Miniscript(under bare context) corresponding /// Script would be larger than `MAX_SCRIPT_SIZE` bytes. - MaxBareScriptSizeExceeded, + MaxBareScriptSizeExceeded { max: usize, got: usize }, /// The policy rules of bitcoin core only permit Script size upto 1650 bytes - MaxScriptSigSizeExceeded, + MaxScriptSigSizeExceeded { actual: usize, limit: usize }, /// Impossible to satisfy the miniscript under the current context ImpossibleSatisfaction, /// No Multi Node in Taproot context @@ -81,11 +81,11 @@ impl error::Error for ScriptContextError { | XOnlyKeysNotAllowed(_, _) | UncompressedKeysNotAllowed | MaxWitnessItemsExceeded { .. } - | MaxOpCountExceeded + | MaxOpCountExceeded { .. } | MaxWitnessScriptSizeExceeded { .. } - | MaxRedeemScriptSizeExceeded - | MaxBareScriptSizeExceeded - | MaxScriptSigSizeExceeded + | MaxRedeemScriptSizeExceeded { .. } + | MaxBareScriptSizeExceeded { .. } + | MaxScriptSigSizeExceeded { .. } | ImpossibleSatisfaction | TaprootMultiDisabled | StackSizeLimitExceeded { .. } @@ -113,35 +113,39 @@ impl fmt::Display for ScriptContextError { } ScriptContextError::MaxWitnessItemsExceeded { actual, limit } => write!( f, - "At least one spending path in the Miniscript fragment has {} more \ - witness items than limit {}.", + "At least one satisfaction path in the Miniscript fragment has {} witness items \ + (limit: {}).", actual, limit ), - ScriptContextError::MaxOpCountExceeded => write!( + ScriptContextError::MaxOpCountExceeded { actual, limit } => write!( f, - "At least one satisfaction path in the Miniscript fragment contains \ - more than MAX_OPS_PER_SCRIPT opcodes." + "At least one satisfaction path in the Miniscript fragment contains {} opcodes \ + (limit: {}).", + actual, limit ), ScriptContextError::MaxWitnessScriptSizeExceeded { max, got } => write!( f, "The Miniscript corresponding Script cannot be larger than \ - {} bytes, but got {} bytes.", + {} bytes, but got {} bytes.", max, got ), - ScriptContextError::MaxRedeemScriptSizeExceeded => write!( + ScriptContextError::MaxRedeemScriptSizeExceeded { max, got } => write!( f, - "The Miniscript corresponding Script would be larger than \ - MAX_SCRIPT_ELEMENT_SIZE bytes." + "The Miniscript corresponding Script cannot be larger than \ + {} bytes, but got {} bytes.", + max, got ), - ScriptContextError::MaxBareScriptSizeExceeded => write!( + ScriptContextError::MaxBareScriptSizeExceeded { max, got } => write!( f, - "The Miniscript corresponding Script would be larger than \ - MAX_SCRIPT_SIZE bytes." + "The Miniscript corresponding Script cannot be larger than \ + {} bytes, but got {} bytes.", + max, got ), - ScriptContextError::MaxScriptSigSizeExceeded => write!( + ScriptContextError::MaxScriptSigSizeExceeded { actual, limit } => write!( f, - "At least one satisfaction in Miniscript would be larger than \ - MAX_SCRIPTSIG_SIZE scriptsig" + "At least one satisfaction path in the Miniscript fragment has {} bytes \ + (limit: {}).", + actual, limit ), ScriptContextError::ImpossibleSatisfaction => { write!(f, "Impossible to satisfy Miniscript under the current context") @@ -396,8 +400,12 @@ impl ScriptContext for Legacy { fn check_witness(witness: &[Vec]) -> Result<(), ScriptContextError> { // In future, we could avoid by having a function to count only // len of script instead of converting it. - if witness_to_scriptsig(witness).len() > MAX_SCRIPTSIG_SIZE { - return Err(ScriptContextError::MaxScriptSigSizeExceeded); + let script_sig = witness_to_scriptsig(witness); + if script_sig.len() > MAX_SCRIPTSIG_SIZE { + return Err(ScriptContextError::MaxScriptSigSizeExceeded { + actual: script_sig.len(), + limit: MAX_SCRIPTSIG_SIZE, + }); } Ok(()) } @@ -421,7 +429,10 @@ impl ScriptContext for Legacy { match node_checked { Ok(_) => { if ms.ext.pk_cost > MAX_SCRIPT_ELEMENT_SIZE { - Err(ScriptContextError::MaxRedeemScriptSizeExceeded) + Err(ScriptContextError::MaxRedeemScriptSizeExceeded { + max: MAX_SCRIPT_ELEMENT_SIZE, + got: ms.ext.pk_cost, + }) } else { Ok(()) } @@ -436,7 +447,10 @@ impl ScriptContext for Legacy { match ms.ext.ops.op_count() { None => Err(ScriptContextError::ImpossibleSatisfaction), Some(op_count) if op_count > MAX_OPS_PER_SCRIPT => { - Err(ScriptContextError::MaxOpCountExceeded) + Err(ScriptContextError::MaxOpCountExceeded { + actual: op_count, + limit: MAX_OPS_PER_SCRIPT, + }) } _ => Ok(()), } @@ -451,7 +465,10 @@ impl ScriptContext for Legacy { match ms.max_satisfaction_size() { Err(_e) => Err(ScriptContextError::ImpossibleSatisfaction), Ok(size) if size > MAX_SCRIPTSIG_SIZE => { - Err(ScriptContextError::MaxScriptSigSizeExceeded) + Err(ScriptContextError::MaxScriptSigSizeExceeded { + actual: size, + limit: MAX_SCRIPTSIG_SIZE, + }) } _ => Ok(()), } @@ -545,7 +562,10 @@ impl ScriptContext for Segwitv0 { match ms.ext.ops.op_count() { None => Err(ScriptContextError::ImpossibleSatisfaction), Some(op_count) if op_count > MAX_OPS_PER_SCRIPT => { - Err(ScriptContextError::MaxOpCountExceeded) + Err(ScriptContextError::MaxOpCountExceeded { + actual: op_count, + limit: MAX_OPS_PER_SCRIPT, + }) } _ => Ok(()), } @@ -760,7 +780,10 @@ impl ScriptContext for BareCtx { match node_checked { Ok(_) => { if ms.ext.pk_cost > MAX_SCRIPT_SIZE { - Err(ScriptContextError::MaxBareScriptSizeExceeded) + Err(ScriptContextError::MaxBareScriptSizeExceeded { + max: MAX_SCRIPT_SIZE, + got: ms.ext.pk_cost, + }) } else { Ok(()) } @@ -775,7 +798,10 @@ impl ScriptContext for BareCtx { match ms.ext.ops.op_count() { None => Err(ScriptContextError::ImpossibleSatisfaction), Some(op_count) if op_count > MAX_OPS_PER_SCRIPT => { - Err(ScriptContextError::MaxOpCountExceeded) + Err(ScriptContextError::MaxOpCountExceeded { + actual: op_count, + limit: MAX_OPS_PER_SCRIPT, + }) } _ => Ok(()), } diff --git a/src/miniscript/mod.rs b/src/miniscript/mod.rs index c2da2855f..c03fbf218 100644 --- a/src/miniscript/mod.rs +++ b/src/miniscript/mod.rs @@ -1686,7 +1686,7 @@ mod tests { // Should panic for exceeding the max consensus size, as multi properly used assert_eq!( legacy_multi_ms.unwrap_err().to_string(), - "The Miniscript corresponding Script would be larger than MAX_SCRIPT_ELEMENT_SIZE bytes." + "The Miniscript corresponding Script cannot be larger than 520 bytes, but got 685 bytes." ); assert_eq!( segwit_multi_ms.unwrap_err().to_string(), @@ -1694,7 +1694,7 @@ mod tests { ); assert_eq!( bare_multi_ms.unwrap_err().to_string(), - "The Miniscript corresponding Script would be larger than MAX_SCRIPT_SIZE bytes." + "The Miniscript corresponding Script cannot be larger than 10000 bytes, but got 10275 bytes." ); } } From 41073f73496e19158a473170da61bd35c170e70a Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Thu, 10 Oct 2024 13:12:06 +0000 Subject: [PATCH 10/13] descriptor: introduce several Taproot accessors When working with Taproot descriptors you typically need to do an annoying (and hard to discover) `match` statement to get the `Tr` out of the descriptor, and then call accessors on that to get the actual data out. Add two new methods to `Descriptor` that directly access the internal key and the taptree. Document that the actual leaves can be obtained by calling `.iter` on the taptree. Next, when a user is trying to sign a Taproot branch, they need to obtain a TapLeafHash. We have internal code which does this (which I have pulled into a helper function since there is some room to optimize it there..) but no exposed code, forcing the user to go digging through the rust-bitcoin docs to figure it out (including knowing the standard Taproot leaf version, which is an arcane detail of the sort that Miniscript otherwise hides). Add a new method `leaf_hash` on Taproot miniscripts, so that the user can directly obtain the leaf hashes. Now you can write e.g. for script in trdesc.tap_tree_iter() { let leaf_hash = script.leaf_hash(); // Do whatever you want... } vs the previous code which was roughly let tr = match trdesc { Descriptor::Tr(ref tr) => tr, _ => unreachable!("I know this is a Taproot descriptor"), }; // Or tr.tap_tree().unwrap().iter() in case you miss the weirdly-named // Tr::iter_scripts for script in tr.iter_scripts() { // Hope you know your rust-bitcoin docs by heart, and also that // .encode is the way to convert a Miniscript to a Script! let leaf_hash = TapLeafHash::from_script( LeafVersion::TapScript, script.encode(), ); } --- src/descriptor/mod.rs | 34 ++++++++++++++++++++++++++++++++++ src/descriptor/tr.rs | 5 +++++ src/miniscript/mod.rs | 40 ++++++++++++++++++++++++++++++---------- 3 files changed, 69 insertions(+), 10 deletions(-) diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 862fc58c6..34ef35961 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -241,6 +241,40 @@ impl Descriptor { Ok(Descriptor::Tr(Tr::new(key, script)?)) } + /// For a Taproot descriptor, returns the internal key. + pub fn internal_key(&self) -> Option<&Pk> { + if let Descriptor::Tr(ref tr) = self { + Some(tr.internal_key()) + } else { + None + } + } + + /// For a Taproot descriptor, returns the [`TapTree`] describing the Taproot tree. + /// + /// To obtain the individual leaves of the tree, call [`TapTree::iter`] on the + /// returned value. + pub fn tap_tree(&self) -> Option<&TapTree> { + if let Descriptor::Tr(ref tr) = self { + tr.tap_tree().as_ref() + } else { + None + } + } + + /// For a Taproot descriptor, returns an iterator over the scripts in the Taptree. + /// + /// If the descriptor is not a Taproot descriptor, **or** if the descriptor is a + /// Taproot descriptor containing only a keyspend, returns an empty iterator. + pub fn tap_tree_iter(&self) -> tr::TapTreeIter { + if let Descriptor::Tr(ref tr) = self { + if let Some(ref tree) = tr.tap_tree() { + return tree.iter(); + } + } + tr::TapTreeIter::empty() + } + /// Get the [DescriptorType] of [Descriptor] pub fn desc_type(&self) -> DescriptorType { match *self { diff --git a/src/descriptor/tr.rs b/src/descriptor/tr.rs index 41845f174..0348368e8 100644 --- a/src/descriptor/tr.rs +++ b/src/descriptor/tr.rs @@ -465,6 +465,11 @@ pub struct TapTreeIter<'a, Pk: MiniscriptKey> { stack: Vec<(u8, &'a TapTree)>, } +impl<'a, Pk: MiniscriptKey> TapTreeIter<'a, Pk> { + /// Helper function to return an empty iterator from Descriptor::tap_tree_iter. + pub(super) fn empty() -> Self { Self { stack: vec![] } } +} + impl<'a, Pk> Iterator for TapTreeIter<'a, Pk> where Pk: MiniscriptKey + 'a, diff --git a/src/miniscript/mod.rs b/src/miniscript/mod.rs index c03fbf218..79a9e9d57 100644 --- a/src/miniscript/mod.rs +++ b/src/miniscript/mod.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: CC0-1.0 -//! # Abstract Syntax Tree +//! Abstract Syntax Tree //! //! Defines a variety of data structures for describing Miniscript, a subset of //! Bitcoin Script which can be efficiently parsed and serialized from Script, @@ -289,6 +289,14 @@ impl Miniscript { Ctx::max_satisfaction_size(self).ok_or(Error::ImpossibleSatisfaction) } + /// Helper function to produce Taproot leaf hashes + fn leaf_hash_internal(&self) -> TapLeafHash + where + Pk: ToPublicKey, + { + TapLeafHash::from_script(&self.encode(), LeafVersion::TapScript) + } + /// Attempt to produce non-malleable satisfying witness for the /// witness script represented by the parse tree pub fn satisfy>(&self, satisfier: S) -> Result>, Error> @@ -296,9 +304,12 @@ impl Miniscript { Pk: ToPublicKey, { // Only satisfactions for default versions (0xc0) are allowed. - let leaf_hash = TapLeafHash::from_script(&self.encode(), LeafVersion::TapScript); - let satisfaction = - satisfy::Satisfaction::satisfy(&self.node, &satisfier, self.ty.mall.safe, &leaf_hash); + let satisfaction = satisfy::Satisfaction::satisfy( + &self.node, + &satisfier, + self.ty.mall.safe, + &self.leaf_hash_internal(), + ); self._satisfy(satisfaction) } @@ -311,12 +322,11 @@ impl Miniscript { where Pk: ToPublicKey, { - let leaf_hash = TapLeafHash::from_script(&self.encode(), LeafVersion::TapScript); let satisfaction = satisfy::Satisfaction::satisfy_mall( &self.node, &satisfier, self.ty.mall.safe, - &leaf_hash, + &self.leaf_hash_internal(), ); self._satisfy(satisfaction) } @@ -344,8 +354,12 @@ impl Miniscript { where Pk: ToPublicKey, { - let leaf_hash = TapLeafHash::from_script(&self.encode(), LeafVersion::TapScript); - satisfy::Satisfaction::build_template(&self.node, provider, self.ty.mall.safe, &leaf_hash) + satisfy::Satisfaction::build_template( + &self.node, + provider, + self.ty.mall.safe, + &self.leaf_hash_internal(), + ) } /// Attempt to produce a malleable witness template given the assets available @@ -356,16 +370,22 @@ impl Miniscript { where Pk: ToPublicKey, { - let leaf_hash = TapLeafHash::from_script(&self.encode(), LeafVersion::TapScript); satisfy::Satisfaction::build_template_mall( &self.node, provider, self.ty.mall.safe, - &leaf_hash, + &self.leaf_hash_internal(), ) } } +impl Miniscript<::Key, Tap> { + /// Returns the leaf hash used within a Taproot signature for this script. + /// + /// Note that this method is only implemented for Taproot Miniscripts. + pub fn leaf_hash(&self) -> TapLeafHash { self.leaf_hash_internal() } +} + impl Miniscript { /// Attempt to parse an insane(scripts don't clear sanity checks) /// script into a Miniscript representation. From 7877be026021e4dc078b0939ac9737ad1056cfc7 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Thu, 10 Oct 2024 13:39:37 +0000 Subject: [PATCH 11/13] ci: fix new clippy lint related to doccomments --- src/miniscript/iter.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/miniscript/iter.rs b/src/miniscript/iter.rs index a9373abf5..175fbfe4a 100644 --- a/src/miniscript/iter.rs +++ b/src/miniscript/iter.rs @@ -199,8 +199,8 @@ impl<'a, Pk: MiniscriptKey, Ctx: ScriptContext> Iterator for PkIter<'a, Pk, Ctx> } } -// Module is public since it export testcase generation which may be used in -// dependent libraries for their own tasts based on Miniscript AST +/// Module is public since it export testcase generation which may be used in +/// dependent libraries for their own tasts based on Miniscript AST #[cfg(test)] pub mod test { use bitcoin::hashes::{hash160, ripemd160, sha256, sha256d, Hash}; @@ -208,6 +208,7 @@ pub mod test { use super::Miniscript; use crate::miniscript::context::Segwitv0; + /// Test case. pub type TestData = ( Miniscript, Vec, @@ -215,6 +216,7 @@ pub mod test { bool, // Indicates that the top-level contains public key or hashes ); + /// Generate a deterministic list of public keys of the given length. pub fn gen_secp_pubkeys(n: usize) -> Vec { let mut ret = Vec::with_capacity(n); let secp = secp256k1::Secp256k1::new(); @@ -233,6 +235,7 @@ pub mod test { ret } + /// Generate a deterministic list of Bitcoin public keys of the given length. pub fn gen_bitcoin_pubkeys(n: usize, compressed: bool) -> Vec { gen_secp_pubkeys(n) .into_iter() @@ -240,6 +243,7 @@ pub mod test { .collect() } + /// Generate a deterministic list of test cases of the given length. pub fn gen_testcases() -> Vec { let k = gen_bitcoin_pubkeys(10, true); let _h: Vec = k From d8db3fabeccd442dbfb1faa3957c5d8400535638 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Thu, 10 Oct 2024 13:43:32 +0000 Subject: [PATCH 12/13] ci: clippy: remove a ton of now-elidable lifetimes --- src/descriptor/checksum.rs | 2 +- src/descriptor/mod.rs | 6 +++--- src/descriptor/tr.rs | 2 +- src/miniscript/iter.rs | 2 +- src/miniscript/lex.rs | 2 +- src/miniscript/satisfy.rs | 4 ++-- src/plan.rs | 2 +- src/primitives/threshold.rs | 4 ++-- src/psbt/mod.rs | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/descriptor/checksum.rs b/src/descriptor/checksum.rs index ec286f7e5..6a79194c2 100644 --- a/src/descriptor/checksum.rs +++ b/src/descriptor/checksum.rs @@ -177,7 +177,7 @@ impl<'f, 'a> Formatter<'f, 'a> { } } -impl<'f, 'a> fmt::Write for Formatter<'f, 'a> { +impl fmt::Write for Formatter<'_, '_> { fn write_str(&mut self, s: &str) -> fmt::Result { self.fmt.write_str(s)?; self.eng.input(s).map_err(|_| fmt::Error) diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 34ef35961..b172aa69e 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -732,7 +732,7 @@ impl Descriptor { struct KeyMapWrapper<'a, C: secp256k1::Signing>(KeyMap, &'a secp256k1::Secp256k1); - impl<'a, C: secp256k1::Signing> Translator for KeyMapWrapper<'a, C> { + impl Translator for KeyMapWrapper<'_, C> { type TargetPk = DescriptorPublicKey; type Error = Error; @@ -780,7 +780,7 @@ impl Descriptor { pub fn to_string_with_secret(&self, key_map: &KeyMap) -> String { struct KeyMapLookUp<'a>(&'a KeyMap); - impl<'a> Translator for KeyMapLookUp<'a> { + impl Translator for KeyMapLookUp<'_> { type TargetPk = String; type Error = core::convert::Infallible; @@ -943,7 +943,7 @@ impl Descriptor { ) -> Result, ConversionError> { struct Derivator<'a, C: secp256k1::Verification>(&'a secp256k1::Secp256k1); - impl<'a, C: secp256k1::Verification> Translator for Derivator<'a, C> { + impl Translator for Derivator<'_, C> { type TargetPk = bitcoin::PublicKey; type Error = ConversionError; diff --git a/src/descriptor/tr.rs b/src/descriptor/tr.rs index 0348368e8..30d6c5c74 100644 --- a/src/descriptor/tr.rs +++ b/src/descriptor/tr.rs @@ -465,7 +465,7 @@ pub struct TapTreeIter<'a, Pk: MiniscriptKey> { stack: Vec<(u8, &'a TapTree)>, } -impl<'a, Pk: MiniscriptKey> TapTreeIter<'a, Pk> { +impl TapTreeIter<'_, Pk> { /// Helper function to return an empty iterator from Descriptor::tap_tree_iter. pub(super) fn empty() -> Self { Self { stack: vec![] } } } diff --git a/src/miniscript/iter.rs b/src/miniscript/iter.rs index 175fbfe4a..f82e329aa 100644 --- a/src/miniscript/iter.rs +++ b/src/miniscript/iter.rs @@ -176,7 +176,7 @@ impl<'a, Pk: MiniscriptKey, Ctx: ScriptContext> PkIter<'a, Pk, Ctx> { } } -impl<'a, Pk: MiniscriptKey, Ctx: ScriptContext> Iterator for PkIter<'a, Pk, Ctx> { +impl Iterator for PkIter<'_, Pk, Ctx> { type Item = Pk; fn next(&mut self) -> Option { diff --git a/src/miniscript/lex.rs b/src/miniscript/lex.rs index 6184572dd..30dab98e9 100644 --- a/src/miniscript/lex.rs +++ b/src/miniscript/lex.rs @@ -50,7 +50,7 @@ pub enum Token<'s> { Bytes65(&'s [u8]), } -impl<'s> fmt::Display for Token<'s> { +impl fmt::Display for Token<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { Token::Num(n) => write!(f, "#{}", n), diff --git a/src/miniscript/satisfy.rs b/src/miniscript/satisfy.rs index 79c30613d..d1413401b 100644 --- a/src/miniscript/satisfy.rs +++ b/src/miniscript/satisfy.rs @@ -262,7 +262,7 @@ impl_satisfier_for_map_hash_tapleafhash_to_key_taproot_sig! { impl Satisfier for HashMap<(hash160::Hash, TapLeafHash), (Pk, bitcoin::taproot::Signature)> } -impl<'a, Pk: MiniscriptKey + ToPublicKey, S: Satisfier> Satisfier for &'a S { +impl> Satisfier for &S { fn lookup_ecdsa_sig(&self, p: &Pk) -> Option { (**self).lookup_ecdsa_sig(p) } @@ -322,7 +322,7 @@ impl<'a, Pk: MiniscriptKey + ToPublicKey, S: Satisfier> Satisfier for &' fn check_after(&self, n: absolute::LockTime) -> bool { (**self).check_after(n) } } -impl<'a, Pk: MiniscriptKey + ToPublicKey, S: Satisfier> Satisfier for &'a mut S { +impl> Satisfier for &mut S { fn lookup_ecdsa_sig(&self, p: &Pk) -> Option { (**self).lookup_ecdsa_sig(p) } diff --git a/src/plan.rs b/src/plan.rs index f3f148273..bcdfb311b 100644 --- a/src/plan.rs +++ b/src/plan.rs @@ -129,7 +129,7 @@ macro_rules! impl_log_method { } #[cfg(feature = "std")] -impl<'a> AssetProvider for LoggerAssetProvider<'a> { +impl AssetProvider for LoggerAssetProvider<'_> { impl_log_method!(provider_lookup_ecdsa_sig, pk: &DefiniteDescriptorKey, -> bool); impl_log_method!(provider_lookup_tap_key_spend_sig, pk: &DefiniteDescriptorKey, -> Option); impl_log_method!(provider_lookup_tap_leaf_script_sig, pk: &DefiniteDescriptorKey, leaf_hash: &TapLeafHash, -> Option); diff --git a/src/primitives/threshold.rs b/src/primitives/threshold.rs index 9a5315030..0045f9183 100644 --- a/src/primitives/threshold.rs +++ b/src/primitives/threshold.rs @@ -263,7 +263,7 @@ struct ThreshDisplay<'t, 's, T, const MAX: usize> { show_k: bool, } -impl<'t, 's, T, const MAX: usize> fmt::Display for ThreshDisplay<'t, 's, T, MAX> +impl fmt::Display for ThreshDisplay<'_, '_, T, MAX> where T: fmt::Display, { @@ -286,7 +286,7 @@ where } } -impl<'t, 's, T, const MAX: usize> fmt::Debug for ThreshDisplay<'t, 's, T, MAX> +impl fmt::Debug for ThreshDisplay<'_, '_, T, MAX> where T: fmt::Debug, { diff --git a/src/psbt/mod.rs b/src/psbt/mod.rs index 96615df4c..655878062 100644 --- a/src/psbt/mod.rs +++ b/src/psbt/mod.rs @@ -254,7 +254,7 @@ impl<'psbt> PsbtInputSatisfier<'psbt> { pub fn new(psbt: &'psbt Psbt, index: usize) -> Self { Self { psbt, index } } } -impl<'psbt, Pk: MiniscriptKey + ToPublicKey> Satisfier for PsbtInputSatisfier<'psbt> { +impl Satisfier for PsbtInputSatisfier<'_> { fn lookup_tap_key_spend_sig(&self) -> Option { self.psbt.inputs[self.index].tap_key_sig } From a620f67d3cac0da3c35ca301b2bf8e134a33627d Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Mon, 23 Sep 2024 10:16:27 +0200 Subject: [PATCH 13/13] feat: add support for OP_DROP opcode Implemented with the fragment `r:`. --- src/interpreter/mod.rs | 8 +++-- src/iter/mod.rs | 3 ++ src/miniscript/analyzable.rs | 22 +++++++++++- src/miniscript/astelem.rs | 1 + src/miniscript/decode.rs | 9 +++++ src/miniscript/display.rs | 2 ++ src/miniscript/mod.rs | 53 ++++++++++++++++++++++++++++ src/miniscript/ms_tests.rs | 9 +++++ src/miniscript/satisfy.rs | 2 ++ src/miniscript/types/correctness.rs | 13 +++++++ src/miniscript/types/extra_props.rs | 7 ++++ src/miniscript/types/malleability.rs | 8 ++++- src/miniscript/types/mod.rs | 12 +++++++ src/policy/compiler.rs | 5 +++ src/policy/mod.rs | 1 + src/psbt/mod.rs | 41 ++++++++++++++++++++- 16 files changed, 191 insertions(+), 5 deletions(-) diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index a7b316f23..29b32374a 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -682,13 +682,17 @@ where Terminal::DupIf(ref _sub) if node_state.n_evaluated == 1 => { self.stack.push(stack::Element::Satisfied); } - Terminal::ZeroNotEqual(ref sub) | Terminal::Verify(ref sub) + Terminal::ZeroNotEqual(ref sub) + | Terminal::Verify(ref sub) + | Terminal::Drop(ref sub) if node_state.n_evaluated == 0 => { self.push_evaluation_state(node_state.node, 1, 0); self.push_evaluation_state(sub, 0, 0); } - Terminal::Verify(ref _sub) if node_state.n_evaluated == 1 => { + Terminal::Verify(ref _sub) | Terminal::Drop(ref _sub) + if node_state.n_evaluated == 1 => + { match self.stack.pop() { Some(stack::Element::Satisfied) => (), Some(_) => return Some(Err(Error::VerifyFailed)), diff --git a/src/iter/mod.rs b/src/iter/mod.rs index abb8eda7b..6f9de12bf 100644 --- a/src/iter/mod.rs +++ b/src/iter/mod.rs @@ -33,6 +33,7 @@ impl<'a, Pk: MiniscriptKey, Ctx: ScriptContext> TreeLike for &'a Miniscript Tree::Unary(sub), AndV(ref left, ref right) @@ -63,6 +64,7 @@ impl<'a, Pk: MiniscriptKey, Ctx: ScriptContext> TreeLike for &'a Arc Tree::Unary(sub), AndV(ref left, ref right) @@ -93,6 +95,7 @@ impl<'a, Pk: MiniscriptKey, Ctx: ScriptContext> TreeLike for &'a Terminal Tree::Unary(sub.as_inner()), AndV(ref left, ref right) diff --git a/src/miniscript/analyzable.rs b/src/miniscript/analyzable.rs index 8fe61867d..39a40a6d7 100644 --- a/src/miniscript/analyzable.rs +++ b/src/miniscript/analyzable.rs @@ -39,6 +39,8 @@ pub struct ExtParams { /// Allow parsing of miniscripts with raw pkh fragments without the pk. /// This could be obtained when parsing miniscript from script pub raw_pkh: bool, + /// Allow parsing of miniscripts with drop fragments (`r`) + pub drop: bool, } impl ExtParams { @@ -51,6 +53,7 @@ impl ExtParams { malleability: false, repeated_pk: false, raw_pkh: false, + drop: false, } } @@ -68,6 +71,7 @@ impl ExtParams { malleability: true, repeated_pk: true, raw_pkh: false, + drop: true, } } @@ -80,6 +84,7 @@ impl ExtParams { malleability: true, repeated_pk: true, raw_pkh: true, + drop: true, } } @@ -118,6 +123,11 @@ impl ExtParams { self.raw_pkh = true; self } + + pub fn drop(mut self) -> ExtParams { + self.drop = true; + self + } } /// Possible reasons Miniscript guarantees can fail @@ -143,6 +153,8 @@ pub enum AnalysisError { Malleable, /// Contains partial descriptor raw pkh ContainsRawPkh, + /// Contains a drop fragment + ContainsDrop, } impl fmt::Display for AnalysisError { @@ -162,6 +174,7 @@ impl fmt::Display for AnalysisError { } AnalysisError::Malleable => f.write_str("Miniscript is malleable"), AnalysisError::ContainsRawPkh => f.write_str("Miniscript contains raw pkh"), + AnalysisError::ContainsDrop => f.write_str("Miniscript contains drop fragment"), } } } @@ -177,7 +190,8 @@ impl error::Error for AnalysisError { | BranchExceedResouceLimits | HeightTimelockCombination | Malleable - | ContainsRawPkh => None, + | ContainsRawPkh + | ContainsDrop => None, } } } @@ -213,6 +227,10 @@ impl Miniscript { self.iter().any(|ms| matches!(ms.node, Terminal::RawPkH(_))) } + pub fn contains_drop(&self) -> bool { + self.iter().any(|ms| matches!(ms.node, Terminal::Drop(_))) + } + /// Check whether the underlying Miniscript is safe under the current context /// Lifting these polices would create a semantic representation that does /// not represent the underlying semantics when miniscript is spent. @@ -252,6 +270,8 @@ impl Miniscript { Err(AnalysisError::HeightTimelockCombination) } else if !ext.raw_pkh && self.contains_raw_pkh() { Err(AnalysisError::ContainsRawPkh) + } else if !ext.drop && self.contains_drop() { + Err(AnalysisError::ContainsDrop) } else { Ok(()) } diff --git a/src/miniscript/astelem.rs b/src/miniscript/astelem.rs index 08ddd71d3..7fff32cf5 100644 --- a/src/miniscript/astelem.rs +++ b/src/miniscript/astelem.rs @@ -193,6 +193,7 @@ impl Terminal { .push_astelem(sub) .push_opcode(opcodes::all::OP_ENDIF), Terminal::Verify(ref sub) => builder.push_astelem(sub).push_verify(), + Terminal::Drop(ref sub) => builder.push_astelem(sub).push_opcode(opcodes::all::OP_DROP), Terminal::NonZero(ref sub) => builder .push_opcode(opcodes::all::OP_SIZE) .push_opcode(opcodes::all::OP_0NOTEQUAL) diff --git a/src/miniscript/decode.rs b/src/miniscript/decode.rs index 0090b7516..8fb515738 100644 --- a/src/miniscript/decode.rs +++ b/src/miniscript/decode.rs @@ -90,6 +90,7 @@ enum NonTerm { Check, DupIf, Verify, + Drop, NonZero, ZeroNotEqual, AndV, @@ -156,6 +157,8 @@ pub enum Terminal { DupIf(Arc>), /// `[T] VERIFY` Verify(Arc>), + /// `[T] DROP` + Drop(Arc>), /// `SIZE 0NOTEQUAL IF [Fn] ENDIF` NonZero(Arc>), /// `[X] 0NOTEQUAL` @@ -207,6 +210,7 @@ impl Clone for Terminal { Terminal::Check(ref sub) => Terminal::Check(Arc::new(Miniscript::clone(sub))), Terminal::DupIf(ref sub) => Terminal::DupIf(Arc::new(Miniscript::clone(sub))), Terminal::Verify(ref sub) => Terminal::Verify(Arc::new(Miniscript::clone(sub))), + Terminal::Drop(ref sub) => Terminal::Drop(Arc::new(Miniscript::clone(sub))), Terminal::NonZero(ref sub) => Terminal::NonZero(Arc::new(Miniscript::clone(sub))), Terminal::ZeroNotEqual(ref sub) => { Terminal::ZeroNotEqual(Arc::new(Miniscript::clone(sub))) @@ -460,6 +464,10 @@ pub fn parse( non_term.push(NonTerm::Expression); }, ), + Tk::Drop => { + non_term.push(NonTerm::Drop); + non_term.push(NonTerm::Expression); + }, Tk::ZeroNotEqual => { non_term.push(NonTerm::ZeroNotEqual); non_term.push(NonTerm::Expression); @@ -612,6 +620,7 @@ pub fn parse( Some(NonTerm::Check) => term.reduce1(Terminal::Check)?, Some(NonTerm::DupIf) => term.reduce1(Terminal::DupIf)?, Some(NonTerm::Verify) => term.reduce1(Terminal::Verify)?, + Some(NonTerm::Drop) => term.reduce1(Terminal::Drop)?, Some(NonTerm::NonZero) => term.reduce1(Terminal::NonZero)?, Some(NonTerm::ZeroNotEqual) => term.reduce1(Terminal::ZeroNotEqual)?, Some(NonTerm::AndV) => { diff --git a/src/miniscript/display.rs b/src/miniscript/display.rs index c6510b7d0..e5a19bd34 100644 --- a/src/miniscript/display.rs +++ b/src/miniscript/display.rs @@ -82,6 +82,7 @@ impl<'a, Pk: MiniscriptKey, Ctx: ScriptContext> TreeLike for DisplayNode<'a, Pk, | Terminal::Swap(ref sub) | Terminal::DupIf(ref sub) | Terminal::Verify(ref sub) + | Terminal::Drop(ref sub) | Terminal::NonZero(ref sub) | Terminal::ZeroNotEqual(ref sub) => { Tree::Unary(DisplayNode::Node(sub.ty, sub.as_inner())) @@ -256,6 +257,7 @@ impl Terminal { Terminal::Check(..) => "c", Terminal::DupIf(..) => "d", Terminal::Verify(..) => "v", + Terminal::Drop(..) => "r", Terminal::NonZero(..) => "j", Terminal::ZeroNotEqual(..) => "n", Terminal::AndV(_, ref r) if matches!(r.as_inner(), Terminal::True) => "t", diff --git a/src/miniscript/mod.rs b/src/miniscript/mod.rs index 79a9e9d57..f35e92fc8 100644 --- a/src/miniscript/mod.rs +++ b/src/miniscript/mod.rs @@ -103,6 +103,7 @@ mod private { Terminal::Check(..) => Terminal::Check(stack.pop().unwrap()), Terminal::DupIf(..) => Terminal::DupIf(stack.pop().unwrap()), Terminal::Verify(..) => Terminal::Verify(stack.pop().unwrap()), + Terminal::Drop(..) => Terminal::Drop(stack.pop().unwrap()), Terminal::NonZero(..) => Terminal::NonZero(stack.pop().unwrap()), Terminal::ZeroNotEqual(..) => Terminal::ZeroNotEqual(stack.pop().unwrap()), Terminal::AndV(..) => { @@ -236,6 +237,7 @@ impl Miniscript { Terminal::After(n) => script_num_size(n.to_consensus_u32() as usize) + 1, Terminal::Older(n) => script_num_size(n.to_consensus_u32() as usize) + 1, Terminal::Verify(ref sub) => usize::from(!sub.ext.has_free_verify), + Terminal::Drop(..) => 1, Terminal::Thresh(ref thresh) => { script_num_size(thresh.k()) // k + 1 // EQUAL @@ -573,6 +575,7 @@ impl Miniscript { Terminal::Check(..) => Terminal::Check(translated.pop().unwrap()), Terminal::DupIf(..) => Terminal::DupIf(translated.pop().unwrap()), Terminal::Verify(..) => Terminal::Verify(translated.pop().unwrap()), + Terminal::Drop(..) => Terminal::Drop(translated.pop().unwrap()), Terminal::NonZero(..) => Terminal::NonZero(translated.pop().unwrap()), Terminal::ZeroNotEqual(..) => Terminal::ZeroNotEqual(translated.pop().unwrap()), Terminal::AndV(..) => { @@ -638,6 +641,7 @@ impl Miniscript { Terminal::Check(..) => Terminal::Check(stack.pop().unwrap()), Terminal::DupIf(..) => Terminal::DupIf(stack.pop().unwrap()), Terminal::Verify(..) => Terminal::Verify(stack.pop().unwrap()), + Terminal::Drop(..) => Terminal::Drop(stack.pop().unwrap()), Terminal::NonZero(..) => Terminal::NonZero(stack.pop().unwrap()), Terminal::ZeroNotEqual(..) => Terminal::ZeroNotEqual(stack.pop().unwrap()), Terminal::AndV(..) => Terminal::AndV(stack.pop().unwrap(), stack.pop().unwrap()), @@ -749,6 +753,7 @@ where 'c' => unwrapped = Terminal::Check(Arc::new(ms)), 'd' => unwrapped = Terminal::DupIf(Arc::new(ms)), 'v' => unwrapped = Terminal::Verify(Arc::new(ms)), + 'r' => unwrapped = Terminal::Drop(Arc::new(ms)), 'j' => unwrapped = Terminal::NonZero(Arc::new(ms)), 'n' => unwrapped = Terminal::ZeroNotEqual(Arc::new(ms)), 't' => unwrapped = Terminal::AndV(Arc::new(ms), Arc::new(Miniscript::TRUE)), @@ -1222,6 +1227,15 @@ mod tests { assert_eq!(abs.minimum_n_keys(), Some(3)); roundtrip(&ms_str!("older(921)"), "OP_PUSHBYTES_2 9903 OP_CSV"); + roundtrip( + &ms_str!("and_v(r:after(1024),1)"), + "OP_PUSHBYTES_2 0004 OP_CLTV OP_DROP OP_PUSHNUM_1", + ); + roundtrip( + &ms_str!("and_v(r:older(1024),1)"), + "OP_PUSHBYTES_2 0004 OP_CSV OP_DROP OP_PUSHNUM_1", + ); + roundtrip(&ms_str!("and_v(r:1,1)"), "OP_PUSHNUM_1 OP_DROP OP_PUSHNUM_1"); roundtrip( &ms_str!("sha256({})",sha256::Hash::hash(&[])), @@ -1481,6 +1495,45 @@ mod tests { assert!(ms_str.is_err()); } + #[test] + fn drop_wrapper() { + type SwMs = Miniscript; + fn assert_error(s: &str, expected_error: Option<&str>) { + match SwMs::from_str_insane(&s) { + Ok(_) => match expected_error { + Some(e) => { + panic!("Expected error: {}", e); + } + None => { + // do nothing + } + }, + Err(e1) => match expected_error { + Some(e2) => assert_eq!(e1.to_string(), e2.to_string()), + None => { + panic!("Unexpected error: {}", e1); + } + }, + } + } + + { + assert_error("and_v(r:after(1024),1)", None); + } + + { + fn assert_error_cannot_wrap(s: &str, prefix: &str) { + let err_cannot_wrap = format!( + "typecheck: fragment «{}:after(1024)» cannot wrap a fragment of type V", + prefix + ); + assert_error(s, Some(&err_cannot_wrap)); + } + assert_error_cannot_wrap("and_v(rr:after(1024),1)", "rr"); + assert_error_cannot_wrap("and_v(rv:after(1024),1)", "rv"); + } + } + #[test] fn translate_tests() { let ms = Miniscript::::from_str("pk(A)").unwrap(); diff --git a/src/miniscript/ms_tests.rs b/src/miniscript/ms_tests.rs index c357a6a64..6d1a30c29 100644 --- a/src/miniscript/ms_tests.rs +++ b/src/miniscript/ms_tests.rs @@ -8,6 +8,7 @@ #[cfg(test)] mod tests { use core::fmt; + use core::str::FromStr; use crate::miniscript::types; use crate::{Miniscript, Segwitv0}; @@ -15047,6 +15048,7 @@ mod tests { #[cfg_attr(feature="cargo-fmt", rustfmt_skip)] fn malleable_tests_from_alloy() { ms_test("and_v(v:after(500000001),or_d(j:multi(2,A,B,C),multi(2,D,E,F)))", "usB"); + ms_test("and_v(r:after(500000001),or_d(j:multi(2,A,B,C),multi(2,D,E,F)))", "usB"); ms_test("or_b(j:multi(2,A,B,C),a:andor(multi(2,D,E,F),multi(2,G,I,J),multi(2,K,L,M)))", "dBesu"); ms_test("andor(or_i(multi(2,A,B,C),0),sha256(c7bcb868ab4db55ca45f8eefe5b1677d9fc2c4111e295baaee1b34ed352c719b),multi(2,D,E,F))", "dBesu"); ms_test("or_d(or_i(0,or_i(multi(2,A,B,C),0)),multi(2,D,E,F))", "dBesu"); @@ -23855,4 +23857,11 @@ mod tests { } } + + #[test] + pub fn test_opdrop() { + Miniscript::::from_str("and_v(v:after(100000),multi(1,A,B))").unwrap(); + let ms: Miniscript = + Miniscript::from_str("and_v(v:after(100000),multi(1,A,B))").unwrap(); + } } diff --git a/src/miniscript/satisfy.rs b/src/miniscript/satisfy.rs index d1413401b..c1ba32e09 100644 --- a/src/miniscript/satisfy.rs +++ b/src/miniscript/satisfy.rs @@ -1338,6 +1338,7 @@ impl Satisfaction> { | Terminal::Swap(ref sub) | Terminal::Check(ref sub) | Terminal::Verify(ref sub) + | Terminal::Drop(ref sub) | Terminal::NonZero(ref sub) | Terminal::ZeroNotEqual(ref sub) => { Self::satisfy_helper(&sub.node, stfr, root_has_sig, leaf_hash, min_fn, thresh_fn) @@ -1647,6 +1648,7 @@ impl Satisfaction> { | Terminal::Older(_) | Terminal::After(_) | Terminal::Verify(_) + | Terminal::Drop(_) | Terminal::OrC(..) => Satisfaction { stack: Witness::Impossible, has_sig: false, diff --git a/src/miniscript/types/correctness.rs b/src/miniscript/types/correctness.rs index de08001dc..4c7459268 100644 --- a/src/miniscript/types/correctness.rs +++ b/src/miniscript/types/correctness.rs @@ -238,6 +238,19 @@ impl Correctness { }) } + /// Constructor for the correctness properties of the `r:` fragment. + pub const fn cast_drop(self) -> Result { + // from https://bitcoin.sipa.be/miniscript/: + // > Every miniscript expression has one of four basic types: + // > ... + // > "V" Verify expressions. Like "B", these take their inputs from the top of the stack. + // > Upon satisfaction however, they continue without pushing anything. + // > They cannot be dissatisfied (will abort instead). + // So while OP_DROP doesn't actually verify anything, the closest type is still `V`. + // We delegate to `cast_verify` to handle the rest of the properties. + Self::cast_verify(self) + } + /// Constructor for the correctness properties of the `j:` fragment. pub const fn cast_nonzero(self) -> Result { if !self.input.constfn_eq(Input::OneNonZero) && !self.input.constfn_eq(Input::AnyNonZero) { diff --git a/src/miniscript/types/extra_props.rs b/src/miniscript/types/extra_props.rs index 4f4a92840..4f8676222 100644 --- a/src/miniscript/types/extra_props.rs +++ b/src/miniscript/types/extra_props.rs @@ -488,6 +488,12 @@ impl ExtData { } } + /// Extra properties for the `r:` fragment. + pub fn cast_drop(self) -> Self { + // delegate to `cast_verify` as the properties are the same + self.cast_verify() + } + /// Extra properties for the `j:` fragment. pub const fn cast_nonzero(self) -> Self { ExtData { @@ -947,6 +953,7 @@ impl ExtData { Terminal::Check(ref sub) => Self::cast_check(sub.ext), Terminal::DupIf(ref sub) => Self::cast_dupif(sub.ext), Terminal::Verify(ref sub) => Self::cast_verify(sub.ext), + Terminal::Drop(ref sub) => Self::cast_drop(sub.ext), Terminal::NonZero(ref sub) => Self::cast_nonzero(sub.ext), Terminal::ZeroNotEqual(ref sub) => Self::cast_zeronotequal(sub.ext), Terminal::AndB(ref l, ref r) => { diff --git a/src/miniscript/types/malleability.rs b/src/miniscript/types/malleability.rs index 2c15127df..4c07aaa94 100644 --- a/src/miniscript/types/malleability.rs +++ b/src/miniscript/types/malleability.rs @@ -141,11 +141,17 @@ impl Malleability { } } - /// Constructor for the malleabilitiy properties of the `v:` fragment. + /// Constructor for the malleability properties of the `v:` fragment. pub const fn cast_verify(self) -> Self { Malleability { dissat: Dissat::None, safe: self.safe, non_malleable: self.non_malleable } } + /// Constructor for the malleability properties of the `r:` fragment. + pub const fn cast_drop(self) -> Self { + // delegate to `cast_verify()` + self.cast_verify() + } + /// Constructor for the malleabilitiy properties of the `j:` fragment. pub const fn cast_nonzero(self) -> Self { Malleability { diff --git a/src/miniscript/types/mod.rs b/src/miniscript/types/mod.rs index 1b1cb2e85..5a13e1e9b 100644 --- a/src/miniscript/types/mod.rs +++ b/src/miniscript/types/mod.rs @@ -296,6 +296,17 @@ impl Type { }) } + /// Constructor for the type of the `r:` fragment. + pub const fn cast_drop(self) -> Result { + Ok(Type { + corr: match Correctness::cast_drop(self.corr) { + Ok(x) => x, + Err(e) => return Err(e), + }, + mall: Malleability::cast_drop(self.mall), + }) + } + /// Constructor for the type of the `j:` fragment. pub const fn cast_nonzero(self) -> Result { Ok(Type { @@ -471,6 +482,7 @@ impl Type { Terminal::Check(ref sub) => wrap_err(Self::cast_check(sub.ty)), Terminal::DupIf(ref sub) => wrap_err(Self::cast_dupif(sub.ty)), Terminal::Verify(ref sub) => wrap_err(Self::cast_verify(sub.ty)), + Terminal::Drop(ref sub) => wrap_err(Self::cast_drop(sub.ty)), Terminal::NonZero(ref sub) => wrap_err(Self::cast_nonzero(sub.ty)), Terminal::ZeroNotEqual(ref sub) => wrap_err(Self::cast_zeronotequal(sub.ty)), Terminal::AndB(ref l, ref r) => { diff --git a/src/policy/compiler.rs b/src/policy/compiler.rs index 63e16579e..848f00c96 100644 --- a/src/policy/compiler.rs +++ b/src/policy/compiler.rs @@ -250,6 +250,10 @@ impl CompilerExtData { CompilerExtData { branch_prob: None, sat_cost: self.sat_cost, dissat_cost: None } } + fn cast_drop(self) -> Self { + CompilerExtData { branch_prob: None, sat_cost: 0.0, dissat_cost: None } + } + fn cast_nonzero(self) -> Self { CompilerExtData { branch_prob: None, sat_cost: self.sat_cost, dissat_cost: Some(1.0) } } @@ -449,6 +453,7 @@ impl CompilerExtData { Terminal::Check(ref sub) => Self::cast_check(get_child(&sub.node, 0)), Terminal::DupIf(ref sub) => Self::cast_dupif(get_child(&sub.node, 0)), Terminal::Verify(ref sub) => Self::cast_verify(get_child(&sub.node, 0)), + Terminal::Drop(ref sub) => Self::cast_drop(get_child(&sub.node, 0)), Terminal::NonZero(ref sub) => Self::cast_nonzero(get_child(&sub.node, 0)), Terminal::ZeroNotEqual(ref sub) => Self::cast_zeronotequal(get_child(&sub.node, 0)), Terminal::AndB(ref l, ref r) => { diff --git a/src/policy/mod.rs b/src/policy/mod.rs index de9577e68..1c9ac2dc7 100644 --- a/src/policy/mod.rs +++ b/src/policy/mod.rs @@ -135,6 +135,7 @@ impl Liftable for Miniscript | Terminal::Check(..) | Terminal::DupIf(..) | Terminal::Verify(..) + | Terminal::Drop(..) | Terminal::NonZero(..) | Terminal::ZeroNotEqual(..) => stack.pop().unwrap(), Terminal::AndV(..) | Terminal::AndB(..) => Arc::new(Semantic::Thresh( diff --git a/src/psbt/mod.rs b/src/psbt/mod.rs index 655878062..fad0a5029 100644 --- a/src/psbt/mod.rs +++ b/src/psbt/mod.rs @@ -1414,7 +1414,7 @@ mod tests { use bitcoin::hashes::hex::FromHex; use bitcoin::key::XOnlyPublicKey; use bitcoin::secp256k1::PublicKey; - use bitcoin::{Amount, OutPoint, TxIn, TxOut}; + use bitcoin::{Amount, OutPoint, Sequence, TxIn, TxOut}; use super::*; use crate::Miniscript; @@ -1696,4 +1696,43 @@ mod tests { "output script_pubkey no longer matches" ); } + + fn test_sign_input_with_descriptor( + desc: Descriptor, + xpriv: bip32::Xpriv, + ) -> Psbt { + let secp = Secp256k1::new(); + let tx = bitcoin::Transaction { + version: transaction::Version::ONE, + lock_time: absolute::LockTime::from_height(1024).expect("locktime"), + input: vec![TxIn { + previous_output: OutPoint { ..OutPoint::default() }, + sequence: Sequence::ENABLE_LOCKTIME_NO_RBF, + ..Default::default() + }], + output: vec![], + }; + let mut psbt = Psbt::from_unsigned_tx(tx).unwrap(); + psbt.inputs[0].witness_utxo = + Some(TxOut { value: Amount::from_sat(1_000), script_pubkey: desc.script_pubkey() }); + psbt.inputs[0] + .update_with_descriptor_unchecked(&desc) + .unwrap(); + psbt.sign(&xpriv, &secp).unwrap(); + psbt.finalize_mut(&secp).unwrap(); + psbt + } + + #[test] + fn test_finalize_with_opdrop() { + let secp = Secp256k1::new(); + let xpriv = bip32::Xpriv::new_master(bitcoin::Network::Testnet, &[42]).expect("master key"); + let desc = format!( + "wsh(and_v(r:after(1024),pk({})))", + xpriv.to_keypair(&secp).public_key().to_string() + ); + // let desc = format!("wsh(pk({}))", xpriv.to_keypair(&secp).public_key().to_string()); + let desc = Descriptor::::from_str(&desc).unwrap(); + test_sign_input_with_descriptor(desc, xpriv); + } }