Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Task/improve evm tracer performance #3521

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/evm/evmutil/tx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package evmutil

import (
"slices"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)

func IsFakeTransaction(tx *types.Transaction) bool {
sender, err := GetSender(tx)

// the error will fire when the transaction is invalid. This is most of the time a fake evm tx we use for internal calls, therefore it's fine to assume both.
if slices.Equal(sender.Bytes(), common.Address{}.Bytes()) || err != nil {
return true
}

return false
}
60 changes: 26 additions & 34 deletions packages/evm/jsonrpc/evmchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"math"
"math/big"
"path"
"slices"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
Expand Down Expand Up @@ -666,17 +665,6 @@ func (e *EVMChain) iscRequestsInBlock(evmBlockNumber uint64) (*blocklog.BlockInf
return blocklog.GetRequestsInBlock(blocklogStatePartition, iscBlockIndex)
}

func (e *EVMChain) isFakeTransaction(tx *types.Transaction) bool {
sender, err := evmutil.GetSender(tx)

// the error will fire when the transaction is invalid. This is most of the time a fake evm tx we use for internal calls, therefore it's fine to assume both.
if slices.Equal(sender.Bytes(), common.Address{}.Bytes()) || err != nil {
return true
}

return false
}

// 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(
Expand All @@ -699,12 +687,12 @@ func (e *EVMChain) traceTransaction(
BlockNumber: new(big.Int).SetUint64(blockNumber),
TxIndex: int(txIndex),
TxHash: tx.Hash(),
}, config.TracerConfig)
}, config.TracerConfig, false, nil)
if err != nil {
return nil, err
}

if e.isFakeTransaction(tx) {
if evmutil.IsFakeTransaction(tx) {
return tracer.TraceFakeTx(tx)
}

Expand All @@ -729,32 +717,36 @@ func (e *EVMChain) debugTraceBlock(config *tracers.TraceConfig, block *types.Blo
return nil, err
}

blockTxs, err := e.txsByBlockNumber(new(big.Int).SetUint64(block.NumberU64()))
tracerType := "callTracer"
if config.Tracer != nil {
tracerType = *config.Tracer
}

blockNumber := uint64(iscBlock.BlockIndex())

blockTxs := block.Transactions()

tracer, err := newTracer(tracerType, &tracers.Context{
BlockHash: block.Hash(),
BlockNumber: new(big.Int).SetUint64(blockNumber),
}, config.TracerConfig, true, blockTxs)
if err != nil {
return nil, err
}

results := make([]TxTraceResult, 0)
for i, tx := range blockTxs {
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 {
results = append(results, TxTraceResult{
TxHash: tx.Hash(),
Result: result,
})
}
err = e.backend.EVMTrace(
iscBlock.PreviousAliasOutput,
iscBlock.Timestamp,
iscRequestsInBlock,
nil,
&blockNumber,
tracer.Tracer,
)
if err != nil {
return nil, err
}

return results, nil
return tracer.GetResult()
}

func (e *EVMChain) TraceTransaction(txHash common.Hash, config *tracers.TraceConfig) (any, error) {
Expand Down
135 changes: 127 additions & 8 deletions packages/evm/jsonrpc/jsonrpctest/jsonrpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
package jsonrpctest

import (
"bytes"
"context"
"crypto/ecdsa"
"encoding/hex"
"encoding/json"
"fmt"
"math/big"
"slices"
"strings"
Expand Down Expand Up @@ -624,20 +627,136 @@ func TestRPCTraceEVMDeposit(t *testing.T) {
require.NoError(t, err)
require.EqualValues(t, types.ReceiptStatusSuccessful, rc.Status)

trace, err := env.traceTransactionWithCallTracer(tx.Hash())
t.Run("callTracer_tx", func(t *testing.T) {
var trace jsonrpc.CallFrame
trace, err = env.traceTransactionWithCallTracer(tx.Hash())
require.NoError(t, err)
require.Equal(t, evmAddr.String(), trace.To.String())
require.Equal(t, hexutil.EncodeUint64(isc.NewAssetsBaseTokens(1000).BaseTokens*1e12), trace.Value.String())
})

t.Run("prestateTracer_tx", func(t *testing.T) {
var prestate jsonrpc.PrestateAccountMap
prestate, err = env.traceTransactionWithPrestate(tx.Hash())
require.NoError(t, err)
require.Empty(t, prestate)
})

t.Run("prestateTracerDiff_tx", func(t *testing.T) {
var prestateDiff jsonrpc.PrestateDiffResult
prestateDiff, err = env.traceTransactionWithPrestateDiff(tx.Hash())
require.NoError(t, err)
require.Empty(t, prestateDiff.Pre)
require.Empty(t, prestateDiff.Post)
})

t.Run("callTracer_block", 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)

traces := make([]jsonrpc.TxTraceResult, 0)
err = json.Unmarshal(res1, &traces)
require.NoError(t, err)
require.Len(t, traces, 1)
require.Equal(t, tx.Hash(), traces[0].TxHash)

cs := jsonrpc.CallFrame{}
err = json.Unmarshal(traces[0].Result, &cs)
require.NoError(t, err)
require.Equal(t, evmAddr.String(), cs.To.String())
require.Equal(t, hexutil.EncodeUint64(isc.NewAssetsBaseTokens(1000).BaseTokens*1e12), cs.Value.String())
})

t.Run("prestateTracer_block", func(t *testing.T) {
tracer := "prestateTracer"
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: &tracer},
)
require.NoError(t, err)

traces := make([]jsonrpc.TxTraceResult, 0)
err = json.Unmarshal(res1, &traces)
require.NoError(t, err)
require.Len(t, traces, 1)
require.Equal(t, tx.Hash(), traces[0].TxHash)

prestate := jsonrpc.PrestateAccountMap{}
err = json.Unmarshal(traces[0].Result, &prestate)
require.NoError(t, err)
require.Empty(t, prestate)
})
}

func addNRequests(n int, env *soloTestEnv, creator *ecdsa.PrivateKey, creatorAddress common.Address, contractABI abi.ABI, contractAddress common.Address) {
rqs := make([]isc.Request, 0, n)
for i := 0; i < n; i++ {
tx1 := types.MustSignNewTx(creator, types.NewEIP155Signer(big.NewInt(int64(env.ChainID))),
&types.LegacyTx{
Nonce: env.NonceAt(creatorAddress) + uint64(i),
To: &contractAddress,
Value: big.NewInt(123),
Gas: 100000,
GasPrice: big.NewInt(10000000000),
Data: lo.Must(contractABI.Pack("sendTo", common.Address{0x1}, big.NewInt(2))),
})

req1 := lo.Must(isc.NewEVMOffLedgerTxRequest(env.soloChain.ChainID, tx1))
rqs = append(rqs, req1)
}

env.soloChain.WaitForRequestsMark()
env.soloChain.Env.AddRequestsToMempool(env.soloChain, rqs)
}

// TestRPCTraceBlockForLargeN requires a large number of requests to be added to the mempool, for that set solo.MaxRequestsInBlock to a large value (>500)
func TestRPCTraceBlockForLargeN(t *testing.T) {
t.Skip("skipping because it requires solo parameters to be set")

n := 400
env := newSoloTestEnv(t)
creator, creatorAddress := env.soloChain.NewEthereumAccountWithL2Funds()
contractABI, err := abi.JSON(strings.NewReader(evmtest.ISCTestContractABI))
require.NoError(t, err)
_, _, contractAddress := env.DeployEVMContract(creator, contractABI, evmtest.ISCTestContractBytecode)

addNRequests(n, env, creator, creatorAddress, contractABI, contractAddress)

require.Equal(t, evmAddr.String(), trace.To.String())
require.Equal(t, hexutil.EncodeUint64(isc.NewAssetsBaseTokens(1000).BaseTokens*1e12), trace.Value.String())
require.True(t, env.soloChain.WaitForRequestsThrough(n, 5*time.Minute))

prestate, err := env.traceTransactionWithPrestate(tx.Hash())
bi := env.soloChain.GetLatestBlockInfo()
require.EqualValues(t, n, 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(
context.Background(),
&res1,
"debug_traceBlockByNumber",
hexutil.Uint64(env.BlockNumber()).String(),
tracers.TraceConfig{Tracer: &callTracer},
)
require.NoError(t, err)
require.Empty(t, prestate)

prestateDiff, err := env.traceTransactionWithPrestateDiff(tx.Hash())
var prettyJSON bytes.Buffer
err = json.Indent(&prettyJSON, res1, "", " ")
require.NoError(t, err)
require.Empty(t, prestateDiff.Pre)
require.Empty(t, prestateDiff.Post)
fmt.Println(prettyJSON.String())
}

func TestRPCTraceBlock(t *testing.T) {
Expand Down
51 changes: 48 additions & 3 deletions packages/evm/jsonrpc/tracer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,70 @@ import (

"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/eth/tracers"

"github.com/iotaledger/wasp/packages/evm/evmutil"
)

type Tracer struct {
*tracers.Tracer
TraceFakeTx func(tx *types.Transaction) (json.RawMessage, error)
}

type tracerFactory func(*tracers.Context, json.RawMessage) (*Tracer, error)
type tracerFactory func(traceCtx *tracers.Context, cfg json.RawMessage, traceBlock bool, initValue any) (*Tracer, error)

var allTracers = map[string]tracerFactory{}

func registerTracer(tracerType string, fn tracerFactory) {
allTracers[tracerType] = fn
}

func newTracer(tracerType string, ctx *tracers.Context, cfg json.RawMessage) (*Tracer, error) {
func newTracer(tracerType string, ctx *tracers.Context, cfg json.RawMessage, traceBlock bool, initValue any) (*Tracer, error) {
fn := allTracers[tracerType]
if fn == nil {
return nil, fmt.Errorf("unsupported tracer type: %s", tracerType)
}
return fn(ctx, cfg)
return fn(ctx, cfg, traceBlock, initValue)
}

func GetTraceResults(
blockTxs []*types.Transaction,
traceBlock bool,
getFakeTxTrace func(tx *types.Transaction) (json.RawMessage, error),
getTxTrace func(tx *types.Transaction) (json.RawMessage, error),
getSingleTxTrace func() (json.RawMessage, error),
reason error,
) (json.RawMessage, error) {
var traceResult []byte
var err error
if traceBlock {
results := make([]TxTraceResult, 0, len(blockTxs))
var jsResult json.RawMessage
for _, tx := range blockTxs {
if evmutil.IsFakeTransaction(tx) {
jsResult, err = getFakeTxTrace(tx)
if err != nil {
return nil, err
}
} else {
jsResult, err = getTxTrace(tx)
if err != nil {
return nil, err
}
}

results = append(results, TxTraceResult{TxHash: tx.Hash(), Result: jsResult})
}

traceResult, err = json.Marshal(results)
if err != nil {
return nil, err
}
} else {
traceResult, err = getSingleTxTrace()
if err != nil {
return nil, err
}
}

return traceResult, reason
}
Loading
Loading