Merkle Mountain Range Multi Proofs

Seun Lanlege — Mad scientist

Merkle Mountain Range Multi Proofs

Merkle mountain ranges[1]^{[1]} are an improvement over conventional merkle trees for growing, potentially unbounded lists. Where conventional merkle tree constructions over growing lists prove very inefficient to compute, as all nodes in the tree must be recomputed. Merkle mountain ranges amortise this cost by growing subtrees incrementally and merging subtrees at the same height, rather than growing the full tree.

Merkle mountain ranges, true to their name, are a type of merkle tree composed of perfectly balanced binary subtrees (each subtree has 2k2^k leaves for some kk) arranged in decreasing height — forming a mountain range silhouette. This construction provides two key benefits over conventional merkle trees:

  • Efficient appends. Inserting a leaf into a conventional merkle tree requires recomputing O(n)O(n) nodes (every node on the path from the new leaf to the root, plus the root changes position). In an MMR, appending a leaf is amortized O(logn)O(\log n) — only the rightmost subtrees may need merging.
  • Compact proofs over large sets. A proof for a single leaf requires at most hh nodes for the subtree proof (where hh is the subtree’s height) plus at most hh peak hashes for bagging — still O(logn)O(\log n) total.

Capacity

An MMR where the tallest subtree has height hh can hold subtrees of every smaller height as well. When all heights from hh down to 00 are present, the total leaf count is:

n=2h+2h1++20=2h+11n = 2^h + 2^{h-1} + \cdots + 2^0 = 2^{h+1} - 1

A conventional merkle tree of height hh holds exactly 2h2^h leaves. So an MMR with the same maximum proof depth holds nearly twice as many leaves:

Height hhMax leavesMax proof size
Merkle treehh2h2^hhh
MMRhh2h+112^{h+1} - 12h2h (subtree + peaks)

The MMR proof is at most twice as large, but this is a constant factor — both are O(logn)O(\log n).

MMR Multi Proofs

Our approach to verifying mmr multi proofs is to decompose the MMR into its constituent subtrees and verify each one independently using the position-based indexing model defined in my previous article.

An MMR with nn leaves decomposes into subtrees whose sizes are the powers of 2 in the binary representation of nn:

n=i2biwhere b0>b1>>bk1 are the set bits of nn = \sum_{i} 2^{b_i} \quad \text{where } b_0 > b_1 > \cdots > b_{k-1} \text{ are the set bits of } n

The number of subtrees (peaks) equals popcount(n)\text{popcount}(n).

MMR structure

Each subtree is treated as a standalone merkle tree with 1-based position indexing. Within a subtree, leaf indices are converted to tree positions and siblings are paired bottom-up, consuming proof nodes as needed for missing siblings.

Proof Schema

The MMR proof uses a minimal format. The proof is a flat array of hashes, and each leaf carries its 0-based index across the entire MMR. Positions within subtrees are inferred from leafCount.

Verification Algorithm

The algorithm proceeds in three phases:

Phase 1: Decompose into subtrees. Walk through the binary representation of nn, peeling off the largest power-of-2 subtree at each step. Partition the leaves into each subtree by their index.

Phase 2: Compute subtree roots. For each subtree, convert leaf indices to 1-based tree positions and walk up level by level. At each level, check if the next node is a sibling (pos1\text{pos} \oplus 1). If both siblings are known, hash them together; otherwise, consume a proof element as the missing sibling.

Phase 3: Bag the peaks. Combine the subtree roots right-to-left: root=H(H(S3,S2),S1)\text{root} = H(H(S_3, S_2), S_1).

Subtree Root Calculation

Each subtree is a perfect binary tree verified independently by converting leaf indices to tree positions and walking up:

Algorithm 1 Subtree Root Calculation

Require: leaves LL, proof hashes PP, position offset

Ensure: peak root hash

1:for each lLl \in L do

2:l.posoffset+l.indexl.\text{pos} \gets \text{offset} + l.\text{index}

3:end for

4:repeat

5:next\textit{next} \gets \emptyset

6:for each node vLv \in L do

7:if next node ww is sibling (v.pos1v.\text{pos} \oplus 1) then

8:nextnext{(v.pos/2, Hpair(v,w))}\textit{next} \gets \textit{next} \cup \{(\lfloor v.\text{pos}/2 \rfloor,\ H_{\text{pair}}(v, w))\}

9:skip ww

10:else

11:sP.next()s \gets P.\text{next}()

12:nextnext{(v.pos/2, Hpair(v,s))}\textit{next} \gets \textit{next} \cup \{(\lfloor v.\text{pos}/2 \rfloor,\ H_{\text{pair}}(v, s))\}

13:end if

14:end for

15:LnextL \gets \textit{next}

16:until L[0].pos=1L[0].\text{pos} = 1

17:return L[0].hashL[0].\text{hash}

HpairH_{\text{pair}} orders the hash inputs by position — even positions are left children, odd are right:

Hpair(v,s)={H(v.hashs.hash)if v.pos is evenH(s.hashv.hash)if v.pos is oddH_{\text{pair}}(v, s) = \begin{cases} H(v.\text{hash} \| s.\text{hash}) & \text{if } v.\text{pos} \text{ is even} \\ H(s.\text{hash} \| v.\text{hash}) & \text{if } v.\text{pos} \text{ is odd} \end{cases}

MMR Verification

With SubtreeRoot defined, we can now describe the full MMR verification:

Algorithm 2 MMR Multi Proof Verification

Require: proof hashes PP, leaves LL with 0-based indices, leaf count nn

Ensure: root hash of the MMR

1:peaks\textit{peaks} \gets \emptyset

2:remainingn\textit{remaining} \gets n, start0\textit{start} \gets 0

3:while remaining0\textit{remaining} \neq 0 do

4:hlog2(remaining)h \gets \lfloor \log_2(\textit{remaining}) \rfloor

5:s2hs \gets 2^h

6:remainingremainings\textit{remaining} \gets \textit{remaining} - s

7:startstart+s\textit{start} \gets \textit{start} + s

8:SS \gets leaves with index <start< \textit{start}

9:if S=0|S| = 0 then

10:peakspeaks{P.next()}\textit{peaks} \gets \textit{peaks} \cup \{P.\text{next}()\}

11:else if S=1|S| = 1 and h=0h = 0 then

12:peakspeaks{S[0].hash}\textit{peaks} \gets \textit{peaks} \cup \{S[0].\text{hash}\}

13:else

14:peakspeaks{SubtreeRoot(S,P,2sstart)}\textit{peaks} \gets \textit{peaks} \cup \{\text{SubtreeRoot}(S, P, 2s - \textit{start})\}

15:end if

16:end while

17:if L0|L| \neq 0 then

18:reject // LL contains indices n\geq n

19:end if // bag peaks right-to-left

20:while peaks>1|\textit{peaks}| > 1 do

21:rpeaks.pop()r \gets \textit{peaks}.\text{pop}(), lpeaks.pop()l \gets \textit{peaks}.\text{pop}()

22:peaks.push(H(r,l))\textit{peaks}.\text{push}(H(r, l))

23:end while

24:return peaks[0]\textit{peaks}[0]

Reference Implementation

A production Solidity implementation of this algorithm is available at polytope-labs/solidity-merkle-trees.

References

[1]^{[1]} P. Todd. Merkle Mountain Ranges. GitHub, 2012. https://opentimestamps.org.