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
@@ -0,0 +1,139 @@
"""
Test that ``input/alloc.json`` written under ``--evm-dump-dir`` uses the
same canonical JSON format as the t8n-produced ``output/alloc.json``.

When both files share the same conventions (32-byte-padded storage keys,
minimal-hex nonces, and omission of empty/default fields), a textual
diff between them surfaces only real state transitions instead of
formatting noise. Any account whose semantic state is unchanged between
pre and post must therefore serialize byte-identically in both files.
"""

import json
import textwrap
from pathlib import Path
from typing import Any

import pytest

CONTRACT_ADDR = "0x000000000000000000000000000000000000c0de"

TEST_MODULE = textwrap.dedent(
f"""\
import pytest

from execution_testing import (
Account,
Environment,
TestAddress,
Transaction,
)

CONTRACT = "{CONTRACT_ADDR}"

@pytest.mark.valid_at("Cancun")
def test_alloc_canonical(state_test) -> None:
state_test(
env=Environment(),
pre={{
TestAddress: Account(balance=10**18),
CONTRACT: Account(
nonce=1,
code=b"",
storage={{0x22: 0xabc}},
),
}},
post={{}},
tx=Transaction(),
)
"""
)


def _load(path: Path) -> Any:
with path.open() as f:
return json.load(f)


def test_input_alloc_matches_output_alloc_format(
pytester: pytest.Pytester, tmp_path: Path
) -> None:
"""
Filling a one-tx test with ``--traces`` must produce an
``input/alloc.json`` whose formatting matches the t8n's
``output/alloc.json``: storage keys padded to 32 bytes, empty fields
omitted, nonces in minimal hex. The contract account at
``CONTRACT_ADDR`` is untouched by the transaction, so its JSON
representation must be byte-identical between the two files.
"""
tests_dir = pytester.mkdir("tests")
cancun_dir = tests_dir / "cancun"
cancun_dir.mkdir()
case_dir = cancun_dir / "alloc_canonical"
case_dir.mkdir()
(case_dir / "test_alloc_canonical.py").write_text(TEST_MODULE)

pytester.copy_example(
name="src/execution_testing/cli/pytest_commands/pytest_ini_files/pytest-fill.ini"
)
dump_dir = tmp_path / "dump"

result = pytester.runpytest(
"-c",
"pytest-fill.ini",
"--fork",
"Cancun",
"tests/cancun/alloc_canonical/",
"--traces",
"--evm-dump-dir",
str(dump_dir),
"--clean",
"-q",
)
assert result.ret == 0, f"fill failed: {result.outlines}"

inputs = sorted(dump_dir.rglob("input/alloc.json"))
outputs = sorted(dump_dir.rglob("output/alloc.json"))
assert inputs and outputs, f"no alloc dumps under {dump_dir}"

input_alloc = _load(inputs[0])
output_alloc = _load(outputs[0])

# Format invariants on the input — all aligned with the output's
# canonical conventions.
for addr, account in input_alloc.items():
# Empty storage / zero balance / empty code must be omitted.
assert "storage" not in account or account["storage"], (
f"{addr}: empty storage dict not omitted"
)
assert "balance" not in account or account["balance"] != "0x00", (
f"{addr}: zero-padded balance not omitted"
)
assert "code" not in account or account["code"] != "0x", (
f"{addr}: empty code not omitted"
)
# Storage keys must be 32-byte zero-padded.
for key in account.get("storage", {}):
assert len(key) == 66 and key.startswith("0x"), (
f"{addr}: storage key not 32-byte-padded: {key!r}"
)
# Nonce uses minimal hex (no leading zero unless value is 0).
if "nonce" in account:
n = account["nonce"]
assert n == "0x0" or not n.startswith("0x0"), (
f"{addr}: nonce uses zero-padding: {n}"
)

# The contract is never touched by the transaction, so its
# representation must be identical in input and output.
assert CONTRACT_ADDR in input_alloc, (
f"contract {CONTRACT_ADDR} missing from input alloc"
)
assert CONTRACT_ADDR in output_alloc, (
f"contract {CONTRACT_ADDR} missing from output alloc"
)
assert input_alloc[CONTRACT_ADDR] == output_alloc[CONTRACT_ADDR], (
"contract account differs between input and output:\n"
f" input: {input_alloc[CONTRACT_ADDR]}\n"
f" output: {output_alloc[CONTRACT_ADDR]}"
)
50 changes: 50 additions & 0 deletions packages/testing/src/execution_testing/client_clis/cli_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,39 @@ def validate(self) -> Alloc:
return Alloc.model_validate(accumulated)


def _alloc_to_canonical_dict(alloc: Alloc) -> Dict[str, Dict[str, Any]]:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One small downside of this function is that it's decoupled from the Alloc model, so if we change it (very unlikely) this function would break.

"""
Convert an ``Alloc`` to a dict in the canonical t8n
``output/alloc.json`` form: empty/default fields (zero balance,
zero nonce, empty code, empty storage) are omitted, storage keys
are zero-padded to 32 bytes, and numeric values use minimal hex.

Used when writing the debug ``input/alloc.json`` artifact so that
a textual diff against the t8n's ``output/alloc.json`` surfaces
only real state transitions instead of formatting noise.
"""
result: Dict[str, Dict[str, Any]] = {}
for address, account in alloc.root.items():
if account is None:
continue
entry: Dict[str, Any] = {}
nonce_int = int(account.nonce)
if nonce_int != 0:
entry["nonce"] = hex(nonce_int)
balance_int = int(account.balance)
if balance_int != 0:
entry["balance"] = hex(balance_int)
if len(account.code) > 0:
entry["code"] = str(account.code)
if account.storage.root:
entry["storage"] = {
"0x" + format(int(k), "064x"): hex(int(v))
for k, v in account.storage.root.items()
}
result[str(address)] = entry
return result


@dataclass
class TransitionToolInput:
"""Transition tool input."""
Expand All @@ -521,6 +554,23 @@ class TransitionToolInput:
env: Environment
blob_params: ForkBlobSchedule | None = None

def alloc_for_debug_dump(
self,
) -> Dict[str, Dict[str, Any]] | LazyAlloc:
"""
Return the alloc in canonical form for the debug
``input/alloc.json`` artifact.

``Alloc`` instances are converted to a dict matching the t8n's
``output/alloc.json`` conventions. ``LazyAlloc`` inputs were
produced by a previous t8n call's output and are already
canonical, so they are returned unchanged for
``dump_files_to_directory`` to dispatch on.
"""
if isinstance(self.alloc, Alloc):
return _alloc_to_canonical_dict(self.alloc)
return self.alloc

def to_files(
self, directory_path: Path, **model_dump_config: Any
) -> Dict[str, str]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,9 @@ def _evaluate(
dump_files_to_directory(
debug_output_path,
{
"input/alloc.json": request_data.input.alloc,
"input/alloc.json": (
request_data.input.alloc_for_debug_dump()
),
"input/env.json": request_data.input.env,
"input/txs.json": [
tx.model_dump(mode="json", **model_dump_config)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,10 +618,8 @@ def _evaluate_server(
dump_files_to_directory(
debug_output_path,
{
"input/alloc.json": request_data.input.alloc.raw
if isinstance(request_data.input.alloc, LazyAlloc)
else request_data.input.alloc.model_dump(
mode="json", **model_dump_config
"input/alloc.json": (
request_data.input.alloc_for_debug_dump()
),
"input/env.json": request_data.input.env,
"input/txs.json": [
Expand Down Expand Up @@ -903,7 +901,7 @@ def dump_debug_stream(
debug_output_path,
{
"args.py": args,
"input/alloc.json": stdin.alloc,
"input/alloc.json": stdin.alloc_for_debug_dump(),
"input/env.json": stdin.env,
"input/txs.json": [
tx.model_dump(mode="json", **model_dump_config)
Expand Down
Loading
Loading