{"record":{"id":"ea232c9d4ea3db2a","repo":"ethereum/go-ethereum","slug":"invalid-fork-simulated-beacon-needs-to-be-started","errorCode":null,"errorMessage":"invalid fork, simulated beacon needs to be started post-merge","messagePattern":"invalid fork, simulated beacon needs to be started post-merge","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"eth/catalyst/simulated_beacon.go","lineNumber":114,"sourceCode":"\n\tfeeRecipient     common.Address\n\tfeeRecipientLock sync.Mutex // lock gates concurrent access to the feeRecipient\n\n\tengineAPI          *ConsensusAPI\n\tcurForkchoiceState engine.ForkchoiceStateV1\n\tlastBlockTime      uint64\n}\n\nfunc payloadVersion(config *params.ChainConfig, time uint64) engine.PayloadVersion {\n\tswitch config.LatestFork(time) {\n\tcase forks.Amsterdam:\n\t\treturn engine.PayloadV4\n\tcase forks.Bogota, forks.BPO5, forks.BPO4, forks.BPO3, forks.BPO2, forks.BPO1, forks.Osaka, forks.Prague, forks.Cancun:\n\t\treturn engine.PayloadV3\n\tcase forks.Paris, forks.Shanghai:\n\t\treturn engine.PayloadV2\n\t}\n\tpanic(\"invalid fork, simulated beacon needs to be started post-merge\")\n}\n\n// NewSimulatedBeacon constructs a new simulated beacon chain.\nfunc NewSimulatedBeacon(period uint64, feeRecipient common.Address, eth *eth.Ethereum) (*SimulatedBeacon, error) {\n\tblock := eth.BlockChain().CurrentBlock()\n\tcurrent := engine.ForkchoiceStateV1{\n\t\tHeadBlockHash:      block.Hash(),\n\t\tSafeBlockHash:      block.Hash(),\n\t\tFinalizedBlockHash: block.Hash(),\n\t}\n\tengineAPI := newConsensusAPIWithoutHeartbeat(eth)\n\n\t// if genesis block, send forkchoiceUpdated to trigger transition to PoS\n\tif block.Number.Sign() == 0 {\n\t\tversion := payloadVersion(eth.BlockChain().Config(), block.Time)\n\t\tif _, err := engineAPI.forkchoiceUpdated(context.Background(), current, nil, version, false); err != nil {\n\t\t\treturn nil, err\n\t\t}","sourceCodeStart":96,"sourceCodeEnd":132,"githubUrl":"https://github.com/ethereum/go-ethereum/blob/6bb0588ad8e7f922e4ad5580f51265a4097af08f/eth/catalyst/simulated_beacon.go#L96-L132","documentation":"payloadVersion maps the chain's latest fork at a given time to an engine-API payload version. The simulated beacon (dev mode) can only produce post-merge payloads (PayloadV2/V3/V4); if the latest active fork is anything pre-Paris (or an unknown fork), the switch falls through and the function panics because no merge payload format exists for that state.","triggerScenarios":"Starting geth with --dev (or --dev.period) against a genesis whose merge (Paris/terminal total difficulty) is not active at the current block/time — e.g. TTD or mergeBlock set in the future, or a custom pre-merge genesis — so config.LatestFork(time) returns a pre-Paris fork.","commonSituations":"Custom dev genesis JSON with pre-merge fork settings or unset merge fields; using a copied mainnet genesis with a future Shanghai timestamp while the simulated beacon starts at genesis time; version drift where a fork enum is not covered by the switch.","solutions":["Use the default dev genesis (geth --dev without a custom genesis file), which is post-merge by construction.","If a custom genesis is required, set terminalTotalDifficulty to 0 so the chain starts post-merge, and ensure all fork timestamps are at or before the dev chain's start time.","Verify with 'geth attach' that eth.getBlock('latest').difficulty == 0 before enabling the simulated beacon."],"exampleFix":"// before: datadir/genesis.json\n\"config\": { \"chainId\": 1337, \"homesteadBlock\": 0 /* no TTD */ }\n\n// after\n\"config\": { \"chainId\": 1337, \"homesteadBlock\": 0, \"terminalTotalDifficulty\": 0, \" parisBlock\": 0 }","handlingStrategy":"validation","validationCode":"// ensure the chain is post-merge before enabling --dev / simulated beacon\nhead := eth.BlockChain().CurrentBlock()\nif head.Difficulty.Cmp(common.Big0) > 0 && cfg.TerminalTotalDifficulty == nil {\n    return errors.New(\"simulated beacon requires a post-merge genesis; set terminalTotalDifficulty: 0\")\n}","typeGuard":null,"tryCatchPattern":"Not applicable — fix the genesis; recovering would leave dev mode without a payload version.","preventionTips":["Prefer stock --dev mode, which ships a valid post-merge dev genesis.","When customizing a dev genesis, set terminalTotalDifficulty to 0 and align fork timestamps with genesis.","Smoke-test custom dev geneses with a single --dev.period block before automation."],"tags":["dev-mode","simulated-beacon","merge","engine-api","genesis","panic"],"backgroundTag":null,"analyzedSha":"6bb0588ad8e7f922e4ad5580f51265a4097af08f","analyzedAt":"2026-08-15T10:06:53.996Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}