diff --git a/CHANGELOG.md b/CHANGELOG.md index 719c1f5cd..be2c32172 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +# Taproot updates +- Changed the ToPublicKey trait to support x-only keys. # 6.0.1 - Aug 5, 2021 - The `lift` method on a Miniscript node was fixed. It would previously mix up diff --git a/fuzz/fuzz_targets/roundtrip_miniscript_script.rs b/fuzz/fuzz_targets/roundtrip_miniscript_script.rs index 44f24623f..31efd7204 100644 --- a/fuzz/fuzz_targets/roundtrip_miniscript_script.rs +++ b/fuzz/fuzz_targets/roundtrip_miniscript_script.rs @@ -8,7 +8,7 @@ fn do_test(data: &[u8]) { // Try round-tripping as a script let script = script::Script::from(data.to_owned()); - if let Ok(pt) = Miniscript::<_, Segwitv0>::parse(&script) { + if let Ok(pt) = Miniscript::::parse(&script) { let output = pt.encode(); assert_eq!(pt.script_size(), output.len()); assert_eq!(output, script); diff --git a/src/descriptor/bare.rs b/src/descriptor/bare.rs index 2279d6d7f..7449afd2e 100644 --- a/src/descriptor/bare.rs +++ b/src/descriptor/bare.rs @@ -339,7 +339,7 @@ impl DescriptorTrait for Pkh { } fn max_satisfaction_weight(&self) -> Result { - Ok(4 * (1 + 73 + self.pk.serialized_len())) + Ok(4 * (1 + 73 + BareCtx::pk_len(&self.pk))) } fn script_code(&self) -> Script diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index fa2b1eeda..e0ee3c227 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -921,7 +921,7 @@ mod tests { struct SimpleSat { sig: secp256k1::Signature, pk: bitcoin::PublicKey, - }; + } impl Satisfier for SimpleSat { fn lookup_sig(&self, pk: &bitcoin::PublicKey) -> Option { diff --git a/src/descriptor/segwitv0.rs b/src/descriptor/segwitv0.rs index d5b47ce3d..9cabf5668 100644 --- a/src/descriptor/segwitv0.rs +++ b/src/descriptor/segwitv0.rs @@ -293,7 +293,9 @@ impl Wpkh { pub fn new(pk: Pk) -> Result { // do the top-level checks if pk.is_uncompressed() { - Err(Error::ContextError(ScriptContextError::CompressedOnly)) + Err(Error::ContextError(ScriptContextError::CompressedOnly( + pk.to_string(), + ))) } else { Ok(Self { pk: pk }) } @@ -376,7 +378,9 @@ where impl DescriptorTrait for Wpkh { fn sanity_check(&self) -> Result<(), Error> { if self.pk.is_uncompressed() { - Err(Error::ContextError(ScriptContextError::CompressedOnly)) + Err(Error::ContextError(ScriptContextError::CompressedOnly( + self.pk.to_string(), + ))) } else { Ok(()) } @@ -430,7 +434,7 @@ impl DescriptorTrait for Wpkh { } fn max_satisfaction_weight(&self) -> Result { - Ok(4 + 1 + 73 + self.pk.serialized_len()) + Ok(4 + 1 + 73 + Segwitv0::pk_len(&self.pk)) } fn script_code(&self) -> Script diff --git a/src/descriptor/sortedmulti.rs b/src/descriptor/sortedmulti.rs index 7c8b4af1d..3f9b79648 100644 --- a/src/descriptor/sortedmulti.rs +++ b/src/descriptor/sortedmulti.rs @@ -178,7 +178,7 @@ impl SortedMultiVec { script_num_size(self.k) + 1 + script_num_size(self.pks.len()) - + self.pks.iter().map(|pk| pk.serialized_len()).sum::() + + self.pks.iter().map(|pk| Ctx::pk_len(pk)).sum::() } /// Maximum number of witness elements used to satisfy the Miniscript diff --git a/src/interpreter/error.rs b/src/interpreter/error.rs index 4db21dfa0..b19fe444f 100644 --- a/src/interpreter/error.rs +++ b/src/interpreter/error.rs @@ -103,7 +103,7 @@ impl From<::Error> for Error { } impl error::Error for Error { - fn cause(&self) -> Option<&error::Error> { + fn cause(&self) -> Option<&dyn error::Error> { match *self { Error::Secp(ref err) => Some(err), ref x => Some(x), diff --git a/src/interpreter/inner.rs b/src/interpreter/inner.rs index 5c49c47ee..3da35ee49 100644 --- a/src/interpreter/inner.rs +++ b/src/interpreter/inner.rs @@ -50,7 +50,8 @@ fn script_from_stackelem<'a>( ) -> Result, Error> { match *elem { stack::Element::Push(sl) => { - Miniscript::parse_insane(&bitcoin::Script::from(sl.to_owned())).map_err(Error::from) + Miniscript::::parse_insane(&bitcoin::Script::from(sl.to_owned())) + .map_err(Error::from) } stack::Element::Satisfied => Miniscript::from_ast(::Terminal::True).map_err(Error::from), stack::Element::Dissatisfied => { @@ -270,7 +271,7 @@ pub fn from_txdata<'txin>( // ** bare script ** } else { if wit_stack.is_empty() { - let miniscript = Miniscript::parse_insane(spk)?; + let miniscript = Miniscript::::parse_insane(spk)?; Ok(( Inner::Script(miniscript, ScriptType::Bare), ssig_stack, diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index 4c46967a3..47bb9de1a 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -847,7 +847,7 @@ mod tests { height: 1002, has_errored: false, } - }; + } let pk = ms_str!("c:pk_k({})", pks[0]); let pkh = ms_str!("c:pk_h({})", pks[1].to_pubkeyhash()); diff --git a/src/lib.rs b/src/lib.rs index ce9eb4361..4e969042b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,7 +87,6 @@ //! ``` //! //! -#![allow(bare_trait_objects)] #![cfg_attr(all(test, feature = "unstable"), feature(test))] // Coding conventions #![deny(unsafe_code)] @@ -125,7 +124,7 @@ use bitcoin::hashes::{hash160, sha256, Hash}; pub use descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait}; pub use interpreter::Interpreter; -pub use miniscript::context::{BareCtx, Legacy, ScriptContext, Segwitv0}; +pub use miniscript::context::{BareCtx, Legacy, ScriptContext, Segwitv0, Tap}; pub use miniscript::decode::Terminal; pub use miniscript::satisfy::{BitcoinSig, Preimage32, Satisfier}; pub use miniscript::Miniscript; @@ -142,16 +141,6 @@ pub trait MiniscriptKey: Clone + Eq + Ord + fmt::Debug + fmt::Display + hash::Ha /// Converts an object to PublicHash fn to_pubkeyhash(&self) -> Self::Hash; - - /// Computes the size of a public key when serialized in a script, - /// including the length bytes - fn serialized_len(&self) -> usize { - if self.is_uncompressed() { - 66 - } else { - 34 - } - } } impl MiniscriptKey for bitcoin::PublicKey { @@ -170,6 +159,14 @@ impl MiniscriptKey for bitcoin::PublicKey { } } +impl MiniscriptKey for bitcoin::schnorr::PublicKey { + type Hash = hash160::Hash; + + fn to_pubkeyhash(&self) -> Self::Hash { + hash160::Hash::hash(&self.serialize()) + } +} + impl MiniscriptKey for String { type Hash = String; @@ -183,6 +180,12 @@ pub trait ToPublicKey: MiniscriptKey { /// Converts an object to a public key fn to_public_key(&self) -> bitcoin::PublicKey; + /// Convert an object to x-only pubkey + fn to_x_only_pubkey(&self) -> bitcoin::schnorr::PublicKey { + let pk = self.to_public_key(); + bitcoin::schnorr::PublicKey::from(pk.key) + } + /// Converts a hashed version of the public key to a `hash160` hash. /// /// This method must be consistent with `to_public_key`, in the sense @@ -202,6 +205,25 @@ impl ToPublicKey for bitcoin::PublicKey { } } +impl ToPublicKey for bitcoin::schnorr::PublicKey { + fn to_public_key(&self) -> bitcoin::PublicKey { + // This code should never be used. + // But is implemented for completeness + let mut data: Vec = vec![0x02]; + data.extend(self.serialize().iter()); + bitcoin::PublicKey::from_slice(&data) + .expect("Failed to construct 33 Publickey from 0x02 appended x-only key") + } + + fn to_x_only_pubkey(&self) -> bitcoin::schnorr::PublicKey { + *self + } + + fn hash_to_hash160(hash: &hash160::Hash) -> hash160::Hash { + *hash + } +} + /// Dummy key which de/serializes to the empty string; useful sometimes for testing #[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Debug)] pub struct DummyKey; @@ -448,7 +470,7 @@ pub enum Error { InvalidOpcode(opcodes::All), /// Some opcode occurred followed by `OP_VERIFY` when it had /// a `VERIFY` version that should have been used instead - NonMinimalVerify(miniscript::lex::Token), + NonMinimalVerify(String), /// Push was illegal in some context InvalidPush(Vec), /// rust-bitcoin script error @@ -517,6 +539,8 @@ pub enum Error { ImpossibleSatisfaction, /// Bare descriptors don't have any addresses BareDescriptorAddr, + /// PubKey invalid under current context + PubKeyCtxError(miniscript::decode::KeyParseError, &'static str), } #[doc(hidden)] @@ -563,7 +587,7 @@ fn errstr(s: &str) -> Error { } impl error::Error for Error { - fn cause(&self) -> Option<&error::Error> { + fn cause(&self) -> Option<&dyn error::Error> { match *self { Error::BadPubkey(ref e) => Some(e), _ => None, @@ -580,7 +604,7 @@ impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { Error::InvalidOpcode(op) => write!(f, "invalid opcode {}", op), - Error::NonMinimalVerify(tok) => write!(f, "{} VERIFY", tok), + Error::NonMinimalVerify(ref tok) => write!(f, "{} VERIFY", tok), Error::InvalidPush(ref push) => write!(f, "invalid push {:?}", push), // TODO hexify this Error::Script(ref e) => fmt::Display::fmt(e, f), Error::CmsTooManyKeys(n) => write!(f, "checkmultisig with {} keys", n), @@ -634,6 +658,9 @@ impl fmt::Display for Error { Error::AnalysisError(ref e) => e.fmt(f), Error::ImpossibleSatisfaction => write!(f, "Impossible to satisfy Miniscript"), Error::BareDescriptorAddr => write!(f, "Bare descriptors don't have address"), + Error::PubKeyCtxError(ref pk, ref ctx) => { + write!(f, "Pubkey error: {} under {} scriptcontext", pk, ctx) + } } } } diff --git a/src/miniscript/astelem.rs b/src/miniscript/astelem.rs index 590f2ffe1..019fdd36f 100644 --- a/src/miniscript/astelem.rs +++ b/src/miniscript/astelem.rs @@ -32,6 +32,8 @@ use expression; use miniscript::types::{self, Property}; use miniscript::ScriptContext; use script_num_size; + +use util::MsKeyBuilder; use {Error, ForEach, ForEachKey, Miniscript, MiniscriptKey, Terminal, ToPublicKey, TranslatePk}; impl Terminal { @@ -628,7 +630,7 @@ impl Terminal { Pk: ToPublicKey, { match *self { - Terminal::PkK(ref pk) => builder.push_key(&pk.to_public_key()), + Terminal::PkK(ref pk) => builder.push_ms_key::<_, Ctx>(pk), Terminal::PkH(ref hash) => builder .push_opcode(opcodes::all::OP_DUP) .push_opcode(opcodes::all::OP_HASH160) @@ -734,6 +736,7 @@ impl Terminal { .push_opcode(opcodes::all::OP_EQUAL) } Terminal::Multi(k, ref keys) => { + debug_assert!(!Ctx::is_tap()); builder = builder.push_int(k as i64); for pk in keys { builder = builder.push_key(&pk.to_public_key()); @@ -754,7 +757,7 @@ impl Terminal { /// will handle the segwit/non-segwit technicalities for you. pub fn script_size(&self) -> usize { match *self { - Terminal::PkK(ref pk) => pk.serialized_len(), + Terminal::PkK(ref pk) => Ctx::pk_len(pk), Terminal::PkH(..) => 24, Terminal::After(n) => script_num_size(n as usize) + 1, Terminal::Older(n) => script_num_size(n as usize) + 1, @@ -794,7 +797,7 @@ impl Terminal { script_num_size(k) + 1 + script_num_size(pks.len()) - + pks.iter().map(|pk| pk.serialized_len()).sum::() + + pks.iter().map(|pk| Ctx::pk_len(pk)).sum::() } } } diff --git a/src/miniscript/context.rs b/src/miniscript/context.rs index 54a662349..169df46af 100644 --- a/src/miniscript/context.rs +++ b/src/miniscript/context.rs @@ -14,17 +14,22 @@ use std::{fmt, hash}; +use bitcoin; +use bitcoin::blockdata::constants::MAX_BLOCK_WEIGHT; use miniscript::limits::{ MAX_OPS_PER_SCRIPT, MAX_SCRIPTSIG_SIZE, MAX_SCRIPT_ELEMENT_SIZE, MAX_SCRIPT_SIZE, - MAX_STANDARD_P2WSH_SCRIPT_SIZE, MAX_STANDARD_P2WSH_STACK_ITEMS, + MAX_STACK_SIZE, MAX_STANDARD_P2WSH_SCRIPT_SIZE, MAX_STANDARD_P2WSH_STACK_ITEMS, }; use miniscript::types; use util::witness_to_scriptsig; use Error; + +use super::decode::ParseableKey; + use {Miniscript, MiniscriptKey, Terminal}; /// Error for Script Context -#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug)] pub enum ScriptContextError { /// Script Context does not permit PkH for non-malleability /// It is not possible to estimate the pubkey size at the creation @@ -38,10 +43,13 @@ pub enum ScriptContextError { MalleableDupIf, /// Only Compressed keys allowed under current descriptor /// Segwitv0 fragments do not allow uncompressed pubkeys - CompressedOnly, + CompressedOnly(String), + /// Tapscript descriptors cannot contain uncompressed keys + /// Tap context can contain compressed or xonly + UncompressedKeysNotAllowed, /// At least one satisfaction path in the Miniscript fragment has more than /// `MAX_STANDARD_P2WSH_STACK_ITEMS` (100) witness elements. - MaxWitnessItemssExceeded, + MaxWitnessItemssExceeded { actual: usize, limit: usize }, /// At least one satisfaction path in the Miniscript fragment contains more /// than `MAX_OPS_PER_SCRIPT`(201) opcodes. MaxOpCountExceeded, @@ -55,6 +63,10 @@ pub enum ScriptContextError { MaxScriptSigSizeExceeded, /// Impossible to satisfy the miniscript under the current context ImpossibleSatisfaction, + /// No Multi Node in Taproot context + TaprootMultiDisabled, + /// Stack size exceeded in script execution + StackSizeLimitExceeded { actual: usize, limit: usize }, } impl fmt::Display for ScriptContextError { @@ -65,13 +77,24 @@ impl fmt::Display for ScriptContextError { ScriptContextError::MalleableDupIf => { write!(f, "DupIf is malleable under Legacy rules") } - ScriptContextError::CompressedOnly => { - write!(f, "Uncompressed pubkeys not allowed in segwit context") + ScriptContextError::CompressedOnly(ref pk) => { + write!( + f, + "Only Compressed pubkeys are allowed in segwit context. Found {}", + pk + ) + } + ScriptContextError::UncompressedKeysNotAllowed => { + write!( + f, + "uncompressed keys cannot be used in Taproot descriptors." + ) } - ScriptContextError::MaxWitnessItemssExceeded => write!( + ScriptContextError::MaxWitnessItemssExceeded { actual, limit } => write!( f, - "At least one spending path in the Miniscript fragment has more \ - witness items than MAX_STANDARD_P2WSH_STACK_ITEMS.", + "At least one spending path in the Miniscript fragment has {} more \ + witness items than limit {}.", + actual, limit ), ScriptContextError::MaxOpCountExceeded => write!( f, @@ -99,6 +122,16 @@ impl fmt::Display for ScriptContextError { "Impossible to satisfy Miniscript under the current context" ) } + ScriptContextError::TaprootMultiDisabled => { + write!(f, "Invalid use of Multi node in taproot context") + } + ScriptContextError::StackSizeLimitExceeded { actual, limit } => { + write!( + f, + "Stack limit {} can exceed the allowed limit {} in at least one script path during script execution", + actual, limit + ) + } } } } @@ -109,7 +142,11 @@ impl fmt::Display for ScriptContextError { /// For example, disallowing uncompressed keys in Segwit context pub trait ScriptContext: fmt::Debug + Clone + Ord + PartialOrd + Eq + PartialEq + hash::Hash + private::Sealed +where + Self::Key: MiniscriptKey, { + /// The consensus key associated with the type. Must be a parseable key + type Key: ParseableKey; /// Depending on ScriptContext, fragments can be malleable. For Example, /// under Legacy context, PkH is malleable because it is possible to /// estimate the cost of satisfaction because of compressed keys @@ -243,6 +280,22 @@ pub trait ScriptContext: Self::top_level_type_check(ms)?; Self::other_top_level_checks(ms) } + + /// Reverse lookup to store whether the context is tapscript. + /// pk(33-byte key) is a valid under both tapscript context and segwitv0 context + /// We need to context decide whether the serialize pk to 33 byte or 32 bytes. + fn is_tap() -> bool { + return false; + } + + /// Get the len of public key when serialized based on context + /// Note that this includes the serialization prefix. Returns + /// 34/66 for Bare/Legacy based on key compressedness + /// 34 for Segwitv0, 33 for Tap + fn pk_len(pk: &Pk) -> usize; + + /// Local helper function to display error messages with context + fn name_str() -> &'static str; } /// Legacy ScriptContext @@ -253,6 +306,7 @@ pub trait ScriptContext: pub enum Legacy {} impl ScriptContext for Legacy { + type Key = bitcoin::PublicKey; fn check_terminal_non_malleable( frag: &Terminal, ) -> Result<(), ScriptContextError> { @@ -317,6 +371,18 @@ impl ScriptContext for Legacy { // The scriptSig cost is the second element of the tuple ms.ext.max_sat_size.map(|x| x.1) } + + fn pk_len(pk: &Pk) -> usize { + if pk.is_uncompressed() { + 66 + } else { + 34 + } + } + + fn name_str() -> &'static str { + "Legacy/p2sh" + } } /// Segwitv0 ScriptContext @@ -324,6 +390,7 @@ impl ScriptContext for Legacy { pub enum Segwitv0 {} impl ScriptContext for Segwitv0 { + type Key = bitcoin::PublicKey; fn check_terminal_non_malleable( _frag: &Terminal, ) -> Result<(), ScriptContextError> { @@ -334,7 +401,10 @@ impl ScriptContext for Segwitv0 { witness: &[Vec], ) -> Result<(), ScriptContextError> { if witness.len() > MAX_STANDARD_P2WSH_STACK_ITEMS { - return Err(ScriptContextError::MaxWitnessItemssExceeded); + return Err(ScriptContextError::MaxWitnessItemssExceeded { + actual: witness.len(), + limit: MAX_STANDARD_P2WSH_STACK_ITEMS, + }); } Ok(()) } @@ -349,7 +419,15 @@ impl ScriptContext for Segwitv0 { match ms.node { Terminal::PkK(ref pk) => { if pk.is_uncompressed() { - return Err(ScriptContextError::CompressedOnly); + return Err(ScriptContextError::CompressedOnly(pk.to_string())); + } + Ok(()) + } + Terminal::Multi(_k, ref pks) => { + for pk in pks.iter() { + if pk.is_uncompressed() { + return Err(ScriptContextError::CompressedOnly(pk.to_string())); + } } Ok(()) } @@ -388,7 +466,10 @@ 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::MaxWitnessItemssExceeded { + actual: max_witness_items, + limit: MAX_STANDARD_P2WSH_STACK_ITEMS, + }) } _ => Ok(()), } @@ -400,6 +481,126 @@ impl ScriptContext for Segwitv0 { // The witness stack cost is the first element of the tuple ms.ext.max_sat_size.map(|x| x.0) } + + fn pk_len(_pk: &Pk) -> usize { + 34 + } + + fn name_str() -> &'static str { + "Segwitv0" + } +} + +/// Tap ScriptContext +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum Tap {} + +impl ScriptContext for Tap { + type Key = bitcoin::schnorr::PublicKey; + fn check_terminal_non_malleable( + _frag: &Terminal, + ) -> Result<(), ScriptContextError> { + // No fragment is malleable in tapscript context. + // Certain fragments like Multi are invalid, but are not malleable + Ok(()) + } + + 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 { + actual: witness.len(), + limit: MAX_STACK_SIZE, + }); + } + Ok(()) + } + + fn check_global_consensus_validity( + ms: &Miniscript, + ) -> Result<(), ScriptContextError> { + // No script size checks for global consensus rules + // Should we really check for block limits here. + // When the transaction sizes get close to block limits, + // some guarantees are not easy to satisfy because of knapsack + // constraints + if ms.ext.pk_cost > MAX_BLOCK_WEIGHT as usize { + return Err(ScriptContextError::MaxWitnessScriptSizeExceeded); + } + + match ms.node { + Terminal::PkK(ref pk) => { + if pk.is_uncompressed() { + return Err(ScriptContextError::UncompressedKeysNotAllowed); + } + Ok(()) + } + Terminal::Multi(..) => { + return Err(ScriptContextError::TaprootMultiDisabled); + } + _ => Ok(()), + } + } + + fn check_local_consensus_validity( + ms: &Miniscript, + ) -> Result<(), ScriptContextError> { + // Taproot introduces the concept of sigops budget. + // All valid miniscripts satisfy the sigops constraint + // Whenever we add new fragment that uses pk(pk() or multi based on checksigadd) + // miniscript typing rules ensure that pk when executed successfully has it's + // own unique signature. That is, there is no way to re-use signatures from one CHECKSIG + // to another checksig. In other words, for each successfully executed checksig + // will have it's corresponding 64 bytes signature. + // sigops budget = witness_script.len() + witness.size() + 50 + // Each signature will cover it's own cost(64 > 50) and thus will will never exceed the budget + if let (Some(s), Some(h)) = ( + ms.ext.exec_stack_elem_count_sat, + ms.ext.stack_elem_count_sat, + ) { + if s + h > MAX_STACK_SIZE { + return Err(ScriptContextError::StackSizeLimitExceeded { + actual: s + h, + limit: MAX_STACK_SIZE, + }); + } + } + Ok(()) + } + + fn check_global_policy_validity( + _ms: &Miniscript, + ) -> Result<(), ScriptContextError> { + // No script rules, rules are subject to entire tx rules + Ok(()) + } + + fn check_local_policy_validity( + _ms: &Miniscript, + ) -> Result<(), ScriptContextError> { + Ok(()) + } + + fn max_satisfaction_size( + ms: &Miniscript, + ) -> Option { + // The witness stack cost is the first element of the tuple + ms.ext.max_sat_size.map(|x| x.0) + } + + fn is_tap() -> bool { + true + } + + fn pk_len(_pk: &Pk) -> usize { + 33 + } + + fn name_str() -> &'static str { + "TapscriptCtx" + } } /// Bare ScriptContext @@ -410,6 +611,7 @@ impl ScriptContext for Segwitv0 { pub enum BareCtx {} impl ScriptContext for BareCtx { + type Key = bitcoin::PublicKey; fn check_terminal_non_malleable( _frag: &Terminal, ) -> Result<(), ScriptContextError> { @@ -461,6 +663,18 @@ impl ScriptContext for BareCtx { // The witness stack cost is the first element of the tuple ms.ext.max_sat_size.map(|x| x.1) } + + fn pk_len(pk: &Pk) -> usize { + if pk.is_uncompressed() { + 65 + } else { + 33 + } + } + + fn name_str() -> &'static str { + "BareCtx" + } } /// "No Checks" Context @@ -471,6 +685,8 @@ impl ScriptContext for BareCtx { #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum NoChecks {} impl ScriptContext for NoChecks { + // todo: When adding support for interpreter, we need a enum with all supported keys here + type Key = bitcoin::PublicKey; fn check_terminal_non_malleable( _frag: &Terminal, ) -> Result<(), ScriptContextError> { @@ -506,11 +722,20 @@ impl ScriptContext for NoChecks { ) -> Option { panic!("Tried to compute a satisfaction size bound on a no-checks miniscript") } + + fn pk_len(_pk: &Pk) -> usize { + panic!("Tried to compute a pk len bound on a no-checks miniscript") + } + + fn name_str() -> &'static str { + // Internally used code + "Nochecks" + } } /// Private Mod to prevent downstream from implementing this public trait mod private { - use super::{BareCtx, Legacy, NoChecks, Segwitv0}; + use super::{BareCtx, Legacy, NoChecks, Segwitv0, Tap}; pub trait Sealed {} @@ -518,5 +743,6 @@ mod private { impl Sealed for BareCtx {} impl Sealed for Legacy {} impl Sealed for Segwitv0 {} + impl Sealed for Tap {} impl Sealed for NoChecks {} } diff --git a/src/miniscript/decode.rs b/src/miniscript/decode.rs index 2f097fc83..ccc176d23 100644 --- a/src/miniscript/decode.rs +++ b/src/miniscript/decode.rs @@ -19,6 +19,7 @@ use bitcoin::hashes::{hash160, ripemd160, sha256, sha256d, Hash}; use std::marker::PhantomData; +use std::{error, fmt}; use {bitcoin, Miniscript}; use miniscript::lex::{Token as Tk, TokenIter}; @@ -30,10 +31,67 @@ use std::sync::Arc; use Error; use MiniscriptKey; +use ToPublicKey; + fn return_none(_: usize) -> Option { None } +/// Trait for parsing keys from byte slices +pub trait ParseableKey: Sized + ToPublicKey + private::Sealed { + /// Parse a key from slice + fn from_slice(sl: &[u8]) -> Result; +} + +/// Decoding error while parsing keys +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum KeyParseError { + /// Bitcoin PublicKey parse error + FullKeyParseError(bitcoin::util::key::Error), + /// Xonly key parse Error + XonlyKeyParseError(bitcoin::secp256k1::Error), +} + +impl ParseableKey for bitcoin::PublicKey { + fn from_slice(sl: &[u8]) -> Result { + bitcoin::PublicKey::from_slice(sl).map_err(KeyParseError::FullKeyParseError) + } +} + +impl ParseableKey for bitcoin::schnorr::PublicKey { + fn from_slice(sl: &[u8]) -> Result { + bitcoin::schnorr::PublicKey::from_slice(sl).map_err(KeyParseError::XonlyKeyParseError) + } +} + +impl error::Error for KeyParseError { + fn cause(&self) -> Option<&(dyn error::Error + 'static)> { + match self { + KeyParseError::FullKeyParseError(e) => Some(e), + KeyParseError::XonlyKeyParseError(e) => Some(e), + } + } +} + +impl fmt::Display for KeyParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + KeyParseError::FullKeyParseError(_e) => write!(f, "FullKey Parse Error"), + KeyParseError::XonlyKeyParseError(_e) => write!(f, "XonlyKey Parse Error"), + } + } +} + +/// Private Mod to prevent downstream from implementing this public trait +mod private { + + pub trait Sealed {} + + // Implement for those same types, but no others. + impl Sealed for super::bitcoin::PublicKey {} + impl Sealed for super::bitcoin::schnorr::PublicKey {} +} + #[derive(Copy, Clone, Debug)] enum NonTerm { Expression, @@ -213,11 +271,11 @@ impl TerminalStack { } } -/// Parse a script fragment into an `Terminal` +/// Parse a script fragment into an `Miniscript` #[allow(unreachable_patterns)] pub fn parse( tokens: &mut TokenIter, -) -> Result, Error> { +) -> Result, Error> { let mut non_term = Vec::with_capacity(tokens.len()); let mut term = TerminalStack(Vec::with_capacity(tokens.len())); @@ -230,7 +288,32 @@ pub fn parse( match_token!( tokens, // pubkey - Tk::Pubkey(pk) => term.reduce0(Terminal::PkK(pk))?, + Tk::Bytes33(pk) => { + let ret = Ctx::Key::from_slice(pk) + .map_err(|e| Error::PubKeyCtxError(e, Ctx::name_str()))?; + term.reduce0(Terminal::PkK(ret))? + }, + Tk::Bytes65(pk) => { + let ret = Ctx::Key::from_slice(pk) + .map_err(|e| Error::PubKeyCtxError(e, Ctx::name_str()))?; + term.reduce0(Terminal::PkK(ret))? + }, + // Note this does not collide with hash32 because they always followed by equal + // and would be parsed in different branch. If we get a naked Bytes32, it must be + // a x-only key + // In miniscript spec, bytes32 only occurs at three places. + // - during parsing XOnly keys in Pk fragment + // - during parsing XOnly keys in MultiA fragment + // - checking for 32 bytes hashlocks (sha256/hash256) + // The second case(MultiA) is disambiguated using NumEqual which is not used anywhere in miniscript + // The third case can only occur hashlocks is disambiguated because hashlocks start from equal, and + // it is impossible for any K type fragment to be followed by EQUAL in miniscript spec. Thus, EQUAL + // after bytes32 means bytes32 is in a hashlock + // Finally for the first case, K being parsed as a solo expression is a Pk type + Tk::Bytes32(pk) => { + let ret = Ctx::Key::from_slice(pk).map_err(|e| Error::PubKeyCtxError(e, Ctx::name_str()))?; + term.reduce0(Terminal::PkK(ret))? + }, // checksig Tk::CheckSig => { non_term.push(NonTerm::Check); @@ -247,36 +330,36 @@ pub fn parse( tokens, Tk::Dup => { term.reduce0(Terminal::PkH( - hash160::Hash::from_inner(hash) + hash160::Hash::from_slice(hash).expect("valid size") ))? }, Tk::Verify, Tk::Equal, Tk::Num(32), Tk::Size => { non_term.push(NonTerm::Verify); term.reduce0(Terminal::Hash160( - hash160::Hash::from_inner(hash) + hash160::Hash::from_slice(hash).expect("valid size") ))? }, ), Tk::Ripemd160, Tk::Verify, Tk::Equal, Tk::Num(32), Tk::Size => { non_term.push(NonTerm::Verify); term.reduce0(Terminal::Ripemd160( - ripemd160::Hash::from_inner(hash) + ripemd160::Hash::from_slice(hash).expect("valid size") ))? }, ), // Tk::Hash20(hash), - Tk::Hash32(hash) => match_token!( + Tk::Bytes32(hash) => match_token!( tokens, Tk::Sha256, Tk::Verify, Tk::Equal, Tk::Num(32), Tk::Size => { non_term.push(NonTerm::Verify); term.reduce0(Terminal::Sha256( - sha256::Hash::from_inner(hash) + sha256::Hash::from_slice(hash).expect("valid size") ))? }, Tk::Hash256, Tk::Verify, Tk::Equal, Tk::Num(32), Tk::Size => { non_term.push(NonTerm::Verify); term.reduce0(Terminal::Hash256( - sha256d::Hash::from_inner(hash) + sha256d::Hash::from_slice(hash).expect("valid size") ))? }, ), @@ -306,21 +389,21 @@ pub fn parse( // hashlocks Tk::Equal => match_token!( tokens, - Tk::Hash32(hash) => match_token!( + Tk::Bytes32(hash) => match_token!( tokens, Tk::Sha256, Tk::Verify, Tk::Equal, Tk::Num(32), Tk::Size => term.reduce0(Terminal::Sha256( - sha256::Hash::from_inner(hash) + sha256::Hash::from_slice(hash).expect("valid size") ))?, Tk::Hash256, Tk::Verify, Tk::Equal, Tk::Num(32), Tk::Size => term.reduce0(Terminal::Hash256( - sha256d::Hash::from_inner(hash) + sha256d::Hash::from_slice(hash).expect("valid size") ))?, ), Tk::Hash20(hash) => match_token!( @@ -330,14 +413,14 @@ pub fn parse( Tk::Equal, Tk::Num(32), Tk::Size => term.reduce0(Terminal::Ripemd160( - ripemd160::Hash::from_inner(hash) + ripemd160::Hash::from_slice(hash).expect("valid size") ))?, Tk::Hash160, Tk::Verify, Tk::Equal, Tk::Num(32), Tk::Size => term.reduce0(Terminal::Hash160( - hash160::Hash::from_inner(hash) + hash160::Hash::from_slice(hash).expect("valid size") ))?, ), // thresholds @@ -389,7 +472,10 @@ pub fn parse( for _ in 0..n { match_token!( tokens, - Tk::Pubkey(pk) => keys.push(pk), + Tk::Bytes33(pk) => keys.push(::from_slice(pk) + .map_err(|e| Error::PubKeyCtxError(e, Ctx::name_str()))?), + Tk::Bytes65(pk) => keys.push(::from_slice(pk) + .map_err(|e| Error::PubKeyCtxError(e, Ctx::name_str()))?), ); } let k = match_token!( diff --git a/src/miniscript/lex.rs b/src/miniscript/lex.rs index f0f551989..7341125ae 100644 --- a/src/miniscript/lex.rs +++ b/src/miniscript/lex.rs @@ -18,7 +18,6 @@ //! use bitcoin::blockdata::{opcodes, script}; -use bitcoin::PublicKey; use std::fmt; @@ -27,7 +26,7 @@ use super::Error; /// Atom of a tokenized version of a script #[derive(Debug, Copy, Clone, PartialEq, Eq)] #[allow(missing_docs)] -pub enum Token { +pub enum Token<'s> { BoolAnd, BoolOr, Add, @@ -54,28 +53,22 @@ pub enum Token { Sha256, Hash256, Num(u32), - Hash20([u8; 20]), - Hash32([u8; 32]), - Pubkey(PublicKey), + Hash20(&'s [u8]), + Bytes32(&'s [u8]), + Bytes33(&'s [u8]), + Bytes65(&'s [u8]), } -impl fmt::Display for Token { +impl<'s> fmt::Display for Token<'s> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { Token::Num(n) => write!(f, "#{}", n), - Token::Hash20(hash) => { - for ch in &hash[..] { + Token::Hash20(b) | Token::Bytes33(b) | Token::Bytes32(b) | Token::Bytes65(b) => { + for ch in &b[..] { write!(f, "{:02x}", *ch)?; } Ok(()) } - Token::Hash32(hash) => { - for ch in &hash[..] { - write!(f, "{:02x}", *ch)?; - } - Ok(()) - } - Token::Pubkey(pk) => write!(f, "{}", pk), x => write!(f, "{:?}", x), } } @@ -84,22 +77,22 @@ impl fmt::Display for Token { #[derive(Debug, Clone)] /// Iterator that goes through a vector of tokens backward (our parser wants to read /// backward and this is more efficient anyway since we can use `Vec::pop()`). -pub struct TokenIter(Vec); +pub struct TokenIter<'s>(Vec>); -impl TokenIter { +impl<'s> TokenIter<'s> { /// Create a new TokenIter - pub fn new(v: Vec) -> TokenIter { + pub fn new(v: Vec>) -> TokenIter<'s> { TokenIter(v) } /// Look at the top at Iterator - pub fn peek(&self) -> Option<&Token> { + pub fn peek(&self) -> Option<&'s Token> { self.0.last() } /// Push a value to the iterator /// This will be first value consumed by popun_ - pub fn un_next(&mut self, tok: Token) { + pub fn un_next(&mut self, tok: Token<'s>) { self.0.push(tok) } @@ -109,16 +102,16 @@ impl TokenIter { } } -impl Iterator for TokenIter { - type Item = Token; +impl<'s> Iterator for TokenIter<'s> { + type Item = Token<'s>; - fn next(&mut self) -> Option { + fn next(&mut self) -> Option> { self.0.pop() } } /// Tokenize a script -pub fn lex(script: &script::Script) -> Result, Error> { +pub fn lex<'s>(script: &'s script::Script) -> Result>, Error> { let mut ret = Vec::with_capacity(script.len()); for ins in script.instructions_minimal() { @@ -199,7 +192,9 @@ pub fn lex(script: &script::Script) -> Result, Error> { match ret.last() { Some(op @ &Token::Equal) | Some(op @ &Token::CheckSig) - | Some(op @ &Token::CheckMultiSig) => return Err(Error::NonMinimalVerify(*op)), + | Some(op @ &Token::CheckMultiSig) => { + return Err(Error::NonMinimalVerify(String::from(format!("{:?}", op)))) + } _ => {} } ret.push(Token::Verify); @@ -218,21 +213,10 @@ pub fn lex(script: &script::Script) -> Result, Error> { } script::Instruction::PushBytes(bytes) => { match bytes.len() { - 20 => { - let mut x = [0; 20]; - x.copy_from_slice(bytes); - ret.push(Token::Hash20(x)) - } - 32 => { - let mut x = [0; 32]; - x.copy_from_slice(bytes); - ret.push(Token::Hash32(x)) - } - 33 | 65 => { - ret.push(Token::Pubkey( - PublicKey::from_slice(bytes).map_err(Error::BadPubkey)?, - )); - } + 20 => ret.push(Token::Hash20(&bytes)), + 32 => ret.push(Token::Bytes32(&bytes)), + 33 => ret.push(Token::Bytes33(&bytes)), + 65 => ret.push(Token::Bytes65(&bytes)), _ => { match script::read_scriptint(bytes) { Ok(v) if v >= 0 => { diff --git a/src/miniscript/limits.rs b/src/miniscript/limits.rs index 64862a926..246042297 100644 --- a/src/miniscript/limits.rs +++ b/src/miniscript/limits.rs @@ -40,3 +40,9 @@ pub const MAX_SCRIPT_ELEMENT_SIZE: usize = 520; /// Maximum script sig size allowed by standardness rules // https://github.com/bitcoin/bitcoin/blob/42b66a6b814bca130a9ccf0a3f747cf33d628232/src/policy/policy.cpp#L102 pub const MAX_SCRIPTSIG_SIZE: usize = 1650; +/// Maximum items during stack execution +// This limits also applies for initial stack satisfaction +// https://github.com/bitcoin/bitcoin/blob/3af495d6972379b07530a5fcc2665aa626d01621/src/script/script.h#L35 +pub const MAX_STACK_SIZE: usize = 1000; +/** The maximum allowed weight for a block, see BIP 141 (network rule) */ +pub const MAX_BLOCK_WEIGHT: usize = 4000000; diff --git a/src/miniscript/mod.rs b/src/miniscript/mod.rs index e43edeea2..0a0f7ae4a 100644 --- a/src/miniscript/mod.rs +++ b/src/miniscript/mod.rs @@ -27,10 +27,9 @@ use std::marker::PhantomData; use std::{fmt, str}; -use bitcoin; use bitcoin::blockdata::script; -pub use self::context::{BareCtx, Legacy, Segwitv0}; +pub use self::context::{BareCtx, Legacy, Segwitv0, Tap}; pub mod analyzable; pub mod astelem; @@ -139,7 +138,7 @@ impl Miniscript { } } -impl Miniscript { +impl Miniscript { /// Attempt to parse an insane(scripts don't clear sanity checks) /// script into a Miniscript representation. /// Use this to parse scripts with repeated pubkeys, timelock mixing, malleable @@ -147,9 +146,7 @@ impl Miniscript { /// Some of the analysis guarantees of miniscript are lost when dealing with /// insane scripts. In general, in a multi-party setting users should only /// accept sane scripts. - pub fn parse_insane( - script: &script::Script, - ) -> Result, Error> { + pub fn parse_insane(script: &script::Script) -> Result, Error> { let tokens = lex(script)?; let mut iter = TokenIter::new(tokens); @@ -170,7 +167,39 @@ impl Miniscript { /// This function will fail parsing for scripts that do not clear /// the [Miniscript::sanity_check] checks. Use [Miniscript::parse_insane] to /// parse such scripts. - pub fn parse(script: &script::Script) -> Result, Error> { + /// + /// ## Decode/Parse a miniscript from script hex + /// + /// ```rust + /// extern crate bitcoin; + /// extern crate miniscript; + /// + /// use miniscript::Miniscript; + /// use miniscript::{Segwitv0, Tap}; + /// type XonlyKey = bitcoin::schnorr::PublicKey; + /// type Segwitv0Script = Miniscript; + /// type TapScript = Miniscript; + /// use bitcoin::hashes::hex::FromHex; + /// fn main() { + /// // parse x-only miniscript in Taproot context + /// let tapscript_ms = TapScript::parse(&bitcoin::Script::from(Vec::::from_hex( + /// "202788ee41e76f4f3af603da5bc8fa22997bc0344bb0f95666ba6aaff0242baa99ac", + /// ).expect("Even length hex"))) + /// .expect("Xonly keys are valid only in taproot context"); + /// // tapscript fails decoding when we use them with compressed keys + /// let err = TapScript::parse(&bitcoin::Script::from(Vec::::from_hex( + /// "21022788ee41e76f4f3af603da5bc8fa22997bc0344bb0f95666ba6aaff0242baa99ac", + /// ).expect("Even length hex"))) + /// .expect_err("Compressed keys cannot be used in Taproot context"); + /// // Segwitv0 succeeds decoding with full keys. + /// Segwitv0Script::parse(&bitcoin::Script::from(Vec::::from_hex( + /// "21022788ee41e76f4f3af603da5bc8fa22997bc0344bb0f95666ba6aaff0242baa99ac", + /// ).expect("Even length hex"))) + /// .expect("Compressed keys are allowed in Segwit context"); + /// + /// } + /// ``` + pub fn parse(script: &script::Script) -> Result, Error> { let ms = Self::parse_insane(script)?; ms.sanity_check()?; Ok(ms) @@ -419,8 +448,8 @@ serde_string_impl_pk!(Miniscript, "a miniscript", Ctx; ScriptContext); #[cfg(test)] mod tests { - use super::Segwitv0; use super::{Miniscript, ScriptContext}; + use super::{Segwitv0, Tap}; use hex_script; use miniscript::types::{self, ExtData, Property, Type}; use miniscript::Terminal; @@ -435,6 +464,7 @@ mod tests { use std::sync::Arc; type Segwitv0Script = Miniscript; + type Tapscript = Miniscript; fn pubkeys(n: usize) -> Vec { let mut ret = Vec::with_capacity(n); @@ -671,19 +701,19 @@ mod tests { fn verify_parse() { let ms = "and_v(v:hash160(20195b5a3d650c17f0f29f91c33f8f6335193d07),or_d(sha256(96de8fc8c256fa1e1556d41af431cace7dca68707c78dd88c3acab8b17164c47),older(16)))"; let ms: Segwitv0Script = Miniscript::from_str_insane(ms).unwrap(); - assert_eq!(ms, Miniscript::parse_insane(&ms.encode()).unwrap()); + assert_eq!(ms, Segwitv0Script::parse_insane(&ms.encode()).unwrap()); let ms = "and_v(v:sha256(96de8fc8c256fa1e1556d41af431cace7dca68707c78dd88c3acab8b17164c47),or_d(sha256(96de8fc8c256fa1e1556d41af431cace7dca68707c78dd88c3acab8b17164c47),older(16)))"; let ms: Segwitv0Script = Miniscript::from_str_insane(ms).unwrap(); - assert_eq!(ms, Miniscript::parse_insane(&ms.encode()).unwrap()); + assert_eq!(ms, Segwitv0Script::parse_insane(&ms.encode()).unwrap()); let ms = "and_v(v:ripemd160(20195b5a3d650c17f0f29f91c33f8f6335193d07),or_d(sha256(96de8fc8c256fa1e1556d41af431cace7dca68707c78dd88c3acab8b17164c47),older(16)))"; let ms: Segwitv0Script = Miniscript::from_str_insane(ms).unwrap(); - assert_eq!(ms, Miniscript::parse_insane(&ms.encode()).unwrap()); + assert_eq!(ms, Segwitv0Script::parse_insane(&ms.encode()).unwrap()); let ms = "and_v(v:hash256(96de8fc8c256fa1e1556d41af431cace7dca68707c78dd88c3acab8b17164c47),or_d(sha256(96de8fc8c256fa1e1556d41af431cace7dca68707c78dd88c3acab8b17164c47),older(16)))"; let ms: Segwitv0Script = Miniscript::from_str_insane(ms).unwrap(); - assert_eq!(ms, Miniscript::parse_insane(&ms.encode()).unwrap()); + assert_eq!(ms, Segwitv0Script::parse_insane(&ms.encode()).unwrap()); } #[test] @@ -909,4 +939,65 @@ mod tests { .to_string() .contains("unprintable character")); } + + #[test] + fn test_tapscript_rtt() { + // Test x-only invalid under segwitc0 context + let ms = Segwitv0Script::from_str_insane(&format!( + "pk(2788ee41e76f4f3af603da5bc8fa22997bc0344bb0f95666ba6aaff0242baa99)" + )); + assert_eq!( + ms.unwrap_err().to_string(), + "unexpected «Key secp256k1 error: secp: malformed public key»" + ); + Tapscript::from_str_insane(&format!( + "pk(2788ee41e76f4f3af603da5bc8fa22997bc0344bb0f95666ba6aaff0242baa99)" + )) + .unwrap(); + + // Now test that bitcoin::PublicKey works with Taproot context + Miniscript::::from_str_insane(&format!( + "pk(022788ee41e76f4f3af603da5bc8fa22997bc0344bb0f95666ba6aaff0242baa99)" + )) + .unwrap(); + + // uncompressed keys should not be allowed + Miniscript::::from_str_insane(&format!( + "pk(04eed24a081bf1b1e49e3300df4bebe04208ac7e516b6f3ea8eb6e094584267c13483f89dcf194132e12238cc5a34b6b286fc7990d68ed1db86b69ebd826c63b29)" + )) + .unwrap_err(); + + //---------------- test script <-> miniscript --------------- + // Test parsing from scripts: x-only fails decoding in segwitv0 ctx + Segwitv0Script::parse_insane(&hex_script( + "202788ee41e76f4f3af603da5bc8fa22997bc0344bb0f95666ba6aaff0242baa99ac", + )) + .unwrap_err(); + // x-only succeeds in tap ctx + Tapscript::parse_insane(&hex_script( + "202788ee41e76f4f3af603da5bc8fa22997bc0344bb0f95666ba6aaff0242baa99ac", + )) + .unwrap(); + // tapscript fails decoding with compressed + Tapscript::parse_insane(&hex_script( + "21022788ee41e76f4f3af603da5bc8fa22997bc0344bb0f95666ba6aaff0242baa99ac", + )) + .unwrap_err(); + // Segwitv0 succeeds decoding with tapscript. + Segwitv0Script::parse_insane(&hex_script( + "21022788ee41e76f4f3af603da5bc8fa22997bc0344bb0f95666ba6aaff0242baa99ac", + )) + .unwrap(); + + // multi not allowed in tapscript + Tapscript::from_str_insane(&format!( + "multi(1,2788ee41e76f4f3af603da5bc8fa22997bc0344bb0f95666ba6aaff0242baa99)" + )) + .unwrap_err(); + // but allowed in segwit + Segwitv0Script::from_str_insane(&format!( + "multi(1,022788ee41e76f4f3af603da5bc8fa22997bc0344bb0f95666ba6aaff0242baa99)" + )) + .unwrap(); + } } diff --git a/src/miniscript/types/extra_props.rs b/src/miniscript/types/extra_props.rs index 7af9ce4dc..238a4e366 100644 --- a/src/miniscript/types/extra_props.rs +++ b/src/miniscript/types/extra_props.rs @@ -121,11 +121,26 @@ pub struct ExtData { pub max_dissat_size: Option<(usize, usize)>, /// The timelock info about heightlocks and timelocks pub timelock_info: TimeLockInfo, + /// Maximum stack + alt stack size during satisfaction execution + /// This does **not** include initial witness elements. This element only captures + /// the additional elements that are pushed during execution. + pub exec_stack_elem_count_sat: Option, + /// Maximum stack + alt stack size during dissat execution + /// This does **not** include initial witness elements. This element only captures + /// the additional elements that are pushed during execution. + pub exec_stack_elem_count_dissat: Option, } impl Property for ExtData { fn sanity_checks(&self) { - //No sanity checks + debug_assert_eq!( + self.stack_elem_count_sat.is_some(), + self.exec_stack_elem_count_sat.is_some() + ); + debug_assert_eq!( + self.stack_elem_count_dissat.is_some(), + self.exec_stack_elem_count_dissat.is_some() + ); } fn from_true() -> Self { @@ -140,6 +155,8 @@ impl Property for ExtData { max_sat_size: Some((0, 0)), max_dissat_size: None, timelock_info: TimeLockInfo::default(), + exec_stack_elem_count_sat: Some(1), + exec_stack_elem_count_dissat: None, } } @@ -155,6 +172,8 @@ impl Property for ExtData { max_sat_size: None, max_dissat_size: Some((0, 0)), timelock_info: TimeLockInfo::default(), + exec_stack_elem_count_sat: None, + exec_stack_elem_count_dissat: Some(1), } } @@ -170,6 +189,8 @@ impl Property for ExtData { max_sat_size: Some((73, 73)), max_dissat_size: Some((1, 1)), timelock_info: TimeLockInfo::default(), + exec_stack_elem_count_sat: Some(1), // pushes the pk + exec_stack_elem_count_dissat: Some(1), } } @@ -185,6 +206,8 @@ impl Property for ExtData { max_sat_size: Some((34 + 73, 34 + 73)), max_dissat_size: Some((35, 35)), timelock_info: TimeLockInfo::default(), + exec_stack_elem_count_sat: Some(2), // dup and hash push + exec_stack_elem_count_dissat: Some(2), } } @@ -206,6 +229,8 @@ impl Property for ExtData { max_sat_size: Some((1 + 73 * k, 1 + 73 * k)), max_dissat_size: Some((1 + k, 1 + k)), timelock_info: TimeLockInfo::default(), + exec_stack_elem_count_sat: Some(n), // n pks + exec_stack_elem_count_dissat: Some(n), } } @@ -226,6 +251,8 @@ impl Property for ExtData { max_sat_size: Some((33, 33)), max_dissat_size: Some((33, 33)), timelock_info: TimeLockInfo::default(), + exec_stack_elem_count_sat: Some(2), // either size <32> or <32 byte> + exec_stack_elem_count_dissat: Some(2), } } @@ -241,6 +268,8 @@ impl Property for ExtData { max_sat_size: Some((33, 33)), max_dissat_size: Some((33, 33)), timelock_info: TimeLockInfo::default(), + exec_stack_elem_count_sat: Some(2), // either size <32> or <32 byte> + exec_stack_elem_count_dissat: Some(2), } } @@ -256,6 +285,8 @@ impl Property for ExtData { max_sat_size: Some((33, 33)), max_dissat_size: Some((33, 33)), timelock_info: TimeLockInfo::default(), + exec_stack_elem_count_sat: Some(2), // either size <32> or <20 byte> + exec_stack_elem_count_dissat: Some(2), } } @@ -271,6 +302,8 @@ impl Property for ExtData { max_sat_size: Some((33, 33)), max_dissat_size: Some((33, 33)), timelock_info: TimeLockInfo::default(), + exec_stack_elem_count_sat: Some(2), // either size <32> or <20 byte> + exec_stack_elem_count_dissat: Some(2), } } @@ -296,6 +329,8 @@ impl Property for ExtData { cltv_with_time: t >= HEIGHT_TIME_THRESHOLD, contains_combination: false, }, + exec_stack_elem_count_sat: Some(1), // + exec_stack_elem_count_dissat: None, } } @@ -317,6 +352,8 @@ impl Property for ExtData { cltv_with_time: false, contains_combination: false, }, + exec_stack_elem_count_sat: Some(1), // + exec_stack_elem_count_dissat: None, } } @@ -332,6 +369,8 @@ impl Property for ExtData { max_sat_size: self.max_sat_size, max_dissat_size: self.max_dissat_size, timelock_info: self.timelock_info, + exec_stack_elem_count_sat: self.exec_stack_elem_count_sat, + exec_stack_elem_count_dissat: self.exec_stack_elem_count_dissat, }) } @@ -347,6 +386,8 @@ impl Property for ExtData { max_sat_size: self.max_sat_size, max_dissat_size: self.max_dissat_size, timelock_info: self.timelock_info, + exec_stack_elem_count_sat: self.exec_stack_elem_count_sat, + exec_stack_elem_count_dissat: self.exec_stack_elem_count_dissat, }) } @@ -362,6 +403,8 @@ impl Property for ExtData { max_sat_size: self.max_sat_size, max_dissat_size: self.max_dissat_size, timelock_info: self.timelock_info, + exec_stack_elem_count_sat: self.exec_stack_elem_count_sat, + exec_stack_elem_count_dissat: self.exec_stack_elem_count_dissat, }) } @@ -377,6 +420,11 @@ impl Property for ExtData { max_sat_size: self.max_sat_size.map(|(w, s)| (w + 2, s + 1)), max_dissat_size: Some((1, 1)), timelock_info: self.timelock_info, + // Technically max(1, self.exec_stack_elem_count_sat), but all miniscript expressions + // that can be satisfied push at least one thing onto the stack. + // Even all V types push something onto the stack and then remove them + exec_stack_elem_count_sat: self.exec_stack_elem_count_sat, + exec_stack_elem_count_dissat: Some(1), }) } @@ -393,6 +441,8 @@ impl Property for ExtData { max_sat_size: self.max_sat_size, max_dissat_size: None, timelock_info: self.timelock_info, + exec_stack_elem_count_sat: self.exec_stack_elem_count_sat, + exec_stack_elem_count_dissat: None, }) } @@ -408,6 +458,8 @@ impl Property for ExtData { max_sat_size: self.max_sat_size, max_dissat_size: Some((1, 1)), timelock_info: self.timelock_info, + exec_stack_elem_count_sat: self.exec_stack_elem_count_sat, + exec_stack_elem_count_dissat: Some(1), }) } @@ -423,6 +475,9 @@ impl Property for ExtData { max_sat_size: self.max_sat_size, max_dissat_size: self.max_dissat_size, timelock_info: self.timelock_info, + // Technically max(1, self.exec_stack_elem_count_sat), same rationale as cast_dupif + exec_stack_elem_count_sat: self.exec_stack_elem_count_sat, + exec_stack_elem_count_dissat: self.exec_stack_elem_count_dissat, }) } @@ -438,6 +493,9 @@ impl Property for ExtData { max_sat_size: self.max_sat_size, max_dissat_size: None, timelock_info: self.timelock_info, + // Technically max(1, self.exec_stack_elem_count_sat), same rationale as cast_dupif + exec_stack_elem_count_sat: self.exec_stack_elem_count_sat, + exec_stack_elem_count_dissat: None, }) } @@ -457,6 +515,10 @@ impl Property for ExtData { stack_elem_count_dissat: self.stack_elem_count_dissat.map(|x| x + 1), max_sat_size: self.max_sat_size.map(|(w, s)| (w + 2, s + 1)), max_dissat_size: self.max_dissat_size.map(|(w, s)| (w + 1, s + 1)), + // TODO: fix dissat stack elem counting above in a later commit + // Technically max(1, self.exec_stack_elem_count_sat), same rationale as cast_dupif + exec_stack_elem_count_sat: self.exec_stack_elem_count_sat, + exec_stack_elem_count_dissat: self.exec_stack_elem_count_dissat, timelock_info: self.timelock_info, }) } @@ -473,6 +535,10 @@ impl Property for ExtData { max_sat_size: self.max_sat_size.map(|(w, s)| (w + 1, s + 1)), max_dissat_size: self.max_dissat_size.map(|(w, s)| (w + 2, s + 1)), timelock_info: self.timelock_info, + // TODO: fix dissat stack elem counting above in a later commit + // Technically max(1, self.exec_stack_elem_count_sat), same rationale as cast_dupif + exec_stack_elem_count_sat: self.exec_stack_elem_count_sat, + exec_stack_elem_count_dissat: self.exec_stack_elem_count_dissat, }) } @@ -500,6 +566,16 @@ impl Property for ExtData { .max_dissat_size .and_then(|(lw, ls)| r.max_dissat_size.map(|(rw, rs)| (lw + rw, ls + rs))), timelock_info: TimeLockInfo::comb_and_timelocks(l.timelock_info, r.timelock_info), + // Left element leaves a stack result on the stack top and then right element is evaluated + // Therefore + 1 is added to execution size of second element + exec_stack_elem_count_sat: opt_max( + l.exec_stack_elem_count_sat, + r.exec_stack_elem_count_sat.map(|x| x + 1), + ), + exec_stack_elem_count_dissat: opt_max( + l.exec_stack_elem_count_dissat, + r.exec_stack_elem_count_dissat.map(|x| x + 1), + ), }) } @@ -519,6 +595,12 @@ impl Property for ExtData { .and_then(|(lw, ls)| r.max_sat_size.map(|(rw, rs)| (lw + rw, ls + rs))), max_dissat_size: None, timelock_info: TimeLockInfo::comb_and_timelocks(l.timelock_info, r.timelock_info), + // [X] leaves no element after evaluation, hence this is the max + exec_stack_elem_count_sat: opt_max( + l.exec_stack_elem_count_sat, + r.exec_stack_elem_count_sat, + ), + exec_stack_elem_count_dissat: None, }) } @@ -555,6 +637,20 @@ impl Property for ExtData { .max_dissat_size .and_then(|(lw, ls)| r.max_dissat_size.map(|(rw, rs)| (lw + rw, ls + rs))), timelock_info: TimeLockInfo::comb_or_timelocks(l.timelock_info, r.timelock_info), + exec_stack_elem_count_sat: cmp::max( + opt_max( + l.exec_stack_elem_count_sat, + r.exec_stack_elem_count_dissat.map(|x| x + 1), + ), + opt_max( + l.exec_stack_elem_count_dissat, + r.exec_stack_elem_count_sat.map(|x| x + 1), + ), + ), + exec_stack_elem_count_dissat: opt_max( + l.exec_stack_elem_count_dissat, + r.exec_stack_elem_count_dissat.map(|x| x + 1), + ), }) } @@ -588,6 +684,14 @@ impl Property for ExtData { .max_dissat_size .and_then(|(lw, ls)| r.max_dissat_size.map(|(rw, rs)| (lw + rw, ls + rs))), timelock_info: TimeLockInfo::comb_or_timelocks(l.timelock_info, r.timelock_info), + exec_stack_elem_count_sat: cmp::max( + opt_max(l.exec_stack_elem_count_sat, r.exec_stack_elem_count_dissat), + r.exec_stack_elem_count_sat, + ), + exec_stack_elem_count_dissat: opt_max( + l.exec_stack_elem_count_dissat, + r.exec_stack_elem_count_dissat.map(|x| x + 1), + ), }) } @@ -615,6 +719,11 @@ impl Property for ExtData { ), max_dissat_size: None, timelock_info: TimeLockInfo::comb_or_timelocks(l.timelock_info, r.timelock_info), + exec_stack_elem_count_sat: cmp::max( + opt_max(l.exec_stack_elem_count_sat, r.exec_stack_elem_count_dissat), + r.exec_stack_elem_count_sat, + ), + exec_stack_elem_count_dissat: None, }) } @@ -658,6 +767,15 @@ impl Property for ExtData { (None, None) => None, }, timelock_info: TimeLockInfo::comb_or_timelocks(l.timelock_info, r.timelock_info), + // TODO: fix elem count dissat bug + exec_stack_elem_count_sat: cmp::max( + l.exec_stack_elem_count_sat, + r.exec_stack_elem_count_sat, + ), + exec_stack_elem_count_dissat: cmp::max( + l.exec_stack_elem_count_dissat, + r.exec_stack_elem_count_dissat, + ), }) } @@ -697,6 +815,14 @@ impl Property for ExtData { TimeLockInfo::comb_and_timelocks(a.timelock_info, b.timelock_info), c.timelock_info, ), + exec_stack_elem_count_sat: cmp::max( + opt_max(a.exec_stack_elem_count_sat, b.exec_stack_elem_count_sat), + opt_max(c.exec_stack_elem_count_sat, a.exec_stack_elem_count_dissat), + ), + exec_stack_elem_count_dissat: opt_max( + a.exec_stack_elem_count_dissat, + c.exec_stack_elem_count_dissat, + ), }) } @@ -718,6 +844,10 @@ impl Property for ExtData { let mut max_sat_size_vec = Vec::with_capacity(n); let mut max_sat_size = Some((0, 0)); let mut max_dissat_size = Some((0, 0)); + // the max element count is same as max sat element count when satisfying one element + 1 + let mut exec_stack_elem_count_sat_vec = Vec::with_capacity(n); + let mut exec_stack_elem_count_sat = Some(0); + let mut exec_stack_elem_count_dissat = Some(0); for i in 0..n { let sub = sub_ck(i)?; @@ -753,6 +883,14 @@ impl Property for ExtData { } _ => {} } + exec_stack_elem_count_sat_vec.push(( + sub.exec_stack_elem_count_sat, + sub.exec_stack_elem_count_dissat, + )); + exec_stack_elem_count_dissat = opt_max( + exec_stack_elem_count_dissat, + sub.exec_stack_elem_count_dissat, + ); } // We sort by [satisfaction cost - dissatisfaction cost] to make a worst-case (the most @@ -770,6 +908,19 @@ impl Property for ExtData { }; } + // Same logic as above + exec_stack_elem_count_sat_vec.sort_by(|a, b| { + a.0.map(|x| a.1.map(|y| x as isize - y as isize)) + .cmp(&b.0.map(|x| b.1.map(|y| x as isize - y as isize))) + }); + for (i, &(x, y)) in exec_stack_elem_count_sat_vec.iter().rev().enumerate() { + exec_stack_elem_count_sat = if i <= k { + opt_max(exec_stack_elem_count_sat, x) + } else { + opt_max(exec_stack_elem_count_sat, y) + }; + } + // Same for the size cost. A bit more intricated as we need to account for both the witness // and scriptSig cost, so we end up with a tuple of Options of tuples. We use the witness // cost (first element of the mentioned tuple) here. @@ -811,6 +962,8 @@ impl Property for ExtData { max_sat_size, max_dissat_size, timelock_info: TimeLockInfo::combine_thresh_timelocks(k, timelocks), + exec_stack_elem_count_sat, + exec_stack_elem_count_dissat, }) } @@ -948,3 +1101,12 @@ impl Property for ExtData { ret } } + +// Returns Some(max(x,y)) is both x and y are Some. Otherwise, return none +fn opt_max(a: Option, b: Option) -> Option { + if let (Some(x), Some(y)) = (a, b) { + Some(cmp::max(x, y)) + } else { + None + } +} diff --git a/src/miniscript/types/mod.rs b/src/miniscript/types/mod.rs index ddf352b26..d3be67ba9 100644 --- a/src/miniscript/types/mod.rs +++ b/src/miniscript/types/mod.rs @@ -105,7 +105,7 @@ pub struct Error { } impl error::Error for Error { - fn cause(&self) -> Option<&error::Error> { + fn cause(&self) -> Option<&dyn error::Error> { None } diff --git a/src/policy/mod.rs b/src/policy/mod.rs index a5d88d1b4..b03d15666 100644 --- a/src/policy/mod.rs +++ b/src/policy/mod.rs @@ -73,7 +73,7 @@ impl error::Error for LiftError { fn description(&self) -> &str { "" } - fn cause(&self) -> Option<&error::Error> { + fn cause(&self) -> Option<&dyn error::Error> { None } } diff --git a/src/psbt/mod.rs b/src/psbt/mod.rs index 0ca670fad..bc0d72110 100644 --- a/src/psbt/mod.rs +++ b/src/psbt/mod.rs @@ -261,7 +261,7 @@ impl<'psbt, Pk: MiniscriptKey + ToPublicKey> Satisfier for PsbtInputSatisfie if seq == 0xffffffff { false } else { - >::check_after(&After(locktime), n) + >::check_after(&After(locktime), n) } } @@ -277,7 +277,7 @@ impl<'psbt, Pk: MiniscriptKey + ToPublicKey> Satisfier for PsbtInputSatisfie // transaction version and sequence check false } else { - >::check_older(&Older(seq), n) + >::check_older(&Older(seq), n) } } diff --git a/src/util.rs b/src/util.rs index 05b57947b..bec3f52f8 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,6 +1,8 @@ use bitcoin; use bitcoin::blockdata::script; use bitcoin::Script; + +use {ScriptContext, ToPublicKey}; pub(crate) fn varint_len(n: usize) -> usize { bitcoin::VarInt(n as u64).len() } @@ -21,3 +23,26 @@ pub(crate) fn witness_to_scriptsig(witness: &[Vec]) -> Script { } b.into_script() } + +// trait for pushing key that depend on context +pub(crate) trait MsKeyBuilder { + /// Serialize the key as bytes based on script context. Used when encoding miniscript into bitcoin script + fn push_ms_key(self, key: &Pk) -> Self + where + Pk: ToPublicKey, + Ctx: ScriptContext; +} + +impl MsKeyBuilder for script::Builder { + fn push_ms_key(self, key: &Pk) -> Self + where + Pk: ToPublicKey, + Ctx: ScriptContext, + { + if Ctx::is_tap() { + self.push_slice(&key.to_x_only_pubkey().serialize()) + } else { + self.push_key(&key.to_public_key()) + } + } +}