Skip to content

Commit 788a03b

Browse files
Implement tr-compiler-v3 enumeration compiler
1 parent 7593458 commit 788a03b

File tree

3 files changed

+322
-5
lines changed

3 files changed

+322
-5
lines changed

src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -985,7 +985,7 @@ mod prelude {
985985
pub use alloc::{
986986
borrow::{Borrow, Cow, ToOwned},
987987
boxed::Box,
988-
collections::{vec_deque::VecDeque, BTreeMap, BinaryHeap},
988+
collections::{vec_deque::VecDeque, BTreeMap, BTreeSet, BinaryHeap},
989989
rc, slice,
990990
string::{String, ToString},
991991
sync,
@@ -995,7 +995,7 @@ mod prelude {
995995
pub use std::{
996996
borrow::{Borrow, Cow, ToOwned},
997997
boxed::Box,
998-
collections::{vec_deque::VecDeque, BTreeMap, BinaryHeap, HashMap, HashSet},
998+
collections::{vec_deque::VecDeque, BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet},
999999
rc, slice,
10001000
string::{String, ToString},
10011001
sync,

src/policy/concrete.rs

Lines changed: 206 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,14 @@ impl<Pk: MiniscriptKey> Policy<Pk> {
187187
/// B C
188188
///
189189
/// gives the vector [(2/3, A), (1/3 * 3/4, B), (1/3 * 1/4, C)].
190+
///
191+
/// ## Constraints
192+
///
193+
/// Since this splitting might lead to exponential blow-up, we constraint the number of
194+
/// leaf-nodes to [`MAX_COMPILATION_LEAVES`].
190195
#[cfg(feature = "compiler")]
191196
fn to_tapleaf_prob_vec(&self, prob: f64) -> Vec<(f64, Policy<Pk>)> {
192-
match *self {
197+
match self {
193198
Policy::Or(ref subs) => {
194199
let total_odds: usize = subs.iter().map(|(ref k, _)| k).sum();
195200
subs.iter()
@@ -199,17 +204,138 @@ impl<Pk: MiniscriptKey> Policy<Pk> {
199204
.flatten()
200205
.collect::<Vec<_>>()
201206
}
202-
Policy::Threshold(k, ref subs) if k == 1 => {
207+
Policy::Threshold(k, ref subs) if *k == 1 => {
203208
let total_odds = subs.len();
204209
subs.iter()
205210
.map(|policy| policy.to_tapleaf_prob_vec(prob / total_odds as f64))
206211
.flatten()
207212
.collect::<Vec<_>>()
208213
}
209-
ref x => vec![(prob, x.clone())],
214+
x => vec![(prob, x.clone())],
215+
}
216+
}
217+
218+
/// Given a [`Policy`], return a vector of policies whose disjunction is isomorphic to the initial one.
219+
/// This function is supposed to incrementally expand i.e. represent the policy as disjunction over
220+
/// sub-policies output by it. The probability calculations are similar as
221+
/// [to_tapleaf_prob_vec][`Policy::to_tapleaf_prob_vec`]
222+
#[cfg(feature = "compiler")]
223+
fn enumerate_pol(&self, prob: f64) -> Vec<(f64, Policy<Pk>)> {
224+
match self {
225+
Policy::Or(subs) => {
226+
let total_odds = subs.iter().fold(0, |acc, x| acc + x.0);
227+
subs.iter()
228+
.map(|(odds, pol)| (prob * *odds as f64 / total_odds as f64, pol.clone()))
229+
.collect::<Vec<_>>()
230+
}
231+
Policy::Threshold(k, subs) if *k == 1 => {
232+
let total_odds = subs.len();
233+
subs.iter()
234+
.map(|pol| (prob / total_odds as f64, pol.clone()))
235+
.collect::<Vec<_>>()
236+
}
237+
Policy::Threshold(k, subs) if *k != subs.len() => generate_combination(subs, prob, *k),
238+
pol => vec![(prob, pol.clone())],
210239
}
211240
}
212241

242+
/// Generates a root-level disjunctive tree over the given policy tree, by using fixed-point
243+
/// algorithm to enumerate the disjunctions until exhaustive root-level enumeration or limits
244+
/// exceed.
245+
/// For a given [policy][`Policy`], we maintain an [ordered set][`BTreeSet`] of `(prob, policy)`
246+
/// (ordered by probability) to maintain the list of enumerated sub-policies whose disjunction
247+
/// is isomorphic to initial policy (*invariant*).
248+
#[cfg(feature = "compiler")]
249+
fn enumerate_policy_tree(&self, prob: f64) -> Vec<(f64, Self)> {
250+
let mut tapleaf_prob_vec = BTreeSet::<(Reverse<OrdF64>, Self)>::new();
251+
// Store probability corresponding to policy in the enumerated tree. This is required since
252+
// owing to the current [policy element enumeration algorithm][`Policy::enumerate_pol`],
253+
// two passes of the algorithm might result in same sub-policy showing up. Currently, we
254+
// merge the nodes by adding up the corresponding probabilities for the same policy.
255+
let mut pol_prob_map = HashMap::<Self, OrdF64>::new();
256+
257+
tapleaf_prob_vec.insert((Reverse(OrdF64(prob)), self.clone()));
258+
pol_prob_map.insert(self.clone(), OrdF64(prob));
259+
260+
// Since we know that policy enumeration *must* result in increase in total number of nodes,
261+
// we can maintain the length of the ordered set to check if the
262+
// [enumeration pass][`Policy::enumerate_pol`] results in further policy split or not.
263+
let mut prev_len = 0usize;
264+
// This is required since we merge some corresponding policy nodes, so we can explicitly
265+
// store the variables
266+
let mut enum_len = tapleaf_prob_vec.len();
267+
268+
let mut ret: Vec<(f64, Self)> = vec![];
269+
270+
// Stopping condition: When NONE of the inputs can be further enumerated.
271+
'outer: loop {
272+
//--- FIND a plausible node ---
273+
274+
let mut prob: Reverse<OrdF64> = Reverse(OrdF64(0.0));
275+
let mut curr_policy: &Self = &Policy::Unsatisfiable;
276+
let mut curr_pol_replace_vec: Vec<(f64, Self)> = vec![(prob.0 .0, curr_policy.clone())];
277+
278+
// The nodes which can't be enumerated further are directly appended to ret and removed
279+
// from the ordered set.
280+
let mut to_del: Vec<(f64, Self)> = vec![];
281+
for (i, (p, pol)) in tapleaf_prob_vec.iter().enumerate() {
282+
curr_pol_replace_vec = pol.enumerate_pol(p.0 .0);
283+
enum_len += curr_pol_replace_vec.len() - 1; // A disjunctive node should have seperated this into more nodes
284+
if prev_len < enum_len {
285+
// Plausible node found
286+
prob = *p;
287+
curr_policy = pol;
288+
break;
289+
} else if i == tapleaf_prob_vec.len() - 1 {
290+
// No enumerable node found i.e. STOP
291+
// Move all the elements to final return set
292+
ret.append(&mut to_del);
293+
ret.push((p.0 .0, pol.clone()));
294+
break 'outer;
295+
} else {
296+
// Either node is enumerable, or we have
297+
// Mark all non-enumerable nodes to remove
298+
to_del.push((p.0 .0, pol.clone()));
299+
}
300+
}
301+
302+
// --- Sanity Checks ---
303+
if enum_len > MAX_COMPILATION_LEAVES || curr_policy == &Policy::Unsatisfiable {
304+
break;
305+
}
306+
307+
// If total number of nodes are in limits, we remove the current node and replace it
308+
// with children nodes
309+
310+
// Remove current node
311+
tapleaf_prob_vec.remove(&(prob, curr_policy.clone()));
312+
// Remove marked nodes (minor optimization)
313+
for (p, pol) in to_del {
314+
tapleaf_prob_vec.remove(&(Reverse(OrdF64(p)), pol.clone()));
315+
ret.push((p, pol.clone()));
316+
}
317+
318+
// Append node if not previously exists, else update the respective probability
319+
for (p, policy) in curr_pol_replace_vec {
320+
match pol_prob_map.get(&policy) {
321+
Some(prev_prob) => {
322+
tapleaf_prob_vec.remove(&(Reverse(*prev_prob), policy.clone()));
323+
tapleaf_prob_vec.insert((Reverse(OrdF64(prev_prob.0 + p)), policy.clone()));
324+
pol_prob_map.insert(policy.clone(), OrdF64(prev_prob.0 + p));
325+
}
326+
None => {
327+
tapleaf_prob_vec.insert((Reverse(OrdF64(p)), policy.clone()));
328+
pol_prob_map.insert(policy.clone(), OrdF64(p));
329+
}
330+
}
331+
}
332+
// --- Update --- total sub-policies count (considering no merging of nodes)
333+
prev_len = enum_len;
334+
}
335+
336+
ret
337+
}
338+
213339
/// Extract the internal_key from policy tree.
214340
#[cfg(feature = "compiler")]
215341
fn extract_key(self, unspendable_key: Option<Pk>) -> Result<(Pk, Policy<Pk>), Error> {
@@ -306,6 +432,56 @@ impl<Pk: MiniscriptKey> Policy<Pk> {
306432
}
307433
}
308434

435+
/// Compile the [`Policy`] into a [`Tr`][`Descriptor::Tr`] Descriptor, with policy-enumeration
436+
/// by [`Policy::enumerate_policy_tree`].
437+
///
438+
/// ### TapTree compilation
439+
///
440+
/// The policy tree constructed by root-level disjunctions over [`Or`][`Policy::Or`] and
441+
/// [`Thresh`][`Policy::Threshold`](k, ..n..) which is flattened into a vector (with respective
442+
/// probabilities derived from odds) of policies.
443+
/// For example, the policy `thresh(1,or(pk(A),pk(B)),and(or(pk(C),pk(D)),pk(E)))` gives the vector
444+
/// `[pk(A),pk(B),and(or(pk(C),pk(D)),pk(E)))]`.
445+
///
446+
/// ### Policy enumeration
447+
///
448+
/// Refer to [`Policy::enumerate_policy_tree`] for the current strategy implemented.
449+
#[cfg(feature = "compiler")]
450+
pub fn compile_tr_private_experimental(
451+
&self,
452+
unspendable_key: Option<Pk>,
453+
) -> Result<Descriptor<Pk>, Error> {
454+
self.is_valid()?; // Check for validity
455+
match self.is_safe_nonmalleable() {
456+
(false, _) => Err(Error::from(CompilerError::TopLevelNonSafe)),
457+
(_, false) => Err(Error::from(
458+
CompilerError::ImpossibleNonMalleableCompilation,
459+
)),
460+
_ => {
461+
let (internal_key, policy) = self.clone().extract_key(unspendable_key)?;
462+
let tree = Descriptor::new_tr(
463+
internal_key,
464+
match policy {
465+
Policy::Trivial => None,
466+
policy => {
467+
let leaf_compilations: Vec<_> = policy
468+
.enumerate_policy_tree(1.0)
469+
.into_iter()
470+
.filter(|x| x.1 != Policy::Unsatisfiable)
471+
.map(|(prob, ref pol)| {
472+
(OrdF64(prob), compiler::best_compilation(pol).unwrap())
473+
})
474+
.collect();
475+
let taptree = with_huffman_tree::<Pk>(leaf_compilations).unwrap();
476+
Some(taptree)
477+
}
478+
},
479+
)?;
480+
Ok(tree)
481+
}
482+
}
483+
}
484+
309485
/// Compile the [`Policy`] into desc_ctx [`Descriptor`]
310486
///
311487
/// In case of [Tr][`DescriptorCtx::Tr`], `internal_key` is used for the Taproot comilation when
@@ -944,3 +1120,30 @@ fn with_huffman_tree<Pk: MiniscriptKey>(
9441120
.1;
9451121
Ok(node)
9461122
}
1123+
1124+
/// Enumerate a [Thresh][`Policy::Threshold`](k, ..n..) into `n` different thresh.
1125+
///
1126+
/// ## Strategy
1127+
///
1128+
/// `thresh(k, x_1...x_n) := thresh(1, thresh(k, x_2...x_n), thresh(k, x_1x_3...x_n), ...., thresh(k, x_1...x_{n-1}))`
1129+
/// by the simple argument that choosing `k` conditions from `n` available conditions might not contain
1130+
/// any one of the conditions exclusively.
1131+
#[cfg(feature = "compiler")]
1132+
fn generate_combination<Pk: MiniscriptKey>(
1133+
policy_vec: &Vec<Policy<Pk>>,
1134+
prob: f64,
1135+
k: usize,
1136+
) -> Vec<(f64, Policy<Pk>)> {
1137+
debug_assert!(k <= policy_vec.len());
1138+
1139+
let mut ret: Vec<(f64, Policy<Pk>)> = vec![];
1140+
for i in 0..policy_vec.len() {
1141+
let mut policies: Vec<Policy<Pk>> = policy_vec.clone();
1142+
policies.remove(i);
1143+
ret.push((
1144+
prob / policy_vec.len() as f64,
1145+
Policy::<Pk>::Threshold(k, policies),
1146+
));
1147+
}
1148+
ret
1149+
}

src/policy/mod.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,4 +487,118 @@ mod tests {
487487
assert_eq!(descriptor, expected_descriptor);
488488
}
489489
}
490+
491+
#[test]
492+
#[cfg(feature = "compiler")]
493+
fn private_enumerative_compiler() {
494+
// Trivial single-node compilation
495+
let unspendable_key: String = "UNSPENDABLE".to_string();
496+
{
497+
let policy: Concrete<String> = policy_str!("thresh(2,pk(A),pk(B),pk(C),pk(D))");
498+
let descriptor = policy
499+
.compile_tr_private_experimental(Some(unspendable_key.clone()))
500+
.unwrap();
501+
let ms_compilations: [Miniscript<String, Tap>; 6] = [
502+
ms_str!("multi_a(2,A,C)"),
503+
ms_str!("multi_a(2,A,B)"),
504+
ms_str!("multi_a(2,C,D)"),
505+
ms_str!("multi_a(2,B,D)"),
506+
ms_str!("multi_a(2,B,C)"),
507+
ms_str!("multi_a(2,A,D)"),
508+
];
509+
510+
let tree = TapTree::Tree(
511+
Arc::new(TapTree::Tree(
512+
Arc::new(TapTree::Leaf(Arc::new(ms_compilations[0].clone()))),
513+
Arc::new(TapTree::Leaf(Arc::new(ms_compilations[1].clone()))),
514+
)),
515+
Arc::new(TapTree::Tree(
516+
Arc::new(TapTree::Tree(
517+
Arc::new(TapTree::Leaf(Arc::new(ms_compilations[2].clone()))),
518+
Arc::new(TapTree::Leaf(Arc::new(ms_compilations[3].clone()))),
519+
)),
520+
Arc::new(TapTree::Tree(
521+
Arc::new(TapTree::Leaf(Arc::new(ms_compilations[4].clone()))),
522+
Arc::new(TapTree::Leaf(Arc::new(ms_compilations[5].clone()))),
523+
)),
524+
)),
525+
);
526+
527+
let expected_descriptor =
528+
Descriptor::new_tr(unspendable_key.clone(), Some(tree)).unwrap();
529+
assert_eq!(descriptor, expected_descriptor);
530+
}
531+
532+
// Trivial multi-node compilation
533+
{
534+
let policy: Concrete<String> =
535+
policy_str!("or(or(and(pk(A),pk(B)),and(pk(E),pk(F))),and(pk(C),pk(D)))");
536+
let descriptor = policy
537+
.compile_tr_private_experimental(Some(unspendable_key.clone()))
538+
.unwrap();
539+
540+
let ms_compilations: [Miniscript<String, Tap>; 3] = [
541+
ms_str!("and_v(v:pk(A),pk(B))"),
542+
ms_str!("and_v(v:pk(E),pk(F))"),
543+
ms_str!("and_v(v:pk(C),pk(D))"),
544+
];
545+
let tree = TapTree::Tree(
546+
Arc::new(TapTree::Leaf(Arc::new(ms_compilations[2].clone()))),
547+
Arc::new(TapTree::Tree(
548+
Arc::new(TapTree::Leaf(Arc::new(ms_compilations[1].clone()))),
549+
Arc::new(TapTree::Leaf(Arc::new(ms_compilations[0].clone()))),
550+
)),
551+
);
552+
let expected_descriptor =
553+
Descriptor::new_tr(unspendable_key.clone(), Some(tree)).unwrap();
554+
assert_eq!(descriptor, expected_descriptor);
555+
}
556+
557+
{
558+
let policy: Concrete<String> = policy_str!(
559+
"or(or(and(pk(A),pk(B)),and(pk(E),pk(F))),thresh(1,and(pk(C),pk(D)),and(pk(G),pk(H))))"
560+
);
561+
let descriptor = policy
562+
.compile_tr_private_experimental(Some(unspendable_key.clone()))
563+
.unwrap();
564+
565+
let ms_compilations: [Miniscript<String, Tap>; 4] = [
566+
ms_str!("and_v(v:pk(A),pk(B))"),
567+
ms_str!("and_v(v:pk(C),pk(D))"),
568+
ms_str!("and_v(v:pk(E),pk(F))"),
569+
ms_str!("and_v(v:pk(G),pk(H))"),
570+
];
571+
let tree = TapTree::Tree(
572+
Arc::new(TapTree::Tree(
573+
Arc::new(TapTree::Leaf(Arc::new(ms_compilations[3].clone()))),
574+
Arc::new(TapTree::Leaf(Arc::new(ms_compilations[2].clone()))),
575+
)),
576+
Arc::new(TapTree::Tree(
577+
Arc::new(TapTree::Leaf(Arc::new(ms_compilations[1].clone()))),
578+
Arc::new(TapTree::Leaf(Arc::new(ms_compilations[0].clone()))),
579+
)),
580+
);
581+
let expected_descriptor =
582+
Descriptor::new_tr(unspendable_key.clone(), Some(tree)).unwrap();
583+
assert_eq!(descriptor, expected_descriptor);
584+
}
585+
586+
{
587+
// Invalid policy compilation (Duplicate PubKeys)
588+
let policy: Concrete<String> = policy_str!("or(and(pk(A),pk(B)),and(pk(A),pk(D)))");
589+
let descriptor = policy.compile_tr_private_experimental(Some(unspendable_key.clone()));
590+
591+
assert_eq!(
592+
descriptor.unwrap_err().to_string(),
593+
"Policy contains duplicate keys"
594+
);
595+
}
596+
597+
{
598+
let policy: Concrete<String> = policy_str!("thresh(51,pk(A),pk(B),pk(C),pk(D),pk(E),pk(F),pk(G),pk(H),pk(I),pk(J),pk(K),pk(L),pk(M),pk(N),pk(O),pk(P),pk(Q),pk(R),pk(S),pk(T),pk(U),pk(V),pk(W),pk(X),pk(Y),pk(Z),pk(AA),pk(BB),pk(CC),pk(DD),pk(EE),pk(FF),pk(GG),pk(HH),pk(II),pk(JJ),pk(KK),pk(LL),pk(MM),pk(NN),pk(OO),pk(PP),pk(QQ),pk(RR),pk(SS),pk(TT),pk(UU),pk(VV),pk(WW),pk(XX),pk(YY),pk(ZZ))");
599+
let descriptor = dbg!(policy
600+
.compile_tr_private_experimental(Some(unspendable_key.clone()))
601+
.unwrap());
602+
}
603+
}
490604
}

0 commit comments

Comments
 (0)