Repository navigation
fix pruning bug #391 - #403
Conversation
Moves out the Converter interfaces and pruning logic into their own file in preparation for adding another converter and logic to pruning. No change in behaviour. Assisted-by: Claude
Pruning assumes that a program has a single node per IHR, as a decoded program does. A program built in memory can have several nodes with the same IHR, and as the IHR does not cover the types inside a node, those nodes can contain case nodes with different IHRs. The tracker records the branches of each of those case nodes separately, so pruning can hide a different branch in each, while the encoding has a single node for all of them, which then reaches a pruned branch. Reduce the program to maximal sharing before running it on the Bit Machine, so that the tracker records branches for the nodes which the encoding will have. Assisted-by: Claude
dd7b621 to
62879ef
Compare
|
I made a few other attempts at this, and created some good fuzzing programs for pruning, which we don't really have. I'll add them in a separate PR |
| (Inner::Case(_, right), Hide::Left) => stack.push(right), | ||
| (Inner::Case(left, _), Hide::Right) => stack.push(left), | ||
| _ => { | ||
| stack.extend(node.left_child()); |
There was a problem hiding this comment.
In 43bde49:
This is a neat idiom. It pushes node.left_child() if it is Some.
|
In 23ae4bf: This approach (just fail) is certainly better than producing a broken program. But we should revisit and try to make pruning work unconditionally. |
Refactors pruning to use a different approach. First, walk top down from the root, and mark kept nodes and what needs to be hidden. Then, rebuild the DAG from bottom up (the usual way), only adding back kept nodes, and replacing cases with hidden, if necessary. Only finalize on the rebuilt program, which should be more reliable with determining the correct types. Because we use IHRs, we need to make sure that there are no different nodes in the tree, with the same IHR, but potentially different types, which is why we reduced the tree to maximal sharing in a previous commit. Fixes BlockstreamResearch#391. Assisted-by: Claude
Pruning can produce a program which consensus rejects. Case nodes and assertions whose IHRs only become equal once pruned are a single node in the encoding, which then hides a branch that one of them uses, or keeps a branch that never runs. After pruning a program, encode it, decode it, run it, and check that every node and both branches of every case node ran, as libsimplicity does. If not, fail with the new ExecutionError::PruningFailed rather than return the program. This runs the program a second time. Assisted-by: Claude
Assisted-by: Claude
62879ef to
21878a5
Compare
Agree |
|
Guys, would you mind adding |
|
I don't mind but could you provide the actual error? |
error[E0004]: non-exhaustive patterns: `&ExecutionError::PruningFailed` not covered
--> crates/test/src/fuzz/outcome.rs:57:92
|
57 | ProgramError::Pruning(execution) | ProgramError::Execution(execution) => match execution {
| ^^^^^^^^^ pattern `&ExecutionError::PruningFailed` not covered
<shorten it a little bit>
550 | pub enum ExecutionError {
| ^^^^^^^^^^^^^^^^^^^^^^^
...
564 | PruningFailed,
| ------------- not covered
= note: the matched value is of type `&ExecutionError` |
|
This error isn't complex; it's easy to fix, but this library was published on Friday. |
|
Yeah, agreed we should add |
|
In the case that you're using the error, I would say that it is better to not add the attribute. It looks like you are fuzz testing something and genuinely need to manually update the case. (I'm guessing, since the path says |
|
Interesting point. In rust-bitcoin we have started adding the attribute to almost all our error enums so that post-1.0 we will still have room to extend the errors without semver-breaking changes. But maybe because this crate is so young and nowhere near 1.0, we want to take the opposite tack, and try to alert people via compiler breakage when we've extended error cases. I'm unsure. |
|
I don't mind adding it at some point, but I think it may be too early for |
|
Okay, agreed. |
698a2b0 fuzz: add a type-directed program generator and a pruning target (stringhandler) cc4c4a7 ci(fuzz): use the workflow's nightly in generate-files.sh (stringhandler) Pull request description: In #403 we added a fix for pruning. This PR adds code to fuzz pruning of programs, which didn't exist before. It is LLM generated. It is also built on top of #403, so it needs that PR to be merged first. ## Results Measured on 20,000 generated programs, from the inputs that `pseudorandom_inputs` gives (the CI test uses the first 1,000): | | | |---|---| | Programs that pass every check | 99.97% (the other 0.03% give `PruningFailed`) | | Program size | median 200 nodes, 90th percentile 523 | | Pruning hides at least one branch | 95% (4.4 branches on average) | | A case node keeps both branches | 43% | | Pruning shrinks the witness data | 88% | | A hidden branch shares a node with kept code | 91% | | Contains a `disconnect` node | 89% (of the CI test's 1,000) | On the commit before the #391 fix, 64 of the CI test's 1,000 programs fail, the first at input 1. libFuzzer, with the `-len_control=0 -max_len=4096` that `fuzz.sh` now passes, finds the bug from an empty corpus in 7, 12 and 32 seconds (3 runs). Coverage of `src/node/prune.rs`, over the CI test's inputs and a 2,712-input libFuzzer corpus: 97.6% of lines and all 23 functions. The uncovered lines are error paths: programs that fail to run, and the rejections in the check after pruning. Not covered: the generator never produces separate nodes with one IHR but different types inside them (0 of 20,000), so only unit tests cover merging such nodes before pruning. ACKs for top commit: apoelstra: ACK 698a2b0; successfully ran local tests Tree-SHA512: f1d95140cab40f3208339dbbaa884a3afef7a4970b79e34f7ca68f7451b4a97c65618b07100398f5fb44d0eadb4acb6f4c256600e2daa87c870f71bc0f11900d
So after a few different approaches to fixing this bug, I settled on this one for now. Basically, run the program on a Bitmachine with a tracker which stores which IHRs were called. Then go top down along the DAG and store kept IHRs, and for
cases, mark their children according to how we will hide them.Then, rebuild the tree from the kept nodes, using the hiding information to replace
casenodes, if necessary. Finally, run finalize on the new tree to determine the correct types.There is an extra step added in the second last commit, which runs the code again like consensus would, and fails if there are any un-run nodes. This can be removed if deemed unnecessary.
Also, because pruning is done by IHR, a step is added to the beginning to remove that share an IHR but are different in memory (reduces to Max Sharing instead of Internal Sharing). For decoded programs, this is always the case anyway, but for programs edited in memory, this may not be a valid assumption.