Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -84,15 +84,15 @@ 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)

| 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

Expand Down
Loading