Conversation
removeFromPendingIndices deleted each of a tx's nullifier keys unconditionally. After a same-nullifier conflict is resolved the key can already point at the surviving tx, so evicting the loser deleted the winner's live index entry. The pool then forgets a pending tx already spends that nullifier, letting a duplicate spend be admitted and the fee-replacement rule be bypassed. Only delete the key when it still points at the tx being removed. Adds a regression test covering the protect -> admit-higher-fee -> evict path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
| // A is pending and owns the shared nullifier. | ||
| indices.addPending(a); | ||
| expect(indices.getTxHashByNullifier(shared)).toBe(a.txHash); | ||
|
|
||
| // A is included in a proposal, so it leaves the pending nullifier index. | ||
| indices.updateProtection(a.txHash, SlotNumber(1)); |
There was a problem hiding this comment.
The comments on lines 94 and 98 only restate the adjacent addPending and updateProtection calls. This violates the repository directive to use inline comments only when they explain a non-obvious reason or constraint. Remove these comments before merging; the later explanation of the conflict scenario remains useful.
| // A is pending and owns the shared nullifier. | |
| indices.addPending(a); | |
| expect(indices.getTxHashByNullifier(shared)).toBe(a.txHash); | |
| // A is included in a proposal, so it leaves the pending nullifier index. | |
| indices.updateProtection(a.txHash, SlotNumber(1)); | |
| indices.addPending(a); | |
| expect(indices.getTxHashByNullifier(shared)).toBe(a.txHash); | |
| indices.updateProtection(a.txHash, SlotNumber(1)); |
Context Used: yarn-project/CLAUDE.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Done — removed both restating comments in 9a8fba7; the conflict-scenario comments that carry the non-obvious reason stay.
The removed comments only restated the adjacent addPending/updateProtection calls; the conflict-scenario comments that carry the non-obvious reason stay. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Problem
TxPoolIndices.#removeFromPendingIndicesdeleted each of a tx's nullifier keys unconditionally. After a same-nullifier conflict is resolved, the key can already point at the surviving tx (e.g. a protected txAleaves the pending nullifier index, a higher-feeBspending the same nullifier is admitted, thenAis restored and evicted). Evicting the loser then deletes the winner's live nullifier entry, so the pool forgets that a pending tx already spends that nullifier — a duplicate spend can be admitted and the fee-replacement rule bypassed. Block building revalidates nullifiers, so this stays a mempool-integrity bug, not a chain-state break.Fix
Only delete the nullifier key when it still points at the tx being removed:
Removal stays idempotent; an evicted loser can no longer delete a key already reassigned to the survivor.
Test
Adds a regression test in
tx_pool_indices.test.tscovering the protect → admit-higher-fee → evict path: after eviction the surviving tx must remain discoverable by the shared nullifier. Verified red→green against the built base (@aztec-labs/p2pjest, 15/15 green; the new test fails without the guard),prettier --checkclean,tsgo -b --emitDeclarationOnlyexit 0.Addresses the LabsBox audit finding "[aztec-node] tx_pool_v2 nullifier-conflict eviction deletes the surviving transaction's nullifier index".
🤖 Generated with Claude Code