From eb2d9be1a77f6073cb637bab77777b7ea7d197e0 Mon Sep 17 00:00:00 2001 From: Alex Forshtat Date: Wed, 30 Oct 2024 13:55:06 +0100 Subject: [PATCH 01/25] WIP: include ERC-7562 tracer --- eth/tracers/native/erc7562.go | 270 ++++++++++++++++++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 eth/tracers/native/erc7562.go diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go new file mode 100644 index 000000000000..01c68a69a5bf --- /dev/null +++ b/eth/tracers/native/erc7562.go @@ -0,0 +1,270 @@ +// Copyright 2021 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package native + +import ( + "encoding/json" + "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/vm" + "github.com/ethereum/go-ethereum/eth/tracers" + "github.com/holiman/uint256" + "math/big" + "regexp" + "strings" +) + +//go:generate go run github.com/fjl/gencodec -type callFrame -field-override callFrameMarshaling -out gen_callframe_json.go + +func init() { + tracers.DefaultDirectory.Register("callTracerWithOpcodes", newCallTracerWithOpcodes, false) +} + +type callFrameWithOpcodes struct { + callFrame + AccessedSlots accessedSlots `json:"accessedSlots"` + ExtCodeAccessInfo []common.Address `json:"extCodeAccessInfo"` + DeployedContracts []common.Address `json:"deployedContracts"` + UsedOpcodes map[string]bool `json:"usedOpcodes"` + GasObserved bool `json:"gasObserved"` + ContractSize map[common.Address]int `json:"contractSize"` + OutOfGas bool `json:"outOfGas"` + Calls []callFrameWithOpcodes `json:"calls,omitempty" rlp:"optional"` +} + +// TODO: I suggest that we provide an `[]string` for all of these fields. Doing it for `Reads` as an example here. +type accessedSlots struct { + Reads map[string][]string `json:"reads"` + Writes map[string]uint64 `json:"writes"` + TransientReads map[string]uint64 `json:"transientReads"` + TransientWrites map[string]uint64 `json:"transientWrites"` +} + +type callTracerWithOpcodes struct { + callTracer + env *tracing.VMContext + + allowedOpcodeRegex *regexp.Regexp + lastOp string + callstack []callFrameWithOpcodes + lastThreeOpCodes []string + Keccak []hexutil.Bytes `json:"keccak"` +} + +// newCallTracer returns a native go tracer which tracks +// call frames of a tx, and implements vm.EVMLogger. +func newCallTracerWithOpcodes(ctx *tracers.Context, cfg json.RawMessage /*, chainConfig *params.ChainConfig*/) (*tracers.Tracer, error) { + t, err := newCallTracerObjectWithOpcodes(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, + }, + GetResult: t.GetResult, + Stop: t.Stop, + }, nil +} + +func newCallTracerObjectWithOpcodes(ctx *tracers.Context, cfg json.RawMessage) (*callTracer, error) { + var config callTracerConfig + if err := json.Unmarshal(cfg, &config); err != nil { + return nil, err + } + // First callframe contains tx context info + // and is populated on start and end. + return &callTracer{callstack: make([]callFrame, 0, 1), config: config}, nil +} + +// OnEnter is called when EVM enters a new scope (via call, create or selfdestruct). +func (t *callTracerWithOpcodes) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + t.depth = depth + if t.config.OnlyTopCall && depth > 0 { + return + } + // Skip if tracing was interrupted + if t.interrupt.Load() { + return + } + + toCopy := to + call := callFrameWithOpcodes{ + callFrame: callFrame{ + Type: vm.OpCode(typ), + From: from, + To: &toCopy, + Input: common.CopyBytes(input), + Gas: gas, + Value: value}, + AccessedSlots: accessedSlots{ + Reads: map[string][]string{}, + Writes: map[string]uint64{}, + TransientReads: map[string]uint64{}, + TransientWrites: map[string]uint64{}, + }, + UsedOpcodes: make(map[string]bool), + } + if depth == 0 { + call.Gas = t.gasLimit + } + t.callstack = append(t.callstack, call) +} + +func (t *callTracerWithOpcodes) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { + opcode := vm.OpCode(op).String() + + stackSize := len(scope.StackData()) + stackTop3 := partialStack{} + for i := 0; i < 3 && i < stackSize; i++ { + stackTop3 = append(stackTop3, peepStack(scope.StackData(), i)) + } + t.lastThreeOpCodes = append(t.lastThreeOpCodes, opcode) + if len(t.lastThreeOpCodes) > 3 { + t.lastThreeOpCodes = t.lastThreeOpCodes[1:] + } + + size := len(t.callstack) + currentCallFrame := t.callstack[size-1] + + t.detectOutOfGas(gas, cost, opcode, currentCallFrame) + t.handleExtOpcodes(opcode, currentCallFrame, stackTop3) + t.handleAccessedContractSize(opcode, scope, currentCallFrame) + t.handleGasObserved(opcode, currentCallFrame) + t.storeUsedOpcode(opcode, currentCallFrame) + t.handleStorageAccess(opcode, scope, currentCallFrame) + t.storeKeccak(opcode, scope) + t.lastOp = opcode +} + +func (t *callTracerWithOpcodes) handleGasObserved(opcode string, currentCallFrame callFrameWithOpcodes) { + // [OP-012] + pendingGasObserved := t.lastOp == "GAS" && !strings.Contains(opcode, "CALL") + if pendingGasObserved { + currentCallFrame.GasObserved = true + } +} + +func (t *callTracerWithOpcodes) storeUsedOpcode(opcode string, currentCallFrame callFrameWithOpcodes) { + // ignore "unimportant" opcodes + if opcode != "GAS" && !t.allowedOpcodeRegex.MatchString(opcode) { + currentCallFrame.UsedOpcodes[opcode] = true + } +} + +func (t *callTracerWithOpcodes) handleStorageAccess(opcode string, scope tracing.OpContext, currentCallFrame callFrameWithOpcodes) { + if opcode == "SLOAD" || opcode == "SSTORE" || opcode == "TLOAD" || opcode == "TSTORE" { + slot := common.BytesToHash(peepStack(scope.StackData(), 0).Bytes()) + slotHex := slot.Hex() + addr := scope.Address() + + if opcode == "SLOAD" { + // read slot values before this UserOp was created + // (so saving it if it was written before the first read) + _, rOk := currentCallFrame.AccessedSlots.Reads[slotHex] + _, wOk := currentCallFrame.AccessedSlots.Writes[slotHex] + if !rOk && !wOk { + currentCallFrame.AccessedSlots.Reads[slotHex] = append(currentCallFrame.AccessedSlots.Reads[slotHex], t.env.StateDB.GetState(addr, slot).Hex()) + } + } else if opcode == "SSTORE" { + incrementCount(currentCallFrame.AccessedSlots.Writes, slotHex) + } else if opcode == "TLOAD" { + incrementCount(currentCallFrame.AccessedSlots.TransientReads, slotHex) + } else if opcode == "TSTORE" { + incrementCount(currentCallFrame.AccessedSlots.TransientWrites, slotHex) + } + } +} + +func (t *callTracerWithOpcodes) storeKeccak(opcode string, scope tracing.OpContext) { + if opcode == "KECCAK256" { + dataOffset := peepStack(scope.StackData(), 0).Uint64() + dataLength := peepStack(scope.StackData(), 1).Uint64() + memory := scope.MemoryData() + keccak := make([]byte, dataLength) + copy(keccak, memory[dataOffset:dataOffset+dataLength]) + t.Keccak = append(t.Keccak, keccak) + } +} + +func (t *callTracerWithOpcodes) detectOutOfGas(gas uint64, cost uint64, opcode string, currentCallFrame callFrameWithOpcodes) { + if gas < cost || (opcode == "SSTORE" && gas < 2300) { + currentCallFrame.OutOfGas = true + } +} + +func (t *callTracerWithOpcodes) handleExtOpcodes(opcode string, currentCallFrame callFrameWithOpcodes, stackTop3 partialStack) { + if strings.HasPrefix(opcode, "EXT") { + addr := common.HexToAddress(stackTop3[0].Hex()) + ops := []string{} + for _, item := range t.lastThreeOpCodes { + ops = append(ops, item) + } + last3OpcodeStr := strings.Join(ops, ",") + + // only store the last EXTCODE* opcode per address - could even be a boolean for our current use-case + // [OP-051] + if !strings.Contains(last3OpcodeStr, ",EXTCODESIZE,ISZERO") { + currentCallFrame.ExtCodeAccessInfo = append(currentCallFrame.ExtCodeAccessInfo, addr) + } + } +} +func (t *callTracerWithOpcodes) handleAccessedContractSize(opcode string, scope tracing.OpContext, currentCallFrame callFrameWithOpcodes) { + // [OP-041] + if isEXTorCALL(opcode) { + n := 0 + if !strings.HasPrefix(opcode, "EXT") { + n = 1 + } + addr := common.BytesToAddress(peepStack(scope.StackData(), n).Bytes()) + + if _, ok := currentCallFrame.ContractSize[addr]; !ok && !isAllowedPrecompile(addr) { + currentCallFrame.ContractSize[addr] = len(t.env.StateDB.GetCode(addr)) + } + } +} + +func peepStack(stackData []uint256.Int, n int) *uint256.Int { + return &stackData[len(stackData)-n-1] +} + +func isEXTorCALL(opcode string) bool { + return strings.HasPrefix(opcode, "EXT") || + opcode == "CALL" || + opcode == "CALLCODE" || + opcode == "DELEGATECALL" || + opcode == "STATICCALL" +} + +// not using 'isPrecompiled' to only allow the ones defined by the ERC-7562 as stateless precompiles +// [OP-062] +func isAllowedPrecompile(addr common.Address) bool { + addrInt := addr.Big() + return addrInt.Cmp(big.NewInt(0)) == 1 && addrInt.Cmp(big.NewInt(10)) == -1 +} + +func incrementCount(m map[string]uint64, k string) { + if _, ok := m[k]; !ok { + m[k] = 0 + } + m[k]++ +} From 0ca4df965277dad629bbea07b531127db1bd6696 Mon Sep 17 00:00:00 2001 From: Alex Forshtat Date: Wed, 30 Oct 2024 14:05:01 +0100 Subject: [PATCH 02/25] Remove checking the 'OnlyTopCall' as it doesn't fit the use-case --- eth/tracers/native/erc7562.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index 01c68a69a5bf..d49dfbcb261c 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -99,9 +99,6 @@ func newCallTracerObjectWithOpcodes(ctx *tracers.Context, cfg json.RawMessage) ( // OnEnter is called when EVM enters a new scope (via call, create or selfdestruct). func (t *callTracerWithOpcodes) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { t.depth = depth - if t.config.OnlyTopCall && depth > 0 { - return - } // Skip if tracing was interrupted if t.interrupt.Load() { return From cde93e706aa2d17e45ac0111239edb6a41c50b04 Mon Sep 17 00:00:00 2001 From: Alex Forshtat Date: Wed, 30 Oct 2024 14:16:27 +0100 Subject: [PATCH 03/25] Remove most of the string-based opcode manipulation --- eth/tracers/native/erc7562.go | 62 ++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index d49dfbcb261c..6ddda14a21f8 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -40,7 +40,7 @@ type callFrameWithOpcodes struct { AccessedSlots accessedSlots `json:"accessedSlots"` ExtCodeAccessInfo []common.Address `json:"extCodeAccessInfo"` DeployedContracts []common.Address `json:"deployedContracts"` - UsedOpcodes map[string]bool `json:"usedOpcodes"` + UsedOpcodes map[vm.OpCode]bool `json:"usedOpcodes"` GasObserved bool `json:"gasObserved"` ContractSize map[common.Address]int `json:"contractSize"` OutOfGas bool `json:"outOfGas"` @@ -59,10 +59,11 @@ type callTracerWithOpcodes struct { callTracer env *tracing.VMContext + // TODO: remove regex based code allowedOpcodeRegex *regexp.Regexp - lastOp string + lastOp vm.OpCode callstack []callFrameWithOpcodes - lastThreeOpCodes []string + lastThreeOpCodes []vm.OpCode Keccak []hexutil.Bytes `json:"keccak"` } @@ -119,7 +120,7 @@ func (t *callTracerWithOpcodes) OnEnter(depth int, typ byte, from common.Address TransientReads: map[string]uint64{}, TransientWrites: map[string]uint64{}, }, - UsedOpcodes: make(map[string]bool), + UsedOpcodes: make(map[vm.OpCode]bool), } if depth == 0 { call.Gas = t.gasLimit @@ -128,7 +129,7 @@ func (t *callTracerWithOpcodes) OnEnter(depth int, typ byte, from common.Address } func (t *callTracerWithOpcodes) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { - opcode := vm.OpCode(op).String() + opcode := vm.OpCode(op) stackSize := len(scope.StackData()) stackTop3 := partialStack{} @@ -153,28 +154,28 @@ func (t *callTracerWithOpcodes) OnOpcode(pc uint64, op byte, gas, cost uint64, s t.lastOp = opcode } -func (t *callTracerWithOpcodes) handleGasObserved(opcode string, currentCallFrame callFrameWithOpcodes) { +func (t *callTracerWithOpcodes) handleGasObserved(opcode vm.OpCode, currentCallFrame callFrameWithOpcodes) { // [OP-012] - pendingGasObserved := t.lastOp == "GAS" && !strings.Contains(opcode, "CALL") + pendingGasObserved := t.lastOp == vm.GAS && !strings.Contains(opcode.String(), "CALL") if pendingGasObserved { currentCallFrame.GasObserved = true } } -func (t *callTracerWithOpcodes) storeUsedOpcode(opcode string, currentCallFrame callFrameWithOpcodes) { +func (t *callTracerWithOpcodes) storeUsedOpcode(opcode vm.OpCode, currentCallFrame callFrameWithOpcodes) { // ignore "unimportant" opcodes - if opcode != "GAS" && !t.allowedOpcodeRegex.MatchString(opcode) { + if opcode != vm.GAS && !t.allowedOpcodeRegex.MatchString(opcode.String()) { currentCallFrame.UsedOpcodes[opcode] = true } } -func (t *callTracerWithOpcodes) handleStorageAccess(opcode string, scope tracing.OpContext, currentCallFrame callFrameWithOpcodes) { - if opcode == "SLOAD" || opcode == "SSTORE" || opcode == "TLOAD" || opcode == "TSTORE" { +func (t *callTracerWithOpcodes) handleStorageAccess(opcode vm.OpCode, scope tracing.OpContext, currentCallFrame callFrameWithOpcodes) { + if opcode == vm.SLOAD || opcode == vm.SSTORE || opcode == vm.TLOAD || opcode == vm.TSTORE { slot := common.BytesToHash(peepStack(scope.StackData(), 0).Bytes()) slotHex := slot.Hex() addr := scope.Address() - if opcode == "SLOAD" { + if opcode == vm.SLOAD { // read slot values before this UserOp was created // (so saving it if it was written before the first read) _, rOk := currentCallFrame.AccessedSlots.Reads[slotHex] @@ -182,18 +183,18 @@ func (t *callTracerWithOpcodes) handleStorageAccess(opcode string, scope tracing if !rOk && !wOk { currentCallFrame.AccessedSlots.Reads[slotHex] = append(currentCallFrame.AccessedSlots.Reads[slotHex], t.env.StateDB.GetState(addr, slot).Hex()) } - } else if opcode == "SSTORE" { + } else if opcode == vm.SSTORE { incrementCount(currentCallFrame.AccessedSlots.Writes, slotHex) - } else if opcode == "TLOAD" { + } else if opcode == vm.TLOAD { incrementCount(currentCallFrame.AccessedSlots.TransientReads, slotHex) - } else if opcode == "TSTORE" { + } else if opcode == vm.TSTORE { incrementCount(currentCallFrame.AccessedSlots.TransientWrites, slotHex) } } } -func (t *callTracerWithOpcodes) storeKeccak(opcode string, scope tracing.OpContext) { - if opcode == "KECCAK256" { +func (t *callTracerWithOpcodes) storeKeccak(opcode vm.OpCode, scope tracing.OpContext) { + if opcode == vm.KECCAK256 { dataOffset := peepStack(scope.StackData(), 0).Uint64() dataLength := peepStack(scope.StackData(), 1).Uint64() memory := scope.MemoryData() @@ -203,18 +204,19 @@ func (t *callTracerWithOpcodes) storeKeccak(opcode string, scope tracing.OpConte } } -func (t *callTracerWithOpcodes) detectOutOfGas(gas uint64, cost uint64, opcode string, currentCallFrame callFrameWithOpcodes) { - if gas < cost || (opcode == "SSTORE" && gas < 2300) { +func (t *callTracerWithOpcodes) detectOutOfGas(gas uint64, cost uint64, opcode vm.OpCode, currentCallFrame callFrameWithOpcodes) { + if gas < cost || (opcode == vm.SSTORE && gas < 2300) { currentCallFrame.OutOfGas = true } } -func (t *callTracerWithOpcodes) handleExtOpcodes(opcode string, currentCallFrame callFrameWithOpcodes, stackTop3 partialStack) { - if strings.HasPrefix(opcode, "EXT") { +// TODO: rewrite using byte opcode values, without relying on string manipulations +func (t *callTracerWithOpcodes) handleExtOpcodes(opcode vm.OpCode, currentCallFrame callFrameWithOpcodes, stackTop3 partialStack) { + if strings.HasPrefix(opcode.String(), "EXT") { addr := common.HexToAddress(stackTop3[0].Hex()) ops := []string{} for _, item := range t.lastThreeOpCodes { - ops = append(ops, item) + ops = append(ops, item.String()) } last3OpcodeStr := strings.Join(ops, ",") @@ -225,11 +227,11 @@ func (t *callTracerWithOpcodes) handleExtOpcodes(opcode string, currentCallFrame } } } -func (t *callTracerWithOpcodes) handleAccessedContractSize(opcode string, scope tracing.OpContext, currentCallFrame callFrameWithOpcodes) { +func (t *callTracerWithOpcodes) handleAccessedContractSize(opcode vm.OpCode, scope tracing.OpContext, currentCallFrame callFrameWithOpcodes) { // [OP-041] if isEXTorCALL(opcode) { n := 0 - if !strings.HasPrefix(opcode, "EXT") { + if !strings.HasPrefix(opcode.String(), "EXT") { n = 1 } addr := common.BytesToAddress(peepStack(scope.StackData(), n).Bytes()) @@ -244,12 +246,12 @@ func peepStack(stackData []uint256.Int, n int) *uint256.Int { return &stackData[len(stackData)-n-1] } -func isEXTorCALL(opcode string) bool { - return strings.HasPrefix(opcode, "EXT") || - opcode == "CALL" || - opcode == "CALLCODE" || - opcode == "DELEGATECALL" || - opcode == "STATICCALL" +func isEXTorCALL(opcode vm.OpCode) bool { + return strings.HasPrefix(opcode.String(), "EXT") || + opcode == vm.CALL || + opcode == vm.CALLCODE || + opcode == vm.DELEGATECALL || + opcode == vm.STATICCALL } // not using 'isPrecompiled' to only allow the ones defined by the ERC-7562 as stateless precompiles From 032fc39769f4f2e08ea714a946b586c3d69c4cda Mon Sep 17 00:00:00 2001 From: Alex Forshtat Date: Wed, 30 Oct 2024 14:22:16 +0100 Subject: [PATCH 04/25] Fix --- eth/tracers/native/erc7562.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index 6ddda14a21f8..5834f7c3b6c3 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -89,8 +89,10 @@ func newCallTracerWithOpcodes(ctx *tracers.Context, cfg json.RawMessage /*, chai func newCallTracerObjectWithOpcodes(ctx *tracers.Context, cfg json.RawMessage) (*callTracer, error) { var config callTracerConfig - if err := json.Unmarshal(cfg, &config); err != nil { - return nil, err + if cfg != nil { + if err := json.Unmarshal(cfg, &config); err != nil { + return nil, err + } } // First callframe contains tx context info // and is populated on start and end. From a882a69de68235f8aaf3fdbdedd6d5b9aa283021 Mon Sep 17 00:00:00 2001 From: Alex Forshtat Date: Wed, 30 Oct 2024 14:24:06 +0100 Subject: [PATCH 05/25] Construct the correct tracer object --- eth/tracers/native/erc7562.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index 5834f7c3b6c3..94105536ed92 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -87,7 +87,7 @@ func newCallTracerWithOpcodes(ctx *tracers.Context, cfg json.RawMessage /*, chai }, nil } -func newCallTracerObjectWithOpcodes(ctx *tracers.Context, cfg json.RawMessage) (*callTracer, error) { +func newCallTracerObjectWithOpcodes(ctx *tracers.Context, cfg json.RawMessage) (*callTracerWithOpcodes, error) { var config callTracerConfig if cfg != nil { if err := json.Unmarshal(cfg, &config); err != nil { @@ -96,7 +96,7 @@ func newCallTracerObjectWithOpcodes(ctx *tracers.Context, cfg json.RawMessage) ( } // First callframe contains tx context info // and is populated on start and end. - return &callTracer{callstack: make([]callFrame, 0, 1), config: config}, nil + return &callTracerWithOpcodes{callstack: make([]callFrameWithOpcodes, 0, 1), callTracer: callTracer{config: config}}, nil } // OnEnter is called when EVM enters a new scope (via call, create or selfdestruct). From 6dbd029f6fdf2b2aa08cc99cfe60b0161f2b5c1a Mon Sep 17 00:00:00 2001 From: Alex Forshtat Date: Wed, 30 Oct 2024 14:33:43 +0100 Subject: [PATCH 06/25] Bring in 'OnTxEnd' and 'GetResult' functions --- eth/tracers/native/erc7562.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index 94105536ed92..e00879169884 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -18,9 +18,11 @@ package native import ( "encoding/json" + "errors" "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/eth/tracers" "github.com/holiman/uint256" @@ -130,6 +132,32 @@ func (t *callTracerWithOpcodes) OnEnter(depth int, typ byte, from common.Address t.callstack = append(t.callstack, call) } +func (t *callTracerWithOpcodes) OnTxEnd(receipt *types.Receipt, err error) { + // Error happened during tx validation. + if err != nil { + return + } + t.callstack[0].GasUsed = receipt.GasUsed + if t.config.WithLog { + // Logs are not emitted when the call fails + clearFailedLogs(&t.callstack[0].callFrame, false) + } +} + +// GetResult returns the json-encoded nested list of call traces, and any +// error arising from the encoding or forceful termination (via `Stop`). +func (t *callTracerWithOpcodes) GetResult() (json.RawMessage, error) { + if len(t.callstack) != 1 { + return nil, errors.New("incorrect number of top-level calls") + } + + res, err := json.Marshal(t.callstack[0]) + if err != nil { + return nil, err + } + return res, t.reason +} + func (t *callTracerWithOpcodes) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { opcode := vm.OpCode(op) From 8984d95c46714827fcc99ca3976fe9ed59e6cce5 Mon Sep 17 00:00:00 2001 From: Alex Forshtat Date: Wed, 30 Oct 2024 14:48:47 +0100 Subject: [PATCH 07/25] Missing 'OnOpcode' hook --- eth/tracers/native/erc7562.go | 1 + 1 file changed, 1 insertion(+) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index e00879169884..476b577383c5 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -79,6 +79,7 @@ func newCallTracerWithOpcodes(ctx *tracers.Context, cfg json.RawMessage /*, chai return &tracers.Tracer{ Hooks: &tracing.Hooks{ OnTxStart: t.OnTxStart, + OnOpcode: t.OnOpcode, OnTxEnd: t.OnTxEnd, OnEnter: t.OnEnter, OnExit: t.OnExit, From 7a1230ee993ac50b993d20b6b6334cd688f97b55 Mon Sep 17 00:00:00 2001 From: shahafn Date: Wed, 30 Oct 2024 16:20:53 +0200 Subject: [PATCH 08/25] native bundlerCollectorTracer Rename to erc7562Tracer --- eth/tracers/native/erc7562.go | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index 476b577383c5..2a72a6bb09e1 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -34,7 +34,7 @@ import ( //go:generate go run github.com/fjl/gencodec -type callFrame -field-override callFrameMarshaling -out gen_callframe_json.go func init() { - tracers.DefaultDirectory.Register("callTracerWithOpcodes", newCallTracerWithOpcodes, false) + tracers.DefaultDirectory.Register("erc7562Tracer", newErc7562Tracer, false) } type callFrameWithOpcodes struct { @@ -57,7 +57,7 @@ type accessedSlots struct { TransientWrites map[string]uint64 `json:"transientWrites"` } -type callTracerWithOpcodes struct { +type erc7562Tracer struct { callTracer env *tracing.VMContext @@ -71,8 +71,8 @@ type callTracerWithOpcodes struct { // newCallTracer returns a native go tracer which tracks // call frames of a tx, and implements vm.EVMLogger. -func newCallTracerWithOpcodes(ctx *tracers.Context, cfg json.RawMessage /*, chainConfig *params.ChainConfig*/) (*tracers.Tracer, error) { - t, err := newCallTracerObjectWithOpcodes(ctx, cfg) +func newErc7562Tracer(ctx *tracers.Context, cfg json.RawMessage /*, chainConfig *params.ChainConfig*/) (*tracers.Tracer, error) { + t, err := newErc7562TracerObject(ctx, cfg) if err != nil { return nil, err } @@ -90,7 +90,7 @@ func newCallTracerWithOpcodes(ctx *tracers.Context, cfg json.RawMessage /*, chai }, nil } -func newCallTracerObjectWithOpcodes(ctx *tracers.Context, cfg json.RawMessage) (*callTracerWithOpcodes, error) { +func newErc7562TracerObject(ctx *tracers.Context, cfg json.RawMessage) (*erc7562Tracer, error) { var config callTracerConfig if cfg != nil { if err := json.Unmarshal(cfg, &config); err != nil { @@ -99,11 +99,11 @@ func newCallTracerObjectWithOpcodes(ctx *tracers.Context, cfg json.RawMessage) ( } // First callframe contains tx context info // and is populated on start and end. - return &callTracerWithOpcodes{callstack: make([]callFrameWithOpcodes, 0, 1), callTracer: callTracer{config: config}}, nil + return &erc7562Tracer{callstack: make([]callFrameWithOpcodes, 0, 1), callTracer: callTracer{config: config}}, nil } // OnEnter is called when EVM enters a new scope (via call, create or selfdestruct). -func (t *callTracerWithOpcodes) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { +func (t *erc7562Tracer) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { t.depth = depth // Skip if tracing was interrupted if t.interrupt.Load() { @@ -133,7 +133,7 @@ func (t *callTracerWithOpcodes) OnEnter(depth int, typ byte, from common.Address t.callstack = append(t.callstack, call) } -func (t *callTracerWithOpcodes) OnTxEnd(receipt *types.Receipt, err error) { +func (t *erc7562Tracer) OnTxEnd(receipt *types.Receipt, err error) { // Error happened during tx validation. if err != nil { return @@ -147,7 +147,7 @@ func (t *callTracerWithOpcodes) 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 *callTracerWithOpcodes) GetResult() (json.RawMessage, error) { +func (t *erc7562Tracer) GetResult() (json.RawMessage, error) { if len(t.callstack) != 1 { return nil, errors.New("incorrect number of top-level calls") } @@ -159,7 +159,7 @@ func (t *callTracerWithOpcodes) GetResult() (json.RawMessage, error) { return res, t.reason } -func (t *callTracerWithOpcodes) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { +func (t *erc7562Tracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { opcode := vm.OpCode(op) stackSize := len(scope.StackData()) @@ -185,7 +185,7 @@ func (t *callTracerWithOpcodes) OnOpcode(pc uint64, op byte, gas, cost uint64, s t.lastOp = opcode } -func (t *callTracerWithOpcodes) handleGasObserved(opcode vm.OpCode, currentCallFrame callFrameWithOpcodes) { +func (t *erc7562Tracer) handleGasObserved(opcode vm.OpCode, currentCallFrame callFrameWithOpcodes) { // [OP-012] pendingGasObserved := t.lastOp == vm.GAS && !strings.Contains(opcode.String(), "CALL") if pendingGasObserved { @@ -193,14 +193,14 @@ func (t *callTracerWithOpcodes) handleGasObserved(opcode vm.OpCode, currentCallF } } -func (t *callTracerWithOpcodes) storeUsedOpcode(opcode vm.OpCode, currentCallFrame callFrameWithOpcodes) { +func (t *erc7562Tracer) storeUsedOpcode(opcode vm.OpCode, currentCallFrame callFrameWithOpcodes) { // ignore "unimportant" opcodes if opcode != vm.GAS && !t.allowedOpcodeRegex.MatchString(opcode.String()) { currentCallFrame.UsedOpcodes[opcode] = true } } -func (t *callTracerWithOpcodes) handleStorageAccess(opcode vm.OpCode, scope tracing.OpContext, currentCallFrame callFrameWithOpcodes) { +func (t *erc7562Tracer) handleStorageAccess(opcode vm.OpCode, scope tracing.OpContext, currentCallFrame callFrameWithOpcodes) { if opcode == vm.SLOAD || opcode == vm.SSTORE || opcode == vm.TLOAD || opcode == vm.TSTORE { slot := common.BytesToHash(peepStack(scope.StackData(), 0).Bytes()) slotHex := slot.Hex() @@ -224,7 +224,7 @@ func (t *callTracerWithOpcodes) handleStorageAccess(opcode vm.OpCode, scope trac } } -func (t *callTracerWithOpcodes) storeKeccak(opcode vm.OpCode, scope tracing.OpContext) { +func (t *erc7562Tracer) storeKeccak(opcode vm.OpCode, scope tracing.OpContext) { if opcode == vm.KECCAK256 { dataOffset := peepStack(scope.StackData(), 0).Uint64() dataLength := peepStack(scope.StackData(), 1).Uint64() @@ -235,14 +235,14 @@ func (t *callTracerWithOpcodes) storeKeccak(opcode vm.OpCode, scope tracing.OpCo } } -func (t *callTracerWithOpcodes) detectOutOfGas(gas uint64, cost uint64, opcode vm.OpCode, currentCallFrame callFrameWithOpcodes) { +func (t *erc7562Tracer) detectOutOfGas(gas uint64, cost uint64, opcode vm.OpCode, currentCallFrame callFrameWithOpcodes) { if gas < cost || (opcode == vm.SSTORE && gas < 2300) { currentCallFrame.OutOfGas = true } } // TODO: rewrite using byte opcode values, without relying on string manipulations -func (t *callTracerWithOpcodes) handleExtOpcodes(opcode vm.OpCode, currentCallFrame callFrameWithOpcodes, stackTop3 partialStack) { +func (t *erc7562Tracer) handleExtOpcodes(opcode vm.OpCode, currentCallFrame callFrameWithOpcodes, stackTop3 partialStack) { if strings.HasPrefix(opcode.String(), "EXT") { addr := common.HexToAddress(stackTop3[0].Hex()) ops := []string{} @@ -258,7 +258,7 @@ func (t *callTracerWithOpcodes) handleExtOpcodes(opcode vm.OpCode, currentCallFr } } } -func (t *callTracerWithOpcodes) handleAccessedContractSize(opcode vm.OpCode, scope tracing.OpContext, currentCallFrame callFrameWithOpcodes) { +func (t *erc7562Tracer) handleAccessedContractSize(opcode vm.OpCode, scope tracing.OpContext, currentCallFrame callFrameWithOpcodes) { // [OP-041] if isEXTorCALL(opcode) { n := 0 From 82019debabf696073e4b7781ea52a51ff4c141cb Mon Sep 17 00:00:00 2001 From: shahafn Date: Wed, 30 Oct 2024 22:48:11 +0200 Subject: [PATCH 09/25] Fixing tracer wip --- eth/tracers/native/erc7562.go | 141 +++++++++++++++++++++++++++++----- 1 file changed, 122 insertions(+), 19 deletions(-) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index 2a72a6bb09e1..20fe7a2f6826 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -25,9 +25,9 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/eth/tracers" + "github.com/ethereum/go-ethereum/log" "github.com/holiman/uint256" "math/big" - "regexp" "strings" ) @@ -61,12 +61,10 @@ type erc7562Tracer struct { callTracer env *tracing.VMContext - // TODO: remove regex based code - allowedOpcodeRegex *regexp.Regexp - lastOp vm.OpCode - callstack []callFrameWithOpcodes - lastThreeOpCodes []vm.OpCode - Keccak []hexutil.Bytes `json:"keccak"` + lastOp vm.OpCode + callstackWithOpcodes []callFrameWithOpcodes + lastThreeOpCodes []vm.OpCode + Keccak []hexutil.Bytes `json:"keccak"` } // newCallTracer returns a native go tracer which tracks @@ -99,7 +97,12 @@ func newErc7562TracerObject(ctx *tracers.Context, cfg json.RawMessage) (*erc7562 } // First callframe contains tx context info // and is populated on start and end. - return &erc7562Tracer{callstack: make([]callFrameWithOpcodes, 0, 1), callTracer: callTracer{config: config}}, nil + return &erc7562Tracer{callstackWithOpcodes: make([]callFrameWithOpcodes, 0, 1), callTracer: callTracer{config: config}}, nil +} + +func (t *erc7562Tracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) { + t.env = env + t.gasLimit = tx.Gas() } // OnEnter is called when EVM enters a new scope (via call, create or selfdestruct). @@ -125,12 +128,48 @@ func (t *erc7562Tracer) OnEnter(depth int, typ byte, from common.Address, to com TransientReads: map[string]uint64{}, TransientWrites: map[string]uint64{}, }, - UsedOpcodes: make(map[vm.OpCode]bool), + UsedOpcodes: make(map[vm.OpCode]bool, 3), + ContractSize: make(map[common.Address]int, 1), } if depth == 0 { call.Gas = t.gasLimit } - t.callstack = append(t.callstack, call) + t.callstackWithOpcodes = append(t.callstackWithOpcodes, call) +} + +func (t *erc7562Tracer) captureEnd(output []byte, gasUsed uint64, err error, reverted bool) { + if len(t.callstackWithOpcodes) != 1 { + return + } + t.callstackWithOpcodes[0].processOutput(output, err, reverted) +} + +// OnExit is called when EVM exits a scope, even if the scope didn't +// execute any code. +func (t *erc7562Tracer) OnExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) { + if depth == 0 { + t.captureEnd(output, gasUsed, err, reverted) + return + } + + t.depth = depth - 1 + if t.config.OnlyTopCall { + return + } + + size := len(t.callstackWithOpcodes) + if size <= 1 { + return + } + // Pop call. + call := t.callstackWithOpcodes[size-1] + t.callstackWithOpcodes = t.callstackWithOpcodes[:size-1] + size -= 1 + + call.GasUsed = gasUsed + call.processOutput(output, err, reverted) + // Nest call into parent. + t.callstackWithOpcodes[size-1].Calls = append(t.callstackWithOpcodes[size-1].Calls, call) } func (t *erc7562Tracer) OnTxEnd(receipt *types.Receipt, err error) { @@ -138,21 +177,44 @@ func (t *erc7562Tracer) OnTxEnd(receipt *types.Receipt, err error) { if err != nil { return } - t.callstack[0].GasUsed = receipt.GasUsed + log.Error("WTF is receipt", receipt) + t.callstackWithOpcodes[0].GasUsed = receipt.GasUsed if t.config.WithLog { // Logs are not emitted when the call fails - clearFailedLogs(&t.callstack[0].callFrame, false) + clearFailedLogs(&t.callstackWithOpcodes[0].callFrame, false) } } +func (t *erc7562Tracer) OnLog(log *types.Log) { + // Only logs need to be captured via opcode processing + if !t.config.WithLog { + return + } + // Avoid processing nested calls when only caring about top call + if t.config.OnlyTopCall && t.depth > 0 { + return + } + // Skip if tracing was interrupted + if t.interrupt.Load() { + return + } + l := callLog{ + Address: log.Address, + Topics: log.Topics, + Data: log.Data, + Position: hexutil.Uint(len(t.callstackWithOpcodes[len(t.callstackWithOpcodes)-1].Calls)), + } + t.callstackWithOpcodes[len(t.callstackWithOpcodes)-1].Logs = append(t.callstackWithOpcodes[len(t.callstackWithOpcodes)-1].Logs, l) +} + // GetResult returns the json-encoded nested list of call traces, and any // error arising from the encoding or forceful termination (via `Stop`). func (t *erc7562Tracer) GetResult() (json.RawMessage, error) { - if len(t.callstack) != 1 { + if len(t.callstackWithOpcodes) != 1 { return nil, errors.New("incorrect number of top-level calls") } - res, err := json.Marshal(t.callstack[0]) + res, err := json.Marshal(t.callstackWithOpcodes[0]) if err != nil { return nil, err } @@ -171,18 +233,27 @@ func (t *erc7562Tracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tra if len(t.lastThreeOpCodes) > 3 { t.lastThreeOpCodes = t.lastThreeOpCodes[1:] } + //log.Error("WTF 2", pc, opcode.String()) - size := len(t.callstack) - currentCallFrame := t.callstack[size-1] - + size := len(t.callstackWithOpcodes) + currentCallFrame := t.callstackWithOpcodes[size-1] + //log.Error("WTF 3") t.detectOutOfGas(gas, cost, opcode, currentCallFrame) + //log.Error("WTF 4") t.handleExtOpcodes(opcode, currentCallFrame, stackTop3) + //log.Error("WTF 5") t.handleAccessedContractSize(opcode, scope, currentCallFrame) + //log.Error("WTF 6") t.handleGasObserved(opcode, currentCallFrame) + //log.Error("WTF 7") t.storeUsedOpcode(opcode, currentCallFrame) + //log.Error("WTF 8") t.handleStorageAccess(opcode, scope, currentCallFrame) + //log.Error("WTF 9") t.storeKeccak(opcode, scope) + //log.Error("WTF 10") t.lastOp = opcode + //log.Error("WTF END??", pc) } func (t *erc7562Tracer) handleGasObserved(opcode vm.OpCode, currentCallFrame callFrameWithOpcodes) { @@ -195,7 +266,7 @@ func (t *erc7562Tracer) handleGasObserved(opcode vm.OpCode, currentCallFrame cal func (t *erc7562Tracer) storeUsedOpcode(opcode vm.OpCode, currentCallFrame callFrameWithOpcodes) { // ignore "unimportant" opcodes - if opcode != vm.GAS && !t.allowedOpcodeRegex.MatchString(opcode.String()) { + if opcode != vm.GAS && !isAllowedOpcodeAfterGAS(opcode) { currentCallFrame.UsedOpcodes[opcode] = true } } @@ -235,6 +306,26 @@ func (t *erc7562Tracer) storeKeccak(opcode vm.OpCode, scope tracing.OpContext) { } } +//func (t *erc7562Tracer) handleLogs(opcode vm.OpCode, scope tracing.OpContext) { +// if opcode == vm.LOG0 || opcode == vm.LOG1 || opcode == vm.LOG2 || opcode == vm.LOG3 || opcode == vm.LOG4 { +// count := int(opcode - vm.LOG0) +// ofs := peepStack(scope.StackData(), 0) +// len := peepStack(scope.StackData(), 1) +// memory := scope.MemoryData() +// topics := []hexutil.Bytes{} +// for i := 0; i < count; i++ { +// topics = append(topics, peepStack(scope.StackData(), 2+i).Bytes()) +// //topics = append(topics, scope.Stack.Back(2+i).Bytes()) +// } +// log := make([]byte, len.Uint64()) +// copy(log, memory[ofs.Uint64():ofs.Uint64()+len.Uint64()]) +// t.Logs = append(t.Logs, &logsItem{ +// Data: log, +// Topic: topics, +// }) +// } +//} + func (t *erc7562Tracer) detectOutOfGas(gas uint64, cost uint64, opcode vm.OpCode, currentCallFrame callFrameWithOpcodes) { if gas < cost || (opcode == vm.SSTORE && gas < 2300) { currentCallFrame.OutOfGas = true @@ -266,7 +357,6 @@ func (t *erc7562Tracer) handleAccessedContractSize(opcode vm.OpCode, scope traci n = 1 } addr := common.BytesToAddress(peepStack(scope.StackData(), n).Bytes()) - if _, ok := currentCallFrame.ContractSize[addr]; !ok && !isAllowedPrecompile(addr) { currentCallFrame.ContractSize[addr] = len(t.env.StateDB.GetCode(addr)) } @@ -285,6 +375,19 @@ func isEXTorCALL(opcode vm.OpCode) bool { opcode == vm.STATICCALL } +func isAllowedOpcodeAfterGAS(opcode vm.OpCode) bool { + if (opcode >= vm.DUP1 && opcode <= vm.DUP16) || (opcode >= vm.SWAP1 && opcode <= vm.SWAP16) || (opcode >= vm.PUSH0 && opcode <= vm.PUSH32) { + return true + } + switch opcode { + case + vm.POP, vm.ADD, vm.SUB, vm.MUL, vm.DIV, vm.EQ, vm.LT, vm.GT, vm.SLT, vm.SGT, + vm.SHL, vm.SHR, vm.AND, vm.OR, vm.NOT, vm.ISZERO: + return true + } + return false +} + // not using 'isPrecompiled' to only allow the ones defined by the ERC-7562 as stateless precompiles // [OP-062] func isAllowedPrecompile(addr common.Address) bool { From 0b0bf581a36c32b0ac5f1f364e8654c90c642c39 Mon Sep 17 00:00:00 2001 From: Alex Forshtat Date: Mon, 4 Nov 2024 00:37:04 +0100 Subject: [PATCH 10/25] WIP: Make 'allowed opcodes' a configurable parameter with a hex string input --- core/vm/opcodes.go | 4 +- eth/tracers/native/erc7562.go | 73 +++++++++++++++++++++++++++++------ 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/core/vm/opcodes.go b/core/vm/opcodes.go index 2b9231fe1af2..722d4e2c6cbd 100644 --- a/core/vm/opcodes.go +++ b/core/vm/opcodes.go @@ -162,7 +162,7 @@ const ( // 0x80 range - dups. const ( - DUP1 = 0x80 + iota + DUP1 OpCode = 0x80 + iota DUP2 DUP3 DUP4 @@ -182,7 +182,7 @@ const ( // 0x90 range - swaps. const ( - SWAP1 = 0x90 + iota + SWAP1 OpCode = 0x90 + iota SWAP2 SWAP3 SWAP4 diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index 20fe7a2f6826..33548c4b5465 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -27,6 +27,7 @@ import ( "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/log" "github.com/holiman/uint256" + "github.com/status-im/keycard-go/hexutils" "math/big" "strings" ) @@ -61,6 +62,7 @@ type erc7562Tracer struct { callTracer env *tracing.VMContext + ignoredOpcodes []vm.OpCode lastOp vm.OpCode callstackWithOpcodes []callFrameWithOpcodes lastThreeOpCodes []vm.OpCode @@ -88,16 +90,46 @@ func newErc7562Tracer(ctx *tracers.Context, cfg json.RawMessage /*, chainConfig }, nil } +type erc7562TracerConfig struct { + IgnoredOpcodes *string `json:"ignoredOpcodes"` // If true, call tracer won't collect any subcalls + WithLog bool `json:"withLog"` // If true, call tracer will collect event logs +} + +// Function to convert byte array to []vm.OpCode +func ConvertBytesToOpCodes(byteArray []byte) []vm.OpCode { + opCodes := make([]vm.OpCode, len(byteArray)) + for i, b := range byteArray { + opCodes[i] = vm.OpCode(b) + } + return opCodes +} + func newErc7562TracerObject(ctx *tracers.Context, cfg json.RawMessage) (*erc7562Tracer, error) { - var config callTracerConfig + var config erc7562TracerConfig if cfg != nil { if err := json.Unmarshal(cfg, &config); err != nil { return nil, err } } + var ignoredOpcodes []vm.OpCode + if config.IgnoredOpcodes == nil { + ignoredOpcodes = defaultIgnoredOpcodes() + } else { + ignoredOpcodes = ConvertBytesToOpCodes(hexutils.HexToBytes(*config.IgnoredOpcodes)) + } // First callframe contains tx context info // and is populated on start and end. - return &erc7562Tracer{callstackWithOpcodes: make([]callFrameWithOpcodes, 0, 1), callTracer: callTracer{config: config}}, nil + return &erc7562Tracer{ + callstackWithOpcodes: make([]callFrameWithOpcodes, 0, 1), + ignoredOpcodes: ignoredOpcodes, + callTracer: callTracer{ + config: callTracerConfig{ + WithLog: config.WithLog, + // Disabling the 'OnlyTopCall' option as it defeats the purpose of ERC-7562 + OnlyTopCall: false, + }, + }, + }, nil } func (t *erc7562Tracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) { @@ -266,7 +298,7 @@ func (t *erc7562Tracer) handleGasObserved(opcode vm.OpCode, currentCallFrame cal func (t *erc7562Tracer) storeUsedOpcode(opcode vm.OpCode, currentCallFrame callFrameWithOpcodes) { // ignore "unimportant" opcodes - if opcode != vm.GAS && !isAllowedOpcodeAfterGAS(opcode) { + if opcode != vm.GAS && !t.isIgnoredOpcode(opcode) { currentCallFrame.UsedOpcodes[opcode] = true } } @@ -375,19 +407,36 @@ func isEXTorCALL(opcode vm.OpCode) bool { opcode == vm.STATICCALL } -func isAllowedOpcodeAfterGAS(opcode vm.OpCode) bool { - if (opcode >= vm.DUP1 && opcode <= vm.DUP16) || (opcode >= vm.SWAP1 && opcode <= vm.SWAP16) || (opcode >= vm.PUSH0 && opcode <= vm.PUSH32) { - return true - } - switch opcode { - case - vm.POP, vm.ADD, vm.SUB, vm.MUL, vm.DIV, vm.EQ, vm.LT, vm.GT, vm.SLT, vm.SGT, - vm.SHL, vm.SHR, vm.AND, vm.OR, vm.NOT, vm.ISZERO: - return true +// Check if this opcode is ignored for the purposes of generating the used opcodes report +func (t *erc7562Tracer) isIgnoredOpcode(opcode vm.OpCode) bool { + for _, ignoredOpcode := range t.ignoredOpcodes { + if opcode == ignoredOpcode { + return true + } } return false } +func defaultIgnoredOpcodes() []vm.OpCode { + i := 0 + ignoredOpcodes := make([]vm.OpCode, 100) // TODO count real length + // Allow all PUSHx, DUPx and SWAPx opcodes as they have sequential codes + for op := vm.PUSH0; op < vm.SWAP16; op++ { + ignoredOpcodes[i] = op + i++ + } + for _, op := range []vm.OpCode{ + vm.POP, vm.ADD, vm.SUB, vm.MUL, + vm.DIV, vm.EQ, vm.LT, vm.GT, + vm.SLT, vm.SGT, vm.SHL, vm.SHR, + vm.AND, vm.OR, vm.NOT, vm.ISZERO, + } { + ignoredOpcodes[i] = op + i++ + } + return ignoredOpcodes +} + // not using 'isPrecompiled' to only allow the ones defined by the ERC-7562 as stateless precompiles // [OP-062] func isAllowedPrecompile(addr common.Address) bool { From e5b71eb585457340258c31904b336a4cdf1075fc Mon Sep 17 00:00:00 2001 From: Alex Forshtat Date: Mon, 4 Nov 2024 00:57:26 +0100 Subject: [PATCH 11/25] WIP: Remove all inefficient code working with opcodes as strings --- eth/tracers/native/erc7562.go | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index 33548c4b5465..a3ddad838d33 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -29,7 +29,6 @@ import ( "github.com/holiman/uint256" "github.com/status-im/keycard-go/hexutils" "math/big" - "strings" ) //go:generate go run github.com/fjl/gencodec -type callFrame -field-override callFrameMarshaling -out gen_callframe_json.go @@ -290,7 +289,8 @@ func (t *erc7562Tracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tra func (t *erc7562Tracer) handleGasObserved(opcode vm.OpCode, currentCallFrame callFrameWithOpcodes) { // [OP-012] - pendingGasObserved := t.lastOp == vm.GAS && !strings.Contains(opcode.String(), "CALL") + isCall := opcode == vm.CALL || opcode == vm.STATICCALL || opcode == vm.DELEGATECALL || opcode == vm.CALLCODE + pendingGasObserved := t.lastOp == vm.GAS && !isCall if pendingGasObserved { currentCallFrame.GasObserved = true } @@ -366,26 +366,24 @@ func (t *erc7562Tracer) detectOutOfGas(gas uint64, cost uint64, opcode vm.OpCode // TODO: rewrite using byte opcode values, without relying on string manipulations func (t *erc7562Tracer) handleExtOpcodes(opcode vm.OpCode, currentCallFrame callFrameWithOpcodes, stackTop3 partialStack) { - if strings.HasPrefix(opcode.String(), "EXT") { + if isEXT(opcode) { addr := common.HexToAddress(stackTop3[0].Hex()) - ops := []string{} - for _, item := range t.lastThreeOpCodes { - ops = append(ops, item.String()) - } - last3OpcodeStr := strings.Join(ops, ",") + // TODO: THIS CODE SEEMS TO BE EQUIVALENT TO THE STRING BASED BUT IT IS LOGICALLY WRONG AND WILL NEVER WORK. + // TODO: INSTEAD WE MAY NEED TO DEFER ADDING ADDRESS TO 'ExtCodeAccessInfo' UNTIL AFTER THE 'ISZERO' CHECK. // only store the last EXTCODE* opcode per address - could even be a boolean for our current use-case // [OP-051] - if !strings.Contains(last3OpcodeStr, ",EXTCODESIZE,ISZERO") { + if !(t.lastThreeOpCodes[1] == vm.EXTCODESIZE && t.lastThreeOpCodes[2] == vm.ISZERO) { currentCallFrame.ExtCodeAccessInfo = append(currentCallFrame.ExtCodeAccessInfo, addr) } } } + func (t *erc7562Tracer) handleAccessedContractSize(opcode vm.OpCode, scope tracing.OpContext, currentCallFrame callFrameWithOpcodes) { // [OP-041] if isEXTorCALL(opcode) { n := 0 - if !strings.HasPrefix(opcode.String(), "EXT") { + if !isEXT(opcode) { n = 1 } addr := common.BytesToAddress(peepStack(scope.StackData(), n).Bytes()) @@ -400,13 +398,19 @@ func peepStack(stackData []uint256.Int, n int) *uint256.Int { } func isEXTorCALL(opcode vm.OpCode) bool { - return strings.HasPrefix(opcode.String(), "EXT") || + return isEXT(opcode) || opcode == vm.CALL || opcode == vm.CALLCODE || opcode == vm.DELEGATECALL || opcode == vm.STATICCALL } +func isEXT(opcode vm.OpCode) bool { + return opcode == vm.EXTCODEHASH || + opcode == vm.EXTCODESIZE || + opcode == vm.EXTCODECOPY +} + // Check if this opcode is ignored for the purposes of generating the used opcodes report func (t *erc7562Tracer) isIgnoredOpcode(opcode vm.OpCode) bool { for _, ignoredOpcode := range t.ignoredOpcodes { From e7200a1379333f8da7dfcb6ae7ac8c4571f530bd Mon Sep 17 00:00:00 2001 From: shahafn Date: Tue, 17 Dec 2024 19:56:04 +0200 Subject: [PATCH 12/25] erc7562 tracer wip --- eth/tracers/native/erc7562.go | 227 +++++++++++++----- .../native/gen_callframewithopcodes_json.go | 143 +++++++++++ 2 files changed, 311 insertions(+), 59 deletions(-) create mode 100644 eth/tracers/native/gen_callframewithopcodes_json.go diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index a3ddad838d33..d5b1cf558556 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -19,6 +19,7 @@ package native import ( "encoding/json" "errors" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/tracing" @@ -29,24 +30,86 @@ import ( "github.com/holiman/uint256" "github.com/status-im/keycard-go/hexutils" "math/big" + "runtime" ) -//go:generate go run github.com/fjl/gencodec -type callFrame -field-override callFrameMarshaling -out gen_callframe_json.go +//go:generate go run github.com/fjl/gencodec -type callFrameWithOpcodes -field-override callFrameWithOpcodesMarshaling -out gen_callframewithopcodes_json.go func init() { tracers.DefaultDirectory.Register("erc7562Tracer", newErc7562Tracer, false) } type callFrameWithOpcodes struct { - callFrame - AccessedSlots accessedSlots `json:"accessedSlots"` - ExtCodeAccessInfo []common.Address `json:"extCodeAccessInfo"` - DeployedContracts []common.Address `json:"deployedContracts"` - UsedOpcodes map[vm.OpCode]bool `json:"usedOpcodes"` - GasObserved bool `json:"gasObserved"` - ContractSize map[common.Address]int `json:"contractSize"` - OutOfGas bool `json:"outOfGas"` - Calls []callFrameWithOpcodes `json:"calls,omitempty" rlp:"optional"` + //callFrame + Type vm.OpCode `json:"-"` + From common.Address `json:"from"` + Gas uint64 `json:"gas"` + GasUsed uint64 `json:"gasUsed"` + To *common.Address `json:"to,omitempty" rlp:"optional"` + Input []byte `json:"input" rlp:"optional"` + Output []byte `json:"output,omitempty" rlp:"optional"` + 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"` + // 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 *big.Int `json:"value,omitempty" rlp:"optional"` + revertedSnapshot bool + + AccessedSlots accessedSlots `json:"accessedSlots"` + ExtCodeAccessInfo []common.Address `json:"extCodeAccessInfo"` + DeployedContracts []common.Address `json:"deployedContracts"` + UsedOpcodes map[vm.OpCode]bool `json:"usedOpcodes"` + //GasObserved bool `json:"gasObserved"` + ContractSize map[common.Address]int `json:"contractSize"` + OutOfGas bool `json:"outOfGas"` + Calls []callFrameWithOpcodes `json:"calls,omitempty" rlp:"optional"` +} + +func (f callFrameWithOpcodes) TypeString() string { + return f.Type.String() +} + +func (f callFrameWithOpcodes) failed() bool { + return len(f.Error) > 0 && f.revertedSnapshot +} + +func (f *callFrameWithOpcodes) processOutput(output []byte, err error, reverted bool) { + output = common.CopyBytes(output) + // Clear error if tx wasn't reverted. This happened + // for pre-homestead contract storage OOG. + if err != nil && !reverted { + err = nil + } + if err == nil { + f.Output = output + return + } + f.Error = err.Error() + f.revertedSnapshot = reverted + if f.Type == vm.CREATE || f.Type == vm.CREATE2 { + f.To = nil + } + if !errors.Is(err, vm.ErrExecutionReverted) || len(output) == 0 { + return + } + f.Output = output + if len(output) < 4 { + return + } + if unpacked, err := abi.UnpackRevert(output); err == nil { + f.RevertReason = unpacked + } +} + +type callFrameWithOpcodesMarshaling struct { + TypeString string `json:"type"` + Gas hexutil.Uint64 + GasUsed hexutil.Uint64 + Value *hexutil.Big + Input hexutil.Bytes + Output hexutil.Bytes } // TODO: I suggest that we provide an `[]string` for all of these fields. Doing it for `Reads` as an example here. @@ -68,6 +131,19 @@ type erc7562Tracer struct { Keccak []hexutil.Bytes `json:"keccak"` } +// catchPanic handles panic recovery and logs the panic and stack trace. +func catchPanic() { + if r := recover(); r != nil { + // Retrieve the function name + pc, _, _, _ := runtime.Caller(1) + funcName := runtime.FuncForPC(pc).Name() + + // Log the panic and function name + log.Error("Panic in", funcName, r) + //debug.PrintStack() + } +} + // newCallTracer returns a native go tracer which tracks // call frames of a tx, and implements vm.EVMLogger. func newErc7562Tracer(ctx *tracers.Context, cfg json.RawMessage /*, chainConfig *params.ChainConfig*/) (*tracers.Tracer, error) { @@ -132,12 +208,14 @@ func newErc7562TracerObject(ctx *tracers.Context, cfg json.RawMessage) (*erc7562 } func (t *erc7562Tracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) { + defer catchPanic() t.env = env t.gasLimit = tx.Gas() } // OnEnter is called when EVM enters a new scope (via call, create or selfdestruct). func (t *erc7562Tracer) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + defer catchPanic() t.depth = depth // Skip if tracing was interrupted if t.interrupt.Load() { @@ -146,21 +224,21 @@ func (t *erc7562Tracer) OnEnter(depth int, typ byte, from common.Address, to com toCopy := to call := callFrameWithOpcodes{ - callFrame: callFrame{ - Type: vm.OpCode(typ), - From: from, - To: &toCopy, - Input: common.CopyBytes(input), - Gas: gas, - Value: value}, + Type: vm.OpCode(typ), + From: from, + To: &toCopy, + Input: common.CopyBytes(input), + Gas: gas, + Value: value, AccessedSlots: accessedSlots{ Reads: map[string][]string{}, Writes: map[string]uint64{}, TransientReads: map[string]uint64{}, TransientWrites: map[string]uint64{}, }, - UsedOpcodes: make(map[vm.OpCode]bool, 3), - ContractSize: make(map[common.Address]int, 1), + UsedOpcodes: make(map[vm.OpCode]bool, 3), + ExtCodeAccessInfo: make([]common.Address, 0), + ContractSize: make(map[common.Address]int, 1), } if depth == 0 { call.Gas = t.gasLimit @@ -178,6 +256,7 @@ func (t *erc7562Tracer) captureEnd(output []byte, gasUsed uint64, err error, rev // OnExit is called when EVM exits a scope, even if the scope didn't // execute any code. func (t *erc7562Tracer) OnExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) { + defer catchPanic() if depth == 0 { t.captureEnd(output, gasUsed, err, reverted) return @@ -204,19 +283,20 @@ func (t *erc7562Tracer) OnExit(depth int, output []byte, gasUsed uint64, err err } func (t *erc7562Tracer) OnTxEnd(receipt *types.Receipt, err error) { + defer catchPanic() // Error happened during tx validation. if err != nil { return } - log.Error("WTF is receipt", receipt) t.callstackWithOpcodes[0].GasUsed = receipt.GasUsed if t.config.WithLog { // Logs are not emitted when the call fails - clearFailedLogs(&t.callstackWithOpcodes[0].callFrame, false) + clearFailedLogs2(&t.callstackWithOpcodes[0], false) } } -func (t *erc7562Tracer) OnLog(log *types.Log) { +func (t *erc7562Tracer) OnLog(log1 *types.Log) { + defer catchPanic() // Only logs need to be captured via opcode processing if !t.config.WithLog { return @@ -230,29 +310,87 @@ func (t *erc7562Tracer) OnLog(log *types.Log) { return } l := callLog{ - Address: log.Address, - Topics: log.Topics, - Data: log.Data, + Address: log1.Address, + Topics: log1.Topics, + Data: log1.Data, Position: hexutil.Uint(len(t.callstackWithOpcodes[len(t.callstackWithOpcodes)-1].Calls)), } t.callstackWithOpcodes[len(t.callstackWithOpcodes)-1].Logs = append(t.callstackWithOpcodes[len(t.callstackWithOpcodes)-1].Logs, l) } +func (t *erc7562Tracer) handleLogs(opcode vm.OpCode, scope tracing.OpContext) { + defer catchPanic() + if opcode == vm.LOG0 || opcode == vm.LOG1 || opcode == vm.LOG2 || opcode == vm.LOG3 || opcode == vm.LOG4 { + } + // count := int(opcode - vm.LOG0) + // ofs := peepStack(scope.StackData(), 0) + // len := peepStack(scope.StackData(), 1) + // memory := scope.MemoryData() + // topics := []hexutil.Bytes{} + // for i := 0; i < count; i++ { + // topics = append(topics, peepStack(scope.StackData(), 2+i).Bytes()) + // //topics = append(topics, scope.Stack.Back(2+i).Bytes()) + // } + // log := make([]byte, len.Uint64()) + // copy(log, memory[ofs.Uint64():ofs.Uint64()+len.Uint64()]) + // t.Logs = append(t.Logs, &logsItem{ + // Data: log, + // Topic: topics, + // }) + //} +} + // GetResult returns the json-encoded nested list of call traces, and any // error arising from the encoding or forceful termination (via `Stop`). func (t *erc7562Tracer) GetResult() (json.RawMessage, error) { + defer catchPanic() if len(t.callstackWithOpcodes) != 1 { return nil, errors.New("incorrect number of top-level calls") } - res, err := json.Marshal(t.callstackWithOpcodes[0]) + callFrameJSON, err := json.Marshal(t.callstackWithOpcodes[0]) + + // Unmarshal the generated JSON into a map + var resultMap map[string]interface{} + if err := json.Unmarshal(callFrameJSON, &resultMap); err != nil { + return nil, err + } + + // Add the additional fields + resultMap["lastOp"] = t.lastOp + resultMap["lastThreeOpCodes"] = t.lastThreeOpCodes + resultMap["keccak"] = t.Keccak + + // Marshal the final map back to JSON + finalJSON, err := json.Marshal(resultMap) if err != nil { return nil, err } - return res, t.reason + return finalJSON, t.reason +} + +// Stop terminates execution of the tracer at the first opportune moment. +func (t *erc7562Tracer) Stop(err error) { + defer catchPanic() + t.reason = err + t.interrupt.Store(true) +} + +// clearFailedLogs clears the logs of a callframe and all its children +// in case of execution failure. +func clearFailedLogs2(cf *callFrameWithOpcodes, parentFailed bool) { + failed := cf.failed() || parentFailed + // Clear own logs + if failed { + cf.Logs = nil + } + for i := range cf.Calls { + clearFailedLogs2(&cf.Calls[i], failed) + } } func (t *erc7562Tracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { + defer catchPanic() opcode := vm.OpCode(op) stackSize := len(scope.StackData()) @@ -264,27 +402,17 @@ func (t *erc7562Tracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tra if len(t.lastThreeOpCodes) > 3 { t.lastThreeOpCodes = t.lastThreeOpCodes[1:] } - //log.Error("WTF 2", pc, opcode.String()) - size := len(t.callstackWithOpcodes) currentCallFrame := t.callstackWithOpcodes[size-1] - //log.Error("WTF 3") t.detectOutOfGas(gas, cost, opcode, currentCallFrame) - //log.Error("WTF 4") t.handleExtOpcodes(opcode, currentCallFrame, stackTop3) - //log.Error("WTF 5") t.handleAccessedContractSize(opcode, scope, currentCallFrame) - //log.Error("WTF 6") t.handleGasObserved(opcode, currentCallFrame) - //log.Error("WTF 7") t.storeUsedOpcode(opcode, currentCallFrame) - //log.Error("WTF 8") t.handleStorageAccess(opcode, scope, currentCallFrame) - //log.Error("WTF 9") t.storeKeccak(opcode, scope) - //log.Error("WTF 10") t.lastOp = opcode - //log.Error("WTF END??", pc) + //t.handleLogs(opcode, scope) } func (t *erc7562Tracer) handleGasObserved(opcode vm.OpCode, currentCallFrame callFrameWithOpcodes) { @@ -292,7 +420,8 @@ func (t *erc7562Tracer) handleGasObserved(opcode vm.OpCode, currentCallFrame cal isCall := opcode == vm.CALL || opcode == vm.STATICCALL || opcode == vm.DELEGATECALL || opcode == vm.CALLCODE pendingGasObserved := t.lastOp == vm.GAS && !isCall if pendingGasObserved { - currentCallFrame.GasObserved = true + //currentCallFrame.GasObserved = true + currentCallFrame.UsedOpcodes[vm.GAS] = true } } @@ -338,26 +467,6 @@ func (t *erc7562Tracer) storeKeccak(opcode vm.OpCode, scope tracing.OpContext) { } } -//func (t *erc7562Tracer) handleLogs(opcode vm.OpCode, scope tracing.OpContext) { -// if opcode == vm.LOG0 || opcode == vm.LOG1 || opcode == vm.LOG2 || opcode == vm.LOG3 || opcode == vm.LOG4 { -// count := int(opcode - vm.LOG0) -// ofs := peepStack(scope.StackData(), 0) -// len := peepStack(scope.StackData(), 1) -// memory := scope.MemoryData() -// topics := []hexutil.Bytes{} -// for i := 0; i < count; i++ { -// topics = append(topics, peepStack(scope.StackData(), 2+i).Bytes()) -// //topics = append(topics, scope.Stack.Back(2+i).Bytes()) -// } -// log := make([]byte, len.Uint64()) -// copy(log, memory[ofs.Uint64():ofs.Uint64()+len.Uint64()]) -// t.Logs = append(t.Logs, &logsItem{ -// Data: log, -// Topic: topics, -// }) -// } -//} - func (t *erc7562Tracer) detectOutOfGas(gas uint64, cost uint64, opcode vm.OpCode, currentCallFrame callFrameWithOpcodes) { if gas < cost || (opcode == vm.SSTORE && gas < 2300) { currentCallFrame.OutOfGas = true diff --git a/eth/tracers/native/gen_callframewithopcodes_json.go b/eth/tracers/native/gen_callframewithopcodes_json.go new file mode 100644 index 000000000000..8e40b699b7a6 --- /dev/null +++ b/eth/tracers/native/gen_callframewithopcodes_json.go @@ -0,0 +1,143 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package native + +import ( + "encoding/json" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/vm" +) + +var _ = (*callFrameWithOpcodesMarshaling)(nil) + +// MarshalJSON marshals as JSON. +func (c callFrameWithOpcodes) MarshalJSON() ([]byte, error) { + type callFrameWithOpcodes0 struct { + Type vm.OpCode `json:"-"` + From common.Address `json:"from"` + Gas hexutil.Uint64 `json:"gas"` + GasUsed hexutil.Uint64 `json:"gasUsed"` + To *common.Address `json:"to,omitempty" rlp:"optional"` + Input hexutil.Bytes `json:"input" rlp:"optional"` + Output hexutil.Bytes `json:"output,omitempty" rlp:"optional"` + Error string `json:"error,omitempty" rlp:"optional"` + RevertReason string `json:"revertReason,omitempty"` + Logs []callLog `json:"logs,omitempty" rlp:"optional"` + Value *hexutil.Big `json:"value,omitempty" rlp:"optional"` + AccessedSlots accessedSlots `json:"accessedSlots"` + ExtCodeAccessInfo []common.Address `json:"extCodeAccessInfo"` + DeployedContracts []common.Address `json:"deployedContracts"` + UsedOpcodes map[vm.OpCode]bool `json:"usedOpcodes"` + ContractSize map[common.Address]int `json:"contractSize"` + OutOfGas bool `json:"outOfGas"` + Calls []callFrameWithOpcodes `json:"calls,omitempty" rlp:"optional"` + TypeString string `json:"type"` + } + var enc callFrameWithOpcodes0 + enc.Type = c.Type + enc.From = c.From + enc.Gas = hexutil.Uint64(c.Gas) + enc.GasUsed = hexutil.Uint64(c.GasUsed) + enc.To = c.To + enc.Input = c.Input + enc.Output = c.Output + enc.Error = c.Error + enc.RevertReason = c.RevertReason + enc.Logs = c.Logs + enc.Value = (*hexutil.Big)(c.Value) + enc.AccessedSlots = c.AccessedSlots + enc.ExtCodeAccessInfo = c.ExtCodeAccessInfo + enc.DeployedContracts = c.DeployedContracts + enc.UsedOpcodes = c.UsedOpcodes + enc.ContractSize = c.ContractSize + enc.OutOfGas = c.OutOfGas + enc.Calls = c.Calls + enc.TypeString = c.TypeString() + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (c *callFrameWithOpcodes) UnmarshalJSON(input []byte) error { + type callFrameWithOpcodes0 struct { + Type *vm.OpCode `json:"-"` + From *common.Address `json:"from"` + Gas *hexutil.Uint64 `json:"gas"` + GasUsed *hexutil.Uint64 `json:"gasUsed"` + To *common.Address `json:"to,omitempty" rlp:"optional"` + Input *hexutil.Bytes `json:"input" rlp:"optional"` + Output *hexutil.Bytes `json:"output,omitempty" rlp:"optional"` + Error *string `json:"error,omitempty" rlp:"optional"` + RevertReason *string `json:"revertReason,omitempty"` + Logs []callLog `json:"logs,omitempty" rlp:"optional"` + Value *hexutil.Big `json:"value,omitempty" rlp:"optional"` + AccessedSlots *accessedSlots `json:"accessedSlots"` + ExtCodeAccessInfo []common.Address `json:"extCodeAccessInfo"` + DeployedContracts []common.Address `json:"deployedContracts"` + UsedOpcodes map[vm.OpCode]bool `json:"usedOpcodes"` + ContractSize map[common.Address]int `json:"contractSize"` + OutOfGas *bool `json:"outOfGas"` + Calls []callFrameWithOpcodes `json:"calls,omitempty" rlp:"optional"` + } + var dec callFrameWithOpcodes0 + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.Type != nil { + c.Type = *dec.Type + } + if dec.From != nil { + c.From = *dec.From + } + if dec.Gas != nil { + c.Gas = uint64(*dec.Gas) + } + if dec.GasUsed != nil { + c.GasUsed = uint64(*dec.GasUsed) + } + if dec.To != nil { + c.To = dec.To + } + if dec.Input != nil { + c.Input = *dec.Input + } + if dec.Output != nil { + c.Output = *dec.Output + } + if dec.Error != nil { + c.Error = *dec.Error + } + if dec.RevertReason != nil { + c.RevertReason = *dec.RevertReason + } + if dec.Logs != nil { + c.Logs = dec.Logs + } + if dec.Value != nil { + c.Value = (*big.Int)(dec.Value) + } + if dec.AccessedSlots != nil { + c.AccessedSlots = *dec.AccessedSlots + } + if dec.ExtCodeAccessInfo != nil { + c.ExtCodeAccessInfo = dec.ExtCodeAccessInfo + } + if dec.DeployedContracts != nil { + c.DeployedContracts = dec.DeployedContracts + } + if dec.UsedOpcodes != nil { + c.UsedOpcodes = dec.UsedOpcodes + } + if dec.ContractSize != nil { + c.ContractSize = dec.ContractSize + } + if dec.OutOfGas != nil { + c.OutOfGas = *dec.OutOfGas + } + if dec.Calls != nil { + c.Calls = dec.Calls + } + return nil +} From 422e99d49b1f69c6944dab66105fbca0cfeb9735 Mon Sep 17 00:00:00 2001 From: shahafn Date: Wed, 18 Dec 2024 13:21:30 +0200 Subject: [PATCH 13/25] Fixing pointer --- eth/tracers/native/erc7562.go | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index d5b1cf558556..5e7d840b0314 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -366,6 +366,14 @@ func (t *erc7562Tracer) GetResult() (json.RawMessage, error) { if err != nil { return nil, err } + if t.callstackWithOpcodes[0].OutOfGas { + log.Error("WTF TOP GETRESULT OOG?") + } + for i := range t.callstackWithOpcodes[0].Calls { + if t.callstackWithOpcodes[0].Calls[i].OutOfGas { + log.Error("WTF GETRESULT OOG?") + } + } return finalJSON, t.reason } @@ -403,7 +411,7 @@ func (t *erc7562Tracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tra t.lastThreeOpCodes = t.lastThreeOpCodes[1:] } size := len(t.callstackWithOpcodes) - currentCallFrame := t.callstackWithOpcodes[size-1] + currentCallFrame := &t.callstackWithOpcodes[size-1] t.detectOutOfGas(gas, cost, opcode, currentCallFrame) t.handleExtOpcodes(opcode, currentCallFrame, stackTop3) t.handleAccessedContractSize(opcode, scope, currentCallFrame) @@ -415,7 +423,7 @@ func (t *erc7562Tracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tra //t.handleLogs(opcode, scope) } -func (t *erc7562Tracer) handleGasObserved(opcode vm.OpCode, currentCallFrame callFrameWithOpcodes) { +func (t *erc7562Tracer) handleGasObserved(opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes) { // [OP-012] isCall := opcode == vm.CALL || opcode == vm.STATICCALL || opcode == vm.DELEGATECALL || opcode == vm.CALLCODE pendingGasObserved := t.lastOp == vm.GAS && !isCall @@ -425,14 +433,14 @@ func (t *erc7562Tracer) handleGasObserved(opcode vm.OpCode, currentCallFrame cal } } -func (t *erc7562Tracer) storeUsedOpcode(opcode vm.OpCode, currentCallFrame callFrameWithOpcodes) { +func (t *erc7562Tracer) storeUsedOpcode(opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes) { // ignore "unimportant" opcodes if opcode != vm.GAS && !t.isIgnoredOpcode(opcode) { currentCallFrame.UsedOpcodes[opcode] = true } } -func (t *erc7562Tracer) handleStorageAccess(opcode vm.OpCode, scope tracing.OpContext, currentCallFrame callFrameWithOpcodes) { +func (t *erc7562Tracer) handleStorageAccess(opcode vm.OpCode, scope tracing.OpContext, currentCallFrame *callFrameWithOpcodes) { if opcode == vm.SLOAD || opcode == vm.SSTORE || opcode == vm.TLOAD || opcode == vm.TSTORE { slot := common.BytesToHash(peepStack(scope.StackData(), 0).Bytes()) slotHex := slot.Hex() @@ -467,14 +475,18 @@ func (t *erc7562Tracer) storeKeccak(opcode vm.OpCode, scope tracing.OpContext) { } } -func (t *erc7562Tracer) detectOutOfGas(gas uint64, cost uint64, opcode vm.OpCode, currentCallFrame callFrameWithOpcodes) { +func (t *erc7562Tracer) detectOutOfGas(gas uint64, cost uint64, opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes) { + if gas < cost { + log.Error("WTF OOG?") + } if gas < cost || (opcode == vm.SSTORE && gas < 2300) { currentCallFrame.OutOfGas = true + log.Error("WTF SSTORE OOG?", currentCallFrame.OutOfGas) } } // TODO: rewrite using byte opcode values, without relying on string manipulations -func (t *erc7562Tracer) handleExtOpcodes(opcode vm.OpCode, currentCallFrame callFrameWithOpcodes, stackTop3 partialStack) { +func (t *erc7562Tracer) handleExtOpcodes(opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes, stackTop3 partialStack) { if isEXT(opcode) { addr := common.HexToAddress(stackTop3[0].Hex()) @@ -488,7 +500,7 @@ func (t *erc7562Tracer) handleExtOpcodes(opcode vm.OpCode, currentCallFrame call } } -func (t *erc7562Tracer) handleAccessedContractSize(opcode vm.OpCode, scope tracing.OpContext, currentCallFrame callFrameWithOpcodes) { +func (t *erc7562Tracer) handleAccessedContractSize(opcode vm.OpCode, scope tracing.OpContext, currentCallFrame *callFrameWithOpcodes) { // [OP-041] if isEXTorCALL(opcode) { n := 0 From e26ec4d05f3a9d8c6e02907e3409066e3ee11017 Mon Sep 17 00:00:00 2001 From: shahafn Date: Wed, 18 Dec 2024 17:50:58 +0200 Subject: [PATCH 14/25] Fixing contract size type --- eth/tracers/native/erc7562.go | 18 +++-- .../native/gen_callframewithopcodes_json.go | 74 +++++++++---------- 2 files changed, 50 insertions(+), 42 deletions(-) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index 5e7d840b0314..6159f82fd437 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -39,6 +39,11 @@ func init() { tracers.DefaultDirectory.Register("erc7562Tracer", newErc7562Tracer, false) } +type contractSizeWithOpcode struct { + ContractSize int `json:"contractSize"` + Opcode vm.OpCode `json:"opcode"` +} + type callFrameWithOpcodes struct { //callFrame Type vm.OpCode `json:"-"` @@ -62,9 +67,9 @@ type callFrameWithOpcodes struct { DeployedContracts []common.Address `json:"deployedContracts"` UsedOpcodes map[vm.OpCode]bool `json:"usedOpcodes"` //GasObserved bool `json:"gasObserved"` - ContractSize map[common.Address]int `json:"contractSize"` - OutOfGas bool `json:"outOfGas"` - Calls []callFrameWithOpcodes `json:"calls,omitempty" rlp:"optional"` + ContractSize map[common.Address]*contractSizeWithOpcode `json:"contractSize"` + OutOfGas bool `json:"outOfGas"` + Calls []callFrameWithOpcodes `json:"calls,omitempty" rlp:"optional"` } func (f callFrameWithOpcodes) TypeString() string { @@ -238,7 +243,7 @@ func (t *erc7562Tracer) OnEnter(depth int, typ byte, from common.Address, to com }, UsedOpcodes: make(map[vm.OpCode]bool, 3), ExtCodeAccessInfo: make([]common.Address, 0), - ContractSize: make(map[common.Address]int, 1), + ContractSize: map[common.Address]*contractSizeWithOpcode{}, } if depth == 0 { call.Gas = t.gasLimit @@ -509,7 +514,10 @@ func (t *erc7562Tracer) handleAccessedContractSize(opcode vm.OpCode, scope traci } addr := common.BytesToAddress(peepStack(scope.StackData(), n).Bytes()) if _, ok := currentCallFrame.ContractSize[addr]; !ok && !isAllowedPrecompile(addr) { - currentCallFrame.ContractSize[addr] = len(t.env.StateDB.GetCode(addr)) + currentCallFrame.ContractSize[addr] = &contractSizeWithOpcode{ + ContractSize: len(t.env.StateDB.GetCode(addr)), + Opcode: opcode, + } } } } diff --git a/eth/tracers/native/gen_callframewithopcodes_json.go b/eth/tracers/native/gen_callframewithopcodes_json.go index 8e40b699b7a6..140167c9d707 100644 --- a/eth/tracers/native/gen_callframewithopcodes_json.go +++ b/eth/tracers/native/gen_callframewithopcodes_json.go @@ -16,25 +16,25 @@ var _ = (*callFrameWithOpcodesMarshaling)(nil) // MarshalJSON marshals as JSON. func (c callFrameWithOpcodes) MarshalJSON() ([]byte, error) { type callFrameWithOpcodes0 struct { - Type vm.OpCode `json:"-"` - From common.Address `json:"from"` - Gas hexutil.Uint64 `json:"gas"` - GasUsed hexutil.Uint64 `json:"gasUsed"` - To *common.Address `json:"to,omitempty" rlp:"optional"` - Input hexutil.Bytes `json:"input" rlp:"optional"` - Output hexutil.Bytes `json:"output,omitempty" rlp:"optional"` - Error string `json:"error,omitempty" rlp:"optional"` - RevertReason string `json:"revertReason,omitempty"` - Logs []callLog `json:"logs,omitempty" rlp:"optional"` - Value *hexutil.Big `json:"value,omitempty" rlp:"optional"` - AccessedSlots accessedSlots `json:"accessedSlots"` - ExtCodeAccessInfo []common.Address `json:"extCodeAccessInfo"` - DeployedContracts []common.Address `json:"deployedContracts"` - UsedOpcodes map[vm.OpCode]bool `json:"usedOpcodes"` - ContractSize map[common.Address]int `json:"contractSize"` - OutOfGas bool `json:"outOfGas"` - Calls []callFrameWithOpcodes `json:"calls,omitempty" rlp:"optional"` - TypeString string `json:"type"` + Type vm.OpCode `json:"-"` + From common.Address `json:"from"` + Gas hexutil.Uint64 `json:"gas"` + GasUsed hexutil.Uint64 `json:"gasUsed"` + To *common.Address `json:"to,omitempty" rlp:"optional"` + Input hexutil.Bytes `json:"input" rlp:"optional"` + Output hexutil.Bytes `json:"output,omitempty" rlp:"optional"` + Error string `json:"error,omitempty" rlp:"optional"` + RevertReason string `json:"revertReason,omitempty"` + Logs []callLog `json:"logs,omitempty" rlp:"optional"` + Value *hexutil.Big `json:"value,omitempty" rlp:"optional"` + AccessedSlots accessedSlots `json:"accessedSlots"` + ExtCodeAccessInfo []common.Address `json:"extCodeAccessInfo"` + DeployedContracts []common.Address `json:"deployedContracts"` + UsedOpcodes map[vm.OpCode]bool `json:"usedOpcodes"` + ContractSize map[common.Address]*contractSizeWithOpcode `json:"contractSize"` + OutOfGas bool `json:"outOfGas"` + Calls []callFrameWithOpcodes `json:"calls,omitempty" rlp:"optional"` + TypeString string `json:"type"` } var enc callFrameWithOpcodes0 enc.Type = c.Type @@ -62,24 +62,24 @@ func (c callFrameWithOpcodes) MarshalJSON() ([]byte, error) { // UnmarshalJSON unmarshals from JSON. func (c *callFrameWithOpcodes) UnmarshalJSON(input []byte) error { type callFrameWithOpcodes0 struct { - Type *vm.OpCode `json:"-"` - From *common.Address `json:"from"` - Gas *hexutil.Uint64 `json:"gas"` - GasUsed *hexutil.Uint64 `json:"gasUsed"` - To *common.Address `json:"to,omitempty" rlp:"optional"` - Input *hexutil.Bytes `json:"input" rlp:"optional"` - Output *hexutil.Bytes `json:"output,omitempty" rlp:"optional"` - Error *string `json:"error,omitempty" rlp:"optional"` - RevertReason *string `json:"revertReason,omitempty"` - Logs []callLog `json:"logs,omitempty" rlp:"optional"` - Value *hexutil.Big `json:"value,omitempty" rlp:"optional"` - AccessedSlots *accessedSlots `json:"accessedSlots"` - ExtCodeAccessInfo []common.Address `json:"extCodeAccessInfo"` - DeployedContracts []common.Address `json:"deployedContracts"` - UsedOpcodes map[vm.OpCode]bool `json:"usedOpcodes"` - ContractSize map[common.Address]int `json:"contractSize"` - OutOfGas *bool `json:"outOfGas"` - Calls []callFrameWithOpcodes `json:"calls,omitempty" rlp:"optional"` + Type *vm.OpCode `json:"-"` + From *common.Address `json:"from"` + Gas *hexutil.Uint64 `json:"gas"` + GasUsed *hexutil.Uint64 `json:"gasUsed"` + To *common.Address `json:"to,omitempty" rlp:"optional"` + Input *hexutil.Bytes `json:"input" rlp:"optional"` + Output *hexutil.Bytes `json:"output,omitempty" rlp:"optional"` + Error *string `json:"error,omitempty" rlp:"optional"` + RevertReason *string `json:"revertReason,omitempty"` + Logs []callLog `json:"logs,omitempty" rlp:"optional"` + Value *hexutil.Big `json:"value,omitempty" rlp:"optional"` + AccessedSlots *accessedSlots `json:"accessedSlots"` + ExtCodeAccessInfo []common.Address `json:"extCodeAccessInfo"` + DeployedContracts []common.Address `json:"deployedContracts"` + UsedOpcodes map[vm.OpCode]bool `json:"usedOpcodes"` + ContractSize map[common.Address]*contractSizeWithOpcode `json:"contractSize"` + OutOfGas *bool `json:"outOfGas"` + Calls []callFrameWithOpcodes `json:"calls,omitempty" rlp:"optional"` } var dec callFrameWithOpcodes0 if err := json.Unmarshal(input, &dec); err != nil { From 9d7481a1f53f90869b65e8df7e2791a8cf68114c Mon Sep 17 00:00:00 2001 From: shahafn Date: Wed, 18 Dec 2024 21:24:05 +0200 Subject: [PATCH 15/25] Fixing handleExtOpcodes --- eth/tracers/native/erc7562.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index 6159f82fd437..c24c360be8c1 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -492,7 +492,7 @@ func (t *erc7562Tracer) detectOutOfGas(gas uint64, cost uint64, opcode vm.OpCode // TODO: rewrite using byte opcode values, without relying on string manipulations func (t *erc7562Tracer) handleExtOpcodes(opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes, stackTop3 partialStack) { - if isEXT(opcode) { + if isEXT(t.lastOp) { addr := common.HexToAddress(stackTop3[0].Hex()) // TODO: THIS CODE SEEMS TO BE EQUIVALENT TO THE STRING BASED BUT IT IS LOGICALLY WRONG AND WILL NEVER WORK. From 792818808035a87a10bdde39ef2ddacc562e0a09 Mon Sep 17 00:00:00 2001 From: shahafn Date: Thu, 19 Dec 2024 00:38:12 +0200 Subject: [PATCH 16/25] Fixing stack pointers --- eth/tracers/native/erc7562.go | 71 +++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index c24c360be8c1..be64f9c8b462 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -31,6 +31,7 @@ import ( "github.com/status-im/keycard-go/hexutils" "math/big" "runtime" + "runtime/debug" ) //go:generate go run github.com/fjl/gencodec -type callFrameWithOpcodes -field-override callFrameWithOpcodesMarshaling -out gen_callframewithopcodes_json.go @@ -125,14 +126,18 @@ type accessedSlots struct { TransientWrites map[string]uint64 `json:"transientWrites"` } +type opcodeWithPartialStack struct { + Opcode vm.OpCode + StackTop3 []uint256.Int +} + type erc7562Tracer struct { callTracer env *tracing.VMContext ignoredOpcodes []vm.OpCode - lastOp vm.OpCode callstackWithOpcodes []callFrameWithOpcodes - lastThreeOpCodes []vm.OpCode + lastThreeOpCodes []*opcodeWithPartialStack Keccak []hexutil.Bytes `json:"keccak"` } @@ -145,7 +150,7 @@ func catchPanic() { // Log the panic and function name log.Error("Panic in", funcName, r) - //debug.PrintStack() + debug.PrintStack() } } @@ -201,6 +206,7 @@ func newErc7562TracerObject(ctx *tracers.Context, cfg json.RawMessage) (*erc7562 // and is populated on start and end. return &erc7562Tracer{ callstackWithOpcodes: make([]callFrameWithOpcodes, 0, 1), + lastThreeOpCodes: make([]*opcodeWithPartialStack, 0), ignoredOpcodes: ignoredOpcodes, callTracer: callTracer{ config: callTracerConfig{ @@ -362,7 +368,6 @@ func (t *erc7562Tracer) GetResult() (json.RawMessage, error) { } // Add the additional fields - resultMap["lastOp"] = t.lastOp resultMap["lastThreeOpCodes"] = t.lastThreeOpCodes resultMap["keccak"] = t.Keccak @@ -371,14 +376,6 @@ func (t *erc7562Tracer) GetResult() (json.RawMessage, error) { if err != nil { return nil, err } - if t.callstackWithOpcodes[0].OutOfGas { - log.Error("WTF TOP GETRESULT OOG?") - } - for i := range t.callstackWithOpcodes[0].Calls { - if t.callstackWithOpcodes[0].Calls[i].OutOfGas { - log.Error("WTF GETRESULT OOG?") - } - } return finalJSON, t.reason } @@ -407,31 +404,51 @@ func (t *erc7562Tracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tra opcode := vm.OpCode(op) stackSize := len(scope.StackData()) - stackTop3 := partialStack{} + var stackTop3 []uint256.Int for i := 0; i < 3 && i < stackSize; i++ { - stackTop3 = append(stackTop3, peepStack(scope.StackData(), i)) + stackTop3 = append(stackTop3, *peepStack(scope.StackData(), i)) } - t.lastThreeOpCodes = append(t.lastThreeOpCodes, opcode) + t.lastThreeOpCodes = append(t.lastThreeOpCodes, &opcodeWithPartialStack{ + Opcode: opcode, + StackTop3: stackTop3, + }) if len(t.lastThreeOpCodes) > 3 { t.lastThreeOpCodes = t.lastThreeOpCodes[1:] } + t.handleReturnRevert(opcode) + var lastOpWithStack *opcodeWithPartialStack + if len(t.lastThreeOpCodes) >= 2 { + lastOpWithStack = t.lastThreeOpCodes[len(t.lastThreeOpCodes)-2] + } size := len(t.callstackWithOpcodes) currentCallFrame := &t.callstackWithOpcodes[size-1] t.detectOutOfGas(gas, cost, opcode, currentCallFrame) - t.handleExtOpcodes(opcode, currentCallFrame, stackTop3) + if lastOpWithStack != nil { + t.handleExtOpcodes(lastOpWithStack, opcode, currentCallFrame) + } t.handleAccessedContractSize(opcode, scope, currentCallFrame) - t.handleGasObserved(opcode, currentCallFrame) + if lastOpWithStack != nil { + t.handleGasObserved(lastOpWithStack.Opcode, opcode, currentCallFrame) + } t.storeUsedOpcode(opcode, currentCallFrame) t.handleStorageAccess(opcode, scope, currentCallFrame) t.storeKeccak(opcode, scope) - t.lastOp = opcode + //t.lastOp = opcode //t.handleLogs(opcode, scope) } -func (t *erc7562Tracer) handleGasObserved(opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes) { +func (t *erc7562Tracer) handleReturnRevert(opcode vm.OpCode) { + if opcode == vm.REVERT || opcode == vm.RETURN { + if len(t.lastThreeOpCodes) > 0 { + t.lastThreeOpCodes = t.lastThreeOpCodes[len(t.lastThreeOpCodes)-1:] + } + } +} + +func (t *erc7562Tracer) handleGasObserved(lastOp vm.OpCode, opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes) { // [OP-012] isCall := opcode == vm.CALL || opcode == vm.STATICCALL || opcode == vm.DELEGATECALL || opcode == vm.CALLCODE - pendingGasObserved := t.lastOp == vm.GAS && !isCall + pendingGasObserved := lastOp == vm.GAS && !isCall if pendingGasObserved { //currentCallFrame.GasObserved = true currentCallFrame.UsedOpcodes[vm.GAS] = true @@ -481,25 +498,21 @@ func (t *erc7562Tracer) storeKeccak(opcode vm.OpCode, scope tracing.OpContext) { } func (t *erc7562Tracer) detectOutOfGas(gas uint64, cost uint64, opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes) { - if gas < cost { - log.Error("WTF OOG?") - } if gas < cost || (opcode == vm.SSTORE && gas < 2300) { currentCallFrame.OutOfGas = true - log.Error("WTF SSTORE OOG?", currentCallFrame.OutOfGas) } } -// TODO: rewrite using byte opcode values, without relying on string manipulations -func (t *erc7562Tracer) handleExtOpcodes(opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes, stackTop3 partialStack) { - if isEXT(t.lastOp) { - addr := common.HexToAddress(stackTop3[0].Hex()) +func (t *erc7562Tracer) handleExtOpcodes(lastOpInfo *opcodeWithPartialStack, opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes) { + if isEXT(lastOpInfo.Opcode) { + addr := common.HexToAddress(lastOpInfo.StackTop3[0].Hex()) // TODO: THIS CODE SEEMS TO BE EQUIVALENT TO THE STRING BASED BUT IT IS LOGICALLY WRONG AND WILL NEVER WORK. // TODO: INSTEAD WE MAY NEED TO DEFER ADDING ADDRESS TO 'ExtCodeAccessInfo' UNTIL AFTER THE 'ISZERO' CHECK. // only store the last EXTCODE* opcode per address - could even be a boolean for our current use-case // [OP-051] - if !(t.lastThreeOpCodes[1] == vm.EXTCODESIZE && t.lastThreeOpCodes[2] == vm.ISZERO) { + + if !(t.lastThreeOpCodes[1].Opcode == vm.EXTCODESIZE && t.lastThreeOpCodes[2].Opcode == vm.ISZERO) { currentCallFrame.ExtCodeAccessInfo = append(currentCallFrame.ExtCodeAccessInfo, addr) } } From 662847e2a4ae6db09a0b81dada8033d9e8ed63df Mon Sep 17 00:00:00 2001 From: shahafn Date: Thu, 19 Dec 2024 13:34:40 +0200 Subject: [PATCH 17/25] Changing UsedOpcodes type --- eth/tracers/native/erc7562.go | 21 ++++++++----------- .../native/gen_callframewithopcodes_json.go | 4 ++-- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index be64f9c8b462..383cbd8bb1fb 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -63,10 +63,10 @@ type callFrameWithOpcodes struct { Value *big.Int `json:"value,omitempty" rlp:"optional"` revertedSnapshot bool - AccessedSlots accessedSlots `json:"accessedSlots"` - ExtCodeAccessInfo []common.Address `json:"extCodeAccessInfo"` - DeployedContracts []common.Address `json:"deployedContracts"` - UsedOpcodes map[vm.OpCode]bool `json:"usedOpcodes"` + AccessedSlots accessedSlots `json:"accessedSlots"` + ExtCodeAccessInfo []common.Address `json:"extCodeAccessInfo"` + DeployedContracts []common.Address `json:"deployedContracts"` + UsedOpcodes map[vm.OpCode]uint64 `json:"usedOpcodes"` //GasObserved bool `json:"gasObserved"` ContractSize map[common.Address]*contractSizeWithOpcode `json:"contractSize"` OutOfGas bool `json:"outOfGas"` @@ -247,7 +247,7 @@ func (t *erc7562Tracer) OnEnter(depth int, typ byte, from common.Address, to com TransientReads: map[string]uint64{}, TransientWrites: map[string]uint64{}, }, - UsedOpcodes: make(map[vm.OpCode]bool, 3), + UsedOpcodes: map[vm.OpCode]uint64{}, ExtCodeAccessInfo: make([]common.Address, 0), ContractSize: map[common.Address]*contractSizeWithOpcode{}, } @@ -451,14 +451,14 @@ func (t *erc7562Tracer) handleGasObserved(lastOp vm.OpCode, opcode vm.OpCode, cu pendingGasObserved := lastOp == vm.GAS && !isCall if pendingGasObserved { //currentCallFrame.GasObserved = true - currentCallFrame.UsedOpcodes[vm.GAS] = true + incrementCount(currentCallFrame.UsedOpcodes, vm.GAS) } } func (t *erc7562Tracer) storeUsedOpcode(opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes) { // ignore "unimportant" opcodes if opcode != vm.GAS && !t.isIgnoredOpcode(opcode) { - currentCallFrame.UsedOpcodes[opcode] = true + incrementCount(currentCallFrame.UsedOpcodes, opcode) } } @@ -590,9 +590,6 @@ func isAllowedPrecompile(addr common.Address) bool { return addrInt.Cmp(big.NewInt(0)) == 1 && addrInt.Cmp(big.NewInt(10)) == -1 } -func incrementCount(m map[string]uint64, k string) { - if _, ok := m[k]; !ok { - m[k] = 0 - } - m[k]++ +func incrementCount[K comparable](m map[K]uint64, k K) { + m[k] = m[k] + 1 } diff --git a/eth/tracers/native/gen_callframewithopcodes_json.go b/eth/tracers/native/gen_callframewithopcodes_json.go index 140167c9d707..d270a9811b3b 100644 --- a/eth/tracers/native/gen_callframewithopcodes_json.go +++ b/eth/tracers/native/gen_callframewithopcodes_json.go @@ -30,7 +30,7 @@ func (c callFrameWithOpcodes) MarshalJSON() ([]byte, error) { AccessedSlots accessedSlots `json:"accessedSlots"` ExtCodeAccessInfo []common.Address `json:"extCodeAccessInfo"` DeployedContracts []common.Address `json:"deployedContracts"` - UsedOpcodes map[vm.OpCode]bool `json:"usedOpcodes"` + UsedOpcodes map[vm.OpCode]uint64 `json:"usedOpcodes"` ContractSize map[common.Address]*contractSizeWithOpcode `json:"contractSize"` OutOfGas bool `json:"outOfGas"` Calls []callFrameWithOpcodes `json:"calls,omitempty" rlp:"optional"` @@ -76,7 +76,7 @@ func (c *callFrameWithOpcodes) UnmarshalJSON(input []byte) error { AccessedSlots *accessedSlots `json:"accessedSlots"` ExtCodeAccessInfo []common.Address `json:"extCodeAccessInfo"` DeployedContracts []common.Address `json:"deployedContracts"` - UsedOpcodes map[vm.OpCode]bool `json:"usedOpcodes"` + UsedOpcodes map[vm.OpCode]uint64 `json:"usedOpcodes"` ContractSize map[common.Address]*contractSizeWithOpcode `json:"contractSize"` OutOfGas *bool `json:"outOfGas"` Calls []callFrameWithOpcodes `json:"calls,omitempty" rlp:"optional"` From 6c94b38c12c3d76d82b689bba5e5fe6ff4494c83 Mon Sep 17 00:00:00 2001 From: shahafn Date: Thu, 19 Dec 2024 15:30:29 +0200 Subject: [PATCH 18/25] Fixing ignoredOpcodes --- eth/tracers/native/erc7562.go | 63 ++++++++++++++++------------------- 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index 383cbd8bb1fb..fb336902967a 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -46,7 +46,6 @@ type contractSizeWithOpcode struct { } type callFrameWithOpcodes struct { - //callFrame Type vm.OpCode `json:"-"` From common.Address `json:"from"` Gas uint64 `json:"gas"` @@ -56,21 +55,19 @@ type callFrameWithOpcodes struct { Output []byte `json:"output,omitempty" rlp:"optional"` 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 *big.Int `json:"value,omitempty" rlp:"optional"` revertedSnapshot bool - AccessedSlots accessedSlots `json:"accessedSlots"` - ExtCodeAccessInfo []common.Address `json:"extCodeAccessInfo"` - DeployedContracts []common.Address `json:"deployedContracts"` - UsedOpcodes map[vm.OpCode]uint64 `json:"usedOpcodes"` - //GasObserved bool `json:"gasObserved"` - ContractSize map[common.Address]*contractSizeWithOpcode `json:"contractSize"` - OutOfGas bool `json:"outOfGas"` - Calls []callFrameWithOpcodes `json:"calls,omitempty" rlp:"optional"` + AccessedSlots accessedSlots `json:"accessedSlots"` + ExtCodeAccessInfo []common.Address `json:"extCodeAccessInfo"` + DeployedContracts []common.Address `json:"deployedContracts"` + UsedOpcodes map[vm.OpCode]uint64 `json:"usedOpcodes"` + ContractSize map[common.Address]*contractSizeWithOpcode `json:"contractSize"` + OutOfGas bool `json:"outOfGas"` + Calls []callFrameWithOpcodes `json:"calls,omitempty" rlp:"optional"` } func (f callFrameWithOpcodes) TypeString() string { @@ -135,7 +132,7 @@ type erc7562Tracer struct { callTracer env *tracing.VMContext - ignoredOpcodes []vm.OpCode + ignoredOpcodes map[vm.OpCode]struct{} callstackWithOpcodes []callFrameWithOpcodes lastThreeOpCodes []*opcodeWithPartialStack Keccak []hexutil.Bytes `json:"keccak"` @@ -154,7 +151,7 @@ func catchPanic() { } } -// newCallTracer returns a native go tracer which tracks +// newErc7562Tracer returns a native go tracer which tracks // call frames of a tx, and implements vm.EVMLogger. func newErc7562Tracer(ctx *tracers.Context, cfg json.RawMessage /*, chainConfig *params.ChainConfig*/) (*tracers.Tracer, error) { t, err := newErc7562TracerObject(ctx, cfg) @@ -176,15 +173,15 @@ func newErc7562Tracer(ctx *tracers.Context, cfg json.RawMessage /*, chainConfig } type erc7562TracerConfig struct { - IgnoredOpcodes *string `json:"ignoredOpcodes"` // If true, call tracer won't collect any subcalls - WithLog bool `json:"withLog"` // If true, call tracer will collect event logs + IgnoredOpcodes *string `json:"ignoredOpcodes"` // Opcodes to ignore during OnOpcode hook execution + WithLog bool `json:"withLog"` // If true, erc7562 tracer will collect event logs } -// Function to convert byte array to []vm.OpCode -func ConvertBytesToOpCodes(byteArray []byte) []vm.OpCode { - opCodes := make([]vm.OpCode, len(byteArray)) - for i, b := range byteArray { - opCodes[i] = vm.OpCode(b) +// Function to convert byte array to map[vm.OpCode]struct{} +func ConvertBytesToOpCodes(byteArray []byte) map[vm.OpCode]struct{} { + var opCodes map[vm.OpCode]struct{} + for _, b := range byteArray { + opCodes[vm.OpCode(b)] = struct{}{} } return opCodes } @@ -196,7 +193,7 @@ func newErc7562TracerObject(ctx *tracers.Context, cfg json.RawMessage) (*erc7562 return nil, err } } - var ignoredOpcodes []vm.OpCode + var ignoredOpcodes map[vm.OpCode]struct{} if config.IgnoredOpcodes == nil { ignoredOpcodes = defaultIgnoredOpcodes() } else { @@ -368,7 +365,6 @@ func (t *erc7562Tracer) GetResult() (json.RawMessage, error) { } // Add the additional fields - resultMap["lastThreeOpCodes"] = t.lastThreeOpCodes resultMap["keccak"] = t.Keccak // Marshal the final map back to JSON @@ -450,7 +446,6 @@ func (t *erc7562Tracer) handleGasObserved(lastOp vm.OpCode, opcode vm.OpCode, cu isCall := opcode == vm.CALL || opcode == vm.STATICCALL || opcode == vm.DELEGATECALL || opcode == vm.CALLCODE pendingGasObserved := lastOp == vm.GAS && !isCall if pendingGasObserved { - //currentCallFrame.GasObserved = true incrementCount(currentCallFrame.UsedOpcodes, vm.GAS) } } @@ -555,32 +550,30 @@ func isEXT(opcode vm.OpCode) bool { // Check if this opcode is ignored for the purposes of generating the used opcodes report func (t *erc7562Tracer) isIgnoredOpcode(opcode vm.OpCode) bool { - for _, ignoredOpcode := range t.ignoredOpcodes { - if opcode == ignoredOpcode { - return true - } + if _, ok := t.ignoredOpcodes[opcode]; ok { + return true } return false } -func defaultIgnoredOpcodes() []vm.OpCode { - i := 0 - ignoredOpcodes := make([]vm.OpCode, 100) // TODO count real length +func defaultIgnoredOpcodes() map[vm.OpCode]struct{} { + ignored := make(map[vm.OpCode]struct{}) + // Allow all PUSHx, DUPx and SWAPx opcodes as they have sequential codes for op := vm.PUSH0; op < vm.SWAP16; op++ { - ignoredOpcodes[i] = op - i++ + ignored[op] = struct{}{} } + for _, op := range []vm.OpCode{ vm.POP, vm.ADD, vm.SUB, vm.MUL, vm.DIV, vm.EQ, vm.LT, vm.GT, vm.SLT, vm.SGT, vm.SHL, vm.SHR, vm.AND, vm.OR, vm.NOT, vm.ISZERO, } { - ignoredOpcodes[i] = op - i++ + ignored[op] = struct{}{} } - return ignoredOpcodes + + return ignored } // not using 'isPrecompiled' to only allow the ones defined by the ERC-7562 as stateless precompiles From 9bc24ba528a3b7328086fd9941a943e2b2664bfc Mon Sep 17 00:00:00 2001 From: shahafn Date: Thu, 19 Dec 2024 16:03:52 +0200 Subject: [PATCH 19/25] Adding isCall(), fixing PR comments --- eth/tracers/native/erc7562.go | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index fb336902967a..0dd731bd5b88 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -299,7 +299,7 @@ func (t *erc7562Tracer) OnTxEnd(receipt *types.Receipt, err error) { t.callstackWithOpcodes[0].GasUsed = receipt.GasUsed if t.config.WithLog { // Logs are not emitted when the call fails - clearFailedLogs2(&t.callstackWithOpcodes[0], false) + t.clearFailedLogs(&t.callstackWithOpcodes[0], false) } } @@ -384,14 +384,14 @@ func (t *erc7562Tracer) Stop(err error) { // clearFailedLogs clears the logs of a callframe and all its children // in case of execution failure. -func clearFailedLogs2(cf *callFrameWithOpcodes, parentFailed bool) { +func (t *erc7562Tracer) clearFailedLogs(cf *callFrameWithOpcodes, parentFailed bool) { failed := cf.failed() || parentFailed // Clear own logs if failed { cf.Logs = nil } for i := range cf.Calls { - clearFailedLogs2(&cf.Calls[i], failed) + t.clearFailedLogs(&cf.Calls[i], failed) } } @@ -429,8 +429,6 @@ func (t *erc7562Tracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tra t.storeUsedOpcode(opcode, currentCallFrame) t.handleStorageAccess(opcode, scope, currentCallFrame) t.storeKeccak(opcode, scope) - //t.lastOp = opcode - //t.handleLogs(opcode, scope) } func (t *erc7562Tracer) handleReturnRevert(opcode vm.OpCode) { @@ -443,8 +441,7 @@ func (t *erc7562Tracer) handleReturnRevert(opcode vm.OpCode) { func (t *erc7562Tracer) handleGasObserved(lastOp vm.OpCode, opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes) { // [OP-012] - isCall := opcode == vm.CALL || opcode == vm.STATICCALL || opcode == vm.DELEGATECALL || opcode == vm.CALLCODE - pendingGasObserved := lastOp == vm.GAS && !isCall + pendingGasObserved := lastOp == vm.GAS && !isCall(opcode) if pendingGasObserved { incrementCount(currentCallFrame.UsedOpcodes, vm.GAS) } @@ -502,8 +499,6 @@ func (t *erc7562Tracer) handleExtOpcodes(lastOpInfo *opcodeWithPartialStack, opc if isEXT(lastOpInfo.Opcode) { addr := common.HexToAddress(lastOpInfo.StackTop3[0].Hex()) - // TODO: THIS CODE SEEMS TO BE EQUIVALENT TO THE STRING BASED BUT IT IS LOGICALLY WRONG AND WILL NEVER WORK. - // TODO: INSTEAD WE MAY NEED TO DEFER ADDING ADDRESS TO 'ExtCodeAccessInfo' UNTIL AFTER THE 'ISZERO' CHECK. // only store the last EXTCODE* opcode per address - could even be a boolean for our current use-case // [OP-051] @@ -535,11 +530,7 @@ func peepStack(stackData []uint256.Int, n int) *uint256.Int { } func isEXTorCALL(opcode vm.OpCode) bool { - return isEXT(opcode) || - opcode == vm.CALL || - opcode == vm.CALLCODE || - opcode == vm.DELEGATECALL || - opcode == vm.STATICCALL + return isEXT(opcode) || isCall(opcode) } func isEXT(opcode vm.OpCode) bool { @@ -548,6 +539,13 @@ func isEXT(opcode vm.OpCode) bool { opcode == vm.EXTCODECOPY } +func isCall(opcode vm.OpCode) bool { + return opcode == vm.CALL || + opcode == vm.CALLCODE || + opcode == vm.DELEGATECALL || + opcode == vm.STATICCALL +} + // Check if this opcode is ignored for the purposes of generating the used opcodes report func (t *erc7562Tracer) isIgnoredOpcode(opcode vm.OpCode) bool { if _, ok := t.ignoredOpcodes[opcode]; ok { From c63983a136f3d2f1891f6bbf3fa8a0821e75067d Mon Sep 17 00:00:00 2001 From: shahafn Date: Thu, 19 Dec 2024 18:59:54 +0200 Subject: [PATCH 20/25] Remving callTracer, callTracerConfig, adding erc7562TracerConfig --- eth/tracers/native/erc7562.go | 91 ++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index 0dd731bd5b88..0482165ddfd3 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -28,10 +28,10 @@ import ( "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/log" "github.com/holiman/uint256" - "github.com/status-im/keycard-go/hexutils" "math/big" "runtime" "runtime/debug" + "sync/atomic" ) //go:generate go run github.com/fjl/gencodec -type callFrameWithOpcodes -field-override callFrameWithOpcodesMarshaling -out gen_callframewithopcodes_json.go @@ -115,7 +115,6 @@ type callFrameWithOpcodesMarshaling struct { Output hexutil.Bytes } -// TODO: I suggest that we provide an `[]string` for all of these fields. Doing it for `Reads` as an example here. type accessedSlots struct { Reads map[string][]string `json:"reads"` Writes map[string]uint64 `json:"writes"` @@ -124,17 +123,21 @@ type accessedSlots struct { } type opcodeWithPartialStack struct { - Opcode vm.OpCode - StackTop3 []uint256.Int + Opcode vm.OpCode + StackTopItems []uint256.Int } type erc7562Tracer struct { - callTracer - env *tracing.VMContext + config erc7562TracerConfig + gasLimit uint64 + depth int + interrupt atomic.Bool // Atomic flag to signal execution interruption + reason error // Textual reason for the interruption + env *tracing.VMContext ignoredOpcodes map[vm.OpCode]struct{} callstackWithOpcodes []callFrameWithOpcodes - lastThreeOpCodes []*opcodeWithPartialStack + lastSeenOpcodes []*opcodeWithPartialStack Keccak []hexutil.Bytes `json:"keccak"` } @@ -173,8 +176,10 @@ func newErc7562Tracer(ctx *tracers.Context, cfg json.RawMessage /*, chainConfig } type erc7562TracerConfig struct { - IgnoredOpcodes *string `json:"ignoredOpcodes"` // Opcodes to ignore during OnOpcode hook execution - WithLog bool `json:"withLog"` // If true, erc7562 tracer will collect event logs + LastSeenOpcodesSize int `json:"iastSeenOpcodesSize"` + StackTopItemsSize int `json:"stackTopItemsSize"` + IgnoredOpcodes map[vm.OpCode]struct{} `json:"ignoredOpcodes"` // Opcodes to ignore during OnOpcode hook execution + WithLog bool `json:"withLog"` // If true, erc7562 tracer will collect event logs } // Function to convert byte array to map[vm.OpCode]struct{} @@ -186,6 +191,22 @@ func ConvertBytesToOpCodes(byteArray []byte) map[vm.OpCode]struct{} { return opCodes } +func getFullConfiguration(partial erc7562TracerConfig) erc7562TracerConfig { + config := partial + + if config.IgnoredOpcodes == nil { + config.IgnoredOpcodes = defaultIgnoredOpcodes() + } + if config.LastSeenOpcodesSize == 0 { + config.LastSeenOpcodesSize = 3 + } + if config.StackTopItemsSize == 0 { + config.StackTopItemsSize = 3 + } + + return config +} + func newErc7562TracerObject(ctx *tracers.Context, cfg json.RawMessage) (*erc7562Tracer, error) { var config erc7562TracerConfig if cfg != nil { @@ -193,25 +214,12 @@ func newErc7562TracerObject(ctx *tracers.Context, cfg json.RawMessage) (*erc7562 return nil, err } } - var ignoredOpcodes map[vm.OpCode]struct{} - if config.IgnoredOpcodes == nil { - ignoredOpcodes = defaultIgnoredOpcodes() - } else { - ignoredOpcodes = ConvertBytesToOpCodes(hexutils.HexToBytes(*config.IgnoredOpcodes)) - } // First callframe contains tx context info // and is populated on start and end. return &erc7562Tracer{ callstackWithOpcodes: make([]callFrameWithOpcodes, 0, 1), - lastThreeOpCodes: make([]*opcodeWithPartialStack, 0), - ignoredOpcodes: ignoredOpcodes, - callTracer: callTracer{ - config: callTracerConfig{ - WithLog: config.WithLog, - // Disabling the 'OnlyTopCall' option as it defeats the purpose of ERC-7562 - OnlyTopCall: false, - }, - }, + lastSeenOpcodes: make([]*opcodeWithPartialStack, 0), + config: getFullConfiguration(config), }, nil } @@ -271,9 +279,6 @@ func (t *erc7562Tracer) OnExit(depth int, output []byte, gasUsed uint64, err err } t.depth = depth - 1 - if t.config.OnlyTopCall { - return - } size := len(t.callstackWithOpcodes) if size <= 1 { @@ -309,10 +314,6 @@ func (t *erc7562Tracer) OnLog(log1 *types.Log) { if !t.config.WithLog { return } - // Avoid processing nested calls when only caring about top call - if t.config.OnlyTopCall && t.depth > 0 { - return - } // Skip if tracing was interrupted if t.interrupt.Load() { return @@ -400,21 +401,21 @@ func (t *erc7562Tracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tra opcode := vm.OpCode(op) stackSize := len(scope.StackData()) - var stackTop3 []uint256.Int - for i := 0; i < 3 && i < stackSize; i++ { - stackTop3 = append(stackTop3, *peepStack(scope.StackData(), i)) + var stackTopItems []uint256.Int + for i := 0; i < t.config.StackTopItemsSize && i < stackSize; i++ { + stackTopItems = append(stackTopItems, *peepStack(scope.StackData(), i)) } - t.lastThreeOpCodes = append(t.lastThreeOpCodes, &opcodeWithPartialStack{ - Opcode: opcode, - StackTop3: stackTop3, + t.lastSeenOpcodes = append(t.lastSeenOpcodes, &opcodeWithPartialStack{ + Opcode: opcode, + StackTopItems: stackTopItems, }) - if len(t.lastThreeOpCodes) > 3 { - t.lastThreeOpCodes = t.lastThreeOpCodes[1:] + if len(t.lastSeenOpcodes) > t.config.LastSeenOpcodesSize { + t.lastSeenOpcodes = t.lastSeenOpcodes[1:] } t.handleReturnRevert(opcode) var lastOpWithStack *opcodeWithPartialStack - if len(t.lastThreeOpCodes) >= 2 { - lastOpWithStack = t.lastThreeOpCodes[len(t.lastThreeOpCodes)-2] + if len(t.lastSeenOpcodes) >= t.config.LastSeenOpcodesSize-1 { + lastOpWithStack = t.lastSeenOpcodes[len(t.lastSeenOpcodes)-2] } size := len(t.callstackWithOpcodes) currentCallFrame := &t.callstackWithOpcodes[size-1] @@ -433,8 +434,8 @@ func (t *erc7562Tracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tra func (t *erc7562Tracer) handleReturnRevert(opcode vm.OpCode) { if opcode == vm.REVERT || opcode == vm.RETURN { - if len(t.lastThreeOpCodes) > 0 { - t.lastThreeOpCodes = t.lastThreeOpCodes[len(t.lastThreeOpCodes)-1:] + if len(t.lastSeenOpcodes) > 0 { + t.lastSeenOpcodes = t.lastSeenOpcodes[len(t.lastSeenOpcodes)-1:] } } } @@ -497,12 +498,12 @@ func (t *erc7562Tracer) detectOutOfGas(gas uint64, cost uint64, opcode vm.OpCode func (t *erc7562Tracer) handleExtOpcodes(lastOpInfo *opcodeWithPartialStack, opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes) { if isEXT(lastOpInfo.Opcode) { - addr := common.HexToAddress(lastOpInfo.StackTop3[0].Hex()) + addr := common.HexToAddress(lastOpInfo.StackTopItems[0].Hex()) // only store the last EXTCODE* opcode per address - could even be a boolean for our current use-case // [OP-051] - if !(t.lastThreeOpCodes[1].Opcode == vm.EXTCODESIZE && t.lastThreeOpCodes[2].Opcode == vm.ISZERO) { + if !(t.lastSeenOpcodes[1].Opcode == vm.EXTCODESIZE && t.lastSeenOpcodes[2].Opcode == vm.ISZERO) { currentCallFrame.ExtCodeAccessInfo = append(currentCallFrame.ExtCodeAccessInfo, addr) } } From 67ea9bbd3425ca446cba7088c1f87491eb7cde7a Mon Sep 17 00:00:00 2001 From: shahafn Date: Thu, 19 Dec 2024 19:55:53 +0200 Subject: [PATCH 21/25] Detecting OOG in OnExit instead of OnOpcode --- eth/tracers/native/erc7562.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index 0482165ddfd3..e477ffe6d7a9 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -289,6 +289,9 @@ func (t *erc7562Tracer) OnExit(depth int, output []byte, gasUsed uint64, err err t.callstackWithOpcodes = t.callstackWithOpcodes[:size-1] size -= 1 + if reverted && errors.Is(err, vm.ErrCodeStoreOutOfGas) || errors.Is(err, vm.ErrOutOfGas) { + call.OutOfGas = true + } call.GasUsed = gasUsed call.processOutput(output, err, reverted) // Nest call into parent. @@ -419,7 +422,6 @@ func (t *erc7562Tracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tra } size := len(t.callstackWithOpcodes) currentCallFrame := &t.callstackWithOpcodes[size-1] - t.detectOutOfGas(gas, cost, opcode, currentCallFrame) if lastOpWithStack != nil { t.handleExtOpcodes(lastOpWithStack, opcode, currentCallFrame) } @@ -490,12 +492,6 @@ func (t *erc7562Tracer) storeKeccak(opcode vm.OpCode, scope tracing.OpContext) { } } -func (t *erc7562Tracer) detectOutOfGas(gas uint64, cost uint64, opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes) { - if gas < cost || (opcode == vm.SSTORE && gas < 2300) { - currentCallFrame.OutOfGas = true - } -} - func (t *erc7562Tracer) handleExtOpcodes(lastOpInfo *opcodeWithPartialStack, opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes) { if isEXT(lastOpInfo.Opcode) { addr := common.HexToAddress(lastOpInfo.StackTopItems[0].Hex()) From 77aa7041d07aa9f156096ecd5a19b8ce92f12dca Mon Sep 17 00:00:00 2001 From: shahafn Date: Sun, 22 Dec 2024 22:41:02 +0200 Subject: [PATCH 22/25] Removing unused function --- eth/tracers/native/erc7562.go | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index e477ffe6d7a9..0c4e50cb8aa9 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -330,28 +330,6 @@ func (t *erc7562Tracer) OnLog(log1 *types.Log) { t.callstackWithOpcodes[len(t.callstackWithOpcodes)-1].Logs = append(t.callstackWithOpcodes[len(t.callstackWithOpcodes)-1].Logs, l) } -func (t *erc7562Tracer) handleLogs(opcode vm.OpCode, scope tracing.OpContext) { - defer catchPanic() - if opcode == vm.LOG0 || opcode == vm.LOG1 || opcode == vm.LOG2 || opcode == vm.LOG3 || opcode == vm.LOG4 { - } - // count := int(opcode - vm.LOG0) - // ofs := peepStack(scope.StackData(), 0) - // len := peepStack(scope.StackData(), 1) - // memory := scope.MemoryData() - // topics := []hexutil.Bytes{} - // for i := 0; i < count; i++ { - // topics = append(topics, peepStack(scope.StackData(), 2+i).Bytes()) - // //topics = append(topics, scope.Stack.Back(2+i).Bytes()) - // } - // log := make([]byte, len.Uint64()) - // copy(log, memory[ofs.Uint64():ofs.Uint64()+len.Uint64()]) - // t.Logs = append(t.Logs, &logsItem{ - // Data: log, - // Topic: topics, - // }) - //} -} - // GetResult returns the json-encoded nested list of call traces, and any // error arising from the encoding or forceful termination (via `Stop`). func (t *erc7562Tracer) GetResult() (json.RawMessage, error) { From 7eef00b3ac5d43c226d847a530548ed9c1525f65 Mon Sep 17 00:00:00 2001 From: shahafn Date: Sun, 22 Dec 2024 22:43:14 +0200 Subject: [PATCH 23/25] Removing unused --- eth/tracers/native/erc7562.go | 3 +-- eth/tracers/native/gen_callframewithopcodes_json.go | 6 ------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index 0c4e50cb8aa9..e632961349eb 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -63,7 +63,6 @@ type callFrameWithOpcodes struct { AccessedSlots accessedSlots `json:"accessedSlots"` ExtCodeAccessInfo []common.Address `json:"extCodeAccessInfo"` - DeployedContracts []common.Address `json:"deployedContracts"` UsedOpcodes map[vm.OpCode]uint64 `json:"usedOpcodes"` ContractSize map[common.Address]*contractSizeWithOpcode `json:"contractSize"` OutOfGas bool `json:"outOfGas"` @@ -453,7 +452,7 @@ func (t *erc7562Tracer) handleStorageAccess(opcode vm.OpCode, scope tracing.OpCo incrementCount(currentCallFrame.AccessedSlots.Writes, slotHex) } else if opcode == vm.TLOAD { incrementCount(currentCallFrame.AccessedSlots.TransientReads, slotHex) - } else if opcode == vm.TSTORE { + } else { incrementCount(currentCallFrame.AccessedSlots.TransientWrites, slotHex) } } diff --git a/eth/tracers/native/gen_callframewithopcodes_json.go b/eth/tracers/native/gen_callframewithopcodes_json.go index d270a9811b3b..1602eb2a2e72 100644 --- a/eth/tracers/native/gen_callframewithopcodes_json.go +++ b/eth/tracers/native/gen_callframewithopcodes_json.go @@ -29,7 +29,6 @@ func (c callFrameWithOpcodes) MarshalJSON() ([]byte, error) { Value *hexutil.Big `json:"value,omitempty" rlp:"optional"` AccessedSlots accessedSlots `json:"accessedSlots"` ExtCodeAccessInfo []common.Address `json:"extCodeAccessInfo"` - DeployedContracts []common.Address `json:"deployedContracts"` UsedOpcodes map[vm.OpCode]uint64 `json:"usedOpcodes"` ContractSize map[common.Address]*contractSizeWithOpcode `json:"contractSize"` OutOfGas bool `json:"outOfGas"` @@ -50,7 +49,6 @@ func (c callFrameWithOpcodes) MarshalJSON() ([]byte, error) { enc.Value = (*hexutil.Big)(c.Value) enc.AccessedSlots = c.AccessedSlots enc.ExtCodeAccessInfo = c.ExtCodeAccessInfo - enc.DeployedContracts = c.DeployedContracts enc.UsedOpcodes = c.UsedOpcodes enc.ContractSize = c.ContractSize enc.OutOfGas = c.OutOfGas @@ -75,7 +73,6 @@ func (c *callFrameWithOpcodes) UnmarshalJSON(input []byte) error { Value *hexutil.Big `json:"value,omitempty" rlp:"optional"` AccessedSlots *accessedSlots `json:"accessedSlots"` ExtCodeAccessInfo []common.Address `json:"extCodeAccessInfo"` - DeployedContracts []common.Address `json:"deployedContracts"` UsedOpcodes map[vm.OpCode]uint64 `json:"usedOpcodes"` ContractSize map[common.Address]*contractSizeWithOpcode `json:"contractSize"` OutOfGas *bool `json:"outOfGas"` @@ -124,9 +121,6 @@ func (c *callFrameWithOpcodes) UnmarshalJSON(input []byte) error { if dec.ExtCodeAccessInfo != nil { c.ExtCodeAccessInfo = dec.ExtCodeAccessInfo } - if dec.DeployedContracts != nil { - c.DeployedContracts = dec.DeployedContracts - } if dec.UsedOpcodes != nil { c.UsedOpcodes = dec.UsedOpcodes } From a8606a6f10fd51a348c2f56f4a1ef93a6ee30a54 Mon Sep 17 00:00:00 2001 From: shahafn Date: Sun, 22 Dec 2024 23:54:48 +0200 Subject: [PATCH 24/25] Changing Keccak member from array to mapping --- eth/tracers/native/erc7562.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index e632961349eb..b219ee5d901c 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -137,7 +137,7 @@ type erc7562Tracer struct { ignoredOpcodes map[vm.OpCode]struct{} callstackWithOpcodes []callFrameWithOpcodes lastSeenOpcodes []*opcodeWithPartialStack - Keccak []hexutil.Bytes `json:"keccak"` + Keccak map[string]struct{} `json:"keccak"` } // catchPanic handles panic recovery and logs the panic and stack trace. @@ -218,6 +218,7 @@ func newErc7562TracerObject(ctx *tracers.Context, cfg json.RawMessage) (*erc7562 return &erc7562Tracer{ callstackWithOpcodes: make([]callFrameWithOpcodes, 0, 1), lastSeenOpcodes: make([]*opcodeWithPartialStack, 0), + Keccak: make(map[string]struct{}), config: getFullConfiguration(config), }, nil } @@ -288,7 +289,7 @@ func (t *erc7562Tracer) OnExit(depth int, output []byte, gasUsed uint64, err err t.callstackWithOpcodes = t.callstackWithOpcodes[:size-1] size -= 1 - if reverted && errors.Is(err, vm.ErrCodeStoreOutOfGas) || errors.Is(err, vm.ErrOutOfGas) { + if errors.Is(err, vm.ErrCodeStoreOutOfGas) || errors.Is(err, vm.ErrOutOfGas) { call.OutOfGas = true } call.GasUsed = gasUsed @@ -345,8 +346,14 @@ func (t *erc7562Tracer) GetResult() (json.RawMessage, error) { return nil, err } - // Add the additional fields - resultMap["keccak"] = t.Keccak + // Converting keccak mapping to array + keccakArray := make([]hexutil.Bytes, len(t.Keccak)) + i := 0 + for k := range t.Keccak { + keccakArray[i] = hexutil.Bytes(k) + i++ + } + resultMap["keccak"] = keccakArray // Marshal the final map back to JSON finalJSON, err := json.Marshal(resultMap) @@ -465,7 +472,7 @@ func (t *erc7562Tracer) storeKeccak(opcode vm.OpCode, scope tracing.OpContext) { memory := scope.MemoryData() keccak := make([]byte, dataLength) copy(keccak, memory[dataOffset:dataOffset+dataLength]) - t.Keccak = append(t.Keccak, keccak) + t.Keccak[string(keccak)] = struct{}{} } } From d839b832f1999fcf6df6d0e59cee1ffa6a92e822 Mon Sep 17 00:00:00 2001 From: shahafn Date: Mon, 23 Dec 2024 12:01:21 +0200 Subject: [PATCH 25/25] Replacing lastSeenOpcodes for lastOpWithStack --- eth/tracers/native/erc7562.go | 49 +++++++++++++---------------------- 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index b219ee5d901c..276f33aa1329 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -136,7 +136,7 @@ type erc7562Tracer struct { ignoredOpcodes map[vm.OpCode]struct{} callstackWithOpcodes []callFrameWithOpcodes - lastSeenOpcodes []*opcodeWithPartialStack + lastOpWithStack *opcodeWithPartialStack Keccak map[string]struct{} `json:"keccak"` } @@ -175,10 +175,9 @@ func newErc7562Tracer(ctx *tracers.Context, cfg json.RawMessage /*, chainConfig } type erc7562TracerConfig struct { - LastSeenOpcodesSize int `json:"iastSeenOpcodesSize"` - StackTopItemsSize int `json:"stackTopItemsSize"` - IgnoredOpcodes map[vm.OpCode]struct{} `json:"ignoredOpcodes"` // Opcodes to ignore during OnOpcode hook execution - WithLog bool `json:"withLog"` // If true, erc7562 tracer will collect event logs + StackTopItemsSize int `json:"stackTopItemsSize"` + IgnoredOpcodes map[vm.OpCode]struct{} `json:"ignoredOpcodes"` // Opcodes to ignore during OnOpcode hook execution + WithLog bool `json:"withLog"` // If true, erc7562 tracer will collect event logs } // Function to convert byte array to map[vm.OpCode]struct{} @@ -196,9 +195,6 @@ func getFullConfiguration(partial erc7562TracerConfig) erc7562TracerConfig { if config.IgnoredOpcodes == nil { config.IgnoredOpcodes = defaultIgnoredOpcodes() } - if config.LastSeenOpcodesSize == 0 { - config.LastSeenOpcodesSize = 3 - } if config.StackTopItemsSize == 0 { config.StackTopItemsSize = 3 } @@ -217,7 +213,6 @@ func newErc7562TracerObject(ctx *tracers.Context, cfg json.RawMessage) (*erc7562 // and is populated on start and end. return &erc7562Tracer{ callstackWithOpcodes: make([]callFrameWithOpcodes, 0, 1), - lastSeenOpcodes: make([]*opcodeWithPartialStack, 0), Keccak: make(map[string]struct{}), config: getFullConfiguration(config), }, nil @@ -386,49 +381,41 @@ func (t *erc7562Tracer) clearFailedLogs(cf *callFrameWithOpcodes, parentFailed b func (t *erc7562Tracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { defer catchPanic() opcode := vm.OpCode(op) - + var opcodeWithStack *opcodeWithPartialStack stackSize := len(scope.StackData()) var stackTopItems []uint256.Int for i := 0; i < t.config.StackTopItemsSize && i < stackSize; i++ { stackTopItems = append(stackTopItems, *peepStack(scope.StackData(), i)) } - t.lastSeenOpcodes = append(t.lastSeenOpcodes, &opcodeWithPartialStack{ + opcodeWithStack = &opcodeWithPartialStack{ Opcode: opcode, StackTopItems: stackTopItems, - }) - if len(t.lastSeenOpcodes) > t.config.LastSeenOpcodesSize { - t.lastSeenOpcodes = t.lastSeenOpcodes[1:] } t.handleReturnRevert(opcode) - var lastOpWithStack *opcodeWithPartialStack - if len(t.lastSeenOpcodes) >= t.config.LastSeenOpcodesSize-1 { - lastOpWithStack = t.lastSeenOpcodes[len(t.lastSeenOpcodes)-2] - } size := len(t.callstackWithOpcodes) currentCallFrame := &t.callstackWithOpcodes[size-1] - if lastOpWithStack != nil { - t.handleExtOpcodes(lastOpWithStack, opcode, currentCallFrame) + if t.lastOpWithStack != nil { + t.handleExtOpcodes(opcode, currentCallFrame) } t.handleAccessedContractSize(opcode, scope, currentCallFrame) - if lastOpWithStack != nil { - t.handleGasObserved(lastOpWithStack.Opcode, opcode, currentCallFrame) + if t.lastOpWithStack != nil { + t.handleGasObserved(opcode, currentCallFrame) } t.storeUsedOpcode(opcode, currentCallFrame) t.handleStorageAccess(opcode, scope, currentCallFrame) t.storeKeccak(opcode, scope) + t.lastOpWithStack = opcodeWithStack } func (t *erc7562Tracer) handleReturnRevert(opcode vm.OpCode) { if opcode == vm.REVERT || opcode == vm.RETURN { - if len(t.lastSeenOpcodes) > 0 { - t.lastSeenOpcodes = t.lastSeenOpcodes[len(t.lastSeenOpcodes)-1:] - } + t.lastOpWithStack = nil } } -func (t *erc7562Tracer) handleGasObserved(lastOp vm.OpCode, opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes) { +func (t *erc7562Tracer) handleGasObserved(opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes) { // [OP-012] - pendingGasObserved := lastOp == vm.GAS && !isCall(opcode) + pendingGasObserved := t.lastOpWithStack.Opcode == vm.GAS && !isCall(opcode) if pendingGasObserved { incrementCount(currentCallFrame.UsedOpcodes, vm.GAS) } @@ -476,14 +463,14 @@ func (t *erc7562Tracer) storeKeccak(opcode vm.OpCode, scope tracing.OpContext) { } } -func (t *erc7562Tracer) handleExtOpcodes(lastOpInfo *opcodeWithPartialStack, opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes) { - if isEXT(lastOpInfo.Opcode) { - addr := common.HexToAddress(lastOpInfo.StackTopItems[0].Hex()) +func (t *erc7562Tracer) handleExtOpcodes(opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes) { + if isEXT(t.lastOpWithStack.Opcode) { + addr := common.HexToAddress(t.lastOpWithStack.StackTopItems[0].Hex()) // only store the last EXTCODE* opcode per address - could even be a boolean for our current use-case // [OP-051] - if !(t.lastSeenOpcodes[1].Opcode == vm.EXTCODESIZE && t.lastSeenOpcodes[2].Opcode == vm.ISZERO) { + if !(t.lastOpWithStack.Opcode == vm.EXTCODESIZE && opcode == vm.ISZERO) { currentCallFrame.ExtCodeAccessInfo = append(currentCallFrame.ExtCodeAccessInfo, addr) } }