-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Proof verification optimisation for pallet-beefy-mmr #12820
Comments
No knock-on effects I can think of.. feel free to move ahead with the PR. |
Unfortunately this optimization is not resistant to second-preimage attacks. Without this safegaurd it becomes possible for an attacker to shorten or lengthen the proofs (essentially describing an alternative tree) in order to fool the verifier that some non-existent items exist in the original tree. For single proofs you are able to easily prevent this by asserting that the proof length is the same as the original tree height. This can be done by calculating the height of the tree using the number of leaves at the time the tree was created. And while this vulnerability only affects the Gas Performance
|
Just an update from the Snowfork side, we're mostly debating this issue on the Polkadot Bridges group on element: https://matrix.to/#/#bridges:web3.foundation. |
Given that keccak256 is (still™) collision resistant (else, Ethereum would have bigger fish to fry), a practical exploit of this attack is infeasible, lest an implementation issue such as https://eprint.iacr.org/2023/331 sneaks through. As such, an exploit seems, for the foreseeable future, theoretical in nature. Nonetheless, @swasilyev pointed me towards a 2nd preimage attack against bitcoin light client SPV proofs, which reduces the brute force requirement from 256 to about 70 bits (modulo economic assumptions re. the attacker plausible at the time) for SPV clients without safety guards. If there's another argument to be made that we should install safeguards against 2nd preimage attacks, here are the options to address/mitigate I've tallied up so far:
As such, I'd keep #12857. If there's still concern that 2nd preimage attacks should be guarded against, we can deploy one of the above fixes. |
I want to add here that the 2d multiproof isn't a new scheme, the previous merkle tree scheme was simply a standard construction with no node sorting. The 2d multiproof is an approach to supporting multiproofs for standard merkle trees. From the benchmarks i've posted here, the multiproof scheme is more efficient for larger sub sets than single proofs. I'm still not fully onboard with openzeppelin's multi proofs. But would be interesting to see benchmarks against solidity-merkle-trees. |
Just a correction: Should check that leaves aren't ever 64 bytes to avoid ambiguity with nodes. I can also confirm that @AlistairStewart's recommendations are backed by the OpenZeppelin team in this issue: OpenZeppelin/openzeppelin-contracts#3091, where they suggest the same methods for ensuring domain separation between internal nodes and leafs. These mitigations are already in place in Polkadot/Substrate For BEEFY validator rootLeaves are 20-byte ethereum addresses
For parachain heads rootLeaves are greater than 64 bytes. They are of the form: (para_id: u32, head_data: Vec<u8>).encode() And for all parachains in existence, head_data is always greater than 32 bytes. Think parent_head, block_number, state_root, etc. Of course this may change, as I think there is nothing in the protocol that prevents I think a simple solution is to just filter out parachains/parathreads with such a property. The merkle tree construction already does this for parathreads. |
Just correcting myself, actually there is no vulnerability here. Supposing So in all cases, leaves for the parachain heads root are always equal or greater than 65 bytes in length, and are thus not exploitable via second-preimage attacks. |
Closing since from discussions with @doubledup and also on other Element channels, multiproofs aren't on the table for Snowfork atm anyway, given that via the benchmarks in https://github.com/doubledup/optimisation-beefy-merkle-proofs, Polytope's multiproof of 14 sigs (i.e. the sample size used in production for now) in a tree of 1000 leaves (i.e. current Kusama) is currently performing worse than doing the same serially with singleproofs, and singleproofs are performing sufficiently well as it stands. As such, let's keep the optimization introduced by #12857. Using Polytope's or OpenZeppelin style multiproofs should be retabled if, say, the parameters change and we reconsider multiproofs for beefy authorities, or if we end up using multiproofs for verifying parachain headers across multiple blocks. |
@Lederstrumpf @acatangiu Can we revert the optimization in #12857? Unfortunately it introduced a security vulnerability, which we only discovered this week. Apologies for the time everyone spent debating this optimization. The problem is that in our interactive BEEFY light client protocol, we assign BEEFY validators a unique index - the position of their leafs in the validator merkle tree. We record these indices in a bitfield, and manipulate the bitfield as part of our validator subsampling algorithm. Offchain relayers must provide the leaf index and leaf (validator address), and the light client verifies those using the merkle proof verifier discussed in this issue. However we were so focused on verifying the leaf in an optimized fashion, that we forgot that we need to make sure the leaf index is verified too. The optimization introduced in #12857 prevents us from verifying the leaf index along with the leaf and proof, because it sorts leaves, messing up the leaf order. |
Of the opinion there should be a more democratic process for core protocol changes like this. |
@Lederstrumpf as per our discussion, here's a high-level overview of why leaf indices are important for our validator subsampling protocol, and how our optimization tripped us up. The offchain relayer scans all Polkadot blocks, and looks for SignedCommitment objects within the justifications for a block. This The index of each signature corresponds to the index of the validator address in the beefy validators merkle tree. Now with this information in hand, relayers are able to participate in an interactive protocol with our beefy light client:
|
@vgeddes thanks for the links to the implementation locations. I currently see three options that might either salvage the lexical hash ordering optimisation, or have a distinct performance gain over the unoptimised scheme - feedback & other suggestions welcome: That being said, I agree with protecting current launch timeline, and deploying any new optimisations post-launch. So ack from me to revert the lexical hash ordering. |
…paritytech#12857)" This reverts commit f9d1dcd. Still require commitment to the leaves - see paritytech#12820.
…paritytech#12857)" This reverts commit f9d1dcd since we still require commitment to the leaves - see paritytech#12820.
…paritytech#12857)" This reverts commit f9d1dcd since we still require commitment to the leaves - see paritytech#12820.
Agree! Right now, BEEFY is still experimental and in development, but once we stabilize and write a SPEC, future changes will be more formalized and democratized. For now, I think for a good balance between development velocity and breaking changes discussions/decisions, the expectation for interested parties is to actively follow the topic and speak up. Even so, I propose we move or at least mirror this conversation to the Polkadot Forum for more visibility. wdyt? |
Agreed ser |
…paritytech#12857)" (paritytech#14176) * Revert "Optimize merkle proofs for efficient verification in Solidity (paritytech#12857)" This reverts commit f9d1dcd since we still require commitment to the leaves - see paritytech#12820. * remove PartialOrd trait from mmr hash type
While looking into Ethereum gas optimisation for Snowbridge, we noticed one of OpenZeppelin's optimisations for proof verification. This amounts to sorting each pair of hashes before hashing them during proof creation, which removes the need to track & reproduce the order in which the hash function is applied during proof verification.
In our case, this makes it more efficient to verify proofs on Ethereum because we can perform a single comparison to determine the order when combining hashes.
This would be a breaking change to proof verification, so we'd need to communicate this to the relevant projects.
I've made this change in a fork and am working on updating the tests there. Are there any other considerations or knock-on effects I should deal with before opening a PR?
cc @acatangiu @Lederstrumpf
The text was updated successfully, but these errors were encountered: