Skip to content

Commit 3d473c9

Browse files
Implement tr-compiler-v3 enumeration compiler
1 parent 625077e commit 3d473c9

File tree

4 files changed

+332
-13
lines changed

4 files changed

+332
-13
lines changed

src/lib.rs

+2-2
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/miniscript/limits.rs

+3
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,6 @@ pub const MAX_BLOCK_WEIGHT: usize = 4000000;
5050
/// Maximum pubkeys as arguments to CHECKMULTISIG
5151
// https://github.com/bitcoin/bitcoin/blob/6acda4b00b3fc1bfac02f5de590e1a5386cbc779/src/script/script.h#L30
5252
pub const MAX_PUBKEYS_PER_MULTISIG: usize = 20;
53+
54+
/// Maximum TapLeafs allowed in a compiled TapTree
55+
pub const MAX_TAPLEAFS_PER_TAPTREE: usize = 10_000;

src/policy/concrete.rs

+213-11
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use bitcoin::hashes::{hash160, ripemd160};
2424
#[cfg(feature = "compiler")]
2525
use {
2626
crate::descriptor::TapTree,
27+
crate::miniscript::limits::MAX_TAPLEAFS_PER_TAPTREE,
2728
crate::miniscript::ScriptContext,
2829
crate::policy::compiler::CompilerError,
2930
crate::policy::compiler::OrdF64,
@@ -186,9 +187,14 @@ impl<Pk: MiniscriptKey> Policy<Pk> {
186187
/// B C
187188
///
188189
/// 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_TAPLEAFS_PER_TAPTREE`].
189195
#[cfg(feature = "compiler")]
190196
fn to_tapleaf_prob_vec(&self, prob: f64) -> Vec<(f64, Policy<Pk>)> {
191-
match *self {
197+
match self {
192198
Policy::Or(ref subs) => {
193199
let total_odds: usize = subs.iter().map(|(ref k, _)| k).sum();
194200
subs.iter()
@@ -198,17 +204,141 @@ impl<Pk: MiniscriptKey> Policy<Pk> {
198204
.flatten()
199205
.collect::<Vec<_>>()
200206
}
201-
Policy::Threshold(k, ref subs) if k == 1 => {
207+
Policy::Threshold(k, ref subs) if *k == 1 => {
202208
let total_odds = subs.len();
203209
subs.iter()
204210
.map(|policy| policy.to_tapleaf_prob_vec(prob / total_odds as f64))
205211
.flatten()
206212
.collect::<Vec<_>>()
207213
}
208-
ref x => vec![(prob, x.clone())],
214+
// Instead of the fixed-point algorithm, consider direct k-choose-n split if limits
215+
// don't exceed
216+
x => vec![(prob, x.clone())],
217+
}
218+
}
219+
220+
/// Given a [`Policy`], return a vector of policies whose disjunction is isomorphic to the initial one.
221+
/// This function is supposed to incrementally expand i.e. represent the policy as disjunction over
222+
/// sub-policies output by it. The probability calculations are similar as
223+
/// [to_tapleaf_prob_vec][`Policy::to_tapleaf_prob_vec`]
224+
#[cfg(feature = "compiler")]
225+
fn enumerate_pol(&self, prob: f64) -> Vec<(f64, Policy<Pk>)> {
226+
match self {
227+
Policy::Or(subs) => {
228+
let total_odds = subs.iter().fold(0, |acc, x| acc + x.0);
229+
subs.iter()
230+
.map(|(odds, pol)| (prob * *odds as f64 / total_odds as f64, pol.clone()))
231+
.collect::<Vec<_>>()
232+
}
233+
Policy::Threshold(k, subs) if *k == 1 => {
234+
let total_odds = subs.len();
235+
subs.iter()
236+
.map(|pol| (prob / total_odds as f64, pol.clone()))
237+
.collect::<Vec<_>>()
238+
}
239+
Policy::Threshold(k, subs) if *k != subs.len() => generate_combination(subs, prob, *k),
240+
pol => vec![(prob, pol.clone())],
209241
}
210242
}
211243

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

438+
/// Compile the [`Policy`] into a [`Tr`][`Descriptor::Tr`] Descriptor, with policy-enumeration
439+
/// by [`Policy::enumerate_policy_tree`].
440+
///
441+
/// ### TapTree compilation
442+
///
443+
/// The policy tree constructed by root-level disjunctions over [`Or`][`Policy::Or`] and
444+
/// [`Thresh`][`Policy::Threshold`](k, ..n..) which is flattened into a vector (with respective
445+
/// probabilities derived from odds) of policies.
446+
/// For example, the policy `thresh(1,or(pk(A),pk(B)),and(or(pk(C),pk(D)),pk(E)))` gives the vector
447+
/// `[pk(A),pk(B),and(or(pk(C),pk(D)),pk(E)))]`.
448+
///
449+
/// ### Policy enumeration
450+
///
451+
/// Refer to [`Policy::enumerate_policy_tree`] for the current strategy implemented.
452+
#[cfg(feature = "compiler")]
453+
pub fn compile_tr_private_experimental(
454+
&self,
455+
unspendable_key: Option<Pk>,
456+
) -> Result<Descriptor<Pk>, Error> {
457+
self.is_valid()?; // Check for validity
458+
match self.is_safe_nonmalleable() {
459+
(false, _) => Err(Error::from(CompilerError::TopLevelNonSafe)),
460+
(_, false) => Err(Error::from(
461+
CompilerError::ImpossibleNonMalleableCompilation,
462+
)),
463+
_ => {
464+
let (internal_key, policy) = self.clone().extract_key(unspendable_key)?;
465+
let tree = Descriptor::new_tr(
466+
internal_key,
467+
match policy {
468+
Policy::Trivial => None,
469+
policy => {
470+
let leaf_compilations: Vec<_> = policy
471+
.enumerate_policy_tree(1.0)
472+
.into_iter()
473+
.filter(|x| x.1 != Policy::Unsatisfiable)
474+
.map(|(prob, ref pol)| {
475+
(OrdF64(prob), compiler::best_compilation(pol).unwrap())
476+
})
477+
.collect();
478+
let taptree = with_huffman_tree::<Pk>(leaf_compilations).unwrap();
479+
Some(taptree)
480+
}
481+
},
482+
)?;
483+
Ok(tree)
484+
}
485+
}
486+
}
487+
308488
/// Compile the [`Policy`] into desc_ctx [`Descriptor`]
309489
///
310490
/// In case of [Tr][`DescriptorCtx::Tr`], `internal_key` is used for the Taproot comilation when
@@ -508,15 +688,17 @@ impl<Pk: MiniscriptKey> Policy<Pk> {
508688
fn num_tap_leaves(&self) -> usize {
509689
match self {
510690
Policy::Or(subs) => subs.iter().map(|(_prob, pol)| pol.num_tap_leaves()).sum(),
511-
Policy::Threshold(k, subs) if *k == 1 => combination(subs.len(), *k),
691+
Policy::Threshold(k, subs) if *k == 1 => {
692+
subs.iter().map(|pol| pol.num_tap_leaves()).sum()
693+
}
512694
_ => 1,
513695
}
514696
}
515697

516698
/// Check on the number of TapLeaves
517699
#[cfg(feature = "compiler")]
518-
fn check_tapleaf(&self) -> Result<(), Error> {
519-
if self.num_tap_leaves() > 100_000 {
700+
fn check_tapleaf(&self) -> Result<(), Error> {
701+
if self.num_tap_leaves() > MAX_TAPLEAFS_PER_TAPTREE {
520702
return Err(errstr("Too many Tapleaves"));
521703
}
522704
Ok(())
@@ -939,12 +1121,32 @@ fn with_huffman_tree<Pk: MiniscriptKey>(
9391121
.pop()
9401122
.expect("huffman tree algorithm is broken")
9411123
.1;
1124+
Ok(node)
1125+
}
9421126

1127+
/// Enumerate a [Thresh][`Policy::Threshold`](k, ..n..) into `n` different thresh.
1128+
///
1129+
/// ## Strategy
1130+
///
1131+
/// `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}))`
1132+
/// by the simple argument that choosing `k` conditions from `n` available conditions might not contain
1133+
/// any one of the conditions exclusively.
9431134
#[cfg(feature = "compiler")]
944-
fn combination(n: usize, k: usize) -> usize {
945-
if k == 0 {
946-
1
947-
} else {
948-
n * combination(n - 1, k - 1) / k
1135+
fn generate_combination<Pk: MiniscriptKey>(
1136+
policy_vec: &Vec<Policy<Pk>>,
1137+
prob: f64,
1138+
k: usize,
1139+
) -> Vec<(f64, Policy<Pk>)> {
1140+
debug_assert!(k <= policy_vec.len());
1141+
1142+
let mut ret: Vec<(f64, Policy<Pk>)> = vec![];
1143+
for i in 0..policy_vec.len() {
1144+
let mut policies: Vec<Policy<Pk>> = policy_vec.clone();
1145+
policies.remove(i);
1146+
ret.push((
1147+
prob / policy_vec.len() as f64,
1148+
Policy::<Pk>::Threshold(k, policies),
1149+
));
9491150
}
1151+
ret
9501152
}

0 commit comments

Comments
 (0)