Skip to content

Commit 2d4e048

Browse files
committed
fix: reject aztecSlotDuration not a multiple of ethereumSlotDuration
L2 slot boundaries must land on L1 slot boundaries, but nothing enforced this — the one existing check only gated the three static mainnet/ testnet/devnet config files. Add assertValidSlotDurations and call it from deployAztecL1Contracts, the shared choke point for e2e tests, spartan deployments, and CLI deploys. Fixes three e2e configs that paired aztecSlotDuration: 36 with ethereumSlotDuration: 8 (4.5 L1 slots per L2 slot).
1 parent f397aa5 commit 2d4e048

7 files changed

Lines changed: 81 additions & 20 deletions

File tree

yarn-project/end-to-end/src/multi-node/block-production/multi_validator_node.parallel.test.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { getContract } from 'viem';
1616
import type { TestWallet } from '../../test-wallet/test_wallet.js';
1717
import {
1818
MOCK_GOSSIP_MULTI_VALIDATOR_OPTS,
19+
MULTI_VALIDATOR_BLOCK_PRODUCTION_TIMING,
1920
MultiNodeTestContext,
2021
NO_REORG_SUBMISSION_EPOCHS,
2122
buildMockGossipValidators,
@@ -27,7 +28,7 @@ const COMMITTEE_SIZE = VALIDATOR_COUNT - 2;
2728
// Tests that a single AztecNodeService hosting multiple validator keys correctly signs attestations
2829
// and filters signing to only active committee members. One node, 5 validators staked, committee
2930
// size 3. Uses MultiNodeTestContext on the mock-gossip bus: all 5 validators on a single physical
30-
// node, ethSlot=8s, aztecSlot=36s, epoch=2, proofSubEpochs=NO_REORG_SUBMISSION_EPOCHS. Each it is an isolated CI job
31+
// node, ethSlot=12s, aztecSlot=36s, epoch=2, proofSubEpochs=NO_REORG_SUBMISSION_EPOCHS. Each it is an isolated CI job
3132
// (parallel convention).
3233
describe('multi-node/block-production/multi_validator_node', () => {
3334
jest.setTimeout(15 * 60 * 1000);
@@ -47,14 +48,12 @@ describe('multi-node/block-production/multi_validator_node', () => {
4748

4849
test = await MultiNodeTestContext.setup({
4950
...MOCK_GOSSIP_MULTI_VALIDATOR_OPTS,
51+
...MULTI_VALIDATOR_BLOCK_PRODUCTION_TIMING,
5052
initialValidators: validators,
5153
aztecTargetCommitteeSize: COMMITTEE_SIZE,
5254
aztecEpochDuration: 2,
53-
ethereumSlotDuration: 8,
54-
aztecSlotDuration: 36,
5555
aztecProofSubmissionEpochs: NO_REORG_SUBMISSION_EPOCHS,
5656
anvilSlotsInAnEpoch: 4,
57-
blockDurationMs: 6000,
5857
minTxsPerBlock: 0,
5958
inboxLag: 2,
6059
});

yarn-project/end-to-end/src/multi-node/invalid-attestations/invalidate_block.parallel.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ describe('multi-node/invalid-attestations/invalidate_block', () => {
6666
// Uses multiple-blocks-per-slot timing configuration.
6767
test = await MultiNodeTestContext.setup({
6868
...MOCK_GOSSIP_MULTI_VALIDATOR_OPTS,
69-
ethereumSlotDuration: 8,
69+
ethereumSlotDuration: 12,
7070
aztecSlotDuration: 36,
7171
blockDurationMs: 6000,
7272
initialValidators: validators,

yarn-project/end-to-end/src/p2p/reqresp/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export async function createReqrespTest(options: ReqrespOptions = {}): Promise<P
3434
// To collect metrics - run in aztec-packages `docker compose --profile metrics up`
3535
metricsPort: shouldCollectMetrics(),
3636
initialConfig: {
37-
ethereumSlotDuration: 8,
37+
ethereumSlotDuration: 12,
3838
aztecSlotDuration: 36,
3939
blockDurationMs: 6000,
4040
minTxsPerBlock: 1,
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { assertValidSlotDurations, validateSlotDurations } from './config.js';
2+
3+
describe('validateSlotDurations', () => {
4+
it('returns no errors for a sound config', () => {
5+
expect(validateSlotDurations({ ethereumSlotDuration: 12, aztecSlotDuration: 36 })).toEqual([]);
6+
});
7+
8+
it('errors when aztecSlotDuration is not a multiple of ethereumSlotDuration', () => {
9+
expect(validateSlotDurations({ ethereumSlotDuration: 8, aztecSlotDuration: 36 })).toContainEqual(
10+
expect.stringContaining('must be a multiple'),
11+
);
12+
});
13+
14+
it('errors when ethereumSlotDuration is non-positive', () => {
15+
expect(validateSlotDurations({ ethereumSlotDuration: 0, aztecSlotDuration: 36 })).toContainEqual(
16+
expect.stringContaining('ethereumSlotDuration must be positive'),
17+
);
18+
});
19+
20+
it('errors when aztecSlotDuration is non-positive', () => {
21+
expect(validateSlotDurations({ ethereumSlotDuration: 12, aztecSlotDuration: 0 })).toContainEqual(
22+
expect.stringContaining('aztecSlotDuration must be positive'),
23+
);
24+
});
25+
});
26+
27+
describe('assertValidSlotDurations', () => {
28+
it('does not throw for a sound config', () => {
29+
expect(() => assertValidSlotDurations({ ethereumSlotDuration: 12, aztecSlotDuration: 36 })).not.toThrow();
30+
});
31+
32+
it('throws when aztecSlotDuration is not a multiple of ethereumSlotDuration', () => {
33+
expect(() => assertValidSlotDurations({ ethereumSlotDuration: 8, aztecSlotDuration: 36 })).toThrow(
34+
/must be a multiple/,
35+
);
36+
});
37+
});

yarn-project/ethereum/src/config.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,41 @@ export const l1ContractsConfigMappings: ConfigMappingsType<L1ContractsConfig> =
246246
*/
247247
export const DefaultL1ContractsConfig = getDefaultConfig(l1ContractsConfigMappings);
248248

249+
/**
250+
* Validates that `ethereumSlotDuration` and `aztecSlotDuration` are positive and that the L2 slot is an exact
251+
* multiple of the L1 slot. Every L2 slot boundary must land on an L1 slot boundary; a non-multiple pairing
252+
* desyncs the epoch/checkpoint timing math throughout the sequencer, validator client, and timetables. Returns
253+
* a list of error messages (empty when valid).
254+
*/
255+
export function validateSlotDurations(
256+
config: Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'>,
257+
): string[] {
258+
const errors: string[] = [];
259+
if (config.ethereumSlotDuration <= 0) {
260+
errors.push(`ethereumSlotDuration must be positive (got ${config.ethereumSlotDuration})`);
261+
}
262+
if (config.aztecSlotDuration <= 0) {
263+
errors.push(`aztecSlotDuration must be positive (got ${config.aztecSlotDuration})`);
264+
}
265+
if (config.ethereumSlotDuration > 0 && config.aztecSlotDuration % config.ethereumSlotDuration !== 0) {
266+
errors.push(
267+
`aztecSlotDuration (${config.aztecSlotDuration}s) must be a multiple of ethereumSlotDuration ` +
268+
`(${config.ethereumSlotDuration}s)`,
269+
);
270+
}
271+
return errors;
272+
}
273+
274+
/** Throws if {@link validateSlotDurations} reports any errors. */
275+
export function assertValidSlotDurations(
276+
config: Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'>,
277+
): void {
278+
const errors = validateSlotDurations(config);
279+
if (errors.length > 0) {
280+
throw new Error(`Invalid slot duration configuration:\n${errors.map(e => ` - ${e}`).join('\n')}`);
281+
}
282+
}
283+
249284
export const genesisStateConfigMappings: ConfigMappingsType<GenesisStateConfig> = {
250285
testAccounts: {
251286
env: 'TEST_ACCOUNTS',

yarn-project/ethereum/src/deploy_aztec_l1_contracts.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { mainnet, sepolia } from 'viem/chains';
1919

2020
import { createEthereumChain, isAnvilTestChain } from './chain.js';
2121
import { createExtendedL1Client } from './client.js';
22-
import type { L1ContractsConfig } from './config.js';
22+
import { type L1ContractsConfig, assertValidSlotDurations } from './config.js';
2323
import { deployMulticall3 } from './contracts/multicall.js';
2424
import { RollupContract } from './contracts/rollup.js';
2525
import type { L1ContractAddresses } from './l1_contract_addresses.js';
@@ -282,6 +282,7 @@ export async function deployAztecL1Contracts(
282282
args: DeployAztecL1ContractsArgs,
283283
): Promise<DeployAztecL1ContractsReturnType> {
284284
logger.info(`Deploying L1 contracts with config: ${jsonStringify(args)}`);
285+
assertValidSlotDurations(args);
285286
if (args.initialValidators && args.initialValidators.length > 0 && args.existingTokenAddress) {
286287
throw new Error(
287288
'Cannot deploy with both initialValidators and existingTokenAddress. ' +

yarn-project/stdlib/src/config/network-consensus-config.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type L1ContractsConfig, l1ContractsConfigMappings } from '@aztec/ethereum/config';
1+
import { type L1ContractsConfig, l1ContractsConfigMappings, validateSlotDurations } from '@aztec/ethereum/config';
22
import { type EnvVar, pickConfigMappings } from '@aztec/foundation/config';
33

44
import type { SequencerConfig } from '../interfaces/configs.js';
@@ -156,21 +156,10 @@ export function validateNetworkConsensusConfig(config: NetworkConsensusConfig):
156156
return errors;
157157
}
158158

159-
if (config.ethereumSlotDuration <= 0) {
160-
errors.push(`ethereumSlotDuration must be positive (got ${config.ethereumSlotDuration})`);
161-
}
159+
errors.push(...validateSlotDurations(config));
162160
if (config.blockDurationMs <= 0) {
163161
errors.push(`blockDurationMs must be positive (got ${config.blockDurationMs})`);
164162
}
165-
if (config.aztecSlotDuration <= 0) {
166-
errors.push(`aztecSlotDuration must be positive (got ${config.aztecSlotDuration})`);
167-
}
168-
if (config.ethereumSlotDuration > 0 && config.aztecSlotDuration % config.ethereumSlotDuration !== 0) {
169-
errors.push(
170-
`aztecSlotDuration (${config.aztecSlotDuration}s) must be a multiple of ethereumSlotDuration ` +
171-
`(${config.ethereumSlotDuration}s)`,
172-
);
173-
}
174163
if (config.blockDurationMs / 1000 > config.aztecSlotDuration) {
175164
errors.push(
176165
`blockDurationMs (${config.blockDurationMs}ms) exceeds aztecSlotDuration (${config.aztecSlotDuration}s)`,

0 commit comments

Comments
 (0)