diff --git a/yarn-project/p2p/src/mem_pools/attestation_pool/attestation_pool.test.ts b/yarn-project/p2p/src/mem_pools/attestation_pool/attestation_pool.test.ts index cc7850c1e902..358366e54fcc 100644 --- a/yarn-project/p2p/src/mem_pools/attestation_pool/attestation_pool.test.ts +++ b/yarn-project/p2p/src/mem_pools/attestation_pool/attestation_pool.test.ts @@ -83,12 +83,12 @@ describe('Attestation Pool', () => { expect(result2.added).toBe(true); expect(result2.count).toBe(2); // This is the first duplicate - triggers slashing - // Third attestation from same signer (if we want to track more) + // Third attestation from same signer should be rejected (cap is 2) const archive3 = Fr.random(); const attestation3 = mockCheckpointAttestation(signer, slotNumber, archive3); const result3 = await attestationPool.tryAddCheckpointAttestation(attestation3); - expect(result3.added).toBe(true); - expect(result3.count).toBe(3); // Attestations from this signer + expect(result3.added).toBe(false); + expect(result3.count).toBe(2); // At cap, rejected }); it('should reject attestations when signer exceeds per-slot cap', async () => { diff --git a/yarn-project/p2p/src/mem_pools/attestation_pool/attestation_pool.ts b/yarn-project/p2p/src/mem_pools/attestation_pool/attestation_pool.ts index e61456ef5cbf..4f925245d443 100644 --- a/yarn-project/p2p/src/mem_pools/attestation_pool/attestation_pool.ts +++ b/yarn-project/p2p/src/mem_pools/attestation_pool/attestation_pool.ts @@ -26,10 +26,10 @@ export type TryAddResult = { count: number; }; -export const MAX_CHECKPOINT_PROPOSALS_PER_SLOT = 5; -export const MAX_BLOCK_PROPOSALS_PER_POSITION = 3; +export const MAX_CHECKPOINT_PROPOSALS_PER_SLOT = 2; +export const MAX_BLOCK_PROPOSALS_PER_POSITION = 2; /** Maximum attestations a single signer can make per slot before being rejected. */ -export const MAX_ATTESTATIONS_PER_SLOT_AND_SIGNER = 3; +export const MAX_ATTESTATIONS_PER_SLOT_AND_SIGNER = 2; /** Public API interface for attestation pools. Used for typing mocks and test implementations. */ export type AttestationPoolApi = Pick< diff --git a/yarn-project/p2p/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts b/yarn-project/p2p/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts index 20a198da71a0..02b1f1357f27 100644 --- a/yarn-project/p2p/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts +++ b/yarn-project/p2p/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts @@ -446,12 +446,12 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo const result2 = await ap.tryAddBlockProposal(proposal2); expect(result2.count).toBe(2); - // Add a third proposal for same position + // Third proposal for same position should be rejected (cap is 2) const proposal3 = await mockBlockProposalWithIndex(signers[2], slotNumber, indexWithinCheckpoint); const result3 = await ap.tryAddBlockProposal(proposal3); - expect(result3.added).toBe(true); - expect(result3.count).toBe(3); + expect(result3.added).toBe(false); + expect(result3.count).toBe(2); }); it('should return added=false when exceeding capacity', async () => { @@ -666,12 +666,12 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo const result2 = await ap.tryAddCheckpointProposal(proposal2); expect(result2.count).toBe(2); - // Add a third proposal for same slot + // Third proposal for same slot should be rejected (cap is 2) const proposal3 = await mockCheckpointProposalCoreForPool(signers[2], slotNumber); const result3 = await ap.tryAddCheckpointProposal(proposal3); - expect(result3.added).toBe(true); - expect(result3.count).toBe(3); + expect(result3.added).toBe(false); + expect(result3.count).toBe(2); }); it('should not count attestations as proposals for duplicate detection', async () => { diff --git a/yarn-project/p2p/src/msg_validators/attestation_validator/README.md b/yarn-project/p2p/src/msg_validators/attestation_validator/README.md index 18e8b1f0aa06..3f2df77a5fb3 100644 --- a/yarn-project/p2p/src/msg_validators/attestation_validator/README.md +++ b/yarn-project/p2p/src/msg_validators/attestation_validator/README.md @@ -24,7 +24,7 @@ This module validates `CheckpointAttestation` gossipsub messages. Attestations a |---|------|-------------| | 8 | Sender recoverable (pool-side) | Silent drop | | 9 | Not a duplicate (same slot + proposalId + signer) | IGNORE | -| 10 | Per-signer cap: `MAX_ATTESTATIONS_PER_SLOT_AND_SIGNER` = 3 | IGNORE | +| 10 | Per-signer cap: `MAX_ATTESTATIONS_PER_SLOT_AND_SIGNER` = 2 | IGNORE | Own attestations added via `addOwnCheckpointAttestations` bypass the per-signer cap. diff --git a/yarn-project/p2p/src/msg_validators/proposal_validator/README.md b/yarn-project/p2p/src/msg_validators/proposal_validator/README.md index fa7a9694dd63..e560f3babc89 100644 --- a/yarn-project/p2p/src/msg_validators/proposal_validator/README.md +++ b/yarn-project/p2p/src/msg_validators/proposal_validator/README.md @@ -28,7 +28,7 @@ Deserialization guards: `BlockProposal.fromBuffer` and `SignedTxs.fromBuffer` bo | # | Rule | Consequence | |---|------|-------------| | 9 | **Duplicate**: same archive root already stored | IGNORE (no penalty) | -| 10 | **Per-position cap**: max 3 proposals per (slot, indexWithinCheckpoint) | REJECT + HighToleranceError | +| 10 | **Per-position cap**: max 2 proposals per (slot, indexWithinCheckpoint) | REJECT + HighToleranceError | | 11 | **Equivocation**: >1 distinct proposal for same (slot, index) | ACCEPT (rebroadcast for detection). At count=2: `duplicateProposalCallback` fires -> slash event (`OffenseType.DUPLICATE_PROPOSAL`, configured via `slashDuplicateProposalPenalty`) | ### Stage 3: Validator-Client Processing (BlockProposalHandler) @@ -84,7 +84,7 @@ The checkpoint's embedded `lastBlock` is extracted via `getBlockProposal()` and | Rule | Consequence | File | |------|-------------|------| | Block proposal must pass `BlockProposalValidator.validate()` | If REJECT: entire checkpoint REJECTED | `libp2p_service.ts` | -| Block proposal must not exceed per-position cap (3) | Checkpoint REJECTED + HighToleranceError | same | +| Block proposal must not exceed per-position cap (2) | Checkpoint REJECTED + HighToleranceError | same | | Block equivocation detected (>1 proposals for same slot+index) | Checkpoint REJECTED (block itself is ACCEPT for re-broadcast) | same | ### Stage 3: Mempool (Attestation Pool) @@ -92,7 +92,7 @@ The checkpoint's embedded `lastBlock` is extracted via `getBlockProposal()` and | Rule | Consequence | File | |------|-------------|------| | Duplicate (same archive ID) | IGNORE (no penalty). Embedded block still processed if valid. | `attestation_pool.ts` | -| Per-slot cap: `MAX_CHECKPOINT_PROPOSALS_PER_SLOT` = 5 | REJECT + HighToleranceError. Embedded block still processed. | same | +| Per-slot cap: `MAX_CHECKPOINT_PROPOSALS_PER_SLOT` = 2 | REJECT + HighToleranceError. Embedded block still processed. | same | ### Stage 4: Equivocation Detection