-
Notifications
You must be signed in to change notification settings - Fork 20
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
Convert rvgo's memory implementation to radix trie #83
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #83 +/- ##
==========================================
+ Coverage 62.05% 62.70% +0.65%
==========================================
Files 26 27 +1
Lines 3236 4108 +872
==========================================
+ Hits 2008 2576 +568
- Misses 1112 1408 +296
- Partials 116 124 +8 ☔ View full report in Codecov by Sentry. |
c5a040b
to
f263b31
Compare
f263b31
to
6229f24
Compare
6229f24
to
17d7ec2
Compare
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.
Great work! It would be better if we have a design and implementation guide docs in the repo, including visualized example if possible. but it should not be in this PR.
Background
Currently, Asterisc's memory layout is constructed using merkle tree represented as hashmaps where keys are a generalized index and values are the merkle root of the sub-tree or the node.
This design is identical to Cannon, and has some room for improvements. Therefore, in order to make Asterisc more memory efficient, we propose modifying the memory layout as follows.
Proposed Change
Currently, the memory of asterisc is laid out as follows.
We can modify it to something like:
In order to better accommodate the typical memory layout, we can use different branching factor at each level of the trie.
Because upper memory levels are more sparse and the pages tend to be adjacent, we can have lower branching factor at upper trie levels, and higher branching factory at lower trie levels. While this is subject to benchmarking and testing, something like this would be possible:
The tradeoff will also have to take account the complexity of managing different branching factor.
Currently, we invalidate a node by traversing from the leaf node to the root node and nullifying each node.
For a radix trie with fixed branching factor, we can directly access each level and nullify them. Because there are less level to traverse through, it is also more efficient than a binary tree.
Expected Result
With the proposed changes, we can see the following improvements to Asterisc.
Impact on binary
Detailed implementation
Consider a memory where
For an address 0x1234567890123ABC, we use the 0x1234567890123 as the page key, and 0xABC as the page address. Since we’re using radix tries to represent each page key, we can lay out the radix trie like following:
0x1234567890123 = 0b0001001000110100010101100111100010010000000100100011
Each node has children which can be up to
2^branchFactor
items. All of the children are merkleized to form a the root hash of the node.For example, radix node
0x123
can have up to 2^12 items. From those 4096 items, we must create the binary merkle tree, with intermediate nodes of the binary merkle tree as well. This results in a total of 8191 merkle hash in one radix node.These intermediate merkle hash will be used in proof generation, thus need to be cached. Following is the definition for a radix node.
To determine whether the hash is generated at a specific generalized index(gindex), we have
hashExists
which is a list of bits that is enabled when a value is set for that specific node.HashValid
is also a list of bits that is enabled when a valid hash is created at that position, and disabled when the node is invalidated.AllocPage
When a page is allocated, we can create a new radix node for every path from the root node to the leaf radix node.
Any other radix branch that is not initialized will be nil, and we can replace any nil branch with a pre-computed zeroHash.
Optimizing hash caches
Previously, we were using
nodes map[uint64]*[32]byte
as hashmap with a pointer value type, so we could denote:However, with a list of cache with plain
[32]byte
type, we need separate structure to remember which hashes are set, and which are valid.Here, we have
where
HashExists
stands for case b) in the above scenario. When the address is set through AllocPages, HashExists is turned on.HashValid
stands for case c) in the above scenario. When the hash is calculated, HashValid is turned on.In order to save space, these types are
[(1 << 12) / 64]uint64
where the whole node length (1 <<12) is divided among 64 equal uint64, where each node is identified a unique bit in the list of uint64.Since 1 << 12 is equally divided by 64, we can use the following calculations
hashIndex := gindex >> 6
: to get the index of the outer uint64’s indexhashBit := gindex & 63
: to get the bit position in the uint64.MerkleizeNode
The overall method of merkleizing the node doesn’t change. We are merkleizing the merkle tree with respect to a specific gindex.
We start from gindex=1 (the top of the tree), to the bottom of the tree by incrementing the gindex by a factor of 2. This allows us to only traverse through the trie once vertically.
Our merkle trie is composed of 5 separate levels of merkle node.
Each individual merkle node may have different branching factor therefore different children/hashes. This will result in different statically determined types of merkle node. Each level will have specific merkleizeNode function.
We can traverse down the radix nodes by creating a statically deterministic branch path from that address.
At the final radix level, the leaf nodes represent the pages.
MerkleProof
When creating a proof for
0x1234567890123ABC
the flow is as follows:0xABC
At each level of the merkle trie, we can go through each level of binary tree hash, and collect the sibling hash
We can run the above code only once for every branch depth.
Invalidation
For invalidation, if address
0x1234567890123ABC
is invalidated, we need to invalidate 5 radix node at each level, as well as all of the intermediate merkle hashes they have created.Tests and Benchmarks
This change should not break any of the existing tests.
We must benchmark for the following items:
See https://github.com/ethereum-optimism/asterisc/blob/6229f246175130e537a8c7fd1ac63b7a9bac303e/docs/radix-memory.md for detailed note on testing radix trie performance.