Skip to content
Draft
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
7 changes: 1 addition & 6 deletions .github/configs/feature.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,4 @@ monad:
# --suppress-no-test-exit-code works around a problem where multi-phase fill
# (triggered by tarball output) fails to proceed on no tests processed
# in 1st phase (exit code 5)
fill-params: --suppress-no-test-exit-code -m blockchain_test --from=MONAD_EIGHT --until=MONAD_NEXT --chain-id=143 -k "not invalid_header"

monad_runloop:
evm-type: eels
# Like `monad`, but `--monad-runloop` and eestnet chain id `30143`
fill-params: --suppress-no-test-exit-code -m blockchain_test --from=MONAD_EIGHT --until=MONAD_NEXT --chain-id=30143 --monad-runloop -k "not invalid_header"
fill-params: --suppress-no-test-exit-code -m "blockchain_test or runloop_test" --from=MONAD_EIGHT --until=MONAD_NEXT --chain-id=143 -k "not invalid_header"
5 changes: 3 additions & 2 deletions .github/workflows/consume_release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ on:
workflow_dispatch:
inputs:
release:
description: "Fixtures source: release spec (e.g. tests-monad_runloop@v1.1.1-rc2), a fixtures.tar.gz URL, or latest"
description: "Fixtures source: release spec (e.g. tests-monad@v1.1.2), a fixtures.tar.gz URL, or latest"
required: true
default: "tests-monad_runloop@latest"
default: "tests-monad@latest"
harness_ref:
description: "monad-eest-rust-harness ref (branch/tag/SHA)"
required: false
Expand Down Expand Up @@ -201,6 +201,7 @@ jobs:
uv run consume direct \
--input "$RELEASE" \
--bin "$GITHUB_WORKSPACE/monad-eest-rust-harness/bin/eest-runner" \
-m runloop_test \
"${FILTER_ARG[@]}" -v | tee consume.log

- name: Summary
Expand Down
22 changes: 13 additions & 9 deletions MONAD_RUNLOOP_TESTING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Running EEST fixtures on the monad runloop

Executes blockchain fixtures (including those generated from state
Executes runloop test fixtures (generated from blockchain and state
tests) against the monad execution client's `runloop`, using the
consensus ledger directory as the block source. Each `postState`
account is compared (balance/nonce/code/storage) against the
Expand Down Expand Up @@ -47,19 +47,23 @@ that fails silently).
## Fill + consume

```sh
uv run fill --clean -m blockchain_test <test paths...> \
--fork MONAD_NINE --chain-id 30143 --monad-runloop \
--output ../fixtures_eestnet
uv run fill --clean -m runloop_test <test paths...> \
--fork MONAD_NINE --output ../fixtures_eestnet

uv run consume direct --input ../fixtures_eestnet \
--bin ../monad-eest-rust-harness/bin/eest-runner
```

- `--monad-runloop` stamps monad blocks with the consensus-derived
header fields the runloop produces (prev_randao from the round-0 BLS
signature, 32-byte extra_data, the proposal gas limit, zero
requests_hash).
- `--chain-id 30143` is EestNet's chain id.
- `-m runloop_test` selects the runloop fixture format, written under
`runloop_tests` (next to `blockchain_tests`). It fills with
EestNet's chain id (30143) and stamps monad blocks with the
consensus-derived header fields the runloop produces (prev_randao
from the round-0 BLS signature, 32-byte extra_data, the proposal gas
limit, zero requests_hash), regardless of `--chain-id`.
- The `tests-monad` fixture release includes `runloop_tests` next to
the other formats; when consuming it (or any mixed fixtures
directory), pass `-m runloop_test` so only runloop fixtures reach
`eest-runner`.
- Block timestamps need no special handling: the consumer derives the
monad revision schedule from the fixture's `network`
(`FORK_REVISION_SCHEDULES` in `clis/monad.py`) and the harness
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from execution_testing.fixtures import (
BaseFixture,
BlockchainFixture,
BlockchainRunloopFixture,
StateFixture,
)
from execution_testing.fixtures.consume import (
Expand Down Expand Up @@ -94,6 +95,7 @@ def pytest_configure(config: pytest.Config) -> None: # noqa: D103
config.supported_fixture_formats = [ # type: ignore[attr-defined]
StateFixture,
BlockchainFixture,
BlockchainRunloopFixture,
]
fixture_consumers = []
for fixture_consumer_bin_path in config.getoption("fixture_consumer_bin"):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
BlockchainEngineFixture,
BlockchainEngineXFixture,
BlockchainFixture,
BlockchainRunloopFixture,
FixtureCollector,
FixtureConsumer,
FixtureFillingPhase,
Expand Down Expand Up @@ -576,18 +577,6 @@ def pytest_addoption(parser: pytest.Parser) -> None:
f"(Default: {EnvironmentDefaults.gas_limit})"
),
)
test_group.addoption(
"--monad-runloop",
action="store_true",
dest="monad_runloop",
default=False,
help=(
"Stamp monad blocks with the consensus-derived header fields "
"the production runloop produces (prev_randao, extra_data, "
"gas_limit, requests_hash), so filled block hashes and EIP-2935 "
"history storage match the monad `eest-runner` consumer."
),
)
test_group.addoption(
"--generate-pre-alloc-groups",
action="store_true",
Expand Down Expand Up @@ -709,17 +698,6 @@ def pytest_configure(config: pytest.Config) -> None:
if option_was_explicitly_set(config, "--block-gas-limit"):
EnvironmentDefaults.gas_limit = config.getoption("block_gas_limit")

# Stamp monad blocks with runloop-conformant header fields if requested.
if config.getoption("monad_runloop"):
MonadRunloopDefaults.enabled = True
# The runloop builds every block at the monad proposal gas limit, so
# make it the default block gas limit too: tests that read
# env.gas_limit (the GASLIMIT opcode, tx-gas-vs-block-limit checks)
# then build expectations that match execution. An explicit
# --block-gas-limit still wins.
if not option_was_explicitly_set(config, "--block-gas-limit"):
EnvironmentDefaults.gas_limit = MonadRunloopDefaults.gas_limit

# Initialize fixture output configuration
config.fixture_output = FixtureOutput.from_config( # type: ignore[attr-defined]
config
Expand Down Expand Up @@ -1853,8 +1831,9 @@ def pytest_collection_modifyitems(
if marker.name == "fill":
for mark in marker.args:
item.add_marker(mark)
if marker.name == "monad_runloop" and config.getoption(
"monad_runloop", False
if (
marker.name == "monad_runloop"
and item.get_closest_marker("runloop_test") is not None
):
for mark in marker.args:
item.add_marker(mark)
Expand Down Expand Up @@ -1969,6 +1948,47 @@ def sort_key(item: pytest.Item) -> tuple[bool, str, bool, str]:
item.add_marker(pytest.mark.xdist_group(name=group_name))


runloop_saved_defaults_key: pytest.StashKey[tuple[int, int]] = (
pytest.StashKey()
)


def pytest_runtest_setup(item: pytest.Item) -> None:
"""
Switch chain defaults to the runloop's for `runloop_test` items.

The chain id and block gas limit feed transaction signing and spec
construction in the test body, which runs once per fixture format
item, so the switch is per item and restored on teardown.
"""
if item.get_closest_marker("runloop_test") is None:
return
item.stash[runloop_saved_defaults_key] = (
ChainConfigDefaults.chain_id,
EnvironmentDefaults.gas_limit,
)
ChainConfigDefaults.chain_id = MonadRunloopDefaults.chain_id
# The runloop builds every block at the monad proposal gas limit, so
# make it the default block gas limit too: tests that read
# env.gas_limit (the GASLIMIT opcode, tx-gas-vs-block-limit checks)
# then build expectations that match execution. An explicit
# --block-gas-limit still wins.
if not option_was_explicitly_set(item.config, "--block-gas-limit"):
EnvironmentDefaults.gas_limit = MonadRunloopDefaults.gas_limit


def pytest_runtest_teardown(item: pytest.Item) -> None:
"""Restore the session chain defaults after a `runloop_test` item."""
saved = item.stash.get(runloop_saved_defaults_key, None)
if saved is None:
return
(
ChainConfigDefaults.chain_id,
EnvironmentDefaults.gas_limit,
) = saved
del item.stash[runloop_saved_defaults_key]


def _verify_fixtures_post_merge(
config: pytest.Config, output_dir: Path
) -> None:
Expand Down Expand Up @@ -2002,6 +2022,9 @@ def _verify_fixtures_post_merge(
dir_to_format: dict[str, type[BaseFixture]] = {
StateFixture.output_base_dir_name(): StateFixture,
BlockchainFixture.output_base_dir_name(): BlockchainFixture,
BlockchainRunloopFixture.output_base_dir_name(): (
BlockchainRunloopFixture
),
BlockchainEngineFixture.output_base_dir_name(): (
BlockchainEngineFixture
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ def test_collect_only_output(pytester: pytest.Pytester) -> None:
in line
for line in result.outlines
), f"Expected test output: {result.outlines}"
# fill generates 3 test variants: state_test,
# blockchain_test_from_state_test, blockchain_test_engine_from_state_test
assert any("3 tests collected" in line for line in result.outlines)
# fill generates 4 test variants: state_test,
# blockchain_test_from_state_test,
# blockchain_test_engine_from_state_test and
# runloop_test_from_state_test (the last two are collected but
# removed for Istanbul, which neither format supports)
assert any("4 tests collected" in line for line in result.outlines)
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
"""Test filling the `runloop_test` fixture format."""

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

import pytest

from execution_testing.test_types import MonadRunloopDefaults

test_module = textwrap.dedent(
"""\
import pytest

from execution_testing import Transaction

@pytest.mark.valid_from("MONAD_NINE")
def test_runloop(state_test, pre) -> None:
tx = Transaction(sender=pre.fund_eoa(), gas_limit=100_000)
state_test(pre=pre, post={}, tx=tx)
"""
)


@pytest.fixture()
def fill_args(testdir: pytest.Testdir) -> list[str]:
"""Copy fill ini and return base fill args."""
testdir.copy_example(
name=(
"src/execution_testing/cli/pytest_commands"
"/pytest_ini_files/pytest-fill.ini"
)
)
testdir.makepyfile(test_module)
return ["-c", "pytest-fill.ini", "--no-html", "--output", "fixtures"]


def load_single_fixture(testdir: pytest.Testdir, format_dir: str) -> Any:
"""Load the only fixture written under the given format directory."""
files = list(
(Path(testdir.tmpdir) / "fixtures" / format_dir).rglob("*.json")
)
assert len(files) == 1
fixtures = json.loads(files[0].read_text())
assert len(fixtures) == 1
return next(iter(fixtures.values()))


def test_fill_blockchain_and_runloop_formats(
testdir: pytest.Testdir, fill_args: list[str]
) -> None:
"""
One fill with both markers writes `blockchain_tests` and
`runloop_tests` siblings; the runloop fixture carries EestNet's
chain id and the runloop-stamped header fields regardless of
`--chain-id`, while the blockchain fixture is unaffected.
"""
result = testdir.runpytest(
*fill_args,
"-m",
"blockchain_test or runloop_test",
"--fork",
"MONAD_NINE",
"--chain-id",
"143",
)
result.assert_outcomes(passed=2)

blockchain = load_single_fixture(testdir, "blockchain_tests")
assert int(blockchain["config"]["chainid"], 16) == 143
header = blockchain["blocks"][0]["blockHeader"]
assert header["extraData"] != "0x" + "00" * 32
assert int(header["gasLimit"], 16) != MonadRunloopDefaults.gas_limit
assert int(header["mixHash"], 16) != MonadRunloopDefaults.prev_randao

runloop = load_single_fixture(testdir, "runloop_tests")
assert (
int(runloop["config"]["chainid"], 16) == MonadRunloopDefaults.chain_id
)
header = runloop["blocks"][0]["blockHeader"]
assert header["extraData"] == "0x" + "00" * 32
assert int(header["gasLimit"], 16) == MonadRunloopDefaults.gas_limit
assert int(header["mixHash"], 16) == MonadRunloopDefaults.prev_randao


def test_runloop_format_not_generated_for_canonical_forks(
testdir: pytest.Testdir, fill_args: list[str]
) -> None:
"""
`runloop_test` items only exist for monad forks, so canonical-fork
fills (e.g. the mainnet release with `--generate-all-formats`) never
produce `runloop_tests` fixtures.
"""
testdir.makepyfile(
test_module.replace('valid_from("MONAD_NINE")', 'valid_from("Paris")')
)
result = testdir.runpytest(
*fill_args,
"-m",
"runloop_test",
"--fork",
"Prague",
)
assert result.ret == pytest.ExitCode.NO_TESTS_COLLECTED


test_module_chain_config = textwrap.dedent(
"""\
import pytest

from execution_testing import Account, Op, Transaction

@pytest.mark.valid_from("MONAD_NINE")
def test_chain_config(state_test, pre, chain_config) -> None:
contract = pre.deploy_contract(Op.SSTORE(1, Op.CHAINID) + Op.STOP)
tx = Transaction(
sender=pre.fund_eoa(), to=contract, gas_limit=100_000
)
state_test(
pre=pre,
post={contract: Account(storage={1: chain_config.chain_id})},
tx=tx,
)
"""
)


def test_chain_config_follows_fixture_format(
testdir: pytest.Testdir, fill_args: list[str]
) -> None:
"""
The `chain_config` fixture must resolve per item: the blockchain
item runs first in the session, and a `chain_config` cached from it
would sign and verify the runloop item with the wrong chain id
(regression test for the session-scoped `chain_config`).
"""
testdir.makepyfile(test_module_chain_config)
result = testdir.runpytest(
*fill_args,
"-m",
"blockchain_test or runloop_test",
"--fork",
"MONAD_NINE",
"--chain-id",
"143",
)
result.assert_outcomes(passed=2)
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,9 @@ def pytest_configure(config: pytest.Config) -> None:
)
config.addinivalue_line(
"markers",
"monad_runloop: Markers to be added only when filling with "
"--monad-runloop (e.g. skips for tests that need a test-controlled "
"block gas limit the runloop overrides).",
"monad_runloop: Markers to be added only when filling a "
"runloop_test fixture (e.g. skips for tests that need a "
"test-controlled block gas limit the runloop overrides).",
)
config.addinivalue_line(
"markers",
Expand Down Expand Up @@ -359,9 +359,16 @@ def sender(pre: Alloc) -> EOA:
return pre.fund_eoa()


@pytest.fixture(scope="session")
@pytest.fixture
def chain_config() -> ChainConfig:
"""Return chain configuration."""
"""
Return chain configuration.

Function-scoped because the chain id differs per fixture format
within one session: `runloop_test` items fill with EestNet's chain
id, so a session-cached `ChainConfig` would leak one format's chain
id into the other's items.
"""
return ChainConfig()


Expand Down
Loading