github.com/ethereum/go-ethereum@v1.16.1/eth/tracers/native/erc7562.go (about)

     1  // Copyright 2025 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package native
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"errors"
    23  	"math/big"
    24  	"slices"
    25  	"sync/atomic"
    26  
    27  	"github.com/ethereum/go-ethereum/accounts/abi"
    28  	"github.com/ethereum/go-ethereum/common"
    29  	"github.com/ethereum/go-ethereum/common/hexutil"
    30  	"github.com/ethereum/go-ethereum/core/tracing"
    31  	"github.com/ethereum/go-ethereum/core/types"
    32  	"github.com/ethereum/go-ethereum/core/vm"
    33  	"github.com/ethereum/go-ethereum/eth/tracers"
    34  	"github.com/ethereum/go-ethereum/eth/tracers/internal"
    35  	"github.com/ethereum/go-ethereum/log"
    36  	"github.com/ethereum/go-ethereum/params"
    37  	"github.com/holiman/uint256"
    38  )
    39  
    40  //go:generate go run github.com/fjl/gencodec -type callFrameWithOpcodes -field-override callFrameWithOpcodesMarshaling -out gen_callframewithopcodes_json.go
    41  
    42  func init() {
    43  	tracers.DefaultDirectory.Register("erc7562Tracer", newErc7562Tracer, false)
    44  }
    45  
    46  type contractSizeWithOpcode struct {
    47  	ContractSize int       `json:"contractSize"`
    48  	Opcode       vm.OpCode `json:"opcode"`
    49  }
    50  
    51  type callFrameWithOpcodes struct {
    52  	Type             vm.OpCode       `json:"-"`
    53  	From             common.Address  `json:"from"`
    54  	Gas              uint64          `json:"gas"`
    55  	GasUsed          uint64          `json:"gasUsed"`
    56  	To               *common.Address `json:"to,omitempty" rlp:"optional"`
    57  	Input            []byte          `json:"input" rlp:"optional"`
    58  	Output           []byte          `json:"output,omitempty" rlp:"optional"`
    59  	Error            string          `json:"error,omitempty" rlp:"optional"`
    60  	RevertReason     string          `json:"revertReason,omitempty"`
    61  	Logs             []callLog       `json:"logs,omitempty" rlp:"optional"`
    62  	Value            *big.Int        `json:"value,omitempty" rlp:"optional"`
    63  	revertedSnapshot bool
    64  
    65  	AccessedSlots     accessedSlots                              `json:"accessedSlots"`
    66  	ExtCodeAccessInfo []common.Address                           `json:"extCodeAccessInfo"`
    67  	UsedOpcodes       map[vm.OpCode]uint64                       `json:"usedOpcodes"`
    68  	ContractSize      map[common.Address]*contractSizeWithOpcode `json:"contractSize"`
    69  	OutOfGas          bool                                       `json:"outOfGas"`
    70  	// Keccak preimages for the whole transaction are stored in the
    71  	// root call frame.
    72  	KeccakPreimages [][]byte               `json:"keccak,omitempty"`
    73  	Calls           []callFrameWithOpcodes `json:"calls,omitempty" rlp:"optional"`
    74  }
    75  
    76  func (f callFrameWithOpcodes) TypeString() string {
    77  	return f.Type.String()
    78  }
    79  
    80  func (f callFrameWithOpcodes) failed() bool {
    81  	return len(f.Error) > 0 && f.revertedSnapshot
    82  }
    83  
    84  func (f *callFrameWithOpcodes) processOutput(output []byte, err error, reverted bool) {
    85  	output = common.CopyBytes(output)
    86  	// Clear error if tx wasn't reverted. This happened
    87  	// for pre-homestead contract storage OOG.
    88  	if err != nil && !reverted {
    89  		err = nil
    90  	}
    91  	if err == nil {
    92  		f.Output = output
    93  		return
    94  	}
    95  	f.Error = err.Error()
    96  	f.revertedSnapshot = reverted
    97  	if f.Type == vm.CREATE || f.Type == vm.CREATE2 {
    98  		f.To = nil
    99  	}
   100  	if !errors.Is(err, vm.ErrExecutionReverted) || len(output) == 0 {
   101  		return
   102  	}
   103  	f.Output = output
   104  	if len(output) < 4 {
   105  		return
   106  	}
   107  	if unpacked, err := abi.UnpackRevert(output); err == nil {
   108  		f.RevertReason = unpacked
   109  	}
   110  }
   111  
   112  type callFrameWithOpcodesMarshaling struct {
   113  	TypeString      string `json:"type"`
   114  	Gas             hexutil.Uint64
   115  	GasUsed         hexutil.Uint64
   116  	Value           *hexutil.Big
   117  	Input           hexutil.Bytes
   118  	Output          hexutil.Bytes
   119  	UsedOpcodes     map[hexutil.Uint64]uint64
   120  	KeccakPreimages []hexutil.Bytes
   121  }
   122  
   123  type accessedSlots struct {
   124  	Reads           map[common.Hash][]common.Hash `json:"reads"`
   125  	Writes          map[common.Hash]uint64        `json:"writes"`
   126  	TransientReads  map[common.Hash]uint64        `json:"transientReads"`
   127  	TransientWrites map[common.Hash]uint64        `json:"transientWrites"`
   128  }
   129  
   130  type opcodeWithPartialStack struct {
   131  	Opcode        vm.OpCode
   132  	StackTopItems []uint256.Int
   133  }
   134  
   135  type erc7562Tracer struct {
   136  	config    erc7562TracerConfig
   137  	gasLimit  uint64
   138  	interrupt atomic.Bool // Atomic flag to signal execution interruption
   139  	reason    error       // Textual reason for the interruption
   140  	env       *tracing.VMContext
   141  
   142  	ignoredOpcodes       map[vm.OpCode]struct{}
   143  	callstackWithOpcodes []callFrameWithOpcodes
   144  	lastOpWithStack      *opcodeWithPartialStack
   145  	keccakPreimages      map[string]struct{}
   146  }
   147  
   148  // newErc7562Tracer returns a native go tracer which tracks
   149  // call frames of a tx, and implements vm.EVMLogger.
   150  func newErc7562Tracer(ctx *tracers.Context, cfg json.RawMessage, _ *params.ChainConfig) (*tracers.Tracer, error) {
   151  	t, err := newErc7562TracerObject(cfg)
   152  	if err != nil {
   153  		return nil, err
   154  	}
   155  	return &tracers.Tracer{
   156  		Hooks: &tracing.Hooks{
   157  			OnTxStart: t.OnTxStart,
   158  			OnOpcode:  t.OnOpcode,
   159  			OnTxEnd:   t.OnTxEnd,
   160  			OnEnter:   t.OnEnter,
   161  			OnExit:    t.OnExit,
   162  			OnLog:     t.OnLog,
   163  		},
   164  		GetResult: t.GetResult,
   165  		Stop:      t.Stop,
   166  	}, nil
   167  }
   168  
   169  type erc7562TracerConfig struct {
   170  	StackTopItemsSize int              `json:"stackTopItemsSize"`
   171  	IgnoredOpcodes    []hexutil.Uint64 `json:"ignoredOpcodes"` // Opcodes to ignore during OnOpcode hook execution
   172  	WithLog           bool             `json:"withLog"`        // If true, erc7562 tracer will collect event logs
   173  }
   174  
   175  func getFullConfiguration(partial erc7562TracerConfig) erc7562TracerConfig {
   176  	config := partial
   177  
   178  	if config.IgnoredOpcodes == nil {
   179  		config.IgnoredOpcodes = defaultIgnoredOpcodes()
   180  	}
   181  	if config.StackTopItemsSize == 0 {
   182  		config.StackTopItemsSize = 3
   183  	}
   184  
   185  	return config
   186  }
   187  
   188  func newErc7562TracerObject(cfg json.RawMessage) (*erc7562Tracer, error) {
   189  	var config erc7562TracerConfig
   190  	if cfg != nil {
   191  		if err := json.Unmarshal(cfg, &config); err != nil {
   192  			return nil, err
   193  		}
   194  	}
   195  	fullConfig := getFullConfiguration(config)
   196  	// Create a map of ignored opcodes for fast lookup
   197  	ignoredOpcodes := make(map[vm.OpCode]struct{}, len(fullConfig.IgnoredOpcodes))
   198  	for _, op := range fullConfig.IgnoredOpcodes {
   199  		ignoredOpcodes[vm.OpCode(op)] = struct{}{}
   200  	}
   201  	// First callframe contains tx context info
   202  	// and is populated on start and end.
   203  	return &erc7562Tracer{
   204  		callstackWithOpcodes: make([]callFrameWithOpcodes, 0, 1),
   205  		config:               fullConfig,
   206  		keccakPreimages:      make(map[string]struct{}),
   207  		ignoredOpcodes:       ignoredOpcodes,
   208  	}, nil
   209  }
   210  
   211  func (t *erc7562Tracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) {
   212  	t.env = env
   213  	t.gasLimit = tx.Gas()
   214  }
   215  
   216  // OnEnter is called when EVM enters a new scope (via call, create or selfdestruct).
   217  func (t *erc7562Tracer) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
   218  	// Skip if tracing was interrupted
   219  	if t.interrupt.Load() {
   220  		return
   221  	}
   222  
   223  	toCopy := to
   224  	call := callFrameWithOpcodes{
   225  		Type:  vm.OpCode(typ),
   226  		From:  from,
   227  		To:    &toCopy,
   228  		Input: common.CopyBytes(input),
   229  		Gas:   gas,
   230  		Value: value,
   231  		AccessedSlots: accessedSlots{
   232  			Reads:           map[common.Hash][]common.Hash{},
   233  			Writes:          map[common.Hash]uint64{},
   234  			TransientReads:  map[common.Hash]uint64{},
   235  			TransientWrites: map[common.Hash]uint64{},
   236  		},
   237  		UsedOpcodes:       map[vm.OpCode]uint64{},
   238  		ExtCodeAccessInfo: make([]common.Address, 0),
   239  		ContractSize:      map[common.Address]*contractSizeWithOpcode{},
   240  	}
   241  	if depth == 0 {
   242  		call.Gas = t.gasLimit
   243  	}
   244  	t.callstackWithOpcodes = append(t.callstackWithOpcodes, call)
   245  }
   246  
   247  func (t *erc7562Tracer) captureEnd(output []byte, err error, reverted bool) {
   248  	if len(t.callstackWithOpcodes) != 1 {
   249  		return
   250  	}
   251  	t.callstackWithOpcodes[0].processOutput(output, err, reverted)
   252  }
   253  
   254  // OnExit is called when EVM exits a scope, even if the scope didn't
   255  // execute any code.
   256  func (t *erc7562Tracer) OnExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) {
   257  	if t.interrupt.Load() {
   258  		return
   259  	}
   260  	if depth == 0 {
   261  		t.captureEnd(output, err, reverted)
   262  		return
   263  	}
   264  
   265  	size := len(t.callstackWithOpcodes)
   266  	if size <= 1 {
   267  		return
   268  	}
   269  	// Pop call.
   270  	call := t.callstackWithOpcodes[size-1]
   271  	t.callstackWithOpcodes = t.callstackWithOpcodes[:size-1]
   272  	size -= 1
   273  
   274  	if errors.Is(err, vm.ErrCodeStoreOutOfGas) || errors.Is(err, vm.ErrOutOfGas) {
   275  		call.OutOfGas = true
   276  	}
   277  	call.GasUsed = gasUsed
   278  	call.processOutput(output, err, reverted)
   279  	// Nest call into parent.
   280  	t.callstackWithOpcodes[size-1].Calls = append(t.callstackWithOpcodes[size-1].Calls, call)
   281  }
   282  
   283  func (t *erc7562Tracer) OnTxEnd(receipt *types.Receipt, err error) {
   284  	if t.interrupt.Load() {
   285  		return
   286  	}
   287  	// Error happened during tx validation.
   288  	if err != nil {
   289  		return
   290  	}
   291  	t.callstackWithOpcodes[0].GasUsed = receipt.GasUsed
   292  	if t.config.WithLog {
   293  		// Logs are not emitted when the call fails
   294  		t.clearFailedLogs(&t.callstackWithOpcodes[0], false)
   295  	}
   296  }
   297  
   298  func (t *erc7562Tracer) OnLog(log1 *types.Log) {
   299  	// Only logs need to be captured via opcode processing
   300  	if !t.config.WithLog {
   301  		return
   302  	}
   303  	// Skip if tracing was interrupted
   304  	if t.interrupt.Load() {
   305  		return
   306  	}
   307  	l := callLog{
   308  		Address:  log1.Address,
   309  		Topics:   log1.Topics,
   310  		Data:     log1.Data,
   311  		Position: hexutil.Uint(len(t.callstackWithOpcodes[len(t.callstackWithOpcodes)-1].Calls)),
   312  	}
   313  	t.callstackWithOpcodes[len(t.callstackWithOpcodes)-1].Logs = append(t.callstackWithOpcodes[len(t.callstackWithOpcodes)-1].Logs, l)
   314  }
   315  
   316  // GetResult returns the json-encoded nested list of call traces, and any
   317  // error arising from the encoding or forceful termination (via `Stop`).
   318  func (t *erc7562Tracer) GetResult() (json.RawMessage, error) {
   319  	if t.interrupt.Load() {
   320  		return nil, t.reason
   321  	}
   322  	if len(t.callstackWithOpcodes) != 1 {
   323  		return nil, errors.New("incorrect number of top-level calls")
   324  	}
   325  
   326  	keccak := make([][]byte, 0, len(t.callstackWithOpcodes[0].KeccakPreimages))
   327  	for k := range t.keccakPreimages {
   328  		keccak = append(keccak, []byte(k))
   329  	}
   330  	t.callstackWithOpcodes[0].KeccakPreimages = keccak
   331  	slices.SortFunc(keccak, func(a, b []byte) int {
   332  		return bytes.Compare(a, b)
   333  	})
   334  
   335  	enc, err := json.Marshal(t.callstackWithOpcodes[0])
   336  	if err != nil {
   337  		return nil, err
   338  	}
   339  
   340  	return enc, t.reason
   341  }
   342  
   343  // Stop terminates execution of the tracer at the first opportune moment.
   344  func (t *erc7562Tracer) Stop(err error) {
   345  	t.reason = err
   346  	t.interrupt.Store(true)
   347  }
   348  
   349  // clearFailedLogs clears the logs of a callframe and all its children
   350  // in case of execution failure.
   351  func (t *erc7562Tracer) clearFailedLogs(cf *callFrameWithOpcodes, parentFailed bool) {
   352  	failed := cf.failed() || parentFailed
   353  	// Clear own logs
   354  	if failed {
   355  		cf.Logs = nil
   356  	}
   357  	for i := range cf.Calls {
   358  		t.clearFailedLogs(&cf.Calls[i], failed)
   359  	}
   360  }
   361  
   362  func (t *erc7562Tracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) {
   363  	if t.interrupt.Load() {
   364  		return
   365  	}
   366  	var (
   367  		opcode          = vm.OpCode(op)
   368  		opcodeWithStack *opcodeWithPartialStack
   369  		stackSize       = len(scope.StackData())
   370  		stackLimit      = min(stackSize, t.config.StackTopItemsSize)
   371  		stackTopItems   = make([]uint256.Int, stackLimit)
   372  	)
   373  	for i := 0; i < stackLimit; i++ {
   374  		stackTopItems[i] = *peepStack(scope.StackData(), i)
   375  	}
   376  	opcodeWithStack = &opcodeWithPartialStack{
   377  		Opcode:        opcode,
   378  		StackTopItems: stackTopItems,
   379  	}
   380  	t.handleReturnRevert(opcode)
   381  	size := len(t.callstackWithOpcodes)
   382  	currentCallFrame := &t.callstackWithOpcodes[size-1]
   383  	if t.lastOpWithStack != nil {
   384  		t.handleExtOpcodes(opcode, currentCallFrame)
   385  	}
   386  	t.handleAccessedContractSize(opcode, scope, currentCallFrame)
   387  	if t.lastOpWithStack != nil {
   388  		t.handleGasObserved(opcode, currentCallFrame)
   389  	}
   390  	t.storeUsedOpcode(opcode, currentCallFrame)
   391  	t.handleStorageAccess(opcode, scope, currentCallFrame)
   392  	t.storeKeccak(opcode, scope)
   393  	t.lastOpWithStack = opcodeWithStack
   394  }
   395  
   396  func (t *erc7562Tracer) handleReturnRevert(opcode vm.OpCode) {
   397  	if opcode == vm.REVERT || opcode == vm.RETURN {
   398  		t.lastOpWithStack = nil
   399  	}
   400  }
   401  
   402  func (t *erc7562Tracer) handleGasObserved(opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes) {
   403  	// [OP-012]
   404  	pendingGasObserved := t.lastOpWithStack.Opcode == vm.GAS && !isCall(opcode)
   405  	if pendingGasObserved {
   406  		currentCallFrame.UsedOpcodes[vm.GAS]++
   407  	}
   408  }
   409  
   410  func (t *erc7562Tracer) storeUsedOpcode(opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes) {
   411  	// ignore "unimportant" opcodes
   412  	if opcode != vm.GAS && !t.isIgnoredOpcode(opcode) {
   413  		currentCallFrame.UsedOpcodes[opcode]++
   414  	}
   415  }
   416  
   417  func (t *erc7562Tracer) handleStorageAccess(opcode vm.OpCode, scope tracing.OpContext, currentCallFrame *callFrameWithOpcodes) {
   418  	if opcode == vm.SLOAD || opcode == vm.SSTORE || opcode == vm.TLOAD || opcode == vm.TSTORE {
   419  		slot := common.BytesToHash(peepStack(scope.StackData(), 0).Bytes())
   420  		addr := scope.Address()
   421  
   422  		if opcode == vm.SLOAD {
   423  			// read slot values before this UserOp was created
   424  			// (so saving it if it was written before the first read)
   425  			_, rOk := currentCallFrame.AccessedSlots.Reads[slot]
   426  			_, wOk := currentCallFrame.AccessedSlots.Writes[slot]
   427  			if !rOk && !wOk {
   428  				currentCallFrame.AccessedSlots.Reads[slot] = append(currentCallFrame.AccessedSlots.Reads[slot], t.env.StateDB.GetState(addr, slot))
   429  			}
   430  		} else if opcode == vm.SSTORE {
   431  			currentCallFrame.AccessedSlots.Writes[slot]++
   432  		} else if opcode == vm.TLOAD {
   433  			currentCallFrame.AccessedSlots.TransientReads[slot]++
   434  		} else {
   435  			currentCallFrame.AccessedSlots.TransientWrites[slot]++
   436  		}
   437  	}
   438  }
   439  
   440  func (t *erc7562Tracer) storeKeccak(opcode vm.OpCode, scope tracing.OpContext) {
   441  	if opcode == vm.KECCAK256 {
   442  		dataOffset := peepStack(scope.StackData(), 0).Uint64()
   443  		dataLength := peepStack(scope.StackData(), 1).Uint64()
   444  		preimage, err := internal.GetMemoryCopyPadded(scope.MemoryData(), int64(dataOffset), int64(dataLength))
   445  		if err != nil {
   446  			log.Warn("erc7562Tracer: failed to copy keccak preimage from memory", "err", err)
   447  			return
   448  		}
   449  		t.keccakPreimages[string(preimage)] = struct{}{}
   450  	}
   451  }
   452  
   453  func (t *erc7562Tracer) handleExtOpcodes(opcode vm.OpCode, currentCallFrame *callFrameWithOpcodes) {
   454  	if isEXT(t.lastOpWithStack.Opcode) {
   455  		addr := common.HexToAddress(t.lastOpWithStack.StackTopItems[0].Hex())
   456  
   457  		// only store the last EXTCODE* opcode per address - could even be a boolean for our current use-case
   458  		// [OP-051]
   459  
   460  		if !(t.lastOpWithStack.Opcode == vm.EXTCODESIZE && opcode == vm.ISZERO) {
   461  			currentCallFrame.ExtCodeAccessInfo = append(currentCallFrame.ExtCodeAccessInfo, addr)
   462  		}
   463  	}
   464  }
   465  
   466  func (t *erc7562Tracer) handleAccessedContractSize(opcode vm.OpCode, scope tracing.OpContext, currentCallFrame *callFrameWithOpcodes) {
   467  	// [OP-041]
   468  	if isEXTorCALL(opcode) {
   469  		n := 0
   470  		if !isEXT(opcode) {
   471  			n = 1
   472  		}
   473  		addr := common.BytesToAddress(peepStack(scope.StackData(), n).Bytes())
   474  		if _, ok := currentCallFrame.ContractSize[addr]; !ok {
   475  			currentCallFrame.ContractSize[addr] = &contractSizeWithOpcode{
   476  				ContractSize: len(t.env.StateDB.GetCode(addr)),
   477  				Opcode:       opcode,
   478  			}
   479  		}
   480  	}
   481  }
   482  
   483  func peepStack(stackData []uint256.Int, n int) *uint256.Int {
   484  	return &stackData[len(stackData)-n-1]
   485  }
   486  
   487  func isEXTorCALL(opcode vm.OpCode) bool {
   488  	return isEXT(opcode) || isCall(opcode)
   489  }
   490  
   491  func isEXT(opcode vm.OpCode) bool {
   492  	return opcode == vm.EXTCODEHASH ||
   493  		opcode == vm.EXTCODESIZE ||
   494  		opcode == vm.EXTCODECOPY
   495  }
   496  
   497  func isCall(opcode vm.OpCode) bool {
   498  	return opcode == vm.CALL ||
   499  		opcode == vm.CALLCODE ||
   500  		opcode == vm.DELEGATECALL ||
   501  		opcode == vm.STATICCALL
   502  }
   503  
   504  // Check if this opcode is ignored for the purposes of generating the used opcodes report
   505  func (t *erc7562Tracer) isIgnoredOpcode(opcode vm.OpCode) bool {
   506  	if _, ok := t.ignoredOpcodes[opcode]; ok {
   507  		return true
   508  	}
   509  	return false
   510  }
   511  
   512  func defaultIgnoredOpcodes() []hexutil.Uint64 {
   513  	ignored := make([]hexutil.Uint64, 0, 64)
   514  
   515  	// Allow all PUSHx, DUPx and SWAPx opcodes as they have sequential codes
   516  	for op := vm.PUSH0; op < vm.SWAP16; op++ {
   517  		ignored = append(ignored, hexutil.Uint64(op))
   518  	}
   519  
   520  	for _, op := range []vm.OpCode{
   521  		vm.POP, vm.ADD, vm.SUB, vm.MUL,
   522  		vm.DIV, vm.EQ, vm.LT, vm.GT,
   523  		vm.SLT, vm.SGT, vm.SHL, vm.SHR,
   524  		vm.AND, vm.OR, vm.NOT, vm.ISZERO,
   525  	} {
   526  		ignored = append(ignored, hexutil.Uint64(op))
   527  	}
   528  
   529  	return ignored
   530  }