From 6b4f359e35639a9a5b997fa4a2dc592e6a3f55d4 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Tue, 24 Feb 2026 19:52:42 +0000 Subject: [PATCH] Reduce tx hash conversions inside tx pool --- .../foundation/src/curves/bn254/field.ts | 8 ++- .../eviction/eviction_manager.test.ts | 30 +-------- .../fee_payer_balance_eviction_rule.test.ts | 50 ++++++--------- .../fee_payer_balance_pre_add_rule.test.ts | 30 +++------ .../fee_payer_balance_pre_add_rule.ts | 3 + .../invalid_txs_after_mining_rule.test.ts | 44 +++++-------- .../invalid_txs_after_reorg_rule.test.ts | 43 +++++-------- .../low_priority_pre_add_rule.test.ts | 22 ++----- .../eviction/nullifier_conflict_rule.test.ts | 21 +------ .../p2p/src/mem_pools/tx_pool_v2/index.ts | 2 +- .../mem_pools/tx_pool_v2/tx_metadata.test.ts | 38 ++---------- .../src/mem_pools/tx_pool_v2/tx_metadata.ts | 62 ++++++++++++++++--- .../mem_pools/tx_pool_v2/tx_pool_indices.ts | 22 +++---- 13 files changed, 147 insertions(+), 228 deletions(-) diff --git a/yarn-project/foundation/src/curves/bn254/field.ts b/yarn-project/foundation/src/curves/bn254/field.ts index a5f81149ea42..80d228b470ac 100644 --- a/yarn-project/foundation/src/curves/bn254/field.ts +++ b/yarn-project/foundation/src/curves/bn254/field.ts @@ -118,14 +118,18 @@ abstract class BaseField { } cmp(rhs: BaseField): -1 | 0 | 1 { - const rhsBigInt = rhs.asBigInt; - return this.asBigInt === rhsBigInt ? 0 : this.asBigInt < rhsBigInt ? -1 : 1; + return BaseField.cmpAsBigInt(this.asBigInt, rhs.asBigInt); } static cmp(lhs: BaseField, rhs: BaseField): -1 | 0 | 1 { return lhs.cmp(rhs); } + // Actual bigint comparison. Arguments must have been validated previously. + static cmpAsBigInt(lhs: bigint, rhs: bigint): -1 | 0 | 1 { + return lhs === rhs ? 0 : lhs < rhs ? -1 : 1; + } + isZero(): boolean { return this.asBigInt === 0n; } diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/eviction_manager.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/eviction_manager.test.ts index 9ffb86919685..a1fda0cd2532 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/eviction_manager.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/eviction_manager.test.ts @@ -4,7 +4,7 @@ import { BlockHeader } from '@aztec/stdlib/tx'; import { type MockProxy, mock } from 'jest-mock-extended'; -import { type TxMetaData, stubTxMetaValidationData } from '../tx_metadata.js'; +import { type TxMetaData, stubTxMetaData } from '../tx_metadata.js'; import { EvictionManager } from './eviction_manager.js'; import { EvictionEvent, @@ -174,19 +174,7 @@ describe('EvictionManager', () => { let preAddRule: MockProxy; let poolAccess: MockProxy; - const createMeta = (txHash: string, priorityFee: bigint): TxMetaData => ({ - txHash, - anchorBlockHeaderHash: '0x1234', - priorityFee, - feePayer: '0xfeepayer', - claimAmount: 0n, - feeLimit: 100n, - nullifiers: [`0x${txHash.slice(2)}null1`], - expirationTimestamp: 0n, - receivedAt: 0, - estimatedSizeBytes: 0, - data: stubTxMetaValidationData(), - }); + const createMeta = (txHash: string, priorityFee: bigint): TxMetaData => stubTxMetaData(txHash, { priorityFee }); beforeEach(() => { preAddRule = mock({ name: 'preAddRule' }); @@ -330,19 +318,7 @@ describe('EvictionManager', () => { const preAddRule2 = mock({ name: 'secondRule' }); const poolAccess = mock(); - const createMeta = (txHash: string, priorityFee: bigint): TxMetaData => ({ - txHash, - anchorBlockHeaderHash: '0x1234', - priorityFee, - feePayer: '0xfeepayer', - claimAmount: 0n, - feeLimit: 100n, - nullifiers: [`0x${txHash.slice(2)}null1`], - expirationTimestamp: 0n, - receivedAt: 0, - estimatedSizeBytes: 0, - data: stubTxMetaValidationData(), - }); + const createMeta = (txHash: string, priorityFee: bigint): TxMetaData => stubTxMetaData(txHash, { priorityFee }); preAddRule1.check.mockRejectedValue(new Error('Rule failed')); preAddRule2.check.mockResolvedValue({ diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.test.ts index ecb671eb7d80..7341a099f5da 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_eviction_rule.test.ts @@ -7,7 +7,7 @@ import { BlockHeader, GlobalVariables } from '@aztec/stdlib/tx'; import { jest } from '@jest/globals'; import { type MockProxy, mock } from 'jest-mock-extended'; -import { type TxMetaData, stubTxMetaValidationData } from '../tx_metadata.js'; +import { type TxMetaData, stubTxMetaData } from '../tx_metadata.js'; import { FeePayerBalanceEvictionRule } from './fee_payer_balance_eviction_rule.js'; import type { EvictionContext, PoolOperations } from './interfaces.js'; import { EvictionEvent } from './interfaces.js'; @@ -33,19 +33,7 @@ describe('FeePayerBalanceEvictionRule', () => { claimAmount?: bigint; feePayer?: string; } = {}, - ): TxMetaData => ({ - txHash, - anchorBlockHeaderHash: '0x1234', - priorityFee: opts.priorityFee ?? 100n, - feePayer: opts.feePayer ?? feePayer1, - claimAmount: opts.claimAmount ?? 0n, - feeLimit: opts.feeLimit ?? 100n, - nullifiers: [`0x${txHash.slice(2)}null1`], - expirationTimestamp: 0n, - receivedAt: 0, - estimatedSizeBytes: 0, - data: stubTxMetaValidationData(), - }); + ) => stubTxMetaData(txHash, { feePayer: feePayer1, ...opts }); // Create mock pool operations const createPoolOps = (txsByFeePayer: Map): PoolOperations => { @@ -144,8 +132,8 @@ describe('FeePayerBalanceEvictionRule', () => { const result = await rule.evict(context, pool); expect(result.success).toBe(true); - expect(result.txsEvicted).toEqual(['0x1111']); // Low priority evicted - expect(deleteTxsMock).toHaveBeenCalledWith(['0x1111'], 'FeePayerBalanceEviction'); + expect(result.txsEvicted).toEqual([lowPriorityMeta.txHash]); // Low priority evicted + expect(deleteTxsMock).toHaveBeenCalledWith([lowPriorityMeta.txHash], 'FeePayerBalanceEviction'); }); it('evicts multiple low-priority txs when balance is insufficient', async () => { @@ -160,7 +148,7 @@ describe('FeePayerBalanceEvictionRule', () => { const context: EvictionContext = { event: EvictionEvent.TXS_ADDED, - newTxHashes: ['0x1111', '0x2222', '0x3333'], + newTxHashes: [lowMeta.txHash, medMeta.txHash, highMeta.txHash], feePayers: [feePayer1], }; @@ -168,9 +156,9 @@ describe('FeePayerBalanceEvictionRule', () => { expect(result.success).toBe(true); // Both low and medium priority should be evicted - expect(result.txsEvicted).toContain('0x1111'); - expect(result.txsEvicted).toContain('0x2222'); - expect(result.txsEvicted).not.toContain('0x3333'); + expect(result.txsEvicted).toContain(lowMeta.txHash); + expect(result.txsEvicted).toContain(medMeta.txHash); + expect(result.txsEvicted).not.toContain(highMeta.txHash); }); it('priority ordering is correct - highest priority gets funded first', async () => { @@ -186,15 +174,15 @@ describe('FeePayerBalanceEvictionRule', () => { const context: EvictionContext = { event: EvictionEvent.TXS_ADDED, - newTxHashes: ['0xaaaa', '0xbbbb', '0xcccc'], + newTxHashes: [tx10.txHash, tx50.txHash, tx100.txHash], feePayers: [feePayer1], }; const result = await rule.evict(context, pool); expect(result.success).toBe(true); - expect(result.txsEvicted).toEqual(['0xaaaa']); // Only lowest priority evicted - expect(deleteTxsMock).toHaveBeenCalledWith(['0xaaaa'], 'FeePayerBalanceEviction'); + expect(result.txsEvicted).toEqual([tx10.txHash]); // Only lowest priority evicted + expect(deleteTxsMock).toHaveBeenCalledWith([tx10.txHash], 'FeePayerBalanceEviction'); }); it('considers claim amount when calculating available balance', async () => { @@ -249,7 +237,7 @@ describe('FeePayerBalanceEvictionRule', () => { const result = await rule.evict(context, pool); expect(result.success).toBe(true); - expect(result.txsEvicted).toEqual(['0x2222']); // Low priority evicted + expect(result.txsEvicted).toEqual([lowMeta.txHash]); // Low priority evicted }); it('handles zero balance', async () => { @@ -268,7 +256,7 @@ describe('FeePayerBalanceEvictionRule', () => { const result = await rule.evict(context, pool); expect(result.success).toBe(true); - expect(result.txsEvicted).toEqual(['0x1111']); + expect(result.txsEvicted).toEqual([meta.txHash]); }); it('handles empty fee payers list', async () => { @@ -347,7 +335,7 @@ describe('FeePayerBalanceEvictionRule', () => { const result = await rule.evict(context, pool); expect(result.success).toBe(true); - expect(result.txsEvicted).toEqual(['0x1111']); + expect(result.txsEvicted).toEqual([lowMeta.txHash]); }); }); @@ -396,7 +384,7 @@ describe('FeePayerBalanceEvictionRule', () => { const result = await rule.evict(context, pool); expect(result.success).toBe(true); - expect(result.txsEvicted).toEqual(['0x1111']); + expect(result.txsEvicted).toEqual([meta.txHash]); }); }); @@ -474,7 +462,7 @@ describe('FeePayerBalanceEvictionRule', () => { const context: EvictionContext = { event: EvictionEvent.TXS_ADDED, - newTxHashes: ['0xaaaa', '0xbbbb', '0xcccc'], + newTxHashes: [tx1.txHash, tx2.txHash, tx3.txHash], feePayers: [feePayer1], }; @@ -482,10 +470,10 @@ describe('FeePayerBalanceEvictionRule', () => { expect(result.success).toBe(true); // tx1 (lowest priority) should be evicted - expect(result.txsEvicted).toEqual(['0xaaaa']); + expect(result.txsEvicted).toEqual([tx1.txHash]); // tx2 and tx3 should be kept - expect(result.txsEvicted).not.toContain('0xbbbb'); - expect(result.txsEvicted).not.toContain('0xcccc'); + expect(result.txsEvicted).not.toContain(tx2.txHash); + expect(result.txsEvicted).not.toContain(tx3.txHash); }); it('uses txHash as tiebreaker when priorities are equal', async () => { diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.test.ts index af21423f39e2..5b8fd070dd8f 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.test.ts @@ -1,4 +1,4 @@ -import { type TxMetaData, stubTxMetaValidationData } from '../tx_metadata.js'; +import { type TxMetaData, stubTxMetaData } from '../tx_metadata.js'; import { FeePayerBalancePreAddRule } from './fee_payer_balance_pre_add_rule.js'; import { type PreAddPoolAccess, TxPoolRejectionCode } from './interfaces.js'; @@ -14,19 +14,7 @@ describe('FeePayerBalancePreAddRule', () => { claimAmount?: bigint; feePayer?: string; } = {}, - ): TxMetaData => ({ - txHash, - anchorBlockHeaderHash: '0x1234', - priorityFee: opts.priorityFee ?? 100n, - feePayer: opts.feePayer ?? '0xfeepayer', - claimAmount: opts.claimAmount ?? 0n, - feeLimit: opts.feeLimit ?? 100n, - nullifiers: [`0x${txHash.slice(2)}null1`], - expirationTimestamp: 0n, - receivedAt: 0, - estimatedSizeBytes: 0, - data: stubTxMetaValidationData(), - }); + ) => stubTxMetaData(txHash, opts); // Mock pool access with configurable behavior const createPoolAccess = (balance: bigint, existingTxs: TxMetaData[] = []): PreAddPoolAccess => ({ @@ -127,7 +115,7 @@ describe('FeePayerBalancePreAddRule', () => { const result = await rule.check(incomingMeta, poolAccess); expect(result.shouldIgnore).toBe(false); - expect(result.txHashesToEvict).toContain('0x2222'); + expect(result.txHashesToEvict).toContain(existingMeta.txHash); }); it('evicts multiple lower-priority txs when high-priority tx is added', async () => { @@ -141,8 +129,8 @@ describe('FeePayerBalancePreAddRule', () => { const result = await rule.check(incomingMeta, poolAccess); expect(result.shouldIgnore).toBe(false); - expect(result.txHashesToEvict).toContain('0x2222'); - expect(result.txHashesToEvict).toContain('0x3333'); + expect(result.txHashesToEvict).toContain(existingMeta1.txHash); + expect(result.txHashesToEvict).toContain(existingMeta2.txHash); }); it('handles priority ordering correctly - highest priority gets funded first', async () => { @@ -157,9 +145,9 @@ describe('FeePayerBalancePreAddRule', () => { const result = await rule.check(incomingMeta, poolAccess); expect(result.shouldIgnore).toBe(false); - expect(result.txHashesToEvict).toContain('0x2222'); // Low priority evicted - expect(result.txHashesToEvict).not.toContain('0x4444'); // High priority kept - expect(result.txHashesToEvict).not.toContain('0x3333'); // Med priority kept + expect(result.txHashesToEvict).toContain(lowPriorityMeta.txHash); // Low priority evicted + expect(result.txHashesToEvict).not.toContain(highPriorityMeta.txHash); // High priority kept + expect(result.txHashesToEvict).not.toContain(medPriorityMeta.txHash); // Med priority kept }); }); @@ -245,7 +233,7 @@ describe('FeePayerBalancePreAddRule', () => { expect(result.shouldIgnore).toBe(false); expect(result.txHashesToEvict).toHaveLength(1); - expect(result.txHashesToEvict[0]).toEqual('0x2222'); + expect(result.txHashesToEvict[0]).toEqual(existingMeta.txHash); }); it('returns empty eviction list when no evictions needed', async () => { diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.ts index 0cdebb94948d..fee5bf80d4e5 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/fee_payer_balance_pre_add_rule.ts @@ -35,6 +35,7 @@ export class FeePayerBalancePreAddRule implements PreAddRule { // Create combined list with incoming tx const allTxs: Array<{ txHash: string; + txHashBigInt: bigint; priorityFee: bigint; feeLimit: bigint; claimAmount: bigint; @@ -42,6 +43,7 @@ export class FeePayerBalancePreAddRule implements PreAddRule { }> = [ ...existingTxs.map(t => ({ txHash: t.txHash, + txHashBigInt: t.txHashBigInt, priorityFee: t.priorityFee, feeLimit: t.feeLimit, claimAmount: t.claimAmount, @@ -49,6 +51,7 @@ export class FeePayerBalancePreAddRule implements PreAddRule { })), { txHash: incomingMeta.txHash, + txHashBigInt: incomingMeta.txHashBigInt, priorityFee: incomingMeta.priorityFee, feeLimit: incomingMeta.feeLimit, claimAmount: incomingMeta.claimAmount, diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/invalid_txs_after_mining_rule.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/invalid_txs_after_mining_rule.test.ts index 8c16d90d9c00..73cc569a6111 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/invalid_txs_after_mining_rule.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/invalid_txs_after_mining_rule.test.ts @@ -3,7 +3,7 @@ import { BlockHeader } from '@aztec/stdlib/tx'; import { jest } from '@jest/globals'; -import { type TxMetaData, stubTxMetaValidationData } from '../tx_metadata.js'; +import { type TxMetaData, stubTxMetaData } from '../tx_metadata.js'; import type { EvictionContext, PoolOperations } from './interfaces.js'; import { EvictionEvent } from './interfaces.js'; import { InvalidTxsAfterMiningRule } from './invalid_txs_after_mining_rule.js'; @@ -24,23 +24,7 @@ describe('InvalidTxsAfterMiningRule', () => { nullifiers?: string[]; expirationTimestamp?: bigint; } = {}, - ): TxMetaData => { - const nullifiers = opts.nullifiers ?? [`0x${txHash.slice(2)}null1`]; - const expirationTimestamp = opts.expirationTimestamp ?? DEFAULT_EXPIRATION_TIMESTAMP; - return { - txHash, - anchorBlockHeaderHash: '0x1234', - priorityFee: 100n, - feePayer: '0xfeepayer', - claimAmount: 0n, - feeLimit: 100n, - nullifiers, - expirationTimestamp, - receivedAt: 0, - estimatedSizeBytes: 0, - data: stubTxMetaValidationData({ expirationTimestamp }), - }; - }; + ) => stubTxMetaData(txHash, { expirationTimestamp: DEFAULT_EXPIRATION_TIMESTAMP, ...opts }); // Create mock pool operations const createPoolOps = (pendingTxs: TxMetaData[]): PoolOperations => { @@ -122,8 +106,8 @@ describe('InvalidTxsAfterMiningRule', () => { const result = await rule.evict(context, pool); expect(result.success).toBe(true); - expect(result.txsEvicted).toEqual(['0x1111']); // Only tx1 has duplicate nullifier - expect(deleteTxsMock).toHaveBeenCalledWith(['0x1111'], 'InvalidTxsAfterMining'); + expect(result.txsEvicted).toEqual([tx1.txHash]); // Only tx1 has duplicate nullifier + expect(deleteTxsMock).toHaveBeenCalledWith([tx1.txHash], 'InvalidTxsAfterMining'); }); it('evicts transactions with expired timestamps', async () => { @@ -142,8 +126,8 @@ describe('InvalidTxsAfterMiningRule', () => { const result = await rule.evict(context, pool); expect(result.success).toBe(true); - expect(result.txsEvicted).toEqual(['0x1111']); // Only tx1 is expired - expect(deleteTxsMock).toHaveBeenCalledWith(['0x1111'], 'InvalidTxsAfterMining'); + expect(result.txsEvicted).toEqual([tx1.txHash]); // Only tx1 is expired + expect(deleteTxsMock).toHaveBeenCalledWith([tx1.txHash], 'InvalidTxsAfterMining'); }); it('evicts transactions with timestamp equal to block timestamp', async () => { @@ -162,8 +146,8 @@ describe('InvalidTxsAfterMiningRule', () => { const result = await rule.evict(context, pool); expect(result.success).toBe(true); - expect(result.txsEvicted).toEqual(['0x1111']); // tx1 has timestamp <= block timestamp - expect(deleteTxsMock).toHaveBeenCalledWith(['0x1111'], 'InvalidTxsAfterMining'); + expect(result.txsEvicted).toEqual([tx1.txHash]); // tx1 has timestamp <= block timestamp + expect(deleteTxsMock).toHaveBeenCalledWith([tx1.txHash], 'InvalidTxsAfterMining'); }); it('handles transactions with both duplicate nullifiers and expired timestamps', async () => { @@ -182,8 +166,8 @@ describe('InvalidTxsAfterMiningRule', () => { const result = await rule.evict(context, pool); expect(result.success).toBe(true); - expect(result.txsEvicted).toEqual(['0x1111']); - expect(deleteTxsMock).toHaveBeenCalledWith(['0x1111'], 'InvalidTxsAfterMining'); + expect(result.txsEvicted).toEqual([tx1.txHash]); + expect(deleteTxsMock).toHaveBeenCalledWith([tx1.txHash], 'InvalidTxsAfterMining'); }); it('handles empty pending transactions list', async () => { @@ -222,7 +206,7 @@ describe('InvalidTxsAfterMiningRule', () => { const result = await rule.evict(context, pool); expect(result.success).toBe(true); - expect(result.txsEvicted).toEqual(['0x1111']); + expect(result.txsEvicted).toEqual([tx1.txHash]); }); it('evicts all matching transactions when multiple share nullifiers with mined block', async () => { @@ -242,9 +226,9 @@ describe('InvalidTxsAfterMiningRule', () => { const result = await rule.evict(context, pool); expect(result.success).toBe(true); - expect(result.txsEvicted).toContain('0x1111'); - expect(result.txsEvicted).toContain('0x2222'); - expect(result.txsEvicted).not.toContain('0x3333'); + expect(result.txsEvicted).toContain(tx1.txHash); + expect(result.txsEvicted).toContain(tx2.txHash); + expect(result.txsEvicted).not.toContain(tx3.txHash); }); it('handles error from deleteTxs operation', async () => { diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/invalid_txs_after_reorg_rule.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/invalid_txs_after_reorg_rule.test.ts index c7ab0ab474a6..c8ccb70890bc 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/invalid_txs_after_reorg_rule.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/invalid_txs_after_reorg_rule.test.ts @@ -7,7 +7,7 @@ import { BlockHeader } from '@aztec/stdlib/tx'; import { jest } from '@jest/globals'; import { type MockProxy, mock } from 'jest-mock-extended'; -import { type TxMetaData, stubTxMetaValidationData } from '../tx_metadata.js'; +import { type TxMetaData, stubTxMetaData } from '../tx_metadata.js'; import type { EvictionContext, PoolOperations } from './interfaces.js'; import { EvictionEvent } from './interfaces.js'; import { InvalidTxsAfterReorgRule } from './invalid_txs_after_reorg_rule.js'; @@ -21,19 +21,8 @@ describe('InvalidTxsAfterReorgRule', () => { let deleteTxsMock: jest.MockedFunction; // Helper to create TxMetaData for testing - const createMeta = (txHash: string, anchorBlockHeaderHash: string): TxMetaData => ({ - txHash, - anchorBlockHeaderHash, - priorityFee: 100n, - feePayer: '0xfeepayer', - claimAmount: 0n, - feeLimit: 100n, - nullifiers: [`0x${txHash.slice(2)}null1`], - expirationTimestamp: 0n, - receivedAt: 0, - estimatedSizeBytes: 0, - data: stubTxMetaValidationData(), - }); + const createMeta = (txHash: string, anchorBlockHeaderHash: string) => + stubTxMetaData(txHash, { anchorBlockHeaderHash }); // Create mock pool operations const createPoolOps = (pendingTxs: TxMetaData[]): PoolOperations => { @@ -134,8 +123,8 @@ describe('InvalidTxsAfterReorgRule', () => { expect(result.success).toBe(true); // Both txs reference pruned blocks (default mock returns undefined) - expect(result.txsEvicted).toContain('0x1111'); - expect(result.txsEvicted).toContain('0x2222'); + expect(result.txsEvicted).toContain(tx1.txHash); + expect(result.txsEvicted).toContain(tx2.txHash); // Ensure syncImmediate is called before accessing the world state snapshot expect(worldState.syncImmediate).toHaveBeenCalledWith(); }); @@ -201,9 +190,9 @@ describe('InvalidTxsAfterReorgRule', () => { expect(result.success).toBe(true); expect(result.txsEvicted).toHaveLength(3); - expect(result.txsEvicted).toContain('0x1111'); - expect(result.txsEvicted).toContain('0x2222'); - expect(result.txsEvicted).toContain('0x3333'); + expect(result.txsEvicted).toContain(tx1.txHash); + expect(result.txsEvicted).toContain(tx2.txHash); + expect(result.txsEvicted).toContain(tx3.txHash); // Only one unique block hash to look up expect(db.findLeafIndices).toHaveBeenCalledTimes(1); const calledHashes = db.findLeafIndices.mock.calls[0][1] as Fr[]; @@ -232,9 +221,9 @@ describe('InvalidTxsAfterReorgRule', () => { expect(result.success).toBe(true); expect(result.txsEvicted).toHaveLength(1); - expect(result.txsEvicted).toContain('0x2222'); - expect(result.txsEvicted).not.toContain('0x1111'); - expect(result.txsEvicted).not.toContain('0x3333'); + expect(result.txsEvicted).toContain(tx2.txHash); + expect(result.txsEvicted).not.toContain(tx1.txHash); + expect(result.txsEvicted).not.toContain(tx3.txHash); }); it('handles mix of shared and unique block hashes with some valid and some pruned', async () => { @@ -263,11 +252,11 @@ describe('InvalidTxsAfterReorgRule', () => { expect(result.success).toBe(true); expect(result.txsEvicted).toHaveLength(3); - expect(result.txsEvicted).toContain('0x3333'); - expect(result.txsEvicted).toContain('0x4444'); - expect(result.txsEvicted).toContain('0x5555'); - expect(result.txsEvicted).not.toContain('0x1111'); - expect(result.txsEvicted).not.toContain('0x2222'); + expect(result.txsEvicted).toContain(tx3.txHash); + expect(result.txsEvicted).toContain(tx4.txHash); + expect(result.txsEvicted).toContain(tx5.txHash); + expect(result.txsEvicted).not.toContain(tx1.txHash); + expect(result.txsEvicted).not.toContain(tx2.txHash); expect(db.findLeafIndices).toHaveBeenCalledTimes(1); const calledHashes = db.findLeafIndices.mock.calls[0][1] as Fr[]; expect(calledHashes).toHaveLength(3); diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.test.ts index b7173ba8bdbe..fd66a0df4aee 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/low_priority_pre_add_rule.test.ts @@ -1,4 +1,4 @@ -import { type TxMetaData, comparePriority, stubTxMetaValidationData } from '../tx_metadata.js'; +import { type TxMetaData, comparePriority, stubTxMetaData } from '../tx_metadata.js'; import { type PreAddContext, type PreAddPoolAccess, TxPoolRejectionCode } from './interfaces.js'; import { LowPriorityPreAddRule } from './low_priority_pre_add_rule.js'; @@ -6,19 +6,7 @@ describe('LowPriorityPreAddRule', () => { let rule: LowPriorityPreAddRule; // Helper to create TxMetaData for testing - const createMeta = (txHash: string, priorityFee: bigint): TxMetaData => ({ - txHash, - anchorBlockHeaderHash: '0x1234', - priorityFee, - feePayer: '0xfeepayer', - claimAmount: 0n, - feeLimit: 100n, - nullifiers: [`0x${txHash.slice(2)}null1`], - expirationTimestamp: 0n, - receivedAt: 0, - estimatedSizeBytes: 0, - data: stubTxMetaValidationData(), - }); + const createMeta = (txHash: string, priorityFee: bigint) => stubTxMetaData(txHash, { priorityFee }); // Mock pool access with configurable behavior const createPoolAccess = (pendingCount: number, lowestPriorityTx?: TxMetaData): PreAddPoolAccess => ({ @@ -88,7 +76,7 @@ describe('LowPriorityPreAddRule', () => { const result = await rule.check(incomingMeta, poolAccess); expect(result.shouldIgnore).toBe(false); - expect(result.txHashesToEvict).toContain('0x2222'); + expect(result.txHashesToEvict).toContain(lowestPriorityMeta.txHash); expect(result.txHashesToEvict).toHaveLength(1); }); @@ -199,12 +187,12 @@ describe('LowPriorityPreAddRule', () => { // Without feeOnly const result1 = await rule.check(incomingMeta, poolAccess); expect(result1.shouldIgnore).toBe(false); - expect(result1.txHashesToEvict).toContain('0x2222'); + expect(result1.txHashesToEvict).toContain(lowestPriorityMeta.txHash); // With feeOnly const result2 = await rule.check(incomingMeta, poolAccess, { feeComparisonOnly: true }); expect(result2.shouldIgnore).toBe(false); - expect(result2.txHashesToEvict).toContain('0x2222'); + expect(result2.txHashesToEvict).toContain(lowestPriorityMeta.txHash); }); it('lower fee is always ignored regardless of feeOnly flag', async () => { diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.test.ts index f5f08c1ece36..5108966f9047 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/nullifier_conflict_rule.test.ts @@ -1,4 +1,4 @@ -import { type TxMetaData, stubTxMetaValidationData } from '../tx_metadata.js'; +import { type TxMetaData, stubTxMetaData } from '../tx_metadata.js'; import type { PreAddPoolAccess } from './interfaces.js'; import { NullifierConflictRule } from './nullifier_conflict_rule.js'; @@ -7,23 +7,8 @@ describe('NullifierConflictRule', () => { let rule: NullifierConflictRule; // Helper to create TxMetaData for testing - const createMeta = ( - txHash: string, - priorityFee: bigint, - nullifiers: string[] = [`0x${txHash.slice(2)}null1`], - ): TxMetaData => ({ - txHash, - anchorBlockHeaderHash: '0x1234', - priorityFee, - feePayer: '0xfeepayer', - claimAmount: 0n, - feeLimit: 1000n, - nullifiers, - expirationTimestamp: 0n, - receivedAt: 0, - estimatedSizeBytes: 0, - data: stubTxMetaValidationData(), - }); + const createMeta = (txHash: string, priorityFee: bigint, nullifiers?: string[]) => + stubTxMetaData(txHash, { priorityFee, feeLimit: 1000n, ...(nullifiers ? { nullifiers } : {}) }); // Mock pool access with configurable behavior const createPoolAccess = ( diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/index.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/index.ts index 391e7edccab0..cee49474dcb3 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/index.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/index.ts @@ -7,6 +7,6 @@ export { type PoolReadAccess, DEFAULT_TX_POOL_V2_CONFIG, } from './interfaces.js'; -export { type TxMetaData, type TxState, buildTxMetaData, comparePriority } from './tx_metadata.js'; +export { type TxMetaData, type TxState, buildTxMetaData, comparePriority, stubTxMetaData } from './tx_metadata.js'; export { TxArchive } from './archive/index.js'; export { DeletedPool } from './deleted_pool.js'; diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_metadata.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_metadata.test.ts index a2e6172c2922..d139138d5489 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_metadata.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_metadata.test.ts @@ -1,13 +1,7 @@ import { mockTx } from '@aztec/stdlib/testing'; import { TxPoolRejectionCode } from './eviction/interfaces.js'; -import { - type TxMetaData, - buildTxMetaData, - checkNullifierConflict, - comparePriority, - stubTxMetaValidationData, -} from './tx_metadata.js'; +import { buildTxMetaData, checkNullifierConflict, comparePriority, stubTxMetaData } from './tx_metadata.js'; describe('TxMetaData', () => { describe('buildTxMetaData', () => { @@ -16,6 +10,7 @@ describe('TxMetaData', () => { const meta = await buildTxMetaData(tx); expect(meta.txHash).toBe(tx.getTxHash().toString()); + expect(meta.txHashBigInt).toBe(tx.getTxHash().toBigInt()); expect(meta.anchorBlockHeaderHash).toBe((await tx.data.constants.anchorBlockHeader.hash()).toString()); expect(meta.feePayer).toBe(tx.data.feePayer.toString()); expect(meta.expirationTimestamp).toBe(tx.data.expirationTimestamp); @@ -68,19 +63,7 @@ describe('TxMetaData', () => { }); describe('comparePriority', () => { - const makeMeta = (fee: bigint, txHash = '0x1234'): TxMetaData => ({ - txHash, - anchorBlockHeaderHash: '0x5678', - priorityFee: fee, - feePayer: '0xabcd', - claimAmount: 0n, - feeLimit: 1000n, - nullifiers: [], - expirationTimestamp: 0n, - receivedAt: 0, - estimatedSizeBytes: 0, - data: stubTxMetaValidationData(), - }); + const makeMeta = (fee: bigint, txHash = '0x1234') => stubTxMetaData(txHash, { priorityFee: fee, nullifiers: [] }); it('returns negative when first has lower priority fee', () => { expect(comparePriority(makeMeta(100n), makeMeta(200n))).toBe(-1); @@ -102,19 +85,8 @@ describe('TxMetaData', () => { }); describe('checkNullifierConflict', () => { - const makeMeta = (txHash: string, priorityFee: bigint, nullifiers: string[]): TxMetaData => ({ - txHash, - anchorBlockHeaderHash: '0x5678', - priorityFee, - feePayer: '0xabcd', - claimAmount: 0n, - feeLimit: 1000n, - nullifiers, - expirationTimestamp: 0n, - receivedAt: 0, - estimatedSizeBytes: 0, - data: stubTxMetaValidationData(), - }); + const makeMeta = (txHash: string, priorityFee: bigint, nullifiers: string[]) => + stubTxMetaData(txHash, { priorityFee, nullifiers }); it('returns no conflict when nullifiers do not overlap', () => { const incoming = makeMeta('0x1111', 100n, ['0xnull1', '0xnull2']); diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_metadata.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_metadata.ts index 45a452c6086d..316f551bcc6c 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_metadata.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_metadata.ts @@ -3,7 +3,7 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { ProtocolContractAddress } from '@aztec/protocol-contracts'; import { BlockHash, type L2BlockId } from '@aztec/stdlib/block'; import { Gas } from '@aztec/stdlib/gas'; -import type { Tx } from '@aztec/stdlib/tx'; +import { type Tx, TxHash } from '@aztec/stdlib/tx'; import { getFeePayerBalanceDelta } from '../../msg_validators/tx_validator/fee_payer_balance.js'; import { getTxPriorityFee } from '../tx_pool/priority.js'; @@ -40,6 +40,9 @@ export type TxMetaData = { /** The transaction hash as hex string */ readonly txHash: string; + /** The transaction hash as bigint (for efficient Fr conversion in comparisons) */ + readonly txHashBigInt: bigint; + /** Block ID (number and hash) in which the transaction was mined (undefined if not mined) */ minedL2BlockId?: L2BlockId; @@ -83,7 +86,9 @@ export type TxState = 'pending' | 'protected' | 'mined' | 'deleted'; * Fr values are captured in closures for zero-cost re-validation. */ export async function buildTxMetaData(tx: Tx): Promise { - const txHash = tx.getTxHash().toString(); + const txHashObj = tx.getTxHash(); + const txHash = txHashObj.toString(); + const txHashBigInt = txHashObj.toBigInt(); const nullifierFrs = tx.data.getNonEmptyNullifiers(); const nullifiers = nullifierFrs.map(n => n.toString()); const anchorBlockHeaderHashFr = await tx.data.constants.anchorBlockHeader.hash(); @@ -99,6 +104,7 @@ export async function buildTxMetaData(tx: Tx): Promise { return { txHash, + txHashBigInt, anchorBlockHeaderHash, priorityFee, feePayer, @@ -134,11 +140,11 @@ const HEX_STRING_BYTES = 98; const BIGINT_BYTES = 32; const FR_BYTES = 80; // Fixed cost: object shell + txHash + anchorBlockHeaderHash + feePayer (3 hex strings) -// + priorityFee + claimAmount + feeLimit + includeByTimestamp (4 bigints) +// + txHashBigInt + priorityFee + claimAmount + feeLimit + includeByTimestamp (5 bigints) // + receivedAt (number, 8 bytes) + estimatedSizeBytes (number, 8 bytes) // + data closure object (~OBJECT_OVERHEAD + anchorBlockHeaderHashFr Fr + anchorBlockNumber number) const FIXED_METADATA_BYTES = - OBJECT_OVERHEAD + 3 * HEX_STRING_BYTES + 4 * BIGINT_BYTES + 8 + 8 + OBJECT_OVERHEAD + FR_BYTES + 8; + OBJECT_OVERHEAD + 3 * HEX_STRING_BYTES + 5 * BIGINT_BYTES + 8 + 8 + OBJECT_OVERHEAD + FR_BYTES + 8; /** Estimates the in-memory size of a TxMetaData object based on the number of nullifiers. */ function estimateTxMetaDataSize(nullifierCount: number): number { @@ -146,8 +152,13 @@ function estimateTxMetaDataSize(nullifierCount: number): number { return FIXED_METADATA_BYTES + nullifierCount * (HEX_STRING_BYTES + FR_BYTES); } +/** Converts a txHash bigint back to the canonical 0x-prefixed 64-char hex string. */ +export function txHashFromBigInt(value: bigint): string { + return TxHash.fromBigInt(value).toString(); +} + /** Minimal fields required for priority comparison. */ -type PriorityComparable = Pick; +type PriorityComparable = Pick; /** * Compares two priority fees in ascending order. @@ -162,10 +173,8 @@ export function compareFee(a: bigint, b: bigint): number { * Uses field element comparison for deterministic ordering. * Returns negative if a < b, positive if a > b, 0 if equal. */ -export function compareTxHash(a: string, b: string): number { - const fieldA = Fr.fromHexString(a); - const fieldB = Fr.fromHexString(b); - return fieldA.cmp(fieldB); +export function compareTxHash(a: bigint, b: bigint): number { + return Fr.cmpAsBigInt(a, b); } /** @@ -178,7 +187,7 @@ export function comparePriority(a: PriorityComparable, b: PriorityComparable): n if (feeComparison !== 0) { return feeComparison; } - return compareTxHash(a.txHash, b.txHash); + return compareTxHash(a.txHashBigInt, b.txHashBigInt); } /** @@ -253,3 +262,36 @@ export function stubTxMetaValidationData(overrides: { expirationTimestamp?: bigi }, }; } + +/** Creates a stub TxMetaData for tests. All fields have sensible defaults and can be overridden. */ +export function stubTxMetaData( + txHash: string, + overrides: { + priorityFee?: bigint; + feePayer?: string; + claimAmount?: bigint; + feeLimit?: bigint; + nullifiers?: string[]; + expirationTimestamp?: bigint; + anchorBlockHeaderHash?: string; + } = {}, +): TxMetaData { + const txHashBigInt = Fr.fromHexString(txHash).toBigInt(); + // Normalize to canonical zero-padded hex so txHashFromBigInt(txHashBigInt) === normalizedTxHash + const normalizedTxHash = txHashFromBigInt(txHashBigInt); + const expirationTimestamp = overrides.expirationTimestamp ?? 0n; + return { + txHash: normalizedTxHash, + txHashBigInt, + anchorBlockHeaderHash: overrides.anchorBlockHeaderHash ?? '0x1234', + priorityFee: overrides.priorityFee ?? 100n, + feePayer: overrides.feePayer ?? '0xfeepayer', + claimAmount: overrides.claimAmount ?? 0n, + feeLimit: overrides.feeLimit ?? 100n, + nullifiers: overrides.nullifiers ?? [`0x${normalizedTxHash.slice(2)}null1`], + expirationTimestamp, + receivedAt: 0, + estimatedSizeBytes: 0, + data: stubTxMetaValidationData({ expirationTimestamp }), + }; +} diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_indices.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_indices.ts index a9a368dce37c..42dd87db5cbf 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_indices.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_indices.ts @@ -1,7 +1,7 @@ import { SlotNumber } from '@aztec/foundation/branded-types'; import type { L2BlockId } from '@aztec/stdlib/block'; -import { type TxMetaData, type TxState, compareFee, compareTxHash } from './tx_metadata.js'; +import { type TxMetaData, type TxState, compareFee, compareTxHash, txHashFromBigInt } from './tx_metadata.js'; /** * Manages in-memory indices for the transaction pool. @@ -22,8 +22,8 @@ export class TxPoolIndices { #nullifierToTxHash: Map = new Map(); /** Fee payer to txHashes index (pending txs only) */ #feePayerToTxHashes: Map> = new Map(); - /** Pending txHashes grouped by priority fee */ - #pendingByPriority: Map> = new Map(); + /** Pending txHash bigints grouped by priority fee */ + #pendingByPriority: Map> = new Map(); /** Protected transactions: txHash -> slotNumber */ #protectedTransactions: Map = new Map(); @@ -73,17 +73,17 @@ export class TxPoolIndices { * @param order - 'desc' for highest priority first, 'asc' for lowest priority first */ *iteratePendingByPriority(order: 'asc' | 'desc', filter?: (hash: string) => boolean): Generator { - // Use compareFee from tx_metadata, swap args for descending order const feeCompareFn = order === 'desc' ? (a: bigint, b: bigint) => compareFee(b, a) : compareFee; - const hashCompareFn = order === 'desc' ? (a: string, b: string) => compareTxHash(b, a) : compareTxHash; + const hashCompareFn = + order === 'desc' ? (a: bigint, b: bigint) => compareTxHash(b, a) : (a: bigint, b: bigint) => compareTxHash(a, b); const sortedFees = [...this.#pendingByPriority.keys()].sort(feeCompareFn); for (const fee of sortedFees) { const hashesAtFee = this.#pendingByPriority.get(fee)!; - // Use compareTxHash from tx_metadata, swap args for descending order const sortedHashes = [...hashesAtFee].sort(hashCompareFn); - for (const hash of sortedHashes) { + for (const hashBigInt of sortedHashes) { + const hash = txHashFromBigInt(hashBigInt); if (filter === undefined || filter(hash)) { yield hash; } @@ -265,8 +265,8 @@ export class TxPoolIndices { getPendingTxs(): TxMetaData[] { const result: TxMetaData[] = []; for (const hashSet of this.#pendingByPriority.values()) { - for (const txHash of hashSet) { - const meta = this.#metadata.get(txHash); + for (const txHashBigInt of hashSet) { + const meta = this.#metadata.get(txHashFromBigInt(txHashBigInt)); if (meta) { result.push(meta); } @@ -414,7 +414,7 @@ export class TxPoolIndices { prioritySet = new Set(); this.#pendingByPriority.set(meta.priorityFee, prioritySet); } - prioritySet.add(meta.txHash); + prioritySet.add(meta.txHashBigInt); } #removeFromPendingIndices(meta: TxMetaData): void { @@ -435,7 +435,7 @@ export class TxPoolIndices { // Remove from priority map const hashSet = this.#pendingByPriority.get(meta.priorityFee); if (hashSet) { - hashSet.delete(meta.txHash); + hashSet.delete(meta.txHashBigInt); if (hashSet.size === 0) { this.#pendingByPriority.delete(meta.priorityFee); }