Core issue
setupGenesis (vms/saevm/sae/always.go) calls core.SetupGenesisBlock on every Initialize. Whenever the genesis state root is absent from the trie database, libevm re-commits genesis and rewrites HeadBlockHash, HeadHeaderHash, and HeadFastBlockHash to the genesis hash; nothing restores them. On a state-synced node that permanently bricks the node: FinalizedBlockHash (untouched by the re-commit) still names the synced settled block, so recovery walks the canonical chain up from it looking for the head marker, never finds the genesis hash, and fails with no canonical block at height N — on this and every later restart. Only a db wipe recovers.
This is not triggerable today, because first boot durably commits genesis state before any sync — but nothing enforces that invariant, and it breaks under genesis-root pruning, a trie-scheme migration (e.g. Firewood), or a db restore. The statesync test harness already works around exactly this clobber (sut_test.go setupGenesis saves and restores the three markers).
Failing test
In package statesync; the three marker writes simulate what SetupGenesisBlock does when the genesis state is absent at restart:
func TestRestartWithMissingGenesisState(t *testing.T) {
source := newVM(t)
source.acceptBlocks(t, defaultCommitInterval+2)
xdb := saetest.NewExecutionResultsDB()
db := memdb.New()
client := newSUT(t, withDatabase(db), withXDB(xdb))
saetest.ConnectTo[saetest.Peer](t, client, source)
summary, err := source.GetLastStateSummary(t.Context())
require.NoError(t, err, "GetLastStateSummary()")
require.NoError(t, client.syncTo(t.Context(), t, summary), "syncTo()")
// What core.SetupGenesisBlock does on restart when the genesis state
// root is absent from the trie database:
genesisHash := client.genesis.ToBlock().Hash()
rawdb.WriteHeadBlockHash(client.db, genesisHash)
rawdb.WriteHeadHeaderHash(client.db, genesisHash)
rawdb.WriteHeadFastBlockHash(client.db, genesisHash)
// The node MUST still be able to start.
clientVM := client.asVM(t, source.clock.Now())
require.Equal(t, summary.Height(), clientVM.lastAcceptedBlock(t).Height(), "last accepted height after restart")
}
Fails inside Initialize:
creating new execution: executing all previously accepted blocks: no canonical block at height 5
Proposed fix
Only call core.SetupGenesisBlock on a fresh database, where its marker writes are the correct initial values. On an initialized database, never touch head markers: verify the stored genesis hash, commit the genesis state (only if absent) with a marker-free state write, and run the chain-config compatibility check directly — the pattern cchain/genesis.go (verifyAndWriteBlock + setupTrieDB) already uses. The test-harness workaround can then be deleted.
Note that restoring the markers after the clobber is not a fix: the writes happen inside libevm against the raw db handle, so no batching makes clobber+restore atomic, and a crash between them leaves the same bricked state.
Core issue
setupGenesis(vms/saevm/sae/always.go) callscore.SetupGenesisBlockon everyInitialize. Whenever the genesis state root is absent from the trie database, libevm re-commits genesis and rewritesHeadBlockHash,HeadHeaderHash, andHeadFastBlockHashto the genesis hash; nothing restores them. On a state-synced node that permanently bricks the node:FinalizedBlockHash(untouched by the re-commit) still names the synced settled block, so recovery walks the canonical chain up from it looking for the head marker, never finds the genesis hash, and fails withno canonical block at height N— on this and every later restart. Only a db wipe recovers.This is not triggerable today, because first boot durably commits genesis state before any sync — but nothing enforces that invariant, and it breaks under genesis-root pruning, a trie-scheme migration (e.g. Firewood), or a db restore. The statesync test harness already works around exactly this clobber (
sut_test.gosetupGenesissaves and restores the three markers).Failing test
In package
statesync; the three marker writes simulate whatSetupGenesisBlockdoes when the genesis state is absent at restart:Fails inside
Initialize:Proposed fix
Only call
core.SetupGenesisBlockon a fresh database, where its marker writes are the correct initial values. On an initialized database, never touch head markers: verify the stored genesis hash, commit the genesis state (only if absent) with a marker-free state write, and run the chain-config compatibility check directly — the patterncchain/genesis.go(verifyAndWriteBlock+setupTrieDB) already uses. The test-harness workaround can then be deleted.Note that restoring the markers after the clobber is not a fix: the writes happen inside libevm against the raw db handle, so no batching makes clobber+restore atomic, and a crash between them leaves the same bricked state.