Skip to content

Commit 662d152

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 fb5cfce commit 662d152

File tree

2 files changed

+227
-1
lines changed

2 files changed

+227
-1
lines changed

src/policy/compiler.rs

+14
Original file line numberDiff line numberDiff line change
@@ -1115,6 +1115,20 @@ pub fn best_compilation<Pk: MiniscriptKey, Ctx: ScriptContext>(
11151115
}
11161116
}
11171117

1118+
/// Obtain the best compilation of for p=1.0 and q=0, along with the satisfaction cost for the script
1119+
pub(crate) fn best_compilation_sat<Pk: MiniscriptKey, Ctx: ScriptContext>(
1120+
policy: &Concrete<Pk>,
1121+
) -> Result<(Arc<Miniscript<Pk, Ctx>>, f64), CompilerError> {
1122+
let mut policy_cache = PolicyCache::<Pk, Ctx>::new();
1123+
let x: AstElemExt<Pk, Ctx> = best_t(&mut policy_cache, policy, 1.0, None)?;
1124+
if !x.ms.ty.mall.safe {
1125+
Err(CompilerError::TopLevelNonSafe)
1126+
} else if !x.ms.ty.mall.non_malleable {
1127+
Err(CompilerError::ImpossibleNonMalleableCompilation)
1128+
} else {
1129+
Ok((x.ms, x.comp_ext_data.sat_cost))
1130+
}
1131+
}
11181132
/// Obtain the best B expression with given sat and dissat
11191133
fn best_t<Pk, Ctx>(
11201134
policy_cache: &mut PolicyCache<Pk, Ctx>,

src/policy/concrete.rs

+213-1
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,19 @@ use {
3737
crate::Miniscript,
3838
crate::Tap,
3939
std::cmp::Reverse,
40+
std::collections::BTreeMap,
4041
std::collections::{BinaryHeap, HashMap},
4142
std::sync::Arc,
4243
};
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
@@ -135,7 +144,10 @@ impl fmt::Display for PolicyError {
135144
}
136145

137146
impl<Pk: MiniscriptKey> Policy<Pk> {
138-
/// Flatten the [`Policy`] tree structure into a Vector with corresponding leaf probability
147+
/// Flatten the [`Policy`] tree structure into a Vector of (sub)-policies with corresponding
148+
/// leaf probabilities by using odds for calculating branch probabilities.
149+
/// We consider splitting the [`Policy`] tree into respective (sub)-policies considering
150+
/// disjunction over [`Policy::Or`] and [`Policy::Threshold`](1, ...)
139151
#[cfg(feature = "compiler")]
140152
fn to_tapleaf_prob_vec(&self, prob: f64) -> Vec<(f64, Policy<Pk>)> {
141153
match *self {
@@ -248,6 +260,62 @@ impl<Pk: MiniscriptKey> Policy<Pk> {
248260
}
249261
}
250262

263+
/// Compile [`Policy`] into a [`TapTree Descriptor`][`Descriptor::Tr`]
264+
///
265+
///
266+
/// This follows the heuristic as described in [`with_huffman_tree_eff`]
267+
#[cfg(feature = "compiler")]
268+
pub fn compile_tr(&self, unspendable_key: Option<Pk>) -> Result<Descriptor<Pk>, Error> {
269+
self.is_valid()?; // Check for validity
270+
match self.is_safe_nonmalleable() {
271+
(false, _) => Err(Error::from(CompilerError::TopLevelNonSafe)),
272+
(_, false) => Err(Error::from(
273+
CompilerError::ImpossibleNonMalleableCompilation,
274+
)),
275+
_ => {
276+
let (internal_key, policy) = self.clone().extract_key(unspendable_key)?;
277+
let tree = Descriptor::new_tr(
278+
internal_key,
279+
match policy {
280+
Policy::Trivial => None,
281+
policy => {
282+
let mut policy_cache = PolicyTapCache::<Pk>::new();
283+
let mut ms_cache = MsTapCache::<Pk>::new();
284+
// Obtain the policy compilations and populate the respective caches for
285+
// creating the huffman tree later on
286+
let leaf_compilations: Vec<_> = policy
287+
.to_tapleaf_prob_vec(1.0)
288+
.into_iter()
289+
.filter(|x| x.1 != Policy::Unsatisfiable)
290+
.map(|(prob, ref pol)| {
291+
let compilation =
292+
compiler::best_compilation_sat::<Pk, Tap>(pol).unwrap();
293+
policy_cache.insert(
294+
TapTree::Leaf(Arc::clone(&compilation.0)),
295+
(pol.clone(), compilation.1), // (policy, sat_cost)
296+
);
297+
ms_cache.insert(
298+
TapTree::Leaf(Arc::from(compilation.0.clone())),
299+
prob,
300+
);
301+
compilation.0
302+
})
303+
.collect();
304+
let taptree = with_huffman_tree_eff(
305+
leaf_compilations,
306+
&mut policy_cache,
307+
&mut ms_cache,
308+
)
309+
.unwrap();
310+
Some(taptree)
311+
}
312+
},
313+
)?;
314+
Ok(tree)
315+
}
316+
}
317+
}
318+
251319
/// Compile the descriptor into an optimized `Miniscript` representation
252320
#[cfg(feature = "compiler")]
253321
pub fn compile<Ctx: ScriptContext>(&self) -> Result<Miniscript<Pk, Ctx>, CompilerError> {
@@ -799,6 +867,36 @@ where
799867
}
800868
}
801869

870+
/// Average satisfaction cost for [`TapTree`] with the leaf [`Miniscript`] nodes having
871+
/// probabilities corresponding to the (sub)policies they're compiled from.
872+
///
873+
/// Average satisfaction cost for [`TapTree`] over script-spend paths is probability times
874+
/// the size of control block + the script size.
875+
#[cfg(feature = "compiler")]
876+
fn taptree_cost<Pk: MiniscriptKey>(
877+
tr: &TapTree<Pk>,
878+
ms_cache: &MsTapCache<Pk>,
879+
policy_cache: &PolicyTapCache<Pk>,
880+
depth: u32,
881+
) -> f64 {
882+
match *tr {
883+
TapTree::Tree(ref l, ref r) => {
884+
taptree_cost(l, ms_cache, policy_cache, depth + 1)
885+
+ taptree_cost(r, ms_cache, policy_cache, depth + 1)
886+
}
887+
TapTree::Leaf(ref ms) => {
888+
let prob = ms_cache
889+
.get(&TapTree::Leaf(Arc::clone(ms)))
890+
.expect("Probability should exist for the given ms");
891+
let sat_cost = policy_cache
892+
.get(&TapTree::Leaf(Arc::clone(ms)))
893+
.expect("Cost should exist for the given ms")
894+
.1;
895+
prob * (ms.script_size() as f64 + sat_cost + 32.0 * depth as f64)
896+
}
897+
}
898+
}
899+
802900
/// Create a Huffman Tree from compiled [Miniscript] nodes
803901
#[cfg(feature = "compiler")]
804902
fn with_huffman_tree<Pk: MiniscriptKey>(
@@ -829,3 +927,117 @@ fn with_huffman_tree<Pk: MiniscriptKey>(
829927
.1;
830928
Ok(node)
831929
}
930+
931+
/// Create a [`TapTree`] from the a list of [`Miniscript`]s having corresponding satisfaction
932+
/// cost and probability.
933+
///
934+
/// Given that satisfaction probability and cost for each script is known, constructing the
935+
/// [`TapTree`] as a huffman tree over the net cost (as defined in [`taptree_cost`]) is
936+
/// the optimal one.
937+
/// For finding the optimal policy to taptree compilation, we are required to search
938+
/// exhaustively over all policies which have the same leaf policies. Owing to the exponential
939+
/// blow-up for such a method, we use a heuristic where we augment the merge to check if the
940+
/// compilation of a new (sub)policy into a [`TapTree::Leaf`] with the policy corresponding to
941+
/// the nodes as children is better than [`TapTree::Tree`] with the nodes as children.
942+
#[cfg(feature = "compiler")]
943+
fn with_huffman_tree_eff<Pk: MiniscriptKey>(
944+
ms: Vec<Arc<Miniscript<Pk, Tap>>>,
945+
policy_cache: &mut PolicyTapCache<Pk>,
946+
ms_cache: &mut MsTapCache<Pk>,
947+
) -> Result<TapTree<Pk>, Error> {
948+
let mut node_weights = BinaryHeap::<(Reverse<OrdF64>, OrdF64, TapTree<Pk>)>::new(); // (cost, branch_prob, tree)
949+
// Populate the heap with each `ms` as a TapLeaf, and the respective cost fields
950+
for script in ms {
951+
let wt = OrdF64(taptree_cost(
952+
&TapTree::Leaf(Arc::clone(&script)),
953+
ms_cache,
954+
policy_cache,
955+
0,
956+
));
957+
let prob = OrdF64(
958+
*ms_cache
959+
.get(&TapTree::Leaf(Arc::clone(&script)))
960+
.expect("Probability should exist for the given ms"),
961+
);
962+
node_weights.push((Reverse(wt), prob, TapTree::Leaf(Arc::clone(&script))));
963+
}
964+
if node_weights.is_empty() {
965+
return Err(errstr("Empty Miniscript compilation"));
966+
}
967+
while node_weights.len() > 1 {
968+
// Obtain the two least-weighted nodes from the heap for merging
969+
let (_prev_cost1, p1, ms1) = node_weights.pop().expect("len must atleast be two");
970+
let (_prev_cost2, p2, ms2) = node_weights.pop().expect("len must atleast be two");
971+
972+
// Retrieve the respective policies
973+
let (left_pol, _c1) = policy_cache
974+
.get(&ms1)
975+
.ok_or_else(|| errstr("No corresponding policy found"))?
976+
.clone();
977+
978+
let (right_pol, _c2) = policy_cache
979+
.get(&ms2)
980+
.ok_or_else(|| errstr("No corresponding policy found"))?
981+
.clone();
982+
983+
// Create a parent policy with the respective node TapTrees as children (with odds
984+
// weighted approximately in ratio to their probabilities)
985+
let parent_policy = Policy::Or(vec![
986+
((p1.0 * 1e4).round() as usize, left_pol),
987+
((p2.0 * 1e4).round() as usize, right_pol),
988+
]);
989+
990+
// Obtain compilation for the parent policy
991+
let (parent_compilation, parent_sat_cost) =
992+
compiler::best_compilation_sat::<Pk, Tap>(&parent_policy)?;
993+
994+
// Probability of the parent node being satisfied equals the probability of either
995+
// nodes to be satisfied. Since we weight the odds appropriately, the children nodes
996+
// still have approximately the same probabilities
997+
let p = p1.0 + p2.0;
998+
// Inserting parent policy's weights (sat_cost and probability) for later usage
999+
ms_cache.insert(TapTree::Leaf(Arc::clone(&parent_compilation)), p);
1000+
policy_cache.insert(
1001+
TapTree::Leaf(Arc::clone(&parent_compilation)),
1002+
(parent_policy.clone(), parent_sat_cost),
1003+
);
1004+
1005+
let parent_cost = OrdF64(taptree_cost(
1006+
&TapTree::Leaf(Arc::clone(&parent_compilation)),
1007+
ms_cache,
1008+
policy_cache,
1009+
0,
1010+
));
1011+
let children_cost = OrdF64(
1012+
taptree_cost(&ms1, ms_cache, policy_cache, 0)
1013+
+ taptree_cost(&ms2, ms_cache, policy_cache, 0),
1014+
);
1015+
1016+
// Merge the children nodes into either TapLeaf of the parent compilation or
1017+
// TapTree children nodes accordingly
1018+
node_weights.push(if parent_cost > children_cost {
1019+
ms_cache.insert(
1020+
TapTree::Tree(Arc::from(ms1.clone()), Arc::from(ms2.clone())),
1021+
p,
1022+
);
1023+
policy_cache.insert(
1024+
TapTree::Tree(Arc::from(ms1.clone()), Arc::from(ms2.clone())),
1025+
(parent_policy, parent_sat_cost),
1026+
);
1027+
(
1028+
Reverse(children_cost),
1029+
OrdF64(p),
1030+
TapTree::Tree(Arc::from(ms1), Arc::from(ms2)),
1031+
)
1032+
} else {
1033+
let node = TapTree::Leaf(Arc::from(parent_compilation));
1034+
(Reverse(parent_cost), OrdF64(p), node)
1035+
});
1036+
}
1037+
debug_assert!(node_weights.len() == 1);
1038+
let node = node_weights
1039+
.pop()
1040+
.expect("huffman tree algorithm is broken")
1041+
.2;
1042+
Ok(node)
1043+
}

0 commit comments

Comments
 (0)