From 3d28958b118025278746089c0d9230ebb9d83cef Mon Sep 17 00:00:00 2001 From: lupin012 <58134934+lupin012@users.noreply.github.com.> Date: Sat, 28 Feb 2026 10:03:20 +0100 Subject: [PATCH 1/4] rpc: unify BlockOverrides into a single type with three override methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two separate BlockOverrides structs existed in the codebase (ethapi.BlockOverrides and transactions.BlockOverrides) with overlapping but divergent fields, different field types, and different JSON tags. This caused inconsistencies across eth_call, eth_estimateGas, eth_simulateV1, and eth_callMany. Changes: - Merge into a single ethapi.BlockOverrides with all 11 fields: Number, Difficulty, Time, GasLimit, FeeRecipient, PrevRandao, BaseFeePerGas, BlobBaseFee, BeaconRoot, BlockHash, Withdrawals - Add ethapi.BlockHashOverrides (was only in transactions package) - Three methods with clearly separated responsibilities: - Override(*BlockContext) error — for eth_call/eth_estimateGas; rejects BeaconRoot and Withdrawals (aligned with Geth); now propagates error (was silently swallowed before) - OverrideHeader(*Header) *Header — for eth_simulateV1 block assembly - OverrideBlockContext(*BlockContext, BlockHashOverrides) — for eth_simulateV1/eth_callMany; applies all fields including BlobBaseFee and BlockHash - Remove duplicate struct from transactions/call.go - Replace manual field-by-field application in eth_callMany with OverrideBlockContext call - Add complete unit test suite: 20 tests covering all three methods, nil receivers, field isolation, overflow checks, rejection of unsupported fields, and BlockHash map merging Co-Authored-By: Claude Sonnet 4.6 --- rpc/ethapi/block_overrides.go | 139 +++++++++++--- rpc/ethapi/block_overrides_test.go | 293 +++++++++++++++++++++++++++-- rpc/jsonrpc/eth_callMany.go | 26 +-- rpc/jsonrpc/eth_simulation.go | 37 ++-- rpc/jsonrpc/eth_simulation_test.go | 29 +-- rpc/transactions/call.go | 80 +------- 6 files changed, 436 insertions(+), 168 deletions(-) diff --git a/rpc/ethapi/block_overrides.go b/rpc/ethapi/block_overrides.go index 6684d6f1e54..4bf1e3d7608 100644 --- a/rpc/ethapi/block_overrides.go +++ b/rpc/ethapi/block_overrides.go @@ -18,6 +18,9 @@ package ethapi import ( "errors" + "maps" + + "github.com/holiman/uint256" "github.com/erigontech/erigon/common" "github.com/erigontech/erigon/common/hexutil" @@ -26,55 +29,139 @@ import ( "github.com/erigontech/erigon/execution/vm/evmtypes" ) +// BlockHashOverrides maps block numbers to their hash overrides, +// used to intercept BLOCKHASH opcode calls during simulation. +type BlockHashOverrides map[uint64]common.Hash + +// BlockOverrides is the unified set of block-level fields that can be +// overridden during simulation or call execution (eth_call, eth_estimateGas, +// eth_simulateV1, eth_callMany). type BlockOverrides struct { - Number *hexutil.Big `json:"number"` - PrevRanDao *common.Hash `json:"prevRandao"` - Time *hexutil.Uint64 `json:"time"` - GasLimit *hexutil.Uint64 `json:"gasLimit"` - FeeRecipient *common.Address `json:"feeRecipient"` - BaseFeePerGas *hexutil.Big `json:"baseFeePerGas"` - BlobBaseFee *hexutil.Big `json:"blobBaseFee"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` + Number *hexutil.Big `json:"number"` + Difficulty *hexutil.Big `json:"difficulty"` + Time *hexutil.Uint64 `json:"time"` + GasLimit *hexutil.Uint64 `json:"gasLimit"` + FeeRecipient *common.Address `json:"feeRecipient"` + PrevRandao *common.Hash `json:"prevRandao"` + BaseFeePerGas *hexutil.Big `json:"baseFeePerGas"` + BlobBaseFee *hexutil.Big `json:"blobBaseFee"` + BeaconRoot *common.Hash `json:"beaconRoot"` + BlockHash *map[uint64]common.Hash `json:"blockHash"` + Withdrawals *types.Withdrawals `json:"withdrawals"` } +// Override applies overrides to an EVM block context used in single-call +// methods (eth_call, eth_estimateGas). BeaconRoot and Withdrawals are +// rejected because they are not meaningful in a single-call context. func (overrides *BlockOverrides) Override(context *evmtypes.BlockContext) error { - + if overrides == nil { + return nil + } + if overrides.BeaconRoot != nil { + return errors.New(`block override "beaconRoot" is not supported for this RPC method`) + } + if overrides.Withdrawals != nil { + return errors.New(`block override "withdrawals" is not supported for this RPC method`) + } if overrides.Number != nil { context.BlockNumber = overrides.Number.Uint64() } - - if overrides.PrevRanDao != nil { - context.PrevRanDao = overrides.PrevRanDao + if overrides.Difficulty != nil { + context.Difficulty.SetFromBig(overrides.Difficulty.ToInt()) + } + if overrides.PrevRandao != nil { + context.PrevRanDao = overrides.PrevRandao } - if overrides.Time != nil { - context.Time = overrides.Time.Uint64() + context.Time = uint64(*overrides.Time) } - if overrides.GasLimit != nil { - context.GasLimit = overrides.GasLimit.Uint64() + context.GasLimit = uint64(*overrides.GasLimit) } - if overrides.FeeRecipient != nil { context.Coinbase = accounts.InternAddress(*overrides.FeeRecipient) } - if overrides.BaseFeePerGas != nil { - overflow := context.BaseFee.SetFromBig(overrides.BaseFeePerGas.ToInt()) - if overflow { + if overflow := context.BaseFee.SetFromBig(overrides.BaseFeePerGas.ToInt()); overflow { return errors.New("BlockOverrides.BaseFee uint256 overflow") } } - if overrides.BlobBaseFee != nil { - overflow := context.BlobBaseFee.SetFromBig(overrides.BlobBaseFee.ToInt()) - if overflow { + if overflow := context.BlobBaseFee.SetFromBig(overrides.BlobBaseFee.ToInt()); overflow { return errors.New("BlockOverrides.BlobBaseFee uint256 overflow") } } + return nil +} - if overrides.Withdrawals != nil { - return errors.New("BlockOverrides.Withdrawals not supported") +// OverrideHeader returns a modified copy of header with the overridden fields +// applied. Used by eth_simulateV1 to build block headers before execution. +// BeaconRoot and Withdrawals are handled separately by the caller at the +// block-assembly level. +func (overrides *BlockOverrides) OverrideHeader(header *types.Header) *types.Header { + if overrides == nil { + return header + } + h := types.CopyHeader(header) + if overrides.Number != nil { + h.Number.SetFromBig(overrides.Number.ToInt()) + } + if overrides.Difficulty != nil { + h.Difficulty.SetFromBig(overrides.Difficulty.ToInt()) + } + if overrides.Time != nil { + h.Time = uint64(*overrides.Time) + } + if overrides.GasLimit != nil { + h.GasLimit = uint64(*overrides.GasLimit) + } + if overrides.FeeRecipient != nil { + h.Coinbase = *overrides.FeeRecipient + } + if overrides.BaseFeePerGas != nil { + baseFee := new(uint256.Int) + baseFee.SetFromBig(overrides.BaseFeePerGas.ToInt()) + h.BaseFee = baseFee + } + if overrides.PrevRandao != nil { + h.MixDigest = *overrides.PrevRandao + } + return h +} + +// OverrideBlockContext applies overrides to an EVM block context used in +// simulation (eth_simulateV1, eth_callMany). Unlike Override, it does not +// reject BeaconRoot or Withdrawals — those are handled at the block-assembly +// level by the caller. +func (overrides *BlockOverrides) OverrideBlockContext(blockCtx *evmtypes.BlockContext, blockHashOverrides BlockHashOverrides) { + if overrides == nil { + return + } + if overrides.Number != nil { + blockCtx.BlockNumber = overrides.Number.Uint64() + } + if overrides.Difficulty != nil { + blockCtx.Difficulty.SetFromBig(overrides.Difficulty.ToInt()) + } + if overrides.Time != nil { + blockCtx.Time = uint64(*overrides.Time) + } + if overrides.GasLimit != nil { + blockCtx.GasLimit = uint64(*overrides.GasLimit) + } + if overrides.FeeRecipient != nil { + blockCtx.Coinbase = accounts.InternAddress(*overrides.FeeRecipient) + } + if overrides.PrevRandao != nil { + blockCtx.PrevRanDao = overrides.PrevRandao + } + if overrides.BaseFeePerGas != nil { + blockCtx.BaseFee.SetFromBig(overrides.BaseFeePerGas.ToInt()) + } + if overrides.BlobBaseFee != nil { + blockCtx.BlobBaseFee.SetFromBig(overrides.BlobBaseFee.ToInt()) + } + if overrides.BlockHash != nil { + maps.Copy(blockHashOverrides, *overrides.BlockHash) } - return nil } diff --git a/rpc/ethapi/block_overrides_test.go b/rpc/ethapi/block_overrides_test.go index 5945d8e1b15..9d88314a065 100644 --- a/rpc/ethapi/block_overrides_test.go +++ b/rpc/ethapi/block_overrides_test.go @@ -1,33 +1,298 @@ package ethapi import ( + "math/big" "testing" + "github.com/holiman/uint256" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" "github.com/erigontech/erigon/common/hexutil" + "github.com/erigontech/erigon/execution/types" + "github.com/erigontech/erigon/execution/types/accounts" "github.com/erigontech/erigon/execution/vm/evmtypes" ) -func TestBlockOverridesOverrideGasLimit(t *testing.T) { - originalTime := uint64(42) - originalGasLimit := uint64(1_000_000) +// helpers + +func bigHex(n uint64) *hexutil.Big { return (*hexutil.Big)(new(big.Int).SetUint64(n)) } +func u64Hex(n uint64) *hexutil.Uint64 { u := hexutil.Uint64(n); return &u } +func hash(b byte) *common.Hash { h := common.Hash{b}; return &h } + +// ───────────────────────────────────────────────────────────────────────────── +// Override — EVM block context (eth_call / eth_estimateGas) +// ───────────────────────────────────────────────────────────────────────────── + +func TestOverride_NilReceiver(t *testing.T) { + var o *BlockOverrides + ctx := evmtypes.BlockContext{GasLimit: 1} + require.NoError(t, o.Override(&ctx)) + assert.Equal(t, uint64(1), ctx.GasLimit, "nil receiver must be a no-op") +} + +func TestOverride_AllFields(t *testing.T) { + prevRandao := common.Hash{0xab} + coinbase := common.Address{0x01} - blockCtx := evmtypes.BlockContext{ - Time: originalTime, - GasLimit: originalGasLimit, + o := BlockOverrides{ + Number: bigHex(100), + Difficulty: bigHex(200), + Time: u64Hex(300), + GasLimit: u64Hex(400), + FeeRecipient: &coinbase, + PrevRandao: &prevRandao, + BaseFeePerGas: bigHex(500), + BlobBaseFee: bigHex(600), } - newGasLimit := hexutil.Uint64(30_000_000) - overrides := BlockOverrides{GasLimit: &newGasLimit} + ctx := evmtypes.BlockContext{} + require.NoError(t, o.Override(&ctx)) + + assert.Equal(t, uint64(100), ctx.BlockNumber) + assert.Equal(t, *uint256.NewInt(200), ctx.Difficulty) + assert.Equal(t, uint64(300), ctx.Time) + assert.Equal(t, uint64(400), ctx.GasLimit) + assert.Equal(t, accounts.InternAddress(coinbase), ctx.Coinbase) + assert.Equal(t, &prevRandao, ctx.PrevRanDao) + assert.Equal(t, *uint256.NewInt(500), ctx.BaseFee) + assert.Equal(t, *uint256.NewInt(600), ctx.BlobBaseFee) +} + +func TestOverride_NilFieldsAreNoOp(t *testing.T) { + ctx := evmtypes.BlockContext{ + BlockNumber: 99, + Time: 42, + GasLimit: 1_000_000, + } + require.NoError(t, (&BlockOverrides{}).Override(&ctx)) + assert.Equal(t, uint64(99), ctx.BlockNumber) + assert.Equal(t, uint64(42), ctx.Time) + assert.Equal(t, uint64(1_000_000), ctx.GasLimit) +} + +func TestOverride_RejectsBeaconRoot(t *testing.T) { + o := BlockOverrides{BeaconRoot: hash(0x01)} + err := o.Override(&evmtypes.BlockContext{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "beaconRoot") +} + +func TestOverride_RejectsWithdrawals(t *testing.T) { + ws := types.Withdrawals{} + o := BlockOverrides{Withdrawals: &ws} + err := o.Override(&evmtypes.BlockContext{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "withdrawals") +} + +func TestOverride_BaseFeeOverflow(t *testing.T) { + // value larger than 2^256-1 → overflow + tooBig := new(big.Int).Lsh(big.NewInt(1), 256) // 2^256 + o := BlockOverrides{BaseFeePerGas: (*hexutil.Big)(tooBig)} + err := o.Override(&evmtypes.BlockContext{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "BaseFee") +} + +func TestOverride_BlobBaseFeeOverflow(t *testing.T) { + tooBig := new(big.Int).Lsh(big.NewInt(1), 256) + o := BlockOverrides{BlobBaseFee: (*hexutil.Big)(tooBig)} + err := o.Override(&evmtypes.BlockContext{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "BlobBaseFee") +} - if err := overrides.Override(&blockCtx); err != nil { - t.Fatalf("override returned error: %v", err) +func TestOverride_PartialOverrideDoesNotTouchOtherFields(t *testing.T) { + ctx := evmtypes.BlockContext{ + BlockNumber: 10, + Time: 99, + GasLimit: 5_000, } + // only override GasLimit + o := BlockOverrides{GasLimit: u64Hex(30_000_000)} + require.NoError(t, o.Override(&ctx)) + assert.Equal(t, uint64(10), ctx.BlockNumber, "BlockNumber must be unchanged") + assert.Equal(t, uint64(99), ctx.Time, "Time must be unchanged") + assert.Equal(t, uint64(30_000_000), ctx.GasLimit) +} + +// ───────────────────────────────────────────────────────────────────────────── +// OverrideHeader — block header copy (eth_simulateV1 block assembly) +// ───────────────────────────────────────────────────────────────────────────── + +func baseHeader() *types.Header { + h := types.NewEmptyHeaderForAssembling() + h.Number.SetUint64(1) + h.Difficulty.SetUint64(1) + h.Time = 1 + h.GasLimit = 1 + h.Coinbase = common.Address{0xFF} + h.BaseFee = uint256.NewInt(1) + h.MixDigest = common.Hash{0xFF} + return h +} - if blockCtx.GasLimit != uint64(newGasLimit) { - t.Fatalf("unexpected gas limit override: have %d want %d", blockCtx.GasLimit, uint64(newGasLimit)) +func TestOverrideHeader_NilReceiver(t *testing.T) { + var o *BlockOverrides + orig := baseHeader() + result := o.OverrideHeader(orig) + assert.Equal(t, orig, result, "nil receiver must return the original header unchanged") +} + +func TestOverrideHeader_ReturnsACopy(t *testing.T) { + orig := baseHeader() + o := BlockOverrides{Number: bigHex(999)} + result := o.OverrideHeader(orig) + assert.NotSame(t, orig, result, "must return a new copy, not the same pointer") + assert.Equal(t, uint64(1), orig.Number.Uint64(), "original must be untouched") + assert.Equal(t, uint64(999), result.Number.Uint64()) +} + +func TestOverrideHeader_AllFields(t *testing.T) { + coinbase := common.Address{0x02} + prevRandao := common.Hash{0xCD} + + o := BlockOverrides{ + Number: bigHex(1000), + Difficulty: bigHex(2000), + Time: u64Hex(3000), + GasLimit: u64Hex(4000), + FeeRecipient: &coinbase, + BaseFeePerGas: bigHex(5000), + PrevRandao: &prevRandao, } - if blockCtx.Time != originalTime { - t.Fatalf("timestamp changed by gas limit override: have %d want %d", blockCtx.Time, originalTime) + result := o.OverrideHeader(baseHeader()) + + assert.Equal(t, uint64(1000), result.Number.Uint64()) + assert.Equal(t, uint64(2000), result.Difficulty.Uint64()) + assert.Equal(t, uint64(3000), result.Time) + assert.Equal(t, uint64(4000), result.GasLimit) + assert.Equal(t, coinbase, result.Coinbase) + assert.Equal(t, uint256.NewInt(5000), result.BaseFee) + assert.Equal(t, prevRandao, result.MixDigest) +} + +func TestOverrideHeader_NilFieldsAreNoOp(t *testing.T) { + orig := baseHeader() + result := (&BlockOverrides{}).OverrideHeader(orig) + assert.Equal(t, orig.Number.Uint64(), result.Number.Uint64()) + assert.Equal(t, orig.Time, result.Time) + assert.Equal(t, orig.GasLimit, result.GasLimit) + assert.Equal(t, orig.Coinbase, result.Coinbase) +} + +func TestOverrideHeader_BeaconRootAndWithdrawalsIgnored(t *testing.T) { + // BeaconRoot and Withdrawals are not header fields — must not panic + ws := types.Withdrawals{} + o := BlockOverrides{ + BeaconRoot: hash(0x01), + Withdrawals: &ws, + } + require.NotPanics(t, func() { + _ = o.OverrideHeader(baseHeader()) + }) +} + +// ───────────────────────────────────────────────────────────────────────────── +// OverrideBlockContext — EVM context for simulation (eth_simulateV1 / eth_callMany) +// ───────────────────────────────────────────────────────────────────────────── + +func TestOverrideBlockContext_NilReceiver(t *testing.T) { + var o *BlockOverrides + ctx := evmtypes.BlockContext{GasLimit: 1} + overrides := BlockHashOverrides{} + require.NotPanics(t, func() { o.OverrideBlockContext(&ctx, overrides) }) + assert.Equal(t, uint64(1), ctx.GasLimit, "nil receiver must be a no-op") +} + +func TestOverrideBlockContext_AllFields(t *testing.T) { + coinbase := common.Address{0x03} + prevRandao := common.Hash{0xEF} + + o := BlockOverrides{ + Number: bigHex(10), + Difficulty: bigHex(20), + Time: u64Hex(30), + GasLimit: u64Hex(40), + FeeRecipient: &coinbase, + PrevRandao: &prevRandao, + BaseFeePerGas: bigHex(50), + BlobBaseFee: bigHex(60), + } + + ctx := evmtypes.BlockContext{} + o.OverrideBlockContext(&ctx, BlockHashOverrides{}) + + assert.Equal(t, uint64(10), ctx.BlockNumber) + assert.Equal(t, *uint256.NewInt(20), ctx.Difficulty) + assert.Equal(t, uint64(30), ctx.Time) + assert.Equal(t, uint64(40), ctx.GasLimit) + assert.Equal(t, accounts.InternAddress(coinbase), ctx.Coinbase) + assert.Equal(t, &prevRandao, ctx.PrevRanDao) + assert.Equal(t, *uint256.NewInt(50), ctx.BaseFee) + assert.Equal(t, *uint256.NewInt(60), ctx.BlobBaseFee) +} + +func TestOverrideBlockContext_NilFieldsAreNoOp(t *testing.T) { + ctx := evmtypes.BlockContext{ + BlockNumber: 77, + Time: 88, + GasLimit: 99, + } + (&BlockOverrides{}).OverrideBlockContext(&ctx, BlockHashOverrides{}) + assert.Equal(t, uint64(77), ctx.BlockNumber) + assert.Equal(t, uint64(88), ctx.Time) + assert.Equal(t, uint64(99), ctx.GasLimit) +} + +func TestOverrideBlockContext_BlockHash(t *testing.T) { + h1 := common.Hash{0x01} + h2 := common.Hash{0x02} + blockHashes := map[uint64]common.Hash{100: h1} + o := BlockOverrides{BlockHash: &map[uint64]common.Hash{101: h2}} + + o.OverrideBlockContext(&evmtypes.BlockContext{}, BlockHashOverrides(blockHashes)) + + assert.Equal(t, h1, blockHashes[100], "existing entry must be preserved") + assert.Equal(t, h2, blockHashes[101], "new entry from override must be present") +} + +func TestOverrideBlockContext_BlockHashOverwritesExisting(t *testing.T) { + orig := common.Hash{0xAA} + replacement := common.Hash{0xBB} + blockHashes := map[uint64]common.Hash{200: orig} + o := BlockOverrides{BlockHash: &map[uint64]common.Hash{200: replacement}} + + o.OverrideBlockContext(&evmtypes.BlockContext{}, BlockHashOverrides(blockHashes)) + + assert.Equal(t, replacement, blockHashes[200], "override must replace existing entry") +} + +func TestOverrideBlockContext_BeaconRootAndWithdrawalsDoNotPanic(t *testing.T) { + // BeaconRoot and Withdrawals are not applied here — caller handles them. + // Verify no panic occurs when they are set. + ws := types.Withdrawals{} + o := BlockOverrides{ + BeaconRoot: hash(0x01), + Withdrawals: &ws, + } + require.NotPanics(t, func() { + o.OverrideBlockContext(&evmtypes.BlockContext{}, BlockHashOverrides{}) + }) +} + +func TestOverrideBlockContext_PartialOverrideDoesNotTouchOtherFields(t *testing.T) { + ctx := evmtypes.BlockContext{ + BlockNumber: 5, + Time: 55, + GasLimit: 555, } + o := BlockOverrides{Time: u64Hex(999)} + o.OverrideBlockContext(&ctx, BlockHashOverrides{}) + assert.Equal(t, uint64(5), ctx.BlockNumber, "BlockNumber must be unchanged") + assert.Equal(t, uint64(999), ctx.Time) + assert.Equal(t, uint64(555), ctx.GasLimit, "GasLimit must be unchanged") } diff --git a/rpc/jsonrpc/eth_callMany.go b/rpc/jsonrpc/eth_callMany.go index 1a3d260649f..86ab049974e 100644 --- a/rpc/jsonrpc/eth_callMany.go +++ b/rpc/jsonrpc/eth_callMany.go @@ -21,7 +21,6 @@ import ( "encoding/hex" "errors" "fmt" - "maps" "time" "github.com/erigontech/erigon/common" @@ -37,12 +36,11 @@ import ( "github.com/erigontech/erigon/rpc" "github.com/erigontech/erigon/rpc/ethapi" "github.com/erigontech/erigon/rpc/rpchelper" - "github.com/erigontech/erigon/rpc/transactions" ) type Bundle struct { Transactions []ethapi.CallArgs - BlockOverride transactions.BlockOverrides + BlockOverride ethapi.BlockOverrides } type StateContext struct { @@ -215,27 +213,7 @@ func (api *APIImpl) CallMany(ctx context.Context, bundles []Bundle, simulateCont for _, bundle := range bundles { // first change blockContext - if bundle.BlockOverride.BlockNumber != nil { - blockCtx.BlockNumber = uint64(*bundle.BlockOverride.BlockNumber) - } - if bundle.BlockOverride.BaseFee != nil { - blockCtx.BaseFee = *bundle.BlockOverride.BaseFee - } - if bundle.BlockOverride.Coinbase != nil { - blockCtx.Coinbase = accounts.InternAddress(*bundle.BlockOverride.Coinbase) - } - if bundle.BlockOverride.Difficulty != nil { - blockCtx.Difficulty.SetUint64(uint64(*bundle.BlockOverride.Difficulty)) - } - if bundle.BlockOverride.Timestamp != nil { - blockCtx.Time = uint64(*bundle.BlockOverride.Timestamp) - } - if bundle.BlockOverride.GasLimit != nil { - blockCtx.GasLimit = uint64(*bundle.BlockOverride.GasLimit) - } - if bundle.BlockOverride.BlockHash != nil { - maps.Copy(overrideBlockHash, *bundle.BlockOverride.BlockHash) - } + bundle.BlockOverride.OverrideBlockContext(&blockCtx, ethapi.BlockHashOverrides(overrideBlockHash)) results := []map[string]any{} for _, txn := range bundle.Transactions { if txn.Gas == nil || *(txn.Gas) == 0 { diff --git a/rpc/jsonrpc/eth_simulation.go b/rpc/jsonrpc/eth_simulation.go index f20a7123b45..e17571dd9f8 100644 --- a/rpc/jsonrpc/eth_simulation.go +++ b/rpc/jsonrpc/eth_simulation.go @@ -72,9 +72,9 @@ type SimulationRequest struct { // SimulatedBlock defines the simulation for a single block. type SimulatedBlock struct { - BlockOverrides *transactions.BlockOverrides `json:"blockOverrides,omitempty"` - StateOverrides *ethapi.StateOverrides `json:"stateOverrides,omitempty"` - Calls []ethapi.CallArgs `json:"calls"` + BlockOverrides *ethapi.BlockOverrides `json:"blockOverrides,omitempty"` + StateOverrides *ethapi.StateOverrides `json:"stateOverrides,omitempty"` + Calls []ethapi.CallArgs `json:"calls"` } // CallResult represents the result of a single call in the simulation. @@ -233,13 +233,13 @@ func (s *simulator) sanitizeSimulatedBlocks(blocks []SimulatedBlock) ([]Simulate prevTimestamp := s.base.Time for _, block := range blocks { if block.BlockOverrides == nil { - block.BlockOverrides = &transactions.BlockOverrides{} + block.BlockOverrides = ðapi.BlockOverrides{} } - if block.BlockOverrides.BlockNumber == nil { + if block.BlockOverrides.Number == nil { nextNumber := prevNumber + 1 - block.BlockOverrides.BlockNumber = (*hexutil.Uint64)(&nextNumber) + block.BlockOverrides.Number = (*hexutil.Big)(new(big.Int).SetUint64(nextNumber)) } - blockNumber := block.BlockOverrides.BlockNumber.Uint64() + blockNumber := block.BlockOverrides.Number.Uint64() if blockNumber <= prevNumber { return nil, invalidBlockNumberError(fmt.Sprintf("block numbers must be in order: %d <= %d", blockNumber, prevNumber)) } @@ -255,9 +255,9 @@ func (s *simulator) sanitizeSimulatedBlocks(blocks []SimulatedBlock) ([]Simulate n := prevNumber + i + 1 t := prevTimestamp + timestampIncrement b := SimulatedBlock{ - BlockOverrides: &transactions.BlockOverrides{ - BlockNumber: (*hexutil.Uint64)(&n), - Timestamp: (*hexutil.Uint64)(&t), + BlockOverrides: ðapi.BlockOverrides{ + Number: (*hexutil.Big)(new(big.Int).SetUint64(n)), + Time: (*hexutil.Uint64)(&t), }, } prevTimestamp = t @@ -267,11 +267,11 @@ func (s *simulator) sanitizeSimulatedBlocks(blocks []SimulatedBlock) ([]Simulate // Only append block after filling a potential gap. prevNumber = blockNumber var timestamp uint64 - if block.BlockOverrides.Timestamp == nil { + if block.BlockOverrides.Time == nil { timestamp = prevTimestamp + timestampIncrement - block.BlockOverrides.Timestamp = (*hexutil.Uint64)(×tamp) + block.BlockOverrides.Time = (*hexutil.Uint64)(×tamp) } else { - timestamp = block.BlockOverrides.Timestamp.Uint64() + timestamp = block.BlockOverrides.Time.Uint64() if timestamp <= prevTimestamp { return nil, invalidBlockTimestampError(fmt.Sprintf("block timestamps must be in order: %d <= %d", timestamp, prevTimestamp)) } @@ -290,17 +290,17 @@ func (s *simulator) makeHeaders(blocks []SimulatedBlock) ([]*types.Header, error header := s.base headers := make([]*types.Header, len(blocks)) for bi, block := range blocks { - if block.BlockOverrides == nil || block.BlockOverrides.BlockNumber == nil { + if block.BlockOverrides == nil || block.BlockOverrides.Number == nil { return nil, errors.New("empty block number") } overrides := block.BlockOverrides var withdrawalsHash *common.Hash - if s.chainConfig.IsShanghai((uint64)(*overrides.Timestamp)) { + if s.chainConfig.IsShanghai((uint64)(*overrides.Time)) { withdrawalsHash = &empty.WithdrawalsHash } var parentBeaconRoot *common.Hash - if s.chainConfig.IsCancun((uint64)(*overrides.Timestamp)) { + if s.chainConfig.IsCancun((uint64)(*overrides.Time)) { parentBeaconRoot = &common.Hash{} if overrides.BeaconRoot != nil { parentBeaconRoot = overrides.BeaconRoot @@ -490,7 +490,7 @@ func (s *simulator) simulateBlock( blockNumber := header.Number.Uint64() - blockHashOverrides := transactions.BlockHashOverrides{} + blockHashOverrides := ethapi.BlockHashOverrides{} txnList := make([]types.Transaction, 0, len(bsc.Calls)) receiptList := make(types.Receipts, 0, len(bsc.Calls)) tracer := rpchelper.NewLogTracer(s.traceTransfers, blockNumber, common.Hash{}, common.Hash{}, 0) @@ -521,9 +521,6 @@ func (s *simulator) simulateBlock( // Create a custom block context and apply any custom block overrides blockCtx := transactions.NewEVMBlockContextWithOverrides(ctx, s.engine, header, tx, s.newSimulatedCanonicalReader(ancestors), s.chainConfig, bsc.BlockOverrides, blockHashOverrides) - if bsc.BlockOverrides.BlobBaseFee != nil { - blockCtx.BlobBaseFee = *bsc.BlockOverrides.BlobBaseFee.ToUint256() - } rules := blockCtx.Rules(s.chainConfig) // Determine the active precompiled contracts for this block. diff --git a/rpc/jsonrpc/eth_simulation_test.go b/rpc/jsonrpc/eth_simulation_test.go index a93f4748f86..c019356c635 100644 --- a/rpc/jsonrpc/eth_simulation_test.go +++ b/rpc/jsonrpc/eth_simulation_test.go @@ -1,13 +1,14 @@ package jsonrpc import ( + "math/big" "testing" "github.com/holiman/uint256" "github.com/erigontech/erigon/common/hexutil" "github.com/erigontech/erigon/execution/types" - "github.com/erigontech/erigon/rpc/transactions" + "github.com/erigontech/erigon/rpc/ethapi" ) func TestSimulateSanitizeBlockOrder(t *testing.T) { @@ -31,37 +32,37 @@ func TestSimulateSanitizeBlockOrder(t *testing.T) { { baseNumber: 10, baseTimestamp: 50, - blocks: []SimulatedBlock{{BlockOverrides: &transactions.BlockOverrides{BlockNumber: newUint64(13), Timestamp: newUint64(80)}}, {}}, + blocks: []SimulatedBlock{{BlockOverrides: ðapi.BlockOverrides{Number: newBig(13), Time: newUint64(80)}}, {}}, expected: []result{{number: 11, timestamp: 62}, {number: 12, timestamp: 74}, {number: 13, timestamp: 80}, {number: 14, timestamp: 92}}, }, { baseNumber: 10, baseTimestamp: 50, - blocks: []SimulatedBlock{{BlockOverrides: &transactions.BlockOverrides{BlockNumber: newUint64(11)}}, {BlockOverrides: &transactions.BlockOverrides{BlockNumber: newUint64(14)}}, {}}, + blocks: []SimulatedBlock{{BlockOverrides: ðapi.BlockOverrides{Number: newBig(11)}}, {BlockOverrides: ðapi.BlockOverrides{Number: newBig(14)}}, {}}, expected: []result{{number: 11, timestamp: 62}, {number: 12, timestamp: 74}, {number: 13, timestamp: 86}, {number: 14, timestamp: 98}, {number: 15, timestamp: 110}}, }, { baseNumber: 10, baseTimestamp: 50, - blocks: []SimulatedBlock{{BlockOverrides: &transactions.BlockOverrides{BlockNumber: newUint64(13)}}, {BlockOverrides: &transactions.BlockOverrides{BlockNumber: newUint64(12)}}}, + blocks: []SimulatedBlock{{BlockOverrides: ðapi.BlockOverrides{Number: newBig(13)}}, {BlockOverrides: ðapi.BlockOverrides{Number: newBig(12)}}}, err: "block numbers must be in order: 12 <= 13", }, { baseNumber: 10, baseTimestamp: 50, - blocks: []SimulatedBlock{{BlockOverrides: &transactions.BlockOverrides{BlockNumber: newUint64(13), Timestamp: newUint64(74)}}}, + blocks: []SimulatedBlock{{BlockOverrides: ðapi.BlockOverrides{Number: newBig(13), Time: newUint64(74)}}}, err: "block timestamps must be in order: 74 <= 74", }, { baseNumber: 10, baseTimestamp: 50, - blocks: []SimulatedBlock{{BlockOverrides: &transactions.BlockOverrides{BlockNumber: newUint64(11), Timestamp: newUint64(60)}}, {BlockOverrides: &transactions.BlockOverrides{BlockNumber: newUint64(12), Timestamp: newUint64(55)}}}, + blocks: []SimulatedBlock{{BlockOverrides: ðapi.BlockOverrides{Number: newBig(11), Time: newUint64(60)}}, {BlockOverrides: ðapi.BlockOverrides{Number: newBig(12), Time: newUint64(55)}}}, err: "block timestamps must be in order: 55 <= 60", }, { baseNumber: 10, baseTimestamp: 50, - blocks: []SimulatedBlock{{BlockOverrides: &transactions.BlockOverrides{BlockNumber: newUint64(11), Timestamp: newUint64(60)}}, {BlockOverrides: &transactions.BlockOverrides{BlockNumber: newUint64(13), Timestamp: newUint64(72)}}}, + blocks: []SimulatedBlock{{BlockOverrides: ðapi.BlockOverrides{Number: newBig(11), Time: newUint64(60)}}, {BlockOverrides: ðapi.BlockOverrides{Number: newBig(13), Time: newUint64(72)}}}, err: "block timestamps must be in order: 72 <= 72", }, } { @@ -84,16 +85,16 @@ func TestSimulateSanitizeBlockOrder(t *testing.T) { if b.BlockOverrides == nil { t.Fatalf("testcase %d: block overrides nil", i) } - if b.BlockOverrides.BlockNumber == nil { + if b.BlockOverrides.Number == nil { t.Fatalf("testcase %d: block number not set", i) } - if b.BlockOverrides.Timestamp == nil { + if b.BlockOverrides.Time == nil { t.Fatalf("testcase %d: block time not set", i) } - if uint64(*b.BlockOverrides.Timestamp) != tc.expected[bi].timestamp { - t.Errorf("testcase %d: block timestamp mismatch. Want %d, have %d", i, tc.expected[bi].timestamp, uint64(*b.BlockOverrides.Timestamp)) + if uint64(*b.BlockOverrides.Time) != tc.expected[bi].timestamp { + t.Errorf("testcase %d: block timestamp mismatch. Want %d, have %d", i, tc.expected[bi].timestamp, uint64(*b.BlockOverrides.Time)) } - have := b.BlockOverrides.BlockNumber.Uint64() + have := b.BlockOverrides.Number.Uint64() if have != tc.expected[bi].number { t.Errorf("testcase %d: block number mismatch. Want %d, have %d", i, tc.expected[bi].number, have) } @@ -105,3 +106,7 @@ func newUint64(n uint64) *hexutil.Uint64 { u := hexutil.Uint64(n) return &u } + +func newBig(n uint64) *hexutil.Big { + return (*hexutil.Big)(new(big.Int).SetUint64(n)) +} diff --git a/rpc/transactions/call.go b/rpc/transactions/call.go index 90c3494adef..8c81785a1dd 100644 --- a/rpc/transactions/call.go +++ b/rpc/transactions/call.go @@ -19,13 +19,11 @@ package transactions import ( "context" "fmt" - "maps" "time" "github.com/holiman/uint256" "github.com/erigontech/erigon/common" - "github.com/erigontech/erigon/common/hexutil" "github.com/erigontech/erigon/common/log/v3" "github.com/erigontech/erigon/db/kv" "github.com/erigontech/erigon/db/services" @@ -41,72 +39,6 @@ import ( ethapi2 "github.com/erigontech/erigon/rpc/ethapi" ) -type BlockOverrides struct { - BlockNumber *hexutil.Uint64 `json:"number"` - Coinbase *common.Address `json:"feeRecipient"` - Timestamp *hexutil.Uint64 `json:"time"` - GasLimit *hexutil.Uint `json:"gasLimit"` - Difficulty *hexutil.Uint64 `json:"difficulty"` - BaseFee *uint256.Int `json:"baseFeePerGas"` - BlobBaseFee *hexutil.Big `json:"blobBaseFee"` - BlockHash *map[uint64]common.Hash `json:"blockHash"` - BeaconRoot *common.Hash `json:"beaconRoot"` - Withdrawals *types.Withdrawals `json:"withdrawals"` - PrevRandao *common.Hash `json:"prevRandao"` -} - -type BlockHashOverrides map[uint64]common.Hash - -func (o *BlockOverrides) OverrideHeader(header *types.Header) *types.Header { - h := types.CopyHeader(header) - if o.BlockNumber != nil { - h.Number.SetUint64(uint64(*o.BlockNumber)) - } - if o.Difficulty != nil { - h.Difficulty.SetUint64(uint64(*o.Difficulty)) - } - if o.Timestamp != nil { - h.Time = o.Timestamp.Uint64() - } - if o.GasLimit != nil { - h.GasLimit = uint64(*o.GasLimit) - } - if o.Coinbase != nil { - h.Coinbase = *o.Coinbase - } - if o.BaseFee != nil { - h.BaseFee = o.BaseFee - } - if o.PrevRandao != nil { - h.MixDigest = *o.PrevRandao - } - return h -} - -func (o *BlockOverrides) OverrideBlockContext(blockCtx *evmtypes.BlockContext, overrideBlockHash BlockHashOverrides) { - if o.BlockNumber != nil { - blockCtx.BlockNumber = uint64(*o.BlockNumber) - } - if o.BaseFee != nil { - blockCtx.BaseFee = *o.BaseFee - } - if o.Coinbase != nil { - blockCtx.Coinbase = accounts.InternAddress(*o.Coinbase) - } - if o.Difficulty != nil { - blockCtx.Difficulty = *uint256.NewInt(uint64(*o.Difficulty)) - } - if o.Timestamp != nil { - blockCtx.Time = uint64(*o.Timestamp) - } - if o.GasLimit != nil { - blockCtx.GasLimit = uint64(*o.GasLimit) - } - if o.BlockHash != nil { - maps.Copy(overrideBlockHash, *o.BlockHash) - } -} - func DoCall( ctx context.Context, engine rules.EngineReader, @@ -152,7 +84,9 @@ func DoCall( } blockCtx := NewEVMBlockContext(engine, header, blockNrOrHash.RequireCanonical, tx, headerReader, chainConfig) if blockOverrides != nil { - blockOverrides.Override(&blockCtx) + if err := blockOverrides.Override(&blockCtx); err != nil { + return nil, err + } } txCtx := protocol.NewEVMTxContext(msg) evm := vm.NewEVM(blockCtx, txCtx, state, chainConfig, vm.Config{NoBaseFee: true}) @@ -187,7 +121,7 @@ func DoCall( } func NewEVMBlockContextWithOverrides(ctx context.Context, engine rules.EngineReader, header *types.Header, tx kv.Getter, - reader services.CanonicalReader, config *chain.Config, blockOverrides *BlockOverrides, blockHashOverrides BlockHashOverrides) evmtypes.BlockContext { + reader services.CanonicalReader, config *chain.Config, blockOverrides *ethapi2.BlockOverrides, blockHashOverrides ethapi2.BlockHashOverrides) evmtypes.BlockContext { blockHashFunc := MakeBlockHashProvider(ctx, tx, reader, blockHashOverrides) blockContext := protocol.NewEVMBlockContext(header, blockHashFunc, engine, accounts.NilAddress /* author */, config) if blockOverrides != nil { @@ -204,7 +138,7 @@ func NewEVMBlockContext(engine rules.EngineReader, header *types.Header, require type BlockHashProvider func(blockNum uint64) (common.Hash, error) -func MakeBlockHashProvider(ctx context.Context, tx kv.Getter, reader services.CanonicalReader, overrides BlockHashOverrides) BlockHashProvider { +func MakeBlockHashProvider(ctx context.Context, tx kv.Getter, reader services.CanonicalReader, overrides ethapi2.BlockHashOverrides) BlockHashProvider { return func(blockNum uint64) (common.Hash, error) { if blockHash, ok := overrides[blockNum]; ok { return blockHash, nil @@ -320,7 +254,9 @@ func NewReusableCaller( blockCtx := NewEVMBlockContext(engine, header, blockNrOrHash.RequireCanonical, tx, headerReader, chainConfig) if blockOverrides != nil { - blockOverrides.Override(&blockCtx) + if err := blockOverrides.Override(&blockCtx); err != nil { + return nil, err + } } txCtx := protocol.NewEVMTxContext(msg) From 0e9a4da8f49e3737ceb36cea311ba5216fb33c98 Mon Sep 17 00:00:00 2001 From: lupin012 <58134934+lupin012@users.noreply.github.com.> Date: Sat, 28 Feb 2026 10:22:44 +0100 Subject: [PATCH 2/4] modified error string to avoid change now rpc-test --- rpc/ethapi/block_overrides.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpc/ethapi/block_overrides.go b/rpc/ethapi/block_overrides.go index 4bf1e3d7608..2c22d537398 100644 --- a/rpc/ethapi/block_overrides.go +++ b/rpc/ethapi/block_overrides.go @@ -61,7 +61,7 @@ func (overrides *BlockOverrides) Override(context *evmtypes.BlockContext) error return errors.New(`block override "beaconRoot" is not supported for this RPC method`) } if overrides.Withdrawals != nil { - return errors.New(`block override "withdrawals" is not supported for this RPC method`) + return errors.New(`BlockOverrides.Withdrawals not supported`) } if overrides.Number != nil { context.BlockNumber = overrides.Number.Uint64() From 710143a9eb5cc7ebee0ec5b69353fd71c521edc9 Mon Sep 17 00:00:00 2001 From: lupin012 <58134934+lupin012@users.noreply.github.com.> Date: Sat, 28 Feb 2026 15:01:22 +0100 Subject: [PATCH 3/4] rpc/ethapi: fix withdrawals error message in BlockOverrides.Override Align the error message for the Withdrawals rejection with the Geth format and with the existing BeaconRoot message: both now use the lowercase-field-name pattern so that callers can reliably check for the field name (e.g. assert.Contains(..., "withdrawals")). Co-Authored-By: Claude Sonnet 4.6 --- rpc/ethapi/block_overrides.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpc/ethapi/block_overrides.go b/rpc/ethapi/block_overrides.go index 2c22d537398..4bf1e3d7608 100644 --- a/rpc/ethapi/block_overrides.go +++ b/rpc/ethapi/block_overrides.go @@ -61,7 +61,7 @@ func (overrides *BlockOverrides) Override(context *evmtypes.BlockContext) error return errors.New(`block override "beaconRoot" is not supported for this RPC method`) } if overrides.Withdrawals != nil { - return errors.New(`BlockOverrides.Withdrawals not supported`) + return errors.New(`block override "withdrawals" is not supported for this RPC method`) } if overrides.Number != nil { context.BlockNumber = overrides.Number.Uint64() From f54fe036538ee0e0faccdf03b08395dd1d609736 Mon Sep 17 00:00:00 2001 From: lupin012 <58134934+lupin012@users.noreply.github.com.> Date: Mon, 2 Mar 2026 22:59:32 +0100 Subject: [PATCH 4/4] update rpc-version --- .github/workflows/scripts/run_rpc_tests_ethereum.sh | 2 +- .github/workflows/scripts/run_rpc_tests_ethereum_latest.sh | 2 +- .github/workflows/scripts/run_rpc_tests_gnosis.sh | 2 +- .github/workflows/scripts/run_rpc_tests_remote_ethereum.sh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/scripts/run_rpc_tests_ethereum.sh b/.github/workflows/scripts/run_rpc_tests_ethereum.sh index 648205d9442..dedcf876679 100755 --- a/.github/workflows/scripts/run_rpc_tests_ethereum.sh +++ b/.github/workflows/scripts/run_rpc_tests_ethereum.sh @@ -43,4 +43,4 @@ DISABLED_TEST_LIST=( DISABLED_TESTS=$(IFS=,; echo "${DISABLED_TEST_LIST[*]}") # Call the main test runner script with the required and optional parameters -"$(dirname "$0")/run_rpc_tests.sh" mainnet v1.119.0 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR" +"$(dirname "$0")/run_rpc_tests.sh" mainnet v1.121.0 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR" diff --git a/.github/workflows/scripts/run_rpc_tests_ethereum_latest.sh b/.github/workflows/scripts/run_rpc_tests_ethereum_latest.sh index 14f5d2b3153..e526c1a782e 100755 --- a/.github/workflows/scripts/run_rpc_tests_ethereum_latest.sh +++ b/.github/workflows/scripts/run_rpc_tests_ethereum_latest.sh @@ -34,4 +34,4 @@ DISABLED_TEST_LIST=( DISABLED_TESTS=$(IFS=,; echo "${DISABLED_TEST_LIST[*]}") # Call the main test runner script with the required and optional parameters -"$(dirname "$0")/run_rpc_tests.sh" mainnet v1.119.0 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR" "latest" "$REFERENCE_HOST" "do-not-compare-error-message" "$DUMP_RESPONSE" +"$(dirname "$0")/run_rpc_tests.sh" mainnet v1.121.0 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR" "latest" "$REFERENCE_HOST" "do-not-compare-error-message" "$DUMP_RESPONSE" diff --git a/.github/workflows/scripts/run_rpc_tests_gnosis.sh b/.github/workflows/scripts/run_rpc_tests_gnosis.sh index 4192b4241be..b3de1a58484 100755 --- a/.github/workflows/scripts/run_rpc_tests_gnosis.sh +++ b/.github/workflows/scripts/run_rpc_tests_gnosis.sh @@ -22,5 +22,5 @@ DISABLED_TEST_LIST=( DISABLED_TESTS=$(IFS=,; echo "${DISABLED_TEST_LIST[*]}") # Call the main test runner script with the required and optional parameters -"$(dirname "$0")/run_rpc_tests.sh" gnosis v1.119.0 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR" +"$(dirname "$0")/run_rpc_tests.sh" gnosis v1.121.0 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR" diff --git a/.github/workflows/scripts/run_rpc_tests_remote_ethereum.sh b/.github/workflows/scripts/run_rpc_tests_remote_ethereum.sh index 0a61da2f91c..17c886fba8f 100755 --- a/.github/workflows/scripts/run_rpc_tests_remote_ethereum.sh +++ b/.github/workflows/scripts/run_rpc_tests_remote_ethereum.sh @@ -28,4 +28,4 @@ DISABLED_TEST_LIST=( DISABLED_TESTS=$(IFS=,; echo "${DISABLED_TEST_LIST[*]}") # Call the main test runner script with the required and optional parameters -"$(dirname "$0")/run_rpc_tests.sh" mainnet v1.119.0 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR" +"$(dirname "$0")/run_rpc_tests.sh" mainnet v1.121.0 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR"