How unauthenticated simulateTransaction requests saturate an Agave RPC node's executor pool
An unauthenticated, gas-free simulateTransaction request makes an Agave validator run the BPF virtual machine synchronously on one of its shared Tokio executor threads, so a handful of concurrent simulate requests occupy those workers for the full simulation duration and add queue latency to every asynchronous JSON-RPC method served by the same runtime — not only to the simulate path.
This brief covers one finding — SOL_F14 — from NullRabbit advisory NR-2026-001, which documents three related Agave RPC findings. See the full advisory write-up for the two sibling findings.
Why this matters
Anyone who operates a Solana validator or RPC node with public JSON-RPC is exposed. simulateTransaction is a normal, useful method — wallets and clients call it to preview a transaction before sending — so it is commonly left enabled. The problem is architectural rather than about request volume: because the handler runs on the same asynchronous worker pool that serves getLatestBlockhash, getTransaction, and the rest of the RPC surface, saturating it with simulate work degrades latency across the whole RPC tier, including methods the attacker never touched.
That is why a naïve requests-per-second rate limit does not close it. Capping request rate per IP treats the attack as a volume spike; here the cost is that each in-flight request holds a scarce executor thread, so even a modest number of concurrent requests raises queueing latency for everyone. The mitigations that work are specific to this method and are operator-applicable today, ahead of any upstream change.
How the attack works
- Entry point. The
simulateTransactionJSON-RPC method on an Agave node's public RPC endpoint. No authentication is required, and the path is effectively free:sigVerifydefaults tofalse, and settingreplaceRecentBlockhash: truerequiressigVerify: false, which is the cheaper and more attacker-friendly combination. - Preconditions. A reachable RPC endpoint with
simulateTransactionenabled, and a funded fee-payer keypair (the bank loads the fee-payer even whensigVerifyisfalse). No on-chain fees are charged for a simulation. - Processing path. In agave
v3.1.9(commit765ee54adc4f574b1cd4f03a5500bf46c0af0817),simulateTransactionis a synchronous handler declared with a sync signature (rpc/src/rpc.rs:3502-3508), and the simulate call atrpc/src/rpc.rs:4009invokesbank.simulate_transaction(...)directly from the Tokio handler with nospawn_blockinginterposed.bank.simulate_transaction(runtime/src/bank.rs:3066-3074) runsload_and_execute_transactions(...), so the BPF VM executes on the calling Tokio worker thread. - Resource boundary crossed. The Tokio async executor pool — the bounded set of worker threads shared by every asynchronous RPC method. There is no RPC-tier compute-budget enforcement; the only ceiling on per-request work is the on-chain
compute_unit_limitvalue, which the attacker sets in the submitted transaction (default maximum 1,400,000 CU). - Resulting behaviour. Each in-flight simulate request pins a worker for the whole simulation. As concurrency rises, aggregate throughput stops scaling and per-request latency climbs; the wait is time spent queueing for a worker that is busy running attacker BPF bytecode. This is distinct from the sibling
getProgramAccountsfinding, which saturates the separatespawn_blockingpool — the two have non-overlapping mitigations.
Affected systems
- Chain: Solana.
- Implementation: Agave validator (
anza-xyz/agave). - Component / method: JSON-RPC
simulateTransaction. - Version measured: agave
v3.1.9, commit765ee54adc4f574b1cd4f03a5500bf46c0af0817, exercised againstsolana-test-validator 2.2.16. NullRabbit reports the architectural pattern (a synchronous simulate handler on the Tokio runtime, with no RPC-tier compute budget) appears stable across recentv3.x; it was not measured againstv2.xor earlier. This is a specific finding on the Agave implementation and should not be read as a claim about every Solana node implementation. - Vendor response: Disclosed to Anza via GitHub Security Advisory at
anza-xyz/agaveand closed as out of scope under the Agave security policy's RPC carve-outs (characterised by Anza as RPC denial-of-service). There is no upstream patch; the operator mitigations below stand on their own.
Evidence
-
Publicly documented. The disclosure is recorded in Anza's GitHub Security Advisory
GHSA-rvxh-p338-j9p3, which Anza closed as out of scope. There is no independent public CVE for this behaviour; it is original NullRabbit research. -
Independently reproduced by NullRabbit. A self-contained reproducer (
SOL_F14_repro.py) drives the handler against an unmodifiedsolana-test-validator 2.2.16. The load-bearing signal is the one-worker-versus-eight-worker comparison, not single-request latency. -
Measured under controlled conditions. Single-process
solana-test-validator 2.2.16on loopback (127.0.0.1:8899), default configuration. The transaction wasComputeBudget::SetComputeUnitLimit(1_400_000)+ComputeBudget::SetComputeUnitPrice(1)+ 40× SPL Memo v2 (small payload), submitted withsigVerify=falseandreplaceRecentBlockhash=true, consuming 237,660 CU per simulation.Metric 1 worker, 30 s 8 workers, 10 s Change Aggregate req/s 918.7 1,036.5 1.13× (vs an ideal 8×) Per-worker req/s 918.7 129.6 7.1× drop p50 latency 1.02 ms 7.66 ms 7.5× p99 latency 2.01 ms 12.12 ms 6.0× HTTP status / simulate result 200 / ok (100%) 200 / ok (100%) unchanged Aggregate throughput rises only 1.13× under 8× the concurrency — the remaining attacker concurrency is absorbed as queue depth rather than served work. That flat-aggregate, rising-per-worker-latency shape is the canonical signature of a synchronous handler saturating an async runtime.
-
Inferred but not separately measured. The measurements above are on the simulate path, because that is what was driven. The degradation of other asynchronous RPC methods is an architectural consequence of those methods sharing the same Tokio runtime — a trading client polling
getLatestBlockhashor an indexer readinggetTransactionqueues for the same busy workers — rather than a per-method measurement. Magnitudes against other validator implementations (for example jito-solana or a Firedancer-family client RPC) or other Agave versions may differ; the architectural pattern is the claim that carries across.
Operator actions
These are documented, operator-applicable mitigations from the advisory — apply them at your load balancer or RPC proxy, ahead of any upstream change:
- Rate-limit
simulateTransactionper IP at the load balancer. Aggressive limits (single-digit requests per second per IP, with a burst budget) are safe, because legitimate simulate workloads are bounded. - Enforce a
compute_unit_limitceiling at the LB or proxy. Reject simulate requests whose embeddedComputeBudget::SetComputeUnitLimitexceeds your target CU cap. The CU cap lives in the transaction bytes, so an envelope-aware proxy can enforce it without touching the validator. - Consider blocking
simulateTransactionat the LB where clients do not need it. Consensus-only and validator-internal monitoring tiers typically do not expose it. - Detection. Watch for sustained
simulateTransactionload from small IP ranges with highunitsConsumedandsigVerify=false.
The architecturally clean fix — dispatching the simulate call into spawn_blocking or a dedicated simulation pool, and enforcing an RPC-tier compute budget — sits upstream and is a suggested direction, not a shipped patch. Operators do not need to wait for it to bound exposure.
Canonical references
- NRDAX technique: NRDAX-T0006 — async-runtime-blocking-vm-execution
- NullRabbit advisory: NR-2026-001 — Three Agave RPC architectural findings · on-site write-up
- Evidence bundle (Hugging Face): NullRabbit/nr-bundles-public — primitive
SOL_F14_simulate_transaction_sync_wedge - Reproducer:
NR-2026-001/reproducers/SOL_F14_repro.py - Upstream: Anza GitHub Security Advisory
GHSA-rvxh-p338-j9p3; AgaveSECURITY.md
Related attacks
- NRDAX-T0006 on IOTA — the same technique (a synchronous VM call blocking an async worker) appears on IOTA via
iota_devInspectTransactionBlock(advisories NR-2026-032 and NR-2026-034). Same mechanism, different implementation. - Agave
getMultipleAccountsresponse amplification (advisory finding SOL_F10, primitiveSOL_F10_multi_get_accounts_amp→ NRDAX-T0329) andgetProgramAccountsspawn_blockingsaturation (advisory finding SOL_P07, primitivesol_gpa_compute_scan→ NRDAX-T0342) — the two sibling findings from the same advisory (NR-2026-001). They saturate different resources (egress bandwidth and the blocking-task pool, respectively) and need independent mitigations.
Track attacks affecting Agave
New Agave and Solana infrastructure findings are catalogued as they are reproduced. Track attacks affecting Agave on NRDAX and in NullRabbit research.
Analysis plus reproducer. No weaponised proof-of-concept code.
Related Posts
NR-2026-001 - Three Agave RPC architectural findings
Three architectural findings in the Agave JSON-RPC layer at v3.1.9: response amplification on getMultipleAccounts, Tokio executor saturation via simulateTransaction, and spawn_blocking pool saturation via getProgramAccounts. Architectural patterns, not rate-limit DoS - operator rate limits don't close them.
How an unauthenticated TLS half-open flood pins rippled's memory and crashes it under low file-descriptor limits
rippled's inbound TLS listeners on the peer port (51235) and the RPC-HTTPS port (5006) accept a partial TLS handshake with no deadline and no per-IP half-open cap, so a single source IP can pin server memory indefinitely and, at a default file-descriptor limit, crash the process.
How CometBFT's SecretConnection handshake spends CPU before authenticating the peer
CometBFT completes the full SecretConnection STS handshake — an X25519 Diffie–Hellman exchange and an Ed25519 signature verification — for any connecting peer before checking whether that peer's node-ID is allowlisted, so an unauthenticated remote source can spend the node's asymmetric-crypto budget at will.
