Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions yarn-project/foundation/src/config/env_var.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ export type EnvVar =
| 'TX_COLLECTION_FILE_STORE_FAST_WORKER_COUNT'
| 'TX_COLLECTION_FILE_STORE_FAST_BACKOFF_BASE_MS'
| 'TX_COLLECTION_FILE_STORE_FAST_BACKOFF_MAX_MS'
| 'TX_VALIDATION_CACHE_SIZE'
| 'TX_FILE_STORE_URL'
| 'TX_FILE_STORE_UPLOAD_CONCURRENCY'
| 'TX_FILE_STORE_MAX_QUEUE_SIZE'
Expand Down
1 change: 1 addition & 0 deletions yarn-project/p2p/src/client/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export async function createP2PClient(
const txValidatorForTxCollection = createTxValidatorForOnDemandReceivedTxs(proofVerifier, config);
const txValidationCache = new SharedTxValidationCache(
txValidatorForTxCollection,
config.txValidationCacheSize,
logger.createChild('shared-tx-validation-cache'),
);
const nodeSources = [
Expand Down
7 changes: 7 additions & 0 deletions yarn-project/p2p/src/services/tx_collection/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export type TxCollectionConfig = {
txCollectionFileStoreFastBackoffBaseMs: number;
/** Max backoff time in ms for fast file store collection retries */
txCollectionFileStoreFastBackoffMaxMs: number;
/** Max size of the tx validation cache (LRU) */
txValidationCacheSize: number;
};

export const txCollectionConfigMappings: ConfigMappingsType<TxCollectionConfig> = {
Expand Down Expand Up @@ -86,4 +88,9 @@ export const txCollectionConfigMappings: ConfigMappingsType<TxCollectionConfig>
description: 'Max backoff time in ms for fast file store collection retries',
...numberConfigHelper(5_000),
},
txValidationCacheSize: {
env: 'TX_VALIDATION_CACHE_SIZE',
description: 'Max size of the tx validation cache (LRU)',
...numberConfigHelper(5_000),
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe('SharedTxValidationCache', () => {
beforeEach(() => {
validator = mock<TxValidator>();
validator.validateTx.mockResolvedValue({ result: 'valid' });
cache = new SharedTxValidationCache(validator, createLogger('test'));
cache = new SharedTxValidationCache(validator, 100, createLogger('test'));
});

it('accepts a valid tx', async () => {
Expand Down Expand Up @@ -176,6 +176,24 @@ describe('SharedTxValidationCache', () => {
expect(validator.validateTx).toHaveBeenCalledTimes(2);
});

it('re-validates after a hash is evicted from the LRU', async () => {
const smallCache = new SharedTxValidationCache(validator, 2, createLogger('test'));
const tx1 = await makeTx();
const tx2 = await makeTx();
const tx3 = await makeTx();

await smallCache.submit(tx1);
await smallCache.submit(tx2);
await smallCache.submit(tx3);
expect(validator.validateTx).toHaveBeenCalledTimes(3);

// tx1 was the least recently used and should have been evicted; resubmitting re-runs the validator.
const replay = await makeTx(tx1.txHash);
const outcome = await smallCache.submit(replay);
expect(outcome.status).toBe('accepted');
expect(validator.validateTx).toHaveBeenCalledTimes(4);
});

it('skips after caching even when submitted from a different caller (cross-source dedup)', async () => {
const hash = TxHash.random();
const txFromSourceA = await makeTx(hash);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { LruSet } from '@aztec/foundation/collection';
import type { Logger } from '@aztec/foundation/log';
import type { Tx, TxValidationResult, TxValidator } from '@aztec/stdlib/tx';

Expand Down Expand Up @@ -41,10 +42,10 @@ export class SharedTxValidationCache implements ISharedTxValidationCache {
/**
* Hashes that have validated successfully at least once. Membership here is the cache:
* any future `submit` for one of these hashes short-circuits to `skipped` without invoking
* the validator. Entries are never removed — see the class-level caching policy for why
* invalid outcomes are not tracked here.
* the validator. Bounded LRU — see the class-level caching policy for why invalid outcomes
* are not tracked here. An evicted hash simply re-validates on next submission.
*/
private readonly validatedHashes = new Set<string>();
private readonly validatedHashes: LruSet<string>;
/**
* Per-hash FIFO of submissions awaiting validation. Presence of a key here means a
* `processHash` drain loop is currently running for that hash; absence means no loop is
Expand All @@ -56,8 +57,11 @@ export class SharedTxValidationCache implements ISharedTxValidationCache {

constructor(
private readonly validator: TxValidator<Tx>,
maxSize: number,
private readonly logger: Logger,
) {}
) {
this.validatedHashes = new LruSet<string>(maxSize);
}

/** Submit a tx for validation. */
public submit(tx: Tx): Promise<TxValidationOutcome> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ async function runAggregatorBenchmark(
const noopTxValidator: TxValidator = {
validateTx: (_tx: Tx): Promise<TxValidationResult> => Promise.resolve({ result: 'valid' }),
};
const validationCache = new SharedTxValidationCache(noopTxValidator, logger);
const validationCache = new SharedTxValidationCache(noopTxValidator, 5_000, logger);

timer = new Timer();
const tracker = RequestTracker.create(txHashes, new Date(Date.now() + timeoutMs));
Expand Down
Loading