← Back to Research
Research · July 13, 2026

How unauthenticated simulateTransaction requests saturate an Agave RPC node's executor pool

Simon Morley·7 min read

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 simulateTransaction JSON-RPC method on an Agave node's public RPC endpoint. No authentication is required, and the path is effectively free: sigVerify defaults to false, and setting replaceRecentBlockhash: true requires sigVerify: false, which is the cheaper and more attacker-friendly combination.
  • Preconditions. A reachable RPC endpoint with simulateTransaction enabled, and a funded fee-payer keypair (the bank loads the fee-payer even when sigVerify is false). No on-chain fees are charged for a simulation.
  • Processing path. In agave v3.1.9 (commit 765ee54adc4f574b1cd4f03a5500bf46c0af0817), simulateTransaction is a synchronous handler declared with a sync signature (rpc/src/rpc.rs:3502-3508), and the simulate call at rpc/src/rpc.rs:4009 invokes bank.simulate_transaction(...) directly from the Tokio handler with no spawn_blocking interposed. bank.simulate_transaction (runtime/src/bank.rs:3066-3074) runs load_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_limit value, 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 getProgramAccounts finding, which saturates the separate spawn_blocking pool — 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, commit 765ee54adc4f574b1cd4f03a5500bf46c0af0817, exercised against solana-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 recent v3.x; it was not measured against v2.x or 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/agave and 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 unmodified solana-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.16 on loopback (127.0.0.1:8899), default configuration. The transaction was ComputeBudget::SetComputeUnitLimit(1_400_000) + ComputeBudget::SetComputeUnitPrice(1) + 40× SPL Memo v2 (small payload), submitted with sigVerify=false and replaceRecentBlockhash=true, consuming 237,660 CU per simulation.

    Metric1 worker, 30 s8 workers, 10 sChange
    Aggregate req/s918.71,036.51.13× (vs an ideal 8×)
    Per-worker req/s918.7129.67.1× drop
    p50 latency1.02 ms7.66 ms7.5×
    p99 latency2.01 ms12.12 ms6.0×
    HTTP status / simulate result200 / 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 getLatestBlockhash or an indexer reading getTransaction queues 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 simulateTransaction per 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_limit ceiling at the LB or proxy. Reject simulate requests whose embedded ComputeBudget::SetComputeUnitLimit exceeds 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 simulateTransaction at the LB where clients do not need it. Consensus-only and validator-internal monitoring tiers typically do not expose it.
  • Detection. Watch for sustained simulateTransaction load from small IP ranges with high unitsConsumed and sigVerify=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

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 getMultipleAccounts response amplification (advisory finding SOL_F10, primitive SOL_F10_multi_get_accounts_ampNRDAX-T0329) and getProgramAccounts spawn_blocking saturation (advisory finding SOL_P07, primitive sol_gpa_compute_scanNRDAX-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.

researchadvisorysolanaagaverpcsimulatetransactioninfrastructure-security
Publication policy:Why we publish these findings →

Related Posts