miniscript: rewrite satisfier to be non-recursive#895
miniscript: rewrite satisfier to be non-recursive#895apoelstra merged 7 commits intorust-bitcoin:masterfrom
Conversation
|
This is ready to review but I will eventually add a commit which addresses the "FIXME find a test case" comment for the logic change in the last commit. |
|
Hrpmh. Apparently I am using a FromIterator impl from 1.79. |
8780041 to
2b9c724
Compare
| Self::thresh | ||
| }; | ||
|
|
||
| let mut stack = vec![]; |
There was a problem hiding this comment.
The slowness comes from using heap-based rather than stack-based recursion
Maybe a simple with_capacity(SWEET_SPOT) could give a quick improvement if this is an issue, but I'm not sure.
There was a problem hiding this comment.
Yeah, not a bad idea. I added with_capacity(128). As I said above, I haven't benchmarked this (it's a little tricky/annoying to benchmark satisfaction in a realistic way).
a36dae0 to
f9c911d
Compare
|
Rebased after #915 |
f9c911d to
a609aec
Compare
This is a very large file with lots of different things in it and poor privacy boundaries. We want to break it up into modules. Start by creating a new directory.
I would like to make the satisfaction logic non-recursive. We have a post order iterator on Miniscript but not on Terminal. So we must use Miniscript in our API functions. (This also is more conceptually correct.)
For every fragment, we are going to create a short helper function which computes the satisfaction and dissatisfaction for that fragment. We create these together because in general satisfactions and dissatisfactions are defined in terms of each other: * Several satisfactions (e.g. `or_b`) require dissatisfactions to create * Satisfaction of `and_v` requires the satisfaction of its V child to create When we un-recursify these functions we will therefore need to compute both satisfactions and dissatisfactions together. (The result will be less efficient than the recursive version, since it will compute both sats and dissats for every fragment rather than computing just the data that's needed. But it should be easier to follow.) The next commit will do this for several more fragments. But I am doing it for pk_k in a commit by itself so you can see the process.
Probably this should just be squashed into the previous commit.
a609aec to
c31154d
Compare
|
Can't provide much for a review. Just admiring the cleanup! |
|
ACK 14a79f2 I can't help with the last patch because its over my head. The first 5 look good. |
|
How do you want to move forward with this @apoelstra? Wait for Sanket or want me and/or @trevarj to dig into something in particular that would give you confidence in our review? (Assuming Trevar wants to do that.) |
|
Hi @tcharding. I reached out to @sanket1729 out of band and he said it's okay if we push forward on this without him. I would appreciate a cursory review from you and/or @trevarj. You don't need to verify correctness of the rewrite. Hopefully the tests are thorough enough to catch any mistakes. Miniscript 12 is the "reference version" and we have pretty thorough fuzztests against it, although I don't think we're fuzzing the satisfier. In parallel to this project we should try to improve those fuzztests -- in particular, finding a way to improve the satisfier. I think we want to have a "fuzz library" similar to what exists in rust-simplicity, which is able to create fuzzer-guided complete transaction chains. |
trevarj
left a comment
There was a problem hiding this comment.
Did another review and tried to study sat_dissat.rs harder. Few doc nits found.
| } | ||
| } | ||
|
|
||
| /// The (dissatisfaction, satisfaction) pair for a `multi` fragment. |
There was a problem hiding this comment.
| /// The (dissatisfaction, satisfaction) pair for a `multi` fragment. | |
| /// The (dissatisfaction, satisfaction) pair for a `multi_a` fragment. |
There was a problem hiding this comment.
Can you indicate which commit you are talking about? Github provides no hint except 4 lines of diff.
| ) | ||
| } | ||
|
|
||
| /// The (dissatisfaction, satisfaction) pair for a `ripemd256` fragment. |
There was a problem hiding this comment.
| /// The (dissatisfaction, satisfaction) pair for a `ripemd256` fragment. | |
| /// The (dissatisfaction, satisfaction) pair for a `sha256` fragment. |
| ) | ||
| } | ||
|
|
||
| /// The (dissatisfaction, satisfaction) pair for a `ripemd256` fragment. |
There was a problem hiding this comment.
| /// The (dissatisfaction, satisfaction) pair for a `ripemd256` fragment. | |
| /// The (dissatisfaction, satisfaction) pair for a `hash256` fragment. |
| // FIXME existing code sets timelocks to None instead of propagating them, | ||
| // in the dissat case. This dates to the original plan module and was not | ||
| // comment on. https://github.com/rust-bitcoin/rust-miniscript/pull/592 | ||
| // Was likely a mistake. Need to find a test case that distinguishes before |
There was a problem hiding this comment.
Will we follow up on this test?
Staring at this for a long time wondering which dissatisfaction actually has a timelock that isn't IMPOSSIBLE.
There was a problem hiding this comment.
Let me follow up on this. Now that we have good LLMs I should be able to gin up a test case.
Staring at this for a long time wondering which dissatisfaction actually has a timelock that isn't IMPOSSIBLE.
For example, to dissatisfy an and_v(<timelock>, <something else>) you need to satisfy the timelock but dissatisfy the <something else>.
This may be the only example.
There was a problem hiding this comment.
I have constructed an example. Amusingly, while the old code would produce an invalid dissatisfaction, the new code produces no dissatisfaction at all, which is arguably no better. But fixing that is nontrivial and out of scope for this PR.
I will create a test case with a detailed comment analyzing this situation and add it to this PR.
There was a problem hiding this comment.
the new code produces no dissatisfaction at all,
Actually I think it produces no dissatisfaction at all because of an unrelated bug (the comment "Dissatisfactions don't need to non-malleable." is false). Probably I need to fix that separately and backport it.
There was a problem hiding this comment.
Ah, the "unrelated bug" is a combination of #734 which is fine to just leave alone for now, and a misunderstanding I had about malleability of dissatisfactions.
Again, candidate for squash.
This rewrites the main satisfy function, which I apologize for, and it's not even a simple code move because I combined satisfy_ and dissatisfy_helper, and had to change the code to pop from the stack rather than doing recursive calls. Furthermore, I suspect that this code will be slower in some cases, though unlikelily in a way that anybody would ever notice, because it computes both the satisfaction and dissatisfaction for every single node, not just the ones that are needed. (Note: it was always the case that, when satisfying, that a satisfaction was attempted for every single node, and that a dissatisfaction would be attempted for most nodes contained in any disjunction. So probably satisfaction is not meaningfully slowed, and importantly, we aren't making any more calls to the satisfier object, which might be an API to some slow USB device.) HOWEVER, the resulting code is much smaller and cleaner. I found a bug, I think (noted with a FIXME to add a test). I deleted over twice as much code as I added, and in particular eliminated tons of extremely noisy generics related to mall/non-mall function pointers. (The next commits will remove even more generics.)
Prior to this PR, when we compute the dissatisfaction for an or_i (which requires dissatisfying one of the child nodes), we would discard the timelock information for the child's dissatisfaction. This is wrong and can lead to defective plans, though apparently only in pathological cases. Add a regression test for this. Incidentally, this also provides a regression test for the "malleability for dissatisfactions doesn't matter" logic in the or_i branch of dissatisfy; previously you could replace Self::minimum_mall with min_fn and nothing would break.
c31154d to
a662453
Compare
This should be good to go now. |
|
On a662453 successfully ran local tests |
|
The module is called // Satisfaction is more complicated.
sat_dissat_vec.push((sub.sat_data, sub.dissat_data));We could do a separate PR to change |
|
@tcharding sure, in my next PR I'll add a commit that swaps them. Probably I should just make a utility struct for these. When writing and reviewing this code I repeatedly got the sat and dissat branches switched in my head. |
Currently the
Miniscript::satisfyfunction is implemented by the functionSatisfaction::satisfy_helperwhich is mutually recursive with the functionSatisfaction::dissatisfy_helper. There are a few issues with this:So I rewrote the whole function. I haven't benchmarked but I suspect the new one is a little slower, though it is asymptotically the same. The slowness comes from using heap-based rather than stack-based recursion, and also because the old algorithm could sometimes skip computing dissatisfactions while this one never does. I think the increase in code clarity justifies the loss of performance (if there is one).
See the last commit message for more details.