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