An autonomous test framework for EEZ sync-rollup chains.
Point it at an L1 / L2 pair, and it launches a set of independent workers that each exercise one facet of the chain — funding, fuzzing, cross-chain contract calls, congestion races, DoS load, and proxy-creation routing — shows their live state on a dashboard, and writes a report after a fixed run (e.g. 1 hour).
┌──────────── EEZ instance (L1 + L2 + composer fronts) ───────────┐
│ │
config.yaml ──▶ Runner ──▶ workers ──▶ shared state ──▶ dashboard (:8799) │
│ └──▶ report (.md/.json)│
└── funder ▸ fuzzer ▸ contract_caller │
▸ congestion ▸ ddos ▸ proxy_builder │
└─────────────────────────────────────────────────────────────────┘
| worker | what it does |
|---|---|
| funder | Funds L2 sub-accounts by depositing to their L1 proxies; the funded pool feeds the fuzzer and DoS workers. Flags deposits that mine on L1 but never credit L2 (lost-deposit / silent stall). |
| fuzzer | Fires randomized L2 transactions from the funded accounts (random value / calldata / gas / edge targets) and asserts the L2 keeps producing blocks under fuzz. |
| contract_caller | Deploys Counter + Logger on both L1 and L2, calls them in cross-chain combinations, and verifies the deployments against Blockscout. Encodes the known regression bugs as assertions (see below). |
| congestion | Races an L2→L1 state write against a parallel L1 write to the same state and classifies the winner / detects lost writes. |
| ddos | Sustained high-rate load on the L2 to find the throughput ceiling, mempool cap, and any liveness break (halt / mempool refusal). |
| proxy_builder | Sends cross-chain txs that explicitly create a proxy vs explicitly don't, across different mempools (inbound front / plain RPC / extras), to see whether the builder lazily creates the proxy — and whether the endpoint you use changes the answer. |
- Bug A — an L2→L1 return path that reverts with
ExecutionNotFoundafter the inner call already ran. - Bug B — a cross-chain call that reports success with empty return data (assert
len(returnData) > 0and the decoded value, never juststatus == 1). - Bug C — the cross-chain derived sender must equal
computeCrossChainProxyAddressand be stable across transactions.
Scope assumption: this build assumes the L2 permits only simple L1↔L2 calls — an outer cross-chain call may not trigger an inner cross-chain call. No worker builds nested cross-chain actions.
git clone https://github.com/koeppelmann/EEZtest.git
cd EEZtest
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt # or: pip install -e .
cp config.example.yaml config.yaml # then edit for your chain
export EEZTEST_PRIVATE_KEY=0x... # a key funded on L1 (and ideally L2)
python -m eeztest check --config config.yaml # connectivity + config sanity (no txs)
python -m eeztest run --config config.yaml # full autonomous run + reportOpen the dashboard at http://localhost:8799 while it runs.
A short run for a quick signal:
python -m eeztest run --config config.yaml --duration 120 # 2-minute smoke run
make smoke # same, via MakefileOne process can supervise several EEZ devnets behind a single dashboard with a devnet switcher — a fleet overview plus per-instance drill-down:
cp instances.example.yaml instances.yaml # add one entry per devnet
python -m eeztest serve --config instances.yaml --foreverdefaults: is deep-merged under every instance, so adding a devnet usually means
just its endpoints and EEZ registry:
instances:
- instance_name: "chiado-6290"
l1: { rpc: "https://rpc.chiadochain.net", chain_id: 10200, xchain_front: "http://host:18999" }
l2: { rpc: "http://host:18688", chain_id: 6290, xchain_front: "http://host:18998" }
eez: { registry: "0x..." }
- instance_name: "devnet-12s"
...Instances are isolated — an unreachable devnet shows as down on the fleet view without affecting the others. Endpoints:
GET /api/overview cross-devnet summary
GET /api/instances instance ids
GET /api/instances/{id}/state one devnet's full snapshot
GET /api/instances/{id}/findings one devnet's findings
GET /api/findings findings across all devnets
Flags: --forever (ignore durations), --monitor-only (watch chains, run no
workers), --port, --duration, --report-dir.
Everything is in config.yaml (start from config.example.yaml). The parameters
you actually need to set:
l1.rpc,l1.chain_id,l1.xchain_front— the L1 chain + the composer's inbound cross-chain front.l2.rpc,l2.chain_id,l2.xchain_front— the L2 chain + the composer's outbound front.eez.rollup_id,eez.registry,eez.ccm_l2— this rollup's id and the proxy contracts.wallet.private_key_env— name of the env var holding your funded key (defaultEEZTEST_PRIVATE_KEY).blockscout.l1_api,blockscout.l2_api— for contract verification (optional).workers.*— enable/disable each worker and tune its knobs.run.duration_seconds— how long a run lasts before the report is written (default3600).
Each worker can be independently toggled; a disabled worker still shows up on the
dashboard as disabled.
- Dashboard (
http://<host>:<port>) — live chain heads, per-worker status / counters / metrics / recent events, and a findings feed. Polls/api/stateand/api/findingsevery 2 s. - Report — written to
reports/at the end of a run as both Markdown and JSON: a verdict, a severity table, chain state, every finding (ranked), and per-worker activity.
eeztest run --config config.yaml [--duration N] [--report-dir DIR]
eeztest check --config config.yaml # verify connectivity + config, send nothing
eeztest report --config config.yaml # serve the dashboard only (monitor, no workers)
eeztest serve --config instances.yaml [--forever] [--monitor-only] [--port N]
eeztest/
config.py rpc.py eez.py # config, JSON-RPC + signing, EEZ mechanics
contracts.py verify.py # solc compile/deploy, Blockscout verification
state.py monitor.py report.py # shared state, chain monitor, report writer
runner.py cli.py # orchestration + CLI
dashboard/ server.py static/index.html
workers/ base.py + one file per worker
contracts/ Counter.sol Logger.sol … # the probe contracts (deployed on L1 and L2)
The ddos worker generates real load and the fuzzer sends real transactions — point EEZtest at a test chain you control or are authorized to test. All value transfers are dust-sized, but transaction volume is deliberately high.
MIT