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 <lightclient@protonmail.com>
This commit is contained in:
Sina M 2025-02-21 09:52:55 +01:00 committed by GitHub
parent 5552ada486
commit 8a14362bf7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 116 additions and 8 deletions

View File

@ -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 // Note, this function doesn't make any changes in the state/blockchain and is
// useful to execute and retrieve values. // 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 { if len(opts.BlockStateCalls) == 0 {
return nil, &invalidParamsError{message: "empty input"} return nil, &invalidParamsError{message: "empty input"}
} else if len(opts.BlockStateCalls) > maxSimulateBlocks { } else if len(opts.BlockStateCalls) > maxSimulateBlocks {

View File

@ -24,6 +24,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"math"
"math/big" "math/big"
"os" "os"
"path/filepath" "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) { func TestSignTransaction(t *testing.T) {
t.Parallel() t.Parallel()
// Initialize test accounts // Initialize test accounts

View File

@ -73,6 +73,20 @@ func (r *simCallResult) MarshalJSON() ([]byte, error) {
return json.Marshal((*callResultAlias)(r)) 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. // simOpts are the inputs to eth_simulateV1.
type simOpts struct { type simOpts struct {
BlockStateCalls []simBlock BlockStateCalls []simBlock
@ -95,7 +109,7 @@ type simulator struct {
} }
// execute runs the simulation of a series of blocks. // 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 { if err := ctx.Err(); err != nil {
return nil, err return nil, err
} }
@ -123,7 +137,7 @@ func (sim *simulator) execute(ctx context.Context, blocks []simBlock) ([]map[str
return nil, err return nil, err
} }
var ( var (
results = make([]map[string]interface{}, len(blocks)) results = make([]*simBlockResult, len(blocks))
parent = sim.base parent = sim.base
) )
for bi, block := range blocks { for bi, block := range blocks {
@ -131,11 +145,9 @@ func (sim *simulator) execute(ctx context.Context, blocks []simBlock) ([]map[str
if err != nil { if err != nil {
return nil, err return nil, err
} }
enc := RPCMarshalBlock(result, true, sim.fullTx, sim.chainConfig) headers[bi] = result.Header()
enc["calls"] = callResults results[bi] = &simBlockResult{fullTx: sim.fullTx, chainConfig: sim.chainConfig, Block: result, Calls: callResults}
results[bi] = enc parent = result.Header()
parent = headers[bi]
} }
return results, nil return results, nil
} }