Skip to content

Commit 3a2d725

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 4f9f3c3 commit 3a2d725

File tree

2 files changed

+227
-3
lines changed

2 files changed

+227
-3
lines changed

src/policy/compiler.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,6 +1117,20 @@ pub fn best_compilation<Pk: MiniscriptKey, Ctx: ScriptContext>(
11171117
}
11181118
}
11191119

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

src/policy/concrete.rs

Lines changed: 213 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,21 @@ use {
3333
policy::Concrete,
3434
policy::{compiler, Liftable, Semantic},
3535
std::cmp::Reverse,
36+
std::collections::BTreeMap,
3637
std::collections::{BinaryHeap, HashMap},
3738
std::sync::Arc,
3839
Descriptor, Miniscript, Tap,
3940
};
4041
use {Error, ForEach, ForEachKey, MiniscriptKey};
4142

43+
/// [`TapTree`] -> ([`Policy`], satisfaction cost) cache
44+
#[cfg(feature = "compiler")]
45+
type PolicyTapCache<Pk> = BTreeMap<TapTree<Pk>, (Policy<Pk>, f64)>;
46+
47+
/// [`Miniscript`] -> leaf probability in policy cache
48+
#[cfg(feature = "compiler")]
49+
type MsTapCache<Pk> = BTreeMap<TapTree<Pk>, f64>;
50+
4251
/// Concrete policy which corresponds directly to a Miniscript structure,
4352
/// and whose disjunctions are annotated with satisfaction probabilities
4453
/// to assist the compiler
@@ -169,9 +178,154 @@ impl<Pk: MiniscriptKey> Policy<Pk> {
169178
Ok(node)
170179
}
171180

172-
/// Flatten the [`Policy`] tree structure into a Vector with corresponding leaf probability
173-
// TODO: 1. Can try to push the maximum of scaling factors and accordingly update later for
174-
// TODO: 1. integer metric. (Accordingly change metrics everywhere)
181+
/// Average satisfaction cost for [`TapTree`] with the leaf [`Miniscript`] nodes having
182+
/// probabilities corresponding to the (sub)policies they're compiled from.
183+
///
184+
/// Average satisfaction cost for [`TapTree`] over script-spend paths is probability times
185+
/// the size of control block + the script size.
186+
#[cfg(feature = "compiler")]
187+
fn taptree_cost(
188+
tr: &TapTree<Pk>,
189+
ms_cache: &MsTapCache<Pk>,
190+
policy_cache: &PolicyTapCache<Pk>,
191+
depth: u32,
192+
) -> f64 {
193+
match *tr {
194+
TapTree::Tree(ref l, ref r) => {
195+
Self::taptree_cost(l, ms_cache, policy_cache, depth + 1)
196+
+ Self::taptree_cost(r, ms_cache, policy_cache, depth + 1)
197+
}
198+
TapTree::Leaf(ref ms) => {
199+
let prob = ms_cache
200+
.get(&TapTree::Leaf(Arc::clone(ms)))
201+
.expect("Probability should exist for the given ms");
202+
let sat_cost = policy_cache
203+
.get(&TapTree::Leaf(Arc::clone(ms)))
204+
.expect("Cost should exist for the given ms")
205+
.1;
206+
prob * (ms.script_size() as f64 + sat_cost + 32.0 * depth as f64)
207+
}
208+
}
209+
}
210+
211+
/// Create a [`TapTree`] from the a list of [`Miniscript`]s having corresponding satisfaction
212+
/// cost and probability.
213+
///
214+
/// Given that satisfaction probability and cost for each script is known, constructing the
215+
/// [`TapTree`] as a huffman tree over the net cost (as defined in [`Policy::taptree_cost()`]) is
216+
/// the optimal one.
217+
/// For finding the optimal policy to taptree compilation, we are required to search
218+
/// exhaustively over all policies which have the same leaf policies. Owing to the exponential
219+
/// blow-up for such a method, we use a heuristic where we augment the merge to check if the
220+
/// compilation of a new (sub)policy into a [`TapTree::Leaf`] with the policy corresponding to
221+
/// the nodes as children is better than [`TapTree::Tree`] with the nodes as children.
222+
#[cfg(feature = "compiler")]
223+
fn with_huffman_tree_eff(
224+
ms: Vec<Arc<Miniscript<Pk, Tap>>>,
225+
policy_cache: &mut PolicyTapCache<Pk>,
226+
ms_cache: &mut MsTapCache<Pk>,
227+
) -> Result<TapTree<Pk>, Error> {
228+
let mut node_weights = BinaryHeap::<(Reverse<OrdF64>, OrdF64, TapTree<Pk>)>::new(); // (cost, branch_prob, tree)
229+
// Populate the heap with each `ms` as a TapLeaf, and the respective cost fields
230+
for script in ms {
231+
let wt = OrdF64(Self::taptree_cost(
232+
&TapTree::Leaf(Arc::clone(&script)),
233+
ms_cache,
234+
policy_cache,
235+
0,
236+
));
237+
let prob = OrdF64(
238+
*ms_cache
239+
.get(&TapTree::Leaf(Arc::clone(&script)))
240+
.expect("Probability should exist for the given ms"),
241+
);
242+
node_weights.push((Reverse(wt), prob, TapTree::Leaf(Arc::clone(&script))));
243+
}
244+
if node_weights.is_empty() {
245+
return Err(errstr("Empty Miniscript compilation"));
246+
}
247+
while node_weights.len() > 1 {
248+
// Obtain the two least-weighted nodes from the heap for merging
249+
let (_prev_cost1, p1, ms1) = node_weights.pop().expect("len must atleast be two");
250+
let (_prev_cost2, p2, ms2) = node_weights.pop().expect("len must atleast be two");
251+
252+
// Retrieve the respective policies
253+
let (left_pol, _c1) = policy_cache
254+
.get(&ms1)
255+
.ok_or_else(|| errstr("No corresponding policy found"))?
256+
.clone();
257+
258+
let (right_pol, _c2) = policy_cache
259+
.get(&ms2)
260+
.ok_or_else(|| errstr("No corresponding policy found"))?
261+
.clone();
262+
263+
// Create a parent policy with the respective node TapTrees as children (with odds
264+
// weighted approximately in ratio to their probabilities)
265+
let parent_policy = Policy::Or(vec![
266+
((p1.0 * 1e4).round() as usize, left_pol),
267+
((p2.0 * 1e4).round() as usize, right_pol),
268+
]);
269+
270+
// Obtain compilation for the parent policy
271+
let (parent_compilation, parent_sat_cost) =
272+
compiler::best_compilation_sat::<Pk, Tap>(&parent_policy)?;
273+
274+
// Probability of the parent node being satisfied equals the probability of either
275+
// nodes to be satisfied. Since we weight the odds appropriately, the children nodes
276+
// still have approximately the same probabilities
277+
let p = p1.0 + p2.0;
278+
// Inserting parent policy's weights (sat_cost and probability) for later usage
279+
ms_cache.insert(TapTree::Leaf(Arc::clone(&parent_compilation)), p);
280+
policy_cache.insert(
281+
TapTree::Leaf(Arc::clone(&parent_compilation)),
282+
(parent_policy.clone(), parent_sat_cost),
283+
);
284+
285+
let parent_cost = OrdF64(Self::taptree_cost(
286+
&TapTree::Leaf(Arc::clone(&parent_compilation)),
287+
ms_cache,
288+
policy_cache,
289+
0,
290+
));
291+
let children_cost = OrdF64(
292+
Self::taptree_cost(&ms1, ms_cache, policy_cache, 0)
293+
+ Self::taptree_cost(&ms2, ms_cache, policy_cache, 0),
294+
);
295+
296+
// Merge the children nodes into either TapLeaf of the parent compilation or
297+
// TapTree children nodes accordingly
298+
node_weights.push(if parent_cost > children_cost {
299+
ms_cache.insert(
300+
TapTree::Tree(Arc::from(ms1.clone()), Arc::from(ms2.clone())),
301+
p,
302+
);
303+
policy_cache.insert(
304+
TapTree::Tree(Arc::from(ms1.clone()), Arc::from(ms2.clone())),
305+
(parent_policy, parent_sat_cost),
306+
);
307+
(
308+
Reverse(children_cost),
309+
OrdF64(p),
310+
TapTree::Tree(Arc::from(ms1), Arc::from(ms2)),
311+
)
312+
} else {
313+
let node = TapTree::Leaf(Arc::from(parent_compilation));
314+
(Reverse(parent_cost), OrdF64(p), node)
315+
});
316+
}
317+
debug_assert!(node_weights.len() == 1);
318+
let node = node_weights
319+
.pop()
320+
.expect("huffman tree algorithm is broken")
321+
.2;
322+
Ok(node)
323+
}
324+
325+
/// Flatten the [`Policy`] tree structure into a Vector of (sub)-policies with corresponding
326+
/// leaf probabilities by using odds for calculating branch probabilities.
327+
/// We consider splitting the [`Policy`] tree into respective (sub)-policies considering
328+
/// disjunction over [`Policy::Or`] and [`Policy::Threshold`](1, ...)
175329
#[cfg(feature = "compiler")]
176330
fn to_tapleaf_prob_vec(&self, prob: f64) -> Vec<(f64, Policy<Pk>)> {
177331
match *self {
@@ -280,6 +434,62 @@ impl<Pk: MiniscriptKey> Policy<Pk> {
280434
}
281435
}
282436

437+
/// Compile [`Policy`] into a [`TapTree Descriptor`][`Descriptor::Tr`]
438+
///
439+
///
440+
/// This follows the heuristic as described in [`Policy::with_huffman_tree_eff`]
441+
#[cfg(feature = "compiler")]
442+
pub fn compile_tr(&self, unspendable_key: Option<Pk>) -> Result<Descriptor<Pk>, Error> {
443+
self.is_valid()?; // Check for validity
444+
match self.is_safe_nonmalleable() {
445+
(false, _) => Err(Error::from(CompilerError::TopLevelNonSafe)),
446+
(_, false) => Err(Error::from(
447+
CompilerError::ImpossibleNonMalleableCompilation,
448+
)),
449+
_ => {
450+
let (internal_key, policy) = self.clone().extract_key(unspendable_key)?;
451+
let tree = Descriptor::new_tr(
452+
internal_key,
453+
match policy {
454+
Policy::Trivial => None,
455+
policy => {
456+
let mut policy_cache = PolicyTapCache::<Pk>::new();
457+
let mut ms_cache = MsTapCache::<Pk>::new();
458+
// Obtain the policy compilations and populate the respective caches for
459+
// creating the huffman tree later on
460+
let leaf_compilations: Vec<_> = policy
461+
.to_tapleaf_prob_vec(1.0)
462+
.into_iter()
463+
.filter(|x| x.1 != Policy::Unsatisfiable)
464+
.map(|(prob, ref pol)| {
465+
let compilation =
466+
compiler::best_compilation_sat::<Pk, Tap>(pol).unwrap();
467+
policy_cache.insert(
468+
TapTree::Leaf(Arc::clone(&compilation.0)),
469+
(pol.clone(), compilation.1), // (policy, sat_cost)
470+
);
471+
ms_cache.insert(
472+
TapTree::Leaf(Arc::from(compilation.0.clone())),
473+
prob,
474+
);
475+
compilation.0
476+
})
477+
.collect();
478+
let taptree = Self::with_huffman_tree_eff(
479+
leaf_compilations,
480+
&mut policy_cache,
481+
&mut ms_cache,
482+
)
483+
.unwrap();
484+
Some(taptree)
485+
}
486+
},
487+
)?;
488+
Ok(tree)
489+
}
490+
}
491+
}
492+
283493
/// Compile the descriptor into an optimized `Miniscript` representation
284494
#[cfg(feature = "compiler")]
285495
pub fn compile<Ctx: ScriptContext>(&self) -> Result<Miniscript<Pk, Ctx>, CompilerError> {

0 commit comments

Comments
 (0)