Skip to content

Commit 3050e0c

Browse files
committed
feat(policy): math notation for semantic::Policy (Display + Parser)
- Display: emit `(a ∧ b)`, `(a ∨ b)`, `#{...} = k` for and/or/thresh - Parser: FromStr accepts math notation; legacy `and(...)`/`or(...)`/`thresh(...)` still parses - Fuzz: roundtrip target asserts parse → display equality up to whitespace for math-form inputs
1 parent 04f1c58 commit 3050e0c

8 files changed

Lines changed: 466 additions & 15 deletions

File tree

examples/htlc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ fn main() {
3737
// Lift the descriptor into an abstract policy.
3838
assert_eq!(
3939
format!("{}", htlc_descriptor.lift().unwrap()),
40-
"or(and(pk(022222222222222222222222222222222222222222222222222222222222222222),sha256(1111111111111111111111111111111111111111111111111111111111111111)),and(pk(020202020202020202020202020202020202020202020202020202020202020202),older(4444)))"
40+
"((pk(022222222222222222222222222222222222222222222222222222222222222222)sha256(1111111111111111111111111111111111111111111111111111111111111111))(pk(020202020202020202020202020202020202020202020202020202020202020202)older(4444)))"
4141
);
4242

4343
// Get the scriptPubkey for this Wsh descriptor.

fuzz/fuzz_targets/regression_descriptor_parse.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ fn do_test(data: &[u8]) {
3535
(Ok(new), Ok(old)) => {
3636
assert_eq!(
3737
old.to_string(),
38-
new.to_string(),
38+
new.to_policy_syntax_string(),
3939
"lifted input {} (left is old, right is new)",
4040
data_str
4141
)

fuzz/fuzz_targets/roundtrip_semantic.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,17 @@ type Policy = policy::Semantic<String>;
99

1010
fn do_test(data: &[u8]) {
1111
let data_str = String::from_utf8_lossy(data);
12+
let is_legacy = !data_str.starts_with('(')
13+
&& !data_str.contains('∧')
14+
&& !data_str.contains('∨')
15+
&& !data_str.contains("#{");
16+
if is_legacy {
17+
return;
18+
}
1219
if let Ok(pol) = Policy::from_str(&data_str) {
1320
let output = pol.to_string();
14-
assert_eq!(data_str.to_lowercase(), output.to_lowercase());
21+
let strip_ws: fn(&str) -> String = |s| s.chars().filter(|c| !c.is_whitespace()).collect();
22+
assert_eq!(strip_ws(&data_str), strip_ws(&output));
1523
}
1624
}
1725

src/descriptor/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2175,15 +2175,15 @@ pk(03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8))";
21752175
// Taproot structure is erased but key order preserved..
21762176
let desc = Descriptor::<String>::from_str("tr(ROOT,{pk(A1),{pk(B1),pk(B2)}})").unwrap();
21772177
let lift = desc.lift().unwrap();
2178-
assert_eq!(lift.to_string(), "or(pk(ROOT),or(pk(A1),pk(B1),pk(B2)))",);
2178+
assert_eq!(lift.to_string(), "(pk(ROOT)(pk(A1)pk(B1)pk(B2)))");
21792179
let desc = Descriptor::<String>::from_str("tr(ROOT,{{pk(A1),pk(B1)},pk(B2)})").unwrap();
21802180
let lift = desc.lift().unwrap();
2181-
assert_eq!(lift.to_string(), "or(pk(ROOT),or(pk(A1),pk(B1),pk(B2)))",);
2181+
assert_eq!(lift.to_string(), "(pk(ROOT)(pk(A1)pk(B1)pk(B2)))");
21822182

21832183
// And normalization happens
21842184
let desc = Descriptor::<String>::from_str("tr(ROOT,{0,{0,0}})").unwrap();
21852185
let lift = desc.lift().unwrap();
2186-
assert_eq!(lift.to_string(), "or(pk(ROOT),UNSATISFIABLE)",);
2186+
assert_eq!(lift.to_string(), "(pk(ROOT)UNSATISFIABLE)");
21872187
}
21882188

21892189
#[test]

src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ pub enum ParseError {
2929
Num(crate::ParseNumError),
3030
/// Error parsing a string into an expression tree.
3131
Tree(crate::ParseTreeError),
32+
/// Syntax error in the mathematical-notation form of a `policy::Semantic`.
33+
Math(crate::MathSyntaxError),
3234
}
3335

3436
impl ParseError {
@@ -54,6 +56,7 @@ impl fmt::Display for ParseError {
5456
ParseError::FromStr(ref e) => e.fmt(f),
5557
ParseError::Num(ref e) => e.fmt(f),
5658
ParseError::Tree(ref e) => e.fmt(f),
59+
ParseError::Math(ref e) => e.fmt(f),
5760
}
5861
}
5962
}
@@ -67,6 +70,7 @@ impl error::Error for ParseError {
6770
ParseError::FromStr(..) => None,
6871
ParseError::Num(ref e) => Some(e),
6972
ParseError::Tree(ref e) => Some(e),
73+
ParseError::Math(ref e) => Some(e),
7074
}
7175
}
7276
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ pub use crate::miniscript::context::{BareCtx, Legacy, ScriptContext, Segwitv0, S
142142
pub use crate::miniscript::decode::Terminal;
143143
pub use crate::miniscript::satisfy::{Preimage32, Satisfier};
144144
pub use crate::miniscript::{hash256, Miniscript};
145+
pub use crate::policy::semantic::MathSyntaxError;
145146
use crate::prelude::*;
146147
pub use crate::primitives::absolute_locktime::{AbsLockTime, AbsLockTimeError};
147148
pub use crate::primitives::relative_locktime::{RelLockTime, RelLockTimeError};

src/policy/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,8 @@ mod tests {
280280
concrete_policy_rtt("and(pk(X),or(99@pk(Y),1@older(12960)))");
281281

282282
semantic_policy_rtt("pk()");
283-
semantic_policy_rtt("or(pk(X),pk(Y))");
284-
semantic_policy_rtt("and(pk(X),pk(Y))");
283+
semantic_policy_rtt("(pk(X)pk(Y))");
284+
semantic_policy_rtt("(pk(X)pk(Y))");
285285

286286
//fuzzer crashes
287287
assert!(ConcretePol::from_str("thresh()").is_err());

0 commit comments

Comments
 (0)