Skip to content

Commit c1e72e3

Browse files
committed
chore: adjust clock tolerance
1 parent 4f49f4a commit c1e72e3

20 files changed

Lines changed: 744 additions & 186 deletions

File tree

yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.pipeline.parallel.test.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ import { EpochsTestContext } from './epochs_test.js';
3030
jest.setTimeout(1000 * 60 * 20);
3131

3232
const NODE_COUNT = 4;
33-
const EXPECTED_BLOCKS_PER_CHECKPOINT = 3;
33+
const EXPECTED_BLOCKS_PER_CHECKPOINT = 8;
3434

3535
// Send enough transactions to trigger multiple blocks within a checkpoint assuming 2 txs per block.
36-
const TX_COUNT = 10;
36+
const TX_COUNT = 24;
3737

3838
/**
3939
* E2E tests for proposer pipelining with Multiple Blocks Per Slot (MBPS).
@@ -72,16 +72,18 @@ describe('e2e_epochs/epochs_mbps_pipeline', () => {
7272
initialValidators: validators,
7373
enableProposerPipelining: true, // <- yehaw
7474
mockGossipSubNetwork: true,
75+
mockGossipSubNetworkLatency: 500, // 200 ms delay in message prop - adverse network conditions
7576
disableAnvilTestWatcher: true,
7677
startProverNode: true,
77-
perBlockAllocationMultiplier: 1,
78+
perBlockAllocationMultiplier: 8,
7879
aztecEpochDuration: 4,
7980
enforceTimeTable: true,
80-
ethereumSlotDuration: 4,
81-
aztecSlotDuration: 36,
81+
ethereumSlotDuration: 12,
82+
aztecSlotDuration: 72,
8283
blockDurationMs: 8000,
83-
l1PublishingTime: 2,
84-
attestationPropagationTime: 0.5,
84+
// maxDABlockGas: 786432, // Set max DA block gas to be the same as the checkpoint
85+
// l1PublishingTime: 2,
86+
// attestationPropagationTime: 1,
8587
aztecTargetCommitteeSize: 3,
8688
...setupOpts,
8789
pxeOpts: { syncChainTip },

yarn-project/end-to-end/src/fixtures/setup.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ export type SetupOptions = {
177177
proverNodeConfig?: Partial<ProverNodeConfig>;
178178
/** Whether to use a mock gossip sub network for p2p clients. */
179179
mockGossipSubNetwork?: boolean;
180+
/** Whether to add simulated latency to the mock gossipsub network (in ms) */
181+
mockGossipSubNetworkLatency?: number;
180182
/** Whether to disable the anvil test watcher (can still be manually started) */
181183
disableAnvilTestWatcher?: boolean;
182184
/** Whether to enable anvil automine during deployment of L1 contracts (consider defaulting this to true). */
@@ -464,7 +466,7 @@ export async function setup(
464466
let p2pClientDeps: P2PClientDeps | undefined = undefined;
465467

466468
if (opts.mockGossipSubNetwork) {
467-
mockGossipSubNetwork = new MockGossipSubNetwork();
469+
mockGossipSubNetwork = new MockGossipSubNetwork(opts.mockGossipSubNetworkLatency);
468470
p2pClientDeps = { p2pServiceFactory: getMockPubSubP2PServiceFactory(mockGossipSubNetwork) };
469471
}
470472

yarn-project/ethereum/src/contracts/rollup.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,27 @@ describe('Rollup', () => {
117117
});
118118
});
119119

120+
describe('makeArchiveOverride', () => {
121+
it('creates state override that correctly sets archive for a checkpoint number', async () => {
122+
const checkpointNumber = CheckpointNumber(5);
123+
const expectedArchive = Fr.random();
124+
125+
// Create the override
126+
const stateOverride = rollup.makeArchiveOverride(checkpointNumber, expectedArchive);
127+
128+
// Test the override using simulateContract to read archiveAt(checkpointNumber)
129+
const { result: overriddenArchive } = await publicClient.simulateContract({
130+
address: rollupAddress,
131+
abi: RollupAbi as Abi,
132+
functionName: 'archiveAt',
133+
args: [BigInt(checkpointNumber)],
134+
stateOverride,
135+
});
136+
137+
expect(Fr.fromString(overriddenArchive as string).equals(expectedArchive)).toBe(true);
138+
});
139+
});
140+
120141
describe('getSlashingProposer', () => {
121142
it('returns a slashing proposer', async () => {
122143
const slashingProposer = await rollup.getSlashingProposer();

yarn-project/p2p/src/msg_validators/attestation_validator/attestation_validator.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,72 @@ describe('CheckpointAttestationValidator', () => {
7474
expect(result).toEqual({ result: 'ignore' });
7575
});
7676

77+
it('accepts attestation for current slot within pipelining grace period', async () => {
78+
// Proposal is for slot 98 (current wallclock slot), but targetSlot is 99 (pipelining)
79+
const header = CheckpointHeader.random({ slotNumber: SlotNumber(98) });
80+
const mockAttestation = makeCheckpointAttestation({
81+
header,
82+
attesterSigner: attester,
83+
proposerSigner: proposer,
84+
});
85+
86+
epochCache.getTargetAndNextSlot.mockReturnValue({
87+
targetSlot: SlotNumber(99),
88+
nextSlot: SlotNumber(100),
89+
});
90+
epochCache.getSlotNow.mockReturnValue(SlotNumber(98));
91+
epochCache.isProposerPipeliningEnabled.mockReturnValue(true);
92+
epochCache.getL1Constants.mockReturnValue({
93+
ethereumSlotDuration: 12,
94+
} as any);
95+
96+
// Within grace period: 1000ms elapsed < 6000ms
97+
epochCache.getEpochAndSlotNow.mockReturnValue({
98+
epoch: EpochNumber(1),
99+
slot: SlotNumber(98),
100+
ts: 1000n,
101+
nowMs: 1001000n, // 1000ms elapsed
102+
});
103+
epochCache.isInCommittee.mockResolvedValue(true);
104+
epochCache.getProposerAttesterAddressInSlot.mockResolvedValue(proposer.address);
105+
106+
const result = await validator.validate(mockAttestation);
107+
expect(result).toEqual({ result: 'accept' });
108+
});
109+
110+
it('rejects attestation for current slot outside pipelining grace period', async () => {
111+
// Proposal is for slot 97 (one behind current wallclock slot 98), targetSlot is 99 (pipelining)
112+
const header = CheckpointHeader.random({ slotNumber: SlotNumber(97) });
113+
const mockAttestation = makeCheckpointAttestation({
114+
header,
115+
attesterSigner: attester,
116+
proposerSigner: proposer,
117+
});
118+
119+
epochCache.getTargetAndNextSlot.mockReturnValue({
120+
targetSlot: SlotNumber(99),
121+
nextSlot: SlotNumber(100),
122+
});
123+
epochCache.getTargetSlot.mockReturnValue(SlotNumber(99));
124+
epochCache.getSlotNow.mockReturnValue(SlotNumber(98));
125+
epochCache.isProposerPipeliningEnabled.mockReturnValue(true);
126+
epochCache.getL1Constants.mockReturnValue({
127+
ethereumSlotDuration: 12,
128+
} as any);
129+
130+
// Outside grace period AND outside clock tolerance: 7000ms elapsed
131+
epochCache.getEpochAndSlotNow.mockReturnValue({
132+
epoch: EpochNumber(1),
133+
slot: SlotNumber(99),
134+
ts: 1000n,
135+
nowMs: 1007000n, // 7000ms elapsed
136+
});
137+
epochCache.isInCommittee.mockResolvedValue(true);
138+
139+
const result = await validator.validate(mockAttestation);
140+
expect(result).toEqual({ result: 'reject', severity: PeerErrorSeverity.HighToleranceError });
141+
});
142+
77143
it('returns high tolerance error if attester is not in committee', async () => {
78144
const header = CheckpointHeader.random({ slotNumber: SlotNumber(100) });
79145
const mockAttestation = makeCheckpointAttestation({

yarn-project/p2p/src/msg_validators/attestation_validator/attestation_validator.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
type ValidationResult,
99
} from '@aztec/stdlib/p2p';
1010

11-
import { isWithinClockTolerance } from '../clock_tolerance.js';
11+
import { isWithinClockTolerance, isWithinPipeliningGracePeriod } from '../clock_tolerance.js';
1212

1313
export class CheckpointAttestationValidator implements P2PValidator<CheckpointAttestation> {
1414
protected epochCache: EpochCacheInterface;
@@ -23,19 +23,23 @@ export class CheckpointAttestationValidator implements P2PValidator<CheckpointAt
2323
const slotNumber = message.payload.header.slotNumber;
2424

2525
try {
26-
// Use target slots since proposals target pipeline slots (slot + 1 when pipelining)
26+
// Use target slots since proposals target pipeline slots (slot + 1 when pipelining).
2727
const { targetSlot, nextSlot } = this.epochCache.getTargetAndNextSlot();
2828

2929
if (slotNumber !== targetSlot && slotNumber !== nextSlot) {
30-
// Check if message is for previous slot and within clock tolerance
31-
if (!isWithinClockTolerance(slotNumber, targetSlot, this.epochCache)) {
30+
// When pipelining, accept attestations for the current slot (built in the previous slot)
31+
// if we're within the first ethereumSlotDuration/2 seconds of the slot.
32+
if (isWithinPipeliningGracePeriod(slotNumber, this.epochCache)) {
33+
// Fall through to remaining validation (signature, committee, etc.)
34+
} else if (!isWithinClockTolerance(slotNumber, targetSlot, this.epochCache)) {
3235
this.logger.warn(
3336
`Checkpoint attestation slot ${slotNumber} is not current (${targetSlot}) or next (${nextSlot}) slot`,
3437
);
3538
return { result: 'reject', severity: PeerErrorSeverity.HighToleranceError };
39+
} else {
40+
this.logger.debug(`Ignoring checkpoint attestation for previous slot ${slotNumber} within clock tolerance`);
41+
return { result: 'ignore' };
3642
}
37-
this.logger.debug(`Ignoring checkpoint attestation for previous slot ${slotNumber} within clock tolerance`);
38-
return { result: 'ignore' };
3943
}
4044

4145
// Verify the signature is valid

yarn-project/p2p/src/msg_validators/clock_tolerance.test.ts

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { SlotNumber } from '@aztec/foundation/branded-types';
33

44
import { mock } from 'jest-mock-extended';
55

6-
import { MAXIMUM_GOSSIP_CLOCK_DISPARITY_MS, isWithinClockTolerance } from './clock_tolerance.js';
6+
import {
7+
MAXIMUM_GOSSIP_CLOCK_DISPARITY_MS,
8+
isWithinClockTolerance,
9+
isWithinPipeliningGracePeriod,
10+
} from './clock_tolerance.js';
711

812
describe('clock_tolerance', () => {
913
describe('MAXIMUM_GOSSIP_CLOCK_DISPARITY_MS', () => {
@@ -204,4 +208,98 @@ describe('clock_tolerance', () => {
204208
expect(isWithinClockTolerance(messageSlot, currentSlot, epochCache)).toBe(false);
205209
});
206210
});
211+
212+
describe('isWithinPipeliningGracePeriod', () => {
213+
let epochCache: ReturnType<typeof mock<EpochCacheInterface>>;
214+
215+
beforeEach(() => {
216+
epochCache = mock<EpochCacheInterface>();
217+
epochCache.getSlotNow.mockReturnValue(SlotNumber(100));
218+
epochCache.isProposerPipeliningEnabled.mockReturnValue(true);
219+
});
220+
221+
it('returns true when pipelining enabled, message is for current slot, and within grace period', () => {
222+
// Grace period = DEFAULT_P2P_PROPAGATION_TIME * 1000 = 2000ms
223+
epochCache.getEpochAndSlotNow.mockReturnValue({
224+
epoch: 1 as any,
225+
slot: SlotNumber(100),
226+
ts: 1000n,
227+
nowMs: 1001000n, // 1000ms elapsed, within 2000ms grace period
228+
});
229+
230+
expect(isWithinPipeliningGracePeriod(SlotNumber(100), epochCache)).toBe(true);
231+
});
232+
233+
it('returns true at exactly 0ms elapsed', () => {
234+
epochCache.getEpochAndSlotNow.mockReturnValue({
235+
epoch: 1 as any,
236+
slot: SlotNumber(100),
237+
ts: 1000n,
238+
nowMs: 1000000n, // 0ms elapsed
239+
});
240+
241+
expect(isWithinPipeliningGracePeriod(SlotNumber(100), epochCache)).toBe(true);
242+
});
243+
244+
it('returns false when elapsed time exceeds grace period', () => {
245+
// 3000ms elapsed > 2000ms grace period
246+
epochCache.getEpochAndSlotNow.mockReturnValue({
247+
epoch: 1 as any,
248+
slot: SlotNumber(100),
249+
ts: 1000n,
250+
nowMs: 1003000n, // 3000ms elapsed
251+
});
252+
253+
expect(isWithinPipeliningGracePeriod(SlotNumber(100), epochCache)).toBe(false);
254+
});
255+
256+
it('returns false at exactly the grace period boundary', () => {
257+
// 2000ms elapsed = DEFAULT_P2P_PROPAGATION_TIME * 1000 (not strictly less than)
258+
epochCache.getEpochAndSlotNow.mockReturnValue({
259+
epoch: 1 as any,
260+
slot: SlotNumber(100),
261+
ts: 1000n,
262+
nowMs: 1002000n, // 2000ms elapsed
263+
});
264+
265+
expect(isWithinPipeliningGracePeriod(SlotNumber(100), epochCache)).toBe(false);
266+
});
267+
268+
it('returns false when pipelining is disabled', () => {
269+
epochCache.isProposerPipeliningEnabled.mockReturnValue(false);
270+
271+
epochCache.getEpochAndSlotNow.mockReturnValue({
272+
epoch: 1 as any,
273+
slot: SlotNumber(100),
274+
ts: 1000n,
275+
nowMs: 1001000n, // 1000ms elapsed, within grace period
276+
});
277+
278+
expect(isWithinPipeliningGracePeriod(SlotNumber(100), epochCache)).toBe(false);
279+
});
280+
281+
it('returns false when message is not for current slot', () => {
282+
epochCache.getEpochAndSlotNow.mockReturnValue({
283+
epoch: 1 as any,
284+
slot: SlotNumber(100),
285+
ts: 1000n,
286+
nowMs: 1001000n,
287+
});
288+
289+
// Message for slot 99, current slot is 100
290+
expect(isWithinPipeliningGracePeriod(SlotNumber(99), epochCache)).toBe(false);
291+
});
292+
293+
it('returns false when message is for a future slot', () => {
294+
epochCache.getEpochAndSlotNow.mockReturnValue({
295+
epoch: 1 as any,
296+
slot: SlotNumber(100),
297+
ts: 1000n,
298+
nowMs: 1001000n,
299+
});
300+
301+
// Message for slot 101, current slot is 100
302+
expect(isWithinPipeliningGracePeriod(SlotNumber(101), epochCache)).toBe(false);
303+
});
304+
});
207305
});

yarn-project/p2p/src/msg_validators/clock_tolerance.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { EpochCacheInterface } from '@aztec/epoch-cache';
22
import { SlotNumber } from '@aztec/foundation/branded-types';
3+
import { DEFAULT_P2P_PROPAGATION_TIME } from '@aztec/stdlib/timetable';
34

45
/**
56
* Maximum clock disparity tolerance for P2P message validation (in milliseconds).
@@ -50,3 +51,33 @@ export function isWithinClockTolerance(
5051

5152
return elapsedMs < MAXIMUM_GOSSIP_CLOCK_DISPARITY_MS;
5253
}
54+
55+
/**
56+
* Checks if a message should be accepted under the pipelining grace period.
57+
*
58+
* When pipelining is enabled, `targetSlot = slotNow + 1`. A proposal built in slot N-1
59+
* for slot N arrives when validators are in slot N, so their `targetSlot = N+1`.
60+
* This function accepts proposals for the current wallclock slot if we're within the
61+
* first `DEFAULT_P2P_PROPAGATION_TIME` seconds of the slot (the pipelining grace period).
62+
*
63+
* @param messageSlot - The slot number from the received message
64+
* @param epochCache - EpochCache to get timing and pipelining state
65+
* @returns true if pipelining is enabled, the message is for the current slot, and we're within the grace period
66+
*/
67+
export function isWithinPipeliningGracePeriod(messageSlot: SlotNumber, epochCache: EpochCacheInterface): boolean {
68+
if (!epochCache.isProposerPipeliningEnabled()) {
69+
return false;
70+
}
71+
72+
const currentSlot = epochCache.getSlotNow();
73+
if (messageSlot !== currentSlot) {
74+
return false;
75+
}
76+
77+
const { ts: slotStartTs, nowMs } = epochCache.getEpochAndSlotNow();
78+
const slotStartMs = slotStartTs * 1000n;
79+
const elapsedMs = Number(nowMs - slotStartMs);
80+
const gracePeriodMs = DEFAULT_P2P_PROPAGATION_TIME * 1000;
81+
82+
return elapsedMs < gracePeriodMs;
83+
}

0 commit comments

Comments
 (0)