Skip to content

Commit

Permalink
internal/ethapi: fix prev hashes in eth_simulate (#31122)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
s1na and lightclient authored Feb 21, 2025
1 parent 5552ada commit 8a14362
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 8 deletions.
2 changes: 1 addition & 1 deletion internal/ethapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
96 changes: 96 additions & 0 deletions internal/ethapi/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"encoding/json"
"errors"
"fmt"
"math"
"math/big"
"os"
"path/filepath"
Expand Down Expand Up @@ -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
Expand Down
26 changes: 19 additions & 7 deletions internal/ethapi/simulate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -123,19 +137,17 @@ 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 {
result, callResults, err := sim.processBlock(ctx, &block, headers[bi], parent, headers[:bi], timeout)
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
}
Expand Down

0 comments on commit 8a14362

Please sign in to comment.