Skip to content

Commit 227da1a

Browse files
Add taproot compiler default version
The `compile_tr` method added here uses the heuristic as specified in the docs and provides better cost guarantees than the `compile_tr_private` method.
1 parent c9c73ef commit 227da1a

File tree

3 files changed

+278
-0
lines changed

3 files changed

+278
-0
lines changed

src/policy/compiler.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,6 +1137,20 @@ pub fn best_compilation<Pk: MiniscriptKey, Ctx: ScriptContext>(
11371137
}
11381138
}
11391139

1140+
/// Obtain the best compilation of for p=1.0 and q=0, along with the satisfaction cost for the script
1141+
pub(crate) fn best_compilation_sat<Pk: MiniscriptKey, Ctx: ScriptContext>(
1142+
policy: &Concrete<Pk>,
1143+
) -> Result<(Arc<Miniscript<Pk, Ctx>>, f64), CompilerError> {
1144+
let mut policy_cache = PolicyCache::<Pk, Ctx>::new();
1145+
let x: AstElemExt<Pk, Ctx> = best_t(&mut policy_cache, policy, 1.0, None)?;
1146+
if !x.ms.ty.mall.safe {
1147+
Err(CompilerError::TopLevelNonSafe)
1148+
} else if !x.ms.ty.mall.non_malleable {
1149+
Err(CompilerError::ImpossibleNonMalleableCompilation)
1150+
} else {
1151+
Ok((x.ms, x.comp_ext_data.sat_cost))
1152+
}
1153+
}
11401154
/// Obtain the best B expression with given sat and dissat
11411155
fn best_t<Pk, Ctx>(
11421156
policy_cache: &mut PolicyCache<Pk, Ctx>,

src/policy/concrete.rs

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use {
3131
crate::Miniscript,
3232
crate::Tap,
3333
std::cmp::Reverse,
34+
std::collections::BTreeMap,
3435
std::collections::{BinaryHeap, HashMap},
3536
std::sync::Arc,
3637
};
@@ -41,6 +42,14 @@ use crate::miniscript::limits::{LOCKTIME_THRESHOLD, SEQUENCE_LOCKTIME_TYPE_FLAG}
4142
use crate::miniscript::types::extra_props::TimelockInfo;
4243
use crate::{errstr, Error, ForEach, ForEachKey, MiniscriptKey};
4344

45+
/// [`TapTree`] -> ([`Policy`], satisfaction cost) cache
46+
#[cfg(feature = "compiler")]
47+
type PolicyTapCache<Pk> = BTreeMap<TapTree<Pk>, (Policy<Pk>, f64)>;
48+
49+
/// [`Miniscript`] -> leaf probability in policy cache
50+
#[cfg(feature = "compiler")]
51+
type MsTapCache<Pk> = BTreeMap<TapTree<Pk>, f64>;
52+
4453
/// Concrete policy which corresponds directly to a Miniscript structure,
4554
/// and whose disjunctions are annotated with satisfaction probabilities
4655
/// to assist the compiler
@@ -283,6 +292,69 @@ impl<Pk: MiniscriptKey> Policy<Pk> {
283292
}
284293
}
285294

295+
/// Compile [`Policy`] into a [`TapTree Descriptor`][`Descriptor::Tr`]
296+
///
297+
///
298+
/// This follows the heuristic as described in [`with_huffman_tree_eff`]
299+
#[cfg(feature = "compiler")]
300+
pub fn compile_tr(&self, unspendable_key: Option<Pk>) -> Result<Descriptor<Pk>, Error> {
301+
self.is_valid()?; // Check for validity
302+
match self.is_safe_nonmalleable() {
303+
(false, _) => Err(Error::from(CompilerError::TopLevelNonSafe)),
304+
(_, false) => Err(Error::from(
305+
CompilerError::ImpossibleNonMalleableCompilation,
306+
)),
307+
_ => {
308+
let (internal_key, policy) = self.clone().extract_key(unspendable_key)?;
309+
let tree = Descriptor::new_tr(
310+
internal_key,
311+
match policy {
312+
Policy::Trivial => None,
313+
policy => {
314+
let mut policy_cache = PolicyTapCache::<Pk>::new();
315+
let mut ms_cache = MsTapCache::<Pk>::new();
316+
// Obtain the policy compilations and populate the respective caches for
317+
// creating the huffman tree later on
318+
let vec_policies: Vec<_> = policy.to_tapleaf_prob_vec(1.0);
319+
let mut leaf_compilations: Vec<Arc<Miniscript<Pk, Tap>>> = vec![];
320+
for (prob, pol) in vec_policies {
321+
// policy corresponding to the key (replaced by unsatisfiable) is skipped
322+
if pol == Policy::Unsatisfiable {
323+
continue;
324+
}
325+
let compilation = compiler::best_compilation_sat::<Pk, Tap>(&pol)?;
326+
compilation.0.sanity_check()?;
327+
let leaf_comp = TapTree::Leaf(compilation.0.clone());
328+
policy_cache.insert(
329+
TapTree::Leaf(Arc::clone(&compilation.0)),
330+
(pol.clone(), compilation.1), // (policy, sat_cost)
331+
);
332+
// In case we hit duplication compilations for sub-policies, we add
333+
// their respective probabilities without pushing the node back again.
334+
match ms_cache.get(&leaf_comp) {
335+
Some(p) => {
336+
ms_cache.insert(leaf_comp, p + prob);
337+
}
338+
None => {
339+
ms_cache.insert(leaf_comp, prob);
340+
leaf_compilations.push(compilation.0);
341+
}
342+
};
343+
}
344+
let taptree = with_huffman_tree_eff(
345+
leaf_compilations,
346+
&mut policy_cache,
347+
&mut ms_cache,
348+
)?;
349+
Some(taptree)
350+
}
351+
},
352+
)?;
353+
Ok(tree)
354+
}
355+
}
356+
}
357+
286358
/// Compile the descriptor into an optimized `Miniscript` representation
287359
#[cfg(feature = "compiler")]
288360
pub fn compile<Ctx: ScriptContext>(&self) -> Result<Miniscript<Pk, Ctx>, CompilerError> {
@@ -825,6 +897,50 @@ where
825897
}
826898
}
827899

900+
/// Average satisfaction cost for [`TapTree`] with the leaf [`Miniscript`] nodes at some depth having
901+
/// probabilities corresponding to the (sub)policies they're compiled from.
902+
///
903+
/// Average satisfaction cost for [`TapTree`] over script-spend paths is probability times
904+
/// the size of control block at depth + the script size.
905+
#[cfg(feature = "compiler")]
906+
fn at_depth_taptree_cost<Pk: MiniscriptKey>(
907+
tr: &TapTree<Pk>,
908+
ms_cache: &MsTapCache<Pk>,
909+
policy_cache: &PolicyTapCache<Pk>,
910+
depth: u32,
911+
) -> f64 {
912+
match *tr {
913+
TapTree::Tree(ref l, ref r) => {
914+
at_depth_taptree_cost(l, ms_cache, policy_cache, depth + 1)
915+
+ at_depth_taptree_cost(r, ms_cache, policy_cache, depth + 1)
916+
}
917+
TapTree::Leaf(ref ms) => {
918+
let prob = ms_cache
919+
.get(&TapTree::Leaf(Arc::clone(ms)))
920+
.expect("Probability should exist for the given ms");
921+
let sat_cost = policy_cache
922+
.get(&TapTree::Leaf(Arc::clone(ms)))
923+
.expect("Cost should exist for the given ms")
924+
.1;
925+
prob * (ms.script_size() as f64 + sat_cost + 32.0 * depth as f64)
926+
}
927+
}
928+
}
929+
930+
/// Average net satisfaction cost for [`TapTree`] with the leaf [`Miniscript`] nodes having
931+
/// probabilities corresponding to the (sub)policies they're compiled from.
932+
///
933+
/// Average satisfaction cost for [`TapTree`] over script-spend paths is probability times
934+
/// the size of control block + the script size.
935+
#[cfg(feature = "compiler")]
936+
fn taptree_cost<Pk: MiniscriptKey>(
937+
tr: &TapTree<Pk>,
938+
ms_cache: &MsTapCache<Pk>,
939+
policy_cache: &PolicyTapCache<Pk>,
940+
) -> f64 {
941+
at_depth_taptree_cost(tr, ms_cache, policy_cache, 0)
942+
}
943+
828944
/// Create a Huffman Tree from compiled [Miniscript] nodes
829945
#[cfg(feature = "compiler")]
830946
fn with_huffman_tree<Pk: MiniscriptKey>(
@@ -855,3 +971,122 @@ fn with_huffman_tree<Pk: MiniscriptKey>(
855971
.1;
856972
Ok(node)
857973
}
974+
975+
/// Create a [`TapTree`] from the a list of [`Miniscript`]s having corresponding satisfaction
976+
/// cost and probability.
977+
///
978+
/// Given that satisfaction probability and cost for each script is known, constructing the
979+
/// [`TapTree`] as a huffman tree over the net cost (as defined in [`taptree_cost`]) is
980+
/// the optimal one.
981+
/// For finding the optimal policy to taptree compilation, we are required to search
982+
/// exhaustively over all policies which have the same leaf policies. Owing to the exponential
983+
/// blow-up for such a method, we use a heuristic where we augment the merge to check if the
984+
/// compilation of a new (sub)policy into a [`TapTree::Leaf`] with the policy corresponding to
985+
/// the nodes as children is better than [`TapTree::Tree`] with the nodes as children.
986+
///
987+
/// # Assumption
988+
///
989+
/// We have no two duplicate policies/ compilations in the given list.
990+
/// In any other case, we'd need to re-engineer the node-merging algorithm here to gracefully
991+
/// handle duplicate intermediate policies/ miniscript compilations by dis-disambiguating them.
992+
#[cfg(feature = "compiler")]
993+
fn with_huffman_tree_eff<Pk: MiniscriptKey>(
994+
ms: Vec<Arc<Miniscript<Pk, Tap>>>,
995+
policy_cache: &mut PolicyTapCache<Pk>,
996+
ms_cache: &mut MsTapCache<Pk>,
997+
) -> Result<TapTree<Pk>, Error> {
998+
let mut node_weights = BinaryHeap::<(Reverse<OrdF64>, OrdF64, TapTree<Pk>)>::new(); // (cost, branch_prob, tree)
999+
// Populate the heap with each `ms` as a TapLeaf, and the respective cost fields
1000+
for script in ms {
1001+
let wt = OrdF64(taptree_cost(
1002+
&TapTree::Leaf(Arc::clone(&script)),
1003+
ms_cache,
1004+
policy_cache,
1005+
));
1006+
let prob = OrdF64(
1007+
*ms_cache
1008+
.get(&TapTree::Leaf(Arc::clone(&script)))
1009+
.expect("Probability should exist for the given ms"),
1010+
);
1011+
node_weights.push((Reverse(wt), prob, TapTree::Leaf(Arc::clone(&script))));
1012+
}
1013+
if node_weights.is_empty() {
1014+
return Err(errstr("Empty Miniscript compilation"));
1015+
}
1016+
while node_weights.len() > 1 {
1017+
// Obtain the two least-weighted nodes from the heap for merging
1018+
let (_prev_cost1, p1, ms1) = node_weights.pop().expect("len must atleast be two");
1019+
let (_prev_cost2, p2, ms2) = node_weights.pop().expect("len must atleast be two");
1020+
1021+
// Retrieve the respective policies
1022+
let (left_pol, _c1) = policy_cache
1023+
.get(&ms1)
1024+
.ok_or_else(|| errstr("No corresponding policy found"))?
1025+
.clone();
1026+
1027+
let (right_pol, _c2) = policy_cache
1028+
.get(&ms2)
1029+
.ok_or_else(|| errstr("No corresponding policy found"))?
1030+
.clone();
1031+
1032+
// Create a parent policy with the respective node TapTrees as children (with odds
1033+
// weighted approximately in ratio to their probabilities)
1034+
let parent_policy = Policy::Or(vec![
1035+
((p1.0 * 1e4).round() as usize, left_pol),
1036+
((p2.0 * 1e4).round() as usize, right_pol),
1037+
]);
1038+
1039+
// Obtain compilation for the parent policy
1040+
let (parent_compilation, parent_sat_cost) =
1041+
compiler::best_compilation_sat::<Pk, Tap>(&parent_policy)?;
1042+
parent_compilation.sanity_check()?;
1043+
1044+
// Probability of the parent node being satisfied equals the probability of either
1045+
// nodes to be satisfied. Since we weight the odds appropriately, the children nodes
1046+
// still have approximately the same probabilities
1047+
let p = p1.0 + p2.0;
1048+
// Inserting parent policy's weights (sat_cost and probability) for later usage, assuming
1049+
// we don't hit duplicate policy/ compilation here.
1050+
ms_cache.insert(TapTree::Leaf(Arc::clone(&parent_compilation)), p);
1051+
policy_cache.insert(
1052+
TapTree::Leaf(Arc::clone(&parent_compilation)),
1053+
(parent_policy.clone(), parent_sat_cost),
1054+
);
1055+
1056+
let parent_cost = OrdF64(taptree_cost(
1057+
&TapTree::Leaf(Arc::clone(&parent_compilation)),
1058+
ms_cache,
1059+
policy_cache,
1060+
));
1061+
let children_cost = OrdF64(
1062+
taptree_cost(&ms1, ms_cache, policy_cache) + taptree_cost(&ms2, ms_cache, policy_cache),
1063+
);
1064+
1065+
// Merge the children nodes into either TapLeaf of the parent compilation or
1066+
// TapTree children nodes accordingly
1067+
node_weights.push(if parent_cost > children_cost {
1068+
ms_cache.insert(
1069+
TapTree::Tree(Arc::from(ms1.clone()), Arc::from(ms2.clone())),
1070+
p,
1071+
);
1072+
policy_cache.insert(
1073+
TapTree::Tree(Arc::from(ms1.clone()), Arc::from(ms2.clone())),
1074+
(parent_policy, parent_sat_cost),
1075+
);
1076+
(
1077+
Reverse(children_cost),
1078+
OrdF64(p),
1079+
TapTree::Tree(Arc::from(ms1), Arc::from(ms2)),
1080+
)
1081+
} else {
1082+
let node = TapTree::Leaf(Arc::from(parent_compilation));
1083+
(Reverse(parent_cost), OrdF64(p), node)
1084+
});
1085+
}
1086+
debug_assert!(node_weights.len() == 1);
1087+
let node = node_weights
1088+
.pop()
1089+
.expect("huffman tree algorithm is broken")
1090+
.2;
1091+
Ok(node)
1092+
}

src/policy/mod.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@ mod tests {
234234
use super::super::miniscript::context::Segwitv0;
235235
use super::super::miniscript::Miniscript;
236236
use super::{Concrete, Liftable, Semantic};
237+
#[cfg(feature = "compiler")]
238+
use crate::descriptor::Tr;
237239
use crate::DummyKey;
238240
#[cfg(feature = "compiler")]
239241
use crate::{descriptor::TapTree, Descriptor, Tap};
@@ -488,5 +490,32 @@ mod tests {
488490
let expected_descriptor = Descriptor::new_tr("E".to_string(), Some(tree)).unwrap();
489491
assert_eq!(descriptor, expected_descriptor);
490492
}
493+
494+
// private and optimized compilation for a given policy
495+
{
496+
let policy = policy_str!(
497+
"thresh(1,or(1@pk(A),1@pk(B)),or(1@pk(C),1@or(1@and(pk(E),pk(F)),1@pk(D))))"
498+
);
499+
let priv_desc = policy
500+
.clone()
501+
.compile_tr_private(Some(unspendable_key.clone()))
502+
.unwrap();
503+
let priv_expected_desc = Descriptor::Tr(
504+
Tr::<String>::from_str("tr(A,{{and_v(v:pk(E),pk(F)),pk(D)},{pk(C),pk(B)}})")
505+
.unwrap(),
506+
);
507+
508+
assert_eq!(priv_desc, priv_expected_desc);
509+
510+
let opt_desc = policy
511+
.clone()
512+
.compile_tr(Some(unspendable_key.clone()))
513+
.unwrap();
514+
let opt_expected_desc = Descriptor::Tr(
515+
Tr::<String>::from_str("tr(A,{{pk(D),pk(C)},{pk(B),and_v(v:pk(E),pk(F))}})")
516+
.unwrap(),
517+
);
518+
assert_eq!(opt_desc, opt_expected_desc);
519+
}
491520
}
492521
}

0 commit comments

Comments
 (0)