Skip to content

Commit 14abcb7

Browse files
authored
Merge branch 'master' into run-mode-struct
2 parents 94a0df3 + 10fe4ef commit 14abcb7

7 files changed

+450
-323
lines changed

eth/tracers/live/tx_gas_dimension_by_opcode.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,14 @@ func (t *TxGasDimensionByOpcodeLiveTracer) OnTxStart(
106106
t.skip = true
107107
return
108108
}
109+
baseGasDimensionTracer, err := native.NewBaseGasDimensionTracer(nil, t.ChainConfig)
110+
if err != nil {
111+
log.Error("Failed to create base gas dimension tracer", "error", err)
112+
return
113+
}
109114

110115
t.nativeGasByOpcodeTracer = &native.TxGasDimensionByOpcodeTracer{
111-
BaseGasDimensionTracer: native.NewBaseGasDimensionTracer(t.ChainConfig),
116+
BaseGasDimensionTracer: baseGasDimensionTracer,
112117
OpcodeToDimensions: make(map[_vm.OpCode]native.GasesByDimension),
113118
}
114119
t.nativeGasByOpcodeTracer.OnTxStart(vm, tx, from)
@@ -199,7 +204,7 @@ func (t *TxGasDimensionByOpcodeLiveTracer) OnBlockEndMetrics(blockNumber uint64,
199204

200205
// if the transaction has any kind of error, try to get as much information
201206
// as you can out of it, and then write that out to a file underneath
202-
// Path/errors/blocknumber/blocknumber_txhash.json
207+
// Path/errors/block_group/blocknumber_txhash.json
203208
func writeTxErrorToFile(t *TxGasDimensionByOpcodeLiveTracer, receipt *types.Receipt, err error, tracerError error) {
204209
var txHashStr string = "no-tx-hash"
205210
var blockNumberStr string = "no-block-number"
@@ -251,8 +256,18 @@ func writeTxErrorToFile(t *TxGasDimensionByOpcodeLiveTracer, receipt *types.Rece
251256
}
252257
}
253258

254-
// Create error directory path
255-
errorDirPath := filepath.Join(t.Path, "errors", blockNumberStr)
259+
// Create error directory path grouped by 1000 blocks
260+
var errorDirPath string
261+
if receipt == nil {
262+
// For nil receipts, use a special directory
263+
errorDirPath = filepath.Join(t.Path, "errors", "nil-receipts")
264+
} else {
265+
// Group by 1000 blocks like successful transactions
266+
blockNumber := receipt.BlockNumber.Uint64()
267+
blockGroup := (blockNumber / 1000) * 1000
268+
errorDirPath = filepath.Join(t.Path, "errors", fmt.Sprintf("%d", blockGroup))
269+
}
270+
256271
if err := os.MkdirAll(errorDirPath, 0755); err != nil {
257272
log.Error("Failed to create error directory", "path", errorDirPath, "error", err)
258273
return

eth/tracers/live/tx_gas_dimension_logger.go

Lines changed: 0 additions & 133 deletions
This file was deleted.
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2025, Offchain Labs, Inc.
3+
# For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md
4+
"""
5+
Struct Logger Call Gas Calculator
6+
7+
This script analyzes EVM struct logs to track gas usage of a particular call,
8+
at a particular program counter and depth.
9+
"""
10+
11+
import argparse
12+
import json
13+
from typing import Dict, List, Tuple, Any
14+
15+
def get_struct_logs_from_file(file_path: str) -> List[Dict[str, Any]]:
16+
with open(file_path, "r") as f:
17+
data = json.load(f)
18+
19+
# Check if structLogs exists in data['result']['structLogs']
20+
if 'result' in data and 'structLogs' in data['result']:
21+
return data['result']['structLogs']
22+
23+
# Check if structLogs exists directly in data['structLogs']
24+
if 'structLogs' in data:
25+
return data['structLogs']
26+
27+
# If neither exists, raise an error
28+
raise KeyError("structLogs not found in either data['result']['structLogs'] or data['structLogs']")
29+
30+
def parse_struct_logs(file_path: str, start_pc: int, depth: int) -> Dict[str, Any]:
31+
"""
32+
Parse struct logs and analyze gas usage for a specific PC and depth.
33+
34+
Args:
35+
file_path: Path to the struct logs JSON file
36+
start_pc: Starting program counter to monitor
37+
depth: Depth level to monitor
38+
39+
Returns:
40+
Dictionary containing analysis results
41+
"""
42+
stack_increasers = ["CALL", "CALLCODE", "DELEGATECALL", "STATICCALL", "CREATE", "CREATE2"]
43+
44+
call_stack: Dict[Tuple[int, int], Dict[str, Any]] = {}
45+
active_calls: List[Tuple[int, int]] = []
46+
gas_sum = 0
47+
gas_start = 0
48+
gas_end = 0
49+
50+
struct_logs = get_struct_logs_from_file(file_path)
51+
monitor = False
52+
53+
for log in struct_logs:
54+
if log['pc'] == start_pc and log['depth'] == depth:
55+
monitor = True
56+
print(f"+ Observed start {log['op']} at {log['pc']}, depth {log['depth']}")
57+
gas_start = log['gas']
58+
continue
59+
60+
if log['pc'] == start_pc + 1 and log['depth'] == depth:
61+
monitor = False
62+
print(f"+ Observed end {log['op']} at {log['pc']}, depth {log['depth']}")
63+
gas_end = log['gas']
64+
break
65+
66+
if monitor:
67+
current_depth = log['depth']
68+
current_pc = log['pc']
69+
70+
if current_depth != depth + 1:
71+
continue
72+
73+
if log['op'] in stack_increasers:
74+
call_info = {
75+
'pc': current_pc,
76+
'depth': current_depth,
77+
'gas_at_call': log['gas'],
78+
'op': log['op']
79+
}
80+
call_stack[(current_pc, current_depth)] = call_info
81+
active_calls.append((current_pc, current_depth))
82+
print(f"Call made at PC {current_pc}, depth {current_depth}, gas {log['gas']}")
83+
continue
84+
else:
85+
gas_sum += log['gasCost']
86+
87+
for call_key in active_calls:
88+
call_pc, call_depth = call_key
89+
if current_pc == call_pc + 1:
90+
call_info = call_stack.pop(call_key)
91+
gas_used = call_info['gas_at_call'] - log['gas']
92+
call_info['gas_used'] = gas_used
93+
call_info['return_pc'] = current_pc
94+
call_info['return_depth'] = current_depth
95+
call_stack[call_key] = call_info
96+
active_calls.remove(call_key)
97+
print(f"Returned from call at PC {call_pc}, depth {call_depth}, gas used: {gas_used}, gas left: {log['gas']}")
98+
99+
# Add gas from completed calls
100+
for _, call_info in call_stack.items():
101+
if 'gas_used' in call_info:
102+
gas_sum += call_info['gas_used']
103+
104+
return {
105+
'gas_sum': gas_sum,
106+
'gas_start': gas_start,
107+
'gas_end': gas_end,
108+
'call_stack': call_stack
109+
}
110+
111+
112+
def print_results(results: Dict[str, Any]) -> None:
113+
"""
114+
Print the analysis results in a formatted way.
115+
116+
Args:
117+
results: Dictionary containing analysis results
118+
"""
119+
gas_sum = results['gas_sum']
120+
gas_start = results['gas_start']
121+
gas_end = results['gas_end']
122+
call_stack = results['call_stack']
123+
124+
print(f"> Execution gas used: {gas_sum}")
125+
gas_total = gas_start - gas_end
126+
print(f"> Total gas used: {gas_total} ({gas_start} - {gas_end})")
127+
print(f"> Non Execution gas used: {gas_total - gas_sum}")
128+
print(f"Call stack details:")
129+
130+
for call_key, call_info in call_stack.items():
131+
print(f" Call at PC {call_info['pc']}, depth {call_info['depth']}: {call_info['op']}")
132+
if 'gas_used' in call_info:
133+
print(f" Gas used: {call_info['gas_used']}")
134+
print(f" Returned at PC {call_info['return_pc']}, depth {call_info['return_depth']}")
135+
else:
136+
print(f" Still active")
137+
138+
139+
def main() -> None:
140+
"""Main function to parse command line arguments and run the analysis."""
141+
parser = argparse.ArgumentParser(
142+
description="Parse EVM struct logs to analyze gas usage and call stack",
143+
formatter_class=argparse.ArgumentDefaultsHelpFormatter
144+
)
145+
parser.add_argument(
146+
"file",
147+
help="Path to the struct logs JSON file"
148+
)
149+
parser.add_argument(
150+
"--start-pc",
151+
type=int,
152+
default=15413,
153+
help="Starting program counter to monitor"
154+
)
155+
parser.add_argument(
156+
"--depth",
157+
type=int,
158+
default=3,
159+
help="Depth level to monitor"
160+
)
161+
args = parser.parse_args()
162+
163+
try:
164+
results = parse_struct_logs(args.file, args.start_pc, args.depth)
165+
print_results(results)
166+
except FileNotFoundError:
167+
print(f"Error: File '{args.file}' not found.")
168+
exit(1)
169+
except json.JSONDecodeError:
170+
print(f"Error: Invalid JSON in file '{args.file}'.")
171+
exit(1)
172+
except KeyError as e:
173+
print(f"Error: Missing required key '{e}' in JSON structure.")
174+
exit(1)
175+
except Exception as e:
176+
print(f"Error: {e}")
177+
exit(1)
178+
179+
180+
if __name__ == "__main__":
181+
main()

0 commit comments

Comments
 (0)