eth,plugin/evm: clamp finalized/safe to accept tip + clear export height on rewind (RED MEDIUM/LOW)

Follow-up to the export-boundary re-point, closing RED's re-review MEDIUM (a finality-safety hole
reachable via our own EVM-rewind→re-import restore runbook) and LOW.

MEDIUM — the rewind path. A rewind / RLP re-import (SetLastAcceptedBlockDirect) can lower the accept
tip BELOW the persisted EXPORT-FINAL (Quasar) height; the monotone SetLastQuasarHeight + advance-only
SyncQuasarFrontier then keep it stuck high, and after the chain rebuilds past that height with a
DIFFERENT canonical, `finalized` would name a block that was NEVER ⅔-certified — reported export-final
to every bridge/DEX consumer. Two parts, both needed:
- Belt: api_backend quasarHeight() resolves finalized/safe at min(LastQuasarHeight, LastAccepted) —
  finalized/safe can NEVER exceed the served accept tip, even with a stale-high durable key.
- Suspenders: VM.reconcileQuasarWithAccept clears the durable quasarHeightKey + the in-mem height
  (eth.ResetLastQuasarHeight, bypassing the monotone guard) when the accept tip is below the export
  height — the ONE legal regression of the otherwise-irreversible frontier. Conservative reset to 0
  (nothing export-final until the rebuilt chain re-certifies). Called on boot (against the restored
  accept tip — catches an offline rewind) and after admin_importChain (the live re-import). On the
  next boot the node re-seeds consensus' export frontier from the cleared (0) height, so the
  advance-only SyncQuasarFrontier yields to the rewind without touching the four shipped fixes.

LOW — SetLastQuasarFinalized now PERSISTS the durable key BEFORE advancing the in-mem height, so the
durable key is the source of truth: a crash between the two can only leave in-mem at/behind the
durable value on boot, never ahead of it (no monotonicity dip). A persist failure does not advance.

Tests: augmented TestFinalizedSafeResolveToQuasarNeverAboveIt with the belt case (seed export height
ABOVE the accept tip → finalized CLAMPS to the tip — the case RED flagged as false confidence, now
ENFORCED); new TestReconcileQuasarWithAccept_ClearsStaleHeightOnRewind (durable key + in-mem cleared
on an accept regression) and TestSetLastQuasarFinalized_PersistsBeforeAdvancing (persist-first,
no-advance-on-failure). eth suite green under -race.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-07-10 05:49:40 -07:00
co-authored by Hanzo Dev
parent 9c6d66ef6b
commit bdebad3632
6 changed files with 163 additions and 10 deletions
+21 -8
View File
@@ -110,18 +110,31 @@ func (b *EthAPIBackend) LastAcceptedBlock() *types.Block {
return b.eth.LastAcceptedBlock()
}
// quasarHeader / quasarBlock resolve the highest EXPORT-FINAL (Quasar, ⅔-by-stake) block — the
// ONLY block the eth `finalized`/`safe` tags may return. The two-tier consensus advances the
// local (Nova) accept tip at a BARE MAJORITY, which is reorgable and MUST NOT be exported; the
// export-final height is pushed by consensus strictly after accept and is durable across restart
// (eth.Ethereum.LastQuasarHeight). It is 0 (→ genesis) before the first export forms, and NEVER
// above the accept tip. Returns nil only if the block at that height is not (yet) canonical.
// quasarHeight is the EXPORT-FINAL (Quasar, ⅔-by-stake) height the eth `finalized`/`safe` tags
// resolve to, CLAMPED to the served accept tip. The two-tier consensus advances the local (Nova)
// accept tip at a BARE MAJORITY, which is reorgable and MUST NOT be exported; the export-final
// height is pushed by consensus strictly after accept and is durable across restart
// (eth.Ethereum.LastQuasarHeight), so in normal operation it is ≤ accept and the clamp is
// invisible. But a rewind / RLP re-import (SetLastAcceptedBlockDirect) can lower the accept tip
// BELOW a persisted quasar height; until reconcileQuasarWithAccept clears that stale key, this
// clamp (belt to that suspenders) guarantees `finalized`/`safe` never name a block above the
// currently-served accept tip. 0 (→ genesis) before the first export.
func (b *EthAPIBackend) quasarHeight() uint64 {
h := b.eth.LastQuasarHeight()
if acc := b.eth.LastAcceptedBlock(); acc != nil {
if n := acc.NumberU64(); n < h {
h = n
}
}
return h
}
func (b *EthAPIBackend) quasarHeader() *types.Header {
return b.eth.blockchain.GetHeaderByNumber(b.eth.LastQuasarHeight())
return b.eth.blockchain.GetHeaderByNumber(b.quasarHeight())
}
func (b *EthAPIBackend) quasarBlock() *types.Block {
return b.eth.blockchain.GetBlockByNumber(b.eth.LastQuasarHeight())
return b.eth.blockchain.GetBlockByNumber(b.quasarHeight())
}
func (b *EthAPIBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) {
+13
View File
@@ -101,6 +101,19 @@ func TestFinalizedSafeResolveToQuasarNeverAboveIt(t *testing.T) {
eth.SetLastQuasarHeight(numBlocks)
require.EqualValues(t, numBlocks, num(rpc.FinalizedBlockNumber))
// BELT — the case RED flagged as false confidence: seed the export height ABOVE the accept tip
// (the rewind-stale-key scenario, where a re-import lowered the accept tip below a persisted
// quasar height) and assert finalized/safe CLAMP to the accept tip. The code now ENFORCES this;
// previously it was only incidentally true because normal op keeps quasar ≤ accept, so a
// stale-high key would have named a block above (or off) the served chain.
eth.SetLastQuasarHeight(numBlocks + 100) // stale-high, far above the accept tip
require.EqualValues(t, tip, num(rpc.FinalizedBlockNumber), "finalized MUST clamp to the accept tip when the export height is stale-high")
require.EqualValues(t, tip, num(rpc.SafeBlockNumber), "safe MUST clamp to the accept tip when the export height is stale-high")
require.EqualValues(t, tip, hdrNum(rpc.FinalizedBlockNumber))
require.LessOrEqual(t, num(rpc.FinalizedBlockNumber), tip, "finalized must NEVER exceed the accept tip, even with a stale-high export key")
// Restore for the exhaustive loop below.
eth.ResetLastQuasarHeight(numBlocks)
// THE INVARIANT, exhaustively: for EVERY export height, `finalized` resolves to exactly that
// height and NEVER above the accepted tip.
for h := uint64(0); h <= numBlocks; h++ {
+7
View File
@@ -506,6 +506,13 @@ func (s *Ethereum) SetLastQuasarHeight(h uint64) {
// NEVER above it, because a Nova-accepted block above it is reorgable and must not be exported.
func (s *Ethereum) LastQuasarHeight() uint64 { return s.lastQuasarHeight.Load() }
// ResetLastQuasarHeight FORCE-sets the in-memory export height, bypassing the monotone guard of
// SetLastQuasarHeight. It exists for the ONE legal regression of the otherwise-irreversible
// export frontier: a rewind / RLP re-import that lowered the accept tip below the persisted
// quasar height (the plugin VM's reconcileQuasarWithAccept calls this with 0 — conservative:
// nothing export-final until the rebuilt chain re-certifies). Normal operation NEVER calls it.
func (s *Ethereum) ResetLastQuasarHeight(h uint64) { s.lastQuasarHeight.Store(h) }
// precheckPopulateMissingTries returns an error if config flags should prevent
// [populateMissingTries]
//
+7
View File
@@ -87,6 +87,13 @@ func (api *AdminAPI) ImportChain(ctx context.Context, file string) (*ImportChain
}
log.Info("admin_importChain: complete", "hash", lastHash.Hex(), "height", lastHeight)
// A re-import (the EVM-rewind→re-import restore runbook) can leave the accept tip below a
// persisted EXPORT-FINAL (Quasar) height. Clear the now-stale export height so `finalized`/
// `safe` and warp never report a rebuilt (never ⅔-certified) block as export-final — the exact
// two-tier safety class this path could otherwise reopen. Conservative reset to 0; the rebuilt
// chain re-certifies from there (the belt clamp in api_backend guards the interim window).
api.vm.reconcileQuasarWithAccept(lastHeight)
return &ImportChainResult{
Success: true,
BlocksImported: totalImported,
+75
View File
@@ -0,0 +1,75 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"encoding/binary"
"testing"
"github.com/luxfi/database"
"github.com/luxfi/database/memdb"
"github.com/luxfi/evm/eth"
"github.com/stretchr/testify/require"
)
// TestReconcileQuasarWithAccept_ClearsStaleHeightOnRewind proves the ONE legal regression of the
// otherwise-irreversible EXPORT (Quasar) frontier: a rewind / RLP re-import (SetLastAcceptedBlockDirect,
// the EVM-rewind→re-import restore runbook) that lowers the accept tip below a persisted export
// height MUST clear that stale height — both the durable acceptedBlockDB key and the in-memory value
// — so a rebuilt-with-a-DIFFERENT-canonical block is never reported export-final to a bridge/DEX
// consumer. This is the case RED flagged: the durable key + the monotone/advance-only guards keep the
// height stuck high across a rewind unless this reconcile yields to it.
func TestReconcileQuasarWithAccept_ClearsStaleHeightOnRewind(t *testing.T) {
e := &eth.Ethereum{}
e.SetLastQuasarHeight(1085000) // a persisted export height (pre-rewind)
db := memdb.New()
var buf [8]byte
binary.BigEndian.PutUint64(buf[:], 1085000)
require.NoError(t, db.Put(quasarHeightKey, buf[:]))
vm := &VM{eth: e, acceptedBlockDB: db}
// Normal operation: the accept tip is at/above the export height → a clean no-op.
vm.reconcileQuasarWithAccept(1085000)
require.EqualValues(t, 1085000, e.LastQuasarHeight(), "no regression: export height unchanged when the accept tip is at/above it")
got, err := db.Get(quasarHeightKey)
require.NoError(t, err)
require.EqualValues(t, 1085000, binary.BigEndian.Uint64(got), "durable key preserved in normal operation")
// REWIND: the accept tip drops to 1084996 (below the persisted export height) → clear it.
vm.reconcileQuasarWithAccept(1084996)
require.Zero(t, e.LastQuasarHeight(), "export height MUST reset to 0 when the accept tip rewinds below it")
_, err = db.Get(quasarHeightKey)
require.ErrorIs(t, err, database.ErrNotFound, "durable export (quasar) key MUST be cleared on a rewind")
// Idempotent: reconciling again after the reset is a clean no-op (no stale height to clear).
vm.reconcileQuasarWithAccept(1084996)
require.Zero(t, e.LastQuasarHeight())
}
// TestSetLastQuasarFinalized_PersistsBeforeAdvancing proves the LOW fix: the durable key is the
// source of truth. SetLastQuasarFinalized writes the acceptedBlockDB BEFORE advancing the in-memory
// height, so a crash between the two can only ever leave the in-mem height at or BEHIND the durable
// key on the next boot — never ahead of it (which would be a monotonicity dip that briefly reports a
// Nova block as export-final). On a persist FAILURE the in-mem height must NOT advance.
func TestSetLastQuasarFinalized_PersistsBeforeAdvancing(t *testing.T) {
e := &eth.Ethereum{}
db := memdb.New()
vm := &VM{eth: e, acceptedBlockDB: db}
vm.SetLastQuasarFinalized(42)
require.EqualValues(t, 42, e.LastQuasarHeight(), "in-mem advances after a successful persist")
got, err := db.Get(quasarHeightKey)
require.NoError(t, err)
require.EqualValues(t, 42, binary.BigEndian.Uint64(got), "durable key holds the advanced height")
// Monotone: a lower height is ignored (in-mem and durable both unchanged).
vm.SetLastQuasarFinalized(40)
require.EqualValues(t, 42, e.LastQuasarHeight())
// Persist failure ⇒ in-mem must NOT advance (the durable key stays authoritative). A closed DB
// makes Put fail.
require.NoError(t, db.Close())
vm.SetLastQuasarFinalized(100)
require.EqualValues(t, 42, e.LastQuasarHeight(), "in-mem must NOT advance when the durable persist fails")
}
+40 -2
View File
@@ -1059,6 +1059,12 @@ func (vm *VM) initializeChain(lastAcceptedHash common.Hash, ethConfig ethconfig.
if h := vm.readLastQuasarHeight(); h > 0 {
vm.eth.SetLastQuasarHeight(h)
}
// Reconcile against the RESTORED accept tip: if an offline rewind / RLP re-import lowered the
// accept tip below the persisted quasar height, clear the now-stale export height (conservative
// reset to 0) so a rebuilt-with-different-canonical block is never reported export-final.
if acc := vm.eth.LastAcceptedBlock(); acc != nil {
vm.reconcileQuasarWithAccept(acc.NumberU64())
}
// Upgrade the consensus context on the live chain to the FULL chain Runtime, which adds
// the cross-chain atomic SharedMemory capability (the DEX 0x9999 C<->D shared-memory seam)
// on top of the identity already bound at construction. Also covers the warp message
@@ -2076,12 +2082,44 @@ func (vm *VM) SetLastQuasarFinalized(height uint64) {
if height <= vm.eth.LastQuasarHeight() {
return // monotone: the export frontier only advances
}
vm.eth.SetLastQuasarHeight(height)
// PERSIST BEFORE advancing the in-memory value: the durable key is the source of truth. A
// crash between the two must never leave the in-mem height ahead of what boot restores — that
// would be a monotonicity DIP on the next boot (a Nova block briefly reported export-final).
// Only advance in-mem once the write has committed.
var buf [8]byte
binary.BigEndian.PutUint64(buf[:], height)
if err := vm.acceptedBlockDB.Put(quasarHeightKey, buf[:]); err != nil {
log.Warn("failed to persist EXPORT-FINAL (quasar) height", "height", height, "err", err)
log.Warn("failed to persist EXPORT-FINAL (quasar) height; NOT advancing in-mem", "height", height, "err", err)
return
}
vm.eth.SetLastQuasarHeight(height)
}
// reconcileQuasarWithAccept clears the durable + in-memory EXPORT (Quasar) height when it exceeds
// the accept tip — the ONE legal regression of the otherwise-irreversible export frontier. A
// rewind / RLP re-import (SetLastAcceptedBlockDirect, e.g. the EVM-rewind→1084996 restore runbook)
// can lower the accept tip below a persisted quasar height; after the chain rebuilds past that
// height with a DIFFERENT canonical, the stale quasar height would name a block that was NEVER
// ⅔-certified — reported as export-final to every bridge / DEX consumer. Conservative: reset to 0
// (nothing export-final until the rebuilt chain re-certifies) rather than guess a still-valid
// lower height. Idempotent — a no-op when the frontier is already within the accept tip. Called
// on boot (against the restored last-accepted height) and after each SetLastAcceptedBlockDirect
// (the live re-import path). On the next boot the node re-seeds consensus' export frontier from
// this cleared (0) height, so its advance-only guard yields to the rewind without special-casing.
func (vm *VM) reconcileQuasarWithAccept(acceptHeight uint64) {
if vm.eth == nil {
return
}
if vm.eth.LastQuasarHeight() <= acceptHeight {
return
}
vm.eth.ResetLastQuasarHeight(0)
if err := vm.acceptedBlockDB.Delete(quasarHeightKey); err != nil {
log.Warn("failed to clear stale EXPORT (quasar) height on rewind", "err", err)
}
log.Warn("EXPORT (quasar) height RESET to 0 — accept tip rewound below it (rewind/re-import); "+
"export re-certifies from the rebuilt chain (nothing is export-final until then)",
"acceptHeight", acceptHeight)
}
// LastQuasarHeight returns the highest EXPORT-FINAL (Quasar) block number the eth `finalized`/