Skip to content

Commit 0029a4a

Browse files
committed
Add flag to count opcodes during execution
1 parent 87a9e65 commit 0029a4a

File tree

6 files changed

+196
-5
lines changed

6 files changed

+196
-5
lines changed

src/ethereum_spec_tools/evm_tools/daemon.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ def do_POST(self) -> None:
8080
]
8181
)
8282

83+
count_opcodes = content.get("count-opcodes", False)
84+
if count_opcodes:
85+
# send full opcode counts if ``count-opcodes`` is ``True``
86+
args.extend(["--opcodes.count", "stdout"])
87+
8388
query_string = urlparse(self.path).query
8489
if query_string:
8590
query = parse_qs(

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_))
158+
if found is None:
159+
raise Exception("no tracer 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: dict[str, int]
19+
20+
def __init__(self) -> None:
21+
self.transaction_environment = None
22+
self.active_traces = {}
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+
Take and clear the current opcode counts.
42+
"""
43+
results = self.active_traces
44+
self.active_traces = {}
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/conftest.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
import shutil
33
import tarfile
44
from pathlib import Path
5-
from typing import Final, Optional, Set
5+
from typing import Callable, Final, Optional, Set
66

77
import git
88
import requests_cache
99
from _pytest.config import Config
1010
from _pytest.config.argparsing import Parser
1111
from filelock import FileLock
1212
from git.exc import GitCommandError, InvalidGitRepositoryError
13-
from pytest import Session, StashKey
13+
from pytest import Session, StashKey, fixture
1414
from requests_cache import CachedSession
1515
from requests_cache.backends.sqlite import SQLiteCache
1616
from typing_extensions import Self
@@ -26,6 +26,14 @@ def get_xdist_worker_id(request_or_session: object) -> str: # noqa: U100
2626
return "master"
2727

2828

29+
@fixture()
30+
def root_relative() -> Callable[[str | Path], Path]:
31+
def _(path: str | Path) -> Path:
32+
return Path(__file__).parent / path
33+
34+
return _
35+
36+
2937
def pytest_addoption(parser: Parser) -> None:
3038
"""
3139
Accept --evm-trace option in pytest.

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)