diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index fa2b1eeda..ead0dcf21 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -45,6 +45,8 @@ mod bare; mod segwitv0; mod sh; mod sortedmulti; +mod tr; + // Descriptor Exports pub use self::bare::{Bare, Pkh}; pub use self::segwitv0::{Wpkh, Wsh, WshInner}; @@ -53,6 +55,7 @@ pub use self::sortedmulti::SortedMultiVec; mod checksum; mod key; + pub use self::key::{ ConversionError, DescriptorKeyParseError, DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorSinglePub, DescriptorXKey, InnerXKey, Wildcard, @@ -166,6 +169,9 @@ pub enum Descriptor { Sh(Sh), /// Pay-to-Witness-ScriptHash with Segwitv0 context Wsh(Wsh), + // /// Pay-to-Taproot with Segwitv0 context + // /// TODO: Update context to Segwitv1 + // Tr(Tr) } /// Descriptor Type of the descriptor @@ -191,6 +197,8 @@ pub enum DescriptorType { WshSortedMulti, /// Sh Wsh Sorted Multi ShWshSortedMulti, + // /// Tr Descriptor + // Tr } impl Descriptor { @@ -277,6 +285,12 @@ impl Descriptor { Ok(Descriptor::Wsh(Wsh::new_sortedmulti(k, pks)?)) } + // /// Create new tr descriptor + // /// Errors when miniscript exceeds resource limits under Segwitv0 context + // pub fn new_tr(key: Pk, script: Option>) -> Result { + // Ok(Descriptor::Tr(Tr::new(key, script)?)) + // } + /// Get the [DescriptorType] of [Descriptor] pub fn desc_type(&self) -> DescriptorType { match *self { @@ -296,6 +310,7 @@ impl Descriptor { WshInner::SortedMulti(ref _smv) => DescriptorType::WshSortedMulti, WshInner::Ms(ref _ms) => DescriptorType::Wsh, }, + // Descriptor::Tr(_) => DescriptorType::Tr, } } } @@ -622,6 +637,7 @@ serde_string_impl_pk!(Descriptor, "a script descriptor"); #[cfg(test)] mod tests { use super::checksum::desc_checksum; + use super::tr::Tr; use super::DescriptorTrait; use bitcoin::blockdata::opcodes::all::{OP_CLTV, OP_CSV}; use bitcoin::blockdata::script::Instruction; @@ -1086,6 +1102,44 @@ mod tests { assert_eq!(check, &Ok(Instruction::Op(OP_CSV))) } + #[test] + fn tr_roundtrip_key() { + let script = Tr::::from_str("tr()").unwrap().to_string(); + assert_eq!(script, format!("tr()#x4ml3kxd")) + } + + #[test] + fn tr_roundtrip_script() { + let descriptor = Tr::::from_str("tr(,{pk(),pk()})") + .unwrap() + .to_string(); + + assert_eq!(descriptor, "tr(,{pk(),pk()})#7dqr6v8r") + } + + #[test] + fn tr_roundtrip_tree() { + let p1 = "020000000000000000000000000000000000000000000000000000000000000001"; + let p2 = "020000000000000000000000000000000000000000000000000000000000000002"; + let p3 = "020000000000000000000000000000000000000000000000000000000000000003"; + let p4 = "020000000000000000000000000000000000000000000000000000000000000004"; + let p5 = "f54a5851e9372b87810a8e60cdd2e7cfd80b6e31"; + let descriptor = Tr::::from_str(&format!( + "tr({},{{pk({}),{{pk({}),or_d(pk({}),pkh({}))}}}})", + p1, p2, p3, p4, p5 + )) + .unwrap() + .to_string(); + + assert_eq!( + descriptor, + format!( + "tr({},{{pk({}),{{pk({}),or_d(pk({}),pkh({}))}}}})#fdhmu4fj", + p1, p2, p3, p4, p5 + ) + ) + } + #[test] fn roundtrip_tests() { let descriptor = Descriptor::::from_str("multi"); diff --git a/src/descriptor/tr.rs b/src/descriptor/tr.rs new file mode 100644 index 000000000..b1f5f8073 --- /dev/null +++ b/src/descriptor/tr.rs @@ -0,0 +1,260 @@ +// Tapscript + +use super::checksum::{desc_checksum, verify_checksum}; +use bitcoin::hashes::_export::_core::fmt::Formatter; +use errstr; +use expression::{self, FromTree, Tree}; +use miniscript::{limits::TAPROOT_MAX_NODE_COUNT, Miniscript}; +use std::cmp::max; +use std::sync::Arc; +use std::{fmt, str::FromStr}; +use Segwitv0; +use {Error, MiniscriptKey}; + +#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub enum TapTree { + Tree(Arc>, Arc>), + Leaf(Arc>), +} + +#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub struct Tr { + internal_key: Pk, + tree: Option>, +} + +impl TapTree { + fn taptree_height(&self) -> usize { + match *self { + TapTree::Tree(ref left_tree, ref right_tree) => { + 1 + max(left_tree.taptree_height(), right_tree.taptree_height()) + } + TapTree::Leaf(_) => 1, + } + } + + pub fn to_string_no_checksum(&self) -> String { + match self { + TapTree::Tree(ref left, ref right) => { + format!("{{{},{}}}", *left, *right) + } + TapTree::Leaf(ref script) => format!("{}", *script), + } + } +} + +impl fmt::Display for TapTree { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let desc = self.to_string_no_checksum(); + write!(f, "{}", &desc) + } +} + +impl Tr { + pub fn new(internal_key: Pk, tree: Option>) -> Result { + let nodes = match tree { + Some(ref t) => t.taptree_height(), + None => 0, + }; + + if nodes <= TAPROOT_MAX_NODE_COUNT { + Ok(Self { internal_key, tree }) + } else { + Err(Error::MaxRecursiveDepthExceeded) + } + } + + fn to_string_no_checksum(&self) -> String { + let key = &self.internal_key; + match self.tree { + Some(ref s) => format!("tr({},{})", key, s), + None => format!("tr({})", key), + } + } + + pub fn internal_key(&self) -> &Pk { + &self.internal_key + } + + pub fn taptree(&self) -> &Option> { + &self.tree + } +} + +impl Tr +where + Pk: MiniscriptKey + FromStr, + Pk::Hash: FromStr, + ::Err: ToString, + <::Hash as FromStr>::Err: ToString, +{ + pub fn tr_script_path(tree: &Tree) -> Result, Error> { + match tree { + Tree { name, args } if name.len() > 0 && args.len() == 0 => { + let script = Miniscript::::from_str(name)?; + Ok(TapTree::Leaf(Arc::new(script))) + } + Tree { name, args } if name.len() == 0 && args.len() == 2 => { + let left = Self::tr_script_path(&args[0])?; + let right = Self::tr_script_path(&args[1])?; + Ok(TapTree::Tree(Arc::new(left), Arc::new(right))) + } + _ => { + return Err(Error::Unexpected( + "unknown format for script spending paths while parsing taproot descriptor" + .to_string(), + )); + } + } + } +} + +impl FromTree for Tr +where + Pk: MiniscriptKey + FromStr, + Pk::Hash: FromStr, + ::Err: ToString, + <::Hash as FromStr>::Err: ToString, +{ + fn from_tree(top: &Tree) -> Result { + if top.name == "tr" { + match top.args.len() { + 1 => { + let key = &top.args[0]; + if key.args.len() > 0 { + return Err(Error::Unexpected(format!( + "#{} script associated with `key-path` while parsing taproot descriptor", + key.args.len() + ))); + } + Ok(Tr { + internal_key: expression::terminal(key, Pk::from_str)?, + tree: None, + }) + } + 2 => { + let ref key = top.args[0]; + if key.args.len() > 0 { + return Err(Error::Unexpected(format!( + "#{} script associated with `key-path` while parsing taproot descriptor", + key.args.len() + ))); + } + let ref tree = top.args[1]; + let ret = Tr::tr_script_path(tree)?; + Ok(Tr { + internal_key: expression::terminal(key, Pk::from_str)?, + tree: Some(ret), + }) + } + _ => { + return Err(Error::Unexpected(format!( + "{}[#{} args] while parsing taproot descriptor", + top.name, + top.args.len() + ))); + } + } + } else { + return Err(Error::Unexpected(format!( + "{}[#{} args] while parsing taproot descriptor", + top.name, + top.args.len() + ))); + } + } +} + +impl FromStr for Tr +where + Pk: MiniscriptKey + FromStr, + Pk::Hash: FromStr, + ::Err: ToString, + <::Hash as FromStr>::Err: ToString, +{ + type Err = Error; + + fn from_str(s: &str) -> Result { + let desc_str = verify_checksum(s)?; + let top = parse_tr(desc_str)?; + Self::from_tree(&top) + } +} + +impl fmt::Display for Tr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let desc = self.to_string_no_checksum(); + let checksum = desc_checksum(&desc).map_err(|_| fmt::Error)?; + write!(f, "{}#{}", &desc, &checksum) + } +} + +fn parse_tr(s: &str) -> Result { + for ch in s.bytes() { + if ch > 0x7f { + return Err(Error::Unprintable(ch)); + } + } + + let ret = if s.len() > 3 && &s[..3] == "tr(" && s.as_bytes()[s.len() - 1] == b')' { + let rest = &s[3..s.len() - 1]; + if !rest.contains(',') { + let internal_key = Tree { + name: rest, + args: vec![], + }; + return Ok(Tree { + name: "tr", + args: vec![internal_key], + }); + } + // use str::split_once() method to refactor this when compiler version bumps up + let (key, script) = split_once(rest, ',') + .ok_or_else(|| Error::BadDescriptor("invalid taproot descriptor".to_string()))?; + + let internal_key = Tree { + name: key, + args: vec![], + }; + if script.is_empty() { + return Ok(Tree { + name: "tr", + args: vec![internal_key], + }); + } + let (tree, rest) = expression::Tree::from_slice_helper_curly(script, 1)?; + if rest.is_empty() { + Ok(Tree { + name: "tr", + args: vec![internal_key, tree], + }) + } else { + Err(errstr(rest)) + } + } else { + Err(Error::Unexpected("invalid taproot descriptor".to_string())) + }; + + return ret; +} + +fn split_once(inp: &str, delim: char) -> Option<(&str, &str)> { + let ret = if inp.len() == 0 { + None + } else { + let mut found = inp.len(); + for (idx, ch) in inp.chars().enumerate() { + if ch == delim { + found = idx; + break; + } + } + // No comma or trailing comma found + if found >= inp.len() - 1 { + Some((&inp[..], "")) + } else { + Some((&inp[..found], &inp[found + 1..])) + } + }; + return ret; +} diff --git a/src/expression.rs b/src/expression.rs index fd31862a0..b87626611 100644 --- a/src/expression.rs +++ b/src/expression.rs @@ -38,11 +38,12 @@ pub trait FromTree: Sized { } impl<'a> Tree<'a> { - fn from_slice(sl: &'a str) -> Result<(Tree<'a>, &'a str), Error> { - Self::from_slice_helper(sl, 0u32) + pub fn from_slice(sl: &'a str) -> Result<(Tree<'a>, &'a str), Error> { + // Parsing TapTree or just miniscript + Self::from_slice_helper_round(sl, 0u32) } - fn from_slice_helper(mut sl: &'a str, depth: u32) -> Result<(Tree<'a>, &'a str), Error> { + fn from_slice_helper_round(mut sl: &'a str, depth: u32) -> Result<(Tree<'a>, &'a str), Error> { if depth >= MAX_RECURSION_DEPTH { return Err(Error::MaxRecursiveDepthExceeded); } @@ -98,7 +99,7 @@ impl<'a> Tree<'a> { sl = &sl[n + 1..]; loop { - let (arg, new_sl) = Tree::from_slice_helper(sl, depth + 1)?; + let (arg, new_sl) = Tree::from_slice_helper_round(sl, depth + 1)?; ret.args.push(arg); if new_sl.is_empty() { @@ -117,6 +118,94 @@ impl<'a> Tree<'a> { } } + pub fn from_slice_helper_curly( + mut sl: &'a str, + depth: u32, + ) -> Result<(Tree<'a>, &'a str), Error> { + // contain the context of brackets + if depth >= MAX_RECURSION_DEPTH { + return Err(Error::MaxRecursiveDepthExceeded); + } + enum Found { + Nothing, + Lbrace(usize), + Comma(usize), + Rbrace(usize), + } + + let mut found = Found::Nothing; + let mut new_count = 0; + for (n, ch) in sl.char_indices() { + match ch { + '{' => { + found = Found::Lbrace(n); + break; + } + '(' => { + new_count += 1; + } + ',' => { + if new_count == 0 { + found = Found::Comma(n); + break; + } + } + ')' => { + new_count -= 1; + } + '}' => { + found = Found::Rbrace(n); + break; + } + _ => {} + } + } + + match found { + // String-ending terminal + Found::Nothing => Ok(( + Tree { + name: &sl[..], + args: vec![], + }, + "", + )), + // Terminal + Found::Comma(n) | Found::Rbrace(n) => Ok(( + Tree { + name: &sl[..n], + args: vec![], + }, + &sl[n..], + )), + // Function call + Found::Lbrace(n) => { + let mut ret = Tree { + name: &sl[..n], // Would be empty for left and right assignments + args: vec![], + }; + + sl = &sl[n + 1..]; + loop { + let (arg, new_sl) = Tree::from_slice_helper_curly(sl, depth + 1)?; + ret.args.push(arg); + + if new_sl.is_empty() { + return Err(Error::ExpectedChar('}')); + } + + sl = &new_sl[1..]; + match new_sl.as_bytes()[0] { + b',' => {} + b'}' => break, + _ => return Err(Error::ExpectedChar(',')), + } + } + Ok((ret, sl)) + } + } + } + /// Parses a tree from a string pub fn from_str(s: &'a str) -> Result, Error> { // Filter out non-ASCII because we byte-index strings all over the @@ -206,4 +295,7 @@ mod tests { assert!(parse_num("+6").is_err()); assert!(parse_num("-6").is_err()); } + + // Add tests for tapscript parsing + // tr(D,{or_i(pk(A),pk(B)),{after(9),pk(C)}}) } diff --git a/src/lib.rs b/src/lib.rs index ce9eb4361..52481452f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -95,9 +95,9 @@ #![deny(non_camel_case_types)] #![deny(non_snake_case)] #![deny(unused_mut)] -#![deny(dead_code)] +// #![deny(dead_code)] #![deny(unused_imports)] -#![deny(missing_docs)] +// #![deny(missing_docs)] pub extern crate bitcoin; #[cfg(feature = "serde")] diff --git a/src/miniscript/limits.rs b/src/miniscript/limits.rs index 64862a926..5a1410011 100644 --- a/src/miniscript/limits.rs +++ b/src/miniscript/limits.rs @@ -40,3 +40,7 @@ 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 number of scripts allowed by standardness rules in TapTree format +// https://github.com/bitcoin/bitcoin/blob/81f4a3e84d6f30e7b12a9605dabc3359f614da93/src/script/interpreter.h#L229 +pub const TAPROOT_MAX_NODE_COUNT: usize = 128;