Skip to content

Commit 5a0282a

Browse files
committed
feat(policy): change semantic::Policy Display to mathematical notation
- Use mathematical symbols (∧, ∨, #{...} = k) instead of and()/or()/thresh() for semantic::Policy Display output. Display is now presentation-only and not a stable machine-readable serialization. - Add Policy::to_policy_syntax_string() that emits the function-call policy syntax, documented narrowly as a cross-version comparison helper (primarily for the regression_descriptor_parse fuzz target). - Restore the full structural cross-version comparison in that fuzz target by serializing the current crate's lifted policy via to_policy_syntax_string().
1 parent 04f1c58 commit 5a0282a

5 files changed

Lines changed: 128 additions & 37 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: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,38 @@ fn do_test(data: &[u8]) {
1414
(Ok(x), Err(e)) => panic!("new logic parses {} as {:?}, old fails with {}", data_str, x, e),
1515
(Err(e), Ok(x)) => panic!("old logic parses {} as {:?}, new fails with {}", data_str, x, e),
1616
(Ok(new), Ok(old)) => {
17-
use miniscript::policy::Liftable as _;
18-
use old_miniscript::policy::Liftable as _;
19-
2017
assert_eq!(
2118
old.to_string(),
2219
new.to_string(),
2320
"input {} (left is old, right is new)",
2421
data_str
2522
);
2623

27-
match (new.lift(), old.lift()) {
28-
(Err(_), Err(_)) => {}
29-
(Ok(x), Err(e)) => {
30-
panic!("new logic lifts {} as {:?}, old fails with {}", data_str, x, e)
31-
}
32-
(Err(e), Ok(x)) => {
33-
panic!("old logic lifts {} as {:?}, new fails with {}", data_str, x, e)
34-
}
35-
(Ok(new), Ok(old)) => {
36-
assert_eq!(
37-
old.to_string(),
38-
new.to_string(),
39-
"lifted input {} (left is old, right is new)",
40-
data_str
41-
)
24+
// The current crate's semantic::Policy Display was changed to
25+
// mathematical notation, which does not match old_miniscript's
26+
// function-call format. Use to_policy_syntax_string() to
27+
// serialize in the policy-syntax form so we can still do a full
28+
// structural comparison across crate versions.
29+
{
30+
use miniscript::policy::Liftable as _;
31+
use old_miniscript::policy::Liftable as _;
32+
33+
match (new.lift(), old.lift()) {
34+
(Err(_), Err(_)) => {}
35+
(Ok(x), Err(e)) => {
36+
panic!("new logic lifts {} as {:?}, old fails with {}", data_str, x, e)
37+
}
38+
(Err(e), Ok(x)) => {
39+
panic!("old logic lifts {} as {:?}, new fails with {}", data_str, x, e)
40+
}
41+
(Ok(new_lift), Ok(old_lift)) => {
42+
assert_eq!(
43+
old_lift.to_string(),
44+
new_lift.to_policy_syntax_string(),
45+
"lifted semantic policy mismatch for input {} (left is old, right is new as policy-syntax string)",
46+
data_str
47+
);
48+
}
4249
}
4350
}
4451
}

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/policy/mod.rs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -251,12 +251,6 @@ mod tests {
251251
assert_eq!(s.to_lowercase(), output.to_lowercase());
252252
}
253253

254-
fn semantic_policy_rtt(s: &str) {
255-
let sem = SemanticPol::from_str(s).unwrap();
256-
let output = sem.normalized().to_string();
257-
assert_eq!(s.to_lowercase(), output.to_lowercase());
258-
}
259-
260254
#[test]
261255
fn test_timelock_validity() {
262256
// only height
@@ -279,17 +273,25 @@ mod tests {
279273
concrete_policy_rtt("or(99@pk(X),1@pk(Y))");
280274
concrete_policy_rtt("and(pk(X),or(99@pk(Y),1@older(12960)))");
281275

282-
semantic_policy_rtt("pk()");
283-
semantic_policy_rtt("or(pk(X),pk(Y))");
284-
semantic_policy_rtt("and(pk(X),pk(Y))");
285-
286276
//fuzzer crashes
287277
assert!(ConcretePol::from_str("thresh()").is_err());
288278
assert!(SemanticPol::from_str("thresh(0)").is_err());
289279
assert!(SemanticPol::from_str("thresh()").is_err());
290280
concrete_policy_rtt("ripemd160()");
291281
}
292282

283+
#[test]
284+
fn semantic_display_uses_mathematical_notation() {
285+
let pol = SemanticPol::from_str("and(pk(A),pk(B))").unwrap();
286+
assert_eq!(pol.normalized().to_string(), "(pk(A) ∧ pk(B))");
287+
288+
let pol = SemanticPol::from_str("or(pk(A),pk(B))").unwrap();
289+
assert_eq!(pol.normalized().to_string(), "(pk(A) ∨ pk(B))");
290+
291+
let pol = SemanticPol::from_str("thresh(2,pk(A),pk(B),pk(C))").unwrap();
292+
assert_eq!(pol.normalized().to_string(), "#{pk(A), pk(B), pk(C)} = 2");
293+
}
294+
293295
#[test]
294296
fn compile_invalid() {
295297
// Since the root Error does not support Eq type, we have to

src/policy/semantic.rs

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,13 @@ impl<Pk: MiniscriptKey> fmt::Debug for Policy<Pk> {
248248
}
249249
}
250250

251+
/// Displays the policy using mathematical notation for readability.
252+
///
253+
/// - `and(a, b)` is displayed as `(a ∧ b)`
254+
/// - `or(a, b)` is displayed as `(a ∨ b)`
255+
/// - `thresh(k, a, b, c)` is displayed as `#{a, b, c} = k`
256+
///
257+
/// Note: this format is not parseable.
251258
impl<Pk: MiniscriptKey> fmt::Display for Policy<Pk> {
252259
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
253260
match *self {
@@ -261,13 +268,90 @@ impl<Pk: MiniscriptKey> fmt::Display for Policy<Pk> {
261268
Policy::Ripemd160(ref h) => write!(f, "ripemd160({})", h),
262269
Policy::Hash160(ref h) => write!(f, "hash160({})", h),
263270
Policy::Thresh(ref thresh) => {
271+
let mut iter = thresh.iter();
272+
let first = iter.next().expect("thresholds are never empty");
264273
if thresh.k() == thresh.n() {
265-
thresh.display("and", false).fmt(f)
274+
write!(f, "({}", first)?;
275+
for sub in iter {
276+
write!(f, " ∧ {}", sub)?;
277+
}
278+
f.write_str(")")
279+
} else if thresh.k() == 1 {
280+
write!(f, "({}", first)?;
281+
for sub in iter {
282+
write!(f, " ∨ {}", sub)?;
283+
}
284+
f.write_str(")")
285+
} else {
286+
write!(f, "#{{{}", first)?;
287+
for sub in iter {
288+
write!(f, ", {}", sub)?;
289+
}
290+
write!(f, "}} = {}", thresh.k())
291+
}
292+
}
293+
}
294+
}
295+
}
296+
297+
impl<Pk: MiniscriptKey> Policy<Pk> {
298+
/// Serializes the policy using function-call notation
299+
/// (`and(..)`, `or(..)`, `thresh(k, ..)`).
300+
///
301+
/// The [`fmt::Display`] impl uses mathematical notation (`∧`, `∨`,
302+
/// `#{..} = k`). That change is deliberate: Display is no longer intended
303+
/// to be a stable, machine-readable serialization of a semantic policy.
304+
/// New code should treat Display as presentation-only.
305+
///
306+
/// This method exists for the narrow case of cross-version comparison
307+
/// against older releases of this crate (notably the
308+
/// `regression_descriptor_parse` fuzz target), which still emit the
309+
/// function-call form. Prefer working with the structured [`Policy`]
310+
/// value directly over parsing this string.
311+
pub fn to_policy_syntax_string(&self) -> String {
312+
let mut s = String::new();
313+
self.write_policy_syntax(&mut s)
314+
.expect("writing to a String is infallible");
315+
s
316+
}
317+
318+
fn write_policy_syntax<W: fmt::Write>(&self, w: &mut W) -> fmt::Result {
319+
match *self {
320+
Policy::Unsatisfiable => w.write_str("UNSATISFIABLE"),
321+
Policy::Trivial => w.write_str("TRIVIAL"),
322+
Policy::Key(ref pkh) => write!(w, "pk({})", pkh),
323+
Policy::After(n) => write!(w, "after({})", n),
324+
Policy::Older(n) => write!(w, "older({})", n),
325+
Policy::Sha256(ref h) => write!(w, "sha256({})", h),
326+
Policy::Hash256(ref h) => write!(w, "hash256({})", h),
327+
Policy::Ripemd160(ref h) => write!(w, "ripemd160({})", h),
328+
Policy::Hash160(ref h) => write!(w, "hash160({})", h),
329+
Policy::Thresh(ref thresh) => {
330+
let (name, show_k) = if thresh.k() == thresh.n() {
331+
("and", false)
266332
} else if thresh.k() == 1 {
267-
thresh.display("or", false).fmt(f)
333+
("or", false)
334+
} else {
335+
("thresh", true)
336+
};
337+
w.write_str(name)?;
338+
w.write_str("(")?;
339+
let mut iter = thresh.iter();
340+
if show_k {
341+
write!(w, "{}", thresh.k())?;
342+
for child in iter {
343+
w.write_str(",")?;
344+
child.write_policy_syntax(w)?;
345+
}
268346
} else {
269-
thresh.display("thresh", true).fmt(f)
347+
let first = iter.next().expect("thresholds are never empty");
348+
first.write_policy_syntax(w)?;
349+
for child in iter {
350+
w.write_str(",")?;
351+
child.write_policy_syntax(w)?;
352+
}
270353
}
354+
w.write_str(")")
271355
}
272356
}
273357
}
@@ -281,8 +365,6 @@ impl<Pk: FromStrKey> str::FromStr for Policy<Pk> {
281365
}
282366
}
283367

284-
serde_string_impl_pk!(Policy, "a miniscript semantic policy");
285-
286368
impl<Pk: FromStrKey> expression::FromTree for Policy<Pk> {
287369
fn from_tree(root: expression::TreeIterItem) -> Result<Policy<Pk>, Error> {
288370
root.verify_no_curly_braces()

0 commit comments

Comments
 (0)