Conversation
The tx-fetch handler looked up and serialized every entry in the request, so a peer could repeat one hash thousands of times within a legal-size request and make the node re-read and re-serialize the same tx per copy, building a huge response and stalling the event loop on compression. De-duplicate the requested hashes before pool reads (matching the BLOCK_TXS handler) and reject requests far above the honest batch size. Adds handler tests for the repeated-hash, normal-batch, and over-cap cases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
| throw new ReqRespStatusError(ReqRespStatus.BADLY_FORMED_REQUEST, { cause: err }); | ||
| } | ||
|
|
||
| if (txHashes.length > MAX_TX_HASHES_PER_REQUEST) { |
There was a problem hiding this comment.
Cap conflicts with configuration
If P2P_BATCH_TX_REQUESTER_TX_BATCH_SIZE is configured above 100, the requester sends a batch that this handler rejects as BADLY_FORMED_REQUEST. The requester does not split and retry that batch; it penalizes the peer and clears its transaction metadata, which can prevent the batch from being fetched. Constrain the configuration and chunking helper to this limit, or derive both sides from a shared constant.
There was a problem hiding this comment.
This handler serves the TX subprotocol, whose requests are chunked by chunkTxHashesRequest (fixed default 8), not P2P_BATCH_TX_REQUESTER_TX_BATCH_SIZE — that config drives the batch requester, which only sends BLOCK_TXS (batch_tx_requester.ts sends ReqRespSubProtocol.BLOCK_TXS). So no configurable path reaches this handler above the cap of 100 (12x the fixed TX chunk). The equivalent config-vs-cap coupling is real for BLOCK_TXS and is handled in that finding's fix (A-2026).
There was a problem hiding this comment.
You're right — I conflated the TX and BLOCK_TXS request paths. chunkTxHashesRequest uses the fixed default of 8 for this handler, while P2P_BATCH_TX_REQUESTER_TX_BATCH_SIZE applies to BLOCK_TXS, not TX. Therefore no configurable requester can exceed this handler's cap of 100, and the cap does not introduce the failure mode I flagged. The configuration-coupling concern belongs to the separate BLOCK_TXS fix; I'll withdraw this comment.
Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.
| // Honest requesters chunk tx-fetch requests at 8 hashes (see chunkTxHashesRequest | ||
| // and the batch requester default). Reject anything far above that so one peer | ||
| // cannot ask for a huge number of txs in a single legal-size request. | ||
| const MAX_TX_HASHES_PER_REQUEST = 100; |
There was a problem hiding this comment.
Response workload remains unbounded
The count limit still allows about 51.2 MiB of uncompressed data because each of the 100 transactions may be 512 KiB. An admitted peer can issue ten such requests per second, and every response is fully materialized and then compressed synchronously without an output-byte or in-flight-work limit. This leaves a practical memory and event-loop exhaustion path; add a responder-side serialized-byte or work ceiling.
How this was verified: A request can reach 100 distinct pool entries of up to 512 KiB each, after which the complete response is serialized and synchronously compressed.
There was a problem hiding this comment.
Agreed this is the stronger bound. The filed finding is the repeated-hash amplification, which the de-duplication fully fixes; the count cap additionally bounds a distinct-hash request to 100. A responder-side serialized-byte / work ceiling before the synchronous Snappy compression is the broader hardening (the finding lists it as a separate suggestion) and belongs in the shared reqresp compression path rather than this handler — tracking it as a follow-up so this fix stays focused.
|
|
||
| describe('reqRespTxHandler', () => { | ||
| const peerId = {} as any; | ||
| const makeMempools = (getTxByHash: (h: TxHash) => Promise<unknown>) => ({ txPool: { getTxByHash } }) as any; |
There was a problem hiding this comment.
The new peerId fixture uses as any, and makeMempools repeats the same pattern on the following line. This violates the repository directive to never use as any and hides whether these fixtures satisfy the handler contract. This repository requirement must be satisfied before merging; use typed fixtures or satisfies with the minimal required interfaces.
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.
Fixed in 46d216c — replaced the as-any fixtures with mock() and mockDeep(), matching the block_txs handler test.
Replace the `as any` peerId/mempools fixtures with mock<PeerId>() and mockDeep<MemPools>(), matching the block_txs handler test style. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Problem
reqRespTxHandlerlooked up and serialized every entry in a tx-fetch request. A legal-size request (~65 KB) fits ~2,047 copies of one 32-byte tx hash, so a peer can repeat a single hash and make the node read and serialize the same (up to 512 KiB) tx once per copy — building ~1 GiB of response and blocking the event loop on synchronous compression. The per-peer rate limit counts requests, not entries, so it doesn't bound this.Fix
Mapkeyed by hash string), mirroring the sibling BLOCK_TXS handler'sSet. A repeated hash is now served once.MAX_TX_HASHES_PER_REQUEST = 100); honest requesters chunk at 8 (chunkTxHashesRequest/ batch-requester default), so this only rejects abusive over-batches, withBADLY_FORMED_REQUEST.Test
Adds
reqRespTxHandlertests: a repeated hash causes exactly one pool read; a normal distinct batch still serves each; an over-cap request is rejected before any pool read. Verified red→green against the built base (@aztec-labs/p2pjest 9/9 green; the dedup + cap tests fail against the original handler),prettier --checkclean,tsgo -b --emitDeclarationOnlyexit 0.Addresses the LabsBox audit findings "[aztec-node] TX req/resp serves repeated hashes repeatedly" and its near-duplicate ("repeats one transaction hash into a 1 GiB response"). A responder-side output ceiling before compression (also suggested in the finding) is left as separate hardening.
🤖 Generated with Claude Code