Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,29 @@ def test_numeric_sorting_not_lexical(self, tmp_path: Path) -> None:
# Verify they are in order 0, 2, 10 by checking the list
# length — ordering is guaranteed by the implementation

def test_empty_placeholder_trace_file(self, tmp_path: Path) -> None:
"""
Empty trace-*.jsonl placeholders load as empty TransactionTraces.

TransitionTool.collect_traces writes a zero-byte file when a tx
produced a receipt but no TransactionEnd tracer event fired
(e.g. EIP-3607 collision). The disk-loaded shape must match the
in-memory shape so --verify-traces does not report a spurious
transaction-count mismatch.
"""
call_dir = tmp_path / "0"
call_dir.mkdir()
# One real trace, one empty placeholder.
_write_trace_file(call_dir / "trace-0-0xaaa.jsonl")
(call_dir / "trace-1-0xbbb.jsonl").write_text("")

result = _load_traces_from_dump_dir(tmp_path)
assert len(result) == 1
assert len(result[0].root) == 2
assert len(result[0].root[1].traces) == 0
assert result[0].root[1].output is None
assert result[0].root[1].gas_used is None


def _make_trace_verifier(
json_formatter: Any = None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ class TransactionTraces(CamelModel):
def from_file(cls, trace_file_path: Path) -> Self:
"""Read a single transaction's traces from a .jsonl file."""
trace_lines = trace_file_path.read_text().splitlines()
if not trace_lines:
# Empty placeholder written for txs whose tracer's
# TransactionEnd event never fired (see
# TransitionTool.collect_traces).
return cls(traces=[])
trace_dict: Dict[str, Any] = {}
if "gasUsed" in trace_lines[-1] and "output" in trace_lines[-1]:
trace_dict |= json.loads(trace_lines.pop())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import Any, Type
from typing import Any, Type, cast

import pytest

Expand All @@ -19,6 +20,7 @@
LazyAlloc,
LazyAllocJson,
LazyAllocStr,
TransactionReceipt,
)
from execution_testing.test_types import Alloc

Expand Down Expand Up @@ -128,3 +130,84 @@ def test_lazy_alloc(ty: Type[LazyAlloc], raw: Any) -> None:
lazy_instance = ty(raw=raw, _state_root=TEST_ALLOC_STATE_ROOT)
assert lazy_instance.get() == TEST_ALLOC
assert lazy_instance.state_root() == TEST_ALLOC_STATE_ROOT


class _CollectTracesSelf:
"""
Minimal stand-in for a ``TransitionTool`` instance.

``TransitionTool.collect_traces`` only touches ``self.traces`` and
``self.append_traces``. Instantiating a real subclass requires an
exception_mapper, binary discovery, etc. — all unrelated to what
we're testing here.
"""

def __init__(self) -> None:
self.traces: list = []

def append_traces(self, new_traces: Any) -> None:
self.traces.append(new_traces)


def _make_receipt(tx_hash: str) -> TransactionReceipt:
return TransactionReceipt.model_validate({"transactionHash": tx_hash})


def test_collect_traces_writes_empty_placeholder_for_missing_trace(
tmp_path: Path,
) -> None:
"""
Regression for issue #2758.

When a tx produces a receipt but no trace file (TransactionEnd
tracer event never fired, e.g. EIP-3607 collisions), an empty
placeholder file must be written into ``debug_output_path`` so a
later ``--verify-traces`` run loads a matching shape from disk.
"""
tx_hash_present = "0x" + "a" * 64
tx_hash_missing = "0x" + "b" * 64

temp_dir = tempfile.TemporaryDirectory()
# Only write a trace for the first receipt; the second is missing.
(Path(temp_dir.name) / f"trace-0-{tx_hash_present}.jsonl").write_text(
'{"output":"0x","gasUsed":"0x5208"}\n'
)

debug_dir = tmp_path / "dump"
debug_dir.mkdir()

receipts = [_make_receipt(tx_hash_present), _make_receipt(tx_hash_missing)]

self_obj = _CollectTracesSelf()
TransitionTool.collect_traces(
cast(TransitionTool, self_obj),
receipts,
temp_dir,
debug_output_path=debug_dir,
)

placeholder = debug_dir / f"trace-1-{tx_hash_missing}.jsonl"
copied = debug_dir / f"trace-0-{tx_hash_present}.jsonl"
assert placeholder.exists(), "missing-trace placeholder not written"
assert placeholder.read_text() == ""
assert copied.exists(), "present trace was not copied to dump dir"


def test_collect_traces_no_placeholder_without_debug_path() -> None:
"""
With ``debug_output_path=None`` the missing-trace branch must still
produce an in-memory empty ``TransactionTraces`` but must not write
to disk.
"""
tx_hash = "0x" + "c" * 64
temp_dir = tempfile.TemporaryDirectory()
self_obj = _CollectTracesSelf()
TransitionTool.collect_traces(
cast(TransitionTool, self_obj),
[_make_receipt(tx_hash)],
temp_dir,
debug_output_path=None,
)
assert len(self_obj.traces) == 1
assert len(self_obj.traces[0].root) == 1
assert self_obj.traces[0].root[0].traces == []
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,15 @@ def collect_traces(
# Transaction was rejected mid-processing (e.g. EIP-3607
# collision): the receipt exists but the tracer's
# TransactionEnd event never fired, so no trace file was
# written. Record an empty trace for this tx.
# written. Record an empty trace for this tx, and also
# emit an empty placeholder file in the dump dir so a
# later --verify-traces run loads a matching shape from
# disk (otherwise the on-disk loader would skip this
# slot and the comparator would report a spurious
# transaction-count mismatch).
traces.append(TransactionTraces(traces=[]))
if debug_output_path:
(Path(debug_output_path) / trace_file_name).write_text("")
continue
if debug_output_path:
shutil.copy(
Expand Down