Merkle Multi Proofs
Merkle multi proofs enable more efficient merkle proofs by re-using the intermediate nodes shared by the proof leaves during the recalculation of the root hash of the tree. In order to understand the benefits provided by merkle multi proofs, consider proving leaf L0 (position 8) individually — we need 3 proof nodes: its sibling (9), its uncle (5), and the right subtree root (3):
Now consider proving leaf L4 (position 12) individually — again 3 proof nodes: its sibling (13), its uncle (7), and the left subtree root (2):
Proving both leaves separately requires proof nodes in total. But notice that node (needed to prove L0) is the ancestor of L4, and node (needed to prove L4) is the ancestor of L0. In a merkle multi proof, these intermediate nodes are computed during verification rather than supplied — so the combined proof needs only proof nodes instead of :
This scheme brings significant space savings when proving the existence of multiple items in a merkle tree, and the savings grow as more leaves share intermediate ancestors. In this article, we introduce a custom proof format that gives us computational savings at the cost of additional space complexity for execution environments where the cost of execution may be too high.
Position-Based Indexing
We use a 1-based position numbering scheme where the root node is at position :
Even positions are always left children, odd positions are always right children. The sibling of any node is found by toggling the least significant bit.
Given leaves in the tree, the height is and the first leaf position is . A leaf at 0-based index maps to tree position .
Unbalanced Trees
In practice, the number of leaves in a merkle tree is rarely a power of 2. A tree with leaves has height , which means the leaf level has slots but only are occupied. The rightmost positions at each level may have no sibling:
When walking up from a node whose sibling doesn’t exist (e.g. position 12 has sibling , but position 13 is beyond the tree), the node is promoted unchanged to the parent level — no hashing occurs. Position 12 promotes to become position 6, which itself has no sibling (position 7 doesn’t exist), so it promotes again to position 3.
To detect this, the verifier tracks the number of valid nodes at each level. Starting from at the leaf level and computing for each level above, the last valid position at any level is:
A sibling at position does not exist in the tree.
Proof Schema
Each leaf carries its 0-based index and hash. The proof is a flat array of sibling hashes — no position metadata. The verifier derives positions internally from the leaf indices and leafCount.
Verification Algorithm
The algorithm converts leaf indices to 1-based tree positions (), then walks up the tree level by level. At each level, for each node it resolves the sibling:
- Sibling in the working set — the next node has position , hash them together
- Sibling in the proof — consume the next proof element as the sibling
- Sibling doesn’t exist — unbalanced tree edge, promote unchanged (see above)
Algorithm 1 Merkle Multi Proof Verification
Require: proof hashes , leaves with 0-based indices, leaf count
Ensure: root hash of the merkle tree
1:
2:for each leaf do
3:
4:end for
5:
6:repeat
7:
8:
9:for each node do
10:if next node is sibling () then
11:
12:skip
13:else if then // sibling exists in tree
14:
15:else // unbalanced edge
16:
17:end if
18:
19:end for
20:
21:
22:until
23:return
orders the hash inputs by position — even positions are left children, odd are right:
The working set is written in-place, shrinking each iteration.
Reference Implementation
A production Solidity implementation of this algorithm is available at polytope-labs/solidity-merkle-trees.