Skip to content

Commit 179dc87

Browse files
committed
expression: move some ad-hoc validation from descriptor module
In the next commit we will change the expression parser to parse both {}s and ()s as parentheses, no longer distinguishing between a "taproot" and "non-taproot" mode. This means that all non-Taproot descriptors need to check that curly-brace {} expressions do not appear. While we are adding this check, we can replace the existing checks for things like "does this start with wsh and have exactly one child" with an encapsulated function with strongly-typed errors. This gets rid of a couple of Error::Unexpected instances. We change one error output (if you pass no children to a sortedmulti). The old text is nonsensical and the new text is explicit about what is wrong. This change is pretty-much mechanical, though unfortunately these are all "manual" calls to validation functions, and if I missed any, the compiler won't give us any help in noticing. But there aren't too many. Anyway, later on I will write a fuzz test which checks that we have not changed the set of parseable descriptors (using normal keys, not Strings or anything that might have braces in them, which we know we broke) and that should catch any mistakes. Also, similar to the last commit, this one doesn't really "do" anything because it's still impossible to parse trees with mixed brace styles. But in the next one, it will be possible, and we will be glad to have moved a bunch of the diff into these prepatory commits.
1 parent f5dcafd commit 179dc87

File tree

8 files changed

+175
-54
lines changed

8 files changed

+175
-54
lines changed

src/descriptor/bare.rs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -370,15 +370,10 @@ impl<Pk: MiniscriptKey> Liftable<Pk> for Pkh<Pk> {
370370

371371
impl<Pk: FromStrKey> FromTree for Pkh<Pk> {
372372
fn from_tree(top: &expression::Tree) -> Result<Self, Error> {
373-
if top.name == "pkh" && top.args.len() == 1 {
374-
Ok(Pkh::new(expression::terminal(&top.args[0], |pk| Pk::from_str(pk))?)?)
375-
} else {
376-
Err(Error::Unexpected(format!(
377-
"{}({} args) while parsing pkh descriptor",
378-
top.name,
379-
top.args.len(),
380-
)))
381-
}
373+
let top = top
374+
.verify_toplevel("pkh", 1..=1)
375+
.map_err(Error::ParseTree)?;
376+
Ok(Pkh::new(expression::terminal(top, |pk| Pk::from_str(pk))?)?)
382377
}
383378
}
384379

src/descriptor/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1102,7 +1102,7 @@ mod tests {
11021102
StdDescriptor::from_str("sh(sortedmulti)")
11031103
.unwrap_err()
11041104
.to_string(),
1105-
"expected threshold, found terminal",
1105+
"sortedmulti must have at least 1 children, but found 0"
11061106
); //issue 202
11071107
assert_eq!(
11081108
StdDescriptor::from_str(&format!("sh(sortedmulti(2,{}))", &TEST_PK[3..69]))

src/descriptor/segwitv0.rs

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -248,21 +248,16 @@ impl<Pk: MiniscriptKey> Liftable<Pk> for Wsh<Pk> {
248248

249249
impl<Pk: FromStrKey> crate::expression::FromTree for Wsh<Pk> {
250250
fn from_tree(top: &expression::Tree) -> Result<Self, Error> {
251-
if top.name == "wsh" && top.args.len() == 1 {
252-
let top = &top.args[0];
253-
if top.name == "sortedmulti" {
254-
return Ok(Wsh { inner: WshInner::SortedMulti(SortedMultiVec::from_tree(top)?) });
255-
}
256-
let sub = Miniscript::from_tree(top)?;
257-
Segwitv0::top_level_checks(&sub)?;
258-
Ok(Wsh { inner: WshInner::Ms(sub) })
259-
} else {
260-
Err(Error::Unexpected(format!(
261-
"{}({} args) while parsing wsh descriptor",
262-
top.name,
263-
top.args.len(),
264-
)))
251+
let top = top
252+
.verify_toplevel("wsh", 1..=1)
253+
.map_err(Error::ParseTree)?;
254+
255+
if top.name == "sortedmulti" {
256+
return Ok(Wsh { inner: WshInner::SortedMulti(SortedMultiVec::from_tree(top)?) });
265257
}
258+
let sub = Miniscript::from_tree(top)?;
259+
Segwitv0::top_level_checks(&sub)?;
260+
Ok(Wsh { inner: WshInner::Ms(sub) })
266261
}
267262
}
268263

@@ -488,15 +483,10 @@ impl<Pk: MiniscriptKey> Liftable<Pk> for Wpkh<Pk> {
488483

489484
impl<Pk: FromStrKey> crate::expression::FromTree for Wpkh<Pk> {
490485
fn from_tree(top: &expression::Tree) -> Result<Self, Error> {
491-
if top.name == "wpkh" && top.args.len() == 1 {
492-
Ok(Wpkh::new(expression::terminal(&top.args[0], |pk| Pk::from_str(pk))?)?)
493-
} else {
494-
Err(Error::Unexpected(format!(
495-
"{}({} args) while parsing wpkh descriptor",
496-
top.name,
497-
top.args.len(),
498-
)))
499-
}
486+
let top = top
487+
.verify_toplevel("wpkh", 1..=1)
488+
.map_err(Error::ParseTree)?;
489+
Ok(Wpkh::new(expression::terminal(top, |pk| Pk::from_str(pk))?)?)
500490
}
501491
}
502492

src/descriptor/sh.rs

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -82,26 +82,19 @@ impl<Pk: MiniscriptKey> fmt::Display for Sh<Pk> {
8282

8383
impl<Pk: FromStrKey> crate::expression::FromTree for Sh<Pk> {
8484
fn from_tree(top: &expression::Tree) -> Result<Self, Error> {
85-
if top.name == "sh" && top.args.len() == 1 {
86-
let top = &top.args[0];
87-
let inner = match top.name {
88-
"wsh" => ShInner::Wsh(Wsh::from_tree(top)?),
89-
"wpkh" => ShInner::Wpkh(Wpkh::from_tree(top)?),
90-
"sortedmulti" => ShInner::SortedMulti(SortedMultiVec::from_tree(top)?),
91-
_ => {
92-
let sub = Miniscript::from_tree(top)?;
93-
Legacy::top_level_checks(&sub)?;
94-
ShInner::Ms(sub)
95-
}
96-
};
97-
Ok(Sh { inner })
98-
} else {
99-
Err(Error::Unexpected(format!(
100-
"{}({} args) while parsing sh descriptor",
101-
top.name,
102-
top.args.len(),
103-
)))
104-
}
85+
let top = top.verify_toplevel("sh", 1..=1).map_err(Error::ParseTree)?;
86+
87+
let inner = match top.name {
88+
"wsh" => ShInner::Wsh(Wsh::from_tree(top)?),
89+
"wpkh" => ShInner::Wpkh(Wpkh::from_tree(top)?),
90+
"sortedmulti" => ShInner::SortedMulti(SortedMultiVec::from_tree(top)?),
91+
_ => {
92+
let sub = Miniscript::from_tree(top)?;
93+
Legacy::top_level_checks(&sub)?;
94+
ShInner::Ms(sub)
95+
}
96+
};
97+
Ok(Sh { inner })
10598
}
10699
}
107100

src/descriptor/sortedmulti.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> SortedMultiVec<Pk, Ctx> {
6464
Pk: FromStr,
6565
<Pk as FromStr>::Err: fmt::Display,
6666
{
67+
tree.verify_toplevel("sortedmulti", 1..)
68+
.map_err(Error::ParseTree)?;
69+
6770
let ret = Self {
6871
inner: tree
6972
.to_null_threshold()

src/expression/error.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,29 @@ pub enum ParseTreeError {
5353
/// The position of the closing parethesis.
5454
close_pos: usize,
5555
},
56+
/// A node had the wrong name.
57+
IncorrectName {
58+
/// The name that was found.
59+
actual: String,
60+
/// The name that was expected.
61+
expected: &'static str,
62+
},
63+
/// A node had the wrong number of children.
64+
IncorrectNumberOfChildren {
65+
/// A description of the node in question.
66+
description: &'static str,
67+
/// The number of children the node had.
68+
n_children: usize,
69+
/// The minimum of children the node should have had.
70+
minimum: Option<usize>,
71+
/// The minimum of children the node should have had.
72+
maximum: Option<usize>,
73+
},
74+
/// A Taproot child occurred somewhere it was not allowed.
75+
IllegalCurlyBrace {
76+
/// The position of the opening curly brace.
77+
pos: usize,
78+
},
5679
/// Data occurred after the final ).
5780
TrailingCharacter {
5881
/// The first trailing character.
@@ -93,6 +116,34 @@ impl fmt::Display for ParseTreeError {
93116
open_ch, open_pos, close_ch, close_pos
94117
)
95118
}
119+
ParseTreeError::IllegalCurlyBrace { pos } => {
120+
write!(f, "illegal `{{` at position {} (Taproot branches not allowed here)", pos)
121+
}
122+
ParseTreeError::IncorrectName { actual, expected } => {
123+
if expected.is_empty() {
124+
write!(f, "found node '{}', expected nameless node", actual)
125+
} else {
126+
write!(f, "expected node '{}', found '{}'", expected, actual)
127+
}
128+
}
129+
ParseTreeError::IncorrectNumberOfChildren {
130+
description,
131+
n_children,
132+
minimum,
133+
maximum,
134+
} => {
135+
write!(f, "{} must have ", description)?;
136+
match (minimum, maximum) {
137+
(_, Some(0)) => f.write_str("no children"),
138+
(Some(min), Some(max)) if min == max => write!(f, "{} children", min),
139+
(Some(min), None) if n_children < min => write!(f, "at least {} children", min),
140+
(Some(min), Some(max)) if n_children < min => write!(f, "at least {} children (maximum {})", min, max),
141+
(None, Some(max)) if n_children > max => write!(f, "at most {} children", max),
142+
(Some(min), Some(max)) if n_children > max => write!(f, "at most {} children (minimum {})", max, min),
143+
(x, y) => panic!("IncorrectNumberOfChildren error was constructed inconsistently (min {:?} max {:?})", x, y),
144+
}?;
145+
write!(f, ", but found {}", n_children)
146+
}
96147
ParseTreeError::TrailingCharacter { ch, pos } => {
97148
write!(f, "trailing data `{}...` (position {})", ch, pos)
98149
}
@@ -109,6 +160,9 @@ impl std::error::Error for ParseTreeError {
109160
| ParseTreeError::UnmatchedOpenParen { .. }
110161
| ParseTreeError::UnmatchedCloseParen { .. }
111162
| ParseTreeError::MismatchedParens { .. }
163+
| ParseTreeError::IllegalCurlyBrace { .. }
164+
| ParseTreeError::IncorrectName { .. }
165+
| ParseTreeError::IncorrectNumberOfChildren { .. }
112166
| ParseTreeError::TrailingCharacter { .. } => None,
113167
}
114168
}

src/expression/mod.rs

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
66
mod error;
77

8-
use core::fmt;
98
use core::str::FromStr;
9+
use core::{fmt, ops};
1010

1111
pub use self::error::{ParseThresholdError, ParseTreeError};
1212
use crate::descriptor::checksum::verify_checksum;
13+
use crate::iter::{self, TreeLike};
1314
use crate::prelude::*;
1415
use crate::{errstr, Error, Threshold, MAX_RECURSION_DEPTH};
1516

@@ -44,6 +45,21 @@ impl PartialEq for Tree<'_> {
4445
}
4546
impl Eq for Tree<'_> {}
4647

48+
impl<'a, 't> TreeLike for &'t Tree<'a> {
49+
type NaryChildren = &'t [Tree<'a>];
50+
51+
fn nary_len(tc: &Self::NaryChildren) -> usize { tc.len() }
52+
fn nary_index(tc: Self::NaryChildren, idx: usize) -> Self { &tc[idx] }
53+
54+
fn as_node(&self) -> iter::Tree<Self, Self::NaryChildren> {
55+
if self.args.is_empty() {
56+
iter::Tree::Nullary
57+
} else {
58+
iter::Tree::Nary(&self.args)
59+
}
60+
}
61+
}
62+
4763
/// The type of parentheses surrounding a node's children.
4864
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
4965
pub enum Parens {
@@ -71,6 +87,75 @@ pub trait FromTree: Sized {
7187
}
7288

7389
impl<'a> Tree<'a> {
90+
/// Check that a tree node has the given number of children.
91+
///
92+
/// The `description` argument is only used to populate the error return,
93+
/// and is not validated in any way.
94+
pub fn verify_n_children(
95+
&self,
96+
description: &'static str,
97+
n_children: impl ops::RangeBounds<usize>,
98+
) -> Result<(), ParseTreeError> {
99+
if n_children.contains(&self.n_children()) {
100+
Ok(())
101+
} else {
102+
let minimum = match n_children.start_bound() {
103+
ops::Bound::Included(n) => Some(*n),
104+
ops::Bound::Excluded(n) => Some(*n + 1),
105+
ops::Bound::Unbounded => None,
106+
};
107+
let maximum = match n_children.end_bound() {
108+
ops::Bound::Included(n) => Some(*n),
109+
ops::Bound::Excluded(n) => Some(*n - 1),
110+
ops::Bound::Unbounded => None,
111+
};
112+
Err(ParseTreeError::IncorrectNumberOfChildren {
113+
description,
114+
n_children: self.n_children(),
115+
minimum,
116+
maximum,
117+
})
118+
}
119+
}
120+
121+
/// Check that a tree node has the given name, one child, and round braces.
122+
///
123+
/// Returns the first child.
124+
///
125+
/// # Panics
126+
///
127+
/// Panics if zero is in bounds for `n_children` (since then there may be
128+
/// no sensible value to return).
129+
pub fn verify_toplevel(
130+
&self,
131+
name: &'static str,
132+
n_children: impl ops::RangeBounds<usize>,
133+
) -> Result<&Self, ParseTreeError> {
134+
assert!(
135+
!n_children.contains(&0),
136+
"verify_toplevel is intended for nodes with >= 1 child"
137+
);
138+
139+
if self.name != name {
140+
Err(ParseTreeError::IncorrectName { actual: self.name.to_owned(), expected: name })
141+
} else if self.parens == Parens::Curly {
142+
Err(ParseTreeError::IllegalCurlyBrace { pos: self.children_pos })
143+
} else {
144+
self.verify_n_children(name, n_children)?;
145+
Ok(&self.args[0])
146+
}
147+
}
148+
149+
/// Check that a tree has no curly-brace children in it.
150+
pub fn verify_no_curly_braces(&self) -> Result<(), ParseTreeError> {
151+
for tree in self.pre_order_iter() {
152+
if tree.parens == Parens::Curly {
153+
return Err(ParseTreeError::IllegalCurlyBrace { pos: tree.children_pos });
154+
}
155+
}
156+
Ok(())
157+
}
158+
74159
/// Parse an expression with round brackets
75160
pub fn from_slice(sl: &'a str) -> Result<Tree<'a>, ParseTreeError> {
76161
Self::from_slice_delim(sl, Delimiter::NonTaproot)

src/miniscript/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,7 @@ impl<Pk: FromStrKey, Ctx: ScriptContext> crate::expression::FromTree for Miniscr
802802
/// Parse an expression tree into a Miniscript. As a general rule, this
803803
/// should not be called directly; rather go through the descriptor API.
804804
fn from_tree(top: &expression::Tree) -> Result<Miniscript<Pk, Ctx>, Error> {
805+
top.verify_no_curly_braces().map_err(Error::ParseTree)?;
805806
let inner: Terminal<Pk, Ctx> = expression::FromTree::from_tree(top)?;
806807
Miniscript::from_ast(inner)
807808
}

0 commit comments

Comments
 (0)