Ethereum

Ethereum

Chain Reorganizations

A chain reorganization is the moment Ethereum decides that a block your app already saw was the wrong one. The canonical chain switches to a competing branch, and one or more blocks at the head get dropped and replaced. On post-Merge Ethereum these are almost always shallow — a single block at the tip, reorged out before it was justified. They happen because two validators' views briefly disagree across a 12-second slot, usually from propagation latency, and the fork-choice rule then settles on one branch.

The crucial nuance for chain 1 is finality. Short head reorgs are possible before a block justifies, but once a block reaches the finalized tag, reverting it would force the protocol to slash a third of the validator set's stake — so deep reorgs of finalized history simply don't happen. The danger zone is the unfinalized tail. If your app credited a deposit, fired a webhook, or updated a balance off a block at latest that then gets reorged away, your state now reflects a transaction that no longer exists on the canonical chain. Handling that gap is what this guide is about.

Understanding Reorgs

  • What triggers one. Two validators can briefly build on different views of the head when a block propagates slowly across the network, or when a proposer is late. Gasper's fork-choice rule (LMD-GHOST with Casper FFG finality) then picks the branch with the most attestation weight, and the losing branch's tip is reorged out.
  • Shallow is the norm, deep is effectively ruled out. On Ethereum a reorg is typically one block deep, occasionally two, and lives entirely in the unfinalized tail. Reorgs of finalized blocks don't occur without mass slashing, so you're only ever defending against the short reorgs near latest, not a Bitcoin-style multi-block rewrite of settled history.
  • What it does to transactions. A reorged transaction returns to the mempool as pending — it isn't lost, but its block number, position, and gas outcome can all change when it's re-included, and it may pay a different effective fee. A receipt you fetched at latest can become stale; only inclusion at or below finalized is safe to treat as permanent.
  • Detecting it over JSON-RPC. There's no dedicated reorg event, so you infer it. The reliable signal is a parentHash mismatch: when a new head's parentHash no longer matches the recorded hash one block below it, or the hash at a height you already saved has since changed, the chain reorganized. Watching newHeads and re-checking finalized vs. latest gives you the same signal in near real time.

Implementation Examples

// Monitor for reorgs
const monitorReorgs = async (callback, checkInterval = 5000) => {
let lastBlock = await web3.eth.getBlockNumber();
let lastBlockHash = (await web3.eth.getBlock(lastBlock)).hash;
return setInterval(async () => {
const currentBlock = await web3.eth.getBlockNumber();
const currentBlockHash = (await web3.eth.getBlock(lastBlock)).hash;
if (currentBlockHash !== lastBlockHash) {
// Reorg detected
callback({
oldBlock: lastBlock,
oldHash: lastBlockHash,
newBlock: currentBlock,
newHash: currentBlockHash,
});
}
lastBlock = currentBlock;
lastBlockHash = currentBlockHash;
}, checkInterval);
};
// Get common ancestor block
const findCommonAncestor = async (hash1, hash2) => {
let block1 = await web3.eth.getBlock(hash1);
let block2 = await web3.eth.getBlock(hash2);
while (block1.number > block2.number) {
block1 = await web3.eth.getBlock(block1.parentHash);
}
while (block2.number > block1.number) {
block2 = await web3.eth.getBlock(block2.parentHash);
}
while (block1.hash !== block2.hash) {
block1 = await web3.eth.getBlock(block1.parentHash);
block2 = await web3.eth.getBlock(block2.parentHash);
}
return block1;
};

Mitigation Strategies

  • Wait for finality on anything irreversible. Rather than picking a confirmation count, treat the finalized tag as your bar: a block at or below the finalized height (about two epochs, ~12.8 minutes back) won't reorg. For lower-stakes flows the safe tag is a reasonable middle ground. Whatever you read at latest, assume it can still vanish.
  • Detect via parentHash, by polling or subscription. In a polling loop, store the hash at each height and compare each new block's parentHash against the previous stored hash. In a subscription model, do the same on every newHeads push. Either way, the mismatch is your reorg trigger; subscriptions just catch it a slot sooner.
  • Resubmit idempotently. When a transaction is reorged back to pending, it usually gets re-included on its own — you rarely need to rebroadcast. If you do resubmit, reuse the same nonce so the network treats it as the same transaction, never a second one, which is how you avoid accidentally sending value twice. Guard every state change with an idempotency key tied to the transaction hash so a re-included transaction can't double-apply.
  • Roll back to the common ancestor. On detecting a reorg, walk both branches back until their hashes meet (the helper above finds that ancestor), undo every state change derived from the orphaned blocks above it, then replay the new canonical blocks forward. Only the unfinalized tail should ever be in scope for rollback — finalized-derived state stays put.
  • Subscriptions for liveness, polling for the floor. A newHeads subscription over wss://ethereum.therpc.io/YOUR_API_KEY gives you the fastest reorg signal and the lowest cost. Keep a slower polling loop as a backstop, since a dropped WebSocket can miss the exact block where the reorg happened — the poller re-derives it from stored hashes on reconnect.

See also

Ready to call this in production?

Free tier covers personal projects. Pay-as-you-go scales without a card.