Skip to content

feat(tests): opt-in background activity mode for chains [concept]#22

Draft
antonbaliasnikov wants to merge 6 commits into
mainfrom
aba-background-activity-mode
Draft

feat(tests): opt-in background activity mode for chains [concept]#22
antonbaliasnikov wants to merge 6 commits into
mainfrom
aba-background-activity-mode

Conversation

@antonbaliasnikov

Copy link
Copy Markdown
Contributor

Concept — for review, not merge

This is a concept PR exploring a "noisy" run mode for test chains, opened in draft. It's working and fully tested, but the API surface is up for discussion before we commit to it.

Motivation

Today every chain runs in silent mode: after setup it sits idle until a test explicitly calls ping() / send_tx() / transfer(). Tests therefore always run against a perfectly still chain, which hides race conditions and ordering bugs between the sequencer, L1 watcher, priority queue, and batch pipeline.

This adds an opt-in background activity mode: a chain continuously fires L2 self-transfers and L1→L2 deposits in the background while a test runs, so the test executes inside a live, moving chain. Inspired by the production matter-labs/watchdog service (flow-per-activity-type, crash-safe restart, start-to-start pacing).

What's here

New tests/src/activity.rs owns the noise:

  • ACTIVITY_WALLET_KEYS — a private pool (Anvil HD accounts feat: test proper migration support #10fix: enforce Anvil block gas limit to catch oversized server blocks #19), separate from test wallets so the two never share nonces. Funded on L1 via --accounts 20 and on L2 during the fixture's cache-miss path, so the deposits land in the cached snapshot (zero cost on cache hit).
  • ActivityConfig — per-flow Option<Duration> (None = disabled). transfers_only() / deposits_only() presets for targeted race tests.
  • ActivityHandlepause() / resume() / stop().await / is_alive() plus txs_sent() / deposits_sent() counters.
  • Crash-safe flow loops — bounded error budget (MAX_CONSECUTIVE_ERRORS), then the task sets a failed flag and exits so is_alive() flips (a broken loop is detectable, not silent). Start-to-start interval pacing avoids drift under slow RPC.

Wiring:

  • Chain::start_background_activity(config) — double-start guard (panics if already running) + per-chain dedicated L1 deposit wallet (chain_index % pool_len), so no two chains share an L1 signer.
  • Ecosystem::start_background_activity(config) — fan-out across all chains, panics if chain_count > pool_len.
  • ecosystem fixture gains activity: Option<ActivityConfig> that auto-starts noise from setup.

Usage

// Silent (existing tests — unchanged)
async fn my_test(#[future] ecosystem: Ecosystem) -> Result<()> { ... }

// Noisy from the start
async fn noisy(
    #[future]
    #[with(vec![6565], Some(ActivityConfig::default()))]
    ecosystem: Ecosystem,
) -> Result<()> { ... }

// Manual handle — full control
let h = eco.chain().start_background_activity(ActivityConfig::deposits_only());
h.pause(); /* sensitive assertion */ h.resume();
assert!(h.deposits_sent() >= 1 && h.is_alive());
h.stop().await; // restart-safe; Drop is teardown-only

Semantics worth a look during review

  • pause() stops new submissions; an in-flight L1→L2 deposit may still land. Not an exact quiesce.
  • stop().await is the only restart-safe teardown; Drop aborts without awaiting.
  • txs_sent counts RPC-accepted hashes (mempool), deposits_sent counts L1-confirmed deposits — neither implies mining/finalization.

Testing

  • Unit tests for config presets + round-robin helper.
  • 5 integration tests: manual-handle lifecycle, restart-after-stop, pause-halts-submissions, fixture-param auto-start, double-start panic.
  • Full suite green (16/16), including pre-existing chain_executes_a_batch, two_chains_settle_on_l1, and the v30→v31 upgrade test — no regressions.

Possible follow-ups (out of scope here)

  • Extract to a lib/activity crate if we ever want to replace the TS watchdog with a Rust one.
  • Withdrawal flows, contract-call activity, Prometheus metrics, mined/finalized counters.

🤖 Generated with Claude Code

antonbaliasnikov and others added 5 commits June 19, 2026 15:20
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a "noisy" mode alongside the default silent mode: when enabled, a
chain continuously fires L2 self-transfers and L1→L2 deposits in the
background while a test runs, so tests exercise a live, moving chain
instead of an idle one — surfacing ordering/interleaving bugs across the
sequencer, L1 watcher, priority queue and batch pipeline.

New `tests/src/activity.rs` owns the noise:
- private `ACTIVITY_WALLET_KEYS` pool (Anvil HD accounts #10-#19),
  funded on L1 via `--accounts 20` and with a small L2 gas balance in the
  fixture cache-miss path so the deposits land in the cached snapshot
- `ActivityConfig` (per-flow `Option<Duration>`; `transfers_only`/
  `deposits_only` presets)
- `ActivityHandle` with `pause`/`resume`/`stop` and a single `stats()`
  accessor returning an `ActivityStats` near-snapshot (independent relaxed
  loads over one shared `Arc`, not an atomic read)
- crash-safe flow loops with a bounded error budget and start-to-start
  pacing

Wiring:
- `Chain::start_background_activity` with a double-start guard and a
  per-chain dedicated L1 deposit wallet (no nonce races)
- `Ecosystem::start_background_activity` fan-out with a chain-count guard;
  waits for each enabled flow's first successful tick so a fixture-driven
  test can't pass against a silently-dead noise loop
- `ecosystem` fixture gains an `activity: Option<ActivityConfig>` param
  that auto-starts noise from setup

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@antonbaliasnikov
antonbaliasnikov force-pushed the aba-background-activity-mode branch from 5be1e3b to 38e4a5b Compare June 22, 2026 19:59
Replace the original background-activity design (10-wallet round-robin
pool, raw ActivityStats polling, ping/self_transfer duplication) with a
simpler, verdict-based model owned by Chain.

- Per-flow config: ActivityConfig { l2_transfers, l1_deposits } where
  each flow is Option<FlowConfig { interval, target }>. None switches a
  flow off, giving transfers-only / deposits-only / both. Target is
  Count(n) | Duration(d) | Unbounded.
- Two dedicated wallets per chain (transfer = pool[2i], deposit =
  pool[2i+1]), so neither flow shares a nonce sequence. Pool serves
  max_activity_chains = pool/2 chains. Index derived from chain position;
  test authors never set it.
- Single finalized-on-L1 verdict via ActivityReport: transfers tracked by
  hash (all must finalize with success receipts); deposits via the
  deposit wallet's finalized L2 balance delta >= count*amount (>= because
  the deposit also collects its own gas refund as refundRecipient).
- Activity is part of Chain: chain.start_activity / pause_activity /
  resume_activity / finish_activity (bounded) / stop_activity
  (unbounded/early). Chain owns the run in a Mutex<Option<ActivityHandle>>
  that doubles as the single-run guard. Ecosystem no longer keeps an
  activity_handles field — chains drop before servers and abort their
  tasks at teardown.
- ping() and the transfer loop now share one self_transfer() primitive.
- Fund only each chain's transfer wallet on L2 (deposit wallet never
  sends L2 txs).
- Collapse the integration tests to one feature-demo test that runs
  short Count/Duration activities and asserts each reaches a passing
  verdict.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant