Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions yarn-project/archiver/src/store/block_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
}

Expand Down
16 changes: 8 additions & 8 deletions yarn-project/archiver/src/store/kv_archiver_store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/aztec-node/src/aztec-node/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -72,15 +72,17 @@ 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,
aztecEpochDuration: 4,
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,
Expand Down
1 change: 1 addition & 0 deletions yarn-project/foundation/src/config/env_var.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
16 changes: 12 additions & 4 deletions yarn-project/p2p/src/services/gossipsub/topic_score_params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand All @@ -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,
});
}

/**
Expand Down Expand Up @@ -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);

Expand Down
8 changes: 8 additions & 0 deletions yarn-project/sequencer-client/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ export const sequencerConfigMappings: ConfigMappingsType<SequencerConfig> = {
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)',
Expand Down
20 changes: 10 additions & 10 deletions yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions yarn-project/sequencer-client/src/sequencer/sequencer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
l1PublishingTime: this.l1PublishingTime,
p2pPropagationTime: this.config.attestationPropagationTime,
blockDurationMs: this.config.blockDurationMs,
lastBlockDurationMs: this.config.lastBlockDurationMs,
enforce: this.config.enforceTimeTable,
pipelining: this.epochCache.isProposerPipeliningEnabled(),
},
Expand Down
150 changes: 150 additions & 0 deletions yarn-project/sequencer-client/src/sequencer/timetable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,156 @@ describe('sequencer-timetable', () => {
});
});

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;

Expand Down
Loading
Loading