Skip to content

Commit 802f7f5

Browse files
spalladinoclaude
andcommitted
refactor(p2p): merge checkpoint attestation methods into tryAddCheckpointAttestation
Replace separate hasCheckpointAttestation, canAddCheckpointAttestation, and hasReachedCheckpointAttestationCap methods with a single tryAddCheckpointAttestation method that handles all validation and addition in one atomic operation. This simplifies the API and ensures consistent behavior between checking and adding attestations. Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent a376239 commit 802f7f5

File tree

7 files changed

+162
-182
lines changed

7 files changed

+162
-182
lines changed

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

Lines changed: 21 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import type { SlotNumber } from '@aztec/foundation/branded-types';
22
import type { BlockProposal, CheckpointAttestation, CheckpointProposalCore } from '@aztec/stdlib/p2p';
33

4-
/** Result of trying to add a proposal (block or checkpoint) */
5-
export type TryAddProposalResult = {
6-
/** Whether proposal was added */
4+
/** Result of trying to add an item (proposal or attestation) to the pool */
5+
export type TryAddResult = {
6+
/** Whether the item was added */
77
added: boolean;
8-
/** Whether exact proposal already existed */
8+
/** Whether the exact item already existed */
99
alreadyExists: boolean;
10-
/** Total proposals for this position - used for duplicate detection */
10+
/** Total items for this position - used for duplicate detection */
1111
totalForPosition: number;
1212
};
1313

@@ -29,7 +29,7 @@ export interface AttestationPool {
2929
* @param blockProposal - The block proposal to add
3030
* @returns Result indicating whether the proposal was added and duplicate detection info
3131
*/
32-
tryAddBlockProposal(blockProposal: BlockProposal): Promise<TryAddProposalResult>;
32+
tryAddBlockProposal(blockProposal: BlockProposal): Promise<TryAddResult>;
3333

3434
/**
3535
* Get block proposal by its ID.
@@ -55,7 +55,7 @@ export interface AttestationPool {
5555
* @param proposal - The checkpoint proposal core to add
5656
* @returns Result indicating whether the proposal was added and duplicate detection info
5757
*/
58-
tryAddCheckpointProposal(proposal: CheckpointProposalCore): Promise<TryAddProposalResult>;
58+
tryAddCheckpointProposal(proposal: CheckpointProposalCore): Promise<TryAddResult>;
5959

6060
/**
6161
* Get checkpoint proposal by its ID.
@@ -75,6 +75,20 @@ export interface AttestationPool {
7575
*/
7676
addCheckpointAttestations(attestations: CheckpointAttestation[]): Promise<void>;
7777

78+
/**
79+
* Attempts to add a checkpoint attestation to the pool.
80+
*
81+
* This method performs validation and addition in a single call:
82+
* - Checks if the attestation already exists (returns alreadyExists: true if so)
83+
* - Checks if the (slot, proposalId) has reached the attestation cap (returns added: false if so)
84+
* - Adds the attestation if validation passes
85+
*
86+
* @param attestation - The checkpoint attestation to add
87+
* @param committeeSize - Committee size for the attestation's slot
88+
* @returns Result indicating whether the attestation was added and existence info
89+
*/
90+
tryAddCheckpointAttestation(attestation: CheckpointAttestation, committeeSize: number): Promise<TryAddResult>;
91+
7892
/**
7993
* Delete all pool data (attestations, proposals) older than the given slot
8094
*
@@ -99,33 +113,6 @@ export interface AttestationPool {
99113
*/
100114
getCheckpointAttestationsForSlotAndProposal(slot: SlotNumber, proposalId: string): Promise<CheckpointAttestation[]>;
101115

102-
/**
103-
* Check if a specific checkpoint attestation exists in the pool
104-
*
105-
* @param attestation - The attestation to check
106-
* @return True if the attestation exists, false otherwise
107-
*/
108-
hasCheckpointAttestation(attestation: CheckpointAttestation): Promise<boolean>;
109-
110-
/**
111-
* Returns whether a checkpoint attestation would be accepted for (slot, proposalId).
112-
*
113-
* @param attestation - The attestation to check
114-
* @param committeeSize - Committee size for the attestation's slot
115-
* @returns True if the attestation can be added, false otherwise.
116-
*/
117-
canAddCheckpointAttestation(attestation: CheckpointAttestation, committeeSize: number): Promise<boolean>;
118-
119-
/**
120-
* Returns whether the checkpoint attestation cap for the given slot and proposal has been reached.
121-
*
122-
* @param slot - The slot to check
123-
* @param proposalId - The proposal to check
124-
* @param committeeSize - Committee size for the slot
125-
* @returns True if the cap has been reached, false otherwise.
126-
*/
127-
hasReachedCheckpointAttestationCap(slot: SlotNumber, proposalId: string, committeeSize: number): Promise<boolean>;
128-
129116
/** Returns whether the pool is empty. */
130117
isEmpty(): Promise<boolean>;
131118
}

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

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,6 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
6767
expect(retrievedAttestations.length).toBe(attestations.length);
6868
compareCheckpointAttestations(retrievedAttestations, attestations);
6969

70-
// Check hasCheckpointAttestation for added attestations
71-
for (const attestation of attestations) {
72-
expect(await ap.hasCheckpointAttestation(attestation)).toBe(true);
73-
}
74-
7570
const retrievedAttestationsForSlot = await ap.getCheckpointAttestationsForSlot(SlotNumber(slotNumber));
7671
expect(retrievedAttestationsForSlot.length).toBe(attestations.length);
7772
compareCheckpointAttestations(retrievedAttestationsForSlot, attestations);
@@ -85,7 +80,6 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
8580
);
8681
expect(retrievedAttestationsAfterAdd.length).toBe(attestations.length + 1);
8782
compareCheckpointAttestations(retrievedAttestationsAfterAdd, [...attestations, newAttestation]);
88-
expect(await ap.hasCheckpointAttestation(newAttestation)).toBe(true);
8983
const retrievedAttestationsForSlotAfterAdd = await ap.getCheckpointAttestationsForSlot(SlotNumber(slotNumber));
9084
expect(retrievedAttestationsForSlotAfterAdd.length).toBe(attestations.length + 1);
9185
compareCheckpointAttestations(retrievedAttestationsForSlotAfterAdd, [...attestations, newAttestation]);
@@ -98,11 +92,6 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo
9892
archive.toString(),
9993
);
10094
expect(retreivedAttestationsAfterDelete.length).toBe(0);
101-
// Check hasCheckpointAttestation after deletion
102-
for (const attestation of attestations) {
103-
expect(await ap.hasCheckpointAttestation(attestation)).toBe(false);
104-
}
105-
expect(await ap.hasCheckpointAttestation(newAttestation)).toBe(false);
10695
});
10796

10897
it('should handle duplicate proposals in a slot', async () => {

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

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,23 +51,25 @@ describe('KV Attestation Pool', () => {
5151
// Create 'limit' distinct checkpoint attestations for the same (slot, proposalId)
5252
const signers = Array.from({ length: limit }, () => Secp256k1Signer.random());
5353
const attestations = signers.map(s => mockCheckpointAttestation(s, slotNumber, archive));
54-
await kvAttestationPool.addCheckpointAttestations(attestations);
5554

56-
// We should now be at cap
57-
expect(
58-
await kvAttestationPool.hasReachedCheckpointAttestationCap(
59-
SlotNumber(slotNumber),
60-
archive.toString(),
61-
committeeSize,
62-
),
63-
).toBe(true);
55+
// Add each attestation using tryAddCheckpointAttestation
56+
for (let i = 0; i < attestations.length; i++) {
57+
const result = await kvAttestationPool.tryAddCheckpointAttestation(attestations[i], committeeSize);
58+
expect(result.added).toBe(true);
59+
expect(result.totalForPosition).toBe(i + 1);
60+
}
6461

65-
// A new attestation from a new signer should not be accepted (per validation helper semantics)
62+
// A new attestation from a new signer should not be added (cap reached)
6663
const extra = mockCheckpointAttestation(Secp256k1Signer.random(), slotNumber, archive);
67-
expect(await kvAttestationPool.canAddCheckpointAttestation(extra, committeeSize)).toBe(false);
64+
const extraResult = await kvAttestationPool.tryAddCheckpointAttestation(extra, committeeSize);
65+
expect(extraResult.added).toBe(false);
66+
expect(extraResult.alreadyExists).toBe(false);
67+
expect(extraResult.totalForPosition).toBe(limit);
6868

69-
// Re-adding an existing attestation should be allowed
70-
expect(await kvAttestationPool.canAddCheckpointAttestation(attestations[0], committeeSize)).toBe(true);
69+
// Re-adding an existing attestation should return alreadyExists
70+
const existingResult = await kvAttestationPool.tryAddCheckpointAttestation(attestations[0], committeeSize);
71+
expect(existingResult.added).toBe(false);
72+
expect(existingResult.alreadyExists).toBe(true);
7173
});
7274
});
7375
});

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

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
1313

1414
import { PoolInstrumentation, PoolName, type PoolStatsCallback } from '../instrumentation.js';
15-
import type { AttestationPool, TryAddProposalResult } from './attestation_pool.js';
15+
import type { AttestationPool, TryAddResult } from './attestation_pool.js';
1616

1717
export const MAX_PROPOSALS_PER_SLOT = 5;
1818
export const MAX_PROPOSALS_PER_POSITION = 3;
@@ -117,7 +117,7 @@ export class KvAttestationPool implements AttestationPool {
117117
return (slot << KvAttestationPool.INDEX_BITS) | indexWithinCheckpoint;
118118
}
119119

120-
public async tryAddBlockProposal(blockProposal: BlockProposal): Promise<TryAddProposalResult> {
120+
public async tryAddBlockProposal(blockProposal: BlockProposal): Promise<TryAddResult> {
121121
const proposalId = blockProposal.archive.toString();
122122

123123
// Check if already exists
@@ -180,7 +180,7 @@ export class KvAttestationPool implements AttestationPool {
180180
return Promise.resolve(undefined);
181181
}
182182

183-
public async tryAddCheckpointProposal(proposal: CheckpointProposalCore): Promise<TryAddProposalResult> {
183+
public async tryAddCheckpointProposal(proposal: CheckpointProposalCore): Promise<TryAddResult> {
184184
const proposalId = proposal.archive.toString();
185185

186186
// Check if already exists
@@ -325,50 +325,52 @@ export class KvAttestationPool implements AttestationPool {
325325
});
326326
}
327327

328-
public async hasCheckpointAttestation(attestation: CheckpointAttestation): Promise<boolean> {
328+
public async tryAddCheckpointAttestation(
329+
attestation: CheckpointAttestation,
330+
committeeSize: number,
331+
): Promise<TryAddResult> {
329332
const slotNumber = attestation.payload.header.slotNumber;
330-
const proposalId = attestation.archive;
333+
const proposalId = attestation.archive.toString();
331334
const sender = attestation.getSender();
332335

333-
// Attestations with invalid signatures are never in the pool
334336
if (!sender) {
335-
return false;
337+
return { added: false, alreadyExists: false, totalForPosition: 0 };
336338
}
337339

338-
const address = sender.toString();
339-
const key = this.getAttestationKey(slotNumber, proposalId, address);
340+
const key = this.getAttestationKey(slotNumber, proposalId, sender.toString());
341+
const alreadyExists = await this.checkpointAttestations.hasAsync(key);
340342

341-
return await this.checkpointAttestations.hasAsync(key);
342-
}
343+
if (alreadyExists) {
344+
const total = await this.getAttestationCount(slotNumber, proposalId);
345+
return { added: false, alreadyExists: true, totalForPosition: total };
346+
}
343347

344-
public async canAddCheckpointAttestation(
345-
attestation: CheckpointAttestation,
346-
committeeSize: number,
347-
): Promise<boolean> {
348-
return (
349-
(await this.hasCheckpointAttestation(attestation)) ||
350-
!(await this.hasReachedCheckpointAttestationCap(
351-
attestation.payload.header.slotNumber,
352-
attestation.archive.toString(),
353-
committeeSize,
354-
))
355-
);
348+
const limit = committeeSize + ATTESTATION_CAP_BUFFER;
349+
const currentCount = await this.getAttestationCount(slotNumber, proposalId);
350+
351+
if (currentCount >= limit) {
352+
return { added: false, alreadyExists: false, totalForPosition: currentCount };
353+
}
354+
355+
await this.checkpointAttestations.set(key, attestation.toBuffer());
356+
357+
this.log.verbose(`Added checkpoint attestation for slot ${slotNumber} from ${sender.toString()}`, {
358+
signature: attestation.signature.toString(),
359+
slotNumber,
360+
address: sender.toString(),
361+
proposalId,
362+
});
363+
364+
return { added: true, alreadyExists: false, totalForPosition: currentCount + 1 };
356365
}
357366

358-
public async hasReachedCheckpointAttestationCap(
359-
slot: SlotNumber,
360-
proposalId: string,
361-
committeeSize: number,
362-
): Promise<boolean> {
363-
const limit = committeeSize + ATTESTATION_CAP_BUFFER;
367+
/** Gets the count of attestations for a given (slot, proposalId). */
368+
private async getAttestationCount(slot: SlotNumber, proposalId: string): Promise<number> {
364369
const range = this.getAttestationKeyRangeForProposal(slot, proposalId);
365370
let count = 0;
366371
for await (const _ of this.checkpointAttestations.keysAsync(range)) {
367372
count++;
368-
if (count >= limit) {
369-
return true;
370-
}
371373
}
372-
return false;
374+
return count;
373375
}
374376
}

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

Lines changed: 29 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { BlockProposal, CheckpointAttestation, CheckpointProposalCore } fro
44
import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
55

66
import { PoolInstrumentation, PoolName, type PoolStatsCallback } from '../instrumentation.js';
7-
import type { AttestationPool, TryAddProposalResult } from './attestation_pool.js';
7+
import type { AttestationPool, TryAddResult } from './attestation_pool.js';
88
import { ATTESTATION_CAP_BUFFER, MAX_PROPOSALS_PER_POSITION, MAX_PROPOSALS_PER_SLOT } from './kv_attestation_pool.js';
99

1010
export class InMemoryAttestationPool implements AttestationPool {
@@ -52,7 +52,7 @@ export class InMemoryAttestationPool implements AttestationPool {
5252
return Promise.resolve(this.checkpointAttestations.size === 0 && this.proposals.size === 0);
5353
}
5454

55-
public tryAddBlockProposal(blockProposal: BlockProposal): Promise<TryAddProposalResult> {
55+
public tryAddBlockProposal(blockProposal: BlockProposal): Promise<TryAddResult> {
5656
const proposalId = blockProposal.archive.toString();
5757
const slot = blockProposal.slotNumber;
5858
const index = blockProposal.indexWithinCheckpoint;
@@ -113,7 +113,7 @@ export class InMemoryAttestationPool implements AttestationPool {
113113

114114
// Checkpoint attestation methods
115115

116-
public tryAddCheckpointProposal(proposal: CheckpointProposalCore): Promise<TryAddProposalResult> {
116+
public tryAddCheckpointProposal(proposal: CheckpointProposalCore): Promise<TryAddResult> {
117117
const proposalId = proposal.archive.toString();
118118

119119
// 1. Check if already exists
@@ -245,51 +245,43 @@ export class InMemoryAttestationPool implements AttestationPool {
245245
return Promise.resolve();
246246
}
247247

248-
public hasReachedCheckpointAttestationCap(
249-
slot: SlotNumber,
250-
proposalId: string,
251-
committeeSize: number,
252-
): Promise<boolean> {
253-
const limit = committeeSize + ATTESTATION_CAP_BUFFER;
254-
const count = this.checkpointAttestations.get(slot)?.get(proposalId)?.size ?? 0;
255-
return Promise.resolve(limit <= 0 || count >= limit);
256-
}
257-
258-
public async canAddCheckpointAttestation(
259-
attestation: CheckpointAttestation,
260-
committeeSize: number,
261-
): Promise<boolean> {
262-
const sender = attestation.getSender();
263-
const slot = attestation.payload.header.slotNumber;
264-
const pid = attestation.archive.toString();
265-
return (
266-
!!sender &&
267-
((this.checkpointAttestations.get(slot)?.get(pid)?.has(sender.toString()) ?? false) ||
268-
!(await this.hasReachedCheckpointAttestationCap(slot, pid, committeeSize)))
269-
);
270-
}
271-
272-
public hasCheckpointAttestation(attestation: CheckpointAttestation): Promise<boolean> {
248+
public tryAddCheckpointAttestation(attestation: CheckpointAttestation, committeeSize: number): Promise<TryAddResult> {
273249
const slotNumber = attestation.payload.header.slotNumber;
274250
const proposalId = attestation.archive.toString();
275251
const sender = attestation.getSender();
276252

277-
// Attestations with invalid signatures are never in the pool
278253
if (!sender) {
279-
return Promise.resolve(false);
254+
return Promise.resolve({ added: false, alreadyExists: false, totalForPosition: 0 });
280255
}
281256

282-
const slotAttestationMap = this.checkpointAttestations.get(slotNumber);
283-
if (!slotAttestationMap) {
284-
return Promise.resolve(false);
257+
const address = sender.toString();
258+
const slotAttestationMap = getCheckpointSlotOrDefault(this.checkpointAttestations, slotNumber);
259+
const proposalAttestationMap = getCheckpointProposalOrDefault(slotAttestationMap, proposalId);
260+
261+
// Check if already exists
262+
const alreadyExists = proposalAttestationMap.has(address);
263+
if (alreadyExists) {
264+
return Promise.resolve({ added: false, alreadyExists: true, totalForPosition: proposalAttestationMap.size });
285265
}
286266

287-
const proposalAttestationMap = slotAttestationMap.get(proposalId);
288-
if (!proposalAttestationMap) {
289-
return Promise.resolve(false);
267+
// Check cap
268+
const limit = committeeSize + ATTESTATION_CAP_BUFFER;
269+
const currentCount = proposalAttestationMap.size;
270+
if (currentCount >= limit) {
271+
return Promise.resolve({ added: false, alreadyExists: false, totalForPosition: currentCount });
290272
}
291273

292-
return Promise.resolve(proposalAttestationMap.has(sender.toString()));
274+
// Add the attestation
275+
proposalAttestationMap.set(address, attestation);
276+
277+
this.log.verbose(`Added checkpoint attestation for slot ${slotNumber} from ${address}`, {
278+
signature: attestation.signature.toString(),
279+
slotNumber,
280+
address,
281+
proposalId,
282+
});
283+
284+
return Promise.resolve({ added: true, alreadyExists: false, totalForPosition: currentCount + 1 });
293285
}
294286
}
295287

0 commit comments

Comments
 (0)