Skip to content

Commit 36f49bc

Browse files
committed
feat(types): Add traceable exceptions
1 parent 825ea2b commit 36f49bc

File tree

4 files changed

+93
-15
lines changed

4 files changed

+93
-15
lines changed

Diff for: src/ethereum_test_tools/common/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Common definitions and types.
33
"""
4+
45
from .base_types import (
56
Address,
67
Bloom,
@@ -41,6 +42,7 @@
4142
JSONEncoder,
4243
Removable,
4344
Storage,
45+
TraceableException,
4446
Transaction,
4547
Withdrawal,
4648
alloc_to_accounts,
@@ -75,6 +77,7 @@
7577
"TestParameterGroup",
7678
"TestPrivateKey",
7779
"TestPrivateKey2",
80+
"TraceableException",
7881
"Transaction",
7982
"Withdrawal",
8083
"ZeroPaddedHexNumber",

Diff for: src/ethereum_test_tools/common/types.py

+80-12
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from trie import HexaryTrie
2929

3030
from ethereum_test_forks import Fork
31+
from evm_transition_tool import EVMTransactionTrace
3132

3233
from ..exceptions import ExceptionList, TransactionException
3334
from .base_types import Address, Bytes, Hash, HexNumber, Number, ZeroPaddedHexNumber
@@ -63,6 +64,55 @@ def __repr__(self) -> str:
6364
return "auto"
6465

6566

67+
class TraceableException(Exception):
68+
"""
69+
Exception that can use a trace to provide more information.
70+
"""
71+
72+
traces: List[List[EVMTransactionTrace]] | None
73+
74+
def set_traces(self, traces: List[List[EVMTransactionTrace]]):
75+
"""
76+
Set the traces for the exception.
77+
"""
78+
self.traces = traces
79+
80+
def get_trace_context(
81+
self,
82+
*,
83+
context_address: Optional[Address] = None,
84+
opcode_name: Optional[str] = None,
85+
stack_item_0: Optional[int] = None,
86+
previous_lines: int = 0,
87+
) -> List[str]:
88+
"""
89+
Returns a list of strings with the context of the exception.
90+
"""
91+
lines = []
92+
if self.traces:
93+
for execution_trace in self.traces:
94+
for tx_trace in execution_trace:
95+
for i, trace in enumerate(tx_trace.trace_lines()):
96+
if (
97+
(context_address is None or trace.context_address == context_address)
98+
and (opcode_name is None or trace.opcode_name == opcode_name)
99+
and (
100+
stack_item_0 is None
101+
or (len(trace.stack) > 0 and trace.stack[-1] == stack_item_0)
102+
)
103+
):
104+
lines += [
105+
*[
106+
f" {dict(trace)}"
107+
for trace in tx_trace.trace_lines()[
108+
max(0, i - previous_lines) : i
109+
]
110+
],
111+
f" {dict(trace)}",
112+
]
113+
return lines
114+
115+
66116
MAX_STORAGE_KEY_VALUE = 2**256 - 1
67117
MIN_STORAGE_KEY_VALUE = -(2**255)
68118

@@ -148,23 +198,31 @@ def __str__(self):
148198
"""
149199

150200
@dataclass(kw_only=True)
151-
class MissingKey(Exception):
201+
class MissingKey(TraceableException):
152202
"""
153203
Test expected to find a storage key set but key was missing.
154204
"""
155205

206+
address: Address
156207
key: int
157208

158-
def __init__(self, key: int, *args):
209+
def __init__(self, address: Address, key: int, *args):
159210
super().__init__(args)
211+
self.address = address
160212
self.key = key
161213

162214
def __str__(self):
163-
"""Print exception string"""
164-
return "key {0} not found in storage".format(Storage.key_value_to_string(self.key))
215+
"""Print exception string lines"""
216+
lines = [
217+
f"key {Storage.key_value_to_string(self.key)} not found in"
218+
+ f"storage of {self.address}"
219+
]
220+
if self.traces:
221+
pass
222+
return "\n".join(lines)
165223

166224
@dataclass(kw_only=True)
167-
class KeyValueMismatch(Exception):
225+
class KeyValueMismatch(TraceableException):
168226
"""
169227
Test expected a certain value in a storage key but value found
170228
was different.
@@ -183,13 +241,23 @@ def __init__(self, address: Address, key: int, want: int, got: int, *args):
183241
self.got = got
184242

185243
def __str__(self):
186-
"""Print exception string"""
187-
return (
244+
"""Print exception string lines"""
245+
lines = [
188246
f"incorrect value in address {self.address} for "
189-
+ f"key {Storage.key_value_to_string(self.key)}:"
190-
+ f" want {Storage.key_value_to_string(self.want)} (dec:{self.want}),"
191-
+ f" got {Storage.key_value_to_string(self.got)} (dec:{self.got})"
192-
)
247+
+ f"key {Storage.key_value_to_string(self.key)}:",
248+
f"want: {Storage.key_value_to_string(self.want)} (dec:{self.want})",
249+
f"got {Storage.key_value_to_string(self.got)} (dec:{self.got})",
250+
]
251+
if self.traces:
252+
lines += ["", "Relevant EVM traces:"]
253+
lines += self.get_trace_context(
254+
context_address=self.address,
255+
opcode_name="SSTORE",
256+
stack_item_0=self.key,
257+
previous_lines=2,
258+
)
259+
260+
return "\n".join(lines)
193261

194262
@staticmethod
195263
def parse_key_value(input: str | int | bytes | SupportsBytes) -> int:
@@ -309,7 +377,7 @@ def must_contain(self, address: Address, other: "Storage"):
309377
if key not in self:
310378
# storage[key]==0 is equal to missing storage
311379
if other[key] != 0:
312-
raise Storage.MissingKey(key=key)
380+
raise Storage.MissingKey(address=address, key=key)
313381
elif self[key] != other[key]:
314382
raise Storage.KeyValueMismatch(
315383
address=address, key=key, want=self[key], got=other[key]

Diff for: src/ethereum_test_tools/spec/blockchain/blockchain_test.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
Hash,
2121
HeaderNonce,
2222
Number,
23+
TraceableException,
2324
Transaction,
2425
ZeroPaddedHexNumber,
2526
alloc_to_accounts,
@@ -303,12 +304,15 @@ def network_info(self, fork: Fork, eips: Optional[List[int]] = None):
303304
else fork.blockchain_test_network_name()
304305
)
305306

306-
def verify_post_state(self, alloc, traces):
307+
def verify_post_state(self, alloc, traces: Optional[List[List[EVMTransactionTrace]]] = None):
307308
"""
308309
Verifies the post alloc after all block/s or payload/s are generated.
309310
"""
310311
try:
311312
verify_post_alloc(self.post, alloc)
313+
except TraceableException as e:
314+
e.set_traces(traces)
315+
raise e
312316
except Exception as e:
313317
print_traces(traces)
314318
raise e
@@ -455,7 +459,7 @@ def make_hive_fixture(
455459
# Most clients require the header to start the sync process, so we create an empty
456460
# block on top of the last block of the test to send it as new payload and trigger the
457461
# sync process.
458-
sync_header, _, _, _, _ = self.generate_block_data(
462+
sync_header, _, _, _, _, _ = self.generate_block_data(
459463
t8n=t8n,
460464
fork=fork,
461465
block=Block(),

Diff for: src/ethereum_test_tools/spec/state/state_test.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from ethereum_test_forks import Fork
1010
from evm_transition_tool import FixtureFormats, TransitionTool
1111

12-
from ...common import Address, Alloc, Environment, Number, Transaction
12+
from ...common import Address, Alloc, Environment, Number, TraceableException, Transaction
1313
from ...common.constants import EngineAPIError
1414
from ...common.json import to_json
1515
from ..base.base_test import BaseFixture, BaseTest, verify_post_alloc
@@ -156,6 +156,9 @@ def make_state_test_fixture(
156156

157157
try:
158158
verify_post_alloc(self.post, next_alloc)
159+
except TraceableException as e:
160+
e.set_traces([traces])
161+
raise e
159162
except Exception as e:
160163
print_traces([traces] if traces is not None else None)
161164
raise e

0 commit comments

Comments
 (0)