From 8a14362bf7489fbfa22b6b261e336156f625bdbb Mon Sep 17 00:00:00 2001 From: Sina M <1591639+s1na@users.noreply.github.com> Date: Fri, 21 Feb 2025 09:52:55 +0100 Subject: [PATCH] internal/ethapi: fix prev hashes in eth_simulate (#31122) Shout-out to @Gabriel-Trintinalia for discovering this issue. The gist of it as follows: When processing a block, we should provide the parent block as well as the last 256 block hashes. Some of these parents data (specifically the hash) was incorrect because even though during the processing of the parent block we have updated the header, that header was not updating the TransactionsRoot and ReceiptsRoot fields (types.NewBlock makes a new copy of the header and changes it only on that instance). --------- Co-authored-by: lightclient --- internal/ethapi/api.go | 2 +- internal/ethapi/api_test.go | 96 +++++++++++++++++++++++++++++++++++++ internal/ethapi/simulate.go | 26 +++++++--- 3 files changed, 116 insertions(+), 8 deletions(-) diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 92249f7829..f3975d35a0 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -775,7 +775,7 @@ func (api *BlockChainAPI) Call(ctx context.Context, args TransactionArgs, blockN // // Note, this function doesn't make any changes in the state/blockchain and is // useful to execute and retrieve values. -func (api *BlockChainAPI) SimulateV1(ctx context.Context, opts simOpts, blockNrOrHash *rpc.BlockNumberOrHash) ([]map[string]interface{}, error) { +func (api *BlockChainAPI) SimulateV1(ctx context.Context, opts simOpts, blockNrOrHash *rpc.BlockNumberOrHash) ([]*simBlockResult, error) { if len(opts.BlockStateCalls) == 0 { return nil, &invalidParamsError{message: "empty input"} } else if len(opts.BlockStateCalls) > maxSimulateBlocks { diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index f00022e3de..d70cb90ec9 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -24,6 +24,7 @@ import ( "encoding/json" "errors" "fmt" + "math" "math/big" "os" "path/filepath" @@ -2309,6 +2310,101 @@ func TestSimulateV1(t *testing.T) { } } +func TestSimulateV1ChainLinkage(t *testing.T) { + var ( + acc = newTestAccount() + sender = acc.addr + contractAddr = common.Address{0xaa, 0xaa} + recipient = common.Address{0xbb, 0xbb} + gspec = &core.Genesis{ + Config: params.MergedTestChainConfig, + Alloc: types.GenesisAlloc{ + sender: {Balance: big.NewInt(params.Ether)}, + contractAddr: {Code: common.Hex2Bytes("5f35405f8114600f575f5260205ff35b5f80fd")}, + }, + } + signer = types.LatestSigner(params.MergedTestChainConfig) + ) + backend := newTestBackend(t, 1, gspec, beacon.New(ethash.NewFaker()), func(i int, b *core.BlockGen) { + tx := types.MustSignNewTx(acc.key, signer, &types.LegacyTx{ + Nonce: uint64(i), + GasPrice: b.BaseFee(), + Gas: params.TxGas, + To: &recipient, + Value: big.NewInt(500), + }) + b.AddTx(tx) + }) + + 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(), + gp: new(core.GasPool).AddGas(math.MaxUint64), + traceTransfers: false, + validate: false, + fullTx: false, + } + + var ( + call1 = TransactionArgs{ + From: &sender, + To: &recipient, + Value: (*hexutil.Big)(big.NewInt(1000)), + } + call2 = TransactionArgs{ + From: &sender, + To: &recipient, + Value: (*hexutil.Big)(big.NewInt(2000)), + } + call3a = TransactionArgs{ + From: &sender, + To: &contractAddr, + Input: uint256ToBytes(uint256.NewInt(baseHeader.Number.Uint64() + 1)), + Gas: newUint64(1000000), + } + call3b = TransactionArgs{ + From: &sender, + To: &contractAddr, + Input: uint256ToBytes(uint256.NewInt(baseHeader.Number.Uint64() + 2)), + Gas: newUint64(1000000), + } + blocks = []simBlock{ + {Calls: []TransactionArgs{call1}}, + {Calls: []TransactionArgs{call2}}, + {Calls: []TransactionArgs{call3a, call3b}}, + } + ) + + results, err := sim.execute(ctx, blocks) + if err != nil { + t.Fatalf("simulation execution failed: %v", err) + } + require.Equal(t, 3, len(results), "expected 3 simulated blocks") + + // Check linkages of simulated blocks: + // Verify that block2's parent hash equals block1's hash. + block1 := results[0].Block + block2 := results[1].Block + block3 := results[2].Block + require.Equal(t, block1.ParentHash(), baseHeader.Hash(), "parent hash of block1 should equal hash of base block") + require.Equal(t, block1.Hash(), block2.Header().ParentHash, "parent hash of block2 should equal hash of block1") + require.Equal(t, block2.Hash(), block3.Header().ParentHash, "parent hash of block3 should equal hash of block2") + + // In block3, two calls were executed to our contract. + // The first call in block3 should return the blockhash for block1 (i.e. block1.Hash()), + // whereas the second call should return the blockhash for block2 (i.e. block2.Hash()). + require.Equal(t, block1.Hash().Bytes(), []byte(results[2].Calls[0].ReturnValue), "returned blockhash for block1 does not match") + require.Equal(t, block2.Hash().Bytes(), []byte(results[2].Calls[1].ReturnValue), "returned blockhash for block2 does not match") +} + func TestSignTransaction(t *testing.T) { t.Parallel() // Initialize test accounts diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index 227fd0e8e5..ac76372424 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -73,6 +73,20 @@ func (r *simCallResult) MarshalJSON() ([]byte, error) { return json.Marshal((*callResultAlias)(r)) } +// simBlockResult is the result of a simulated block. +type simBlockResult struct { + fullTx bool + chainConfig *params.ChainConfig + Block *types.Block + Calls []simCallResult +} + +func (r *simBlockResult) MarshalJSON() ([]byte, error) { + blockData := RPCMarshalBlock(r.Block, true, r.fullTx, r.chainConfig) + blockData["calls"] = r.Calls + return json.Marshal(blockData) +} + // simOpts are the inputs to eth_simulateV1. type simOpts struct { BlockStateCalls []simBlock @@ -95,7 +109,7 @@ type simulator struct { } // execute runs the simulation of a series of blocks. -func (sim *simulator) execute(ctx context.Context, blocks []simBlock) ([]map[string]interface{}, error) { +func (sim *simulator) execute(ctx context.Context, blocks []simBlock) ([]*simBlockResult, error) { if err := ctx.Err(); err != nil { return nil, err } @@ -123,7 +137,7 @@ func (sim *simulator) execute(ctx context.Context, blocks []simBlock) ([]map[str return nil, err } var ( - results = make([]map[string]interface{}, len(blocks)) + results = make([]*simBlockResult, len(blocks)) parent = sim.base ) for bi, block := range blocks { @@ -131,11 +145,9 @@ func (sim *simulator) execute(ctx context.Context, blocks []simBlock) ([]map[str if err != nil { return nil, err } - enc := RPCMarshalBlock(result, true, sim.fullTx, sim.chainConfig) - enc["calls"] = callResults - results[bi] = enc - - parent = headers[bi] + headers[bi] = result.Header() + results[bi] = &simBlockResult{fullTx: sim.fullTx, chainConfig: sim.chainConfig, Block: result, Calls: callResults} + parent = result.Header() } return results, nil }