Skip to content

refactor(spec-tools): use testing pydantic models in t8n#2924

Open
gurukamath wants to merge 7 commits into
ethereum:forks/amsterdamfrom
gurukamath:refactor/testing-alloc-as-prestate
Open

refactor(spec-tools): use testing pydantic models in t8n#2924
gurukamath wants to merge 7 commits into
ethereum:forks/amsterdamfrom
gurukamath:refactor/testing-alloc-as-prestate

Conversation

@gurukamath
Copy link
Copy Markdown
Contributor

@gurukamath gurukamath commented May 27, 2026

🗒️ Description

The PR re-shapes the boundary between the testing package and
ethereum_spec_tools so that the T8N transition tool becomes a small
JSON-free engine that consumes testing pydantic types directly. Six
commits, one architectural thread per commit:

  1. Alloc becomes a PreState (24b3e4d492c). The testing-side
    Alloc now implements ethereum.state.PreState
    (get_account_optional, get_storage, get_code,
    account_has_storage, compute_state_root_and_trie_changes),
    removing the need for a bespoke adapter between testing's
    allocation model and the spec's state-transition entry point. A
    _Phase machine (CONSTRUCTION → LIVE → FROZEN) governs when free
    mutations are allowed; once any PreState method is called, the
    alloc transitions to LIVE and only apply_diff may mutate it.

  2. T8N rewritten to consume testing pydantic types
    (11eeda46d97). The bespoke Alloc / Env /
    Result / Txs JSON-parsing classes (t8n_types.py, 440 lines)
    are deleted. T8N no longer parses argparse output into custom
    containers — it accepts a TransitionTool.TransitionToolData
    directly.

  3. In-process EELS path stops round-tripping through JSON
    (f71df5daa49).
    ExecutionSpecsTransitionTool._evaluate hands its
    TransitionToolData straight to T8N and assembles the
    TransitionToolOutput from T8N's in-memory state.

  4. CLI surface extracted into t8n/cli.py (5e366b57c9a). Argparse setup, --input.*/--output.* flags,
    stdin/file readers, tracer construction from CLI flags, and the
    t8n-output JSON serialization all live in a new module. Core T8N
    knows nothing about argparse, JSON, or stdin/stdout.

  5. Duplicate testing-package state machinery removed
    (84a416cab69). The parallel State dataclass and
    set_account / set_storage / state_root / storage_root free
    helpers in account_types.py deleted. Alloc.state_root() now
    routes through spec_state.state_root(_materialize_state()). The
    testing package no longer maintains a fork of trie/state plumbing.

  6. Final cleanup pass (3ac3e80300b) - Small miscellanous clean-ups.

🔗 Related Issues or PRs

#1883
#1359

✅ Checklist

  • All: Ran fast static checks to avoid unnecessary CI fails, see also Code Standards and Enabling Pre-commit Checks:
    just static
  • All: PR title adheres to the repo standard - it will be used as the squash commit message and should start type(scope):.
  • All: Set appropriate labels for the changes (only maintainers can apply labels).
  • Tests: Ran mkdocs serve locally and verified the auto-generated docs for new tests in the Test Case Reference are correctly formatted.

Cute Animal Picture

Cute Animals - 1 of 7

Add a CONSTRUCTION/LIVE/FROZEN lifecycle to the testing-side Alloc so
it directly satisfies ethereum.state.PreState. The first PreState
read transitions the alloc to LIVE and rejects further __setitem__/
__delitem__; apply_diff(BlockDiff) is the sole mutation entry point
in LIVE, and freeze() locks the allocation for assertion use.

Groundwork for the t8n refactor: with Alloc directly usable as a
PreState, fork.BlockState(pre_state=alloc) works without an adapter,
and t8n can drop its bespoke Alloc/Env/Result/Txs JSON types.
T8N now wires the testing-package types end-to-end:

* __init__ accepts an optional ``t8n_data`` and otherwise parses the
  JSON inputs into testing ``Alloc``/``Environment``/``Transaction``
  via ``model_validate``. The CLI ingress is structured so the future
  in-process lift only needs to swap the caller, not the constructor.
* ``env.py`` drops the bespoke ``Env`` class in favour of
  ``build_block_environment(fork, env, pre_state, chain_id, ommers,
  state_test)`` plus a handful of ``_resolve_*`` helpers. ``Ommer``
  stays as a small dataclass for the pre-PoS reward path.
* ``convert_transaction`` routes through ``TransactionLoad`` rather
  than ``rlp.decode_to`` so contract-creating typed txs (Blob /
  SetCode with ``to=null``) construct successfully and let
  ``check_transaction`` raise the canonical
  ``TransactionTypeContractCreationError``.
* The bespoke ``Result`` / ``Txs`` / ``Alloc`` classes are gone;
  ``build_result`` and ``get_receipts_from_output`` produce a
  ``cli_types.Result`` directly, and ``T8N.run()`` emits the
  ``TransitionToolOutput``-shaped JSON.
* The per-tx ``backup_state`` / ``restore_state`` pattern disappears
  with the snapshot-based State: a failed ``process_transaction`` no
  longer reaches ``incorporate_tx_into_block``, so ``BlockState`` is
  untouched without explicit rollback. After execution the block
  diff is applied in-place via ``Alloc.apply_diff``.

JSON ingress smooths two boundary mismatches: ``yParity`` on auth
tuples (duplicated by the testing serializer, rejected by the
validator) and ``secretKey`` left on already-signed txs (rejected by
``InvalidSignaturePrivateKeyError``). Unsigned txs with only a
``secretKey`` are signed post-validation; pre-Spurious-Dragon forks
get ``protected=False`` so the v-value stays in {27, 28}.
The testing-side EELS caller no longer marshals the input through a
JSON ``StringIO`` and back. ``_evaluate`` now hands the testing
``TransitionToolInput`` directly to ``T8N`` via the existing
``t8n_data`` kwarg and assembles the ``TransitionToolOutput`` from
``T8N``'s in-memory ``alloc``/``result``/``body``.

To make the in-process path symmetric with the CLI path, ``T8N``:

* pulls ``blob_params`` from ``t8n_data.blob_params`` (camelCase dump
  matches the existing parse), so BPO-fork blob schedules don't have
  to be re-serialized through ``--input.blobParams=stdin``;
T8N now takes a testing ``TransitionTool.TransitionToolData`` and
nothing else from the JSON/CLI surface. The CLI plumbing
(``argparse`` namespace, ``--input.*``/``--output.*`` flags, stdin,
file paths, tracer construction from CLI flags) lives in a new
``t8n.cli`` module:

* ``build_t8n_from_cli_options(options, in_file, cache) -> T8N``
  reads the JSON inputs (stdin / files), validates each piece into
  testing pydantic types, resolves the fork, bundles everything into
  a ``TransitionToolData``, builds tracers from the CLI flags, and
  hands them to ``T8N``.
* ``write_t8n_outputs(t8n, output, options, out_file)`` serialises
  the t8n output + opcode counts per ``--output.*``.
* ``run_t8n_cli(options, out_file, in_file, cache) -> int`` chains
  the two for the CLI entry point.

``T8N`` internally calls ``resolve_fork(t8n_data.fork_name,
t8n_data.env)`` to translate the testing-side fork name into a spec
``Hardfork`` + optional ``ByBlockNumber`` criteria (handles both
canonical names and CLI exception aliases like ``Paris``,
``ConstantinopleFix``, ``HomesteadToDaoAt5``). ``T8N.run()`` returns
the ``TransitionToolOutput`` directly — no more out_file writing.

Callers updated:

* ``evm_tools.__init__.main`` now calls ``run_t8n_cli``.
* ``statetest`` and ``tests/json_loader`` use
  ``build_t8n_from_cli_options``.
* ``tests/evm_tools/test_count_opcodes`` uses ``run_t8n_cli``.
* ``ExecutionSpecsTransitionTool._evaluate`` hands its
  ``transition_tool_data`` straight to ``T8N`` — no argparse dance.

The CLI ↔ testing fork-name mapping is title-case + a one-entry
override for ``DAOFork`` (testing's irregular capitalisation).
``state_reward=None`` is resolved to the fork's ``BLOCK_REWARD`` (or
``-1`` for PoS forks) in the wrapper before constructing
``TransitionToolData.reward: int``.
``Alloc`` used to maintain its own parallel ``State`` dataclass plus
``set_account``/``set_storage``/``state_root``/``storage_root`` free
functions to compute its root. Now that ``Alloc`` implements the
``PreState`` protocol, ``state_root()`` can route through
``_materialize_state()`` and ``ethereum.state.state_root``, so the
in-package trie machinery is redundant.

* ``Alloc.state_root()`` reduced to a one-liner over
  ``spec_state.state_root(self._materialize_state())``. The materialize
  call doesn't transition the alloc out of ``CONSTRUCTION``, so existing
  callers that compute a genesis root and then keep mutating the alloc
  are unaffected.
* Local ``State`` dataclass + trie helpers (``set_account``,
  ``set_storage``, ``storage_root``, ``state_root``) removed; they had
  no consumers outside the deleted ``Alloc.state_root`` body.
* ``test_types/trie.py`` deleted along with its now-tautological
  ``test_eest_trie_keccak256_matches_eels`` keccak-dispatch check
  (the module just re-exported ``ethereum.crypto.hash.keccak256``).
@gurukamath
Copy link
Copy Markdown
Contributor Author

Note to reviewers: I have already run uv run hasher compare between the pre and post changes commit and they match, implying consistency in generated fixtures

@codecov
Copy link
Copy Markdown

codecov Bot commented May 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.33%. Comparing base (55d774b) to head (6578aea).
⚠️ Report is 14 commits behind head on forks/amsterdam.

Additional details and impacted files
@@                 Coverage Diff                 @@
##           forks/amsterdam    #2924      +/-   ##
===================================================
- Coverage            90.44%   90.33%   -0.11%     
===================================================
  Files                  535      535              
  Lines                32439    32430       -9     
  Branches              3012     3012              
===================================================
- Hits                 29338    29296      -42     
- Misses                2573     2613      +40     
+ Partials               528      521       -7     
Flag Coverage Δ
unittests 90.33% <ø> (-0.11%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

"""
Convert a testing ``Transaction`` into the fork's tx object.

TODO: Replace with ``self.fork.decode_transaction(tx.rlp())``
Copy link
Copy Markdown
Contributor Author

@gurukamath gurukamath May 27, 2026

Choose a reason for hiding this comment

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

Transactions are the only part of the t8n which use some sort of a json round trip workaround in this PR. In order to avoid this workaround (and fully make use of the rlp that the testing framework generates) we will need to make some small changes on the specs side, particularly wrt. the decode_transaction function. I aim to tackle this in a subsequent PR.

See #2925

@gurukamath gurukamath requested review from SamWilsn and marioevz May 27, 2026 09:51
@petertdavies petertdavies self-requested a review May 28, 2026 10:01
Copy link
Copy Markdown
Contributor

@petertdavies petertdavies left a comment

Choose a reason for hiding this comment

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

This looks great! I have a few minor comments.

Comment thread src/ethereum_spec_tools/evm_tools/t8n/__init__.py Outdated
Comment thread src/ethereum_spec_tools/evm_tools/t8n/block_environment.py Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants