From 49fba66f63ce3d8b7c5b6b82529a686b88972951 Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Sat, 27 Jun 2026 10:54:20 +0200 Subject: [PATCH 1/3] internal/ethapi: make difficulty override a no-op post-merge in eth_simulateV1 A difficulty block override is documented as a no-op for post-merge blocks (see override.BlockOverrides.Difficulty), but makeHeaders sets the simulated header's difficulty to zero only before calling overrides.MakeHeader, which then re-applies the override unconditionally. The resulting non-zero difficulty makes the simulated header fail beacon.IsPoSHeader, so block assembly (Finalize/AssembleBlock) is routed through the legacy PoW engine even though the block carries post-merge fields. On a live post-merge chain this panics, and the RPC returns -32603 "method handler crashed" for any eth_simulateV1 call that overrides difficulty (e.g. {"blockOverrides":{"difficulty":"0x1"}}). eth_call is unaffected because it does not assemble a block. Re-zero the difficulty after MakeHeader for post-merge blocks to honour the documented no-op and keep the header on the PoS assembly path. Add a regression test asserting the simulated post-merge block reports difficulty 0x0 when the override is supplied. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/ethapi/api_test.go | 46 +++++++++++++++++++++++++++++++++++++ internal/ethapi/simulate.go | 11 ++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 80a9036ecc..6087de9c3d 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -2745,6 +2745,52 @@ func TestSimulateV1WithdrawalsByFork(t *testing.T) { }) } +// TestSimulateV1DifficultyOverridePostMerge checks that a difficulty block +// override is a no-op for post-merge blocks. Applying it would set a non-zero +// difficulty on the simulated header, which fails IsPoSHeader and routes block +// assembly through the legacy (PoW) engine even though the block carries +// post-merge fields, leading to a panic. +func TestSimulateV1DifficultyOverridePostMerge(t *testing.T) { + t.Parallel() + + gspec := &core.Genesis{Config: params.MergedTestChainConfig, Alloc: types.GenesisAlloc{}} + backend := newTestBackend(t, 1, gspec, beacon.New(ethash.NewFaker()), func(i int, b *core.BlockGen) { b.SetPoS() }) + + ctx := context.Background() + stateDB, baseHeader, err := backend.StateAndHeaderByNumberOrHash(ctx, rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)) + if err != nil { + t.Fatalf("failed to get state and header: %v", err) + } + sim := &simulator{ + b: backend, + state: stateDB, + base: baseHeader, + chainConfig: backend.ChainConfig(), + budget: newGasBudget(0), + } + + diff := (*hexutil.Big)(big.NewInt(0xCAFE0000)) + results, err := sim.execute(ctx, []simBlock{{ + BlockOverrides: &override.BlockOverrides{Difficulty: diff}, + }}) + if err != nil { + t.Fatalf("simulation execution failed: %v", err) + } + require.Len(t, results, 1) + + enc, err := json.Marshal(results[0]) + if err != nil { + t.Fatalf("failed to marshal result: %v", err) + } + var raw map[string]json.RawMessage + if err := json.Unmarshal(enc, &raw); err != nil { + t.Fatalf("failed to unmarshal result: %v", err) + } + if got := string(raw["difficulty"]); got != `"0x0"` { + t.Fatalf("difficulty override was not a no-op post-merge: got %s, want \"0x0\"\n%s", got, enc) + } +} + func TestSignTransaction(t *testing.T) { t.Parallel() // Initialize test accounts diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index 8462194b1d..c5752ce48e 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -562,8 +562,9 @@ func (sim *simulator) makeHeaders(blocks []simBlock) ([]*types.Header, error) { } // Set difficulty to zero if the given block is post-merge. Without this, all post-merge hardforks would remain inactive. // For example, calling eth_simulateV1(..., blockParameter: 0x0) on hoodi network will cause all blocks to have a difficulty of 1 and be treated as pre-merge. + isPostMerge := sim.chainConfig.IsPostMerge(number.Uint64(), timestamp) difficulty := header.Difficulty - if sim.chainConfig.IsPostMerge(number.Uint64(), timestamp) { + if isPostMerge { difficulty = big.NewInt(0) } header = overrides.MakeHeader(&types.Header{ @@ -576,6 +577,14 @@ func (sim *simulator) makeHeaders(blocks []simBlock) ([]*types.Header, error) { WithdrawalsHash: withdrawalsHash, ParentBeaconRoot: parentBeaconRoot, }) + // A difficulty override is documented as a no-op for post-merge blocks. + // MakeHeader applies it unconditionally though, so re-zero it here: a + // non-zero difficulty makes the simulated header fail IsPoSHeader, which + // routes block assembly through the legacy (PoW) engine even though the + // block carries post-merge fields, leading to a panic. + if isPostMerge { + header.Difficulty = big.NewInt(0) + } res[bi] = header } return res, nil From dd9ea020c3e7d8f9fe243961600a993a2f44c3e9 Mon Sep 17 00:00:00 2001 From: Manuel Arto <43347768+manusw7@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:23:25 +0200 Subject: [PATCH 2/3] Update simulate.go --- internal/ethapi/simulate.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index c5752ce48e..4d9c54464b 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -578,10 +578,6 @@ func (sim *simulator) makeHeaders(blocks []simBlock) ([]*types.Header, error) { ParentBeaconRoot: parentBeaconRoot, }) // A difficulty override is documented as a no-op for post-merge blocks. - // MakeHeader applies it unconditionally though, so re-zero it here: a - // non-zero difficulty makes the simulated header fail IsPoSHeader, which - // routes block assembly through the legacy (PoW) engine even though the - // block carries post-merge fields, leading to a panic. if isPostMerge { header.Difficulty = big.NewInt(0) } From 9e445ae7f3d2b6bfae96bf41f581c64535f0b96c Mon Sep 17 00:00:00 2001 From: Manuel Arto <43347768+manusw7@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:24:35 +0200 Subject: [PATCH 3/3] Remove obsolete comment from TestSimulateV1DifficultyOverridePostMerge Remove outdated test description for difficulty override. --- internal/ethapi/api_test.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 6087de9c3d..53a98fcf67 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -2745,11 +2745,6 @@ func TestSimulateV1WithdrawalsByFork(t *testing.T) { }) } -// TestSimulateV1DifficultyOverridePostMerge checks that a difficulty block -// override is a no-op for post-merge blocks. Applying it would set a non-zero -// difficulty on the simulated header, which fails IsPoSHeader and routes block -// assembly through the legacy (PoW) engine even though the block carries -// post-merge fields, leading to a panic. func TestSimulateV1DifficultyOverridePostMerge(t *testing.T) { t.Parallel()