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 }