-
Notifications
You must be signed in to change notification settings - Fork 38.8k
Stricter & More Performant Invariant Checking in CheckBlock #14837
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
Stricter & More Performant Invariant Checking in CheckBlock #14837
Conversation
Fix hash computations to get unique bits Use less error-prone code for bitset revert tx_verify.* to master Add probabalistic duplicate detector to CheckBlock Clean up the implementation of fast deduplication checking Small clean up of comment & code layout for Duplicate Input checking Make duplicate checker also check for longchain validity Cleanup the DuplicateInputs interface to CheckInputInvariants and remove some redundant checks Change test reasons to match new behavior Switch to PCG generator
|
The argument that the CVE fix was a performance regression is based on a misunderstand of the system's current operation: Block validation is only very rarely on the critical path for block propagation. This wasn't the case when the duplicate checking skipping was added, but it is the case now. I can't imagine that the PR to skip the "redundant" duplicate check would have gone through if it wasn't on the block propagation critical path then, so I can't see a change that is 10x+ more complicated being adopted now that its off the critical path. This adds hundreds of lines of code and a homebrew cryptographic hash that AFAICT isn't particularly cryptographic but if broken turns into a DOS attack (and no, XORing a seed is does not obviously produce pairwise independence, which some approximation of is required to achieve the claimed property that a bad input for one user would be okay for others), -- and it looks like doesn't actually result in an observable benefit except on microbenchmarks. NAK. |
|
The main benefit I'm emphasizing here is that it checks more strict properties. As noted. the stricter check need not introduce a 'DoS attack' -- it can revert to the existing runtime easily. In any case, our goal isn't really to validate a maliciously created block quickly, it is to validate an honestly created block as quickly as possible and a maliciously created block in tolerable time -- I figured that they O(N / log(N)) speedup to switch back to the set algorithm upon collision wasn't worth the added complexity there, but it can certainly be done. The PCG I am using is not homebrew (except in implementation). If you prefer, we could add a dependency to the standard PCG library which contains a similar function. Thus the unstudied portion is mostly limited to the inputs to the function. My understanding of PCG is such that the xor'd seed should produce pairwise independence, although I grant you that it may be better to use two different seeds k1_1 and k1_2. Perhaps there are other efficiently computable prfs which have pairwise independence that would be suited for this purpose -- I previously used SIPHASH for this. |
|
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ConflictsReviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first. |
What's the list of properties that this PR checks for? "Invalid longchain order" seems to mean that "outputs being created by this transaction being have not been spent by an earlier transaction." Are there other checks, too? |
Found the list here: The actual implementation of this is change is short, clean and not hard to understand. This change doesn't add "hundreds of lines of code", though it does add a lot of comments and analysis. If it's true that "Block validation is only very rarely on the critical path for block propagation" then making this change by itself try to help with performance and complexity is probably not worth the risks. But I am curious about:
This is an interesting idea, even though it seems like it would require a lock-free CCoinsViewCache to improve performance. It does seem conceptually like adding an "Invalid longchain order" invariant could make future optimizations possible, so maybe this is worth thinking about more. |
| }; | ||
|
|
||
| std::unique_ptr<std::bitset<1 << 21>> pTable = MakeUnique<std::bitset<1 << 21>>(); | ||
| auto& table = *pTable.get(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This .get() is redundant, right?
|
|
||
|
|
||
| const auto pcg = [](uint64_t start, uint64_t inc) { | ||
| uint64_t nxt = (inc | 1) + (start * 6364136223846793005ULL); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make it explicit that the unsigned integer wraparound that will take place here at run-time is intentional? Or alternatively rewrite so that no integer wraparound takes place at run-time? Verify with -fsanitize=integer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do I make the wraparound explicit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would that be preferable to the sanitizer suppressions in ./test/?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I'm not sure about it. Unsigned integer overflow isn't a bug, it's a feature... I would also like a suppression that operates at the statement level (e.g., like rust's wrapping_mul) rather than marking the entire function with an attribute. What if there is an unsigned wraparound issue elsewhere in the code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@MarcoFalke In contrast to the sanitizer suppressions it documents if the wraparound is intentional or not.
What do you think about using say INTENTIONAL_WRAPAROUND for the intentional wraparounds and the file based sanitizer suppressions to cover the unintentional wraparounds?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JeremyRubin I assume you're talking about intentional wraparounds as features? The problem isn't intentional wraparounds – they are totally fine by definition. The problem is unintentional wraparounds.
I share your wish for suppressions that operated at the statement level, but they don't exist yet AFAIK :-)
| uint64_t nxt = (inc | 1) + (start * 6364136223846793005ULL); | ||
| uint32_t x = (nxt ^ (nxt >> 18)) >> 27; | ||
| uint32_t r = nxt >> 59; | ||
| return (x >> r) | (x << (31 & (-r))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make it explicit that the unsigned integer wraparound that will take place here at run-time is intentional? Or alternatively rewrite so that no integer wraparound takes place at run-time? Verify with -fsanitize=integer.
| return true; | ||
| } | ||
|
|
||
| /* CheckInputInvariants checks for three criticial invariants for the inputs in a block: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical :-)
| }; | ||
| }; | ||
|
|
||
| std::unique_ptr<std::bitset<1 << 21>> pTable = MakeUnique<std::bitset<1 << 21>>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#include <bitset> to make appveyor happy.
|
I agree with @gmaxwell. Adding complexity to the consensus code (or, as I often argue, changing it at all) should be something we do for only very good reasons, both because of the high review burden consensus code changes incur on the project, and because of the maintenance burden and cognitive load we put on future developers as well. I really don't think we should consider making consensus code changes that are designed to have no effect but prep the way for unspecified future code changes (which is why I often chime in with a Concept NACK on consensus refactoring PRs). These incur all the review and testing costs of a consensus change, but by design have no benefit to the network. This project is way too busy for this to be a good use of developer effort IMO. In my view, if we're going to make changes to the consensus code for performance reasons, then (a) those performance numbers should be demonstrably and meaningfully better for the network, and (b) we should generally discuss all the designs that might achieve the same performance benefit, and have a very good reason for not choosing the simplest such design. In the case of this change, the performance benefits could likely be realized by far simpler changes, as has been pointed out in the review comments on other versions of this PR. I do think that it would be a useful discussion to figure out exactly what would be the simplest code design for where the duplicate inputs check should live -- I've had some offline conversations and in my view it's not obvious whether it should naturally be considered a context-free check we perform on transactions, or whether the check should reside instead at the utxo database layer. If we care to improve the underlying issue here, I think we would best served by engaging in that design discussion to come up with the simplest way of reasoning about things in the future. Finally -- I think we should remember that we're not just writing code for ourselves, but for hopefully a much larger set of future engineers. If we are going to burden people with complicated reasoning about a consensus topic, it should be because it's really important. |
|
If there were a PR with some massive validation speedup that needed the new behaviour this provides, then it might be worth considering on the basis of that improvement. But as conjectural prepatory work I just don't see the gain. To the extent that there is an argument that belt-and-suspendering the consistent check would make the code safer holds -- I'm not sure about that, but I think it's at least plausible-- that could be accomplished with a simpler and clearer check. |
|
@sdaftuar Thanks for the review and feedback.
@gmaxwell, will let you know when I have something ready. I do think that the belt-and-suspendering the check holds merit; I will include the simplest version possible in that version, to the extent it isn't worse than master. Do you have a falsifiable framework for testing this sort of change? I'd like to produce a testable change where I can demonstrate a change in an environment that you're happy with. Would it be -reindex-chainstate with assumevalid set to one month prior? Is there something simpler that would work and be convincing & permit more rapid iteration? |
|
A reindex (-chainstate is fine) would be the standard benchmark, you could set assumevalid to whatever you think will highlight the improvement the most-- both extremes of AV setting fairly characterize different but important aspects of sync performance (the performance on older vs more recent history). Similarly, default dbcache or maximized dbcache are both defensible benchmarking configurations (the performance on standard vs high performance hosts). |
| Needs rebase |
|
There hasn't been much activity lately and the patch still needs rebase, so I am closing this for now. Please let me know when you want to continue working on this, so the pull request can be re-opened. |
This PR supersedes #14387 and is an alternative to #14397.
Summary
Previously, "no duplicate inputs" was checked on a per transaction basis, with this PR, they are checked block wide. This PR also checks that long chains are in valid order, which was previously implicitly checked only. The resulting code is also more performant.
Benefits
Before this PR:
After this PR:
Potential Risks & Redresses
In this section I'll try to exhaustively go through the various risks of this PR.