The lazy mirror
RustAG replays mainnet on a local SVM with no block to fork from. It does that by fetching the exact accounts a transaction touches, on first access, and tracking every write through a four-state machine.
The lazy account mirror
The lazy account mirror is the core idea of RustAG. Rather than copying all of mainnet up front, it fetches the exact accounts a transaction touches — on first access — and caches them locally.
How it works
When a transaction reads account X:
- Local hit? Return the stagenet's local copy.
- Miss? Fetch it from mainnet → cache it → mark it
Clean→ return it. - A transaction writes X? Mark it
Dirty— it is now frozen from mainnet sync forever, so your local changes are never clobbered.
Local hit? → return local copyMiss? → fetch from mainnet → cache → mark Clean → returnWrite to X? → mark Dirty (frozen from mainnet sync forever) Background: re-fetch Clean ORACLE accounts every 30s Dirty + Pinned accounts: never overwrittenA background task re-fetches Clean oracle accounts every 30 seconds (the default interval), so Pyth prices stay fresh. In the transaction path, a pre-load step batch-fetches any static account key that is not already loaded and not Dirty, loading it into LiteSVM as Clean; fetch failures are logged and tolerated.
Why this matters on the SVM
This is how “mainnet replay” works on Solana. EVM tools (Tenderly, Anvil's --fork-url) fork at a block hash and pull state from that fixed point; the SVM has no equivalent block to fork from. So RustAG instead fetches accounts on demand and tracks every write, so it always knows what it may and may not refresh from mainnet.
The mirror itself (rustag-mirror) is a deliberately dependency-light read side: given pubkeys, it returns current mainnet state via a raw getMultipleAccounts JSON-RPC call over reqwest (≤100 keys per call), avoiding solana-rpc-client so it doesn't fork the Solana crate versions LiteSVM 0.12 unifies on.
Account state machine
Every account in a stagenet carries one of four sync states — the AccountSync enum in crates/rustag-core/src/account_state.rs. The state decides whether the background scheduler is allowed to overwrite the account from mainnet.
| State | Meaning | Background sync? |
|---|---|---|
| Unknown | Never fetched; pulled lazily on first access. | Never |
| Clean | A faithful mainnet copy. | Yes |
| Dirty | Modified by a local transaction. | Never |
| Pinned | Set via the override API. | Never |
Clean carries a fetched_at timestamp and Dirty carries a modified_at timestamp; Unknown and Pinned are plain variants. An account is_syncable() only when it is Clean or Unknown — exactly the set the background oracle loop is allowed to refresh.
pub enum AccountSync { /// Never fetched. Will be fetched lazily on first access. Unknown, /// Fetched from mainnet. May be re-synced by the background scheduler. Clean { fetched_at: DateTime<Utc> }, /// Modified by a local transaction. Never overwritten by mainnet sync. Dirty { modified_at: DateTime<Utc> }, /// Explicitly set by the user via the override API. Immune to everything. Pinned,}Transitions
- Unknown → Clean: first access misses locally, so RustAG fetches from mainnet, caches it, and stamps it
Clean(from_remote/mark_clean). - Clean → Clean (refreshed): the background oracle sync re-fetches
Cleanoracle accounts every 30s, re-stampingfetched_at. - Clean / Unknown → Dirty: a local transaction writes the account. Writable accounts are derived from the message header's
(num_required_signatures, num_readonly_signed, num_readonly_unsigned)layout and markedDirty; their post-state is persisted. Read-only accounts (programs, oracles, sysvars) stayCleanand keep syncing. - any → Pinned: the override API (
rustag override) callspin(), making the account immune to everything — no background sync, no clobbering.
Once an account is Dirty or Pinned, the background mirror never touches it again, so user-modified and explicitly-pinned state is preserved deterministically.
Oracle freshness
Oracle accounts are the one category RustAG actively keeps fresh. A background loop (spawn_oracle_sync) re-fetches Clean oracle accounts on the default 30s interval (clamped to a 1s minimum), so Pyth prices don't go stale under your tests.
Phase 2 · Preview A push path over the standard accountSubscribe WebSocket — the protocol Geyser/Yellowstone providers serve — drops oracle staleness to a p99 target of under 2 seconds. It is behind the realtime cargo feature; build with --features realtime.
Dirty and Pinned accounts are never overwritten by any sync — neither the 30s poll nor the realtime push path. Whatever you write or pin stays put, so a test stays deterministic.Why staging, not testnet
RustAG is a staging environment, not another testnet — and that distinction is the whole value proposition. Devnet pools are empty or fake, faucets cap around ~5 SOL/day while an integration suite needs 20–50 SOL/day, and you can't fork the SVM at a block the way EVM tools do. RustAG mirrors the actual mainnet account state on demand instead.
| Real mainnet state | Persistent + RPC | Real-time oracles | Unlimited airdrop | Cloud / multi-tenant | |
|---|---|---|---|---|---|
| solana-test-validator | |||||
| LiteSVM / Bankrun (libs) | |||||
| Devnet / Testnet | |||||
| RustAG |
Because writes are tracked as Dirty and pins are honored, you can reproduce a mainnet incident locally — pin the exact account state and replay the failing transaction against a frozen snapshot. State persists across restarts (SQLite via sqlx), so a stagenet behaves like a real, always-on environment rather than a throwaway fixture.