Skip to content

Commit e090fe9

Browse files
committed
Merge #725: Eliminate more recursion, and Liftable impl for Terminal
8e2d90c policy: make lifting algorithm non-recursive, remove Liftable for Terminal (Andrew Poelstra) 44e0550 miniscript: add unit test for substitute_raw_pkh (Andrew Poelstra) e31d52b iter: introduce right-to-left post-order iterator, use throughout codebase (Andrew Poelstra) 5da250a miniscript: add unit test for repeated pubkeys (Andrew Poelstra) 7bb9b04 miniscript: add unit test for timelock mixing (Andrew Poelstra) ff0509f policy: stop using duplicate keys in unit tests (Andrew Poelstra) 1b49a39 types: clean up some error messages (Andrew Poelstra) Pull request description: This PR does a few things which are mostly related: * Adds unit tests and does a couple cleanups. * Introduces a new right-to-left post-order iterator and uses that to eliminate a bunch of Arc::clones and extra memory usage from existing non-recursive algorithms * Fixes a bug in `Miniscript::substitute_raw_pkh` where the function completely didn't work (this fell out of the right-to-left change) * Converts `Miniscript::lift` to be non-recursive * Eleminates `Terminal::lift` because this impl is conceptually confused (`Terminal` as a type should never be used except to construct `Miniscript`s; they are not typechecked or sanity-checked and have no capacity to store type information). It was originally implemented as a convenience to help the `Miniscript` implementation but we don't need it now It also stops using duplicate keys in a couple unit tests. Right now our "sanity checking" logic for things like duplicate keys is super ad-hoc and inconsistent. For example, the duplicate-key check is implemented three times (in miniscript::analyzable, policy::semantic and policy::concrete) and the check is actually done in somewhat-random places. This PR doesn't change that, but since I was touching unit tests I figured I'd clean it up. ACKs for top commit: sanket1729: ACK 8e2d90c Tree-SHA512: 55edb0b8ac055215d4f733b663a8b5279aa53d360f17bf9034be00f9d30f0329eaa58faae34446c4f739241ee53b7b4d36127f4cc2dacf7c9e49a57feab292f0
2 parents cab7762 + 8e2d90c commit e090fe9

File tree

6 files changed

+326
-180
lines changed

6 files changed

+326
-180
lines changed

src/iter/tree.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ pub trait TreeLike: Clone + Sized {
104104
fn post_order_iter(self) -> PostOrderIter<Self> {
105105
PostOrderIter { index: 0, stack: vec![IterStackItem::unprocessed(self, None)] }
106106
}
107+
108+
/// Obtains an iterator of all the nodes rooted at the DAG, in right-to-left post order.
109+
///
110+
/// This ordering is useful for "translation" algorithms which iterate over a
111+
/// structure, pushing translated nodes and popping children.
112+
fn rtl_post_order_iter(self) -> RtlPostOrderIter<Self> {
113+
RtlPostOrderIter { inner: Rtl(self).post_order_iter() }
114+
}
107115
}
108116

109117
/// Element stored internally on the stack of a [`PostOrderIter`].
@@ -202,6 +210,53 @@ impl<T: TreeLike> Iterator for PostOrderIter<T> {
202210
}
203211
}
204212

213+
/// Adaptor structure to allow iterating in right-to-left order.
214+
#[derive(Clone, Debug)]
215+
struct Rtl<T>(pub T);
216+
217+
impl<T: TreeLike> TreeLike for Rtl<T> {
218+
type NaryChildren = T::NaryChildren;
219+
220+
fn nary_len(tc: &Self::NaryChildren) -> usize { T::nary_len(tc) }
221+
fn nary_index(tc: Self::NaryChildren, idx: usize) -> Self {
222+
let rtl_idx = T::nary_len(&tc) - idx - 1;
223+
Rtl(T::nary_index(tc, rtl_idx))
224+
}
225+
226+
fn as_node(&self) -> Tree<Self, Self::NaryChildren> {
227+
match self.0.as_node() {
228+
Tree::Nullary => Tree::Nullary,
229+
Tree::Unary(a) => Tree::Unary(Rtl(a)),
230+
Tree::Binary(a, b) => Tree::Binary(Rtl(b), Rtl(a)),
231+
Tree::Ternary(a, b, c) => Tree::Ternary(Rtl(c), Rtl(b), Rtl(a)),
232+
Tree::Nary(data) => Tree::Nary(data),
233+
}
234+
}
235+
}
236+
237+
/// Iterates over a DAG in _right-to-left post order_.
238+
///
239+
/// That means nodes are yielded in the order (right child, left child, parent).
240+
#[derive(Clone, Debug)]
241+
pub struct RtlPostOrderIter<T> {
242+
inner: PostOrderIter<Rtl<T>>,
243+
}
244+
245+
impl<T: TreeLike> Iterator for RtlPostOrderIter<T> {
246+
type Item = PostOrderIterItem<T>;
247+
248+
fn next(&mut self) -> Option<Self::Item> {
249+
self.inner.next().map(|mut item| {
250+
item.child_indices.reverse();
251+
PostOrderIterItem {
252+
child_indices: item.child_indices,
253+
index: item.index,
254+
node: item.node.0,
255+
}
256+
})
257+
}
258+
}
259+
205260
/// Iterates over a [`TreeLike`] in _pre order_.
206261
///
207262
/// Unlike the post-order iterator, this one does not keep track of indices

src/miniscript/mod.rs

Lines changed: 169 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,7 @@ mod private {
8686
/// and they can call `Miniscript::clone`.
8787
fn clone(&self) -> Self {
8888
let mut stack = vec![];
89-
for item in self.post_order_iter() {
90-
let child_n = |n| Arc::clone(&stack[item.child_indices[n]]);
91-
89+
for item in self.rtl_post_order_iter() {
9290
let new_term = match item.node.node {
9391
Terminal::PkK(ref p) => Terminal::PkK(p.clone()),
9492
Terminal::PkH(ref p) => Terminal::PkH(p.clone()),
@@ -101,23 +99,31 @@ mod private {
10199
Terminal::Hash160(ref x) => Terminal::Hash160(x.clone()),
102100
Terminal::True => Terminal::True,
103101
Terminal::False => Terminal::False,
104-
Terminal::Alt(..) => Terminal::Alt(child_n(0)),
105-
Terminal::Swap(..) => Terminal::Swap(child_n(0)),
106-
Terminal::Check(..) => Terminal::Check(child_n(0)),
107-
Terminal::DupIf(..) => Terminal::DupIf(child_n(0)),
108-
Terminal::Verify(..) => Terminal::Verify(child_n(0)),
109-
Terminal::NonZero(..) => Terminal::NonZero(child_n(0)),
110-
Terminal::ZeroNotEqual(..) => Terminal::ZeroNotEqual(child_n(0)),
111-
Terminal::AndV(..) => Terminal::AndV(child_n(0), child_n(1)),
112-
Terminal::AndB(..) => Terminal::AndB(child_n(0), child_n(1)),
113-
Terminal::AndOr(..) => Terminal::AndOr(child_n(0), child_n(1), child_n(2)),
114-
Terminal::OrB(..) => Terminal::OrB(child_n(0), child_n(1)),
115-
Terminal::OrD(..) => Terminal::OrD(child_n(0), child_n(1)),
116-
Terminal::OrC(..) => Terminal::OrC(child_n(0), child_n(1)),
117-
Terminal::OrI(..) => Terminal::OrI(child_n(0), child_n(1)),
118-
Terminal::Thresh(ref thresh) => Terminal::Thresh(
119-
thresh.map_from_post_order_iter(&item.child_indices, &stack),
102+
Terminal::Alt(..) => Terminal::Alt(stack.pop().unwrap()),
103+
Terminal::Swap(..) => Terminal::Swap(stack.pop().unwrap()),
104+
Terminal::Check(..) => Terminal::Check(stack.pop().unwrap()),
105+
Terminal::DupIf(..) => Terminal::DupIf(stack.pop().unwrap()),
106+
Terminal::Verify(..) => Terminal::Verify(stack.pop().unwrap()),
107+
Terminal::NonZero(..) => Terminal::NonZero(stack.pop().unwrap()),
108+
Terminal::ZeroNotEqual(..) => Terminal::ZeroNotEqual(stack.pop().unwrap()),
109+
Terminal::AndV(..) => {
110+
Terminal::AndV(stack.pop().unwrap(), stack.pop().unwrap())
111+
}
112+
Terminal::AndB(..) => {
113+
Terminal::AndB(stack.pop().unwrap(), stack.pop().unwrap())
114+
}
115+
Terminal::AndOr(..) => Terminal::AndOr(
116+
stack.pop().unwrap(),
117+
stack.pop().unwrap(),
118+
stack.pop().unwrap(),
120119
),
120+
Terminal::OrB(..) => Terminal::OrB(stack.pop().unwrap(), stack.pop().unwrap()),
121+
Terminal::OrD(..) => Terminal::OrD(stack.pop().unwrap(), stack.pop().unwrap()),
122+
Terminal::OrC(..) => Terminal::OrC(stack.pop().unwrap(), stack.pop().unwrap()),
123+
Terminal::OrI(..) => Terminal::OrI(stack.pop().unwrap(), stack.pop().unwrap()),
124+
Terminal::Thresh(ref thresh) => {
125+
Terminal::Thresh(thresh.map_ref(|_| stack.pop().unwrap()))
126+
}
121127
Terminal::Multi(ref thresh) => Terminal::Multi(thresh.clone()),
122128
Terminal::MultiA(ref thresh) => Terminal::MultiA(thresh.clone()),
123129
};
@@ -130,6 +136,7 @@ mod private {
130136
}));
131137
}
132138

139+
assert_eq!(stack.len(), 1);
133140
Arc::try_unwrap(stack.pop().unwrap()).unwrap()
134141
}
135142
}
@@ -536,9 +543,7 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> Miniscript<Pk, Ctx> {
536543
T: Translator<Pk, Q, FuncError>,
537544
{
538545
let mut translated = vec![];
539-
for data in Arc::new(self.clone()).post_order_iter() {
540-
let child_n = |n| Arc::clone(&translated[data.child_indices[n]]);
541-
546+
for data in self.rtl_post_order_iter() {
542547
let new_term = match data.node.node {
543548
Terminal::PkK(ref p) => Terminal::PkK(t.pk(p)?),
544549
Terminal::PkH(ref p) => Terminal::PkH(t.pk(p)?),
@@ -551,23 +556,39 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> Miniscript<Pk, Ctx> {
551556
Terminal::Hash160(ref x) => Terminal::Hash160(t.hash160(x)?),
552557
Terminal::True => Terminal::True,
553558
Terminal::False => Terminal::False,
554-
Terminal::Alt(..) => Terminal::Alt(child_n(0)),
555-
Terminal::Swap(..) => Terminal::Swap(child_n(0)),
556-
Terminal::Check(..) => Terminal::Check(child_n(0)),
557-
Terminal::DupIf(..) => Terminal::DupIf(child_n(0)),
558-
Terminal::Verify(..) => Terminal::Verify(child_n(0)),
559-
Terminal::NonZero(..) => Terminal::NonZero(child_n(0)),
560-
Terminal::ZeroNotEqual(..) => Terminal::ZeroNotEqual(child_n(0)),
561-
Terminal::AndV(..) => Terminal::AndV(child_n(0), child_n(1)),
562-
Terminal::AndB(..) => Terminal::AndB(child_n(0), child_n(1)),
563-
Terminal::AndOr(..) => Terminal::AndOr(child_n(0), child_n(1), child_n(2)),
564-
Terminal::OrB(..) => Terminal::OrB(child_n(0), child_n(1)),
565-
Terminal::OrD(..) => Terminal::OrD(child_n(0), child_n(1)),
566-
Terminal::OrC(..) => Terminal::OrC(child_n(0), child_n(1)),
567-
Terminal::OrI(..) => Terminal::OrI(child_n(0), child_n(1)),
568-
Terminal::Thresh(ref thresh) => Terminal::Thresh(
569-
thresh.map_from_post_order_iter(&data.child_indices, &translated),
559+
Terminal::Alt(..) => Terminal::Alt(translated.pop().unwrap()),
560+
Terminal::Swap(..) => Terminal::Swap(translated.pop().unwrap()),
561+
Terminal::Check(..) => Terminal::Check(translated.pop().unwrap()),
562+
Terminal::DupIf(..) => Terminal::DupIf(translated.pop().unwrap()),
563+
Terminal::Verify(..) => Terminal::Verify(translated.pop().unwrap()),
564+
Terminal::NonZero(..) => Terminal::NonZero(translated.pop().unwrap()),
565+
Terminal::ZeroNotEqual(..) => Terminal::ZeroNotEqual(translated.pop().unwrap()),
566+
Terminal::AndV(..) => {
567+
Terminal::AndV(translated.pop().unwrap(), translated.pop().unwrap())
568+
}
569+
Terminal::AndB(..) => {
570+
Terminal::AndB(translated.pop().unwrap(), translated.pop().unwrap())
571+
}
572+
Terminal::AndOr(..) => Terminal::AndOr(
573+
translated.pop().unwrap(),
574+
translated.pop().unwrap(),
575+
translated.pop().unwrap(),
570576
),
577+
Terminal::OrB(..) => {
578+
Terminal::OrB(translated.pop().unwrap(), translated.pop().unwrap())
579+
}
580+
Terminal::OrD(..) => {
581+
Terminal::OrD(translated.pop().unwrap(), translated.pop().unwrap())
582+
}
583+
Terminal::OrC(..) => {
584+
Terminal::OrC(translated.pop().unwrap(), translated.pop().unwrap())
585+
}
586+
Terminal::OrI(..) => {
587+
Terminal::OrI(translated.pop().unwrap(), translated.pop().unwrap())
588+
}
589+
Terminal::Thresh(ref thresh) => {
590+
Terminal::Thresh(thresh.map_ref(|_| translated.pop().unwrap()))
591+
}
571592
Terminal::Multi(ref thresh) => Terminal::Multi(thresh.translate_ref(|k| t.pk(k))?),
572593
Terminal::MultiA(ref thresh) => {
573594
Terminal::MultiA(thresh.translate_ref(|k| t.pk(k))?)
@@ -582,22 +603,58 @@ impl<Pk: MiniscriptKey, Ctx: ScriptContext> Miniscript<Pk, Ctx> {
582603

583604
/// Substitutes raw public keys hashes with the public keys as provided by map.
584605
pub fn substitute_raw_pkh(&self, pk_map: &BTreeMap<hash160::Hash, Pk>) -> Miniscript<Pk, Ctx> {
585-
let mut translated = vec![];
586-
for data in Arc::new(self.clone()).post_order_iter() {
587-
let new_term = if let Terminal::RawPkH(ref p) = data.node.node {
588-
match pk_map.get(p) {
589-
Some(pk) => Terminal::PkH(pk.clone()),
590-
None => Terminal::RawPkH(*p),
606+
let mut stack = vec![];
607+
for item in self.rtl_post_order_iter() {
608+
let new_term = match item.node.node {
609+
Terminal::PkK(ref p) => Terminal::PkK(p.clone()),
610+
Terminal::PkH(ref p) => Terminal::PkH(p.clone()),
611+
// This algorithm is identical to Clone::clone except for this line.
612+
Terminal::RawPkH(ref hash) => match pk_map.get(hash) {
613+
Some(p) => Terminal::PkH(p.clone()),
614+
None => Terminal::RawPkH(*hash),
615+
},
616+
Terminal::After(ref n) => Terminal::After(*n),
617+
Terminal::Older(ref n) => Terminal::Older(*n),
618+
Terminal::Sha256(ref x) => Terminal::Sha256(x.clone()),
619+
Terminal::Hash256(ref x) => Terminal::Hash256(x.clone()),
620+
Terminal::Ripemd160(ref x) => Terminal::Ripemd160(x.clone()),
621+
Terminal::Hash160(ref x) => Terminal::Hash160(x.clone()),
622+
Terminal::True => Terminal::True,
623+
Terminal::False => Terminal::False,
624+
Terminal::Alt(..) => Terminal::Alt(stack.pop().unwrap()),
625+
Terminal::Swap(..) => Terminal::Swap(stack.pop().unwrap()),
626+
Terminal::Check(..) => Terminal::Check(stack.pop().unwrap()),
627+
Terminal::DupIf(..) => Terminal::DupIf(stack.pop().unwrap()),
628+
Terminal::Verify(..) => Terminal::Verify(stack.pop().unwrap()),
629+
Terminal::NonZero(..) => Terminal::NonZero(stack.pop().unwrap()),
630+
Terminal::ZeroNotEqual(..) => Terminal::ZeroNotEqual(stack.pop().unwrap()),
631+
Terminal::AndV(..) => Terminal::AndV(stack.pop().unwrap(), stack.pop().unwrap()),
632+
Terminal::AndB(..) => Terminal::AndB(stack.pop().unwrap(), stack.pop().unwrap()),
633+
Terminal::AndOr(..) => Terminal::AndOr(
634+
stack.pop().unwrap(),
635+
stack.pop().unwrap(),
636+
stack.pop().unwrap(),
637+
),
638+
Terminal::OrB(..) => Terminal::OrB(stack.pop().unwrap(), stack.pop().unwrap()),
639+
Terminal::OrD(..) => Terminal::OrD(stack.pop().unwrap(), stack.pop().unwrap()),
640+
Terminal::OrC(..) => Terminal::OrC(stack.pop().unwrap(), stack.pop().unwrap()),
641+
Terminal::OrI(..) => Terminal::OrI(stack.pop().unwrap(), stack.pop().unwrap()),
642+
Terminal::Thresh(ref thresh) => {
643+
Terminal::Thresh(thresh.map_ref(|_| stack.pop().unwrap()))
591644
}
592-
} else {
593-
data.node.node.clone()
645+
Terminal::Multi(ref thresh) => Terminal::Multi(thresh.clone()),
646+
Terminal::MultiA(ref thresh) => Terminal::MultiA(thresh.clone()),
594647
};
595648

596-
let new_ms = Miniscript::from_ast(new_term).expect("typeck");
597-
translated.push(Arc::new(new_ms));
649+
stack.push(Arc::new(Miniscript::from_components_unchecked(
650+
new_term,
651+
item.node.ty,
652+
item.node.ext,
653+
)));
598654
}
599655

600-
Arc::try_unwrap(translated.pop().unwrap()).unwrap()
656+
assert_eq!(stack.len(), 1);
657+
Arc::try_unwrap(stack.pop().unwrap()).unwrap()
601658
}
602659
}
603660

@@ -822,6 +879,7 @@ mod tests {
822879
}
823880
let roundtrip = Miniscript::from_str(&display).expect("parse string serialization");
824881
assert_eq!(roundtrip, script);
882+
assert_eq!(roundtrip.clone(), script);
825883
}
826884

827885
fn string_display_debug_test<Ctx: ScriptContext>(
@@ -1373,8 +1431,12 @@ mod tests {
13731431
#[test]
13741432
fn expr_features() {
13751433
// test that parsing raw hash160 does not work with
1376-
let hash160_str = "e9f171df53e04b270fa6271b42f66b0f4a99c5a2";
1377-
let ms_str = &format!("c:expr_raw_pkh({})", hash160_str);
1434+
let pk = bitcoin::PublicKey::from_str(
1435+
"02c2fd50ceae468857bb7eb32ae9cd4083e6c7e42fbbec179d81134b3e3830586c",
1436+
)
1437+
.unwrap();
1438+
let hash160 = pk.pubkey_hash().to_raw_hash();
1439+
let ms_str = &format!("c:expr_raw_pkh({})", hash160);
13781440
type SegwitMs = Miniscript<bitcoin::PublicKey, Segwitv0>;
13791441

13801442
// Test that parsing raw hash160 from string does not work without extra features
@@ -1387,6 +1449,12 @@ mod tests {
13871449
SegwitMs::parse(&script).unwrap_err();
13881450
SegwitMs::parse_insane(&script).unwrap_err();
13891451
SegwitMs::parse_with_ext(&script, &ExtParams::allow_all()).unwrap();
1452+
1453+
// Try replacing the raw_pkh with a pkh
1454+
let mut map = BTreeMap::new();
1455+
map.insert(hash160, pk);
1456+
let ms_no_raw = ms.substitute_raw_pkh(&map);
1457+
assert_eq!(ms_no_raw.to_string(), format!("pkh({})", pk),);
13901458
}
13911459

13921460
#[test]
@@ -1408,6 +1476,56 @@ mod tests {
14081476
ms.translate_pk(&mut t).unwrap_err();
14091477
}
14101478

1479+
#[test]
1480+
fn duplicate_keys() {
1481+
// You cannot parse a Miniscript that has duplicate keys
1482+
let err = Miniscript::<String, Segwitv0>::from_str("and_v(v:pk(A),pk(A))").unwrap_err();
1483+
assert_eq!(err, Error::AnalysisError(crate::AnalysisError::RepeatedPubkeys));
1484+
1485+
// ...though you can parse one with from_str_insane
1486+
let ok_insane =
1487+
Miniscript::<String, Segwitv0>::from_str_insane("and_v(v:pk(A),pk(A))").unwrap();
1488+
// ...but this cannot be sanity checked.
1489+
assert_eq!(ok_insane.sanity_check().unwrap_err(), crate::AnalysisError::RepeatedPubkeys);
1490+
// ...it can be lifted, though it's unclear whether this is a deliberate
1491+
// choice or just an accident. It seems weird given that duplicate public
1492+
// keys are forbidden in several other places.
1493+
ok_insane.lift().unwrap();
1494+
}
1495+
1496+
#[test]
1497+
fn mixed_timelocks() {
1498+
// You cannot parse a Miniscript that mixes timelocks.
1499+
let err = Miniscript::<String, Segwitv0>::from_str(
1500+
"and_v(v:and_v(v:older(4194304),pk(A)),and_v(v:older(1),pk(B)))",
1501+
)
1502+
.unwrap_err();
1503+
assert_eq!(err, Error::AnalysisError(crate::AnalysisError::HeightTimelockCombination));
1504+
1505+
// Though you can in an or() rather than and()
1506+
let ok_or = Miniscript::<String, Segwitv0>::from_str(
1507+
"or_i(and_v(v:older(4194304),pk(A)),and_v(v:older(1),pk(B)))",
1508+
)
1509+
.unwrap();
1510+
ok_or.sanity_check().unwrap();
1511+
ok_or.lift().unwrap();
1512+
1513+
// ...and you can parse one with from_str_insane
1514+
let ok_insane = Miniscript::<String, Segwitv0>::from_str_insane(
1515+
"and_v(v:and_v(v:older(4194304),pk(A)),and_v(v:older(1),pk(B)))",
1516+
)
1517+
.unwrap();
1518+
// ...but this cannot be sanity checked or lifted
1519+
assert_eq!(
1520+
ok_insane.sanity_check().unwrap_err(),
1521+
crate::AnalysisError::HeightTimelockCombination
1522+
);
1523+
assert_eq!(
1524+
ok_insane.lift().unwrap_err(),
1525+
Error::LiftError(crate::policy::LiftError::HeightTimelockCombination)
1526+
);
1527+
}
1528+
14111529
#[test]
14121530
fn template_timelocks() {
14131531
use crate::{AbsLockTime, RelLockTime};

src/miniscript/types/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,19 +87,19 @@ impl fmt::Display for Error {
8787
),
8888
ErrorKind::SwapNonOne => write!(
8989
f,
90-
"fragment «{}» attempts to use `SWAP` to prefix something
90+
"fragment «{}» attempts to use `SWAP` to prefix something \
9191
which does not take exactly one input",
9292
self.fragment_string,
9393
),
9494
ErrorKind::NonZeroZero => write!(
9595
f,
96-
"fragment «{}» attempts to use use the `j:` wrapper around a
96+
"fragment «{}» attempts to use use the `j:` wrapper around a \
9797
fragment which might be satisfied by an input of size zero",
9898
self.fragment_string,
9999
),
100100
ErrorKind::LeftNotUnit => write!(
101101
f,
102-
"fragment «{}» requires its left child be a unit (outputs
102+
"fragment «{}» requires its left child be a unit (outputs \
103103
exactly 1 given a satisfying input)",
104104
self.fragment_string,
105105
),

0 commit comments

Comments
 (0)