From f1b92112b7063af80044a2b3bc6daa98a8446d9f Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Wed, 15 Jan 2025 15:19:22 -0300 Subject: [PATCH] fix: Sequencer timetable accounts for spare time (#11221) --- .../end-to-end/src/e2e_block_building.test.ts | 16 +- .../src/sequencer/sequencer.test.ts | 13 +- .../src/sequencer/sequencer.ts | 143 +++--------------- .../src/sequencer/timetable.test.ts | 130 ++++++++++++++++ .../src/sequencer/timetable.ts | 123 +++++++++++++++ .../sequencer-client/src/test/index.ts | 5 +- 6 files changed, 286 insertions(+), 144 deletions(-) create mode 100644 yarn-project/sequencer-client/src/sequencer/timetable.test.ts create mode 100644 yarn-project/sequencer-client/src/sequencer/timetable.ts diff --git a/yarn-project/end-to-end/src/e2e_block_building.test.ts b/yarn-project/end-to-end/src/e2e_block_building.test.ts index 97e0e550f6f..91be2b9f950 100644 --- a/yarn-project/end-to-end/src/e2e_block_building.test.ts +++ b/yarn-project/end-to-end/src/e2e_block_building.test.ts @@ -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, @@ -61,6 +61,10 @@ describe('e2e_block_building', () => { const { aztecEpochProofClaimWindowInL2Slots } = getL1ContractsConfigEnvVars(); + afterEach(() => { + jest.restoreAllMocks(); + }); + describe('multi-txs block', () => { const artifact = StatefulTestContractArtifact; @@ -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)); diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index 76d19334f51..784d40d5edb 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -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(); @@ -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`), }), ); @@ -658,7 +655,7 @@ describe('sequencer', () => { class TestSubject extends Sequencer { public getTimeTable() { - return this.timeTable; + return this.timetable; } public setL1GenesisTime(l1GenesisTime: number) { diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index 8a31b3825c5..96cdeef225f 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -36,8 +36,6 @@ 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'; @@ -45,24 +43,11 @@ import { createValidatorsForBlockBuilding } from '../tx_validator/tx_validator_f 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; /** @@ -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; + /** 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( @@ -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 = { - // 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 }); } /** @@ -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. @@ -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; } @@ -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 diff --git a/yarn-project/sequencer-client/src/sequencer/timetable.test.ts b/yarn-project/sequencer-client/src/sequencer/timetable.test.ts new file mode 100644 index 00000000000..718d0e7bf34 --- /dev/null +++ b/yarn-project/sequencer-client/src/sequencer/timetable.test.ts @@ -0,0 +1,130 @@ +import { SequencerTimetable } from './timetable.js'; +import { SequencerState } from './utils.js'; + +describe('sequencer-timetable', () => { + let timetable: SequencerTimetable; + + const ethereumSlotDuration = 12; + const aztecSlotDuration = 36; + const maxL1TxInclusionTimeIntoSlot = 0; + const enforce = true; + + beforeEach(() => { + timetable = new SequencerTimetable(ethereumSlotDuration, aztecSlotDuration, maxL1TxInclusionTimeIntoSlot, enforce); + }); + + describe('maxAllowedTime', () => { + it('computes time from slot start', () => { + expect(timetable.getMaxAllowedTime(SequencerState.INITIALIZING_PROPOSAL)).toEqual(timetable.initialTime); + }); + + it('computes time from slot end', () => { + expect(timetable.getMaxAllowedTime(SequencerState.COLLECTING_ATTESTATIONS)).toEqual( + aztecSlotDuration - + (ethereumSlotDuration - maxL1TxInclusionTimeIntoSlot) - + timetable.attestationPropagationTime * 2, + ); + }); + }); + + describe('assertTimeLeft', () => { + it('throws if time is up', () => { + expect(() => timetable.assertTimeLeft(SequencerState.INITIALIZING_PROPOSAL, 5)).toThrow(/Too far into slot/); + }); + + it('does not throw if enough time left', () => { + expect(() => timetable.assertTimeLeft(SequencerState.INITIALIZING_PROPOSAL, 1)).not.toThrow(); + }); + + it('handles negative seconds into slot', () => { + expect(() => timetable.assertTimeLeft(SequencerState.INITIALIZING_PROPOSAL, -1)).not.toThrow(); + expect(() => timetable.assertTimeLeft(SequencerState.PUBLISHING_BLOCK, -1)).not.toThrow(); + }); + + it('skips check if enforcement is off', () => { + timetable = new SequencerTimetable(ethereumSlotDuration, aztecSlotDuration, maxL1TxInclusionTimeIntoSlot, false); + expect(() => timetable.assertTimeLeft(SequencerState.INITIALIZING_PROPOSAL, 1000)).not.toThrow(); + }); + }); + + describe('getBlockProposalExecTimeEnd', () => { + it('sets deadline considering unused time from init phase', () => { + const actual = timetable.getBlockProposalExecTimeEnd(1); + const available = + aztecSlotDuration - + timetable.attestationPropagationTime * 2 - + timetable.l1PublishingTime - + timetable.blockValidationTime - + 1; + const expected = available / 2 + 1; + expect(actual).toEqual(expected); + expect(expected).toEqual(10); + }); + + it('sets deadline considering starting before slot', () => { + const actual = timetable.getBlockProposalExecTimeEnd(-1); + const available = + aztecSlotDuration - + timetable.attestationPropagationTime * 2 - + timetable.l1PublishingTime - + timetable.blockValidationTime + + 1; + const expected = available / 2 - 1; + expect(actual).toEqual(expected); + expect(expected).toEqual(9); + }); + + it('sets deadline when building on time', () => { + const intoSlot = timetable.initialTime + timetable.blockPrepareTime; + const actual = timetable.getBlockProposalExecTimeEnd(intoSlot); + const available = + aztecSlotDuration - + timetable.attestationPropagationTime * 2 - + timetable.l1PublishingTime - + timetable.blockValidationTime - + intoSlot; + const expected = available / 2 + intoSlot; + expect(actual).toEqual(expected); + expect(expected).toEqual(11.5); + }); + + it('sets deadline before current time if too late', () => { + const intoSlot = aztecSlotDuration - 4; + const actual = timetable.getBlockProposalExecTimeEnd(intoSlot); + expect(actual).toBeLessThan(intoSlot); + }); + }); + + describe('getValidatorReexecTimeEnd', () => { + it('sets deadline', () => { + const actual = timetable.getValidatorReexecTimeEnd(10); + const available = aztecSlotDuration - timetable.attestationPropagationTime - timetable.l1PublishingTime - 10; + const expected = available + 10; + expect(actual).toEqual(expected); + expect(expected).toEqual(22); + }); + + it('sets time available equal to block building', () => { + const { blockValidationTime, attestationPropagationTime, l1PublishingTime } = timetable; + + const intoSlot = 3; + const blockBuildDeadline = timetable.getBlockProposalExecTimeEnd(intoSlot); + const blockBuildAvailable = blockBuildDeadline - intoSlot; + + const validatorIntoSlot = blockBuildDeadline + blockValidationTime + attestationPropagationTime; + const validatorDeadline = timetable.getValidatorReexecTimeEnd(validatorIntoSlot); + const validatorAvailable = validatorDeadline - validatorIntoSlot; + + expect(blockBuildAvailable).toEqual(validatorAvailable); + + expect( + blockBuildAvailable + + validatorAvailable + + intoSlot + + blockValidationTime + + attestationPropagationTime * 2 + + l1PublishingTime, + ).toEqual(aztecSlotDuration); + }); + }); +}); diff --git a/yarn-project/sequencer-client/src/sequencer/timetable.ts b/yarn-project/sequencer-client/src/sequencer/timetable.ts new file mode 100644 index 00000000000..5ee463a1a4d --- /dev/null +++ b/yarn-project/sequencer-client/src/sequencer/timetable.ts @@ -0,0 +1,123 @@ +import { createLogger } from '@aztec/aztec.js'; + +import { type SequencerMetrics } from './metrics.js'; +import { SequencerState } from './utils.js'; + +export class SequencerTimetable { + /** How late into the slot can we be to start working */ + public readonly initialTime = 3; + + /** How long it takes to get ready to start building */ + public readonly blockPrepareTime = 1; + + /** How long it takes to for proposals and attestations to travel across the p2p layer (one-way) */ + public readonly attestationPropagationTime = 2; + + /** How much time we spend validating and processing a block after building it, and assembling the proposal to send to attestors */ + public readonly blockValidationTime = 1; + + /** + * 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. + */ + public readonly l1PublishingTime; + + constructor( + private readonly ethereumSlotDuration: number, + private readonly aztecSlotDuration: number, + private readonly maxL1TxInclusionTimeIntoSlot: number, + private readonly enforce: boolean = true, + private readonly metrics?: SequencerMetrics, + private readonly log = createLogger('sequencer:timetable'), + ) { + this.l1PublishingTime = this.ethereumSlotDuration - this.maxL1TxInclusionTimeIntoSlot; + } + + private get afterBlockBuildingTimeNeededWithoutReexec() { + return this.blockValidationTime + this.attestationPropagationTime * 2 + this.l1PublishingTime; + } + + public getBlockProposalExecTimeEnd(secondsIntoSlot: number): number { + // We are N seconds into the slot. We need to account for `afterBlockBuildingTimeNeededWithoutReexec` seconds, + // send then split the remaining time between the re-execution and the block building. + const maxAllowed = this.aztecSlotDuration - this.afterBlockBuildingTimeNeededWithoutReexec; + const available = maxAllowed - secondsIntoSlot; + const executionTimeEnd = secondsIntoSlot + available / 2; + this.log.debug(`Block proposal execution time deadline is ${executionTimeEnd}`, { + secondsIntoSlot, + maxAllowed, + available, + executionTimeEnd, + }); + return executionTimeEnd; + } + + private get afterBlockReexecTimeNeeded() { + return this.attestationPropagationTime + this.l1PublishingTime; + } + + public getValidatorReexecTimeEnd(secondsIntoSlot: number): number { + // We need to leave for `afterBlockReexecTimeNeeded` seconds available. + const validationTimeEnd = this.aztecSlotDuration - this.afterBlockReexecTimeNeeded; + this.log.debug(`Validator re-execution time deadline is ${validationTimeEnd}`, { + secondsIntoSlot, + validationTimeEnd, + }); + return validationTimeEnd; + } + + public getMaxAllowedTime(state: SequencerState): number | undefined { + switch (state) { + case SequencerState.STOPPED: + case SequencerState.IDLE: + case SequencerState.SYNCHRONIZING: + case SequencerState.PROPOSER_CHECK: + return; // We don't really care about times for this states + case SequencerState.INITIALIZING_PROPOSAL: + return this.initialTime; + case SequencerState.CREATING_BLOCK: + return this.initialTime + this.blockPrepareTime; + case SequencerState.COLLECTING_ATTESTATIONS: + return this.aztecSlotDuration - this.l1PublishingTime - 2 * this.attestationPropagationTime; + case SequencerState.PUBLISHING_BLOCK: + return this.aztecSlotDuration - this.l1PublishingTime; + default: { + const _exhaustiveCheck: never = state; + throw new Error(`Unexpected state: ${state}`); + } + } + } + + public assertTimeLeft(newState: SequencerState, secondsIntoSlot: number) { + if (!this.enforce) { + return; + } + + const maxAllowedTime = this.getMaxAllowedTime(newState); + if (maxAllowedTime === undefined) { + return; + } + + const bufferSeconds = maxAllowedTime - secondsIntoSlot; + if (bufferSeconds < 0) { + throw new SequencerTooSlowError(newState, maxAllowedTime, secondsIntoSlot); + } + + this.metrics?.recordStateTransitionBufferMs(Math.floor(bufferSeconds * 1000), newState); + this.log.trace(`Enough time to transition to ${newState}`, { maxAllowedTime, secondsIntoSlot }); + } +} + +export class SequencerTooSlowError extends Error { + constructor( + public readonly proposedState: SequencerState, + public readonly maxAllowedTime: number, + public readonly currentTime: number, + ) { + super( + `Too far into slot for ${proposedState} (time into slot ${currentTime}s greater than ${maxAllowedTime}s allowance)`, + ); + this.name = 'SequencerTooSlowError'; + } +} diff --git a/yarn-project/sequencer-client/src/test/index.ts b/yarn-project/sequencer-client/src/test/index.ts index 7a1304461ae..e27d25b6500 100644 --- a/yarn-project/sequencer-client/src/test/index.ts +++ b/yarn-project/sequencer-client/src/test/index.ts @@ -3,12 +3,11 @@ import { type PublicProcessorFactory } from '@aztec/simulator/server'; import { SequencerClient } from '../client/sequencer-client.js'; import { type L1Publisher } from '../publisher/l1-publisher.js'; import { Sequencer } from '../sequencer/sequencer.js'; -import { type SequencerState } from '../sequencer/utils.js'; +import { type SequencerTimetable } from '../sequencer/timetable.js'; class TestSequencer_ extends Sequencer { public override publicProcessorFactory!: PublicProcessorFactory; - public override timeTable!: Record; - public override processTxTime!: number; + public override timetable!: SequencerTimetable; public override publisher!: L1Publisher; }