diff --git a/yarn-project/archiver/src/store/block_store.ts b/yarn-project/archiver/src/store/block_store.ts index 6d2c5dc79382..315445d61670 100644 --- a/yarn-project/archiver/src/store/block_store.ts +++ b/yarn-project/archiver/src/store/block_store.ts @@ -179,7 +179,9 @@ export class BlockStore { // Extract the latest block and checkpoint numbers const previousBlockNumber = await this.getLatestBlockNumber(); - const pendingCheckpointNumber = await this.getPendingCheckpointNumber(); + const pendingCheckpoint = await this.getPendingCheckpoint(); + const pendingCheckpointNumber = + pendingCheckpoint?.checkpointNumber ?? CheckpointNumber(INITIAL_CHECKPOINT_NUMBER - 1); const previousCheckpointNumber = await this.getLatestCheckpointNumber(); // Verify we're not overwriting checkpointed blocks @@ -358,11 +360,11 @@ export class BlockStore { // Clear the pending checkpoint if the confirmed checkpoints have caught up to it, // but only if there are no uncheckpointed blocks beyond the confirmed chain. // Pipelining may have built blocks for the next checkpoint on top of the pending one; - // clearing pendingCheckpointNumber while those blocks exist breaks the pipelining skip + // clearing pending checkpoint while those blocks exist breaks the pipelining skip // condition, causing the sequencer to fall through to L1 checks with a stale archive. - const pendingCheckpointNumber = await this.getPendingCheckpointNumber(); + const pendingCheckpoint = await this.getPendingCheckpoint(); const lastConfirmedCheckpointNumber = checkpoints[checkpoints.length - 1].checkpoint.number; - if (pendingCheckpointNumber <= lastConfirmedCheckpointNumber) { + if (pendingCheckpoint && pendingCheckpoint.checkpointNumber <= lastConfirmedCheckpointNumber) { const lastConfirmedBlock = checkpoints[checkpoints.length - 1].checkpoint.blocks.at(-1); const lastBlockNumber = await this.getLatestBlockNumber(); if (!lastConfirmedBlock || lastBlockNumber <= lastConfirmedBlock.number) { @@ -468,8 +470,8 @@ export class BlockStore { } // Clear any pending checkpoint that was removed - const pendingCheckpointNumber = await this.getPendingCheckpointNumber(); - if (pendingCheckpointNumber > checkpointNumber) { + const pendingCheckpoint = await this.getPendingCheckpoint(); + if (pendingCheckpoint && pendingCheckpoint.checkpointNumber > checkpointNumber) { await this.#pendingCheckpoint.delete(); } diff --git a/yarn-project/archiver/src/store/kv_archiver_store.test.ts b/yarn-project/archiver/src/store/kv_archiver_store.test.ts index 79a18d4cff59..4fa599e87195 100644 --- a/yarn-project/archiver/src/store/kv_archiver_store.test.ts +++ b/yarn-project/archiver/src/store/kv_archiver_store.test.ts @@ -3579,10 +3579,10 @@ describe('KVArchiverDataStore', () => { }); }); - describe('pendingCheckpointNumber', () => { - it('returns initial value when no pending checkpoint is set', async () => { - const pending = await store.blockStore.getPendingCheckpointNumber(); - expect(pending).toBe(INITIAL_CHECKPOINT_NUMBER - 1); + describe('pendingCheckpoint', () => { + it('returns undefined when no pending checkpoint is set', async () => { + const pending = await store.blockStore.getPendingCheckpoint(); + expect(pending).toBeUndefined(); }); it('stores and retrieves pending checkpoint number', async () => { @@ -3645,8 +3645,8 @@ describe('KVArchiverDataStore', () => { await store.addCheckpoints([checkpoint2]); // Pending checkpoint should be cleared - const pending = await store.blockStore.getPendingCheckpointNumber(); - expect(pending).toBe(INITIAL_CHECKPOINT_NUMBER - 1); + const pending = await store.blockStore.getPendingCheckpoint(); + expect(pending).toBeUndefined(); }); it('ignores pending checkpoint that is more than 1 ahead of confirmed', async () => { @@ -3722,8 +3722,8 @@ describe('KVArchiverDataStore', () => { // Remove checkpoints after 1 (removes checkpoint 2, and pending 3 should be cleared) await store.removeCheckpointsAfter(CheckpointNumber(1)); - const pending = await store.blockStore.getPendingCheckpointNumber(); - expect(pending).toBe(INITIAL_CHECKPOINT_NUMBER - 1); + const pending = await store.blockStore.getPendingCheckpoint(); + expect(pending).toBeUndefined(); }); it('does not clear pending checkpoint when removing checkpoints before it', async () => { diff --git a/yarn-project/aztec-node/src/aztec-node/server.test.ts b/yarn-project/aztec-node/src/aztec-node/server.test.ts index 81bbcfd60207..943a8f0c25e1 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.test.ts @@ -1007,7 +1007,7 @@ describe('aztec node', () => { jest.restoreAllMocks(); }); - it('sets pending checkpoint from proposal archive', async () => { + it('sets pending checkpoint data from proposal archive', async () => { const archive = Fr.random(); const checkpoint = (await makeCheckpointProposal({ archiveRoot: archive })).toCore(); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.pipeline.parallel.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.pipeline.parallel.test.ts index 76e228065a4d..5cb3ff51616a 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.pipeline.parallel.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.pipeline.parallel.test.ts @@ -30,10 +30,10 @@ import { EpochsTestContext } from './epochs_test.js'; jest.setTimeout(1000 * 60 * 20); const NODE_COUNT = 4; -const EXPECTED_BLOCKS_PER_CHECKPOINT = 8; +const EXPECTED_BLOCKS_PER_CHECKPOINT = 12; // Send enough transactions to trigger multiple blocks within a checkpoint assuming 2 txs per block. -const TX_COUNT = 24; +const TX_COUNT = 34; /** * E2E tests for proposer pipelining with Multiple Blocks Per Slot (MBPS). @@ -72,7 +72,7 @@ describe('e2e_epochs/epochs_mbps_pipeline', () => { initialValidators: validators, enableProposerPipelining: true, // <- yehaw mockGossipSubNetwork: true, - mockGossipSubNetworkLatency: 500, // 200 ms delay in message prop - adverse network conditions + mockGossipSubNetworkLatency: 200, // 200 ms delay in message prop - adverse network conditions disableAnvilTestWatcher: true, startProverNode: true, perBlockAllocationMultiplier: 8, @@ -80,7 +80,9 @@ describe('e2e_epochs/epochs_mbps_pipeline', () => { enforceTimeTable: true, ethereumSlotDuration: 12, aztecSlotDuration: 72, - blockDurationMs: 8000, + blockDurationMs: 5800, + lastBlockDurationMs: 3000, + maxTxsPerCheckpoint: 24, // maxDABlockGas: 786432, // Set max DA block gas to be the same as the checkpoint // l1PublishingTime: 2, // attestationPropagationTime: 1, diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index 9cc98c492272..ed8b2ffa8351 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -224,6 +224,7 @@ export type EnvVar = | 'SEQ_L1_PUBLISHING_TIME_ALLOWANCE_IN_SLOT' | 'SEQ_ATTESTATION_PROPAGATION_TIME' | 'SEQ_BLOCK_DURATION_MS' + | 'SEQ_LAST_BLOCK_DURATION_MS' | 'SEQ_EXPECTED_BLOCK_PROPOSALS_PER_SLOT' | 'SEQ_BUILD_CHECKPOINT_IF_EMPTY' | 'SEQ_SECONDS_BEFORE_INVALIDATING_BLOCK_AS_COMMITTEE_MEMBER' diff --git a/yarn-project/p2p/src/services/gossipsub/topic_score_params.ts b/yarn-project/p2p/src/services/gossipsub/topic_score_params.ts index b7fce0e87b46..7fbc527b8a1f 100644 --- a/yarn-project/p2p/src/services/gossipsub/topic_score_params.ts +++ b/yarn-project/p2p/src/services/gossipsub/topic_score_params.ts @@ -15,6 +15,8 @@ export type TopicScoringNetworkParams = { targetCommitteeSize: number; /** Duration per block in milliseconds when building multiple blocks per slot. If undefined, single block mode. */ blockDurationMs?: number; + /** Duration of the last block in milliseconds when shorter than blockDurationMs. */ + lastBlockDurationMs?: number; /** Expected number of block proposals per slot for scoring override. 0 disables scoring, undefined falls back to blocksPerSlot - 1. */ expectedBlockProposalsPerSlot?: number; }; @@ -27,8 +29,14 @@ export type TopicScoringNetworkParams = { * @param blockDurationMs - Duration per block in milliseconds (undefined = single block mode) * @returns Number of blocks per slot */ -export function calculateBlocksPerSlot(slotDurationMs: number, blockDurationMs: number | undefined): number { - return calculateMaxBlocksPerSlot(slotDurationMs / 1000, blockDurationMs ? blockDurationMs / 1000 : undefined); +export function calculateBlocksPerSlot( + slotDurationMs: number, + blockDurationMs: number | undefined, + lastBlockDurationMs?: number, +): number { + return calculateMaxBlocksPerSlot(slotDurationMs / 1000, blockDurationMs ? blockDurationMs / 1000 : undefined, { + lastBlockDurationSec: lastBlockDurationMs ? lastBlockDurationMs / 1000 : undefined, + }); } /** @@ -276,10 +284,10 @@ export class TopicScoreParamsFactory { }; constructor(private readonly params: TopicScoringNetworkParams) { - const { slotDurationMs, heartbeatIntervalMs, blockDurationMs } = params; + const { slotDurationMs, heartbeatIntervalMs, blockDurationMs, lastBlockDurationMs } = params; // Compute values that are the same for all topics - this.blocksPerSlot = calculateBlocksPerSlot(slotDurationMs, blockDurationMs); + this.blocksPerSlot = calculateBlocksPerSlot(slotDurationMs, blockDurationMs, lastBlockDurationMs); this.heartbeatsPerSlot = slotDurationMs / heartbeatIntervalMs; this.invalidDecay = computeDecay(heartbeatIntervalMs, slotDurationMs, INVALID_DECAY_WINDOW_SLOTS); diff --git a/yarn-project/sequencer-client/src/config.ts b/yarn-project/sequencer-client/src/config.ts index 68865991147d..9dbd40b7e520 100644 --- a/yarn-project/sequencer-client/src/config.ts +++ b/yarn-project/sequencer-client/src/config.ts @@ -155,6 +155,14 @@ export const sequencerConfigMappings: ConfigMappingsType = { description: 'How much time (in seconds) we allow in the slot for publishing the L1 tx (defaults to 1 L1 slot).', parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined), }, + lastBlockDurationMs: { + env: 'SEQ_LAST_BLOCK_DURATION_MS', + description: + 'Duration of the last block in ms when building multiple blocks per slot. ' + + 'When set and less than blockDurationMs, the checkpoint broadcasts earlier. ' + + 'Defaults to blockDurationMs when 0 or unset.', + parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined), + }, attestationPropagationTime: { env: 'SEQ_ATTESTATION_PROPAGATION_TIME', description: 'How many seconds it takes for proposals and attestations to travel across the p2p layer (one-way)', diff --git a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts index 69701581c7b8..b40be1e1991a 100644 --- a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts @@ -1509,16 +1509,6 @@ export class SequencerPublisher { : [] ).flatMap(override => override.stateDiff ?? []); - // override the fee header for a specific checkpoint number if requested (used when pipelining) - const forcePendingFeeHeaderStateDiff = ( - options.forcePendingFeeHeader !== undefined - ? await this.rollupContract.makeFeeHeaderOverride( - options.forcePendingFeeHeader.checkpointNumber, - options.forcePendingFeeHeader.feeHeader, - ) - : [] - ).flatMap(override => override.stateDiff ?? []); - // override the archive for a specific checkpoint number if requested (used when pipelining) const forcePendingArchiveStateDiff = ( options.forcePendingArchive !== undefined @@ -1529,6 +1519,16 @@ export class SequencerPublisher { : [] ).flatMap(override => override.stateDiff ?? []); + // override the fee header for a specific checkpoint number if requested (used when pipelining) + const forcePendingFeeHeaderStateDiff = ( + options.forcePendingFeeHeader !== undefined + ? await this.rollupContract.makeFeeHeaderOverride( + options.forcePendingFeeHeader.checkpointNumber, + options.forcePendingFeeHeader.feeHeader, + ) + : [] + ).flatMap(override => override.stateDiff ?? []); + const stateOverrides: StateOverride = [ { address: this.rollupContract.address, diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index 0ed3f4b50602..705138873437 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -122,6 +122,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter { }); }); + describe('last block duration', () => { + const AZTEC_SLOT_DURATION = 72; + const BLOCK_DURATION_MS = 6000; + + it('uses shorter deadline for last block', () => { + const tt = new SequencerTimetable({ + ethereumSlotDuration: ETHEREUM_SLOT_DURATION, + aztecSlotDuration: AZTEC_SLOT_DURATION, + l1PublishingTime: L1_PUBLISHING_TIME, + blockDurationMs: BLOCK_DURATION_MS, + lastBlockDurationMs: 2000, + enforce: ENFORCE_TIMETABLE, + }); + + const blockDuration = BLOCK_DURATION_MS / 1000; + const lastBlockDuration = 2; + + // Non-last sub-slots should have standard deadlines + const firstResult = tt.canStartNextBlock(0); + expect(firstResult.canStart).toBe(true); + expect(firstResult.isLastBlock).toBe(false); + expect(firstResult.deadline).toBe(tt.initializationOffset + blockDuration); + + // Last sub-slot should have shorter deadline + const lastSlotStart = tt.initializationOffset + (tt.maxNumberOfBlocks - 1) * blockDuration; + const lastResult = tt.canStartNextBlock(lastSlotStart); + expect(lastResult.canStart).toBe(true); + expect(lastResult.isLastBlock).toBe(true); + expect(lastResult.deadline).toBe( + tt.initializationOffset + (tt.maxNumberOfBlocks - 1) * blockDuration + lastBlockDuration, + ); + }); + + it('increases maxNumberOfBlocks by one', () => { + const withoutLastBlock = new SequencerTimetable({ + ethereumSlotDuration: ETHEREUM_SLOT_DURATION, + aztecSlotDuration: AZTEC_SLOT_DURATION, + l1PublishingTime: L1_PUBLISHING_TIME, + blockDurationMs: BLOCK_DURATION_MS, + enforce: ENFORCE_TIMETABLE, + }); + + const withLastBlock = new SequencerTimetable({ + ethereumSlotDuration: ETHEREUM_SLOT_DURATION, + aztecSlotDuration: AZTEC_SLOT_DURATION, + l1PublishingTime: L1_PUBLISHING_TIME, + blockDurationMs: BLOCK_DURATION_MS, + lastBlockDurationMs: 2000, + enforce: ENFORCE_TIMETABLE, + }); + + expect(withLastBlock.maxNumberOfBlocks).toBe(withoutLastBlock.maxNumberOfBlocks + 1); + }); + + it('verifies total block time fits within available time', () => { + const tt = new SequencerTimetable({ + ethereumSlotDuration: ETHEREUM_SLOT_DURATION, + aztecSlotDuration: AZTEC_SLOT_DURATION, + l1PublishingTime: L1_PUBLISHING_TIME, + blockDurationMs: BLOCK_DURATION_MS, + lastBlockDurationMs: 2000, + enforce: ENFORCE_TIMETABLE, + }); + + const blockDuration = BLOCK_DURATION_MS / 1000; + const lastBlockDuration = 2; + const timeReservedAtEnd = lastBlockDuration + tt.checkpointFinalizationTime; + const timeAvailableForBlocks = AZTEC_SLOT_DURATION - tt.initializationOffset - timeReservedAtEnd; + + // (N-1) full blocks + 1 shorter last block should fit + const totalBlockTime = (tt.maxNumberOfBlocks - 1) * blockDuration + lastBlockDuration; + expect(totalBlockTime).toBeLessThanOrEqual(timeAvailableForBlocks); + + // Adding one more full block would exceed the available time + const totalBlockTimePlusOne = tt.maxNumberOfBlocks * blockDuration + lastBlockDuration; + expect(totalBlockTimePlusOne).toBeGreaterThan(timeAvailableForBlocks); + }); + + it('falls back to blockDuration when unset', () => { + const tt = new SequencerTimetable({ + ethereumSlotDuration: ETHEREUM_SLOT_DURATION, + aztecSlotDuration: AZTEC_SLOT_DURATION, + l1PublishingTime: L1_PUBLISHING_TIME, + blockDurationMs: BLOCK_DURATION_MS, + enforce: ENFORCE_TIMETABLE, + }); + + expect(tt.lastBlockDuration).toBe(tt.blockDuration); + }); + + it('ignores when >= blockDuration', () => { + const tt = new SequencerTimetable({ + ethereumSlotDuration: ETHEREUM_SLOT_DURATION, + aztecSlotDuration: AZTEC_SLOT_DURATION, + l1PublishingTime: L1_PUBLISHING_TIME, + blockDurationMs: BLOCK_DURATION_MS, + lastBlockDurationMs: BLOCK_DURATION_MS, // equal to blockDuration + enforce: ENFORCE_TIMETABLE, + }); + + expect(tt.lastBlockDuration).toBe(tt.blockDuration); + + const ttLarger = new SequencerTimetable({ + ethereumSlotDuration: ETHEREUM_SLOT_DURATION, + aztecSlotDuration: AZTEC_SLOT_DURATION, + l1PublishingTime: L1_PUBLISHING_TIME, + blockDurationMs: BLOCK_DURATION_MS, + lastBlockDurationMs: BLOCK_DURATION_MS + 1000, // larger than blockDuration + enforce: ENFORCE_TIMETABLE, + }); + + expect(ttLarger.lastBlockDuration).toBe(ttLarger.blockDuration); + }); + + it('works with pipelining', () => { + const tt = new SequencerTimetable({ + ethereumSlotDuration: ETHEREUM_SLOT_DURATION, + aztecSlotDuration: AZTEC_SLOT_DURATION, + l1PublishingTime: L1_PUBLISHING_TIME, + blockDurationMs: BLOCK_DURATION_MS, + lastBlockDurationMs: 2000, + enforce: ENFORCE_TIMETABLE, + pipelining: true, + }); + + const blockDuration = BLOCK_DURATION_MS / 1000; + + // pipeliningAttestationGracePeriod should still use full blockDuration + expect(tt.pipeliningAttestationGracePeriod).toBe(blockDuration + tt.p2pPropagationTime); + + // But last block deadline should be shorter + expect(tt.lastBlockDuration).toBe(2); + }); + + it('clamps to minExecutionTime', () => { + // With ethereumSlotDuration >= 8, minExecutionTime defaults to MIN_EXECUTION_TIME (2s) + // Set lastBlockDurationMs to 500ms (0.5s) which is less than minExecutionTime + const tt = new SequencerTimetable({ + ethereumSlotDuration: ETHEREUM_SLOT_DURATION, + aztecSlotDuration: AZTEC_SLOT_DURATION, + l1PublishingTime: L1_PUBLISHING_TIME, + blockDurationMs: BLOCK_DURATION_MS, + lastBlockDurationMs: 500, + enforce: ENFORCE_TIMETABLE, + }); + + expect(tt.lastBlockDuration).toBe(tt.minExecutionTime); + }); + }); + describe('pipelining mode', () => { const BLOCK_DURATION_MS = 8000; diff --git a/yarn-project/sequencer-client/src/sequencer/timetable.ts b/yarn-project/sequencer-client/src/sequencer/timetable.ts index 0f5d8fe8cdae..7282ffc190fb 100644 --- a/yarn-project/sequencer-client/src/sequencer/timetable.ts +++ b/yarn-project/sequencer-client/src/sequencer/timetable.ts @@ -67,6 +67,10 @@ export class SequencerTimetable { /** Duration per block when building multiple blocks per slot (undefined = single block per slot) */ public readonly blockDuration: number | undefined; + /** Duration of the last block when building multiple blocks per slot. + * When different from blockDuration, the last sub-slot has a shorter deadline. */ + public readonly lastBlockDuration: number | undefined; + /** Maximum number of blocks that can be built in this slot configuration */ public readonly maxNumberOfBlocks: number; @@ -86,6 +90,7 @@ export class SequencerTimetable { l1PublishingTime: number; p2pPropagationTime?: number; blockDurationMs?: number; + lastBlockDurationMs?: number; enforce: boolean; pipelining?: boolean; }, @@ -100,6 +105,13 @@ export class SequencerTimetable { this.enforce = opts.enforce; this.pipelining = opts.pipelining ?? false; + // Last block duration: use if set, valid, and less than blockDuration + if (opts.lastBlockDurationMs && this.blockDuration && opts.lastBlockDurationMs / 1000 < this.blockDuration) { + this.lastBlockDuration = opts.lastBlockDurationMs / 1000; + } else { + this.lastBlockDuration = this.blockDuration; + } + // Assume zero-cost propagation time and faster runs in test environments where L1 slot duration is shortened if (this.ethereumSlotDuration < 8) { this.p2pPropagationTime = 0; @@ -113,6 +125,11 @@ export class SequencerTimetable { this.minExecutionTime = this.blockDuration; } + // Last block duration cannot be less than min execution time + if (this.lastBlockDuration !== undefined && this.lastBlockDuration < this.minExecutionTime) { + this.lastBlockDuration = this.minExecutionTime; + } + // Calculate initialization offset - estimate of time needed for sync + proposer check // This is the baseline for all sub-slot deadlines this.initializationOffset = this.checkpointInitializationTime; @@ -135,11 +152,18 @@ export class SequencerTimetable { // Proposal must reach validators within build slot: assembly + one-way broadcast timeReservedAtEnd = this.checkpointAssembleTime + this.p2pPropagationTime; } else { - timeReservedAtEnd = this.blockDuration + this.checkpointFinalizationTime; + // Use lastBlockDuration for validator re-execution of the (shorter) last block + timeReservedAtEnd = (this.lastBlockDuration ?? this.blockDuration) + this.checkpointFinalizationTime; } const timeAvailableForBlocks = this.aztecSlotDuration - this.initializationOffset - timeReservedAtEnd; - this.maxNumberOfBlocks = Math.floor(timeAvailableForBlocks / this.blockDuration); + + if (this.lastBlockDuration !== undefined && this.lastBlockDuration < this.blockDuration) { + // (N-1) full blocks + 1 shorter last block + this.maxNumberOfBlocks = Math.floor((timeAvailableForBlocks - this.lastBlockDuration) / this.blockDuration) + 1; + } else { + this.maxNumberOfBlocks = Math.floor(timeAvailableForBlocks / this.blockDuration); + } } // Minimum work to do within a slot for building a block with the minimum time for execution and publishing its checkpoint. @@ -168,6 +192,7 @@ export class SequencerTimetable { pipeliningAttestationGracePeriod: this.pipeliningAttestationGracePeriod, minWorkToDo, blockDuration: this.blockDuration, + lastBlockDuration: this.lastBlockDuration, maxNumberOfBlocks: this.maxNumberOfBlocks, }, ); @@ -282,8 +307,12 @@ export class SequencerTimetable { // Otherwise, we're in multi-block-per-slot mode, the default when running in production // Find the next available sub-slot that has enough time remaining for (let subSlot = 1; subSlot <= this.maxNumberOfBlocks; subSlot++) { - // Calculate end for this sub-slot - const deadline = this.initializationOffset + subSlot * this.blockDuration; + // Calculate end for this sub-slot. The last sub-slot may have a shorter duration. + const isLastSubSlot = subSlot === this.maxNumberOfBlocks; + const deadline = + isLastSubSlot && this.lastBlockDuration !== undefined && this.lastBlockDuration !== this.blockDuration + ? this.initializationOffset + (subSlot - 1) * this.blockDuration + this.lastBlockDuration + : this.initializationOffset + subSlot * this.blockDuration; // Check if we have enough time to build a block with this deadline const timeUntilDeadline = deadline - secondsIntoSlot; diff --git a/yarn-project/stdlib/src/block/l2_block_source.ts b/yarn-project/stdlib/src/block/l2_block_source.ts index d3444948f7d6..56610da2cf3c 100644 --- a/yarn-project/stdlib/src/block/l2_block_source.ts +++ b/yarn-project/stdlib/src/block/l2_block_source.ts @@ -6,7 +6,7 @@ import { type EpochNumber, type SlotNumber, } from '@aztec/foundation/branded-types'; -import type { Fr } from '@aztec/foundation/curves/bn254'; +import { Fr } from '@aztec/foundation/curves/bn254'; import type { EthAddress } from '@aztec/foundation/eth-address'; import type { TypedEventEmitter } from '@aztec/foundation/types'; diff --git a/yarn-project/stdlib/src/interfaces/configs.ts b/yarn-project/stdlib/src/interfaces/configs.ts index 55f9142aca33..585afcd72231 100644 --- a/yarn-project/stdlib/src/interfaces/configs.ts +++ b/yarn-project/stdlib/src/interfaces/configs.ts @@ -73,6 +73,10 @@ export interface SequencerConfig { shuffleAttestationOrdering?: boolean; /** Duration per block in milliseconds when building multiple blocks per slot (default: undefined = single block per slot) */ blockDurationMs?: number; + /** Duration of the last block in ms when building multiple blocks per slot. + * When set and less than blockDurationMs, the checkpoint broadcasts earlier. + * Defaults to blockDurationMs when 0 or unset. */ + lastBlockDurationMs?: number; /** Expected number of block proposals per slot for P2P peer scoring. 0 disables scoring, undefined falls back to blocksPerSlot - 1. */ expectedBlockProposalsPerSlot?: number; /** Have sequencer build and publish an empty checkpoint if there are no txs */ @@ -119,6 +123,7 @@ export const SequencerConfigSchema = zodFor()( fishermanMode: z.boolean().optional(), shuffleAttestationOrdering: z.boolean().optional(), blockDurationMs: z.number().positive().optional(), + lastBlockDurationMs: z.number().positive().optional(), expectedBlockProposalsPerSlot: z.number().nonnegative().optional(), buildCheckpointIfEmpty: z.boolean().optional(), skipPushProposedBlocksToArchiver: z.boolean().optional(), @@ -130,6 +135,7 @@ export const SequencerConfigSchema = zodFor()( type SequencerConfigOptionalKeys = | 'governanceProposerPayload' | 'blockDurationMs' + | 'lastBlockDurationMs' | 'expectedBlockProposalsPerSlot' | 'coinbase' | 'feeRecipient' diff --git a/yarn-project/stdlib/src/timetable/index.ts b/yarn-project/stdlib/src/timetable/index.ts index 053b616e103d..8aba9558ba6d 100644 --- a/yarn-project/stdlib/src/timetable/index.ts +++ b/yarn-project/stdlib/src/timetable/index.ts @@ -43,6 +43,7 @@ export function calculateMaxBlocksPerSlot( p2pPropagationTime?: number; l1PublishingTime?: number; pipelining?: boolean; + lastBlockDurationSec?: number; } = {}, ): number { if (!blockDurationSec) { @@ -53,22 +54,27 @@ export function calculateMaxBlocksPerSlot( const assembleTime = opts.checkpointAssembleTime ?? CHECKPOINT_ASSEMBLE_TIME; const p2pTime = opts.p2pPropagationTime ?? DEFAULT_P2P_PROPAGATION_TIME; const l1Time = opts.l1PublishingTime ?? DEFAULT_L1_PUBLISHING_TIME; + const lastBlockDur = opts.lastBlockDurationSec; // Calculate checkpoint finalization time (assembly + round-trip propagation + L1 publishing) const checkpointFinalizationTime = assembleTime + p2pTime * 2 + l1Time; // When pipelining, finalization is deferred to the next slot, so we only reserve - // time for assembly + one-way broadcast. Without pipelining, we also need a full - // block duration for validator re-execution plus full checkpoint finalization. + // time for assembly + one-way broadcast. Without pipelining, we also need + // validator re-execution (using lastBlockDuration if shorter) plus full checkpoint finalization. let timeReservedAtEnd: number; if (opts.pipelining) { timeReservedAtEnd = assembleTime + p2pTime; } else { - timeReservedAtEnd = blockDurationSec + checkpointFinalizationTime; + const reexecutionTime = lastBlockDur ?? blockDurationSec; + timeReservedAtEnd = reexecutionTime + checkpointFinalizationTime; } // Time available for building blocks const timeAvailableForBlocks = aztecSlotDurationSec - initOffset - timeReservedAtEnd; + if (lastBlockDur !== undefined && lastBlockDur < blockDurationSec) { + return Math.max(1, Math.floor((timeAvailableForBlocks - lastBlockDur) / blockDurationSec) + 1); + } return Math.max(1, Math.floor(timeAvailableForBlocks / blockDurationSec)); }