Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor extract_key logic in Taproot compiler #732

Merged
merged 7 commits into from
Aug 30, 2024

Conversation

apoelstra
Copy link
Member

Right now our Taproot compiler does some complicated analysis to identify a key within a policy that may be extracted to use as an internal key. Specifically, if you look at the "disjunction tree" at the root of a policy, every leaf of this tree can be a tapbranch and any leaf which is just a pubkey-check and nothing else is a candidate to be the internal key.

However, our logic to do this is convoluted, has many unnecessary error paths, and lots of gratuitous allocations (which ironically make the code harder, not easier, to follow).

This PR smooths all that out by introducing a new iterator over the potential tapbranches of a policy (along with their probabilities). It also cleans up much of the error handling in the policy module, eliminating 3 calls to the stringly-typed errstr function.

This test takes ~28 seconds on my system, but it is doing two independent
compilations in serial. If I split it into two, then they can run in
parallel and the total time is about 16 seconds.
The Taproot compiler has a couple algorithms used to find an optimal
internal key to extract from a policy. One is `to_tapleaf_prob_vec`
which is a fundamental part of the compiler, which iterates over the
disjunction-only root of the tree and returns every branch along with
its probabilities.

This is currently implemented using both allocations and recursion. By
pulling the logic out into an iterator we can get a clearer, faster
algorithm that cannot stack-overflow.

This algorithm is then fed into Concrete::extract_key. The goal of this
algorithm is to find the highest-probability tapbranch consisting of
just a single key. This algorithm inexplicably works by:

* Lifting the policy to a semantic policy, maybe returning an error
* Iterating over the entire policy, extracting a list of every key.
* Calling to_tapleaf_prob_vec to get a vector of tapleaves, which it
  then iterates over to find the key-only branches, which it collects
  into a BTreeMap mapping the key to probabilities.
* Iterating over the extracted lists of keys, and for each key, reducing
  the semantic policy by that key and comparing to Trivial (logically,
  this is asking "is this key in a tree of disjunctions from the root").
* For each such key that it finds, looking it up in the map, potentially
  returning an error (actually this error path is impossible to hit).
* and manually minimizing the looked up probability.

With to_tapleaf_prob_vec replaced by an iterator there is a simpler and
more direct algorithm:

* Iterate through all the tapbranches/probability pairs, filtering for
  key-only branches, and maximizing by probability.

This can only fail if there are no key-only branches, and this is
reflected by only having one error branch.
We have a few error returns that are impossible to hit:

* A sanity check on a tapleaf that just came out of the compiler
  (if this is hit it is a compiler bug and we want to know about it).
* An error return from with_huffman_tree which can only happen if it's
  given an empty input (impossible)
* An error if the final compilation (all tapleaves assembled into a tree)
  can't fit into a Descriptor::tr; but again, this is a compiler bug if
  we hit it. (Actually, I think that by manually constructing a policy
  that exceeds the maximum recursion depth you can trigger this error
  path, but the compiler output is not the place to flag this manual
  violation of invariants).

The next commit will clean up the error types. These changes are in
their own commit because they are potentially controversial.
This adds a couple variants to policy::compiler::Error and eliminates a
couple calls to errstr. It also changes a few internal functions to
return compiler errors instead of the top-level error.
Copy link
Member

@sanket1729 sanket1729 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK a195c81

Awesome. Thanks for the code cleanup.

Copy link
Member Author

@apoelstra apoelstra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully ran local tests on a195c81.

@apoelstra apoelstra merged commit 4490289 into rust-bitcoin:master Aug 30, 2024
30 checks passed
@apoelstra apoelstra deleted the 2024-08--taproot-compiler branch August 30, 2024 16:38
@shesek
Copy link
Contributor

shesek commented Sep 17, 2024

We have a few error returns that are impossible to hit:
...
* An error return from with_huffman_tree which can only happen if it's given an empty input (impossible)

This one was not actually impossible. 😅 See #677 for a test (that now panics) and a fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants