Skip to content

Commit 24abcfe

Browse files
authored
feat: delete attestations older than a slot (#10326)
Prune attestation pool fixes: #7971
1 parent d7bd306 commit 24abcfe

File tree

8 files changed

+105
-2
lines changed

8 files changed

+105
-2
lines changed

yarn-project/foundation/src/config/env_var.ts

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export type EnvVar =
9191
| 'P2P_TCP_LISTEN_ADDR'
9292
| 'P2P_TCP_ANNOUNCE_ADDR'
9393
| 'P2P_TX_POOL_KEEP_PROVEN_FOR'
94+
| 'P2P_ATTESTATION_POOL_KEEP_FOR'
9495
| 'P2P_TX_PROTOCOL'
9596
| 'P2P_UDP_ANNOUNCE_ADDR'
9697
| 'P2P_UDP_LISTEN_ADDR'

yarn-project/p2p/src/client/p2p_client.test.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ describe('In-Memory P2P Client', () => {
5858
addAttestations: jest.fn(),
5959
deleteAttestations: jest.fn(),
6060
deleteAttestationsForSlot: jest.fn(),
61+
deleteAttestationsOlderThan: jest.fn(),
6162
getAttestationsForSlot: jest.fn().mockReturnValue(undefined),
6263
};
6364

@@ -326,5 +327,22 @@ describe('In-Memory P2P Client', () => {
326327
});
327328
});
328329

329-
// TODO(https://github.com/AztecProtocol/aztec-packages/issues/7971): tests for attestation pool pruning
330+
describe('Attestation pool pruning', () => {
331+
it('deletes attestations older than the number of slots we want to keep in the pool', async () => {
332+
const advanceToProvenBlockNumber = 20;
333+
const keepAttestationsInPoolFor = 12;
334+
335+
blockSource.setProvenBlockNumber(0);
336+
(client as any).keepAttestationsInPoolFor = keepAttestationsInPoolFor;
337+
await client.start();
338+
expect(attestationPool.deleteAttestationsOlderThan).not.toHaveBeenCalled();
339+
340+
await advanceToProvenBlock(advanceToProvenBlockNumber);
341+
342+
expect(attestationPool.deleteAttestationsOlderThan).toHaveBeenCalledTimes(1);
343+
expect(attestationPool.deleteAttestationsOlderThan).toHaveBeenCalledWith(
344+
BigInt(advanceToProvenBlockNumber - keepAttestationsInPoolFor),
345+
);
346+
});
347+
});
330348
});

yarn-project/p2p/src/client/p2p_client.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@ export class P2PClient extends WithTracer implements P2P {
202202
private attestationPool: AttestationPool;
203203
private epochProofQuotePool: EpochProofQuotePool;
204204

205+
/** How many slots to keep attestations for. */
206+
private keepAttestationsInPoolFor: number;
207+
205208
private blockStream;
206209

207210
/**
@@ -224,7 +227,9 @@ export class P2PClient extends WithTracer implements P2P {
224227
) {
225228
super(telemetry, 'P2PClient');
226229

227-
const { blockCheckIntervalMS, blockRequestBatchSize } = getP2PConfigFromEnv();
230+
const { blockCheckIntervalMS, blockRequestBatchSize, keepAttestationsInPoolFor } = getP2PConfigFromEnv();
231+
232+
this.keepAttestationsInPoolFor = keepAttestationsInPoolFor;
228233

229234
this.blockStream = new L2BlockStream(l2BlockSource, this, this, {
230235
batchSize: blockRequestBatchSize,
@@ -615,7 +620,9 @@ export class P2PClient extends WithTracer implements P2P {
615620

616621
const firstBlockNum = blocks[0].number;
617622
const lastBlockNum = blocks[blocks.length - 1].number;
623+
const lastBlockSlot = blocks[blocks.length - 1].header.globalVariables.slotNumber.toBigInt();
618624

625+
// If keepProvenTxsFor is 0, we delete all txs from all proven blocks.
619626
if (this.keepProvenTxsFor === 0) {
620627
await this.deleteTxsFromBlocks(blocks);
621628
} else if (lastBlockNum - this.keepProvenTxsFor >= INITIAL_L2_BLOCK_NUM) {
@@ -626,12 +633,19 @@ export class P2PClient extends WithTracer implements P2P {
626633
await this.deleteTxsFromBlocks(blocksToDeleteTxsFrom);
627634
}
628635

636+
// We delete attestations older than the last block slot minus the number of slots we want to keep in the pool.
637+
const lastBlockSlotMinusKeepAttestationsInPoolFor = lastBlockSlot - BigInt(this.keepAttestationsInPoolFor);
638+
if (lastBlockSlotMinusKeepAttestationsInPoolFor >= BigInt(INITIAL_L2_BLOCK_NUM)) {
639+
await this.attestationPool.deleteAttestationsOlderThan(lastBlockSlotMinusKeepAttestationsInPoolFor);
640+
}
641+
629642
await this.synchedProvenBlockNumber.set(lastBlockNum);
630643
this.log.debug(`Synched to proven block ${lastBlockNum}`);
631644
const provenEpochNumber = await this.l2BlockSource.getProvenL2EpochNumber();
632645
if (provenEpochNumber !== undefined) {
633646
this.epochProofQuotePool.deleteQuotesToEpoch(BigInt(provenEpochNumber));
634647
}
648+
635649
await this.startServiceIfSynched();
636650
}
637651

yarn-project/p2p/src/config.ts

+8
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ export interface P2PConfig extends P2PReqRespConfig {
9292
/** How many blocks have to pass after a block is proven before its txs are deleted (zero to delete immediately once proven) */
9393
keepProvenTxsInPoolFor: number;
9494

95+
/** How many slots to keep attestations for. */
96+
keepAttestationsInPoolFor: number;
97+
9598
/**
9699
* The interval of the gossipsub heartbeat to perform maintenance tasks.
97100
*/
@@ -230,6 +233,11 @@ export const p2pConfigMappings: ConfigMappingsType<P2PConfig> = {
230233
'How many blocks have to pass after a block is proven before its txs are deleted (zero to delete immediately once proven)',
231234
...numberConfigHelper(0),
232235
},
236+
keepAttestationsInPoolFor: {
237+
env: 'P2P_ATTESTATION_POOL_KEEP_FOR',
238+
description: 'How many slots to keep attestations for.',
239+
...numberConfigHelper(96),
240+
},
233241
gossipsubInterval: {
234242
env: 'P2P_GOSSIPSUB_INTERVAL_MS',
235243
description: 'The interval of the gossipsub heartbeat to perform maintenance tasks.',

yarn-project/p2p/src/mem_pools/attestation_pool/attestation_pool.ts

+9
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ export interface AttestationPool {
2121
*/
2222
deleteAttestations(attestations: BlockAttestation[]): Promise<void>;
2323

24+
/**
25+
* Delete Attestations with a slot number smaller than the given slot
26+
*
27+
* Removes all attestations associated with a slot
28+
*
29+
* @param slot - The oldest slot to keep.
30+
*/
31+
deleteAttestationsOlderThan(slot: bigint): Promise<void>;
32+
2433
/**
2534
* Delete Attestations for slot
2635
*

yarn-project/p2p/src/mem_pools/attestation_pool/memory_attestation_pool.test.ts

+31
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Secp256k1Signer } from '@aztec/foundation/crypto';
33
import { Fr } from '@aztec/foundation/fields';
44
import { NoopTelemetryClient } from '@aztec/telemetry-client/noop';
55

6+
import { jest } from '@jest/globals';
67
import { type MockProxy, mock } from 'jest-mock-extended';
78

89
import { type PoolInstrumentation } from '../instrumentation.js';
@@ -30,6 +31,11 @@ describe('MemoryAttestationPool', () => {
3031
(ap as any).metrics = metricsMock;
3132
});
3233

34+
const createAttestationsForSlot = (slotNumber: number) => {
35+
const archive = Fr.random();
36+
return signers.map(signer => mockAttestation(signer, slotNumber, archive));
37+
};
38+
3339
it('should add attestations to pool', async () => {
3440
const slotNumber = 420;
3541
const archive = Fr.random();
@@ -171,4 +177,29 @@ describe('MemoryAttestationPool', () => {
171177
const retreivedAttestationsAfterDelete = await ap.getAttestationsForSlot(BigInt(slotNumber), proposalId);
172178
expect(retreivedAttestationsAfterDelete.length).toBe(0);
173179
});
180+
181+
it('Should delete attestations older than a given slot', async () => {
182+
const slotNumbers = [1, 2, 3, 69, 72, 74, 88, 420];
183+
const attestations = slotNumbers.map(slotNumber => createAttestationsForSlot(slotNumber)).flat();
184+
const proposalId = attestations[0].archive.toString();
185+
186+
await ap.addAttestations(attestations);
187+
188+
const attestationsForSlot1 = await ap.getAttestationsForSlot(BigInt(1), proposalId);
189+
expect(attestationsForSlot1.length).toBe(signers.length);
190+
191+
const deleteAttestationsSpy = jest.spyOn(ap, 'deleteAttestationsForSlot');
192+
193+
await ap.deleteAttestationsOlderThan(BigInt(73));
194+
195+
const attestationsForSlot1AfterDelete = await ap.getAttestationsForSlot(BigInt(1), proposalId);
196+
expect(attestationsForSlot1AfterDelete.length).toBe(0);
197+
198+
expect(deleteAttestationsSpy).toHaveBeenCalledTimes(5);
199+
expect(deleteAttestationsSpy).toHaveBeenCalledWith(BigInt(1));
200+
expect(deleteAttestationsSpy).toHaveBeenCalledWith(BigInt(2));
201+
expect(deleteAttestationsSpy).toHaveBeenCalledWith(BigInt(3));
202+
expect(deleteAttestationsSpy).toHaveBeenCalledWith(BigInt(69));
203+
expect(deleteAttestationsSpy).toHaveBeenCalledWith(BigInt(72));
204+
});
174205
});

yarn-project/p2p/src/mem_pools/attestation_pool/memory_attestation_pool.ts

+21
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,27 @@ export class InMemoryAttestationPool implements AttestationPool {
5858
return total;
5959
}
6060

61+
public async deleteAttestationsOlderThan(oldestSlot: bigint): Promise<void> {
62+
const olderThan = [];
63+
64+
// Entries are iterated in insertion order, so we can break as soon as we find a slot that is older than the oldestSlot.
65+
// Note: this will only prune correctly if attestations are added in order of rising slot, it is important that we do not allow
66+
// insertion of attestations that are old. #(https://github.com/AztecProtocol/aztec-packages/issues/10322)
67+
const slots = this.attestations.keys();
68+
for (const slot of slots) {
69+
if (slot < oldestSlot) {
70+
olderThan.push(slot);
71+
} else {
72+
break;
73+
}
74+
}
75+
76+
for (const oldSlot of olderThan) {
77+
await this.deleteAttestationsForSlot(oldSlot);
78+
}
79+
return Promise.resolve();
80+
}
81+
6182
public deleteAttestationsForSlot(slot: bigint): Promise<void> {
6283
// We count the number of attestations we are removing
6384
const numberOfAttestations = this.#getNumberOfAttestationsInSlot(slot);

yarn-project/p2p/src/service/reqresp/reqresp.integration.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const makeMockPools = () => {
6363
addAttestations: jest.fn(),
6464
deleteAttestations: jest.fn(),
6565
deleteAttestationsForSlot: jest.fn(),
66+
deleteAttestationsOlderThan: jest.fn(),
6667
getAttestationsForSlot: jest.fn().mockReturnValue(undefined),
6768
},
6869
epochProofQuotePool: {

0 commit comments

Comments
 (0)