Skip to content

Commit 38e45e5

Browse files
committed
Add flag to count opcodes during execution
1 parent ea802a0 commit 38e45e5

File tree

6 files changed

+254
-51
lines changed

6 files changed

+254
-51
lines changed

src/ethereum_spec_tools/evm_tools/daemon.py

Lines changed: 58 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -42,56 +42,68 @@ def log_request(
4242
def do_POST(self) -> None:
4343
from . import main
4444

45-
content_length = int(self.headers["Content-Length"])
46-
content_bytes = self.rfile.read(content_length)
47-
content = json.loads(content_bytes)
48-
49-
input_string = json.dumps(content["input"])
50-
input = StringIO(input_string)
51-
52-
args = [
53-
"t8n",
54-
"--input.env=stdin",
55-
"--input.alloc=stdin",
56-
"--input.txs=stdin",
57-
"--output.result=stdout",
58-
"--output.body=stdout",
59-
"--output.alloc=stdout",
60-
f"--state.fork={content['state']['fork']}",
61-
f"--state.chainid={content['state']['chainid']}",
62-
f"--state.reward={content['state']['reward']}",
63-
]
64-
65-
trace = content.get("trace", False)
66-
output_basedir = content.get("output-basedir")
67-
if trace:
68-
if not output_basedir:
69-
raise ValueError(
70-
"`output-basedir` should be provided when `--trace` "
71-
"is enabled."
45+
try:
46+
content_length = int(self.headers["Content-Length"])
47+
content_bytes = self.rfile.read(content_length)
48+
content = json.loads(content_bytes)
49+
50+
input_string = json.dumps(content["input"])
51+
input = StringIO(input_string)
52+
53+
args = [
54+
"t8n",
55+
"--input.env=stdin",
56+
"--input.alloc=stdin",
57+
"--input.txs=stdin",
58+
"--output.result=stdout",
59+
"--output.body=stdout",
60+
"--output.alloc=stdout",
61+
f"--state.fork={content['state']['fork']}",
62+
f"--state.chainid={content['state']['chainid']}",
63+
f"--state.reward={content['state']['reward']}",
64+
]
65+
66+
trace = content.get("trace", False)
67+
output_basedir = content.get("output-basedir")
68+
if trace:
69+
if not output_basedir:
70+
raise ValueError(
71+
"`output-basedir` should be provided when `--trace` "
72+
"is enabled."
73+
)
74+
# send full trace output if ``trace`` is ``True``
75+
args.extend(
76+
[
77+
"--trace",
78+
"--trace.memory",
79+
"--trace.returndata",
80+
f"--output.basedir={output_basedir}",
81+
]
7282
)
73-
# send full trace output if ``trace`` is ``True``
74-
args.extend(
75-
[
76-
"--trace",
77-
"--trace.memory",
78-
"--trace.returndata",
79-
f"--output.basedir={output_basedir}",
80-
]
81-
)
8283

83-
query_string = urlparse(self.path).query
84-
if query_string:
85-
query = parse_qs(
86-
query_string,
87-
keep_blank_values=True,
88-
strict_parsing=True,
89-
errors="strict",
90-
)
91-
args += query.get("arg", [])
84+
count_opcodes = content.get("count-opcodes", False)
85+
if count_opcodes:
86+
# send opcode counts if ``count-opcodes`` is ``True``
87+
args.extend(["--opcodes.count", "stdout"])
88+
89+
query_string = urlparse(self.path).query
90+
if query_string:
91+
query = parse_qs(
92+
query_string,
93+
keep_blank_values=True,
94+
strict_parsing=True,
95+
errors="strict",
96+
)
97+
args += query.get("arg", [])
98+
except Exception as e:
99+
self.send_response(500)
100+
self.send_header("Content-Type", "text/plain")
101+
self.end_headers()
102+
self.wfile.write(str(e).encode("utf-8"))
103+
raise
92104

93105
self.send_response(200)
94-
self.send_header("Content-type", "application/octet-stream")
106+
self.send_header("Content-Type", "application/octet-stream")
95107
self.end_headers()
96108

97109
# `self.wfile` is missing the `name` attribute so it doesn't strictly

src/ethereum_spec_tools/evm_tools/t8n/__init__.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import fnmatch
77
import json
88
import os
9-
from typing import Any, TextIO
9+
from typing import Any, Final, TextIO, Type, TypeVar
1010

1111
from ethereum_rlp import rlp
1212
from ethereum_types.numeric import U64, U256, Uint
@@ -24,9 +24,13 @@
2424
parse_hex_or_int,
2525
)
2626
from .env import Env
27+
from .evm_trace.count import CountTracer
2728
from .evm_trace.eip3155 import Eip3155Tracer
29+
from .evm_trace.group import GroupTracer
2830
from .t8n_types import Alloc, Result, Txs
2931

32+
T = TypeVar("T")
33+
3034

3135
def t8n_arguments(subparsers: argparse._SubParsersAction) -> None:
3236
"""
@@ -72,12 +76,16 @@ def t8n_arguments(subparsers: argparse._SubParsersAction) -> None:
7276
t8n_parser.add_argument("--trace.nostack", action="store_true")
7377
t8n_parser.add_argument("--trace.returndata", action="store_true")
7478

79+
t8n_parser.add_argument("--opcode.count", dest="opcode_count", type=str)
80+
7581
t8n_parser.add_argument("--state-test", action="store_true")
7682

7783

7884
class T8N(Load):
7985
"""The class that carries out the transition"""
8086

87+
tracers: Final[GroupTracer | None]
88+
8189
def __init__(
8290
self, options: Any, out_file: TextIO, in_file: TextIO
8391
) -> None:
@@ -100,18 +108,33 @@ def __init__(
100108
)
101109
self.fork = ForkLoad(fork_module)
102110

111+
tracers = GroupTracer()
112+
103113
if self.options.trace:
104114
trace_memory = getattr(self.options, "trace.memory", False)
105115
trace_stack = not getattr(self.options, "trace.nostack", False)
106116
trace_return_data = getattr(self.options, "trace.returndata")
107-
trace.set_evm_trace(
117+
tracers.add(
108118
Eip3155Tracer(
109119
trace_memory=trace_memory,
110120
trace_stack=trace_stack,
111121
trace_return_data=trace_return_data,
112122
output_basedir=self.options.output_basedir,
113123
)
114124
)
125+
126+
if self.options.opcode_count is not None:
127+
tracers.add(CountTracer())
128+
129+
maybe_tracers: GroupTracer | None
130+
if tracers.tracers:
131+
trace.set_evm_trace(tracers)
132+
maybe_tracers = tracers
133+
else:
134+
maybe_tracers = None
135+
136+
self.tracers = maybe_tracers
137+
115138
self.logger = get_stream_logger("T8N")
116139

117140
super().__init__(
@@ -127,6 +150,15 @@ def __init__(
127150
self.env.block_difficulty, self.env.base_fee_per_gas
128151
)
129152

153+
def _tracer(self, type_: Type[T]) -> T:
154+
group = self.tracers
155+
if group is None:
156+
raise Exception("no tracer configured")
157+
found = next((x for x in group.tracers if isinstance(x, type_)), None)
158+
if found is None:
159+
raise Exception(f"no tracer of type `{type_}` found")
160+
return found
161+
130162
def block_environment(self) -> Any:
131163
"""
132164
Create the environment for the transaction. The keyword
@@ -310,7 +342,7 @@ def run(self) -> int:
310342
json_state = self.alloc.to_json()
311343
json_result = self.result.to_json()
312344

313-
json_output = {}
345+
json_output: dict[str, object] = {}
314346

315347
if self.options.output_body == "stdout":
316348
txs_rlp = "0x" + rlp.encode(self.txs.all_txs).hex()
@@ -347,6 +379,19 @@ def run(self) -> int:
347379
json.dump(json_result, f, indent=4)
348380
self.logger.info(f"Wrote result to {result_output_path}")
349381

382+
if self.options.opcode_count == "stdout":
383+
opcode_count_results = self._tracer(CountTracer).results()
384+
json_output["opcodeCount"] = opcode_count_results
385+
elif self.options.opcode_count is not None:
386+
opcode_count_results = self._tracer(CountTracer).results()
387+
result_output_path = os.path.join(
388+
self.options.output_basedir,
389+
self.options.opcode_count,
390+
)
391+
with open(result_output_path, "w") as f:
392+
json.dump(opcode_count_results, f, indent=4)
393+
self.logger.info(f"Wrote opcode counts to {result_output_path}")
394+
350395
if json_output:
351396
json.dump(json_output, self.out_file, indent=4)
352397

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""
2+
EVM trace implementation that counts how many times each opcode is executed.
3+
"""
4+
from collections import defaultdict
5+
6+
from ethereum.trace import EvmTracer, OpStart, TraceEvent
7+
8+
from .protocols import Evm
9+
10+
11+
class CountTracer(EvmTracer):
12+
"""
13+
EVM trace implementation that counts how many times each opcode is
14+
executed.
15+
"""
16+
17+
transaction_environment: object | None
18+
active_traces: defaultdict[str, int]
19+
20+
def __init__(self) -> None:
21+
self.transaction_environment = None
22+
self.active_traces = defaultdict(lambda: 0)
23+
24+
def __call__(self, evm: object, event: TraceEvent) -> None:
25+
"""
26+
Create a trace of the event.
27+
"""
28+
if not isinstance(event, OpStart):
29+
return
30+
31+
assert isinstance(evm, Evm)
32+
33+
if self.transaction_environment is not evm.message.tx_env:
34+
self.active_traces = defaultdict(lambda: 0)
35+
self.transaction_environment = evm.message.tx_env
36+
37+
self.active_traces[event.op.name] += 1
38+
39+
def results(self) -> dict[str, int]:
40+
"""
41+
Return and clear the current opcode counts.
42+
"""
43+
results = self.active_traces
44+
self.active_traces = defaultdict(lambda: 0)
45+
self.transaction_environment = None
46+
return results
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""
2+
EVM trace implementation that fans out to many concrete trace implementations.
3+
"""
4+
from typing import Final
5+
6+
from typing_extensions import override
7+
8+
from ethereum.trace import EvmTracer, TraceEvent
9+
10+
11+
class GroupTracer(EvmTracer):
12+
"""
13+
EVM trace implementation that fans out to many concrete trace
14+
implementations.
15+
"""
16+
17+
tracers: Final[set[EvmTracer]]
18+
19+
def __init__(self) -> None:
20+
self.tracers = set()
21+
22+
def add(self, tracer: EvmTracer) -> None:
23+
"""
24+
Insert a new tracer.
25+
"""
26+
self.tracers.add(tracer)
27+
28+
@override
29+
def __call__(
30+
self,
31+
evm: object,
32+
event: TraceEvent,
33+
) -> None:
34+
"""
35+
Record a trace event.
36+
"""
37+
for tracer in self.tracers:
38+
tracer(evm, event)

tests/evm_tools/test_count_opcodes.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import json
2+
from io import StringIO
3+
from pathlib import Path
4+
from typing import Callable
5+
6+
import pytest
7+
8+
from ethereum_spec_tools.evm_tools import create_parser
9+
from ethereum_spec_tools.evm_tools.t8n import T8N
10+
11+
parser = create_parser()
12+
13+
14+
@pytest.mark.evm_tools
15+
def test_count_opcodes(root_relative: Callable[[str | Path], Path]) -> None:
16+
base_path = root_relative(
17+
"fixtures/evm_tools_testdata/t8n/fixtures/testdata/2"
18+
)
19+
20+
options = parser.parse_args(
21+
[
22+
"t8n",
23+
f"--input.env={base_path / 'env.json'}",
24+
f"--input.alloc={base_path / 'alloc.json'}",
25+
f"--input.txs={base_path / 'txs.json'}",
26+
"--output.result=stdout",
27+
"--output.body=stdout",
28+
"--output.alloc=stdout",
29+
"--opcode.count=stdout",
30+
"--state-test",
31+
]
32+
)
33+
34+
in_file = StringIO()
35+
out_file = StringIO()
36+
37+
t8n_tool = T8N(options, out_file=out_file, in_file=in_file)
38+
exit_code = t8n_tool.run()
39+
assert 0 == exit_code
40+
41+
results = json.loads(out_file.getvalue())
42+
43+
assert results["opcodeCount"] == {
44+
"PUSH1": 5,
45+
"MSTORE8": 1,
46+
"CREATE": 1,
47+
"ADD": 1,
48+
"SELFDESTRUCT": 1,
49+
}

0 commit comments

Comments
 (0)