From 5e477c54368c8b2dfdaa530a1fb183139eb29406 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Fri, 12 Jun 2026 07:40:28 +0000 Subject: [PATCH 01/22] Add MonadFixtureConsumer for consuming fixtures on the monad runloop Co-Authored-By: Claude (cherry picked from commit 68a3578e2876b70dfada44a756b4c43445508d47) --- MONAD_RUNLOOP_TESTING.md | 77 +++++ .../execution_testing/client_clis/__init__.py | 2 + .../client_clis/clis/monad.py | 266 ++++++++++++++++++ 3 files changed, 345 insertions(+) create mode 100644 MONAD_RUNLOOP_TESTING.md create mode 100644 packages/testing/src/execution_testing/client_clis/clis/monad.py diff --git a/MONAD_RUNLOOP_TESTING.md b/MONAD_RUNLOOP_TESTING.md new file mode 100644 index 00000000000..61a42b375d7 --- /dev/null +++ b/MONAD_RUNLOOP_TESTING.md @@ -0,0 +1,77 @@ +# Running EEST fixtures on the monad runloop + +Executes blockchain fixtures (including those generated from state +tests) against the production monad execution client, via its +`runloop` and the consensus ledger directory. The oracle is the +fixture's `postState`, compared account by account +(balance/nonce/code/storage). + +## Repos + +| Repo / branch | Role | +|---|---| +| `monad-exp/monad-eest-rust-harness` @ `execute-with-eestnet` | `eest-runner` harness: builds consensus blocks from a digested fixture and runs them on the runloop | +| `pdobacz/monad-bft` @ `execute-with-eestnet` (submodule of the above) | consensus block types + ledger writer; pins monad-execution below | +| `pdobacz/monad` @ `execute-with-eestnet` (submodule of monad-bft) | execution client with the `EestNet` chain (id 30143, per-fixture revision schedule, runtime genesis) and the extended `monad_runloop_*` FFI | +| this repo @ `execute-with-eestnet` | `MonadFixtureConsumer` (`packages/testing/.../client_clis/clis/monad.py`) wired into `consume direct` | + +## One-time setup + +Requirements: docker, ~10 GB disk for the builder image and build +artifacts, ~6 GB RAM for hugepages. + +```sh +git clone --branch execute-with-eestnet \ + https://github.com/monad-exp/monad-eest-rust-harness.git +cd monad-eest-rust-harness +git submodule update --init --recursive + +# Toolchain image (gcc-15 + rust), from monad-bft's Dockerfile: +curl -fsSL https://raw.githubusercontent.com/category-labs/monad-bft/master/docker/builder/Dockerfile \ + | docker build -t monad-builder:latest - + +./build.sh # builds + syncs binaries/libs into install/ +bin/eest-runner --version +``` + +Always rebuild with `./build.sh`; it syncs `libmonad_execution.so` +alongside the binary (copying only the binary leaves a stale library +that fails silently). + +## Fill + consume + +```sh +source .venv/bin/activate # in this repo + +fill --clean -m blockchain_test \ + --fork MONAD_NINE --chain-id 30143 \ + --output ../fixtures_eestnet + +consume direct --input ../fixtures_eestnet \ + --bin /bin/eest-runner +``` + +- `--chain-id 30143` is EestNet's chain id; transactions must be + signed for it, so it is required at fill time. +- 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 + injects it into the chain. +- `consume` options: `-n 3` parallelizes (hugepages support roughly + three concurrent runloops), `-k ` filters tests, + `--dump-dir ` saves harness input/output/logs per test. + +## Behavior and known limits + +- Fixtures containing expected-invalid blocks are skipped: the ledger + machine validates blocks at proposal time and cannot write invalid + ones. +- The fixture's genesis block is installed verbatim (state and + header), so block 1's parent hash and EIP-2935 slot 0 match. + Hashes of blocks the runloop itself executed differ from the + fixture's (delayed execution: headers carry monad's own roots), so + tests asserting BLOCKHASH/EIP-2935 values of non-genesis blocks + fail by design — exclude them from monad consume sets. +- The runloop runs in a privileged container (io_uring) and needs + `vm.nr_hugepages >= 2048` on the host; the `bin/` wrappers + re-provision this automatically (it resets on host reboot). diff --git a/packages/testing/src/execution_testing/client_clis/__init__.py b/packages/testing/src/execution_testing/client_clis/__init__.py index 2a9e603a118..85ee33f52e0 100644 --- a/packages/testing/src/execution_testing/client_clis/__init__.py +++ b/packages/testing/src/execution_testing/client_clis/__init__.py @@ -32,6 +32,7 @@ ) from .clis.execution_specs import ExecutionSpecsTransitionTool from .clis.geth import GethFixtureConsumer, GethTransitionTool +from .clis.monad import MonadFixtureConsumer from .clis.nethermind import Nethtest, NethtestFixtureConsumer from .clis.nimbus import NimbusTransitionTool from .ethereum_cli import CLINotFoundInPathError, UnknownCLIError @@ -74,6 +75,7 @@ "GethFixtureConsumer", "GethTransitionTool", "LazyAlloc", + "MonadFixtureConsumer", "Nethtest", "NethtestFixtureConsumer", "NimbusTransitionTool", diff --git a/packages/testing/src/execution_testing/client_clis/clis/monad.py b/packages/testing/src/execution_testing/client_clis/clis/monad.py new file mode 100644 index 00000000000..e67fa0f7a13 --- /dev/null +++ b/packages/testing/src/execution_testing/client_clis/clis/monad.py @@ -0,0 +1,266 @@ +""" +Monad `eest-runner` fixture consumer. + +Executes blockchain fixtures against the production monad runloop via +the `eest-runner` harness binary (built from monad-bft + monad +execution, EestNet chain). The fixture is digested into a simple input +document (genesis allocation + per-block timestamp/base fee/beneficiary +and raw signed transactions); the harness drives the consensus ledger +and the runloop, then emits the resulting post-state, which is compared +against the fixture's `postState`. +""" + +import json +import re +import subprocess +import tempfile +from pathlib import Path +from typing import Any, Dict, Optional + +import pytest + +from execution_testing.fixtures import BlockchainFixture, FixtureFormat +from execution_testing.test_types import Transaction + +from ..fixture_consumer_tool import FixtureConsumerTool + +# Monad revision schedule per fixture `network`, as +# (monad_revision, activation timestamp) pairs. Non-transition +# fixtures run a single revision from genesis; transition fixtures +# activate at the EEST transition fork's boundary. +FORK_REVISION_SCHEDULES = { + "MONAD_EIGHT": [(8, 0)], + "MONAD_NINE": [(9, 0)], + "MONAD_EIGHTToMONAD_NINEAtTime15k": [(8, 0), (9, 15_000)], +} + + +def _hex32(value: str) -> str: + """Normalize a hex quantity to a 0x-prefixed 64-nibble word.""" + return f"0x{int(value, 16):064x}" + + +def _digest_genesis_alloc(pre: Dict[str, Any]) -> Dict[str, Any]: + """Convert a fixture `pre` alloc into the harness genesis alloc.""" + alloc: Dict[str, Any] = {} + for address, account in pre.items(): + entry: Dict[str, Any] = { + "wei_balance": account.get("balance", "0x0"), + "nonce": account.get("nonce", "0x0"), + } + code = account.get("code", "0x") + if code and code != "0x": + entry["code"] = code + storage = { + _hex32(slot): _hex32(value) + for slot, value in account.get("storage", {}).items() + if int(value, 16) != 0 + } + if storage: + entry["storage"] = storage + alloc[address] = entry + return alloc + + +def _digest_blocks(fixture: Dict[str, Any]) -> list: + """Convert fixture blocks into the harness block list.""" + blocks = [] + for block in fixture["blocks"]: + header = block["blockHeader"] + txs = [] + for tx_json in block.get("transactions", []): + for auth in tx_json.get("authorizationList", []): + # Fixtures serialize both `v` and `yParity`; the + # Transaction model accepts only one of them. + if "v" in auth: + auth.pop("yParity", None) + tx = Transaction.model_validate(tx_json) + txs.append(tx.rlp().hex()) + blocks.append( + { + "timestamp": int(header["timestamp"], 16), + "base_fee": int(header["baseFeePerGas"], 16), + "beneficiary": header["coinbase"], + "txs": txs, + } + ) + return blocks + + +def _compare_account( + address: str, expected: Dict[str, Any], actual: Optional[Dict[str, Any]] +) -> list: + """Compare one expected post-state account; return mismatch strings.""" + mismatches = [] + if actual is None: + actual = {} + for field in ("balance", "nonce"): + expected_value = int(expected.get(field, "0x0"), 16) + actual_value = int(actual.get(field, "0x0"), 16) + if expected_value != actual_value: + mismatches.append( + f"{address}: {field} expected {hex(expected_value)}, " + f"got {hex(actual_value)}" + ) + expected_code = expected.get("code", "0x") or "0x" + actual_code = actual.get("code", "0x") or "0x" + if expected_code.lower() != actual_code.lower(): + mismatches.append( + f"{address}: code expected {expected_code}, got {actual_code}" + ) + actual_storage = { + int(slot, 16): int(value, 16) + for slot, value in actual.get("storage", {}).items() + } + expected_storage = { + int(slot, 16): int(value, 16) + for slot, value in expected.get("storage", {}).items() + } + for slot, expected_value in expected_storage.items(): + actual_value = actual_storage.get(slot, 0) + if expected_value != actual_value: + mismatches.append( + f"{address}: storage[{hex(slot)}] expected " + f"{hex(expected_value)}, got {hex(actual_value)}" + ) + for slot, actual_value in actual_storage.items(): + if slot not in expected_storage and actual_value != 0: + mismatches.append( + f"{address}: unexpected storage[{hex(slot)}] = " + f"{hex(actual_value)}" + ) + return mismatches + + +class MonadFixtureConsumer( + FixtureConsumerTool, + fixture_formats=[BlockchainFixture], +): + """Monad's `eest-runner` fixture consumer for blockchain tests.""" + + default_binary = Path("eest-runner") + detect_binary_pattern = re.compile(r"^eest-runner\b") + version_flag: str = "--version" + + def __init__( + self, + binary: Optional[Path] = None, + trace: bool = False, + ): + """Initialize the MonadFixtureConsumer.""" + super().__init__(binary=binary) + self.trace = trace + + def _init_triedb(self, db_path: Path) -> None: + """Create and format a fresh triedb file via `monad-mpt`.""" + monad_mpt = self.binary.parent / "monad-mpt" + with open(db_path, "wb") as f: + f.truncate(8 * 1024**3) + subprocess.run( + [str(monad_mpt), "--storage", str(db_path), "--create"], + capture_output=True, + text=True, + check=True, + ) + + def consume_fixture( + self, + fixture_format: FixtureFormat, + fixture_path: Path, + fixture_name: Optional[str] = None, + debug_output_path: Optional[Path] = None, + ) -> None: + """Execute a blockchain fixture on the monad runloop and verify.""" + assert fixture_format == BlockchainFixture + + with open(fixture_path) as f: + fixtures = json.load(f) + if fixture_name is None: + assert len(fixtures) == 1, "fixture_name required" + fixture_name = next(iter(fixtures)) + fixture = fixtures[fixture_name] + + network = fixture["network"] + assert network in FORK_REVISION_SCHEDULES, ( + f"no monad revision schedule for network {network}" + ) + if any("expectException" in block for block in fixture["blocks"]): + pytest.skip("invalid blocks cannot be proposed through the ledger") + input_doc = { + "genesis_alloc": _digest_genesis_alloc(fixture["pre"]), + "genesis_rlp": fixture["genesisRLP"], + "revision_schedule": [ + {"revision": revision, "from_timestamp": from_timestamp} + for revision, from_timestamp in FORK_REVISION_SCHEDULES[ + network + ] + ], + "blocks": _digest_blocks(fixture), + } + + with tempfile.TemporaryDirectory(prefix="eest-runner-") as tmp: + tmp_path = Path(tmp) + input_path = tmp_path / "input.json" + output_path = tmp_path / "output.json" + ledger_dir = tmp_path / "ledger" + ledger_dir.mkdir() + db_path = tmp_path / "triedb" + self._init_triedb(db_path) + + input_path.write_text(json.dumps(input_doc, indent=2)) + + result = subprocess.run( + [ + str(self.binary), + "--input", + str(input_path), + "--output", + str(output_path), + "--ledger-dir", + str(ledger_dir), + "--db", + str(db_path), + ], + capture_output=True, + text=True, + ) + + if debug_output_path is not None: + debug_output_path.mkdir(parents=True, exist_ok=True) + (debug_output_path / "input.json").write_text( + input_path.read_text() + ) + (debug_output_path / "stdout.txt").write_text(result.stdout) + (debug_output_path / "stderr.txt").write_text(result.stderr) + if output_path.exists(): + (debug_output_path / "output.json").write_text( + output_path.read_text() + ) + + if result.returncode != 0: + raise Exception( + f"eest-runner failed (exit {result.returncode}):\n" + f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" + ) + + output = json.loads(output_path.read_text()) + + actual_post = { + address.lower(): account + for address, account in output["post_state"].items() + } + post_state = fixture.get("postState") + assert post_state is not None, ( + "fixture has no postState (hash-only fixtures not supported)" + ) + + mismatches = [] + for address, expected in post_state.items(): + actual = actual_post.get(address.lower()) + mismatches.extend(_compare_account(address, expected, actual)) + + if mismatches: + raise Exception( + "post-state mismatch on the monad runloop:\n" + + "\n".join(mismatches) + ) From 9b739722f73cc3c93a09e894d522d230d1e1a225 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 18 Jun 2026 09:09:51 +0000 Subject: [PATCH 02/22] docs: use uv for monad runloop testing setup Co-Authored-By: Claude (cherry picked from commit 84ae0fca7ea9439ef37b11d8aeccd724922e6c82) --- MONAD_RUNLOOP_TESTING.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/MONAD_RUNLOOP_TESTING.md b/MONAD_RUNLOOP_TESTING.md index 61a42b375d7..bf55a6878ff 100644 --- a/MONAD_RUNLOOP_TESTING.md +++ b/MONAD_RUNLOOP_TESTING.md @@ -34,21 +34,29 @@ curl -fsSL https://raw.githubusercontent.com/category-labs/monad-bft/master/dock bin/eest-runner --version ``` +In this repo, sync the Python environment once so `uv run fill` and +`uv run consume` resolve the workspace deps: + +```sh +uv sync +``` + Always rebuild with `./build.sh`; it syncs `libmonad_execution.so` alongside the binary (copying only the binary leaves a stale library that fails silently). ## Fill + consume -```sh -source .venv/bin/activate # in this repo +Run from this repo; `uv run` resolves the project environment, so no +manual `source .venv/bin/activate` is needed. -fill --clean -m blockchain_test \ +```sh +uv run fill --clean -m blockchain_test \ --fork MONAD_NINE --chain-id 30143 \ --output ../fixtures_eestnet -consume direct --input ../fixtures_eestnet \ - --bin /bin/eest-runner +uv run consume direct --input ../fixtures_eestnet \ + --bin ../monad-eest-rust-harness/bin/eest-runner ``` - `--chain-id 30143` is EestNet's chain id; transactions must be From b9faa854b2810d0da4b296d2f85e69d3b1123cde Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 18 Jun 2026 09:10:29 +0000 Subject: [PATCH 03/22] perf: shrink triedb chunk sizing for monad fixture consumer Cuts per-test runtime ~70x (112s to ~1.6s) with identical post-state. Co-Authored-By: Claude (cherry picked from commit 2561c49fc4af772f121459a7a5e1ee389d4560be) --- .../client_clis/clis/monad.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/testing/src/execution_testing/client_clis/clis/monad.py b/packages/testing/src/execution_testing/client_clis/clis/monad.py index e67fa0f7a13..1fe320d45ec 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/monad.py +++ b/packages/testing/src/execution_testing/client_clis/clis/monad.py @@ -155,9 +155,23 @@ def _init_triedb(self, db_path: Path) -> None: """Create and format a fresh triedb file via `monad-mpt`.""" monad_mpt = self.binary.parent / "monad-mpt" with open(db_path, "wb") as f: - f.truncate(8 * 1024**3) + f.truncate(2 * 1024**3) + # Fixtures persist a tiny state across a few blocks; the + # production defaults (256 MiB chunks at an 18-chunk minimum, + # plus a 268M-entry history ring) make formatting and the + # genesis commit dominate runtime. Shrinking both cuts per-test + # time from ~100s to ~2s with identical post-state. subprocess.run( - [str(monad_mpt), "--storage", str(db_path), "--create"], + [ + str(monad_mpt), + "--storage", + str(db_path), + "--create", + "--chunk-capacity", + "26", + "--root-offsets-chunk-count", + "2", + ], capture_output=True, text=True, check=True, From 6d969a3ea2fc05bc63d6d61a30fb428c33cd83cf Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 18 Jun 2026 10:03:08 +0000 Subject: [PATCH 04/22] fix: tear down eest-runner container when pytest exits Names the container and sets PR_SET_PDEATHSIG so a killed docker client no longer leaves the runloop spinning past pytest. Co-Authored-By: Claude (cherry picked from commit 428fc1f4e65c7bede0284ba937bdc2ad7490b30a) --- .../client_clis/clis/monad.py | 107 ++++++++++++++---- 1 file changed, 87 insertions(+), 20 deletions(-) diff --git a/packages/testing/src/execution_testing/client_clis/clis/monad.py b/packages/testing/src/execution_testing/client_clis/clis/monad.py index 1fe320d45ec..d1c55092e47 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/monad.py +++ b/packages/testing/src/execution_testing/client_clis/clis/monad.py @@ -10,12 +10,16 @@ against the fixture's `postState`. """ +import ctypes import json +import os import re +import signal import subprocess import tempfile +import uuid from pathlib import Path -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Tuple import pytest @@ -35,6 +39,26 @@ } +_LIBC: Optional[ctypes.CDLL] +try: + _LIBC = ctypes.CDLL("libc.so.6", use_errno=True) +except OSError: + _LIBC = None + + +def _set_pdeathsig() -> None: + """ + Have the kernel SIGTERM this child when its parent dies. + + Runs in the child between fork and exec. `PR_SET_PDEATHSIG` (1) + fires even if pytest is SIGKILLed, so the docker client receives a + signal it can forward to stop the container instead of leaving the + runloop orphaned. + """ + if _LIBC is not None: + _LIBC.prctl(1, signal.SIGTERM) + + def _hex32(value: str) -> str: """Normalize a hex quantity to a 0x-prefixed 64-nibble word.""" return f"0x{int(value, 16):064x}" @@ -177,6 +201,61 @@ def _init_triedb(self, db_path: Path) -> None: check=True, ) + def _run_harness( + self, + input_path: Path, + output_path: Path, + ledger_dir: Path, + db_path: Path, + ) -> Tuple[str, str, int]: + """ + Run `eest-runner`, tearing down its container when we exit. + + A killed docker client does not stop its container, so a + dockerized runloop would otherwise keep spinning past pytest. + Two teardown paths cover this: an explicit `docker kill` on the + named container when this call is interrupted, and a parent-death + signal so the client is also stopped if pytest dies without + unwinding (SIGTERM/SIGHUP, or SIGKILL). + """ + container_name = f"eest-runner-{uuid.uuid4().hex}" + process = subprocess.Popen( + [ + str(self.binary), + "--input", + str(input_path), + "--output", + str(output_path), + "--ledger-dir", + str(ledger_dir), + "--db", + str(db_path), + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + start_new_session=True, + preexec_fn=_set_pdeathsig, + env={ + **os.environ, + "EEST_RUNNER_CONTAINER_NAME": container_name, + }, + ) + try: + stdout, stderr = process.communicate() + except BaseException: + try: + subprocess.run( + ["docker", "kill", container_name], + capture_output=True, + ) + except Exception: + pass + process.kill() + process.wait() + raise + return stdout, stderr, process.returncode + def consume_fixture( self, fixture_format: FixtureFormat, @@ -223,20 +302,8 @@ def consume_fixture( input_path.write_text(json.dumps(input_doc, indent=2)) - result = subprocess.run( - [ - str(self.binary), - "--input", - str(input_path), - "--output", - str(output_path), - "--ledger-dir", - str(ledger_dir), - "--db", - str(db_path), - ], - capture_output=True, - text=True, + stdout, stderr, returncode = self._run_harness( + input_path, output_path, ledger_dir, db_path ) if debug_output_path is not None: @@ -244,17 +311,17 @@ def consume_fixture( (debug_output_path / "input.json").write_text( input_path.read_text() ) - (debug_output_path / "stdout.txt").write_text(result.stdout) - (debug_output_path / "stderr.txt").write_text(result.stderr) + (debug_output_path / "stdout.txt").write_text(stdout) + (debug_output_path / "stderr.txt").write_text(stderr) if output_path.exists(): (debug_output_path / "output.json").write_text( output_path.read_text() ) - if result.returncode != 0: + if returncode != 0: raise Exception( - f"eest-runner failed (exit {result.returncode}):\n" - f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" + f"eest-runner failed (exit {returncode}):\n" + f"stdout:\n{stdout}\nstderr:\n{stderr}" ) output = json.loads(output_path.read_text()) From db3a35f7ec87ba9d9fb5a64ec6484aca2908a479 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 18 Jun 2026 10:33:31 +0000 Subject: [PATCH 05/22] docs: note runloop parallelism is CPU-bound Co-Authored-By: Claude (cherry picked from commit e62599e77a8e1738c928b11e38ba0d36fa878da3) --- MONAD_RUNLOOP_TESTING.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/MONAD_RUNLOOP_TESTING.md b/MONAD_RUNLOOP_TESTING.md index bf55a6878ff..488092659a5 100644 --- a/MONAD_RUNLOOP_TESTING.md +++ b/MONAD_RUNLOOP_TESTING.md @@ -65,9 +65,12 @@ uv run consume direct --input ../fixtures_eestnet \ monad revision schedule from the fixture's `network` (`FORK_REVISION_SCHEDULES` in `clis/monad.py`) and the harness injects it into the chain. -- `consume` options: `-n 3` parallelizes (hugepages support roughly - three concurrent runloops), `-k ` filters tests, - `--dump-dir ` saves harness input/output/logs per test. +- `consume` options: `-n ` parallelizes, `-k ` filters + tests, `--dump-dir ` saves harness input/output/logs per test. +- Parallelism is CPU-bound: one runloop peaks near 4 cores (~386% CPU + across ~14 threads), so budget ~5 vCPUs per worker (`-n N` needs + roughly `5 * N` cores). On a small host (e.g. 4 vCPUs) run + sequentially — `-n` above 1 oversubscribes and runs slower. ## Behavior and known limits From cf47e1fae9f8f4d2319ff035ed351c3cbb81d570 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:09:57 +0000 Subject: [PATCH 06/22] feat: --monad-runloop fill flag for runloop-conformant block hashes Stamps monad consensus header fields so filled block hashes + EIP-2935 storage match the runloop; consumer now checks them and the state root. Co-Authored-By: Claude (cherry picked from commit de3b3cb84effcb1d7d0f49e0d9253440f9b431a5) --- MONAD_RUNLOOP_TESTING.md | 8 +++- .../pytest_commands/plugins/filler/filler.py | 21 ++++++++- .../client_clis/clis/monad.py | 12 +++++ .../src/execution_testing/specs/blockchain.py | 44 ++++++++++++++++++- .../execution_testing/test_types/__init__.py | 2 + .../test_types/block_types.py | 23 ++++++++++ 6 files changed, 107 insertions(+), 3 deletions(-) diff --git a/MONAD_RUNLOOP_TESTING.md b/MONAD_RUNLOOP_TESTING.md index 488092659a5..82293b47c72 100644 --- a/MONAD_RUNLOOP_TESTING.md +++ b/MONAD_RUNLOOP_TESTING.md @@ -52,13 +52,19 @@ manual `source .venv/bin/activate` is needed. ```sh uv run fill --clean -m blockchain_test \ - --fork MONAD_NINE --chain-id 30143 \ + --fork MONAD_NINE --chain-id 30143 --monad-runloop \ --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) so filled block hashes and EIP-2935 history storage + match the runloop; without it those slots diverge and fail the + post-state compare. - `--chain-id 30143` is EestNet's chain id; transactions must be signed for it, so it is required at fill time. - Block timestamps need no special handling: the consumer derives the diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/filler.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/filler.py index fb7fac5f239..5fce92e8868 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/filler.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/filler.py @@ -70,7 +70,10 @@ ) from execution_testing.specs import BaseTest from execution_testing.specs.base import FillResult, OpMode -from execution_testing.test_types import EnvironmentDefaults +from execution_testing.test_types import ( + EnvironmentDefaults, + MonadRunloopDefaults, +) from execution_testing.test_types.chain_config_types import ( DEFAULT_CHAIN_ID, ChainConfigDefaults, @@ -573,6 +576,18 @@ 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", @@ -694,6 +709,10 @@ 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 + # Initialize fixture output configuration config.fixture_output = FixtureOutput.from_config( # type: ignore[attr-defined] config diff --git a/packages/testing/src/execution_testing/client_clis/clis/monad.py b/packages/testing/src/execution_testing/client_clis/clis/monad.py index d1c55092e47..0891ed5aced 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/monad.py +++ b/packages/testing/src/execution_testing/client_clis/clis/monad.py @@ -340,6 +340,18 @@ def consume_fixture( actual = actual_post.get(address.lower()) mismatches.extend(_compare_account(address, expected, actual)) + # Assert the final state root, the last executed block's root. Under + # monad's synchronous execution it equals the fixture's last block + # `stateRoot`; invalid-block fixtures are skipped above, so every + # block carries a header. + expected_state_root = fixture["blocks"][-1]["blockHeader"]["stateRoot"] + actual_state_root = output["state_root"] + if int(expected_state_root, 16) != int(actual_state_root, 16): + mismatches.append( + f"state_root expected {expected_state_root}, " + f"got {actual_state_root}" + ) + if mismatches: raise Exception( "post-state mismatch on the monad runloop:\n" diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index 853d10bb1af..d3c86e001ef 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -83,10 +83,11 @@ FixtureTransactionReceipt, ) from execution_testing.fixtures.post_verifications import PostVerifications -from execution_testing.forks import Fork, TransitionFork +from execution_testing.forks import MONAD_EIGHT, Fork, TransitionFork from execution_testing.test_types import ( Alloc, Environment, + MonadRunloopDefaults, Removable, Requests, TestPhase, @@ -773,6 +774,22 @@ def make_genesis( ) -> Tuple[Alloc, FixtureBlock]: """Create a genesis block from the blockchain test definition.""" env = self.get_genesis_environment() + + # Keep the genesis gas limit at monad's proposal_gas_limit so the + # whole chain has a constant gas limit; otherwise the jump from the + # EEST default to block 1's stamped limit fails the EIP-1559 gas + # limit bound check during fill. Resolve the fork at genesis so + # transition forks are covered too. + genesis_fork = self.fork.fork_at( + block_number=env.number, timestamp=env.timestamp + ) + if ( + MonadRunloopDefaults.enabled + and isinstance(genesis_fork, type) + and issubclass(genesis_fork, MONAD_EIGHT) + ): + env = env.copy(gas_limit=MonadRunloopDefaults.gas_limit) + assert env.withdrawals is None or len(env.withdrawals) == 0, ( "withdrawals must be empty at genesis" ) @@ -825,6 +842,25 @@ def generate_block_data( block_number=env.number, timestamp=env.timestamp ) env = env.set_fork_requirements(fork) + + # When filling with --monad-runloop, monad blocks must carry the + # consensus-derived header fields the production runloop produces. + # gas_limit and prev_randao feed execution (gas, PREVRANDAO) and the + # EIP-2935 block-hash chain, so they are set on the env before t8n; + # extra_data and requests_hash are stamped onto the header below. + # Gate on the per-block resolved fork so transition forks (whose + # `self.fork` is the transition class) are covered too. + monad_runloop = ( + MonadRunloopDefaults.enabled + and isinstance(fork, type) + and issubclass(fork, MONAD_EIGHT) + ) + if monad_runloop: + env = env.copy( + gas_limit=MonadRunloopDefaults.gas_limit, + prev_randao=MonadRunloopDefaults.prev_randao, + ) + txs = [tx.with_signature_and_sender() for tx in block.txs] if failing_tx_count := len([tx for tx in txs if tx.error]) > 0: @@ -935,6 +971,12 @@ def generate_block_data( ) requests_list = block.requests + if monad_runloop: + # monad stamps a 32-zero-byte extra_data and requests_hash, + # rather than empty extra_data and the EIP-7685 requests root. + header.extra_data = Bytes(b"\x00" * 32) + header.requests_hash = Hash(0) + # Decode BAL from RLP bytes provided by the transition tool. t8n_bal_rlp = transition_tool_output.result.block_access_list t8n_bal: BlockAccessList | None = None diff --git a/packages/testing/src/execution_testing/test_types/__init__.py b/packages/testing/src/execution_testing/test_types/__init__.py index b9029ceb3ad..976edd477ef 100644 --- a/packages/testing/src/execution_testing/test_types/__init__.py +++ b/packages/testing/src/execution_testing/test_types/__init__.py @@ -17,6 +17,7 @@ from .block_types import ( Environment, EnvironmentDefaults, + MonadRunloopDefaults, Withdrawal, ) from .chain_config_types import ChainConfig, ChainConfigDefaults @@ -72,6 +73,7 @@ "DepositRequest", "Environment", "EnvironmentDefaults", + "MonadRunloopDefaults", "EOA", "NetworkWrappedTransaction", "Removable", diff --git a/packages/testing/src/execution_testing/test_types/block_types.py b/packages/testing/src/execution_testing/test_types/block_types.py index 56d367e2e68..b9c8a93a792 100644 --- a/packages/testing/src/execution_testing/test_types/block_types.py +++ b/packages/testing/src/execution_testing/test_types/block_types.py @@ -37,6 +37,29 @@ class EnvironmentDefaults: gas_limit: int = DEFAULT_BLOCK_GAS_LIMIT +@dataclass +class MonadRunloopDefaults: + """ + Consensus-derived header values the monad runloop produces. + + When `fill` runs with `--monad-runloop`, monad blocks are stamped + with these so filled block hashes and EIP-2935 history storage match + the production runloop (which the `eest-runner` consumer executes). + They are the only header fields that differ from the synchronous EEST + defaults; the execution-result roots already agree. + """ + + enabled: bool = False + # prev_randao = blake3(rlp(BLS round-signature over Round(0))) with the + # harness's all-zero proposer key. Constant because the harness never + # advances the round (Round(0) for every block). + prev_randao: int = ( + 0x15B2551B9CE2307DCFF661B7DEE9CC4D09F304F03E7887B0E948A1C29F0E5826 + ) + # proposal_gas_limit of the active monad chain revision (V_0_11_0). + gas_limit: int = 200_000_000 + + class WithdrawalGeneric(CamelModel, Generic[NumberBoundTypeVar]): """ Withdrawal generic type, used as a parent class for `Withdrawal` and From d156e0fc330a06a21b8d134d181f0db8d08b7562 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:04:57 +0200 Subject: [PATCH 07/22] Minor fixes to MONAD_RUNLOOP_TESTING.md (cherry picked from commit 13aad44dece1fe18d3696b7a1e00c8fdd6514bb7) --- MONAD_RUNLOOP_TESTING.md | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/MONAD_RUNLOOP_TESTING.md b/MONAD_RUNLOOP_TESTING.md index 82293b47c72..927160aedce 100644 --- a/MONAD_RUNLOOP_TESTING.md +++ b/MONAD_RUNLOOP_TESTING.md @@ -11,8 +11,8 @@ fixture's `postState`, compared account by account | Repo / branch | Role | |---|---| | `monad-exp/monad-eest-rust-harness` @ `execute-with-eestnet` | `eest-runner` harness: builds consensus blocks from a digested fixture and runs them on the runloop | -| `pdobacz/monad-bft` @ `execute-with-eestnet` (submodule of the above) | consensus block types + ledger writer; pins monad-execution below | -| `pdobacz/monad` @ `execute-with-eestnet` (submodule of monad-bft) | execution client with the `EestNet` chain (id 30143, per-fixture revision schedule, runtime genesis) and the extended `monad_runloop_*` FFI | +| `monad-bft` @ `execute-with-eestnet` (submodule of the above) | consensus block types + ledger writer; pins monad-execution below | +| `monad` @ `execute-with-eestnet` (submodule of monad-bft) | execution client with the `EestNet` chain (id 30143, per-fixture revision schedule, runtime genesis) and the extended `monad_runloop_*` FFI | | this repo @ `execute-with-eestnet` | `MonadFixtureConsumer` (`packages/testing/.../client_clis/clis/monad.py`) wired into `consume direct` | ## One-time setup @@ -34,8 +34,7 @@ curl -fsSL https://raw.githubusercontent.com/category-labs/monad-bft/master/dock bin/eest-runner --version ``` -In this repo, sync the Python environment once so `uv run fill` and -`uv run consume` resolve the workspace deps: +In this repo: ```sh uv sync @@ -47,9 +46,6 @@ that fails silently). ## Fill + consume -Run from this repo; `uv run` resolves the project environment, so no -manual `source .venv/bin/activate` is needed. - ```sh uv run fill --clean -m blockchain_test \ --fork MONAD_NINE --chain-id 30143 --monad-runloop \ @@ -71,9 +67,7 @@ uv run consume direct --input ../fixtures_eestnet \ monad revision schedule from the fixture's `network` (`FORK_REVISION_SCHEDULES` in `clis/monad.py`) and the harness injects it into the chain. -- `consume` options: `-n ` parallelizes, `-k ` filters - tests, `--dump-dir ` saves harness input/output/logs per test. -- Parallelism is CPU-bound: one runloop peaks near 4 cores (~386% CPU +- `consume` parallelism is CPU-bound: one runloop peaks near 4 cores (~386% CPU across ~14 threads), so budget ~5 vCPUs per worker (`-n N` needs roughly `5 * N` cores). On a small host (e.g. 4 vCPUs) run sequentially — `-n` above 1 oversubscribes and runs slower. @@ -83,12 +77,6 @@ uv run consume direct --input ../fixtures_eestnet \ - Fixtures containing expected-invalid blocks are skipped: the ledger machine validates blocks at proposal time and cannot write invalid ones. -- The fixture's genesis block is installed verbatim (state and - header), so block 1's parent hash and EIP-2935 slot 0 match. - Hashes of blocks the runloop itself executed differ from the - fixture's (delayed execution: headers carry monad's own roots), so - tests asserting BLOCKHASH/EIP-2935 values of non-genesis blocks - fail by design — exclude them from monad consume sets. - The runloop runs in a privileged container (io_uring) and needs `vm.nr_hugepages >= 2048` on the host; the `bin/` wrappers re-provision this automatically (it resets on host reboot). From 4816466f14ebe98f990e1ee71f980c89c7bca6c0 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:19:45 +0000 Subject: [PATCH 08/22] consumer: support MONAD_NEXT (rev 10) + page-encoded triedb Adds MONAD_NEXT/transition revision schedules and --state-machine monad for MIP-8. Co-Authored-By: Claude (cherry picked from commit c5b6f2eab97584495107d2fcebf4cae706bd353d) --- .../src/execution_testing/client_clis/clis/monad.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/testing/src/execution_testing/client_clis/clis/monad.py b/packages/testing/src/execution_testing/client_clis/clis/monad.py index 0891ed5aced..c8c596883b5 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/monad.py +++ b/packages/testing/src/execution_testing/client_clis/clis/monad.py @@ -35,7 +35,9 @@ FORK_REVISION_SCHEDULES = { "MONAD_EIGHT": [(8, 0)], "MONAD_NINE": [(9, 0)], + "MONAD_NEXT": [(10, 0)], "MONAD_EIGHTToMONAD_NINEAtTime15k": [(8, 0), (9, 15_000)], + "MONAD_NINEToMONAD_NEXTAtTime15k": [(9, 0), (10, 15_000)], } @@ -195,6 +197,13 @@ def _init_triedb(self, db_path: Path) -> None: "26", "--root-offsets-chunk-count", "2", + # The page-encoded monad state machine; required for + # MONAD_NEXT (MIP-8 pageified storage) read_storage_page, + # and handles earlier monad revisions too. + # NOTE: may BREAK for pre-MIP-8 setups. + # TODO this + "--state-machine", + "monad", ], capture_output=True, text=True, From 1f56129a0658a22a59537a80d90b026fb8d7bfc6 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:27:40 +0000 Subject: [PATCH 09/22] consumer: schedule-aware triedb encoding for MIP-8 transition Slot/page/dual-db init from the fixture revision schedule. Co-Authored-By: Claude Opus 4.8 (1M context) (cherry picked from commit cad717d1723ce2bdb054574c516e2c7cf633fe09) --- .../client_clis/clis/monad.py | 56 ++++++++++++++----- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/packages/testing/src/execution_testing/client_clis/clis/monad.py b/packages/testing/src/execution_testing/client_clis/clis/monad.py index c8c596883b5..7fa29612b21 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/monad.py +++ b/packages/testing/src/execution_testing/client_clis/clis/monad.py @@ -19,7 +19,7 @@ import tempfile import uuid from pathlib import Path -from typing import Any, Dict, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple import pytest @@ -177,16 +177,31 @@ def __init__( super().__init__(binary=binary) self.trace = trace - def _init_triedb(self, db_path: Path) -> None: - """Create and format a fresh triedb file via `monad-mpt`.""" + def _init_triedb( + self, db_path: Path, schedule: List[Tuple[int, int]] + ) -> None: + """ + Create and format a fresh triedb file via `monad-mpt`. + + The storage encoding depends on the fixture's revision schedule. + MIP-8 (MONAD_NEXT, revision >= 10) uses a page-encoded triedb; older + revisions use slot encoding. A fixture that crosses the boundary + (e.g. a MONAD_NINE->MONAD_NEXT transition) needs both: a slot-encoded + primary plus an activated page-encoded secondary timeline, so the + runloop can dual-write across the fork. + """ monad_mpt = self.binary.parent / "monad-mpt" + revisions = [revision for revision, _ in schedule] + uses_page = any(revision >= 10 for revision in revisions) + uses_slot = any(revision < 10 for revision in revisions) + with open(db_path, "wb") as f: f.truncate(2 * 1024**3) - # Fixtures persist a tiny state across a few blocks; the - # production defaults (256 MiB chunks at an 18-chunk minimum, - # plus a 268M-entry history ring) make formatting and the - # genesis commit dominate runtime. Shrinking both cuts per-test - # time from ~100s to ~2s with identical post-state. + + # Shrunk chunk capacity / history ring keep per-test time at ~2s + # (the production defaults dominate runtime). `monad` is the + # page-encoded state machine, `ethereum` the slot-encoded one. + primary = "monad" if uses_page and not uses_slot else "ethereum" subprocess.run( [ str(monad_mpt), @@ -197,19 +212,30 @@ def _init_triedb(self, db_path: Path) -> None: "26", "--root-offsets-chunk-count", "2", - # The page-encoded monad state machine; required for - # MONAD_NEXT (MIP-8 pageified storage) read_storage_page, - # and handles earlier monad revisions too. - # NOTE: may BREAK for pre-MIP-8 setups. - # TODO this "--state-machine", - "monad", + primary, ], capture_output=True, text=True, check=True, ) + if uses_page and uses_slot: + # Transition fixture: add a page-encoded secondary timeline. + subprocess.run( + [ + str(monad_mpt), + "--storage", + str(db_path), + "--activate-secondary", + "--state-machine", + "monad", + ], + capture_output=True, + text=True, + check=True, + ) + def _run_harness( self, input_path: Path, @@ -307,7 +333,7 @@ def consume_fixture( ledger_dir = tmp_path / "ledger" ledger_dir.mkdir() db_path = tmp_path / "triedb" - self._init_triedb(db_path) + self._init_triedb(db_path, FORK_REVISION_SCHEDULES[network]) input_path.write_text(json.dumps(input_doc, indent=2)) From e01e1ef2149bbd07a74def02cca103ae9bad3f9b Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:04:33 +0000 Subject: [PATCH 10/22] docs: MIP-8 dual-db fork-transition flow in MONAD_RUNLOOP_TESTING Co-Authored-By: Claude Opus 4.8 (1M context) (cherry picked from commit 702b90af39878bf7a0d9605648357a1eb6fb3d66) --- MONAD_RUNLOOP_TESTING.md | 196 +++++++++++++++++++++++++++++++++++++++ whitelist.txt | 1 + 2 files changed, 197 insertions(+) diff --git a/MONAD_RUNLOOP_TESTING.md b/MONAD_RUNLOOP_TESTING.md index 927160aedce..7b2e9c50a81 100644 --- a/MONAD_RUNLOOP_TESTING.md +++ b/MONAD_RUNLOOP_TESTING.md @@ -80,3 +80,199 @@ uv run consume direct --input ../fixtures_eestnet \ - The runloop runs in a privileged container (io_uring) and needs `vm.nr_hugepages >= 2048` on the host; the `bin/` wrappers re-provision this automatically (it resets on host reboot). + +## Fork-transition flow (MIP-8 dual-db) + +End-to-end trace of one `MONAD_NINEToMONAD_NEXTAtTime15k` blockchain-test +fixture, from JSON input through the consumer, the `eest-runner` harness, +the C++ FFI, the dual-db, and back to the consumer's assertions. The +execution core (everything in `monad-bft/monad-execution`) is the same +code a production node runs when consuming consensus blocks; only the +block source and the revision schedule differ. MONAD_NINE storage is +slot-encoded; MONAD_NEXT (MIP-8) is page-encoded, so a transition fixture +crosses an encoding boundary mid-run. + +### The fixture + +Declared in `tests/monad_ten/mip8_pageified_storage/test_fork_transition.py` +with `@pytest.mark.valid_at_transition_to("MONAD_NEXT")`; the transition +fork is `@transition_fork(to_fork=MONAD_NEXT, from_fork=MONAD_NINE, +at_timestamp=15_000)` (`forks/forks/transition.py`). The filler emits two +blocks straddling the boundary: block 1 at `timestamp=14_999` (executes +under MONAD_NINE, slot-encoded) and block 2 at `timestamp=15_000` +(MONAD_NEXT, page-encoded). The fixture carries +`"network": "MONAD_NINEToMONAD_NEXTAtTime15k"`, `genesisRLP`, `pre`, +`blocks[]`, and `postState`. The final block's `blockHeader.stateRoot` is +the page-encoded root. + +### Consumer: fixture to harness input + +`MonadFixtureConsumer.consume_fixture()` in `clis/monad.py` loads the +fixture, asserts the network is in `FORK_REVISION_SCHEDULES`, and skips +`expectException` blocks (the ledger cannot propose invalid blocks). The +schedule maps EEST fork names to `(revision, from_timestamp)` tuples: + +```python +"MONAD_NINE": [(9, 0)], +"MONAD_NEXT": [(10, 0)], +"MONAD_NINEToMONAD_NEXTAtTime15k": [(9, 0), (10, 15_000)], +``` + +It builds the harness input doc (`genesis_alloc`, `genesis_rlp`, +`revision_schedule`, digested `blocks`). + +### Dual-db provisioning (`_init_triedb`) + +The slot/page decision is made before the harness runs: + +```python +uses_page = any(rev >= 10 for rev in revisions) # MONAD_NEXT+ +uses_slot = any(rev < 10 for rev in revisions) # pre-MIP-8 +primary = "monad" if uses_page and not uses_slot else "ethereum" +``` + +- Pure MONAD_NINE -> `ethereum` (slot) primary, single timeline. +- Pure MONAD_NEXT -> `monad` (page) primary, single timeline. +- Transition -> `ethereum` primary **plus** a second invocation: + +```bash +monad-mpt --storage --create --chunk-capacity 26 \ + --root-offsets-chunk-count 2 --state-machine ethereum +monad-mpt --storage --activate-secondary --state-machine monad +``` + +`--activate-secondary` stamps the secondary timeline's +`state_machine_kind = monad`, shrinks the primary chunk ring, and hands +chunks to the secondary. Result on disk: one triedb file with a +slot-encoded **primary** timeline and an empty page-encoded **secondary** +timeline (`timeline_id` is `{primary=0, secondary=1}`). + +### Harness: simulate consensus, then execute + +`_run_harness` launches `eest-runner` (`--input/--output/--ledger-dir/ +--db`) with `PR_SET_PDEATHSIG` and a named container for cleanup. The +Rust harness (`rust-harness/eest-runner/src/main.rs`): + +1. Parses the input JSON. +2. Mimics consensus with a Ledger simulator (`propose` then `finalize` + per block), writing BFT headers/bodies into `--ledger-dir` — the same + artifacts monad-bft persists on a real network. +3. Builds the runloop over the FFI (`monad_runloop_new_eest` parses the + schedule into an `EestNet` chain). +4. Calls `runloop.run(n_blocks)`, then reads the root and post-state + back, writing `output.json`. + +The `monad-cxx` crate links dynamically against `libmonad_execution.so`. + +### FFI: opening the dual-db + +`MonadRunloopImpl` ctor in +`category/execution/runloop/runloop_interface_monad.cpp` registers the +state machines (`register_ethereum_state_machines` -> `OnDiskMachine` +slot; `register_monad_state_machines` -> `MonadOnDiskMachine` page) before +constructing `mpt::Db`, since the ctor rebuilds each timeline's machine +from its persisted kind. Then: + +```cpp +if (raw_db.timeline_active(mpt::timeline_id::secondary)) { + secondary_raw_db = raw_db.open_secondary_timeline(); + secondary_triedb.emplace(*secondary_raw_db); + MONAD_ASSERT(secondary_triedb->is_page_encoded(), ...); +} +``` + +Genesis is loaded into both timelines. For pure fixtures the secondary +stays empty and the paths below collapse to a single timeline. + +### Execution loop (`runloop_monad`) + +`monad_runloop_run` calls `runloop_monad(... secondary_db, is_first_run)`, +passing the secondary as a raw pointer. `runloop_monad` reads each block +from the ledger directory (the same files the harness, or production +consensus, wrote) and dispatches per block: + +```cpp +auto const rev = chain.get_monad_revision(header.timestamp); // 9 or 10 +SWITCH_MONAD_TRAITS(propose_block, ..., db, ..., secondary_db); +``` + +`SWITCH_MONAD_TRAITS` turns the runtime revision into a compile-time +`MonadTraits<...>`. Block 1 dispatches to MONAD_NINE, block 2 to +MONAD_NEXT — the fork boundary is purely `get_monad_revision(timestamp)`. +The secondary db is threaded through every state-mutating call +(`set_block_and_prefix`, `update_voted_metadata`, `finalize`, +`BlockState(db, vm, secondary_db)`). + +### Per-block dual-write (`commit_block`) + +`category/execution/monad/db/commit_block_migration.cpp`. Single timeline +(`secondary_db == nullptr`): `PageCommitBuilder` at MONAD_NEXT+, else +`CommitBuilder`. Dual timeline (the transition path): both timelines get +the same `StateDeltas` every block, only the encoding differs, and the +canonical db commits first so its live roots feed the other's +`populate_header`: + +```cpp +CommitBuilder builder (header.number); // slot -> primary +PageCommitBuilder builder2(header.number, *secondary_db); // page -> secondary +if constexpr (traits::monad_rev() >= MONAD_NEXT) { // post-fork: page canonical + correct_db = secondary_db; + secondary_db->commit(...); primary_db.commit(...); +} else { // pre-fork: slot canonical + correct_db = &primary_db; + primary_db.commit(...); secondary_db->commit(...); +} +``` + +The flip at `>= MONAD_NEXT` is the entire transition: pre-fork the slot +timeline is authoritative, post-fork the page timeline is. Each +`Db::commit` is two-stage (upsert deltas, populate header roots from +stage-1 state, upsert header + write root). + +Encoding difference: slot stores trie path `keccak(addr)|keccak(slot)` +-> raw 32-byte value. Page groups slots by `page_key = slot >> 7` (128 +slots/page), reads the whole page on first touch, merges deltas, and +stores one entry per page keyed `keccak(addr)|keccak(page_key)`; the page +hash is MIP-8's Induced-Subtree Merkle Commitment (BLAKE3 over a sparse +128-slot bitmap). A single static-encoding db cannot span the transition +because block 1 must emit slot entries and block 2 page entries against a +consistent account trie; the dual-db runs both encodings in lockstep so +each side stays internally consistent across the boundary. + +### Final root and assertions + +`monad_runloop_get_state_root` returns the secondary (page) root in +dual-db mode, else the single timeline's root. A transition fixture ends +in block 2 (MONAD_NEXT), so the canonical root is the page timeline's — +matching the fixture's final `blockHeader.stateRoot`. The consumer then +asserts (`clis/monad.py`): + +- `output["state_root"]` == `fixture["blocks"][-1]["blockHeader"]["stateRoot"]`. +- Every `postState` account's balance, nonce, code, and per-slot storage + matches `output["post_state"]`, with no unexpected non-zero slots. + +### Correspondence to production + +The execution core is identical; only the block source and schedule +differ. + +| Concern | eest-runner | Production node | +|---|---|---| +| Block source | Ledger sim writes headers/bodies to `--ledger-dir` | `monad-bft` `MonadBlockFileLedger` persists consensus-committed blocks to the ledger dir | +| Revision schedule | runtime, from fixture (`EestNet::get_monad_revision`) | compiled-in timestamps (`MonadMainnet::get_monad_revision`) | +| Execution loop | `runloop_monad` | the same `runloop_monad` | +| Per-block dispatch | `SWITCH_MONAD_TRAITS` on `get_monad_revision(timestamp)` | the same | +| Block execution | `execute_block` | the same | +| Commit | `commit_block` with dual-write | the same function; dual-write engages whenever a secondary timeline is active | +| State root / db | `mpt::Db` + `TrieDb`, optional secondary | the same | + +eest-only shims: the C ABI in `runloop_interface_monad.cpp`, the `EestNet` +chain (runtime schedule), `MonadRunloopDbCache` / `set_balance` (balance +overrides, unused here), genesis loading from the fixture, and the Rust +Ledger simulator that fabricates the BFT blocks consensus would deliver. +The production fork migration uses the same dual-db mechanism: an operator +runs `monad-mpt --activate-secondary --state-machine monad` on the live +slot-encoded db before the fork, the node opens the secondary timeline and +dual-writes both encodings across the boundary, and the authoritative root +flips from slot to page at the first MONAD_NEXT block — exactly what these +transition fixtures verify. diff --git a/whitelist.txt b/whitelist.txt index 958faf89571..4bb6a579cc1 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -322,6 +322,7 @@ copytree cp CPUs CPython +crate create create2 create3 From 476c732aa1addf760ed497fd5ba557ed5f658e1f Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:00:01 +0000 Subject: [PATCH 11/22] fill: make --monad-runloop transition fills pass 200M default gas limit under --monad-runloop, monad_runloop skip marker, mip4 transition fork-scope fix. Co-Authored-By: Claude Opus 4.8 (1M context) (cherry picked from commit c0379897e412d8a54ee3e999ae6ed2022b70e6e5) --- .../cli/pytest_commands/plugins/filler/filler.py | 12 ++++++++++++ .../pytest_commands/plugins/shared/execute_fill.py | 6 ++++++ tests/frontier/validation/test_transaction.py | 7 +++++++ .../mip4_checkreservebalance/test_fork_transition.py | 2 +- 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/filler.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/filler.py index 5fce92e8868..d446fbef525 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/filler.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/filler.py @@ -712,6 +712,13 @@ def pytest_configure(config: pytest.Config) -> None: # 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] @@ -1846,6 +1853,11 @@ 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 + ): + for mark in marker.args: + item.add_marker(mark) if "yul" in item.fixturenames: # type: ignore item.add_marker(pytest.mark.yul_test) diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/execute_fill.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/execute_fill.py index d827d96927a..2651faaac6b 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/execute_fill.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/execute_fill.py @@ -188,6 +188,12 @@ def pytest_configure(config: pytest.Config) -> None: "markers", "execute: Markers to be added in execute mode only.", ) + 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).", + ) config.addinivalue_line( "markers", "exception_test: Negative tests that include an invalid block or " diff --git a/tests/frontier/validation/test_transaction.py b/tests/frontier/validation/test_transaction.py index 1b665a4e073..237b4186ce1 100644 --- a/tests/frontier/validation/test_transaction.py +++ b/tests/frontier/validation/test_transaction.py @@ -21,6 +21,13 @@ from execution_testing.test_types.transaction_types import TransactionDefaults +@pytest.mark.monad_runloop( + pytest.mark.skip( + reason="Requires a test-controlled block gas limit; the monad " + "runloop fixes every block to the proposal gas limit, so the " + "over-limit tx no longer exceeds it." + ) +) @pytest.mark.exception_test @pytest.mark.eels_base_coverage def test_tx_gas_limit( diff --git a/tests/monad_nine/mip4_checkreservebalance/test_fork_transition.py b/tests/monad_nine/mip4_checkreservebalance/test_fork_transition.py index 3ed92bd2ac1..450e8f62f37 100644 --- a/tests/monad_nine/mip4_checkreservebalance/test_fork_transition.py +++ b/tests/monad_nine/mip4_checkreservebalance/test_fork_transition.py @@ -23,7 +23,7 @@ REFERENCE_SPEC_VERSION = ref_spec_mip4.version -@pytest.mark.valid_at_transition_to("MONAD_NINE", subsequent_forks=True) +@pytest.mark.valid_at_transition_to("MONAD_NINE") def test_fork_transition( blockchain_test: BlockchainTestFiller, pre: Alloc, From e9f7baa22c0a63165a3774567412eb074acd72da Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:12:38 +0000 Subject: [PATCH 12/22] ci: add monad_runloop fixture release feature Fills with --monad-runloop on eestnet chain through MONAD_NEXT for the eest-runner consumer. Co-Authored-By: Claude Opus 4.8 (1M context) (cherry picked from commit 64b5770008898e741ed709af152279848366a6d1) --- .github/configs/feature.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/configs/feature.yaml b/.github/configs/feature.yaml index e966f7f6b7a..9f8cbfa260e 100644 --- a/.github/configs/feature.yaml +++ b/.github/configs/feature.yaml @@ -9,3 +9,8 @@ monad: # (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" From de3afcbd9e59ba4149301f48209c4e4d17463e5a Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:47:47 +0000 Subject: [PATCH 13/22] forks: drop unused PragueToMONAD_EIGHT transition fork Removes the pre-monad entry transition; its only consumer (p256verify before-fork test) now resolves to no forks. Co-Authored-By: Claude Opus 4.8 (1M context) (cherry picked from commit 632173631642e66fd3485d2d158c49d8e535b76b) --- packages/testing/src/execution_testing/forks/__init__.py | 2 -- .../src/execution_testing/forks/forks/transition.py | 7 ------- .../test_p256verify_before_fork.py | 4 ++++ 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/testing/src/execution_testing/forks/__init__.py b/packages/testing/src/execution_testing/forks/__init__.py index 1041e1157a3..a35793a41ce 100644 --- a/packages/testing/src/execution_testing/forks/__init__.py +++ b/packages/testing/src/execution_testing/forks/__init__.py @@ -41,7 +41,6 @@ MONAD_NINEToMONAD_NEXTAtTime15k, OsakaToBPO1AtTime15k, ParisToShanghaiAtTime15k, - PragueToMONAD_EIGHTAtTime15k, PragueToOsakaAtTime15k, ShanghaiToCancunAtTime15k, ) @@ -122,7 +121,6 @@ "PragueToOsakaAtTime15k", "Osaka", "OsakaToBPO1AtTime15k", - "PragueToMONAD_EIGHTAtTime15k", "MONAD_EIGHT", "MONAD_EIGHTToMONAD_NINEAtTime15k", "MONAD_NINE", diff --git a/packages/testing/src/execution_testing/forks/forks/transition.py b/packages/testing/src/execution_testing/forks/forks/transition.py index 6500b48b788..e63edafc18d 100644 --- a/packages/testing/src/execution_testing/forks/forks/transition.py +++ b/packages/testing/src/execution_testing/forks/forks/transition.py @@ -56,13 +56,6 @@ class PragueToOsakaAtTime15k(TransitionBaseClass): pass -@transition_fork(to_fork=MONAD_EIGHT, from_fork=Prague, at_timestamp=15_000) -class PragueToMONAD_EIGHTAtTime15k(TransitionBaseClass): # noqa: N801 - """Prague to MONAD_EIGHT transition at Timestamp 15k.""" - - pass - - @transition_fork( to_fork=MONAD_NINE, from_fork=MONAD_EIGHT, at_timestamp=15_000 ) diff --git a/tests/osaka/eip7951_p256verify_precompiles/test_p256verify_before_fork.py b/tests/osaka/eip7951_p256verify_precompiles/test_p256verify_before_fork.py index 6e1e1f08317..7bc643d49c5 100644 --- a/tests/osaka/eip7951_p256verify_precompiles/test_p256verify_before_fork.py +++ b/tests/osaka/eip7951_p256verify_precompiles/test_p256verify_before_fork.py @@ -22,6 +22,10 @@ REFERENCE_SPEC_GIT_PATH = ref_spec_7951.git_path REFERENCE_SPEC_VERSION = ref_spec_7951.version +# NOTE: the PragueToMONAD_EIGHT transition fork this targets was dropped as +# unused, so this marker currently resolves to no forks and the test +# generates nothing. P256VERIFY at MONAD_EIGHT+ is covered by the +# non-transition p256verify tests. pytestmark = pytest.mark.valid_at_transition_to("MONAD_EIGHT") From 67a8275d2d35bb84f37cb4f67d24d87053fb7173 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:34:53 +0000 Subject: [PATCH 14/22] ci: add consume_release workflow Run a fixtures release against a pinned monad stack via consume direct + eest-runner. Co-Authored-By: Claude Opus 4.8 (1M context) (cherry picked from commit c3b7aff38e9c299bb95d1bd49f97c852b26261b7) --- .github/workflows/consume_release.yaml | 182 +++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 .github/workflows/consume_release.yaml diff --git a/.github/workflows/consume_release.yaml b/.github/workflows/consume_release.yaml new file mode 100644 index 00000000000..6f997d441a5 --- /dev/null +++ b/.github/workflows/consume_release.yaml @@ -0,0 +1,182 @@ +# Manually run a filled fixtures release against a pinned monad stack via +# `consume direct` + the eest-runner runloop client. + +name: Consume Release (monad runloop) + +on: + workflow_dispatch: + inputs: + release: + description: "Fixtures source: release spec (e.g. monad_runloop@v1.1.0-rc1), a fixtures.tar.gz URL, or latest" + required: true + default: "monad_runloop@latest" + harness_ref: + description: "monad-eest-rust-harness ref (branch/tag/SHA)" + required: false + default: "execute-with-eestnet-mip8" + monad_bft_repo: + description: "Override monad-bft from this owner/repo (e.g. category-labs/monad-bft); empty = keep the harness's pinned commit" + required: false + default: "" + monad_bft_ref: + description: "Ref/SHA in monad_bft_repo (default branch if empty); applies only when monad_bft_repo is set" + required: false + default: "" + monad_execution_repo: + description: "Override monad-execution from this owner/repo; empty = keep the harness's pinned commit" + required: false + default: "" + monad_execution_ref: + description: "Ref/SHA in monad_execution_repo (default branch if empty); applies only when monad_execution_repo is set" + required: false + default: "" + filter: + description: "Optional -k expression to consume a subset" + required: false + default: "" + +permissions: + contents: read + +jobs: + consume: + runs-on: ubuntu-24.04 + timeout-minutes: 1440 # hosted runners still cap at 6h; raise the runner for a full release + steps: + - name: Validate inputs + shell: bash + env: + BFT_REPO: ${{ inputs.monad_bft_repo }} + BFT_REF: ${{ inputs.monad_bft_ref }} + EXEC_REPO: ${{ inputs.monad_execution_repo }} + EXEC_REF: ${{ inputs.monad_execution_ref }} + run: | + set -euo pipefail + fail=0 + if [ -n "$BFT_REF" ] && [ -z "$BFT_REPO" ]; then + echo "::error::monad_bft_ref is set but monad_bft_repo is empty; set monad_bft_repo to override monad-bft." + fail=1 + fi + if [ -n "$EXEC_REF" ] && [ -z "$EXEC_REPO" ]; then + echo "::error::monad_execution_ref is set but monad_execution_repo is empty; set monad_execution_repo to override monad-execution." + fail=1 + fi + [ "$fail" -eq 0 ] + + - name: Checkout consumer (execution-specs) + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Checkout monad-eest-rust-harness (build input) + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: monad-exp/monad-eest-rust-harness + ref: ${{ inputs.harness_ref }} + path: monad-eest-rust-harness + submodules: recursive + fetch-depth: 0 + token: ${{ secrets.MONAD_STACK_TOKEN }} + + # Optional submodule overrides. Set a *_repo to pull that submodule from + # a different fork at *_ref (default branch if ref is empty); leave it + # empty to keep the harness's pinned commit. actions/checkout handles + # auth, so no manual credential setup is needed. The monad-bft override + # recurses (so monad-execution lands at the new bft's pin); the + # monad-execution override runs after, so it is not reset. + - name: Override monad-bft + if: ${{ inputs.monad_bft_repo != '' }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: ${{ inputs.monad_bft_repo }} + ref: ${{ inputs.monad_bft_ref }} + path: monad-eest-rust-harness/monad-bft + submodules: recursive + fetch-depth: 0 + token: ${{ secrets.MONAD_STACK_TOKEN }} + + - name: Override monad-execution + if: ${{ inputs.monad_execution_repo != '' }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: ${{ inputs.monad_execution_repo }} + ref: ${{ inputs.monad_execution_ref }} + path: monad-eest-rust-harness/monad-bft/monad-execution + fetch-depth: 0 + token: ${{ secrets.MONAD_STACK_TOKEN }} + + - name: Log pinned stack + working-directory: monad-eest-rust-harness + shell: bash + run: | + echo "monad-bft: $(git -C monad-bft rev-parse HEAD)" + echo "monad-execution: $(git -C monad-bft/monad-execution rev-parse HEAD)" + + - name: Build the monad-builder image + working-directory: monad-eest-rust-harness + shell: bash + run: docker build -t monad-builder:latest - < monad-bft/docker/builder/Dockerfile + + - name: Build the eest-runner stack (build.sh) + working-directory: monad-eest-rust-harness + shell: bash + run: ./build.sh + + - name: Setup uv + uses: ./.github/actions/setup-uv + + - name: Install EEST + shell: bash + run: uv sync --no-progress + + - name: Provision hugepages + shell: bash + run: sudo sysctl -w vm.nr_hugepages=3072 + + - name: Consume release + id: consume + shell: bash + run: | + set -o pipefail + FILTER_ARG=() + if [ -n "${{ inputs.filter }}" ]; then + FILTER_ARG=(-k "${{ inputs.filter }}") + fi + uv run consume direct \ + --input "${{ inputs.release }}" \ + --bin "$GITHUB_WORKSPACE/monad-eest-rust-harness/bin/eest-runner" \ + "${FILTER_ARG[@]}" \ + --html report.html | tee consume.log + + - name: Summary + if: always() + shell: bash + run: | + { + echo "## consume \`${{ inputs.release }}\`" + echo "" + echo "| input | value |" + echo "|---|---|" + echo "| harness_ref | \`${{ inputs.harness_ref }}\` |" + echo "| monad_bft_ref | \`${{ inputs.monad_bft_ref || 'pinned' }}\` |" + echo "| monad_execution_ref | \`${{ inputs.monad_execution_ref || 'pinned' }}\` |" + echo "| filter | \`${{ inputs.filter || '(none)' }}\` |" + echo "" + echo '```' + grep -E "= .*(passed|failed|error|skipped).*in " consume.log | tail -1 || echo "no result line" + echo '```' + if grep -qE "^FAILED" consume.log; then + echo "### Failures" + echo '```' + grep -E "^FAILED" consume.log | sed -E 's/ - .*//' | sort -u | head -200 + echo '```' + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload report + log + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: consume-report + path: | + report.html + consume.log + if-no-files-found: warn From 8c1a23f056285386a413f05bee657ba04ac4b92c Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:12:15 +0000 Subject: [PATCH 15/22] consume: resolve releases from monad-developers/execution-specs Add the monad fork's release repo to SUPPORTED_REPOS so monad_runloop@... etc. resolve. Releases live in execution-specs now that EEST mirroring is disabled. Co-Authored-By: Claude --- .../cli/pytest_commands/plugins/consume/releases.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/releases.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/releases.py index 556fa7fe2c3..9d3a3ba12a7 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/releases.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/releases.py @@ -19,6 +19,7 @@ ) SUPPORTED_REPOS = [ + "monad-developers/execution-specs", "ethereum/execution-spec-tests", "ethereum/execution-specs", "ethereum/tests", From 01e2ca5d7a95d3c6075c1df3ee6b221aa8a25a59 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:08:09 +0000 Subject: [PATCH 16/22] ci(consume): paste full consume.log in summary, fix fields, drop html Summary now sources effective release/filter and resolved stack SHAs from the steps that ran (push events leave inputs empty); artifact keeps consume.log only. Co-Authored-By: Claude Opus 4.8 (1M context) (cherry picked from commit 5e20ed4fcf02d163cec36682fc82a19d8ef18be9) --- .github/workflows/consume_release.yaml | 46 ++++++++++++++------------ 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/.github/workflows/consume_release.yaml b/.github/workflows/consume_release.yaml index 6f997d441a5..4eb0dac16f4 100644 --- a/.github/workflows/consume_release.yaml +++ b/.github/workflows/consume_release.yaml @@ -13,7 +13,7 @@ on: harness_ref: description: "monad-eest-rust-harness ref (branch/tag/SHA)" required: false - default: "execute-with-eestnet-mip8" + default: "execute-with-eestnet" monad_bft_repo: description: "Override monad-bft from this owner/repo (e.g. category-labs/monad-bft); empty = keep the harness's pinned commit" required: false @@ -104,11 +104,16 @@ jobs: token: ${{ secrets.MONAD_STACK_TOKEN }} - name: Log pinned stack + id: stack working-directory: monad-eest-rust-harness shell: bash run: | - echo "monad-bft: $(git -C monad-bft rev-parse HEAD)" - echo "monad-execution: $(git -C monad-bft/monad-execution rev-parse HEAD)" + bft=$(git -C monad-bft rev-parse HEAD) + exec=$(git -C monad-bft/monad-execution rev-parse HEAD) + echo "monad-bft: $bft" + echo "monad-execution: $exec" + echo "bft=$bft" >> "$GITHUB_OUTPUT" + echo "exec=$exec" >> "$GITHUB_OUTPUT" - name: Build the monad-builder image working-directory: monad-eest-rust-harness @@ -136,6 +141,8 @@ jobs: shell: bash run: | set -o pipefail + echo "release=${{ inputs.release }}" >> "$GITHUB_OUTPUT" + echo "filter=${{ inputs.filter || '(none)' }}" >> "$GITHUB_OUTPUT" FILTER_ARG=() if [ -n "${{ inputs.filter }}" ]; then FILTER_ARG=(-k "${{ inputs.filter }}") @@ -143,40 +150,37 @@ jobs: uv run consume direct \ --input "${{ inputs.release }}" \ --bin "$GITHUB_WORKSPACE/monad-eest-rust-harness/bin/eest-runner" \ - "${FILTER_ARG[@]}" \ - --html report.html | tee consume.log + "${FILTER_ARG[@]}" | tee consume.log - name: Summary if: always() shell: bash + env: + RELEASE: ${{ steps.consume.outputs.release }} + FILTER: ${{ steps.consume.outputs.filter }} + HARNESS_REF: ${{ inputs.harness_ref }} + BFT_SHA: ${{ steps.stack.outputs.bft }} + EXEC_SHA: ${{ steps.stack.outputs.exec }} run: | { - echo "## consume \`${{ inputs.release }}\`" + echo "## consume \`${RELEASE:-(unknown)}\`" echo "" echo "| input | value |" echo "|---|---|" - echo "| harness_ref | \`${{ inputs.harness_ref }}\` |" - echo "| monad_bft_ref | \`${{ inputs.monad_bft_ref || 'pinned' }}\` |" - echo "| monad_execution_ref | \`${{ inputs.monad_execution_ref || 'pinned' }}\` |" - echo "| filter | \`${{ inputs.filter || '(none)' }}\` |" + echo "| harness_ref | \`${HARNESS_REF}\` |" + echo "| monad-bft | \`${BFT_SHA:-(unknown)}\` |" + echo "| monad-execution | \`${EXEC_SHA:-(unknown)}\` |" + echo "| filter | \`${FILTER:-(none)}\` |" echo "" echo '```' - grep -E "= .*(passed|failed|error|skipped).*in " consume.log | tail -1 || echo "no result line" + cat consume.log 2>/dev/null || echo "(no consume.log produced)" echo '```' - if grep -qE "^FAILED" consume.log; then - echo "### Failures" - echo '```' - grep -E "^FAILED" consume.log | sed -E 's/ - .*//' | sort -u | head -200 - echo '```' - fi } >> "$GITHUB_STEP_SUMMARY" - - name: Upload report + log + - name: Upload log if: always() uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: consume-report - path: | - report.html - consume.log + path: consume.log if-no-files-found: warn From d415ab4a9625352fc2058f83395f72246625c973 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Mon, 29 Jun 2026 14:37:29 +0000 Subject: [PATCH 17/22] ci(consume): run consume direct with -v Co-Authored-By: Claude Opus 4.8 (1M context) (cherry picked from commit 6d1fa4ddbe00c390690ed2c32cd17e9da9aecdaa) --- .github/workflows/consume_release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/consume_release.yaml b/.github/workflows/consume_release.yaml index 4eb0dac16f4..73f2be402ba 100644 --- a/.github/workflows/consume_release.yaml +++ b/.github/workflows/consume_release.yaml @@ -150,7 +150,7 @@ jobs: uv run consume direct \ --input "${{ inputs.release }}" \ --bin "$GITHUB_WORKSPACE/monad-eest-rust-harness/bin/eest-runner" \ - "${FILTER_ARG[@]}" | tee consume.log + "${FILTER_ARG[@]}" -v | tee consume.log - name: Summary if: always() From 386f94604600eaca80d78c54c713a2ab11dfe497 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Wed, 15 Jul 2026 12:59:56 +0000 Subject: [PATCH 18/22] docs: condense MONAD_RUNLOOP_TESTING to running + overview Co-Authored-By: Claude --- MONAD_RUNLOOP_TESTING.md | 220 +++++++-------------------------------- 1 file changed, 37 insertions(+), 183 deletions(-) diff --git a/MONAD_RUNLOOP_TESTING.md b/MONAD_RUNLOOP_TESTING.md index 7b2e9c50a81..a0b79d3fff9 100644 --- a/MONAD_RUNLOOP_TESTING.md +++ b/MONAD_RUNLOOP_TESTING.md @@ -1,16 +1,16 @@ # Running EEST fixtures on the monad runloop Executes blockchain fixtures (including those generated from state -tests) against the production monad execution client, via its -`runloop` and the consensus ledger directory. The oracle is the -fixture's `postState`, compared account by account -(balance/nonce/code/storage). +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 +executed result. ## Repos | Repo / branch | Role | |---|---| -| `monad-exp/monad-eest-rust-harness` @ `execute-with-eestnet` | `eest-runner` harness: builds consensus blocks from a digested fixture and runs them on the runloop | +| `monad-exp/monad-eest-rust-harness` @ `execute-with-eestnet` | `eest-runner` harness: builds consensus blocks from a fixture and runs them on the runloop | | `monad-bft` @ `execute-with-eestnet` (submodule of the above) | consensus block types + ledger writer; pins monad-execution below | | `monad` @ `execute-with-eestnet` (submodule of monad-bft) | execution client with the `EestNet` chain (id 30143, per-fixture revision schedule, runtime genesis) and the extended `monad_runloop_*` FFI | | this repo @ `execute-with-eestnet` | `MonadFixtureConsumer` (`packages/testing/.../client_clis/clis/monad.py`) wired into `consume direct` | @@ -58,19 +58,15 @@ uv run consume direct --input ../fixtures_eestnet \ - `--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) so filled block hashes and EIP-2935 history storage - match the runloop; without it those slots diverge and fail the - post-state compare. -- `--chain-id 30143` is EestNet's chain id; transactions must be - signed for it, so it is required at fill time. + requests_hash). +- `--chain-id 30143` is EestNet's chain id. - 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 injects it into the chain. - `consume` parallelism is CPU-bound: one runloop peaks near 4 cores (~386% CPU across ~14 threads), so budget ~5 vCPUs per worker (`-n N` needs - roughly `5 * N` cores). On a small host (e.g. 4 vCPUs) run - sequentially — `-n` above 1 oversubscribes and runs slower. + roughly `5 * N` cores). ## Behavior and known limits @@ -83,178 +79,37 @@ uv run consume direct --input ../fixtures_eestnet \ ## Fork-transition flow (MIP-8 dual-db) -End-to-end trace of one `MONAD_NINEToMONAD_NEXTAtTime15k` blockchain-test -fixture, from JSON input through the consumer, the `eest-runner` harness, -the C++ FFI, the dual-db, and back to the consumer's assertions. The -execution core (everything in `monad-bft/monad-execution`) is the same -code a production node runs when consuming consensus blocks; only the -block source and the revision schedule differ. MONAD_NINE storage is -slot-encoded; MONAD_NEXT (MIP-8) is page-encoded, so a transition fixture -crosses an encoding boundary mid-run. - -### The fixture - -Declared in `tests/monad_ten/mip8_pageified_storage/test_fork_transition.py` -with `@pytest.mark.valid_at_transition_to("MONAD_NEXT")`; the transition -fork is `@transition_fork(to_fork=MONAD_NEXT, from_fork=MONAD_NINE, -at_timestamp=15_000)` (`forks/forks/transition.py`). The filler emits two -blocks straddling the boundary: block 1 at `timestamp=14_999` (executes -under MONAD_NINE, slot-encoded) and block 2 at `timestamp=15_000` -(MONAD_NEXT, page-encoded). The fixture carries -`"network": "MONAD_NINEToMONAD_NEXTAtTime15k"`, `genesisRLP`, `pre`, -`blocks[]`, and `postState`. The final block's `blockHeader.stateRoot` is -the page-encoded root. - -### Consumer: fixture to harness input - -`MonadFixtureConsumer.consume_fixture()` in `clis/monad.py` loads the -fixture, asserts the network is in `FORK_REVISION_SCHEDULES`, and skips -`expectException` blocks (the ledger cannot propose invalid blocks). The -schedule maps EEST fork names to `(revision, from_timestamp)` tuples: - -```python -"MONAD_NINE": [(9, 0)], -"MONAD_NEXT": [(10, 0)], -"MONAD_NINEToMONAD_NEXTAtTime15k": [(9, 0), (10, 15_000)], -``` - -It builds the harness input doc (`genesis_alloc`, `genesis_rlp`, -`revision_schedule`, digested `blocks`). - -### Dual-db provisioning (`_init_triedb`) - -The slot/page decision is made before the harness runs: - -```python -uses_page = any(rev >= 10 for rev in revisions) # MONAD_NEXT+ -uses_slot = any(rev < 10 for rev in revisions) # pre-MIP-8 -primary = "monad" if uses_page and not uses_slot else "ethereum" -``` - -- Pure MONAD_NINE -> `ethereum` (slot) primary, single timeline. -- Pure MONAD_NEXT -> `monad` (page) primary, single timeline. -- Transition -> `ethereum` primary **plus** a second invocation: - -```bash -monad-mpt --storage --create --chunk-capacity 26 \ - --root-offsets-chunk-count 2 --state-machine ethereum -monad-mpt --storage --activate-secondary --state-machine monad -``` - -`--activate-secondary` stamps the secondary timeline's -`state_machine_kind = monad`, shrinks the primary chunk ring, and hands -chunks to the secondary. Result on disk: one triedb file with a -slot-encoded **primary** timeline and an empty page-encoded **secondary** -timeline (`timeline_id` is `{primary=0, secondary=1}`). - -### Harness: simulate consensus, then execute - -`_run_harness` launches `eest-runner` (`--input/--output/--ledger-dir/ ---db`) with `PR_SET_PDEATHSIG` and a named container for cleanup. The -Rust harness (`rust-harness/eest-runner/src/main.rs`): - -1. Parses the input JSON. -2. Mimics consensus with a Ledger simulator (`propose` then `finalize` - per block), writing BFT headers/bodies into `--ledger-dir` — the same - artifacts monad-bft persists on a real network. -3. Builds the runloop over the FFI (`monad_runloop_new_eest` parses the - schedule into an `EestNet` chain). -4. Calls `runloop.run(n_blocks)`, then reads the root and post-state - back, writing `output.json`. - -The `monad-cxx` crate links dynamically against `libmonad_execution.so`. - -### FFI: opening the dual-db - -`MonadRunloopImpl` ctor in -`category/execution/runloop/runloop_interface_monad.cpp` registers the -state machines (`register_ethereum_state_machines` -> `OnDiskMachine` -slot; `register_monad_state_machines` -> `MonadOnDiskMachine` page) before -constructing `mpt::Db`, since the ctor rebuilds each timeline's machine -from its persisted kind. Then: - -```cpp -if (raw_db.timeline_active(mpt::timeline_id::secondary)) { - secondary_raw_db = raw_db.open_secondary_timeline(); - secondary_triedb.emplace(*secondary_raw_db); - MONAD_ASSERT(secondary_triedb->is_page_encoded(), ...); -} -``` - -Genesis is loaded into both timelines. For pure fixtures the secondary -stays empty and the paths below collapse to a single timeline. - -### Execution loop (`runloop_monad`) - -`monad_runloop_run` calls `runloop_monad(... secondary_db, is_first_run)`, -passing the secondary as a raw pointer. `runloop_monad` reads each block -from the ledger directory (the same files the harness, or production -consensus, wrote) and dispatches per block: - -```cpp -auto const rev = chain.get_monad_revision(header.timestamp); // 9 or 10 -SWITCH_MONAD_TRAITS(propose_block, ..., db, ..., secondary_db); -``` - -`SWITCH_MONAD_TRAITS` turns the runtime revision into a compile-time -`MonadTraits<...>`. Block 1 dispatches to MONAD_NINE, block 2 to -MONAD_NEXT — the fork boundary is purely `get_monad_revision(timestamp)`. -The secondary db is threaded through every state-mutating call -(`set_block_and_prefix`, `update_voted_metadata`, `finalize`, -`BlockState(db, vm, secondary_db)`). - -### Per-block dual-write (`commit_block`) - -`category/execution/monad/db/commit_block_migration.cpp`. Single timeline -(`secondary_db == nullptr`): `PageCommitBuilder` at MONAD_NEXT+, else -`CommitBuilder`. Dual timeline (the transition path): both timelines get -the same `StateDeltas` every block, only the encoding differs, and the -canonical db commits first so its live roots feed the other's -`populate_header`: - -```cpp -CommitBuilder builder (header.number); // slot -> primary -PageCommitBuilder builder2(header.number, *secondary_db); // page -> secondary -if constexpr (traits::monad_rev() >= MONAD_NEXT) { // post-fork: page canonical - correct_db = secondary_db; - secondary_db->commit(...); primary_db.commit(...); -} else { // pre-fork: slot canonical - correct_db = &primary_db; - primary_db.commit(...); secondary_db->commit(...); -} -``` - -The flip at `>= MONAD_NEXT` is the entire transition: pre-fork the slot -timeline is authoritative, post-fork the page timeline is. Each -`Db::commit` is two-stage (upsert deltas, populate header roots from -stage-1 state, upsert header + write root). - -Encoding difference: slot stores trie path `keccak(addr)|keccak(slot)` --> raw 32-byte value. Page groups slots by `page_key = slot >> 7` (128 -slots/page), reads the whole page on first touch, merges deltas, and -stores one entry per page keyed `keccak(addr)|keccak(page_key)`; the page -hash is MIP-8's Induced-Subtree Merkle Commitment (BLAKE3 over a sparse -128-slot bitmap). A single static-encoding db cannot span the transition -because block 1 must emit slot entries and block 2 page entries against a -consistent account trie; the dual-db runs both encodings in lockstep so -each side stays internally consistent across the boundary. - -### Final root and assertions - -`monad_runloop_get_state_root` returns the secondary (page) root in -dual-db mode, else the single timeline's root. A transition fixture ends -in block 2 (MONAD_NEXT), so the canonical root is the page timeline's — -matching the fixture's final `blockHeader.stateRoot`. The consumer then -asserts (`clis/monad.py`): - -- `output["state_root"]` == `fixture["blocks"][-1]["blockHeader"]["stateRoot"]`. -- Every `postState` account's balance, nonce, code, and per-slot storage - matches `output["post_state"]`, with no unexpected non-zero slots. +MONAD_NINE storage is slot-encoded; MONAD_NEXT (MIP-8) is +page-encoded. A transition fixture (e.g. +`MONAD_NINEToMONAD_NEXTAtTime15k`) crosses that boundary mid-run, so +the run keeps both encodings live in one triedb: a slot-encoded +primary timeline and a page-encoded secondary timeline. + +- **Consumer** (`clis/monad.py`): loads the fixture, maps its + `network` to a `(revision, from_timestamp)` schedule, skips + `expectException` blocks, and provisions the db. Revisions spanning + the fork activate the secondary timeline via `monad-mpt + --activate-secondary`. +- **Harness** (`eest-runner`): fakes consensus with a Ledger + simulator (`propose` then `finalize` per block) that writes BFT + headers/bodies to `--ledger-dir` — the same artifacts monad-bft + persists on a real network — then drives the runloop over the FFI + and writes `output.json`. +- **Execution** (`runloop_monad`): reads each block from the ledger + dir and dispatches on `get_monad_revision(timestamp)` via + `SWITCH_MONAD_TRAITS` — block 1 runs MONAD_NINE, block 2 MONAD_NEXT. +- **Dual-write** (`commit_block`): both timelines get the same + `StateDeltas` every block. Slot is canonical before the fork, page + after; the canonical root flips to page at the first MONAD_NEXT + block, matching the fixture's final `blockHeader.stateRoot`. +- **Assertions**: `state_root` and every `postState` account + (balance, nonce, code, per-slot storage) match `output`, with no + unexpected non-zero slots. ### Correspondence to production -The execution core is identical; only the block source and schedule -differ. +eest-runner runs the same execution core as a production node, with +two differences: the block source and the revision schedule. | Concern | eest-runner | Production node | |---|---|---| @@ -274,5 +129,4 @@ The production fork migration uses the same dual-db mechanism: an operator runs `monad-mpt --activate-secondary --state-machine monad` on the live slot-encoded db before the fork, the node opens the secondary timeline and dual-writes both encodings across the boundary, and the authoritative root -flips from slot to page at the first MONAD_NEXT block — exactly what these -transition fixtures verify. +flips from slot to page at the first MONAD_NEXT block. From c1e541d95a060abf7663d0751c45ae256174f74c Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:53:13 +0000 Subject: [PATCH 19/22] ci(consume): route workflow inputs through env vars Prevents shell/output injection via inputs.release and inputs.filter. Co-Authored-By: Claude --- .github/workflows/consume_release.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/consume_release.yaml b/.github/workflows/consume_release.yaml index 73f2be402ba..97e89a1fbcf 100644 --- a/.github/workflows/consume_release.yaml +++ b/.github/workflows/consume_release.yaml @@ -137,18 +137,18 @@ jobs: run: sudo sysctl -w vm.nr_hugepages=3072 - name: Consume release - id: consume shell: bash + env: + RELEASE: ${{ inputs.release }} + FILTER: ${{ inputs.filter }} run: | set -o pipefail - echo "release=${{ inputs.release }}" >> "$GITHUB_OUTPUT" - echo "filter=${{ inputs.filter || '(none)' }}" >> "$GITHUB_OUTPUT" FILTER_ARG=() - if [ -n "${{ inputs.filter }}" ]; then - FILTER_ARG=(-k "${{ inputs.filter }}") + if [ -n "$FILTER" ]; then + FILTER_ARG=(-k "$FILTER") fi uv run consume direct \ - --input "${{ inputs.release }}" \ + --input "$RELEASE" \ --bin "$GITHUB_WORKSPACE/monad-eest-rust-harness/bin/eest-runner" \ "${FILTER_ARG[@]}" -v | tee consume.log @@ -156,8 +156,8 @@ jobs: if: always() shell: bash env: - RELEASE: ${{ steps.consume.outputs.release }} - FILTER: ${{ steps.consume.outputs.filter }} + RELEASE: ${{ inputs.release }} + FILTER: ${{ inputs.filter || '(none)' }} HARNESS_REF: ${{ inputs.harness_ref }} BFT_SHA: ${{ steps.stack.outputs.bft }} EXEC_SHA: ${{ steps.stack.outputs.exec }} From bca49942ef194975a12010b79a98126810e5a88b Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 16 Jul 2026 07:07:44 +0000 Subject: [PATCH 20/22] docs: use ssh clone url for the private harness repo Co-Authored-By: Claude --- MONAD_RUNLOOP_TESTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MONAD_RUNLOOP_TESTING.md b/MONAD_RUNLOOP_TESTING.md index a0b79d3fff9..db759b04212 100644 --- a/MONAD_RUNLOOP_TESTING.md +++ b/MONAD_RUNLOOP_TESTING.md @@ -22,7 +22,7 @@ artifacts, ~6 GB RAM for hugepages. ```sh git clone --branch execute-with-eestnet \ - https://github.com/monad-exp/monad-eest-rust-harness.git + git@github.com:monad-exp/monad-eest-rust-harness.git cd monad-eest-rust-harness git submodule update --init --recursive From 797da40099804aa2e8bed4880e7813c9d88ccf5c Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 16 Jul 2026 07:23:34 +0000 Subject: [PATCH 21/22] fix(consume): resolve release asset for tests- prefixed tags Strip the tests- tag prefix when deriving fixtures_.tar.gz. Co-Authored-By: Claude --- .../cli/pytest_commands/plugins/consume/releases.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/releases.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/releases.py index 9d3a3ba12a7..963e13fca1f 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/releases.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/releases.py @@ -92,7 +92,10 @@ def __eq__(self, value: object) -> bool: @property def asset_name(self) -> str: """Get the asset name.""" - return f"fixtures_{self.tag_name}.tar.gz" + # Monad release tags are `tests-@`, but the asset + # is `fixtures_.tar.gz` (the release workflow strips the + # `tests-` prefix). Mirror that here so spec resolution finds it. + return f"fixtures_{self.tag_name.removeprefix('tests-')}.tar.gz" class Asset(BaseModel): From b39a4e8343fb6db08a71d593319f0f8ab809a728 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Fri, 17 Jul 2026 08:57:01 +0200 Subject: [PATCH 22/22] chore(ci): Use monad-eest-rust-harness and EELS main branches --- .github/workflows/consume_release.yaml | 2 +- MONAD_RUNLOOP_TESTING.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/consume_release.yaml b/.github/workflows/consume_release.yaml index 97e89a1fbcf..0edcf2b4ac5 100644 --- a/.github/workflows/consume_release.yaml +++ b/.github/workflows/consume_release.yaml @@ -13,7 +13,7 @@ on: harness_ref: description: "monad-eest-rust-harness ref (branch/tag/SHA)" required: false - default: "execute-with-eestnet" + default: "main" monad_bft_repo: description: "Override monad-bft from this owner/repo (e.g. category-labs/monad-bft); empty = keep the harness's pinned commit" required: false diff --git a/MONAD_RUNLOOP_TESTING.md b/MONAD_RUNLOOP_TESTING.md index db759b04212..dda1a92041b 100644 --- a/MONAD_RUNLOOP_TESTING.md +++ b/MONAD_RUNLOOP_TESTING.md @@ -10,10 +10,10 @@ executed result. | Repo / branch | Role | |---|---| -| `monad-exp/monad-eest-rust-harness` @ `execute-with-eestnet` | `eest-runner` harness: builds consensus blocks from a fixture and runs them on the runloop | +| `monad-exp/monad-eest-rust-harness` | `eest-runner` harness: builds consensus blocks from a fixture and runs them on the runloop | | `monad-bft` @ `execute-with-eestnet` (submodule of the above) | consensus block types + ledger writer; pins monad-execution below | | `monad` @ `execute-with-eestnet` (submodule of monad-bft) | execution client with the `EestNet` chain (id 30143, per-fixture revision schedule, runtime genesis) and the extended `monad_runloop_*` FFI | -| this repo @ `execute-with-eestnet` | `MonadFixtureConsumer` (`packages/testing/.../client_clis/clis/monad.py`) wired into `consume direct` | +| this repo | `MonadFixtureConsumer` (`packages/testing/.../client_clis/clis/monad.py`) wired into `consume direct` | ## One-time setup @@ -21,7 +21,7 @@ Requirements: docker, ~10 GB disk for the builder image and build artifacts, ~6 GB RAM for hugepages. ```sh -git clone --branch execute-with-eestnet \ +git clone --branch main \ git@github.com:monad-exp/monad-eest-rust-harness.git cd monad-eest-rust-harness git submodule update --init --recursive