Thanks, @rjl493456442 , for your feedback.
It’s very helpful coming from EL core developers.
I’ll share my thoughts about your questions.
I don’t see a significant advantage in terms of simplicity with the single-layer structure. One notable change is that the database key for storage trie nodes has been shortened to 32 bytes from the original 64 bytes. However, many database engines (e.g., those based on LSM-Tree) typically optimize key size compression effectively when consecutive entries share a common key prefix. Can you please provide some concrete examples?
This part of the rationale mentions that using a unified try gets us simplicity at the spec level.
What you mention about database engines compacting consecutive keys that share a prefix is an implementation trick EL clients must do (or your dependencies) to deal with the complexity of having multiple trees. Having a single tree thus not having 64 bytes (even if they share a prefix in your implementation) is a way to remove complexity directly at the spec level, instead of needing an implementation trick.
This quoted text mainly highlights that having a single tree has downstream simplification effects in algorithms related to the state tree, for example:
- Today, creating a state proof involves conceptually dealing with two trees. Proving a storage slot involves two branches that have different semantics. Having a single tree simplifies proof semantics since it’s proving a branch in a single tree (so no extra semantics are required).
- Syncing the tree also involves syncing more than one trie compared to one. This means extra overhead tracking the cursor of the sync or related proofs.
- The current tree design with multiple MPTs leaks this complexity to the state conversion algorithm. The design has to deal with walking algorithms that now should decide and clarify the walking order since there are multiple trees; compared to having a single tree, you don’t have to define how and when storage tries are walked while you’re walking the accounts trie. (After the upcoming tree conversion, hopefully, we won’t have to do a state transition again in the future… but this can’t be discarded, so having a unified tree simplifies this compared to what we have to do now)
- The same applies to caching strategies in implementations, as you have to deal with two scopes (account trie or storage trie(s)) for caching trees instead of a single one.
- Another example of simplification is the removal of RLP.
The overall argument of this simplicity angle mainly leverages this opportunity to keep simplifying the protocol at the spec level. Implementations can always push further with optimizations. As mentioned, simplifying the spec has helpful downstream simplifications.
I don’t think having data locality for storage slots is a bad thing.
The intended message is that data locality (in the database) is an implementation detail. Now that you mention it that way, I think we can better explain this in the text. I’m also not sure separating “Uniformity” from “Simplification” was a good idea, but maybe other authors have a different opinion. We should probably improve this part of the EIP since I agree it feels pretty confusing.
Unrelated to this Binary EIP but with data locality: this EIP will come bundled with EIP-4762 as a gas cost model (or similar; this depends on many factors). EIP-4762 introduces better data locality justified by access patterns in block execution. This has the benefits of proving fewer branches and saving gas costs for applications (but we need to do more research about overall effects since code access has to be charged). For example, consecutive storage slots live in groups under the same main branch (i.e. stem) — since storage slot access is not purely random, this should have gas benefits. The same applies to chunkified code, since although you can have JUMP(I)s, code execution is naturally “linear”.
With the two-layer structure, the storage slots associated with a specific account can be iterated, which is not possible with the new structure.
Yes, this is a known drawback of the design. However, iterating the storage tree isn’t a requirement for the protocol. You never need to iterate the tree to execute a block.
I agree that losing this capability for some client tooling or analysis can be unfortunate. I suffered from this myself while working with Verkle trees, which has the same drawback, when trying to do some analysis—but unless tree iteration is a necessity for the actual protocol, it feels more like an optional feature.
Can you elabrate why it’s useful?
I think I addressed this in my previous “Simplicity” response. Since I agree it can be confusing, this argument should probably be moved from “Uniformity.” We should make that change.
I don’t believe this is achieved through the single-layer structure but rather through the key hashing mechanism, which evenly distributes the entries. Notably, key hashing is a double-edged sword and has caused numerous issues. For instance, entries iterated from the tree lack associated preimages unless those preimages are explicitly maintained.
I agree key hashing is a double-edged sword. Actually, the preimages problem adds a decent amount of complexity to the upcoming tree conversion, I did some Ethresearch post some days ago about this topic if you’re interested. However, key hashing is required to balance the tree.
Regarding the single-layer structure and attacks, for a storage slot “full path” you can attack both the account and storage paths independently.
Additionally, why is it necessary to perform a hash operation on the basic header fields?
For SNARK circuits, it’s helpful to rely only on a 32byte x 32byte→32 byte
primitive for merkelization (or a slight variant if we use arithmetic hash functions). This is why we propose packing the nonce, balance, and code size (and version) in a single 32-byte to reduce this overhead and be efficient (actually, this was already a stretch since involves bitwise operations which aren’t nice for circuits). (The code-hash is another 32-byte leaf value). We can add this to the rationale since it isn’t obvious.
The account address is already derived through a hash calculation, which ensures it is evenly distributed. Adding another hash operation here feels redundant.
Using plain addresses as keys in the tree isn’t safe. At the cost of 21_000 gas, you can send ETH to any address you want (i.e., without knowing the public key). This means external actors can manipulate insertions in the tree at a very low cost (21k gas), creating long branches.