Skip to content

Commit a269dff

Browse files
committed
refactor(fast-inbox): extract shared inbox consumption predicate (A-1383)
Move the streaming-Inbox cutoff formula and the mandatory-consumption / cap-escape rule into a shared `stdlib/messaging/inbox_consumption` module so the sequencer's bucket selection and the validator's last-block censorship check share one source of truth for the boundary semantics. - `getInboxCutoffTimestamp(slot, l1Constants, lagSeconds)`: buildFrameStart(slot) - lagSeconds, mirroring `ProposeLib.validateInboxConsumption`. The sequencer's `selectStreamingBundle` now calls it instead of inlining the computation (behavior unchanged). - `isInboxConsumptionSufficient(...)`: the none / past-cutoff (strict >) / cap-escape predicate. - Tests pin the A-1371 section-13 cross-layer vectors (genesisTime=100000, slotDuration=36): the cutoff table and the S=10 mandatory-consumption boundary.
1 parent 00a7d98 commit a269dff

4 files changed

Lines changed: 156 additions & 4 deletions

File tree

yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,12 @@ import {
5252
type ResolvedSequencerConfig,
5353
type WorldStateSynchronizer,
5454
} from '@aztec/stdlib/interfaces/server';
55-
import { InboxBucketRef, type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
55+
import {
56+
InboxBucketRef,
57+
type L1ToL2MessageSource,
58+
computeInHashFromL1ToL2Messages,
59+
getInboxCutoffTimestamp,
60+
} from '@aztec/stdlib/messaging';
5661
import type {
5762
BlockProposal,
5863
BlockProposalOptions,
@@ -1127,9 +1132,7 @@ export class CheckpointProposalJob implements Traceable {
11271132
isLastBlock: boolean,
11281133
nowSeconds: number,
11291134
): Promise<InboxBucketSelection> {
1130-
// Mirror ProposeLib's cutoff exactly: buildFrameStart = toTimestamp(slot - 1), cutoff = buildFrameStart - lag.
1131-
const buildFrameStart = getTimestampForSlot(SlotNumber(this.targetSlot - 1), this.l1Constants);
1132-
const cutoffTimestamp = buildFrameStart - BigInt(INBOX_LAG_SECONDS);
1135+
const cutoffTimestamp = getInboxCutoffTimestamp(this.targetSlot, this.l1Constants, INBOX_LAG_SECONDS);
11331136
return selectInboxBucketForBlock({
11341137
messageSource: this.l1ToL2MessageSource,
11351138
now: BigInt(Math.floor(nowSeconds)),
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { SlotNumber } from '@aztec/foundation/branded-types';
2+
3+
import { describe, expect, it } from '@jest/globals';
4+
5+
import type { L1RollupConstants } from '../epoch-helpers/index.js';
6+
import { getInboxCutoffTimestamp, isInboxConsumptionSufficient } from './inbox_consumption.js';
7+
8+
// Cross-layer test vectors pinned by A-1371 decision 13 (the `ProposeInboxConsumptionTest` Foundry harness values).
9+
// The same vectors are asserted against `ProposeLib.validateInboxConsumption` on L1; keeping them identical here makes
10+
// L1, TS, and the design doc agree on the cutoff formula and the mandatory-consumption boundary.
11+
const GENESIS_TIME = 100_000n;
12+
const SLOT_DURATION = 36;
13+
const LAG_SECONDS = 12;
14+
15+
const l1Constants = { l1GenesisTime: GENESIS_TIME, slotDuration: SLOT_DURATION } as Pick<
16+
L1RollupConstants,
17+
'l1GenesisTime' | 'slotDuration'
18+
>;
19+
20+
describe('inbox_consumption', () => {
21+
describe('getInboxCutoffTimestamp', () => {
22+
// buildFrameStart(S) = 100000 + (S - 1) * 36; cutoff(S) = buildFrameStart(S) - 12. Pinned in A-1371 §13.
23+
it.each([
24+
[1, 99_988n],
25+
[2, 100_024n],
26+
[10, 100_312n],
27+
[11, 100_348n],
28+
])('matches the A-1371 §13 cutoff table for slot %i', (slot, expectedCutoff) => {
29+
expect(getInboxCutoffTimestamp(SlotNumber(slot), l1Constants, LAG_SECONDS)).toBe(expectedCutoff);
30+
});
31+
});
32+
33+
describe('isInboxConsumptionSufficient', () => {
34+
const base = { cutoffTimestamp: 100_312n, checkpointStartTotalMsgCount: 0n, perCheckpointCap: 1024 };
35+
36+
it('is sufficient when there is no next bucket (consumed everything)', () => {
37+
expect(isInboxConsumptionSufficient({ ...base, nextBucket: undefined })).toBe(true);
38+
});
39+
40+
// A-1371 §13 boundary at S=10 (cutoff = 100312): a bucket opened exactly at the cutoff is mandatory (strict `>`).
41+
it('is insufficient when the next bucket opened exactly at the cutoff is left unconsumed', () => {
42+
expect(isInboxConsumptionSufficient({ ...base, nextBucket: { timestamp: 100_312n, totalMsgCount: 5n } })).toBe(
43+
false,
44+
);
45+
});
46+
47+
// A-1371 §13 boundary at S=10: a bucket opened at cutoff + 1 is past the cutoff and need not be consumed.
48+
it('is sufficient when the next bucket opened at cutoff + 1 is left unconsumed', () => {
49+
expect(isInboxConsumptionSufficient({ ...base, nextBucket: { timestamp: 100_313n, totalMsgCount: 5n } })).toBe(
50+
true,
51+
);
52+
});
53+
54+
it('is sufficient via cap-escape when consuming through the next bucket would exceed the per-checkpoint cap', () => {
55+
// Next bucket is at/before the cutoff (would otherwise be mandatory), but consuming through it consumes
56+
// 1025 > 1024 messages, so leaving it unconsumed is allowed (the cap-escape branch).
57+
expect(
58+
isInboxConsumptionSufficient({
59+
...base,
60+
nextBucket: { timestamp: 100_000n, totalMsgCount: 1025n },
61+
}),
62+
).toBe(true);
63+
});
64+
65+
it('is insufficient when consuming through the next bucket exactly reaches the per-checkpoint cap', () => {
66+
// Delta of exactly 1024 does not escape (strict `>` on the cap), so a mandatory bucket must still be consumed.
67+
expect(
68+
isInboxConsumptionSufficient({
69+
...base,
70+
nextBucket: { timestamp: 100_000n, totalMsgCount: 1024n },
71+
}),
72+
).toBe(false);
73+
});
74+
75+
it('measures the cap-escape delta from the checkpoint start, not from zero', () => {
76+
// With a non-zero checkpoint start, only the delta consumed this checkpoint counts against the cap.
77+
expect(
78+
isInboxConsumptionSufficient({
79+
cutoffTimestamp: 100_312n,
80+
checkpointStartTotalMsgCount: 500n,
81+
perCheckpointCap: 1024,
82+
nextBucket: { timestamp: 100_000n, totalMsgCount: 1524n },
83+
}),
84+
).toBe(false); // delta = 1024, not an escape
85+
expect(
86+
isInboxConsumptionSufficient({
87+
cutoffTimestamp: 100_312n,
88+
checkpointStartTotalMsgCount: 500n,
89+
perCheckpointCap: 1024,
90+
nextBucket: { timestamp: 100_000n, totalMsgCount: 1525n },
91+
}),
92+
).toBe(true); // delta = 1025, escapes
93+
});
94+
});
95+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { SlotNumber } from '@aztec/foundation/branded-types';
2+
3+
import { type L1RollupConstants, getTimestampForSlot } from '../epoch-helpers/index.js';
4+
import type { InboxBucket } from './inbox_bucket.js';
5+
6+
/**
7+
* Censorship cutoff timestamp for a checkpoint proposed in `slot` (AZIP-22 Fast Inbox), mirroring the cutoff in
8+
* `ProposeLib.validateInboxConsumption`. A checkpoint proposed in slot `S` is built during slot `S - 1`, so
9+
* `buildFrameStart(S) = toTimestamp(S - 1)` and `cutoff(S) = buildFrameStart(S) - lagSeconds`. Buckets opened at or
10+
* before the cutoff are mandatory to consume by the checkpoint's last block; the strict `>` on the L1 "past cutoff"
11+
* test (see {@link isInboxConsumptionSufficient}) makes a bucket opened exactly at the cutoff mandatory.
12+
*
13+
* This is the single source of truth for the cutoff formula shared by the sequencer's streaming bucket selection and
14+
* the validator's last-block censorship check.
15+
*/
16+
export function getInboxCutoffTimestamp(
17+
slot: SlotNumber,
18+
l1Constants: Pick<L1RollupConstants, 'l1GenesisTime' | 'slotDuration'>,
19+
lagSeconds: number,
20+
): bigint {
21+
return getTimestampForSlot(SlotNumber(slot - 1), l1Constants) - BigInt(lagSeconds);
22+
}
23+
24+
/**
25+
* Whether a checkpoint whose last-consumed bucket is immediately followed by `nextBucket` meets the censorship floor,
26+
* mirroring the mandatory-consumption assert in `ProposeLib.validateInboxConsumption` (AZIP-22 Fast Inbox).
27+
* Consumption is sufficient when the first unconsumed bucket:
28+
* - does not exist (the checkpoint consumed everything the Inbox has), or
29+
* - was opened strictly after the cutoff (`timestamp > cutoffTimestamp`), or
30+
* - consuming through it would exceed the per-checkpoint cap (the cap-escape).
31+
*
32+
* The strict `>` matches L1: a bucket opened exactly at the cutoff must be consumed. This is the single source of
33+
* truth for the minimum-consumption / cap-escape rule shared by the sequencer's selection floor and the validator.
34+
*/
35+
export function isInboxConsumptionSufficient(input: {
36+
/** The first unconsumed bucket (the one after the checkpoint's last-consumed bucket), or undefined if none exists. */
37+
nextBucket: Pick<InboxBucket, 'timestamp' | 'totalMsgCount'> | undefined;
38+
/** Censorship cutoff timestamp from {@link getInboxCutoffTimestamp}. */
39+
cutoffTimestamp: bigint;
40+
/** Cumulative Inbox message count consumed as of the parent checkpoint; the per-checkpoint cap origin. */
41+
checkpointStartTotalMsgCount: bigint;
42+
/** Maximum number of messages the checkpoint may consume in total (`MAX_L1_TO_L2_MSGS_PER_CHECKPOINT`). */
43+
perCheckpointCap: number;
44+
}): boolean {
45+
const { nextBucket, cutoffTimestamp, checkpointStartTotalMsgCount, perCheckpointCap } = input;
46+
if (nextBucket === undefined) {
47+
return true;
48+
}
49+
if (nextBucket.timestamp > cutoffTimestamp) {
50+
return true;
51+
}
52+
return nextBucket.totalMsgCount - checkpointStartTotalMsgCount > BigInt(perCheckpointCap);
53+
}

yarn-project/stdlib/src/messaging/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './append_l1_to_l2_messages.js';
22
export * from './in_hash.js';
33
export * from './inbox_bucket.js';
4+
export * from './inbox_consumption.js';
45
export * from './inbox_leaf.js';
56
export * from './inbox_rolling_hash.js';
67
export * from './l1_to_l2_message_bundle.js';

0 commit comments

Comments
 (0)