<?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: Chad</title>
    <description>The latest articles on DEV Community by Chad (@cleanfixdev).</description>
    <link>https://dev.to/cleanfixdev</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%2F3993371%2Fc7817ef8-65de-40d6-8756-f7162d687961.jpg</url>
      <title>DEV Community: Chad</title>
      <link>https://dev.to/cleanfixdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cleanfixdev"/>
    <language>en</language>
    <item>
      <title>The Reorg Recovery Code Was There. Production Never Called It.</title>
      <dc:creator>Chad</dc:creator>
      <pubDate>Sat, 08 Aug 2026 02:08:35 +0000</pubDate>
      <link>https://dev.to/cleanfixdev/the-reorg-recovery-code-was-there-production-never-called-it-41k2</link>
      <guid>https://dev.to/cleanfixdev/the-reorg-recovery-code-was-there-production-never-called-it-41k2</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5e2xe3proypu3rt6long.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5e2xe3proypu3rt6long.jpeg" alt="A broken amber ledger branch being repaired into a canonical teal chain" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Soroban-Smart-Block-Explorer/Soroban-Smart-Block" rel="noopener noreferrer"&gt;Soroban Smart Block Explorer&lt;/a&gt; turns raw Soroban contract events on Stellar into human-readable activity. Its Node.js indexer polls RPC data, decodes events, stores them in PostgreSQL, and serves them to the explorer frontend.&lt;/p&gt;

&lt;p&gt;That makes chain reorganizations a correctness problem, not just a blockchain detail. When the canonical chain changes, events indexed from the abandoned branch must be removed and indexed again from the fork point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;The repository already had most of a reorganization recovery system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ledger hashes were recorded during indexing;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;checkForReorg()&lt;/code&gt; could compare stored hashes with the network;&lt;/li&gt;
&lt;li&gt;rollback code could remove orphaned rows; and&lt;/li&gt;
&lt;li&gt;an operator alert existed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the production daemon imported only &lt;code&gt;recordLedgerHash&lt;/code&gt;. It never called &lt;code&gt;checkForReorg()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;recordLedgerHash&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./reorgWorker.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The safety mechanism was present, but disconnected from the process that owned the ledger cursor. A reorganization could therefore leave orphaned events, hashes, and cursor state in disagreement until someone intervened.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn3665i5c7p4wczj88474.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn3665i5c7p4wczj88474.png" alt="Before and after control flow for reorganization recovery" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The merged fix is &lt;a href="https://github.com/Soroban-Smart-Block-Explorer/Soroban-Smart-Block/pull/503" rel="noopener noreferrer"&gt;PR #503: &lt;code&gt;fix(indexer): wire reorganization detection&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The core wiring now lives inside the daemon's existing polling loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;ledgersSinceReorgCheck&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;polledFrom&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ledgersSinceReorgCheck&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;REORG_CHECK_INTERVAL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;forkLedger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;checkForReorg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ledgersSinceReorgCheck&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;forkLedger&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;_cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;forkLedger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;forkLedger&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chain reorganization detected; cursor rewound&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;continue&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rollback now makes the database cleanup and durable cursor rewind one operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BEGIN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DELETE FROM events WHERE ledger &amp;gt;= $1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;forkLedger&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DELETE FROM ledger_hashes WHERE ledger &amp;gt;= $1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;forkLedger&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`INSERT INTO daemon_state (key, value) VALUES ('cursor', $1)
   ON CONFLICT (key) DO UPDATE SET value = $1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;forkLedger&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;COMMIT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. I kept recovery single-flight
&lt;/h3&gt;

&lt;p&gt;A separate timer would be easy to add, but it could race &lt;code&gt;indexLedger()&lt;/code&gt; while both paths read and write the daemon cursor. The reorg check now runs synchronously in the same loop that owns &lt;code&gt;_cursor&lt;/code&gt;. Recovery either completes and rewinds the loop, or normal indexing advances and persists the next cursor.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. I made the scan bounded without creating a detection gap
&lt;/h3&gt;

&lt;p&gt;The default cadence is 100 ledgers and the supported reorg depth is another 100. The checker therefore asks for at most:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lookback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;checkInterval&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;maxDepth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That matters during catch-up. A large polling span can trigger a check, but it must not accidentally turn one pass into an unbounded SQL query and a long sequence of RPC calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. I select the earliest mismatch
&lt;/h3&gt;

&lt;p&gt;Stored hashes are returned newest first. Returning on the first mismatch would choose the newest orphaned ledger and leave older orphaned rows behind. The checker scans the bounded window and keeps the minimum mismatching ledger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;earliestFork&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;earliestFork&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;ledgerNumber&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;earliestFork&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ledgerNumber&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. I made rollback atomic
&lt;/h3&gt;

&lt;p&gt;Deleting events, deleting hashes, and saving the rewind cursor now share one PostgreSQL transaction. If any statement fails, the transaction rolls back. A crash cannot leave half-cleaned chain data paired with a cursor that points somewhere else.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. I kept alerting off the critical path
&lt;/h3&gt;

&lt;p&gt;Operators should hear about a reorg, but a notification outage should not prevent data recovery. The rollback completes first; alert delivery is best effort; the detected fork is still returned to the main loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. I added focused seams and coverage
&lt;/h3&gt;

&lt;p&gt;The checker accepts injected hash lookup, rollback, alert, cadence, and depth dependencies. That made the hard cases testable without hiding production behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cadence plus maximum-depth lookback;&lt;/li&gt;
&lt;li&gt;the deepest supported fork behind the prior check boundary;&lt;/li&gt;
&lt;li&gt;rollback succeeding even when alert delivery fails;&lt;/li&gt;
&lt;li&gt;orphaned event and hash deletion;&lt;/li&gt;
&lt;li&gt;durable cursor rewind; and&lt;/li&gt;
&lt;li&gt;transaction rollback on database failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;The final patch changed seven files: 278 additions and 41 deletions. I verified it with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;node --check&lt;/code&gt; on every changed or added JavaScript file;&lt;/li&gt;
&lt;li&gt;JSON parsing for &lt;code&gt;indexer/package.json&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git diff --check&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;a dependency-free boundary/depth/alert-failure smoke (2/2);&lt;/li&gt;
&lt;li&gt;a dependency-free transaction commit/rollback smoke (2/2); and&lt;/li&gt;
&lt;li&gt;an independent post-fix review that returned no findings after several correctness issues were addressed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I did &lt;strong&gt;not&lt;/strong&gt; claim a full local Jest/PostgreSQL run. The isolated checkout did not contain &lt;code&gt;node_modules&lt;/code&gt; or a local test database, so those tests were added for CI rather than simulated. Hosted CI was also blocked by the same unrelated Rust dead-code failure already present on the exact base commit, plus a fork-token permission job. No reorg file caused those failures, and the maintainer merged the PR after review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Soroban-Smart-Block-Explorer/Soroban-Smart-Block/pull/503" rel="noopener noreferrer"&gt;PR #503&lt;/a&gt; merged on July 20, 2026, and &lt;a href="https://github.com/Soroban-Smart-Block-Explorer/Soroban-Smart-Block/issues/489" rel="noopener noreferrer"&gt;issue #489&lt;/a&gt; closed with it.&lt;/p&gt;

&lt;p&gt;The interesting part was not writing a brand-new recovery system. It was finding that the system already existed in pieces, then connecting those pieces without introducing a cursor race, an unbounded scan, or a half-committed rollback.&lt;/p&gt;

&lt;p&gt;The bug was silent because nothing crashed. The code simply never asked the question it was built to answer.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>javascript</category>
      <category>devchallenge</category>
      <category>bugsmash</category>
    </item>
  </channel>
</rss>
