diff --git a/packages/chainutil/evmtrace.go b/packages/chainutil/evmtrace.go index 119c810699..0166c8eb64 100644 --- a/packages/chainutil/evmtrace.go +++ b/packages/chainutil/evmtrace.go @@ -14,8 +14,6 @@ func EVMTrace( aliasOutput *isc.AliasOutputWithID, blockTime time.Time, iscRequestsInBlock []isc.Request, - txIndex *uint64, - blockNumber *uint64, tracer *tracers.Tracer, ) error { _, err := runISCTask( @@ -24,11 +22,7 @@ func EVMTrace( blockTime, iscRequestsInBlock, false, - &isc.EVMTracer{ - Tracer: tracer, - TxIndex: txIndex, - BlockNumber: blockNumber, - }, + tracer, ) return err } diff --git a/packages/chainutil/runvm.go b/packages/chainutil/runvm.go index ce67fc08eb..bfd204883c 100644 --- a/packages/chainutil/runvm.go +++ b/packages/chainutil/runvm.go @@ -4,9 +4,9 @@ import ( "errors" "time" - "go.uber.org/zap" - + "github.com/ethereum/go-ethereum/eth/tracers" "github.com/samber/lo" + "go.uber.org/zap" "github.com/iotaledger/wasp/packages/chain" "github.com/iotaledger/wasp/packages/hashing" @@ -27,7 +27,7 @@ func runISCTask( blockTime time.Time, reqs []isc.Request, estimateGasMode bool, - evmTracer *isc.EVMTracer, + evmTracer *tracers.Tracer, ) ([]*vm.RequestResult, error) { store := ch.Store() migs, err := getMigrationsForBlock(store, aliasOutput) diff --git a/packages/evm/jsonrpc/chainbackend.go b/packages/evm/jsonrpc/chainbackend.go index c6e9088ecb..c04fb284b7 100644 --- a/packages/evm/jsonrpc/chainbackend.go +++ b/packages/evm/jsonrpc/chainbackend.go @@ -23,7 +23,7 @@ type ChainBackend interface { EVMSendTransaction(tx *types.Transaction) error EVMCall(aliasOutput *isc.AliasOutputWithID, callMsg ethereum.CallMsg) ([]byte, error) EVMEstimateGas(aliasOutput *isc.AliasOutputWithID, callMsg ethereum.CallMsg) (uint64, error) - EVMTrace(aliasOutput *isc.AliasOutputWithID, blockTime time.Time, iscRequestsInBlock []isc.Request, txIndex *uint64, blockNumber *uint64, tracer *tracers.Tracer) error + EVMTrace(aliasOutput *isc.AliasOutputWithID, blockTime time.Time, iscRequestsInBlock []isc.Request, tracer *tracers.Tracer) error FeePolicy(blockIndex uint32) (*gas.FeePolicy, error) ISCChainID() *isc.ChainID ISCCallView(chainState state.State, scName string, funName string, args dict.Dict) (dict.Dict, error) diff --git a/packages/evm/jsonrpc/evmchain.go b/packages/evm/jsonrpc/evmchain.go index a15ef78a15..c6fd07a7c0 100644 --- a/packages/evm/jsonrpc/evmchain.go +++ b/packages/evm/jsonrpc/evmchain.go @@ -681,34 +681,45 @@ func (e *EVMChain) traceTransaction( } blockNumber := uint64(blockInfo.BlockIndex()) - - tracer, err := newTracer(tracerType, &tracers.Context{ - BlockHash: blockHash, - BlockNumber: new(big.Int).SetUint64(blockNumber), - TxIndex: int(txIndex), - TxHash: tx.Hash(), - }, config.TracerConfig, false, nil) + tracer, err := newTracer( + tracerType, + &tracers.Context{ + BlockHash: blockHash, + BlockNumber: new(big.Int).SetUint64(blockNumber), + TxIndex: int(txIndex), + TxHash: tx.Hash(), + }, + config.TracerConfig, + ) if err != nil { return nil, err } - if evmutil.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 } - return tracer.GetResult() + res, err := tracer.GetResult() + if err != nil { + return nil, err + } + + var txResults []TxTraceResult + err = json.Unmarshal(res, &txResults) + if err != nil { + return nil, err + } + + if len(txResults) <= int(txIndex) { + return nil, errors.New("tx trace not found in tracer result") + } + return txResults[int(txIndex)].Result, nil } func (e *EVMChain) debugTraceBlock(config *tracers.TraceConfig, block *types.Block) (any, error) { @@ -723,13 +734,14 @@ func (e *EVMChain) debugTraceBlock(config *tracers.TraceConfig, block *types.Blo } 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) + tracer, err := newTracer( + tracerType, + &tracers.Context{ + BlockHash: block.Hash(), + BlockNumber: new(big.Int).SetUint64(blockNumber), + }, + config.TracerConfig, + ) if err != nil { return nil, err } @@ -738,14 +750,11 @@ func (e *EVMChain) debugTraceBlock(config *tracers.TraceConfig, block *types.Blo iscBlock.PreviousAliasOutput, iscBlock.Timestamp, iscRequestsInBlock, - nil, - &blockNumber, - tracer.Tracer, + tracer, ) if err != nil { return nil, err } - return tracer.GetResult() } diff --git a/packages/evm/jsonrpc/jsonrpctest/jsonrpc_test.go b/packages/evm/jsonrpc/jsonrpctest/jsonrpc_test.go index 20f3aef73f..23dbad2b9e 100644 --- a/packages/evm/jsonrpc/jsonrpctest/jsonrpc_test.go +++ b/packages/evm/jsonrpc/jsonrpctest/jsonrpc_test.go @@ -592,15 +592,15 @@ func TestRPCTraceTx(t *testing.T) { t.Run("prestate", func(t *testing.T) { accountMap, err := env.traceTransactionWithPrestate(tx1.Hash()) + // t.Logf("%s", lo.Must(json.MarshalIndent(accountMap, "", " "))) require.NoError(t, err) require.NotEmpty(t, accountMap) - // t.Logf("%s", lo.Must(json.MarshalIndent(accountMap, "", " "))) diff, err := env.traceTransactionWithPrestateDiff(tx1.Hash()) + // t.Logf("%s", lo.Must(json.MarshalIndent(diff, "", " "))) require.NoError(t, err) require.NotEmpty(t, diff.Pre) require.NotEmpty(t, diff.Post) - // t.Logf("%s", lo.Must(json.MarshalIndent(diff, "", " "))) }) } diff --git a/packages/evm/jsonrpc/tracer.go b/packages/evm/jsonrpc/tracer.go index 84156944bc..0477e088ac 100644 --- a/packages/evm/jsonrpc/tracer.go +++ b/packages/evm/jsonrpc/tracer.go @@ -4,18 +4,11 @@ import ( "encoding/json" "fmt" - "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/common" "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(traceCtx *tracers.Context, cfg json.RawMessage, traceBlock bool, initValue any) (*Tracer, error) +type tracerFactory func(traceCtx *tracers.Context, cfg json.RawMessage) (*tracers.Tracer, error) var allTracers = map[string]tracerFactory{} @@ -23,53 +16,20 @@ func registerTracer(tracerType string, fn tracerFactory) { allTracers[tracerType] = fn } -func newTracer(tracerType string, ctx *tracers.Context, cfg json.RawMessage, traceBlock bool, initValue any) (*Tracer, error) { +func newTracer( + tracerType string, + ctx *tracers.Context, + cfg json.RawMessage, +) (*tracers.Tracer, error) { fn := allTracers[tracerType] if fn == nil { return nil, fmt.Errorf("unsupported tracer type: %s", tracerType) } - return fn(ctx, cfg, traceBlock, initValue) + return fn(ctx, cfg) } -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 +type TxTraceResult struct { + TxHash common.Hash `json:"txHash"` // transaction hash + Result json.RawMessage `json:"result,omitempty"` // Trace results produced by the tracer + Error string `json:"error,omitempty"` // Trace failure produced by the tracer } diff --git a/packages/evm/jsonrpc/tracer_call.go b/packages/evm/jsonrpc/tracer_call.go index 758b5aeb1e..d68769716a 100644 --- a/packages/evm/jsonrpc/tracer_call.go +++ b/packages/evm/jsonrpc/tracer_call.go @@ -6,7 +6,6 @@ package jsonrpc import ( "encoding/json" "errors" - "fmt" "math/big" "strings" "sync/atomic" @@ -18,17 +17,13 @@ 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" + "github.com/samber/lo" ) func init() { registerTracer("callTracer", newCallTracer) } -// 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 { Address common.Address `json:"address"` Topics []common.Hash `json:"topics"` @@ -59,6 +54,8 @@ func (o *OpCodeJSON) UnmarshalJSON(data []byte) error { return nil } +// CallFrame contains the result of a trace with "callTracer". +// Code is 100% copied from go-ethereum (since the type is unexported there) type CallFrame struct { Type OpCodeJSON `json:"type"` From common.Address `json:"from"` @@ -113,22 +110,18 @@ func (f *CallFrame) processOutput(output []byte, err error, reverted bool) { } } -type TxTraceResult struct { - TxHash common.Hash `json:"txHash"` // transaction hash - Result json.RawMessage `json:"result,omitempty"` // Trace results produced by the tracer - Error string `json:"error,omitempty"` // Trace failure produced by the tracer +type callTxTracer struct { + txHash common.Hash + frames []CallFrame + gasLimit uint64 + depth int } type callTracer struct { - txToStack map[common.Hash][]CallFrame - config callTracerConfig - gasLimit uint64 - depth int - interrupt atomic.Bool // Atomic flag to signal execution interruption - reason error // Textual reason for the interruption - currentTx common.Hash - traceBlock bool - blockTxs []*types.Transaction + txTraces []*callTxTracer + config callTracerConfig + interrupt atomic.Bool // Atomic flag to signal execution interruption + reason error // Textual reason for the interruption } type callTracerConfig struct { @@ -138,41 +131,25 @@ 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, traceBlock bool, initValue any) (*Tracer, error) { - var fakeTxs types.Transactions - - if initValue == nil && traceBlock { - return nil, fmt.Errorf("initValue with block transactions is required for block tracing") - } - - if initValue != nil { - var ok bool - fakeTxs, ok = initValue.(types.Transactions) - if !ok { - return nil, fmt.Errorf("invalid init value type for calltracer: %T", initValue) - } - } - t, err := newCallTracerObject(ctx, cfg, traceBlock, fakeTxs) +func newCallTracer(ctx *tracers.Context, cfg json.RawMessage) (*tracers.Tracer, error) { + t, err := newCallTracerObject(ctx, cfg) if err != nil { return nil, err } - 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, + return &tracers.Tracer{ + Hooks: &tracing.Hooks{ + OnTxStart: t.OnTxStart, + OnTxEnd: t.OnTxEnd, + OnEnter: t.OnEnter, + OnExit: t.OnExit, + OnLog: t.OnLog, }, - TraceFakeTx: t.TraceFakeTx, + GetResult: t.GetResult, + Stop: t.Stop, }, nil } -func newCallTracerObject(_ *tracers.Context, cfg json.RawMessage, traceBlock bool, blockTxs []*types.Transaction) (*callTracer, error) { +func newCallTracerObject(_ *tracers.Context, cfg json.RawMessage) (*callTracer, error) { var config callTracerConfig if cfg != nil { if err := json.Unmarshal(cfg, &config); err != nil { @@ -181,12 +158,18 @@ func newCallTracerObject(_ *tracers.Context, cfg json.RawMessage, traceBlock boo } // First callframe contains tx context info // and is populated on start and end. - return &callTracer{txToStack: make(map[common.Hash][]CallFrame), currentTx: common.Hash{}, config: config, traceBlock: traceBlock, blockTxs: blockTxs}, nil + return &callTracer{ + config: config, + }, nil +} + +func (t *callTracer) currentTxTrace() *callTxTracer { + return t.txTraces[len(t.txTraces)-1] } // OnEnter is called when EVM enters a new scope (via call, create or selfdestruct). func (t *callTracer) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { - t.depth = depth + t.currentTxTrace().depth = depth if t.config.OnlyTopCall && depth > 0 { return } @@ -205,9 +188,9 @@ func (t *callTracer) OnEnter(depth int, typ byte, from common.Address, to common Value: hexutil.Big(*value), } if depth == 0 { - call.Gas = hexutil.Uint64(t.gasLimit) + call.Gas = hexutil.Uint64(t.currentTxTrace().gasLimit) } - t.txToStack[t.currentTx] = append(t.txToStack[t.currentTx], call) + t.currentTxTrace().frames = append(t.currentTxTrace().frames, call) } // OnExit is called when EVM exits a scope, even if the scope didn't @@ -218,37 +201,39 @@ func (t *callTracer) OnExit(depth int, output []byte, gasUsed uint64, err error, return } - t.depth = depth - 1 + t.currentTxTrace().depth = depth - 1 if t.config.OnlyTopCall { return } - size := len(t.txToStack[t.currentTx]) + size := len(t.currentTxTrace().frames) if size <= 1 { return } // Pop call. - call := t.txToStack[t.currentTx][size-1] - t.txToStack[t.currentTx] = t.txToStack[t.currentTx][:size-1] + call := t.currentTxTrace().frames[size-1] + t.currentTxTrace().frames = t.currentTxTrace().frames[:size-1] size-- call.GasUsed = hexutil.Uint64(gasUsed) call.processOutput(output, err, reverted) // Nest call into parent. - t.txToStack[t.currentTx][size-1].Calls = append(t.txToStack[t.currentTx][size-1].Calls, call) + t.currentTxTrace().frames[size-1].Calls = append(t.currentTxTrace().frames[size-1].Calls, call) } func (t *callTracer) captureEnd(output []byte, _ uint64, err error, reverted bool) { - if len(t.txToStack[t.currentTx]) != 1 { + if len(t.currentTxTrace().frames) != 1 { return } - t.txToStack[t.currentTx][0].processOutput(output, err, reverted) + t.currentTxTrace().frames[0].processOutput(output, err, reverted) } func (t *callTracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) { - t.gasLimit = tx.Gas() - t.currentTx = tx.Hash() - t.txToStack[t.currentTx] = make([]CallFrame, 0, 1) + t.txTraces = append(t.txTraces, &callTxTracer{ + txHash: tx.Hash(), + frames: make([]CallFrame, 0, 1), + gasLimit: tx.Gas(), + }) } func (t *callTracer) OnTxEnd(receipt *types.Receipt, err error) { @@ -256,10 +241,10 @@ func (t *callTracer) OnTxEnd(receipt *types.Receipt, err error) { if err != nil { return } - t.txToStack[t.currentTx][0].GasUsed = hexutil.Uint64(receipt.GasUsed) + t.currentTxTrace().frames[0].GasUsed = hexutil.Uint64(receipt.GasUsed) if t.config.WithLog { // Logs are not emitted when the call fails - clearFailedLogs(&t.txToStack[t.currentTx][0], false) + clearFailedLogs(&t.currentTxTrace().frames[0], false) } } @@ -269,7 +254,7 @@ func (t *callTracer) OnLog(log *types.Log) { return } // Avoid processing nested calls when only caring about top call - if t.config.OnlyTopCall && t.depth > 0 { + if t.config.OnlyTopCall && t.currentTxTrace().depth > 0 { return } // Skip if tracing was interrupted @@ -280,31 +265,24 @@ func (t *callTracer) OnLog(log *types.Log) { Address: log.Address, Topics: log.Topics, Data: log.Data, - Position: hexutil.Uint(len(t.txToStack[t.currentTx][len(t.txToStack[t.currentTx])-1].Calls)), + Position: hexutil.Uint(len(t.currentTxTrace().frames[len(t.currentTxTrace().frames)-1].Calls)), } - t.txToStack[t.currentTx][len(t.txToStack[t.currentTx])-1].Logs = append(t.txToStack[t.currentTx][len(t.txToStack[t.currentTx])-1].Logs, l) + t.currentTxTrace().frames[len(t.currentTxTrace().frames)-1].Logs = append(t.currentTxTrace().frames[len(t.currentTxTrace().frames)-1].Logs, l) } -var ErrIncorrectTopLevelCalls = errors.New("incorrect number of top-level calls") - // GetResult returns the json-encoded nested list of call traces, and any // error arising from the encoding or forceful termination (via `Stop`). func (t *callTracer) GetResult() (json.RawMessage, error) { - return GetTraceResults( - t.blockTxs, - t.traceBlock, - t.TraceFakeTx, - func(tx *types.Transaction) (json.RawMessage, error) { - stack, ok := t.txToStack[tx.Hash()] - if !ok { - return nil, fmt.Errorf("no call stack for tx %s", tx.Hash().Hex()) - } - return json.Marshal(stack[0]) - }, - func() (json.RawMessage, error) { - return json.Marshal(t.txToStack[t.currentTx][0]) - }, - t.reason) + r := lo.Map(t.txTraces, func(tr *callTxTracer, _ int) TxTraceResult { + if len(tr.frames) != 1 { + panic("expected exactly 1 top-level call") + } + return TxTraceResult{ + TxHash: tr.txHash, + Result: lo.Must(json.Marshal(tr.frames[0])), + } + }) + return json.Marshal(r) } // Stop terminates execution of the tracer at the first opportune moment. @@ -325,16 +303,3 @@ 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_prestate.go b/packages/evm/jsonrpc/tracer_prestate.go index 8966913d74..4dfa51ee4d 100644 --- a/packages/evm/jsonrpc/tracer_prestate.go +++ b/packages/evm/jsonrpc/tracer_prestate.go @@ -6,7 +6,6 @@ package jsonrpc import ( "bytes" "encoding/json" - "fmt" "sync/atomic" "github.com/ethereum/go-ethereum/common" @@ -17,6 +16,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/log" + "github.com/samber/lo" ) func init() { @@ -42,67 +42,53 @@ 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 PrestateTxValue struct { - Pre PrestateAccountMap `json:"pre"` - Post PrestateAccountMap `json:"post"` +type prestateTxTrace struct { + txHash common.Hash + env *tracing.VMContext + pre PrestateAccountMap + post PrestateAccountMap + to common.Address created map[common.Address]bool deleted map[common.Address]bool - to common.Address } type prestateTracer struct { - env *tracing.VMContext - currentTxHash common.Hash - states map[common.Hash]*PrestateTxValue // key is the tx hash, value is the state diff - config prestateTracerConfig - interrupt atomic.Bool // Atomic flag to signal execution interruption - reason error // Textual reason for the interruption - traceBlock bool - blockTxs types.Transactions + txTraces []*prestateTxTrace + config prestateTracerConfig + interrupt atomic.Bool // Atomic flag to signal execution interruption + reason error // Textual reason for the interruption } type prestateTracerConfig struct { DiffMode bool `json:"diffMode"` // If true, this tracer will return state modifications } -func newPrestateTracer(ctx *tracers.Context, cfg json.RawMessage, traceBlock bool, initValue any) (*Tracer, error) { - var blockTxs types.Transactions - - if initValue == nil && traceBlock { - return nil, fmt.Errorf("initValue with block transactions is required for block tracing") - } - - if initValue != nil { - var ok bool - blockTxs, ok = initValue.(types.Transactions) - if !ok { - return nil, fmt.Errorf("invalid init value type for prestateTracer: %T", initValue) - } - } +func newPrestateTracer( + ctx *tracers.Context, + cfg json.RawMessage, +) (*tracers.Tracer, error) { var config prestateTracerConfig if err := json.Unmarshal(cfg, &config); err != nil { return nil, err } t := &prestateTracer{ - config: config, - traceBlock: traceBlock, - states: make(map[common.Hash]*PrestateTxValue), - blockTxs: blockTxs, + config: config, } - return &Tracer{ - Tracer: &tracers.Tracer{ - Hooks: &tracing.Hooks{ - OnTxStart: t.OnTxStart, - OnTxEnd: t.OnTxEnd, - OnOpcode: t.OnOpcode, - }, - GetResult: t.GetResult, - Stop: t.Stop, + return &tracers.Tracer{ + Hooks: &tracing.Hooks{ + OnTxStart: t.OnTxStart, + OnTxEnd: t.OnTxEnd, + OnOpcode: t.OnOpcode, }, - TraceFakeTx: t.TraceFakeTx, + GetResult: t.GetResult, + Stop: t.Stop, }, nil } +func (t *prestateTracer) currentTxTrace() *prestateTxTrace { + return t.txTraces[len(t.txTraces)-1] +} + // OnOpcode implements the EVMLogger interface to trace a single step of VM execution. // //nolint:gocyclo @@ -121,21 +107,21 @@ func (t *prestateTracer) OnOpcode(pc uint64, opcode byte, gas, cost uint64, scop switch { case stackLen >= 1 && (op == vm.SLOAD || op == vm.SSTORE): slot := common.Hash(stackData[stackLen-1].Bytes32()) - t.lookupStorage(t.currentTxHash, caller, slot) + 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(t.currentTxHash, addr) + t.lookupAccount(addr) if op == vm.SELFDESTRUCT { - t.states[t.currentTxHash].deleted[caller] = true + t.currentTxTrace().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(t.currentTxHash, addr) + t.lookupAccount(addr) case op == vm.CREATE: - nonce := t.env.StateDB.GetNonce(caller) + nonce := t.currentTxTrace().env.StateDB.GetNonce(caller) addr := crypto.CreateAddress(caller, nonce) - t.lookupAccount(t.currentTxHash, addr) - t.states[t.currentTxHash].created[addr] = true + t.lookupAccount(addr) + t.currentTxTrace().created[addr] = true case stackLen >= 4 && op == vm.CREATE2: offset := stackData[stackLen-2] size := stackData[stackLen-3] @@ -147,38 +133,43 @@ func (t *prestateTracer) OnOpcode(pc uint64, opcode byte, gas, cost uint64, scop inithash := crypto.Keccak256(init) salt := stackData[stackLen-4] addr := crypto.CreateAddress2(caller, salt.Bytes32(), inithash) - t.lookupAccount(t.currentTxHash, addr) - t.states[t.currentTxHash].created[addr] = true + t.lookupAccount(addr) + t.currentTxTrace().created[addr] = true } } func (t *prestateTracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) { - t.env = env - t.currentTxHash = tx.Hash() - - t.states[tx.Hash()] = &PrestateTxValue{ - Pre: make(PrestateAccountMap), - Post: make(PrestateAccountMap), + t.txTraces = append(t.txTraces, &prestateTxTrace{ + txHash: tx.Hash(), + env: env, + pre: make(PrestateAccountMap), + post: make(PrestateAccountMap), created: make(map[common.Address]bool), deleted: make(map[common.Address]bool), - } + }) - txState := t.states[tx.Hash()] + txTrace := t.currentTxTrace() if tx.To() == nil { createdAddr := crypto.CreateAddress(from, env.StateDB.GetNonce(from)) - txState.to = createdAddr - txState.created[createdAddr] = true + txTrace.to = createdAddr + txTrace.created[createdAddr] = true } else { - txState.to = *tx.To() + txTrace.to = *tx.To() } - t.lookupAccount(tx.Hash(), from) - t.lookupAccount(tx.Hash(), txState.to) - t.lookupAccount(tx.Hash(), env.Coinbase) + t.lookupAccount(from) + t.lookupAccount(txTrace.to) + if env != nil { + t.lookupAccount(env.Coinbase) + } } func (t *prestateTracer) OnTxEnd(receipt *types.Receipt, err error) { + defer func() { + // don't keep pointer to a VMContext that is no longer needed + t.currentTxTrace().env = nil + }() if err != nil { return } @@ -186,10 +177,10 @@ func (t *prestateTracer) OnTxEnd(receipt *types.Receipt, err error) { t.processDiffState() } // the new created contracts' prestate were empty, so delete them - for a := range t.states[t.currentTxHash].created { + for a := range t.currentTxTrace().created { // the created contract maybe exists in statedb before the creating tx - if s := t.states[t.currentTxHash].Pre[a]; s != nil && s.empty { - delete(t.states[t.currentTxHash].Pre, a) + if s := t.currentTxTrace().pre[a]; s != nil && s.empty { + delete(t.currentTxTrace().pre, a) } } } @@ -197,22 +188,19 @@ func (t *prestateTracer) OnTxEnd(receipt *types.Receipt, err error) { // 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) { - return GetTraceResults( - t.blockTxs, - t.traceBlock, - t.TraceFakeTx, - func(tx *types.Transaction) (json.RawMessage, error) { - txState := t.states[tx.Hash()] - if t.config.DiffMode { - return json.Marshal(PrestateDiffResult{txState.Post, txState.Pre}) - } - return json.Marshal(txState.Pre) - }, func() (json.RawMessage, error) { - if t.config.DiffMode { - return json.Marshal(PrestateDiffResult{t.states[t.currentTxHash].Post, t.states[t.currentTxHash].Pre}) - } - return json.Marshal(t.states[t.currentTxHash].Pre) - }, t.reason) + r := lo.Map(t.txTraces, func(tr *prestateTxTrace, _ int) TxTraceResult { + var b json.RawMessage + if t.config.DiffMode { + b = lo.Must(json.Marshal(PrestateDiffResult{tr.post, tr.pre})) + } else { + b = lo.Must(json.Marshal(tr.pre)) + } + return TxTraceResult{ + TxHash: tr.txHash, + Result: b, + } + }) + return json.Marshal(r) } // Stop terminates execution of the tracer at the first opportune moment. @@ -222,27 +210,27 @@ func (t *prestateTracer) Stop(err error) { } func (t *prestateTracer) processDiffState() { - txState := t.states[t.currentTxHash] - for addr, state := range txState.Pre { + txTrace := t.currentTxTrace() + for addr, state := range txTrace.pre { // The deleted account's state is pruned from `post` but kept in `pre` - if _, ok := txState.deleted[addr]; ok { + if _, ok := txTrace.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) + newBalance := t.currentTxTrace().env.StateDB.GetBalance(addr).ToBig() + newNonce := t.currentTxTrace().env.StateDB.GetNonce(addr) + newCode := t.currentTxTrace().env.StateDB.GetCode(addr) - if newBalance.Cmp(txState.Pre[addr].Balance.ToInt()) != 0 { + if newBalance.Cmp(txTrace.pre[addr].Balance.ToInt()) != 0 { modified = true postAccount.Balance = (*hexutil.Big)(newBalance) } - if newNonce != txState.Pre[addr].Nonce { + if newNonce != txTrace.pre[addr].Nonce { modified = true postAccount.Nonce = newNonce } - if !bytes.Equal(newCode, txState.Pre[addr].Code) { + if !bytes.Equal(newCode, txTrace.pre[addr].Code) { modified = true postAccount.Code = newCode } @@ -250,13 +238,13 @@ func (t *prestateTracer) processDiffState() { for key, val := range state.Storage { // don't include the empty slot if val == (common.Hash{}) { - delete(txState.Pre[addr].Storage, key) + delete(txTrace.pre[addr].Storage, key) } - newVal := t.env.StateDB.GetState(addr, key) + newVal := t.currentTxTrace().env.StateDB.GetState(addr, key) if val == newVal { // Omit unchanged slots - delete(txState.Pre[addr].Storage, key) + delete(txTrace.pre[addr].Storage, key) } else { modified = true if newVal != (common.Hash{}) { @@ -266,52 +254,43 @@ func (t *prestateTracer) processDiffState() { } if modified { - txState.Post[addr] = postAccount + txTrace.post[addr] = postAccount } else { // if state is not modified, then no need to include into the pre state - delete(txState.Pre, addr) + delete(txTrace.pre, addr) } } } // lookupAccount fetches details of an account and adds it to the prestate // if it doesn't exist there. -func (t *prestateTracer) lookupAccount(tx common.Hash, addr common.Address) { - if _, ok := t.states[tx].Pre[addr]; ok { +func (t *prestateTracer) lookupAccount(addr common.Address) { + if t.currentTxTrace().env == nil { + return + } + if _, ok := t.currentTxTrace().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), + Balance: (*hexutil.Big)(t.currentTxTrace().env.StateDB.GetBalance(addr).ToBig()), + Nonce: t.currentTxTrace().env.StateDB.GetNonce(addr), + Code: t.currentTxTrace().env.StateDB.GetCode(addr), Storage: make(map[common.Hash]common.Hash), } if !acc.exists() { acc.empty = true } - t.states[tx].Pre[addr] = acc + t.currentTxTrace().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(tx common.Hash, addr common.Address, key common.Hash) { - if _, ok := t.states[tx].Pre[addr].Storage[key]; ok { +func (t *prestateTracer) lookupStorage(addr common.Address, key common.Hash) { + if _, ok := t.currentTxTrace().pre[addr].Storage[key]; ok { return } - t.states[tx].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 + t.currentTxTrace().pre[addr].Storage[key] = t.currentTxTrace().env.StateDB.GetState(addr, key) } diff --git a/packages/evm/jsonrpc/waspevmbackend.go b/packages/evm/jsonrpc/waspevmbackend.go index b28d16557d..28bdbedb6b 100644 --- a/packages/evm/jsonrpc/waspevmbackend.go +++ b/packages/evm/jsonrpc/waspevmbackend.go @@ -88,8 +88,6 @@ func (b *WaspEVMBackend) EVMTrace( aliasOutput *isc.AliasOutputWithID, blockTime time.Time, iscRequestsInBlock []isc.Request, - txIndex *uint64, - blockNumber *uint64, tracer *tracers.Tracer, ) error { return chainutil.EVMTrace( @@ -97,8 +95,6 @@ func (b *WaspEVMBackend) EVMTrace( aliasOutput, blockTime, iscRequestsInBlock, - txIndex, - blockNumber, tracer, ) } diff --git a/packages/isc/sandbox_interface.go b/packages/isc/sandbox_interface.go index 8f3f54389e..fec29a3a22 100644 --- a/packages/isc/sandbox_interface.go +++ b/packages/isc/sandbox_interface.go @@ -125,7 +125,7 @@ type Sandbox interface { // EVMTracer returns a non-nil tracer if an EVM tx is being traced // (e.g. with the debug_traceTransaction JSONRPC method). - EVMTracer() *EVMTracer + EVMTracer() *tracers.Tracer // TakeStateSnapshot takes a snapshot of the state. This is useful to implement the try/catch // behavior in Solidity, where the state is reverted after a low level call fails. @@ -239,9 +239,3 @@ type BLS interface { AddressFromPublicKey(pubKey []byte) (iotago.Address, error) AggregateBLSSignatures(pubKeysBin [][]byte, sigsBin [][]byte) ([]byte, []byte, error) } - -type EVMTracer struct { - Tracer *tracers.Tracer - TxIndex *uint64 - BlockNumber *uint64 -} diff --git a/packages/solo/evm.go b/packages/solo/evm.go index 9002810135..8844aa6556 100644 --- a/packages/solo/evm.go +++ b/packages/solo/evm.go @@ -66,8 +66,6 @@ func (b *jsonRPCSoloBackend) EVMTrace( aliasOutput *isc.AliasOutputWithID, blockTime time.Time, iscRequestsInBlock []isc.Request, - txIndex *uint64, - blockNumber *uint64, tracer *tracers.Tracer, ) error { return chainutil.EVMTrace( @@ -75,8 +73,6 @@ func (b *jsonRPCSoloBackend) EVMTrace( aliasOutput, blockTime, iscRequestsInBlock, - txIndex, - blockNumber, tracer, ) } diff --git a/packages/vm/core/evm/evmimpl/impl.go b/packages/vm/core/evm/evmimpl/impl.go index 15d5ad1f3f..6dc62aa934 100644 --- a/packages/vm/core/evm/evmimpl/impl.go +++ b/packages/vm/core/evm/evmimpl/impl.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/samber/lo" @@ -24,7 +25,7 @@ import ( "github.com/iotaledger/wasp/packages/parameters" "github.com/iotaledger/wasp/packages/util" "github.com/iotaledger/wasp/packages/util/panicutil" - "github.com/iotaledger/wasp/packages/vm" + iscvm "github.com/iotaledger/wasp/packages/vm" "github.com/iotaledger/wasp/packages/vm/core/accounts" "github.com/iotaledger/wasp/packages/vm/core/errors/coreerrors" "github.com/iotaledger/wasp/packages/vm/core/evm" @@ -408,7 +409,7 @@ func tryGetRevertError(res *core.ExecutionResult) error { return nil } if len(res.Revert()) > 0 { - return vm.ErrEVMExecutionReverted.Create(hex.EncodeToString(res.Revert())) + return iscvm.ErrEVMExecutionReverted.Create(hex.EncodeToString(res.Revert())) } return res.Err } @@ -472,7 +473,7 @@ func AddDummyTxWithTransferEvents( toAddress common.Address, assets *isc.Assets, txData []byte, - reuseCurrentTxContext bool, + isInRequestContext bool, ) { zeroAddress := common.Address{} logs := makeTransferEvents(ctx, zeroAddress, toAddress, assets) @@ -510,9 +511,33 @@ func AddDummyTxWithTransferEvents( } receipt.Bloom = types.CreateBloom(types.Receipts{receipt}) - if !reuseCurrentTxContext { + callTracerHooks := func() { + tracer := ctx.EVMTracer() + if tracer != nil { + if tracer.Hooks.OnTxStart != nil { + tracer.Hooks.OnTxStart(nil, tx, common.Address{}) + } + if tracer.Hooks.OnEnter != nil { + tracer.Hooks.OnEnter(0, byte(vm.CALL), common.Address{}, toAddress, txData, 0, wei) + } + if tracer.Hooks.OnLog != nil { + for _, log := range logs { + tracer.Hooks.OnLog(log) + } + } + if tracer.Hooks.OnExit != nil { + tracer.Hooks.OnExit(0, nil, receipt.GasUsed, nil, false) + } + if tracer.Hooks.OnTxEnd != nil { + tracer.Hooks.OnTxEnd(receipt, nil) + } + } + } + + if !isInRequestContext { // called from outside vmrun, just add the tx without a gas value createBlockchainDB(ctx.State(), chainInfo).AddTransaction(tx, receipt) + callTracerHooks() return } @@ -524,6 +549,7 @@ func AddDummyTxWithTransferEvents( blockchainDB := createBlockchainDB(evmPartition, chainInfo) receipt.CumulativeGasUsed = blockchainDB.GetPendingCumulativeGasUsed() + receipt.GasUsed blockchainDB.AddTransaction(tx, receipt) + callTracerHooks() }) } diff --git a/packages/vm/core/evm/evmimpl/internal.go b/packages/vm/core/evm/evmimpl/internal.go index c0a4bd47f6..1a70774df9 100644 --- a/packages/vm/core/evm/evmimpl/internal.go +++ b/packages/vm/core/evm/evmimpl/internal.go @@ -36,18 +36,7 @@ func getTracer(ctx isc.Sandbox) *tracing.Hooks { if tracer == nil { return nil } - - // if block number is set and the TxIndex is null, we're tracing the whole block - if tracer.TxIndex == nil && tracer.BlockNumber != nil { - return tracer.Tracer.Hooks - } - - // if tx index is set, we're tracing a specific transaction - if tracer.TxIndex != nil && *tracer.TxIndex == uint64(ctx.RequestIndex()) { - return tracer.Tracer.Hooks - } - - return nil + return tracer.Hooks } func createEmulator(ctx isc.Sandbox) *emulator.EVMEmulator { diff --git a/packages/vm/vmimpl/sandbox.go b/packages/vm/vmimpl/sandbox.go index c2b5d8534b..fe4918c9d7 100644 --- a/packages/vm/vmimpl/sandbox.go +++ b/packages/vm/vmimpl/sandbox.go @@ -6,6 +6,8 @@ package vmimpl import ( "math/big" + "github.com/ethereum/go-ethereum/eth/tracers" + iotago "github.com/iotaledger/iota.go/v3" "github.com/iotaledger/wasp/packages/hashing" "github.com/iotaledger/wasp/packages/isc" @@ -99,7 +101,7 @@ func (s *contractSandbox) RegisterError(messageFormat string) *isc.VMErrorTempla return s.reqctx.registerError(messageFormat) } -func (s *contractSandbox) EVMTracer() *isc.EVMTracer { +func (s *contractSandbox) EVMTracer() *tracers.Tracer { return s.reqctx.vm.task.EVMTracer } diff --git a/packages/vm/vmimpl/vmrun_test.go b/packages/vm/vmimpl/vmrun_test.go index 522b1388b4..1f43fab447 100644 --- a/packages/vm/vmimpl/vmrun_test.go +++ b/packages/vm/vmimpl/vmrun_test.go @@ -101,7 +101,6 @@ func simulateRunOutput(t *testing.T, output iotago.Output) *vm.VMTaskResult { Entropy: [32]byte{}, ValidatorFeeTarget: nil, EstimateGasMode: false, - EVMTracer: &isc.EVMTracer{}, EnableGasBurnLogging: false, Migrations: &migrations.MigrationScheme{}, Log: testlogger.NewLogger(t), diff --git a/packages/vm/vmtask.go b/packages/vm/vmtask.go index d9fd9e2ef2..7950b3d7cc 100644 --- a/packages/vm/vmtask.go +++ b/packages/vm/vmtask.go @@ -3,6 +3,8 @@ package vm import ( "time" + "github.com/ethereum/go-ethereum/eth/tracers" + "github.com/iotaledger/hive.go/logger" iotago "github.com/iotaledger/iota.go/v3" "github.com/iotaledger/wasp/packages/hashing" @@ -27,10 +29,8 @@ type VMTask struct { Entropy hashing.HashValue ValidatorFeeTarget isc.AgentID // If EstimateGasMode is enabled, signature and nonce checks will be skipped - EstimateGasMode bool - // If EVMTracer is set, all requests will be executed normally up until the EVM - // tx with the given index, which will then be executed with the given tracer. - EVMTracer *isc.EVMTracer + EstimateGasMode bool + EVMTracer *tracers.Tracer EnableGasBurnLogging bool // for testing and Solo only Migrations *migrations.MigrationScheme // for testing and Solo only