Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5e477c5
Add MonadFixtureConsumer for consuming fixtures on the monad runloop
pdobacz Jun 12, 2026
9b73972
docs: use uv for monad runloop testing setup
pdobacz Jun 18, 2026
b9faa85
perf: shrink triedb chunk sizing for monad fixture consumer
pdobacz Jun 18, 2026
6d969a3
fix: tear down eest-runner container when pytest exits
pdobacz Jun 18, 2026
db3a35f
docs: note runloop parallelism is CPU-bound
pdobacz Jun 18, 2026
cf47e1f
feat: --monad-runloop fill flag for runloop-conformant block hashes
pdobacz Jun 18, 2026
d156e0f
Minor fixes to MONAD_RUNLOOP_TESTING.md
pdobacz Jun 18, 2026
4816466
consumer: support MONAD_NEXT (rev 10) + page-encoded triedb
pdobacz Jun 18, 2026
1f56129
consumer: schedule-aware triedb encoding for MIP-8 transition
pdobacz Jun 19, 2026
e01e1ef
docs: MIP-8 dual-db fork-transition flow in MONAD_RUNLOOP_TESTING
pdobacz Jun 23, 2026
476c732
fill: make --monad-runloop transition fills pass
pdobacz Jun 23, 2026
e9f7baa
ci: add monad_runloop fixture release feature
pdobacz Jun 23, 2026
de3afcb
forks: drop unused PragueToMONAD_EIGHT transition fork
pdobacz Jun 23, 2026
67a8275
ci: add consume_release workflow
pdobacz Jun 24, 2026
8c1a23f
consume: resolve releases from monad-developers/execution-specs
pdobacz Jun 24, 2026
01e2ca5
ci(consume): paste full consume.log in summary, fix fields, drop html
pdobacz Jun 29, 2026
d415ab4
ci(consume): run consume direct with -v
pdobacz Jun 29, 2026
386f946
docs: condense MONAD_RUNLOOP_TESTING to running + overview
pdobacz Jul 15, 2026
c1e541d
ci(consume): route workflow inputs through env vars
pdobacz Jul 15, 2026
bca4994
docs: use ssh clone url for the private harness repo
pdobacz Jul 16, 2026
797da40
fix(consume): resolve release asset for tests- prefixed tags
pdobacz Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/configs/feature.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
186 changes: 186 additions & 0 deletions .github/workflows/consume_release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# 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"
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
Comment thread
pdobacz marked this conversation as resolved.
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
id: stack
working-directory: monad-eest-rust-harness
shell: bash
run: |
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
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
shell: bash
env:
RELEASE: ${{ inputs.release }}
FILTER: ${{ inputs.filter }}
run: |
set -o pipefail
FILTER_ARG=()
if [ -n "$FILTER" ]; then
FILTER_ARG=(-k "$FILTER")
fi
uv run consume direct \
--input "$RELEASE" \
--bin "$GITHUB_WORKSPACE/monad-eest-rust-harness/bin/eest-runner" \
"${FILTER_ARG[@]}" -v | tee consume.log
Comment thread
pdobacz marked this conversation as resolved.

- name: Summary
if: always()
shell: bash
env:
RELEASE: ${{ inputs.release }}
FILTER: ${{ inputs.filter || '(none)' }}
HARNESS_REF: ${{ inputs.harness_ref }}
BFT_SHA: ${{ steps.stack.outputs.bft }}
EXEC_SHA: ${{ steps.stack.outputs.exec }}
run: |
{
echo "## consume \`${RELEASE:-(unknown)}\`"
echo ""
echo "| input | value |"
echo "|---|---|"
echo "| harness_ref | \`${HARNESS_REF}\` |"
echo "| monad-bft | \`${BFT_SHA:-(unknown)}\` |"
echo "| monad-execution | \`${EXEC_SHA:-(unknown)}\` |"
echo "| filter | \`${FILTER:-(none)}\` |"
echo ""
echo '```'
cat consume.log 2>/dev/null || echo "(no consume.log produced)"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

- name: Upload log
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: consume-report
path: consume.log
if-no-files-found: warn
132 changes: 132 additions & 0 deletions MONAD_RUNLOOP_TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Running EEST fixtures on the monad runloop

Executes blockchain fixtures (including those generated from state
tests) against the monad execution client's `runloop`, using the
consensus ledger directory as the block source. Each `postState`
account is compared (balance/nonce/code/storage) against the
executed result.

## Repos

| 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-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

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@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
```

In this repo:

```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
uv run fill --clean -m blockchain_test <test paths...> \
--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).
- `--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).

## 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 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_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

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 |
|---|---|---|
| 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<traits>` | the same |
| Commit | `commit_block<traits>` 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.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
)

SUPPORTED_REPOS = [
"monad-developers/execution-specs",
"ethereum/execution-spec-tests",
"ethereum/execution-specs",
"ethereum/tests",
Expand Down Expand Up @@ -91,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-<feature>@<version>`, but the asset
# is `fixtures_<feature>.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):
Expand Down
Loading