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:
- 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.
- 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.
- 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:
- 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.
- 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).
- 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)
Summary
AztecNode.getTxReceiptreturns aDroppedTxReceiptfor 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 samestatus: "dropped"/error: "Tx dropped by P2P node"in both cases.Behind a load-balanced public RPC (multiple replicas, no session affinity),
sendTxand a subsequentgetTxReceiptcan be served by different replicas. A just-submitted, perfectly healthy tx therefore transiently reads asDROPPEDuntil 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>):sendTxresolves successfully (tx accepted by the serving replica)getTxReceipt(0x2a1894…ddf593)→status: "dropped",error: "Tx dropped by P2P node"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:with
WaitOpts.ignoreDroppedReceiptsFor(default 5s). But:waitForTx/SentTx.wait()callers. Anything usingnode.getTxReceiptdirectly — 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.DROPPEDwell past 5s.Suggestions
Any of these would let clients handle the ambiguity correctly:
DroppedTxReceipt(not_seenvsevicted/expired), or a distinctUNKNOWNstatus if a breaking change is acceptable. A node that has never had the tx in its pool should be able to say so.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).ignoreDroppedReceiptsForand/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
DROPPEDas 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#327Environment
@aztec/*5.0.1(client side)lb.drpc.live/aztec-testnet)