Skip to content

getTxReceipt answers DROPPED for unknown txs — behind load-balanced RPCs, healthy just-submitted txs read as dropped #25018

Description

@alejoamiras

Summary

AztecNode.getTxReceipt returns a DroppedTxReceipt for any tx hash the queried node does not know. "This node has never seen this tx" and "this tx was dropped from the mempool" are indistinguishable to callers — the receipt carries the same status: "dropped" / error: "Tx dropped by P2P node" in both cases.

Behind a load-balanced public RPC (multiple replicas, no session affinity), sendTx and a subsequent getTxReceipt can be served by different replicas. A just-submitted, perfectly healthy tx therefore transiently reads as DROPPED until p2p gossip catches up. Any client that treats receipt status as authoritative and persists it (wallets, indexers, monitoring bots) records a false terminal failure for a tx that later mines and finalizes.

Observed instance (alpha-testnet)

We hit this with the Nulo wallet extension on 2026-07-27, submitting through dRPC's load-balanced gateway (https://lb.drpc.live/aztec-testnet/<key>):

Time Event
T+0 sendTx resolves successfully (tx accepted by the serving replica)
T+667ms getTxReceipt(0x2a1894…ddf593)status: "dropped", error: "Tx dropped by P2P node"
later tx mines and finalizes normally

The same endpoint, queried later for the same hash (explorer):

{
  "txHash": "0x2a1894640dc528f418d97d7ff518210c2901e4f807ba305a6962e68df9ddf593",
  "status": "finalized",
  "executionResult": "success",
  "blockNumber": 19899,
  "slotNumber": 19504,
  "epochNumber": 609,
  "txIndexInBlock": 0,
  "transactionFee": "1347497461427061582"
}

Our wallet's status poller persisted the T+667ms answer as terminal, so the UI showed a successfully finalized transfer as failed — a state that invites the user to re-send the funds.

Why the existing mitigation doesn't cover it

aztec.js already acknowledges this exact race inside waitForTx:

// If the tx was "dropped", either return it or ignore based on timing.
// We can ignore it at first because the transaction may have been sent to node 1, and now we're asking node 2 for the receipt.
// If we don't allow a short grace period, we could incorrectly return a TxReceipt with status DROPPED.

with WaitOpts.ignoreDroppedReceiptsFor (default 5s). But:

  1. It only protects waitForTx / SentTx.wait() callers. Anything using node.getTxReceipt directly — long-lived trackers that outlive a blocking wait, e.g. a wallet activity feed that survives restarts — has to rediscover the race and reimplement the workaround.
  2. The grace runs from wait start, not from submission, and 5s presumes you're polling the node you submitted to. Behind an LB, a lagging replica can answer DROPPED well past 5s.
  3. The receipt carries no signal to disambiguate, so even a client that wants to be smarter has nothing to branch on.

Suggestions

Any of these would let clients handle the ambiguity correctly:

  1. Distinguish "unknown" from "dropped" in the receipt — e.g. a reason discriminator on DroppedTxReceipt (not_seen vs evicted/expired), or a distinct UNKNOWN status if a breaking change is acceptable. A node that has never had the tx in its pool should be able to say so.
  2. Document the client contract for TxStatus.DROPPED: it is a point-in-time answer from one node, not a network-wide terminal verdict, and clients should debounce it (ideally with guidance on how long is safe relative to tx expiration).
  3. Consider raising the default ignoreDroppedReceiptsFor and/or applying the same grace in any SDK surface that reports tx status, since public RPC endpoints are commonly load-balanced.

Workaround (for other integrators)

We now accept DROPPED as terminal only when the tx is older than a 60s submission grace and was observed dropped on 3 consecutive polls, and we keep re-checking dropped txs for 30 minutes so a late mine flips the record back to its mined status: alejoamiras/nulo#327

Environment

  • @aztec/* 5.0.1 (client side)
  • alpha-testnet, 2026-07-27
  • RPC: dRPC load-balanced gateway (lb.drpc.live/aztec-testnet)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions