docs(m2): testnet runbook + engine.m2.toml + just run-m2 (validated boot) - #31
Closed
brunota20 wants to merge 1 commit into
Closed
Conversation
… boot)
Wires up the M2 milestone for actual testnet exercise on Sepolia.
Closes the gap "M2 is fully tested in unit + integration but has
never been run against a real chain".
## New files
- `engine.m2.toml` - workspace-root engine config that boots
`twap-monitor` + `ethflow-watcher` against Sepolia public WS.
Separate `state_dir = "./data/m2"` so it never collides with
the M1 example runbook.
- `docs/operations/m2-testnet-runbook.md` - 200-line runbook with
6 sections:
0. Prerequisites (rustup target, just, Sepolia RPC, faucet)
1. Smoke run (passive, observe traffic on Sepolia)
2. Round-trip run (author a TWAP via Safe + Compose + an
EthFlow swap via cow.fi, watch end-to-end submission)
3. Inspecting state after a run
4. What this run does NOT prove (and which issues cover that)
5. Troubleshooting matrix
6. References (engine_config schema, ADRs, PR range)
- `justfile` recipes:
build-m2: cargo build both M2 wasm modules
run-m2: build-m2 + build-engine + cargo run engine
## Validated locally
Booted `cargo run -p nexum-engine -- --engine-config engine.m2.toml`
against Sepolia public WS. Observed in ~1s wall clock:
- WS provider opened against ethereum-sepolia-rpc.publicnode.com
- Both manifests parsed; both capability sets resolved
(logging + local-store + chain + cow-api)
- Both wasm components compiled
- Both `init` succeeded
- `supervisor up count=2`, `supervisor ready modules=2 chains=1`
- All 3 subscriptions opened cleanly:
block subscription chain_id=11155111
log subscription module=twap-monitor chain_id=11155111
log subscription module=ethflow-watcher chain_id=11155111
- Clean SIGTERM shutdown
The actual observed log output is captured verbatim in the runbook
section 1 so future operators know what "healthy" looks like.
## Scope
- The smoke half (section 1) is passive: it validates boot +
subscription health without producing traffic. Useful before
every round-trip.
- The round-trip half (section 2) requires a Sepolia Safe + test
ETH + interaction with the Compose Safe app / cow.fi UI. Cannot
be automated from CI (chain-side actions need a wallet). Operator
works through the steps.
- What this does NOT prove is explicit in section 4: throughput
/ soak (COW-1031), cross-module isolation under load (COW-1064),
adversarial resource exhaustion (COW-1036), security review
(COW-1065).
## Not addressed
- Env-var substitution in engine.toml (e.g. `${SEPOLIA_RPC}`) is
not wired in the engine today; runbook documents the workaround
(edit URL inline). Filing as a follow-up is out of scope here -
if needed, add as an M4 nice-to-have.
- `ls-dump` CLI binary referenced in section 3 does not exist yet;
section explicitly says "no ls-dump bin in 0.2; proper inspector
is M4 scope" and falls back to re-booting the engine on the same
state_dir to inspect rows via the dispatch logs.
Linear: stacks on COW-1068. No new issue created - this is
documentation work supporting the existing M2 milestone, not a new
deliverable.
Author
|
Work landed via |
brunota20
added a commit
that referenced
this pull request
Jun 25, 2026
… 3-module E2E)
Sister doc to `docs/operations/m2-testnet-runbook.md`. Same shape,
different modules. Closes the gap "M3 is unit + integration tested
but has never been exercised against a real chain", same as the M2
runbook closed for M2.
## New files
- `engine.m3.toml` - workspace-root engine config that boots the 3
M3 example modules (price-alert + balance-tracker + stop-loss)
against Sepolia public WS. Separate `state_dir = "./data/m3"` so
it never collides with M1 / M2 runbook state.
- `docs/operations/m3-testnet-runbook.md` - operator runbook
mirroring the M2 one: prerequisites, smoke+active run (M3 is
active by default since the example modules trigger on every
block), optional pre-signature setup for real stop-loss
settlement, state inspection, scope boundaries, troubleshooting,
references.
- `justfile` recipes: `build-m3` + `run-m3`.
## Validated locally
A single Sepolia block dispatch (~10 s wall clock) drove all 3 M3
strategy paths through the live testnet:
- **price-alert**: `chain::request eth_call` -> Chainlink
AggregatorV3Interface -> ABI decode -> `TRIGGERED answer=
174553978080 threshold=250000000000 (Below)` (Sepolia ETH/USD
feed reports $1745.54, below the $2500 default threshold).
- **balance-tracker**: 2 `chain::request eth_getBalance` calls
(one per configured address) - SDK chain helper + multi-key
local-store path.
- **stop-loss**: `eth_call` oracle -> `from_signed_order_data`
`OrderCreation` with `Signature::PreSign` -> `cow-api::submit-
order` bytes=561 -> orderbook returns typed
`TransferSimulationFailed` -> `classify_api_error` tags as
retriable -> `retry on next block`. Full submit path
confirmed; the orderbook rejection is the typed-retry
contract working as designed (the default config's
`owner = 0x70997970...` does not hold the sell token on
Sepolia, so simulation correctly fails).
This validates everything the SDK BLEU-840 / BLEU-841 / BLEU-851 /
-852 / -854 / -855 PR series builds: Host trait surface, chain
helpers, cow helpers, MockHost recipe, strategy/lib split. The
same code paths that pass 145 unit tests + 6 doctests + 5
supervisor integration tests now also work against live Sepolia.
## What this validates that the M2 runbook does not
M2 only exercises the orderbook submit path indirectly (through
the EthFlow watcher reacting to swap.cow.fi traffic, and only when
app_data is empty - documented limitation). M3 stop-loss submits
proactively on every poll, so the orderbook always sees a real
`OrderCreation` body even if it rejects. The typed-retry SDK
contract (`classify_api_error` mapping `TransferSimulationFailed`
-> `RetryAction::TryNextBlock`) is exercised end-to-end with a
real orderbook response, not a fixture.
## Stacks on
- `fix(event_loop)` commit immediately preceding this one - the
bug surfaced wiring up `engine.m3.toml` (block-only subscriptions
bailed the engine pre-fix).
- PR #31 (M2 runbook) - same operator-doc shape, same conventions.
brunota20
added a commit
that referenced
this pull request
Jun 25, 2026
… 3-module E2E)
Sister doc to `docs/operations/m2-testnet-runbook.md`. Same shape,
different modules. Closes the gap "M3 is unit + integration tested
but has never been exercised against a real chain", same as the M2
runbook closed for M2.
## New files
- `engine.m3.toml` - workspace-root engine config that boots the 3
M3 example modules (price-alert + balance-tracker + stop-loss)
against Sepolia public WS. Separate `state_dir = "./data/m3"` so
it never collides with M1 / M2 runbook state.
- `docs/operations/m3-testnet-runbook.md` - operator runbook
mirroring the M2 one: prerequisites, smoke+active run (M3 is
active by default since the example modules trigger on every
block), optional pre-signature setup for real stop-loss
settlement, state inspection, scope boundaries, troubleshooting,
references.
- `justfile` recipes: `build-m3` + `run-m3`.
## Validated locally
A single Sepolia block dispatch (~10 s wall clock) drove all 3 M3
strategy paths through the live testnet:
- **price-alert**: `chain::request eth_call` -> Chainlink
AggregatorV3Interface -> ABI decode -> `TRIGGERED answer=
174553978080 threshold=250000000000 (Below)` (Sepolia ETH/USD
feed reports $1745.54, below the $2500 default threshold).
- **balance-tracker**: 2 `chain::request eth_getBalance` calls
(one per configured address) - SDK chain helper + multi-key
local-store path.
- **stop-loss**: `eth_call` oracle -> `from_signed_order_data`
`OrderCreation` with `Signature::PreSign` -> `cow-api::submit-
order` bytes=561 -> orderbook returns typed
`TransferSimulationFailed` -> `classify_api_error` tags as
retriable -> `retry on next block`. Full submit path
confirmed; the orderbook rejection is the typed-retry
contract working as designed (the default config's
`owner = 0x70997970...` does not hold the sell token on
Sepolia, so simulation correctly fails).
This validates everything the SDK BLEU-840 / BLEU-841 / BLEU-851 /
-852 / -854 / -855 PR series builds: Host trait surface, chain
helpers, cow helpers, MockHost recipe, strategy/lib split. The
same code paths that pass 145 unit tests + 6 doctests + 5
supervisor integration tests now also work against live Sepolia.
## What this validates that the M2 runbook does not
M2 only exercises the orderbook submit path indirectly (through
the EthFlow watcher reacting to swap.cow.fi traffic, and only when
app_data is empty - documented limitation). M3 stop-loss submits
proactively on every poll, so the orderbook always sees a real
`OrderCreation` body even if it rejects. The typed-retry SDK
contract (`classify_api_error` mapping `TransferSimulationFailed`
-> `RetryAction::TryNextBlock`) is exercised end-to-end with a
real orderbook response, not a fixture.
## Stacks on
- `fix(event_loop)` commit immediately preceding this one - the
bug surfaced wiring up `engine.m3.toml` (block-only subscriptions
bailed the engine pre-fix).
- PR #31 (M2 runbook) - same operator-doc shape, same conventions.
brunota20
added a commit
that referenced
this pull request
Jun 25, 2026
… 3-module E2E)
Sister doc to `docs/operations/m2-testnet-runbook.md`. Same shape,
different modules. Closes the gap "M3 is unit + integration tested
but has never been exercised against a real chain", same as the M2
runbook closed for M2.
## New files
- `engine.m3.toml` - workspace-root engine config that boots the 3
M3 example modules (price-alert + balance-tracker + stop-loss)
against Sepolia public WS. Separate `state_dir = "./data/m3"` so
it never collides with M1 / M2 runbook state.
- `docs/operations/m3-testnet-runbook.md` - operator runbook
mirroring the M2 one: prerequisites, smoke+active run (M3 is
active by default since the example modules trigger on every
block), optional pre-signature setup for real stop-loss
settlement, state inspection, scope boundaries, troubleshooting,
references.
- `justfile` recipes: `build-m3` + `run-m3`.
## Validated locally
A single Sepolia block dispatch (~10 s wall clock) drove all 3 M3
strategy paths through the live testnet:
- **price-alert**: `chain::request eth_call` -> Chainlink
AggregatorV3Interface -> ABI decode -> `TRIGGERED answer=
174553978080 threshold=250000000000 (Below)` (Sepolia ETH/USD
feed reports $1745.54, below the $2500 default threshold).
- **balance-tracker**: 2 `chain::request eth_getBalance` calls
(one per configured address) - SDK chain helper + multi-key
local-store path.
- **stop-loss**: `eth_call` oracle -> `from_signed_order_data`
`OrderCreation` with `Signature::PreSign` -> `cow-api::submit-
order` bytes=561 -> orderbook returns typed
`TransferSimulationFailed` -> `classify_api_error` tags as
retriable -> `retry on next block`. Full submit path
confirmed; the orderbook rejection is the typed-retry
contract working as designed (the default config's
`owner = 0x70997970...` does not hold the sell token on
Sepolia, so simulation correctly fails).
This validates everything the SDK BLEU-840 / BLEU-841 / BLEU-851 /
-852 / -854 / -855 PR series builds: Host trait surface, chain
helpers, cow helpers, MockHost recipe, strategy/lib split. The
same code paths that pass 145 unit tests + 6 doctests + 5
supervisor integration tests now also work against live Sepolia.
## What this validates that the M2 runbook does not
M2 only exercises the orderbook submit path indirectly (through
the EthFlow watcher reacting to swap.cow.fi traffic, and only when
app_data is empty - documented limitation). M3 stop-loss submits
proactively on every poll, so the orderbook always sees a real
`OrderCreation` body even if it rejects. The typed-retry SDK
contract (`classify_api_error` mapping `TransferSimulationFailed`
-> `RetryAction::TryNextBlock`) is exercised end-to-end with a
real orderbook response, not a fixture.
## Stacks on
- `fix(event_loop)` commit immediately preceding this one - the
bug surfaced wiring up `engine.m3.toml` (block-only subscriptions
bailed the engine pre-fix).
- PR #31 (M2 runbook) - same operator-doc shape, same conventions.
brunota20
added a commit
that referenced
this pull request
Jun 25, 2026
… 3-module E2E)
Sister doc to `docs/operations/m2-testnet-runbook.md`. Same shape,
different modules. Closes the gap "M3 is unit + integration tested
but has never been exercised against a real chain", same as the M2
runbook closed for M2.
## New files
- `engine.m3.toml` - workspace-root engine config that boots the 3
M3 example modules (price-alert + balance-tracker + stop-loss)
against Sepolia public WS. Separate `state_dir = "./data/m3"` so
it never collides with M1 / M2 runbook state.
- `docs/operations/m3-testnet-runbook.md` - operator runbook
mirroring the M2 one: prerequisites, smoke+active run (M3 is
active by default since the example modules trigger on every
block), optional pre-signature setup for real stop-loss
settlement, state inspection, scope boundaries, troubleshooting,
references.
- `justfile` recipes: `build-m3` + `run-m3`.
## Validated locally
A single Sepolia block dispatch (~10 s wall clock) drove all 3 M3
strategy paths through the live testnet:
- **price-alert**: `chain::request eth_call` -> Chainlink
AggregatorV3Interface -> ABI decode -> `TRIGGERED answer=
174553978080 threshold=250000000000 (Below)` (Sepolia ETH/USD
feed reports $1745.54, below the $2500 default threshold).
- **balance-tracker**: 2 `chain::request eth_getBalance` calls
(one per configured address) - SDK chain helper + multi-key
local-store path.
- **stop-loss**: `eth_call` oracle -> `from_signed_order_data`
`OrderCreation` with `Signature::PreSign` -> `cow-api::submit-
order` bytes=561 -> orderbook returns typed
`TransferSimulationFailed` -> `classify_api_error` tags as
retriable -> `retry on next block`. Full submit path
confirmed; the orderbook rejection is the typed-retry
contract working as designed (the default config's
`owner = 0x70997970...` does not hold the sell token on
Sepolia, so simulation correctly fails).
This validates everything the SDK BLEU-840 / BLEU-841 / BLEU-851 /
-852 / -854 / -855 PR series builds: Host trait surface, chain
helpers, cow helpers, MockHost recipe, strategy/lib split. The
same code paths that pass 145 unit tests + 6 doctests + 5
supervisor integration tests now also work against live Sepolia.
## What this validates that the M2 runbook does not
M2 only exercises the orderbook submit path indirectly (through
the EthFlow watcher reacting to swap.cow.fi traffic, and only when
app_data is empty - documented limitation). M3 stop-loss submits
proactively on every poll, so the orderbook always sees a real
`OrderCreation` body even if it rejects. The typed-retry SDK
contract (`classify_api_error` mapping `TransferSimulationFailed`
-> `RetryAction::TryNextBlock`) is exercised end-to-end with a
real orderbook response, not a fixture.
## Stacks on
- `fix(event_loop)` commit immediately preceding this one - the
bug surfaced wiring up `engine.m3.toml` (block-only subscriptions
bailed the engine pre-fix).
- PR #31 (M2 runbook) - same operator-doc shape, same conventions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Adds an M2 testnet runbook + the engine config to actually run M2 against Sepolia. Closes the gap "M2 is unit + integration tested but has never been exercised against a real chain".
Why
The 5 supervisor integration tests (COW-1068) confirm wit-bindgen + WitBindgenHost + dispatch work on synthetic events. They do NOT confirm:
This PR wires the operator path so any of us can run M2 end-to-end against Sepolia in ~30s.
Changes
Validated
Booted the engine against Sepolia public WS locally - the observed log output is captured verbatim in runbook section 1 so future operators know what healthy looks like:
```
INFO opening chain RPC provider chain_id=11155111 url="wss://..."
INFO loading module manifest manifest=modules/twap-monitor/module.toml
[manifest] required capabilities: logging, local-store, chain, cow-api
INFO compiling component component=target/wasm32-wasip2/release/twap_monitor.wasm
INFO twap-monitor init module="twap-monitor"
INFO init succeeded module=twap-monitor
INFO loading module manifest manifest=modules/ethflow-watcher/module.toml
[manifest] required capabilities: logging, local-store, chain, cow-api
INFO compiling component component=target/wasm32-wasip2/release/ethflow_watcher.wasm
INFO ethflow-watcher init module="ethflow-watcher"
INFO supervisor up count=2
INFO supervisor ready modules=2 chains=1
INFO block subscription open chain_id=11155111
INFO log subscription open module=twap-monitor chain_id=11155111
INFO log subscription open module=ethflow-watcher chain_id=11155111
```
Clean SIGTERM shutdown.
Breaking changes
None. New files + 2 new justfile recipes.
Testing
What this PR does NOT do
Explicit in runbook section 4:
AI assistance disclosure
AI Assistance: this change + description was produced by a Claude Code agent (Claude Opus 4.7 1M context). A human (Bruno) reviewed and is accountable for the result. The Sepolia boot validation was run by the agent in the local repo.
Stacks on #30 (COW-1068 supervisor integration tests) -> #29 (COW-1067 SDK doctests) -> #28 (COW-1069 rustdoc gate) -> #27 (COW-1066 CI matrix) -> #26 (COW-1063 QA cleanup).