consensus v1.36 export-boundary: close 3 LOW items for tag (DRY clamp, one admin_importChain, locked export writes + boot-wiring tests)

Two-tier finality (Nova = local majority accept, drives VM.Accept; Quasar =
⅔-by-stake export) requires that NO export surface ever names a block above the
served accept tip. RED's final pass approved SHIP with three LOW items to close
first.

LOW-1 — DRY the accept-tip clamp into ONE place. New eth.Ethereum.ClampedQuasarHeight()
= min(LastQuasarHeight, LastAccepted.Number) is the single clamp; BOTH export
surfaces read it: EthAPIBackend.quasarHeight() (RPC finalized/safe) and
warpBlockClient.LastQuasarHeight() (warp export gate, via the BlockClient
interface). Removes the second inlined clamp copy that could drift. LastQuasarHeight()
is documented as RAW (node boot re-seed only, where reconcile has already run).

LOW-2 — exactly ONE admin_importChain. Deleted the eth-package AdminAPI.ImportChain
(which neither reconciled the export height nor skipped <=head blocks); the plugin VM's
(plugin/evm/admin_api.go) is the sole one — it runs under vmLock and calls
reconcileQuasarWithAccept after each SetLastAcceptedBlockDirect. Left a NOTE so it is
not re-added and reopens the rewind hole. Dropped the now-unused rlp import.

LOW-3 — exclude concurrent export-height writers + prove the real boot wiring.
SetLastQuasarFinalized now takes vmLock (the same lock reconcileQuasarWithAccept's
admin_importChain caller holds), so the observer callback's durable Put+Store can never
interleave with a re-import's Reset(0)+Delete on the same key. Fixed a boot ordering
gap: the startup auto-import (importBlocksFromFile) runs AFTER initializeChain's
reconcile, so a --import-chain-data rewind could leave a stale export height — added a
reconcile right after the auto-import. New eth.NewTestEthereum test-support ctor.

Tests (all green): eth -race adds TestClampedQuasarHeight_RealRewind (genuine
SetLastAcceptedBlockDirect rewind; belt clamps finalized/safe to the rewound tip while
raw is unchanged); plugin/evm adds TestBootReconcile_RealLastAccepted_ClearsStaleQuasarOnRewind
(drives the REAL restored LastAcceptedBlock through belt + suspenders) and
TestSetLastQuasarFinalized_LockedAgainstReconcile (concurrency: durable key <-> in-mem
always consistent). gofmt/vet/ontology clean.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-07-10 09:22:12 -07:00
co-authored by Hanzo Dev
parent bdebad3632
commit 6cbb326615
7 changed files with 245 additions and 125 deletions
+7 -93
View File
@@ -38,7 +38,6 @@ import (
"github.com/luxfi/geth/core/rawdb"
"github.com/luxfi/geth/core/types"
"github.com/luxfi/geth/rlp"
log "github.com/luxfi/log"
)
@@ -92,98 +91,13 @@ func (api *AdminAPI) ExportChain(file string, first *uint64, last *uint64) (bool
return true, nil
}
// ImportChain imports a blockchain from a local file.
// State is committed periodically during import for restart-safety.
func (api *AdminAPI) ImportChain(file string) (bool, error) {
in, err := os.Open(file)
if err != nil {
return false, err
}
defer in.Close()
var reader io.Reader = in
if strings.HasSuffix(file, ".gz") {
if reader, err = gzip.NewReader(reader); err != nil {
return false, err
}
}
chain := api.eth.BlockChain()
if err := chain.EnsureGenesisState(); err != nil {
return false, fmt.Errorf("failed to ensure genesis state: %w", err)
}
stream := rlp.NewStream(reader, 0)
blocks, index := make([]*types.Block, 0, 2500), 0
var lastInsertedBlock *types.Block
commitInterval := chain.CommitInterval()
var lastCommitHeight uint64
log.Info("ImportChain: starting", "file", file, "currentBlock", chain.CurrentBlock().Number, "commitInterval", commitInterval)
for batch := 0; ; batch++ {
for len(blocks) < cap(blocks) {
block := new(types.Block)
if err := stream.Decode(block); err == io.EOF {
break
} else if err != nil {
return false, fmt.Errorf("block %d: failed to parse: %v", index, err)
}
if block.NumberU64() == 0 {
continue
}
blocks = append(blocks, block)
index++
}
if len(blocks) == 0 {
break
}
log.Info("ImportChain: inserting batch", "batch", batch, "blocks", len(blocks), "firstNum", blocks[0].NumberU64())
n, err := chain.InsertChain(blocks)
if err != nil {
return false, fmt.Errorf("batch %d: failed to insert after %d blocks: %v", batch, n, err)
}
if n > 0 {
lastInsertedBlock = blocks[n-1]
currentHeight := lastInsertedBlock.NumberU64()
if err := chain.SetLastAcceptedBlockDirect(lastInsertedBlock); err != nil {
return false, fmt.Errorf("failed to set last accepted block: %v", err)
}
// Periodically commit state trie to disk for restart-safety
if currentHeight-lastCommitHeight >= commitInterval {
log.Info("ImportChain: committing state", "block", currentHeight)
if err := chain.ForceCommitState(lastInsertedBlock); err != nil {
return false, fmt.Errorf("failed to commit state at block %d: %v", currentHeight, err)
}
lastCommitHeight = currentHeight
}
}
blocks = blocks[:0]
}
if lastInsertedBlock != nil {
// Final state commit
finalHeight := lastInsertedBlock.NumberU64()
if finalHeight > lastCommitHeight {
log.Info("ImportChain: final state commit", "block", finalHeight)
if err := chain.ForceCommitState(lastInsertedBlock); err != nil {
return false, fmt.Errorf("failed to commit final state: %v", err)
}
}
// Update VM layer via callback
if err := api.eth.CallPostImportCallback(lastInsertedBlock.Hash(), finalHeight); err != nil {
log.Warn("ImportChain: post-import callback failed", "error", err)
}
log.Info("ImportChain: completed", "blocks", index, "height", finalHeight)
}
return true, nil
}
// NOTE: admin_importChain is DELIBERATELY not implemented on this eth-package AdminAPI. There is
// EXACTLY ONE admin_importChain — the plugin VM's (plugin/evm/admin_api.go), which runs under
// vmLock AND calls reconcileQuasarWithAccept after each SetLastAcceptedBlockDirect so a re-import
// that lowers the accept tip clears the stale EXPORT-FINAL (Quasar) height. An eth-package
// ImportChain here (which neither reconciled nor skipped ≤-head blocks) would reopen the two-tier
// finality-safety rewind hole the moment a future RegisterName reorder stopped the plugin one from
// shadowing it. Do not re-add it here.
// WriteGenesisStateSpec writes the genesis state spec to the database.
// This is required for RLP import to work when the genesis state trie is not accessible.
+7 -14
View File
@@ -111,22 +111,15 @@ func (b *EthAPIBackend) LastAcceptedBlock() *types.Block {
}
// 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)
// resolve to — the accept-tip-CLAMPED export height (eth.Ethereum.ClampedQuasarHeight, the ONE
// shared clamp the warp export gate also reads). 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.
// height is pushed by consensus strictly after accept and is durable across restart, so normally it
// is ≤ accept and the clamp is invisible — it binds only in the transient rewind window before
// reconcileQuasarWithAccept clears the stale key, guaranteeing `finalized`/`safe` never name a block
// above the 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
return b.eth.ClampedQuasarHeight()
}
func (b *EthAPIBackend) quasarHeader() *types.Header {
+46 -11
View File
@@ -26,22 +26,20 @@ import (
// trails, and can never exceed, the accepted tip). This proves `eth_getBlockByNumber("finalized")`
// never returns a block above the Quasar height, that it is monotone (never regresses on a
// restart-seed race), and that `latest` stays on the accepted tip (a correct non-finality tag).
func TestFinalizedSafeResolveToQuasarNeverAboveIt(t *testing.T) {
// buildAcceptedChain builds a REAL canonical chain (genesis + n accepted blocks) so the
// `finalized`/`safe` + export-height resolution is exercised against a real chain (LastAccepted /
// GetBlockByNumber / the clamp), not a mock. Accept tip = n.
func buildAcceptedChain(t *testing.T, n int) *core.BlockChain {
t.Helper()
key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
cryptoAddr := crypto.PubkeyToAddress(key.PublicKey)
addr := common.BytesToAddress(cryptoAddr[:])
bal, _ := new(big.Int).SetString("100000000000000000000000", 10)
gspec := &core.Genesis{
Config: params.TestChainConfig,
Alloc: types.GenesisAlloc{addr: {Balance: bal}},
}
gspec := &core.Genesis{Config: params.TestChainConfig, Alloc: types.GenesisAlloc{addr: {Balance: bal}}}
engine := dummy.NewETHFaker()
const numBlocks = 5
// The dummy engine requires the fee-recipient (blackhole) coinbase on generated blocks.
coinbase := common.HexToAddress("0x0100000000000000000000000000000000000000")
genDb, blocks, _, err := core.GenerateChainWithGenesis(gspec, engine, numBlocks, 10, func(i int, b *core.BlockGen) {
genDb, blocks, _, err := core.GenerateChainWithGenesis(gspec, engine, n, 10, func(i int, b *core.BlockGen) {
b.SetCoinbase(coinbase)
})
require.NoError(t, err)
@@ -50,12 +48,17 @@ func TestFinalizedSafeResolveToQuasarNeverAboveIt(t *testing.T) {
t.Cleanup(func() { chain.Stop() })
_, err = chain.InsertChain(blocks)
require.NoError(t, err)
// Accept every block so the blockchain's LastAcceptedBlock is the tip — the Nova/accept tip
// the `latest` tag tracks (distinct from the export/Quasar tip the `finalized` tag tracks).
for _, blk := range blocks {
require.NoError(t, chain.Accept(blk))
}
chain.DrainAcceptorQueue()
require.EqualValues(t, n, chain.LastAcceptedBlock().NumberU64())
return chain
}
func TestFinalizedSafeResolveToQuasarNeverAboveIt(t *testing.T) {
const numBlocks = 5
chain := buildAcceptedChain(t, numBlocks)
// The eth backend's blockchain is the canonical chain (genesis..numBlocks). LastAccepted /
// `latest` is the Nova tip (numBlocks); the export (Quasar) height is set separately below.
@@ -126,3 +129,35 @@ func TestFinalizedSafeResolveToQuasarNeverAboveIt(t *testing.T) {
require.LessOrEqual(t, blk.NumberU64(), tip, "finalized must never exceed the accepted tip")
}
}
// TestClampedQuasarHeight_RealRewind exercises the BELT against a GENUINE rewind
// (SetLastAcceptedBlockDirect — the primitive the EVM-rewind→re-import restore runbook uses): with a
// persisted export height of 8 and the accept tip rewound to 5, ClampedQuasarHeight (the ONE shared
// clamp both `finalized`/`safe` and the warp gate read) resolves to 5 — never the stale 8 above the
// served accept tip — WITHOUT relying on the VM's reconcile having run. This is the structural belt
// that the warp gate now shares (LOW-1), verified under -race at the eth level.
func TestClampedQuasarHeight_RealRewind(t *testing.T) {
chain := buildAcceptedChain(t, 10)
eth := &Ethereum{blockchain: chain}
backend := &EthAPIBackend{eth: eth}
ctx := context.Background()
eth.SetLastQuasarHeight(8) // valid, ≤ accept tip 10
require.EqualValues(t, 8, eth.ClampedQuasarHeight())
// REWIND to block 5 (SetLastAcceptedBlockDirect): accept tip drops below the export height.
require.NoError(t, chain.SetLastAcceptedBlockDirect(chain.GetBlockByNumber(5)))
require.EqualValues(t, 5, chain.LastAcceptedBlock().NumberU64())
// The RAW export height is still 8 (clearing it is the VM reconcile's job) — but the BELT clamps.
require.EqualValues(t, 8, eth.LastQuasarHeight())
require.EqualValues(t, 5, eth.ClampedQuasarHeight(), "ClampedQuasarHeight MUST clamp to the rewound accept tip")
// `finalized`/`safe` therefore resolve to block 5, never the stale block 8.
fin, err := backend.BlockByNumber(ctx, rpc.FinalizedBlockNumber)
require.NoError(t, err)
require.EqualValues(t, 5, fin.NumberU64(), "finalized MUST clamp to the rewound accept tip, never the stale export height")
safe, err := backend.BlockByNumber(ctx, rpc.SafeBlockNumber)
require.NoError(t, err)
require.EqualValues(t, 5, safe.NumberU64())
}
+30 -3
View File
@@ -485,6 +485,15 @@ func (s *Ethereum) LastAcceptedBlock() *types.Block {
return s.blockchain.LastAcceptedBlock()
}
// NewTestEthereum builds an Ethereum wrapping only the given blockchain — TEST SUPPORT so a test
// in another package (e.g. plugin/evm's boot-wiring reconcile test) can drive the
// `finalized`/`safe` + export-height resolution against a REAL chain without the full node stack.
// ONLY the blockchain-backed reads (LastAcceptedBlock, GetBlockByNumber, the export-height clamp)
// are wired; everything else is nil. Not for production use.
func NewTestEthereum(chain *core.BlockChain) *Ethereum {
return &Ethereum{blockchain: chain}
}
// SetLastQuasarHeight records the highest EXPORT-FINAL (Quasar, ⅔-by-stake) block number.
// Monotone: a value at or below the current height is ignored (the export frontier only ever
// advances). Called by the plugin VM from the consensus export-frontier observer, and once on
@@ -501,11 +510,29 @@ func (s *Ethereum) SetLastQuasarHeight(h uint64) {
}
}
// LastQuasarHeight returns the highest EXPORT-FINAL (Quasar) block number (0 before the first
// export forms). The eth `finalized`/`safe` block tags resolve to the block at this number —
// NEVER above it, because a Nova-accepted block above it is reorgable and must not be exported.
// LastQuasarHeight returns the RAW highest EXPORT-FINAL (Quasar) block number the VM has recorded
// (0 before the first export). This is the persisted/in-mem value; export SURFACES must instead
// read ClampedQuasarHeight (which never exceeds the accept tip). Used by the node's boot re-seed
// of the consensus export frontier, where the reconcile has already run so raw == clamped.
func (s *Ethereum) LastQuasarHeight() uint64 { return s.lastQuasarHeight.Load() }
// ClampedQuasarHeight is the EXPORT-FINAL (Quasar) height CLAMPED to the served accept tip —
// min(LastQuasarHeight, LastAccepted.Number). This is the ONE place the clamp lives: BOTH the RPC
// `finalized`/`safe` resolver (EthAPIBackend.quasarHeight) and the warp export gate
// (warpBlockClient.LastQuasarHeight) read it, so an export surface can NEVER name a block above the
// served accept tip — STRUCTURALLY, regardless of reconcile timing — with no second inlined copy of
// the clamp to drift. In normal operation quasar ≤ accept so the clamp is invisible; it binds only
// in the transient rewind window before reconcileQuasarWithAccept clears the stale key.
func (s *Ethereum) ClampedQuasarHeight() uint64 {
h := s.lastQuasarHeight.Load()
if acc := s.blockchain.LastAcceptedBlock(); acc != nil {
if n := acc.NumberU64(); n < h {
return n
}
}
return h
}
// 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
+128
View File
@@ -0,0 +1,128 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"encoding/binary"
"math/big"
"sync"
"testing"
"github.com/luxfi/crypto"
"github.com/luxfi/database"
"github.com/luxfi/database/memdb"
"github.com/luxfi/evm/consensus/dummy"
"github.com/luxfi/evm/core"
"github.com/luxfi/evm/eth"
"github.com/luxfi/evm/params"
gethcommon "github.com/luxfi/geth/common"
gethtypes "github.com/luxfi/geth/core/types"
gethvm "github.com/luxfi/geth/core/vm"
"github.com/stretchr/testify/require"
)
// buildAcceptedTestChain builds a REAL canonical chain (genesis + n accepted blocks) and returns
// the eth backend wrapping it. Accept tip = n. This is the actual blockchain the boot reconcile
// reads LastAcceptedBlock() / GetBlockByNumber() from — no hand-picked heights.
func buildAcceptedTestChain(t *testing.T, n int) (*core.BlockChain, *eth.Ethereum) {
t.Helper()
key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
cryptoAddr := crypto.PubkeyToAddress(key.PublicKey)
addr := gethcommon.BytesToAddress(cryptoAddr[:])
bal, _ := new(big.Int).SetString("100000000000000000000000", 10)
gspec := &core.Genesis{Config: params.TestChainConfig, Alloc: gethtypes.GenesisAlloc{addr: {Balance: bal}}}
engine := dummy.NewETHFaker()
coinbase := gethcommon.HexToAddress("0x0100000000000000000000000000000000000000")
genDb, blocks, _, err := core.GenerateChainWithGenesis(gspec, engine, n, 10, func(i int, b *core.BlockGen) {
b.SetCoinbase(coinbase)
})
require.NoError(t, err)
chain, err := core.NewBlockChain(genDb, core.DefaultCacheConfig, gspec, engine, gethvm.Config{}, gethcommon.Hash{}, false, nil)
require.NoError(t, err)
t.Cleanup(func() { chain.Stop() })
_, err = chain.InsertChain(blocks)
require.NoError(t, err)
for _, b := range blocks {
require.NoError(t, chain.Accept(b))
}
chain.DrainAcceptorQueue()
require.EqualValues(t, n, chain.LastAcceptedBlock().NumberU64())
return chain, eth.NewTestEthereum(chain)
}
// TestBootReconcile_RealLastAccepted_ClearsStaleQuasarOnRewind exercises the REAL boot wiring
// (vm.go initializeChain:1065-1066 and the auto-import-after-reconcile at Initialize): the reconcile
// is driven by the ACTUAL restored LastAcceptedBlock of a real chain — not a hand-picked height —
// across a genuine rewind (SetLastAcceptedBlockDirect, the primitive both the runbook rewind and
// admin/auto importBlocksFromFile use). It proves: (a) the belt (ClampedQuasarHeight) clamps
// `finalized` to the rewound tip IMMEDIATELY; and (b) the suspenders (reconcileQuasarWithAccept)
// clear the stale durable + in-mem export height once given the real post-rewind accept tip.
func TestBootReconcile_RealLastAccepted_ClearsStaleQuasarOnRewind(t *testing.T) {
const n = 10
chain, e := buildAcceptedTestChain(t, n)
db := memdb.New()
pvm := &VM{eth: e, acceptedBlockDB: db}
// A valid export height (8 ≤ accept tip 10). Boot reconcile with the REAL LastAcceptedBlock is a
// clean no-op (export frontier within the accept tip).
pvm.SetLastQuasarFinalized(8)
require.EqualValues(t, 8, e.LastQuasarHeight())
pvm.reconcileQuasarWithAccept(pvm.eth.LastAcceptedBlock().NumberU64()) // real height = 10
require.EqualValues(t, 8, e.LastQuasarHeight(), "no reconcile while export height ≤ real accept tip")
require.EqualValues(t, 8, e.ClampedQuasarHeight(), "finalized == export height when it is within the accept tip")
// REWIND to block 5 (the SetLastAcceptedBlockDirect primitive the rewind/re-import runbook uses):
// accept tip drops to 5, below the persisted export height 8.
require.NoError(t, chain.SetLastAcceptedBlockDirect(chain.GetBlockByNumber(5)))
require.EqualValues(t, 5, e.LastAcceptedBlock().NumberU64())
// BELT — structural, before any reconcile: `finalized` (ClampedQuasarHeight) clamps to the new
// accept tip (5), NEVER naming the stale block 8 above the served tip.
require.EqualValues(t, 5, e.ClampedQuasarHeight(), "finalized MUST clamp to the rewound accept tip immediately")
// SUSPENDERS — the REAL boot/auto-import wiring: reconcile with the ACTUAL restored
// LastAcceptedBlock (5), not a hand-picked height → clears the stale export height (8).
pvm.reconcileQuasarWithAccept(pvm.eth.LastAcceptedBlock().NumberU64())
require.Zero(t, e.LastQuasarHeight(), "boot reconcile must clear the stale export height on a real rewind")
_, err := db.Get(quasarHeightKey)
require.ErrorIs(t, err, database.ErrNotFound, "durable export key must be cleared on a real rewind")
require.Zero(t, e.ClampedQuasarHeight(), "nothing export-final until the rebuilt chain re-certifies")
}
// TestSetLastQuasarFinalized_LockedAgainstReconcile drives the observer callback
// (SetLastQuasarFinalized) and the re-import reconcile (reconcileQuasarWithAccept, run under the
// caller's vmLock exactly as admin_importChain does) concurrently against the SAME atomic + durable
// key. With SetLastQuasarFinalized taking vmLock, the two writers are mutually excluded: the final
// state is always consistent (durable key == in-mem height), never a torn Put(H)/Delete interleave.
// Runs under -race in a CGO-on environment; the lock is the fix.
func TestSetLastQuasarFinalized_LockedAgainstReconcile(t *testing.T) {
e := &eth.Ethereum{}
db := memdb.New()
pvm := &VM{eth: e, acceptedBlockDB: db}
var wg sync.WaitGroup
for i := uint64(1); i <= 200; i++ {
wg.Add(2)
go func(h uint64) { defer wg.Done(); pvm.SetLastQuasarFinalized(h) }(i)
go func(h uint64) {
defer wg.Done()
// Mirror admin_importChain: hold vmLock across the reconcile (its documented invariant).
pvm.vmLock.Lock()
pvm.reconcileQuasarWithAccept(h % 50) // sometimes below the export height ⇒ a reset
pvm.vmLock.Unlock()
}(i)
}
wg.Wait()
// Consistency invariant: the durable key and the in-mem height agree (no torn write). A cleared
// key ⇔ in-mem 0; a present key ⇔ its uint64 == in-mem height.
inMem := e.LastQuasarHeight()
b, err := db.Get(quasarHeightKey)
if err == database.ErrNotFound {
require.Zero(t, inMem, "durable key cleared ⇒ in-mem must be 0")
} else {
require.NoError(t, err)
require.EqualValues(t, binary.BigEndian.Uint64(b), inMem, "durable key and in-mem height must agree (no torn write)")
}
}
+17
View File
@@ -680,6 +680,13 @@ func (vm *VM) Initialize(ctx context.Context, init block.Init) error {
if err != nil {
return fmt.Errorf("startup import failed: %w", err)
}
// A startup import can leave the accept tip below a persisted EXPORT-FINAL (Quasar) height.
// reconcile AFTER it — the boot reconcile in initializeChain ran BEFORE this import (against
// the pre-import tip), and importBlocksFromFile (unlike admin_importChain) does not reconcile
// itself — so without this a stale export height would survive an --import-chain-data rewind
// and `finalized`/warp would report a rebuilt (never-⅔-certified) block export-final. Single
// threaded at boot (no observer yet), so no lock needed here.
vm.reconcileQuasarWithAccept(lastHeight)
log.Info("Startup import complete", "blocks", imported, "lastHash", lastHash.Hex(), "height", lastHeight)
}
@@ -2076,6 +2083,16 @@ func (vm *VM) readLastAccepted() (common.Hash, uint64, error) {
// surface survives a restart without regressing (the in-memory consensus frontier resets to 0
// until a cert re-forms). No-op before the eth backend exists.
func (vm *VM) SetLastQuasarFinalized(height uint64) {
// Acquire vmLock — the SAME lock reconcileQuasarWithAccept relies on (held by its
// admin_importChain caller) — so the durable-key writes here (Put + Store) are MUTUALLY
// EXCLUDED with a concurrent reconcile's Reset(0)+Delete on the same atomic + acceptedBlockDB
// key. This observer callback (fired by the consensus export frontier, holding NO VM lock) could
// otherwise interleave a live cert-finalization against a re-import's reconcile and leave the
// in-mem height and the durable key inconsistent. Never re-entrant: the only caller is the
// consensus observer, which holds no VM lock; reconcile itself does NOT take vmLock (its caller
// does), so no double-acquire.
vm.vmLock.Lock()
defer vm.vmLock.Unlock()
if vm.eth == nil {
return
}
+10 -4
View File
@@ -34,9 +34,15 @@ func (w *warpBlockClient) GetAcceptedBlock(ctx context.Context, blkID ids.ID) (n
return w.vm.newBlock(ethBlock), nil
}
// LastQuasarHeight returns the VM's highest EXPORT-FINAL (Quasar, ⅔-by-stake) block number — the
// export-tier gate for warp block signatures (a cross-chain export must sit at export finality,
// not the reorgable local Accept tip).
// LastQuasarHeight returns the accept-tip-CLAMPED EXPORT-FINAL (Quasar, ⅔-by-stake) height — the
// export-tier gate for warp block signatures (a cross-chain export must sit at export finality, not
// the reorgable local Accept tip). It reads the SAME shared clamp (eth.ClampedQuasarHeight) the RPC
// `finalized`/`safe` resolver uses, so the warp gate is structurally belt-protected — it can never
// admit a block above the served accept tip even during the transient rewind window (before
// reconcileQuasarWithAccept clears a stale key), independent of reconcile ordering.
func (w *warpBlockClient) LastQuasarHeight() uint64 {
return w.vm.LastQuasarHeight()
if w.vm.eth == nil {
return 0
}
return w.vm.eth.ClampedQuasarHeight()
}