<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Galadd</title>
    <description>The latest articles on DEV Community by Galadd (@galadd).</description>
    <link>https://dev.to/galadd</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1080374%2Fac21e2e0-edc3-4b7b-ab69-91345352929b.jpeg</url>
      <title>DEV Community: Galadd</title>
      <link>https://dev.to/galadd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/galadd"/>
    <language>en</language>
    <item>
      <title>Cutting Beacon State Storage by ~97%: Validator Sectors with Structural Sharing</title>
      <dc:creator>Galadd</dc:creator>
      <pubDate>Fri, 02 Oct 2026 10:54:18 +0000</pubDate>
      <link>https://dev.to/galadd/cutting-beacon-state-storage-by-97-validator-sectors-with-structural-sharing-3p5c</link>
      <guid>https://dev.to/galadd/cutting-beacon-state-storage-by-97-validator-sectors-with-structural-sharing-3p5c</guid>
      <description>&lt;p&gt;&lt;em&gt;A design prototype. I’d like your feedback before I build the reconstruction measurements.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;Storing full beacon states at regular intervals costs roughly 3 TB. Diffing against a base reference (eth-state-diff) brings that down to about 400 GB. But ~85% of a beacon state is the &lt;code&gt;validators&lt;/code&gt; list, which barely changes. By treating validators separately as &lt;strong&gt;sectors&lt;/strong&gt; of structurally shared chunks, plus layered &lt;strong&gt;checkpoints&lt;/strong&gt; of diffs, the estimate drops to &lt;strong&gt;~70 GB&lt;/strong&gt;, a reduction of roughly 97% from the naive approach.&lt;/p&gt;

&lt;p&gt;All numbers are &lt;strong&gt;maximum approximations&lt;/strong&gt;. This post is about the design prototype, not benchmarks.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;I’ve been looking into designs that reduce redundant on-disk data and speed up state reconstruction and historical state lookups. That work led to &lt;strong&gt;eth-state-diff&lt;/strong&gt;, which computes the difference between two beacon states and can be reapplied to reconstruct one from the other.&lt;/p&gt;

&lt;p&gt;It has a clear limitation: it always needs a base reference, and the farther the base is from the target, the larger the diff. A pure diff scheme is not a complete solution. It needs periodic full-state anchors (I’m thinking every 256 epochs).&lt;/p&gt;

&lt;p&gt;Full anchors are still wasteful, because most of each one is almost identical to the last.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The observation
&lt;/h2&gt;

&lt;p&gt;About &lt;strong&gt;85% of the beacon state is the &lt;code&gt;validators&lt;/code&gt; list&lt;/strong&gt;, and that list is &lt;em&gt;append-mostly&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New validators are appended, and the appends are clustered.&lt;/li&gt;
&lt;li&gt;In-place mutations are extremely rare—as few as &lt;strong&gt;0.015%&lt;/strong&gt; of validators.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the list were strictly append-only, a checkpoint would only need to store a length. It isn’t, but it’s close enough that storing a full copy every time is almost pure redundancy.&lt;/p&gt;

&lt;p&gt;So the design question is: &lt;strong&gt;how do you exploit near-100% redundancy?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Validators need their own representation.&lt;br&gt;
&lt;br&gt;&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The design
&lt;/h2&gt;

&lt;p&gt;Validators are stored in two cooperating layers: sectors (structural sharing) and checkpoints (diffs).&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Layer 1: Sectors (structural sharing)
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;sector&lt;/strong&gt; spans 8 checkpoints of 256 epochs each → &lt;strong&gt;2,048 epochs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At every sector boundary the full validators list is materialised, but not as a monolithic blob. It is split into &lt;strong&gt;chunks of 64 validators&lt;/strong&gt; (64 × 121 bytes ≈ 7.7 KB each). Each chunk is stored once and addressed by name (think content-addressed). A sector’s validator list is simply a &lt;strong&gt;list of chunk references&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The reference list is small (~250 KB), so a plain list is sufficient. A persistent tree would be over-engineering here.&lt;/li&gt;
&lt;li&gt;When validators change, only the affected chunks are written. Unchanged chunks are shared with earlier sectors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cost at a maximum of 150 changes per sector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;150 changes × 121 B × 64 (whole chunk copied) ≈ 1.2 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Appends are clustered, so they create few chunks (max ~25 KB). Across the full history the changed chunks total roughly &lt;strong&gt;300 MB&lt;/strong&gt; and the chunk-reference lists roughly &lt;strong&gt;57 MB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The tradeoff is intentional: a change to a single 8-byte field still copies its whole chunk, so this is not space-optimal in the small. But it never requires a deep copy of the validators list, and at this scale the overhead is tiny.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Layer 2: Checkpoints (layered diffs)
&lt;/h3&gt;

&lt;p&gt;Inside a sector, checkpoints record &lt;em&gt;diffs of the validators list&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every &lt;strong&gt;256 epochs&lt;/strong&gt; a “major” checkpoint stores a diff against the &lt;strong&gt;sector base&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Every &lt;strong&gt;32 epochs&lt;/strong&gt; a “minor” checkpoint stores a diff against the &lt;strong&gt;nearest preceding major checkpoint&lt;/strong&gt;.

&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Concrete layout (example at epoch 8768)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sector 0 (epoch 0): full validators (chunks)
  ├─ minor 32:   diff(32, sector 0)
  ├─ minor 64:   diff(64, sector 0)
  ├─ … 
  └─ major 256:  diff(256, sector 0)
       ├─ minor 288: diff(288, major 256)
       ├─ … 
       └─ major 512: diff(512, sector 0)
            …

Sector 8192 (epoch 8192): full validators (new ChunksRef list)
  ├─ minor 8224: diff(8224, sector 8192)
  ├─ …
  ├─ major 8448: diff(8448, sector 8192)
  ├─ …
  ├─ major 8704: diff(8704, sector 8192)
  └─ minor 8768: diff(8768, major 8704)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Reconstruction at epoch E
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Locate the sector base: &lt;code&gt;floor(E / 2048) × 2048&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Locate the nearest major checkpoint ≤ E: &lt;code&gt;floor(E / 256) × 256&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Locate the exact minor checkpoint (or the target epoch itself).&lt;/li&gt;
&lt;li&gt;Start from the sector’s full validators (resolved via its ChunksRef list).&lt;/li&gt;
&lt;li&gt;Apply the major-checkpoint diff.&lt;/li&gt;
&lt;li&gt;Apply the minor-checkpoint diff.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example for epoch 8768:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;validators@8768 = sector@8192 + diff@8704 + diff@8768
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;h2&gt;
  
  
  Estimated storage
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Naive: full state every 32 epochs&lt;/td&gt;
&lt;td&gt;~3 TB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diffs every 32 epochs, base every 256&lt;/td&gt;
&lt;td&gt;~400 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;This design&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sector bases (full state without validators)&lt;/td&gt;
&lt;td&gt;~60 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Changed validator chunks&lt;/td&gt;
&lt;td&gt;~300 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chunk references&lt;/td&gt;
&lt;td&gt;~57 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validator patches / minor diffs&lt;/td&gt;
&lt;td&gt;~550 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diffs (remaining state + major checkpoints)&lt;/td&gt;
&lt;td&gt;~10 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~70 GB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;≈ &lt;strong&gt;97.7 % reduction&lt;/strong&gt; versus the naive approach and ≈ &lt;strong&gt;82 % reduction&lt;/strong&gt; versus plain eth-state-diff.&lt;br&gt;
&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What I still need to measure
&lt;/h3&gt;

&lt;p&gt;Storage is only half the story. I will look into concrete reconstruction-cost numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Latency vs distance from the sector base&lt;/li&gt;
&lt;li&gt;Bytes read per reconstruction&lt;/li&gt;
&lt;li&gt;Number of diffs applied and chunks fetched

&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Feedback wanted
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Is a 64-validator chunk the right granularity? Smaller chunks copy less on mutation but grow the reference list.&lt;/li&gt;
&lt;li&gt;Has anyone seen similar layered structural-sharing + diff approaches in other clients’ state stores?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All figures above are max approximates. The focus is the design prototype.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>blockchain</category>
      <category>performance</category>
    </item>
    <item>
      <title>Optimizing Persistent Storage with State Deltas</title>
      <dc:creator>Galadd</dc:creator>
      <pubDate>Thu, 22 Jan 2026 11:20:28 +0000</pubDate>
      <link>https://dev.to/galadd/optimizing-persistent-storage-with-state-deltas-4emp</link>
      <guid>https://dev.to/galadd/optimizing-persistent-storage-with-state-deltas-4emp</guid>
      <description>&lt;p&gt;Optimizing persistent storage through deltas might sound like a general solution—but this article is intentionally specific. This approach worked because I had full control over the data model and could make assumptions about how the data evolves over time.&lt;/p&gt;

&lt;p&gt;That won’t always be the case. You might not be able to reshape your data this way, or your workload might look completely different. Still, optimization often comes down to understanding your data deeply and being willing to look at it from unconventional angles. Even if you don’t apply this exact approach, I hope it offers a useful perspective.&lt;/p&gt;

&lt;p&gt;The idea itself is simple—almost offensively so. There’s nothing particularly novel or academically complex here. But sometimes, stepping away from heavy abstractions and focusing on the shape of your data can yield outsized wins.&lt;/p&gt;

&lt;p&gt;So I’ll get straight to the point.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;Every 3.5 hours, a full system state was written to disk. Even after compression, each snapshot was roughly &lt;strong&gt;140 MB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This works, but it doesn’t scale. A new node syncing the network would need to download and store every historical snapshot. Over time, this balloons into terabytes of required disk space.&lt;/p&gt;

&lt;p&gt;To put concrete numbers on it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Before&lt;/strong&gt;: ~10 TB of required storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After&lt;/strong&gt;: ~2 TB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s roughly an &lt;strong&gt;80% reduction&lt;/strong&gt; in disk usage.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;em&gt;Why Not Just Snapshot Less Often?&lt;/em&gt;
&lt;/h4&gt;

&lt;p&gt;Although a new state exists every 12 seconds, not all states are equal. Each state contains specific information that allows reconstruction of prior states and verification of the chain’s history.&lt;/p&gt;

&lt;p&gt;As snapshots get farther apart, reconstructing intermediate states becomes increasingly expensive. The farther a target state is from the nearest snapshot, the more work is required to replay or reconstruct it. This directly impacts sync time and verification latency.&lt;/p&gt;

&lt;p&gt;Snapshots therefore can’t be spaced arbitrarily far apart without shifting cost elsewhere. There’s a practical lower bound on snapshot frequency if you want reasonable reconstruction times.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  The Core Idea: State Deltas
&lt;/h3&gt;

&lt;p&gt;The key observation was simple: consecutive full states written only 3.5 hours apart are extremely similar.&lt;/p&gt;

&lt;p&gt;Instead of storing the entire state every time, I switched to storing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One &lt;strong&gt;base state&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Followed by &lt;strong&gt;deltas&lt;/strong&gt;, which encode only the differences between states&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a full state is needed, the base state is loaded and the deltas are applied sequentially.&lt;/p&gt;

&lt;p&gt;This slightly changes complexity—from pure &lt;code&gt;O(x)&lt;/code&gt; reads to &lt;code&gt;O(x + delta_application)&lt;/code&gt;—but in practice, the tradeoff is well worth it. Applying a delta took roughly ~&lt;strong&gt;200 ms&lt;/strong&gt;, which was acceptable for this workload.&lt;/p&gt;

&lt;p&gt;The result is dramatically less data written to disk and significantly less data read during sync.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  Avoiding Disk Reads: Caching the Base State
&lt;/h4&gt;

&lt;p&gt;Initially, delta computation was slower than expected. The reason was obvious in hindsight: computing a delta requires access to both the base state and the new state.&lt;/p&gt;

&lt;p&gt;My first implementation read the base state from disk every time. That was a mistake.&lt;/p&gt;

&lt;p&gt;If something is accessed repeatedly and fits comfortably in memory, it should be cached.&lt;/p&gt;

&lt;p&gt;Each base state is reused for roughly &lt;strong&gt;7 subsequent deltas&lt;/strong&gt;, making caching an easy win. I introduced a simple in-memory cache using an &lt;code&gt;Arc&amp;lt;RwLock&amp;lt;Option&amp;lt;_&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;base_state_cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;RwLock&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The logic is straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the base state is cached, reuse it&lt;/li&gt;
&lt;li&gt;Otherwise, load it once from disk and cache it&lt;/li&gt;
&lt;li&gt;Subsequent accesses should not hit disk again
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;load_base_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;H256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BeaconState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.load_cached_base_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.state_by_block_root&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.set_cached_base_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this change, both read and write performance improved significantly.&lt;/p&gt;

&lt;p&gt;Overall, the new approach reduced time spent on state storage by roughly &lt;strong&gt;70%&lt;/strong&gt;, while also drastically reducing disk usage.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  A Closer Look: Optimizing Balances
&lt;/h3&gt;

&lt;p&gt;One field deserved special treatment: &lt;strong&gt;balances&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Balances are stored as a Merkle tree containing over &lt;strong&gt;2,000,000&lt;/strong&gt; &lt;code&gt;u64&lt;/code&gt; &lt;strong&gt;values&lt;/strong&gt;, and roughly &lt;strong&gt;1,000,000 of them change&lt;/strong&gt; between states. At first glance, this suggests that storing half the values is unavoidable—but that assumption turns out to be wrong.&lt;/p&gt;

&lt;p&gt;The key lesson here is simple: &lt;strong&gt;know your data&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;After analyzing balance changes, four patterns emerged:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Many values don’t change&lt;/li&gt;
&lt;li&gt;Many values change to zero&lt;/li&gt;
&lt;li&gt;Many values change, but only by a small amount&lt;/li&gt;
&lt;li&gt;Very few values change by a large amount&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once this distribution is clear, storing every changed value as a full &lt;code&gt;u64&lt;/code&gt; becomes wasteful.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  Encoding Changes with 2 Bits
&lt;/h4&gt;

&lt;p&gt;Each balance change can be classified into one of four categories, meaning it can be represented using &lt;strong&gt;2 bits&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I used a compact tag vector where each entry encodes the change type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;00&lt;/code&gt; → no change&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;10&lt;/code&gt; → set to zero&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;11&lt;/code&gt; → apply a small diff&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;01&lt;/code&gt; → set to a full target value&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four balance entries fit into a single byte:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;BitTagVec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 4 entries per byte&lt;/span&gt;
    &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;BitTagVec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="nf"&gt;.div_ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;#[inline]&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;byte&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;shift&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;0b11&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;#[inline]&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;byte&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;shift&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;0b11&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For ~2,000,000 balances, this tag vector alone takes roughly &lt;strong&gt;500 KB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The associated data is stored separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;BalanceDiffs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BitTagVec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;small_diffs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;target_values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Gwei&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;new_balances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Gwei&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;small_diffs&lt;/strong&gt; store small balance changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;target_values&lt;/strong&gt; store large or special-case updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;new_balances&lt;/strong&gt; represent newly added balances
Applying the delta is a linear pass over the tag vector, applying the corresponding operation. The logic is predictable, cache-friendly, and fast.



&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;This work started as a practical optimization and became a reminder of something fundamental: effective storage optimization often has less to do with clever algorithms and more to do with understanding your data.&lt;/p&gt;

&lt;p&gt;By switching from full-state storage to delta-based storage—and by tailoring the delta format to how the data actually changes—I was able to significantly reduce both disk usage and runtime costs.&lt;/p&gt;

&lt;p&gt;If you have ideas on how this approach could be improved, or alternative ways you would model these deltas, I’d love to hear your thoughts. The full PR is available here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/grandinetech/grandine/pull/523/files" rel="noopener noreferrer"&gt;https://github.com/grandinetech/grandine/pull/523/files&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Writing this was also an attempt to write more and engage more openly. Constructive critique—technical or otherwise—is very welcome.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>database</category>
    </item>
  </channel>
</rss>
