From b7a1e7e7d5adf5373c72541e89a90819b5bdf6a6 Mon Sep 17 00:00:00 2001 From: Diego Essaya Date: Wed, 23 Oct 2024 13:31:26 -0300 Subject: [PATCH] feat(evm): add prestate tracer (fixes #3512) --- packages/evm/jsonrpc/evmchain.go | 77 ++--- packages/evm/jsonrpc/jsonrpctest/env.go | 49 ++- .../evm/jsonrpc/jsonrpctest/jsonrpc_test.go | 272 ++++++++++------- packages/evm/jsonrpc/tracer.go | 10 +- packages/evm/jsonrpc/tracer_call.go | 47 ++- packages/evm/jsonrpc/tracer_internal.go | 69 +++++ packages/evm/jsonrpc/tracer_prestate.go | 278 ++++++++++++++++++ packages/evm/jsonrpc/types.go | 14 - packages/vm/core/evm/emulator/emulator.go | 23 +- 9 files changed, 660 insertions(+), 179 deletions(-) create mode 100644 packages/evm/jsonrpc/tracer_internal.go create mode 100644 packages/evm/jsonrpc/tracer_prestate.go diff --git a/packages/evm/jsonrpc/evmchain.go b/packages/evm/jsonrpc/evmchain.go index 55790c49ce..8e97a30b33 100644 --- a/packages/evm/jsonrpc/evmchain.go +++ b/packages/evm/jsonrpc/evmchain.go @@ -677,8 +677,16 @@ func (e *EVMChain) isFakeTransaction(tx *types.Transaction) bool { return false } -// Trace allows the tracing of EVM transactions and considers "fake" evm transactions that are emitted when ISC internal requests are being made. (Transfer of funds from L1->L2EVM for example) -func (e *EVMChain) trace(config *tracers.TraceConfig, blockInfo *blocklog.BlockInfo, requestsInBlock []isc.Request, evmTxs types.Transactions, txIndex uint64, txHash common.Hash, blockHash common.Hash) (json.RawMessage, error) { +// traceTransaction allows the tracing of a single EVM transaction. +// "Fake" transactions that are emitted e.g. for L1 deposits return some mocked trace. +func (e *EVMChain) traceTransaction( + config *tracers.TraceConfig, + blockInfo *blocklog.BlockInfo, + requestsInBlock []isc.Request, + tx *types.Transaction, + txIndex uint64, + blockHash common.Hash, +) (json.RawMessage, error) { tracerType := "callTracer" if config.Tracer != nil { tracerType = *config.Tracer @@ -690,55 +698,29 @@ func (e *EVMChain) trace(config *tracers.TraceConfig, blockInfo *blocklog.BlockI BlockHash: blockHash, BlockNumber: new(big.Int).SetUint64(blockNumber), TxIndex: int(txIndex), - TxHash: txHash, + TxHash: tx.Hash(), }, config.TracerConfig) if err != nil { return nil, err } + if e.isFakeTransaction(tx) { + return tracer.TraceFakeTx(tx) + } + err = e.backend.EVMTrace( blockInfo.PreviousAliasOutput, blockInfo.Timestamp, requestsInBlock, &txIndex, &blockNumber, - tracer, + tracer.Tracer, ) if err != nil { return nil, err } - result, err := tracer.GetResult() - if err != nil { - if !errors.Is(err, ErrIncorrectTopLevelCalls) { - return nil, err - } - - tx, ok := lo.Find(evmTxs, func(tx *types.Transaction) bool { return slices.Equal(txHash.Bytes(), tx.Hash().Bytes()) }) - if !ok { - return nil, fmt.Errorf("can not find transaction: %v", txHash.String()) - } - - if e.isFakeTransaction(tx) { - return json.Marshal(RPCMarshalTransactionTraceForFakeTX(tx, tx.GasPrice())) - } - } - - return result, nil -} - -func (e *EVMChain) traceTransaction(config *tracers.TraceConfig, txIndex uint64, txHash common.Hash, blockNumber uint64, blockHash common.Hash) (any, error) { - iscBlock, iscRequestsInBlock, err := e.iscRequestsInBlock(blockNumber) - if err != nil { - return nil, err - } - - blockTxs, err := e.txsByBlockNumber(new(big.Int).SetUint64(blockNumber)) - if err != nil { - return nil, err - } - - return e.trace(config, iscBlock, iscRequestsInBlock, blockTxs, txIndex, txHash, blockHash) + return tracer.GetResult() } func (e *EVMChain) traceBlock(config *tracers.TraceConfig, block *types.Block) (any, error) { @@ -754,7 +736,14 @@ func (e *EVMChain) traceBlock(config *tracers.TraceConfig, block *types.Block) ( results := make([]TxTraceResult, 0) for i, tx := range blockTxs { - result, err := e.trace(config, iscBlock, iscRequestsInBlock, blockTxs, uint64(i), tx.Hash(), block.Hash()) + result, err := e.traceTransaction( + config, + iscBlock, + iscRequestsInBlock, + tx, + uint64(i), + block.Hash(), + ) // Transactions which failed tracing will be omitted, so the rest of the block can be returned if err == nil { @@ -771,7 +760,7 @@ func (e *EVMChain) traceBlock(config *tracers.TraceConfig, block *types.Block) ( func (e *EVMChain) TraceTransaction(txHash common.Hash, config *tracers.TraceConfig) (any, error) { e.log.Debugf("TraceTransaction(txHash=%v, config=?)", txHash) - _, blockHash, blockNumber, txIndex, err := e.TransactionByHash(txHash) + tx, blockHash, blockNumber, txIndex, err := e.TransactionByHash(txHash) if err != nil { return nil, err } @@ -779,7 +768,19 @@ func (e *EVMChain) TraceTransaction(txHash common.Hash, config *tracers.TraceCon return nil, errors.New("transaction not found") } - return e.traceTransaction(config, txIndex, txHash, blockNumber, blockHash) + iscBlock, iscRequestsInBlock, err := e.iscRequestsInBlock(blockNumber) + if err != nil { + return nil, err + } + + return e.traceTransaction( + config, + iscBlock, + iscRequestsInBlock, + tx, + txIndex, + blockHash, + ) } func (e *EVMChain) TraceBlockByHash(blockHash common.Hash, config *tracers.TraceConfig) (any, error) { diff --git a/packages/evm/jsonrpc/jsonrpctest/env.go b/packages/evm/jsonrpc/jsonrpctest/env.go index 0d24c26124..2a5cb4b641 100644 --- a/packages/evm/jsonrpc/jsonrpctest/env.go +++ b/packages/evm/jsonrpc/jsonrpctest/env.go @@ -293,12 +293,13 @@ func (e *Env) getLogs(q ethereum.FilterQuery) []types.Log { func (e *Env) traceTransactionWithCallTracer(txHash common.Hash) (jsonrpc.CallFrame, error) { var res json.RawMessage // we have to use the raw client, because the normal client does not support debug methods + tracer := "callTracer" err := e.RawClient.CallContext( context.Background(), &res, "debug_traceTransaction", txHash, - tracers.TraceConfig{TracerConfig: []byte(`{"tracer": "callTracer"}`)}, + tracers.TraceConfig{Tracer: &tracer}, ) if err != nil { return jsonrpc.CallFrame{}, err @@ -309,6 +310,52 @@ func (e *Env) traceTransactionWithCallTracer(txHash common.Hash) (jsonrpc.CallFr return trace, nil } +func (e *Env) traceTransactionWithPrestate(txHash common.Hash) (jsonrpc.PrestateAccountMap, error) { + var res json.RawMessage + // we have to use the raw client, because the normal client does not support debug methods + tracer := "prestateTracer" + err := e.RawClient.CallContext( + context.Background(), + &res, + "debug_traceTransaction", + txHash, + tracers.TraceConfig{ + Tracer: &tracer, + TracerConfig: []byte(`{"diffMode": false}`), + }, + ) + if err != nil { + return nil, err + } + var ret jsonrpc.PrestateAccountMap + err = json.Unmarshal(res, &ret) + require.NoError(e.T, err) + return ret, nil +} + +func (e *Env) traceTransactionWithPrestateDiff(txHash common.Hash) (jsonrpc.PrestateDiffResult, error) { + var res json.RawMessage + // we have to use the raw client, because the normal client does not support debug methods + tracer := "prestateTracer" + err := e.RawClient.CallContext( + context.Background(), + &res, + "debug_traceTransaction", + txHash, + tracers.TraceConfig{ + Tracer: &tracer, + TracerConfig: []byte(`{"diffMode": true}`), + }, + ) + if err != nil { + return jsonrpc.PrestateDiffResult{}, err + } + var ret jsonrpc.PrestateDiffResult + err = json.Unmarshal(res, &ret) + require.NoError(e.T, err) + return ret, nil +} + func (e *Env) TestRPCGetLogs() { creator, creatorAddress := e.NewAccountWithL2Funds() contractABI, err := abi.JSON(strings.NewReader(evmtest.ERC20ContractABI)) diff --git a/packages/evm/jsonrpc/jsonrpctest/jsonrpc_test.go b/packages/evm/jsonrpc/jsonrpctest/jsonrpc_test.go index c8b61d6324..57062e4aef 100644 --- a/packages/evm/jsonrpc/jsonrpctest/jsonrpc_test.go +++ b/packages/evm/jsonrpc/jsonrpctest/jsonrpc_test.go @@ -6,7 +6,6 @@ package jsonrpctest import ( "context" "encoding/json" - "fmt" "math/big" "slices" "strings" @@ -557,33 +556,48 @@ func TestRPCTraceTx(t *testing.T) { bi := env.soloChain.GetLatestBlockInfo() require.EqualValues(t, 2, bi.NumSuccessfulRequests) - // assert each tx can be individually traced - trace1, err := env.traceTransactionWithCallTracer(tx1.Hash()) - require.NoError(t, err) - _, err = env.traceTransactionWithCallTracer(tx2.Hash()) - require.NoError(t, err) + t.Run("callTracer", func(t *testing.T) { + // assert each tx can be individually traced + trace1, err := env.traceTransactionWithCallTracer(tx1.Hash()) + require.NoError(t, err) + _, err = env.traceTransactionWithCallTracer(tx2.Hash()) + require.NoError(t, err) - require.Equal(t, creatorAddress, trace1.From) - require.Equal(t, contractAddress, *trace1.To) - require.Equal(t, "0x7b", trace1.Value.String()) - expectedInput, err := contractABI.Pack("sendTo", common.Address{0x1}, big.NewInt(1)) - require.NoError(t, err) - require.Equal(t, expectedInput, []byte(trace1.Input)) - require.Empty(t, trace1.Error) - require.Empty(t, trace1.RevertReason) - require.Equal(t, "0x0", trace1.Gas.String()) - require.Equal(t, "0x0", trace1.GasUsed.String()) + require.Equal(t, creatorAddress, trace1.From) + require.Equal(t, contractAddress, *trace1.To) + require.Equal(t, "0x7b", trace1.Value.String()) + expectedInput, err := contractABI.Pack("sendTo", common.Address{0x1}, big.NewInt(1)) + require.NoError(t, err) + require.Equal(t, expectedInput, []byte(trace1.Input)) + require.Empty(t, trace1.Error) + require.Empty(t, trace1.RevertReason) + require.Contains(t, trace1.Gas.String(), "0x") + require.Contains(t, trace1.GasUsed.String(), "0x") + + require.Len(t, trace1.Calls, 1) + trace2 := trace1.Calls[0] + require.Equal(t, contractAddress, trace2.From) + require.Equal(t, common.Address{0x1}, *trace2.To) + require.Equal(t, "0x1", trace2.Value.String()) + require.Empty(t, trace2.Input) + require.Empty(t, trace2.Error) + require.Empty(t, trace2.RevertReason) + require.Contains(t, trace2.Gas.String(), "0x") + require.Contains(t, trace2.GasUsed.String(), "0x") + }) - require.Len(t, trace1.Calls, 1) - trace2 := trace1.Calls[0] - require.Equal(t, contractAddress, trace2.From) - require.Equal(t, common.Address{0x1}, *trace2.To) - require.Equal(t, "0x1", trace2.Value.String()) - require.Empty(t, trace2.Input) - require.Empty(t, trace2.Error) - require.Empty(t, trace2.RevertReason) - require.Contains(t, trace2.Gas.String(), "0x") - require.Contains(t, trace2.GasUsed.String(), "0x") + t.Run("prestate", func(t *testing.T) { + accountMap, err := env.traceTransactionWithPrestate(tx1.Hash()) + require.NoError(t, err) + require.NotEmpty(t, accountMap) + // t.Logf("%s", lo.Must(json.MarshalIndent(accountMap, "", " "))) + + diff, err := env.traceTransactionWithPrestateDiff(tx1.Hash()) + require.NoError(t, err) + require.NotEmpty(t, diff.Pre) + require.NotEmpty(t, diff.Post) + // t.Logf("%s", lo.Must(json.MarshalIndent(diff, "", " "))) + }) } // Transfer calls produce "fake" Transactions to simulate EVM behavior. @@ -612,10 +626,17 @@ func TestRPCTraceEVMDeposit(t *testing.T) { trace, err := env.traceTransactionWithCallTracer(tx.Hash()) require.NoError(t, err) - fmt.Print(hexutil.EncodeUint64(isc.NewAssetsBaseTokens(1000).BaseTokens)) - require.Equal(t, evmAddr.String(), trace.To.String()) require.Equal(t, hexutil.EncodeUint64(isc.NewAssetsBaseTokens(1000).BaseTokens*1e12), trace.Value.String()) + + prestate, err := env.traceTransactionWithPrestate(tx.Hash()) + require.NoError(t, err) + require.Empty(t, prestate) + + prestateDiff, err := env.traceTransactionWithPrestateDiff(tx.Hash()) + require.NoError(t, err) + require.Empty(t, prestateDiff.Pre) + require.Empty(t, prestateDiff.Post) } func TestRPCTraceBlock(t *testing.T) { @@ -656,91 +677,120 @@ func TestRPCTraceBlock(t *testing.T) { bi := env.soloChain.GetLatestBlockInfo() require.EqualValues(t, 2, bi.NumSuccessfulRequests) - var res1 json.RawMessage - // we have to use the raw client, because the normal client does not support debug methods - err = env.RawClient.CallContext( - context.Background(), - &res1, - "debug_traceBlockByNumber", - hexutil.Uint64(env.BlockNumber()).String(), - tracers.TraceConfig{TracerConfig: []byte(`{"tracer": "callTracer"}`)}, - ) - require.NoError(t, err) - - var res2 json.RawMessage - // we have to use the raw client, because the normal client does not support debug methods - err = env.RawClient.CallContext( - context.Background(), - &res2, - "debug_traceBlockByHash", - env.BlockByNumber(big.NewInt(int64(env.BlockNumber()))).Hash(), - tracers.TraceConfig{TracerConfig: []byte(`{"tracer": "callTracer"}`)}, - ) - require.NoError(t, err) - - require.Equal(t, res1, res2, "debug_traceBlockByNumber and debug_traceBlockByNumber should produce equal results") + t.Run("callTracer", func(t *testing.T) { + callTracer := "callTracer" + var res1 json.RawMessage + // we have to use the raw client, because the normal client does not support debug methods + err = env.RawClient.CallContext( + context.Background(), + &res1, + "debug_traceBlockByNumber", + hexutil.Uint64(env.BlockNumber()).String(), + tracers.TraceConfig{Tracer: &callTracer}, + ) + require.NoError(t, err) - traceBlock := make([]jsonrpc.TxTraceResult, 0) - err = json.Unmarshal(res1, &traceBlock) - require.NoError(t, err) + var res2 json.RawMessage + // we have to use the raw client, because the normal client does not support debug methods + err = env.RawClient.CallContext( + context.Background(), + &res2, + "debug_traceBlockByHash", + env.BlockByNumber(big.NewInt(int64(env.BlockNumber()))).Hash(), + tracers.TraceConfig{Tracer: &callTracer}, + ) + require.NoError(t, err) - require.Len(t, traceBlock, 2) + require.Equal(t, res1, res2, "debug_traceBlockByNumber and debug_traceBlockByNumber should produce equal results") - var trace1 jsonrpc.CallFrame - err = json.Unmarshal(traceBlock[slices.IndexFunc(traceBlock, func(v jsonrpc.TxTraceResult) bool { - return v.TxHash == tx1.Hash() - })].Result, &trace1) - require.NoError(t, err) + traceBlock := make([]jsonrpc.TxTraceResult, 0) + err = json.Unmarshal(res1, &traceBlock) + require.NoError(t, err) - var trace2 jsonrpc.CallFrame - err = json.Unmarshal(traceBlock[slices.IndexFunc(traceBlock, func(v jsonrpc.TxTraceResult) bool { - return v.TxHash == tx2.Hash() - })].Result, &trace2) - require.NoError(t, err) + require.Len(t, traceBlock, 2) - require.Equal(t, creatorAddress, trace1.From) - require.Equal(t, contractAddress, *trace1.To) - require.Equal(t, "0x7b", trace1.Value.String()) - expectedInput, err := contractABI.Pack("sendTo", common.Address{0x1}, big.NewInt(2)) - require.NoError(t, err) - require.Equal(t, expectedInput, []byte(trace1.Input)) - require.Empty(t, trace1.Error) - require.Empty(t, trace1.RevertReason) - require.Equal(t, "0x0", trace1.Gas.String()) - require.Equal(t, "0x0", trace1.GasUsed.String()) + var trace1 jsonrpc.CallFrame + err = json.Unmarshal(traceBlock[slices.IndexFunc(traceBlock, func(v jsonrpc.TxTraceResult) bool { + return v.TxHash == tx1.Hash() + })].Result, &trace1) + require.NoError(t, err) - require.Len(t, trace1.Calls, 1) - innerCall1 := trace1.Calls[0] - require.Equal(t, contractAddress, innerCall1.From) - require.Equal(t, common.Address{0x1}, *innerCall1.To) - require.Equal(t, "0x2", innerCall1.Value.String()) - require.Empty(t, innerCall1.Input) - require.Empty(t, innerCall1.Error) - require.Empty(t, innerCall1.RevertReason) - require.Contains(t, innerCall1.Gas.String(), "0x") - require.Contains(t, innerCall1.GasUsed.String(), "0x") + var trace2 jsonrpc.CallFrame + err = json.Unmarshal(traceBlock[slices.IndexFunc(traceBlock, func(v jsonrpc.TxTraceResult) bool { + return v.TxHash == tx2.Hash() + })].Result, &trace2) + require.NoError(t, err) - require.Equal(t, creatorAddress2, trace2.From) - require.Equal(t, contractAddress, *trace2.To) - require.Equal(t, "0x141", trace2.Value.String()) - expectedInput, err = contractABI.Pack("sendTo", common.Address{0x2}, big.NewInt(3)) - require.NoError(t, err) - require.Equal(t, expectedInput, []byte(trace2.Input)) - require.Empty(t, trace2.Error) - require.Empty(t, trace2.RevertReason) - require.Equal(t, "0x0", trace2.Gas.String()) - require.Equal(t, "0x0", trace2.GasUsed.String()) - - require.Len(t, trace2.Calls, 1) - innerCall2 := trace2.Calls[0] - require.Equal(t, contractAddress, innerCall2.From) - require.Equal(t, common.Address{0x2}, *innerCall2.To) - require.Equal(t, "0x3", innerCall2.Value.String()) - require.Empty(t, innerCall2.Input) - require.Empty(t, innerCall2.Error) - require.Empty(t, innerCall2.RevertReason) - require.Contains(t, innerCall2.Gas.String(), "0x") - require.Contains(t, innerCall2.GasUsed.String(), "0x") + require.Equal(t, creatorAddress, trace1.From) + require.Equal(t, contractAddress, *trace1.To) + require.Equal(t, "0x7b", trace1.Value.String()) + expectedInput, err := contractABI.Pack("sendTo", common.Address{0x1}, big.NewInt(2)) //nolint:govet + require.NoError(t, err) + require.Equal(t, expectedInput, []byte(trace1.Input)) + require.Empty(t, trace1.Error) + require.Empty(t, trace1.RevertReason) + require.Contains(t, trace1.Gas.String(), "0x") + require.Contains(t, trace1.GasUsed.String(), "0x") + + require.Len(t, trace1.Calls, 1) + innerCall1 := trace1.Calls[0] + require.Equal(t, contractAddress, innerCall1.From) + require.Equal(t, common.Address{0x1}, *innerCall1.To) + require.Equal(t, "0x2", innerCall1.Value.String()) + require.Empty(t, innerCall1.Input) + require.Empty(t, innerCall1.Error) + require.Empty(t, innerCall1.RevertReason) + require.Contains(t, innerCall1.Gas.String(), "0x") + require.Contains(t, innerCall1.GasUsed.String(), "0x") + + require.Equal(t, creatorAddress2, trace2.From) + require.Equal(t, contractAddress, *trace2.To) + require.Equal(t, "0x141", trace2.Value.String()) + expectedInput, err = contractABI.Pack("sendTo", common.Address{0x2}, big.NewInt(3)) + require.NoError(t, err) + require.Equal(t, expectedInput, []byte(trace2.Input)) + require.Empty(t, trace2.Error) + require.Empty(t, trace2.RevertReason) + require.Contains(t, trace2.Gas.String(), "0x") + require.Contains(t, trace2.GasUsed.String(), "0x") + + require.Len(t, trace2.Calls, 1) + innerCall2 := trace2.Calls[0] + require.Equal(t, contractAddress, innerCall2.From) + require.Equal(t, common.Address{0x2}, *innerCall2.To) + require.Equal(t, "0x3", innerCall2.Value.String()) + require.Empty(t, innerCall2.Input) + require.Empty(t, innerCall2.Error) + require.Empty(t, innerCall2.RevertReason) + require.Contains(t, innerCall2.Gas.String(), "0x") + require.Contains(t, innerCall2.GasUsed.String(), "0x") + }) + t.Run("prestate", func(t *testing.T) { + prestateTracer := "prestateTracer" + var res2 json.RawMessage + // we have to use the raw client, because the normal client does not support debug methods + err = env.RawClient.CallContext( + context.Background(), + &res2, + "debug_traceBlockByHash", + env.BlockByNumber(big.NewInt(int64(env.BlockNumber()))).Hash(), + tracers.TraceConfig{ + Tracer: &prestateTracer, + TracerConfig: []byte(`{"diffMode": false}`), + }, + ) + require.NoError(t, err) + var results []jsonrpc.TxTraceResult + err = json.Unmarshal(res2, &results) + require.NoError(t, err) + require.Len(t, results, 2) + for _, r := range results { + var p jsonrpc.PrestateAccountMap + err = json.Unmarshal(r.Result, &p) + require.NoError(t, err) + require.NotEmpty(t, p) + } + }) } func TestRPCTraceBlockSingleCall(t *testing.T) { @@ -769,6 +819,7 @@ func TestRPCTraceBlockSingleCall(t *testing.T) { bi := env.soloChain.GetLatestBlockInfo() require.EqualValues(t, 1, bi.NumSuccessfulRequests) + callTracer := "callTracer" var res1 json.RawMessage // we have to use the raw client, because the normal client does not support debug methods err = env.RawClient.CallContext( @@ -776,7 +827,7 @@ func TestRPCTraceBlockSingleCall(t *testing.T) { &res1, "debug_traceBlockByNumber", hexutil.Uint64(env.BlockNumber()).String(), - tracers.TraceConfig{TracerConfig: []byte(`{"tracer": "callTracer"}`)}, + tracers.TraceConfig{Tracer: &callTracer}, ) require.NoError(t, err) @@ -787,7 +838,7 @@ func TestRPCTraceBlockSingleCall(t *testing.T) { &res2, "debug_traceBlockByHash", env.BlockByNumber(big.NewInt(int64(env.BlockNumber()))).Hash(), - tracers.TraceConfig{TracerConfig: []byte(`{"tracer": "callTracer"}`)}, + tracers.TraceConfig{Tracer: &callTracer}, ) require.NoError(t, err) @@ -813,8 +864,8 @@ func TestRPCTraceBlockSingleCall(t *testing.T) { require.Equal(t, expectedInput, []byte(trace1.Input)) require.Empty(t, trace1.Error) require.Empty(t, trace1.RevertReason) - require.Equal(t, "0x0", trace1.Gas.String()) - require.Equal(t, "0x0", trace1.GasUsed.String()) + require.Contains(t, trace1.Gas.String(), "0x") + require.Contains(t, trace1.GasUsed.String(), "0x") require.Len(t, trace1.Calls, 1) innerCall1 := trace1.Calls[0] @@ -869,6 +920,7 @@ func TestRPCBlockReceipt(t *testing.T) { context.Background(), rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(env.BlockNumber())), ) + require.NoError(t, err) require.Len(t, receipts, 2) r1 := receipts[slices.IndexFunc(receipts, func(v *types.Receipt) bool { diff --git a/packages/evm/jsonrpc/tracer.go b/packages/evm/jsonrpc/tracer.go index 8f0a419c15..38a478f9c3 100644 --- a/packages/evm/jsonrpc/tracer.go +++ b/packages/evm/jsonrpc/tracer.go @@ -4,10 +4,16 @@ import ( "encoding/json" "fmt" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth/tracers" ) -type tracerFactory func(*tracers.Context, json.RawMessage) (*tracers.Tracer, error) +type Tracer struct { + *tracers.Tracer + TraceFakeTx func(tx *types.Transaction) (json.RawMessage, error) +} + +type tracerFactory func(*tracers.Context, json.RawMessage) (*Tracer, error) var allTracers = map[string]tracerFactory{} @@ -15,7 +21,7 @@ func registerTracer(tracerType string, fn tracerFactory) { allTracers[tracerType] = fn } -func newTracer(tracerType string, ctx *tracers.Context, cfg json.RawMessage) (*tracers.Tracer, error) { +func newTracer(tracerType string, ctx *tracers.Context, cfg json.RawMessage) (*Tracer, error) { fn := allTracers[tracerType] if fn == nil { return nil, fmt.Errorf("unsupported tracer type: %s", tracerType) diff --git a/packages/evm/jsonrpc/tracer_call.go b/packages/evm/jsonrpc/tracer_call.go index 5e45c86b63..54b7e53b7a 100644 --- a/packages/evm/jsonrpc/tracer_call.go +++ b/packages/evm/jsonrpc/tracer_call.go @@ -1,3 +1,6 @@ +// Code on this file adapted from +// https://github.com/ethereum/go-ethereum/blob/master/eth/tracers/native/call.go + package jsonrpc import ( @@ -14,6 +17,8 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/eth/tracers" + + "github.com/iotaledger/wasp/packages/evm/evmutil" ) func init() { @@ -23,7 +28,7 @@ func init() { // CallFrame contains the result of a trace with "callTracer". // Code is 100% copied from go-ethereum (since the type is unexported there) -type callLog struct { +type CallLog struct { Address common.Address `json:"address"` Topics []common.Hash `json:"topics"` Data hexutil.Bytes `json:"data"` @@ -64,7 +69,7 @@ type CallFrame struct { Error string `json:"error,omitempty" rlp:"optional"` RevertReason string `json:"revertReason,omitempty"` Calls []CallFrame `json:"calls,omitempty" rlp:"optional"` - Logs []callLog `json:"logs,omitempty" rlp:"optional"` + Logs []CallLog `json:"logs,omitempty" rlp:"optional"` // Placed at end on purpose. The RLP will be decoded to 0 instead of // nil if there are non-empty elements after in the struct. Value hexutil.Big `json:"value,omitempty" rlp:"optional"` @@ -129,21 +134,24 @@ type callTracerConfig struct { // newCallTracer returns a native go tracer which tracks // call frames of a tx, and implements vm.EVMLogger. -func newCallTracer(ctx *tracers.Context, cfg json.RawMessage) (*tracers.Tracer, error) { +func newCallTracer(ctx *tracers.Context, cfg json.RawMessage) (*Tracer, error) { t, err := newCallTracerObject(ctx, cfg) if err != nil { return nil, err } - return &tracers.Tracer{ - Hooks: &tracing.Hooks{ - OnTxStart: t.OnTxStart, - OnTxEnd: t.OnTxEnd, - OnEnter: t.OnEnter, - OnExit: t.OnExit, - OnLog: t.OnLog, + return &Tracer{ + Tracer: &tracers.Tracer{ + Hooks: &tracing.Hooks{ + OnTxStart: t.OnTxStart, + OnTxEnd: t.OnTxEnd, + OnEnter: t.OnEnter, + OnExit: t.OnExit, + OnLog: t.OnLog, + }, + GetResult: t.GetResult, + Stop: t.Stop, }, - GetResult: t.GetResult, - Stop: t.Stop, + TraceFakeTx: t.TraceFakeTx, }, nil } @@ -249,7 +257,7 @@ func (t *callTracer) OnLog(log *types.Log) { if t.interrupt.Load() { return } - l := callLog{ + l := CallLog{ Address: log.Address, Topics: log.Topics, Data: log.Data, @@ -293,3 +301,16 @@ func clearFailedLogs(cf *CallFrame, parentFailed bool) { clearFailedLogs(&cf.Calls[i], failed) } } + +func (t *callTracer) TraceFakeTx(tx *types.Transaction) (json.RawMessage, error) { + return json.Marshal(CallFrame{ + Type: NewOpCodeJSON(vm.CALL), + From: evmutil.MustGetSenderIfTxSigned(tx), + Gas: hexutil.Uint64(tx.Gas()), + GasUsed: hexutil.Uint64(tx.Gas()), + To: tx.To(), + Input: []byte{}, + Output: []byte{}, + Value: hexutil.Big(*tx.Value()), + }) +} diff --git a/packages/evm/jsonrpc/tracer_internal.go b/packages/evm/jsonrpc/tracer_internal.go new file mode 100644 index 0000000000..edc76cc8fd --- /dev/null +++ b/packages/evm/jsonrpc/tracer_internal.go @@ -0,0 +1,69 @@ +// Code on this file adapted from +// https://github.com/ethereum/go-ethereum/blob/master/eth/tracers/internal/util.go + +package jsonrpc + +import ( + "errors" + "fmt" + + "github.com/holiman/uint256" +) + +const ( + memoryPadLimit = 1024 * 1024 +) + +// getMemoryCopyPadded returns offset + size as a new slice. +// It zero-pads the slice if it extends beyond memory bounds. +func getMemoryCopyPadded(m []byte, offset, size int64) ([]byte, error) { + if offset < 0 || size < 0 { + return nil, errors.New("offset or size must not be negative") + } + length := int64(len(m)) + if offset+size < length { // slice fully inside memory + return memoryCopy(m, offset, size), nil + } + paddingNeeded := offset + size - length + if paddingNeeded > memoryPadLimit { + return nil, fmt.Errorf("reached limit for padding memory slice: %d", paddingNeeded) + } + cpy := make([]byte, size) + if overlap := length - offset; overlap > 0 { + copy(cpy, MemoryPtr(m, offset, overlap)) + } + return cpy, nil +} + +func memoryCopy(m []byte, offset, size int64) (cpy []byte) { + if size == 0 { + return nil + } + + if len(m) > int(offset) { + cpy = make([]byte, size) + copy(cpy, m[offset:offset+size]) + + return + } + + return +} + +// MemoryPtr returns a pointer to a slice of memory. +func MemoryPtr(m []byte, offset, size int64) []byte { + if size == 0 { + return nil + } + + if len(m) > int(offset) { + return m[offset : offset+size] + } + + return nil +} + +// StackBack returns the n'th item in stack +func StackBack(st []uint256.Int, n int) *uint256.Int { + return &st[len(st)-n-1] +} diff --git a/packages/evm/jsonrpc/tracer_prestate.go b/packages/evm/jsonrpc/tracer_prestate.go new file mode 100644 index 0000000000..beb443b4a7 --- /dev/null +++ b/packages/evm/jsonrpc/tracer_prestate.go @@ -0,0 +1,278 @@ +// Code on this file adapted from +// https://github.com/ethereum/go-ethereum/blob/master/eth/tracers/native/prestate.go + +package jsonrpc + +import ( + "bytes" + "encoding/json" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/eth/tracers" + "github.com/ethereum/go-ethereum/log" +) + +func init() { + registerTracer("prestateTracer", newPrestateTracer) +} + +type PrestateAccountMap = map[common.Address]*PrestateAccount + +type PrestateAccount struct { + Balance *hexutil.Big `json:"balance,omitempty"` + Code hexutil.Bytes `json:"code,omitempty"` + Nonce uint64 `json:"nonce,omitempty"` + Storage map[common.Hash]common.Hash `json:"storage,omitempty"` + empty bool +} + +type PrestateDiffResult struct { + Post PrestateAccountMap `json:"post"` + Pre PrestateAccountMap `json:"pre"` +} + +func (a *PrestateAccount) exists() bool { + return a.Nonce > 0 || len(a.Code) > 0 || len(a.Storage) > 0 || (a.Balance != nil && a.Balance.ToInt().Sign() != 0) +} + +type prestateTracer struct { + env *tracing.VMContext + pre PrestateAccountMap + post PrestateAccountMap + to common.Address + config prestateTracerConfig + interrupt atomic.Bool // Atomic flag to signal execution interruption + reason error // Textual reason for the interruption + created map[common.Address]bool + deleted map[common.Address]bool +} + +type prestateTracerConfig struct { + DiffMode bool `json:"diffMode"` // If true, this tracer will return state modifications +} + +func newPrestateTracer(ctx *tracers.Context, cfg json.RawMessage) (*Tracer, error) { + var config prestateTracerConfig + if err := json.Unmarshal(cfg, &config); err != nil { + return nil, err + } + t := &prestateTracer{ + pre: PrestateAccountMap{}, + post: PrestateAccountMap{}, + config: config, + created: make(map[common.Address]bool), + deleted: make(map[common.Address]bool), + } + return &Tracer{ + Tracer: &tracers.Tracer{ + Hooks: &tracing.Hooks{ + OnTxStart: t.OnTxStart, + OnTxEnd: t.OnTxEnd, + OnOpcode: t.OnOpcode, + }, + GetResult: t.GetResult, + Stop: t.Stop, + }, + TraceFakeTx: t.TraceFakeTx, + }, nil +} + +// OnOpcode implements the EVMLogger interface to trace a single step of VM execution. +// +//nolint:gocyclo +func (t *prestateTracer) OnOpcode(pc uint64, opcode byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { + if err != nil { + return + } + // Skip if tracing was interrupted + if t.interrupt.Load() { + return + } + op := vm.OpCode(opcode) + stackData := scope.StackData() + stackLen := len(stackData) + caller := scope.Address() + switch { + case stackLen >= 1 && (op == vm.SLOAD || op == vm.SSTORE): + slot := common.Hash(stackData[stackLen-1].Bytes32()) + t.lookupStorage(caller, slot) + case stackLen >= 1 && (op == vm.EXTCODECOPY || op == vm.EXTCODEHASH || op == vm.EXTCODESIZE || op == vm.BALANCE || op == vm.SELFDESTRUCT): + addr := common.Address(stackData[stackLen-1].Bytes20()) + t.lookupAccount(addr) + if op == vm.SELFDESTRUCT { + t.deleted[caller] = true + } + case stackLen >= 5 && (op == vm.DELEGATECALL || op == vm.CALL || op == vm.STATICCALL || op == vm.CALLCODE): + addr := common.Address(stackData[stackLen-2].Bytes20()) + t.lookupAccount(addr) + case op == vm.CREATE: + nonce := t.env.StateDB.GetNonce(caller) + addr := crypto.CreateAddress(caller, nonce) + t.lookupAccount(addr) + t.created[addr] = true + case stackLen >= 4 && op == vm.CREATE2: + offset := stackData[stackLen-2] + size := stackData[stackLen-3] + init, err := getMemoryCopyPadded(scope.MemoryData(), int64(offset.Uint64()), int64(size.Uint64())) + if err != nil { + log.Warn("failed to copy CREATE2 input", "err", err, "tracer", "prestateTracer", "offset", offset, "size", size) + return + } + inithash := crypto.Keccak256(init) + salt := stackData[stackLen-4] + addr := crypto.CreateAddress2(caller, salt.Bytes32(), inithash) + t.lookupAccount(addr) + t.created[addr] = true + } +} + +func (t *prestateTracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) { + t.env = env + if tx.To() == nil { + t.to = crypto.CreateAddress(from, env.StateDB.GetNonce(from)) + t.created[t.to] = true + } else { + t.to = *tx.To() + } + + t.lookupAccount(from) + t.lookupAccount(t.to) + t.lookupAccount(env.Coinbase) +} + +func (t *prestateTracer) OnTxEnd(receipt *types.Receipt, err error) { + if err != nil { + return + } + if t.config.DiffMode { + t.processDiffState() + } + // the new created contracts' prestate were empty, so delete them + for a := range t.created { + // the created contract maybe exists in statedb before the creating tx + if s := t.pre[a]; s != nil && s.empty { + delete(t.pre, a) + } + } +} + +// GetResult returns the json-encoded nested list of call traces, and any +// error arising from the encoding or forceful termination (via `Stop`). +func (t *prestateTracer) GetResult() (json.RawMessage, error) { + var res []byte + var err error + if t.config.DiffMode { + res, err = json.Marshal(PrestateDiffResult{t.post, t.pre}) + } else { + res, err = json.Marshal(t.pre) + } + if err != nil { + return nil, err + } + return json.RawMessage(res), t.reason +} + +// Stop terminates execution of the tracer at the first opportune moment. +func (t *prestateTracer) Stop(err error) { + t.reason = err + t.interrupt.Store(true) +} + +func (t *prestateTracer) processDiffState() { + for addr, state := range t.pre { + // The deleted account's state is pruned from `post` but kept in `pre` + if _, ok := t.deleted[addr]; ok { + continue + } + modified := false + postAccount := &PrestateAccount{Storage: make(map[common.Hash]common.Hash)} + newBalance := t.env.StateDB.GetBalance(addr).ToBig() + newNonce := t.env.StateDB.GetNonce(addr) + newCode := t.env.StateDB.GetCode(addr) + + if newBalance.Cmp(t.pre[addr].Balance.ToInt()) != 0 { + modified = true + postAccount.Balance = (*hexutil.Big)(newBalance) + } + if newNonce != t.pre[addr].Nonce { + modified = true + postAccount.Nonce = newNonce + } + if !bytes.Equal(newCode, t.pre[addr].Code) { + modified = true + postAccount.Code = newCode + } + + for key, val := range state.Storage { + // don't include the empty slot + if val == (common.Hash{}) { + delete(t.pre[addr].Storage, key) + } + + newVal := t.env.StateDB.GetState(addr, key) + if val == newVal { + // Omit unchanged slots + delete(t.pre[addr].Storage, key) + } else { + modified = true + if newVal != (common.Hash{}) { + postAccount.Storage[key] = newVal + } + } + } + + if modified { + t.post[addr] = postAccount + } else { + // if state is not modified, then no need to include into the pre state + delete(t.pre, addr) + } + } +} + +// lookupAccount fetches details of an account and adds it to the prestate +// if it doesn't exist there. +func (t *prestateTracer) lookupAccount(addr common.Address) { + if _, ok := t.pre[addr]; ok { + return + } + + acc := &PrestateAccount{ + Balance: (*hexutil.Big)(t.env.StateDB.GetBalance(addr).ToBig()), + Nonce: t.env.StateDB.GetNonce(addr), + Code: t.env.StateDB.GetCode(addr), + Storage: make(map[common.Hash]common.Hash), + } + if !acc.exists() { + acc.empty = true + } + t.pre[addr] = acc +} + +// lookupStorage fetches the requested storage slot and adds +// it to the prestate of the given contract. It assumes `lookupAccount` +// has been performed on the contract before. +func (t *prestateTracer) lookupStorage(addr common.Address, key common.Hash) { + if _, ok := t.pre[addr].Storage[key]; ok { + return + } + t.pre[addr].Storage[key] = t.env.StateDB.GetState(addr, key) +} + +func (t *prestateTracer) TraceFakeTx(tx *types.Transaction) (res json.RawMessage, err error) { + if t.config.DiffMode { + res, err = json.Marshal(PrestateDiffResult{ + Post: PrestateAccountMap{}, + Pre: PrestateAccountMap{}, + }) + } else { + res, err = json.Marshal(PrestateAccountMap{}) + } + return res, err +} diff --git a/packages/evm/jsonrpc/types.go b/packages/evm/jsonrpc/types.go index c8cba20f48..1df4d48ca8 100644 --- a/packages/evm/jsonrpc/types.go +++ b/packages/evm/jsonrpc/types.go @@ -159,20 +159,6 @@ func parseBlockNumber(bn rpc.BlockNumber) *big.Int { return big.NewInt(n) } -const FakeTxOpcode = "CALL" - -func RPCMarshalTransactionTraceForFakeTX(tx *types.Transaction, effectiveGasPrice *big.Int) map[string]interface{} { - return map[string]interface{}{ - "from": evmutil.MustGetSenderIfTxSigned(tx), - "gas": hexutil.Uint64(tx.Gas()), - "gasUsed": hexutil.Uint64(tx.Gas()), - "to": tx.To(), - "type": FakeTxOpcode, - "input": "0x", - "value": hexutil.Big(*tx.Value()), - } -} - func RPCMarshalReceipt(r *types.Receipt, tx *types.Transaction, effectiveGasPrice *big.Int) map[string]interface{} { // fix for an already fixed bug where some old failed receipts contain non-empty logs if r.Status != types.ReceiptStatusSuccessful { diff --git a/packages/vm/core/evm/emulator/emulator.go b/packages/vm/core/evm/emulator/emulator.go index d2b2c002cf..f4fe86071a 100644 --- a/packages/vm/core/evm/emulator/emulator.go +++ b/packages/vm/core/evm/emulator/emulator.go @@ -223,7 +223,13 @@ func (e *EVMEmulator) CallContract(call ethereum.CallMsg, gasEstimateMode bool) i := statedb.Snapshot() defer statedb.RevertToSnapshot(i) - return e.applyMessage(coreMsgFromCallMsg(call, gasEstimateMode, statedb), statedb, pendingHeader, nil) + return e.applyMessage( + coreMsgFromCallMsg(call, gasEstimateMode, statedb), + statedb, + pendingHeader, + nil, + nil, + ) } func (e *EVMEmulator) applyMessage( @@ -231,6 +237,7 @@ func (e *EVMEmulator) applyMessage( statedb vm.StateDB, header *types.Header, tracer *tracing.Hooks, + onTxStart func(vmEnv *vm.EVM), ) (res *core.ExecutionResult, err error) { // Set msg gas price to 0 msg.GasPrice = big.NewInt(0) @@ -253,6 +260,9 @@ func (e *EVMEmulator) applyMessage( gasPool := core.GasPool(msg.GasLimit) vmEnv.Reset(txContext, statedb) + if onTxStart != nil { + onTxStart(vmEnv) + } // catch any exceptions during the execution, so that an EVM receipt is always produced caughtErr := panicutil.CatchAllExcept(func() { res, err = core.ApplyMessage(vmEnv, msg, &gasPool) @@ -307,11 +317,18 @@ func (e *EVMEmulator) SendTransaction( return nil, nil, err } + onTxStart := func(vmEnv *vm.EVM) { + if tracer != nil && tracer.OnTxStart != nil { + tracer.OnTxStart(vmEnv.GetVMContext(), tx, msg.From) + } + } + result, err = e.applyMessage( msg, statedb, pendingHeader, tracer, + onTxStart, ) gasUsed := uint64(0) @@ -338,6 +355,10 @@ func (e *EVMEmulator) SendTransaction( receipt.ContractAddress = crypto.CreateAddress(msg.From, tx.Nonce()) } + if tracer != nil && tracer.OnTxEnd != nil { + tracer.OnTxEnd(receipt, err) + } + // add the tx and receipt to the blockchain unless addToBlockchain == false if len(addToBlockchain) == 0 || addToBlockchain[0] { e.BlockchainDB().AddTransaction(tx, receipt)