From c1da45b5b61bb3deb9f4d77b355cc17ecff7973f Mon Sep 17 00:00:00 2001 From: relyt29 Date: Thu, 3 Apr 2025 21:47:35 -0400 Subject: [PATCH 01/35] Gas Dimension tracing initial work. Support live tracing and by RPC, support all pure-CPU opcodes. Support the simple state access opcodes. Rest still TODO --- eth/tracers/live/gas_dimension.go | 176 +++++++++++++ eth/tracers/native/gas_dimension.go | 238 +++++++++++++++++ eth/tracers/native/gas_dimension_calc.go | 318 +++++++++++++++++++++++ 3 files changed, 732 insertions(+) create mode 100644 eth/tracers/live/gas_dimension.go create mode 100644 eth/tracers/native/gas_dimension.go create mode 100644 eth/tracers/native/gas_dimension_calc.go diff --git a/eth/tracers/live/gas_dimension.go b/eth/tracers/live/gas_dimension.go new file mode 100644 index 0000000000..6c81f61789 --- /dev/null +++ b/eth/tracers/live/gas_dimension.go @@ -0,0 +1,176 @@ +package live + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "path/filepath" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/eth/tracers" + + "gopkg.in/natefinch/lumberjack.v2" +) + +type gasDimensionLiveTracer struct { + logger *log.Logger +} + +type ExecutionResult struct { + Relyt string `json:"relyt"` + TxHash string `json:"txHash"` +} + +func init() { + tracers.LiveDirectory.Register("gasDimension", newGasDimensionLiveTracer) +} + +type gasDimensionLiveTracerConfig struct { + Path string `json:"path"` // Path to directory for output + MaxSize int `json:"maxSize"` // MaxSize default 100 MB +} + +func newGasDimensionLiveTracer(cfg json.RawMessage) (*tracing.Hooks, error) { + var config gasDimensionLiveTracerConfig + if err := json.Unmarshal(cfg, &config); err != nil { + return nil, err + } + + if config.Path == "" { + return nil, errors.New("gas dimension live tracer path for output is required") + } + + loggerOutput := &lumberjack.Logger{ + Filename: filepath.Join(config.Path, "gas_dimension.jsonl"), + } + + logger := log.New(loggerOutput, "", 0) + + t := &gasDimensionLiveTracer{ + logger: logger, + } + + return &tracing.Hooks{ + OnTxStart: t.OnTxStart, + OnTxEnd: t.OnTxEnd, + //OnEnter: t.OnEnter, + //OnExit: t.OnExit, + //OnOpcode: t.OnOpcode, + //OnFault: t.OnFault, + //OnGasChange: t.OnGasChange, + //OnBlockchainInit: t.OnBlockchainInit, + OnBlockStart: t.OnBlockStart, + OnBlockEnd: t.OnBlockEnd, + //OnSkippedBlock: t.OnSkippedBlock, + //OnGenesisBlock: t.OnGenesisBlock, + //OnBalanceChange: t.OnBalanceChange, + //OnNonceChange: t.OnNonceChange, + //OnCodeChange: t.OnCodeChange, + //OnStorageChange: t.OnStorageChange, + //OnLog: t.OnLog, + }, nil +} + +/* +func (t *gasDimensionLiveTracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { + t.logger.Println("Opcode Seen") +} + +func (t *gasDimensionLiveTracer) OnFault(pc uint64, op byte, gas, cost uint64, _ tracing.OpContext, depth int, err error) { + t.logger.Println("Fault Seen") +} + +func (t *gasDimensionLiveTracer) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + t.logger.Println("Enter Seen") +} + +func (t *gasDimensionLiveTracer) OnExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) { + t.logger.Println("Exit Seen") +} +*/ + +func (t *gasDimensionLiveTracer) OnTxStart(vm *tracing.VMContext, tx *types.Transaction, from common.Address) { + t.logger.Println("Tx Start Seen") +} + +func (t *gasDimensionLiveTracer) OnTxEnd(receipt *types.Receipt, err error) { + var executionResult ExecutionResult = ExecutionResult{ + Relyt: "Uninitialized", + TxHash: "Uninitialized", + } + if err != nil { + executionResult = ExecutionResult{ + Relyt: err.Error(), + TxHash: "nil", + } + } else { + if receipt == nil { + executionResult = ExecutionResult{ + Relyt: "Receipt is nil", + TxHash: "Receipt is nil", + } + } else { + executionResult = ExecutionResult{ + Relyt: "hello world relyt29", + TxHash: receipt.TxHash.Hex(), + } + } + } + executionResultJsonBytes, errMarshalling := json.Marshal(executionResult) + if errMarshalling != nil { + errorJsonString := fmt.Sprintf("{\"errorMarshallingJson\": \"%s\"}", errMarshalling.Error()) + t.logger.Println(errorJsonString) + } else { + t.logger.Println(string(executionResultJsonBytes)) + } +} + +func (t *gasDimensionLiveTracer) OnBlockStart(ev tracing.BlockEvent) { + t.logger.Println("Block Start") +} + +func (t *gasDimensionLiveTracer) OnBlockEnd(err error) { + t.logger.Println("Block End") +} + +/* +func (t *gasDimensionLiveTracer) OnSkippedBlock(ev tracing.BlockEvent) { + t.logger.Println("Skipped Block") +} + +func (t *gasDimensionLiveTracer) OnBlockchainInit(chainConfig *params.ChainConfig) { + t.logger.Println("Blockchain Init") +} + +func (t *gasDimensionLiveTracer) OnGenesisBlock(b *types.Block, alloc types.GenesisAlloc) { + t.logger.Println("Genesis Block") +} + +func (t *gasDimensionLiveTracer) OnBalanceChange(a common.Address, prev, new *big.Int, reason tracing.BalanceChangeReason) { + t.logger.Println("Balance Change") +} + +func (t *gasDimensionLiveTracer) OnNonceChange(a common.Address, prev, new uint64) { + t.logger.Println("Nonce Change") +} + +func (t *gasDimensionLiveTracer) OnCodeChange(a common.Address, prevCodeHash common.Hash, prev []byte, codeHash common.Hash, code []byte) { + t.logger.Println("Code Change") +} + +func (t *gasDimensionLiveTracer) OnStorageChange(a common.Address, k, prev, new common.Hash) { + t.logger.Println("Storage Change") +} + +func (t *gasDimensionLiveTracer) OnLog(l *types.Log) { + t.logger.Println("Log") +} + +func (t *gasDimensionLiveTracer) OnGasChange(old, new uint64, reason tracing.GasChangeReason) { + t.logger.Println("Gas Change") +} +*/ diff --git a/eth/tracers/native/gas_dimension.go b/eth/tracers/native/gas_dimension.go new file mode 100644 index 0000000000..95995b9dcc --- /dev/null +++ b/eth/tracers/native/gas_dimension.go @@ -0,0 +1,238 @@ +package native + +import ( + "encoding/json" + "fmt" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "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" +) + +// initializer for the tracer +func init() { + tracers.DefaultDirectory.Register("gasDimension", NewGasDimensionTracer, false) +} + +// DimensionLog emitted to the EVM each cycle and lists information about each opcode +// and its gas dimensions prior to the execution of the statement. +type DimensionLog struct { + Pc uint64 `json:"pc"` + Op vm.OpCode `json:"op"` + Depth int `json:"depth"` + OneDimensionalGasCost uint64 `json:"oneDimensionalGasCost"` + Computation uint64 `json:"computation"` + StateAccess uint64 `json:"stateAccess"` + StateGrowth uint64 `json:"stateGrowth"` + HistoryGrowth uint64 `json:"historyGrowth"` + StateGrowthRefund uint64 `json:"stateGrowthRefund"` + Err error `json:"-"` +} + +// ErrorString formats the log's error as a string. +func (d *DimensionLog) ErrorString() string { + if d.Err != nil { + return d.Err.Error() + } + return "" +} + +// gasDimensionTracer struct +type GasDimensionTracer struct { + env *tracing.VMContext + txHash common.Hash + logs []DimensionLog + err error + usedGas uint64 + + interrupt atomic.Bool // Atomic flag to signal execution interruption + reason error // Textual reason for the interruption +} + +// gasDimensionTracer returns a new tracer that traces gas +// usage for each opcode against the dimension of that opcode +// takes a context, and json input for configuration parameters +func NewGasDimensionTracer( + ctx *tracers.Context, + _ json.RawMessage, +) (*tracers.Tracer, error) { + + t := &GasDimensionTracer{} + + return &tracers.Tracer{ + Hooks: &tracing.Hooks{ + OnOpcode: t.OnOpcode, + OnTxStart: t.OnTxStart, + OnTxEnd: t.OnTxEnd, + OnGasChange: t.OnGasChange, + }, + GetResult: t.GetResult, + Stop: t.Stop, + }, nil +} + +// ############################################################################ +// HOOKS +// ############################################################################ + +// hook into each opcode execution +func (t *GasDimensionTracer) OnOpcode( + pc uint64, + op byte, + gas, cost uint64, + scope tracing.OpContext, + rData []byte, + depth int, + err error, +) { + if t.interrupt.Load() { + return + } + + f := getCalcGasDimensionFunc(vm.OpCode(op)) + gasesByDimension := f(pc, op, gas, cost, scope, rData, depth, err, t.env.StateDB) + + t.logs = append(t.logs, DimensionLog{ + Pc: pc, + Op: vm.OpCode(op), + Depth: depth, + OneDimensionalGasCost: cost, + Computation: gasesByDimension[Computation], + StateAccess: gasesByDimension[StateAccess], + StateGrowth: gasesByDimension[StateGrowth], + HistoryGrowth: gasesByDimension[HistoryGrowth], + StateGrowthRefund: gasesByDimension[StateGrowthRefund], + Err: err, + }) +} + +// hook into gas changes +// used to observe Cold Storage Accesses +// We do not have access to StateDb.AddressInAccessList and StateDb.SlotInAccessList +// to check cold storage access directly. So instead what we do here is we +// assign all of the gas to the CPU dimension, which is what it would be if the +// state access was warm. If the state access is cold, then immediately after +// the onOpcode event is fired, we should observe an OnGasChange event +// that will indicate the GasChangeReason is a GasChangeCallStorageColdAccess +// and then we modify the logs in that hook. +func (t *GasDimensionTracer) OnGasChange(old, new uint64, reason tracing.GasChangeReason) { + fmt.Println("OnGasChange", old, new, reason) + if reason == tracing.GasChangeCallStorageColdAccess { + lastLog := t.logs[len(t.logs)-1] + fmt.Println("lastLog", lastLog) + if canHaveColdStorageAccess(lastLog.Op) { + coldCost := new - old + fmt.Println("coldCost", coldCost) + lastLog.StateAccess += coldCost + lastLog.Computation -= coldCost + // replace the last log with the corrected log + t.logs[len(t.logs)-1] = lastLog + fmt.Println("correctedLog", lastLog) + } else { + lastLog.Err = fmt.Errorf("cold storage access on opcode that is unsupported??? %s", lastLog.Op.String()) + t.interrupt.Store(true) + t.reason = lastLog.Err + return + } + } +} + +func (t *GasDimensionTracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) { + t.env = env +} + +func (t *GasDimensionTracer) OnTxEnd(receipt *types.Receipt, err error) { + if err != nil { + // Don't override vm error + if t.err == nil { + t.err = err + } + return + } + t.usedGas = receipt.GasUsed + t.txHash = receipt.TxHash +} + +// signal the tracer to stop tracing, e.g. on timeout +func (t *GasDimensionTracer) Stop(err error) { + t.reason = err + t.interrupt.Store(true) +} + +// ############################################################################ +// JSON OUTPUT PRODUCTION +// ############################################################################ + +// DimensionLogs returns the captured log entries. +func (t *GasDimensionTracer) DimensionLogs() []DimensionLog { return t.logs } + +// Error returns the VM error captured by the trace. +func (t *GasDimensionTracer) Error() error { return t.err } + +// ExecutionResult groups all dimension logs emitted by the EVM +// while replaying a transaction in debug mode as well as transaction +// execution status, the amount of gas used and the return value +type ExecutionResult struct { + Gas uint64 `json:"gas"` + Failed bool `json:"failed"` + DimensionLogs []DimensionLogRes `json:"dimensionLogs"` + TxHash string `json:"txHash"` + BlockTimetamp uint64 `json:"blockTimestamp"` +} + +// produce json result for output from tracer +// this is what the end-user actually gets from the RPC endpoint +func (t *GasDimensionTracer) GetResult() (json.RawMessage, error) { + // Tracing aborted + if t.reason != nil { + return nil, t.reason + } + failed := t.err != nil + + return json.Marshal(&ExecutionResult{ + Gas: t.usedGas, + Failed: failed, + DimensionLogs: formatLogs(t.DimensionLogs()), + TxHash: t.txHash.Hex(), + BlockTimetamp: t.env.Time, + }) +} + +// formatted logs for json output +// as opposed to DimensionLog which has real non-string types +// keep json field names as short as possible to save on bandwidth bytes +type DimensionLogRes struct { + Pc uint64 `json:"pc"` + Op string `json:"op"` + Depth int `json:"depth"` + OneDimensionalGasCost uint64 `json:"gasCost"` + Computation uint64 `json:"cpu"` + StateAccess uint64 `json:"access,omitempty"` + StateGrowth uint64 `json:"growth,omitempty"` + HistoryGrowth uint64 `json:"hist,omitempty"` + StateGrowthRefund uint64 `json:"refund,omitempty"` + Err error `json:"error,omitempty"` +} + +// formatLogs formats EVM returned structured logs for json output +func formatLogs(logs []DimensionLog) []DimensionLogRes { + formatted := make([]DimensionLogRes, len(logs)) + for index, trace := range logs { + formatted[index] = DimensionLogRes{ + Pc: trace.Pc, + Op: trace.Op.String(), + Depth: trace.Depth, + OneDimensionalGasCost: trace.OneDimensionalGasCost, + Computation: trace.Computation, + StateAccess: trace.StateAccess, + StateGrowth: trace.StateGrowth, + HistoryGrowth: trace.HistoryGrowth, + StateGrowthRefund: trace.StateGrowthRefund, + Err: trace.Err, + } + } + return formatted +} diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go new file mode 100644 index 0000000000..0e173a1b02 --- /dev/null +++ b/eth/tracers/native/gas_dimension_calc.go @@ -0,0 +1,318 @@ +package native + +import ( + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" +) + +// GasesByDimension is a type that represents the gas consumption for each dimension +// for a given opcode. +// The dimensions in order of 0 - 3 are: +// 0: Computation +// 1: Storage Access (Read/Write) +// 2: State Growth (Expanding the size of the state) +// 3: History Growth (Expanding the size of the history, especially on archive nodes) +type GasesByDimension [5]uint64 +type GasDimension = uint8 + +const ( + Computation GasDimension = 0 + StateAccess GasDimension = 1 + StateGrowth GasDimension = 2 + HistoryGrowth GasDimension = 3 + StateGrowthRefund GasDimension = 4 +) + +// calcGasDimensionFunc defines a type signature that takes the opcode +// tracing data for an opcode and return the gas consumption for each dimension +// for that given opcode. +// +// INVARIANT: the sum of the gas consumption for each dimension +// equals the input `gas` to this function +type calcGasDimensionFunc func( + pc uint64, + op byte, + gas, cost uint64, + scope tracing.OpContext, + rData []byte, + depth int, + err error, + stateDB tracing.StateDB, +) GasesByDimension + +// getCalcGasDimensionFunc is a massive case switch +// statement that returns which function to call +// based on which opcode is being traced/executed +func getCalcGasDimensionFunc(op vm.OpCode) calcGasDimensionFunc { + switch op { + // Opcodes that Only Operate on Storage Read/Write (storage access in the short run) + // `BALANCE, EXTCODESIZE, EXTCODEHASH,` + // `SLOAD` + // `EXTCODECOPY` + // `DELEGATECALL, STATICCALL` + case vm.BALANCE, vm.EXTCODESIZE, vm.EXTCODEHASH: + return calcSimpleAddressAccessSetGas + case vm.SLOAD: + return calcSLOADGas + case vm.EXTCODECOPY: + return calcExtCodeCopyGas + case vm.DELEGATECALL, vm.STATICCALL: + return calcStateReadCallGas + // Opcodes that only grow the history + // all of the LOG opcodes: `LOG0, LOG1, LOG2, LOG3, LOG4` + case vm.LOG0, vm.LOG1, vm.LOG2, vm.LOG3, vm.LOG4: + return calcLogGas + // Opcodes that grow state without reading existing state + // `CREATE, CREATE2` + case vm.CREATE, vm.CREATE2: + return calcCreateGas + // Opcodes that Operate on Both R/W and State Growth + // `CALL, CALLCODE` + // `SSTORE` + // `SELFDESTRUCT` + case vm.CALL, vm.CALLCODE: + return calcReadAndStoreCallGas + case vm.SSTORE: + return calcSStoreGas + case vm.SELFDESTRUCT: + return calcSelfDestructGas + // Everything else is all CPU! + // ADD, PUSH, etc etc etc + default: + return calcSimpleSingleDimensionGas + } +} + +// canHaveColdStorageAccess returns true if the opcode can have cold storage access +func canHaveColdStorageAccess(op vm.OpCode) bool { + // todo make this list fully complete + switch op { + case vm.BALANCE, vm.EXTCODESIZE, vm.EXTCODEHASH: + return true + case vm.SLOAD: + return true + default: + return false + } +} + +// calcSimpleSingleDimensionGas returns the gas used for the +// simplest of transactions, that only use the computation dimension +func calcSimpleSingleDimensionGas( + pc uint64, + op byte, + gas, cost uint64, + scope tracing.OpContext, + rData []byte, + depth int, + err error, + stateDB tracing.StateDB, +) GasesByDimension { + return GasesByDimension{ + Computation: cost, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + } +} + +// calcSimpleAddressAccessSetGas returns the gas used +// for relatively simple state access (read/write) +// operations. These opcodes only read addresses +// +// from the state and do not expand it +// +// this includes: +// `BALANCE, EXTCODESIZE, EXTCODEHASH +func calcSimpleAddressAccessSetGas( + pc uint64, + op byte, + gas, cost uint64, + scope tracing.OpContext, + rData []byte, + depth int, + err error, + stateDB tracing.StateDB, +) GasesByDimension { + // We do not have access to StateDb.AddressInAccessList and StateDb.SlotInAccessList + // to check cold storage access directly. + // Additionally, cold storage access for these address opcodes are handled differently + // than for other operations like SSTORE or SLOAD. + // for these opcodes, cold access cost is handled directly in the gas calculation + // through gasEip2929AccountCheck. This function adds the address to the access list + // and charges the cold access cost upfront as part of the initial gas calculation, + // rather than as a separate gas change event, so no OnGasChange event is fired. + // + // Therefore, for these opcodes, we do a simple check based on the raw value + // and we can deduce the dimensions directly from that value. + + if cost == params.ColdAccountAccessCostEIP2929 { + return GasesByDimension{ + Computation: params.WarmStorageReadCostEIP2929, + StateAccess: params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + } + } + return GasesByDimension{ + Computation: cost, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + } +} + +// calcSLOADGas returns the gas used for the `SLOAD` opcode +// SLOAD reads a slot from the state. It cannot expand the state +func calcSLOADGas( + pc uint64, + op byte, + gas, cost uint64, + scope tracing.OpContext, + rData []byte, + depth int, + err error, + stateDB tracing.StateDB, +) GasesByDimension { + // need access to StateDb.AddressInAccessList and StateDb.SlotInAccessList + return GasesByDimension{} +} + +// calcExtCodeCopyGas returns the gas used +// for the `EXTCODECOPY` opcode, which reads from +// the code of an external contract. +// Hence only state read implications +func calcExtCodeCopyGas( + pc uint64, + op byte, + gas, cost uint64, + scope tracing.OpContext, + rData []byte, + depth int, + err error, + stateDB tracing.StateDB, +) GasesByDimension { + // todo: implement + return GasesByDimension{} +} + +// calcStateReadCallGas returns the gas used +// for opcodes that read from the state but do not write to it +// this includes: +// `DELEGATECALL, STATICCALL` +// even though delegatecalls can modify state, they modify the local +// state of the calling contract, not the state of the called contract +// therefore, the state writes happen inside the calling context and their gas implications +// are accounted for on those opcodes (e.g. SSTORE), which means the delegatecall opcode itself +// only has state read implications. Staticcall is the same but even more read-only. +func calcStateReadCallGas( + pc uint64, + op byte, + gas, cost uint64, + scope tracing.OpContext, + rData []byte, + depth int, + err error, + stateDB tracing.StateDB, +) GasesByDimension { + + // todo: implement + return GasesByDimension{} +} + +// calcLogGas returns the gas used for the `LOG0, LOG1, LOG2, LOG3, LOG4` opcodes +// which only grow the history tree. History does not need to be referenced with +// every block since it does not produce the block hashes. So the cost implications +// of growing it and the prunability of it are not as important as state growth. +// The relevant opcodes here are: +// `LOG0, LOG1, LOG2, LOG3, LOG4` +func calcLogGas( + pc uint64, + op byte, + gas, cost uint64, + scope tracing.OpContext, + rData []byte, + depth int, + err error, + stateDB tracing.StateDB, +) GasesByDimension { + // todo: implement + return GasesByDimension{} +} + +// calcCreateGas returns the gas used for the CREATE set of opcodes +// which do storage growth when the store the newly created contract code. +// the relevant opcodes here are: +// `CREATE, CREATE2` +func calcCreateGas( + pc uint64, + op byte, + gas, cost uint64, + scope tracing.OpContext, + rData []byte, + depth int, + err error, + stateDB tracing.StateDB, +) GasesByDimension { + // todo: implement + return GasesByDimension{} +} + +// calcReadAndStoreCallGas returns the gas used for the `CALL, CALLCODE` opcodes +// which both read from the state to figure out where to jump to and write to the state +// in limited cases, e.g. when CALLing an empty address, which is the equivalent of an +// ether transfer. +// the relevant opcodes here are: +// `CALL, CALLCODE` +func calcReadAndStoreCallGas( + pc uint64, + op byte, + gas, cost uint64, + scope tracing.OpContext, + rData []byte, + depth int, + err error, + stateDB tracing.StateDB, +) GasesByDimension { + // todo: implement + return GasesByDimension{} +} + +// calcSStoreGas returns the gas used for the `SSTORE` opcode +// which writes to the state. There is a whole lot of complexity around +// gas refunds based on the state of the storage slot before and whether +// refunds happened before in this transaction, +// which manipulate the gas cost of this specific opcode. +func calcSStoreGas( + pc uint64, + op byte, + gas, cost uint64, + scope tracing.OpContext, + rData []byte, + depth int, + err error, + stateDB tracing.StateDB, +) GasesByDimension { + // todo: implement + return GasesByDimension{} +} + +// calcSelfDestructGas returns the gas used for the `SELFDESTRUCT` opcode +// which deletes a contract which is a write to the state +func calcSelfDestructGas( + pc uint64, + op byte, + gas, cost uint64, + scope tracing.OpContext, + rData []byte, + depth int, + err error, + stateDB tracing.StateDB, +) GasesByDimension { + // todo: implement + return GasesByDimension{} +} From 0af42c1388726312db14a6ec033a15119f73a4f3 Mon Sep 17 00:00:00 2001 From: relyt29 Date: Fri, 4 Apr 2025 13:06:03 -0400 Subject: [PATCH 02/35] gas dimension support for EXTCODECOPY. Tested by hand: 1. EXTCODECOPY with no memory expansion, and warm storage access 2. EXTCODECOPY with memory expansion and warm storage access 3. EXTCODECOPY with memory expansion and cold storage access 4. EXTCODECOPY with no memory expansion and cold storage access --- eth/tracers/native/gas_dimension.go | 28 +++-- eth/tracers/native/gas_dimension_calc.go | 126 ++++++++++++++++++----- eth/tracers/native/prev_opcode_state.go | 69 +++++++++++++ 3 files changed, 191 insertions(+), 32 deletions(-) create mode 100644 eth/tracers/native/prev_opcode_state.go diff --git a/eth/tracers/native/gas_dimension.go b/eth/tracers/native/gas_dimension.go index 95995b9dcc..34bdf2f6e8 100644 --- a/eth/tracers/native/gas_dimension.go +++ b/eth/tracers/native/gas_dimension.go @@ -2,7 +2,6 @@ package native import ( "encoding/json" - "fmt" "sync/atomic" "github.com/ethereum/go-ethereum/common" @@ -50,6 +49,8 @@ type GasDimensionTracer struct { interrupt atomic.Bool // Atomic flag to signal execution interruption reason error // Textual reason for the interruption + + prevOpcodeState *PrevOpcodeState // Track previous opcode state, to observe changes on a per-opcode basis } // gasDimensionTracer returns a new tracer that traces gas @@ -64,10 +65,10 @@ func NewGasDimensionTracer( return &tracers.Tracer{ Hooks: &tracing.Hooks{ - OnOpcode: t.OnOpcode, - OnTxStart: t.OnTxStart, - OnTxEnd: t.OnTxEnd, - OnGasChange: t.OnGasChange, + OnOpcode: t.OnOpcode, + OnTxStart: t.OnTxStart, + OnTxEnd: t.OnTxEnd, + //OnGasChange: t.OnGasChange, }, GetResult: t.GetResult, Stop: t.Stop, @@ -92,8 +93,15 @@ func (t *GasDimensionTracer) OnOpcode( return } + // Free the previous state if it exists + if t.prevOpcodeState != nil { + freePrevOpcodeState(t.prevOpcodeState) + } + // Create new state for this opcode + t.prevOpcodeState = DeepCopyOpcodeState(scope) + f := getCalcGasDimensionFunc(vm.OpCode(op)) - gasesByDimension := f(pc, op, gas, cost, scope, rData, depth, err, t.env.StateDB) + gasesByDimension := f(pc, op, gas, cost, scope, rData, depth, err, t.prevOpcodeState) t.logs = append(t.logs, DimensionLog{ Pc: pc, @@ -109,6 +117,7 @@ func (t *GasDimensionTracer) OnOpcode( }) } +/* // hook into gas changes // used to observe Cold Storage Accesses // We do not have access to StateDb.AddressInAccessList and StateDb.SlotInAccessList @@ -139,6 +148,7 @@ func (t *GasDimensionTracer) OnGasChange(old, new uint64, reason tracing.GasChan } } } +*/ func (t *GasDimensionTracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) { t.env = env @@ -154,6 +164,12 @@ func (t *GasDimensionTracer) OnTxEnd(receipt *types.Receipt, err error) { } t.usedGas = receipt.GasUsed t.txHash = receipt.TxHash + + // Free the final state + if t.prevOpcodeState != nil { + freePrevOpcodeState(t.prevOpcodeState) + t.prevOpcodeState = nil + } } // signal the tracer to stop tracing, e.g. on timeout diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index 0e173a1b02..c61e6a06aa 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -1,6 +1,8 @@ package native import ( + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/params" @@ -38,7 +40,7 @@ type calcGasDimensionFunc func( rData []byte, depth int, err error, - stateDB tracing.StateDB, + prevOpcodeState *PrevOpcodeState, ) GasesByDimension // getCalcGasDimensionFunc is a massive case switch @@ -84,19 +86,6 @@ func getCalcGasDimensionFunc(op vm.OpCode) calcGasDimensionFunc { } } -// canHaveColdStorageAccess returns true if the opcode can have cold storage access -func canHaveColdStorageAccess(op vm.OpCode) bool { - // todo make this list fully complete - switch op { - case vm.BALANCE, vm.EXTCODESIZE, vm.EXTCODEHASH: - return true - case vm.SLOAD: - return true - default: - return false - } -} - // calcSimpleSingleDimensionGas returns the gas used for the // simplest of transactions, that only use the computation dimension func calcSimpleSingleDimensionGas( @@ -107,7 +96,7 @@ func calcSimpleSingleDimensionGas( rData []byte, depth int, err error, - stateDB tracing.StateDB, + prevOpcodeState *PrevOpcodeState, ) GasesByDimension { return GasesByDimension{ Computation: cost, @@ -134,7 +123,7 @@ func calcSimpleAddressAccessSetGas( rData []byte, depth int, err error, - stateDB tracing.StateDB, + prevOpcodeState *PrevOpcodeState, ) GasesByDimension { // We do not have access to StateDb.AddressInAccessList and StateDb.SlotInAccessList // to check cold storage access directly. @@ -176,12 +165,54 @@ func calcSLOADGas( rData []byte, depth int, err error, - stateDB tracing.StateDB, + prevOpcodeState *PrevOpcodeState, ) GasesByDimension { // need access to StateDb.AddressInAccessList and StateDb.SlotInAccessList return GasesByDimension{} } +// copied from go-ethereum/core/vm/gas_table.go because not exported there +// toWordSize returns the ceiled word size required for memory expansion. +func toWordSize(size uint64) uint64 { + if size > math.MaxUint64-31 { + return math.MaxUint64/32 + 1 + } + + return (size + 31) / 32 +} + +// This code is copied and edited from go-ethereum/core/vm/gas_table.go +// because the code there is not exported. +// memoryGasCost calculates the quadratic gas for memory expansion. It does so +// only for the memory region that is expanded, not the total memory. +func memoryGasCost(mem []byte, lastGasCost uint64, newMemSize uint64) (fee uint64, newTotalFee uint64, err error) { + if newMemSize == 0 { + return 0, 0, nil + } + // The maximum that will fit in a uint64 is max_word_count - 1. Anything above + // that will result in an overflow. Additionally, a newMemSize which results in + // a newMemSizeWords larger than 0xFFFFFFFF will cause the square operation to + // overflow. The constant 0x1FFFFFFFE0 is the highest number that can be used + // without overflowing the gas calculation. + if newMemSize > 0x1FFFFFFFE0 { + return 0, 0, vm.ErrGasUintOverflow + } + newMemSizeWords := toWordSize(newMemSize) + newMemSize = newMemSizeWords * 32 + + if newMemSize > uint64(len(mem)) { + square := newMemSizeWords * newMemSizeWords + linCoef := newMemSizeWords * params.MemoryGas + quadCoef := square / params.QuadCoeffDiv + newTotalFee = linCoef + quadCoef + + fee = newTotalFee - lastGasCost + + return fee, newTotalFee, nil + } + return 0, 0, nil +} + // calcExtCodeCopyGas returns the gas used // for the `EXTCODECOPY` opcode, which reads from // the code of an external contract. @@ -194,10 +225,53 @@ func calcExtCodeCopyGas( rData []byte, depth int, err error, - stateDB tracing.StateDB, + prevOpcodeState *PrevOpcodeState, ) GasesByDimension { - // todo: implement - return GasesByDimension{} + // extcodecody has three components to its gas cost: + // 1. minimum_word_size = (size + 31) / 32 + // 2. memory_expansion_cost + // 3. address_access_cost - the access set. + // gas for extcodecopy is 3 * minimum_word_size + memory_expansion_cost + address_access_cost + // + // at time of opcode trace, we know the state of the memory, and stack + // and we know the total cost of the opcode. + // therefore, we calculate minimum_word_size, and memory_expansion_cost + // and observe if the subtraction of cost - memory_expansion_cost - minimum_word_size = 100 or 2600 + // 3*minimum_word_size is always state access + // if it is 2600, then have 2500 for state access. + // rest is computation. + + stack := scope.StackData() + lenStack := len(stack) + size := stack[lenStack-4].Uint64() // size in stack position 4 + offset := stack[lenStack-2].Uint64() // destination offset in stack position 2 + + // computing the memory gas cost requires knowing a "previous" price + var emptyMem []byte = make([]byte, 0) + memoryDataBeforeExtCodeCopyApplied := scope.MemoryData() + _, lastGasCost, memErr := memoryGasCost(emptyMem, 0, uint64(len(memoryDataBeforeExtCodeCopyApplied))) + if memErr != nil { + return GasesByDimension{} + } + memoryExpansionCost, _, memErr := memoryGasCost(memoryDataBeforeExtCodeCopyApplied, lastGasCost, size+offset) + if memErr != nil { + return GasesByDimension{} + } + minimumWordSizeCost := (size + 31) / 32 * 3 + leftOver := cost - memoryExpansionCost - minimumWordSizeCost + stateAccess := minimumWordSizeCost + // check if the access set was hot or cold + if leftOver == params.ColdAccountAccessCostEIP2929 { + stateAccess += params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 + } + computation := cost - stateAccess + return GasesByDimension{ + Computation: computation, + StateAccess: stateAccess, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + } } // calcStateReadCallGas returns the gas used @@ -217,7 +291,7 @@ func calcStateReadCallGas( rData []byte, depth int, err error, - stateDB tracing.StateDB, + prevOpcodeState *PrevOpcodeState, ) GasesByDimension { // todo: implement @@ -238,7 +312,7 @@ func calcLogGas( rData []byte, depth int, err error, - stateDB tracing.StateDB, + prevOpcodeState *PrevOpcodeState, ) GasesByDimension { // todo: implement return GasesByDimension{} @@ -256,7 +330,7 @@ func calcCreateGas( rData []byte, depth int, err error, - stateDB tracing.StateDB, + prevOpcodeState *PrevOpcodeState, ) GasesByDimension { // todo: implement return GasesByDimension{} @@ -276,7 +350,7 @@ func calcReadAndStoreCallGas( rData []byte, depth int, err error, - stateDB tracing.StateDB, + prevOpcodeState *PrevOpcodeState, ) GasesByDimension { // todo: implement return GasesByDimension{} @@ -295,7 +369,7 @@ func calcSStoreGas( rData []byte, depth int, err error, - stateDB tracing.StateDB, + prevOpcodeState *PrevOpcodeState, ) GasesByDimension { // todo: implement return GasesByDimension{} @@ -311,7 +385,7 @@ func calcSelfDestructGas( rData []byte, depth int, err error, - stateDB tracing.StateDB, + prevOpcodeState *PrevOpcodeState, ) GasesByDimension { // todo: implement return GasesByDimension{} diff --git a/eth/tracers/native/prev_opcode_state.go b/eth/tracers/native/prev_opcode_state.go new file mode 100644 index 0000000000..1455231320 --- /dev/null +++ b/eth/tracers/native/prev_opcode_state.go @@ -0,0 +1,69 @@ +package native + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/holiman/uint256" +) + +type PrevOpcodeState struct { + MemoryData []byte + StackData []uint256.Int + Caller common.Address + Address common.Address + CallValue *uint256.Int + CallInput []byte +} + +// Deep copies an opcontext into a PrevOpcodeState +// so you can introspect state changes on a per-opcode level +func DeepCopyOpcodeState(scope tracing.OpContext) *PrevOpcodeState { + // Create new slices and copy memory data + memCopy := make([]byte, len(scope.MemoryData())) + copy(memCopy, scope.MemoryData()) + + // Create new slice and copy stack data + stackData := scope.StackData() + stackCopy := make([]uint256.Int, len(stackData)) + for i := range stackData { + stackCopy[i].Set(&stackData[i]) // Assuming uint256.Int has a Set method + } + + // Create new slice and copy call input + inputCopy := make([]byte, len(scope.CallInput())) + copy(inputCopy, scope.CallInput()) + + // Deep copy the call value + var callValueCopy uint256.Int + if scope.CallValue() != nil { + callValueCopy.Set(scope.CallValue()) + } + + return &PrevOpcodeState{ + MemoryData: memCopy, + StackData: stackCopy, + Caller: scope.Caller(), // Value type, copy is automatic + Address: scope.Address(), // Value type, copy is automatic + CallValue: &callValueCopy, + CallInput: inputCopy, + } +} + +// freePrevOpcodeState helps manage memory by clearing references to allow GC +// to reclaim memory more aggressively. This is particularly useful when we know +// the lifecycle of the data and want to ensure memory is freed as soon as possible. +func freePrevOpcodeState(state *PrevOpcodeState) { + if state == nil { + return + } + + // Clear slice references to allow GC + state.MemoryData = nil + state.StackData = nil + state.CallInput = nil + + // Clear pointer reference + state.CallValue = nil + + // Address and Caller are value types, no need to clear +} From 74e19e2d65239612b7c298988acd6c2a459b3fbf Mon Sep 17 00:00:00 2001 From: relyt29 Date: Fri, 4 Apr 2025 13:22:47 -0400 Subject: [PATCH 03/35] Stop tracking previous opcode state - we don't need it --- eth/tracers/native/gas_dimension.go | 22 +++----- eth/tracers/native/gas_dimension_calc.go | 11 ---- eth/tracers/native/prev_opcode_state.go | 69 ------------------------ 3 files changed, 6 insertions(+), 96 deletions(-) delete mode 100644 eth/tracers/native/prev_opcode_state.go diff --git a/eth/tracers/native/gas_dimension.go b/eth/tracers/native/gas_dimension.go index 34bdf2f6e8..9dcb766531 100644 --- a/eth/tracers/native/gas_dimension.go +++ b/eth/tracers/native/gas_dimension.go @@ -49,8 +49,6 @@ type GasDimensionTracer struct { interrupt atomic.Bool // Atomic flag to signal execution interruption reason error // Textual reason for the interruption - - prevOpcodeState *PrevOpcodeState // Track previous opcode state, to observe changes on a per-opcode basis } // gasDimensionTracer returns a new tracer that traces gas @@ -93,15 +91,8 @@ func (t *GasDimensionTracer) OnOpcode( return } - // Free the previous state if it exists - if t.prevOpcodeState != nil { - freePrevOpcodeState(t.prevOpcodeState) - } - // Create new state for this opcode - t.prevOpcodeState = DeepCopyOpcodeState(scope) - f := getCalcGasDimensionFunc(vm.OpCode(op)) - gasesByDimension := f(pc, op, gas, cost, scope, rData, depth, err, t.prevOpcodeState) + gasesByDimension := f(pc, op, gas, cost, scope, rData, depth, err) t.logs = append(t.logs, DimensionLog{ Pc: pc, @@ -118,6 +109,11 @@ func (t *GasDimensionTracer) OnOpcode( } /* + +This code is garbage left over from when I was trying to figure out how to directly +compute state access costs. It doesn't work but I'm keeping it around for when +its time to do sload or sstore which will fire this onGasChange event. + // hook into gas changes // used to observe Cold Storage Accesses // We do not have access to StateDb.AddressInAccessList and StateDb.SlotInAccessList @@ -164,12 +160,6 @@ func (t *GasDimensionTracer) OnTxEnd(receipt *types.Receipt, err error) { } t.usedGas = receipt.GasUsed t.txHash = receipt.TxHash - - // Free the final state - if t.prevOpcodeState != nil { - freePrevOpcodeState(t.prevOpcodeState) - t.prevOpcodeState = nil - } } // signal the tracer to stop tracing, e.g. on timeout diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index c61e6a06aa..892bfe9c44 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -40,7 +40,6 @@ type calcGasDimensionFunc func( rData []byte, depth int, err error, - prevOpcodeState *PrevOpcodeState, ) GasesByDimension // getCalcGasDimensionFunc is a massive case switch @@ -96,7 +95,6 @@ func calcSimpleSingleDimensionGas( rData []byte, depth int, err error, - prevOpcodeState *PrevOpcodeState, ) GasesByDimension { return GasesByDimension{ Computation: cost, @@ -123,7 +121,6 @@ func calcSimpleAddressAccessSetGas( rData []byte, depth int, err error, - prevOpcodeState *PrevOpcodeState, ) GasesByDimension { // We do not have access to StateDb.AddressInAccessList and StateDb.SlotInAccessList // to check cold storage access directly. @@ -165,7 +162,6 @@ func calcSLOADGas( rData []byte, depth int, err error, - prevOpcodeState *PrevOpcodeState, ) GasesByDimension { // need access to StateDb.AddressInAccessList and StateDb.SlotInAccessList return GasesByDimension{} @@ -225,7 +221,6 @@ func calcExtCodeCopyGas( rData []byte, depth int, err error, - prevOpcodeState *PrevOpcodeState, ) GasesByDimension { // extcodecody has three components to its gas cost: // 1. minimum_word_size = (size + 31) / 32 @@ -291,7 +286,6 @@ func calcStateReadCallGas( rData []byte, depth int, err error, - prevOpcodeState *PrevOpcodeState, ) GasesByDimension { // todo: implement @@ -312,7 +306,6 @@ func calcLogGas( rData []byte, depth int, err error, - prevOpcodeState *PrevOpcodeState, ) GasesByDimension { // todo: implement return GasesByDimension{} @@ -330,7 +323,6 @@ func calcCreateGas( rData []byte, depth int, err error, - prevOpcodeState *PrevOpcodeState, ) GasesByDimension { // todo: implement return GasesByDimension{} @@ -350,7 +342,6 @@ func calcReadAndStoreCallGas( rData []byte, depth int, err error, - prevOpcodeState *PrevOpcodeState, ) GasesByDimension { // todo: implement return GasesByDimension{} @@ -369,7 +360,6 @@ func calcSStoreGas( rData []byte, depth int, err error, - prevOpcodeState *PrevOpcodeState, ) GasesByDimension { // todo: implement return GasesByDimension{} @@ -385,7 +375,6 @@ func calcSelfDestructGas( rData []byte, depth int, err error, - prevOpcodeState *PrevOpcodeState, ) GasesByDimension { // todo: implement return GasesByDimension{} diff --git a/eth/tracers/native/prev_opcode_state.go b/eth/tracers/native/prev_opcode_state.go deleted file mode 100644 index 1455231320..0000000000 --- a/eth/tracers/native/prev_opcode_state.go +++ /dev/null @@ -1,69 +0,0 @@ -package native - -import ( - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/tracing" - "github.com/holiman/uint256" -) - -type PrevOpcodeState struct { - MemoryData []byte - StackData []uint256.Int - Caller common.Address - Address common.Address - CallValue *uint256.Int - CallInput []byte -} - -// Deep copies an opcontext into a PrevOpcodeState -// so you can introspect state changes on a per-opcode level -func DeepCopyOpcodeState(scope tracing.OpContext) *PrevOpcodeState { - // Create new slices and copy memory data - memCopy := make([]byte, len(scope.MemoryData())) - copy(memCopy, scope.MemoryData()) - - // Create new slice and copy stack data - stackData := scope.StackData() - stackCopy := make([]uint256.Int, len(stackData)) - for i := range stackData { - stackCopy[i].Set(&stackData[i]) // Assuming uint256.Int has a Set method - } - - // Create new slice and copy call input - inputCopy := make([]byte, len(scope.CallInput())) - copy(inputCopy, scope.CallInput()) - - // Deep copy the call value - var callValueCopy uint256.Int - if scope.CallValue() != nil { - callValueCopy.Set(scope.CallValue()) - } - - return &PrevOpcodeState{ - MemoryData: memCopy, - StackData: stackCopy, - Caller: scope.Caller(), // Value type, copy is automatic - Address: scope.Address(), // Value type, copy is automatic - CallValue: &callValueCopy, - CallInput: inputCopy, - } -} - -// freePrevOpcodeState helps manage memory by clearing references to allow GC -// to reclaim memory more aggressively. This is particularly useful when we know -// the lifecycle of the data and want to ensure memory is freed as soon as possible. -func freePrevOpcodeState(state *PrevOpcodeState) { - if state == nil { - return - } - - // Clear slice references to allow GC - state.MemoryData = nil - state.StackData = nil - state.CallInput = nil - - // Clear pointer reference - state.CallValue = nil - - // Address and Caller are value types, no need to clear -} From 99dc3ab44fea3965424298af611e4c49c82e2878 Mon Sep 17 00:00:00 2001 From: relyt29 Date: Fri, 4 Apr 2025 14:56:18 -0400 Subject: [PATCH 04/35] gas dimensions for SLOAD. Tested by hand with: Cold access list SLOAD Warm access list SLOAD --- eth/tracers/native/gas_dimension_calc.go | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index 892bfe9c44..d04b4df25e 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -163,8 +163,27 @@ func calcSLOADGas( depth int, err error, ) GasesByDimension { - // need access to StateDb.AddressInAccessList and StateDb.SlotInAccessList - return GasesByDimension{} + // we don't have access to StateDb.SlotInAccessList + // so we have to infer whether the slot was cold or warm based on the absolute cost + // and then deduce the dimensions from that + if cost == params.ColdSloadCostEIP2929 { + accessCost := params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929 + leftOver := cost - accessCost + return GasesByDimension{ + Computation: leftOver, + StateAccess: accessCost, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + } + } + return GasesByDimension{ + Computation: cost, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + } } // copied from go-ethereum/core/vm/gas_table.go because not exported there From f82c8f6124b34dc8fd57014135813d7f0ca2fd82 Mon Sep 17 00:00:00 2001 From: relyt29 Date: Fri, 4 Apr 2025 17:13:23 -0400 Subject: [PATCH 05/35] gas emission for LOG0-4, tested by hand: all of the LOG opcodes with both indexed and unindexed data --- eth/tracers/native/gas_dimension_calc.go | 126 +++++++++++++++-------- 1 file changed, 82 insertions(+), 44 deletions(-) diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index d04b4df25e..8b08e3c9cb 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -186,48 +186,6 @@ func calcSLOADGas( } } -// copied from go-ethereum/core/vm/gas_table.go because not exported there -// toWordSize returns the ceiled word size required for memory expansion. -func toWordSize(size uint64) uint64 { - if size > math.MaxUint64-31 { - return math.MaxUint64/32 + 1 - } - - return (size + 31) / 32 -} - -// This code is copied and edited from go-ethereum/core/vm/gas_table.go -// because the code there is not exported. -// memoryGasCost calculates the quadratic gas for memory expansion. It does so -// only for the memory region that is expanded, not the total memory. -func memoryGasCost(mem []byte, lastGasCost uint64, newMemSize uint64) (fee uint64, newTotalFee uint64, err error) { - if newMemSize == 0 { - return 0, 0, nil - } - // The maximum that will fit in a uint64 is max_word_count - 1. Anything above - // that will result in an overflow. Additionally, a newMemSize which results in - // a newMemSizeWords larger than 0xFFFFFFFF will cause the square operation to - // overflow. The constant 0x1FFFFFFFE0 is the highest number that can be used - // without overflowing the gas calculation. - if newMemSize > 0x1FFFFFFFE0 { - return 0, 0, vm.ErrGasUintOverflow - } - newMemSizeWords := toWordSize(newMemSize) - newMemSize = newMemSizeWords * 32 - - if newMemSize > uint64(len(mem)) { - square := newMemSizeWords * newMemSizeWords - linCoef := newMemSizeWords * params.MemoryGas - quadCoef := square / params.QuadCoeffDiv - newTotalFee = linCoef + quadCoef - - fee = newTotalFee - lastGasCost - - return fee, newTotalFee, nil - } - return 0, 0, nil -} - // calcExtCodeCopyGas returns the gas used // for the `EXTCODECOPY` opcode, which reads from // the code of an external contract. @@ -326,8 +284,42 @@ func calcLogGas( depth int, err error, ) GasesByDimension { - // todo: implement - return GasesByDimension{} + // log gas = 375 + 375 * topic_count + 8 * size + memory_expansion_cost + // 8 * size is always history growth + // the size is charged 8 gas per byte, and 32 bytes per topic are + // stored in the bloom filter in the history so at 8 gas per byte, + // 32 bytes per topic is 256 gas per topic. + // rest is computation (for the bloom filter computation, memory expansion, etc) + numTopics := uint64(0) + switch vm.OpCode(op) { + case vm.LOG0: + numTopics = 0 + case vm.LOG1: + numTopics = 1 + case vm.LOG2: + numTopics = 2 + case vm.LOG3: + numTopics = 3 + case vm.LOG4: + numTopics = 4 + default: + numTopics = 0 + } + bloomHistoryGrowthCost := 256 * numTopics + // size is on stack position 2 + stackData := scope.StackData() + size := stackData[len(stackData)-2].Uint64() + sizeHistoryGrowthCost := 8 * size + historyGrowthCost := sizeHistoryGrowthCost + bloomHistoryGrowthCost + computationCost := cost - historyGrowthCost + + return GasesByDimension{ + Computation: computationCost, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: historyGrowthCost, + StateGrowthRefund: 0, + } } // calcCreateGas returns the gas used for the CREATE set of opcodes @@ -398,3 +390,49 @@ func calcSelfDestructGas( // todo: implement return GasesByDimension{} } + +// ############################################################################ +// HELPER FUNCTIONS +// ############################################################################ + +// copied from go-ethereum/core/vm/gas_table.go because not exported there +// toWordSize returns the ceiled word size required for memory expansion. +func toWordSize(size uint64) uint64 { + if size > math.MaxUint64-31 { + return math.MaxUint64/32 + 1 + } + + return (size + 31) / 32 +} + +// This code is copied and edited from go-ethereum/core/vm/gas_table.go +// because the code there is not exported. +// memoryGasCost calculates the quadratic gas for memory expansion. It does so +// only for the memory region that is expanded, not the total memory. +func memoryGasCost(mem []byte, lastGasCost uint64, newMemSize uint64) (fee uint64, newTotalFee uint64, err error) { + if newMemSize == 0 { + return 0, 0, nil + } + // The maximum that will fit in a uint64 is max_word_count - 1. Anything above + // that will result in an overflow. Additionally, a newMemSize which results in + // a newMemSizeWords larger than 0xFFFFFFFF will cause the square operation to + // overflow. The constant 0x1FFFFFFFE0 is the highest number that can be used + // without overflowing the gas calculation. + if newMemSize > 0x1FFFFFFFE0 { + return 0, 0, vm.ErrGasUintOverflow + } + newMemSizeWords := toWordSize(newMemSize) + newMemSize = newMemSizeWords * 32 + + if newMemSize > uint64(len(mem)) { + square := newMemSizeWords * newMemSizeWords + linCoef := newMemSizeWords * params.MemoryGas + quadCoef := square / params.QuadCoeffDiv + newTotalFee = linCoef + quadCoef + + fee = newTotalFee - lastGasCost + + return fee, newTotalFee, nil + } + return 0, 0, nil +} From 8da713522c7c775e896643cc8040fc9691d3af4e Mon Sep 17 00:00:00 2001 From: relyt29 Date: Fri, 4 Apr 2025 18:01:32 -0400 Subject: [PATCH 06/35] gas dimensions for SELFDESTRUCT opcode, needs testing --- eth/tracers/native/gas_dimension_calc.go | 60 +++++++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index 8b08e3c9cb..e87269ff5b 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -387,8 +387,64 @@ func calcSelfDestructGas( depth int, err error, ) GasesByDimension { - // todo: implement - return GasesByDimension{} + // reverse engineer the gas dimensions from the cost + // two things we care about: + // address being cold or warm for the access set + // account being empty or not for the target of the selfdestruct. + // basically there are only 4 possible cases for the cost: + // 5000, 7600 (cold), 30000 (warm but target for funds is empty), 32600 (warm and target for funds is not empty) + // we consider the static cost of 5000 as a state read/write because selfdestruct, + // excepting 100 for the warm access set + // doesn't actually delete anything from disk, it just marks it as deleted. + if cost == params.CreateBySelfdestructGas+params.SelfdestructGasEIP150 { + // warm but funds target empty + // 30000 gas total + // 100 for warm cost (computation) + // 25000 for the selfdestruct (state growth) + // 4900 for read/write (deleting the contract) + return GasesByDimension{ + Computation: params.WarmStorageReadCostEIP2929, + StateAccess: params.SelfdestructGasEIP150 - params.WarmStorageReadCostEIP2929, + StateGrowth: params.CreateBySelfdestructGas, + HistoryGrowth: 0, + StateGrowthRefund: 0, + } + } else if cost == params.CreateBySelfdestructGas+params.SelfdestructGasEIP150+params.ColdAccountAccessCostEIP2929 { + // cold and funds target empty + // 32600 gas total + // 100 for warm cost (computation) + // 25000 for the selfdestruct (state growth) + // 2500 + 5000 for read/write (deleting the contract) + return GasesByDimension{ + Computation: params.WarmStorageReadCostEIP2929, + StateAccess: params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 + params.SelfdestructGasEIP150, + StateGrowth: params.CreateBySelfdestructGas, + HistoryGrowth: 0, + StateGrowthRefund: 0, + } + } else if cost == params.SelfdestructGasEIP150+params.ColdAccountAccessCostEIP2929 { + // address lookup was cold but funds target has money already. Cost is 7600 + // 100 for warm cost (computation) + // 2500 to access the address cold (access) + // 5000 for the selfdestruct (access) + return GasesByDimension{ + Computation: params.WarmStorageReadCostEIP2929, + StateAccess: params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 + params.SelfdestructGasEIP150, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + } + } + // if you reach here, then the cost was 5000 + // in which case give 100 for a warm access read + // and 4900 for the state access (deleting the contract) + return GasesByDimension{ + Computation: params.WarmStorageReadCostEIP2929, + StateAccess: params.SelfdestructGasEIP150 - params.WarmStorageReadCostEIP2929, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + } } // ############################################################################ From 5abd69f9d42c8591a5d05f9406b30ca2f1862baa Mon Sep 17 00:00:00 2001 From: relyt29 Date: Mon, 7 Apr 2025 16:52:34 -0400 Subject: [PATCH 07/35] gas emission for selfdestruct, tested by hand: Selfdestruct to: * cold, empty account target, sender has balance * warm, empty account target, sender has balance * warm, code/value at target, sender has balance * warm, code/value at target, no money to send * cold, code/value at target, sender has balance --- eth/tracers/native/gas_dimension_calc.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index e87269ff5b..2e5e5bc1a7 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -392,7 +392,10 @@ func calcSelfDestructGas( // address being cold or warm for the access set // account being empty or not for the target of the selfdestruct. // basically there are only 4 possible cases for the cost: - // 5000, 7600 (cold), 30000 (warm but target for funds is empty), 32600 (warm and target for funds is not empty) + // 5000 (warm, target for funds is not empty), + // 7600 (cold, target for funds is not empty), + // 30000 (warm, target for funds is empty), + // 32600 (cold, target for funds is empty) // we consider the static cost of 5000 as a state read/write because selfdestruct, // excepting 100 for the warm access set // doesn't actually delete anything from disk, it just marks it as deleted. From e5c941e0414a015056243a23540546b3933a0a16 Mon Sep 17 00:00:00 2001 From: relyt29 Date: Tue, 8 Apr 2025 22:09:58 -0400 Subject: [PATCH 08/35] gas emission for staticcall, tested by hand Tested: * calling to a contract that is cold but has code * calling to a contract that is cold and has no code --- eth/tracers/native/gas_dimension.go | 103 ++++++++- eth/tracers/native/gas_dimension_calc.go | 262 +++++++++++++++++++---- 2 files changed, 321 insertions(+), 44 deletions(-) diff --git a/eth/tracers/native/gas_dimension.go b/eth/tracers/native/gas_dimension.go index 9dcb766531..f90bcec6bc 100644 --- a/eth/tracers/native/gas_dimension.go +++ b/eth/tracers/native/gas_dimension.go @@ -2,6 +2,7 @@ package native import ( "encoding/json" + "fmt" "sync/atomic" "github.com/ethereum/go-ethereum/common" @@ -28,6 +29,9 @@ type DimensionLog struct { StateGrowth uint64 `json:"stateGrowth"` HistoryGrowth uint64 `json:"historyGrowth"` StateGrowthRefund uint64 `json:"stateGrowthRefund"` + CallRealGas uint64 `json:"callRealGas"` + CallExecutionCost uint64 `json:"callExecutionCost"` + CallMemoryExpansion uint64 `json:"callMemoryExpansion"` Err error `json:"-"` } @@ -41,11 +45,14 @@ func (d *DimensionLog) ErrorString() string { // gasDimensionTracer struct type GasDimensionTracer struct { - env *tracing.VMContext - txHash common.Hash - logs []DimensionLog - err error - usedGas uint64 + env *tracing.VMContext + txHash common.Hash + logs []DimensionLog + err error + usedGas uint64 + callStack CallGasDimensionStack + depth int + previousOpcode vm.OpCode interrupt atomic.Bool // Atomic flag to signal execution interruption reason error // Textual reason for the interruption @@ -59,7 +66,9 @@ func NewGasDimensionTracer( _ json.RawMessage, ) (*tracers.Tracer, error) { - t := &GasDimensionTracer{} + t := &GasDimensionTracer{ + depth: 1, + } return &tracers.Tracer{ Hooks: &tracing.Hooks{ @@ -91,12 +100,29 @@ func (t *GasDimensionTracer) OnOpcode( return } + if depth > t.depth { + t.depth = depth + } + f := getCalcGasDimensionFunc(vm.OpCode(op)) - gasesByDimension := f(pc, op, gas, cost, scope, rData, depth, err) + gasesByDimension, callStackInfo := f(pc, op, gas, cost, scope, rData, depth, err) + // if callStackInfo is not nil then we need to take a note of the index of the + // DimensionLog that represents this opcode and save the callStackInfo + // to call finishX after the call has returned + if callStackInfo != nil { + opcodeLogIndex := len(t.logs) + t.callStack.Push( + CallGasDimensionStackInfo{ + gasDimensionInfo: *callStackInfo, + dimensionLogPosition: opcodeLogIndex, + executionCost: 0, + }) + } + opcode := vm.OpCode(op) t.logs = append(t.logs, DimensionLog{ Pc: pc, - Op: vm.OpCode(op), + Op: opcode, Depth: depth, OneDimensionalGasCost: cost, Computation: gasesByDimension[Computation], @@ -106,6 +132,55 @@ func (t *GasDimensionTracer) OnOpcode( StateGrowthRefund: gasesByDimension[StateGrowthRefund], Err: err, }) + + // if the opcode returns from the call stack depth, or + // if this is an opcode immediately after a call that did not increase the stack depth + // because it called an empty account or contract or wrong function signature, + // call the appropriate finishX function to write the gas dimensions + // for the call that increased the stack depth in the past + if depth < t.depth || (t.depth == depth && wasCall(t.previousOpcode)) { + stackInfo, ok := t.callStack.Pop() + // base case, stack is empty, do nothing + if !ok { + // I am not sure if we should consider an empty stack an error + // theoretically the top-level of the stack could be empty if + // the transaction was not inside a call and one of the four key opcodes + // was fired. That would halt execution of the transaction so theoretically + // doing nothing should be the correct behavior - i thiiiiiiink + //t.interrupt.Store(true) + //t.reason = fmt.Errorf("call stack depth is empty, top level should now halt execution") + return + } + finishFunction := getFinishCalcGasDimensionFunc(stackInfo.gasDimensionInfo.op) + if finishFunction == nil { + t.interrupt.Store(true) + t.reason = fmt.Errorf("no finish function found for RETURN opcode, call stack is messed up") + return + } + // IMPORTANT NOTE: for some reason the only reliable way to actually get the gas cost of the call + // is to subtract gas at time of call from gas at opcode AFTER return + gasUsedByCall := stackInfo.gasDimensionInfo.gasCounterAtTimeOfCall - gas + gasesByDimension := finishFunction(gasUsedByCall, stackInfo.executionCost, stackInfo.gasDimensionInfo) + callDimensionLog := t.logs[stackInfo.dimensionLogPosition] + callDimensionLog.Computation = gasesByDimension[Computation] + callDimensionLog.StateAccess = gasesByDimension[StateAccess] + callDimensionLog.StateGrowth = gasesByDimension[StateGrowth] + callDimensionLog.HistoryGrowth = gasesByDimension[HistoryGrowth] + callDimensionLog.StateGrowthRefund = gasesByDimension[StateGrowthRefund] + callDimensionLog.CallRealGas = gasUsedByCall + callDimensionLog.CallExecutionCost = stackInfo.executionCost + callDimensionLog.CallMemoryExpansion = stackInfo.gasDimensionInfo.memoryExpansionCost + t.logs[stackInfo.dimensionLogPosition] = callDimensionLog + t.depth = depth + } + + // if we are in a call stack depth greater than 0, then we need to track the execution gas + // of our own code so that when the call returns, we can write the gas dimensions for the call opcode itself + // but we shouldn't do this if we are the call ourselves! + if len(t.callStack) > 0 && callStackInfo == nil { + t.callStack.UpdateExecutionCost(cost) + } + t.previousOpcode = opcode } /* @@ -172,6 +247,12 @@ func (t *GasDimensionTracer) Stop(err error) { // JSON OUTPUT PRODUCTION // ############################################################################ +// wasCall returns true if the opcode is a type of opcode that makes calls increasing the stack depth +// todo: does CREATE and CREATE2 count? +func wasCall(opcode vm.OpCode) bool { + return opcode == vm.CALL || opcode == vm.CALLCODE || opcode == vm.DELEGATECALL || opcode == vm.STATICCALL +} + // DimensionLogs returns the captured log entries. func (t *GasDimensionTracer) DimensionLogs() []DimensionLog { return t.logs } @@ -220,6 +301,9 @@ type DimensionLogRes struct { StateGrowth uint64 `json:"growth,omitempty"` HistoryGrowth uint64 `json:"hist,omitempty"` StateGrowthRefund uint64 `json:"refund,omitempty"` + CallRealGas uint64 `json:"callRealGas,omitempty"` + CallExecutionCost uint64 `json:"callExecutionCost,omitempty"` + CallMemoryExpansion uint64 `json:"callMemoryExpansion,omitempty"` Err error `json:"error,omitempty"` } @@ -237,6 +321,9 @@ func formatLogs(logs []DimensionLog) []DimensionLogRes { StateGrowth: trace.StateGrowth, HistoryGrowth: trace.HistoryGrowth, StateGrowthRefund: trace.StateGrowthRefund, + CallRealGas: trace.CallRealGas, + CallExecutionCost: trace.CallExecutionCost, + CallMemoryExpansion: trace.CallMemoryExpansion, Err: trace.Err, } } diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index 2e5e5bc1a7..6451b0849b 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -26,11 +26,62 @@ const ( StateGrowthRefund GasDimension = 4 ) +// in the case of opcodes like CALL, STATICCALL, DELEGATECALL, etc, +// in order to calculate the gas dimensions we need to allow the call to complete +// and then look at the data from that completion after the call has returned. +// CallGasDimensionInfo retains the relevant information that needs to be remembered +// from the start of the call to compute the gas dimensions after the call has returned. +type CallGasDimensionInfo struct { + op vm.OpCode + gasCounterAtTimeOfCall uint64 + memoryExpansionCost uint64 +} + +// CallGasDimensionStackInfo is a struct that contains the gas dimension info +// and the position of the dimension log in the dimension logs array +// so that the finish functions can directly write into the dimension logs +type CallGasDimensionStackInfo struct { + gasDimensionInfo CallGasDimensionInfo + dimensionLogPosition int + executionCost uint64 +} + +// CallGasDimensionStack is a stack of CallGasDimensionStackInfo +// so that RETURN opcodes can pop the appropriate gas dimension info +// and then write the gas dimensions into the dimension logs +type CallGasDimensionStack []CallGasDimensionStackInfo + +// Push a new CallGasDimensionStackInfo onto the stack +func (c *CallGasDimensionStack) Push(info CallGasDimensionStackInfo) { // gasDimensionInfo CallGasDimensionInfo, dimensionLogPosition int, executionCost uint64) { + *c = append(*c, info) +} + +// Pop a CallGasDimensionStackInfo from the stack, returning false if the stack is empty +func (c *CallGasDimensionStack) Pop() (CallGasDimensionStackInfo, bool) { + if len(*c) == 0 { + return CallGasDimensionStackInfo{}, false + } + last := (*c)[len(*c)-1] + *c = (*c)[:len(*c)-1] + return last, true +} + +// UpdateExecutionCost updates the execution cost for the top layer of the call stack +// so that the call knows how much gas was consumed by child opcodes in that call depth +func (c *CallGasDimensionStack) UpdateExecutionCost(executionCost uint64) { + if len(*c) == 0 { + return + } + top := (*c)[len(*c)-1] + top.executionCost += executionCost + (*c)[len(*c)-1] = top +} + // calcGasDimensionFunc defines a type signature that takes the opcode // tracing data for an opcode and return the gas consumption for each dimension // for that given opcode. // -// INVARIANT: the sum of the gas consumption for each dimension +// INVARIANT (for non-call opcodes): the sum of the gas consumption for each dimension // equals the input `gas` to this function type calcGasDimensionFunc func( pc uint64, @@ -40,6 +91,19 @@ type calcGasDimensionFunc func( rData []byte, depth int, err error, +) (GasesByDimension, *CallGasDimensionInfo) + +// finishCalcGasDimensionFunc defines a type signature that takes the +// code execution cost of the call and the callGasDimensionInfo +// and returns the gas dimensions for the call opcode itself +// THIS EXPLICITLY BREAKS THE ABOVE INVARIANT FOR THE NON-CALL OPCODES +// as call opcodes only contain the dimensions for the call itself, +// and the dimensions of their children are computed as their children are +// seen/traced. +type finishCalcGasDimensionFunc func( + totalCallGasUsed uint64, + codeExecutionCost uint64, + callGasDimensionInfo CallGasDimensionInfo, ) GasesByDimension // getCalcGasDimensionFunc is a massive case switch @@ -85,6 +149,20 @@ func getCalcGasDimensionFunc(op vm.OpCode) calcGasDimensionFunc { } } +// for any opcode that increases the depth of the stack, +// we have to call a finish function after the call has returned +// to know the code_execution_cost of the call +// and then use that to compute the gas dimensions +// for the call opcode itself. +func getFinishCalcGasDimensionFunc(op vm.OpCode) finishCalcGasDimensionFunc { + switch op { + case vm.DELEGATECALL, vm.STATICCALL: + return finishCalcStateReadCallGas + default: + return nil + } +} + // calcSimpleSingleDimensionGas returns the gas used for the // simplest of transactions, that only use the computation dimension func calcSimpleSingleDimensionGas( @@ -95,14 +173,14 @@ func calcSimpleSingleDimensionGas( rData []byte, depth int, err error, -) GasesByDimension { +) (GasesByDimension, *CallGasDimensionInfo) { return GasesByDimension{ Computation: cost, StateAccess: 0, StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, - } + }, nil } // calcSimpleAddressAccessSetGas returns the gas used @@ -121,7 +199,7 @@ func calcSimpleAddressAccessSetGas( rData []byte, depth int, err error, -) GasesByDimension { +) (GasesByDimension, *CallGasDimensionInfo) { // We do not have access to StateDb.AddressInAccessList and StateDb.SlotInAccessList // to check cold storage access directly. // Additionally, cold storage access for these address opcodes are handled differently @@ -141,7 +219,7 @@ func calcSimpleAddressAccessSetGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, - } + }, nil } return GasesByDimension{ Computation: cost, @@ -149,7 +227,7 @@ func calcSimpleAddressAccessSetGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, - } + }, nil } // calcSLOADGas returns the gas used for the `SLOAD` opcode @@ -162,7 +240,7 @@ func calcSLOADGas( rData []byte, depth int, err error, -) GasesByDimension { +) (GasesByDimension, *CallGasDimensionInfo) { // we don't have access to StateDb.SlotInAccessList // so we have to infer whether the slot was cold or warm based on the absolute cost // and then deduce the dimensions from that @@ -175,7 +253,7 @@ func calcSLOADGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, - } + }, nil } return GasesByDimension{ Computation: cost, @@ -183,7 +261,7 @@ func calcSLOADGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, - } + }, nil } // calcExtCodeCopyGas returns the gas used @@ -198,7 +276,7 @@ func calcExtCodeCopyGas( rData []byte, depth int, err error, -) GasesByDimension { +) (GasesByDimension, *CallGasDimensionInfo) { // extcodecody has three components to its gas cost: // 1. minimum_word_size = (size + 31) / 32 // 2. memory_expansion_cost @@ -218,16 +296,9 @@ func calcExtCodeCopyGas( size := stack[lenStack-4].Uint64() // size in stack position 4 offset := stack[lenStack-2].Uint64() // destination offset in stack position 2 - // computing the memory gas cost requires knowing a "previous" price - var emptyMem []byte = make([]byte, 0) - memoryDataBeforeExtCodeCopyApplied := scope.MemoryData() - _, lastGasCost, memErr := memoryGasCost(emptyMem, 0, uint64(len(memoryDataBeforeExtCodeCopyApplied))) - if memErr != nil { - return GasesByDimension{} - } - memoryExpansionCost, _, memErr := memoryGasCost(memoryDataBeforeExtCodeCopyApplied, lastGasCost, size+offset) + memoryExpansionCost, memErr := memoryExpansionCost(scope.MemoryData(), offset, size) if memErr != nil { - return GasesByDimension{} + return GasesByDimension{}, nil } minimumWordSizeCost := (size + 31) / 32 * 3 leftOver := cost - memoryExpansionCost - minimumWordSizeCost @@ -243,7 +314,7 @@ func calcExtCodeCopyGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, - } + }, nil } // calcStateReadCallGas returns the gas used @@ -263,10 +334,77 @@ func calcStateReadCallGas( rData []byte, depth int, err error, -) GasesByDimension { +) (GasesByDimension, *CallGasDimensionInfo) { + stack := scope.StackData() + lenStack := len(stack) + // argsOffset in stack position 3 (1-indexed) + // argsSize in stack position 4 + argsOffset := stack[lenStack-3].Uint64() + argsSize := stack[lenStack-4].Uint64() + // return data offset in stack position 5 + // return data size in stack position 6 + returnDataOffset := stack[lenStack-5].Uint64() + returnDataSize := stack[lenStack-6].Uint64() - // todo: implement - return GasesByDimension{} + // to figure out memory expansion cost, take the bigger of the two memory writes + // which will determine how big memory is expanded to + var memExpansionOffset uint64 = argsOffset + var memExpansionSize uint64 = argsSize + if returnDataOffset+returnDataSize > argsOffset+argsSize { + memExpansionOffset = returnDataOffset + memExpansionSize = returnDataSize + } + + memoryExpansionCost, memErr := memoryExpansionCost(scope.MemoryData(), memExpansionOffset, memExpansionSize) + if memErr != nil { + return GasesByDimension{}, nil + } + + // at a minimum, the cost is 100 for the warm access set + // and the memory expansion cost + computation := memoryExpansionCost + params.WarmStorageReadCostEIP2929 + // see finishCalcStateReadCallGas for more details + return GasesByDimension{ + Computation: computation, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + }, &CallGasDimensionInfo{ + op: vm.OpCode(op), + gasCounterAtTimeOfCall: gas, + memoryExpansionCost: memoryExpansionCost, + } +} + +// In order to calculate the gas dimensions for opcodes that +// increase the stack depth, we need to know +// the computed gas consumption of the code executed in the call +// AFAIK, this is only computable after the call has returned +// the caller is responsible for maintaining the state of the CallGasDimensionInfo +// when the call is first seen, and then calling finishX after the call has returned. +func finishCalcStateReadCallGas( + totalCallGasUsed uint64, + codeExecutionCost uint64, + callGasDimensionInfo CallGasDimensionInfo, +) GasesByDimension { + leftOver := totalCallGasUsed - callGasDimensionInfo.memoryExpansionCost - codeExecutionCost + if leftOver == params.ColdAccountAccessCostEIP2929 { + return GasesByDimension{ + Computation: params.WarmStorageReadCostEIP2929 + callGasDimensionInfo.memoryExpansionCost, + StateAccess: params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + } + } + return GasesByDimension{ + Computation: leftOver + callGasDimensionInfo.memoryExpansionCost, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + } } // calcLogGas returns the gas used for the `LOG0, LOG1, LOG2, LOG3, LOG4` opcodes @@ -283,7 +421,7 @@ func calcLogGas( rData []byte, depth int, err error, -) GasesByDimension { +) (GasesByDimension, *CallGasDimensionInfo) { // log gas = 375 + 375 * topic_count + 8 * size + memory_expansion_cost // 8 * size is always history growth // the size is charged 8 gas per byte, and 32 bytes per topic are @@ -319,7 +457,7 @@ func calcLogGas( StateGrowth: 0, HistoryGrowth: historyGrowthCost, StateGrowthRefund: 0, - } + }, nil } // calcCreateGas returns the gas used for the CREATE set of opcodes @@ -334,9 +472,9 @@ func calcCreateGas( rData []byte, depth int, err error, -) GasesByDimension { +) (GasesByDimension, *CallGasDimensionInfo) { // todo: implement - return GasesByDimension{} + return GasesByDimension{}, nil } // calcReadAndStoreCallGas returns the gas used for the `CALL, CALLCODE` opcodes @@ -353,9 +491,9 @@ func calcReadAndStoreCallGas( rData []byte, depth int, err error, -) GasesByDimension { +) (GasesByDimension, *CallGasDimensionInfo) { // todo: implement - return GasesByDimension{} + return GasesByDimension{}, nil } // calcSStoreGas returns the gas used for the `SSTORE` opcode @@ -371,9 +509,9 @@ func calcSStoreGas( rData []byte, depth int, err error, -) GasesByDimension { +) (GasesByDimension, *CallGasDimensionInfo) { // todo: implement - return GasesByDimension{} + return GasesByDimension{}, nil } // calcSelfDestructGas returns the gas used for the `SELFDESTRUCT` opcode @@ -386,7 +524,7 @@ func calcSelfDestructGas( rData []byte, depth int, err error, -) GasesByDimension { +) (GasesByDimension, *CallGasDimensionInfo) { // reverse engineer the gas dimensions from the cost // two things we care about: // address being cold or warm for the access set @@ -411,7 +549,7 @@ func calcSelfDestructGas( StateGrowth: params.CreateBySelfdestructGas, HistoryGrowth: 0, StateGrowthRefund: 0, - } + }, nil } else if cost == params.CreateBySelfdestructGas+params.SelfdestructGasEIP150+params.ColdAccountAccessCostEIP2929 { // cold and funds target empty // 32600 gas total @@ -424,7 +562,7 @@ func calcSelfDestructGas( StateGrowth: params.CreateBySelfdestructGas, HistoryGrowth: 0, StateGrowthRefund: 0, - } + }, nil } else if cost == params.SelfdestructGasEIP150+params.ColdAccountAccessCostEIP2929 { // address lookup was cold but funds target has money already. Cost is 7600 // 100 for warm cost (computation) @@ -436,7 +574,7 @@ func calcSelfDestructGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, - } + }, nil } // if you reach here, then the cost was 5000 // in which case give 100 for a warm access read @@ -447,13 +585,65 @@ func calcSelfDestructGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, - } + }, nil } // ############################################################################ // HELPER FUNCTIONS // ############################################################################ +// Calculating the memory expansion cost requires calling `memoryGasCost` twice +// because computing the memory gas cost expects to know a "previous" price +func memoryExpansionCost(memoryBefore []byte, offset uint64, size uint64) (memoryExpansionCost uint64, err error) { + // calculating the "lastGasCost" requires working around uint64 overflow + var lastGasCost uint64 = calculateLastGasCost(toWordSize(uint64(len(memoryBefore)))) + memoryExpansionCost, _, err = memoryGasCost(memoryBefore, lastGasCost, size+offset) + return memoryExpansionCost, err +} + +func calculateLastGasCost(targetWords uint64) uint64 { + // Start from 0 words and build up + var currentWords uint64 = 0 + var totalGas uint64 = 0 + + // Use a reasonable step size that won't cause overflow + // We'll increase by 2^20 words (about 1M words) at a time maximum + const maxStep uint64 = 1 << 20 + + for currentWords < targetWords { + var nextWords uint64 + if targetWords-currentWords > maxStep { + nextWords = currentWords + maxStep + } else { + nextWords = targetWords + } + // Calculate incremental cost from currentWords to nextWords + // Using the formula: (newWords - oldWords) * (3 + (newWords + oldWords) / 512) + wordsDiff := nextWords - currentWords + wordsSum := nextWords + currentWords + + // Use 64-bit math carefully to avoid overflow + // First part: 3 * wordsDiff + linearCost := 3 * wordsDiff + + // Second part: wordsDiff * wordsSum / 512 + // We do this carefully to avoid overflow: + // 1. Calculate wordsDiff * (wordsSum / 512) + // 2. Add any remainder: wordsDiff * (wordsSum % 512) / 512 + quadraticCost := wordsDiff * (wordsSum / 512) + remainder := wordsDiff * (wordsSum % 512) / 512 + + incrementalCost := linearCost + quadraticCost + remainder + + // Add to total cost + totalGas += incrementalCost + + // Move to next chunk + currentWords = nextWords + } + return totalGas +} + // copied from go-ethereum/core/vm/gas_table.go because not exported there // toWordSize returns the ceiled word size required for memory expansion. func toWordSize(size uint64) uint64 { From deed6c82c89cf0386a08d2d9ee55758ad4c5a0dd Mon Sep 17 00:00:00 2001 From: relyt29 Date: Wed, 9 Apr 2025 10:47:02 -0400 Subject: [PATCH 09/35] squash bugs, gas emission for staticcall, tested by hand Tested: * calling to a contract that is warm and has code * calling to a contract that is cold and has code * calling to a contract that is warm and has no code * calling to a contract that is cold and has no code --- eth/tracers/native/gas_dimension.go | 203 ++++++++++++++-------------- 1 file changed, 98 insertions(+), 105 deletions(-) diff --git a/eth/tracers/native/gas_dimension.go b/eth/tracers/native/gas_dimension.go index f90bcec6bc..5ac2a7614c 100644 --- a/eth/tracers/native/gas_dimension.go +++ b/eth/tracers/native/gas_dimension.go @@ -45,14 +45,13 @@ func (d *DimensionLog) ErrorString() string { // gasDimensionTracer struct type GasDimensionTracer struct { - env *tracing.VMContext - txHash common.Hash - logs []DimensionLog - err error - usedGas uint64 - callStack CallGasDimensionStack - depth int - previousOpcode vm.OpCode + env *tracing.VMContext + txHash common.Hash + logs []DimensionLog + err error + usedGas uint64 + callStack CallGasDimensionStack + depth int interrupt atomic.Bool // Atomic flag to signal execution interruption reason error // Textual reason for the interruption @@ -99,27 +98,45 @@ func (t *GasDimensionTracer) OnOpcode( if t.interrupt.Load() { return } - - if depth > t.depth { - t.depth = depth + if depth != t.depth && depth != t.depth-1 { + t.interrupt.Store(true) + t.reason = fmt.Errorf( + "expected depth fell out of sync with actual depth: %d %d != %d, callStack: %v", + pc, + t.depth, + depth, + t.callStack, + ) + return + } + if t.depth != len(t.callStack)+1 { + t.interrupt.Store(true) + t.reason = fmt.Errorf( + "depth fell out of sync with callStack: %d %d != %d, callStack: %v", + pc, + t.depth, + len(t.callStack), + t.callStack, + ) } + // get the gas dimension function + // if it's not a call, directly calculate the gas dimensions for the opcode f := getCalcGasDimensionFunc(vm.OpCode(op)) gasesByDimension, callStackInfo := f(pc, op, gas, cost, scope, rData, depth, err) - // if callStackInfo is not nil then we need to take a note of the index of the - // DimensionLog that represents this opcode and save the callStackInfo - // to call finishX after the call has returned - if callStackInfo != nil { - opcodeLogIndex := len(t.logs) - t.callStack.Push( - CallGasDimensionStackInfo{ - gasDimensionInfo: *callStackInfo, - dimensionLogPosition: opcodeLogIndex, - executionCost: 0, - }) - } opcode := vm.OpCode(op) + if wasCall(opcode) && callStackInfo == nil || !wasCall(opcode) && callStackInfo != nil { + t.interrupt.Store(true) + t.reason = fmt.Errorf( + "logic bug, calls should always be accompanied by callStackInfo and non-calls should not have callStackInfo %d %s %v", + pc, + opcode.String(), + callStackInfo, + ) + return + } + t.logs = append(t.logs, DimensionLog{ Pc: pc, Op: opcode, @@ -133,93 +150,69 @@ func (t *GasDimensionTracer) OnOpcode( Err: err, }) - // if the opcode returns from the call stack depth, or - // if this is an opcode immediately after a call that did not increase the stack depth - // because it called an empty account or contract or wrong function signature, - // call the appropriate finishX function to write the gas dimensions - // for the call that increased the stack depth in the past - if depth < t.depth || (t.depth == depth && wasCall(t.previousOpcode)) { - stackInfo, ok := t.callStack.Pop() - // base case, stack is empty, do nothing - if !ok { - // I am not sure if we should consider an empty stack an error - // theoretically the top-level of the stack could be empty if - // the transaction was not inside a call and one of the four key opcodes - // was fired. That would halt execution of the transaction so theoretically - // doing nothing should be the correct behavior - i thiiiiiiink - //t.interrupt.Store(true) - //t.reason = fmt.Errorf("call stack depth is empty, top level should now halt execution") - return - } - finishFunction := getFinishCalcGasDimensionFunc(stackInfo.gasDimensionInfo.op) - if finishFunction == nil { - t.interrupt.Store(true) - t.reason = fmt.Errorf("no finish function found for RETURN opcode, call stack is messed up") - return + // if callStackInfo is not nil then we need to take a note of the index of the + // DimensionLog that represents this opcode and save the callStackInfo + // to call finishX after the call has returned + if wasCall(opcode) { + opcodeLogIndex := len(t.logs) - 1 // minus 1 because we've already appended the log + t.callStack.Push( + CallGasDimensionStackInfo{ + gasDimensionInfo: *callStackInfo, + dimensionLogPosition: opcodeLogIndex, + executionCost: 0, + }) + t.depth += 1 + } else { + // if the opcode returns from the call stack depth, or + // if this is an opcode immediately after a call that did not increase the stack depth + // because it called an empty account or contract or wrong function signature, + // call the appropriate finishX function to write the gas dimensions + // for the call that increased the stack depth in the past + if depth < t.depth { + stackInfo, ok := t.callStack.Pop() + // base case, stack is empty, do nothing + if !ok { + t.interrupt.Store(true) + t.reason = fmt.Errorf("call stack is unexpectedly empty %d %d %d", pc, depth, t.depth) + return + } + finishFunction := getFinishCalcGasDimensionFunc(stackInfo.gasDimensionInfo.op) + if finishFunction == nil { + t.interrupt.Store(true) + t.reason = fmt.Errorf( + "no finish function found for opcode %s, call stack is messed up %d", + stackInfo.gasDimensionInfo.op.String(), + pc, + ) + return + } + // IMPORTANT NOTE: for some reason the only reliable way to actually get the gas cost of the call + // is to subtract gas at time of call from gas at opcode AFTER return + // you can't trust the `gas` field on the call itself. I wonder if the gas field is an estimation + gasUsedByCall := stackInfo.gasDimensionInfo.gasCounterAtTimeOfCall - gas + gasesByDimension := finishFunction(gasUsedByCall, stackInfo.executionCost, stackInfo.gasDimensionInfo) + callDimensionLog := t.logs[stackInfo.dimensionLogPosition] + callDimensionLog.Computation = gasesByDimension[Computation] + callDimensionLog.StateAccess = gasesByDimension[StateAccess] + callDimensionLog.StateGrowth = gasesByDimension[StateGrowth] + callDimensionLog.HistoryGrowth = gasesByDimension[HistoryGrowth] + callDimensionLog.StateGrowthRefund = gasesByDimension[StateGrowthRefund] + callDimensionLog.CallRealGas = gasUsedByCall + callDimensionLog.CallExecutionCost = stackInfo.executionCost + callDimensionLog.CallMemoryExpansion = stackInfo.gasDimensionInfo.memoryExpansionCost + t.logs[stackInfo.dimensionLogPosition] = callDimensionLog + t.depth -= 1 } - // IMPORTANT NOTE: for some reason the only reliable way to actually get the gas cost of the call - // is to subtract gas at time of call from gas at opcode AFTER return - gasUsedByCall := stackInfo.gasDimensionInfo.gasCounterAtTimeOfCall - gas - gasesByDimension := finishFunction(gasUsedByCall, stackInfo.executionCost, stackInfo.gasDimensionInfo) - callDimensionLog := t.logs[stackInfo.dimensionLogPosition] - callDimensionLog.Computation = gasesByDimension[Computation] - callDimensionLog.StateAccess = gasesByDimension[StateAccess] - callDimensionLog.StateGrowth = gasesByDimension[StateGrowth] - callDimensionLog.HistoryGrowth = gasesByDimension[HistoryGrowth] - callDimensionLog.StateGrowthRefund = gasesByDimension[StateGrowthRefund] - callDimensionLog.CallRealGas = gasUsedByCall - callDimensionLog.CallExecutionCost = stackInfo.executionCost - callDimensionLog.CallMemoryExpansion = stackInfo.gasDimensionInfo.memoryExpansionCost - t.logs[stackInfo.dimensionLogPosition] = callDimensionLog - t.depth = depth - } - - // if we are in a call stack depth greater than 0, then we need to track the execution gas - // of our own code so that when the call returns, we can write the gas dimensions for the call opcode itself - // but we shouldn't do this if we are the call ourselves! - if len(t.callStack) > 0 && callStackInfo == nil { - t.callStack.UpdateExecutionCost(cost) - } - t.previousOpcode = opcode -} - -/* - -This code is garbage left over from when I was trying to figure out how to directly -compute state access costs. It doesn't work but I'm keeping it around for when -its time to do sload or sstore which will fire this onGasChange event. -// hook into gas changes -// used to observe Cold Storage Accesses -// We do not have access to StateDb.AddressInAccessList and StateDb.SlotInAccessList -// to check cold storage access directly. So instead what we do here is we -// assign all of the gas to the CPU dimension, which is what it would be if the -// state access was warm. If the state access is cold, then immediately after -// the onOpcode event is fired, we should observe an OnGasChange event -// that will indicate the GasChangeReason is a GasChangeCallStorageColdAccess -// and then we modify the logs in that hook. -func (t *GasDimensionTracer) OnGasChange(old, new uint64, reason tracing.GasChangeReason) { - fmt.Println("OnGasChange", old, new, reason) - if reason == tracing.GasChangeCallStorageColdAccess { - lastLog := t.logs[len(t.logs)-1] - fmt.Println("lastLog", lastLog) - if canHaveColdStorageAccess(lastLog.Op) { - coldCost := new - old - fmt.Println("coldCost", coldCost) - lastLog.StateAccess += coldCost - lastLog.Computation -= coldCost - // replace the last log with the corrected log - t.logs[len(t.logs)-1] = lastLog - fmt.Println("correctedLog", lastLog) - } else { - lastLog.Err = fmt.Errorf("cold storage access on opcode that is unsupported??? %s", lastLog.Op.String()) - t.interrupt.Store(true) - t.reason = lastLog.Err - return + // if we are in a call stack depth greater than 0, + // then we need to track the execution gas + // of our own code so that when the call returns, + // we can write the gas dimensions for the call opcode itself + if len(t.callStack) > 0 { + t.callStack.UpdateExecutionCost(cost) } } } -*/ func (t *GasDimensionTracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) { t.env = env From 547c7d2969cadd3dc644706afb166f5cf1dd68d3 Mon Sep 17 00:00:00 2001 From: relyt29 Date: Wed, 9 Apr 2025 11:55:53 -0400 Subject: [PATCH 10/35] kill memExpansion bug where size is zero, DelegatCall tests Tested by hand: * Delegatecall to warm address, to a contract with code * Delegatecall to cold address, to a contract with code * Delegatecall to cold address, to a contract with no code * Delegatecall to warm address, to a contract with no code --- eth/tracers/native/gas_dimension.go | 7 ++- eth/tracers/native/gas_dimension_calc.go | 76 ++++++++++++++---------- 2 files changed, 50 insertions(+), 33 deletions(-) diff --git a/eth/tracers/native/gas_dimension.go b/eth/tracers/native/gas_dimension.go index 5ac2a7614c..3cf684d1d4 100644 --- a/eth/tracers/native/gas_dimension.go +++ b/eth/tracers/native/gas_dimension.go @@ -123,7 +123,12 @@ func (t *GasDimensionTracer) OnOpcode( // get the gas dimension function // if it's not a call, directly calculate the gas dimensions for the opcode f := getCalcGasDimensionFunc(vm.OpCode(op)) - gasesByDimension, callStackInfo := f(pc, op, gas, cost, scope, rData, depth, err) + gasesByDimension, callStackInfo, err := f(pc, op, gas, cost, scope, rData, depth, err) + if err != nil { + t.interrupt.Store(true) + t.reason = err + return + } opcode := vm.OpCode(op) if wasCall(opcode) && callStackInfo == nil || !wasCall(opcode) && callStackInfo != nil { diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index 6451b0849b..6ba751038f 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -91,7 +91,7 @@ type calcGasDimensionFunc func( rData []byte, depth int, err error, -) (GasesByDimension, *CallGasDimensionInfo) +) (GasesByDimension, *CallGasDimensionInfo, error) // finishCalcGasDimensionFunc defines a type signature that takes the // code execution cost of the call and the callGasDimensionInfo @@ -173,14 +173,14 @@ func calcSimpleSingleDimensionGas( rData []byte, depth int, err error, -) (GasesByDimension, *CallGasDimensionInfo) { +) (GasesByDimension, *CallGasDimensionInfo, error) { return GasesByDimension{ Computation: cost, StateAccess: 0, StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, - }, nil + }, nil, nil } // calcSimpleAddressAccessSetGas returns the gas used @@ -199,7 +199,7 @@ func calcSimpleAddressAccessSetGas( rData []byte, depth int, err error, -) (GasesByDimension, *CallGasDimensionInfo) { +) (GasesByDimension, *CallGasDimensionInfo, error) { // We do not have access to StateDb.AddressInAccessList and StateDb.SlotInAccessList // to check cold storage access directly. // Additionally, cold storage access for these address opcodes are handled differently @@ -219,7 +219,7 @@ func calcSimpleAddressAccessSetGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, - }, nil + }, nil, nil } return GasesByDimension{ Computation: cost, @@ -227,7 +227,7 @@ func calcSimpleAddressAccessSetGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, - }, nil + }, nil, nil } // calcSLOADGas returns the gas used for the `SLOAD` opcode @@ -240,7 +240,7 @@ func calcSLOADGas( rData []byte, depth int, err error, -) (GasesByDimension, *CallGasDimensionInfo) { +) (GasesByDimension, *CallGasDimensionInfo, error) { // we don't have access to StateDb.SlotInAccessList // so we have to infer whether the slot was cold or warm based on the absolute cost // and then deduce the dimensions from that @@ -253,7 +253,7 @@ func calcSLOADGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, - }, nil + }, nil, nil } return GasesByDimension{ Computation: cost, @@ -261,7 +261,7 @@ func calcSLOADGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, - }, nil + }, nil, nil } // calcExtCodeCopyGas returns the gas used @@ -276,7 +276,7 @@ func calcExtCodeCopyGas( rData []byte, depth int, err error, -) (GasesByDimension, *CallGasDimensionInfo) { +) (GasesByDimension, *CallGasDimensionInfo, error) { // extcodecody has three components to its gas cost: // 1. minimum_word_size = (size + 31) / 32 // 2. memory_expansion_cost @@ -298,7 +298,7 @@ func calcExtCodeCopyGas( memoryExpansionCost, memErr := memoryExpansionCost(scope.MemoryData(), offset, size) if memErr != nil { - return GasesByDimension{}, nil + return GasesByDimension{}, nil, memErr } minimumWordSizeCost := (size + 31) / 32 * 3 leftOver := cost - memoryExpansionCost - minimumWordSizeCost @@ -314,7 +314,7 @@ func calcExtCodeCopyGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, - }, nil + }, nil, nil } // calcStateReadCallGas returns the gas used @@ -334,17 +334,25 @@ func calcStateReadCallGas( rData []byte, depth int, err error, -) (GasesByDimension, *CallGasDimensionInfo) { +) (GasesByDimension, *CallGasDimensionInfo, error) { stack := scope.StackData() lenStack := len(stack) // argsOffset in stack position 3 (1-indexed) // argsSize in stack position 4 argsOffset := stack[lenStack-3].Uint64() argsSize := stack[lenStack-4].Uint64() + // Note that opcodes with a byte size parameter of 0 will not trigger memory expansion, regardless of their offset parameters + if argsSize == 0 { + argsOffset = 0 + } // return data offset in stack position 5 // return data size in stack position 6 returnDataOffset := stack[lenStack-5].Uint64() returnDataSize := stack[lenStack-6].Uint64() + // Note that opcodes with a byte size parameter of 0 will not trigger memory expansion, regardless of their offset parameters + if returnDataSize == 0 { + returnDataOffset = 0 + } // to figure out memory expansion cost, take the bigger of the two memory writes // which will determine how big memory is expanded to @@ -355,14 +363,18 @@ func calcStateReadCallGas( memExpansionSize = returnDataSize } - memoryExpansionCost, memErr := memoryExpansionCost(scope.MemoryData(), memExpansionOffset, memExpansionSize) - if memErr != nil { - return GasesByDimension{}, nil + var memExpansionCost uint64 = 0 + var memErr error = nil + if memExpansionOffset+memExpansionSize != 0 { + memExpansionCost, memErr = memoryExpansionCost(scope.MemoryData(), memExpansionOffset, memExpansionSize) + if memErr != nil { + return GasesByDimension{}, nil, memErr + } } // at a minimum, the cost is 100 for the warm access set // and the memory expansion cost - computation := memoryExpansionCost + params.WarmStorageReadCostEIP2929 + computation := memExpansionCost + params.WarmStorageReadCostEIP2929 // see finishCalcStateReadCallGas for more details return GasesByDimension{ Computation: computation, @@ -373,8 +385,8 @@ func calcStateReadCallGas( }, &CallGasDimensionInfo{ op: vm.OpCode(op), gasCounterAtTimeOfCall: gas, - memoryExpansionCost: memoryExpansionCost, - } + memoryExpansionCost: memExpansionCost, + }, nil } // In order to calculate the gas dimensions for opcodes that @@ -421,7 +433,7 @@ func calcLogGas( rData []byte, depth int, err error, -) (GasesByDimension, *CallGasDimensionInfo) { +) (GasesByDimension, *CallGasDimensionInfo, error) { // log gas = 375 + 375 * topic_count + 8 * size + memory_expansion_cost // 8 * size is always history growth // the size is charged 8 gas per byte, and 32 bytes per topic are @@ -457,7 +469,7 @@ func calcLogGas( StateGrowth: 0, HistoryGrowth: historyGrowthCost, StateGrowthRefund: 0, - }, nil + }, nil, nil } // calcCreateGas returns the gas used for the CREATE set of opcodes @@ -472,9 +484,9 @@ func calcCreateGas( rData []byte, depth int, err error, -) (GasesByDimension, *CallGasDimensionInfo) { +) (GasesByDimension, *CallGasDimensionInfo, error) { // todo: implement - return GasesByDimension{}, nil + return GasesByDimension{}, nil, nil } // calcReadAndStoreCallGas returns the gas used for the `CALL, CALLCODE` opcodes @@ -491,9 +503,9 @@ func calcReadAndStoreCallGas( rData []byte, depth int, err error, -) (GasesByDimension, *CallGasDimensionInfo) { +) (GasesByDimension, *CallGasDimensionInfo, error) { // todo: implement - return GasesByDimension{}, nil + return GasesByDimension{}, nil, nil } // calcSStoreGas returns the gas used for the `SSTORE` opcode @@ -509,9 +521,9 @@ func calcSStoreGas( rData []byte, depth int, err error, -) (GasesByDimension, *CallGasDimensionInfo) { +) (GasesByDimension, *CallGasDimensionInfo, error) { // todo: implement - return GasesByDimension{}, nil + return GasesByDimension{}, nil, nil } // calcSelfDestructGas returns the gas used for the `SELFDESTRUCT` opcode @@ -524,7 +536,7 @@ func calcSelfDestructGas( rData []byte, depth int, err error, -) (GasesByDimension, *CallGasDimensionInfo) { +) (GasesByDimension, *CallGasDimensionInfo, error) { // reverse engineer the gas dimensions from the cost // two things we care about: // address being cold or warm for the access set @@ -549,7 +561,7 @@ func calcSelfDestructGas( StateGrowth: params.CreateBySelfdestructGas, HistoryGrowth: 0, StateGrowthRefund: 0, - }, nil + }, nil, nil } else if cost == params.CreateBySelfdestructGas+params.SelfdestructGasEIP150+params.ColdAccountAccessCostEIP2929 { // cold and funds target empty // 32600 gas total @@ -562,7 +574,7 @@ func calcSelfDestructGas( StateGrowth: params.CreateBySelfdestructGas, HistoryGrowth: 0, StateGrowthRefund: 0, - }, nil + }, nil, nil } else if cost == params.SelfdestructGasEIP150+params.ColdAccountAccessCostEIP2929 { // address lookup was cold but funds target has money already. Cost is 7600 // 100 for warm cost (computation) @@ -574,7 +586,7 @@ func calcSelfDestructGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, - }, nil + }, nil, nil } // if you reach here, then the cost was 5000 // in which case give 100 for a warm access read @@ -585,7 +597,7 @@ func calcSelfDestructGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, - }, nil + }, nil, nil } // ############################################################################ From b0ef715352f8391e3c7b7ad166588b82ee5e081e Mon Sep 17 00:00:00 2001 From: relyt29 Date: Wed, 9 Apr 2025 14:57:32 -0400 Subject: [PATCH 11/35] gas dimensions for CALL opcode (untested: CALLCODE also) Tested by Hand: * warm access list+ target has code + zero value * warm access list+ target has code + positive value * warm access list+ target has no code + zero value * warm access list+ target has no code + positive value * cold access list+ target has code + zero value * cold access list+ target has code + positive value * cold access list+ target has no code + zero value * cold access list+ target has no code + positive value --- eth/tracers/native/gas_dimension.go | 20 +-- eth/tracers/native/gas_dimension_calc.go | 149 +++++++++++++++++++++-- 2 files changed, 147 insertions(+), 22 deletions(-) diff --git a/eth/tracers/native/gas_dimension.go b/eth/tracers/native/gas_dimension.go index 3cf684d1d4..de37397053 100644 --- a/eth/tracers/native/gas_dimension.go +++ b/eth/tracers/native/gas_dimension.go @@ -147,11 +147,11 @@ func (t *GasDimensionTracer) OnOpcode( Op: opcode, Depth: depth, OneDimensionalGasCost: cost, - Computation: gasesByDimension[Computation], - StateAccess: gasesByDimension[StateAccess], - StateGrowth: gasesByDimension[StateGrowth], - HistoryGrowth: gasesByDimension[HistoryGrowth], - StateGrowthRefund: gasesByDimension[StateGrowthRefund], + Computation: gasesByDimension.Computation, + StateAccess: gasesByDimension.StateAccess, + StateGrowth: gasesByDimension.StateGrowth, + HistoryGrowth: gasesByDimension.HistoryGrowth, + StateGrowthRefund: gasesByDimension.StateGrowthRefund, Err: err, }) @@ -197,11 +197,11 @@ func (t *GasDimensionTracer) OnOpcode( gasUsedByCall := stackInfo.gasDimensionInfo.gasCounterAtTimeOfCall - gas gasesByDimension := finishFunction(gasUsedByCall, stackInfo.executionCost, stackInfo.gasDimensionInfo) callDimensionLog := t.logs[stackInfo.dimensionLogPosition] - callDimensionLog.Computation = gasesByDimension[Computation] - callDimensionLog.StateAccess = gasesByDimension[StateAccess] - callDimensionLog.StateGrowth = gasesByDimension[StateGrowth] - callDimensionLog.HistoryGrowth = gasesByDimension[HistoryGrowth] - callDimensionLog.StateGrowthRefund = gasesByDimension[StateGrowthRefund] + callDimensionLog.Computation = gasesByDimension.Computation + callDimensionLog.StateAccess = gasesByDimension.StateAccess + callDimensionLog.StateGrowth = gasesByDimension.StateGrowth + callDimensionLog.HistoryGrowth = gasesByDimension.HistoryGrowth + callDimensionLog.StateGrowthRefund = gasesByDimension.StateGrowthRefund callDimensionLog.CallRealGas = gasUsedByCall callDimensionLog.CallExecutionCost = stackInfo.executionCost callDimensionLog.CallMemoryExpansion = stackInfo.gasDimensionInfo.memoryExpansionCost diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index 6ba751038f..fbc960727f 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -15,16 +15,25 @@ import ( // 1: Storage Access (Read/Write) // 2: State Growth (Expanding the size of the state) // 3: History Growth (Expanding the size of the history, especially on archive nodes) -type GasesByDimension [5]uint64 -type GasDimension = uint8 - -const ( - Computation GasDimension = 0 - StateAccess GasDimension = 1 - StateGrowth GasDimension = 2 - HistoryGrowth GasDimension = 3 - StateGrowthRefund GasDimension = 4 -) +// type GasesByDimension [5]uint64 +// type GasDimension = uint8 +// +// const ( +// +// Computation GasDimension = 0 +// StateAccess GasDimension = 1 +// StateGrowth GasDimension = 2 +// HistoryGrowth GasDimension = 3 +// StateGrowthRefund GasDimension = 4 +// +// ) +type GasesByDimension struct { + Computation uint64 + StateAccess uint64 + StateGrowth uint64 + HistoryGrowth uint64 + StateGrowthRefund uint64 +} // in the case of opcodes like CALL, STATICCALL, DELEGATECALL, etc, // in order to calculate the gas dimensions we need to allow the call to complete @@ -35,6 +44,7 @@ type CallGasDimensionInfo struct { op vm.OpCode gasCounterAtTimeOfCall uint64 memoryExpansionCost uint64 + isValueSentWithCall bool } // CallGasDimensionStackInfo is a struct that contains the gas dimension info @@ -158,6 +168,8 @@ func getFinishCalcGasDimensionFunc(op vm.OpCode) finishCalcGasDimensionFunc { switch op { case vm.DELEGATECALL, vm.STATICCALL: return finishCalcStateReadCallGas + case vm.CALL, vm.CALLCODE: + return finishCalcStateReadAndStoreCallGas default: return nil } @@ -386,6 +398,7 @@ func calcStateReadCallGas( op: vm.OpCode(op), gasCounterAtTimeOfCall: gas, memoryExpansionCost: memExpansionCost, + isValueSentWithCall: false, }, nil } @@ -395,6 +408,7 @@ func calcStateReadCallGas( // AFAIK, this is only computable after the call has returned // the caller is responsible for maintaining the state of the CallGasDimensionInfo // when the call is first seen, and then calling finishX after the call has returned. +// this function finishes the DELEGATECALL and STATICCALL opcodes func finishCalcStateReadCallGas( totalCallGasUsed uint64, codeExecutionCost uint64, @@ -504,8 +518,119 @@ func calcReadAndStoreCallGas( depth int, err error, ) (GasesByDimension, *CallGasDimensionInfo, error) { - // todo: implement - return GasesByDimension{}, nil, nil + stack := scope.StackData() + lenStack := len(stack) + // value is in stack position 3 + valueSentWithCall := stack[lenStack-3].Uint64() + // argsOffset in stack position 4 (1-indexed) + // argsSize in stack position 5 + argsOffset := stack[lenStack-4].Uint64() + argsSize := stack[lenStack-5].Uint64() + // Note that opcodes with a byte size parameter of 0 will not trigger memory expansion, regardless of their offset parameters + if argsSize == 0 { + argsOffset = 0 + } + // return data offset in stack position 6 + // return data size in stack position 7 + returnDataOffset := stack[lenStack-6].Uint64() + returnDataSize := stack[lenStack-7].Uint64() + // Note that opcodes with a byte size parameter of 0 will not trigger memory expansion, regardless of their offset parameters + if returnDataSize == 0 { + returnDataOffset = 0 + } + + // to figure out memory expansion cost, take the bigger of the two memory writes + // which will determine how big memory is expanded to + var memExpansionOffset uint64 = argsOffset + var memExpansionSize uint64 = argsSize + if returnDataOffset+returnDataSize > argsOffset+argsSize { + memExpansionOffset = returnDataOffset + memExpansionSize = returnDataSize + } + + var memExpansionCost uint64 = 0 + var memErr error = nil + if memExpansionOffset+memExpansionSize != 0 { + memExpansionCost, memErr = memoryExpansionCost(scope.MemoryData(), memExpansionOffset, memExpansionSize) + if memErr != nil { + return GasesByDimension{}, nil, memErr + } + } + + // at a minimum, the cost is 100 for the warm access set + // and the memory expansion cost + computation := memExpansionCost + params.WarmStorageReadCostEIP2929 + // see finishCalcStateReadCallGas for more details + return GasesByDimension{ + Computation: computation, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + }, &CallGasDimensionInfo{ + op: vm.OpCode(op), + gasCounterAtTimeOfCall: gas, + memoryExpansionCost: memExpansionCost, + isValueSentWithCall: valueSentWithCall > 0, + }, nil + +} + +// In order to calculate the gas dimensions for opcodes that +// increase the stack depth, we need to know +// the computed gas consumption of the code executed in the call +// AFAIK, this is only computable after the call has returned +// the caller is responsible for maintaining the state of the CallGasDimensionInfo +// when the call is first seen, and then calling finishX after the call has returned. +// this function finishes the CALL and CALLCODE opcodes +func finishCalcStateReadAndStoreCallGas( + totalCallGasUsed uint64, + codeExecutionCost uint64, + callGasDimensionInfo CallGasDimensionInfo, +) GasesByDimension { + // the stipend is 2300 and it is not charged to the call itself but used in the execution cost + var positiveValueCostLessStipend uint64 = 0 + if callGasDimensionInfo.isValueSentWithCall { + positiveValueCostLessStipend = params.CallValueTransferGas - params.CallStipend + } + // the formula for call is: + // dynamic_gas = memory_expansion_cost + code_execution_cost + address_access_cost + positive_value_cost + value_to_empty_account_cost + // now with leftOver, we are left with address_access_cost + value_to_empty_account_cost + leftOver := totalCallGasUsed - callGasDimensionInfo.memoryExpansionCost - codeExecutionCost - positiveValueCostLessStipend + // the maximum address_access_cost can ever be is 2600. Meanwhile value_to_empty_account_cost is at minimum 25000 + // so if leftOver is greater than 2600 then we know that the value_to_empty_account_cost was 25000 + // and whatever was left over after that was address_access_cost + // callcode is the same as call except does not have value_to_empty_account_cost, + // so this code properly handles it coincidentally, too + if leftOver > params.ColdAccountAccessCostEIP2929 { // there is a value being sent to an empty account + var coldCost uint64 = 0 + if leftOver-params.CallNewAccountGas == params.ColdAccountAccessCostEIP2929 { + coldCost = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 + } + return GasesByDimension{ + Computation: callGasDimensionInfo.memoryExpansionCost + params.WarmStorageReadCostEIP2929, + StateAccess: coldCost + positiveValueCostLessStipend, + StateGrowth: params.CallNewAccountGas, + HistoryGrowth: 0, + StateGrowthRefund: 0, + } + } else if leftOver == params.ColdAccountAccessCostEIP2929 { + var coldCost uint64 = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 + return GasesByDimension{ + Computation: callGasDimensionInfo.memoryExpansionCost + params.WarmStorageReadCostEIP2929, + StateAccess: coldCost + positiveValueCostLessStipend, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + } + } + return GasesByDimension{ + Computation: callGasDimensionInfo.memoryExpansionCost + params.WarmStorageReadCostEIP2929, + StateAccess: positiveValueCostLessStipend, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + } } // calcSStoreGas returns the gas used for the `SSTORE` opcode From cdeb04549239df9e48713db69a20b7b0e97f6e3a Mon Sep 17 00:00:00 2001 From: relyt29 Date: Wed, 9 Apr 2025 16:33:43 -0400 Subject: [PATCH 12/35] gas dimensions for CREATE and CREATE2 Tested By hand: * CREATE * CREATE2 * CREATE sending value * CREATE2 sending value sending value does not affect gas dimensions but I tested it anyways --- eth/tracers/native/gas_dimension.go | 19 +++-- eth/tracers/native/gas_dimension_calc.go | 88 +++++++++++++++++++++--- 2 files changed, 93 insertions(+), 14 deletions(-) diff --git a/eth/tracers/native/gas_dimension.go b/eth/tracers/native/gas_dimension.go index de37397053..5fd5f5a81b 100644 --- a/eth/tracers/native/gas_dimension.go +++ b/eth/tracers/native/gas_dimension.go @@ -32,6 +32,8 @@ type DimensionLog struct { CallRealGas uint64 `json:"callRealGas"` CallExecutionCost uint64 `json:"callExecutionCost"` CallMemoryExpansion uint64 `json:"callMemoryExpansion"` + CreateInitCodeCost uint64 `json:"createInitCodeCost"` + Create2HashCost uint64 `json:"create2HashCost"` Err error `json:"-"` } @@ -131,10 +133,10 @@ func (t *GasDimensionTracer) OnOpcode( } opcode := vm.OpCode(op) - if wasCall(opcode) && callStackInfo == nil || !wasCall(opcode) && callStackInfo != nil { + if wasCallOrCreate(opcode) && callStackInfo == nil || !wasCallOrCreate(opcode) && callStackInfo != nil { t.interrupt.Store(true) t.reason = fmt.Errorf( - "logic bug, calls should always be accompanied by callStackInfo and non-calls should not have callStackInfo %d %s %v", + "logic bug, calls/creates should always be accompanied by callStackInfo and non-calls should not have callStackInfo %d %s %v", pc, opcode.String(), callStackInfo, @@ -158,7 +160,7 @@ func (t *GasDimensionTracer) OnOpcode( // if callStackInfo is not nil then we need to take a note of the index of the // DimensionLog that represents this opcode and save the callStackInfo // to call finishX after the call has returned - if wasCall(opcode) { + if wasCallOrCreate(opcode) { opcodeLogIndex := len(t.logs) - 1 // minus 1 because we've already appended the log t.callStack.Push( CallGasDimensionStackInfo{ @@ -205,6 +207,8 @@ func (t *GasDimensionTracer) OnOpcode( callDimensionLog.CallRealGas = gasUsedByCall callDimensionLog.CallExecutionCost = stackInfo.executionCost callDimensionLog.CallMemoryExpansion = stackInfo.gasDimensionInfo.memoryExpansionCost + callDimensionLog.CreateInitCodeCost = stackInfo.gasDimensionInfo.initCodeCost + callDimensionLog.Create2HashCost = stackInfo.gasDimensionInfo.hashCost t.logs[stackInfo.dimensionLogPosition] = callDimensionLog t.depth -= 1 } @@ -246,9 +250,8 @@ func (t *GasDimensionTracer) Stop(err error) { // ############################################################################ // wasCall returns true if the opcode is a type of opcode that makes calls increasing the stack depth -// todo: does CREATE and CREATE2 count? -func wasCall(opcode vm.OpCode) bool { - return opcode == vm.CALL || opcode == vm.CALLCODE || opcode == vm.DELEGATECALL || opcode == vm.STATICCALL +func wasCallOrCreate(opcode vm.OpCode) bool { + return opcode == vm.CALL || opcode == vm.CALLCODE || opcode == vm.DELEGATECALL || opcode == vm.STATICCALL || opcode == vm.CREATE || opcode == vm.CREATE2 } // DimensionLogs returns the captured log entries. @@ -302,6 +305,8 @@ type DimensionLogRes struct { CallRealGas uint64 `json:"callRealGas,omitempty"` CallExecutionCost uint64 `json:"callExecutionCost,omitempty"` CallMemoryExpansion uint64 `json:"callMemoryExpansion,omitempty"` + CreateInitCodeCost uint64 `json:"createInitCodeCost,omitempty"` + Create2HashCost uint64 `json:"create2HashCost,omitempty"` Err error `json:"error,omitempty"` } @@ -322,6 +327,8 @@ func formatLogs(logs []DimensionLog) []DimensionLogRes { CallRealGas: trace.CallRealGas, CallExecutionCost: trace.CallExecutionCost, CallMemoryExpansion: trace.CallMemoryExpansion, + CreateInitCodeCost: trace.CreateInitCodeCost, + Create2HashCost: trace.Create2HashCost, Err: trace.Err, } } diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index fbc960727f..a7e2363f24 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -45,6 +45,8 @@ type CallGasDimensionInfo struct { gasCounterAtTimeOfCall uint64 memoryExpansionCost uint64 isValueSentWithCall bool + initCodeCost uint64 + hashCost uint64 } // CallGasDimensionStackInfo is a struct that contains the gas dimension info @@ -111,7 +113,7 @@ type calcGasDimensionFunc func( // and the dimensions of their children are computed as their children are // seen/traced. type finishCalcGasDimensionFunc func( - totalCallGasUsed uint64, + totalGasUsed uint64, codeExecutionCost uint64, callGasDimensionInfo CallGasDimensionInfo, ) GasesByDimension @@ -170,6 +172,8 @@ func getFinishCalcGasDimensionFunc(op vm.OpCode) finishCalcGasDimensionFunc { return finishCalcStateReadCallGas case vm.CALL, vm.CALLCODE: return finishCalcStateReadAndStoreCallGas + case vm.CREATE, vm.CREATE2: + return finishCalcCreateGas default: return nil } @@ -399,6 +403,8 @@ func calcStateReadCallGas( gasCounterAtTimeOfCall: gas, memoryExpansionCost: memExpansionCost, isValueSentWithCall: false, + initCodeCost: 0, + hashCost: 0, }, nil } @@ -410,11 +416,11 @@ func calcStateReadCallGas( // when the call is first seen, and then calling finishX after the call has returned. // this function finishes the DELEGATECALL and STATICCALL opcodes func finishCalcStateReadCallGas( - totalCallGasUsed uint64, + totalGasUsed uint64, codeExecutionCost uint64, callGasDimensionInfo CallGasDimensionInfo, ) GasesByDimension { - leftOver := totalCallGasUsed - callGasDimensionInfo.memoryExpansionCost - codeExecutionCost + leftOver := totalGasUsed - callGasDimensionInfo.memoryExpansionCost - codeExecutionCost if leftOver == params.ColdAccountAccessCostEIP2929 { return GasesByDimension{ Computation: params.WarmStorageReadCostEIP2929 + callGasDimensionInfo.memoryExpansionCost, @@ -486,7 +492,7 @@ func calcLogGas( }, nil, nil } -// calcCreateGas returns the gas used for the CREATE set of opcodes +// calcCreateGas returns the gas used for the CREATE and CREATE2 opcodes // which do storage growth when the store the newly created contract code. // the relevant opcodes here are: // `CREATE, CREATE2` @@ -499,8 +505,72 @@ func calcCreateGas( depth int, err error, ) (GasesByDimension, *CallGasDimensionInfo, error) { - // todo: implement - return GasesByDimension{}, nil, nil + // Create costs + // minimum_word_size = (size + 31) / 32 + // init_code_cost = 2 * minimum_word_size + // code_deposit_cost = 200 * deployed_code_size + // static_gas = 32000 + // dynamic_gas = init_code_cost + memory_expansion_cost + deployment_code_execution_cost + code_deposit_cost + stack := scope.StackData() + lenStack := len(stack) + // size is on stack position 3 (1-indexed) + size := stack[lenStack-3].Uint64() + // offset is on stack position 2 (1-indexed) + offset := stack[lenStack-2].Uint64() + minimumWordSize := toWordSize(size) + initCodeCost := 2 * minimumWordSize + // if create2, then additionally we have hash_cost = 6*minimum_word_size + var hashCost uint64 = 0 + if vm.OpCode(op) == vm.CREATE2 { + hashCost = 6 * minimumWordSize + } + + memExpansionCost, memErr := memoryExpansionCost(scope.MemoryData(), offset, size) + if memErr != nil { + return GasesByDimension{}, nil, memErr + } + // at this point we know everything except deployment_code_execution_cost and code_deposit_cost + // so we can get those from the finishCalcCreateGas function + return GasesByDimension{ + Computation: initCodeCost + memExpansionCost + params.CreateGas + hashCost, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + }, &CallGasDimensionInfo{ + op: vm.OpCode(op), + gasCounterAtTimeOfCall: gas, + memoryExpansionCost: memExpansionCost, + isValueSentWithCall: false, + initCodeCost: initCodeCost, + hashCost: hashCost, + }, nil +} + +// finishCalcCreateGas returns the gas used for the CREATE and CREATE2 opcodes +// after finding out the deployment_code_execution_cost +func finishCalcCreateGas( + totalGasUsed uint64, + codeExecutionCost uint64, + callGasDimensionInfo CallGasDimensionInfo, +) GasesByDimension { + // totalGasUsed = init_code_cost + memory_expansion_cost + deployment_code_execution_cost + code_deposit_cost + codeDepositCost := totalGasUsed - params.CreateGas - callGasDimensionInfo.initCodeCost - + callGasDimensionInfo.memoryExpansionCost - callGasDimensionInfo.hashCost - codeExecutionCost + // CALL costs 25000 for write to an empty account, + // so of the 32000 static cost of CREATE and CREATE2 give 25000 to storage growth, + // and then cut the last 7000 in half for compute and state growth to + // manage the cost of intializing new accounts + staticNonNewAccountCost := params.CreateGas - params.CallNewAccountGas + computeNonNewAccountCost := staticNonNewAccountCost / 2 + growthNonNewAccountCost := staticNonNewAccountCost - computeNonNewAccountCost + return GasesByDimension{ + Computation: callGasDimensionInfo.initCodeCost + callGasDimensionInfo.memoryExpansionCost + callGasDimensionInfo.hashCost + computeNonNewAccountCost, + StateAccess: 0, + StateGrowth: growthNonNewAccountCost + params.CallNewAccountGas + codeDepositCost, + HistoryGrowth: 0, + StateGrowthRefund: 0, + } } // calcReadAndStoreCallGas returns the gas used for the `CALL, CALLCODE` opcodes @@ -572,6 +642,8 @@ func calcReadAndStoreCallGas( gasCounterAtTimeOfCall: gas, memoryExpansionCost: memExpansionCost, isValueSentWithCall: valueSentWithCall > 0, + initCodeCost: 0, + hashCost: 0, }, nil } @@ -584,7 +656,7 @@ func calcReadAndStoreCallGas( // when the call is first seen, and then calling finishX after the call has returned. // this function finishes the CALL and CALLCODE opcodes func finishCalcStateReadAndStoreCallGas( - totalCallGasUsed uint64, + totalGasUsed uint64, codeExecutionCost uint64, callGasDimensionInfo CallGasDimensionInfo, ) GasesByDimension { @@ -596,7 +668,7 @@ func finishCalcStateReadAndStoreCallGas( // the formula for call is: // dynamic_gas = memory_expansion_cost + code_execution_cost + address_access_cost + positive_value_cost + value_to_empty_account_cost // now with leftOver, we are left with address_access_cost + value_to_empty_account_cost - leftOver := totalCallGasUsed - callGasDimensionInfo.memoryExpansionCost - codeExecutionCost - positiveValueCostLessStipend + leftOver := totalGasUsed - callGasDimensionInfo.memoryExpansionCost - codeExecutionCost - positiveValueCostLessStipend // the maximum address_access_cost can ever be is 2600. Meanwhile value_to_empty_account_cost is at minimum 25000 // so if leftOver is greater than 2600 then we know that the value_to_empty_account_cost was 25000 // and whatever was left over after that was address_access_cost From 765818cb4cf1565f03d73642ab46bec32ae0d545 Mon Sep 17 00:00:00 2001 From: relyt29 Date: Wed, 9 Apr 2025 20:25:30 -0400 Subject: [PATCH 13/35] gas dimensions for SSTORE tested, and with refunds Tested by hand in variations: * writing to a new slot * writing to the same slot twice with the same value * writing to the same slot twice with a different value * writing to a slot that was uninitialized and setting it back to zero * writing to a slot that was cold but set to non-zero and setting it to nonzero --- eth/tracers/native/gas_dimension.go | 24 +++--- eth/tracers/native/gas_dimension_calc.go | 95 ++++++++++++++++++------ 2 files changed, 84 insertions(+), 35 deletions(-) diff --git a/eth/tracers/native/gas_dimension.go b/eth/tracers/native/gas_dimension.go index 5fd5f5a81b..c078c66fb0 100644 --- a/eth/tracers/native/gas_dimension.go +++ b/eth/tracers/native/gas_dimension.go @@ -28,7 +28,7 @@ type DimensionLog struct { StateAccess uint64 `json:"stateAccess"` StateGrowth uint64 `json:"stateGrowth"` HistoryGrowth uint64 `json:"historyGrowth"` - StateGrowthRefund uint64 `json:"stateGrowthRefund"` + StateGrowthRefund int64 `json:"stateGrowthRefund"` CallRealGas uint64 `json:"callRealGas"` CallExecutionCost uint64 `json:"callExecutionCost"` CallMemoryExpansion uint64 `json:"callMemoryExpansion"` @@ -47,13 +47,14 @@ func (d *DimensionLog) ErrorString() string { // gasDimensionTracer struct type GasDimensionTracer struct { - env *tracing.VMContext - txHash common.Hash - logs []DimensionLog - err error - usedGas uint64 - callStack CallGasDimensionStack - depth int + env *tracing.VMContext + txHash common.Hash + logs []DimensionLog + err error + usedGas uint64 + callStack CallGasDimensionStack + depth int + refundAccumulated uint64 interrupt atomic.Bool // Atomic flag to signal execution interruption reason error // Textual reason for the interruption @@ -68,7 +69,8 @@ func NewGasDimensionTracer( ) (*tracers.Tracer, error) { t := &GasDimensionTracer{ - depth: 1, + depth: 1, + refundAccumulated: 0, } return &tracers.Tracer{ @@ -125,7 +127,7 @@ func (t *GasDimensionTracer) OnOpcode( // get the gas dimension function // if it's not a call, directly calculate the gas dimensions for the opcode f := getCalcGasDimensionFunc(vm.OpCode(op)) - gasesByDimension, callStackInfo, err := f(pc, op, gas, cost, scope, rData, depth, err) + gasesByDimension, callStackInfo, err := f(t, pc, op, gas, cost, scope, rData, depth, err) if err != nil { t.interrupt.Store(true) t.reason = err @@ -301,7 +303,7 @@ type DimensionLogRes struct { StateAccess uint64 `json:"access,omitempty"` StateGrowth uint64 `json:"growth,omitempty"` HistoryGrowth uint64 `json:"hist,omitempty"` - StateGrowthRefund uint64 `json:"refund,omitempty"` + StateGrowthRefund int64 `json:"refund,omitempty"` CallRealGas uint64 `json:"callRealGas,omitempty"` CallExecutionCost uint64 `json:"callExecutionCost,omitempty"` CallMemoryExpansion uint64 `json:"callMemoryExpansion,omitempty"` diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index a7e2363f24..2e4e4c77e3 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -10,29 +10,12 @@ import ( // GasesByDimension is a type that represents the gas consumption for each dimension // for a given opcode. -// The dimensions in order of 0 - 3 are: -// 0: Computation -// 1: Storage Access (Read/Write) -// 2: State Growth (Expanding the size of the state) -// 3: History Growth (Expanding the size of the history, especially on archive nodes) -// type GasesByDimension [5]uint64 -// type GasDimension = uint8 -// -// const ( -// -// Computation GasDimension = 0 -// StateAccess GasDimension = 1 -// StateGrowth GasDimension = 2 -// HistoryGrowth GasDimension = 3 -// StateGrowthRefund GasDimension = 4 -// -// ) type GasesByDimension struct { Computation uint64 StateAccess uint64 StateGrowth uint64 HistoryGrowth uint64 - StateGrowthRefund uint64 + StateGrowthRefund int64 } // in the case of opcodes like CALL, STATICCALL, DELEGATECALL, etc, @@ -96,6 +79,7 @@ func (c *CallGasDimensionStack) UpdateExecutionCost(executionCost uint64) { // INVARIANT (for non-call opcodes): the sum of the gas consumption for each dimension // equals the input `gas` to this function type calcGasDimensionFunc func( + t *GasDimensionTracer, pc uint64, op byte, gas, cost uint64, @@ -182,6 +166,7 @@ func getFinishCalcGasDimensionFunc(op vm.OpCode) finishCalcGasDimensionFunc { // calcSimpleSingleDimensionGas returns the gas used for the // simplest of transactions, that only use the computation dimension func calcSimpleSingleDimensionGas( + t *GasDimensionTracer, pc uint64, op byte, gas, cost uint64, @@ -208,6 +193,7 @@ func calcSimpleSingleDimensionGas( // this includes: // `BALANCE, EXTCODESIZE, EXTCODEHASH func calcSimpleAddressAccessSetGas( + t *GasDimensionTracer, pc uint64, op byte, gas, cost uint64, @@ -249,6 +235,7 @@ func calcSimpleAddressAccessSetGas( // calcSLOADGas returns the gas used for the `SLOAD` opcode // SLOAD reads a slot from the state. It cannot expand the state func calcSLOADGas( + t *GasDimensionTracer, pc uint64, op byte, gas, cost uint64, @@ -285,6 +272,7 @@ func calcSLOADGas( // the code of an external contract. // Hence only state read implications func calcExtCodeCopyGas( + t *GasDimensionTracer, pc uint64, op byte, gas, cost uint64, @@ -343,6 +331,7 @@ func calcExtCodeCopyGas( // are accounted for on those opcodes (e.g. SSTORE), which means the delegatecall opcode itself // only has state read implications. Staticcall is the same but even more read-only. func calcStateReadCallGas( + t *GasDimensionTracer, pc uint64, op byte, gas, cost uint64, @@ -446,6 +435,7 @@ func finishCalcStateReadCallGas( // The relevant opcodes here are: // `LOG0, LOG1, LOG2, LOG3, LOG4` func calcLogGas( + t *GasDimensionTracer, pc uint64, op byte, gas, cost uint64, @@ -497,6 +487,7 @@ func calcLogGas( // the relevant opcodes here are: // `CREATE, CREATE2` func calcCreateGas( + t *GasDimensionTracer, pc uint64, op byte, gas, cost uint64, @@ -580,6 +571,7 @@ func finishCalcCreateGas( // the relevant opcodes here are: // `CALL, CALLCODE` func calcReadAndStoreCallGas( + t *GasDimensionTracer, pc uint64, op byte, gas, cost uint64, @@ -706,11 +698,8 @@ func finishCalcStateReadAndStoreCallGas( } // calcSStoreGas returns the gas used for the `SSTORE` opcode -// which writes to the state. There is a whole lot of complexity around -// gas refunds based on the state of the storage slot before and whether -// refunds happened before in this transaction, -// which manipulate the gas cost of this specific opcode. func calcSStoreGas( + t *GasDimensionTracer, pc uint64, op byte, gas, cost uint64, @@ -719,13 +708,71 @@ func calcSStoreGas( depth int, err error, ) (GasesByDimension, *CallGasDimensionInfo, error) { - // todo: implement - return GasesByDimension{}, nil, nil + // if value == current_value + // base_dynamic_gas = 100 + // else if current_value == original_value + // if original_value == 0 + // base_dynamic_gas = 20000 + // else + // base_dynamic_gas = 2900 + // else + // base_dynamic_gas = 100 + // plus cost of access set cold vs warm + // basically sstore is always state access unless the value is 0 + // in which case it is state growth + + // you have the following cases: + // gas = 100 // warm, value == current_value, + // or value != current_value and current_value != original_value + // in which case its all compute you're just modifying memory really + // gas = 20000 // warm, original_value == 0 + // state_access = 19900, 100 for compute + // gas = 22100 // cold, original_value == 0 + // state_access = 22000, 100 for compute + // gas = 2900 // warm, value != current_value, current_value == original_value, original_value != 0 + // this 2900 is state access, give warm 100 for compute + // gas = 5000 // cold, value != current_value, current_value == original_value, original_value != 0 + // state access for the 2900 and also for the cold access set, give warm 100 for compute + + // REFUND LOGIC + // refunds are tracked in the statedb + // to find per-step changes, we track the accumulated refund + // and compare it to the current refund + currentRefund := t.env.StateDB.GetRefund() + accumulatedRefund := t.refundAccumulated + var diff int64 = 0 + if accumulatedRefund != currentRefund { + if accumulatedRefund < currentRefund { + diff = int64(currentRefund - accumulatedRefund) + } else { + diff = int64(accumulatedRefund - currentRefund) + } + t.refundAccumulated = currentRefund + } + + if cost >= params.SstoreSetGas { // 22100 case and 20000 case + accessCost := cost - params.SstoreSetGas + return GasesByDimension{ + Computation: params.WarmStorageReadCostEIP2929, + StateAccess: accessCost, + StateGrowth: params.SstoreSetGas - params.WarmStorageReadCostEIP2929, + HistoryGrowth: 0, + StateGrowthRefund: diff, + }, nil, nil + } + return GasesByDimension{ + Computation: params.WarmStorageReadCostEIP2929, + StateAccess: cost - params.WarmStorageReadCostEIP2929, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: diff, + }, nil, nil } // calcSelfDestructGas returns the gas used for the `SELFDESTRUCT` opcode // which deletes a contract which is a write to the state func calcSelfDestructGas( + t *GasDimensionTracer, pc uint64, op byte, gas, cost uint64, From 2a1ee9e8eb43d0095eb1729a710573d335e19d31 Mon Sep 17 00:00:00 2001 From: relyt29 Date: Wed, 9 Apr 2025 21:24:56 -0400 Subject: [PATCH 14/35] live tracer leverage native tracer and write txs to folder --- eth/tracers/live/gas_dimension.go | 162 ++++++----------------- eth/tracers/native/gas_dimension.go | 5 + eth/tracers/native/gas_dimension_calc.go | 17 ++- 3 files changed, 56 insertions(+), 128 deletions(-) diff --git a/eth/tracers/live/gas_dimension.go b/eth/tracers/live/gas_dimension.go index 6c81f61789..234c3c7dc4 100644 --- a/eth/tracers/live/gas_dimension.go +++ b/eth/tracers/live/gas_dimension.go @@ -2,27 +2,20 @@ package live import ( "encoding/json" - "errors" "fmt" - "log" + "os" "path/filepath" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/eth/tracers" - - "gopkg.in/natefinch/lumberjack.v2" + "github.com/ethereum/go-ethereum/eth/tracers/native" ) type gasDimensionLiveTracer struct { - logger *log.Logger -} - -type ExecutionResult struct { - Relyt string `json:"relyt"` - TxHash string `json:"txHash"` + Path string `json:"path"` + GasDimensionTracer *tracers.Tracer } func init() { @@ -30,8 +23,7 @@ func init() { } type gasDimensionLiveTracerConfig struct { - Path string `json:"path"` // Path to directory for output - MaxSize int `json:"maxSize"` // MaxSize default 100 MB + Path string `json:"path"` // Path to directory for output } func newGasDimensionLiveTracer(cfg json.RawMessage) (*tracing.Hooks, error) { @@ -41,136 +33,64 @@ func newGasDimensionLiveTracer(cfg json.RawMessage) (*tracing.Hooks, error) { } if config.Path == "" { - return nil, errors.New("gas dimension live tracer path for output is required") + return nil, fmt.Errorf("gas dimension live tracer path for output is required: %v", config) } - loggerOutput := &lumberjack.Logger{ - Filename: filepath.Join(config.Path, "gas_dimension.jsonl"), + gasDimensionTracer, err := native.NewGasDimensionTracer(nil, nil) + if err != nil { + return nil, err } - logger := log.New(loggerOutput, "", 0) - t := &gasDimensionLiveTracer{ - logger: logger, + Path: config.Path, + GasDimensionTracer: gasDimensionTracer, } return &tracing.Hooks{ - OnTxStart: t.OnTxStart, - OnTxEnd: t.OnTxEnd, - //OnEnter: t.OnEnter, - //OnExit: t.OnExit, - //OnOpcode: t.OnOpcode, - //OnFault: t.OnFault, - //OnGasChange: t.OnGasChange, - //OnBlockchainInit: t.OnBlockchainInit, + OnOpcode: t.GasDimensionTracer.OnOpcode, + OnTxStart: t.GasDimensionTracer.OnTxStart, + OnTxEnd: t.OnTxEnd, OnBlockStart: t.OnBlockStart, OnBlockEnd: t.OnBlockEnd, - //OnSkippedBlock: t.OnSkippedBlock, - //OnGenesisBlock: t.OnGenesisBlock, - //OnBalanceChange: t.OnBalanceChange, - //OnNonceChange: t.OnNonceChange, - //OnCodeChange: t.OnCodeChange, - //OnStorageChange: t.OnStorageChange, - //OnLog: t.OnLog, }, nil } -/* -func (t *gasDimensionLiveTracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { - t.logger.Println("Opcode Seen") -} - -func (t *gasDimensionLiveTracer) OnFault(pc uint64, op byte, gas, cost uint64, _ tracing.OpContext, depth int, err error) { - t.logger.Println("Fault Seen") -} - -func (t *gasDimensionLiveTracer) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { - t.logger.Println("Enter Seen") -} +func (t *gasDimensionLiveTracer) OnTxEnd(receipt *types.Receipt, err error) { + // first call the navive tracer's OnTxEnd + t.GasDimensionTracer.OnTxEnd(receipt, err) + + // then get the json from the native tracer + executionResultJsonBytes, errGettingResult := t.GasDimensionTracer.GetResult() + if errGettingResult != nil { + errorJsonString := fmt.Sprintf("{\"errorGettingResult\": \"%s\"}", errGettingResult.Error()) + fmt.Println(errorJsonString) + return + } -func (t *gasDimensionLiveTracer) OnExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) { - t.logger.Println("Exit Seen") -} -*/ + blockNumber := receipt.BlockNumber.String() + txHashString := receipt.TxHash.Hex() -func (t *gasDimensionLiveTracer) OnTxStart(vm *tracing.VMContext, tx *types.Transaction, from common.Address) { - t.logger.Println("Tx Start Seen") -} + // Create the filename + filename := fmt.Sprintf("%s_%s.json", blockNumber, txHashString) + filepath := filepath.Join(t.Path, filename) -func (t *gasDimensionLiveTracer) OnTxEnd(receipt *types.Receipt, err error) { - var executionResult ExecutionResult = ExecutionResult{ - Relyt: "Uninitialized", - TxHash: "Uninitialized", - } - if err != nil { - executionResult = ExecutionResult{ - Relyt: err.Error(), - TxHash: "nil", - } - } else { - if receipt == nil { - executionResult = ExecutionResult{ - Relyt: "Receipt is nil", - TxHash: "Receipt is nil", - } - } else { - executionResult = ExecutionResult{ - Relyt: "hello world relyt29", - TxHash: receipt.TxHash.Hex(), - } - } + // Ensure the directory exists + if err := os.MkdirAll(t.Path, 0755); err != nil { + fmt.Printf("Failed to create directory %s: %v\n", t.Path, err) + return } - executionResultJsonBytes, errMarshalling := json.Marshal(executionResult) - if errMarshalling != nil { - errorJsonString := fmt.Sprintf("{\"errorMarshallingJson\": \"%s\"}", errMarshalling.Error()) - t.logger.Println(errorJsonString) - } else { - t.logger.Println(string(executionResultJsonBytes)) + + // Write the file + if err := os.WriteFile(filepath, executionResultJsonBytes, 0644); err != nil { + fmt.Printf("Failed to write file %s: %v\n", filepath, err) + return } } func (t *gasDimensionLiveTracer) OnBlockStart(ev tracing.BlockEvent) { - t.logger.Println("Block Start") + fmt.Println("Live Tracer Seen: new block", ev.Block.Number()) } func (t *gasDimensionLiveTracer) OnBlockEnd(err error) { - t.logger.Println("Block End") -} - -/* -func (t *gasDimensionLiveTracer) OnSkippedBlock(ev tracing.BlockEvent) { - t.logger.Println("Skipped Block") -} - -func (t *gasDimensionLiveTracer) OnBlockchainInit(chainConfig *params.ChainConfig) { - t.logger.Println("Blockchain Init") -} - -func (t *gasDimensionLiveTracer) OnGenesisBlock(b *types.Block, alloc types.GenesisAlloc) { - t.logger.Println("Genesis Block") -} - -func (t *gasDimensionLiveTracer) OnBalanceChange(a common.Address, prev, new *big.Int, reason tracing.BalanceChangeReason) { - t.logger.Println("Balance Change") -} - -func (t *gasDimensionLiveTracer) OnNonceChange(a common.Address, prev, new uint64) { - t.logger.Println("Nonce Change") -} - -func (t *gasDimensionLiveTracer) OnCodeChange(a common.Address, prevCodeHash common.Hash, prev []byte, codeHash common.Hash, code []byte) { - t.logger.Println("Code Change") -} - -func (t *gasDimensionLiveTracer) OnStorageChange(a common.Address, k, prev, new common.Hash) { - t.logger.Println("Storage Change") -} - -func (t *gasDimensionLiveTracer) OnLog(l *types.Log) { - t.logger.Println("Log") -} - -func (t *gasDimensionLiveTracer) OnGasChange(old, new uint64, reason tracing.GasChangeReason) { - t.logger.Println("Gas Change") + fmt.Println("Live Tracer Seen block end") } -*/ diff --git a/eth/tracers/native/gas_dimension.go b/eth/tracers/native/gas_dimension.go index c078c66fb0..46a586ffcb 100644 --- a/eth/tracers/native/gas_dimension.go +++ b/eth/tracers/native/gas_dimension.go @@ -3,6 +3,7 @@ package native import ( "encoding/json" "fmt" + "math/big" "sync/atomic" "github.com/ethereum/go-ethereum/common" @@ -271,6 +272,8 @@ type ExecutionResult struct { DimensionLogs []DimensionLogRes `json:"dimensionLogs"` TxHash string `json:"txHash"` BlockTimetamp uint64 `json:"blockTimestamp"` + BlockNumber *big.Int `json:"blockNumber"` + GasPrice *big.Int `json:"gasPrice"` } // produce json result for output from tracer @@ -288,6 +291,8 @@ func (t *GasDimensionTracer) GetResult() (json.RawMessage, error) { DimensionLogs: formatLogs(t.DimensionLogs()), TxHash: t.txHash.Hex(), BlockTimetamp: t.env.Time, + BlockNumber: t.env.BlockNumber, + GasPrice: t.env.GasPrice, }) } diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index 2e4e4c77e3..066b051cdf 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -759,14 +759,17 @@ func calcSStoreGas( HistoryGrowth: 0, StateGrowthRefund: diff, }, nil, nil + } else if cost > 0 { + return GasesByDimension{ + Computation: params.WarmStorageReadCostEIP2929, + StateAccess: cost - params.WarmStorageReadCostEIP2929, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: diff, + }, nil, nil } - return GasesByDimension{ - Computation: params.WarmStorageReadCostEIP2929, - StateAccess: cost - params.WarmStorageReadCostEIP2929, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: diff, - }, nil, nil + // bizarre "system transactions" that can have costs of zero... + return GasesByDimension{}, nil, nil } // calcSelfDestructGas returns the gas used for the `SELFDESTRUCT` opcode From fe87615f2be863c65d97a8ced850da277a34780a Mon Sep 17 00:00:00 2001 From: relyt29 Date: Wed, 9 Apr 2025 22:03:27 -0400 Subject: [PATCH 15/35] wrap OnTxStart and OnOpcode for live tracer --- eth/tracers/live/gas_dimension.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/eth/tracers/live/gas_dimension.go b/eth/tracers/live/gas_dimension.go index 234c3c7dc4..b34be01301 100644 --- a/eth/tracers/live/gas_dimension.go +++ b/eth/tracers/live/gas_dimension.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/tracing" @@ -47,14 +48,22 @@ func newGasDimensionLiveTracer(cfg json.RawMessage) (*tracing.Hooks, error) { } return &tracing.Hooks{ - OnOpcode: t.GasDimensionTracer.OnOpcode, - OnTxStart: t.GasDimensionTracer.OnTxStart, + OnOpcode: t.OnOpcode, + OnTxStart: t.OnTxStart, OnTxEnd: t.OnTxEnd, OnBlockStart: t.OnBlockStart, OnBlockEnd: t.OnBlockEnd, }, nil } +func (t *gasDimensionLiveTracer) OnTxStart(vm *tracing.VMContext, tx *types.Transaction, from common.Address) { + t.GasDimensionTracer.OnTxStart(vm, tx, from) +} + +func (t *gasDimensionLiveTracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { + t.GasDimensionTracer.OnOpcode(pc, op, gas, cost, scope, rData, depth, err) +} + func (t *gasDimensionLiveTracer) OnTxEnd(receipt *types.Receipt, err error) { // first call the navive tracer's OnTxEnd t.GasDimensionTracer.OnTxEnd(receipt, err) From 1776dc0d139adc213a99e19f77381210ce9297a9 Mon Sep 17 00:00:00 2001 From: relyt29 Date: Wed, 9 Apr 2025 22:31:40 -0400 Subject: [PATCH 16/35] upstream updates from offchainlabs/go-ethereum --- eth/tracers/live/gas_dimension.go | 2 +- eth/tracers/native/gas_dimension.go | 4 ++-- eth/tracers/native/gas_dimension_calc.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/eth/tracers/live/gas_dimension.go b/eth/tracers/live/gas_dimension.go index b34be01301..386d0e2fa7 100644 --- a/eth/tracers/live/gas_dimension.go +++ b/eth/tracers/live/gas_dimension.go @@ -37,7 +37,7 @@ func newGasDimensionLiveTracer(cfg json.RawMessage) (*tracing.Hooks, error) { return nil, fmt.Errorf("gas dimension live tracer path for output is required: %v", config) } - gasDimensionTracer, err := native.NewGasDimensionTracer(nil, nil) + gasDimensionTracer, err := native.NewGasDimensionTracer(nil, nil, nil) if err != nil { return nil, err } diff --git a/eth/tracers/native/gas_dimension.go b/eth/tracers/native/gas_dimension.go index 46a586ffcb..c53fe60e1e 100644 --- a/eth/tracers/native/gas_dimension.go +++ b/eth/tracers/native/gas_dimension.go @@ -11,6 +11,7 @@ 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/params" ) // initializer for the tracer @@ -67,6 +68,7 @@ type GasDimensionTracer struct { func NewGasDimensionTracer( ctx *tracers.Context, _ json.RawMessage, + _ *params.ChainConfig, ) (*tracers.Tracer, error) { t := &GasDimensionTracer{ @@ -273,7 +275,6 @@ type ExecutionResult struct { TxHash string `json:"txHash"` BlockTimetamp uint64 `json:"blockTimestamp"` BlockNumber *big.Int `json:"blockNumber"` - GasPrice *big.Int `json:"gasPrice"` } // produce json result for output from tracer @@ -292,7 +293,6 @@ func (t *GasDimensionTracer) GetResult() (json.RawMessage, error) { TxHash: t.txHash.Hex(), BlockTimetamp: t.env.Time, BlockNumber: t.env.BlockNumber, - GasPrice: t.env.GasPrice, }) } diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index 066b051cdf..56cf11ca91 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -1,7 +1,7 @@ package native import ( - "github.com/ethereum/go-ethereum/common/math" + "math" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/vm" From d5d2ad2b2db6ed487eeeda689c24ac4c06fdf988 Mon Sep 17 00:00:00 2001 From: relyt29 Date: Thu, 10 Apr 2025 12:56:29 -0400 Subject: [PATCH 17/35] Rename files, add tx gas dimension by opcode tracer --- eth/tracers/live/block_gas_dimension.go | 147 +++++++++ ...imension.go => tx_gas_dimension_logger.go} | 26 +- eth/tracers/native/gas_dimension_calc.go | 290 ++++++++++-------- .../native/tx_gas_dimension_by_opcode.go | 283 +++++++++++++++++ ...imension.go => tx_gas_dimension_logger.go} | 44 ++- 5 files changed, 634 insertions(+), 156 deletions(-) create mode 100644 eth/tracers/live/block_gas_dimension.go rename eth/tracers/live/{gas_dimension.go => tx_gas_dimension_logger.go} (68%) create mode 100644 eth/tracers/native/tx_gas_dimension_by_opcode.go rename eth/tracers/native/{gas_dimension.go => tx_gas_dimension_logger.go} (89%) diff --git a/eth/tracers/live/block_gas_dimension.go b/eth/tracers/live/block_gas_dimension.go new file mode 100644 index 0000000000..b39d576f53 --- /dev/null +++ b/eth/tracers/live/block_gas_dimension.go @@ -0,0 +1,147 @@ +package live + +/* +import ( + "encoding/json" + "fmt" + "math/big" + "os" + "path/filepath" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/eth/tracers" + "github.com/ethereum/go-ethereum/eth/tracers/native" +) + +// tracks the state of the tracer over the lifecycle of the tracer +type blockGasDimensionLiveTracer struct { + Path string `json:"path"` // path directory to write files out to + GasDimensionTracer *tracers.Tracer // gas dimension tracer. Changes every tx + blockNumber *big.Int // block number. changes every block + interrupt atomic.Bool // Atomic flag to signal execution interruption + txInterruptor common.Hash // track which tx broke the tracer + reason error // Textual reason for the interruption +} + +// initialize the tracer +func init() { + tracers.LiveDirectory.Register("blockGasDimension", newBlockGasDimensionLiveTracer) +} + +// config for the tracer +type blockGasDimensionLiveTracerConfig struct { + Path string `json:"path"` // Path to directory for output +} + +// create a new tracer +func newBlockGasDimensionLiveTracer(cfg json.RawMessage) (*tracing.Hooks, error) { + var config blockGasDimensionLiveTracerConfig + if err := json.Unmarshal(cfg, &config); err != nil { + return nil, err + } + + if config.Path == "" { + return nil, fmt.Errorf("gas dimension live tracer path for output is required: %v", config) + } + t := &blockGasDimensionLiveTracer{ + Path: config.Path, + GasDimensionTracer: nil, + blockNumber: nil, + } + + return &tracing.Hooks{ + OnOpcode: t.OnOpcode, + OnTxStart: t.OnTxStart, + OnTxEnd: t.OnTxEnd, + OnBlockStart: t.OnBlockStart, + OnBlockEnd: t.OnBlockEnd, + }, nil +} + +// create a gas dimension tracer for this TX +// then hook it to the txStart event +func (t *blockGasDimensionLiveTracer) OnTxStart(vm *tracing.VMContext, tx *types.Transaction, from common.Address) { + gasDimensionTracer, err := native.NewGasDimensionLogger(nil, nil, nil) + if err != nil { + t.reason = err + t.interrupt.Store(true) + return + } + if t.GasDimensionTracer != nil { + t.reason = fmt.Errorf("single-threaded execution order violation: gas dimension tracer already exists at time of txStart") + t.interrupt.Store(true) + return + } + t.GasDimensionTracer = gasDimensionTracer + t.GasDimensionTracer.OnTxStart(vm, tx, from) +} + +// hook the gasDimensionTracer to the opcode event +func (t *blockGasDimensionLiveTracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { + if t.interrupt.Load() { + return + } + if t.GasDimensionTracer == nil { + t.reason = fmt.Errorf("single-threaded execution order violation: gas dimension tracer not found when onOpcode fired") + t.interrupt.Store(true) + return + } + t.GasDimensionTracer.OnOpcode(pc, op, gas, cost, scope, rData, depth, err) +} + +// on TxEnd collate all the information from this transaction +// and store it for the block +func (t *blockGasDimensionLiveTracer) OnTxEnd(receipt *types.Receipt, err error) { + if t.interrupt.Load() { + return + } + if t.GasDimensionTracer == nil { + t.reason = fmt.Errorf("single-threaded execution order violation: gas dimension tracer not found when onTxEnd fired") + t.interrupt.Store(true) + return + } + t.GasDimensionTracer.OnTxEnd(receipt, err) + + // todo + // go through the gasDimensionTracer's results for this transaction. + + // free the gasDimensionTracer for the next tx + t.GasDimensionTracer = nil +} + +// on block start create a struct to store the gas dimensions for the block +func (t *blockGasDimensionLiveTracer) OnBlockStart(ev tracing.BlockEvent) { + fmt.Println("Live Tracer Seen: new block", ev.Block.Number()) +} + +// on block end write the block's data to a file +func (t *blockGasDimensionLiveTracer) OnBlockEnd(err error) { + fmt.Println("Live Tracer Seen block end") + blockNumber := t.blockNumber.String() + + // Create the filename + filename := fmt.Sprintf("%s.json", blockNumber) + filepath := filepath.Join(t.Path, filename) + + // Ensure the directory exists + if err := os.MkdirAll(t.Path, 0755); err != nil { + fmt.Printf("Failed to create directory %s: %v\n", t.Path, err) + return + } + + // todo + // create executionResultJsonBytes + + // Write the file + if err := os.WriteFile(filepath, executionResultJsonBytes, 0644); err != nil { + fmt.Printf("Failed to write file %s: %v\n", filepath, err) + return + } +} + + +*/ diff --git a/eth/tracers/live/gas_dimension.go b/eth/tracers/live/tx_gas_dimension_logger.go similarity index 68% rename from eth/tracers/live/gas_dimension.go rename to eth/tracers/live/tx_gas_dimension_logger.go index 386d0e2fa7..8ea7024458 100644 --- a/eth/tracers/live/gas_dimension.go +++ b/eth/tracers/live/tx_gas_dimension_logger.go @@ -14,21 +14,21 @@ import ( "github.com/ethereum/go-ethereum/eth/tracers/native" ) -type gasDimensionLiveTracer struct { +type txGasDimensionLiveTraceLogger struct { Path string `json:"path"` GasDimensionTracer *tracers.Tracer } func init() { - tracers.LiveDirectory.Register("gasDimension", newGasDimensionLiveTracer) + tracers.LiveDirectory.Register("txGasDimensionLogger", newTxGasDimensionLiveTraceLogger) } -type gasDimensionLiveTracerConfig struct { +type txGasDimensionLiveTraceLoggerConfig struct { Path string `json:"path"` // Path to directory for output } -func newGasDimensionLiveTracer(cfg json.RawMessage) (*tracing.Hooks, error) { - var config gasDimensionLiveTracerConfig +func newTxGasDimensionLiveTraceLogger(cfg json.RawMessage) (*tracing.Hooks, error) { + var config txGasDimensionLiveTraceLoggerConfig if err := json.Unmarshal(cfg, &config); err != nil { return nil, err } @@ -37,12 +37,12 @@ func newGasDimensionLiveTracer(cfg json.RawMessage) (*tracing.Hooks, error) { return nil, fmt.Errorf("gas dimension live tracer path for output is required: %v", config) } - gasDimensionTracer, err := native.NewGasDimensionTracer(nil, nil, nil) + gasDimensionTracer, err := native.NewTxGasDimensionLogger(nil, nil, nil) if err != nil { return nil, err } - t := &gasDimensionLiveTracer{ + t := &txGasDimensionLiveTraceLogger{ Path: config.Path, GasDimensionTracer: gasDimensionTracer, } @@ -56,16 +56,16 @@ func newGasDimensionLiveTracer(cfg json.RawMessage) (*tracing.Hooks, error) { }, nil } -func (t *gasDimensionLiveTracer) OnTxStart(vm *tracing.VMContext, tx *types.Transaction, from common.Address) { +func (t *txGasDimensionLiveTraceLogger) OnTxStart(vm *tracing.VMContext, tx *types.Transaction, from common.Address) { t.GasDimensionTracer.OnTxStart(vm, tx, from) } -func (t *gasDimensionLiveTracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { +func (t *txGasDimensionLiveTraceLogger) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { t.GasDimensionTracer.OnOpcode(pc, op, gas, cost, scope, rData, depth, err) } -func (t *gasDimensionLiveTracer) OnTxEnd(receipt *types.Receipt, err error) { - // first call the navive tracer's OnTxEnd +func (t *txGasDimensionLiveTraceLogger) OnTxEnd(receipt *types.Receipt, err error) { + // first call the native tracer's OnTxEnd t.GasDimensionTracer.OnTxEnd(receipt, err) // then get the json from the native tracer @@ -96,10 +96,10 @@ func (t *gasDimensionLiveTracer) OnTxEnd(receipt *types.Receipt, err error) { } } -func (t *gasDimensionLiveTracer) OnBlockStart(ev tracing.BlockEvent) { +func (t *txGasDimensionLiveTraceLogger) OnBlockStart(ev tracing.BlockEvent) { fmt.Println("Live Tracer Seen: new block", ev.Block.Number()) } -func (t *gasDimensionLiveTracer) OnBlockEnd(err error) { +func (t *txGasDimensionLiveTraceLogger) OnBlockEnd(err error) { fmt.Println("Live Tracer Seen block end") } diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index 56cf11ca91..4975a383ca 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -11,11 +11,12 @@ import ( // GasesByDimension is a type that represents the gas consumption for each dimension // for a given opcode. type GasesByDimension struct { - Computation uint64 - StateAccess uint64 - StateGrowth uint64 - HistoryGrowth uint64 - StateGrowthRefund int64 + OneDimensionalGasCost uint64 `json:"total"` + Computation uint64 `json:"cpu"` + StateAccess uint64 `json:"access,omitempty"` + StateGrowth uint64 `json:"growth,omitempty"` + HistoryGrowth uint64 `json:"hist,omitempty"` + StateGrowthRefund int64 `json:"refund,omitempty"` } // in the case of opcodes like CALL, STATICCALL, DELEGATECALL, etc, @@ -72,6 +73,15 @@ func (c *CallGasDimensionStack) UpdateExecutionCost(executionCost uint64) { (*c)[len(*c)-1] = top } +// define interface for a dimension tracer +// that provides the minimum necessary methods +// to make the calcSstore function work +type DimensionTracer interface { + GetOpRefund() uint64 + GetRefundAccumulated() uint64 + SetRefundAccumulated(uint64) +} + // calcGasDimensionFunc defines a type signature that takes the opcode // tracing data for an opcode and return the gas consumption for each dimension // for that given opcode. @@ -79,7 +89,7 @@ func (c *CallGasDimensionStack) UpdateExecutionCost(executionCost uint64) { // INVARIANT (for non-call opcodes): the sum of the gas consumption for each dimension // equals the input `gas` to this function type calcGasDimensionFunc func( - t *GasDimensionTracer, + t DimensionTracer, pc uint64, op byte, gas, cost uint64, @@ -166,7 +176,7 @@ func getFinishCalcGasDimensionFunc(op vm.OpCode) finishCalcGasDimensionFunc { // calcSimpleSingleDimensionGas returns the gas used for the // simplest of transactions, that only use the computation dimension func calcSimpleSingleDimensionGas( - t *GasDimensionTracer, + t DimensionTracer, pc uint64, op byte, gas, cost uint64, @@ -176,11 +186,12 @@ func calcSimpleSingleDimensionGas( err error, ) (GasesByDimension, *CallGasDimensionInfo, error) { return GasesByDimension{ - Computation: cost, - StateAccess: 0, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, + OneDimensionalGasCost: cost, + Computation: cost, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, }, nil, nil } @@ -193,7 +204,7 @@ func calcSimpleSingleDimensionGas( // this includes: // `BALANCE, EXTCODESIZE, EXTCODEHASH func calcSimpleAddressAccessSetGas( - t *GasDimensionTracer, + t DimensionTracer, pc uint64, op byte, gas, cost uint64, @@ -216,26 +227,28 @@ func calcSimpleAddressAccessSetGas( if cost == params.ColdAccountAccessCostEIP2929 { return GasesByDimension{ - Computation: params.WarmStorageReadCostEIP2929, - StateAccess: params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, + OneDimensionalGasCost: cost, + Computation: params.WarmStorageReadCostEIP2929, + StateAccess: params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, }, nil, nil } return GasesByDimension{ - Computation: cost, - StateAccess: 0, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, + OneDimensionalGasCost: cost, + Computation: cost, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, }, nil, nil } // calcSLOADGas returns the gas used for the `SLOAD` opcode // SLOAD reads a slot from the state. It cannot expand the state func calcSLOADGas( - t *GasDimensionTracer, + t DimensionTracer, pc uint64, op byte, gas, cost uint64, @@ -251,19 +264,21 @@ func calcSLOADGas( accessCost := params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929 leftOver := cost - accessCost return GasesByDimension{ - Computation: leftOver, - StateAccess: accessCost, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, + OneDimensionalGasCost: cost, + Computation: leftOver, + StateAccess: accessCost, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, }, nil, nil } return GasesByDimension{ - Computation: cost, - StateAccess: 0, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, + OneDimensionalGasCost: cost, + Computation: cost, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, }, nil, nil } @@ -272,7 +287,7 @@ func calcSLOADGas( // the code of an external contract. // Hence only state read implications func calcExtCodeCopyGas( - t *GasDimensionTracer, + t DimensionTracer, pc uint64, op byte, gas, cost uint64, @@ -313,11 +328,12 @@ func calcExtCodeCopyGas( } computation := cost - stateAccess return GasesByDimension{ - Computation: computation, - StateAccess: stateAccess, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, + OneDimensionalGasCost: cost, + Computation: computation, + StateAccess: stateAccess, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, }, nil, nil } @@ -331,7 +347,7 @@ func calcExtCodeCopyGas( // are accounted for on those opcodes (e.g. SSTORE), which means the delegatecall opcode itself // only has state read implications. Staticcall is the same but even more read-only. func calcStateReadCallGas( - t *GasDimensionTracer, + t DimensionTracer, pc uint64, op byte, gas, cost uint64, @@ -382,11 +398,12 @@ func calcStateReadCallGas( computation := memExpansionCost + params.WarmStorageReadCostEIP2929 // see finishCalcStateReadCallGas for more details return GasesByDimension{ - Computation: computation, - StateAccess: 0, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, + OneDimensionalGasCost: cost, + Computation: computation, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, }, &CallGasDimensionInfo{ op: vm.OpCode(op), gasCounterAtTimeOfCall: gas, @@ -412,19 +429,21 @@ func finishCalcStateReadCallGas( leftOver := totalGasUsed - callGasDimensionInfo.memoryExpansionCost - codeExecutionCost if leftOver == params.ColdAccountAccessCostEIP2929 { return GasesByDimension{ - Computation: params.WarmStorageReadCostEIP2929 + callGasDimensionInfo.memoryExpansionCost, - StateAccess: params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, + OneDimensionalGasCost: totalGasUsed, + Computation: params.WarmStorageReadCostEIP2929 + callGasDimensionInfo.memoryExpansionCost, + StateAccess: params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, } } return GasesByDimension{ - Computation: leftOver + callGasDimensionInfo.memoryExpansionCost, - StateAccess: 0, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, + OneDimensionalGasCost: totalGasUsed, + Computation: leftOver + callGasDimensionInfo.memoryExpansionCost, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, } } @@ -435,7 +454,7 @@ func finishCalcStateReadCallGas( // The relevant opcodes here are: // `LOG0, LOG1, LOG2, LOG3, LOG4` func calcLogGas( - t *GasDimensionTracer, + t DimensionTracer, pc uint64, op byte, gas, cost uint64, @@ -474,11 +493,12 @@ func calcLogGas( computationCost := cost - historyGrowthCost return GasesByDimension{ - Computation: computationCost, - StateAccess: 0, - StateGrowth: 0, - HistoryGrowth: historyGrowthCost, - StateGrowthRefund: 0, + OneDimensionalGasCost: cost, + Computation: computationCost, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: historyGrowthCost, + StateGrowthRefund: 0, }, nil, nil } @@ -487,7 +507,7 @@ func calcLogGas( // the relevant opcodes here are: // `CREATE, CREATE2` func calcCreateGas( - t *GasDimensionTracer, + t DimensionTracer, pc uint64, op byte, gas, cost uint64, @@ -523,11 +543,12 @@ func calcCreateGas( // at this point we know everything except deployment_code_execution_cost and code_deposit_cost // so we can get those from the finishCalcCreateGas function return GasesByDimension{ - Computation: initCodeCost + memExpansionCost + params.CreateGas + hashCost, - StateAccess: 0, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, + OneDimensionalGasCost: cost, + Computation: initCodeCost + memExpansionCost + params.CreateGas + hashCost, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, }, &CallGasDimensionInfo{ op: vm.OpCode(op), gasCounterAtTimeOfCall: gas, @@ -556,11 +577,12 @@ func finishCalcCreateGas( computeNonNewAccountCost := staticNonNewAccountCost / 2 growthNonNewAccountCost := staticNonNewAccountCost - computeNonNewAccountCost return GasesByDimension{ - Computation: callGasDimensionInfo.initCodeCost + callGasDimensionInfo.memoryExpansionCost + callGasDimensionInfo.hashCost + computeNonNewAccountCost, - StateAccess: 0, - StateGrowth: growthNonNewAccountCost + params.CallNewAccountGas + codeDepositCost, - HistoryGrowth: 0, - StateGrowthRefund: 0, + OneDimensionalGasCost: totalGasUsed, + Computation: callGasDimensionInfo.initCodeCost + callGasDimensionInfo.memoryExpansionCost + callGasDimensionInfo.hashCost + computeNonNewAccountCost, + StateAccess: 0, + StateGrowth: growthNonNewAccountCost + params.CallNewAccountGas + codeDepositCost, + HistoryGrowth: 0, + StateGrowthRefund: 0, } } @@ -571,7 +593,7 @@ func finishCalcCreateGas( // the relevant opcodes here are: // `CALL, CALLCODE` func calcReadAndStoreCallGas( - t *GasDimensionTracer, + t DimensionTracer, pc uint64, op byte, gas, cost uint64, @@ -624,11 +646,12 @@ func calcReadAndStoreCallGas( computation := memExpansionCost + params.WarmStorageReadCostEIP2929 // see finishCalcStateReadCallGas for more details return GasesByDimension{ - Computation: computation, - StateAccess: 0, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, + OneDimensionalGasCost: cost, + Computation: computation, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, }, &CallGasDimensionInfo{ op: vm.OpCode(op), gasCounterAtTimeOfCall: gas, @@ -672,34 +695,37 @@ func finishCalcStateReadAndStoreCallGas( coldCost = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 } return GasesByDimension{ - Computation: callGasDimensionInfo.memoryExpansionCost + params.WarmStorageReadCostEIP2929, - StateAccess: coldCost + positiveValueCostLessStipend, - StateGrowth: params.CallNewAccountGas, - HistoryGrowth: 0, - StateGrowthRefund: 0, + OneDimensionalGasCost: totalGasUsed, + Computation: callGasDimensionInfo.memoryExpansionCost + params.WarmStorageReadCostEIP2929, + StateAccess: coldCost + positiveValueCostLessStipend, + StateGrowth: params.CallNewAccountGas, + HistoryGrowth: 0, + StateGrowthRefund: 0, } } else if leftOver == params.ColdAccountAccessCostEIP2929 { var coldCost uint64 = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 return GasesByDimension{ - Computation: callGasDimensionInfo.memoryExpansionCost + params.WarmStorageReadCostEIP2929, - StateAccess: coldCost + positiveValueCostLessStipend, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, + OneDimensionalGasCost: totalGasUsed, + Computation: callGasDimensionInfo.memoryExpansionCost + params.WarmStorageReadCostEIP2929, + StateAccess: coldCost + positiveValueCostLessStipend, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, } } return GasesByDimension{ - Computation: callGasDimensionInfo.memoryExpansionCost + params.WarmStorageReadCostEIP2929, - StateAccess: positiveValueCostLessStipend, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, + OneDimensionalGasCost: totalGasUsed, + Computation: callGasDimensionInfo.memoryExpansionCost + params.WarmStorageReadCostEIP2929, + StateAccess: positiveValueCostLessStipend, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, } } // calcSStoreGas returns the gas used for the `SSTORE` opcode func calcSStoreGas( - t *GasDimensionTracer, + t DimensionTracer, pc uint64, op byte, gas, cost uint64, @@ -738,8 +764,8 @@ func calcSStoreGas( // refunds are tracked in the statedb // to find per-step changes, we track the accumulated refund // and compare it to the current refund - currentRefund := t.env.StateDB.GetRefund() - accumulatedRefund := t.refundAccumulated + currentRefund := t.GetOpRefund() + accumulatedRefund := t.GetRefundAccumulated() var diff int64 = 0 if accumulatedRefund != currentRefund { if accumulatedRefund < currentRefund { @@ -747,25 +773,27 @@ func calcSStoreGas( } else { diff = int64(accumulatedRefund - currentRefund) } - t.refundAccumulated = currentRefund + t.SetRefundAccumulated(currentRefund) } if cost >= params.SstoreSetGas { // 22100 case and 20000 case accessCost := cost - params.SstoreSetGas return GasesByDimension{ - Computation: params.WarmStorageReadCostEIP2929, - StateAccess: accessCost, - StateGrowth: params.SstoreSetGas - params.WarmStorageReadCostEIP2929, - HistoryGrowth: 0, - StateGrowthRefund: diff, + OneDimensionalGasCost: cost, + Computation: params.WarmStorageReadCostEIP2929, + StateAccess: accessCost, + StateGrowth: params.SstoreSetGas - params.WarmStorageReadCostEIP2929, + HistoryGrowth: 0, + StateGrowthRefund: diff, }, nil, nil } else if cost > 0 { return GasesByDimension{ - Computation: params.WarmStorageReadCostEIP2929, - StateAccess: cost - params.WarmStorageReadCostEIP2929, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: diff, + OneDimensionalGasCost: cost, + Computation: params.WarmStorageReadCostEIP2929, + StateAccess: cost - params.WarmStorageReadCostEIP2929, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: diff, }, nil, nil } // bizarre "system transactions" that can have costs of zero... @@ -775,7 +803,7 @@ func calcSStoreGas( // calcSelfDestructGas returns the gas used for the `SELFDESTRUCT` opcode // which deletes a contract which is a write to the state func calcSelfDestructGas( - t *GasDimensionTracer, + t DimensionTracer, pc uint64, op byte, gas, cost uint64, @@ -803,11 +831,12 @@ func calcSelfDestructGas( // 25000 for the selfdestruct (state growth) // 4900 for read/write (deleting the contract) return GasesByDimension{ - Computation: params.WarmStorageReadCostEIP2929, - StateAccess: params.SelfdestructGasEIP150 - params.WarmStorageReadCostEIP2929, - StateGrowth: params.CreateBySelfdestructGas, - HistoryGrowth: 0, - StateGrowthRefund: 0, + OneDimensionalGasCost: cost, + Computation: params.WarmStorageReadCostEIP2929, + StateAccess: params.SelfdestructGasEIP150 - params.WarmStorageReadCostEIP2929, + StateGrowth: params.CreateBySelfdestructGas, + HistoryGrowth: 0, + StateGrowthRefund: 0, }, nil, nil } else if cost == params.CreateBySelfdestructGas+params.SelfdestructGasEIP150+params.ColdAccountAccessCostEIP2929 { // cold and funds target empty @@ -816,11 +845,12 @@ func calcSelfDestructGas( // 25000 for the selfdestruct (state growth) // 2500 + 5000 for read/write (deleting the contract) return GasesByDimension{ - Computation: params.WarmStorageReadCostEIP2929, - StateAccess: params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 + params.SelfdestructGasEIP150, - StateGrowth: params.CreateBySelfdestructGas, - HistoryGrowth: 0, - StateGrowthRefund: 0, + OneDimensionalGasCost: cost, + Computation: params.WarmStorageReadCostEIP2929, + StateAccess: params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 + params.SelfdestructGasEIP150, + StateGrowth: params.CreateBySelfdestructGas, + HistoryGrowth: 0, + StateGrowthRefund: 0, }, nil, nil } else if cost == params.SelfdestructGasEIP150+params.ColdAccountAccessCostEIP2929 { // address lookup was cold but funds target has money already. Cost is 7600 @@ -828,22 +858,24 @@ func calcSelfDestructGas( // 2500 to access the address cold (access) // 5000 for the selfdestruct (access) return GasesByDimension{ - Computation: params.WarmStorageReadCostEIP2929, - StateAccess: params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 + params.SelfdestructGasEIP150, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, + OneDimensionalGasCost: cost, + Computation: params.WarmStorageReadCostEIP2929, + StateAccess: params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 + params.SelfdestructGasEIP150, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, }, nil, nil } // if you reach here, then the cost was 5000 // in which case give 100 for a warm access read // and 4900 for the state access (deleting the contract) return GasesByDimension{ - Computation: params.WarmStorageReadCostEIP2929, - StateAccess: params.SelfdestructGasEIP150 - params.WarmStorageReadCostEIP2929, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, + OneDimensionalGasCost: cost, + Computation: params.WarmStorageReadCostEIP2929, + StateAccess: params.SelfdestructGasEIP150 - params.WarmStorageReadCostEIP2929, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, }, nil, nil } diff --git a/eth/tracers/native/tx_gas_dimension_by_opcode.go b/eth/tracers/native/tx_gas_dimension_by_opcode.go new file mode 100644 index 0000000000..95ed81ddd0 --- /dev/null +++ b/eth/tracers/native/tx_gas_dimension_by_opcode.go @@ -0,0 +1,283 @@ +package native + +import ( + "encoding/json" + "fmt" + "math/big" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "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/ethereum/go-ethereum/params" +) + +// initializer for the tracer +func init() { + tracers.DefaultDirectory.Register("txGasDimensionByOpcode", NewTxGasDimensionByOpcodeLogger, false) +} + +// gasDimensionTracer struct +type TxGasDimensionByOpcodeTracer struct { + env *tracing.VMContext + txHash common.Hash + opcodeToDimensions map[vm.OpCode]GasesByDimension + err error + usedGas uint64 + callStack CallGasDimensionStack + depth int + refundAccumulated uint64 + + interrupt atomic.Bool // Atomic flag to signal execution interruption + reason error // Textual reason for the interruption +} + +// gasDimensionTracer returns a new tracer that traces gas +// usage for each opcode against the dimension of that opcode +// takes a context, and json input for configuration parameters +func NewTxGasDimensionByOpcodeLogger( + ctx *tracers.Context, + _ json.RawMessage, + _ *params.ChainConfig, +) (*tracers.Tracer, error) { + + t := &TxGasDimensionByOpcodeTracer{ + depth: 1, + refundAccumulated: 0, + opcodeToDimensions: make(map[vm.OpCode]GasesByDimension), + } + + return &tracers.Tracer{ + Hooks: &tracing.Hooks{ + OnOpcode: t.OnOpcode, + OnTxStart: t.OnTxStart, + OnTxEnd: t.OnTxEnd, + }, + GetResult: t.GetResult, + Stop: t.Stop, + }, nil +} + +// ############################################################################ +// HOOKS +// ############################################################################ + +// hook into each opcode execution +func (t *TxGasDimensionByOpcodeTracer) OnOpcode( + pc uint64, + op byte, + gas, cost uint64, + scope tracing.OpContext, + rData []byte, + depth int, + err error, +) { + if t.interrupt.Load() { + return + } + if depth != t.depth && depth != t.depth-1 { + t.interrupt.Store(true) + t.reason = fmt.Errorf( + "expected depth fell out of sync with actual depth: %d %d != %d, callStack: %v", + pc, + t.depth, + depth, + t.callStack, + ) + return + } + if t.depth != len(t.callStack)+1 { + t.interrupt.Store(true) + t.reason = fmt.Errorf( + "depth fell out of sync with callStack: %d %d != %d, callStack: %v", + pc, + t.depth, + len(t.callStack), + t.callStack, + ) + } + + // get the gas dimension function + // if it's not a call, directly calculate the gas dimensions for the opcode + f := getCalcGasDimensionFunc(vm.OpCode(op)) + gasesByDimension, callStackInfo, err := f(t, pc, op, gas, cost, scope, rData, depth, err) + if err != nil { + t.interrupt.Store(true) + t.reason = err + return + } + opcode := vm.OpCode(op) + + if wasCallOrCreate(opcode) && callStackInfo == nil || !wasCallOrCreate(opcode) && callStackInfo != nil { + t.interrupt.Store(true) + t.reason = fmt.Errorf( + "logic bug, calls/creates should always be accompanied by callStackInfo and non-calls should not have callStackInfo %d %s %v", + pc, + opcode.String(), + callStackInfo, + ) + return + } + + // if callStackInfo is not nil then we need to take a note of the index of the + // DimensionLog that represents this opcode and save the callStackInfo + // to call finishX after the call has returned + if wasCallOrCreate(opcode) { + t.callStack.Push( + CallGasDimensionStackInfo{ + gasDimensionInfo: *callStackInfo, + dimensionLogPosition: 0, //unused in this tracer + executionCost: 0, + }) + t.depth += 1 + } else { + + // update the aggregrate map for this opcode + accumulatedDimensions := t.opcodeToDimensions[opcode] + + accumulatedDimensions.OneDimensionalGasCost += gasesByDimension.OneDimensionalGasCost + accumulatedDimensions.Computation += gasesByDimension.Computation + accumulatedDimensions.StateAccess += gasesByDimension.StateAccess + accumulatedDimensions.StateGrowth += gasesByDimension.StateGrowth + accumulatedDimensions.HistoryGrowth += gasesByDimension.HistoryGrowth + accumulatedDimensions.StateGrowthRefund += gasesByDimension.StateGrowthRefund + + t.opcodeToDimensions[opcode] = accumulatedDimensions + + // if the opcode returns from the call stack depth, or + // if this is an opcode immediately after a call that did not increase the stack depth + // because it called an empty account or contract or wrong function signature, + // call the appropriate finishX function to write the gas dimensions + // for the call that increased the stack depth in the past + if depth < t.depth { + stackInfo, ok := t.callStack.Pop() + // base case, stack is empty, do nothing + if !ok { + t.interrupt.Store(true) + t.reason = fmt.Errorf("call stack is unexpectedly empty %d %d %d", pc, depth, t.depth) + return + } + finishFunction := getFinishCalcGasDimensionFunc(stackInfo.gasDimensionInfo.op) + if finishFunction == nil { + t.interrupt.Store(true) + t.reason = fmt.Errorf( + "no finish function found for opcode %s, call stack is messed up %d", + stackInfo.gasDimensionInfo.op.String(), + pc, + ) + return + } + // IMPORTANT NOTE: for some reason the only reliable way to actually get the gas cost of the call + // is to subtract gas at time of call from gas at opcode AFTER return + // you can't trust the `gas` field on the call itself. I wonder if the gas field is an estimation + gasUsedByCall := stackInfo.gasDimensionInfo.gasCounterAtTimeOfCall - gas + gasesByDimensionCall := finishFunction(gasUsedByCall, stackInfo.executionCost, stackInfo.gasDimensionInfo) + accumulatedDimensionsCall := t.opcodeToDimensions[stackInfo.gasDimensionInfo.op] + + accumulatedDimensionsCall.OneDimensionalGasCost += gasesByDimensionCall.OneDimensionalGasCost + accumulatedDimensionsCall.Computation += gasesByDimensionCall.Computation + accumulatedDimensionsCall.StateAccess += gasesByDimensionCall.StateAccess + accumulatedDimensionsCall.StateGrowth += gasesByDimensionCall.StateGrowth + accumulatedDimensionsCall.HistoryGrowth += gasesByDimensionCall.HistoryGrowth + accumulatedDimensionsCall.StateGrowthRefund += gasesByDimensionCall.StateGrowthRefund + + t.opcodeToDimensions[stackInfo.gasDimensionInfo.op] = accumulatedDimensionsCall + t.depth -= 1 + } + + // if we are in a call stack depth greater than 0, + // then we need to track the execution gas + // of our own code so that when the call returns, + // we can write the gas dimensions for the call opcode itself + if len(t.callStack) > 0 { + t.callStack.UpdateExecutionCost(cost) + } + } +} + +func (t *TxGasDimensionByOpcodeTracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) { + t.env = env +} + +func (t *TxGasDimensionByOpcodeTracer) OnTxEnd(receipt *types.Receipt, err error) { + if err != nil { + // Don't override vm error + if t.err == nil { + t.err = err + } + return + } + t.usedGas = receipt.GasUsed + t.txHash = receipt.TxHash +} + +// signal the tracer to stop tracing, e.g. on timeout +func (t *TxGasDimensionByOpcodeTracer) Stop(err error) { + t.reason = err + t.interrupt.Store(true) +} + +// ############################################################################ +// HELPERS +// ############################################################################ + +func (t *TxGasDimensionByOpcodeTracer) GetOpRefund() uint64 { + return t.env.StateDB.GetRefund() +} + +func (t *TxGasDimensionByOpcodeTracer) GetRefundAccumulated() uint64 { + return t.refundAccumulated +} + +func (t *TxGasDimensionByOpcodeTracer) SetRefundAccumulated(refund uint64) { + t.refundAccumulated = refund +} + +// ############################################################################ +// JSON OUTPUT PRODUCTION +// ############################################################################ + +// Error returns the VM error captured by the trace. +func (t *TxGasDimensionByOpcodeTracer) Error() error { return t.err } + +// ExecutionResult groups all dimension logs emitted by the EVM +// while replaying a transaction in debug mode as well as transaction +// execution status, the amount of gas used and the return value +type TxGasDimensionByOpcodeExecutionResult struct { + Gas uint64 `json:"gas"` + Failed bool `json:"failed"` + Dimensions map[string]GasesByDimension `json:"dimensions"` + TxHash string `json:"txHash"` + BlockTimetamp uint64 `json:"blockTimestamp"` + BlockNumber *big.Int `json:"blockNumber"` +} + +// produce json result for output from tracer +// this is what the end-user actually gets from the RPC endpoint +func (t *TxGasDimensionByOpcodeTracer) GetResult() (json.RawMessage, error) { + // Tracing aborted + if t.reason != nil { + return nil, t.reason + } + failed := t.err != nil + + return json.Marshal(&TxGasDimensionByOpcodeExecutionResult{ + Gas: t.usedGas, + Failed: failed, + Dimensions: t.GetOpcodeDimensionSummary(), + TxHash: t.txHash.Hex(), + BlockTimetamp: t.env.Time, + BlockNumber: t.env.BlockNumber, + }) +} + +// stringify opcodes for dimension log output +func (t *TxGasDimensionByOpcodeTracer) GetOpcodeDimensionSummary() map[string]GasesByDimension { + summary := make(map[string]GasesByDimension) + for opcode, dimensions := range t.opcodeToDimensions { + summary[opcode.String()] = dimensions + } + return summary +} diff --git a/eth/tracers/native/gas_dimension.go b/eth/tracers/native/tx_gas_dimension_logger.go similarity index 89% rename from eth/tracers/native/gas_dimension.go rename to eth/tracers/native/tx_gas_dimension_logger.go index c53fe60e1e..7a5d5df10d 100644 --- a/eth/tracers/native/gas_dimension.go +++ b/eth/tracers/native/tx_gas_dimension_logger.go @@ -16,7 +16,7 @@ import ( // initializer for the tracer func init() { - tracers.DefaultDirectory.Register("gasDimension", NewGasDimensionTracer, false) + tracers.DefaultDirectory.Register("txGasDimensionLogger", NewTxGasDimensionLogger, false) } // DimensionLog emitted to the EVM each cycle and lists information about each opcode @@ -48,7 +48,7 @@ func (d *DimensionLog) ErrorString() string { } // gasDimensionTracer struct -type GasDimensionTracer struct { +type TxGasDimensionLogger struct { env *tracing.VMContext txHash common.Hash logs []DimensionLog @@ -65,13 +65,13 @@ type GasDimensionTracer struct { // gasDimensionTracer returns a new tracer that traces gas // usage for each opcode against the dimension of that opcode // takes a context, and json input for configuration parameters -func NewGasDimensionTracer( +func NewTxGasDimensionLogger( ctx *tracers.Context, _ json.RawMessage, _ *params.ChainConfig, ) (*tracers.Tracer, error) { - t := &GasDimensionTracer{ + t := &TxGasDimensionLogger{ depth: 1, refundAccumulated: 0, } @@ -81,7 +81,6 @@ func NewGasDimensionTracer( OnOpcode: t.OnOpcode, OnTxStart: t.OnTxStart, OnTxEnd: t.OnTxEnd, - //OnGasChange: t.OnGasChange, }, GetResult: t.GetResult, Stop: t.Stop, @@ -93,7 +92,7 @@ func NewGasDimensionTracer( // ############################################################################ // hook into each opcode execution -func (t *GasDimensionTracer) OnOpcode( +func (t *TxGasDimensionLogger) OnOpcode( pc uint64, op byte, gas, cost uint64, @@ -153,7 +152,7 @@ func (t *GasDimensionTracer) OnOpcode( Pc: pc, Op: opcode, Depth: depth, - OneDimensionalGasCost: cost, + OneDimensionalGasCost: gasesByDimension.OneDimensionalGasCost, Computation: gasesByDimension.Computation, StateAccess: gasesByDimension.StateAccess, StateGrowth: gasesByDimension.StateGrowth, @@ -204,6 +203,7 @@ func (t *GasDimensionTracer) OnOpcode( gasUsedByCall := stackInfo.gasDimensionInfo.gasCounterAtTimeOfCall - gas gasesByDimension := finishFunction(gasUsedByCall, stackInfo.executionCost, stackInfo.gasDimensionInfo) callDimensionLog := t.logs[stackInfo.dimensionLogPosition] + callDimensionLog.OneDimensionalGasCost = gasesByDimension.OneDimensionalGasCost callDimensionLog.Computation = gasesByDimension.Computation callDimensionLog.StateAccess = gasesByDimension.StateAccess callDimensionLog.StateGrowth = gasesByDimension.StateGrowth @@ -228,11 +228,11 @@ func (t *GasDimensionTracer) OnOpcode( } } -func (t *GasDimensionTracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) { +func (t *TxGasDimensionLogger) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) { t.env = env } -func (t *GasDimensionTracer) OnTxEnd(receipt *types.Receipt, err error) { +func (t *TxGasDimensionLogger) OnTxEnd(receipt *types.Receipt, err error) { if err != nil { // Don't override vm error if t.err == nil { @@ -245,13 +245,13 @@ func (t *GasDimensionTracer) OnTxEnd(receipt *types.Receipt, err error) { } // signal the tracer to stop tracing, e.g. on timeout -func (t *GasDimensionTracer) Stop(err error) { +func (t *TxGasDimensionLogger) Stop(err error) { t.reason = err t.interrupt.Store(true) } // ############################################################################ -// JSON OUTPUT PRODUCTION +// HELPERS // ############################################################################ // wasCall returns true if the opcode is a type of opcode that makes calls increasing the stack depth @@ -259,11 +259,27 @@ func wasCallOrCreate(opcode vm.OpCode) bool { return opcode == vm.CALL || opcode == vm.CALLCODE || opcode == vm.DELEGATECALL || opcode == vm.STATICCALL || opcode == vm.CREATE || opcode == vm.CREATE2 } +func (t *TxGasDimensionLogger) GetOpRefund() uint64 { + return t.env.StateDB.GetRefund() +} + +func (t *TxGasDimensionLogger) GetRefundAccumulated() uint64 { + return t.refundAccumulated +} + +func (t *TxGasDimensionLogger) SetRefundAccumulated(refund uint64) { + t.refundAccumulated = refund +} + +// ############################################################################ +// JSON OUTPUT PRODUCTION +// ############################################################################ + // DimensionLogs returns the captured log entries. -func (t *GasDimensionTracer) DimensionLogs() []DimensionLog { return t.logs } +func (t *TxGasDimensionLogger) DimensionLogs() []DimensionLog { return t.logs } // Error returns the VM error captured by the trace. -func (t *GasDimensionTracer) Error() error { return t.err } +func (t *TxGasDimensionLogger) Error() error { return t.err } // ExecutionResult groups all dimension logs emitted by the EVM // while replaying a transaction in debug mode as well as transaction @@ -279,7 +295,7 @@ type ExecutionResult struct { // produce json result for output from tracer // this is what the end-user actually gets from the RPC endpoint -func (t *GasDimensionTracer) GetResult() (json.RawMessage, error) { +func (t *TxGasDimensionLogger) GetResult() (json.RawMessage, error) { // Tracing aborted if t.reason != nil { return nil, t.reason From 5c2a2f8533eaf5a859e86aca352c4cd05a7faf8f Mon Sep 17 00:00:00 2001 From: relyt29 Date: Thu, 10 Apr 2025 16:57:36 -0400 Subject: [PATCH 18/35] Gas dimensions by Block live tracer --- eth/tracers/live/block_gas_dimension.go | 147 -------- .../live/block_gas_dimension_by_opcode.go | 352 ++++++++++++++++++ eth/tracers/native/gas_dimension_calc.go | 88 ++--- .../native/tx_gas_dimension_by_opcode.go | 24 +- eth/tracers/native/tx_gas_dimension_logger.go | 36 +- 5 files changed, 426 insertions(+), 221 deletions(-) delete mode 100644 eth/tracers/live/block_gas_dimension.go create mode 100644 eth/tracers/live/block_gas_dimension_by_opcode.go diff --git a/eth/tracers/live/block_gas_dimension.go b/eth/tracers/live/block_gas_dimension.go deleted file mode 100644 index b39d576f53..0000000000 --- a/eth/tracers/live/block_gas_dimension.go +++ /dev/null @@ -1,147 +0,0 @@ -package live - -/* -import ( - "encoding/json" - "fmt" - "math/big" - "os" - "path/filepath" - "sync/atomic" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - - "github.com/ethereum/go-ethereum/core/tracing" - "github.com/ethereum/go-ethereum/eth/tracers" - "github.com/ethereum/go-ethereum/eth/tracers/native" -) - -// tracks the state of the tracer over the lifecycle of the tracer -type blockGasDimensionLiveTracer struct { - Path string `json:"path"` // path directory to write files out to - GasDimensionTracer *tracers.Tracer // gas dimension tracer. Changes every tx - blockNumber *big.Int // block number. changes every block - interrupt atomic.Bool // Atomic flag to signal execution interruption - txInterruptor common.Hash // track which tx broke the tracer - reason error // Textual reason for the interruption -} - -// initialize the tracer -func init() { - tracers.LiveDirectory.Register("blockGasDimension", newBlockGasDimensionLiveTracer) -} - -// config for the tracer -type blockGasDimensionLiveTracerConfig struct { - Path string `json:"path"` // Path to directory for output -} - -// create a new tracer -func newBlockGasDimensionLiveTracer(cfg json.RawMessage) (*tracing.Hooks, error) { - var config blockGasDimensionLiveTracerConfig - if err := json.Unmarshal(cfg, &config); err != nil { - return nil, err - } - - if config.Path == "" { - return nil, fmt.Errorf("gas dimension live tracer path for output is required: %v", config) - } - t := &blockGasDimensionLiveTracer{ - Path: config.Path, - GasDimensionTracer: nil, - blockNumber: nil, - } - - return &tracing.Hooks{ - OnOpcode: t.OnOpcode, - OnTxStart: t.OnTxStart, - OnTxEnd: t.OnTxEnd, - OnBlockStart: t.OnBlockStart, - OnBlockEnd: t.OnBlockEnd, - }, nil -} - -// create a gas dimension tracer for this TX -// then hook it to the txStart event -func (t *blockGasDimensionLiveTracer) OnTxStart(vm *tracing.VMContext, tx *types.Transaction, from common.Address) { - gasDimensionTracer, err := native.NewGasDimensionLogger(nil, nil, nil) - if err != nil { - t.reason = err - t.interrupt.Store(true) - return - } - if t.GasDimensionTracer != nil { - t.reason = fmt.Errorf("single-threaded execution order violation: gas dimension tracer already exists at time of txStart") - t.interrupt.Store(true) - return - } - t.GasDimensionTracer = gasDimensionTracer - t.GasDimensionTracer.OnTxStart(vm, tx, from) -} - -// hook the gasDimensionTracer to the opcode event -func (t *blockGasDimensionLiveTracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { - if t.interrupt.Load() { - return - } - if t.GasDimensionTracer == nil { - t.reason = fmt.Errorf("single-threaded execution order violation: gas dimension tracer not found when onOpcode fired") - t.interrupt.Store(true) - return - } - t.GasDimensionTracer.OnOpcode(pc, op, gas, cost, scope, rData, depth, err) -} - -// on TxEnd collate all the information from this transaction -// and store it for the block -func (t *blockGasDimensionLiveTracer) OnTxEnd(receipt *types.Receipt, err error) { - if t.interrupt.Load() { - return - } - if t.GasDimensionTracer == nil { - t.reason = fmt.Errorf("single-threaded execution order violation: gas dimension tracer not found when onTxEnd fired") - t.interrupt.Store(true) - return - } - t.GasDimensionTracer.OnTxEnd(receipt, err) - - // todo - // go through the gasDimensionTracer's results for this transaction. - - // free the gasDimensionTracer for the next tx - t.GasDimensionTracer = nil -} - -// on block start create a struct to store the gas dimensions for the block -func (t *blockGasDimensionLiveTracer) OnBlockStart(ev tracing.BlockEvent) { - fmt.Println("Live Tracer Seen: new block", ev.Block.Number()) -} - -// on block end write the block's data to a file -func (t *blockGasDimensionLiveTracer) OnBlockEnd(err error) { - fmt.Println("Live Tracer Seen block end") - blockNumber := t.blockNumber.String() - - // Create the filename - filename := fmt.Sprintf("%s.json", blockNumber) - filepath := filepath.Join(t.Path, filename) - - // Ensure the directory exists - if err := os.MkdirAll(t.Path, 0755); err != nil { - fmt.Printf("Failed to create directory %s: %v\n", t.Path, err) - return - } - - // todo - // create executionResultJsonBytes - - // Write the file - if err := os.WriteFile(filepath, executionResultJsonBytes, 0644); err != nil { - fmt.Printf("Failed to write file %s: %v\n", filepath, err) - return - } -} - - -*/ diff --git a/eth/tracers/live/block_gas_dimension_by_opcode.go b/eth/tracers/live/block_gas_dimension_by_opcode.go new file mode 100644 index 0000000000..e4cddc97b1 --- /dev/null +++ b/eth/tracers/live/block_gas_dimension_by_opcode.go @@ -0,0 +1,352 @@ +package live + +import ( + "encoding/json" + "fmt" + "math/big" + "os" + "path/filepath" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "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/ethereum/go-ethereum/eth/tracers/native" +) + +// initializer for the tracer +func init() { + tracers.LiveDirectory.Register("blockGasDimensionByOpcode", NewBlockGasDimensionByOpcodeLogger) +} + +// could just be paranoia but better safe than sorry +// avoids overflow by addition +type GasesByDimensionBigInt struct { + OneDimensionalGasCost *big.Int + Computation *big.Int + StateAccess *big.Int + StateGrowth *big.Int + HistoryGrowth *big.Int + StateGrowthRefund *big.Int +} + +// initializer for empty GasesByDimensionBigInt +func NewGasesByDimensionBigInt() GasesByDimensionBigInt { + return GasesByDimensionBigInt{ + OneDimensionalGasCost: big.NewInt(0), + Computation: big.NewInt(0), + StateAccess: big.NewInt(0), + StateGrowth: big.NewInt(0), + HistoryGrowth: big.NewInt(0), + StateGrowthRefund: big.NewInt(0), + } +} + +type blockGasDimensionByOpcodeLiveTraceConfig struct { + Path string `json:"path"` // Path to directory for output +} + +// gasDimensionTracer struct +type BlockGasDimensionByOpcodeLiveTracer struct { + Path string `json:"path"` // Path to directory for output + env *tracing.VMContext + blockTimestamp uint64 + blockNumber *big.Int + opcodeToDimensions map[vm.OpCode]GasesByDimensionBigInt + blockGas *big.Int + callStack native.CallGasDimensionStack + depth int + refundAccumulated uint64 + + // temp big int to avoid a bunch of allocations + tempBigInt *big.Int + + interrupt atomic.Bool // Atomic flag to signal execution interruption + reason error // Textual reason for the interruption +} + +// gasDimensionTracer returns a new tracer that traces gas +// usage for each opcode against the dimension of that opcode +// takes a context, and json input for configuration parameters +func NewBlockGasDimensionByOpcodeLogger( + cfg json.RawMessage, +) (*tracing.Hooks, error) { + var config blockGasDimensionByOpcodeLiveTraceConfig + if err := json.Unmarshal(cfg, &config); err != nil { + return nil, err + } + + if config.Path == "" { + return nil, fmt.Errorf("block gas dimension live tracer path for output is required: %v", config) + } + + t := &BlockGasDimensionByOpcodeLiveTracer{ + Path: config.Path, + depth: 1, + refundAccumulated: 0, + blockGas: big.NewInt(0), + blockNumber: big.NewInt(-1), + tempBigInt: big.NewInt(0), + blockTimestamp: 0, + opcodeToDimensions: make(map[vm.OpCode]GasesByDimensionBigInt), + } + + return &tracing.Hooks{ + OnOpcode: t.OnOpcode, + OnTxStart: t.OnTxStart, + OnTxEnd: t.OnTxEnd, + OnBlockStart: t.OnBlockStart, + OnBlockEnd: t.OnBlockEnd, + }, nil +} + +// ############################################################################ +// HOOKS +// ############################################################################ + +// hook into each opcode execution +func (t *BlockGasDimensionByOpcodeLiveTracer) OnOpcode( + pc uint64, + op byte, + gas, cost uint64, + scope tracing.OpContext, + rData []byte, + depth int, + err error, +) { + if t.interrupt.Load() { + return + } + if depth != t.depth && depth != t.depth-1 { + t.interrupt.Store(true) + t.reason = fmt.Errorf( + "expected depth fell out of sync with actual depth: %d %d != %d, callStack: %v", + pc, + t.depth, + depth, + t.callStack, + ) + return + } + if t.depth != len(t.callStack)+1 { + t.interrupt.Store(true) + t.reason = fmt.Errorf( + "depth fell out of sync with callStack: %d %d != %d, callStack: %v", + pc, + t.depth, + len(t.callStack), + t.callStack, + ) + return + } + + // get the gas dimension function + // if it's not a call, directly calculate the gas dimensions for the opcode + f := native.GetCalcGasDimensionFunc(vm.OpCode(op)) + gasesByDimension, callStackInfo, err := f(t, pc, op, gas, cost, scope, rData, depth, err) + if err != nil { + t.interrupt.Store(true) + t.reason = err + return + } + opcode := vm.OpCode(op) + + if native.WasCallOrCreate(opcode) && callStackInfo == nil || !native.WasCallOrCreate(opcode) && callStackInfo != nil { + t.interrupt.Store(true) + t.reason = fmt.Errorf( + "logic bug, calls/creates should always be accompanied by callStackInfo and non-calls should not have callStackInfo %d %s %v", + pc, + opcode.String(), + callStackInfo, + ) + return + } + + // if callStackInfo is not nil then we need to take a note of the index of the + // DimensionLog that represents this opcode and save the callStackInfo + // to call finishX after the call has returned + if native.WasCallOrCreate(opcode) { + t.callStack.Push( + native.CallGasDimensionStackInfo{ + GasDimensionInfo: *callStackInfo, + DimensionLogPosition: 0, //unused in this tracer + ExecutionCost: 0, + }) + t.depth += 1 + } else { + + // update the aggregrate map for this opcode + accumulatedDimensions, exists := t.opcodeToDimensions[opcode] + if !exists { + accumulatedDimensions = NewGasesByDimensionBigInt() + } + + // add the gas dimensions for this opcode to the accumulated dimensions + t.addGasesByDimension(&accumulatedDimensions, gasesByDimension) + + t.opcodeToDimensions[opcode] = accumulatedDimensions + + // if the opcode returns from the call stack depth, or + // if this is an opcode immediately after a call that did not increase the stack depth + // because it called an empty account or contract or wrong function signature, + // call the appropriate finishX function to write the gas dimensions + // for the call that increased the stack depth in the past + if depth < t.depth { + stackInfo, ok := t.callStack.Pop() + // base case, stack is empty, do nothing + if !ok { + t.interrupt.Store(true) + t.reason = fmt.Errorf("call stack is unexpectedly empty %d %d %d", pc, depth, t.depth) + return + } + finishFunction := native.GetFinishCalcGasDimensionFunc(stackInfo.GasDimensionInfo.Op) + if finishFunction == nil { + t.interrupt.Store(true) + t.reason = fmt.Errorf( + "no finish function found for opcode %s, call stack is messed up %d", + stackInfo.GasDimensionInfo.Op.String(), + pc, + ) + return + } + // IMPORTANT NOTE: for some reason the only reliable way to actually get the gas cost of the call + // is to subtract gas at time of call from gas at opcode AFTER return + // you can't trust the `gas` field on the call itself. I wonder if the gas field is an estimation + gasUsedByCall := stackInfo.GasDimensionInfo.GasCounterAtTimeOfCall - gas + gasesByDimensionCall := finishFunction(gasUsedByCall, stackInfo.ExecutionCost, stackInfo.GasDimensionInfo) + accumulatedDimensionsCall, exists := t.opcodeToDimensions[stackInfo.GasDimensionInfo.Op] + if !exists { + accumulatedDimensionsCall = NewGasesByDimensionBigInt() + } + + t.addGasesByDimension(&accumulatedDimensionsCall, gasesByDimensionCall) + t.opcodeToDimensions[stackInfo.GasDimensionInfo.Op] = accumulatedDimensionsCall + t.depth -= 1 + } + + // if we are in a call stack depth greater than 0, + // then we need to track the execution gas + // of our own code so that when the call returns, + // we can write the gas dimensions for the call opcode itself + if len(t.callStack) > 0 { + t.callStack.UpdateExecutionCost(cost) + } + } +} + +// on tx start, get the environment and set the depth to 1 +func (t *BlockGasDimensionByOpcodeLiveTracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) { + t.env = env + t.depth = 1 + t.refundAccumulated = 0 // refunds per tx +} + +// on tx end, add the gas used to the block gas +func (t *BlockGasDimensionByOpcodeLiveTracer) OnTxEnd(receipt *types.Receipt, err error) { + t.blockGas.Add(t.blockGas, new(big.Int).SetUint64(receipt.GasUsed)) +} + +// on block start take note of the block timestamp and number +func (t *BlockGasDimensionByOpcodeLiveTracer) OnBlockStart(ev tracing.BlockEvent) { + t.blockTimestamp = ev.Block.Time() + t.blockNumber = ev.Block.Number() +} + +// on block end, write out the gas dimensions for each opcode in the block to file +func (t *BlockGasDimensionByOpcodeLiveTracer) OnBlockEnd(err error) { + resultJsonBytes, errGettingResult := t.GetResult() + if errGettingResult != nil { + errorJsonString := fmt.Sprintf("{\"errorGettingResult\": \"%s\"}", errGettingResult.Error()) + fmt.Println(errorJsonString) + resultJsonBytes = []byte(errorJsonString) + return + } + + filename := fmt.Sprintf("%s.json", t.blockNumber.String()) + filepath := filepath.Join(t.Path, filename) + + // Ensure the directory exists + if err := os.MkdirAll(t.Path, 0755); err != nil { + fmt.Printf("Failed to create directory %s: %v\n", t.Path, err) + return + } + + // Write the file + if err := os.WriteFile(filepath, resultJsonBytes, 0644); err != nil { + fmt.Printf("Failed to write file %s: %v\n", filepath, err) + return + } + +} + +// ############################################################################ +// HELPERS +// ############################################################################ + +func (t *BlockGasDimensionByOpcodeLiveTracer) GetOpRefund() uint64 { + return t.env.StateDB.GetRefund() +} + +func (t *BlockGasDimensionByOpcodeLiveTracer) GetRefundAccumulated() uint64 { + return t.refundAccumulated +} + +func (t *BlockGasDimensionByOpcodeLiveTracer) SetRefundAccumulated(refund uint64) { + t.refundAccumulated = refund +} + +// avoid allocating a lot of big ints in a loop +func (t *BlockGasDimensionByOpcodeLiveTracer) addGasesByDimension(target *GasesByDimensionBigInt, value native.GasesByDimension) { + t.tempBigInt.SetUint64(value.OneDimensionalGasCost) + target.OneDimensionalGasCost.Add(target.OneDimensionalGasCost, t.tempBigInt) + t.tempBigInt.SetUint64(value.Computation) + target.Computation.Add(target.Computation, t.tempBigInt) + t.tempBigInt.SetUint64(value.StateAccess) + target.StateAccess.Add(target.StateAccess, t.tempBigInt) + t.tempBigInt.SetUint64(value.StateGrowth) + target.StateGrowth.Add(target.StateGrowth, t.tempBigInt) + t.tempBigInt.SetUint64(value.HistoryGrowth) + target.HistoryGrowth.Add(target.HistoryGrowth, t.tempBigInt) + t.tempBigInt.SetInt64(value.StateGrowthRefund) + target.StateGrowthRefund.Add(target.StateGrowthRefund, t.tempBigInt) +} + +// ############################################################################ +// JSON OUTPUT PRODUCTION +// ############################################################################ + +// ExecutionResult groups all dimension logs emitted by the EVM +// while replaying a transaction in debug mode as well as transaction +// execution status, the amount of gas used and the return value +type BlockGasDimensionByOpcodeExecutionResult struct { + Gas *big.Int `json:"gas"` + BlockTimetamp uint64 `json:"timestamp"` + BlockNumber *big.Int `json:"blockNumber"` + Dimensions map[string]GasesByDimensionBigInt `json:"dimensions"` +} + +// produce json result for output from tracer +// this is what the end-user actually gets from the RPC endpoint +func (t *BlockGasDimensionByOpcodeLiveTracer) GetResult() (json.RawMessage, error) { + // Tracing aborted + if t.reason != nil { + return nil, t.reason + } + return json.Marshal(&BlockGasDimensionByOpcodeExecutionResult{ + Gas: t.blockGas, + Dimensions: t.GetOpcodeDimensionSummary(), + BlockTimetamp: t.blockTimestamp, + BlockNumber: t.blockNumber, + }) +} + +// stringify opcodes for dimension log output +func (t *BlockGasDimensionByOpcodeLiveTracer) GetOpcodeDimensionSummary() map[string]GasesByDimensionBigInt { + summary := make(map[string]GasesByDimensionBigInt) + for opcode, dimensions := range t.opcodeToDimensions { + summary[opcode.String()] = dimensions + } + return summary +} diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index 4975a383ca..649d9ff3e0 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -25,21 +25,21 @@ type GasesByDimension struct { // CallGasDimensionInfo retains the relevant information that needs to be remembered // from the start of the call to compute the gas dimensions after the call has returned. type CallGasDimensionInfo struct { - op vm.OpCode - gasCounterAtTimeOfCall uint64 - memoryExpansionCost uint64 - isValueSentWithCall bool - initCodeCost uint64 - hashCost uint64 + Op vm.OpCode + GasCounterAtTimeOfCall uint64 + MemoryExpansionCost uint64 + IsValueSentWithCall bool + InitCodeCost uint64 + HashCost uint64 } // CallGasDimensionStackInfo is a struct that contains the gas dimension info // and the position of the dimension log in the dimension logs array // so that the finish functions can directly write into the dimension logs type CallGasDimensionStackInfo struct { - gasDimensionInfo CallGasDimensionInfo - dimensionLogPosition int - executionCost uint64 + GasDimensionInfo CallGasDimensionInfo + DimensionLogPosition int + ExecutionCost uint64 } // CallGasDimensionStack is a stack of CallGasDimensionStackInfo @@ -69,7 +69,7 @@ func (c *CallGasDimensionStack) UpdateExecutionCost(executionCost uint64) { return } top := (*c)[len(*c)-1] - top.executionCost += executionCost + top.ExecutionCost += executionCost (*c)[len(*c)-1] = top } @@ -88,7 +88,7 @@ type DimensionTracer interface { // // INVARIANT (for non-call opcodes): the sum of the gas consumption for each dimension // equals the input `gas` to this function -type calcGasDimensionFunc func( +type CalcGasDimensionFunc func( t DimensionTracer, pc uint64, op byte, @@ -99,14 +99,14 @@ type calcGasDimensionFunc func( err error, ) (GasesByDimension, *CallGasDimensionInfo, error) -// finishCalcGasDimensionFunc defines a type signature that takes the +// FinishCalcGasDimensionFunc defines a type signature that takes the // code execution cost of the call and the callGasDimensionInfo // and returns the gas dimensions for the call opcode itself // THIS EXPLICITLY BREAKS THE ABOVE INVARIANT FOR THE NON-CALL OPCODES // as call opcodes only contain the dimensions for the call itself, // and the dimensions of their children are computed as their children are // seen/traced. -type finishCalcGasDimensionFunc func( +type FinishCalcGasDimensionFunc func( totalGasUsed uint64, codeExecutionCost uint64, callGasDimensionInfo CallGasDimensionInfo, @@ -115,7 +115,7 @@ type finishCalcGasDimensionFunc func( // getCalcGasDimensionFunc is a massive case switch // statement that returns which function to call // based on which opcode is being traced/executed -func getCalcGasDimensionFunc(op vm.OpCode) calcGasDimensionFunc { +func GetCalcGasDimensionFunc(op vm.OpCode) CalcGasDimensionFunc { switch op { // Opcodes that Only Operate on Storage Read/Write (storage access in the short run) // `BALANCE, EXTCODESIZE, EXTCODEHASH,` @@ -160,7 +160,7 @@ func getCalcGasDimensionFunc(op vm.OpCode) calcGasDimensionFunc { // to know the code_execution_cost of the call // and then use that to compute the gas dimensions // for the call opcode itself. -func getFinishCalcGasDimensionFunc(op vm.OpCode) finishCalcGasDimensionFunc { +func GetFinishCalcGasDimensionFunc(op vm.OpCode) FinishCalcGasDimensionFunc { switch op { case vm.DELEGATECALL, vm.STATICCALL: return finishCalcStateReadCallGas @@ -405,12 +405,12 @@ func calcStateReadCallGas( HistoryGrowth: 0, StateGrowthRefund: 0, }, &CallGasDimensionInfo{ - op: vm.OpCode(op), - gasCounterAtTimeOfCall: gas, - memoryExpansionCost: memExpansionCost, - isValueSentWithCall: false, - initCodeCost: 0, - hashCost: 0, + Op: vm.OpCode(op), + GasCounterAtTimeOfCall: gas, + MemoryExpansionCost: memExpansionCost, + IsValueSentWithCall: false, + InitCodeCost: 0, + HashCost: 0, }, nil } @@ -426,11 +426,11 @@ func finishCalcStateReadCallGas( codeExecutionCost uint64, callGasDimensionInfo CallGasDimensionInfo, ) GasesByDimension { - leftOver := totalGasUsed - callGasDimensionInfo.memoryExpansionCost - codeExecutionCost + leftOver := totalGasUsed - callGasDimensionInfo.MemoryExpansionCost - codeExecutionCost if leftOver == params.ColdAccountAccessCostEIP2929 { return GasesByDimension{ OneDimensionalGasCost: totalGasUsed, - Computation: params.WarmStorageReadCostEIP2929 + callGasDimensionInfo.memoryExpansionCost, + Computation: params.WarmStorageReadCostEIP2929 + callGasDimensionInfo.MemoryExpansionCost, StateAccess: params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929, StateGrowth: 0, HistoryGrowth: 0, @@ -439,7 +439,7 @@ func finishCalcStateReadCallGas( } return GasesByDimension{ OneDimensionalGasCost: totalGasUsed, - Computation: leftOver + callGasDimensionInfo.memoryExpansionCost, + Computation: leftOver + callGasDimensionInfo.MemoryExpansionCost, StateAccess: 0, StateGrowth: 0, HistoryGrowth: 0, @@ -550,12 +550,12 @@ func calcCreateGas( HistoryGrowth: 0, StateGrowthRefund: 0, }, &CallGasDimensionInfo{ - op: vm.OpCode(op), - gasCounterAtTimeOfCall: gas, - memoryExpansionCost: memExpansionCost, - isValueSentWithCall: false, - initCodeCost: initCodeCost, - hashCost: hashCost, + Op: vm.OpCode(op), + GasCounterAtTimeOfCall: gas, + MemoryExpansionCost: memExpansionCost, + IsValueSentWithCall: false, + InitCodeCost: initCodeCost, + HashCost: hashCost, }, nil } @@ -567,8 +567,8 @@ func finishCalcCreateGas( callGasDimensionInfo CallGasDimensionInfo, ) GasesByDimension { // totalGasUsed = init_code_cost + memory_expansion_cost + deployment_code_execution_cost + code_deposit_cost - codeDepositCost := totalGasUsed - params.CreateGas - callGasDimensionInfo.initCodeCost - - callGasDimensionInfo.memoryExpansionCost - callGasDimensionInfo.hashCost - codeExecutionCost + codeDepositCost := totalGasUsed - params.CreateGas - callGasDimensionInfo.InitCodeCost - + callGasDimensionInfo.MemoryExpansionCost - callGasDimensionInfo.HashCost - codeExecutionCost // CALL costs 25000 for write to an empty account, // so of the 32000 static cost of CREATE and CREATE2 give 25000 to storage growth, // and then cut the last 7000 in half for compute and state growth to @@ -578,7 +578,7 @@ func finishCalcCreateGas( growthNonNewAccountCost := staticNonNewAccountCost - computeNonNewAccountCost return GasesByDimension{ OneDimensionalGasCost: totalGasUsed, - Computation: callGasDimensionInfo.initCodeCost + callGasDimensionInfo.memoryExpansionCost + callGasDimensionInfo.hashCost + computeNonNewAccountCost, + Computation: callGasDimensionInfo.InitCodeCost + callGasDimensionInfo.MemoryExpansionCost + callGasDimensionInfo.HashCost + computeNonNewAccountCost, StateAccess: 0, StateGrowth: growthNonNewAccountCost + params.CallNewAccountGas + codeDepositCost, HistoryGrowth: 0, @@ -653,12 +653,12 @@ func calcReadAndStoreCallGas( HistoryGrowth: 0, StateGrowthRefund: 0, }, &CallGasDimensionInfo{ - op: vm.OpCode(op), - gasCounterAtTimeOfCall: gas, - memoryExpansionCost: memExpansionCost, - isValueSentWithCall: valueSentWithCall > 0, - initCodeCost: 0, - hashCost: 0, + Op: vm.OpCode(op), + GasCounterAtTimeOfCall: gas, + MemoryExpansionCost: memExpansionCost, + IsValueSentWithCall: valueSentWithCall > 0, + InitCodeCost: 0, + HashCost: 0, }, nil } @@ -677,13 +677,13 @@ func finishCalcStateReadAndStoreCallGas( ) GasesByDimension { // the stipend is 2300 and it is not charged to the call itself but used in the execution cost var positiveValueCostLessStipend uint64 = 0 - if callGasDimensionInfo.isValueSentWithCall { + if callGasDimensionInfo.IsValueSentWithCall { positiveValueCostLessStipend = params.CallValueTransferGas - params.CallStipend } // the formula for call is: // dynamic_gas = memory_expansion_cost + code_execution_cost + address_access_cost + positive_value_cost + value_to_empty_account_cost // now with leftOver, we are left with address_access_cost + value_to_empty_account_cost - leftOver := totalGasUsed - callGasDimensionInfo.memoryExpansionCost - codeExecutionCost - positiveValueCostLessStipend + leftOver := totalGasUsed - callGasDimensionInfo.MemoryExpansionCost - codeExecutionCost - positiveValueCostLessStipend // the maximum address_access_cost can ever be is 2600. Meanwhile value_to_empty_account_cost is at minimum 25000 // so if leftOver is greater than 2600 then we know that the value_to_empty_account_cost was 25000 // and whatever was left over after that was address_access_cost @@ -696,7 +696,7 @@ func finishCalcStateReadAndStoreCallGas( } return GasesByDimension{ OneDimensionalGasCost: totalGasUsed, - Computation: callGasDimensionInfo.memoryExpansionCost + params.WarmStorageReadCostEIP2929, + Computation: callGasDimensionInfo.MemoryExpansionCost + params.WarmStorageReadCostEIP2929, StateAccess: coldCost + positiveValueCostLessStipend, StateGrowth: params.CallNewAccountGas, HistoryGrowth: 0, @@ -706,7 +706,7 @@ func finishCalcStateReadAndStoreCallGas( var coldCost uint64 = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 return GasesByDimension{ OneDimensionalGasCost: totalGasUsed, - Computation: callGasDimensionInfo.memoryExpansionCost + params.WarmStorageReadCostEIP2929, + Computation: callGasDimensionInfo.MemoryExpansionCost + params.WarmStorageReadCostEIP2929, StateAccess: coldCost + positiveValueCostLessStipend, StateGrowth: 0, HistoryGrowth: 0, @@ -715,7 +715,7 @@ func finishCalcStateReadAndStoreCallGas( } return GasesByDimension{ OneDimensionalGasCost: totalGasUsed, - Computation: callGasDimensionInfo.memoryExpansionCost + params.WarmStorageReadCostEIP2929, + Computation: callGasDimensionInfo.MemoryExpansionCost + params.WarmStorageReadCostEIP2929, StateAccess: positiveValueCostLessStipend, StateGrowth: 0, HistoryGrowth: 0, diff --git a/eth/tracers/native/tx_gas_dimension_by_opcode.go b/eth/tracers/native/tx_gas_dimension_by_opcode.go index 95ed81ddd0..a6485bbf75 100644 --- a/eth/tracers/native/tx_gas_dimension_by_opcode.go +++ b/eth/tracers/native/tx_gas_dimension_by_opcode.go @@ -101,7 +101,7 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( // get the gas dimension function // if it's not a call, directly calculate the gas dimensions for the opcode - f := getCalcGasDimensionFunc(vm.OpCode(op)) + f := GetCalcGasDimensionFunc(vm.OpCode(op)) gasesByDimension, callStackInfo, err := f(t, pc, op, gas, cost, scope, rData, depth, err) if err != nil { t.interrupt.Store(true) @@ -110,7 +110,7 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( } opcode := vm.OpCode(op) - if wasCallOrCreate(opcode) && callStackInfo == nil || !wasCallOrCreate(opcode) && callStackInfo != nil { + if WasCallOrCreate(opcode) && callStackInfo == nil || !WasCallOrCreate(opcode) && callStackInfo != nil { t.interrupt.Store(true) t.reason = fmt.Errorf( "logic bug, calls/creates should always be accompanied by callStackInfo and non-calls should not have callStackInfo %d %s %v", @@ -124,12 +124,12 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( // if callStackInfo is not nil then we need to take a note of the index of the // DimensionLog that represents this opcode and save the callStackInfo // to call finishX after the call has returned - if wasCallOrCreate(opcode) { + if WasCallOrCreate(opcode) { t.callStack.Push( CallGasDimensionStackInfo{ - gasDimensionInfo: *callStackInfo, - dimensionLogPosition: 0, //unused in this tracer - executionCost: 0, + GasDimensionInfo: *callStackInfo, + DimensionLogPosition: 0, //unused in this tracer + ExecutionCost: 0, }) t.depth += 1 } else { @@ -159,12 +159,12 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( t.reason = fmt.Errorf("call stack is unexpectedly empty %d %d %d", pc, depth, t.depth) return } - finishFunction := getFinishCalcGasDimensionFunc(stackInfo.gasDimensionInfo.op) + finishFunction := GetFinishCalcGasDimensionFunc(stackInfo.GasDimensionInfo.Op) if finishFunction == nil { t.interrupt.Store(true) t.reason = fmt.Errorf( "no finish function found for opcode %s, call stack is messed up %d", - stackInfo.gasDimensionInfo.op.String(), + stackInfo.GasDimensionInfo.Op.String(), pc, ) return @@ -172,9 +172,9 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( // IMPORTANT NOTE: for some reason the only reliable way to actually get the gas cost of the call // is to subtract gas at time of call from gas at opcode AFTER return // you can't trust the `gas` field on the call itself. I wonder if the gas field is an estimation - gasUsedByCall := stackInfo.gasDimensionInfo.gasCounterAtTimeOfCall - gas - gasesByDimensionCall := finishFunction(gasUsedByCall, stackInfo.executionCost, stackInfo.gasDimensionInfo) - accumulatedDimensionsCall := t.opcodeToDimensions[stackInfo.gasDimensionInfo.op] + gasUsedByCall := stackInfo.GasDimensionInfo.GasCounterAtTimeOfCall - gas + gasesByDimensionCall := finishFunction(gasUsedByCall, stackInfo.ExecutionCost, stackInfo.GasDimensionInfo) + accumulatedDimensionsCall := t.opcodeToDimensions[stackInfo.GasDimensionInfo.Op] accumulatedDimensionsCall.OneDimensionalGasCost += gasesByDimensionCall.OneDimensionalGasCost accumulatedDimensionsCall.Computation += gasesByDimensionCall.Computation @@ -183,7 +183,7 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( accumulatedDimensionsCall.HistoryGrowth += gasesByDimensionCall.HistoryGrowth accumulatedDimensionsCall.StateGrowthRefund += gasesByDimensionCall.StateGrowthRefund - t.opcodeToDimensions[stackInfo.gasDimensionInfo.op] = accumulatedDimensionsCall + t.opcodeToDimensions[stackInfo.GasDimensionInfo.Op] = accumulatedDimensionsCall t.depth -= 1 } diff --git a/eth/tracers/native/tx_gas_dimension_logger.go b/eth/tracers/native/tx_gas_dimension_logger.go index 7a5d5df10d..755a903cc4 100644 --- a/eth/tracers/native/tx_gas_dimension_logger.go +++ b/eth/tracers/native/tx_gas_dimension_logger.go @@ -128,7 +128,7 @@ func (t *TxGasDimensionLogger) OnOpcode( // get the gas dimension function // if it's not a call, directly calculate the gas dimensions for the opcode - f := getCalcGasDimensionFunc(vm.OpCode(op)) + f := GetCalcGasDimensionFunc(vm.OpCode(op)) gasesByDimension, callStackInfo, err := f(t, pc, op, gas, cost, scope, rData, depth, err) if err != nil { t.interrupt.Store(true) @@ -137,7 +137,7 @@ func (t *TxGasDimensionLogger) OnOpcode( } opcode := vm.OpCode(op) - if wasCallOrCreate(opcode) && callStackInfo == nil || !wasCallOrCreate(opcode) && callStackInfo != nil { + if WasCallOrCreate(opcode) && callStackInfo == nil || !WasCallOrCreate(opcode) && callStackInfo != nil { t.interrupt.Store(true) t.reason = fmt.Errorf( "logic bug, calls/creates should always be accompanied by callStackInfo and non-calls should not have callStackInfo %d %s %v", @@ -164,13 +164,13 @@ func (t *TxGasDimensionLogger) OnOpcode( // if callStackInfo is not nil then we need to take a note of the index of the // DimensionLog that represents this opcode and save the callStackInfo // to call finishX after the call has returned - if wasCallOrCreate(opcode) { + if WasCallOrCreate(opcode) { opcodeLogIndex := len(t.logs) - 1 // minus 1 because we've already appended the log t.callStack.Push( CallGasDimensionStackInfo{ - gasDimensionInfo: *callStackInfo, - dimensionLogPosition: opcodeLogIndex, - executionCost: 0, + GasDimensionInfo: *callStackInfo, + DimensionLogPosition: opcodeLogIndex, + ExecutionCost: 0, }) t.depth += 1 } else { @@ -187,12 +187,12 @@ func (t *TxGasDimensionLogger) OnOpcode( t.reason = fmt.Errorf("call stack is unexpectedly empty %d %d %d", pc, depth, t.depth) return } - finishFunction := getFinishCalcGasDimensionFunc(stackInfo.gasDimensionInfo.op) + finishFunction := GetFinishCalcGasDimensionFunc(stackInfo.GasDimensionInfo.Op) if finishFunction == nil { t.interrupt.Store(true) t.reason = fmt.Errorf( "no finish function found for opcode %s, call stack is messed up %d", - stackInfo.gasDimensionInfo.op.String(), + stackInfo.GasDimensionInfo.Op.String(), pc, ) return @@ -200,9 +200,9 @@ func (t *TxGasDimensionLogger) OnOpcode( // IMPORTANT NOTE: for some reason the only reliable way to actually get the gas cost of the call // is to subtract gas at time of call from gas at opcode AFTER return // you can't trust the `gas` field on the call itself. I wonder if the gas field is an estimation - gasUsedByCall := stackInfo.gasDimensionInfo.gasCounterAtTimeOfCall - gas - gasesByDimension := finishFunction(gasUsedByCall, stackInfo.executionCost, stackInfo.gasDimensionInfo) - callDimensionLog := t.logs[stackInfo.dimensionLogPosition] + gasUsedByCall := stackInfo.GasDimensionInfo.GasCounterAtTimeOfCall - gas + gasesByDimension := finishFunction(gasUsedByCall, stackInfo.ExecutionCost, stackInfo.GasDimensionInfo) + callDimensionLog := t.logs[stackInfo.DimensionLogPosition] callDimensionLog.OneDimensionalGasCost = gasesByDimension.OneDimensionalGasCost callDimensionLog.Computation = gasesByDimension.Computation callDimensionLog.StateAccess = gasesByDimension.StateAccess @@ -210,11 +210,11 @@ func (t *TxGasDimensionLogger) OnOpcode( callDimensionLog.HistoryGrowth = gasesByDimension.HistoryGrowth callDimensionLog.StateGrowthRefund = gasesByDimension.StateGrowthRefund callDimensionLog.CallRealGas = gasUsedByCall - callDimensionLog.CallExecutionCost = stackInfo.executionCost - callDimensionLog.CallMemoryExpansion = stackInfo.gasDimensionInfo.memoryExpansionCost - callDimensionLog.CreateInitCodeCost = stackInfo.gasDimensionInfo.initCodeCost - callDimensionLog.Create2HashCost = stackInfo.gasDimensionInfo.hashCost - t.logs[stackInfo.dimensionLogPosition] = callDimensionLog + callDimensionLog.CallExecutionCost = stackInfo.ExecutionCost + callDimensionLog.CallMemoryExpansion = stackInfo.GasDimensionInfo.MemoryExpansionCost + callDimensionLog.CreateInitCodeCost = stackInfo.GasDimensionInfo.InitCodeCost + callDimensionLog.Create2HashCost = stackInfo.GasDimensionInfo.HashCost + t.logs[stackInfo.DimensionLogPosition] = callDimensionLog t.depth -= 1 } @@ -254,8 +254,8 @@ func (t *TxGasDimensionLogger) Stop(err error) { // HELPERS // ############################################################################ -// wasCall returns true if the opcode is a type of opcode that makes calls increasing the stack depth -func wasCallOrCreate(opcode vm.OpCode) bool { +// returns true if the opcode is a type of opcode that makes calls increasing the stack depth +func WasCallOrCreate(opcode vm.OpCode) bool { return opcode == vm.CALL || opcode == vm.CALLCODE || opcode == vm.DELEGATECALL || opcode == vm.STATICCALL || opcode == vm.CREATE || opcode == vm.CREATE2 } From 3d2a3abf80343f6cf39d63529deccf3634fb53b9 Mon Sep 17 00:00:00 2001 From: relyt29 Date: Sat, 12 Apr 2025 14:55:48 -0400 Subject: [PATCH 19/35] Add live tracer: gas dimension by opcode for tx --- .../live/tx_gas_dimension_by_opcode.go | 139 ++++++++++++++++++ eth/tracers/live/tx_gas_dimension_logger.go | 23 ++- eth/tracers/native/gas_dimension_calc.go | 10 +- .../native/tx_gas_dimension_by_opcode.go | 12 +- eth/tracers/native/tx_gas_dimension_logger.go | 36 ++--- 5 files changed, 186 insertions(+), 34 deletions(-) create mode 100644 eth/tracers/live/tx_gas_dimension_by_opcode.go diff --git a/eth/tracers/live/tx_gas_dimension_by_opcode.go b/eth/tracers/live/tx_gas_dimension_by_opcode.go new file mode 100644 index 0000000000..edbee424bb --- /dev/null +++ b/eth/tracers/live/tx_gas_dimension_by_opcode.go @@ -0,0 +1,139 @@ +package live + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/eth/tracers" + "github.com/ethereum/go-ethereum/eth/tracers/native" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// initializer for the tracer +func init() { + tracers.LiveDirectory.Register("txGasDimensionByOpcode", NewTxGasDimensionByOpcodeLogger) +} + +type txGasDimensionByOpcodeLiveTraceConfig struct { + Path string `json:"path"` // Path to directory for output +} + +// gasDimensionTracer struct +type TxGasDimensionByOpcodeLiveTracer struct { + Path string `json:"path"` // Path to directory for output + GasDimensionTracer *tracers.Tracer +} + +// gasDimensionTracer returns a new tracer that traces gas +// usage for each opcode against the dimension of that opcode +// takes a context, and json input for configuration parameters +func NewTxGasDimensionByOpcodeLogger( + cfg json.RawMessage, +) (*tracing.Hooks, error) { + var config txGasDimensionByOpcodeLiveTraceConfig + if err := json.Unmarshal(cfg, &config); err != nil { + return nil, err + } + + if config.Path == "" { + return nil, fmt.Errorf("tx gas dimension live tracer path for output is required: %v", config) + } + + t := &TxGasDimensionByOpcodeLiveTracer{ + Path: config.Path, + GasDimensionTracer: nil, + } + + return &tracing.Hooks{ + OnOpcode: t.OnOpcode, + OnTxStart: t.OnTxStart, + OnTxEnd: t.OnTxEnd, + OnBlockStart: t.OnBlockStart, + OnBlockEnd: t.OnBlockEnd, + }, nil +} + +func (t *TxGasDimensionByOpcodeLiveTracer) OnTxStart( + vm *tracing.VMContext, + tx *types.Transaction, + from common.Address, +) { + if t.GasDimensionTracer != nil { + fmt.Println("Error seen in the gas dimension live tracer lifecycle!") + } + + var err error + t.GasDimensionTracer, err = native.NewTxGasDimensionByOpcodeLogger(nil, nil, nil) + if err != nil { + fmt.Printf("Failed to create tx gas dimension tracer: %v\n", err) + t.GasDimensionTracer = nil + return + } + t.GasDimensionTracer.OnTxStart(vm, tx, from) +} + +func (t *TxGasDimensionByOpcodeLiveTracer) OnOpcode( + pc uint64, + op byte, + gas, cost uint64, + scope tracing.OpContext, + rData []byte, + depth int, + err error, +) { + t.GasDimensionTracer.OnOpcode(pc, op, gas, cost, scope, rData, depth, err) +} + +func (t *TxGasDimensionByOpcodeLiveTracer) OnTxEnd( + receipt *types.Receipt, + err error, +) { + // first call the native tracer's OnTxEnd + t.GasDimensionTracer.OnTxEnd(receipt, err) + + // system transactions don't use any gas + // they can be skipped + if receipt.GasUsed != 0 { + + // then get the json from the native tracer + executionResultJsonBytes, errGettingResult := t.GasDimensionTracer.GetResult() + if errGettingResult != nil { + errorJsonString := fmt.Sprintf("{\"errorGettingResult\": \"%s\"}", errGettingResult.Error()) + fmt.Println(errorJsonString) + return + } + + blockNumber := receipt.BlockNumber.String() + txHashString := receipt.TxHash.Hex() + + // Create the filename + filename := fmt.Sprintf("%s_%s.json", blockNumber, txHashString) + filepath := filepath.Join(t.Path, filename) + + // Ensure the directory exists + if err := os.MkdirAll(t.Path, 0755); err != nil { + fmt.Printf("Failed to create directory %s: %v\n", t.Path, err) + return + } + + // Write the file + if err := os.WriteFile(filepath, executionResultJsonBytes, 0644); err != nil { + fmt.Printf("Failed to write file %s: %v\n", filepath, err) + return + } + } + + // reset the tracer + t.GasDimensionTracer = nil +} + +func (t *TxGasDimensionByOpcodeLiveTracer) OnBlockStart(ev tracing.BlockEvent) { +} + +func (t *TxGasDimensionByOpcodeLiveTracer) OnBlockEnd(err error) { +} diff --git a/eth/tracers/live/tx_gas_dimension_logger.go b/eth/tracers/live/tx_gas_dimension_logger.go index 8ea7024458..9fbb016574 100644 --- a/eth/tracers/live/tx_gas_dimension_logger.go +++ b/eth/tracers/live/tx_gas_dimension_logger.go @@ -56,15 +56,30 @@ func newTxGasDimensionLiveTraceLogger(cfg json.RawMessage) (*tracing.Hooks, erro }, nil } -func (t *txGasDimensionLiveTraceLogger) OnTxStart(vm *tracing.VMContext, tx *types.Transaction, from common.Address) { +func (t *txGasDimensionLiveTraceLogger) OnTxStart( + vm *tracing.VMContext, + tx *types.Transaction, + from common.Address, +) { t.GasDimensionTracer.OnTxStart(vm, tx, from) } -func (t *txGasDimensionLiveTraceLogger) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { +func (t *txGasDimensionLiveTraceLogger) OnOpcode( + pc uint64, + op byte, + gas, cost uint64, + scope tracing.OpContext, + rData []byte, + depth int, + err error, +) { t.GasDimensionTracer.OnOpcode(pc, op, gas, cost, scope, rData, depth, err) } +func (t *txGasDimensionLiveTraceLogger) OnTxEnd( + receipt *types.Receipt, + err error, +) { -func (t *txGasDimensionLiveTraceLogger) OnTxEnd(receipt *types.Receipt, err error) { // first call the native tracer's OnTxEnd t.GasDimensionTracer.OnTxEnd(receipt, err) @@ -97,9 +112,7 @@ func (t *txGasDimensionLiveTraceLogger) OnTxEnd(receipt *types.Receipt, err erro } func (t *txGasDimensionLiveTraceLogger) OnBlockStart(ev tracing.BlockEvent) { - fmt.Println("Live Tracer Seen: new block", ev.Block.Number()) } func (t *txGasDimensionLiveTraceLogger) OnBlockEnd(err error) { - fmt.Println("Live Tracer Seen block end") } diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index 649d9ff3e0..20fb494548 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -11,12 +11,12 @@ import ( // GasesByDimension is a type that represents the gas consumption for each dimension // for a given opcode. type GasesByDimension struct { - OneDimensionalGasCost uint64 `json:"total"` + OneDimensionalGasCost uint64 `json:"g1"` Computation uint64 `json:"cpu"` - StateAccess uint64 `json:"access,omitempty"` - StateGrowth uint64 `json:"growth,omitempty"` - HistoryGrowth uint64 `json:"hist,omitempty"` - StateGrowthRefund int64 `json:"refund,omitempty"` + StateAccess uint64 `json:"rw,omitempty"` + StateGrowth uint64 `json:"gr,omitempty"` + HistoryGrowth uint64 `json:"h,omitempty"` + StateGrowthRefund int64 `json:"rf,omitempty"` } // in the case of opcodes like CALL, STATICCALL, DELEGATECALL, etc, diff --git a/eth/tracers/native/tx_gas_dimension_by_opcode.go b/eth/tracers/native/tx_gas_dimension_by_opcode.go index a6485bbf75..69ee8b6efd 100644 --- a/eth/tracers/native/tx_gas_dimension_by_opcode.go +++ b/eth/tracers/native/tx_gas_dimension_by_opcode.go @@ -38,7 +38,7 @@ type TxGasDimensionByOpcodeTracer struct { // usage for each opcode against the dimension of that opcode // takes a context, and json input for configuration parameters func NewTxGasDimensionByOpcodeLogger( - ctx *tracers.Context, + _ *tracers.Context, _ json.RawMessage, _ *params.ChainConfig, ) (*tracers.Tracer, error) { @@ -247,11 +247,11 @@ func (t *TxGasDimensionByOpcodeTracer) Error() error { return t.err } // execution status, the amount of gas used and the return value type TxGasDimensionByOpcodeExecutionResult struct { Gas uint64 `json:"gas"` - Failed bool `json:"failed"` - Dimensions map[string]GasesByDimension `json:"dimensions"` - TxHash string `json:"txHash"` - BlockTimetamp uint64 `json:"blockTimestamp"` - BlockNumber *big.Int `json:"blockNumber"` + Failed bool `json:"fail"` + Dimensions map[string]GasesByDimension `json:"dim"` + TxHash string `json:"hash"` + BlockTimetamp uint64 `json:"btime"` + BlockNumber *big.Int `json:"num"` } // produce json result for output from tracer diff --git a/eth/tracers/native/tx_gas_dimension_logger.go b/eth/tracers/native/tx_gas_dimension_logger.go index 755a903cc4..bba1dfe161 100644 --- a/eth/tracers/native/tx_gas_dimension_logger.go +++ b/eth/tracers/native/tx_gas_dimension_logger.go @@ -286,11 +286,11 @@ func (t *TxGasDimensionLogger) Error() error { return t.err } // execution status, the amount of gas used and the return value type ExecutionResult struct { Gas uint64 `json:"gas"` - Failed bool `json:"failed"` - DimensionLogs []DimensionLogRes `json:"dimensionLogs"` - TxHash string `json:"txHash"` - BlockTimetamp uint64 `json:"blockTimestamp"` - BlockNumber *big.Int `json:"blockNumber"` + Failed bool `json:"fail"` + DimensionLogs []DimensionLogRes `json:"dim"` + TxHash string `json:"hash"` + BlockTimetamp uint64 `json:"time"` + BlockNumber *big.Int `json:"num"` } // produce json result for output from tracer @@ -318,19 +318,19 @@ func (t *TxGasDimensionLogger) GetResult() (json.RawMessage, error) { type DimensionLogRes struct { Pc uint64 `json:"pc"` Op string `json:"op"` - Depth int `json:"depth"` - OneDimensionalGasCost uint64 `json:"gasCost"` - Computation uint64 `json:"cpu"` - StateAccess uint64 `json:"access,omitempty"` - StateGrowth uint64 `json:"growth,omitempty"` - HistoryGrowth uint64 `json:"hist,omitempty"` - StateGrowthRefund int64 `json:"refund,omitempty"` - CallRealGas uint64 `json:"callRealGas,omitempty"` - CallExecutionCost uint64 `json:"callExecutionCost,omitempty"` - CallMemoryExpansion uint64 `json:"callMemoryExpansion,omitempty"` - CreateInitCodeCost uint64 `json:"createInitCodeCost,omitempty"` - Create2HashCost uint64 `json:"create2HashCost,omitempty"` - Err error `json:"error,omitempty"` + Depth int `json:"d"` + OneDimensionalGasCost uint64 `json:"cost"` + Computation uint64 `json:"cpu,omitempty"` + StateAccess uint64 `json:"rw,omitempty"` + StateGrowth uint64 `json:"g,omitempty"` + HistoryGrowth uint64 `json:"h,omitempty"` + StateGrowthRefund int64 `json:"rf,omitempty"` + CallRealGas uint64 `json:"crg,omitempty"` + CallExecutionCost uint64 `json:"cec,omitempty"` + CallMemoryExpansion uint64 `json:"cme,omitempty"` + CreateInitCodeCost uint64 `json:"cic,omitempty"` + Create2HashCost uint64 `json:"c2h,omitempty"` + Err error `json:"err,omitempty"` } // formatLogs formats EVM returned structured logs for json output From 57ca6601e83e940e1ac0d9b6a40daebe376fcf12 Mon Sep 17 00:00:00 2001 From: relyt29 Date: Sat, 12 Apr 2025 16:20:38 -0400 Subject: [PATCH 20/35] save tx data to protobuf serialized format --- .../live/tx_gas_dimension_by_opcode.go | 44 +-- .../proto/gas_dimension_by_opcode.pb.go | 269 ++++++++++++++++++ .../proto/gas_dimension_by_opcode.proto | 25 ++ .../native/tx_gas_dimension_by_opcode.go | 76 +++-- 4 files changed, 375 insertions(+), 39 deletions(-) create mode 100644 eth/tracers/native/proto/gas_dimension_by_opcode.pb.go create mode 100644 eth/tracers/native/proto/gas_dimension_by_opcode.proto diff --git a/eth/tracers/live/tx_gas_dimension_by_opcode.go b/eth/tracers/live/tx_gas_dimension_by_opcode.go index edbee424bb..8d413f4d2a 100644 --- a/eth/tracers/live/tx_gas_dimension_by_opcode.go +++ b/eth/tracers/live/tx_gas_dimension_by_opcode.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + _vm "github.com/ethereum/go-ethereum/core/vm" ) // initializer for the tracer @@ -26,7 +27,7 @@ type txGasDimensionByOpcodeLiveTraceConfig struct { // gasDimensionTracer struct type TxGasDimensionByOpcodeLiveTracer struct { Path string `json:"path"` // Path to directory for output - GasDimensionTracer *tracers.Tracer + GasDimensionTracer *native.TxGasDimensionByOpcodeTracer } // gasDimensionTracer returns a new tracer that traces gas @@ -67,12 +68,10 @@ func (t *TxGasDimensionByOpcodeLiveTracer) OnTxStart( fmt.Println("Error seen in the gas dimension live tracer lifecycle!") } - var err error - t.GasDimensionTracer, err = native.NewTxGasDimensionByOpcodeLogger(nil, nil, nil) - if err != nil { - fmt.Printf("Failed to create tx gas dimension tracer: %v\n", err) - t.GasDimensionTracer = nil - return + t.GasDimensionTracer = &native.TxGasDimensionByOpcodeTracer{ + Depth: 1, + RefundAccumulated: 0, + OpcodeToDimensions: make(map[_vm.OpCode]native.GasesByDimension), } t.GasDimensionTracer.OnTxStart(vm, tx, from) } @@ -100,29 +99,38 @@ func (t *TxGasDimensionByOpcodeLiveTracer) OnTxEnd( // they can be skipped if receipt.GasUsed != 0 { + // previously: json // then get the json from the native tracer - executionResultJsonBytes, errGettingResult := t.GasDimensionTracer.GetResult() - if errGettingResult != nil { - errorJsonString := fmt.Sprintf("{\"errorGettingResult\": \"%s\"}", errGettingResult.Error()) - fmt.Println(errorJsonString) + // executionResultJsonBytes, errGettingResult := t.GasDimensionTracer.GetResult() + // if errGettingResult != nil { + // errorJsonString := fmt.Sprintf("{\"errorGettingResult\": \"%s\"}", errGettingResult.Error()) + // fmt.Println(errorJsonString) + // return + // } + + // now: protobuf + executionResultBytes, err := t.GasDimensionTracer.GetProtobufResult() + if err != nil { + fmt.Printf("Failed to get protobuf result: %v\n", err) return } blockNumber := receipt.BlockNumber.String() txHashString := receipt.TxHash.Hex() - // Create the filename - filename := fmt.Sprintf("%s_%s.json", blockNumber, txHashString) - filepath := filepath.Join(t.Path, filename) + // Create the file path + filename := fmt.Sprintf("%s.pb", txHashString) + dirPath := filepath.Join(t.Path, blockNumber) + filepath := filepath.Join(dirPath, filename) - // Ensure the directory exists - if err := os.MkdirAll(t.Path, 0755); err != nil { - fmt.Printf("Failed to create directory %s: %v\n", t.Path, err) + // Ensure the directory exists (including block number subdirectory) + if err := os.MkdirAll(dirPath, 0755); err != nil { + fmt.Printf("Failed to create directory %s: %v\n", dirPath, err) return } // Write the file - if err := os.WriteFile(filepath, executionResultJsonBytes, 0644); err != nil { + if err := os.WriteFile(filepath, executionResultBytes, 0644); err != nil { fmt.Printf("Failed to write file %s: %v\n", filepath, err) return } diff --git a/eth/tracers/native/proto/gas_dimension_by_opcode.pb.go b/eth/tracers/native/proto/gas_dimension_by_opcode.pb.go new file mode 100644 index 0000000000..9d7d1bf99a --- /dev/null +++ b/eth/tracers/native/proto/gas_dimension_by_opcode.pb.go @@ -0,0 +1,269 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc v5.29.3 +// source: gas_dimension_by_opcode.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// GasesByDimension represents the gas consumption for each dimension +type GasesByDimension struct { + state protoimpl.MessageState `protogen:"open.v1"` + OneDimensionalGasCost uint64 `protobuf:"varint,1,opt,name=one_dimensional_gas_cost,json=oneDimensionalGasCost,proto3" json:"one_dimensional_gas_cost,omitempty"` + Computation uint64 `protobuf:"varint,2,opt,name=computation,proto3" json:"computation,omitempty"` + StateAccess uint64 `protobuf:"varint,3,opt,name=state_access,json=stateAccess,proto3" json:"state_access,omitempty"` + StateGrowth uint64 `protobuf:"varint,4,opt,name=state_growth,json=stateGrowth,proto3" json:"state_growth,omitempty"` + HistoryGrowth uint64 `protobuf:"varint,5,opt,name=history_growth,json=historyGrowth,proto3" json:"history_growth,omitempty"` + StateGrowthRefund int64 `protobuf:"varint,6,opt,name=state_growth_refund,json=stateGrowthRefund,proto3" json:"state_growth_refund,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GasesByDimension) Reset() { + *x = GasesByDimension{} + mi := &file_gas_dimension_by_opcode_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GasesByDimension) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GasesByDimension) ProtoMessage() {} + +func (x *GasesByDimension) ProtoReflect() protoreflect.Message { + mi := &file_gas_dimension_by_opcode_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GasesByDimension.ProtoReflect.Descriptor instead. +func (*GasesByDimension) Descriptor() ([]byte, []int) { + return file_gas_dimension_by_opcode_proto_rawDescGZIP(), []int{0} +} + +func (x *GasesByDimension) GetOneDimensionalGasCost() uint64 { + if x != nil { + return x.OneDimensionalGasCost + } + return 0 +} + +func (x *GasesByDimension) GetComputation() uint64 { + if x != nil { + return x.Computation + } + return 0 +} + +func (x *GasesByDimension) GetStateAccess() uint64 { + if x != nil { + return x.StateAccess + } + return 0 +} + +func (x *GasesByDimension) GetStateGrowth() uint64 { + if x != nil { + return x.StateGrowth + } + return 0 +} + +func (x *GasesByDimension) GetHistoryGrowth() uint64 { + if x != nil { + return x.HistoryGrowth + } + return 0 +} + +func (x *GasesByDimension) GetStateGrowthRefund() int64 { + if x != nil { + return x.StateGrowthRefund + } + return 0 +} + +// TxGasDimensionByOpcodeExecutionResult represents the execution result +type TxGasDimensionByOpcodeExecutionResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + Gas uint64 `protobuf:"varint,1,opt,name=gas,proto3" json:"gas,omitempty"` + Failed bool `protobuf:"varint,2,opt,name=failed,proto3" json:"failed,omitempty"` + Dimensions map[string]*GasesByDimension `protobuf:"bytes,3,rep,name=dimensions,proto3" json:"dimensions,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + TxHash string `protobuf:"bytes,4,opt,name=tx_hash,json=txHash,proto3" json:"tx_hash,omitempty"` + BlockTimestamp uint64 `protobuf:"varint,5,opt,name=block_timestamp,json=blockTimestamp,proto3" json:"block_timestamp,omitempty"` + BlockNumber string `protobuf:"bytes,6,opt,name=block_number,json=blockNumber,proto3" json:"block_number,omitempty"` // Using string to represent big.Int + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TxGasDimensionByOpcodeExecutionResult) Reset() { + *x = TxGasDimensionByOpcodeExecutionResult{} + mi := &file_gas_dimension_by_opcode_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TxGasDimensionByOpcodeExecutionResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TxGasDimensionByOpcodeExecutionResult) ProtoMessage() {} + +func (x *TxGasDimensionByOpcodeExecutionResult) ProtoReflect() protoreflect.Message { + mi := &file_gas_dimension_by_opcode_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TxGasDimensionByOpcodeExecutionResult.ProtoReflect.Descriptor instead. +func (*TxGasDimensionByOpcodeExecutionResult) Descriptor() ([]byte, []int) { + return file_gas_dimension_by_opcode_proto_rawDescGZIP(), []int{1} +} + +func (x *TxGasDimensionByOpcodeExecutionResult) GetGas() uint64 { + if x != nil { + return x.Gas + } + return 0 +} + +func (x *TxGasDimensionByOpcodeExecutionResult) GetFailed() bool { + if x != nil { + return x.Failed + } + return false +} + +func (x *TxGasDimensionByOpcodeExecutionResult) GetDimensions() map[string]*GasesByDimension { + if x != nil { + return x.Dimensions + } + return nil +} + +func (x *TxGasDimensionByOpcodeExecutionResult) GetTxHash() string { + if x != nil { + return x.TxHash + } + return "" +} + +func (x *TxGasDimensionByOpcodeExecutionResult) GetBlockTimestamp() uint64 { + if x != nil { + return x.BlockTimestamp + } + return 0 +} + +func (x *TxGasDimensionByOpcodeExecutionResult) GetBlockNumber() string { + if x != nil { + return x.BlockNumber + } + return "" +} + +var File_gas_dimension_by_opcode_proto protoreflect.FileDescriptor + +const file_gas_dimension_by_opcode_proto_rawDesc = "" + + "\n" + + "\x1dgas_dimension_by_opcode.proto\x12\x18eth.tracers.native.proto\"\x8a\x02\n" + + "\x10GasesByDimension\x127\n" + + "\x18one_dimensional_gas_cost\x18\x01 \x01(\x04R\x15oneDimensionalGasCost\x12 \n" + + "\vcomputation\x18\x02 \x01(\x04R\vcomputation\x12!\n" + + "\fstate_access\x18\x03 \x01(\x04R\vstateAccess\x12!\n" + + "\fstate_growth\x18\x04 \x01(\x04R\vstateGrowth\x12%\n" + + "\x0ehistory_growth\x18\x05 \x01(\x04R\rhistoryGrowth\x12.\n" + + "\x13state_growth_refund\x18\x06 \x01(\x03R\x11stateGrowthRefund\"\x92\x03\n" + + "%TxGasDimensionByOpcodeExecutionResult\x12\x10\n" + + "\x03gas\x18\x01 \x01(\x04R\x03gas\x12\x16\n" + + "\x06failed\x18\x02 \x01(\bR\x06failed\x12o\n" + + "\n" + + "dimensions\x18\x03 \x03(\v2O.eth.tracers.native.proto.TxGasDimensionByOpcodeExecutionResult.DimensionsEntryR\n" + + "dimensions\x12\x17\n" + + "\atx_hash\x18\x04 \x01(\tR\x06txHash\x12'\n" + + "\x0fblock_timestamp\x18\x05 \x01(\x04R\x0eblockTimestamp\x12!\n" + + "\fblock_number\x18\x06 \x01(\tR\vblockNumber\x1ai\n" + + "\x0fDimensionsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12@\n" + + "\x05value\x18\x02 \x01(\v2*.eth.tracers.native.proto.GasesByDimensionR\x05value:\x028\x01B:Z8github.com/ethereum/go-ethereum/eth/tracers/native/protob\x06proto3" + +var ( + file_gas_dimension_by_opcode_proto_rawDescOnce sync.Once + file_gas_dimension_by_opcode_proto_rawDescData []byte +) + +func file_gas_dimension_by_opcode_proto_rawDescGZIP() []byte { + file_gas_dimension_by_opcode_proto_rawDescOnce.Do(func() { + file_gas_dimension_by_opcode_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_gas_dimension_by_opcode_proto_rawDesc), len(file_gas_dimension_by_opcode_proto_rawDesc))) + }) + return file_gas_dimension_by_opcode_proto_rawDescData +} + +var file_gas_dimension_by_opcode_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_gas_dimension_by_opcode_proto_goTypes = []any{ + (*GasesByDimension)(nil), // 0: eth.tracers.native.proto.GasesByDimension + (*TxGasDimensionByOpcodeExecutionResult)(nil), // 1: eth.tracers.native.proto.TxGasDimensionByOpcodeExecutionResult + nil, // 2: eth.tracers.native.proto.TxGasDimensionByOpcodeExecutionResult.DimensionsEntry +} +var file_gas_dimension_by_opcode_proto_depIdxs = []int32{ + 2, // 0: eth.tracers.native.proto.TxGasDimensionByOpcodeExecutionResult.dimensions:type_name -> eth.tracers.native.proto.TxGasDimensionByOpcodeExecutionResult.DimensionsEntry + 0, // 1: eth.tracers.native.proto.TxGasDimensionByOpcodeExecutionResult.DimensionsEntry.value:type_name -> eth.tracers.native.proto.GasesByDimension + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_gas_dimension_by_opcode_proto_init() } +func file_gas_dimension_by_opcode_proto_init() { + if File_gas_dimension_by_opcode_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_gas_dimension_by_opcode_proto_rawDesc), len(file_gas_dimension_by_opcode_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_gas_dimension_by_opcode_proto_goTypes, + DependencyIndexes: file_gas_dimension_by_opcode_proto_depIdxs, + MessageInfos: file_gas_dimension_by_opcode_proto_msgTypes, + }.Build() + File_gas_dimension_by_opcode_proto = out.File + file_gas_dimension_by_opcode_proto_goTypes = nil + file_gas_dimension_by_opcode_proto_depIdxs = nil +} diff --git a/eth/tracers/native/proto/gas_dimension_by_opcode.proto b/eth/tracers/native/proto/gas_dimension_by_opcode.proto new file mode 100644 index 0000000000..d5252af83c --- /dev/null +++ b/eth/tracers/native/proto/gas_dimension_by_opcode.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package eth.tracers.native.proto; + +option go_package = "github.com/ethereum/go-ethereum/eth/tracers/native/proto"; + +// GasesByDimension represents the gas consumption for each dimension +message GasesByDimension { + uint64 one_dimensional_gas_cost = 1; + uint64 computation = 2; + uint64 state_access = 3; + uint64 state_growth = 4; + uint64 history_growth = 5; + int64 state_growth_refund = 6; +} + +// TxGasDimensionByOpcodeExecutionResult represents the execution result +message TxGasDimensionByOpcodeExecutionResult { + uint64 gas = 1; + bool failed = 2; + map dimensions = 3; + string tx_hash = 4; + uint64 block_timestamp = 5; + string block_number = 6; // Using string to represent big.Int +} \ No newline at end of file diff --git a/eth/tracers/native/tx_gas_dimension_by_opcode.go b/eth/tracers/native/tx_gas_dimension_by_opcode.go index 69ee8b6efd..1c3444dda1 100644 --- a/eth/tracers/native/tx_gas_dimension_by_opcode.go +++ b/eth/tracers/native/tx_gas_dimension_by_opcode.go @@ -6,12 +6,15 @@ import ( "math/big" "sync/atomic" + "github.com/ethereum/go-ethereum/eth/tracers/native/proto" + "github.com/ethereum/go-ethereum/common" "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/ethereum/go-ethereum/params" + protobuf "google.golang.org/protobuf/proto" ) // initializer for the tracer @@ -23,12 +26,12 @@ func init() { type TxGasDimensionByOpcodeTracer struct { env *tracing.VMContext txHash common.Hash - opcodeToDimensions map[vm.OpCode]GasesByDimension + OpcodeToDimensions map[vm.OpCode]GasesByDimension err error usedGas uint64 callStack CallGasDimensionStack - depth int - refundAccumulated uint64 + Depth int + RefundAccumulated uint64 interrupt atomic.Bool // Atomic flag to signal execution interruption reason error // Textual reason for the interruption @@ -44,9 +47,9 @@ func NewTxGasDimensionByOpcodeLogger( ) (*tracers.Tracer, error) { t := &TxGasDimensionByOpcodeTracer{ - depth: 1, - refundAccumulated: 0, - opcodeToDimensions: make(map[vm.OpCode]GasesByDimension), + Depth: 1, + RefundAccumulated: 0, + OpcodeToDimensions: make(map[vm.OpCode]GasesByDimension), } return &tracers.Tracer{ @@ -77,23 +80,23 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( if t.interrupt.Load() { return } - if depth != t.depth && depth != t.depth-1 { + if depth != t.Depth && depth != t.Depth-1 { t.interrupt.Store(true) t.reason = fmt.Errorf( "expected depth fell out of sync with actual depth: %d %d != %d, callStack: %v", pc, - t.depth, + t.Depth, depth, t.callStack, ) return } - if t.depth != len(t.callStack)+1 { + if t.Depth != len(t.callStack)+1 { t.interrupt.Store(true) t.reason = fmt.Errorf( "depth fell out of sync with callStack: %d %d != %d, callStack: %v", pc, - t.depth, + t.Depth, len(t.callStack), t.callStack, ) @@ -131,11 +134,11 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( DimensionLogPosition: 0, //unused in this tracer ExecutionCost: 0, }) - t.depth += 1 + t.Depth += 1 } else { // update the aggregrate map for this opcode - accumulatedDimensions := t.opcodeToDimensions[opcode] + accumulatedDimensions := t.OpcodeToDimensions[opcode] accumulatedDimensions.OneDimensionalGasCost += gasesByDimension.OneDimensionalGasCost accumulatedDimensions.Computation += gasesByDimension.Computation @@ -144,19 +147,19 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( accumulatedDimensions.HistoryGrowth += gasesByDimension.HistoryGrowth accumulatedDimensions.StateGrowthRefund += gasesByDimension.StateGrowthRefund - t.opcodeToDimensions[opcode] = accumulatedDimensions + t.OpcodeToDimensions[opcode] = accumulatedDimensions // if the opcode returns from the call stack depth, or // if this is an opcode immediately after a call that did not increase the stack depth // because it called an empty account or contract or wrong function signature, // call the appropriate finishX function to write the gas dimensions // for the call that increased the stack depth in the past - if depth < t.depth { + if depth < t.Depth { stackInfo, ok := t.callStack.Pop() // base case, stack is empty, do nothing if !ok { t.interrupt.Store(true) - t.reason = fmt.Errorf("call stack is unexpectedly empty %d %d %d", pc, depth, t.depth) + t.reason = fmt.Errorf("call stack is unexpectedly empty %d %d %d", pc, depth, t.Depth) return } finishFunction := GetFinishCalcGasDimensionFunc(stackInfo.GasDimensionInfo.Op) @@ -174,7 +177,7 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( // you can't trust the `gas` field on the call itself. I wonder if the gas field is an estimation gasUsedByCall := stackInfo.GasDimensionInfo.GasCounterAtTimeOfCall - gas gasesByDimensionCall := finishFunction(gasUsedByCall, stackInfo.ExecutionCost, stackInfo.GasDimensionInfo) - accumulatedDimensionsCall := t.opcodeToDimensions[stackInfo.GasDimensionInfo.Op] + accumulatedDimensionsCall := t.OpcodeToDimensions[stackInfo.GasDimensionInfo.Op] accumulatedDimensionsCall.OneDimensionalGasCost += gasesByDimensionCall.OneDimensionalGasCost accumulatedDimensionsCall.Computation += gasesByDimensionCall.Computation @@ -183,8 +186,8 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( accumulatedDimensionsCall.HistoryGrowth += gasesByDimensionCall.HistoryGrowth accumulatedDimensionsCall.StateGrowthRefund += gasesByDimensionCall.StateGrowthRefund - t.opcodeToDimensions[stackInfo.GasDimensionInfo.Op] = accumulatedDimensionsCall - t.depth -= 1 + t.OpcodeToDimensions[stackInfo.GasDimensionInfo.Op] = accumulatedDimensionsCall + t.Depth -= 1 } // if we are in a call stack depth greater than 0, @@ -228,11 +231,11 @@ func (t *TxGasDimensionByOpcodeTracer) GetOpRefund() uint64 { } func (t *TxGasDimensionByOpcodeTracer) GetRefundAccumulated() uint64 { - return t.refundAccumulated + return t.RefundAccumulated } func (t *TxGasDimensionByOpcodeTracer) SetRefundAccumulated(refund uint64) { - t.refundAccumulated = refund + t.RefundAccumulated = refund } // ############################################################################ @@ -273,10 +276,41 @@ func (t *TxGasDimensionByOpcodeTracer) GetResult() (json.RawMessage, error) { }) } +// produce protobuf serialized result +// for storing to file in compact format +func (t *TxGasDimensionByOpcodeTracer) GetProtobufResult() ([]byte, error) { + if t.reason != nil { + return nil, t.reason + } + failed := t.err != nil + + executionResult := &proto.TxGasDimensionByOpcodeExecutionResult{ + Gas: t.usedGas, + Failed: failed, + Dimensions: make(map[string]*proto.GasesByDimension), + TxHash: t.txHash.Hex(), + BlockTimestamp: t.env.Time, + BlockNumber: t.env.BlockNumber.String(), + } + + for opcode, dimensions := range t.OpcodeToDimensions { + executionResult.Dimensions[opcode.String()] = &proto.GasesByDimension{ + OneDimensionalGasCost: dimensions.OneDimensionalGasCost, + Computation: dimensions.Computation, + StateAccess: dimensions.StateAccess, + StateGrowth: dimensions.StateGrowth, + HistoryGrowth: dimensions.HistoryGrowth, + StateGrowthRefund: dimensions.StateGrowthRefund, + } + } + + return protobuf.Marshal(executionResult) +} + // stringify opcodes for dimension log output func (t *TxGasDimensionByOpcodeTracer) GetOpcodeDimensionSummary() map[string]GasesByDimension { summary := make(map[string]GasesByDimension) - for opcode, dimensions := range t.opcodeToDimensions { + for opcode, dimensions := range t.OpcodeToDimensions { summary[opcode.String()] = dimensions } return summary From 95d81b0edf9150f15933c0b89ae3ebe6cfaef362 Mon Sep 17 00:00:00 2001 From: relyt29 Date: Wed, 16 Apr 2025 17:19:46 -0400 Subject: [PATCH 21/35] gas dimensions: handle out of gas errors correctly --- .../live/block_gas_dimension_by_opcode.go | 352 ------------------ eth/tracers/native/gas_dimension_calc.go | 102 ++++- .../native/tx_gas_dimension_by_opcode.go | 6 +- eth/tracers/native/tx_gas_dimension_logger.go | 22 +- 4 files changed, 114 insertions(+), 368 deletions(-) delete mode 100644 eth/tracers/live/block_gas_dimension_by_opcode.go diff --git a/eth/tracers/live/block_gas_dimension_by_opcode.go b/eth/tracers/live/block_gas_dimension_by_opcode.go deleted file mode 100644 index e4cddc97b1..0000000000 --- a/eth/tracers/live/block_gas_dimension_by_opcode.go +++ /dev/null @@ -1,352 +0,0 @@ -package live - -import ( - "encoding/json" - "fmt" - "math/big" - "os" - "path/filepath" - "sync/atomic" - - "github.com/ethereum/go-ethereum/common" - "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/ethereum/go-ethereum/eth/tracers/native" -) - -// initializer for the tracer -func init() { - tracers.LiveDirectory.Register("blockGasDimensionByOpcode", NewBlockGasDimensionByOpcodeLogger) -} - -// could just be paranoia but better safe than sorry -// avoids overflow by addition -type GasesByDimensionBigInt struct { - OneDimensionalGasCost *big.Int - Computation *big.Int - StateAccess *big.Int - StateGrowth *big.Int - HistoryGrowth *big.Int - StateGrowthRefund *big.Int -} - -// initializer for empty GasesByDimensionBigInt -func NewGasesByDimensionBigInt() GasesByDimensionBigInt { - return GasesByDimensionBigInt{ - OneDimensionalGasCost: big.NewInt(0), - Computation: big.NewInt(0), - StateAccess: big.NewInt(0), - StateGrowth: big.NewInt(0), - HistoryGrowth: big.NewInt(0), - StateGrowthRefund: big.NewInt(0), - } -} - -type blockGasDimensionByOpcodeLiveTraceConfig struct { - Path string `json:"path"` // Path to directory for output -} - -// gasDimensionTracer struct -type BlockGasDimensionByOpcodeLiveTracer struct { - Path string `json:"path"` // Path to directory for output - env *tracing.VMContext - blockTimestamp uint64 - blockNumber *big.Int - opcodeToDimensions map[vm.OpCode]GasesByDimensionBigInt - blockGas *big.Int - callStack native.CallGasDimensionStack - depth int - refundAccumulated uint64 - - // temp big int to avoid a bunch of allocations - tempBigInt *big.Int - - interrupt atomic.Bool // Atomic flag to signal execution interruption - reason error // Textual reason for the interruption -} - -// gasDimensionTracer returns a new tracer that traces gas -// usage for each opcode against the dimension of that opcode -// takes a context, and json input for configuration parameters -func NewBlockGasDimensionByOpcodeLogger( - cfg json.RawMessage, -) (*tracing.Hooks, error) { - var config blockGasDimensionByOpcodeLiveTraceConfig - if err := json.Unmarshal(cfg, &config); err != nil { - return nil, err - } - - if config.Path == "" { - return nil, fmt.Errorf("block gas dimension live tracer path for output is required: %v", config) - } - - t := &BlockGasDimensionByOpcodeLiveTracer{ - Path: config.Path, - depth: 1, - refundAccumulated: 0, - blockGas: big.NewInt(0), - blockNumber: big.NewInt(-1), - tempBigInt: big.NewInt(0), - blockTimestamp: 0, - opcodeToDimensions: make(map[vm.OpCode]GasesByDimensionBigInt), - } - - return &tracing.Hooks{ - OnOpcode: t.OnOpcode, - OnTxStart: t.OnTxStart, - OnTxEnd: t.OnTxEnd, - OnBlockStart: t.OnBlockStart, - OnBlockEnd: t.OnBlockEnd, - }, nil -} - -// ############################################################################ -// HOOKS -// ############################################################################ - -// hook into each opcode execution -func (t *BlockGasDimensionByOpcodeLiveTracer) OnOpcode( - pc uint64, - op byte, - gas, cost uint64, - scope tracing.OpContext, - rData []byte, - depth int, - err error, -) { - if t.interrupt.Load() { - return - } - if depth != t.depth && depth != t.depth-1 { - t.interrupt.Store(true) - t.reason = fmt.Errorf( - "expected depth fell out of sync with actual depth: %d %d != %d, callStack: %v", - pc, - t.depth, - depth, - t.callStack, - ) - return - } - if t.depth != len(t.callStack)+1 { - t.interrupt.Store(true) - t.reason = fmt.Errorf( - "depth fell out of sync with callStack: %d %d != %d, callStack: %v", - pc, - t.depth, - len(t.callStack), - t.callStack, - ) - return - } - - // get the gas dimension function - // if it's not a call, directly calculate the gas dimensions for the opcode - f := native.GetCalcGasDimensionFunc(vm.OpCode(op)) - gasesByDimension, callStackInfo, err := f(t, pc, op, gas, cost, scope, rData, depth, err) - if err != nil { - t.interrupt.Store(true) - t.reason = err - return - } - opcode := vm.OpCode(op) - - if native.WasCallOrCreate(opcode) && callStackInfo == nil || !native.WasCallOrCreate(opcode) && callStackInfo != nil { - t.interrupt.Store(true) - t.reason = fmt.Errorf( - "logic bug, calls/creates should always be accompanied by callStackInfo and non-calls should not have callStackInfo %d %s %v", - pc, - opcode.String(), - callStackInfo, - ) - return - } - - // if callStackInfo is not nil then we need to take a note of the index of the - // DimensionLog that represents this opcode and save the callStackInfo - // to call finishX after the call has returned - if native.WasCallOrCreate(opcode) { - t.callStack.Push( - native.CallGasDimensionStackInfo{ - GasDimensionInfo: *callStackInfo, - DimensionLogPosition: 0, //unused in this tracer - ExecutionCost: 0, - }) - t.depth += 1 - } else { - - // update the aggregrate map for this opcode - accumulatedDimensions, exists := t.opcodeToDimensions[opcode] - if !exists { - accumulatedDimensions = NewGasesByDimensionBigInt() - } - - // add the gas dimensions for this opcode to the accumulated dimensions - t.addGasesByDimension(&accumulatedDimensions, gasesByDimension) - - t.opcodeToDimensions[opcode] = accumulatedDimensions - - // if the opcode returns from the call stack depth, or - // if this is an opcode immediately after a call that did not increase the stack depth - // because it called an empty account or contract or wrong function signature, - // call the appropriate finishX function to write the gas dimensions - // for the call that increased the stack depth in the past - if depth < t.depth { - stackInfo, ok := t.callStack.Pop() - // base case, stack is empty, do nothing - if !ok { - t.interrupt.Store(true) - t.reason = fmt.Errorf("call stack is unexpectedly empty %d %d %d", pc, depth, t.depth) - return - } - finishFunction := native.GetFinishCalcGasDimensionFunc(stackInfo.GasDimensionInfo.Op) - if finishFunction == nil { - t.interrupt.Store(true) - t.reason = fmt.Errorf( - "no finish function found for opcode %s, call stack is messed up %d", - stackInfo.GasDimensionInfo.Op.String(), - pc, - ) - return - } - // IMPORTANT NOTE: for some reason the only reliable way to actually get the gas cost of the call - // is to subtract gas at time of call from gas at opcode AFTER return - // you can't trust the `gas` field on the call itself. I wonder if the gas field is an estimation - gasUsedByCall := stackInfo.GasDimensionInfo.GasCounterAtTimeOfCall - gas - gasesByDimensionCall := finishFunction(gasUsedByCall, stackInfo.ExecutionCost, stackInfo.GasDimensionInfo) - accumulatedDimensionsCall, exists := t.opcodeToDimensions[stackInfo.GasDimensionInfo.Op] - if !exists { - accumulatedDimensionsCall = NewGasesByDimensionBigInt() - } - - t.addGasesByDimension(&accumulatedDimensionsCall, gasesByDimensionCall) - t.opcodeToDimensions[stackInfo.GasDimensionInfo.Op] = accumulatedDimensionsCall - t.depth -= 1 - } - - // if we are in a call stack depth greater than 0, - // then we need to track the execution gas - // of our own code so that when the call returns, - // we can write the gas dimensions for the call opcode itself - if len(t.callStack) > 0 { - t.callStack.UpdateExecutionCost(cost) - } - } -} - -// on tx start, get the environment and set the depth to 1 -func (t *BlockGasDimensionByOpcodeLiveTracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) { - t.env = env - t.depth = 1 - t.refundAccumulated = 0 // refunds per tx -} - -// on tx end, add the gas used to the block gas -func (t *BlockGasDimensionByOpcodeLiveTracer) OnTxEnd(receipt *types.Receipt, err error) { - t.blockGas.Add(t.blockGas, new(big.Int).SetUint64(receipt.GasUsed)) -} - -// on block start take note of the block timestamp and number -func (t *BlockGasDimensionByOpcodeLiveTracer) OnBlockStart(ev tracing.BlockEvent) { - t.blockTimestamp = ev.Block.Time() - t.blockNumber = ev.Block.Number() -} - -// on block end, write out the gas dimensions for each opcode in the block to file -func (t *BlockGasDimensionByOpcodeLiveTracer) OnBlockEnd(err error) { - resultJsonBytes, errGettingResult := t.GetResult() - if errGettingResult != nil { - errorJsonString := fmt.Sprintf("{\"errorGettingResult\": \"%s\"}", errGettingResult.Error()) - fmt.Println(errorJsonString) - resultJsonBytes = []byte(errorJsonString) - return - } - - filename := fmt.Sprintf("%s.json", t.blockNumber.String()) - filepath := filepath.Join(t.Path, filename) - - // Ensure the directory exists - if err := os.MkdirAll(t.Path, 0755); err != nil { - fmt.Printf("Failed to create directory %s: %v\n", t.Path, err) - return - } - - // Write the file - if err := os.WriteFile(filepath, resultJsonBytes, 0644); err != nil { - fmt.Printf("Failed to write file %s: %v\n", filepath, err) - return - } - -} - -// ############################################################################ -// HELPERS -// ############################################################################ - -func (t *BlockGasDimensionByOpcodeLiveTracer) GetOpRefund() uint64 { - return t.env.StateDB.GetRefund() -} - -func (t *BlockGasDimensionByOpcodeLiveTracer) GetRefundAccumulated() uint64 { - return t.refundAccumulated -} - -func (t *BlockGasDimensionByOpcodeLiveTracer) SetRefundAccumulated(refund uint64) { - t.refundAccumulated = refund -} - -// avoid allocating a lot of big ints in a loop -func (t *BlockGasDimensionByOpcodeLiveTracer) addGasesByDimension(target *GasesByDimensionBigInt, value native.GasesByDimension) { - t.tempBigInt.SetUint64(value.OneDimensionalGasCost) - target.OneDimensionalGasCost.Add(target.OneDimensionalGasCost, t.tempBigInt) - t.tempBigInt.SetUint64(value.Computation) - target.Computation.Add(target.Computation, t.tempBigInt) - t.tempBigInt.SetUint64(value.StateAccess) - target.StateAccess.Add(target.StateAccess, t.tempBigInt) - t.tempBigInt.SetUint64(value.StateGrowth) - target.StateGrowth.Add(target.StateGrowth, t.tempBigInt) - t.tempBigInt.SetUint64(value.HistoryGrowth) - target.HistoryGrowth.Add(target.HistoryGrowth, t.tempBigInt) - t.tempBigInt.SetInt64(value.StateGrowthRefund) - target.StateGrowthRefund.Add(target.StateGrowthRefund, t.tempBigInt) -} - -// ############################################################################ -// JSON OUTPUT PRODUCTION -// ############################################################################ - -// ExecutionResult groups all dimension logs emitted by the EVM -// while replaying a transaction in debug mode as well as transaction -// execution status, the amount of gas used and the return value -type BlockGasDimensionByOpcodeExecutionResult struct { - Gas *big.Int `json:"gas"` - BlockTimetamp uint64 `json:"timestamp"` - BlockNumber *big.Int `json:"blockNumber"` - Dimensions map[string]GasesByDimensionBigInt `json:"dimensions"` -} - -// produce json result for output from tracer -// this is what the end-user actually gets from the RPC endpoint -func (t *BlockGasDimensionByOpcodeLiveTracer) GetResult() (json.RawMessage, error) { - // Tracing aborted - if t.reason != nil { - return nil, t.reason - } - return json.Marshal(&BlockGasDimensionByOpcodeExecutionResult{ - Gas: t.blockGas, - Dimensions: t.GetOpcodeDimensionSummary(), - BlockTimetamp: t.blockTimestamp, - BlockNumber: t.blockNumber, - }) -} - -// stringify opcodes for dimension log output -func (t *BlockGasDimensionByOpcodeLiveTracer) GetOpcodeDimensionSummary() map[string]GasesByDimensionBigInt { - summary := make(map[string]GasesByDimensionBigInt) - for opcode, dimensions := range t.opcodeToDimensions { - summary[opcode.String()] = dimensions - } - return summary -} diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index 20fb494548..77475dd63b 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -185,6 +185,16 @@ func calcSimpleSingleDimensionGas( depth int, err error, ) (GasesByDimension, *CallGasDimensionInfo, error) { + if err != nil { + return GasesByDimension{ + OneDimensionalGasCost: gas, + Computation: gas, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + }, nil, nil + } return GasesByDimension{ OneDimensionalGasCost: cost, Computation: cost, @@ -224,7 +234,16 @@ func calcSimpleAddressAccessSetGas( // // Therefore, for these opcodes, we do a simple check based on the raw value // and we can deduce the dimensions directly from that value. - + if err != nil { + return GasesByDimension{ + OneDimensionalGasCost: gas, + Computation: gas, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + }, nil, nil + } if cost == params.ColdAccountAccessCostEIP2929 { return GasesByDimension{ OneDimensionalGasCost: cost, @@ -260,6 +279,16 @@ func calcSLOADGas( // we don't have access to StateDb.SlotInAccessList // so we have to infer whether the slot was cold or warm based on the absolute cost // and then deduce the dimensions from that + if err != nil { + return GasesByDimension{ + OneDimensionalGasCost: gas, + Computation: gas, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + }, nil, nil + } if cost == params.ColdSloadCostEIP2929 { accessCost := params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929 leftOver := cost - accessCost @@ -309,7 +338,16 @@ func calcExtCodeCopyGas( // 3*minimum_word_size is always state access // if it is 2600, then have 2500 for state access. // rest is computation. - + if err != nil { + return GasesByDimension{ + OneDimensionalGasCost: gas, + Computation: gas, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + }, nil, nil + } stack := scope.StackData() lenStack := len(stack) size := stack[lenStack-4].Uint64() // size in stack position 4 @@ -356,6 +394,16 @@ func calcStateReadCallGas( depth int, err error, ) (GasesByDimension, *CallGasDimensionInfo, error) { + if err != nil { + return GasesByDimension{ + OneDimensionalGasCost: gas, + Computation: gas, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + }, nil, nil + } stack := scope.StackData() lenStack := len(stack) // argsOffset in stack position 3 (1-indexed) @@ -469,6 +517,16 @@ func calcLogGas( // stored in the bloom filter in the history so at 8 gas per byte, // 32 bytes per topic is 256 gas per topic. // rest is computation (for the bloom filter computation, memory expansion, etc) + if err != nil { + return GasesByDimension{ + OneDimensionalGasCost: gas, + Computation: gas, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + }, nil, nil + } numTopics := uint64(0) switch vm.OpCode(op) { case vm.LOG0: @@ -522,6 +580,16 @@ func calcCreateGas( // code_deposit_cost = 200 * deployed_code_size // static_gas = 32000 // dynamic_gas = init_code_cost + memory_expansion_cost + deployment_code_execution_cost + code_deposit_cost + if err != nil { + return GasesByDimension{ + OneDimensionalGasCost: gas, + Computation: gas, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + }, nil, nil + } stack := scope.StackData() lenStack := len(stack) // size is on stack position 3 (1-indexed) @@ -602,6 +670,16 @@ func calcReadAndStoreCallGas( depth int, err error, ) (GasesByDimension, *CallGasDimensionInfo, error) { + if err != nil { + return GasesByDimension{ + OneDimensionalGasCost: gas, + Computation: gas, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + }, nil, nil + } stack := scope.StackData() lenStack := len(stack) // value is in stack position 3 @@ -764,6 +842,16 @@ func calcSStoreGas( // refunds are tracked in the statedb // to find per-step changes, we track the accumulated refund // and compare it to the current refund + if err != nil { + return GasesByDimension{ + OneDimensionalGasCost: gas, + Computation: gas, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + }, nil, nil + } currentRefund := t.GetOpRefund() accumulatedRefund := t.GetRefundAccumulated() var diff int64 = 0 @@ -824,6 +912,16 @@ func calcSelfDestructGas( // we consider the static cost of 5000 as a state read/write because selfdestruct, // excepting 100 for the warm access set // doesn't actually delete anything from disk, it just marks it as deleted. + if err != nil { + return GasesByDimension{ + OneDimensionalGasCost: gas, + Computation: gas, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + }, nil, nil + } if cost == params.CreateBySelfdestructGas+params.SelfdestructGasEIP150 { // warm but funds target empty // 30000 gas total diff --git a/eth/tracers/native/tx_gas_dimension_by_opcode.go b/eth/tracers/native/tx_gas_dimension_by_opcode.go index 1c3444dda1..7747134f00 100644 --- a/eth/tracers/native/tx_gas_dimension_by_opcode.go +++ b/eth/tracers/native/tx_gas_dimension_by_opcode.go @@ -105,10 +105,10 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( // get the gas dimension function // if it's not a call, directly calculate the gas dimensions for the opcode f := GetCalcGasDimensionFunc(vm.OpCode(op)) - gasesByDimension, callStackInfo, err := f(t, pc, op, gas, cost, scope, rData, depth, err) - if err != nil { + gasesByDimension, callStackInfo, fErr := f(t, pc, op, gas, cost, scope, rData, depth, err) + if fErr != nil { t.interrupt.Store(true) - t.reason = err + t.reason = fErr return } opcode := vm.OpCode(op) diff --git a/eth/tracers/native/tx_gas_dimension_logger.go b/eth/tracers/native/tx_gas_dimension_logger.go index bba1dfe161..29c96967f1 100644 --- a/eth/tracers/native/tx_gas_dimension_logger.go +++ b/eth/tracers/native/tx_gas_dimension_logger.go @@ -129,10 +129,10 @@ func (t *TxGasDimensionLogger) OnOpcode( // get the gas dimension function // if it's not a call, directly calculate the gas dimensions for the opcode f := GetCalcGasDimensionFunc(vm.OpCode(op)) - gasesByDimension, callStackInfo, err := f(t, pc, op, gas, cost, scope, rData, depth, err) - if err != nil { + gasesByDimension, callStackInfo, fErr := f(t, pc, op, gas, cost, scope, rData, depth, err) + if fErr != nil { t.interrupt.Store(true) - t.reason = err + t.reason = fErr return } opcode := vm.OpCode(op) @@ -201,14 +201,14 @@ func (t *TxGasDimensionLogger) OnOpcode( // is to subtract gas at time of call from gas at opcode AFTER return // you can't trust the `gas` field on the call itself. I wonder if the gas field is an estimation gasUsedByCall := stackInfo.GasDimensionInfo.GasCounterAtTimeOfCall - gas - gasesByDimension := finishFunction(gasUsedByCall, stackInfo.ExecutionCost, stackInfo.GasDimensionInfo) + finishGasesByDimension := finishFunction(gasUsedByCall, stackInfo.ExecutionCost, stackInfo.GasDimensionInfo) callDimensionLog := t.logs[stackInfo.DimensionLogPosition] - callDimensionLog.OneDimensionalGasCost = gasesByDimension.OneDimensionalGasCost - callDimensionLog.Computation = gasesByDimension.Computation - callDimensionLog.StateAccess = gasesByDimension.StateAccess - callDimensionLog.StateGrowth = gasesByDimension.StateGrowth - callDimensionLog.HistoryGrowth = gasesByDimension.HistoryGrowth - callDimensionLog.StateGrowthRefund = gasesByDimension.StateGrowthRefund + callDimensionLog.OneDimensionalGasCost = finishGasesByDimension.OneDimensionalGasCost + callDimensionLog.Computation = finishGasesByDimension.Computation + callDimensionLog.StateAccess = finishGasesByDimension.StateAccess + callDimensionLog.StateGrowth = finishGasesByDimension.StateGrowth + callDimensionLog.HistoryGrowth = finishGasesByDimension.HistoryGrowth + callDimensionLog.StateGrowthRefund = finishGasesByDimension.StateGrowthRefund callDimensionLog.CallRealGas = gasUsedByCall callDimensionLog.CallExecutionCost = stackInfo.ExecutionCost callDimensionLog.CallMemoryExpansion = stackInfo.GasDimensionInfo.MemoryExpansionCost @@ -223,7 +223,7 @@ func (t *TxGasDimensionLogger) OnOpcode( // of our own code so that when the call returns, // we can write the gas dimensions for the call opcode itself if len(t.callStack) > 0 { - t.callStack.UpdateExecutionCost(cost) + t.callStack.UpdateExecutionCost(gasesByDimension.OneDimensionalGasCost) } } } From a1aeae6a05587cb28e1428ea0a9cdbc0fb03da6a Mon Sep 17 00:00:00 2001 From: relyt29 Date: Wed, 16 Apr 2025 18:14:22 -0400 Subject: [PATCH 22/35] gas dimension protobuf opcodes as uint32 instead of string --- .../proto/gas_dimension_by_opcode.pb.go | 64 +++++++++---------- .../proto/gas_dimension_by_opcode.proto | 2 +- .../native/tx_gas_dimension_by_opcode.go | 4 +- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/eth/tracers/native/proto/gas_dimension_by_opcode.pb.go b/eth/tracers/native/proto/gas_dimension_by_opcode.pb.go index 9d7d1bf99a..bcf85f036b 100644 --- a/eth/tracers/native/proto/gas_dimension_by_opcode.pb.go +++ b/eth/tracers/native/proto/gas_dimension_by_opcode.pb.go @@ -2,7 +2,7 @@ // versions: // protoc-gen-go v1.36.6 // protoc v5.29.3 -// source: gas_dimension_by_opcode.proto +// source: eth/tracers/native/proto/gas_dimension_by_opcode.proto package proto @@ -36,7 +36,7 @@ type GasesByDimension struct { func (x *GasesByDimension) Reset() { *x = GasesByDimension{} - mi := &file_gas_dimension_by_opcode_proto_msgTypes[0] + mi := &file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -48,7 +48,7 @@ func (x *GasesByDimension) String() string { func (*GasesByDimension) ProtoMessage() {} func (x *GasesByDimension) ProtoReflect() protoreflect.Message { - mi := &file_gas_dimension_by_opcode_proto_msgTypes[0] + mi := &file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -61,7 +61,7 @@ func (x *GasesByDimension) ProtoReflect() protoreflect.Message { // Deprecated: Use GasesByDimension.ProtoReflect.Descriptor instead. func (*GasesByDimension) Descriptor() ([]byte, []int) { - return file_gas_dimension_by_opcode_proto_rawDescGZIP(), []int{0} + return file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_rawDescGZIP(), []int{0} } func (x *GasesByDimension) GetOneDimensionalGasCost() uint64 { @@ -111,7 +111,7 @@ type TxGasDimensionByOpcodeExecutionResult struct { state protoimpl.MessageState `protogen:"open.v1"` Gas uint64 `protobuf:"varint,1,opt,name=gas,proto3" json:"gas,omitempty"` Failed bool `protobuf:"varint,2,opt,name=failed,proto3" json:"failed,omitempty"` - Dimensions map[string]*GasesByDimension `protobuf:"bytes,3,rep,name=dimensions,proto3" json:"dimensions,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Dimensions map[uint32]*GasesByDimension `protobuf:"bytes,3,rep,name=dimensions,proto3" json:"dimensions,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` TxHash string `protobuf:"bytes,4,opt,name=tx_hash,json=txHash,proto3" json:"tx_hash,omitempty"` BlockTimestamp uint64 `protobuf:"varint,5,opt,name=block_timestamp,json=blockTimestamp,proto3" json:"block_timestamp,omitempty"` BlockNumber string `protobuf:"bytes,6,opt,name=block_number,json=blockNumber,proto3" json:"block_number,omitempty"` // Using string to represent big.Int @@ -121,7 +121,7 @@ type TxGasDimensionByOpcodeExecutionResult struct { func (x *TxGasDimensionByOpcodeExecutionResult) Reset() { *x = TxGasDimensionByOpcodeExecutionResult{} - mi := &file_gas_dimension_by_opcode_proto_msgTypes[1] + mi := &file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -133,7 +133,7 @@ func (x *TxGasDimensionByOpcodeExecutionResult) String() string { func (*TxGasDimensionByOpcodeExecutionResult) ProtoMessage() {} func (x *TxGasDimensionByOpcodeExecutionResult) ProtoReflect() protoreflect.Message { - mi := &file_gas_dimension_by_opcode_proto_msgTypes[1] + mi := &file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -146,7 +146,7 @@ func (x *TxGasDimensionByOpcodeExecutionResult) ProtoReflect() protoreflect.Mess // Deprecated: Use TxGasDimensionByOpcodeExecutionResult.ProtoReflect.Descriptor instead. func (*TxGasDimensionByOpcodeExecutionResult) Descriptor() ([]byte, []int) { - return file_gas_dimension_by_opcode_proto_rawDescGZIP(), []int{1} + return file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_rawDescGZIP(), []int{1} } func (x *TxGasDimensionByOpcodeExecutionResult) GetGas() uint64 { @@ -163,7 +163,7 @@ func (x *TxGasDimensionByOpcodeExecutionResult) GetFailed() bool { return false } -func (x *TxGasDimensionByOpcodeExecutionResult) GetDimensions() map[string]*GasesByDimension { +func (x *TxGasDimensionByOpcodeExecutionResult) GetDimensions() map[uint32]*GasesByDimension { if x != nil { return x.Dimensions } @@ -191,11 +191,11 @@ func (x *TxGasDimensionByOpcodeExecutionResult) GetBlockNumber() string { return "" } -var File_gas_dimension_by_opcode_proto protoreflect.FileDescriptor +var File_eth_tracers_native_proto_gas_dimension_by_opcode_proto protoreflect.FileDescriptor -const file_gas_dimension_by_opcode_proto_rawDesc = "" + +const file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_rawDesc = "" + "\n" + - "\x1dgas_dimension_by_opcode.proto\x12\x18eth.tracers.native.proto\"\x8a\x02\n" + + "6eth/tracers/native/proto/gas_dimension_by_opcode.proto\x12\x18eth.tracers.native.proto\"\x8a\x02\n" + "\x10GasesByDimension\x127\n" + "\x18one_dimensional_gas_cost\x18\x01 \x01(\x04R\x15oneDimensionalGasCost\x12 \n" + "\vcomputation\x18\x02 \x01(\x04R\vcomputation\x12!\n" + @@ -213,28 +213,28 @@ const file_gas_dimension_by_opcode_proto_rawDesc = "" + "\x0fblock_timestamp\x18\x05 \x01(\x04R\x0eblockTimestamp\x12!\n" + "\fblock_number\x18\x06 \x01(\tR\vblockNumber\x1ai\n" + "\x0fDimensionsEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x12@\n" + + "\x03key\x18\x01 \x01(\rR\x03key\x12@\n" + "\x05value\x18\x02 \x01(\v2*.eth.tracers.native.proto.GasesByDimensionR\x05value:\x028\x01B:Z8github.com/ethereum/go-ethereum/eth/tracers/native/protob\x06proto3" var ( - file_gas_dimension_by_opcode_proto_rawDescOnce sync.Once - file_gas_dimension_by_opcode_proto_rawDescData []byte + file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_rawDescOnce sync.Once + file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_rawDescData []byte ) -func file_gas_dimension_by_opcode_proto_rawDescGZIP() []byte { - file_gas_dimension_by_opcode_proto_rawDescOnce.Do(func() { - file_gas_dimension_by_opcode_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_gas_dimension_by_opcode_proto_rawDesc), len(file_gas_dimension_by_opcode_proto_rawDesc))) +func file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_rawDescGZIP() []byte { + file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_rawDescOnce.Do(func() { + file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_rawDesc), len(file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_rawDesc))) }) - return file_gas_dimension_by_opcode_proto_rawDescData + return file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_rawDescData } -var file_gas_dimension_by_opcode_proto_msgTypes = make([]protoimpl.MessageInfo, 3) -var file_gas_dimension_by_opcode_proto_goTypes = []any{ +var file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_goTypes = []any{ (*GasesByDimension)(nil), // 0: eth.tracers.native.proto.GasesByDimension (*TxGasDimensionByOpcodeExecutionResult)(nil), // 1: eth.tracers.native.proto.TxGasDimensionByOpcodeExecutionResult nil, // 2: eth.tracers.native.proto.TxGasDimensionByOpcodeExecutionResult.DimensionsEntry } -var file_gas_dimension_by_opcode_proto_depIdxs = []int32{ +var file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_depIdxs = []int32{ 2, // 0: eth.tracers.native.proto.TxGasDimensionByOpcodeExecutionResult.dimensions:type_name -> eth.tracers.native.proto.TxGasDimensionByOpcodeExecutionResult.DimensionsEntry 0, // 1: eth.tracers.native.proto.TxGasDimensionByOpcodeExecutionResult.DimensionsEntry.value:type_name -> eth.tracers.native.proto.GasesByDimension 2, // [2:2] is the sub-list for method output_type @@ -244,26 +244,26 @@ var file_gas_dimension_by_opcode_proto_depIdxs = []int32{ 0, // [0:2] is the sub-list for field type_name } -func init() { file_gas_dimension_by_opcode_proto_init() } -func file_gas_dimension_by_opcode_proto_init() { - if File_gas_dimension_by_opcode_proto != nil { +func init() { file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_init() } +func file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_init() { + if File_eth_tracers_native_proto_gas_dimension_by_opcode_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_gas_dimension_by_opcode_proto_rawDesc), len(file_gas_dimension_by_opcode_proto_rawDesc)), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_rawDesc), len(file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_rawDesc)), NumEnums: 0, NumMessages: 3, NumExtensions: 0, NumServices: 0, }, - GoTypes: file_gas_dimension_by_opcode_proto_goTypes, - DependencyIndexes: file_gas_dimension_by_opcode_proto_depIdxs, - MessageInfos: file_gas_dimension_by_opcode_proto_msgTypes, + GoTypes: file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_goTypes, + DependencyIndexes: file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_depIdxs, + MessageInfos: file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_msgTypes, }.Build() - File_gas_dimension_by_opcode_proto = out.File - file_gas_dimension_by_opcode_proto_goTypes = nil - file_gas_dimension_by_opcode_proto_depIdxs = nil + File_eth_tracers_native_proto_gas_dimension_by_opcode_proto = out.File + file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_goTypes = nil + file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_depIdxs = nil } diff --git a/eth/tracers/native/proto/gas_dimension_by_opcode.proto b/eth/tracers/native/proto/gas_dimension_by_opcode.proto index d5252af83c..7c9ebc5e60 100644 --- a/eth/tracers/native/proto/gas_dimension_by_opcode.proto +++ b/eth/tracers/native/proto/gas_dimension_by_opcode.proto @@ -18,7 +18,7 @@ message GasesByDimension { message TxGasDimensionByOpcodeExecutionResult { uint64 gas = 1; bool failed = 2; - map dimensions = 3; + map dimensions = 3; string tx_hash = 4; uint64 block_timestamp = 5; string block_number = 6; // Using string to represent big.Int diff --git a/eth/tracers/native/tx_gas_dimension_by_opcode.go b/eth/tracers/native/tx_gas_dimension_by_opcode.go index 7747134f00..2e11b75f07 100644 --- a/eth/tracers/native/tx_gas_dimension_by_opcode.go +++ b/eth/tracers/native/tx_gas_dimension_by_opcode.go @@ -287,14 +287,14 @@ func (t *TxGasDimensionByOpcodeTracer) GetProtobufResult() ([]byte, error) { executionResult := &proto.TxGasDimensionByOpcodeExecutionResult{ Gas: t.usedGas, Failed: failed, - Dimensions: make(map[string]*proto.GasesByDimension), + Dimensions: make(map[uint32]*proto.GasesByDimension), TxHash: t.txHash.Hex(), BlockTimestamp: t.env.Time, BlockNumber: t.env.BlockNumber.String(), } for opcode, dimensions := range t.OpcodeToDimensions { - executionResult.Dimensions[opcode.String()] = &proto.GasesByDimension{ + executionResult.Dimensions[uint32(opcode)] = &proto.GasesByDimension{ OneDimensionalGasCost: dimensions.OneDimensionalGasCost, Computation: dimensions.Computation, StateAccess: dimensions.StateAccess, From 668f0b398cde780af255ab9243cf50ebed80d726 Mon Sep 17 00:00:00 2001 From: relyt29 Date: Wed, 16 Apr 2025 18:28:11 -0400 Subject: [PATCH 23/35] error handling on calls with out of gas --- eth/tracers/native/tx_gas_dimension_by_opcode.go | 4 ++-- eth/tracers/native/tx_gas_dimension_logger.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/eth/tracers/native/tx_gas_dimension_by_opcode.go b/eth/tracers/native/tx_gas_dimension_by_opcode.go index 2e11b75f07..385f42e589 100644 --- a/eth/tracers/native/tx_gas_dimension_by_opcode.go +++ b/eth/tracers/native/tx_gas_dimension_by_opcode.go @@ -113,7 +113,7 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( } opcode := vm.OpCode(op) - if WasCallOrCreate(opcode) && callStackInfo == nil || !WasCallOrCreate(opcode) && callStackInfo != nil { + if WasCallOrCreate(opcode) && callStackInfo == nil && err == nil || !WasCallOrCreate(opcode) && callStackInfo != nil { t.interrupt.Store(true) t.reason = fmt.Errorf( "logic bug, calls/creates should always be accompanied by callStackInfo and non-calls should not have callStackInfo %d %s %v", @@ -127,7 +127,7 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( // if callStackInfo is not nil then we need to take a note of the index of the // DimensionLog that represents this opcode and save the callStackInfo // to call finishX after the call has returned - if WasCallOrCreate(opcode) { + if WasCallOrCreate(opcode) && err == nil { t.callStack.Push( CallGasDimensionStackInfo{ GasDimensionInfo: *callStackInfo, diff --git a/eth/tracers/native/tx_gas_dimension_logger.go b/eth/tracers/native/tx_gas_dimension_logger.go index 29c96967f1..ef3df8b0a1 100644 --- a/eth/tracers/native/tx_gas_dimension_logger.go +++ b/eth/tracers/native/tx_gas_dimension_logger.go @@ -137,7 +137,7 @@ func (t *TxGasDimensionLogger) OnOpcode( } opcode := vm.OpCode(op) - if WasCallOrCreate(opcode) && callStackInfo == nil || !WasCallOrCreate(opcode) && callStackInfo != nil { + if WasCallOrCreate(opcode) && callStackInfo == nil && err == nil || !WasCallOrCreate(opcode) && callStackInfo != nil { t.interrupt.Store(true) t.reason = fmt.Errorf( "logic bug, calls/creates should always be accompanied by callStackInfo and non-calls should not have callStackInfo %d %s %v", @@ -164,7 +164,7 @@ func (t *TxGasDimensionLogger) OnOpcode( // if callStackInfo is not nil then we need to take a note of the index of the // DimensionLog that represents this opcode and save the callStackInfo // to call finishX after the call has returned - if WasCallOrCreate(opcode) { + if WasCallOrCreate(opcode) && err == nil { opcodeLogIndex := len(t.logs) - 1 // minus 1 because we've already appended the log t.callStack.Push( CallGasDimensionStackInfo{ From 9720b9df581aeb97671ce8982d16300624eb9c88 Mon Sep 17 00:00:00 2001 From: relyt29 Date: Mon, 21 Apr 2025 14:30:57 -0400 Subject: [PATCH 24/35] gas dimension tracing: refactor shared duplicate code --- .../live/tx_gas_dimension_by_opcode.go | 16 +- .../native/base_gas_dimension_tracer.go | 278 ++++++++++++++++++ .../native/tx_gas_dimension_by_opcode.go | 180 ++---------- eth/tracers/native/tx_gas_dimension_logger.go | 194 ++---------- 4 files changed, 342 insertions(+), 326 deletions(-) create mode 100644 eth/tracers/native/base_gas_dimension_tracer.go diff --git a/eth/tracers/live/tx_gas_dimension_by_opcode.go b/eth/tracers/live/tx_gas_dimension_by_opcode.go index 8d413f4d2a..452ed5083d 100644 --- a/eth/tracers/live/tx_gas_dimension_by_opcode.go +++ b/eth/tracers/live/tx_gas_dimension_by_opcode.go @@ -69,9 +69,8 @@ func (t *TxGasDimensionByOpcodeLiveTracer) OnTxStart( } t.GasDimensionTracer = &native.TxGasDimensionByOpcodeTracer{ - Depth: 1, - RefundAccumulated: 0, - OpcodeToDimensions: make(map[_vm.OpCode]native.GasesByDimension), + BaseGasDimensionTracer: native.NewBaseGasDimensionTracer(), + OpcodeToDimensions: make(map[_vm.OpCode]native.GasesByDimension), } t.GasDimensionTracer.OnTxStart(vm, tx, from) } @@ -98,17 +97,6 @@ func (t *TxGasDimensionByOpcodeLiveTracer) OnTxEnd( // system transactions don't use any gas // they can be skipped if receipt.GasUsed != 0 { - - // previously: json - // then get the json from the native tracer - // executionResultJsonBytes, errGettingResult := t.GasDimensionTracer.GetResult() - // if errGettingResult != nil { - // errorJsonString := fmt.Sprintf("{\"errorGettingResult\": \"%s\"}", errGettingResult.Error()) - // fmt.Println(errorJsonString) - // return - // } - - // now: protobuf executionResultBytes, err := t.GasDimensionTracer.GetProtobufResult() if err != nil { fmt.Printf("Failed to get protobuf result: %v\n", err) diff --git a/eth/tracers/native/base_gas_dimension_tracer.go b/eth/tracers/native/base_gas_dimension_tracer.go new file mode 100644 index 0000000000..e7f4c7a250 --- /dev/null +++ b/eth/tracers/native/base_gas_dimension_tracer.go @@ -0,0 +1,278 @@ +package native + +import ( + "fmt" + "math/big" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" +) + +// BaseGasDimensionTracer contains the shared functionality between different gas dimension tracers +type BaseGasDimensionTracer struct { + // hold on to the context + env *tracing.VMContext + // the hash of the transactionh + txHash common.Hash + // the amount of gas used in the transaction + usedGas uint64 + // the call stack for the transaction + callStack CallGasDimensionStack + // the depth at the current step of execution of the call stack + depth int + // the amount of refund accumulated at the current step of execution + refundAccumulated uint64 + // whether the transaction had an error, like out of gas + err error + // whether the tracer itself was interrupted + interrupt atomic.Bool + // reason or error for the interruption in the tracer itself (as opposed to the transaction) + reason error +} + +func NewBaseGasDimensionTracer() BaseGasDimensionTracer { + return BaseGasDimensionTracer{ + depth: 1, + refundAccumulated: 0, + } +} + +// OnOpcode handles the shared opcode execution logic +// since this is so sensitive, we supply helper methods for +// different parts of the logic but expect child tracers +// to implement their own specific logic in their own +// OnOpcode method +func (t *BaseGasDimensionTracer) OnOpcode( + pc uint64, + op byte, + gas, cost uint64, + scope tracing.OpContext, + rData []byte, + depth int, + err error, +) error { + return fmt.Errorf("OnOpcode not implemented") +} + +// onOpcodeStart is a helper function that +// implements the shared logic for the start of an OnOpcode function +// between all of the gas dimension tracers +func (t *BaseGasDimensionTracer) onOpcodeStart( + pc uint64, + op byte, + gas, cost uint64, + scope tracing.OpContext, + rData []byte, + depth int, + err error, +) ( + interrupted bool, + gasesByDimension GasesByDimension, + callStackInfo *CallGasDimensionInfo, + opcode vm.OpCode, +) { + if t.interrupt.Load() { + return true, GasesByDimension{}, nil, vm.OpCode(op) + } + if depth != t.depth && depth != t.depth-1 { + t.interrupt.Store(true) + t.reason = fmt.Errorf( + "expected depth fell out of sync with actual depth: %d %d != %d, callStack: %v", + pc, + t.depth, + depth, + t.callStack, + ) + return true, GasesByDimension{}, nil, vm.OpCode(op) + } + if t.depth != len(t.callStack)+1 { + t.interrupt.Store(true) + t.reason = fmt.Errorf( + "depth fell out of sync with callStack: %d %d != %d, callStack: %v", + pc, + t.depth, + len(t.callStack), + t.callStack, + ) + return true, GasesByDimension{}, nil, vm.OpCode(op) + } + + // get the gas dimension function + // if it's not a call, directly calculate the gas dimensions for the opcode + f := GetCalcGasDimensionFunc(vm.OpCode(op)) + var fErr error = nil + gasesByDimension, callStackInfo, fErr = f(t, pc, op, gas, cost, scope, rData, depth, err) + if fErr != nil { + t.interrupt.Store(true) + t.reason = fErr + return true, GasesByDimension{}, nil, vm.OpCode(op) + } + opcode = vm.OpCode(op) + + if WasCallOrCreate(opcode) && callStackInfo == nil && err == nil || !WasCallOrCreate(opcode) && callStackInfo != nil { + t.interrupt.Store(true) + t.reason = fmt.Errorf( + "logic bug, calls/creates should always be accompanied by callStackInfo and non-calls should not have callStackInfo %d %s %v", + pc, + opcode.String(), + callStackInfo, + ) + return true, GasesByDimension{}, nil, vm.OpCode(op) + } + return false, gasesByDimension, callStackInfo, opcode +} + +// handleCallStackPush is a helper function that +// implements the shared logic for the push of a call stack +// between all of the gas dimension tracers +// some of the tracers will need to implement their own +// because DimensionLogPosition is not used in all tracers +func (t *BaseGasDimensionTracer) handleCallStackPush(callStackInfo *CallGasDimensionInfo) { + t.callStack.Push( + CallGasDimensionStackInfo{ + GasDimensionInfo: *callStackInfo, + DimensionLogPosition: 0, // unused in opcode tracer, other tracers should implement their own. + ExecutionCost: 0, + }) + t.depth += 1 +} + +// if the opcode returns from the call stack depth, or +// if this is an opcode immediately after a call that did not increase the stack depth +// because it called an empty account or contract or wrong function signature, +// call the appropriate finishX function to write the gas dimensions +// for the call that increased the stack depth in the past +func (t *BaseGasDimensionTracer) callFinishFunction( + pc uint64, + depth int, + gas uint64, +) ( + interrupted bool, + gasUsedByCall uint64, + stackInfo CallGasDimensionStackInfo, + finishGasesByDimension GasesByDimension, +) { + stackInfo, ok := t.callStack.Pop() + // base case, stack is empty, do nothing + if !ok { + t.interrupt.Store(true) + t.reason = fmt.Errorf("call stack is unexpectedly empty %d %d %d", pc, depth, t.depth) + return true, 0, CallGasDimensionStackInfo{}, GasesByDimension{} + } + finishFunction := GetFinishCalcGasDimensionFunc(stackInfo.GasDimensionInfo.Op) + if finishFunction == nil { + t.interrupt.Store(true) + t.reason = fmt.Errorf( + "no finish function found for opcode %s, call stack is messed up %d", + stackInfo.GasDimensionInfo.Op.String(), + pc, + ) + return true, 0, CallGasDimensionStackInfo{}, GasesByDimension{} + } + // IMPORTANT NOTE: for some reason the only reliable way to actually get the gas cost of the call + // is to subtract gas at time of call from gas at opcode AFTER return + // you can't trust the `gas` field on the call itself. I wonder if the gas field is an estimation + gasUsedByCall = stackInfo.GasDimensionInfo.GasCounterAtTimeOfCall - gas + finishGasesByDimension = finishFunction(gasUsedByCall, stackInfo.ExecutionCost, stackInfo.GasDimensionInfo) + return false, gasUsedByCall, stackInfo, finishGasesByDimension +} + +// if we are in a call stack depth greater than 0, +// then we need to track the execution gas +// of our own code so that when the call returns, +// we can write the gas dimensions for the call opcode itself +func (t *BaseGasDimensionTracer) updateExecutionCost(cost uint64) { + if len(t.callStack) > 0 { + t.callStack.UpdateExecutionCost(cost) + } +} + +// OnTxStart handles transaction start +func (t *BaseGasDimensionTracer) OnTxStart(env *tracing.VMContext, _ *types.Transaction, _ common.Address) { + t.env = env +} + +// OnTxEnd handles transaction end +func (t *BaseGasDimensionTracer) OnTxEnd(receipt *types.Receipt, err error) { + if err != nil { + // Don't override vm error + if t.err == nil { + t.err = err + } + return + } + t.usedGas = receipt.GasUsed + t.txHash = receipt.TxHash +} + +// Stop signals the tracer to stop tracing +func (t *BaseGasDimensionTracer) Stop(err error) { + t.reason = err + t.interrupt.Store(true) +} + +// ############################################################################ +// HELPERS +// ############################################################################ + +// WasCallOrCreate returns true if the opcode is a type of opcode that makes calls increasing the stack depth +func WasCallOrCreate(opcode vm.OpCode) bool { + return opcode == vm.CALL || opcode == vm.CALLCODE || opcode == vm.DELEGATECALL || + opcode == vm.STATICCALL || opcode == vm.CREATE || opcode == vm.CREATE2 +} + +// GetOpRefund returns the current op refund +func (t *BaseGasDimensionTracer) GetOpRefund() uint64 { + return t.env.StateDB.GetRefund() +} + +// GetRefundAccumulated returns the accumulated refund +func (t *BaseGasDimensionTracer) GetRefundAccumulated() uint64 { + return t.refundAccumulated +} + +// SetRefundAccumulated sets the accumulated refund +func (t *BaseGasDimensionTracer) SetRefundAccumulated(refund uint64) { + t.refundAccumulated = refund +} + +// GetStateDB returns the state database +func (t *BaseGasDimensionTracer) GetStateDB() tracing.StateDB { + return t.env.StateDB +} + +// Error returns the VM error captured by the trace +func (t *BaseGasDimensionTracer) Error() error { return t.err } + +// ############################################################################ +// OUTPUTS +// ############################################################################ + +// BaseExecutionResult has shared fields for execution results +type BaseExecutionResult struct { + Gas uint64 `json:"gas"` + Failed bool `json:"fail"` + TxHash string `json:"hash"` + BlockTimestamp uint64 `json:"time"` + BlockNumber *big.Int `json:"num"` +} + +// get the result of the transaction execution that we will hand to the json output +func (t *BaseGasDimensionTracer) GetBaseExecutionResult() (BaseExecutionResult, error) { + // Tracing aborted + if t.reason != nil { + return BaseExecutionResult{}, t.reason + } + failed := t.err != nil + + return BaseExecutionResult{ + Gas: t.usedGas, + Failed: failed, + TxHash: t.txHash.Hex(), + BlockTimestamp: t.env.Time, + BlockNumber: t.env.BlockNumber, + }, nil +} diff --git a/eth/tracers/native/tx_gas_dimension_by_opcode.go b/eth/tracers/native/tx_gas_dimension_by_opcode.go index 385f42e589..b2ddccb001 100644 --- a/eth/tracers/native/tx_gas_dimension_by_opcode.go +++ b/eth/tracers/native/tx_gas_dimension_by_opcode.go @@ -3,14 +3,10 @@ package native import ( "encoding/json" "fmt" - "math/big" - "sync/atomic" "github.com/ethereum/go-ethereum/eth/tracers/native/proto" - "github.com/ethereum/go-ethereum/common" "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/ethereum/go-ethereum/params" @@ -24,17 +20,8 @@ func init() { // gasDimensionTracer struct type TxGasDimensionByOpcodeTracer struct { - env *tracing.VMContext - txHash common.Hash + BaseGasDimensionTracer OpcodeToDimensions map[vm.OpCode]GasesByDimension - err error - usedGas uint64 - callStack CallGasDimensionStack - Depth int - RefundAccumulated uint64 - - interrupt atomic.Bool // Atomic flag to signal execution interruption - reason error // Textual reason for the interruption } // gasDimensionTracer returns a new tracer that traces gas @@ -47,9 +34,8 @@ func NewTxGasDimensionByOpcodeLogger( ) (*tracers.Tracer, error) { t := &TxGasDimensionByOpcodeTracer{ - Depth: 1, - RefundAccumulated: 0, - OpcodeToDimensions: make(map[vm.OpCode]GasesByDimension), + BaseGasDimensionTracer: NewBaseGasDimensionTracer(), + OpcodeToDimensions: make(map[vm.OpCode]GasesByDimension), } return &tracers.Tracer{ @@ -77,50 +63,8 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( depth int, err error, ) { - if t.interrupt.Load() { - return - } - if depth != t.Depth && depth != t.Depth-1 { - t.interrupt.Store(true) - t.reason = fmt.Errorf( - "expected depth fell out of sync with actual depth: %d %d != %d, callStack: %v", - pc, - t.Depth, - depth, - t.callStack, - ) - return - } - if t.Depth != len(t.callStack)+1 { - t.interrupt.Store(true) - t.reason = fmt.Errorf( - "depth fell out of sync with callStack: %d %d != %d, callStack: %v", - pc, - t.Depth, - len(t.callStack), - t.callStack, - ) - } - - // get the gas dimension function - // if it's not a call, directly calculate the gas dimensions for the opcode - f := GetCalcGasDimensionFunc(vm.OpCode(op)) - gasesByDimension, callStackInfo, fErr := f(t, pc, op, gas, cost, scope, rData, depth, err) - if fErr != nil { - t.interrupt.Store(true) - t.reason = fErr - return - } - opcode := vm.OpCode(op) - - if WasCallOrCreate(opcode) && callStackInfo == nil && err == nil || !WasCallOrCreate(opcode) && callStackInfo != nil { - t.interrupt.Store(true) - t.reason = fmt.Errorf( - "logic bug, calls/creates should always be accompanied by callStackInfo and non-calls should not have callStackInfo %d %s %v", - pc, - opcode.String(), - callStackInfo, - ) + interrupted, gasesByDimension, callStackInfo, opcode := t.onOpcodeStart(pc, op, gas, cost, scope, rData, depth, err) + if interrupted { return } @@ -128,13 +72,7 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( // DimensionLog that represents this opcode and save the callStackInfo // to call finishX after the call has returned if WasCallOrCreate(opcode) && err == nil { - t.callStack.Push( - CallGasDimensionStackInfo{ - GasDimensionInfo: *callStackInfo, - DimensionLogPosition: 0, //unused in this tracer - ExecutionCost: 0, - }) - t.Depth += 1 + t.handleCallStackPush(callStackInfo) } else { // update the aggregrate map for this opcode @@ -154,12 +92,12 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( // because it called an empty account or contract or wrong function signature, // call the appropriate finishX function to write the gas dimensions // for the call that increased the stack depth in the past - if depth < t.Depth { + if depth < t.depth { stackInfo, ok := t.callStack.Pop() // base case, stack is empty, do nothing if !ok { t.interrupt.Store(true) - t.reason = fmt.Errorf("call stack is unexpectedly empty %d %d %d", pc, depth, t.Depth) + t.reason = fmt.Errorf("call stack is unexpectedly empty %d %d %d", pc, depth, t.depth) return } finishFunction := GetFinishCalcGasDimensionFunc(stackInfo.GasDimensionInfo.Op) @@ -176,68 +114,23 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( // is to subtract gas at time of call from gas at opcode AFTER return // you can't trust the `gas` field on the call itself. I wonder if the gas field is an estimation gasUsedByCall := stackInfo.GasDimensionInfo.GasCounterAtTimeOfCall - gas - gasesByDimensionCall := finishFunction(gasUsedByCall, stackInfo.ExecutionCost, stackInfo.GasDimensionInfo) + finishGasesByDimension := finishFunction(gasUsedByCall, stackInfo.ExecutionCost, stackInfo.GasDimensionInfo) accumulatedDimensionsCall := t.OpcodeToDimensions[stackInfo.GasDimensionInfo.Op] - accumulatedDimensionsCall.OneDimensionalGasCost += gasesByDimensionCall.OneDimensionalGasCost - accumulatedDimensionsCall.Computation += gasesByDimensionCall.Computation - accumulatedDimensionsCall.StateAccess += gasesByDimensionCall.StateAccess - accumulatedDimensionsCall.StateGrowth += gasesByDimensionCall.StateGrowth - accumulatedDimensionsCall.HistoryGrowth += gasesByDimensionCall.HistoryGrowth - accumulatedDimensionsCall.StateGrowthRefund += gasesByDimensionCall.StateGrowthRefund - + accumulatedDimensionsCall.OneDimensionalGasCost += finishGasesByDimension.OneDimensionalGasCost + accumulatedDimensionsCall.Computation += finishGasesByDimension.Computation + accumulatedDimensionsCall.StateAccess += finishGasesByDimension.StateAccess + accumulatedDimensionsCall.StateGrowth += finishGasesByDimension.StateGrowth + accumulatedDimensionsCall.HistoryGrowth += finishGasesByDimension.HistoryGrowth + accumulatedDimensionsCall.StateGrowthRefund += finishGasesByDimension.StateGrowthRefund t.OpcodeToDimensions[stackInfo.GasDimensionInfo.Op] = accumulatedDimensionsCall - t.Depth -= 1 - } - // if we are in a call stack depth greater than 0, - // then we need to track the execution gas - // of our own code so that when the call returns, - // we can write the gas dimensions for the call opcode itself - if len(t.callStack) > 0 { - t.callStack.UpdateExecutionCost(cost) + t.depth -= 1 } + t.updateExecutionCost(cost) } } -func (t *TxGasDimensionByOpcodeTracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) { - t.env = env -} - -func (t *TxGasDimensionByOpcodeTracer) OnTxEnd(receipt *types.Receipt, err error) { - if err != nil { - // Don't override vm error - if t.err == nil { - t.err = err - } - return - } - t.usedGas = receipt.GasUsed - t.txHash = receipt.TxHash -} - -// signal the tracer to stop tracing, e.g. on timeout -func (t *TxGasDimensionByOpcodeTracer) Stop(err error) { - t.reason = err - t.interrupt.Store(true) -} - -// ############################################################################ -// HELPERS -// ############################################################################ - -func (t *TxGasDimensionByOpcodeTracer) GetOpRefund() uint64 { - return t.env.StateDB.GetRefund() -} - -func (t *TxGasDimensionByOpcodeTracer) GetRefundAccumulated() uint64 { - return t.RefundAccumulated -} - -func (t *TxGasDimensionByOpcodeTracer) SetRefundAccumulated(refund uint64) { - t.RefundAccumulated = refund -} - // ############################################################################ // JSON OUTPUT PRODUCTION // ############################################################################ @@ -249,48 +142,39 @@ func (t *TxGasDimensionByOpcodeTracer) Error() error { return t.err } // while replaying a transaction in debug mode as well as transaction // execution status, the amount of gas used and the return value type TxGasDimensionByOpcodeExecutionResult struct { - Gas uint64 `json:"gas"` - Failed bool `json:"fail"` - Dimensions map[string]GasesByDimension `json:"dim"` - TxHash string `json:"hash"` - BlockTimetamp uint64 `json:"btime"` - BlockNumber *big.Int `json:"num"` + BaseExecutionResult + Dimensions map[string]GasesByDimension `json:"dim"` } // produce json result for output from tracer // this is what the end-user actually gets from the RPC endpoint func (t *TxGasDimensionByOpcodeTracer) GetResult() (json.RawMessage, error) { - // Tracing aborted - if t.reason != nil { - return nil, t.reason + baseExecutionResult, err := t.GetBaseExecutionResult() + if err != nil { + return nil, err } - failed := t.err != nil return json.Marshal(&TxGasDimensionByOpcodeExecutionResult{ - Gas: t.usedGas, - Failed: failed, - Dimensions: t.GetOpcodeDimensionSummary(), - TxHash: t.txHash.Hex(), - BlockTimetamp: t.env.Time, - BlockNumber: t.env.BlockNumber, + BaseExecutionResult: baseExecutionResult, + Dimensions: t.GetOpcodeDimensionSummary(), }) } // produce protobuf serialized result // for storing to file in compact format func (t *TxGasDimensionByOpcodeTracer) GetProtobufResult() ([]byte, error) { - if t.reason != nil { - return nil, t.reason + baseExecutionResult, err := t.GetBaseExecutionResult() + if err != nil { + return nil, err } - failed := t.err != nil executionResult := &proto.TxGasDimensionByOpcodeExecutionResult{ - Gas: t.usedGas, - Failed: failed, + Gas: baseExecutionResult.Gas, + Failed: baseExecutionResult.Failed, Dimensions: make(map[uint32]*proto.GasesByDimension), - TxHash: t.txHash.Hex(), - BlockTimestamp: t.env.Time, - BlockNumber: t.env.BlockNumber.String(), + TxHash: baseExecutionResult.TxHash, + BlockTimestamp: baseExecutionResult.BlockTimestamp, + BlockNumber: baseExecutionResult.BlockNumber.String(), } for opcode, dimensions := range t.OpcodeToDimensions { diff --git a/eth/tracers/native/tx_gas_dimension_logger.go b/eth/tracers/native/tx_gas_dimension_logger.go index ef3df8b0a1..08a998ab9d 100644 --- a/eth/tracers/native/tx_gas_dimension_logger.go +++ b/eth/tracers/native/tx_gas_dimension_logger.go @@ -2,13 +2,8 @@ package native import ( "encoding/json" - "fmt" - "math/big" - "sync/atomic" - "github.com/ethereum/go-ethereum/common" "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/ethereum/go-ethereum/params" @@ -47,33 +42,24 @@ func (d *DimensionLog) ErrorString() string { return "" } -// gasDimensionTracer struct +// TxGasDimensionLogger struct type TxGasDimensionLogger struct { - env *tracing.VMContext - txHash common.Hash - logs []DimensionLog - err error - usedGas uint64 - callStack CallGasDimensionStack - depth int - refundAccumulated uint64 - - interrupt atomic.Bool // Atomic flag to signal execution interruption - reason error // Textual reason for the interruption + BaseGasDimensionTracer + logs []DimensionLog } // gasDimensionTracer returns a new tracer that traces gas // usage for each opcode against the dimension of that opcode // takes a context, and json input for configuration parameters func NewTxGasDimensionLogger( - ctx *tracers.Context, + _ *tracers.Context, _ json.RawMessage, _ *params.ChainConfig, ) (*tracers.Tracer, error) { t := &TxGasDimensionLogger{ - depth: 1, - refundAccumulated: 0, + BaseGasDimensionTracer: NewBaseGasDimensionTracer(), + logs: make([]DimensionLog, 0), } return &tracers.Tracer{ @@ -101,50 +87,10 @@ func (t *TxGasDimensionLogger) OnOpcode( depth int, err error, ) { - if t.interrupt.Load() { - return - } - if depth != t.depth && depth != t.depth-1 { - t.interrupt.Store(true) - t.reason = fmt.Errorf( - "expected depth fell out of sync with actual depth: %d %d != %d, callStack: %v", - pc, - t.depth, - depth, - t.callStack, - ) - return - } - if t.depth != len(t.callStack)+1 { - t.interrupt.Store(true) - t.reason = fmt.Errorf( - "depth fell out of sync with callStack: %d %d != %d, callStack: %v", - pc, - t.depth, - len(t.callStack), - t.callStack, - ) - } - - // get the gas dimension function - // if it's not a call, directly calculate the gas dimensions for the opcode - f := GetCalcGasDimensionFunc(vm.OpCode(op)) - gasesByDimension, callStackInfo, fErr := f(t, pc, op, gas, cost, scope, rData, depth, err) - if fErr != nil { - t.interrupt.Store(true) - t.reason = fErr - return - } - opcode := vm.OpCode(op) - - if WasCallOrCreate(opcode) && callStackInfo == nil && err == nil || !WasCallOrCreate(opcode) && callStackInfo != nil { - t.interrupt.Store(true) - t.reason = fmt.Errorf( - "logic bug, calls/creates should always be accompanied by callStackInfo and non-calls should not have callStackInfo %d %s %v", - pc, - opcode.String(), - callStackInfo, - ) + interrupted, gasesByDimension, callStackInfo, opcode := t.onOpcodeStart(pc, op, gas, cost, scope, rData, depth, err) + // if an error occured, it was stored in the tracer's reason field + // and we should return immediately + if interrupted { return } @@ -165,43 +111,13 @@ func (t *TxGasDimensionLogger) OnOpcode( // DimensionLog that represents this opcode and save the callStackInfo // to call finishX after the call has returned if WasCallOrCreate(opcode) && err == nil { - opcodeLogIndex := len(t.logs) - 1 // minus 1 because we've already appended the log - t.callStack.Push( - CallGasDimensionStackInfo{ - GasDimensionInfo: *callStackInfo, - DimensionLogPosition: opcodeLogIndex, - ExecutionCost: 0, - }) - t.depth += 1 + t.handleCallStackPush(callStackInfo) } else { - // if the opcode returns from the call stack depth, or - // if this is an opcode immediately after a call that did not increase the stack depth - // because it called an empty account or contract or wrong function signature, - // call the appropriate finishX function to write the gas dimensions - // for the call that increased the stack depth in the past if depth < t.depth { - stackInfo, ok := t.callStack.Pop() - // base case, stack is empty, do nothing - if !ok { - t.interrupt.Store(true) - t.reason = fmt.Errorf("call stack is unexpectedly empty %d %d %d", pc, depth, t.depth) - return - } - finishFunction := GetFinishCalcGasDimensionFunc(stackInfo.GasDimensionInfo.Op) - if finishFunction == nil { - t.interrupt.Store(true) - t.reason = fmt.Errorf( - "no finish function found for opcode %s, call stack is messed up %d", - stackInfo.GasDimensionInfo.Op.String(), - pc, - ) + interrupted, gasUsedByCall, stackInfo, finishGasesByDimension := t.callFinishFunction(pc, depth, gas) + if interrupted { return } - // IMPORTANT NOTE: for some reason the only reliable way to actually get the gas cost of the call - // is to subtract gas at time of call from gas at opcode AFTER return - // you can't trust the `gas` field on the call itself. I wonder if the gas field is an estimation - gasUsedByCall := stackInfo.GasDimensionInfo.GasCounterAtTimeOfCall - gas - finishGasesByDimension := finishFunction(gasUsedByCall, stackInfo.ExecutionCost, stackInfo.GasDimensionInfo) callDimensionLog := t.logs[stackInfo.DimensionLogPosition] callDimensionLog.OneDimensionalGasCost = finishGasesByDimension.OneDimensionalGasCost callDimensionLog.Computation = finishGasesByDimension.Computation @@ -215,60 +131,23 @@ func (t *TxGasDimensionLogger) OnOpcode( callDimensionLog.CreateInitCodeCost = stackInfo.GasDimensionInfo.InitCodeCost callDimensionLog.Create2HashCost = stackInfo.GasDimensionInfo.HashCost t.logs[stackInfo.DimensionLogPosition] = callDimensionLog - t.depth -= 1 - } - // if we are in a call stack depth greater than 0, - // then we need to track the execution gas - // of our own code so that when the call returns, - // we can write the gas dimensions for the call opcode itself - if len(t.callStack) > 0 { - t.callStack.UpdateExecutionCost(gasesByDimension.OneDimensionalGasCost) + t.depth -= 1 } - } -} - -func (t *TxGasDimensionLogger) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) { - t.env = env -} -func (t *TxGasDimensionLogger) OnTxEnd(receipt *types.Receipt, err error) { - if err != nil { - // Don't override vm error - if t.err == nil { - t.err = err - } - return + t.updateExecutionCost(cost) } - t.usedGas = receipt.GasUsed - t.txHash = receipt.TxHash -} - -// signal the tracer to stop tracing, e.g. on timeout -func (t *TxGasDimensionLogger) Stop(err error) { - t.reason = err - t.interrupt.Store(true) -} - -// ############################################################################ -// HELPERS -// ############################################################################ - -// returns true if the opcode is a type of opcode that makes calls increasing the stack depth -func WasCallOrCreate(opcode vm.OpCode) bool { - return opcode == vm.CALL || opcode == vm.CALLCODE || opcode == vm.DELEGATECALL || opcode == vm.STATICCALL || opcode == vm.CREATE || opcode == vm.CREATE2 } -func (t *TxGasDimensionLogger) GetOpRefund() uint64 { - return t.env.StateDB.GetRefund() -} - -func (t *TxGasDimensionLogger) GetRefundAccumulated() uint64 { - return t.refundAccumulated -} - -func (t *TxGasDimensionLogger) SetRefundAccumulated(refund uint64) { - t.refundAccumulated = refund +func (t *TxGasDimensionLogger) handleCallStackPush(callStackInfo *CallGasDimensionInfo) { + opcodeLogIndex := len(t.logs) - 1 // minus 1 because we've already appended the log + t.callStack.Push( + CallGasDimensionStackInfo{ + GasDimensionInfo: *callStackInfo, + DimensionLogPosition: opcodeLogIndex, + ExecutionCost: 0, + }) + t.depth += 1 } // ############################################################################ @@ -278,37 +157,24 @@ func (t *TxGasDimensionLogger) SetRefundAccumulated(refund uint64) { // DimensionLogs returns the captured log entries. func (t *TxGasDimensionLogger) DimensionLogs() []DimensionLog { return t.logs } -// Error returns the VM error captured by the trace. -func (t *TxGasDimensionLogger) Error() error { return t.err } - // ExecutionResult groups all dimension logs emitted by the EVM // while replaying a transaction in debug mode as well as transaction // execution status, the amount of gas used and the return value type ExecutionResult struct { - Gas uint64 `json:"gas"` - Failed bool `json:"fail"` + BaseExecutionResult DimensionLogs []DimensionLogRes `json:"dim"` - TxHash string `json:"hash"` - BlockTimetamp uint64 `json:"time"` - BlockNumber *big.Int `json:"num"` } // produce json result for output from tracer // this is what the end-user actually gets from the RPC endpoint func (t *TxGasDimensionLogger) GetResult() (json.RawMessage, error) { - // Tracing aborted - if t.reason != nil { - return nil, t.reason + baseResult, err := t.GetBaseExecutionResult() + if err != nil { + return nil, err } - failed := t.err != nil - return json.Marshal(&ExecutionResult{ - Gas: t.usedGas, - Failed: failed, - DimensionLogs: formatLogs(t.DimensionLogs()), - TxHash: t.txHash.Hex(), - BlockTimetamp: t.env.Time, - BlockNumber: t.env.BlockNumber, + BaseExecutionResult: baseResult, + DimensionLogs: formatLogs(t.DimensionLogs()), }) } From 6ff9569c6876b94af857872933b7924b2c98f4bb Mon Sep 17 00:00:00 2001 From: relyt29 Date: Mon, 21 Apr 2025 16:23:23 -0400 Subject: [PATCH 25/35] gas dimensions - small tweaks while debugging --- .../native/base_gas_dimension_tracer.go | 8 +++---- eth/tracers/native/gas_dimension_calc.go | 8 +++---- .../native/tx_gas_dimension_by_opcode.go | 4 ++-- eth/tracers/native/tx_gas_dimension_logger.go | 22 ++++++++++--------- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/eth/tracers/native/base_gas_dimension_tracer.go b/eth/tracers/native/base_gas_dimension_tracer.go index e7f4c7a250..8634d69b9d 100644 --- a/eth/tracers/native/base_gas_dimension_tracer.go +++ b/eth/tracers/native/base_gas_dimension_tracer.go @@ -254,10 +254,10 @@ func (t *BaseGasDimensionTracer) Error() error { return t.err } // BaseExecutionResult has shared fields for execution results type BaseExecutionResult struct { Gas uint64 `json:"gas"` - Failed bool `json:"fail"` - TxHash string `json:"hash"` - BlockTimestamp uint64 `json:"time"` - BlockNumber *big.Int `json:"num"` + Failed bool `json:"failed"` + TxHash string `json:"txHash"` + BlockTimestamp uint64 `json:"blockTimestamp"` + BlockNumber *big.Int `json:"blockNumber"` } // get the result of the transaction execution that we will hand to the json output diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index 77475dd63b..2247e23fb2 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -65,12 +65,13 @@ func (c *CallGasDimensionStack) Pop() (CallGasDimensionStackInfo, bool) { // UpdateExecutionCost updates the execution cost for the top layer of the call stack // so that the call knows how much gas was consumed by child opcodes in that call depth func (c *CallGasDimensionStack) UpdateExecutionCost(executionCost uint64) { - if len(*c) == 0 { + stackLen := len(*c) + if stackLen == 0 { return } - top := (*c)[len(*c)-1] + top := (*c)[stackLen-1] top.ExecutionCost += executionCost - (*c)[len(*c)-1] = top + (*c)[stackLen-1] = top } // define interface for a dimension tracer @@ -738,7 +739,6 @@ func calcReadAndStoreCallGas( InitCodeCost: 0, HashCost: 0, }, nil - } // In order to calculate the gas dimensions for opcodes that diff --git a/eth/tracers/native/tx_gas_dimension_by_opcode.go b/eth/tracers/native/tx_gas_dimension_by_opcode.go index b2ddccb001..92ae205fb7 100644 --- a/eth/tracers/native/tx_gas_dimension_by_opcode.go +++ b/eth/tracers/native/tx_gas_dimension_by_opcode.go @@ -127,7 +127,7 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( t.depth -= 1 } - t.updateExecutionCost(cost) + t.updateExecutionCost(gasesByDimension.OneDimensionalGasCost) } } @@ -143,7 +143,7 @@ func (t *TxGasDimensionByOpcodeTracer) Error() error { return t.err } // execution status, the amount of gas used and the return value type TxGasDimensionByOpcodeExecutionResult struct { BaseExecutionResult - Dimensions map[string]GasesByDimension `json:"dim"` + Dimensions map[string]GasesByDimension `json:"dimensions"` } // produce json result for output from tracer diff --git a/eth/tracers/native/tx_gas_dimension_logger.go b/eth/tracers/native/tx_gas_dimension_logger.go index 08a998ab9d..347cad625b 100644 --- a/eth/tracers/native/tx_gas_dimension_logger.go +++ b/eth/tracers/native/tx_gas_dimension_logger.go @@ -135,10 +135,12 @@ func (t *TxGasDimensionLogger) OnOpcode( t.depth -= 1 } - t.updateExecutionCost(cost) + t.updateExecutionCost(gasesByDimension.OneDimensionalGasCost) } } +// save the relevant information for the call stack when a call or create +// is made that increases the stack depth func (t *TxGasDimensionLogger) handleCallStackPush(callStackInfo *CallGasDimensionInfo) { opcodeLogIndex := len(t.logs) - 1 // minus 1 because we've already appended the log t.callStack.Push( @@ -184,18 +186,18 @@ func (t *TxGasDimensionLogger) GetResult() (json.RawMessage, error) { type DimensionLogRes struct { Pc uint64 `json:"pc"` Op string `json:"op"` - Depth int `json:"d"` + Depth int `json:"depth"` OneDimensionalGasCost uint64 `json:"cost"` Computation uint64 `json:"cpu,omitempty"` StateAccess uint64 `json:"rw,omitempty"` - StateGrowth uint64 `json:"g,omitempty"` - HistoryGrowth uint64 `json:"h,omitempty"` - StateGrowthRefund int64 `json:"rf,omitempty"` - CallRealGas uint64 `json:"crg,omitempty"` - CallExecutionCost uint64 `json:"cec,omitempty"` - CallMemoryExpansion uint64 `json:"cme,omitempty"` - CreateInitCodeCost uint64 `json:"cic,omitempty"` - Create2HashCost uint64 `json:"c2h,omitempty"` + StateGrowth uint64 `json:"growth,omitempty"` + HistoryGrowth uint64 `json:"history,omitempty"` + StateGrowthRefund int64 `json:"refund,omitempty"` + CallRealGas uint64 `json:"callRealGas,omitempty"` + CallExecutionCost uint64 `json:"callExecutionCost,omitempty"` + CallMemoryExpansion uint64 `json:"callMemoryExpansion,omitempty"` + CreateInitCodeCost uint64 `json:"createInitCodeCost,omitempty"` + Create2HashCost uint64 `json:"create2HashCost,omitempty"` Err error `json:"err,omitempty"` } From 427601c4ce791d072738c9fb1cfc766fdfc9786b Mon Sep 17 00:00:00 2001 From: relyt29 Date: Tue, 22 Apr 2025 13:22:31 -0400 Subject: [PATCH 26/35] Directly expose accessList to tracers. Test it works with BALANCE opcode --- core/state/statedb.go | 9 ++++ core/state/statedb_hooked.go | 4 ++ core/tracing/hooks.go | 1 + core/vm/interface.go | 2 + .../native/base_gas_dimension_tracer.go | 28 ++++++++-- eth/tracers/native/gas_dimension_calc.go | 52 +++++++++++-------- .../native/tx_gas_dimension_by_opcode.go | 2 + eth/tracers/native/tx_gas_dimension_logger.go | 2 + 8 files changed, 75 insertions(+), 25 deletions(-) diff --git a/core/state/statedb.go b/core/state/statedb.go index 915cd8669e..17add36598 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -1548,6 +1548,15 @@ func (s *StateDB) SlotInAccessList(addr common.Address, slot common.Hash) (addre return s.accessList.Contains(addr, slot) } +// GetAccessList returns the data of the access list directly for tracers to consume +// this is necessary because the accessList is not exported from the state package +func (s *StateDB) GetAccessList() (addresses map[common.Address]int, slots []map[common.Hash]struct{}) { + accessListCopy := s.accessList.Copy() + addresses = accessListCopy.addresses + slots = accessListCopy.slots + return +} + // markDelete is invoked when an account is deleted but the deletion is // not yet committed. The pending mutation is cached and will be applied // all together diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index 83eede0398..ab652fc6fc 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -369,3 +369,7 @@ func (s *hookedStateDB) GetSelfDestructs() []common.Address { func (s *hookedStateDB) GetCurrentTxLogs() []*types.Log { return s.inner.GetCurrentTxLogs() } + +func (s *hookedStateDB) GetAccessList() (addresses map[common.Address]int, slots []map[common.Hash]struct{}) { + return s.inner.GetAccessList() +} diff --git a/core/tracing/hooks.go b/core/tracing/hooks.go index 1ab20da521..70dee8fea2 100644 --- a/core/tracing/hooks.go +++ b/core/tracing/hooks.go @@ -55,6 +55,7 @@ type StateDB interface { GetTransientState(common.Address, common.Hash) common.Hash Exist(common.Address) bool GetRefund() uint64 + GetAccessList() (addresses map[common.Address]int, slots []map[common.Hash]struct{}) } // VMContext provides the context for the EVM execution. diff --git a/core/vm/interface.go b/core/vm/interface.go index 173ccb146c..769602a5b8 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -115,6 +115,8 @@ type StateDB interface { // AddSlotToAccessList adds the given (address,slot) to the access list. This operation is safe to perform // even if the feature/fork is not active yet AddSlotToAccessList(addr common.Address, slot common.Hash) + // GetAccessList returns the access list + GetAccessList() (addresses map[common.Address]int, slots []map[common.Hash]struct{}) // PointCache returns the point cache used in computations PointCache() *utils.PointCache diff --git a/eth/tracers/native/base_gas_dimension_tracer.go b/eth/tracers/native/base_gas_dimension_tracer.go index 8634d69b9d..16c94b90ac 100644 --- a/eth/tracers/native/base_gas_dimension_tracer.go +++ b/eth/tracers/native/base_gas_dimension_tracer.go @@ -23,6 +23,9 @@ type BaseGasDimensionTracer struct { callStack CallGasDimensionStack // the depth at the current step of execution of the call stack depth int + // maintain an access list tracer to check previous access list statuses. + prevAccessListAddresses map[common.Address]int + prevAccessListSlots []map[common.Hash]struct{} // the amount of refund accumulated at the current step of execution refundAccumulated uint64 // whether the transaction had an error, like out of gas @@ -35,8 +38,10 @@ type BaseGasDimensionTracer struct { func NewBaseGasDimensionTracer() BaseGasDimensionTracer { return BaseGasDimensionTracer{ - depth: 1, - refundAccumulated: 0, + depth: 1, + refundAccumulated: 0, + prevAccessListAddresses: map[common.Address]int{}, + prevAccessListSlots: []map[common.Hash]struct{}{}, } } @@ -190,8 +195,20 @@ func (t *BaseGasDimensionTracer) updateExecutionCost(cost uint64) { } } +// at the end of each OnOpcode, update the previous access list, so that we can +// use it should the next opcode need to check if an address is in the access list +func (t *BaseGasDimensionTracer) updatePrevAccessList(addresses map[common.Address]int, slots []map[common.Hash]struct{}) { + if addresses == nil { + t.prevAccessListAddresses = map[common.Address]int{} + t.prevAccessListSlots = []map[common.Hash]struct{}{} + return + } + t.prevAccessListAddresses = addresses + t.prevAccessListSlots = slots +} + // OnTxStart handles transaction start -func (t *BaseGasDimensionTracer) OnTxStart(env *tracing.VMContext, _ *types.Transaction, _ common.Address) { +func (t *BaseGasDimensionTracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) { t.env = env } @@ -244,6 +261,11 @@ func (t *BaseGasDimensionTracer) GetStateDB() tracing.StateDB { return t.env.StateDB } +// GetPrevAccessList returns the previous access list +func (t *BaseGasDimensionTracer) GetPrevAccessList() (addresses map[common.Address]int, slots []map[common.Hash]struct{}) { + return t.prevAccessListAddresses, t.prevAccessListSlots +} + // Error returns the VM error captured by the trace func (t *BaseGasDimensionTracer) Error() error { return t.err } diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index 2247e23fb2..f2017f8f6f 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -1,8 +1,10 @@ package native import ( + "fmt" "math" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/params" @@ -81,6 +83,7 @@ type DimensionTracer interface { GetOpRefund() uint64 GetRefundAccumulated() uint64 SetRefundAccumulated(uint64) + GetPrevAccessList() (addresses map[common.Address]int, slots []map[common.Hash]struct{}) } // calcGasDimensionFunc defines a type signature that takes the opcode @@ -224,17 +227,6 @@ func calcSimpleAddressAccessSetGas( depth int, err error, ) (GasesByDimension, *CallGasDimensionInfo, error) { - // We do not have access to StateDb.AddressInAccessList and StateDb.SlotInAccessList - // to check cold storage access directly. - // Additionally, cold storage access for these address opcodes are handled differently - // than for other operations like SSTORE or SLOAD. - // for these opcodes, cold access cost is handled directly in the gas calculation - // through gasEip2929AccountCheck. This function adds the address to the access list - // and charges the cold access cost upfront as part of the initial gas calculation, - // rather than as a separate gas change event, so no OnGasChange event is fired. - // - // Therefore, for these opcodes, we do a simple check based on the raw value - // and we can deduce the dimensions directly from that value. if err != nil { return GasesByDimension{ OneDimensionalGasCost: gas, @@ -245,20 +237,36 @@ func calcSimpleAddressAccessSetGas( StateGrowthRefund: 0, }, nil, nil } - if cost == params.ColdAccountAccessCostEIP2929 { - return GasesByDimension{ - OneDimensionalGasCost: cost, - Computation: params.WarmStorageReadCostEIP2929, - StateAccess: params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, - }, nil, nil + // for all these opcodes the address being checked is in stack position 0 + addressAsInt := scope.StackData()[len(scope.StackData())-1] + address := common.Address(addressAsInt.Bytes20()) + accessListAddresses, _ := t.GetPrevAccessList() + _, addressInAccessList := accessListAddresses[address] + var computationCost uint64 = 0 + var stateAccessCost uint64 = 0 + if !addressInAccessList { + computationCost = params.WarmStorageReadCostEIP2929 + stateAccessCost = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 + } else { + computationCost = params.WarmStorageReadCostEIP2929 + stateAccessCost = 0 + } + if cost != computationCost+stateAccessCost { + return GasesByDimension{}, nil, fmt.Errorf( + "unexpected gas cost mismatch: pc %d, op %d, depth %d, gas %d, cost %d != %d + %d", + pc, + op, + depth, + gas, + cost, + computationCost, + stateAccessCost, + ) } return GasesByDimension{ OneDimensionalGasCost: cost, - Computation: cost, - StateAccess: 0, + Computation: computationCost, + StateAccess: stateAccessCost, StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, diff --git a/eth/tracers/native/tx_gas_dimension_by_opcode.go b/eth/tracers/native/tx_gas_dimension_by_opcode.go index 92ae205fb7..f231519a00 100644 --- a/eth/tracers/native/tx_gas_dimension_by_opcode.go +++ b/eth/tracers/native/tx_gas_dimension_by_opcode.go @@ -129,6 +129,8 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( } t.updateExecutionCost(gasesByDimension.OneDimensionalGasCost) } + addresses, slots := t.env.StateDB.GetAccessList() + t.updatePrevAccessList(addresses, slots) } // ############################################################################ diff --git a/eth/tracers/native/tx_gas_dimension_logger.go b/eth/tracers/native/tx_gas_dimension_logger.go index 347cad625b..be36bb774c 100644 --- a/eth/tracers/native/tx_gas_dimension_logger.go +++ b/eth/tracers/native/tx_gas_dimension_logger.go @@ -137,6 +137,8 @@ func (t *TxGasDimensionLogger) OnOpcode( t.updateExecutionCost(gasesByDimension.OneDimensionalGasCost) } + addresses, slots := t.env.StateDB.GetAccessList() + t.updatePrevAccessList(addresses, slots) } // save the relevant information for the call stack when a call or create From 4bf0635a38d0785fef45e1b760688c935d858d28 Mon Sep 17 00:00:00 2001 From: relyt29 Date: Tue, 22 Apr 2025 20:00:00 -0400 Subject: [PATCH 27/35] gas dimension tracers directly calc access list gas for: CALL, DELEGATECALL, EXTCODECOPY, etc. Tested by hand. --- .../native/base_gas_dimension_tracer.go | 8 +- eth/tracers/native/gas_dimension_calc.go | 437 ++++++++++-------- .../native/tx_gas_dimension_by_opcode.go | 24 +- 3 files changed, 242 insertions(+), 227 deletions(-) diff --git a/eth/tracers/native/base_gas_dimension_tracer.go b/eth/tracers/native/base_gas_dimension_tracer.go index 16c94b90ac..b71bf28f45 100644 --- a/eth/tracers/native/base_gas_dimension_tracer.go +++ b/eth/tracers/native/base_gas_dimension_tracer.go @@ -181,7 +181,13 @@ func (t *BaseGasDimensionTracer) callFinishFunction( // is to subtract gas at time of call from gas at opcode AFTER return // you can't trust the `gas` field on the call itself. I wonder if the gas field is an estimation gasUsedByCall = stackInfo.GasDimensionInfo.GasCounterAtTimeOfCall - gas - finishGasesByDimension = finishFunction(gasUsedByCall, stackInfo.ExecutionCost, stackInfo.GasDimensionInfo) + var finishErr error = nil + finishGasesByDimension, finishErr = finishFunction(gasUsedByCall, stackInfo.ExecutionCost, stackInfo.GasDimensionInfo) + if finishErr != nil { + t.interrupt.Store(true) + t.reason = finishErr + return true, 0, CallGasDimensionStackInfo{}, GasesByDimension{} + } return false, gasUsedByCall, stackInfo, finishGasesByDimension } diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index f2017f8f6f..775806d770 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -27,12 +27,15 @@ type GasesByDimension struct { // CallGasDimensionInfo retains the relevant information that needs to be remembered // from the start of the call to compute the gas dimensions after the call has returned. type CallGasDimensionInfo struct { - Op vm.OpCode - GasCounterAtTimeOfCall uint64 - MemoryExpansionCost uint64 - IsValueSentWithCall bool - InitCodeCost uint64 - HashCost uint64 + Pc uint64 + Op vm.OpCode + GasCounterAtTimeOfCall uint64 + MemoryExpansionCost uint64 + AccessListComputationCost uint64 + AccessListStateAccessCost uint64 + IsValueSentWithCall bool + InitCodeCost uint64 + HashCost uint64 } // CallGasDimensionStackInfo is a struct that contains the gas dimension info @@ -114,7 +117,7 @@ type FinishCalcGasDimensionFunc func( totalGasUsed uint64, codeExecutionCost uint64, callGasDimensionInfo CallGasDimensionInfo, -) GasesByDimension +) (GasesByDimension, error) // getCalcGasDimensionFunc is a massive case switch // statement that returns which function to call @@ -190,14 +193,7 @@ func calcSimpleSingleDimensionGas( err error, ) (GasesByDimension, *CallGasDimensionInfo, error) { if err != nil { - return GasesByDimension{ - OneDimensionalGasCost: gas, - Computation: gas, - StateAccess: 0, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, - }, nil, nil + return outOfGas(gas) } return GasesByDimension{ OneDimensionalGasCost: cost, @@ -228,49 +224,24 @@ func calcSimpleAddressAccessSetGas( err error, ) (GasesByDimension, *CallGasDimensionInfo, error) { if err != nil { - return GasesByDimension{ - OneDimensionalGasCost: gas, - Computation: gas, - StateAccess: 0, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, - }, nil, nil + return outOfGas(gas) } // for all these opcodes the address being checked is in stack position 0 addressAsInt := scope.StackData()[len(scope.StackData())-1] address := common.Address(addressAsInt.Bytes20()) - accessListAddresses, _ := t.GetPrevAccessList() - _, addressInAccessList := accessListAddresses[address] - var computationCost uint64 = 0 - var stateAccessCost uint64 = 0 - if !addressInAccessList { - computationCost = params.WarmStorageReadCostEIP2929 - stateAccessCost = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 - } else { - computationCost = params.WarmStorageReadCostEIP2929 - stateAccessCost = 0 - } - if cost != computationCost+stateAccessCost { - return GasesByDimension{}, nil, fmt.Errorf( - "unexpected gas cost mismatch: pc %d, op %d, depth %d, gas %d, cost %d != %d + %d", - pc, - op, - depth, - gas, - cost, - computationCost, - stateAccessCost, - ) - } - return GasesByDimension{ + computationCost, stateAccessCost := addressAccessListCost(t, address) + ret := GasesByDimension{ OneDimensionalGasCost: cost, Computation: computationCost, StateAccess: stateAccessCost, StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, - }, nil, nil + } + if err := checkGasDimensionsEqualOneDimensionalGas(pc, op, depth, gas, cost, ret); err != nil { + return GasesByDimension{}, nil, err + } + return ret, nil, nil } // calcSLOADGas returns the gas used for the `SLOAD` opcode @@ -285,39 +256,25 @@ func calcSLOADGas( depth int, err error, ) (GasesByDimension, *CallGasDimensionInfo, error) { - // we don't have access to StateDb.SlotInAccessList - // so we have to infer whether the slot was cold or warm based on the absolute cost - // and then deduce the dimensions from that if err != nil { - return GasesByDimension{ - OneDimensionalGasCost: gas, - Computation: gas, - StateAccess: 0, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, - }, nil, nil + return outOfGas(gas) } - if cost == params.ColdSloadCostEIP2929 { - accessCost := params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929 - leftOver := cost - accessCost - return GasesByDimension{ - OneDimensionalGasCost: cost, - Computation: leftOver, - StateAccess: accessCost, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, - }, nil, nil - } - return GasesByDimension{ + stackData := scope.StackData() + stackLen := len(stackData) + slot := common.Hash(stackData[stackLen-1].Bytes32()) + computationCost, stateAccessCost := storageSlotAccessListCost(t, scope.Address(), slot) + ret := GasesByDimension{ OneDimensionalGasCost: cost, - Computation: cost, - StateAccess: 0, + Computation: computationCost, + StateAccess: stateAccessCost, StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, - }, nil, nil + } + if err := checkGasDimensionsEqualOneDimensionalGas(pc, op, depth, gas, cost, ret); err != nil { + return GasesByDimension{}, nil, err + } + return ret, nil, nil } // calcExtCodeCopyGas returns the gas used @@ -339,49 +296,39 @@ func calcExtCodeCopyGas( // 2. memory_expansion_cost // 3. address_access_cost - the access set. // gas for extcodecopy is 3 * minimum_word_size + memory_expansion_cost + address_access_cost - // - // at time of opcode trace, we know the state of the memory, and stack - // and we know the total cost of the opcode. - // therefore, we calculate minimum_word_size, and memory_expansion_cost - // and observe if the subtraction of cost - memory_expansion_cost - minimum_word_size = 100 or 2600 // 3*minimum_word_size is always state access - // if it is 2600, then have 2500 for state access. + // if state access is 2600, then have 2500 for state access // rest is computation. if err != nil { - return GasesByDimension{ - OneDimensionalGasCost: gas, - Computation: gas, - StateAccess: 0, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, - }, nil, nil + return outOfGas(gas) } stack := scope.StackData() lenStack := len(stack) - size := stack[lenStack-4].Uint64() // size in stack position 4 - offset := stack[lenStack-2].Uint64() // destination offset in stack position 2 + size := stack[lenStack-4].Uint64() // size in stack position 4 + offset := stack[lenStack-2].Uint64() // destination offset in stack position 2 + address := stack[lenStack-1].Bytes20() // address in stack position 1 + + accessListComputationCost, accessListStateAccessCost := addressAccessListCost(t, common.Address(address)) memoryExpansionCost, memErr := memoryExpansionCost(scope.MemoryData(), offset, size) if memErr != nil { return GasesByDimension{}, nil, memErr } - minimumWordSizeCost := (size + 31) / 32 * 3 - leftOver := cost - memoryExpansionCost - minimumWordSizeCost - stateAccess := minimumWordSizeCost - // check if the access set was hot or cold - if leftOver == params.ColdAccountAccessCostEIP2929 { - stateAccess += params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 - } - computation := cost - stateAccess - return GasesByDimension{ + minimumWordSizeCost := 3 * ((size + 31) / 32) + stateAccess := minimumWordSizeCost + accessListStateAccessCost + computation := memoryExpansionCost + accessListComputationCost + ret := GasesByDimension{ OneDimensionalGasCost: cost, Computation: computation, StateAccess: stateAccess, StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, - }, nil, nil + } + if err := checkGasDimensionsEqualOneDimensionalGas(pc, op, depth, gas, cost, ret); err != nil { + return GasesByDimension{}, nil, err + } + return ret, nil, nil } // calcStateReadCallGas returns the gas used @@ -404,14 +351,7 @@ func calcStateReadCallGas( err error, ) (GasesByDimension, *CallGasDimensionInfo, error) { if err != nil { - return GasesByDimension{ - OneDimensionalGasCost: gas, - Computation: gas, - StateAccess: 0, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, - }, nil, nil + return outOfGas(gas) } stack := scope.StackData() lenStack := len(stack) @@ -419,6 +359,8 @@ func calcStateReadCallGas( // argsSize in stack position 4 argsOffset := stack[lenStack-3].Uint64() argsSize := stack[lenStack-4].Uint64() + // address in stack position 2 + address := stack[lenStack-2].Bytes20() // Note that opcodes with a byte size parameter of 0 will not trigger memory expansion, regardless of their offset parameters if argsSize == 0 { argsOffset = 0 @@ -450,9 +392,11 @@ func calcStateReadCallGas( } } + accessListComputationCost, accessListStateAccessCost := addressAccessListCost(t, address) + // at a minimum, the cost is 100 for the warm access set // and the memory expansion cost - computation := memExpansionCost + params.WarmStorageReadCostEIP2929 + computation := memExpansionCost + accessListComputationCost // see finishCalcStateReadCallGas for more details return GasesByDimension{ OneDimensionalGasCost: cost, @@ -462,12 +406,15 @@ func calcStateReadCallGas( HistoryGrowth: 0, StateGrowthRefund: 0, }, &CallGasDimensionInfo{ - Op: vm.OpCode(op), - GasCounterAtTimeOfCall: gas, - MemoryExpansionCost: memExpansionCost, - IsValueSentWithCall: false, - InitCodeCost: 0, - HashCost: 0, + Pc: pc, + Op: vm.OpCode(op), + GasCounterAtTimeOfCall: gas, + MemoryExpansionCost: memExpansionCost, + AccessListComputationCost: accessListComputationCost, + AccessListStateAccessCost: accessListStateAccessCost, + IsValueSentWithCall: false, + InitCodeCost: 0, + HashCost: 0, }, nil } @@ -482,26 +429,24 @@ func finishCalcStateReadCallGas( totalGasUsed uint64, codeExecutionCost uint64, callGasDimensionInfo CallGasDimensionInfo, -) GasesByDimension { - leftOver := totalGasUsed - callGasDimensionInfo.MemoryExpansionCost - codeExecutionCost - if leftOver == params.ColdAccountAccessCostEIP2929 { - return GasesByDimension{ - OneDimensionalGasCost: totalGasUsed, - Computation: params.WarmStorageReadCostEIP2929 + callGasDimensionInfo.MemoryExpansionCost, - StateAccess: params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, - } - } - return GasesByDimension{ +) (GasesByDimension, error) { + computation := callGasDimensionInfo.AccessListComputationCost + callGasDimensionInfo.MemoryExpansionCost + stateAccess := callGasDimensionInfo.AccessListStateAccessCost + ret := GasesByDimension{ OneDimensionalGasCost: totalGasUsed, - Computation: leftOver + callGasDimensionInfo.MemoryExpansionCost, - StateAccess: 0, + Computation: computation, + StateAccess: stateAccess, StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, } + err := checkGasDimensionsEqualCallGas( + callGasDimensionInfo.Pc, + byte(callGasDimensionInfo.Op), + codeExecutionCost, + ret, + ) + return ret, err } // calcLogGas returns the gas used for the `LOG0, LOG1, LOG2, LOG3, LOG4` opcodes @@ -527,14 +472,7 @@ func calcLogGas( // 32 bytes per topic is 256 gas per topic. // rest is computation (for the bloom filter computation, memory expansion, etc) if err != nil { - return GasesByDimension{ - OneDimensionalGasCost: gas, - Computation: gas, - StateAccess: 0, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, - }, nil, nil + return outOfGas(gas) } numTopics := uint64(0) switch vm.OpCode(op) { @@ -590,14 +528,7 @@ func calcCreateGas( // static_gas = 32000 // dynamic_gas = init_code_cost + memory_expansion_cost + deployment_code_execution_cost + code_deposit_cost if err != nil { - return GasesByDimension{ - OneDimensionalGasCost: gas, - Computation: gas, - StateAccess: 0, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, - }, nil, nil + return outOfGas(gas) } stack := scope.StackData() lenStack := len(stack) @@ -627,12 +558,15 @@ func calcCreateGas( HistoryGrowth: 0, StateGrowthRefund: 0, }, &CallGasDimensionInfo{ - Op: vm.OpCode(op), - GasCounterAtTimeOfCall: gas, - MemoryExpansionCost: memExpansionCost, - IsValueSentWithCall: false, - InitCodeCost: initCodeCost, - HashCost: hashCost, + Pc: pc, + Op: vm.OpCode(op), + GasCounterAtTimeOfCall: gas, + MemoryExpansionCost: memExpansionCost, + AccessListComputationCost: 0, + AccessListStateAccessCost: 0, + IsValueSentWithCall: false, + InitCodeCost: 0, + HashCost: 0, }, nil } @@ -642,7 +576,7 @@ func finishCalcCreateGas( totalGasUsed uint64, codeExecutionCost uint64, callGasDimensionInfo CallGasDimensionInfo, -) GasesByDimension { +) (GasesByDimension, error) { // totalGasUsed = init_code_cost + memory_expansion_cost + deployment_code_execution_cost + code_deposit_cost codeDepositCost := totalGasUsed - params.CreateGas - callGasDimensionInfo.InitCodeCost - callGasDimensionInfo.MemoryExpansionCost - callGasDimensionInfo.HashCost - codeExecutionCost @@ -653,7 +587,7 @@ func finishCalcCreateGas( staticNonNewAccountCost := params.CreateGas - params.CallNewAccountGas computeNonNewAccountCost := staticNonNewAccountCost / 2 growthNonNewAccountCost := staticNonNewAccountCost - computeNonNewAccountCost - return GasesByDimension{ + ret := GasesByDimension{ OneDimensionalGasCost: totalGasUsed, Computation: callGasDimensionInfo.InitCodeCost + callGasDimensionInfo.MemoryExpansionCost + callGasDimensionInfo.HashCost + computeNonNewAccountCost, StateAccess: 0, @@ -661,6 +595,13 @@ func finishCalcCreateGas( HistoryGrowth: 0, StateGrowthRefund: 0, } + err := checkGasDimensionsEqualCallGas( + callGasDimensionInfo.Pc, + byte(callGasDimensionInfo.Op), + codeExecutionCost, + ret, + ) + return ret, err } // calcReadAndStoreCallGas returns the gas used for the `CALL, CALLCODE` opcodes @@ -680,14 +621,7 @@ func calcReadAndStoreCallGas( err error, ) (GasesByDimension, *CallGasDimensionInfo, error) { if err != nil { - return GasesByDimension{ - OneDimensionalGasCost: gas, - Computation: gas, - StateAccess: 0, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, - }, nil, nil + return outOfGas(gas) } stack := scope.StackData() lenStack := len(stack) @@ -697,6 +631,8 @@ func calcReadAndStoreCallGas( // argsSize in stack position 5 argsOffset := stack[lenStack-4].Uint64() argsSize := stack[lenStack-5].Uint64() + // address in stack position 2 + address := stack[lenStack-2].Bytes20() // Note that opcodes with a byte size parameter of 0 will not trigger memory expansion, regardless of their offset parameters if argsSize == 0 { argsOffset = 0 @@ -728,24 +664,29 @@ func calcReadAndStoreCallGas( } } + accessListComputationCost, accessListStateAccessCost := addressAccessListCost(t, address) + // at a minimum, the cost is 100 for the warm access set // and the memory expansion cost - computation := memExpansionCost + params.WarmStorageReadCostEIP2929 + computation := memExpansionCost + accessListComputationCost // see finishCalcStateReadCallGas for more details return GasesByDimension{ OneDimensionalGasCost: cost, Computation: computation, - StateAccess: 0, + StateAccess: accessListStateAccessCost, StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, }, &CallGasDimensionInfo{ - Op: vm.OpCode(op), - GasCounterAtTimeOfCall: gas, - MemoryExpansionCost: memExpansionCost, - IsValueSentWithCall: valueSentWithCall > 0, - InitCodeCost: 0, - HashCost: 0, + Pc: pc, + Op: vm.OpCode(op), + GasCounterAtTimeOfCall: gas, + AccessListComputationCost: accessListComputationCost, + AccessListStateAccessCost: accessListStateAccessCost, + MemoryExpansionCost: memExpansionCost, + IsValueSentWithCall: valueSentWithCall > 0, + InitCodeCost: 0, + HashCost: 0, }, nil } @@ -760,7 +701,7 @@ func finishCalcStateReadAndStoreCallGas( totalGasUsed uint64, codeExecutionCost uint64, callGasDimensionInfo CallGasDimensionInfo, -) GasesByDimension { +) (GasesByDimension, error) { // the stipend is 2300 and it is not charged to the call itself but used in the execution cost var positiveValueCostLessStipend uint64 = 0 if callGasDimensionInfo.IsValueSentWithCall { @@ -775,12 +716,20 @@ func finishCalcStateReadAndStoreCallGas( // and whatever was left over after that was address_access_cost // callcode is the same as call except does not have value_to_empty_account_cost, // so this code properly handles it coincidentally, too + ret := GasesByDimension{ + OneDimensionalGasCost: totalGasUsed, + Computation: callGasDimensionInfo.MemoryExpansionCost + params.WarmStorageReadCostEIP2929, + StateAccess: positiveValueCostLessStipend, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + } if leftOver > params.ColdAccountAccessCostEIP2929 { // there is a value being sent to an empty account var coldCost uint64 = 0 if leftOver-params.CallNewAccountGas == params.ColdAccountAccessCostEIP2929 { coldCost = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 } - return GasesByDimension{ + ret = GasesByDimension{ OneDimensionalGasCost: totalGasUsed, Computation: callGasDimensionInfo.MemoryExpansionCost + params.WarmStorageReadCostEIP2929, StateAccess: coldCost + positiveValueCostLessStipend, @@ -790,7 +739,7 @@ func finishCalcStateReadAndStoreCallGas( } } else if leftOver == params.ColdAccountAccessCostEIP2929 { var coldCost uint64 = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 - return GasesByDimension{ + ret = GasesByDimension{ OneDimensionalGasCost: totalGasUsed, Computation: callGasDimensionInfo.MemoryExpansionCost + params.WarmStorageReadCostEIP2929, StateAccess: coldCost + positiveValueCostLessStipend, @@ -799,14 +748,13 @@ func finishCalcStateReadAndStoreCallGas( StateGrowthRefund: 0, } } - return GasesByDimension{ - OneDimensionalGasCost: totalGasUsed, - Computation: callGasDimensionInfo.MemoryExpansionCost + params.WarmStorageReadCostEIP2929, - StateAccess: positiveValueCostLessStipend, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, - } + err := checkGasDimensionsEqualCallGas( + callGasDimensionInfo.Pc, + byte(callGasDimensionInfo.Op), + codeExecutionCost, + ret, + ) + return ret, err } // calcSStoreGas returns the gas used for the `SSTORE` opcode @@ -851,14 +799,7 @@ func calcSStoreGas( // to find per-step changes, we track the accumulated refund // and compare it to the current refund if err != nil { - return GasesByDimension{ - OneDimensionalGasCost: gas, - Computation: gas, - StateAccess: 0, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, - }, nil, nil + return outOfGas(gas) } currentRefund := t.GetOpRefund() accumulatedRefund := t.GetRefundAccumulated() @@ -871,29 +812,33 @@ func calcSStoreGas( } t.SetRefundAccumulated(currentRefund) } - + ret := GasesByDimension{ + OneDimensionalGasCost: cost, + } if cost >= params.SstoreSetGas { // 22100 case and 20000 case accessCost := cost - params.SstoreSetGas - return GasesByDimension{ + ret = GasesByDimension{ OneDimensionalGasCost: cost, Computation: params.WarmStorageReadCostEIP2929, StateAccess: accessCost, StateGrowth: params.SstoreSetGas - params.WarmStorageReadCostEIP2929, HistoryGrowth: 0, StateGrowthRefund: diff, - }, nil, nil + } } else if cost > 0 { - return GasesByDimension{ + ret = GasesByDimension{ OneDimensionalGasCost: cost, Computation: params.WarmStorageReadCostEIP2929, StateAccess: cost - params.WarmStorageReadCostEIP2929, StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: diff, - }, nil, nil + } + } + if err := checkGasDimensionsEqualOneDimensionalGas(pc, op, depth, gas, cost, ret); err != nil { + return GasesByDimension{}, nil, err } - // bizarre "system transactions" that can have costs of zero... - return GasesByDimension{}, nil, nil + return ret, nil, nil } // calcSelfDestructGas returns the gas used for the `SELFDESTRUCT` opcode @@ -921,14 +866,7 @@ func calcSelfDestructGas( // excepting 100 for the warm access set // doesn't actually delete anything from disk, it just marks it as deleted. if err != nil { - return GasesByDimension{ - OneDimensionalGasCost: gas, - Computation: gas, - StateAccess: 0, - StateGrowth: 0, - HistoryGrowth: 0, - StateGrowthRefund: 0, - }, nil, nil + return outOfGas(gas) } if cost == params.CreateBySelfdestructGas+params.SelfdestructGasEIP150 { // warm but funds target empty @@ -1082,3 +1020,92 @@ func memoryGasCost(mem []byte, lastGasCost uint64, newMemSize uint64) (fee uint6 } return 0, 0, nil } + +// helper function that calculates the gas dimensions for an access list access for an address +func addressAccessListCost(t DimensionTracer, address common.Address) (computationGasCost uint64, stateAccessGasCost uint64) { + accessListAddresses, _ := t.GetPrevAccessList() + _, addressInAccessList := accessListAddresses[address] + if !addressInAccessList { + computationGasCost = params.WarmStorageReadCostEIP2929 + stateAccessGasCost = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 + } else { + computationGasCost = params.WarmStorageReadCostEIP2929 + stateAccessGasCost = 0 + } + return +} + +// helper function that calculates the gas dimensions for an access list access for a storage slot +func storageSlotAccessListCost(t DimensionTracer, address common.Address, slot common.Hash) (computationGasCost uint64, stateAccessGasCost uint64) { + accessListAddresses, accessListSlots := t.GetPrevAccessList() + idx, ok := accessListAddresses[address] + if !ok { + // no such address (and hence zero slots) + return params.WarmStorageReadCostEIP2929, params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929 + } + if idx == -1 { + // address yes, but no slots + return params.WarmStorageReadCostEIP2929, params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929 + } + _, slotPresent := accessListSlots[idx][slot] + if !slotPresent { + return params.WarmStorageReadCostEIP2929, params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929 + } + return params.WarmStorageReadCostEIP2929, 0 +} + +// wherever it's possible, check that the gas dimensions are sane +func checkGasDimensionsEqualOneDimensionalGas( + pc uint64, + op byte, + depth int, + gas uint64, + cost uint64, + dim GasesByDimension, +) error { + if cost != dim.Computation+dim.StateAccess+dim.StateGrowth+dim.HistoryGrowth { + return fmt.Errorf( + "unexpected gas cost mismatch: pc %d, op %d, depth %d, gas %d, cost %d != %v", + pc, + op, + depth, + gas, + cost, + dim, + ) + } + return nil +} + +// for calls and other opcodes that increase stack depth, +// use this function to check that the total computed gas +// is consistent with the expected gas +func checkGasDimensionsEqualCallGas( + pc uint64, + op byte, + codeExecutionCost uint64, + dim GasesByDimension, +) error { + if dim.OneDimensionalGasCost != codeExecutionCost+dim.Computation+dim.StateAccess+dim.StateGrowth+dim.HistoryGrowth { + return fmt.Errorf( + "unexpected gas cost mismatch: pc %d, op %d, codeExecutionCost %d != %v", + pc, + op, + codeExecutionCost, + dim, + ) + } + return nil +} + +// helper function that purely makes the golang prettier for the out of gas case +func outOfGas(gas uint64) (GasesByDimension, *CallGasDimensionInfo, error) { + return GasesByDimension{ + OneDimensionalGasCost: gas, + Computation: gas, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + }, nil, nil +} diff --git a/eth/tracers/native/tx_gas_dimension_by_opcode.go b/eth/tracers/native/tx_gas_dimension_by_opcode.go index f231519a00..dfe66d1e91 100644 --- a/eth/tracers/native/tx_gas_dimension_by_opcode.go +++ b/eth/tracers/native/tx_gas_dimension_by_opcode.go @@ -2,7 +2,6 @@ package native import ( "encoding/json" - "fmt" "github.com/ethereum/go-ethereum/eth/tracers/native/proto" @@ -93,28 +92,11 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( // call the appropriate finishX function to write the gas dimensions // for the call that increased the stack depth in the past if depth < t.depth { - stackInfo, ok := t.callStack.Pop() - // base case, stack is empty, do nothing - if !ok { - t.interrupt.Store(true) - t.reason = fmt.Errorf("call stack is unexpectedly empty %d %d %d", pc, depth, t.depth) + interrupted, _, stackInfo, finishGasesByDimension := t.callFinishFunction(pc, depth, gas) + if interrupted { return } - finishFunction := GetFinishCalcGasDimensionFunc(stackInfo.GasDimensionInfo.Op) - if finishFunction == nil { - t.interrupt.Store(true) - t.reason = fmt.Errorf( - "no finish function found for opcode %s, call stack is messed up %d", - stackInfo.GasDimensionInfo.Op.String(), - pc, - ) - return - } - // IMPORTANT NOTE: for some reason the only reliable way to actually get the gas cost of the call - // is to subtract gas at time of call from gas at opcode AFTER return - // you can't trust the `gas` field on the call itself. I wonder if the gas field is an estimation - gasUsedByCall := stackInfo.GasDimensionInfo.GasCounterAtTimeOfCall - gas - finishGasesByDimension := finishFunction(gasUsedByCall, stackInfo.ExecutionCost, stackInfo.GasDimensionInfo) + accumulatedDimensionsCall := t.OpcodeToDimensions[stackInfo.GasDimensionInfo.Op] accumulatedDimensionsCall.OneDimensionalGasCost += finishGasesByDimension.OneDimensionalGasCost From aa80bb1d479647b8f54397b56861dcc6d005e10f Mon Sep 17 00:00:00 2001 From: relyt29 Date: Tue, 29 Apr 2025 20:17:58 -0400 Subject: [PATCH 28/35] gas dimension logger debug string --- eth/tracers/native/tx_gas_dimension_logger.go | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/eth/tracers/native/tx_gas_dimension_logger.go b/eth/tracers/native/tx_gas_dimension_logger.go index be36bb774c..4ff4d853d2 100644 --- a/eth/tracers/native/tx_gas_dimension_logger.go +++ b/eth/tracers/native/tx_gas_dimension_logger.go @@ -2,6 +2,7 @@ package native import ( "encoding/json" + "fmt" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/vm" @@ -203,6 +204,29 @@ type DimensionLogRes struct { Err error `json:"err,omitempty"` } +// DebugString returns a string +// representation of the DimensionLogRes for debugging +func (d *DimensionLogRes) DebugString() string { + return fmt.Sprintf( + "{Pc: %d, Op: %s, Depth: %d, OneDimensionalGasCost: %d, Computation: %d, StateAccess: %d, StateGrowth: %d, HistoryGrowth: %d, StateGrowthRefund: %d, CallRealGas: %d, CallExecutionCost: %d, CallMemoryExpansion: %d, CreateInitCodeCost: %d, Create2HashCost: %d, Err: %v}", + d.Pc, + d.Op, + d.Depth, + d.OneDimensionalGasCost, + d.Computation, + d.StateAccess, + d.StateGrowth, + d.HistoryGrowth, + d.StateGrowthRefund, + d.CallRealGas, + d.CallExecutionCost, + d.CallMemoryExpansion, + d.CreateInitCodeCost, + d.Create2HashCost, + d.Err, + ) +} + // formatLogs formats EVM returned structured logs for json output func formatLogs(logs []DimensionLog) []DimensionLogRes { formatted := make([]DimensionLogRes, len(logs)) From 76b20dbaba6194f99ebb47382c2630a65a1736d0 Mon Sep 17 00:00:00 2001 From: relyt29 Date: Wed, 30 Apr 2025 12:50:05 -0400 Subject: [PATCH 29/35] gas dimension tracing: simplify gas dimensions for SSTORE --- eth/tracers/native/gas_dimension_calc.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index 775806d770..98eb12b6d6 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -819,17 +819,17 @@ func calcSStoreGas( accessCost := cost - params.SstoreSetGas ret = GasesByDimension{ OneDimensionalGasCost: cost, - Computation: params.WarmStorageReadCostEIP2929, + Computation: 0, StateAccess: accessCost, - StateGrowth: params.SstoreSetGas - params.WarmStorageReadCostEIP2929, + StateGrowth: params.SstoreSetGas, HistoryGrowth: 0, StateGrowthRefund: diff, } } else if cost > 0 { ret = GasesByDimension{ OneDimensionalGasCost: cost, - Computation: params.WarmStorageReadCostEIP2929, - StateAccess: cost - params.WarmStorageReadCostEIP2929, + Computation: 0, + StateAccess: cost, StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: diff, From 2ea695766bc5c5f5f35dc72ed4f6498be4226244 Mon Sep 17 00:00:00 2001 From: relyt29 Date: Wed, 30 Apr 2025 15:00:10 -0400 Subject: [PATCH 30/35] gas dimension tracing bugfix: sign error on subtraction of gas refund --- eth/tracers/native/gas_dimension_calc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index 98eb12b6d6..8335ed9245 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -808,7 +808,7 @@ func calcSStoreGas( if accumulatedRefund < currentRefund { diff = int64(currentRefund - accumulatedRefund) } else { - diff = int64(accumulatedRefund - currentRefund) + diff = -1 * int64(accumulatedRefund-currentRefund) } t.SetRefundAccumulated(currentRefund) } From f0e39db850c3adeba04a13666c3dd421bddcd680 Mon Sep 17 00:00:00 2001 From: relyt29 Date: Mon, 5 May 2025 17:29:49 -0400 Subject: [PATCH 31/35] gas dimension tracing bugfix: create opcode missing pass-through values --- eth/tracers/native/gas_dimension_calc.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index 8335ed9245..49ff8b5c18 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -565,8 +565,8 @@ func calcCreateGas( AccessListComputationCost: 0, AccessListStateAccessCost: 0, IsValueSentWithCall: false, - InitCodeCost: 0, - HashCost: 0, + InitCodeCost: initCodeCost, + HashCost: hashCost, }, nil } From e09f142a80c569b189766f6dd89581ac8b2f1e8a Mon Sep 17 00:00:00 2001 From: relyt29 Date: Tue, 6 May 2025 19:47:04 -0400 Subject: [PATCH 32/35] gas dimension tracing: Expose L1GasUsed, CallExecutionGas explicitly This PR exposes the GasUsed, L1GasUsed, and L2GasUsed from the transaction reciept directly in the tracer response. It also changes the tracers for CALL, CALLCODE, STATICCALL, DELEGATECALL, CREATE and CREATE2 to explicitly return the gas consumed by the child execution of the call when the call stack depth is increased by those opcodes. --- .../live/tx_gas_dimension_by_opcode.go | 15 ++- .../native/base_gas_dimension_tracer.go | 41 ++++++- eth/tracers/native/gas_dimension_calc.go | 45 ++++++-- .../proto/gas_dimension_by_opcode.pb.go | 101 +++++++++++++----- .../proto/gas_dimension_by_opcode.proto | 25 ++++- .../native/tx_gas_dimension_by_opcode.go | 14 ++- eth/tracers/native/tx_gas_dimension_logger.go | 14 +-- 7 files changed, 203 insertions(+), 52 deletions(-) diff --git a/eth/tracers/live/tx_gas_dimension_by_opcode.go b/eth/tracers/live/tx_gas_dimension_by_opcode.go index 452ed5083d..a6f0ddd759 100644 --- a/eth/tracers/live/tx_gas_dimension_by_opcode.go +++ b/eth/tracers/live/tx_gas_dimension_by_opcode.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/eth/tracers/native" + "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -21,12 +22,14 @@ func init() { } type txGasDimensionByOpcodeLiveTraceConfig struct { - Path string `json:"path"` // Path to directory for output + Path string `json:"path"` // Path to directory for output + ChainConfig *params.ChainConfig `json:"chainConfig"` } // gasDimensionTracer struct type TxGasDimensionByOpcodeLiveTracer struct { Path string `json:"path"` // Path to directory for output + ChainConfig *params.ChainConfig GasDimensionTracer *native.TxGasDimensionByOpcodeTracer } @@ -45,8 +48,16 @@ func NewTxGasDimensionByOpcodeLogger( return nil, fmt.Errorf("tx gas dimension live tracer path for output is required: %v", config) } + // if you get stuck here, look at + // cmd/chaininfo/arbitrum_chain_info.json + // for a sample chain config + if config.ChainConfig == nil { + return nil, fmt.Errorf("tx gas dimension live tracer chain config is required: %v", config) + } + t := &TxGasDimensionByOpcodeLiveTracer{ Path: config.Path, + ChainConfig: config.ChainConfig, GasDimensionTracer: nil, } @@ -69,7 +80,7 @@ func (t *TxGasDimensionByOpcodeLiveTracer) OnTxStart( } t.GasDimensionTracer = &native.TxGasDimensionByOpcodeTracer{ - BaseGasDimensionTracer: native.NewBaseGasDimensionTracer(), + BaseGasDimensionTracer: native.NewBaseGasDimensionTracer(t.ChainConfig), OpcodeToDimensions: make(map[_vm.OpCode]native.GasesByDimension), } t.GasDimensionTracer.OnTxStart(vm, tx, from) diff --git a/eth/tracers/native/base_gas_dimension_tracer.go b/eth/tracers/native/base_gas_dimension_tracer.go index b71bf28f45..9b1c254a5d 100644 --- a/eth/tracers/native/base_gas_dimension_tracer.go +++ b/eth/tracers/native/base_gas_dimension_tracer.go @@ -6,9 +6,11 @@ import ( "sync/atomic" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" "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/params" ) // BaseGasDimensionTracer contains the shared functionality between different gas dimension tracers @@ -18,7 +20,13 @@ type BaseGasDimensionTracer struct { // the hash of the transactionh txHash common.Hash // the amount of gas used in the transaction - usedGas uint64 + gasUsed uint64 + // the amount of gas used for L1 in the transaction + gasUsedForL1 uint64 + // the amount of gas used for L2 in the transaction + gasUsedForL2 uint64 + // the intrinsic gas of the transaction, the static cost + calldata bytes cost + intrinsicGas uint64 // the call stack for the transaction callStack CallGasDimensionStack // the depth at the current step of execution of the call stack @@ -34,10 +42,13 @@ type BaseGasDimensionTracer struct { interrupt atomic.Bool // reason or error for the interruption in the tracer itself (as opposed to the transaction) reason error + // cached chain config for use in hooks + chainConfig *params.ChainConfig } -func NewBaseGasDimensionTracer() BaseGasDimensionTracer { +func NewBaseGasDimensionTracer(chainConfig *params.ChainConfig) BaseGasDimensionTracer { return BaseGasDimensionTracer{ + chainConfig: chainConfig, depth: 1, refundAccumulated: 0, prevAccessListAddresses: map[common.Address]int{}, @@ -216,6 +227,18 @@ func (t *BaseGasDimensionTracer) updatePrevAccessList(addresses map[common.Addre // OnTxStart handles transaction start func (t *BaseGasDimensionTracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction, from common.Address) { t.env = env + isContractCreation := tx.To() == nil + rules := t.chainConfig.Rules(env.BlockNumber, env.Random != nil, env.Time, env.ArbOSVersion) + intrinsicGas, _ := core.IntrinsicGas( + tx.Data(), + tx.AccessList(), + tx.SetCodeAuthorizations(), + isContractCreation, + rules.IsHomestead, + rules.IsIstanbul, + rules.IsShanghai, + ) + t.intrinsicGas = intrinsicGas } // OnTxEnd handles transaction end @@ -227,7 +250,9 @@ func (t *BaseGasDimensionTracer) OnTxEnd(receipt *types.Receipt, err error) { } return } - t.usedGas = receipt.GasUsed + t.gasUsed = receipt.GasUsed + t.gasUsedForL1 = receipt.GasUsedForL1 + t.gasUsedForL2 = receipt.GasUsedForL2() t.txHash = receipt.TxHash } @@ -281,7 +306,10 @@ func (t *BaseGasDimensionTracer) Error() error { return t.err } // BaseExecutionResult has shared fields for execution results type BaseExecutionResult struct { - Gas uint64 `json:"gas"` + GasUsed uint64 `json:"gasUsed"` + GasUsedForL1 uint64 `json:"gasUsedForL1"` + GasUsedForL2 uint64 `json:"gasUsedForL2"` + IntrinsicGas uint64 `json:"intrinsicGas"` Failed bool `json:"failed"` TxHash string `json:"txHash"` BlockTimestamp uint64 `json:"blockTimestamp"` @@ -297,7 +325,10 @@ func (t *BaseGasDimensionTracer) GetBaseExecutionResult() (BaseExecutionResult, failed := t.err != nil return BaseExecutionResult{ - Gas: t.usedGas, + GasUsed: t.gasUsed, + GasUsedForL1: t.gasUsedForL1, + GasUsedForL2: t.gasUsedForL2, + IntrinsicGas: t.intrinsicGas, Failed: failed, TxHash: t.txHash.Hex(), BlockTimestamp: t.env.Time, diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index 49ff8b5c18..ea9d510eb5 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -19,6 +19,7 @@ type GasesByDimension struct { StateGrowth uint64 `json:"gr,omitempty"` HistoryGrowth uint64 `json:"h,omitempty"` StateGrowthRefund int64 `json:"rf,omitempty"` + ChildExecutionCost uint64 `json:"exc,omitempty"` } // in the case of opcodes like CALL, STATICCALL, DELEGATECALL, etc, @@ -202,6 +203,7 @@ func calcSimpleSingleDimensionGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, + ChildExecutionCost: 0, }, nil, nil } @@ -237,6 +239,7 @@ func calcSimpleAddressAccessSetGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, + ChildExecutionCost: 0, } if err := checkGasDimensionsEqualOneDimensionalGas(pc, op, depth, gas, cost, ret); err != nil { return GasesByDimension{}, nil, err @@ -270,6 +273,7 @@ func calcSLOADGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, + ChildExecutionCost: 0, } if err := checkGasDimensionsEqualOneDimensionalGas(pc, op, depth, gas, cost, ret); err != nil { return GasesByDimension{}, nil, err @@ -324,6 +328,7 @@ func calcExtCodeCopyGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, + ChildExecutionCost: 0, } if err := checkGasDimensionsEqualOneDimensionalGas(pc, op, depth, gas, cost, ret); err != nil { return GasesByDimension{}, nil, err @@ -405,6 +410,7 @@ func calcStateReadCallGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, + ChildExecutionCost: 0, }, &CallGasDimensionInfo{ Pc: pc, Op: vm.OpCode(op), @@ -430,15 +436,17 @@ func finishCalcStateReadCallGas( codeExecutionCost uint64, callGasDimensionInfo CallGasDimensionInfo, ) (GasesByDimension, error) { + oneDimensionalGas := totalGasUsed - codeExecutionCost computation := callGasDimensionInfo.AccessListComputationCost + callGasDimensionInfo.MemoryExpansionCost stateAccess := callGasDimensionInfo.AccessListStateAccessCost ret := GasesByDimension{ - OneDimensionalGasCost: totalGasUsed, + OneDimensionalGasCost: oneDimensionalGas, Computation: computation, StateAccess: stateAccess, StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, + ChildExecutionCost: codeExecutionCost, } err := checkGasDimensionsEqualCallGas( callGasDimensionInfo.Pc, @@ -504,6 +512,7 @@ func calcLogGas( StateGrowth: 0, HistoryGrowth: historyGrowthCost, StateGrowthRefund: 0, + ChildExecutionCost: 0, }, nil, nil } @@ -557,6 +566,7 @@ func calcCreateGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, + ChildExecutionCost: 0, }, &CallGasDimensionInfo{ Pc: pc, Op: vm.OpCode(op), @@ -577,6 +587,7 @@ func finishCalcCreateGas( codeExecutionCost uint64, callGasDimensionInfo CallGasDimensionInfo, ) (GasesByDimension, error) { + oneDimensionalGas := totalGasUsed - codeExecutionCost // totalGasUsed = init_code_cost + memory_expansion_cost + deployment_code_execution_cost + code_deposit_cost codeDepositCost := totalGasUsed - params.CreateGas - callGasDimensionInfo.InitCodeCost - callGasDimensionInfo.MemoryExpansionCost - callGasDimensionInfo.HashCost - codeExecutionCost @@ -588,12 +599,13 @@ func finishCalcCreateGas( computeNonNewAccountCost := staticNonNewAccountCost / 2 growthNonNewAccountCost := staticNonNewAccountCost - computeNonNewAccountCost ret := GasesByDimension{ - OneDimensionalGasCost: totalGasUsed, + OneDimensionalGasCost: oneDimensionalGas, Computation: callGasDimensionInfo.InitCodeCost + callGasDimensionInfo.MemoryExpansionCost + callGasDimensionInfo.HashCost + computeNonNewAccountCost, StateAccess: 0, StateGrowth: growthNonNewAccountCost + params.CallNewAccountGas + codeDepositCost, HistoryGrowth: 0, StateGrowthRefund: 0, + ChildExecutionCost: codeExecutionCost, } err := checkGasDimensionsEqualCallGas( callGasDimensionInfo.Pc, @@ -677,6 +689,7 @@ func calcReadAndStoreCallGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, + ChildExecutionCost: 0, }, &CallGasDimensionInfo{ Pc: pc, Op: vm.OpCode(op), @@ -702,6 +715,8 @@ func finishCalcStateReadAndStoreCallGas( codeExecutionCost uint64, callGasDimensionInfo CallGasDimensionInfo, ) (GasesByDimension, error) { + oneDimensionalGas := totalGasUsed - codeExecutionCost + fmt.Println("finishCalcStateReadAndStoreCallGas is called") // the stipend is 2300 and it is not charged to the call itself but used in the execution cost var positiveValueCostLessStipend uint64 = 0 if callGasDimensionInfo.IsValueSentWithCall { @@ -716,44 +731,53 @@ func finishCalcStateReadAndStoreCallGas( // and whatever was left over after that was address_access_cost // callcode is the same as call except does not have value_to_empty_account_cost, // so this code properly handles it coincidentally, too + fmt.Println("finishCalcStateReadAndStoreCallGas is continuing") ret := GasesByDimension{ - OneDimensionalGasCost: totalGasUsed, + OneDimensionalGasCost: oneDimensionalGas, Computation: callGasDimensionInfo.MemoryExpansionCost + params.WarmStorageReadCostEIP2929, StateAccess: positiveValueCostLessStipend, StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, + ChildExecutionCost: codeExecutionCost, } if leftOver > params.ColdAccountAccessCostEIP2929 { // there is a value being sent to an empty account + fmt.Println("leftOver > params.ColdAccountAccessCostEIP2929") var coldCost uint64 = 0 if leftOver-params.CallNewAccountGas == params.ColdAccountAccessCostEIP2929 { coldCost = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 } ret = GasesByDimension{ - OneDimensionalGasCost: totalGasUsed, + OneDimensionalGasCost: oneDimensionalGas, Computation: callGasDimensionInfo.MemoryExpansionCost + params.WarmStorageReadCostEIP2929, StateAccess: coldCost + positiveValueCostLessStipend, StateGrowth: params.CallNewAccountGas, HistoryGrowth: 0, StateGrowthRefund: 0, + ChildExecutionCost: codeExecutionCost, } } else if leftOver == params.ColdAccountAccessCostEIP2929 { + fmt.Println("leftOver == params.ColdAccountAccessCostEIP2929") var coldCost uint64 = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 ret = GasesByDimension{ - OneDimensionalGasCost: totalGasUsed, + OneDimensionalGasCost: oneDimensionalGas, Computation: callGasDimensionInfo.MemoryExpansionCost + params.WarmStorageReadCostEIP2929, StateAccess: coldCost + positiveValueCostLessStipend, StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, + ChildExecutionCost: codeExecutionCost, } } + fmt.Println("finishCalcStateReadAndStoreCallGas is returning") err := checkGasDimensionsEqualCallGas( callGasDimensionInfo.Pc, byte(callGasDimensionInfo.Op), codeExecutionCost, ret, ) + fmt.Println("End result: ", ret) + fmt.Println("End error: ", err) return ret, err } @@ -824,6 +848,7 @@ func calcSStoreGas( StateGrowth: params.SstoreSetGas, HistoryGrowth: 0, StateGrowthRefund: diff, + ChildExecutionCost: 0, } } else if cost > 0 { ret = GasesByDimension{ @@ -833,6 +858,7 @@ func calcSStoreGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: diff, + ChildExecutionCost: 0, } } if err := checkGasDimensionsEqualOneDimensionalGas(pc, op, depth, gas, cost, ret); err != nil { @@ -881,6 +907,7 @@ func calcSelfDestructGas( StateGrowth: params.CreateBySelfdestructGas, HistoryGrowth: 0, StateGrowthRefund: 0, + ChildExecutionCost: 0, }, nil, nil } else if cost == params.CreateBySelfdestructGas+params.SelfdestructGasEIP150+params.ColdAccountAccessCostEIP2929 { // cold and funds target empty @@ -895,6 +922,7 @@ func calcSelfDestructGas( StateGrowth: params.CreateBySelfdestructGas, HistoryGrowth: 0, StateGrowthRefund: 0, + ChildExecutionCost: 0, }, nil, nil } else if cost == params.SelfdestructGasEIP150+params.ColdAccountAccessCostEIP2929 { // address lookup was cold but funds target has money already. Cost is 7600 @@ -908,6 +936,7 @@ func calcSelfDestructGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, + ChildExecutionCost: 0, }, nil, nil } // if you reach here, then the cost was 5000 @@ -920,6 +949,7 @@ func calcSelfDestructGas( StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, + ChildExecutionCost: 0, }, nil, nil } @@ -1086,12 +1116,13 @@ func checkGasDimensionsEqualCallGas( codeExecutionCost uint64, dim GasesByDimension, ) error { - if dim.OneDimensionalGasCost != codeExecutionCost+dim.Computation+dim.StateAccess+dim.StateGrowth+dim.HistoryGrowth { + if dim.OneDimensionalGasCost != dim.Computation+dim.StateAccess+dim.StateGrowth+dim.HistoryGrowth { return fmt.Errorf( - "unexpected gas cost mismatch: pc %d, op %d, codeExecutionCost %d != %v", + "unexpected gas cost mismatch: pc %d, op %d, with codeExecutionCost %d, expected %d == %v", pc, op, codeExecutionCost, + dim.OneDimensionalGasCost, dim, ) } diff --git a/eth/tracers/native/proto/gas_dimension_by_opcode.pb.go b/eth/tracers/native/proto/gas_dimension_by_opcode.pb.go index bcf85f036b..7d2d0f6e66 100644 --- a/eth/tracers/native/proto/gas_dimension_by_opcode.pb.go +++ b/eth/tracers/native/proto/gas_dimension_by_opcode.pb.go @@ -23,15 +23,23 @@ const ( // GasesByDimension represents the gas consumption for each dimension type GasesByDimension struct { - state protoimpl.MessageState `protogen:"open.v1"` - OneDimensionalGasCost uint64 `protobuf:"varint,1,opt,name=one_dimensional_gas_cost,json=oneDimensionalGasCost,proto3" json:"one_dimensional_gas_cost,omitempty"` - Computation uint64 `protobuf:"varint,2,opt,name=computation,proto3" json:"computation,omitempty"` - StateAccess uint64 `protobuf:"varint,3,opt,name=state_access,json=stateAccess,proto3" json:"state_access,omitempty"` - StateGrowth uint64 `protobuf:"varint,4,opt,name=state_growth,json=stateGrowth,proto3" json:"state_growth,omitempty"` - HistoryGrowth uint64 `protobuf:"varint,5,opt,name=history_growth,json=historyGrowth,proto3" json:"history_growth,omitempty"` - StateGrowthRefund int64 `protobuf:"varint,6,opt,name=state_growth_refund,json=stateGrowthRefund,proto3" json:"state_growth_refund,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + // the total gas cost for the opcode, across all dimensions. + OneDimensionalGasCost uint64 `protobuf:"varint,1,opt,name=one_dimensional_gas_cost,json=oneDimensionalGasCost,proto3" json:"one_dimensional_gas_cost,omitempty"` + // how much of the gas was used for computation or local memory access, stack operations, etc. + Computation uint64 `protobuf:"varint,2,opt,name=computation,proto3" json:"computation,omitempty"` + // how much of the gas was used for state access, like reading or writing to the state. + StateAccess uint64 `protobuf:"varint,3,opt,name=state_access,json=stateAccess,proto3" json:"state_access,omitempty"` + // how much of the gas was used for state growth, like creating new contracts or storage slots. + StateGrowth uint64 `protobuf:"varint,4,opt,name=state_growth,json=stateGrowth,proto3" json:"state_growth,omitempty"` + // how much of the gas was used for history growth, like writing to the history (event logs) + HistoryGrowth uint64 `protobuf:"varint,5,opt,name=history_growth,json=historyGrowth,proto3" json:"history_growth,omitempty"` + // how much gas was refunded for removing state, only applicable to SSTORE opcodes to zero. + StateGrowthRefund int64 `protobuf:"varint,6,opt,name=state_growth_refund,json=stateGrowthRefund,proto3" json:"state_growth_refund,omitempty"` + // how much of the gas was used for child execution, for CALLs, CREATEs, etc. + ChildExecutionCost uint64 `protobuf:"varint,7,opt,name=child_execution_cost,json=childExecutionCost,proto3" json:"child_execution_cost,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GasesByDimension) Reset() { @@ -106,17 +114,37 @@ func (x *GasesByDimension) GetStateGrowthRefund() int64 { return 0 } +func (x *GasesByDimension) GetChildExecutionCost() uint64 { + if x != nil { + return x.ChildExecutionCost + } + return 0 +} + // TxGasDimensionByOpcodeExecutionResult represents the execution result type TxGasDimensionByOpcodeExecutionResult struct { - state protoimpl.MessageState `protogen:"open.v1"` - Gas uint64 `protobuf:"varint,1,opt,name=gas,proto3" json:"gas,omitempty"` - Failed bool `protobuf:"varint,2,opt,name=failed,proto3" json:"failed,omitempty"` - Dimensions map[uint32]*GasesByDimension `protobuf:"bytes,3,rep,name=dimensions,proto3" json:"dimensions,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - TxHash string `protobuf:"bytes,4,opt,name=tx_hash,json=txHash,proto3" json:"tx_hash,omitempty"` - BlockTimestamp uint64 `protobuf:"varint,5,opt,name=block_timestamp,json=blockTimestamp,proto3" json:"block_timestamp,omitempty"` - BlockNumber string `protobuf:"bytes,6,opt,name=block_number,json=blockNumber,proto3" json:"block_number,omitempty"` // Using string to represent big.Int - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + // the total amount of gas used in the transaction + GasUsed uint64 `protobuf:"varint,1,opt,name=gas_used,json=gasUsed,proto3" json:"gas_used,omitempty"` + // the gas paid to post the compressed transaction to the L1 chain + GasUsedL1 uint64 `protobuf:"varint,7,opt,name=gas_used_l1,json=gasUsedL1,proto3" json:"gas_used_l1,omitempty"` + // the gas paid to execute the transaction on the L2 chain + GasUsedL2 uint64 `protobuf:"varint,8,opt,name=gas_used_l2,json=gasUsedL2,proto3" json:"gas_used_l2,omitempty"` + // the intrinsic gas of the transaction, the static cost + calldata bytes cost + IntrinsicGas uint64 `protobuf:"varint,9,opt,name=intrinsic_gas,json=intrinsicGas,proto3" json:"intrinsic_gas,omitempty"` + // whether the transaction had an revert or error, like out of gas + Failed bool `protobuf:"varint,2,opt,name=failed,proto3" json:"failed,omitempty"` + // a map of each opcode to the sum of the gas consumption categorized by dimension for that opcode + Dimensions map[uint32]*GasesByDimension `protobuf:"bytes,3,rep,name=dimensions,proto3" json:"dimensions,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // the hash of the transaction + TxHash string `protobuf:"bytes,4,opt,name=tx_hash,json=txHash,proto3" json:"tx_hash,omitempty"` + // the timestamp of the block + BlockTimestamp uint64 `protobuf:"varint,5,opt,name=block_timestamp,json=blockTimestamp,proto3" json:"block_timestamp,omitempty"` + // the block number of the transaction + // Using string to represent big.Int + BlockNumber string `protobuf:"bytes,6,opt,name=block_number,json=blockNumber,proto3" json:"block_number,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *TxGasDimensionByOpcodeExecutionResult) Reset() { @@ -149,9 +177,30 @@ func (*TxGasDimensionByOpcodeExecutionResult) Descriptor() ([]byte, []int) { return file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_rawDescGZIP(), []int{1} } -func (x *TxGasDimensionByOpcodeExecutionResult) GetGas() uint64 { +func (x *TxGasDimensionByOpcodeExecutionResult) GetGasUsed() uint64 { + if x != nil { + return x.GasUsed + } + return 0 +} + +func (x *TxGasDimensionByOpcodeExecutionResult) GetGasUsedL1() uint64 { + if x != nil { + return x.GasUsedL1 + } + return 0 +} + +func (x *TxGasDimensionByOpcodeExecutionResult) GetGasUsedL2() uint64 { + if x != nil { + return x.GasUsedL2 + } + return 0 +} + +func (x *TxGasDimensionByOpcodeExecutionResult) GetIntrinsicGas() uint64 { if x != nil { - return x.Gas + return x.IntrinsicGas } return 0 } @@ -195,16 +244,20 @@ var File_eth_tracers_native_proto_gas_dimension_by_opcode_proto protoreflect.Fil const file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_rawDesc = "" + "\n" + - "6eth/tracers/native/proto/gas_dimension_by_opcode.proto\x12\x18eth.tracers.native.proto\"\x8a\x02\n" + + "6eth/tracers/native/proto/gas_dimension_by_opcode.proto\x12\x18eth.tracers.native.proto\"\xbc\x02\n" + "\x10GasesByDimension\x127\n" + "\x18one_dimensional_gas_cost\x18\x01 \x01(\x04R\x15oneDimensionalGasCost\x12 \n" + "\vcomputation\x18\x02 \x01(\x04R\vcomputation\x12!\n" + "\fstate_access\x18\x03 \x01(\x04R\vstateAccess\x12!\n" + "\fstate_growth\x18\x04 \x01(\x04R\vstateGrowth\x12%\n" + "\x0ehistory_growth\x18\x05 \x01(\x04R\rhistoryGrowth\x12.\n" + - "\x13state_growth_refund\x18\x06 \x01(\x03R\x11stateGrowthRefund\"\x92\x03\n" + - "%TxGasDimensionByOpcodeExecutionResult\x12\x10\n" + - "\x03gas\x18\x01 \x01(\x04R\x03gas\x12\x16\n" + + "\x13state_growth_refund\x18\x06 \x01(\x03R\x11stateGrowthRefund\x120\n" + + "\x14child_execution_cost\x18\a \x01(\x04R\x12childExecutionCost\"\x80\x04\n" + + "%TxGasDimensionByOpcodeExecutionResult\x12\x19\n" + + "\bgas_used\x18\x01 \x01(\x04R\agasUsed\x12\x1e\n" + + "\vgas_used_l1\x18\a \x01(\x04R\tgasUsedL1\x12\x1e\n" + + "\vgas_used_l2\x18\b \x01(\x04R\tgasUsedL2\x12#\n" + + "\rintrinsic_gas\x18\t \x01(\x04R\fintrinsicGas\x12\x16\n" + "\x06failed\x18\x02 \x01(\bR\x06failed\x12o\n" + "\n" + "dimensions\x18\x03 \x03(\v2O.eth.tracers.native.proto.TxGasDimensionByOpcodeExecutionResult.DimensionsEntryR\n" + diff --git a/eth/tracers/native/proto/gas_dimension_by_opcode.proto b/eth/tracers/native/proto/gas_dimension_by_opcode.proto index 7c9ebc5e60..608509ff33 100644 --- a/eth/tracers/native/proto/gas_dimension_by_opcode.proto +++ b/eth/tracers/native/proto/gas_dimension_by_opcode.proto @@ -6,20 +6,41 @@ option go_package = "github.com/ethereum/go-ethereum/eth/tracers/native/proto"; // GasesByDimension represents the gas consumption for each dimension message GasesByDimension { + // the total gas cost for the opcode, across all dimensions. uint64 one_dimensional_gas_cost = 1; + // how much of the gas was used for computation or local memory access, stack operations, etc. uint64 computation = 2; + // how much of the gas was used for state access, like reading or writing to the state. uint64 state_access = 3; + // how much of the gas was used for state growth, like creating new contracts or storage slots. uint64 state_growth = 4; + // how much of the gas was used for history growth, like writing to the history (event logs) uint64 history_growth = 5; + // how much gas was refunded for removing state, only applicable to SSTORE opcodes to zero. int64 state_growth_refund = 6; + // how much of the gas was used for child execution, for CALLs, CREATEs, etc. + uint64 child_execution_cost = 7; } // TxGasDimensionByOpcodeExecutionResult represents the execution result message TxGasDimensionByOpcodeExecutionResult { - uint64 gas = 1; + // the total amount of gas used in the transaction + uint64 gas_used = 1; + // the gas paid to post the compressed transaction to the L1 chain + uint64 gas_used_l1 = 7; + // the gas paid to execute the transaction on the L2 chain + uint64 gas_used_l2 = 8; + // the intrinsic gas of the transaction, the static cost + calldata bytes cost + uint64 intrinsic_gas = 9; + // whether the transaction had an revert or error, like out of gas bool failed = 2; + // a map of each opcode to the sum of the gas consumption categorized by dimension for that opcode map dimensions = 3; + // the hash of the transaction string tx_hash = 4; + // the timestamp of the block uint64 block_timestamp = 5; - string block_number = 6; // Using string to represent big.Int + // the block number of the transaction + // Using string to represent big.Int + string block_number = 6; } \ No newline at end of file diff --git a/eth/tracers/native/tx_gas_dimension_by_opcode.go b/eth/tracers/native/tx_gas_dimension_by_opcode.go index dfe66d1e91..550a83d955 100644 --- a/eth/tracers/native/tx_gas_dimension_by_opcode.go +++ b/eth/tracers/native/tx_gas_dimension_by_opcode.go @@ -14,7 +14,7 @@ import ( // initializer for the tracer func init() { - tracers.DefaultDirectory.Register("txGasDimensionByOpcode", NewTxGasDimensionByOpcodeLogger, false) + tracers.DefaultDirectory.Register("txGasDimensionByOpcode", NewTxGasDimensionByOpcodeTracer, false) } // gasDimensionTracer struct @@ -26,14 +26,14 @@ type TxGasDimensionByOpcodeTracer struct { // gasDimensionTracer returns a new tracer that traces gas // usage for each opcode against the dimension of that opcode // takes a context, and json input for configuration parameters -func NewTxGasDimensionByOpcodeLogger( +func NewTxGasDimensionByOpcodeTracer( _ *tracers.Context, _ json.RawMessage, - _ *params.ChainConfig, + chainConfig *params.ChainConfig, ) (*tracers.Tracer, error) { t := &TxGasDimensionByOpcodeTracer{ - BaseGasDimensionTracer: NewBaseGasDimensionTracer(), + BaseGasDimensionTracer: NewBaseGasDimensionTracer(chainConfig), OpcodeToDimensions: make(map[vm.OpCode]GasesByDimension), } @@ -153,7 +153,10 @@ func (t *TxGasDimensionByOpcodeTracer) GetProtobufResult() ([]byte, error) { } executionResult := &proto.TxGasDimensionByOpcodeExecutionResult{ - Gas: baseExecutionResult.Gas, + GasUsed: baseExecutionResult.GasUsed, + GasUsedL1: baseExecutionResult.GasUsedForL1, + GasUsedL2: baseExecutionResult.GasUsedForL2, + IntrinsicGas: baseExecutionResult.IntrinsicGas, Failed: baseExecutionResult.Failed, Dimensions: make(map[uint32]*proto.GasesByDimension), TxHash: baseExecutionResult.TxHash, @@ -169,6 +172,7 @@ func (t *TxGasDimensionByOpcodeTracer) GetProtobufResult() ([]byte, error) { StateGrowth: dimensions.StateGrowth, HistoryGrowth: dimensions.HistoryGrowth, StateGrowthRefund: dimensions.StateGrowthRefund, + ChildExecutionCost: dimensions.ChildExecutionCost, } } diff --git a/eth/tracers/native/tx_gas_dimension_logger.go b/eth/tracers/native/tx_gas_dimension_logger.go index 4ff4d853d2..10ef67b22f 100644 --- a/eth/tracers/native/tx_gas_dimension_logger.go +++ b/eth/tracers/native/tx_gas_dimension_logger.go @@ -28,7 +28,7 @@ type DimensionLog struct { HistoryGrowth uint64 `json:"historyGrowth"` StateGrowthRefund int64 `json:"stateGrowthRefund"` CallRealGas uint64 `json:"callRealGas"` - CallExecutionCost uint64 `json:"callExecutionCost"` + ChildExecutionCost uint64 `json:"callExecutionCost"` CallMemoryExpansion uint64 `json:"callMemoryExpansion"` CreateInitCodeCost uint64 `json:"createInitCodeCost"` Create2HashCost uint64 `json:"create2HashCost"` @@ -55,11 +55,11 @@ type TxGasDimensionLogger struct { func NewTxGasDimensionLogger( _ *tracers.Context, _ json.RawMessage, - _ *params.ChainConfig, + chainConfig *params.ChainConfig, ) (*tracers.Tracer, error) { t := &TxGasDimensionLogger{ - BaseGasDimensionTracer: NewBaseGasDimensionTracer(), + BaseGasDimensionTracer: NewBaseGasDimensionTracer(chainConfig), logs: make([]DimensionLog, 0), } @@ -127,7 +127,7 @@ func (t *TxGasDimensionLogger) OnOpcode( callDimensionLog.HistoryGrowth = finishGasesByDimension.HistoryGrowth callDimensionLog.StateGrowthRefund = finishGasesByDimension.StateGrowthRefund callDimensionLog.CallRealGas = gasUsedByCall - callDimensionLog.CallExecutionCost = stackInfo.ExecutionCost + callDimensionLog.ChildExecutionCost = finishGasesByDimension.ChildExecutionCost callDimensionLog.CallMemoryExpansion = stackInfo.GasDimensionInfo.MemoryExpansionCost callDimensionLog.CreateInitCodeCost = stackInfo.GasDimensionInfo.InitCodeCost callDimensionLog.Create2HashCost = stackInfo.GasDimensionInfo.HashCost @@ -197,7 +197,7 @@ type DimensionLogRes struct { HistoryGrowth uint64 `json:"history,omitempty"` StateGrowthRefund int64 `json:"refund,omitempty"` CallRealGas uint64 `json:"callRealGas,omitempty"` - CallExecutionCost uint64 `json:"callExecutionCost,omitempty"` + ChildExecutionCost uint64 `json:"childExecutionCost,omitempty"` CallMemoryExpansion uint64 `json:"callMemoryExpansion,omitempty"` CreateInitCodeCost uint64 `json:"createInitCodeCost,omitempty"` Create2HashCost uint64 `json:"create2HashCost,omitempty"` @@ -219,7 +219,7 @@ func (d *DimensionLogRes) DebugString() string { d.HistoryGrowth, d.StateGrowthRefund, d.CallRealGas, - d.CallExecutionCost, + d.ChildExecutionCost, d.CallMemoryExpansion, d.CreateInitCodeCost, d.Create2HashCost, @@ -242,7 +242,7 @@ func formatLogs(logs []DimensionLog) []DimensionLogRes { HistoryGrowth: trace.HistoryGrowth, StateGrowthRefund: trace.StateGrowthRefund, CallRealGas: trace.CallRealGas, - CallExecutionCost: trace.CallExecutionCost, + ChildExecutionCost: trace.ChildExecutionCost, CallMemoryExpansion: trace.CallMemoryExpansion, CreateInitCodeCost: trace.CreateInitCodeCost, Create2HashCost: trace.Create2HashCost, From 4bfd741bef11c0d37f76f2c69710b1ae8cd64e39 Mon Sep 17 00:00:00 2001 From: relyt29 Date: Wed, 7 May 2025 15:42:44 -0400 Subject: [PATCH 33/35] gas dimension tracing: handle gas refund cap adjustment --- .../native/base_gas_dimension_tracer.go | 28 +++++++++++++++++++ .../proto/gas_dimension_by_opcode.pb.go | 15 ++++++++-- .../proto/gas_dimension_by_opcode.proto | 2 ++ .../native/tx_gas_dimension_by_opcode.go | 6 ++++ eth/tracers/native/tx_gas_dimension_logger.go | 4 +++ 5 files changed, 53 insertions(+), 2 deletions(-) diff --git a/eth/tracers/native/base_gas_dimension_tracer.go b/eth/tracers/native/base_gas_dimension_tracer.go index 9b1c254a5d..cebd5a0372 100644 --- a/eth/tracers/native/base_gas_dimension_tracer.go +++ b/eth/tracers/native/base_gas_dimension_tracer.go @@ -36,6 +36,11 @@ type BaseGasDimensionTracer struct { prevAccessListSlots []map[common.Hash]struct{} // the amount of refund accumulated at the current step of execution refundAccumulated uint64 + // in order to calculate the refund adjusted, we need to know the total execution gas + // of just the opcodes of the transaction with no refunds + executionGasAccumulated uint64 + // the amount of refund allowed at the end of the transaction, adjusted by EIP-3529 + refundAdjusted uint64 // whether the transaction had an error, like out of gas err error // whether the tracer itself was interrupted @@ -254,6 +259,7 @@ func (t *BaseGasDimensionTracer) OnTxEnd(receipt *types.Receipt, err error) { t.gasUsedForL1 = receipt.GasUsedForL1 t.gasUsedForL2 = receipt.GasUsedForL2() t.txHash = receipt.TxHash + t.refundAdjusted = t.adjustRefund(t.executionGasAccumulated+t.intrinsicGas, t.GetRefundAccumulated()) } // Stop signals the tracer to stop tracing @@ -300,6 +306,26 @@ func (t *BaseGasDimensionTracer) GetPrevAccessList() (addresses map[common.Addre // Error returns the VM error captured by the trace func (t *BaseGasDimensionTracer) Error() error { return t.err } +// Add to the execution gas accumulated, for tracking adjusted refund +func (t *BaseGasDimensionTracer) AddToExecutionGasAccumulated(gas uint64) { + t.executionGasAccumulated += gas +} + +// this function implements the EIP-3529 refund adjustment +// this is a copy of the logic in the state transition function +func (t *BaseGasDimensionTracer) adjustRefund(gasUsedByL2BeforeRefunds, refund uint64) uint64 { + var refundAdjusted uint64 + if !t.chainConfig.IsLondon(t.env.BlockNumber) { + refundAdjusted = gasUsedByL2BeforeRefunds / params.RefundQuotient + } else { + refundAdjusted = gasUsedByL2BeforeRefunds / params.RefundQuotientEIP3529 + } + if refundAdjusted > refund { + return refund + } + return refundAdjusted +} + // ############################################################################ // OUTPUTS // ############################################################################ @@ -310,6 +336,7 @@ type BaseExecutionResult struct { GasUsedForL1 uint64 `json:"gasUsedForL1"` GasUsedForL2 uint64 `json:"gasUsedForL2"` IntrinsicGas uint64 `json:"intrinsicGas"` + AdjustedRefund uint64 `json:"adjustedRefund"` Failed bool `json:"failed"` TxHash string `json:"txHash"` BlockTimestamp uint64 `json:"blockTimestamp"` @@ -329,6 +356,7 @@ func (t *BaseGasDimensionTracer) GetBaseExecutionResult() (BaseExecutionResult, GasUsedForL1: t.gasUsedForL1, GasUsedForL2: t.gasUsedForL2, IntrinsicGas: t.intrinsicGas, + AdjustedRefund: t.refundAdjusted, Failed: failed, TxHash: t.txHash.Hex(), BlockTimestamp: t.env.Time, diff --git a/eth/tracers/native/proto/gas_dimension_by_opcode.pb.go b/eth/tracers/native/proto/gas_dimension_by_opcode.pb.go index 7d2d0f6e66..b9b556491a 100644 --- a/eth/tracers/native/proto/gas_dimension_by_opcode.pb.go +++ b/eth/tracers/native/proto/gas_dimension_by_opcode.pb.go @@ -132,6 +132,8 @@ type TxGasDimensionByOpcodeExecutionResult struct { GasUsedL2 uint64 `protobuf:"varint,8,opt,name=gas_used_l2,json=gasUsedL2,proto3" json:"gas_used_l2,omitempty"` // the intrinsic gas of the transaction, the static cost + calldata bytes cost IntrinsicGas uint64 `protobuf:"varint,9,opt,name=intrinsic_gas,json=intrinsicGas,proto3" json:"intrinsic_gas,omitempty"` + // the adjusted gas refund amount after EIP-3529 + AdjustedRefund uint64 `protobuf:"varint,10,opt,name=adjusted_refund,json=adjustedRefund,proto3" json:"adjusted_refund,omitempty"` // whether the transaction had an revert or error, like out of gas Failed bool `protobuf:"varint,2,opt,name=failed,proto3" json:"failed,omitempty"` // a map of each opcode to the sum of the gas consumption categorized by dimension for that opcode @@ -205,6 +207,13 @@ func (x *TxGasDimensionByOpcodeExecutionResult) GetIntrinsicGas() uint64 { return 0 } +func (x *TxGasDimensionByOpcodeExecutionResult) GetAdjustedRefund() uint64 { + if x != nil { + return x.AdjustedRefund + } + return 0 +} + func (x *TxGasDimensionByOpcodeExecutionResult) GetFailed() bool { if x != nil { return x.Failed @@ -252,12 +261,14 @@ const file_eth_tracers_native_proto_gas_dimension_by_opcode_proto_rawDesc = "" + "\fstate_growth\x18\x04 \x01(\x04R\vstateGrowth\x12%\n" + "\x0ehistory_growth\x18\x05 \x01(\x04R\rhistoryGrowth\x12.\n" + "\x13state_growth_refund\x18\x06 \x01(\x03R\x11stateGrowthRefund\x120\n" + - "\x14child_execution_cost\x18\a \x01(\x04R\x12childExecutionCost\"\x80\x04\n" + + "\x14child_execution_cost\x18\a \x01(\x04R\x12childExecutionCost\"\xa9\x04\n" + "%TxGasDimensionByOpcodeExecutionResult\x12\x19\n" + "\bgas_used\x18\x01 \x01(\x04R\agasUsed\x12\x1e\n" + "\vgas_used_l1\x18\a \x01(\x04R\tgasUsedL1\x12\x1e\n" + "\vgas_used_l2\x18\b \x01(\x04R\tgasUsedL2\x12#\n" + - "\rintrinsic_gas\x18\t \x01(\x04R\fintrinsicGas\x12\x16\n" + + "\rintrinsic_gas\x18\t \x01(\x04R\fintrinsicGas\x12'\n" + + "\x0fadjusted_refund\x18\n" + + " \x01(\x04R\x0eadjustedRefund\x12\x16\n" + "\x06failed\x18\x02 \x01(\bR\x06failed\x12o\n" + "\n" + "dimensions\x18\x03 \x03(\v2O.eth.tracers.native.proto.TxGasDimensionByOpcodeExecutionResult.DimensionsEntryR\n" + diff --git a/eth/tracers/native/proto/gas_dimension_by_opcode.proto b/eth/tracers/native/proto/gas_dimension_by_opcode.proto index 608509ff33..18ef64c662 100644 --- a/eth/tracers/native/proto/gas_dimension_by_opcode.proto +++ b/eth/tracers/native/proto/gas_dimension_by_opcode.proto @@ -32,6 +32,8 @@ message TxGasDimensionByOpcodeExecutionResult { uint64 gas_used_l2 = 8; // the intrinsic gas of the transaction, the static cost + calldata bytes cost uint64 intrinsic_gas = 9; + // the adjusted gas refund amount after EIP-3529 + uint64 adjusted_refund = 10; // whether the transaction had an revert or error, like out of gas bool failed = 2; // a map of each opcode to the sum of the gas consumption categorized by dimension for that opcode diff --git a/eth/tracers/native/tx_gas_dimension_by_opcode.go b/eth/tracers/native/tx_gas_dimension_by_opcode.go index 550a83d955..4aa741ed08 100644 --- a/eth/tracers/native/tx_gas_dimension_by_opcode.go +++ b/eth/tracers/native/tx_gas_dimension_by_opcode.go @@ -73,6 +73,8 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( if WasCallOrCreate(opcode) && err == nil { t.handleCallStackPush(callStackInfo) } else { + // track the execution gas of all opcodes (but not the opcodes that do calls) + t.AddToExecutionGasAccumulated(gasesByDimension.OneDimensionalGasCost) // update the aggregrate map for this opcode accumulatedDimensions := t.OpcodeToDimensions[opcode] @@ -97,6 +99,9 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( return } + // track the execution gas of all opcodes that do calls + t.AddToExecutionGasAccumulated(finishGasesByDimension.OneDimensionalGasCost) + accumulatedDimensionsCall := t.OpcodeToDimensions[stackInfo.GasDimensionInfo.Op] accumulatedDimensionsCall.OneDimensionalGasCost += finishGasesByDimension.OneDimensionalGasCost @@ -157,6 +162,7 @@ func (t *TxGasDimensionByOpcodeTracer) GetProtobufResult() ([]byte, error) { GasUsedL1: baseExecutionResult.GasUsedForL1, GasUsedL2: baseExecutionResult.GasUsedForL2, IntrinsicGas: baseExecutionResult.IntrinsicGas, + AdjustedRefund: baseExecutionResult.AdjustedRefund, Failed: baseExecutionResult.Failed, Dimensions: make(map[uint32]*proto.GasesByDimension), TxHash: baseExecutionResult.TxHash, diff --git a/eth/tracers/native/tx_gas_dimension_logger.go b/eth/tracers/native/tx_gas_dimension_logger.go index 10ef67b22f..f81b9eb4a0 100644 --- a/eth/tracers/native/tx_gas_dimension_logger.go +++ b/eth/tracers/native/tx_gas_dimension_logger.go @@ -114,11 +114,15 @@ func (t *TxGasDimensionLogger) OnOpcode( if WasCallOrCreate(opcode) && err == nil { t.handleCallStackPush(callStackInfo) } else { + // track the execution gas of all opcodes (but not the opcodes that do calls) + t.AddToExecutionGasAccumulated(gasesByDimension.OneDimensionalGasCost) if depth < t.depth { interrupted, gasUsedByCall, stackInfo, finishGasesByDimension := t.callFinishFunction(pc, depth, gas) if interrupted { return } + // track the execution gas of all opcodes that do calls + t.AddToExecutionGasAccumulated(finishGasesByDimension.OneDimensionalGasCost) callDimensionLog := t.logs[stackInfo.DimensionLogPosition] callDimensionLog.OneDimensionalGasCost = finishGasesByDimension.OneDimensionalGasCost callDimensionLog.Computation = finishGasesByDimension.Computation From 9be91066b080353b5e4006e98ee487d0b3bb3519 Mon Sep 17 00:00:00 2001 From: relyt29 Date: Thu, 8 May 2025 12:06:03 -0400 Subject: [PATCH 34/35] gas dimension tracing: linter issues, exhaustruct on gas dimension work --- eth/tracers/live/tx_gas_dimension_logger.go | 1 - .../native/base_gas_dimension_tracer.go | 71 ++++++++++++++++--- eth/tracers/native/gas_dimension_calc.go | 12 ++-- .../native/tx_gas_dimension_by_opcode.go | 3 +- eth/tracers/native/tx_gas_dimension_logger.go | 14 ++-- 5 files changed, 77 insertions(+), 24 deletions(-) diff --git a/eth/tracers/live/tx_gas_dimension_logger.go b/eth/tracers/live/tx_gas_dimension_logger.go index 9fbb016574..969b2182c7 100644 --- a/eth/tracers/live/tx_gas_dimension_logger.go +++ b/eth/tracers/live/tx_gas_dimension_logger.go @@ -79,7 +79,6 @@ func (t *txGasDimensionLiveTraceLogger) OnTxEnd( receipt *types.Receipt, err error, ) { - // first call the native tracer's OnTxEnd t.GasDimensionTracer.OnTxEnd(receipt, err) diff --git a/eth/tracers/native/base_gas_dimension_tracer.go b/eth/tracers/native/base_gas_dimension_tracer.go index cebd5a0372..139639c2d5 100644 --- a/eth/tracers/native/base_gas_dimension_tracer.go +++ b/eth/tracers/native/base_gas_dimension_tracer.go @@ -58,6 +58,18 @@ func NewBaseGasDimensionTracer(chainConfig *params.ChainConfig) BaseGasDimension refundAccumulated: 0, prevAccessListAddresses: map[common.Address]int{}, prevAccessListSlots: []map[common.Hash]struct{}{}, + env: nil, + txHash: common.Hash{}, + gasUsed: 0, + gasUsedForL1: 0, + gasUsedForL2: 0, + intrinsicGas: 0, + callStack: CallGasDimensionStack{}, + executionGasAccumulated: 0, + refundAdjusted: 0, + err: nil, + interrupt: atomic.Bool{}, + reason: nil, } } @@ -96,7 +108,7 @@ func (t *BaseGasDimensionTracer) onOpcodeStart( opcode vm.OpCode, ) { if t.interrupt.Load() { - return true, GasesByDimension{}, nil, vm.OpCode(op) + return true, zeroGasesByDimension(), nil, vm.OpCode(op) } if depth != t.depth && depth != t.depth-1 { t.interrupt.Store(true) @@ -107,7 +119,7 @@ func (t *BaseGasDimensionTracer) onOpcodeStart( depth, t.callStack, ) - return true, GasesByDimension{}, nil, vm.OpCode(op) + return true, zeroGasesByDimension(), nil, vm.OpCode(op) } if t.depth != len(t.callStack)+1 { t.interrupt.Store(true) @@ -118,18 +130,18 @@ func (t *BaseGasDimensionTracer) onOpcodeStart( len(t.callStack), t.callStack, ) - return true, GasesByDimension{}, nil, vm.OpCode(op) + return true, zeroGasesByDimension(), nil, vm.OpCode(op) } // get the gas dimension function // if it's not a call, directly calculate the gas dimensions for the opcode f := GetCalcGasDimensionFunc(vm.OpCode(op)) - var fErr error = nil + var fErr error gasesByDimension, callStackInfo, fErr = f(t, pc, op, gas, cost, scope, rData, depth, err) if fErr != nil { t.interrupt.Store(true) t.reason = fErr - return true, GasesByDimension{}, nil, vm.OpCode(op) + return true, zeroGasesByDimension(), nil, vm.OpCode(op) } opcode = vm.OpCode(op) @@ -141,7 +153,7 @@ func (t *BaseGasDimensionTracer) onOpcodeStart( opcode.String(), callStackInfo, ) - return true, GasesByDimension{}, nil, vm.OpCode(op) + return true, zeroGasesByDimension(), nil, vm.OpCode(op) } return false, gasesByDimension, callStackInfo, opcode } @@ -181,7 +193,7 @@ func (t *BaseGasDimensionTracer) callFinishFunction( if !ok { t.interrupt.Store(true) t.reason = fmt.Errorf("call stack is unexpectedly empty %d %d %d", pc, depth, t.depth) - return true, 0, CallGasDimensionStackInfo{}, GasesByDimension{} + return true, 0, zeroCallGasDimensionStackInfo(), zeroGasesByDimension() } finishFunction := GetFinishCalcGasDimensionFunc(stackInfo.GasDimensionInfo.Op) if finishFunction == nil { @@ -191,18 +203,18 @@ func (t *BaseGasDimensionTracer) callFinishFunction( stackInfo.GasDimensionInfo.Op.String(), pc, ) - return true, 0, CallGasDimensionStackInfo{}, GasesByDimension{} + return true, 0, zeroCallGasDimensionStackInfo(), zeroGasesByDimension() } // IMPORTANT NOTE: for some reason the only reliable way to actually get the gas cost of the call // is to subtract gas at time of call from gas at opcode AFTER return // you can't trust the `gas` field on the call itself. I wonder if the gas field is an estimation gasUsedByCall = stackInfo.GasDimensionInfo.GasCounterAtTimeOfCall - gas - var finishErr error = nil + var finishErr error finishGasesByDimension, finishErr = finishFunction(gasUsedByCall, stackInfo.ExecutionCost, stackInfo.GasDimensionInfo) if finishErr != nil { t.interrupt.Store(true) t.reason = finishErr - return true, 0, CallGasDimensionStackInfo{}, GasesByDimension{} + return true, 0, zeroCallGasDimensionStackInfo(), zeroGasesByDimension() } return false, gasUsedByCall, stackInfo, finishGasesByDimension } @@ -211,7 +223,7 @@ func (t *BaseGasDimensionTracer) callFinishFunction( // then we need to track the execution gas // of our own code so that when the call returns, // we can write the gas dimensions for the call opcode itself -func (t *BaseGasDimensionTracer) updateExecutionCost(cost uint64) { +func (t *BaseGasDimensionTracer) updateCallChildExecutionCost(cost uint64) { if len(t.callStack) > 0 { t.callStack.UpdateExecutionCost(cost) } @@ -326,6 +338,43 @@ func (t *BaseGasDimensionTracer) adjustRefund(gasUsedByL2BeforeRefunds, refund u return refundAdjusted } +// zeroGasesByDimension returns a GasesByDimension struct with all fields set to zero +func zeroGasesByDimension() GasesByDimension { + return GasesByDimension{ + OneDimensionalGasCost: 0, + Computation: 0, + StateAccess: 0, + StateGrowth: 0, + HistoryGrowth: 0, + StateGrowthRefund: 0, + ChildExecutionCost: 0, + } +} + +// zeroCallGasDimensionInfo returns a CallGasDimensionInfo struct with all fields set to zero +func zeroCallGasDimensionInfo() CallGasDimensionInfo { + return CallGasDimensionInfo{ + Pc: 0, + Op: 0, + GasCounterAtTimeOfCall: 0, + MemoryExpansionCost: 0, + AccessListComputationCost: 0, + AccessListStateAccessCost: 0, + IsValueSentWithCall: false, + InitCodeCost: 0, + HashCost: 0, + } +} + +// zeroCallGasDimensionStackInfo returns a CallGasDimensionStackInfo struct with all fields set to zero +func zeroCallGasDimensionStackInfo() CallGasDimensionStackInfo { + return CallGasDimensionStackInfo{ + GasDimensionInfo: zeroCallGasDimensionInfo(), + ExecutionCost: 0, + DimensionLogPosition: 0, + } +} + // ############################################################################ // OUTPUTS // ############################################################################ diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index ea9d510eb5..83f306f195 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -61,7 +61,7 @@ func (c *CallGasDimensionStack) Push(info CallGasDimensionStackInfo) { // gasDim // Pop a CallGasDimensionStackInfo from the stack, returning false if the stack is empty func (c *CallGasDimensionStack) Pop() (CallGasDimensionStackInfo, bool) { if len(*c) == 0 { - return CallGasDimensionStackInfo{}, false + return zeroCallGasDimensionStackInfo(), false } last := (*c)[len(*c)-1] *c = (*c)[:len(*c)-1] @@ -389,8 +389,8 @@ func calcStateReadCallGas( } var memExpansionCost uint64 = 0 - var memErr error = nil if memExpansionOffset+memExpansionSize != 0 { + var memErr error memExpansionCost, memErr = memoryExpansionCost(scope.MemoryData(), memExpansionOffset, memExpansionSize) if memErr != nil { return GasesByDimension{}, nil, memErr @@ -668,8 +668,8 @@ func calcReadAndStoreCallGas( } var memExpansionCost uint64 = 0 - var memErr error = nil if memExpansionOffset+memExpansionSize != 0 { + var memErr error memExpansionCost, memErr = memoryExpansionCost(scope.MemoryData(), memExpansionOffset, memExpansionSize) if memErr != nil { return GasesByDimension{}, nil, memErr @@ -836,9 +836,8 @@ func calcSStoreGas( } t.SetRefundAccumulated(currentRefund) } - ret := GasesByDimension{ - OneDimensionalGasCost: cost, - } + ret := zeroGasesByDimension() + ret.OneDimensionalGasCost = cost if cost >= params.SstoreSetGas { // 22100 case and 20000 case accessCost := cost - params.SstoreSetGas ret = GasesByDimension{ @@ -1138,5 +1137,6 @@ func outOfGas(gas uint64) (GasesByDimension, *CallGasDimensionInfo, error) { StateGrowth: 0, HistoryGrowth: 0, StateGrowthRefund: 0, + ChildExecutionCost: 0, }, nil, nil } diff --git a/eth/tracers/native/tx_gas_dimension_by_opcode.go b/eth/tracers/native/tx_gas_dimension_by_opcode.go index 4aa741ed08..bc985dc516 100644 --- a/eth/tracers/native/tx_gas_dimension_by_opcode.go +++ b/eth/tracers/native/tx_gas_dimension_by_opcode.go @@ -31,7 +31,6 @@ func NewTxGasDimensionByOpcodeTracer( _ json.RawMessage, chainConfig *params.ChainConfig, ) (*tracers.Tracer, error) { - t := &TxGasDimensionByOpcodeTracer{ BaseGasDimensionTracer: NewBaseGasDimensionTracer(chainConfig), OpcodeToDimensions: make(map[vm.OpCode]GasesByDimension), @@ -114,7 +113,7 @@ func (t *TxGasDimensionByOpcodeTracer) OnOpcode( t.depth -= 1 } - t.updateExecutionCost(gasesByDimension.OneDimensionalGasCost) + t.updateCallChildExecutionCost(gasesByDimension.OneDimensionalGasCost) } addresses, slots := t.env.StateDB.GetAccessList() t.updatePrevAccessList(addresses, slots) diff --git a/eth/tracers/native/tx_gas_dimension_logger.go b/eth/tracers/native/tx_gas_dimension_logger.go index f81b9eb4a0..a07ade0ae7 100644 --- a/eth/tracers/native/tx_gas_dimension_logger.go +++ b/eth/tracers/native/tx_gas_dimension_logger.go @@ -57,7 +57,6 @@ func NewTxGasDimensionLogger( _ json.RawMessage, chainConfig *params.ChainConfig, ) (*tracers.Tracer, error) { - t := &TxGasDimensionLogger{ BaseGasDimensionTracer: NewBaseGasDimensionTracer(chainConfig), logs: make([]DimensionLog, 0), @@ -89,7 +88,7 @@ func (t *TxGasDimensionLogger) OnOpcode( err error, ) { interrupted, gasesByDimension, callStackInfo, opcode := t.onOpcodeStart(pc, op, gas, cost, scope, rData, depth, err) - // if an error occured, it was stored in the tracer's reason field + // if an error occurred, it was stored in the tracer's reason field // and we should return immediately if interrupted { return @@ -105,7 +104,14 @@ func (t *TxGasDimensionLogger) OnOpcode( StateGrowth: gasesByDimension.StateGrowth, HistoryGrowth: gasesByDimension.HistoryGrowth, StateGrowthRefund: gasesByDimension.StateGrowthRefund, - Err: err, + ChildExecutionCost: gasesByDimension.ChildExecutionCost, + // the following are considered unknown at this point in the tracer lifecycle + // and are only filled in after the finish function is called + CallRealGas: 0, + CallMemoryExpansion: 0, + CreateInitCodeCost: 0, + Create2HashCost: 0, + Err: err, }) // if callStackInfo is not nil then we need to take a note of the index of the @@ -140,7 +146,7 @@ func (t *TxGasDimensionLogger) OnOpcode( t.depth -= 1 } - t.updateExecutionCost(gasesByDimension.OneDimensionalGasCost) + t.updateCallChildExecutionCost(gasesByDimension.OneDimensionalGasCost) } addresses, slots := t.env.StateDB.GetAccessList() t.updatePrevAccessList(addresses, slots) From 329f31d96c80d8cda36241dddbbacc7a98277b03 Mon Sep 17 00:00:00 2001 From: relyt29 Date: Fri, 9 May 2025 15:52:57 -0400 Subject: [PATCH 35/35] gas dimension tracing: use descriptive names for tracer output --- eth/tracers/native/gas_dimension_calc.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/eth/tracers/native/gas_dimension_calc.go b/eth/tracers/native/gas_dimension_calc.go index 83f306f195..9040d0c51a 100644 --- a/eth/tracers/native/gas_dimension_calc.go +++ b/eth/tracers/native/gas_dimension_calc.go @@ -13,13 +13,13 @@ import ( // GasesByDimension is a type that represents the gas consumption for each dimension // for a given opcode. type GasesByDimension struct { - OneDimensionalGasCost uint64 `json:"g1"` + OneDimensionalGasCost uint64 `json:"gas1d"` Computation uint64 `json:"cpu"` StateAccess uint64 `json:"rw,omitempty"` - StateGrowth uint64 `json:"gr,omitempty"` - HistoryGrowth uint64 `json:"h,omitempty"` - StateGrowthRefund int64 `json:"rf,omitempty"` - ChildExecutionCost uint64 `json:"exc,omitempty"` + StateGrowth uint64 `json:"growth,omitempty"` + HistoryGrowth uint64 `json:"hist,omitempty"` + StateGrowthRefund int64 `json:"refund,omitempty"` + ChildExecutionCost uint64 `json:"childcost,omitempty"` } // in the case of opcodes like CALL, STATICCALL, DELEGATECALL, etc,