<?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>Forem: Galadd</title>
    <description>The latest articles on Forem by Galadd (@galadd).</description>
    <link>https://forem.com/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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1080374%2Fac21e2e0-edc3-4b7b-ab69-91345352929b.jpeg</url>
      <title>Forem: Galadd</title>
      <link>https://forem.com/galadd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/galadd"/>
    <language>en</language>
    <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://forem.com/galadd/optimizing-persistent-storage-with-state-deltas-4emp</link>
      <guid>https://forem.com/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>
