Skip to content

Commit 2842270

Browse files
committed
feat(fast-inbox): reject over-cap block counts at attestation time (A-1539)
The checkpoint root circuit caps how many blocks a checkpoint may contain, but L1 cannot enforce that cap when the checkpoint is proposed: `ProposedHeader` carries no block count, only `blockHeadersHash`. An over-cap checkpoint would therefore be accepted on L1 and then be unprovable, forcing an epoch reorg — so validators have to reject it before attesting, the same enforcement pattern the streaming Inbox uses for `MAX_L1_TO_L2_MSGS_PER_CHECKPOINT`. The checkpoint-proposal check already existed but only fired for an operator-configured `maxBlocksPerCheckpoint`. It now always applies the protocol cap, with the operator limit only able to tighten it. Test: an over-cap checkpoint proposal is rejected with no operator limit configured.
1 parent a8cbe0c commit 2842270

2 files changed

Lines changed: 30 additions & 4 deletions

File tree

yarn-project/validator-client/src/proposal_handler.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Archiver } from '@aztec/archiver';
22
import type { BlobClientInterface } from '@aztec/blob-client/client';
3-
import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
3+
import { INITIAL_L2_BLOCK_NUM, MAX_BLOCKS_PER_CHECKPOINT } from '@aztec/constants';
44
import type { EpochCache } from '@aztec/epoch-cache';
55
import { MAX_FEE_ASSET_PRICE_MODIFIER_BPS } from '@aztec/ethereum/contracts';
66
import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
@@ -196,6 +196,25 @@ describe('ProposalHandler checkpoint validation', () => {
196196
expect(result).toEqual({ isValid: false, reason: 'too_many_blocks_in_checkpoint' });
197197
});
198198

199+
it('returns too_many_blocks_in_checkpoint when blocks exceed the protocol cap without an operator limit', async () => {
200+
// The checkpoint root circuit caps the blocks a checkpoint may contain; an over-cap checkpoint is unprovable,
201+
// and L1 cannot reject it at propose time, so the validator must not attest to it.
202+
expect(config.maxBlocksPerCheckpoint).toBeUndefined();
203+
204+
const archiveRoot = Fr.random();
205+
blockSource.getBlockData.mockResolvedValue({ header: makeBlockHeader() } as BlockData);
206+
const overCap = MAX_BLOCKS_PER_CHECKPOINT + 1;
207+
const blocks = Array.from({ length: overCap }, (_, i) => ({
208+
archive: new AppendOnlyTreeSnapshot(i === overCap - 1 ? archiveRoot : Fr.random(), i + 1),
209+
number: i + 1,
210+
})) as unknown as L2Block[];
211+
blockSource.getBlocksForSlot.mockResolvedValue(blocks);
212+
213+
const proposal = await makeProposal({ archiveRoot });
214+
const result = await handler.handleCheckpointProposal(proposal, proposalInfo);
215+
expect(result).toEqual({ isValid: false, reason: 'too_many_blocks_in_checkpoint' });
216+
});
217+
199218
it('caches validation result and returns it on second call', async () => {
200219
blockSource.getBlockData.mockResolvedValue(undefined);
201220
const proposal = await makeProposal();

yarn-project/validator-client/src/proposal_handler.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { type Blob, encodeCheckpointBlobDataFromBlocks, getBlobsPerL1Block } fro
44
import {
55
INBOX_LAG_SECONDS,
66
INITIAL_L2_BLOCK_NUM,
7+
MAX_BLOCKS_PER_CHECKPOINT,
78
MAX_L1_TO_L2_MSGS_PER_BLOCK,
89
MAX_L1_TO_L2_MSGS_PER_CHECKPOINT,
910
} from '@aztec/constants';
@@ -1282,9 +1283,15 @@ export class ProposalHandler {
12821283
};
12831284
}
12841285

1285-
// Note this condition should never trigger, since we dont process block proposals that exceed indexWithinCheckpoint
1286-
const maxBlocksPerCheckpoint = this.config.maxBlocksPerCheckpoint;
1287-
if (maxBlocksPerCheckpoint !== undefined && blocks.length > maxBlocksPerCheckpoint) {
1286+
// The checkpoint root circuit caps how many blocks a checkpoint may contain, and L1 cannot check it when the
1287+
// checkpoint is proposed (`ProposedHeader` carries no block count, only `blockHeadersHash`). An over-cap
1288+
// checkpoint would therefore be accepted on L1 and then be unprovable, forcing an epoch reorg, so validators
1289+
// reject it before attesting. An operator-configured limit may only tighten the protocol cap, never relax it.
1290+
const maxBlocksPerCheckpoint = Math.min(
1291+
this.config.maxBlocksPerCheckpoint ?? MAX_BLOCKS_PER_CHECKPOINT,
1292+
MAX_BLOCKS_PER_CHECKPOINT,
1293+
);
1294+
if (blocks.length > maxBlocksPerCheckpoint) {
12881295
this.log.warn(`Checkpoint proposal exceeds maxBlocksPerCheckpoint`, {
12891296
...proposalInfo,
12901297
blocksInProposal: blocks.length,

0 commit comments

Comments
 (0)