<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Eric Woolsey (0xForerunner) - Merkle Trees</title>
    <subtitle>Eric Woolsey (0xForerunner)</subtitle>
    <link rel="self" type="application/atom+xml" href="https://blog.woolsey.dev/tags/merkle-trees/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://blog.woolsey.dev"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-10-22T00:00:00+00:00</updated>
    <id>https://blog.woolsey.dev/tags/merkle-trees/atom.xml</id>
    <entry xml:lang="en">
        <title>The Cascading Merkle Tree</title>
        <published>2026-10-22T00:00:00+00:00</published>
        <updated>2026-10-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            Eric Woolsey
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://blog.woolsey.dev/blog/cascading-merkle-tree/"/>
        <id>https://blog.woolsey.dev/blog/cascading-merkle-tree/</id>
        
        <content type="html" xml:base="https://blog.woolsey.dev/blog/cascading-merkle-tree/">&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;📚 Source Code&lt;/strong&gt;: This article walks through &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https://github.com/worldcoin/semaphore-rs/pull/70&quot;&gt;the pull request&lt;/a&gt; that introduced &lt;code&gt;CascadingMerkleTree&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The usual way to store a binary tree in an array is a heap. The root goes at index 1, and the children of node &lt;em&gt;i&lt;/em&gt; go at 2&lt;em&gt;i&lt;/em&gt; and 2&lt;em&gt;i&lt;/em&gt; + 1. It’s compact and fast, and you never chase a pointer. The catch is that when the tree gets one level taller, every node ends up at a new index.&lt;/p&gt;
&lt;p&gt;That’s fine for a tree whose size never changes. It’s a real problem for a fixed-depth Merkle tree that lives on disk and keeps growing. This post covers a layout where growing the tree just means appending to the array, and explains why that leads to some unusual-looking indices.&lt;/p&gt;
&lt;h2 id=&quot;what-we-want&quot;&gt;What we want&lt;/h2&gt;
&lt;p&gt;Sometimes a Merkle tree’s depth isn’t yours to choose. Whatever consumes the proofs expects a set number of siblings, say 30, and that number holds from the first leaf to the billionth. There’s no “small tree for now, bigger tree later”.&lt;/p&gt;
&lt;p&gt;A depth-30 tree has 2³⁰ leaves and 2³¹ − 1 nodes. With 256-bit (32-byte) hashes, a fully materialized tree comes to about &lt;strong&gt;64 GiB&lt;/strong&gt;. Almost all of it is the hash of nothing, hashed with more nothing, all the way up.&lt;/p&gt;
&lt;p&gt;So we want:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fixed depth.&lt;/strong&gt; Proofs always have 30 levels.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dense storage.&lt;/strong&gt; A flat array, not a forest of &lt;code&gt;Box&lt;/code&gt;es. We need fast proofs and fast bulk inserts, and we want to &lt;code&gt;mmap&lt;/code&gt; the whole thing and pick up right where we left off after a restart.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Growable.&lt;/strong&gt; Pay for the leaves we have, not the leaves we might have someday.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The usual compromise gets two out of three. You allocate a dense bottom-left region whose size &lt;em&gt;you choose up front&lt;/em&gt; and put sparse, pointer-based nodes above it. Pick too small and you spill into the slow part. Pick too large and you’re back to paying for nothing.&lt;/p&gt;
&lt;h2 id=&quot;why-the-heap-layout-bites&quot;&gt;Why the heap layout bites&lt;/h2&gt;
&lt;p&gt;The heap is the obvious way to get dense storage, so let’s grow one. If the tree is full and you push one more leaf, the tree needs another level. The old root stops being the root and becomes the left child of a new one. And since a heap index encodes &lt;em&gt;depth from the top&lt;/em&gt;, every node in the old tree gets a new index.&lt;/p&gt;
&lt;figure class=&quot;cmt-fig&quot; data-fig=&quot;heap-shuffle&quot;&gt;
&lt;figcaption&gt;Figure 1. A heap-indexed tree before and after growing by one level. Colors follow each node. Leaf 0 was at index 4 and is now at index 8, and the old root moved from 1 to 2. Nothing stayed put.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;On a toy tree, that’s a shrug. On a memory-mapped file holding tens of millions of hashes, it means rewriting the entire file while everything else waits. That’s the kind of operation that looks great in a unit test and much less great on a pager at 3am.&lt;/p&gt;
&lt;p&gt;We need a layout where growing the tree means &lt;em&gt;appending&lt;/em&gt; to the array and nothing else.&lt;/p&gt;
&lt;h2 id=&quot;two-observations-and-one-free-slot&quot;&gt;Two observations and one free slot&lt;/h2&gt;
&lt;p&gt;First, some vocabulary. A node’s &lt;em&gt;height&lt;/em&gt; is how many levels it sits above the leaves, so leaves are at height 0. A &lt;em&gt;subtree&lt;/em&gt; is a node together with everything below it. Pick a node at height &lt;em&gt;h&lt;/em&gt; and you’ve picked a subtree of height &lt;em&gt;h&lt;/em&gt;: a complete binary tree with 2ʰ leaves of its own, with that node as its root. The whole tree is just the subtree rooted at the top.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Observation one: emptiness is cheap.&lt;/strong&gt; Every empty subtree of height &lt;em&gt;h&lt;/em&gt; has the same hash. Call it Zₕ. Z₀ is the empty leaf value, and Zₕ₊₁ = hash(Zₕ, Zₕ). We can precompute all 31 of them once (the PR calls this the &lt;code&gt;sparse_column&lt;/code&gt;). Now suppose our real data fits in a subtree of height &lt;em&gt;h&lt;/em&gt; in the bottom-left corner. The root of the full depth-30 tree is just that subtree’s root hashed against Zₕ, then Zₕ₊₁, and so on up to the top. That’s 30 − &lt;em&gt;h&lt;/em&gt; hashes, and none of it has to be stored.&lt;/p&gt;
&lt;p&gt;So we only store the bottom-left subtree. The real question is how to &lt;em&gt;grow&lt;/em&gt; it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Observation two: grow the tree sideways, not downward.&lt;/strong&gt; When the stored subtree of height &lt;em&gt;h&lt;/em&gt; fills up, make it the left child of a new node at height &lt;em&gt;h&lt;/em&gt; + 1. That new node’s right child is a brand-new, empty subtree of height &lt;em&gt;h&lt;/em&gt;. The leftmost path, from leaf 0 straight up, becomes a &lt;em&gt;spine&lt;/em&gt;, and each spine node has a perfectly ordinary complete subtree hanging off its right side.&lt;/p&gt;
&lt;p&gt;Each of those right subtrees is a complete binary tree, so it gets the textbook heap layout. That raises the question of where the spine node goes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The free slot.&lt;/strong&gt; A 1-indexed heap of size 2ᵏ stores its nodes in slots 1 through 2ᵏ − 1, and slot 0 goes to waste. Every data structures course has a footnote apologizing for it.&lt;/p&gt;
&lt;p&gt;So here’s the trick. &lt;strong&gt;Array chunk [2ᵏ, 2ᵏ⁺¹) holds the spine node at height &lt;em&gt;k&lt;/em&gt; in its wasted slot 0, followed by the heap for its right subtree.&lt;/strong&gt; Doubling the array appends exactly one new chunk, which is exactly one new spine node plus its new right subtree. The old chunks never move.&lt;/p&gt;
&lt;p&gt;That leaves the very first slot of the whole array, index 0, which nobody wanted. The tree keeps the leaf count there.&lt;/p&gt;
&lt;figure class=&quot;cmt-fig&quot; data-fig=&quot;cascade-index&quot;&gt;
&lt;figcaption&gt;Figure 2. Cascading indices for an 8-leaf tree. The spine (1, 2, 4, 8) runs up the left edge. Each array chunk [2ᵏ, 2ᵏ⁺¹) holds spine node 2ᵏ followed by the heap for its right subtree, and slot 0 holds the leaf count.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;see-it-move&quot;&gt;See it move&lt;/h2&gt;
&lt;p&gt;The figure plays on its own and loops. Pause it, or push leaves one at a time. The same tree is stored two ways, growing in lockstep: the cascading layout on top and a classic heap below. Every node and its array slot share a color, and hovering over a node highlights it in both layouts. Watch what happens each time the tree passes a power of two. The cascading array just gets longer. In the heap, every index moves to a new node and the whole array shuffles. Click a leaf number to trace its path to the root. The dashed rings mark the siblings that make up its Merkle proof. Above the stored part, the siblings are all Z values from the sparse column, so they cost nothing to read.&lt;/p&gt;
&lt;figure class=&quot;cmt-fig&quot; data-fig=&quot;grow&quot;&gt;
&lt;figcaption&gt;Figure 3. The same depth-5 tree (pretend it says 30), stored two ways. Top: the cascading layout, where the array grows by appending and each chunk is one spine node plus one complete subtree. Bottom: a classic heap, where every entry moves each time the tree grows a level. In both, solid nodes are stored in the array, and dashed nodes and triangles are computed from the sparse column.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;the-arithmetic&quot;&gt;The arithmetic&lt;/h2&gt;
&lt;p&gt;With that picture in mind, here are the index functions from &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https://github.com/worldcoin/semaphore-rs/pull/70/files&quot;&gt;&lt;code&gt;storage_ops.rs&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-6 z-l-i z-d-3 z-d-i&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-6 z-l-i z-d-3 z-d-i&quot;&gt; leaves are 0 indexed&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt;pub&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; fn&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt; index_from_leaf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;leaf&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-8&quot;&gt; usize&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; -&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-8&quot;&gt; usize&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;    leaf&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; +&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;leaf&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; +&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-9&quot;&gt; 1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;next_power_of_two&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Leaf &lt;em&gt;L&lt;/em&gt; lives in the right subtree whose leaves are [2ᵏ⁻¹, 2ᵏ), where 2ᵏ = &lt;code&gt;next_power_of_two(L + 1)&lt;/code&gt;. That subtree’s chunk starts at index 2ᵏ. Its bottom row is the back half of the chunk, starting at 2ᵏ + 2ᵏ⁻¹. Add &lt;em&gt;L&lt;/em&gt;’s offset within the row, &lt;em&gt;L&lt;/em&gt; − 2ᵏ⁻¹, and the 2ᵏ⁻¹ terms cancel. What’s left is 2ᵏ + &lt;em&gt;L&lt;/em&gt;. That’s why leaf 4 lands at index 8 + 4 = &lt;strong&gt;12&lt;/strong&gt;.&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt;pub&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; fn&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt; parent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-8&quot;&gt; usize&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; -&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-8&quot;&gt; usize&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt;    if&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt; i&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;is_power_of_two&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt;        return&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt; i&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; &amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-9&quot;&gt; 1&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span class=&quot;z-l-6 z-l-i z-d-3 z-d-i&quot;&gt;               //&lt;/span&gt;&lt;span class=&quot;z-l-6 z-l-i z-d-3 z-d-i&quot;&gt; spine: go up the spine&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-3 z-l-b z-d-6 z-d-b&quot;&gt;    let&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt; prev_pow&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt; i&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;next_power_of_two&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; &amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-9&quot;&gt; 1&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    (&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; -&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt; prev_pow&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; &amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-9&quot;&gt; 1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; +&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt; prev_pow&lt;/span&gt;&lt;span class=&quot;z-l-6 z-l-i z-d-3 z-d-i&quot;&gt; //&lt;/span&gt;&lt;span class=&quot;z-l-6 z-l-i z-d-3 z-d-i&quot;&gt; ordinary heap parent, inside this chunk&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A spine node’s parent is the next spine node. Every other node does a normal heap step within its own chunk: subtract the chunk base, shift, then add the base back. &lt;code&gt;sibling&lt;/code&gt; follows the same pattern, with two edge cases. A spine node’s sibling is the root of the right subtree next to it (2&lt;em&gt;i&lt;/em&gt; + 1). That right subtree’s root, in turn, has a spine node as its sibling (&lt;em&gt;prev_pow&lt;/em&gt; / 2).&lt;/p&gt;
&lt;p&gt;A proof walks up through the stored levels using those functions. It then tops off the remaining levels with entries from the sparse column, since everything to the right of the stored region is empty by definition.&lt;/p&gt;
&lt;h2 id=&quot;parallelism-with-the-borrow-checker-s-blessing&quot;&gt;Parallelism, with the borrow checker’s blessing&lt;/h2&gt;
&lt;p&gt;Building a tree from a big batch of leaves is the expensive part, so it should use every core. The layout makes that easy, and Rust makes it safe.&lt;/p&gt;
&lt;p&gt;Inside a chunk, the subtree is an ordinary heap, so each row of the subtree is a contiguous slice. Better still, the row of parents sits directly in front of the row of children. For the subtree’s local storage, the children at depth &lt;em&gt;j&lt;/em&gt; occupy [2ʲ, 2ʲ⁺¹), and their parents occupy [2ʲ⁻¹, 2ʲ). A single &lt;code&gt;split_at_mut&lt;/code&gt; hands us both rows as separate borrows:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-6 z-l-i z-d-3 z-d-i&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-6 z-l-i z-d-3 z-d-i&quot;&gt; Iterate over mutable layers of the tree&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt; current_depth&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; in&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-9&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt;..=&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;depth&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;rev&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-3 z-l-b z-d-6 z-d-b&quot;&gt;    let&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;top&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt; child_layer&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt; storage&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;split_at_mut&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-9&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; &amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt; current_depth&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-3 z-l-b z-d-6 z-d-b&quot;&gt;    let&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt; parent_layer&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; &amp;amp;&lt;/span&gt;&lt;span class=&quot;z-l-3 z-l-b z-d-6 z-d-b&quot;&gt;mut&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt; top&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-9&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; &amp;lt;&amp;lt;&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;current_depth&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; -&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-9&quot;&gt; 1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt;..&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;    parent_layer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt;        .&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;par_iter_mut&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt;        .&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;enumerate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt;        .&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;for_each&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt;|&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;i&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt; value&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt;|&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-3 z-l-b z-d-6 z-d-b&quot;&gt;            let&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt; left&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; &amp;amp;&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;child_layer&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-9&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; *&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt; i&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-3 z-l-b z-d-6 z-d-b&quot;&gt;            let&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt; right&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; &amp;amp;&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;child_layer&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-9&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; *&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt; i&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; +&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-9&quot;&gt; 1&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt;            *&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-8&quot;&gt; H&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-5&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;hash_node&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt;left&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt; right&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The borrow checker can’t prove that &lt;code&gt;storage[i]&lt;/code&gt; and &lt;code&gt;storage[2 * i]&lt;/code&gt; never overlap, so naive indexing into one &lt;code&gt;&amp;amp;mut [Hash]&lt;/code&gt; from many threads won’t compile. Nor should it. &lt;code&gt;split_at_mut&lt;/code&gt; settles the question by construction. &lt;code&gt;parent_layer&lt;/code&gt; and &lt;code&gt;child_layer&lt;/code&gt; are two non-overlapping slices, and the compiler knows it. Rayon’s &lt;code&gt;par_iter_mut&lt;/code&gt; then gives each thread exclusive &lt;code&gt;&amp;amp;mut&lt;/code&gt; access to its own parent slots. Meanwhile the child row is only ever read, so every thread can share it. That’s all the synchronization there is: no locks, no atomics, and no &lt;code&gt;unsafe&lt;/code&gt;. Each row is one parallel pass over two contiguous slices, working from the leaves up to the subtree root.&lt;/p&gt;
&lt;p&gt;Filling in the leaves works the same way, with a parallel &lt;code&gt;zip&lt;/code&gt; of the new leaves into the bottom row. The chunks are built one after another, each with its rows in parallel. Stitching them together along the spine costs one hash per chunk: the previous spine node hashed with the new subtree’s root.&lt;/p&gt;
&lt;h2 id=&quot;why-it-s-worth-the-weirdness&quot;&gt;Why it’s worth the weirdness&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Growth is append-only.&lt;/strong&gt; When the array fills, &lt;code&gt;push&lt;/code&gt; extends it to the next power of two with empty values. On an &lt;code&gt;MmapVec&lt;/code&gt; that means growing the file. Nothing gets rewritten.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parallel by construction.&lt;/strong&gt; Bulk construction is one safe, lock-free parallel pass per row, as shown above.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SIMD-friendly.&lt;/strong&gt; Every row is a contiguous slice, and its parents sit in another contiguous slice right in front of it. Each pass reads adjacent pairs and writes sequentially, which is exactly the access pattern that vectorized and batched hash implementations want.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It’s still just an array.&lt;/strong&gt; State is trivially serialized. A restart is an &lt;code&gt;mmap&lt;/code&gt; plus an optional &lt;code&gt;validate()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You pay for what you use&lt;/strong&gt;, rounded up to a power of two. You never pay for 64 GiB of nothing.&lt;/li&gt;
&lt;/ul&gt;
</content>
        
    </entry>
</feed>
