diff --git a/.github/configs/feature.yaml b/.github/configs/feature.yaml index e5e3c06fc0b..063e207be9e 100644 --- a/.github/configs/feature.yaml +++ b/.github/configs/feature.yaml @@ -5,7 +5,7 @@ monad: monad_runloop: evm-type: eels # Like `monad`, but `--monad-runloop` and eestnet chain id `30143` - fill-params: -m blockchain_test --from=MONAD_EIGHT --until=MONAD_ELEVEN --chain-id=30143 --monad-runloop -k "not invalid_header" + fill-params: -m blockchain_test --from=MONAD_TEN --until=MONAD_ELEVEN --chain-id=30143 --monad-runloop -k "not invalid_header" monad_benchmark: evm-type: eels @@ -15,4 +15,4 @@ monad_benchmark: # transaction. The excluded `page_spread` cases size their work from `m` # rather than the gas budget, so they need ~29M and ~177M whatever is # passed here. - fill-params: -m blockchain_test tests/benchmark/stateful/mip8_pageified_storage/ --from=MONAD_NINE --until=MONAD_TEN --chain-id=30143 --monad-runloop --gas-benchmark-values=10 -k "not m4096 and not m1024" + fill-params: -m blockchain_test tests/benchmark/stateful/mip8_pageified_storage/ --fork=MONAD_ELEVEN --chain-id=30143 --monad-runloop --gas-benchmark-values=10 -k "not m4096 and not m1024" diff --git a/MONAD_RUNLOOP_TESTING.md b/MONAD_RUNLOOP_TESTING.md index 1873c0de3ea..a3ff7085b01 100644 --- a/MONAD_RUNLOOP_TESTING.md +++ b/MONAD_RUNLOOP_TESTING.md @@ -44,7 +44,7 @@ leaves a stale library that fails silently). ```sh uv run fill --clean -m blockchain_test \ - --fork MONAD_NINE --chain-id 30143 --monad-runloop \ + --fork MONAD_TEN --chain-id 30143 --monad-runloop \ --output ../fixtures_eestnet uv run consume direct --input ../fixtures_eestnet \ @@ -64,12 +64,12 @@ uv run consume direct --input ../fixtures_eestnet \ across ~14 threads), so budget ~5 vCPUs per worker (`-n N` needs roughly `5 * N` cores). -## MIP-8 perf-regression tests +## Storage perf-regression tests `tests/benchmark/stateful/mip8_pageified_storage/test_perf_regression.py` fills SLOAD/SSTORE workloads at both forks and times block execution on -the runloop to compare MONAD_NINE (slot-encoded) vs MONAD_TEN -(page-encoded). +the runloop to compare MONAD_TEN vs MONAD_ELEVEN, both on page-encoded +(MIP-8) storage. ### Setup @@ -91,7 +91,7 @@ From the repo root: tmux new -s perf 'TAG=v4 scripts/perf_cycle.sh' ``` -Fills once, consumes `RUNS` times, and writes the NINE-vs-TEN table to +Fills once, consumes `RUNS` times, and writes the TEN-vs-ELEVEN table to `../timing_${TAG}__table.md` (headed with the cycle time and the four repo SHAs). Knobs: @@ -111,23 +111,20 @@ Run on a quiet host; timings are noisy under contention. - Fixtures containing expected-invalid blocks are skipped: the ledger machine validates blocks at proposal time and cannot write invalid ones. +- Fixtures with blocks before MONAD_TEN fail before the runloop starts, + transitions into MONAD_TEN included: monad executes only on a + page-encoded (MIP-8) triedb, and pre-MIP-8 blocks need the + slot-encoded one. - 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) - -MONAD_NINE storage is slot-encoded; MONAD_TEN (MIP-8) is -page-encoded. A transition fixture (e.g. -`MONAD_NINEToMONAD_TENAtTime15k`) 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. +## Execution flow - **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`. + `expectException` blocks, and provisions the db with `monad-mpt + --create --state-machine monad`. - **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 @@ -135,11 +132,8 @@ primary timeline and a page-encoded secondary timeline. 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_TEN. -- **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_TEN - block, matching the fixture's final `blockHeader.stateRoot`. + `SWITCH_MONAD_TRAITS`, e.g. MONAD_TEN then MONAD_ELEVEN in a + `MONAD_TENToMONAD_ELEVENAtTime15k` fixture. - **Assertions**: `state_root` and every `postState` account (balance, nonce, code, per-slot storage) match `output`, with no unexpected non-zero slots. @@ -156,15 +150,10 @@ two differences: the block source and the revision schedule. | 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 | +| Commit | `commit_block` | the same | +| State root / db | page-encoded `mpt::Db` + `TrieDb` | 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_TEN block. diff --git a/monad-runloop/README.md b/monad-runloop/README.md index f68085ebfd3..f641c5dd55c 100644 --- a/monad-runloop/README.md +++ b/monad-runloop/README.md @@ -83,8 +83,7 @@ for block N: eest-runner ─ runloop.run(1) ─▶ C++ engine ─ reads ledger dir ─ executes ─▶ triedb after the last block: - eest-runner ─ runloop.get_primary_state_root() or - get_secondary_state_root() (page canonical from MONAD_TEN) + eest-runner ─ runloop.get_state_root() (page-encoded) + dump_json() ─▶ normalize_dump() ─▶ output.json ─▶ (Python compares vs fixture) ``` @@ -102,7 +101,7 @@ monad-runloop/bin/eest-runner --version A plain `git clone` leaves all of this empty, which costs nothing for contributors who never run the harness. `git clone --recurse-submodules` -does populate it, but pulls the full 3.6 GB rather than the 300 MB +does populate it, but pulls the full 6.5 GB rather than the 300 MB `init-stack.sh` selects; re-running the script afterwards is harmless but will not shrink an already-fetched checkout. diff --git a/monad-runloop/bin/eest-runner b/monad-runloop/bin/eest-runner index 99c161f7277..295ba2cbb21 100755 --- a/monad-runloop/bin/eest-runner +++ b/monad-runloop/bin/eest-runner @@ -15,9 +15,11 @@ fi # --name (when the caller sets it): a stable handle so the consumer can # `docker kill` the container if its client is killed without forwarding # a stop, which would otherwise leave the runloop spinning past pytest. +# --ulimit core=0: an aborting runloop would otherwise leave a multi-GB +# core dump on the host for every failing fixture. exec docker run --rm --init \ ${EEST_RUNNER_CONTAINER_NAME:+--name "$EEST_RUNNER_CONTAINER_NAME"} \ - --privileged --ulimit nofile=16384:16384 --ulimit memlock=-1:-1 --user $(id -u):$(id -g) \ + --privileged --ulimit nofile=16384:16384 --ulimit memlock=-1:-1 --ulimit core=0 --user $(id -u):$(id -g) \ -v /tmp:/tmp \ -v "$ROOT":"$ROOT" \ -e LD_LIBRARY_PATH="$ROOT"/install/lib \ diff --git a/monad-runloop/init-stack.sh b/monad-runloop/init-stack.sh index da08c436953..346fe56356c 100755 --- a/monad-runloop/init-stack.sh +++ b/monad-runloop/init-stack.sh @@ -3,7 +3,7 @@ # monad-execution submodule, and the third_party submodules the CMake # build needs. # -# monad declares 23 third_party submodules, 3.3 GB, not all needed. +# monad declares 21 third_party submodules, 6.5 GB, not all needed. # # SKIP is a denylist on purpose. A submodule the build turns out to need # fails as a confusing CMake error deep into the build, while an extra @@ -42,7 +42,7 @@ EXEC="$ROOT/monad-bft/monad-execution" SKIP=( third_party/ethereum-tests - third_party/yellowpaper + third_party/monad-execution-specs third_party/zkevm-standards ) diff --git a/monad-runloop/monad-bft b/monad-runloop/monad-bft index c4e3899f240..7c4c28cb4fb 160000 --- a/monad-runloop/monad-bft +++ b/monad-runloop/monad-bft @@ -1 +1 @@ -Subproject commit c4e3899f240b1498ab3b1dedaa165b6708240428 +Subproject commit 7c4c28cb4fb74a0716943541c9019ae5e89617be diff --git a/monad-runloop/rust-harness/eest-runner/src/main.rs b/monad-runloop/rust-harness/eest-runner/src/main.rs index 045ecac48ca..3032572c58b 100644 --- a/monad-runloop/rust-harness/eest-runner/src/main.rs +++ b/monad-runloop/rust-harness/eest-runner/src/main.rs @@ -241,20 +241,7 @@ fn main() { runloop.run(1); } - // The canonical state root is the page-encoded secondary timeline's - // from MONAD_ELEVEN (MIP-8, revision 10) on, the slot-encoded - // primary's before. The schedule is ascending, so the last entry is - // the revision the run ends in. - let final_revision = input - .revision_schedule - .last() - .expect("non-empty revision schedule") - .revision; - let state_root = if final_revision >= 10 { - runloop.get_secondary_state_root() - } else { - runloop.get_primary_state_root() - }; + let state_root = runloop.get_state_root(); let post_state = normalize_dump(&runloop.dump_json()); let output = json!({ diff --git a/monad-runloop/rust-harness/runloop/src/runloop.rs b/monad-runloop/rust-harness/runloop/src/runloop.rs index 100d9656900..80595e54448 100644 --- a/monad-runloop/rust-harness/runloop/src/runloop.rs +++ b/monad-runloop/rust-harness/runloop/src/runloop.rs @@ -2,6 +2,5 @@ use alloy_primitives::U256; pub trait Runloop { fn run(&mut self, n_blocks: u64); - fn get_primary_state_root(&mut self) -> U256; - fn get_secondary_state_root(&mut self) -> U256; + fn get_state_root(&mut self) -> U256; } diff --git a/monad-runloop/rust-harness/runloop/src/runloop_interface_monad.rs b/monad-runloop/rust-harness/runloop/src/runloop_interface_monad.rs index 37436bfbb31..0efabacd3dd 100644 --- a/monad-runloop/rust-harness/runloop/src/runloop_interface_monad.rs +++ b/monad-runloop/rust-harness/runloop/src/runloop_interface_monad.rs @@ -38,14 +38,8 @@ extern "C" { // Execute and finalize `nblocks` number of blocks. fn monad_runloop_run(runloop: *mut RawMonadRunloop, nblocks: u64); - // Store current primary (slot-encoded) state root. - fn monad_runloop_get_primary_state_root( - runloop: *mut RawMonadRunloop, - result_state_root: *mut RawMonadRunloopWord, - ); - - // Store current secondary (page-encoded) state root. - fn monad_runloop_get_secondary_state_root( + // Store current (page-encoded) state root. + fn monad_runloop_get_state_root( runloop: *mut RawMonadRunloop, result_state_root: *mut RawMonadRunloopWord, ); @@ -130,22 +124,12 @@ impl Runloop for MonadRunloop { } } - fn get_primary_state_root(&mut self) -> U256 { - let mut state_root = RawMonadRunloopWord{ - bytes: [0; 32], - }; - unsafe { - monad_runloop_get_primary_state_root(self.raw, &mut state_root) - }; - U256::from_be_bytes(state_root.bytes) - } - - fn get_secondary_state_root(&mut self) -> U256 { + fn get_state_root(&mut self) -> U256 { let mut state_root = RawMonadRunloopWord{ bytes: [0; 32], }; unsafe { - monad_runloop_get_secondary_state_root(self.raw, &mut state_root) + monad_runloop_get_state_root(self.raw, &mut state_root) }; U256::from_be_bytes(state_root.bytes) } diff --git a/packages/testing/src/execution_testing/cli/perf_regression.py b/packages/testing/src/execution_testing/cli/perf_regression.py index d420bd75bfa..2ddf8c573c0 100644 --- a/packages/testing/src/execution_testing/cli/perf_regression.py +++ b/packages/testing/src/execution_testing/cli/perf_regression.py @@ -1,11 +1,11 @@ """ -Build a NINE-vs-TEN significance table from perf timing runs. +Build a TEN-vs-ELEVEN significance table from perf timing runs. Reads the `timing_consume.csv` produced by `consume direct --timing-report` for several identical runs (one dir each) and, per (test, params) case, reports each measure's mean +/- sd over the runs for both forks, the two-sided Mann-Whitney U p-value comparing the forks, -and (for significant measures) the NINE->TEN average change. +and (for significant measures) the TEN->ELEVEN average change. Writes GitHub-flavored Markdown to `--md`, or stdout if omitted; when `--now` is given, prefixes a provenance header. @@ -29,7 +29,7 @@ # A case, identified by (test, params). CaseKey = Tuple[str, str] -# One metric's paired samples: [(NINE, TEN) per run measuring both forks]. +# One metric's paired samples: [(TEN, ELEVEN) per run measuring both forks]. PairedSamples = List[Tuple[int, int]] METRICS = ["tx_exec_us", "commit_us", "total_us"] @@ -42,7 +42,7 @@ # wall time is what a retry count belongs to: retries are a property of # executing the whole block, not of one of its phases. REFERENCE_METRIC = "total_us" -FORKS = ["MONAD_NINE", "MONAD_TEN"] +FORKS = ["MONAD_TEN", "MONAD_ELEVEN"] # A measure is significant at this Benjamini-Hochberg adjusted p-value. # The table runs one test per (case, measure), so the unadjusted rate # would produce a steady trickle of false positives across a cycle. The @@ -52,8 +52,8 @@ # signed-rank test can reach scales with 2^RUNS. ALPHA = 0.05 -UP = "🔴⬆️" # significant measures all rise NINE->TEN (TEN slower) -DOWN = "🟢⬇️" # significant measures all fall NINE->TEN (TEN faster) +UP = "🔴⬆️" # significant measures all rise TEN->ELEVEN (ELEVEN slower) +DOWN = "🟢⬇️" # significant measures all fall TEN->ELEVEN (ELEVEN faster) MIXED = "⚠️" # significant measures move both up and down # Per-case workload descriptions live here; pinned to the execution-specs @@ -151,7 +151,7 @@ def _wilcoxon_p(diffs: List[int]) -> float: Two-sided Wilcoxon signed-rank p-value for paired differences. Both forks are measured inside the same consume pass, so run `i` - yields a matched (NINE, TEN) pair. Testing the differences cancels + yields a matched (TEN, ELEVEN) pair. Testing the differences cancels whatever drifted between runs — host thermals, page cache, other load — which an unpaired test would instead charge to both samples' variance. @@ -243,13 +243,13 @@ def _paired_samples( } missing = 0 for run in runs: - nine = run.get((*case, FORKS[0])) - ten = run.get((*case, FORKS[1])) - if nine is None or ten is None: + ten = run.get((*case, FORKS[0])) + eleven = run.get((*case, FORKS[1])) + if ten is None or eleven is None: missing += 1 continue for m in (*METRICS, *COUNTS): - per_metric[m].append((nine[m], ten[m])) + per_metric[m].append((ten[m], eleven[m])) samples[case] = per_metric if missing: dropped[case] = missing @@ -274,16 +274,16 @@ def build(runs: List[Run]) -> List[str]: header = ["test-params"] for m in METRICS: - header += [f"{m} NINE", f"{m} TEN", f"{m} q"] + header += [f"{m} TEN", f"{m} ELEVEN", f"{m} q"] for c in COUNTS: - header += [f"{c} NINE", f"{c} TEN"] - header += ["significant", "Δ avg NINE→TEN (sig)"] + header += [f"{c} TEN", f"{c} ELEVEN"] + header += ["significant", "Δ avg TEN→ELEVEN (sig)"] lines = [ f"Mean ± sd over up to {len(runs)} runs; durations in µs, " f"{'/'.join(COUNTS)} a count. Bold = measure significant (paired " f"Wilcoxon signed-rank, Benjamini–Hochberg adjusted q ≤ {ALPHA}). " - f"Significant flag: {UP} TEN slower, {DOWN} TEN faster, " + f"Significant flag: {UP} ELEVEN slower, {DOWN} ELEVEN faster, " f"{MIXED} mixed.", "", "| " + " | ".join(header) + " |", @@ -301,8 +301,8 @@ def build(runs: List[Run]) -> List[str]: chgs = [] for m in METRICS: pairs = samples[case][m] - n = [nine for nine, _ in pairs] - x = [ten for _, ten in pairs] + n = [ten for ten, _ in pairs] + x = [eleven for _, eleven in pairs] q = adjusted[(case, m)] ncell, xcell = _stat(n), _stat(x) if q <= ALPHA: @@ -315,8 +315,8 @@ def build(runs: List[Run]) -> List[str]: for c in COUNTS: pairs = samples[case][c] cells += [ - _stat([nine for nine, _ in pairs]), - _stat([ten for _, ten in pairs]), + _stat([ten for ten, _ in pairs]), + _stat([eleven for _, eleven in pairs]), ] cells.append(_direction(chgs) if chgs else "-") cells.append(", ".join(deltas) if deltas else "-") @@ -325,7 +325,7 @@ def build(runs: List[Run]) -> List[str]: lines += [ "", "q is the Benjamini–Hochberg adjusted p-value of a two-sided " - "paired Wilcoxon signed-rank test on the per-run NINE→TEN " + "paired Wilcoxon signed-rank test on the per-run TEN→ELEVEN " f"differences for that measure; significant at q ≤ {ALPHA}. " f"Smallest raw p in this table: {_pfmt(min(ps)) if ps else 'n/a'}. " f"{'/'.join(COUNTS)} is reported, not tested: it tells a block " @@ -490,7 +490,7 @@ def main( monad: Optional[str], ) -> None: """ - Build a NINE-vs-TEN significance table from perf timing runs. + Build a TEN-vs-ELEVEN significance table from perf timing runs. Each RUN_DIRS argument is one `consume direct --timing-report` output directory holding a `timing_consume.csv`. diff --git a/packages/testing/src/execution_testing/cli/tests/test_perf_regression.py b/packages/testing/src/execution_testing/cli/tests/test_perf_regression.py index 3da662b47e2..685da3d8b99 100644 --- a/packages/testing/src/execution_testing/cli/tests/test_perf_regression.py +++ b/packages/testing/src/execution_testing/cli/tests/test_perf_regression.py @@ -1,4 +1,4 @@ -"""Tests for the `perf_regression` NINE-vs-TEN significance table.""" +"""Tests for the `perf_regression` TEN-vs-ELEVEN significance table.""" from pathlib import Path from typing import Dict, List, Sequence @@ -53,12 +53,12 @@ def _row(fork: str, total: int, block: int = 1) -> Dict[str, object]: } -def _run_dir(tmp_path: Path, name: str, nine: int, ten: int) -> Path: +def _run_dir(tmp_path: Path, name: str, ten: int, eleven: int) -> Path: """Write a one-case run directory with the given per-fork totals.""" path = tmp_path / name path.mkdir() (path / "timing_consume.csv").write_text( - _csv([_row("MONAD_NINE", nine), _row("MONAD_TEN", ten)]) + _csv([_row("MONAD_TEN", ten), _row("MONAD_ELEVEN", eleven)]) ) return path @@ -69,33 +69,36 @@ def test_parse_reduces_blocks_to_minimum(tmp_path: Path) -> None: path.write_text( _csv( [ - _row("MONAD_NINE", 300, block=1), - _row("MONAD_NINE", 100, block=2), - _row("MONAD_NINE", 200, block=3), + _row("MONAD_TEN", 300, block=1), + _row("MONAD_TEN", 100, block=2), + _row("MONAD_TEN", 200, block=3), ] ) ) parsed = parse(path) assert ( - parsed[("page_ops", "sstore_fresh-k0", "MONAD_NINE")]["total_us"] - == 100 + parsed[("page_ops", "sstore_fresh-k0", "MONAD_TEN")]["total_us"] == 100 ) def test_parse_ignores_other_forks(tmp_path: Path) -> None: """Rows for forks outside the comparison are dropped.""" path = tmp_path / "timing_consume.csv" - path.write_text(_csv([_row("Prague", 100), _row("MONAD_TEN", 200)])) - assert list(parse(path)) == [("page_ops", "sstore_fresh-k0", "MONAD_TEN")] + path.write_text(_csv([_row("Prague", 100), _row("MONAD_ELEVEN", 200)])) + assert list(parse(path)) == [ + ("page_ops", "sstore_fresh-k0", "MONAD_ELEVEN") + ] def test_parse_skips_unparsable_metrics(tmp_path: Path) -> None: """A row with a non-numeric metric is skipped, not fatal.""" - bad = _row("MONAD_NINE", 100) + bad = _row("MONAD_TEN", 100) bad["total_us"] = "oops" path = tmp_path / "timing_consume.csv" - path.write_text(_csv([bad, _row("MONAD_TEN", 200)])) - assert list(parse(path)) == [("page_ops", "sstore_fresh-k0", "MONAD_TEN")] + path.write_text(_csv([bad, _row("MONAD_ELEVEN", 200)])) + assert list(parse(path)) == [ + ("page_ops", "sstore_fresh-k0", "MONAD_ELEVEN") + ] def test_avg_ranks_averages_ties() -> None: @@ -127,11 +130,11 @@ def test_wilcoxon_p_mixed_signs_is_not_significant() -> None: def test_wilcoxon_p_is_paired_not_pooled() -> None: """A consistent shift is detected even when the levels overlap.""" - # Both forks drift upward across runs, but TEN is always the slower + # Both forks drift upward across runs, but ELEVEN is always the slower # of the pair; an unpaired test would drown this in the drift. - nine = [100, 200, 300, 400, 500, 600, 700, 800] - ten = [110, 210, 310, 410, 510, 610, 710, 810] - diffs = [t - n for n, t in zip(nine, ten, strict=True)] + ten = [100, 200, 300, 400, 500, 600, 700, 800] + eleven = [110, 210, 310, 410, 510, 610, 710, 810] + diffs = [t - n for n, t in zip(ten, eleven, strict=True)] assert _wilcoxon_p(diffs) == pytest.approx(2 / 2**8) @@ -236,7 +239,7 @@ def _case_row(table: str) -> str: def test_build_flags_significant_slowdown(tmp_path: Path) -> None: """A clean slowdown is flagged, bolded and quantified.""" dirs = [ - _run_dir(tmp_path, f"r{i}", nine=100 + i, ten=200 + i) + _run_dir(tmp_path, f"r{i}", ten=100 + i, eleven=200 + i) for i in range(9) ] row = _case_row(report(tuple(dirs))) @@ -248,7 +251,7 @@ def test_build_flags_significant_slowdown(tmp_path: Path) -> None: def test_build_flags_significant_speedup(tmp_path: Path) -> None: """A clean speedup is flagged in the other direction.""" dirs = [ - _run_dir(tmp_path, f"r{i}", nine=200 + i, ten=100 + i) + _run_dir(tmp_path, f"r{i}", ten=200 + i, eleven=100 + i) for i in range(9) ] row = _case_row(report(tuple(dirs))) @@ -270,7 +273,7 @@ def test_build_marks_noise_insignificant(tmp_path: Path) -> None: (96, 105), ] dirs = [ - _run_dir(tmp_path, f"r{i}", nine=n, ten=t) + _run_dir(tmp_path, f"r{i}", ten=n, eleven=t) for i, (n, t) in enumerate(totals) ] row = _case_row(report(tuple(dirs))) @@ -292,7 +295,7 @@ def _one_fork_dir(tmp_path: Path, name: str, fork: str, total: int) -> Path: def test_report_names_cases_never_compared(tmp_path: Path) -> None: """A case no run measured at both forks is named, not dropped.""" dirs = [ - _one_fork_dir(tmp_path, f"r{i}", "MONAD_NINE", 100 + i) + _one_fork_dir(tmp_path, f"r{i}", "MONAD_TEN", 100 + i) for i in range(3) ] table = report(tuple(dirs)) @@ -303,10 +306,10 @@ def test_report_names_cases_never_compared(tmp_path: Path) -> None: def test_report_names_partially_compared_cases(tmp_path: Path) -> None: """A case missing from some runs reports its reduced run count.""" dirs = [ - _run_dir(tmp_path, f"r{i}", nine=100 + i, ten=200 + i) + _run_dir(tmp_path, f"r{i}", ten=100 + i, eleven=200 + i) for i in range(3) ] - dirs.append(_one_fork_dir(tmp_path, "partial", "MONAD_NINE", 100)) + dirs.append(_one_fork_dir(tmp_path, "partial", "MONAD_TEN", 100)) table = report(tuple(dirs)) assert "compared on fewer than 4 runs" in table assert "3 of 4 runs" in table @@ -314,14 +317,14 @@ def test_report_names_partially_compared_cases(tmp_path: Path) -> None: def test_report_counts_hypothesis_tests(tmp_path: Path) -> None: """The report states how many tests the adjustment covers.""" - dirs = [_run_dir(tmp_path, f"r{i}", nine=100, ten=200) for i in range(3)] + dirs = [_run_dir(tmp_path, f"r{i}", ten=100, eleven=200) for i in range(3)] assert "3 hypothesis tests in this table" in report(tuple(dirs)) def test_report_warns_when_underpowered(tmp_path: Path) -> None: """Too few pairs to clear the floor is called out, not hidden.""" dirs = [ - _run_dir(tmp_path, f"r{i}", nine=100 + i, ten=200 + i) + _run_dir(tmp_path, f"r{i}", ten=100 + i, eleven=200 + i) for i in range(4) ] table = report(tuple(dirs)) @@ -333,18 +336,18 @@ def test_report_no_power_warning_when_resolvable(tmp_path: Path) -> None: """Enough pairs to clear the floor leaves the warning out.""" # 3 measures at 2/2**10 clears q <= 0.05; 6 runs would not. dirs = [ - _run_dir(tmp_path, f"r{i}", nine=100 + i, ten=200 + i) + _run_dir(tmp_path, f"r{i}", ten=100 + i, eleven=200 + i) for i in range(10) ] assert "cannot reach q" not in report(tuple(dirs)) -def _retry_dir(tmp_path: Path, name: str, nine: int, ten: int) -> Path: +def _retry_dir(tmp_path: Path, name: str, ten: int, eleven: int) -> Path: """Write a run directory whose blocks report differing retry counts.""" path = tmp_path / name path.mkdir() rows = [] - for fork, retries in (("MONAD_NINE", nine), ("MONAD_TEN", ten)): + for fork, retries in (("MONAD_TEN", ten), ("MONAD_ELEVEN", eleven)): for block, r in enumerate((0, retries), start=1): row = _row(fork, 100, block=block) row["retries"] = r @@ -355,10 +358,10 @@ def _retry_dir(tmp_path: Path, name: str, nine: int, ten: int) -> Path: def test_report_shows_retries_of_the_reported_block(tmp_path: Path) -> None: """Retries come from the block whose timing the row reports.""" - dirs = [_retry_dir(tmp_path, f"r{i}", nine=0, ten=4) for i in range(3)] + dirs = [_retry_dir(tmp_path, f"r{i}", ten=0, eleven=4) for i in range(3)] table = report(tuple(dirs)) - assert "retries NINE" in table assert "retries TEN" in table + assert "retries ELEVEN" in table cells = [c.strip() for c in _case_row(table).split("|")] # Both blocks tie on total_us, so the earlier one (retries 0) stands. assert cells.count("0 ± 0") >= 2 @@ -372,13 +375,13 @@ def test_parse_takes_the_counter_from_the_fastest_block( for block, (total, retries) in enumerate( [(300, 9), (100, 2), (200, 5)], start=1 ): - row = _row("MONAD_NINE", total, block=block) + row = _row("MONAD_TEN", total, block=block) row["retries"] = retries rows.append(row) path = tmp_path / "timing_consume.csv" path.write_text(_csv(rows)) - parsed = parse(path)[("page_ops", "sstore_fresh-k0", "MONAD_NINE")] + parsed = parse(path)[("page_ops", "sstore_fresh-k0", "MONAD_TEN")] assert parsed["total_us"] == 100 assert parsed["retries"] == 2 # not 9 (max) and not 5 @@ -390,13 +393,13 @@ def test_parse_counter_ignores_a_slower_block_with_more_retries( """A later, slower block does not drag its counter into the row.""" rows = [] for block, (total, retries) in enumerate([(100, 1), (500, 99)], start=1): - row = _row("MONAD_NINE", total, block=block) + row = _row("MONAD_TEN", total, block=block) row["retries"] = retries rows.append(row) path = tmp_path / "timing_consume.csv" path.write_text(_csv(rows)) - parsed = parse(path)[("page_ops", "sstore_fresh-k0", "MONAD_NINE")] + parsed = parse(path)[("page_ops", "sstore_fresh-k0", "MONAD_TEN")] assert parsed["total_us"] == 100 assert parsed["retries"] == 1 @@ -405,7 +408,7 @@ def test_parse_counter_ignores_a_slower_block_with_more_retries( def test_parse_tolerates_csv_without_retries(tmp_path: Path) -> None: """A csv written before the counter existed still parses.""" legacy = HEADER.replace(",retries", "") - row = _row("MONAD_NINE", 100) + row = _row("MONAD_TEN", 100) path = tmp_path / "timing_consume.csv" path.write_text( legacy @@ -414,22 +417,20 @@ def test_parse_tolerates_csv_without_retries(tmp_path: Path) -> None: + "\n" ) parsed = parse(path) - assert ( - parsed[("page_ops", "sstore_fresh-k0", "MONAD_NINE")]["retries"] == 0 - ) + assert parsed[("page_ops", "sstore_fresh-k0", "MONAD_TEN")]["retries"] == 0 def test_report_needs_two_runs(tmp_path: Path) -> None: """A single run cannot support a comparison.""" with pytest.raises(ClickException, match="need >=2 runs"): - report((_run_dir(tmp_path, "only", nine=100, ten=200),)) + report((_run_dir(tmp_path, "only", ten=100, eleven=200),)) def test_report_warns_about_dirs_without_csv( tmp_path: Path, capsys: pytest.CaptureFixture[str] ) -> None: """A run dir without a CSV is named rather than silently dropped.""" - dirs = [_run_dir(tmp_path, f"r{i}", nine=100, ten=200) for i in range(2)] + dirs = [_run_dir(tmp_path, f"r{i}", ten=100, eleven=200) for i in range(2)] empty = tmp_path / "empty" empty.mkdir() @@ -441,7 +442,7 @@ def test_report_warns_about_dirs_without_csv( def test_cli_writes_markdown(tmp_path: Path) -> None: """The CLI writes the report and the provenance header.""" dirs = [ - _run_dir(tmp_path, f"r{i}", nine=100 + i, ten=200 + i) + _run_dir(tmp_path, f"r{i}", ten=100 + i, eleven=200 + i) for i in range(9) ] md_path = tmp_path / "table.md" 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 4c7f95b9b76..3455b7c928b 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/monad.py +++ b/packages/testing/src/execution_testing/client_clis/clis/monad.py @@ -321,43 +321,21 @@ def last_block_timings(self) -> Sequence[BlockExecutionTiming]: """ return self._block_timings - def _init_triedb( - self, db_path: Path, schedule: List[Tuple[int, int]] - ) -> None: + def _init_triedb(self, db_path: Path) -> None: """ - Create and format a fresh triedb file via `monad-mpt`. - - The storage encoding depends on the fixture's revision schedule. - MIP-8 (MONAD_TEN, revision >= 10) uses a page-encoded triedb; older - revisions use slot encoding. A fixture that crosses the boundary - (e.g. a MONAD_NINE->MONAD_TEN transition) needs both: a slot-encoded - primary plus an activated page-encoded secondary timeline, so the - runloop can dual-write across the fork. - - Only the secondary's kind is decided here. `Db::Db` re-stamps the - primary to whatever state machine the runloop opens it with, so the - `--state-machine` passed for the primary below does not survive - into the run. + Create and format a fresh page-encoded triedb file via `monad-mpt`. + + The runloop requires a page-encoded db and reads the encoding from + the state machine kind `--create` stamps (`monad` is the + page-encoded one). """ 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) # 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. - # This only sets the kind `--create` stamps; the runloop re-stamps - # the primary when it opens the db, so the value is inert. Kept - # matched to the schedule so the two agree on disk; it can collapse - # to one constant once a TEN-only run confirms nothing reads the - # db in between. - initial_primary = ( - "monad" if uses_page and not uses_slot else "ethereum" - ) + # (the production defaults dominate runtime). subprocess.run( [ str(monad_mpt), @@ -369,29 +347,13 @@ def _init_triedb( "--root-offsets-chunk-count", "2", "--state-machine", - initial_primary, + "monad", ], 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, @@ -479,6 +441,14 @@ def _consume_one( ) if any("expectException" in block for block in fixture["blocks"]): pytest.skip("invalid blocks cannot be proposed through the ledger") + # The runloop only executes on a page-encoded db, which MIP-8 + # (MONAD_TEN, revision 10) introduced; earlier revisions need the + # slot-encoded one monad no longer supports. + if FORK_REVISION_SCHEDULES[network][0][0] < 10: + raise Exception( + f"network {network} has blocks before MONAD_TEN, which the " + "runloop cannot execute" + ) input_doc = { "genesis_alloc": _digest_genesis_alloc(fixture["pre"]), "genesis_rlp": fixture["genesisRLP"], @@ -498,7 +468,7 @@ def _consume_one( ledger_dir = tmp_path / "ledger" ledger_dir.mkdir() db_path = tmp_path / "triedb" - self._init_triedb(db_path, FORK_REVISION_SCHEDULES[network]) + self._init_triedb(db_path) input_path.write_text(json.dumps(input_doc, indent=2)) diff --git a/scripts/perf_cycle.sh b/scripts/perf_cycle.sh index b7dc11f22b7..08ee5253ac9 100755 --- a/scripts/perf_cycle.sh +++ b/scripts/perf_cycle.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Fill once, consume RUNS times, emit the NINE-vs-TEN perf table. +# Fill once, consume RUNS times, emit the TEN-vs-ELEVEN perf table. set -euo pipefail TAG="${TAG:?set TAG (names all artifacts, e.g. TAG=v4)}" @@ -31,7 +31,7 @@ if [ -z "${SKIP_FILL:-}" ]; then echo "=== fill $FIX (REPEATS=$REPEATS, ${BLOCK_GAS_M}M blocks) $NOW ===" MIP8_PERF_REPEATS="$REPEATS" \ uv run fill -m blockchain_test "$TEST" \ - --from MONAD_NINE --until MONAD_TEN --chain-id 30143 --monad-runloop \ + --from MONAD_TEN --until MONAD_ELEVEN --chain-id 30143 --monad-runloop \ --gas-benchmark-values "$BLOCK_GAS_M" \ --output "$FIX" -n auto || { echo "fill failed (a non-empty $FIX aborts fill); rerun with" \ diff --git a/tests/benchmark/stateful/mip8_pageified_storage/test_perf_regression.py b/tests/benchmark/stateful/mip8_pageified_storage/test_perf_regression.py index 10ddf17d68b..325fb12e84f 100644 --- a/tests/benchmark/stateful/mip8_pageified_storage/test_perf_regression.py +++ b/tests/benchmark/stateful/mip8_pageified_storage/test_perf_regression.py @@ -1,14 +1,14 @@ """ -MIP-8 performance-regression tests. +Page-encoded storage performance-regression tests. These blockchain tests compare block/transaction execution time on the -monad runloop before MIP-8 (MONAD_NINE, slot-encoded storage) and after -MIP-8 (MONAD_TEN, page-encoded storage). The same Python builds the -workload for both forks; the `fork` fixture is only consulted where the -two forks genuinely differ (storage-op gas, used to size a workload to a -gas budget). We do not assert gas — the oracle is post-state (success -markers, written slots, and read-checksum slots), so the identical -workload can be timed on both forks via the consume timing report. +monad runloop between MONAD_TEN and MONAD_ELEVEN, both on page-encoded +(MIP-8) storage. The same Python builds the workload for both forks; the +`fork` fixture is only consulted where the two forks could differ +(storage-op gas, used to size a workload to a gas budget). We do not +assert gas — the oracle is post-state (success markers, written slots, +and read-checksum slots), so the identical workload can be timed on both +forks via the consume timing report. Each test fills a monad block with one SLOAD/SSTORE pattern, sized to the `gas_benchmark_value` the framework injects from @@ -24,14 +24,14 @@ chain that would serialise it regardless of storage access. The block-shape sweep parametrizes this to measure what the chain alone costs. -Workloads are sized to MONAD_NINE (its cold storage costs >= MONAD_TEN), -so the same iteration count never out-of-gases on either fork; a -post-fork block may therefore be gas-underfull while doing identical I/O -work, which is exactly the effect being measured. +Workloads are sized to the costlier of the two forks, so the same +iteration count never out-of-gases on either fork; a block on the +cheaper fork may therefore be gas-underfull while doing identical I/O +work. That is also why these tests pass `skip_gas_used_validation`: the gas a -block actually uses differs between the forks by construction, so no -single `expected_benchmark_gas_used` can match both. The framework still +block actually uses may differ between the forks, so no single +`expected_benchmark_gas_used` has to match both. The framework still enforces that a block stays within its budget, `_assert_tx_within_budget` checks the plan against it, and `_per_iter_gas` bounds its own estimate, so a block cannot quietly end up sized for less work than the budget. @@ -70,7 +70,7 @@ While, WhileGas, ) -from execution_testing.forks import MONAD_NINE, MONAD_TEN +from execution_testing.forks import MONAD_ELEVEN, MONAD_TEN from execution_testing.forks.helpers import Fork from tests.monad_ten.mip8_pageified_storage.helpers import fresh_sstore_cold @@ -213,7 +213,7 @@ def _senders(pre: Alloc, distinct: bool = True) -> Iterator[EOA]: yield shared -@pytest.mark.valid_from("MONAD_NINE") +@pytest.mark.valid_from("MONAD_TEN") def test_compute_loop( benchmark_test: BenchmarkTestFiller, pre: Alloc, @@ -229,10 +229,9 @@ def test_compute_loop( This is the suite's control, so the contract is built from a fixed fork rather than the one under test: sizing the loop against the - running fork would emit different bytecode either side of MIP-8, and - a control has to run the same code. MONAD_NINE prices the trailing - cold SSTORE at least as high as MONAD_TEN, so its reserve is safe on - both. + running fork could emit different bytecode on each fork, and a + control has to run the same code. MONAD_ELEVEN prices the trailing + cold SSTORE the same as MONAD_TEN, so its reserve is safe on both. """ senders = _senders(pre) budget = gas_benchmark_value // FULL_BLOCK_TXS @@ -241,8 +240,8 @@ def test_compute_loop( contract_address = pre.deploy_contract( WhileGas( body=body, - fork=MONAD_NINE, - extra_gas=fresh_sstore_cold(MONAD_NINE), + fork=MONAD_TEN, + extra_gas=fresh_sstore_cold(MONAD_TEN), ) + Op.SSTORE(slot_code_worked, value_code_worked) ) @@ -307,7 +306,7 @@ def _sstore( `value` is what is written at run time; `original`/`current`/`new`/ `growth` describe the slot transition for gas sizing only. `original` (the slot's value at tx start) is required so a nonzero-to-nonzero or - nonzero-to-zero reset is priced as a reset on MONAD_NINE, not a no-op. + nonzero-to-zero reset is priced as a reset, not a no-op. """ return Op.SSTORE( slot, @@ -464,9 +463,9 @@ def _while_control_gas(fork: Fork) -> int: def _per_iter_gas(op: StorageOp, k: int) -> int: """Gas for one loop iteration, sized to the costlier of both forks.""" body = _body(op, k) - per_op = max(body.gas_cost(MONAD_NINE), body.gas_cost(MONAD_TEN)) + per_op = max(body.gas_cost(MONAD_TEN), body.gas_cost(MONAD_ELEVEN)) control = max( - _while_control_gas(MONAD_NINE), _while_control_gas(MONAD_TEN) + _while_control_gas(MONAD_TEN), _while_control_gas(MONAD_ELEVEN) ) assert control <= WHILE_CONTROL_GAS <= control + MAX_CONTROL_SLACK, ( f"WHILE_CONTROL_GAS is {WHILE_CONTROL_GAS} but a While control step " @@ -547,7 +546,7 @@ def _repeat_domains(repeat: int) -> Tuple[int, int, int]: @pytest.mark.parametrize("op, k", _PAGE_OP_PARAMS) -@pytest.mark.valid_from("MONAD_NINE") +@pytest.mark.valid_from("MONAD_TEN") def test_page_ops( benchmark_test: BenchmarkTestFiller, pre: Alloc, @@ -693,7 +692,7 @@ def test_page_ops( @pytest.mark.parametrize("m, n", _SPREAD_PARAMS) -@pytest.mark.valid_from("MONAD_NINE") +@pytest.mark.valid_from("MONAD_TEN") def test_page_spread( benchmark_test: BenchmarkTestFiller, pre: Alloc, @@ -793,7 +792,7 @@ def test_page_spread( @pytest.mark.parametrize("op, k, shape, distinct_senders", _SHAPE_PARAMS) -@pytest.mark.valid_from("MONAD_NINE") +@pytest.mark.valid_from("MONAD_TEN") def test_block_shape( benchmark_test: BenchmarkTestFiller, pre: Alloc, @@ -876,7 +875,7 @@ def test_block_shape( @pytest.mark.parametrize("mode", ["success", "halt", "mix"]) -@pytest.mark.valid_from("MONAD_NINE") +@pytest.mark.valid_from("MONAD_TEN") def test_tx_halt( benchmark_test: BenchmarkTestFiller, pre: Alloc, @@ -956,7 +955,7 @@ def test_tx_halt( def _size_count(body: Bytecode, budget: int) -> int: """Loop iterations of `body` that fit `budget`, sized to both forks.""" per_iter = ( - max(body.gas_cost(MONAD_NINE), body.gas_cost(MONAD_TEN)) + max(body.gas_cost(MONAD_TEN), body.gas_cost(MONAD_ELEVEN)) + WHILE_CONTROL_GAS ) count = max(1, (budget - TX_RESERVE) // per_iter) @@ -1008,7 +1007,7 @@ def _rand_sload_contract(slots: int) -> Bytecode: @pytest.mark.parametrize("slots, k", _RAND_PARAMS) -@pytest.mark.valid_from("MONAD_NINE") +@pytest.mark.valid_from("MONAD_TEN") def test_random_sload( benchmark_test: BenchmarkTestFiller, pre: Alloc, @@ -1083,7 +1082,7 @@ def test_random_sload( ) -@pytest.mark.valid_from("MONAD_NINE") +@pytest.mark.valid_from("MONAD_TEN") def test_bad_block_serial( benchmark_test: BenchmarkTestFiller, pre: Alloc, @@ -1155,7 +1154,7 @@ def test_bad_block_serial( ) -@pytest.mark.valid_from("MONAD_NINE") +@pytest.mark.valid_from("MONAD_TEN") def test_bad_block_chained( benchmark_test: BenchmarkTestFiller, pre: Alloc,