Skip to content

Commit

Permalink
fix: Sequencer timetable accounts for spare time (#11221)
Browse files Browse the repository at this point in the history
  • Loading branch information
spalladino authored Jan 15, 2025
1 parent 4f06b97 commit f1b9211
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 144 deletions.
16 changes: 9 additions & 7 deletions yarn-project/end-to-end/src/e2e_block_building.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { type TestDateProvider } from '@aztec/foundation/timer';
import { StatefulTestContract, StatefulTestContractArtifact } from '@aztec/noir-contracts.js/StatefulTest';
import { TestContract } from '@aztec/noir-contracts.js/Test';
import { TokenContract } from '@aztec/noir-contracts.js/Token';
import { type SequencerClient, SequencerState } from '@aztec/sequencer-client';
import { type SequencerClient } from '@aztec/sequencer-client';
import { type TestSequencerClient } from '@aztec/sequencer-client/test';
import {
PublicProcessorFactory,
Expand Down Expand Up @@ -61,6 +61,10 @@ describe('e2e_block_building', () => {

const { aztecEpochProofClaimWindowInL2Slots } = getL1ContractsConfigEnvVars();

afterEach(() => {
jest.restoreAllMocks();
});

describe('multi-txs block', () => {
const artifact = StatefulTestContractArtifact;

Expand Down Expand Up @@ -209,12 +213,10 @@ describe('e2e_block_building', () => {
// We also cheat the sequencer's timetable so it allocates little time to processing.
// This will leave the sequencer with just a few seconds to build the block, so it shouldn't
// be able to squeeze in more than ~12 txs in each. This is sensitive to the time it takes
// to pick up and validate the txs, so we may need to bump it to work on CI. Note that we need
// at least 3s here so the archiver has time to loop once and sync, and the sequencer has at
// least 1s to loop.
sequencer.sequencer.timeTable[SequencerState.INITIALIZING_PROPOSAL] = 4;
sequencer.sequencer.timeTable[SequencerState.CREATING_BLOCK] = 4;
sequencer.sequencer.processTxTime = 1;
// to pick up and validate the txs, so we may need to bump it to work on CI.
jest
.spyOn(sequencer.sequencer.timetable, 'getBlockProposalExecTimeEnd')
.mockImplementation((secondsIntoSlot: number) => secondsIntoSlot + 1);

// Flood the mempool with TX_COUNT simultaneous txs
const methods = times(TX_COUNT, i => contract.methods.increment_public_value(ownerAddress, i));
Expand Down
13 changes: 5 additions & 8 deletions yarn-project/sequencer-client/src/sequencer/sequencer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,13 +267,10 @@ describe('sequencer', () => {
expectPublisherProposeL2Block([txHash]);
});

it.each([
{ delayedState: SequencerState.INITIALIZING_PROPOSAL },
// It would be nice to add the other states, but we would need to inject delays within the `work` loop
])('does not build a block if it does not have enough time left in the slot', async ({ delayedState }) => {
// trick the sequencer into thinking that we are just too far into slot 1
it('does not build a block if it does not have enough time left in the slot', async () => {
// Trick the sequencer into thinking that we are just too far into slot 1
sequencer.setL1GenesisTime(
Math.floor(Date.now() / 1000) - slotDuration * 1 - (sequencer.getTimeTable()[delayedState] + 1),
Math.floor(Date.now() / 1000) - slotDuration * 1 - (sequencer.getTimeTable().initialTime + 1),
);

const tx = makeTx();
Expand All @@ -283,7 +280,7 @@ describe('sequencer', () => {
await expect(sequencer.doRealWork()).rejects.toThrow(
expect.objectContaining({
name: 'SequencerTooSlowError',
message: expect.stringContaining(`Too far into slot to transition to ${delayedState}`),
message: expect.stringContaining(`Too far into slot`),
}),
);

Expand Down Expand Up @@ -658,7 +655,7 @@ describe('sequencer', () => {

class TestSubject extends Sequencer {
public getTimeTable() {
return this.timeTable;
return this.timetable;
}

public setL1GenesisTime(l1GenesisTime: number) {
Expand Down
143 changes: 17 additions & 126 deletions yarn-project/sequencer-client/src/sequencer/sequencer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,33 +36,18 @@ import { type PublicProcessorFactory } from '@aztec/simulator/server';
import { Attributes, type TelemetryClient, type Tracer, trackSpan } from '@aztec/telemetry-client';
import { type ValidatorClient } from '@aztec/validator-client';

import assert from 'assert';

import { type GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
import { type L1Publisher, VoteType } from '../publisher/l1-publisher.js';
import { type SlasherClient } from '../slasher/slasher_client.js';
import { createValidatorsForBlockBuilding } from '../tx_validator/tx_validator_factory.js';
import { getDefaultAllowedSetupFunctions } from './allowed.js';
import { type SequencerConfig } from './config.js';
import { SequencerMetrics } from './metrics.js';
import { SequencerTimetable, SequencerTooSlowError } from './timetable.js';
import { SequencerState, orderAttestations } from './utils.js';

export { SequencerState };

export class SequencerTooSlowError extends Error {
constructor(
public readonly currentState: SequencerState,
public readonly proposedState: SequencerState,
public readonly maxAllowedTime: number,
public readonly currentTime: number,
) {
super(
`Too far into slot to transition to ${proposedState} (max allowed: ${maxAllowedTime}s, time into slot: ${currentTime}s)`,
);
this.name = 'SequencerTooSlowError';
}
}

type SequencerRollupConstants = Pick<L1RollupConstants, 'ethereumSlotDuration' | 'l1GenesisTime' | 'slotDuration'>;

/**
Expand All @@ -87,16 +72,12 @@ export class Sequencer {
private allowedInSetup: AllowedElement[] = getDefaultAllowedSetupFunctions();
private maxBlockSizeInBytes: number = 1024 * 1024;
private maxBlockGas: Gas = new Gas(10e9, 10e9);
protected processTxTime: number = 12;
private attestationPropagationTime: number = 2;
private metrics: SequencerMetrics;
private isFlushing: boolean = false;

/**
* The maximum number of seconds that the sequencer can be into a slot to transition to a particular state.
* For example, in order to transition into WAITING_FOR_ATTESTATIONS, the sequencer can be at most 3 seconds into the slot.
*/
protected timeTable!: Record<SequencerState, number>;
/** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
protected timetable!: SequencerTimetable;

protected enforceTimeTable: boolean = false;

constructor(
Expand Down Expand Up @@ -182,78 +163,15 @@ export class Sequencer {
}

private setTimeTable() {
// How late into the slot can we be to start working
const initialTime = 2;

// How long it takes to get ready to start building
const blockPrepareTime = 1;

// How long it takes to for proposals and attestations to travel across the p2p layer (one-way)
const attestationPropagationTime = 2;
this.attestationPropagationTime = attestationPropagationTime;

// How long it takes to get a published block into L1. L1 builders typically accept txs up to 4 seconds into their slot,
// but we'll timeout sooner to give it more time to propagate (remember we also have blobs!). Still, when working in anvil,
// we can just post in the very last second of the L1 slot and still expect the tx to be accepted.
const l1PublishingTime = this.l1Constants.ethereumSlotDuration - this.maxL1TxInclusionTimeIntoSlot;

// How much time we spend validating and processing a block after building it,
// and assembling the proposal to send to attestors
const blockValidationTime = 1;

// How much time we have left in the slot for actually processing txs and building the block.
const remainingTimeInSlot =
this.aztecSlotDuration -
initialTime -
blockPrepareTime -
blockValidationTime -
2 * attestationPropagationTime -
l1PublishingTime;

// Check that we actually have time left for processing txs
if (this.enforceTimeTable && remainingTimeInSlot < 0) {
throw new Error(`Not enough time for block building in ${this.aztecSlotDuration}s slot`);
}

// How much time we have for actually processing txs. Note that we need both the sequencer and the validators to execute txs.
const processTxsTime = remainingTimeInSlot / 2;
this.processTxTime = processTxsTime;

// Sanity check
const totalSlotTime =
initialTime + // Archiver, world-state, and p2p sync
blockPrepareTime + // Setup globals, initial checks, etc
processTxsTime + // Processing public txs for building the block
blockValidationTime + // Validating the block produced
attestationPropagationTime + // Propagating the block proposal to validators
processTxsTime + // Validators run public txs before signing
attestationPropagationTime + // Attestations fly back to the proposer
l1PublishingTime; // The publish tx sits on the L1 mempool waiting to be picked up

assert(
totalSlotTime === this.aztecSlotDuration,
`Computed total slot time does not match slot duration: ${totalSlotTime}s`,
this.timetable = new SequencerTimetable(
this.l1Constants.ethereumSlotDuration,
this.aztecSlotDuration,
this.maxL1TxInclusionTimeIntoSlot,
this.enforceTimeTable,
this.metrics,
this.log,
);

const newTimeTable: Record<SequencerState, number> = {
// No checks needed for any of these transitions
[SequencerState.STOPPED]: this.aztecSlotDuration,
[SequencerState.IDLE]: this.aztecSlotDuration,
[SequencerState.SYNCHRONIZING]: this.aztecSlotDuration,
// We always want to allow the full slot to check if we are the proposer
[SequencerState.PROPOSER_CHECK]: this.aztecSlotDuration,
// How late we can start initializing a new block proposal
[SequencerState.INITIALIZING_PROPOSAL]: initialTime,
// When we start building a block
[SequencerState.CREATING_BLOCK]: initialTime + blockPrepareTime,
// We start collecting attestations after building the block
[SequencerState.COLLECTING_ATTESTATIONS]: initialTime + blockPrepareTime + processTxsTime + blockValidationTime,
// We publish the block after collecting attestations
[SequencerState.PUBLISHING_BLOCK]: this.aztecSlotDuration - l1PublishingTime,
};

this.log.verbose(`Sequencer time table updated with ${processTxsTime}s for processing txs`, newTimeTable);
this.timeTable = newTimeTable;
this.log.verbose(`Sequencer timetable updated`, { enforceTimeTable: this.enforceTimeTable });
}

/**
Expand Down Expand Up @@ -427,29 +345,6 @@ export class Sequencer {
}
}

doIHaveEnoughTimeLeft(proposedState: SequencerState, secondsIntoSlot: number): boolean {
if (!this.enforceTimeTable) {
return true;
}

const maxAllowedTime = this.timeTable[proposedState];
if (maxAllowedTime === this.aztecSlotDuration) {
return true;
}

const bufferSeconds = maxAllowedTime - secondsIntoSlot;

if (bufferSeconds < 0) {
this.log.debug(`Too far into slot to transition to ${proposedState}`, { maxAllowedTime, secondsIntoSlot });
return false;
}

this.metrics.recordStateTransitionBufferMs(Math.floor(bufferSeconds * 1000), proposedState);

this.log.trace(`Enough time to transition to ${proposedState}`, { maxAllowedTime, secondsIntoSlot });
return true;
}

/**
* Sets the sequencer state and checks if we have enough time left in the slot to transition to the new state.
* @param proposedState - The new state to transition to.
Expand All @@ -465,9 +360,7 @@ export class Sequencer {
return;
}
const secondsIntoSlot = this.getSecondsIntoSlot(currentSlotNumber);
if (!this.doIHaveEnoughTimeLeft(proposedState, secondsIntoSlot)) {
throw new SequencerTooSlowError(this.state, proposedState, this.timeTable[proposedState], secondsIntoSlot);
}
this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
this.log.debug(`Transitioning from ${this.state} to ${proposedState}`);
this.state = proposedState;
}
Expand Down Expand Up @@ -521,13 +414,11 @@ export class Sequencer {
const blockBuilder = this.blockBuilderFactory.create(orchestratorFork);
await blockBuilder.startNewBlock(newGlobalVariables, l1ToL2Messages);

// When building a block as a proposer, we set the deadline for tx processing to the start of the
// CREATING_BLOCK phase, plus the expected time for tx processing. When validating, we start counting
// the time for tx processing from the start of the COLLECTING_ATTESTATIONS phase plus the attestation
// propagation time. See the comments in setTimeTable for more details.
// Deadline for processing depends on whether we're proposing a block
const secondsIntoSlot = this.getSecondsIntoSlot(slot);
const processingEndTimeWithinSlot = opts.validateOnly
? this.timeTable[SequencerState.COLLECTING_ATTESTATIONS] + this.attestationPropagationTime + this.processTxTime
: this.timeTable[SequencerState.CREATING_BLOCK] + this.processTxTime;
? this.timetable.getValidatorReexecTimeEnd(secondsIntoSlot)
: this.timetable.getBlockProposalExecTimeEnd(secondsIntoSlot);

// Deadline is only set if enforceTimeTable is enabled.
const deadline = this.enforceTimeTable
Expand Down
Loading

0 comments on commit f1b9211

Please sign in to comment.