From f4800a9ab9ac0f73751140528255f01a8617af5b Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Fri, 6 Feb 2026 13:09:48 -0300 Subject: [PATCH 01/23] refactor missing txs collection --- .../batch-tx-requester/batch_tx_requester.ts | 9 +-- .../reqresp/batch-tx-requester/interface.ts | 6 +- .../reqresp/batch-tx-requester/missing_txs.ts | 75 +++++-------------- 3 files changed, 25 insertions(+), 65 deletions(-) diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.ts index 2a2f8ca60cc7..3d38c82d6c04 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.ts @@ -20,7 +20,7 @@ import { DEFAULT_BATCH_TX_REQUESTER_TX_BATCH_SIZE, } from './config.js'; import type { BatchTxRequesterLibP2PService, BatchTxRequesterOptions, ITxMetadataCollection } from './interface.js'; -import { MissingTxMetadata, MissingTxMetadataCollection } from './missing_txs.js'; +import { MissingTxMetadataCollection } from './missing_txs.js'; import { type IPeerCollection, PeerCollection } from './peer_collection.js'; import { BatchRequestTxValidator, type IBatchRequestTxValidator } from './tx_validator.js'; @@ -99,8 +99,7 @@ export class BatchTxRequester { this.p2pService.peerScoring, ); } - const entries: Array<[string, MissingTxMetadata]> = missingTxs.map(h => [h.toString(), new MissingTxMetadata(h)]); - this.txsMetadata = new MissingTxMetadataCollection(entries, this.txBatchSize); + this.txsMetadata = new MissingTxMetadataCollection(new Set(missingTxs.map(e => e.toString())), this.txBatchSize); this.smartRequesterSemaphore = this.opts.semaphore ?? new Semaphore(0); } @@ -661,7 +660,7 @@ export class BatchTxRequester { /* * @returns true if all missing txs have been fetched */ private fetchedAllTxs() { - return Array.from(this.txsMetadata.values()).every(tx => tx.fetched); + return this.txsMetadata.getMissingTxHashes().size == 0; } /* @@ -679,7 +678,7 @@ export class BatchTxRequester { this.unlockSmartRequesterSemaphores(); } - return aborted || this.txsMetadata.size === 0 || this.fetchedAllTxs() || this.dateProvider.now() > this.deadline; + return aborted || this.fetchedAllTxs() || this.dateProvider.now() > this.deadline; } /* diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts index 811c77dff630..f3e895b2d7a9 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts @@ -6,7 +6,6 @@ import type { PeerId } from '@libp2p/interface'; import type { ConnectionSampler } from '../connection-sampler/connection_sampler.js'; import type { ReqRespInterface } from '../interface.js'; -import type { MissingTxMetadata } from './missing_txs.js'; import type { IPeerCollection } from './peer_collection.js'; import type { BatchRequestTxValidatorConfig, IBatchRequestTxValidator } from './tx_validator.js'; @@ -15,18 +14,15 @@ export interface IPeerPenalizer { } export interface ITxMetadataCollection { - size: number; - values(): IterableIterator; getMissingTxHashes(): Set; + markFetched(peerId: PeerId, tx: Tx): boolean; getTxsToRequestFromThePeer(peer: PeerId): TxHash[]; markRequested(txHash: TxHash): void; markInFlightBySmartPeer(txHash: TxHash): void; markNotInFlightBySmartPeer(txHash: TxHash): void; alreadyFetched(txHash: TxHash): boolean; // Returns true if tx was marked as fetched, false if it was already marked as fetched - markFetched(peerId: PeerId, tx: Tx): boolean; markPeerHas(peerId: PeerId, txHashes: TxHash[]): void; - getFetchedTxs(): Tx[]; } /** diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts index 6ef3c2b3c722..fa9f05e7935c 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts @@ -5,9 +5,9 @@ import type { PeerId } from '@libp2p/interface'; import { DEFAULT_BATCH_TX_REQUESTER_TX_BATCH_SIZE } from './config.js'; import type { ITxMetadataCollection } from './interface.js'; -export class MissingTxMetadata { +class MissingTxMetadata { constructor( - public readonly txHash: TxHash, + public readonly txHash: string, public fetched = false, public requestedCount = 0, public inFlightCount = 0, @@ -44,10 +44,6 @@ export class MissingTxMetadata { return true; } - - public toString() { - return this.txHash.toString(); - } } /* @@ -55,21 +51,18 @@ export class MissingTxMetadata { * This could be better optimized but given expected count of missing txs (N < 100) * At the moment there is no need for it. And benefit is that we have everything in single store * */ -export class MissingTxMetadataCollection extends Map implements ITxMetadataCollection { +export class MissingTxMetadataCollection implements ITxMetadataCollection { + private txMetadata = new Map(); + constructor( - entries?: readonly (readonly [string, MissingTxMetadata])[] | null, + private missingTxHashes: Set, private readonly txBatchSize: number = DEFAULT_BATCH_TX_REQUESTER_TX_BATCH_SIZE, ) { - super(entries); - } - public getSortedByRequestedCountAsc(txs: string[]): MissingTxMetadata[] { - return Array.from(this.values().filter(txMeta => txs.includes(txMeta.txHash.toString()))).sort( - (a, b) => a.requestedCount - b.requestedCount, - ); + missingTxHashes.forEach(hash => this.txMetadata.set(hash, new MissingTxMetadata(hash))); } public getPrioritizingNotInFlightAndLowerRequestCount(txs: string[]): MissingTxMetadata[] { - const filtered = Array.from(this.values()).filter(txMeta => txs.includes(txMeta.txHash.toString())); + const filtered = Array.from(this.txMetadata.values()).filter(txMeta => txs.includes(txMeta.txHash.toString())); const [notInFlight, inFlight] = filtered.reduce<[MissingTxMetadata[], MissingTxMetadata[]]>( (buckets, tx) => { @@ -85,43 +78,15 @@ export class MissingTxMetadataCollection extends Map return [...notInFlight, ...inFlight]; } - public getFetchedTxHashes(): Set { - return new Set( - this.values() - .filter(t => t.fetched) - .map(t => t.txHash.toString()), - ); - } - public getMissingTxHashes(): Set { - return new Set( - this.values() - .filter(t => !t.fetched) - .map(t => t.txHash.toString()), - ); - } - - public getInFlightTxHashes(): Set { - return new Set( - this.values() - .filter(t => t.isInFlight()) - .map(t => t.txHash.toString()), - ); - } - - public getFetchedTxs(): Tx[] { - return Array.from( - this.values() - .map(t => t.tx) - .filter(t => !!t), - ); + return new Set(this.missingTxHashes.values().filter(hash => this.txMetadata.has(hash))); } public getTxsPeerHas(peer: PeerId): Set { const peerIdStr = peer.toString(); const txsPeerHas = new Set(); - this.values().forEach(txMeta => { + this.txMetadata.values().forEach(txMeta => { if (txMeta.peers.has(peerIdStr)) { txsPeerHas.add(txMeta.txHash.toString()); } @@ -132,13 +97,13 @@ export class MissingTxMetadataCollection extends Map public getTxsToRequestFromThePeer(peer: PeerId): TxHash[] { const txsPeerHas = this.getTxsPeerHas(peer); - const fetchedTxs = this.getFetchedTxHashes(); + const missingTxHashes = this.getMissingTxHashes(); - const txsToRequest = txsPeerHas.difference(fetchedTxs); + const txsToRequest = txsPeerHas.intersection(missingTxHashes); if (txsToRequest.size >= this.txBatchSize) { return this.getPrioritizingNotInFlightAndLowerRequestCount(Array.from(txsToRequest)) - .map(t => t.txHash) + .map(t => TxHash.fromString(t.txHash)) .slice(0, this.txBatchSize); } @@ -150,13 +115,13 @@ export class MissingTxMetadataCollection extends Map Array.from(this.getMissingTxHashes().difference(txsToRequest)), ) .slice(0, countToFill) - .map(t => t.txHash); + .map(t => TxHash.fromString(t.txHash)); return [...Array.from(txsToRequest).map(t => TxHash.fromString(t)), ...txsToFill]; } public markRequested(txHash: TxHash) { - this.get(txHash.toString())?.markAsRequested(); + this.txMetadata.get(txHash.toString())?.markAsRequested(); } /* @@ -165,7 +130,7 @@ export class MissingTxMetadataCollection extends Map * "dumb" peer might return it, or might not - we don't know * */ public markInFlightBySmartPeer(txHash: TxHash) { - this.get(txHash.toString())?.markInFlight(); + this.txMetadata.get(txHash.toString())?.markInFlight(); } /* @@ -173,16 +138,16 @@ export class MissingTxMetadataCollection extends Map * Because the smart peer should return this tx, whereas * "dumb" peer might return it, or might not - we don't know*/ public markNotInFlightBySmartPeer(txHash: TxHash) { - this.get(txHash.toString())?.markNotInFlight(); + this.txMetadata.get(txHash.toString())?.markNotInFlight(); } public alreadyFetched(txHash: TxHash): boolean { - return this.get(txHash.toString())?.fetched ?? false; + return this.txMetadata.get(txHash.toString())?.fetched ?? false; } public markFetched(peerId: PeerId, tx: Tx): boolean { const txHashStr = tx.txHash.toString(); - const txMeta = this.get(txHashStr); + const txMeta = this.txMetadata.get(txHashStr); if (!txMeta) { //TODO: what to do about peer which sent txs we didn't request? // 1. don't request from it in the scope of this batch request @@ -200,7 +165,7 @@ export class MissingTxMetadataCollection extends Map txHash .map(t => t.toString()) .forEach(txh => { - const txMeta = this.get(txh); + const txMeta = this.txMetadata.get(txh); if (txMeta) { txMeta.peers.add(peerIdStr); } From bd47ea015c3acd5a6bccfd66bceadbe46af5f697 Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Fri, 6 Feb 2026 14:20:02 -0300 Subject: [PATCH 02/23] missing txs tracker --- .../p2p_client.integration_batch_txs.test.ts | 3 +- .../batch_tx_requester.test.ts | 47 ++++++++++--------- .../batch-tx-requester/batch_tx_requester.ts | 11 +++-- .../reqresp/batch-tx-requester/interface.ts | 5 ++ .../reqresp/batch-tx-requester/missing_txs.ts | 25 ++++++++-- .../tx_collection/proposal_tx_collector.ts | 3 +- 6 files changed, 62 insertions(+), 32 deletions(-) diff --git a/yarn-project/p2p/src/client/test/p2p_client.integration_batch_txs.test.ts b/yarn-project/p2p/src/client/test/p2p_client.integration_batch_txs.test.ts index a8e926c6c956..0369c2941f43 100644 --- a/yarn-project/p2p/src/client/test/p2p_client.integration_batch_txs.test.ts +++ b/yarn-project/p2p/src/client/test/p2p_client.integration_batch_txs.test.ts @@ -20,6 +20,7 @@ import type { AttestationPool } from '../../mem_pools/attestation_pool/attestati import type { TxPool } from '../../mem_pools/tx_pool/index.js'; import { BatchTxRequester } from '../../services/reqresp/batch-tx-requester/batch_tx_requester.js'; import type { BatchTxRequesterLibP2PService } from '../../services/reqresp/batch-tx-requester/interface.js'; +import { MissingTxsTracker } from '../../services/reqresp/batch-tx-requester/missing_txs.js'; import type { IBatchRequestTxValidator } from '../../services/reqresp/batch-tx-requester/tx_validator.js'; import type { ConnectionSampler } from '../../services/reqresp/connection-sampler/connection_sampler.js'; import { generatePeerIdPrivateKeys } from '../../test-helpers/generate-peer-id-private-keys.js'; @@ -228,7 +229,7 @@ describe('p2p client integration batch txs', () => { mockP2PService.reqResp = (client0 as any).p2pService.reqresp; const requester = new BatchTxRequester( - missingTxHashes, + new MissingTxsTracker(missingTxHashes), blockProposal, undefined, // no pinned peer 5_000, diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.test.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.test.ts index 8e09dd76bacb..daf27b32c991 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.test.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.test.ts @@ -24,6 +24,7 @@ import { ReqRespStatus } from '../status.js'; import { BatchTxRequester } from './batch_tx_requester.js'; import { DEFAULT_BATCH_TX_REQUESTER_BAD_PEER_THRESHOLD, DEFAULT_BATCH_TX_REQUESTER_TX_BATCH_SIZE } from './config.js'; import type { BatchTxRequesterLibP2PService, IPeerPenalizer } from './interface.js'; +import { MissingTxsTracker } from './missing_txs.js'; import { type IPeerCollection, PeerCollection, RATE_LIMIT_EXCEEDED_PEER_CACHE_TTL } from './peer_collection.js'; import type { IBatchRequestTxValidator } from './tx_validator.js'; @@ -95,7 +96,7 @@ describe('BatchTxRequester', () => { const clock = new TestClock(); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, undefined, deadline, @@ -150,7 +151,7 @@ describe('BatchTxRequester', () => { const clock = new TestClock(); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, undefined, deadline, @@ -276,7 +277,7 @@ describe('BatchTxRequester', () => { }); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, undefined, deadline, @@ -330,7 +331,7 @@ describe('BatchTxRequester', () => { const semaphore = new TestSemaphore(new Semaphore(0)); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, undefined, deadline, @@ -382,7 +383,7 @@ describe('BatchTxRequester', () => { const semaphore = new TestSemaphore(new Semaphore(0)); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, pinnedPeer, deadline, @@ -449,7 +450,7 @@ describe('BatchTxRequester', () => { const semaphore = new TestSemaphore(new Semaphore(0)); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, pinnedPeer, deadline, @@ -505,7 +506,7 @@ describe('BatchTxRequester', () => { const semaphore = new TestSemaphore(new Semaphore(0)); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, pinnedPeer, deadline, @@ -561,7 +562,7 @@ describe('BatchTxRequester', () => { ); reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, pinnedPeer, deadline, @@ -635,7 +636,7 @@ describe('BatchTxRequester', () => { }); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, pinnedPeer, deadline, @@ -749,7 +750,7 @@ describe('BatchTxRequester', () => { }); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, pinnedPeer, deadline, @@ -885,7 +886,7 @@ describe('BatchTxRequester', () => { }); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, pinnedPeer, deadline, @@ -960,7 +961,7 @@ describe('BatchTxRequester', () => { const clock = new TestClock(); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, undefined, shortDeadline, @@ -1018,7 +1019,7 @@ describe('BatchTxRequester', () => { }); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, undefined, deadline, @@ -1090,7 +1091,7 @@ describe('BatchTxRequester', () => { }); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, undefined, deadline, @@ -1143,7 +1144,7 @@ describe('BatchTxRequester', () => { // Create semaphore that starts with 0 permits to block smart workers const semaphore = new TestSemaphore(new Semaphore(0)); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, undefined, deadline, @@ -1217,7 +1218,7 @@ describe('BatchTxRequester', () => { reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, undefined, deadline, @@ -1298,7 +1299,7 @@ describe('BatchTxRequester', () => { reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, undefined, deadline, @@ -1369,7 +1370,7 @@ describe('BatchTxRequester', () => { reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, undefined, deadline, @@ -1430,7 +1431,7 @@ describe('BatchTxRequester', () => { reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, pinnedPeer, deadline, @@ -1480,7 +1481,7 @@ describe('BatchTxRequester', () => { reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, pinnedPeer, deadline, @@ -1561,7 +1562,7 @@ describe('BatchTxRequester', () => { }); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, pinnedPeer, deadline, @@ -1615,7 +1616,7 @@ describe('BatchTxRequester', () => { reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, pinnedPeer, deadline, @@ -1671,7 +1672,7 @@ describe('BatchTxRequester', () => { reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - missing, + new MissingTxsTracker(missing), blockProposal, pinnedPeer, deadline, diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.ts index 3d38c82d6c04..f2616091801d 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.ts @@ -19,7 +19,12 @@ import { DEFAULT_BATCH_TX_REQUESTER_SMART_PARALLEL_WORKER_COUNT, DEFAULT_BATCH_TX_REQUESTER_TX_BATCH_SIZE, } from './config.js'; -import type { BatchTxRequesterLibP2PService, BatchTxRequesterOptions, ITxMetadataCollection } from './interface.js'; +import type { + BatchTxRequesterLibP2PService, + BatchTxRequesterOptions, + IMissingTxsTracker, + ITxMetadataCollection, +} from './interface.js'; import { MissingTxMetadataCollection } from './missing_txs.js'; import { type IPeerCollection, PeerCollection } from './peer_collection.js'; import { BatchRequestTxValidator, type IBatchRequestTxValidator } from './tx_validator.js'; @@ -60,7 +65,7 @@ export class BatchTxRequester { private readonly txBatchSize: number; constructor( - missingTxs: TxHash[], + missingTxsTracker: IMissingTxsTracker, blockTxsSource: BlockTxsSource, pinnedPeer: PeerId | undefined, timeoutMs: number, @@ -99,7 +104,7 @@ export class BatchTxRequester { this.p2pService.peerScoring, ); } - this.txsMetadata = new MissingTxMetadataCollection(new Set(missingTxs.map(e => e.toString())), this.txBatchSize); + this.txsMetadata = new MissingTxMetadataCollection(missingTxsTracker, this.txBatchSize); this.smartRequesterSemaphore = this.opts.semaphore ?? new Semaphore(0); } diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts index f3e895b2d7a9..b0ae08e9c111 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts @@ -13,6 +13,11 @@ export interface IPeerPenalizer { penalizePeer(peerId: PeerId, penalty: PeerErrorSeverity): void; } +export interface IMissingTxsTracker { + getMissingTxHashes(): Set; + markFetched(tx: Tx): boolean; +} + export interface ITxMetadataCollection { getMissingTxHashes(): Set; markFetched(peerId: PeerId, tx: Tx): boolean; diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts index fa9f05e7935c..3122dd2dbef2 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts @@ -3,7 +3,23 @@ import { type Tx, TxHash } from '@aztec/stdlib/tx'; import type { PeerId } from '@libp2p/interface'; import { DEFAULT_BATCH_TX_REQUESTER_TX_BATCH_SIZE } from './config.js'; -import type { ITxMetadataCollection } from './interface.js'; +import type { IMissingTxsTracker, ITxMetadataCollection } from './interface.js'; + +export class MissingTxsTracker implements IMissingTxsTracker { + private readonly pendingTxs: Set; + + constructor(missingTxHashes: TxHash[]) { + this.pendingTxs = new Set(missingTxHashes.map(hash => hash.toString())); + } + + getMissingTxHashes(): Set { + return this.pendingTxs; + } + + markFetched(tx: Tx): boolean { + return this.pendingTxs.delete(tx.txHash.toString()); + } +} class MissingTxMetadata { constructor( @@ -55,10 +71,10 @@ export class MissingTxMetadataCollection implements ITxMetadataCollection { private txMetadata = new Map(); constructor( - private missingTxHashes: Set, + private missingTxsTracker: IMissingTxsTracker, private readonly txBatchSize: number = DEFAULT_BATCH_TX_REQUESTER_TX_BATCH_SIZE, ) { - missingTxHashes.forEach(hash => this.txMetadata.set(hash, new MissingTxMetadata(hash))); + missingTxsTracker.getMissingTxHashes().forEach(hash => this.txMetadata.set(hash, new MissingTxMetadata(hash))); } public getPrioritizingNotInFlightAndLowerRequestCount(txs: string[]): MissingTxMetadata[] { @@ -79,7 +95,7 @@ export class MissingTxMetadataCollection implements ITxMetadataCollection { } public getMissingTxHashes(): Set { - return new Set(this.missingTxHashes.values().filter(hash => this.txMetadata.has(hash))); + return this.missingTxsTracker.getMissingTxHashes(); } public getTxsPeerHas(peer: PeerId): Set { @@ -157,6 +173,7 @@ export class MissingTxMetadataCollection implements ITxMetadataCollection { return false; } + this.missingTxsTracker.markFetched(tx); return txMeta.markAsFetched(peerId, tx); } diff --git a/yarn-project/p2p/src/services/tx_collection/proposal_tx_collector.ts b/yarn-project/p2p/src/services/tx_collection/proposal_tx_collector.ts index bf777a37fdb8..27ba87d9dfc9 100644 --- a/yarn-project/p2p/src/services/tx_collection/proposal_tx_collector.ts +++ b/yarn-project/p2p/src/services/tx_collection/proposal_tx_collector.ts @@ -7,6 +7,7 @@ import type { PeerId } from '@libp2p/interface'; import { BatchTxRequester } from '../reqresp/batch-tx-requester/batch_tx_requester.js'; import type { BatchTxRequesterConfig } from '../reqresp/batch-tx-requester/config.js'; import type { BatchTxRequesterLibP2PService } from '../reqresp/batch-tx-requester/interface.js'; +import { MissingTxsTracker } from '../reqresp/batch-tx-requester/missing_txs.js'; import type { IBatchRequestTxValidator } from '../reqresp/batch-tx-requester/tx_validator.js'; import { type BlockTxsSource, ReqRespSubProtocol, chunkTxHashesRequest } from '../reqresp/index.js'; @@ -58,7 +59,7 @@ export class BatchTxRequesterCollector implements MissingTxsCollector { } = this.batchTxRequesterConfig ?? {}; const batchRequester = new BatchTxRequester( - txHashes, + new MissingTxsTracker(txHashes), blockTxsSource, pinnedPeer, timeoutMs, From 07a27f0a46f0286375b5f5f653e879895f70521c Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Fri, 6 Feb 2026 14:31:02 -0300 Subject: [PATCH 03/23] add test --- .../batch_tx_requester.test.ts | 56 +++++++++++++++++++ .../reqresp/batch-tx-requester/interface.ts | 7 +++ 2 files changed, 63 insertions(+) diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.test.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.test.ts index daf27b32c991..5dd518ae5744 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.test.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.test.ts @@ -1402,6 +1402,62 @@ describe('BatchTxRequester', () => { }); }); + describe('External fetching', () => { + it('should not request transactions that were marked as fetched externally', async () => { + const txCount = 16; + const deadline = 5_000; + const missing = Array.from({ length: txCount }, () => TxHash.random()); + + blockProposal = await makeBlockProposal({ + signer: Secp256k1Signer.random(), + blockHeader: makeBlockHeader(1, { blockNumber: BlockNumber(1) }), + archiveRoot: Fr.random(), + txHashes: missing, + }); + + const peer = await createSecp256k1PeerId(); + connectionSampler.getPeerListSortedByConnectionCountAsc.mockReturnValue([peer]); + + const tracker = new MissingTxsTracker(missing); + + // Peer has only first half of transactions + const peerTransactions = new Map([[peer.toString(), Array.from({ length: TX_BATCH_SIZE }, (_, i) => i)]]); + const { requestLog, mockImplementation } = createRequestLogger(blockProposal, new Set(), peerTransactions); + reqResp.sendRequestToPeer.mockImplementation(mockImplementation); + + // Create requester first + const requester = new BatchTxRequester( + tracker, + blockProposal, + undefined, + deadline, + mockP2PService, + logger, + new DateProvider(), + { + smartParallelWorkerCount: 0, + dumbParallelWorkerCount: 1, + txValidator, + }, + ); + + // Mark transactions 8-15 as fetched externally after creating requester + for (let i = TX_BATCH_SIZE; i < txCount; i++) { + tracker.markFetched(makeTx(missing[i])); + } + + // Run and collect results + const result = await BatchTxRequester.collectAllTxs(requester.run()); + + // Verify only transactions 0-7 were requested (indices 8-15 were marked fetched) + const allRequestedIndices = requestLog.get(peer.toString())?.flatMap(r => r.indices) || []; + const requestedExternallyFetched = allRequestedIndices.filter(idx => idx >= TX_BATCH_SIZE); + + expect(requestedExternallyFetched).toEqual([]); + expect(result.length).toBe(TX_BATCH_SIZE); + }); + }); + describe('Pinned peer functionality', () => { it('Should query pinned peer if available', async () => { const txCount = 10; diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts index b0ae08e9c111..f53192faf2ad 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts @@ -13,8 +13,15 @@ export interface IPeerPenalizer { penalizePeer(peerId: PeerId, penalty: PeerErrorSeverity): void; } +/** + * Tracks which transactions are still missing and need to be fetched. + * Allows external code to mark transactions as fetched, enabling coordination + * between multiple fetching mechanisms (e.g., BatchTxRequester and Rpc Node requests). + */ export interface IMissingTxsTracker { + /** Returns the set of transaction hashes that are still missing. */ getMissingTxHashes(): Set; + /** Marks a transaction as fetched. Returns true if it was previously missing. */ markFetched(tx: Tx): boolean; } From e8953636137c53e1059a226a278a7e6301cf1c22 Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Mon, 9 Feb 2026 12:13:28 -0300 Subject: [PATCH 04/23] track txs between requests --- .../p2p_client.integration_batch_txs.test.ts | 2 +- .../proposal_tx_collector_worker.ts | 19 +++++- .../batch_tx_requester.test.ts | 48 ++++++------- .../reqresp/batch-tx-requester/interface.ts | 6 +- .../reqresp/batch-tx-requester/missing_txs.ts | 18 ++--- .../tx_collection/fast_tx_collection.ts | 67 +++++++++++-------- .../tx_collection/proposal_tx_collector.ts | 17 +++-- .../tx_collection/slow_tx_collection.ts | 13 ++-- .../services/tx_collection/tx_collection.ts | 4 +- .../tx_collection/tx_collection_sink.ts | 4 +- .../testbench/p2p_client_testbench_worker.ts | 29 +++++--- 11 files changed, 131 insertions(+), 96 deletions(-) diff --git a/yarn-project/p2p/src/client/test/p2p_client.integration_batch_txs.test.ts b/yarn-project/p2p/src/client/test/p2p_client.integration_batch_txs.test.ts index 0369c2941f43..98b51c9c670e 100644 --- a/yarn-project/p2p/src/client/test/p2p_client.integration_batch_txs.test.ts +++ b/yarn-project/p2p/src/client/test/p2p_client.integration_batch_txs.test.ts @@ -229,7 +229,7 @@ describe('p2p client integration batch txs', () => { mockP2PService.reqResp = (client0 as any).p2pService.reqresp; const requester = new BatchTxRequester( - new MissingTxsTracker(missingTxHashes), + new MissingTxsTracker(new Set(missingTxHashes.map(TxHash.toString))), blockProposal, undefined, // no pinned peer 5_000, diff --git a/yarn-project/p2p/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts b/yarn-project/p2p/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts index fa270e79e6dd..9998dc2da04c 100644 --- a/yarn-project/p2p/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts +++ b/yarn-project/p2p/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts @@ -9,13 +9,14 @@ import type { L2BlockSource } from '@aztec/stdlib/block'; import type { ContractDataSource } from '@aztec/stdlib/contract'; import type { ClientProtocolCircuitVerifier } from '@aztec/stdlib/interfaces/server'; import { P2PClientType, PeerErrorSeverity } from '@aztec/stdlib/p2p'; -import type { Tx, TxValidationResult } from '@aztec/stdlib/tx'; +import { type Tx, TxHash, type TxValidationResult } from '@aztec/stdlib/tx'; import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client'; import type { PeerId } from '@libp2p/interface'; import { peerIdFromString } from '@libp2p/peer-id'; import type { P2PConfig } from '../../../config.js'; +import { MissingTxsTracker } from '../../../services/reqresp/batch-tx-requester/missing_txs.js'; import type { IBatchRequestTxValidator } from '../../../services/reqresp/batch-tx-requester/tx_validator.js'; import { RateLimitStatus } from '../../../services/reqresp/rate-limiter/rate_limiter.js'; import { @@ -214,7 +215,13 @@ async function runCollector(cmd: Extract collector.collectTxs(parsedTxHashes, parsedProposal, pinnedPeer, internalTimeoutMs), + (_signal: AbortSignal) => + collector.collectTxs( + new MissingTxsTracker(new Set(parsedTxHashes.map(TxHash.toString))), + parsedProposal, + pinnedPeer, + internalTimeoutMs, + ), timeoutMs, () => new Error(`Collector timed out after ${timeoutMs}ms`), ); @@ -226,7 +233,13 @@ async function runCollector(cmd: Extract collector.collectTxs(parsedTxHashes, parsedProposal, pinnedPeer, internalTimeoutMs), + (_signal: AbortSignal) => + collector.collectTxs( + new MissingTxsTracker(new Set(parsedTxHashes.map(TxHash.toString))), + parsedProposal, + pinnedPeer, + internalTimeoutMs, + ), timeoutMs, () => new Error(`Collector timed out after ${timeoutMs}ms`), ); diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.test.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.test.ts index 5dd518ae5744..26b399799a59 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.test.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.test.ts @@ -96,7 +96,7 @@ describe('BatchTxRequester', () => { const clock = new TestClock(); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, undefined, deadline, @@ -151,7 +151,7 @@ describe('BatchTxRequester', () => { const clock = new TestClock(); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, undefined, deadline, @@ -277,7 +277,7 @@ describe('BatchTxRequester', () => { }); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, undefined, deadline, @@ -331,7 +331,7 @@ describe('BatchTxRequester', () => { const semaphore = new TestSemaphore(new Semaphore(0)); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, undefined, deadline, @@ -383,7 +383,7 @@ describe('BatchTxRequester', () => { const semaphore = new TestSemaphore(new Semaphore(0)); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, pinnedPeer, deadline, @@ -450,7 +450,7 @@ describe('BatchTxRequester', () => { const semaphore = new TestSemaphore(new Semaphore(0)); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, pinnedPeer, deadline, @@ -506,7 +506,7 @@ describe('BatchTxRequester', () => { const semaphore = new TestSemaphore(new Semaphore(0)); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, pinnedPeer, deadline, @@ -562,7 +562,7 @@ describe('BatchTxRequester', () => { ); reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, pinnedPeer, deadline, @@ -636,7 +636,7 @@ describe('BatchTxRequester', () => { }); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, pinnedPeer, deadline, @@ -750,7 +750,7 @@ describe('BatchTxRequester', () => { }); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, pinnedPeer, deadline, @@ -886,7 +886,7 @@ describe('BatchTxRequester', () => { }); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, pinnedPeer, deadline, @@ -961,7 +961,7 @@ describe('BatchTxRequester', () => { const clock = new TestClock(); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, undefined, shortDeadline, @@ -1019,7 +1019,7 @@ describe('BatchTxRequester', () => { }); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, undefined, deadline, @@ -1091,7 +1091,7 @@ describe('BatchTxRequester', () => { }); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, undefined, deadline, @@ -1144,7 +1144,7 @@ describe('BatchTxRequester', () => { // Create semaphore that starts with 0 permits to block smart workers const semaphore = new TestSemaphore(new Semaphore(0)); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, undefined, deadline, @@ -1218,7 +1218,7 @@ describe('BatchTxRequester', () => { reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, undefined, deadline, @@ -1299,7 +1299,7 @@ describe('BatchTxRequester', () => { reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, undefined, deadline, @@ -1370,7 +1370,7 @@ describe('BatchTxRequester', () => { reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, undefined, deadline, @@ -1418,7 +1418,7 @@ describe('BatchTxRequester', () => { const peer = await createSecp256k1PeerId(); connectionSampler.getPeerListSortedByConnectionCountAsc.mockReturnValue([peer]); - const tracker = new MissingTxsTracker(missing); + const tracker = new MissingTxsTracker(new Set(missing.map(TxHash.toString))); // Peer has only first half of transactions const peerTransactions = new Map([[peer.toString(), Array.from({ length: TX_BATCH_SIZE }, (_, i) => i)]]); @@ -1487,7 +1487,7 @@ describe('BatchTxRequester', () => { reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, pinnedPeer, deadline, @@ -1537,7 +1537,7 @@ describe('BatchTxRequester', () => { reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, pinnedPeer, deadline, @@ -1618,7 +1618,7 @@ describe('BatchTxRequester', () => { }); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, pinnedPeer, deadline, @@ -1672,7 +1672,7 @@ describe('BatchTxRequester', () => { reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, pinnedPeer, deadline, @@ -1728,7 +1728,7 @@ describe('BatchTxRequester', () => { reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - new MissingTxsTracker(missing), + new MissingTxsTracker(new Set(missing.map(TxHash.toString))), blockProposal, pinnedPeer, deadline, diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts index f53192faf2ad..6f47db2ccb67 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts @@ -20,7 +20,11 @@ export interface IPeerPenalizer { */ export interface IMissingTxsTracker { /** Returns the set of transaction hashes that are still missing. */ - getMissingTxHashes(): Set; + get missingTxHashes(): Set; + /** Size of this.missingTxHashes */ + get numberOfMissingTxs(): number; + /** Checks that transaction is still missing */ + isMissing(txHash: string): boolean; /** Marks a transaction as fetched. Returns true if it was previously missing. */ markFetched(tx: Tx): boolean; } diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts index 3122dd2dbef2..b341dbab90db 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts @@ -6,18 +6,18 @@ import { DEFAULT_BATCH_TX_REQUESTER_TX_BATCH_SIZE } from './config.js'; import type { IMissingTxsTracker, ITxMetadataCollection } from './interface.js'; export class MissingTxsTracker implements IMissingTxsTracker { - private readonly pendingTxs: Set; + constructor(public readonly missingTxHashes: Set) {} - constructor(missingTxHashes: TxHash[]) { - this.pendingTxs = new Set(missingTxHashes.map(hash => hash.toString())); + markFetched(tx: Tx): boolean { + return this.missingTxHashes.delete(tx.txHash.toString()); } - getMissingTxHashes(): Set { - return this.pendingTxs; + get numberOfMissingTxs(): number { + return this.missingTxHashes.size; } - markFetched(tx: Tx): boolean { - return this.pendingTxs.delete(tx.txHash.toString()); + isMissing(txHash: string): boolean { + return this.missingTxHashes.has(txHash.toString()); } } @@ -74,7 +74,7 @@ export class MissingTxMetadataCollection implements ITxMetadataCollection { private missingTxsTracker: IMissingTxsTracker, private readonly txBatchSize: number = DEFAULT_BATCH_TX_REQUESTER_TX_BATCH_SIZE, ) { - missingTxsTracker.getMissingTxHashes().forEach(hash => this.txMetadata.set(hash, new MissingTxMetadata(hash))); + missingTxsTracker.missingTxHashes.forEach(hash => this.txMetadata.set(hash, new MissingTxMetadata(hash))); } public getPrioritizingNotInFlightAndLowerRequestCount(txs: string[]): MissingTxMetadata[] { @@ -95,7 +95,7 @@ export class MissingTxMetadataCollection implements ITxMetadataCollection { } public getMissingTxHashes(): Set { - return this.missingTxsTracker.getMissingTxHashes(); + return this.missingTxsTracker.missingTxHashes; } public getTxsPeerHas(peer: PeerId): Set { diff --git a/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts index bbb6c2c54b97..3d88ea1d76f9 100644 --- a/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts @@ -13,6 +13,7 @@ import type { PeerId } from '@libp2p/interface'; import type { BatchTxRequesterConfig } from '../reqresp/batch-tx-requester/config.js'; import type { BatchTxRequesterLibP2PService } from '../reqresp/batch-tx-requester/interface.js'; +import { MissingTxsTracker } from '../reqresp/batch-tx-requester/missing_txs.js'; import type { TxCollectionConfig } from './config.js'; import { BatchTxRequesterCollector, @@ -84,7 +85,7 @@ export class FastTxCollection { blockInfo, promise, foundTxs: new Map(), - missingTxHashes: new Set(txHashes.map(t => t.toString())), + missingTxTracker: new MissingTxsTracker(new Set(txHashes.map(t => t.toString()))), deadline: opts.deadline, }; @@ -97,7 +98,7 @@ export class FastTxCollection { ...blockInfo, duration, requestType: input.type, - missingTxs: [...request.missingTxHashes], + missingTxs: [...request.missingTxTracker.missingTxHashes], }, ); return [...request.foundTxs.values()]; @@ -111,7 +112,7 @@ export class FastTxCollection { const { blockInfo } = request; this.log.debug( - `Starting fast collection of ${request.missingTxHashes.size} txs for ${request.type} at slot ${blockInfo.slotNumber}`, + `Starting fast collection of ${request.missingTxTracker.numberOfMissingTxs} txs for ${request.type} at slot ${blockInfo.slotNumber}`, { ...blockInfo, requestType: request.type, deadline: opts.deadline }, ); @@ -124,7 +125,7 @@ export class FastTxCollection { await Promise.race([request.promise.promise, waitBeforeReqResp]); // If we have collected all txs, we can stop here - if (request.missingTxHashes.size === 0) { + if (request.missingTxTracker.numberOfMissingTxs === 0) { this.log.debug(`All txs collected for slot ${blockInfo.slotNumber} without reqresp`, blockInfo); return; } @@ -138,7 +139,7 @@ export class FastTxCollection { const logCtx = { ...blockInfo, errorMessage: err instanceof Error ? err.message : undefined, - missingTxs: [...request.missingTxHashes].map(txHash => txHash.toString()), + missingTxs: [...request.missingTxTracker.missingTxHashes].map(txHash => txHash.toString()), }; if (err instanceof Error && err.name === 'TimeoutError') { this.log.warn(`Timed out collecting txs for ${request.type} at slot ${blockInfo.slotNumber}`, logCtx); @@ -166,7 +167,11 @@ export class FastTxCollection { } // Keep a shared priority queue of all txs pending to be requested, sorted by the number of attempts made to collect them. - const attemptsPerTx = [...request.missingTxHashes].map(txHash => ({ txHash, attempts: 0, found: false })); + const attemptsPerTx = [...request.missingTxTracker.missingTxHashes].map(txHash => ({ + txHash, + attempts: 0, + found: false, + })); // Returns once we have finished all node loops. Each loop finishes when the deadline is hit, or all txs have been collected. await Promise.allSettled(this.nodes.map(node => this.collectFastFromNode(request, node, attemptsPerTx, opts))); @@ -179,7 +184,9 @@ export class FastTxCollection { opts: { deadline: Date }, ) { const notFinished = () => - this.dateProvider.now() <= +opts.deadline && request.missingTxHashes.size > 0 && this.requests.has(request); + this.dateProvider.now() <= +opts.deadline && + request.missingTxTracker.numberOfMissingTxs > 0 && + this.requests.has(request); const maxParallelRequests = this.config.txCollectionFastMaxParallelRequestsPerNode; const maxBatchSize = this.config.txCollectionNodeRpcMaxBatchSize; @@ -196,7 +203,7 @@ export class FastTxCollection { if (!txToRequest) { // No more txs to process break; - } else if (!request.missingTxHashes.has(txToRequest.txHash)) { + } else if (!request.missingTxTracker.isMissing(txToRequest.txHash)) { // Mark as found if it was found somewhere else, we'll then remove it from the array. // We don't delete it now since 'array.splice' is pretty expensive, so we do it after sorting. txToRequest.found = true; @@ -225,17 +232,14 @@ export class FastTxCollection { return; } + const txHashes = batch.map(({ txHash }) => TxHash.fromString(txHash)); // Collect this batch from the node - await this.txCollectionSink.collect( - txHashes => node.getTxsByHash(txHashes), - batch.map(({ txHash }) => TxHash.fromString(txHash)), - { - description: `fast ${node.getInfo()}`, - node: node.getInfo(), - method: 'fast-node-rpc', - ...request.blockInfo, - }, - ); + await this.txCollectionSink.collect(() => node.getTxsByHash(txHashes), txHashes, { + description: `fast ${node.getInfo()}`, + node: node.getInfo(), + method: 'fast-node-rpc', + ...request.blockInfo, + }); // Clear from the active requests the txs we just requested for (const requestedTx of batch) { @@ -267,31 +271,41 @@ export class FastTxCollection { } this.log.debug( - `Starting fast reqresp for ${request.missingTxHashes.size} txs for ${request.type} at slot ${blockInfo.slotNumber}`, + `Starting fast reqresp for ${request.missingTxTracker.numberOfMissingTxs} txs for ${request.type} at slot ${blockInfo.slotNumber}`, { ...blockInfo, timeoutMs, pinnedPeer }, ); try { await this.txCollectionSink.collect( - async txHashes => { + async () => { if (request.type === 'proposal') { - return await this.missingTxsCollector.collectTxs(txHashes, request.blockProposal, pinnedPeer, timeoutMs); + return await this.missingTxsCollector.collectTxs( + request.missingTxTracker, + request.blockProposal, + pinnedPeer, + timeoutMs, + ); } else if (request.type === 'block') { const blockTxsSource = { txHashes: request.block.body.txEffects.map(e => e.txHash), archive: request.block.archive.root, }; - return await this.missingTxsCollector.collectTxs(txHashes, blockTxsSource, pinnedPeer, timeoutMs); + return await this.missingTxsCollector.collectTxs( + request.missingTxTracker, + blockTxsSource, + pinnedPeer, + timeoutMs, + ); } else { throw new Error(`Unknown request type: ${(request as any).type}`); } }, - Array.from(request.missingTxHashes).map(txHash => TxHash.fromString(txHash)), + Array.from(request.missingTxTracker.missingTxHashes).map(txHash => TxHash.fromString(txHash)), { description: `reqresp for slot ${slotNumber}`, method: 'fast-req-resp', ...opts, ...request.blockInfo }, ); } catch (err) { this.log.error(`Error sending fast reqresp request for txs`, err, { - txs: [...request.missingTxHashes], + txs: [...request.missingTxTracker.missingTxHashes], ...blockInfo, }); } @@ -306,8 +320,7 @@ export class FastTxCollection { for (const tx of txs) { const txHash = tx.txHash.toString(); // Remove the tx hash from the missing set, and add it to the found set. - if (request.missingTxHashes.has(txHash)) { - request.missingTxHashes.delete(txHash); + if (request.missingTxTracker.markFetched(tx)) { request.foundTxs.set(txHash, tx); this.log.trace(`Found tx ${txHash} for fast collection request`, { ...request.blockInfo, @@ -315,7 +328,7 @@ export class FastTxCollection { type: request.type, }); // If we found all txs for this request, we resolve the promise - if (request.missingTxHashes.size === 0) { + if (request.missingTxTracker.numberOfMissingTxs === 0) { this.log.trace(`All txs found for fast collection request`, { ...request.blockInfo, type: request.type, diff --git a/yarn-project/p2p/src/services/tx_collection/proposal_tx_collector.ts b/yarn-project/p2p/src/services/tx_collection/proposal_tx_collector.ts index 27ba87d9dfc9..448e7e01c987 100644 --- a/yarn-project/p2p/src/services/tx_collection/proposal_tx_collector.ts +++ b/yarn-project/p2p/src/services/tx_collection/proposal_tx_collector.ts @@ -1,13 +1,12 @@ import type { Logger } from '@aztec/foundation/log'; import type { DateProvider } from '@aztec/foundation/timer'; -import type { Tx, TxHash } from '@aztec/stdlib/tx'; +import { type Tx, TxHash } from '@aztec/stdlib/tx'; import type { PeerId } from '@libp2p/interface'; import { BatchTxRequester } from '../reqresp/batch-tx-requester/batch_tx_requester.js'; import type { BatchTxRequesterConfig } from '../reqresp/batch-tx-requester/config.js'; -import type { BatchTxRequesterLibP2PService } from '../reqresp/batch-tx-requester/interface.js'; -import { MissingTxsTracker } from '../reqresp/batch-tx-requester/missing_txs.js'; +import type { BatchTxRequesterLibP2PService, IMissingTxsTracker } from '../reqresp/batch-tx-requester/interface.js'; import type { IBatchRequestTxValidator } from '../reqresp/batch-tx-requester/tx_validator.js'; import { type BlockTxsSource, ReqRespSubProtocol, chunkTxHashesRequest } from '../reqresp/index.js'; @@ -18,14 +17,14 @@ import { type BlockTxsSource, ReqRespSubProtocol, chunkTxHashesRequest } from '. export interface MissingTxsCollector { /** * Collect missing transactions for a block or proposal. - * @param txHashes - The transaction hashes to collect + * @param missingTxsTracker - The missing transactions tracker * @param blockTxsSource - The block or proposal containing the transactions * @param pinnedPeer - Optional peer expected to have the transactions * @param timeoutMs - Timeout in milliseconds * @returns The collected transactions */ collectTxs( - txHashes: TxHash[], + missingTxsTracker: IMissingTxsTracker, blockTxsSource: BlockTxsSource, pinnedPeer: PeerId | undefined, timeoutMs: number, @@ -46,7 +45,7 @@ export class BatchTxRequesterCollector implements MissingTxsCollector { ) {} async collectTxs( - txHashes: TxHash[], + missingTxsTracker: IMissingTxsTracker, blockTxsSource: BlockTxsSource, pinnedPeer: PeerId | undefined, timeoutMs: number, @@ -59,7 +58,7 @@ export class BatchTxRequesterCollector implements MissingTxsCollector { } = this.batchTxRequesterConfig ?? {}; const batchRequester = new BatchTxRequester( - new MissingTxsTracker(txHashes), + missingTxsTracker, blockTxsSource, pinnedPeer, timeoutMs, @@ -94,14 +93,14 @@ export class SendBatchRequestCollector implements MissingTxsCollector { ) {} async collectTxs( - txHashes: TxHash[], + missingTxsTracker: IMissingTxsTracker, _blockTxsSource: BlockTxsSource, pinnedPeer: PeerId | undefined, timeoutMs: number, ): Promise { const txs = await this.p2pService.reqResp.sendBatchRequest( ReqRespSubProtocol.TX, - chunkTxHashesRequest(txHashes), + chunkTxHashesRequest(Array.from(missingTxsTracker.missingTxHashes).map(TxHash.fromString)), pinnedPeer, timeoutMs, this.maxPeers, diff --git a/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts index bdda8e17b422..486b47aa50ab 100644 --- a/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts @@ -8,8 +8,7 @@ import type { L2Block } from '@aztec/stdlib/block'; import { type L1RollupConstants, getEpochAtSlot, getTimestampRangeForEpoch } from '@aztec/stdlib/epoch-helpers'; import { type Tx, TxHash } from '@aztec/stdlib/tx'; -import { type ReqRespInterface, ReqRespSubProtocol } from '../reqresp/interface.js'; -import { chunkTxHashesRequest } from '../reqresp/protocols/tx.js'; +import { type ReqRespInterface, ReqRespSubProtocol, chunkTxHashesRequest } from '../reqresp/index.js'; import type { TxCollectionConfig } from './config.js'; import type { FastTxCollection } from './fast_tx_collection.js'; import type { MissingTxInfo } from './tx_collection.js'; @@ -116,7 +115,7 @@ export class SlowTxCollection { // Request in chunks to avoid hitting RPC limits for (const batch of chunk(missingTxHashes, this.config.txCollectionNodeRpcMaxBatchSize)) { - await this.txCollectionSink.collect(txHashes => node.getTxsByHash(txHashes), batch, { + await this.txCollectionSink.collect(() => node.getTxsByHash(batch), batch, { description: `node ${node.getInfo()}`, node: node.getInfo(), method: 'slow-node-rpc', @@ -151,9 +150,10 @@ export class SlowTxCollection { const timeoutMs = this.config.txCollectionSlowReqRespTimeoutMs; const maxPeers = boundInclusive(Math.ceil(missingTxs.length / 3), 4, 16); const maxRetryAttempts = 3; + const txHashes = missingTxs.map(([txHash]) => TxHash.fromString(txHash)); // Send a batch request via reqresp for the missing txs await this.txCollectionSink.collect( - async txHashes => { + async () => { const txs = await this.reqResp.sendBatchRequest( ReqRespSubProtocol.TX, chunkTxHashesRequest(txHashes), @@ -162,10 +162,9 @@ export class SlowTxCollection { maxPeers, maxRetryAttempts, ); - return txs.flat(); }, - missingTxs.map(([txHash]) => TxHash.fromString(txHash)), + txHashes, { description: 'slow reqresp', timeoutMs, method: 'slow-req-resp' }, ); } @@ -183,7 +182,7 @@ export class SlowTxCollection { // from mined unproven blocks it has seen in the past. const fastRequests = this.fastCollection.getFastCollectionRequests(); const fastCollectionTxs: Set = new Set( - ...Array.from(fastRequests.values()).flatMap(r => r.missingTxHashes), + ...Array.from(fastRequests.values()).flatMap(r => r.missingTxTracker.missingTxHashes), ); // Return all missing txs that are not in fastCollectionTxs and are ready for reqresp if requested diff --git a/yarn-project/p2p/src/services/tx_collection/tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/tx_collection.ts index 38c3f65cbcbd..3a3e7800e4ec 100644 --- a/yarn-project/p2p/src/services/tx_collection/tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/tx_collection.ts @@ -14,7 +14,7 @@ import type { PeerId } from '@libp2p/interface'; import type { TxPool } from '../../mem_pools/index.js'; import type { TxPoolEvents } from '../../mem_pools/tx_pool/tx_pool.js'; -import type { BatchTxRequesterLibP2PService } from '../reqresp/batch-tx-requester/interface.js'; +import type { BatchTxRequesterLibP2PService, IMissingTxsTracker } from '../reqresp/batch-tx-requester/interface.js'; import type { TxCollectionConfig } from './config.js'; import { FastTxCollection } from './fast_tx_collection.js'; import { FileStoreTxCollection } from './file_store_tx_collection.js'; @@ -32,7 +32,7 @@ export type FastCollectionRequestInput = | { type: 'proposal'; blockProposal: BlockProposal; blockNumber: BlockNumber }; export type FastCollectionRequest = FastCollectionRequestInput & { - missingTxHashes: Set; + missingTxTracker: IMissingTxsTracker; deadline: Date; blockInfo: L2BlockInfo; promise: PromiseWithResolvers; diff --git a/yarn-project/p2p/src/services/tx_collection/tx_collection_sink.ts b/yarn-project/p2p/src/services/tx_collection/tx_collection_sink.ts index 5c7b5d1ff138..2a3a2e3d8824 100644 --- a/yarn-project/p2p/src/services/tx_collection/tx_collection_sink.ts +++ b/yarn-project/p2p/src/services/tx_collection/tx_collection_sink.ts @@ -27,7 +27,7 @@ export class TxCollectionSink extends (EventEmitter as new () => TypedEventEmitt } public async collect( - collectValidTxsFn: (txHashes: TxHash[]) => Promise<(Tx | undefined)[]>, + collectValidTxsFn: () => Promise<(Tx | undefined)[]>, requested: TxHash[], info: Record & { description: string; method: CollectionMethod }, ) { @@ -39,7 +39,7 @@ export class TxCollectionSink extends (EventEmitter as new () => TypedEventEmitt // Execute collection function and measure the time taken, catching any errors. const [duration, txs] = await elapsed(async () => { try { - const response = await collectValidTxsFn(requested); + const response = await collectValidTxsFn(); return response.filter(tx => tx !== undefined); } catch (err) { this.log.error(`Error collecting txs via ${info.description}`, err, { diff --git a/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts b/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts index 77bf3d47338b..cd624c0499c5 100644 --- a/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts +++ b/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts @@ -29,22 +29,19 @@ import type { Message, PeerId } from '@libp2p/interface'; import { TopicValidatorResult } from '@libp2p/interface'; import { peerIdFromString } from '@libp2p/peer-id'; -import type { P2PClient } from '../client/p2p_client.js'; +import type { P2PClient } from '../client/index.js'; import type { P2PConfig } from '../config.js'; import { createP2PClient } from '../index.js'; -import type { MemPools } from '../mem_pools/interface.js'; -import { LibP2PService } from '../services/libp2p/libp2p_service.js'; +import type { MemPools } from '../mem_pools/index.js'; +import { BatchTxRequesterCollector, LibP2PService, SendBatchRequestCollector } from '../services/index.js'; import type { PeerManager } from '../services/peer-manager/peer_manager.js'; import type { BatchTxRequesterLibP2PService } from '../services/reqresp/batch-tx-requester/interface.js'; +import { MissingTxsTracker } from '../services/reqresp/batch-tx-requester/missing_txs.js'; import type { IBatchRequestTxValidator } from '../services/reqresp/batch-tx-requester/tx_validator.js'; import { RateLimitStatus } from '../services/reqresp/rate-limiter/rate_limiter.js'; import type { ReqResp } from '../services/reqresp/reqresp.js'; import type { PeerDiscoveryService } from '../services/service.js'; -import { - BatchTxRequesterCollector, - SendBatchRequestCollector, -} from '../services/tx_collection/proposal_tx_collector.js'; -import { AlwaysTrueCircuitVerifier } from '../test-helpers/reqresp-nodes.js'; +import { AlwaysTrueCircuitVerifier } from '../test-helpers/index.js'; import { BENCHMARK_CONSTANTS, type CollectorType, @@ -55,7 +52,7 @@ import { createMockEpochCache, createMockWorldStateSynchronizer, filterTxsByDistribution, -} from '../test-helpers/testbench-utils.js'; +} from '../test-helpers/index.js'; import type { PubSubLibp2p } from '../util.js'; export type { DistributionPattern, CollectorType } from '../test-helpers/testbench-utils.js'; @@ -277,7 +274,12 @@ async function runAggregatorBenchmark( new DateProvider(), noopTxValidator, ); - const fetchedTxs = await collector.collectTxs(txHashes, blockProposal, pinnedPeer, timeoutMs); + const fetchedTxs = await collector.collectTxs( + new MissingTxsTracker(new Set(txHashes.map(TxHash.toString))), + blockProposal, + pinnedPeer, + timeoutMs, + ); const durationMs = timer.ms(); return { type: 'BENCH_RESULT', @@ -292,7 +294,12 @@ async function runAggregatorBenchmark( BENCHMARK_CONSTANTS.FIXED_MAX_PEERS, BENCHMARK_CONSTANTS.FIXED_MAX_RETRY_ATTEMPTS, ); - const fetchedTxs = await collector.collectTxs(txHashes, blockProposal, pinnedPeer, timeoutMs); + const fetchedTxs = await collector.collectTxs( + new MissingTxsTracker(new Set(txHashes.map(TxHash.toString))), + blockProposal, + pinnedPeer, + timeoutMs, + ); const durationMs = timer.ms(); return { type: 'BENCH_RESULT', From dee87b0035b548bf61cd78fd746407255d50e640 Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Mon, 9 Feb 2026 13:37:49 -0300 Subject: [PATCH 05/23] fromArray --- .../p2p_client.integration_batch_txs.test.ts | 2 +- .../proposal_tx_collector_worker.ts | 15 +++--- .../batch_tx_requester.test.ts | 48 +++++++++---------- .../reqresp/batch-tx-requester/missing_txs.ts | 6 ++- .../tx_collection/fast_tx_collection.ts | 2 +- .../testbench/p2p_client_testbench_worker.ts | 4 +- 6 files changed, 39 insertions(+), 38 deletions(-) diff --git a/yarn-project/p2p/src/client/test/p2p_client.integration_batch_txs.test.ts b/yarn-project/p2p/src/client/test/p2p_client.integration_batch_txs.test.ts index 98b51c9c670e..a8509e23ded1 100644 --- a/yarn-project/p2p/src/client/test/p2p_client.integration_batch_txs.test.ts +++ b/yarn-project/p2p/src/client/test/p2p_client.integration_batch_txs.test.ts @@ -229,7 +229,7 @@ describe('p2p client integration batch txs', () => { mockP2PService.reqResp = (client0 as any).p2pService.reqresp; const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missingTxHashes.map(TxHash.toString))), + MissingTxsTracker.fromArray(missingTxHashes), blockProposal, undefined, // no pinned peer 5_000, diff --git a/yarn-project/p2p/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts b/yarn-project/p2p/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts index 9998dc2da04c..68e068b6ca32 100644 --- a/yarn-project/p2p/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts +++ b/yarn-project/p2p/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts @@ -9,21 +9,18 @@ import type { L2BlockSource } from '@aztec/stdlib/block'; import type { ContractDataSource } from '@aztec/stdlib/contract'; import type { ClientProtocolCircuitVerifier } from '@aztec/stdlib/interfaces/server'; import { P2PClientType, PeerErrorSeverity } from '@aztec/stdlib/p2p'; -import { type Tx, TxHash, type TxValidationResult } from '@aztec/stdlib/tx'; +import type { Tx, TxValidationResult } from '@aztec/stdlib/tx'; import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client'; import type { PeerId } from '@libp2p/interface'; import { peerIdFromString } from '@libp2p/peer-id'; import type { P2PConfig } from '../../../config.js'; +import { BatchTxRequesterCollector, SendBatchRequestCollector } from '../../../services/index.js'; import { MissingTxsTracker } from '../../../services/reqresp/batch-tx-requester/missing_txs.js'; import type { IBatchRequestTxValidator } from '../../../services/reqresp/batch-tx-requester/tx_validator.js'; import { RateLimitStatus } from '../../../services/reqresp/rate-limiter/rate_limiter.js'; -import { - BatchTxRequesterCollector, - SendBatchRequestCollector, -} from '../../../services/tx_collection/proposal_tx_collector.js'; -import { AlwaysTrueCircuitVerifier } from '../../../test-helpers/reqresp-nodes.js'; +import { AlwaysTrueCircuitVerifier } from '../../../test-helpers/index.js'; import { BENCHMARK_CONSTANTS, InMemoryAttestationPool, @@ -32,7 +29,7 @@ import { calculateInternalTimeout, createMockEpochCache, createMockWorldStateSynchronizer, -} from '../../../test-helpers/testbench-utils.js'; +} from '../../../test-helpers/index.js'; import { createP2PClient } from '../../index.js'; import type { P2PClient } from '../../p2p_client.js'; import { @@ -217,7 +214,7 @@ async function runCollector(cmd: Extract collector.collectTxs( - new MissingTxsTracker(new Set(parsedTxHashes.map(TxHash.toString))), + MissingTxsTracker.fromArray(parsedTxHashes), parsedProposal, pinnedPeer, internalTimeoutMs, @@ -235,7 +232,7 @@ async function runCollector(cmd: Extract collector.collectTxs( - new MissingTxsTracker(new Set(parsedTxHashes.map(TxHash.toString))), + MissingTxsTracker.fromArray(parsedTxHashes), parsedProposal, pinnedPeer, internalTimeoutMs, diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.test.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.test.ts index 26b399799a59..9645d7a5970b 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.test.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.test.ts @@ -96,7 +96,7 @@ describe('BatchTxRequester', () => { const clock = new TestClock(); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, undefined, deadline, @@ -151,7 +151,7 @@ describe('BatchTxRequester', () => { const clock = new TestClock(); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, undefined, deadline, @@ -277,7 +277,7 @@ describe('BatchTxRequester', () => { }); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, undefined, deadline, @@ -331,7 +331,7 @@ describe('BatchTxRequester', () => { const semaphore = new TestSemaphore(new Semaphore(0)); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, undefined, deadline, @@ -383,7 +383,7 @@ describe('BatchTxRequester', () => { const semaphore = new TestSemaphore(new Semaphore(0)); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, pinnedPeer, deadline, @@ -450,7 +450,7 @@ describe('BatchTxRequester', () => { const semaphore = new TestSemaphore(new Semaphore(0)); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, pinnedPeer, deadline, @@ -506,7 +506,7 @@ describe('BatchTxRequester', () => { const semaphore = new TestSemaphore(new Semaphore(0)); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, pinnedPeer, deadline, @@ -562,7 +562,7 @@ describe('BatchTxRequester', () => { ); reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, pinnedPeer, deadline, @@ -636,7 +636,7 @@ describe('BatchTxRequester', () => { }); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, pinnedPeer, deadline, @@ -750,7 +750,7 @@ describe('BatchTxRequester', () => { }); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, pinnedPeer, deadline, @@ -886,7 +886,7 @@ describe('BatchTxRequester', () => { }); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, pinnedPeer, deadline, @@ -961,7 +961,7 @@ describe('BatchTxRequester', () => { const clock = new TestClock(); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, undefined, shortDeadline, @@ -1019,7 +1019,7 @@ describe('BatchTxRequester', () => { }); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, undefined, deadline, @@ -1091,7 +1091,7 @@ describe('BatchTxRequester', () => { }); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, undefined, deadline, @@ -1144,7 +1144,7 @@ describe('BatchTxRequester', () => { // Create semaphore that starts with 0 permits to block smart workers const semaphore = new TestSemaphore(new Semaphore(0)); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, undefined, deadline, @@ -1218,7 +1218,7 @@ describe('BatchTxRequester', () => { reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, undefined, deadline, @@ -1299,7 +1299,7 @@ describe('BatchTxRequester', () => { reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, undefined, deadline, @@ -1370,7 +1370,7 @@ describe('BatchTxRequester', () => { reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, undefined, deadline, @@ -1418,7 +1418,7 @@ describe('BatchTxRequester', () => { const peer = await createSecp256k1PeerId(); connectionSampler.getPeerListSortedByConnectionCountAsc.mockReturnValue([peer]); - const tracker = new MissingTxsTracker(new Set(missing.map(TxHash.toString))); + const tracker = MissingTxsTracker.fromArray(missing); // Peer has only first half of transactions const peerTransactions = new Map([[peer.toString(), Array.from({ length: TX_BATCH_SIZE }, (_, i) => i)]]); @@ -1487,7 +1487,7 @@ describe('BatchTxRequester', () => { reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, pinnedPeer, deadline, @@ -1537,7 +1537,7 @@ describe('BatchTxRequester', () => { reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, pinnedPeer, deadline, @@ -1618,7 +1618,7 @@ describe('BatchTxRequester', () => { }); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, pinnedPeer, deadline, @@ -1672,7 +1672,7 @@ describe('BatchTxRequester', () => { reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, pinnedPeer, deadline, @@ -1728,7 +1728,7 @@ describe('BatchTxRequester', () => { reqResp.sendRequestToPeer.mockImplementation(mockImplementation); const requester = new BatchTxRequester( - new MissingTxsTracker(new Set(missing.map(TxHash.toString))), + MissingTxsTracker.fromArray(missing), blockProposal, pinnedPeer, deadline, diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts index b341dbab90db..6de52e8de474 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts @@ -6,7 +6,11 @@ import { DEFAULT_BATCH_TX_REQUESTER_TX_BATCH_SIZE } from './config.js'; import type { IMissingTxsTracker, ITxMetadataCollection } from './interface.js'; export class MissingTxsTracker implements IMissingTxsTracker { - constructor(public readonly missingTxHashes: Set) {} + private constructor(public readonly missingTxHashes: Set) {} + + public static fromArray(hashes: TxHash[] | string[]) { + return new MissingTxsTracker(new Set(hashes.map(hash => hash.toString()))); + } markFetched(tx: Tx): boolean { return this.missingTxHashes.delete(tx.txHash.toString()); diff --git a/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts index 3d88ea1d76f9..be7cde03f19e 100644 --- a/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts @@ -85,7 +85,7 @@ export class FastTxCollection { blockInfo, promise, foundTxs: new Map(), - missingTxTracker: new MissingTxsTracker(new Set(txHashes.map(t => t.toString()))), + missingTxTracker: MissingTxsTracker.fromArray(txHashes), deadline: opts.deadline, }; diff --git a/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts b/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts index cd624c0499c5..65e2d34b0e38 100644 --- a/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts +++ b/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts @@ -275,7 +275,7 @@ async function runAggregatorBenchmark( noopTxValidator, ); const fetchedTxs = await collector.collectTxs( - new MissingTxsTracker(new Set(txHashes.map(TxHash.toString))), + MissingTxsTracker.fromArray(txHashes), blockProposal, pinnedPeer, timeoutMs, @@ -295,7 +295,7 @@ async function runAggregatorBenchmark( BENCHMARK_CONSTANTS.FIXED_MAX_RETRY_ATTEMPTS, ); const fetchedTxs = await collector.collectTxs( - new MissingTxsTracker(new Set(txHashes.map(TxHash.toString))), + MissingTxsTracker.fromArray(txHashes), blockProposal, pinnedPeer, timeoutMs, From dd81ca33e21b4122a5a1bee617418a80706438d2 Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Mon, 9 Feb 2026 15:10:57 -0300 Subject: [PATCH 06/23] verification inside node tx source --- .../tx_collection/fast_tx_collection.ts | 32 ++++++++++----- .../tx_collection/slow_tx_collection.ts | 8 ++-- .../tx_collection/tx_collection.test.ts | 7 +++- .../tx_collection/tx_collection_sink.ts | 41 ++++++------------- .../src/services/tx_collection/tx_source.ts | 23 +++++++++-- 5 files changed, 64 insertions(+), 47 deletions(-) diff --git a/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts index be7cde03f19e..ace3459e4358 100644 --- a/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts @@ -232,14 +232,24 @@ export class FastTxCollection { return; } - const txHashes = batch.map(({ txHash }) => TxHash.fromString(txHash)); + const txHashes = batch.map(({ txHash }) => txHash); // Collect this batch from the node - await this.txCollectionSink.collect(() => node.getTxsByHash(txHashes), txHashes, { - description: `fast ${node.getInfo()}`, - node: node.getInfo(), - method: 'fast-node-rpc', - ...request.blockInfo, - }); + await this.txCollectionSink.collect( + async () => { + const result = await node.getTxsByHash(txHashes.map(TxHash.fromString)); + for (const tx of result.validTxs) { + request.missingTxTracker.markFetched(tx); + } + return result; + }, + txHashes, + { + description: `fast ${node.getInfo()}`, + node: node.getInfo(), + method: 'fast-node-rpc', + ...request.blockInfo, + }, + ); // Clear from the active requests the txs we just requested for (const requestedTx of batch) { @@ -278,8 +288,9 @@ export class FastTxCollection { try { await this.txCollectionSink.collect( async () => { + let result: Tx[]; if (request.type === 'proposal') { - return await this.missingTxsCollector.collectTxs( + result = await this.missingTxsCollector.collectTxs( request.missingTxTracker, request.blockProposal, pinnedPeer, @@ -290,7 +301,7 @@ export class FastTxCollection { txHashes: request.block.body.txEffects.map(e => e.txHash), archive: request.block.archive.root, }; - return await this.missingTxsCollector.collectTxs( + result = await this.missingTxsCollector.collectTxs( request.missingTxTracker, blockTxsSource, pinnedPeer, @@ -299,8 +310,9 @@ export class FastTxCollection { } else { throw new Error(`Unknown request type: ${(request as any).type}`); } + return { validTxs: result, invalidTxHashes: [] }; }, - Array.from(request.missingTxTracker.missingTxHashes).map(txHash => TxHash.fromString(txHash)), + Array.from(request.missingTxTracker.missingTxHashes), { description: `reqresp for slot ${slotNumber}`, method: 'fast-req-resp', ...opts, ...request.blockInfo }, ); } catch (err) { diff --git a/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts index 486b47aa50ab..1e039f112c56 100644 --- a/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts @@ -115,7 +115,7 @@ export class SlowTxCollection { // Request in chunks to avoid hitting RPC limits for (const batch of chunk(missingTxHashes, this.config.txCollectionNodeRpcMaxBatchSize)) { - await this.txCollectionSink.collect(() => node.getTxsByHash(batch), batch, { + await this.txCollectionSink.collect(() => node.getTxsByHash(batch), batch.map(TxHash.toString), { description: `node ${node.getInfo()}`, node: node.getInfo(), method: 'slow-node-rpc', @@ -150,19 +150,19 @@ export class SlowTxCollection { const timeoutMs = this.config.txCollectionSlowReqRespTimeoutMs; const maxPeers = boundInclusive(Math.ceil(missingTxs.length / 3), 4, 16); const maxRetryAttempts = 3; - const txHashes = missingTxs.map(([txHash]) => TxHash.fromString(txHash)); + const txHashes = missingTxs.map(([txHash]) => txHash); // Send a batch request via reqresp for the missing txs await this.txCollectionSink.collect( async () => { const txs = await this.reqResp.sendBatchRequest( ReqRespSubProtocol.TX, - chunkTxHashesRequest(txHashes), + chunkTxHashesRequest(txHashes.map(TxHash.fromString)), pinnedPeer, timeoutMs, maxPeers, maxRetryAttempts, ); - return txs.flat(); + return { validTxs: txs.flat(), invalidTxHashes: [] }; }, txHashes, { description: 'slow reqresp', timeoutMs, method: 'slow-req-resp' }, diff --git a/yarn-project/p2p/src/services/tx_collection/tx_collection.test.ts b/yarn-project/p2p/src/services/tx_collection/tx_collection.test.ts index 4cbb82e43ed5..b94def99c1da 100644 --- a/yarn-project/p2p/src/services/tx_collection/tx_collection.test.ts +++ b/yarn-project/p2p/src/services/tx_collection/tx_collection.test.ts @@ -47,7 +47,7 @@ describe('TxCollection', () => { const makeNode = (name: string) => { const node = mock(); node.getInfo.mockReturnValue(name); - node.getTxsByHash.mockResolvedValue([]); + node.getTxsByHash.mockResolvedValue({ validTxs: [], invalidTxHashes: [] }); return node; }; @@ -77,7 +77,10 @@ describe('TxCollection', () => { const setNodeTxs = (node: MockProxy, txs: Tx[]) => { node.getTxsByHash.mockImplementation(async hashes => { await sleep(1); - return hashes.map(h => txs.find(tx => tx.txHash.equals(h))); + return { + validTxs: hashes.map(h => txs.find(tx => tx.txHash.equals(h))).filter(tx => tx !== undefined), + invalidTxHashes: [], + }; }); }; diff --git a/yarn-project/p2p/src/services/tx_collection/tx_collection_sink.ts b/yarn-project/p2p/src/services/tx_collection/tx_collection_sink.ts index 2a3a2e3d8824..be7498b56012 100644 --- a/yarn-project/p2p/src/services/tx_collection/tx_collection_sink.ts +++ b/yarn-project/p2p/src/services/tx_collection/tx_collection_sink.ts @@ -1,7 +1,7 @@ import type { Logger } from '@aztec/foundation/log'; import { elapsed } from '@aztec/foundation/timer'; import type { TypedEventEmitter } from '@aztec/foundation/types'; -import { Tx, type TxHash } from '@aztec/stdlib/tx'; +import { Tx } from '@aztec/stdlib/tx'; import type { TelemetryClient } from '@aztec/telemetry-client'; import EventEmitter from 'node:events'; @@ -27,51 +27,36 @@ export class TxCollectionSink extends (EventEmitter as new () => TypedEventEmitt } public async collect( - collectValidTxsFn: () => Promise<(Tx | undefined)[]>, - requested: TxHash[], + collectValidTxsFn: () => Promise<{ validTxs: Tx[]; invalidTxHashes: string[] }>, + requested: string[], info: Record & { description: string; method: CollectionMethod }, ) { this.log.trace(`Requesting ${requested.length} txs via ${info.description}`, { ...info, - requestedTxs: requested.map(t => t.toString()), + requestedTxs: requested, }); // Execute collection function and measure the time taken, catching any errors. - const [duration, txs] = await elapsed(async () => { + const [duration, { validTxs, invalidTxHashes }] = await elapsed(async () => { try { - const response = await collectValidTxsFn(); - return response.filter(tx => tx !== undefined); + return await collectValidTxsFn(); } catch (err) { this.log.error(`Error collecting txs via ${info.description}`, err, { ...info, - requestedTxs: requested.map(hash => hash.toString()), + requestedTxs: requested, }); - return [] as Tx[]; + return { validTxs: [] as Tx[], invalidTxHashes: [] as string[] }; } }); - if (txs.length === 0) { + if (validTxs.length === 0 && invalidTxHashes.length === 0) { this.log.trace(`No txs found via ${info.description}`, { ...info, - requestedTxs: requested.map(t => t.toString()), + requestedTxs: requested, }); - return { txs, requested, duration }; + return { validTxs, requested, duration }; } - // Validate tx hashes for all collected txs from external sources - const validTxs: Tx[] = []; - const invalidTxHashes: string[] = []; - await Promise.all( - txs.map(async tx => { - const isValid = await tx.validateTxHash(); - if (isValid) { - validTxs.push(tx); - } else { - invalidTxHashes.push(tx.getTxHash().toString()); - } - }), - ); - if (invalidTxHashes.length > 0) { this.log.warn(`Rejecting ${invalidTxHashes.length} txs with invalid hashes from ${info.description}`, { ...info, @@ -82,7 +67,7 @@ export class TxCollectionSink extends (EventEmitter as new () => TypedEventEmitt if (validTxs.length === 0) { this.log.trace(`No valid txs found via ${info.description} after validation`, { ...info, - requestedTxs: requested.map(t => t.toString()), + requestedTxs: requested, invalidTxHashes, }); return { txs: [], requested, duration }; @@ -94,7 +79,7 @@ export class TxCollectionSink extends (EventEmitter as new () => TypedEventEmitt ...info, duration, txs: validTxs.map(t => t.getTxHash().toString()), - requestedTxs: requested.map(t => t.toString()), + requestedTxs: requested, rejectedCount: invalidTxHashes.length, }, ); diff --git a/yarn-project/p2p/src/services/tx_collection/tx_source.ts b/yarn-project/p2p/src/services/tx_collection/tx_source.ts index e9b6131b9ffd..da580294825f 100644 --- a/yarn-project/p2p/src/services/tx_collection/tx_source.ts +++ b/yarn-project/p2p/src/services/tx_collection/tx_source.ts @@ -8,7 +8,7 @@ import { makeTracedFetch } from '@aztec/telemetry-client'; export interface TxSource { getInfo(): string; - getTxsByHash(txHashes: TxHash[]): Promise<(Tx | undefined)[]>; + getTxsByHash(txHashes: TxHash[]): Promise<{ validTxs: Tx[]; invalidTxHashes: string[] }>; } export class NodeRpcTxSource implements TxSource { @@ -26,8 +26,25 @@ export class NodeRpcTxSource implements TxSource { return this.info; } - public getTxsByHash(txHashes: TxHash[]): Promise<(Tx | undefined)[]> { - return this.client.getTxsByHash(txHashes); + public async getTxsByHash(txHashes: TxHash[]): Promise<{ validTxs: Tx[]; invalidTxHashes: string[] }> { + return this.verifyTxs(await this.client.getTxsByHash(txHashes)); + } + + private async verifyTxs(txs: Tx[]): Promise<{ validTxs: Tx[]; invalidTxHashes: string[] }> { + // Validate tx hashes for all collected txs from external sources + const validTxs: Tx[] = []; + const invalidTxHashes: string[] = []; + await Promise.all( + txs.map(async tx => { + const isValid = await tx.validateTxHash(); + if (isValid) { + validTxs.push(tx); + } else { + invalidTxHashes.push(tx.getTxHash().toString()); + } + }), + ); + return { validTxs: validTxs, invalidTxHashes: invalidTxHashes }; } } From c6f14714c7200d3a2f0a9900ba9660f7ee9221ee Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Mon, 9 Feb 2026 15:38:01 -0300 Subject: [PATCH 07/23] fix file store --- .../tx_collection/file_store_tx_collection.ts | 2 +- .../tx_collection/file_store_tx_source.ts | 31 +++++++++++-------- .../tx_collection/tx_collection_sink.ts | 2 +- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/yarn-project/p2p/src/services/tx_collection/file_store_tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/file_store_tx_collection.ts index 13a2a0b52ca3..1fdf5e3a784b 100644 --- a/yarn-project/p2p/src/services/tx_collection/file_store_tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/file_store_tx_collection.ts @@ -127,7 +127,7 @@ export class FileStoreTxCollection { const source = this.fileStoreSources[i % this.fileStoreSources.length]; try { - const result = await this.txCollectionSink.collect(hashes => source.getTxsByHash(hashes), [txHash], { + const result = await this.txCollectionSink.collect(() => source.getTxsByHash([txHash]), [txHash.toString()], { description: `file-store ${source.getInfo()}`, method: 'file-store', fileStore: source.getInfo(), diff --git a/yarn-project/p2p/src/services/tx_collection/file_store_tx_source.ts b/yarn-project/p2p/src/services/tx_collection/file_store_tx_source.ts index b88f6b028ede..d23080c311d6 100644 --- a/yarn-project/p2p/src/services/tx_collection/file_store_tx_source.ts +++ b/yarn-project/p2p/src/services/tx_collection/file_store_tx_source.ts @@ -39,19 +39,24 @@ export class FileStoreTxSource implements TxSource { return `file-store:${this.baseUrl}`; } - public getTxsByHash(txHashes: TxHash[]): Promise<(Tx | undefined)[]> { - return Promise.all( - txHashes.map(async txHash => { - const path = `txs/${txHash.toString()}.bin`; - try { - const buffer = await this.fileStore.read(path); - return Tx.fromBuffer(buffer); - } catch { - // Tx not found or error reading - return undefined - return undefined; - } - }), - ); + public async getTxsByHash(txHashes: TxHash[]): Promise<{ validTxs: Tx[]; invalidTxHashes: string[] }> { + return { + validTxs: ( + await Promise.all( + txHashes.map(async txHash => { + const path = `txs/${txHash.toString()}.bin`; + try { + const buffer = await this.fileStore.read(path); + return Tx.fromBuffer(buffer); + } catch { + // Tx not found or error reading - return undefined + return undefined; + } + }), + ) + ).filter(tx => tx !== undefined), + invalidTxHashes: [], + }; } } diff --git a/yarn-project/p2p/src/services/tx_collection/tx_collection_sink.ts b/yarn-project/p2p/src/services/tx_collection/tx_collection_sink.ts index be7498b56012..dec6290184ed 100644 --- a/yarn-project/p2p/src/services/tx_collection/tx_collection_sink.ts +++ b/yarn-project/p2p/src/services/tx_collection/tx_collection_sink.ts @@ -54,7 +54,7 @@ export class TxCollectionSink extends (EventEmitter as new () => TypedEventEmitt ...info, requestedTxs: requested, }); - return { validTxs, requested, duration }; + return { txs: validTxs, requested, duration }; } if (invalidTxHashes.length > 0) { From 3d0e4982318bb3f8bae5dcc96afa8da89f9a13ae Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Mon, 9 Feb 2026 15:47:36 -0300 Subject: [PATCH 08/23] fix build --- .../tx_collection/file_store_tx_collection.test.ts | 7 +++++-- .../p2p/src/services/tx_collection/tx_collection.test.ts | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/yarn-project/p2p/src/services/tx_collection/file_store_tx_collection.test.ts b/yarn-project/p2p/src/services/tx_collection/file_store_tx_collection.test.ts index e2f702ea4154..d9b298241aa3 100644 --- a/yarn-project/p2p/src/services/tx_collection/file_store_tx_collection.test.ts +++ b/yarn-project/p2p/src/services/tx_collection/file_store_tx_collection.test.ts @@ -23,7 +23,7 @@ describe('FileStoreTxCollection', () => { const makeFileStoreSource = (name: string) => { const source = mock(); source.getInfo.mockReturnValue(name); - source.getTxsByHash.mockResolvedValue([]); + source.getTxsByHash.mockResolvedValue({ validTxs: [], invalidTxHashes: [] }); return source; }; @@ -35,7 +35,10 @@ describe('FileStoreTxCollection', () => { const setFileStoreTxs = (source: MockProxy, txs: Tx[]) => { source.getTxsByHash.mockImplementation(hashes => { - return Promise.resolve(hashes.map(h => txs.find(tx => tx.getTxHash().equals(h)))); + return Promise.resolve({ + validTxs: hashes.map(h => txs.find(tx => tx.getTxHash().equals(h))).filter(tx => tx !== undefined), + invalidTxHashes: [], + }); }); }; diff --git a/yarn-project/p2p/src/services/tx_collection/tx_collection.test.ts b/yarn-project/p2p/src/services/tx_collection/tx_collection.test.ts index b94def99c1da..6e0bb8a4d23f 100644 --- a/yarn-project/p2p/src/services/tx_collection/tx_collection.test.ts +++ b/yarn-project/p2p/src/services/tx_collection/tx_collection.test.ts @@ -54,7 +54,7 @@ describe('TxCollection', () => { const makeFileStoreSource = (name: string) => { const source = mock(); source.getInfo.mockReturnValue(name); - source.getTxsByHash.mockResolvedValue([]); + source.getTxsByHash.mockResolvedValue({ validTxs: [], invalidTxHashes: [] }); return source; }; @@ -518,7 +518,10 @@ describe('TxCollection', () => { const setFileStoreTxs = (source: MockProxy, txsToReturn: Tx[]) => { source.getTxsByHash.mockImplementation(hashes => { - return Promise.resolve(hashes.map(h => txsToReturn.find(tx => tx.txHash.equals(h)))); + return Promise.resolve({ + validTxs: hashes.map(h => txsToReturn.find(tx => tx.txHash.equals(h))).filter(tx => tx !== undefined), + invalidTxHashes: [], + }); }); }; From 5881f2180d029e431129fe39bd01ebae73e13cd7 Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Tue, 10 Feb 2026 13:35:28 -0300 Subject: [PATCH 09/23] fix tests --- .../reqresp/batch-tx-requester/interface.ts | 2 ++ .../reqresp/batch-tx-requester/missing_txs.ts | 8 +++++++- .../services/tx_collection/fast_tx_collection.ts | 6 ++---- .../services/tx_collection/slow_tx_collection.ts | 14 +++++++++----- .../src/services/tx_collection/tx_collection.ts | 1 - 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts index 6f47db2ccb67..8da8fe400ebc 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts @@ -27,6 +27,8 @@ export interface IMissingTxsTracker { isMissing(txHash: string): boolean; /** Marks a transaction as fetched. Returns true if it was previously missing. */ markFetched(tx: Tx): boolean; + /** Get list of collected txs */ + get collectedTxs(): Tx[]; } export interface ITxMetadataCollection { diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts index 6de52e8de474..6749df2cc832 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts @@ -6,6 +6,8 @@ import { DEFAULT_BATCH_TX_REQUESTER_TX_BATCH_SIZE } from './config.js'; import type { IMissingTxsTracker, ITxMetadataCollection } from './interface.js'; export class MissingTxsTracker implements IMissingTxsTracker { + public readonly collectedTxs: Tx[] = []; + private constructor(public readonly missingTxHashes: Set) {} public static fromArray(hashes: TxHash[] | string[]) { @@ -13,7 +15,11 @@ export class MissingTxsTracker implements IMissingTxsTracker { } markFetched(tx: Tx): boolean { - return this.missingTxHashes.delete(tx.txHash.toString()); + if (this.missingTxHashes.delete(tx.txHash.toString())) { + this.collectedTxs.push(tx); + return true; + } + return false; } get numberOfMissingTxs(): number { diff --git a/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts index ace3459e4358..8e0a5c04d3af 100644 --- a/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts @@ -84,7 +84,6 @@ export class FastTxCollection { ...input, blockInfo, promise, - foundTxs: new Map(), missingTxTracker: MissingTxsTracker.fromArray(txHashes), deadline: opts.deadline, }; @@ -93,7 +92,7 @@ export class FastTxCollection { clearTimeout(timeoutTimer); this.log.verbose( - `Collected ${request.foundTxs.size} txs out of ${txHashes.length} for ${input.type} at slot ${blockInfo.slotNumber}`, + `Collected ${request.missingTxTracker.collectedTxs.length} txs out of ${txHashes.length} for ${input.type} at slot ${blockInfo.slotNumber}`, { ...blockInfo, duration, @@ -101,7 +100,7 @@ export class FastTxCollection { missingTxs: [...request.missingTxTracker.missingTxHashes], }, ); - return [...request.foundTxs.values()]; + return request.missingTxTracker.collectedTxs; } protected async collectFast( @@ -333,7 +332,6 @@ export class FastTxCollection { const txHash = tx.txHash.toString(); // Remove the tx hash from the missing set, and add it to the found set. if (request.missingTxTracker.markFetched(tx)) { - request.foundTxs.set(txHash, tx); this.log.trace(`Found tx ${txHash} for fast collection request`, { ...request.blockInfo, txHash: tx.txHash.toString(), diff --git a/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts index 1e039f112c56..875a681d99d5 100644 --- a/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts @@ -115,11 +115,15 @@ export class SlowTxCollection { // Request in chunks to avoid hitting RPC limits for (const batch of chunk(missingTxHashes, this.config.txCollectionNodeRpcMaxBatchSize)) { - await this.txCollectionSink.collect(() => node.getTxsByHash(batch), batch.map(TxHash.toString), { - description: `node ${node.getInfo()}`, - node: node.getInfo(), - method: 'slow-node-rpc', - }); + await this.txCollectionSink.collect( + () => node.getTxsByHash(batch), + batch.map(hash => hash.toString()), + { + description: `node ${node.getInfo()}`, + node: node.getInfo(), + method: 'slow-node-rpc', + }, + ); } // Mark every tx that is still missing as ready for reqresp. diff --git a/yarn-project/p2p/src/services/tx_collection/tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/tx_collection.ts index 3a3e7800e4ec..9c2dd21667af 100644 --- a/yarn-project/p2p/src/services/tx_collection/tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/tx_collection.ts @@ -36,7 +36,6 @@ export type FastCollectionRequest = FastCollectionRequestInput & { deadline: Date; blockInfo: L2BlockInfo; promise: PromiseWithResolvers; - foundTxs: Map; }; /** From 87b2cef7a361f7df6669d75f2666895dabaee55b Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Tue, 10 Feb 2026 15:56:26 -0300 Subject: [PATCH 10/23] verification in file store tx source --- .../tx_collection/file_store_tx_source.ts | 11 +++++-- .../tx_file_store/tx_file_store.test.ts | 29 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/yarn-project/p2p/src/services/tx_collection/file_store_tx_source.ts b/yarn-project/p2p/src/services/tx_collection/file_store_tx_source.ts index d23080c311d6..db6498d3e18b 100644 --- a/yarn-project/p2p/src/services/tx_collection/file_store_tx_source.ts +++ b/yarn-project/p2p/src/services/tx_collection/file_store_tx_source.ts @@ -40,6 +40,7 @@ export class FileStoreTxSource implements TxSource { } public async getTxsByHash(txHashes: TxHash[]): Promise<{ validTxs: Tx[]; invalidTxHashes: string[] }> { + const invalidTxHashes: string[] = []; return { validTxs: ( await Promise.all( @@ -47,7 +48,13 @@ export class FileStoreTxSource implements TxSource { const path = `txs/${txHash.toString()}.bin`; try { const buffer = await this.fileStore.read(path); - return Tx.fromBuffer(buffer); + const tx = Tx.fromBuffer(buffer); + if ((await tx.validateTxHash()) && txHash === tx.txHash) { + return tx; + } else { + invalidTxHashes.push(tx.txHash.toString()); + return undefined; + } } catch { // Tx not found or error reading - return undefined return undefined; @@ -55,7 +62,7 @@ export class FileStoreTxSource implements TxSource { }), ) ).filter(tx => tx !== undefined), - invalidTxHashes: [], + invalidTxHashes: invalidTxHashes, }; } } diff --git a/yarn-project/p2p/src/services/tx_file_store/tx_file_store.test.ts b/yarn-project/p2p/src/services/tx_file_store/tx_file_store.test.ts index 47ad859c1ef6..a0ce4ddc4d48 100644 --- a/yarn-project/p2p/src/services/tx_file_store/tx_file_store.test.ts +++ b/yarn-project/p2p/src/services/tx_file_store/tx_file_store.test.ts @@ -9,6 +9,7 @@ import { tmpdir } from 'os'; import { join } from 'path'; import { InMemoryTxPool } from '../../test-helpers/testbench-utils.js'; +import { FileStoreTxSource } from '../tx_collection/file_store_tx_source.js'; import type { TxFileStoreConfig } from './config.js'; import { TxFileStore } from './tx_file_store.js'; @@ -303,6 +304,34 @@ describe('TxFileStore', () => { }, 10000); }); + describe('tx download validation', () => { + it('rejects tx with invalid hash when reading from file store', async () => { + // Write a tx with a mismatched hash directly to the file store + const invalidTx = Tx.random(); // random hash does not match computed hash + await fileStore.save(`txs/${invalidTx.txHash.toString()}.bin`, invalidTx.toBuffer(), { compress: false }); + + // Read it back via FileStoreTxSource + const source = (await FileStoreTxSource.create(`file://${tmpDir}`, log))!; + const result = await source.getTxsByHash([invalidTx.txHash]); + + expect(result.validTxs).toHaveLength(0); + expect(result.invalidTxHashes).toEqual([invalidTx.txHash.toString()]); + }); + + it('rejects tx when tx with wrong hash is returned', async () => { + // Write a tx with a mismatched hash directly to the file store + const invalidTx = Tx.random(); // random hash does not match computed hash + await fileStore.save(`txs/${invalidTx.txHash.toString()}.bin`, (await makeTx()).toBuffer(), { compress: false }); + + // Read it back via FileStoreTxSource + const source = (await FileStoreTxSource.create(`file://${tmpDir}`, log))!; + const result = await source.getTxsByHash([invalidTx.txHash]); + + expect(result.validTxs).toHaveLength(0); + expect(result.invalidTxHashes).toEqual([invalidTx.txHash.toString()]); + }); + }); + describe('getPendingUploadCount', () => { it('returns correct count of pending uploads', async () => { txFileStore = await TxFileStore.create(txPool, config, log, undefined, fileStore); From 014b6b3c3d7e3f598d577c31e507004ea1d320ef Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Tue, 10 Feb 2026 16:16:44 -0300 Subject: [PATCH 11/23] fix build --- .../services/tx_collection/file_store_tx_collection.ts | 4 ++-- .../p2p/src/services/tx_collection/slow_tx_collection.ts | 8 ++++---- .../p2p/src/services/tx_collection/tx_collection.ts | 3 +-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/yarn-project/p2p/src/services/tx_collection/file_store_tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/file_store_tx_collection.ts index 709238344a5d..a9886841cb28 100644 --- a/yarn-project/p2p/src/services/tx_collection/file_store_tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/file_store_tx_collection.ts @@ -129,8 +129,8 @@ export class FileStoreTxCollection { try { const result = await this.txCollectionSink.collect( - hashes => source.getTxsByHash(hashes), - [txHash], + () => source.getTxsByHash([txHash]), + [txHash.toString()], { description: `file-store ${source.getInfo()}`, method: 'file-store', diff --git a/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts index bd125d7a7461..221550bf4bb2 100644 --- a/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts @@ -120,7 +120,7 @@ export class SlowTxCollection { for (const batch of chunk(txHashes, this.config.txCollectionNodeRpcMaxBatchSize)) { await this.txCollectionSink.collect( () => node.getTxsByHash(batch), - batch, + batch.map(h => h.toString()), { description: `node ${node.getInfo()}`, node: node.getInfo(), @@ -165,10 +165,10 @@ export class SlowTxCollection { const txHashes = entries.map(([txHash]) => TxHash.fromString(txHash)); const maxPeers = boundInclusive(Math.ceil(txHashes.length / 3), 4, 16); await this.txCollectionSink.collect( - async hashes => { + async () => { const txs = await this.reqResp.sendBatchRequest( ReqRespSubProtocol.TX, - chunkTxHashesRequest(hashes), + chunkTxHashesRequest(txHashes), pinnedPeer, timeoutMs, maxPeers, @@ -176,7 +176,7 @@ export class SlowTxCollection { ); return { validTxs: txs.flat(), invalidTxHashes: [] }; }, - txHashes, + txHashes.map(h => h.toString()), { description: 'slow reqresp', timeoutMs, method: 'slow-req-resp' }, { type: 'mined', block }, ); diff --git a/yarn-project/p2p/src/services/tx_collection/tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/tx_collection.ts index 775fcbbe5f05..3cbdeb1248e9 100644 --- a/yarn-project/p2p/src/services/tx_collection/tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/tx_collection.ts @@ -12,8 +12,7 @@ import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-clien import type { PeerId } from '@libp2p/interface'; -import type { TxPool } from '../../mem_pools/index.js'; -import type { TxPoolEvents } from '../../mem_pools/tx_pool/tx_pool.js'; +import type { TxPoolV2, TxPoolV2Events } from '../../mem_pools/tx_pool_v2/interfaces.js'; import type { BatchTxRequesterLibP2PService, IMissingTxsTracker } from '../reqresp/batch-tx-requester/interface.js'; import type { TxCollectionConfig } from './config.js'; import { FastTxCollection } from './fast_tx_collection.js'; From 6682f5a6381e6897a7c7227e6a36345b885fe4b2 Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Tue, 10 Feb 2026 16:47:22 -0300 Subject: [PATCH 12/23] fix lint --- .../p2p/src/services/tx_collection/tx_collection_sink.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn-project/p2p/src/services/tx_collection/tx_collection_sink.ts b/yarn-project/p2p/src/services/tx_collection/tx_collection_sink.ts index 191487a25559..c70f1dbe5f87 100644 --- a/yarn-project/p2p/src/services/tx_collection/tx_collection_sink.ts +++ b/yarn-project/p2p/src/services/tx_collection/tx_collection_sink.ts @@ -2,12 +2,12 @@ import type { Logger } from '@aztec/foundation/log'; import { elapsed } from '@aztec/foundation/timer'; import type { TypedEventEmitter } from '@aztec/foundation/types'; import type { L2Block } from '@aztec/stdlib/block'; -import { type BlockHeader, type Tx, TxHash } from '@aztec/stdlib/tx'; +import type { BlockHeader, Tx } from '@aztec/stdlib/tx'; import type { TelemetryClient } from '@aztec/telemetry-client'; import EventEmitter from 'node:events'; -import type { TxPoolV2, TxPoolV2Events } from '../../mem_pools/tx_pool_v2/interfaces.js'; +import type { TxPoolV2, TxPoolV2Events } from '../../mem_pools/index.js'; import { TxCollectionInstrumentation } from './instrumentation.js'; import type { CollectionMethod } from './tx_collection.js'; From 4b4440679b61ed59a8c8141a7596c55ef2b95b85 Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Tue, 10 Feb 2026 17:05:49 -0300 Subject: [PATCH 13/23] fix test --- .../p2p/src/services/tx_file_store/tx_file_store.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/yarn-project/p2p/src/services/tx_file_store/tx_file_store.test.ts b/yarn-project/p2p/src/services/tx_file_store/tx_file_store.test.ts index 7baf7e4d1c99..e93ac6090203 100644 --- a/yarn-project/p2p/src/services/tx_file_store/tx_file_store.test.ts +++ b/yarn-project/p2p/src/services/tx_file_store/tx_file_store.test.ts @@ -321,14 +321,15 @@ describe('TxFileStore', () => { it('rejects tx when tx with wrong hash is returned', async () => { // Write a tx with a mismatched hash directly to the file store const invalidTx = Tx.random(); // random hash does not match computed hash - await fileStore.save(`txs/${invalidTx.txHash.toString()}.bin`, (await makeTx()).toBuffer(), { compress: false }); + const validTx = await makeTx(); + await fileStore.save(`txs/${invalidTx.txHash.toString()}.bin`, validTx.toBuffer(), { compress: false }); // Read it back via FileStoreTxSource const source = (await FileStoreTxSource.create(`file://${tmpDir}`, log))!; const result = await source.getTxsByHash([invalidTx.txHash]); expect(result.validTxs).toHaveLength(0); - expect(result.invalidTxHashes).toEqual([invalidTx.txHash.toString()]); + expect(result.invalidTxHashes).toEqual([validTx.txHash.toString()]); }); }); From 73f2bb154f2566e179baef8de32d5ce77c1b10ab Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Wed, 11 Feb 2026 13:18:28 -0300 Subject: [PATCH 14/23] fix build & suggestion --- .../tx_collection/file_store_tx_collection.ts | 2 +- .../services/tx_collection/file_store_tx_source.ts | 2 +- .../services/tx_file_store/tx_file_store.test.ts | 13 +++++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/yarn-project/p2p/src/services/tx_collection/file_store_tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/file_store_tx_collection.ts index ab2d1e48aa86..165ba3d9928a 100644 --- a/yarn-project/p2p/src/services/tx_collection/file_store_tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/file_store_tx_collection.ts @@ -126,7 +126,7 @@ export class FileStoreTxCollection { try { const result = await this.txCollectionSink.collect( - () => source.getTxsByHash([entry.txHash]), + () => source.getTxsByHash([TxHash.fromString(entry.txHash)]), [entry.txHash], { description: `file-store ${source.getInfo()}`, diff --git a/yarn-project/p2p/src/services/tx_collection/file_store_tx_source.ts b/yarn-project/p2p/src/services/tx_collection/file_store_tx_source.ts index db6498d3e18b..4dc2a1efcd6b 100644 --- a/yarn-project/p2p/src/services/tx_collection/file_store_tx_source.ts +++ b/yarn-project/p2p/src/services/tx_collection/file_store_tx_source.ts @@ -49,7 +49,7 @@ export class FileStoreTxSource implements TxSource { try { const buffer = await this.fileStore.read(path); const tx = Tx.fromBuffer(buffer); - if ((await tx.validateTxHash()) && txHash === tx.txHash) { + if ((await tx.validateTxHash()) && txHash.equals(tx.txHash)) { return tx; } else { invalidTxHashes.push(tx.txHash.toString()); diff --git a/yarn-project/p2p/src/services/tx_file_store/tx_file_store.test.ts b/yarn-project/p2p/src/services/tx_file_store/tx_file_store.test.ts index e93ac6090203..206b8b9cb3d8 100644 --- a/yarn-project/p2p/src/services/tx_file_store/tx_file_store.test.ts +++ b/yarn-project/p2p/src/services/tx_file_store/tx_file_store.test.ts @@ -331,6 +331,19 @@ describe('TxFileStore', () => { expect(result.validTxs).toHaveLength(0); expect(result.invalidTxHashes).toEqual([validTx.txHash.toString()]); }); + + it('accepts correct tx', async () => { + // Write a tx with a mismatched hash directly to the file store + const validTx = await makeTx(); + await fileStore.save(`txs/${validTx.txHash.toString()}.bin`, validTx.toBuffer(), { compress: false }); + + // Read it back via FileStoreTxSource + const source = (await FileStoreTxSource.create(`file://${tmpDir}`, log))!; + const result = await source.getTxsByHash([validTx.txHash]); + + expect(result.validTxs).toHaveLength(1); + expect(result.invalidTxHashes).toHaveLength(0); + }); }); describe('getPendingUploadCount', () => { From 5a3074f5b672baa1714be8e6195282c51fd81858 Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Wed, 11 Feb 2026 13:25:34 -0300 Subject: [PATCH 15/23] fix other suggestions --- .../services/tx_collection/fast_tx_collection.ts | 16 ++++++++-------- .../services/tx_collection/slow_tx_collection.ts | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts index 84cd0271b67b..ee8b2ce473cd 100644 --- a/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts @@ -348,14 +348,14 @@ export class FastTxCollection { txHash: tx.txHash.toString(), type: request.type, }); - // If we found all txs for this request, we resolve the promise - if (request.missingTxTracker.numberOfMissingTxs === 0) { - this.log.trace(`All txs found for fast collection request`, { - ...request.blockInfo, - type: request.type, - }); - request.promise.resolve(); - } + } + // If we found all txs for this request, we resolve the promise + if (request.missingTxTracker.numberOfMissingTxs === 0) { + this.log.trace(`All txs found for fast collection request`, { + ...request.blockInfo, + type: request.type, + }); + request.promise.resolve(); } } } diff --git a/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts index 0f9f371a94ee..a8f662c43c68 100644 --- a/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts @@ -196,7 +196,7 @@ export class SlowTxCollection { // from mined unproven blocks it has seen in the past. const fastRequests = this.fastCollection.getFastCollectionRequests(); const fastCollectionTxs: Set = new Set( - ...Array.from(fastRequests.values()).flatMap(r => r.missingTxTracker.missingTxHashes), + Array.from(fastRequests.values()).flatMap(r => r.missingTxTracker.missingTxHashes), ); // Return all missing txs that are not in fastCollectionTxs and are ready for reqresp if requested From 2c85faf35bcb40137a6f657fd524a5e6e72a50d1 Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Wed, 11 Feb 2026 13:38:04 -0300 Subject: [PATCH 16/23] actual fix --- .../p2p/src/services/tx_collection/slow_tx_collection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts index a8f662c43c68..12f75ff61514 100644 --- a/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/slow_tx_collection.ts @@ -196,7 +196,7 @@ export class SlowTxCollection { // from mined unproven blocks it has seen in the past. const fastRequests = this.fastCollection.getFastCollectionRequests(); const fastCollectionTxs: Set = new Set( - Array.from(fastRequests.values()).flatMap(r => r.missingTxTracker.missingTxHashes), + fastRequests.values().flatMap(r => Array.from(r.missingTxTracker.missingTxHashes)), ); // Return all missing txs that are not in fastCollectionTxs and are ready for reqresp if requested From cab041b64f8de566691b9c6475848749b77e70db Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Thu, 12 Feb 2026 11:09:10 -0300 Subject: [PATCH 17/23] fix merge --- .../tx_file_store/tx_file_store.test.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/yarn-project/p2p/src/services/tx_file_store/tx_file_store.test.ts b/yarn-project/p2p/src/services/tx_file_store/tx_file_store.test.ts index a8dd3c6ad5ab..c6c689fe279f 100644 --- a/yarn-project/p2p/src/services/tx_file_store/tx_file_store.test.ts +++ b/yarn-project/p2p/src/services/tx_file_store/tx_file_store.test.ts @@ -313,10 +313,12 @@ describe('TxFileStore', () => { it('rejects tx with invalid hash when reading from file store', async () => { // Write a tx with a mismatched hash directly to the file store const invalidTx = Tx.random(); // random hash does not match computed hash - await fileStore.save(`txs/${invalidTx.txHash.toString()}.bin`, invalidTx.toBuffer(), { compress: false }); + await fileStore.save(`${basePath}/txs/${invalidTx.txHash.toString()}.bin`, invalidTx.toBuffer(), { + compress: false, + }); // Read it back via FileStoreTxSource - const source = (await FileStoreTxSource.create(`file://${tmpDir}`, log))!; + const source = (await FileStoreTxSource.create(`file://${tmpDir}`, basePath, log))!; const result = await source.getTxsByHash([invalidTx.txHash]); expect(result.validTxs).toHaveLength(0); @@ -327,10 +329,12 @@ describe('TxFileStore', () => { // Write a tx with a mismatched hash directly to the file store const invalidTx = Tx.random(); // random hash does not match computed hash const validTx = await makeTx(); - await fileStore.save(`txs/${invalidTx.txHash.toString()}.bin`, validTx.toBuffer(), { compress: false }); + await fileStore.save(`${basePath}/txs/${invalidTx.txHash.toString()}.bin`, validTx.toBuffer(), { + compress: false, + }); // Read it back via FileStoreTxSource - const source = (await FileStoreTxSource.create(`file://${tmpDir}`, log))!; + const source = (await FileStoreTxSource.create(`file://${tmpDir}`, basePath, log))!; const result = await source.getTxsByHash([invalidTx.txHash]); expect(result.validTxs).toHaveLength(0); @@ -338,12 +342,14 @@ describe('TxFileStore', () => { }); it('accepts correct tx', async () => { - // Write a tx with a mismatched hash directly to the file store + // Write a tx with a correct hash directly to the file store const validTx = await makeTx(); - await fileStore.save(`txs/${validTx.txHash.toString()}.bin`, validTx.toBuffer(), { compress: false }); + await fileStore.save(`${basePath}/txs/${validTx.txHash.toString()}.bin`, validTx.toBuffer(), { + compress: false, + }); // Read it back via FileStoreTxSource - const source = (await FileStoreTxSource.create(`file://${tmpDir}`, log))!; + const source = (await FileStoreTxSource.create(`file://${tmpDir}`, basePath, log))!; const result = await source.getTxsByHash([validTx.txHash]); expect(result.validTxs).toHaveLength(1); From 497f35f07eeb6b2a4e49a7c7120d34e8b343a083 Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Mon, 16 Feb 2026 13:46:22 -0300 Subject: [PATCH 18/23] suggestions --- .../p2p_client.integration_batch_txs.test.ts | 2 +- .../proposal_tx_collector_worker.ts | 2 +- .../batch_tx_requester.test.ts | 2 +- .../batch-tx-requester/batch_tx_requester.ts | 8 +-- .../reqresp/batch-tx-requester/interface.ts | 18 ------- .../reqresp/batch-tx-requester/missing_txs.ts | 49 ++--------------- .../tx_collection/fast_tx_collection.ts | 6 +-- .../tx_collection/missing_txs_tracker.ts | 52 +++++++++++++++++++ .../tx_collection/proposal_tx_collector.ts | 3 +- .../services/tx_collection/tx_collection.ts | 6 ++- .../testbench/p2p_client_testbench_worker.ts | 2 +- 11 files changed, 71 insertions(+), 79 deletions(-) create mode 100644 yarn-project/p2p/src/services/tx_collection/missing_txs_tracker.ts diff --git a/yarn-project/p2p/src/client/test/p2p_client.integration_batch_txs.test.ts b/yarn-project/p2p/src/client/test/p2p_client.integration_batch_txs.test.ts index 648f1d0000e1..2a935748e48d 100644 --- a/yarn-project/p2p/src/client/test/p2p_client.integration_batch_txs.test.ts +++ b/yarn-project/p2p/src/client/test/p2p_client.integration_batch_txs.test.ts @@ -20,9 +20,9 @@ import type { AttestationPool } from '../../mem_pools/attestation_pool/attestati import type { TxPoolV2 } from '../../mem_pools/tx_pool_v2/interfaces.js'; import { BatchTxRequester } from '../../services/reqresp/batch-tx-requester/batch_tx_requester.js'; import type { BatchTxRequesterLibP2PService } from '../../services/reqresp/batch-tx-requester/interface.js'; -import { MissingTxsTracker } from '../../services/reqresp/batch-tx-requester/missing_txs.js'; import type { IBatchRequestTxValidator } from '../../services/reqresp/batch-tx-requester/tx_validator.js'; import type { ConnectionSampler } from '../../services/reqresp/connection-sampler/connection_sampler.js'; +import { MissingTxsTracker } from '../../services/tx_collection/missing_txs_tracker.js'; import { generatePeerIdPrivateKeys } from '../../test-helpers/generate-peer-id-private-keys.js'; import { getPorts } from '../../test-helpers/get-ports.js'; import { makeEnrs } from '../../test-helpers/make-enrs.js'; diff --git a/yarn-project/p2p/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts b/yarn-project/p2p/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts index 68e068b6ca32..45fba6b2d92a 100644 --- a/yarn-project/p2p/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts +++ b/yarn-project/p2p/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts @@ -17,9 +17,9 @@ import { peerIdFromString } from '@libp2p/peer-id'; import type { P2PConfig } from '../../../config.js'; import { BatchTxRequesterCollector, SendBatchRequestCollector } from '../../../services/index.js'; -import { MissingTxsTracker } from '../../../services/reqresp/batch-tx-requester/missing_txs.js'; import type { IBatchRequestTxValidator } from '../../../services/reqresp/batch-tx-requester/tx_validator.js'; import { RateLimitStatus } from '../../../services/reqresp/rate-limiter/rate_limiter.js'; +import { MissingTxsTracker } from '../../../services/tx_collection/missing_txs_tracker.js'; import { AlwaysTrueCircuitVerifier } from '../../../test-helpers/index.js'; import { BENCHMARK_CONSTANTS, diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.test.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.test.ts index 9645d7a5970b..03f37cad2482 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.test.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.test.ts @@ -17,6 +17,7 @@ import { describe, expect, it, jest } from '@jest/globals'; import { type MockProxy, mock } from 'jest-mock-extended'; import { createSecp256k1PeerId } from '../../../index.js'; +import { MissingTxsTracker } from '../../tx_collection/missing_txs_tracker.js'; import type { ConnectionSampler } from '../connection-sampler/connection_sampler.js'; import type { ReqRespInterface } from '../interface.js'; import { BitVector, BlockTxsRequest, BlockTxsResponse } from '../protocols/index.js'; @@ -24,7 +25,6 @@ import { ReqRespStatus } from '../status.js'; import { BatchTxRequester } from './batch_tx_requester.js'; import { DEFAULT_BATCH_TX_REQUESTER_BAD_PEER_THRESHOLD, DEFAULT_BATCH_TX_REQUESTER_TX_BATCH_SIZE } from './config.js'; import type { BatchTxRequesterLibP2PService, IPeerPenalizer } from './interface.js'; -import { MissingTxsTracker } from './missing_txs.js'; import { type IPeerCollection, PeerCollection, RATE_LIMIT_EXCEEDED_PEER_CACHE_TTL } from './peer_collection.js'; import type { IBatchRequestTxValidator } from './tx_validator.js'; diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.ts index f2616091801d..309b41c3f4bd 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/batch_tx_requester.ts @@ -10,6 +10,7 @@ import { Tx, TxArray, TxHash } from '@aztec/stdlib/tx'; import type { PeerId } from '@libp2p/interface'; import { peerIdFromString } from '@libp2p/peer-id'; +import type { IMissingTxsTracker } from '../../tx_collection/missing_txs_tracker.js'; import { ReqRespSubProtocol } from '.././interface.js'; import { BlockTxsRequest, BlockTxsResponse, type BlockTxsSource } from '.././protocols/index.js'; import { ReqRespStatus } from '.././status.js'; @@ -19,12 +20,7 @@ import { DEFAULT_BATCH_TX_REQUESTER_SMART_PARALLEL_WORKER_COUNT, DEFAULT_BATCH_TX_REQUESTER_TX_BATCH_SIZE, } from './config.js'; -import type { - BatchTxRequesterLibP2PService, - BatchTxRequesterOptions, - IMissingTxsTracker, - ITxMetadataCollection, -} from './interface.js'; +import type { BatchTxRequesterLibP2PService, BatchTxRequesterOptions, ITxMetadataCollection } from './interface.js'; import { MissingTxMetadataCollection } from './missing_txs.js'; import { type IPeerCollection, PeerCollection } from './peer_collection.js'; import { BatchRequestTxValidator, type IBatchRequestTxValidator } from './tx_validator.js'; diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts index 8da8fe400ebc..f3e895b2d7a9 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/interface.ts @@ -13,24 +13,6 @@ export interface IPeerPenalizer { penalizePeer(peerId: PeerId, penalty: PeerErrorSeverity): void; } -/** - * Tracks which transactions are still missing and need to be fetched. - * Allows external code to mark transactions as fetched, enabling coordination - * between multiple fetching mechanisms (e.g., BatchTxRequester and Rpc Node requests). - */ -export interface IMissingTxsTracker { - /** Returns the set of transaction hashes that are still missing. */ - get missingTxHashes(): Set; - /** Size of this.missingTxHashes */ - get numberOfMissingTxs(): number; - /** Checks that transaction is still missing */ - isMissing(txHash: string): boolean; - /** Marks a transaction as fetched. Returns true if it was previously missing. */ - markFetched(tx: Tx): boolean; - /** Get list of collected txs */ - get collectedTxs(): Tx[]; -} - export interface ITxMetadataCollection { getMissingTxHashes(): Set; markFetched(peerId: PeerId, tx: Tx): boolean; diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts index 6749df2cc832..6ab065061f0f 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts @@ -2,39 +2,13 @@ import { type Tx, TxHash } from '@aztec/stdlib/tx'; import type { PeerId } from '@libp2p/interface'; +import type { IMissingTxsTracker } from '../../tx_collection/missing_txs_tracker.js'; import { DEFAULT_BATCH_TX_REQUESTER_TX_BATCH_SIZE } from './config.js'; -import type { IMissingTxsTracker, ITxMetadataCollection } from './interface.js'; - -export class MissingTxsTracker implements IMissingTxsTracker { - public readonly collectedTxs: Tx[] = []; - - private constructor(public readonly missingTxHashes: Set) {} - - public static fromArray(hashes: TxHash[] | string[]) { - return new MissingTxsTracker(new Set(hashes.map(hash => hash.toString()))); - } - - markFetched(tx: Tx): boolean { - if (this.missingTxHashes.delete(tx.txHash.toString())) { - this.collectedTxs.push(tx); - return true; - } - return false; - } - - get numberOfMissingTxs(): number { - return this.missingTxHashes.size; - } - - isMissing(txHash: string): boolean { - return this.missingTxHashes.has(txHash.toString()); - } -} +import type { ITxMetadataCollection } from './interface.js'; class MissingTxMetadata { constructor( public readonly txHash: string, - public fetched = false, public requestedCount = 0, public inFlightCount = 0, public tx: Tx | undefined = undefined, @@ -56,20 +30,6 @@ class MissingTxMetadata { public isInFlight(): boolean { return this.inFlightCount > 0; } - - //Returns true if this is the first time we mark it as fetched - public markAsFetched(peerId: PeerId, tx: Tx): boolean { - if (this.fetched) { - return false; - } - - this.fetched = true; - this.tx = tx; - - this.peers.add(peerId.toString()); - - return true; - } } /* @@ -168,7 +128,7 @@ export class MissingTxMetadataCollection implements ITxMetadataCollection { } public alreadyFetched(txHash: TxHash): boolean { - return this.txMetadata.get(txHash.toString())?.fetched ?? false; + return this.missingTxsTracker.isMissing(txHash.toString()); } public markFetched(peerId: PeerId, tx: Tx): boolean { @@ -183,8 +143,7 @@ export class MissingTxMetadataCollection implements ITxMetadataCollection { return false; } - this.missingTxsTracker.markFetched(tx); - return txMeta.markAsFetched(peerId, tx); + return this.missingTxsTracker.markFetched(tx); } public markPeerHas(peerId: PeerId, txHash: TxHash[]) { diff --git a/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts index ee8b2ce473cd..3bb42ffb907f 100644 --- a/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts @@ -13,8 +13,8 @@ import type { PeerId } from '@libp2p/interface'; import type { BatchTxRequesterConfig } from '../reqresp/batch-tx-requester/config.js'; import type { BatchTxRequesterLibP2PService } from '../reqresp/batch-tx-requester/interface.js'; -import { MissingTxsTracker } from '../reqresp/batch-tx-requester/missing_txs.js'; import type { TxCollectionConfig } from './config.js'; +import { MissingTxsTracker } from './missing_txs_tracker.js'; import { BatchTxRequesterCollector, type MissingTxsCollector, @@ -124,7 +124,7 @@ export class FastTxCollection { await Promise.race([request.promise.promise, waitBeforeReqResp]); // If we have collected all txs, we can stop here - if (request.missingTxTracker.numberOfMissingTxs === 0) { + if (request.missingTxTracker.allFetched()) { this.log.debug(`All txs collected for slot ${blockInfo.slotNumber} without reqresp`, blockInfo); return; } @@ -138,7 +138,7 @@ export class FastTxCollection { const logCtx = { ...blockInfo, errorMessage: err instanceof Error ? err.message : undefined, - missingTxs: [...request.missingTxTracker.missingTxHashes].map(txHash => txHash.toString()), + missingTxs: request.missingTxTracker.missingTxHashes.values().map(txHash => txHash.toString()), }; if (err instanceof Error && err.name === 'TimeoutError') { this.log.warn(`Timed out collecting txs for ${request.type} at slot ${blockInfo.slotNumber}`, logCtx); diff --git a/yarn-project/p2p/src/services/tx_collection/missing_txs_tracker.ts b/yarn-project/p2p/src/services/tx_collection/missing_txs_tracker.ts new file mode 100644 index 000000000000..d206a2fdccd9 --- /dev/null +++ b/yarn-project/p2p/src/services/tx_collection/missing_txs_tracker.ts @@ -0,0 +1,52 @@ +import { TxHash } from '@aztec/stdlib/tx'; +import type { Tx } from '@aztec/stdlib/tx'; + +/** + * Tracks which transactions are still missing and need to be fetched. + * Allows external code to mark transactions as fetched, enabling coordination + * between multiple fetching mechanisms (e.g., BatchTxRequester and Rpc Node requests). + */ +export interface IMissingTxsTracker { + /** Returns the set of transaction hashes that are still missing. */ + get missingTxHashes(): Set; + /** Size of this.missingTxHashes */ + get numberOfMissingTxs(): number; + /** Are all requested txs are fetched */ + allFetched(): boolean; + /** Checks that transaction is still missing */ + isMissing(txHash: string): boolean; + /** Marks a transaction as fetched. Returns true if it was previously missing. */ + markFetched(tx: Tx): boolean; + /** Get list of collected txs */ + get collectedTxs(): Tx[]; +} + +export class MissingTxsTracker implements IMissingTxsTracker { + public readonly collectedTxs: Tx[] = []; + + private constructor(public readonly missingTxHashes: Set) {} + + public static fromArray(hashes: TxHash[] | string[]) { + return new MissingTxsTracker(new Set(hashes.map(hash => hash.toString()))); + } + + markFetched(tx: Tx): boolean { + if (this.missingTxHashes.delete(tx.txHash.toString())) { + this.collectedTxs.push(tx); + return true; + } + return false; + } + + get numberOfMissingTxs(): number { + return this.missingTxHashes.size; + } + + allFetched(): boolean { + return this.numberOfMissingTxs === 0; + } + + isMissing(txHash: string): boolean { + return this.missingTxHashes.has(txHash.toString()); + } +} diff --git a/yarn-project/p2p/src/services/tx_collection/proposal_tx_collector.ts b/yarn-project/p2p/src/services/tx_collection/proposal_tx_collector.ts index 448e7e01c987..5955631c4247 100644 --- a/yarn-project/p2p/src/services/tx_collection/proposal_tx_collector.ts +++ b/yarn-project/p2p/src/services/tx_collection/proposal_tx_collector.ts @@ -6,9 +6,10 @@ import type { PeerId } from '@libp2p/interface'; import { BatchTxRequester } from '../reqresp/batch-tx-requester/batch_tx_requester.js'; import type { BatchTxRequesterConfig } from '../reqresp/batch-tx-requester/config.js'; -import type { BatchTxRequesterLibP2PService, IMissingTxsTracker } from '../reqresp/batch-tx-requester/interface.js'; +import type { BatchTxRequesterLibP2PService } from '../reqresp/batch-tx-requester/interface.js'; import type { IBatchRequestTxValidator } from '../reqresp/batch-tx-requester/tx_validator.js'; import { type BlockTxsSource, ReqRespSubProtocol, chunkTxHashesRequest } from '../reqresp/index.js'; +import type { IMissingTxsTracker } from './missing_txs_tracker.js'; /** * Strategy interface for collecting missing transactions for a block or proposal. diff --git a/yarn-project/p2p/src/services/tx_collection/tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/tx_collection.ts index e3a4fa1dc11e..9797b6bd34a5 100644 --- a/yarn-project/p2p/src/services/tx_collection/tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/tx_collection.ts @@ -7,17 +7,19 @@ import { DateProvider } from '@aztec/foundation/timer'; import type { L2Block, L2BlockInfo } from '@aztec/stdlib/block'; import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers'; import type { BlockProposal } from '@aztec/stdlib/p2p'; -import { Tx, TxHash } from '@aztec/stdlib/tx'; +import type { Tx } from '@aztec/stdlib/tx'; +import { TxHash } from '@aztec/stdlib/tx'; import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client'; import type { PeerId } from '@libp2p/interface'; import type { TxPoolV2, TxPoolV2Events } from '../../mem_pools/tx_pool_v2/interfaces.js'; -import type { BatchTxRequesterLibP2PService, IMissingTxsTracker } from '../reqresp/batch-tx-requester/interface.js'; +import type { BatchTxRequesterLibP2PService } from '../reqresp/batch-tx-requester/interface.js'; import type { TxCollectionConfig } from './config.js'; import { FastTxCollection } from './fast_tx_collection.js'; import { FileStoreTxCollection } from './file_store_tx_collection.js'; import type { FileStoreTxSource } from './file_store_tx_source.js'; +import type { IMissingTxsTracker } from './missing_txs_tracker.js'; import { SlowTxCollection, getProofDeadlineForSlot } from './slow_tx_collection.js'; import { type TxAddContext, TxCollectionSink } from './tx_collection_sink.js'; import type { TxSource } from './tx_source.js'; diff --git a/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts b/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts index 313d41e693b5..8dc07e2ceb42 100644 --- a/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts +++ b/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts @@ -36,11 +36,11 @@ import type { MemPools } from '../mem_pools/index.js'; import { BatchTxRequesterCollector, LibP2PService, SendBatchRequestCollector } from '../services/index.js'; import type { PeerManager } from '../services/peer-manager/peer_manager.js'; import type { BatchTxRequesterLibP2PService } from '../services/reqresp/batch-tx-requester/interface.js'; -import { MissingTxsTracker } from '../services/reqresp/batch-tx-requester/missing_txs.js'; import type { IBatchRequestTxValidator } from '../services/reqresp/batch-tx-requester/tx_validator.js'; import { RateLimitStatus } from '../services/reqresp/rate-limiter/rate_limiter.js'; import type { ReqResp } from '../services/reqresp/reqresp.js'; import type { PeerDiscoveryService } from '../services/service.js'; +import { MissingTxsTracker } from '../services/tx_collection/missing_txs_tracker.js'; import { AlwaysTrueCircuitVerifier } from '../test-helpers/index.js'; import { BENCHMARK_CONSTANTS, From 1e81f41b8f45ece15ff3eeb2b0642e8afc1a394f Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Mon, 16 Feb 2026 13:49:33 -0300 Subject: [PATCH 19/23] fix --- .../p2p/src/services/tx_collection/fast_tx_collection.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts index 3bb42ffb907f..ebd7b628f57a 100644 --- a/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts @@ -183,9 +183,7 @@ export class FastTxCollection { opts: { deadline: Date }, ) { const notFinished = () => - this.dateProvider.now() <= +opts.deadline && - request.missingTxTracker.numberOfMissingTxs > 0 && - this.requests.has(request); + this.dateProvider.now() <= +opts.deadline && !request.missingTxTracker.allFetched() && this.requests.has(request); const maxParallelRequests = this.config.txCollectionFastMaxParallelRequestsPerNode; const maxBatchSize = this.config.txCollectionNodeRpcMaxBatchSize; @@ -350,7 +348,7 @@ export class FastTxCollection { }); } // If we found all txs for this request, we resolve the promise - if (request.missingTxTracker.numberOfMissingTxs === 0) { + if (request.missingTxTracker.allFetched()) { this.log.trace(`All txs found for fast collection request`, { ...request.blockInfo, type: request.type, From 0bfa8427cf668a5bb79e1ad47ae87df405dcf51a Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Mon, 16 Feb 2026 13:52:11 -0300 Subject: [PATCH 20/23] fix --- .../p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts index 6ab065061f0f..1072a3f36f48 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts @@ -143,6 +143,7 @@ export class MissingTxMetadataCollection implements ITxMetadataCollection { return false; } + txMeta.peers.add(peerId.toString()); return this.missingTxsTracker.markFetched(tx); } From c43786e88c7806ece0db2cf54b92521f2a385e3f Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Tue, 17 Feb 2026 13:56:49 -0300 Subject: [PATCH 21/23] fix inverted flag --- .../p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts index 1072a3f36f48..56705595faf9 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/missing_txs.ts @@ -128,7 +128,7 @@ export class MissingTxMetadataCollection implements ITxMetadataCollection { } public alreadyFetched(txHash: TxHash): boolean { - return this.missingTxsTracker.isMissing(txHash.toString()); + return !this.missingTxsTracker.isMissing(txHash.toString()); } public markFetched(peerId: PeerId, tx: Tx): boolean { From dcb856cd06ed33e80dac2a536441e712111c28a0 Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Tue, 17 Feb 2026 14:53:10 -0300 Subject: [PATCH 22/23] type --- .../src/services/tx_collection/file_store_tx_source.ts | 4 ++-- .../p2p/src/services/tx_collection/tx_collection_sink.ts | 3 ++- yarn-project/p2p/src/services/tx_collection/tx_source.ts | 8 +++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/yarn-project/p2p/src/services/tx_collection/file_store_tx_source.ts b/yarn-project/p2p/src/services/tx_collection/file_store_tx_source.ts index aeeee247ad17..3769e2544e4a 100644 --- a/yarn-project/p2p/src/services/tx_collection/file_store_tx_source.ts +++ b/yarn-project/p2p/src/services/tx_collection/file_store_tx_source.ts @@ -2,7 +2,7 @@ import { type Logger, createLogger } from '@aztec/foundation/log'; import { type ReadOnlyFileStore, createReadOnlyFileStore } from '@aztec/stdlib/file-store'; import { Tx, type TxHash } from '@aztec/stdlib/tx'; -import type { TxSource } from './tx_source.js'; +import type { TxSource, TxSourceCollectionResult } from './tx_source.js'; /** TxSource implementation that downloads txs from a file store. */ export class FileStoreTxSource implements TxSource { @@ -41,7 +41,7 @@ export class FileStoreTxSource implements TxSource { return `file-store:${this.baseUrl}`; } - public async getTxsByHash(txHashes: TxHash[]): Promise<{ validTxs: Tx[]; invalidTxHashes: string[] }> { + public async getTxsByHash(txHashes: TxHash[]): Promise { const invalidTxHashes: string[] = []; return { validTxs: ( diff --git a/yarn-project/p2p/src/services/tx_collection/tx_collection_sink.ts b/yarn-project/p2p/src/services/tx_collection/tx_collection_sink.ts index c70f1dbe5f87..7848111d16b0 100644 --- a/yarn-project/p2p/src/services/tx_collection/tx_collection_sink.ts +++ b/yarn-project/p2p/src/services/tx_collection/tx_collection_sink.ts @@ -10,6 +10,7 @@ import EventEmitter from 'node:events'; import type { TxPoolV2, TxPoolV2Events } from '../../mem_pools/index.js'; import { TxCollectionInstrumentation } from './instrumentation.js'; import type { CollectionMethod } from './tx_collection.js'; +import type { TxSourceCollectionResult } from './tx_source.js'; /** Context determining how collected txs should be added to the pool. */ export type TxAddContext = { type: 'proposal'; blockHeader: BlockHeader } | { type: 'mined'; block: L2Block }; @@ -31,7 +32,7 @@ export class TxCollectionSink extends (EventEmitter as new () => TypedEventEmitt } public async collect( - collectValidTxsFn: () => Promise<{ validTxs: Tx[]; invalidTxHashes: string[] }>, + collectValidTxsFn: () => Promise, requested: string[], info: Record & { description: string; method: CollectionMethod }, context: TxAddContext, diff --git a/yarn-project/p2p/src/services/tx_collection/tx_source.ts b/yarn-project/p2p/src/services/tx_collection/tx_source.ts index da580294825f..68e2ad8c1c43 100644 --- a/yarn-project/p2p/src/services/tx_collection/tx_source.ts +++ b/yarn-project/p2p/src/services/tx_collection/tx_source.ts @@ -6,9 +6,11 @@ import type { Tx, TxHash } from '@aztec/stdlib/tx'; import { type ComponentsVersions, getComponentsVersionsFromConfig } from '@aztec/stdlib/versioning'; import { makeTracedFetch } from '@aztec/telemetry-client'; +export type TxSourceCollectionResult = { validTxs: Tx[]; invalidTxHashes: string[] }; + export interface TxSource { getInfo(): string; - getTxsByHash(txHashes: TxHash[]): Promise<{ validTxs: Tx[]; invalidTxHashes: string[] }>; + getTxsByHash(txHashes: TxHash[]): Promise; } export class NodeRpcTxSource implements TxSource { @@ -26,11 +28,11 @@ export class NodeRpcTxSource implements TxSource { return this.info; } - public async getTxsByHash(txHashes: TxHash[]): Promise<{ validTxs: Tx[]; invalidTxHashes: string[] }> { + public async getTxsByHash(txHashes: TxHash[]): Promise { return this.verifyTxs(await this.client.getTxsByHash(txHashes)); } - private async verifyTxs(txs: Tx[]): Promise<{ validTxs: Tx[]; invalidTxHashes: string[] }> { + private async verifyTxs(txs: Tx[]): Promise { // Validate tx hashes for all collected txs from external sources const validTxs: Tx[] = []; const invalidTxHashes: string[] = []; From b103742ab25b515b73365f2d43002866b0cbbebc Mon Sep 17 00:00:00 2001 From: Nikita Meshcheriakov Date: Tue, 17 Feb 2026 15:42:36 -0300 Subject: [PATCH 23/23] fix build --- .../p2p/src/services/tx_collection/file_store_tx_source.ts | 1 + .../p2p/src/services/tx_file_store/tx_file_store.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/yarn-project/p2p/src/services/tx_collection/file_store_tx_source.ts b/yarn-project/p2p/src/services/tx_collection/file_store_tx_source.ts index 4f8fe32b8290..d682e303a0cf 100644 --- a/yarn-project/p2p/src/services/tx_collection/file_store_tx_source.ts +++ b/yarn-project/p2p/src/services/tx_collection/file_store_tx_source.ts @@ -71,6 +71,7 @@ export class FileStoreTxSource implements TxSource { await Promise.all( txHashes.map(async txHash => { const path = `${this.basePath}/txs/${txHash.toString()}.bin`; + const timer = new Timer(); try { const buffer = await this.fileStore.read(path); const tx = Tx.fromBuffer(buffer); diff --git a/yarn-project/p2p/src/services/tx_file_store/tx_file_store.test.ts b/yarn-project/p2p/src/services/tx_file_store/tx_file_store.test.ts index 08c40c9283e5..893a0b54aa9d 100644 --- a/yarn-project/p2p/src/services/tx_file_store/tx_file_store.test.ts +++ b/yarn-project/p2p/src/services/tx_file_store/tx_file_store.test.ts @@ -392,9 +392,9 @@ describe('TxFileStore', () => { expect(txSource).toBeDefined(); const results = await txSource!.getTxsByHash([tx.getTxHash()]); - expect(results).toHaveLength(1); - expect(results[0]).toBeDefined(); - expect(results[0]!.toBuffer()).toEqual(tx.toBuffer()); + expect(results.validTxs).toHaveLength(1); + expect(results.validTxs[0]).toBeDefined(); + expect(results.validTxs[0]!.toBuffer()).toEqual(tx.toBuffer()); }); }); });