Skip to content

[saevm] setupGenesis silently rewrites head markers when genesis state is absent, permanently bricking a state-synced node #5926

Description

@rahulmutt-ava

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    evmRelated to EVM functionalitysae

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions