diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_invalidate_block.parallel.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_invalidate_block.parallel.test.ts index 01ac7e29aec0..78ebee3bb157 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_invalidate_block.parallel.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_invalidate_block.parallel.test.ts @@ -7,8 +7,8 @@ import { RollupContract } from '@aztec/ethereum/contracts'; import type { Operator } from '@aztec/ethereum/deploy-aztec-l1-contracts'; import type { ExtendedViemWalletClient } from '@aztec/ethereum/types'; import { asyncMap } from '@aztec/foundation/async-map'; -import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types'; -import { times } from '@aztec/foundation/collection'; +import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types'; +import { times, timesAsync } from '@aztec/foundation/collection'; import { SecretValue } from '@aztec/foundation/config'; import { EthAddress } from '@aztec/foundation/eth-address'; import { promiseWithResolvers } from '@aztec/foundation/promise'; @@ -16,14 +16,16 @@ import { retryUntil } from '@aztec/foundation/retry'; import { bufferToHex } from '@aztec/foundation/string'; import { timeoutPromise } from '@aztec/foundation/timer'; import { RollupAbi } from '@aztec/l1-artifacts'; -import type { SpamContract } from '@aztec/noir-test-contracts.js/Spam'; +import type { TestContract } from '@aztec/noir-test-contracts.js/Test'; import { OffenseType } from '@aztec/slasher'; import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers'; import { jest } from '@jest/globals'; import { privateKeyToAccount } from 'viem/accounts'; +import { getAnvilPort } from '../fixtures/fixtures.js'; import { type EndToEndContext, getPrivateKeyFromIndex } from '../fixtures/utils.js'; +import { proveInteraction } from '../test-wallet/utils.js'; import { EpochsTestContext } from './epochs_test.js'; jest.setTimeout(1000 * 60 * 10); @@ -31,17 +33,19 @@ jest.setTimeout(1000 * 60 * 10); const NODE_COUNT = 5; const VALIDATOR_COUNT = 5; +const BASE_ANVIL_PORT = getAnvilPort(); + describe('e2e_epochs/epochs_invalidate_block', () => { let context: EndToEndContext; let logger: Logger; let l1Client: ExtendedViemWalletClient; let rollupContract: RollupContract; - let anvilPort = 8545; + let anvilPortOffset = 0; let test: EpochsTestContext; let validators: (Operator & { privateKey: `0x${string}` })[]; let nodes: AztecNodeService[]; - let contract: SpamContract; + let testContract: TestContract; beforeEach(async () => { validators = times(VALIDATOR_COUNT, i => { @@ -51,8 +55,13 @@ describe('e2e_epochs/epochs_invalidate_block', () => { }); // Setup context with the given set of validators, mocked gossip sub network, and no anvil test watcher. + // Uses multiple-blocks-per-slot timing configuration. test = await EpochsTestContext.setup({ ethereumSlotDuration: 8, + aztecSlotDuration: 36, + blockDurationMs: 6000, + l1PublishingTime: 8, + enforceTimeTable: true, numberOfAccounts: 1, initialValidators: validators, mockGossipSubNetwork: true, @@ -62,11 +71,12 @@ describe('e2e_epochs/epochs_invalidate_block', () => { aztecTargetCommitteeSize: VALIDATOR_COUNT, archiverPollingIntervalMS: 200, anvilAccounts: 20, - anvilPort: ++anvilPort, + anvilPort: BASE_ANVIL_PORT + ++anvilPortOffset, slashingRoundSizeInEpochs: 4, slashingOffsetInRounds: 256, slasherFlavor: 'tally', minTxsPerBlock: 1, + maxTxsPerBlock: 1, }); ({ context, logger, l1Client } = test); @@ -88,8 +98,9 @@ describe('e2e_epochs/epochs_invalidate_block', () => { ); logger.warn(`Started ${NODE_COUNT} validator nodes.`, { validators: validatorNodes.map(v => v.attester) }); - // Register spam contract for sending txs. - contract = await test.registerSpamContract(context.wallet); + // Register test contract for lightweight txs + testContract = await test.registerTestContract(context.wallet); + logger.warn(`Test setup completed.`, { validators: validators.map(v => v.attester.toString()) }); }); @@ -98,23 +109,29 @@ describe('e2e_epochs/epochs_invalidate_block', () => { await test.teardown(); }); - it('proposer invalidates previous block while posting its own', async () => { + it('proposer invalidates previous checkpoint with multiple blocks while posting its own', async () => { const sequencers = nodes.map(node => node.getSequencer()!); - const initialBlockNumber = await nodes[0].getBlockNumber(); + const [initialCheckpointNumber, initialBlockNumber] = await nodes[0] + .getL2Tips() + .then(t => [t.checkpointed.checkpoint.number, t.checkpointed.block.number] as const); // Configure all sequencers to skip collecting attestations before starting + // Also set minBlocksForCheckpoint to ensure multi-block checkpoints logger.warn('Configuring all sequencers to skip attestation collection'); sequencers.forEach(sequencer => { - sequencer.updateConfig({ skipCollectingAttestations: true }); + sequencer.updateConfig({ skipCollectingAttestations: true, minBlocksForCheckpoint: 2 }); }); - // Send a transaction so the sequencer builds a block - logger.warn('Sending transaction to trigger block building'); - const sentTx = await contract.methods.spam(1, 1n, false).send({ from: context.accounts[0], wait: NO_WAIT }); + // Send a few transactions so the sequencer builds multiple blocks in the checkpoint + // We'll later check that the first tx at least was picked up and mined + logger.warn('Sending multiple transactions to trigger block building'); + const [sentTx] = await timesAsync(8, i => + testContract.methods.emit_nullifier(BigInt(i + 1)).send({ from: context.accounts[0], wait: NO_WAIT }), + ); - // Disable skipCollectingAttestations after the first L2 block is mined - test.monitor.once('checkpoint', ({ checkpointNumber }) => { - logger.warn(`Disabling skipCollectingAttestations after L2 block ${checkpointNumber} has been mined`); + // Disable skipCollectingAttestations after the first checkpoint and capture its number + test.monitor.on('checkpoint', ({ checkpointNumber }) => { + logger.warn(`Disabling skipCollectingAttestations after checkpoint ${checkpointNumber} has been mined`); sequencers.forEach(sequencer => { sequencer.updateConfig({ skipCollectingAttestations: false }); }); @@ -133,8 +150,8 @@ describe('e2e_epochs/epochs_invalidate_block', () => { toBlock: 'latest', }); - // The next proposer should invalidate the previous block and publish a new one - logger.warn('Waiting for next proposer to invalidate the previous block'); + // The next proposer should invalidate the previous checkpoint and publish a new one + logger.warn('Waiting for next proposer to invalidate the previous checkpoint'); // Wait for the CheckpointInvalidated event const checkpointInvalidatedEvents = await retryUntil( @@ -150,10 +167,10 @@ describe('e2e_epochs/epochs_invalidate_block', () => { // Verify the CheckpointInvalidated event was emitted and that the block was removed const [event] = checkpointInvalidatedEvents; logger.warn(`CheckpointInvalidated event emitted`, { event }); - expect(event.args.checkpointNumber).toBeGreaterThan(initialBlockNumber); + expect(event.args.checkpointNumber).toBeGreaterThan(initialCheckpointNumber); expect(test.rollup.address).toEqual(event.address); - // Wait for all nodes to sync the new block + // Wait for all nodes to sync the new block proposed logger.warn('Waiting for all nodes to sync'); await retryUntil( async () => { @@ -167,7 +184,7 @@ describe('e2e_epochs/epochs_invalidate_block', () => { ); // Verify the transaction was eventually included - const receipt = await waitForTx(context.aztecNode, sentTx, { timeout: 30 }); + const receipt = await waitForTx(context.aztecNode, sentTx, { timeout: test.L2_SLOT_DURATION_IN_S * 4 }); expect(receipt.isMined()).toBeTrue(); logger.warn(`Transaction included in block ${receipt.blockNumber}`); @@ -177,15 +194,19 @@ describe('e2e_epochs/epochs_invalidate_block', () => { const invalidBlockOffense = offenses.find(o => o.offenseType === OffenseType.PROPOSED_INSUFFICIENT_ATTESTATIONS); expect(invalidBlockOffense).toBeDefined(); + const currentCheckpoint = await test.rollup.getCheckpointNumber(); + logger.warn(`Waiting for checkpoint ${currentCheckpoint + 2} to be mined to ensure chain can progress`); + await test.waitUntilCheckpointNumber(CheckpointNumber(currentCheckpoint + 2), test.L2_SLOT_DURATION_IN_S * 8); + logger.warn(`Test succeeded '${expect.getState().currentTestName}'`); }); - // Regression for an issue where, if the invalidator proposed another invalid block, the next proposer would + // Regression for an issue where, if the invalidator proposed another invalid checkpoint, the next proposer would // try invalidating the first one, which would fail due to mismatching attestations. For example: - // Slot S: Block N is proposed with invalid attestations - // Slot S+1: Block N is invalidated, and block N' (same number) is proposed instead, but also has invalid attestations - // Slot S+2: Proposer tries to invalidate block N, when they should invalidate block N' instead, and fails - it('chain progresses if a block with insufficient attestations is invalidated with an invalid one', async () => { + // Slot S: Checkpoint N is proposed with invalid attestations + // Slot S+1: Checkpoint N is invalidated, and checkpoint N' (same number) is proposed instead, but also has invalid attestations + // Slot S+2: Proposer tries to invalidate checkpoint N, when they should invalidate checkpoint N' instead, and fails + it('chain progresses if a checkpoint with insufficient attestations is invalidated with an invalid one', async () => { // Configure all sequencers to skip collecting attestations before starting and always build blocks logger.warn('Configuring all sequencers to skip attestation collection'); const sequencers = nodes.map(node => node.getSequencer()!); @@ -212,10 +233,16 @@ describe('e2e_epochs/epochs_invalidate_block', () => { }); await Promise.race([timeoutPromise(1000 * test.L2_SLOT_DURATION_IN_S * 8), invalidatePromise.promise]); - // Disable skipCollectingAttestations + // Disable skipCollectingAttestations and send txs so MBPS can produce multi-block checkpoints sequencers.forEach(sequencer => { sequencer.updateConfig({ skipCollectingAttestations: false }); }); + logger.warn('Sending transactions to enable multi-block checkpoints'); + const from = context.accounts[0]; + for (let i = 0; i < 4; i++) { + const tx = await proveInteraction(context.wallet, testContract.methods.emit_nullifier(new Fr(100 + i)), { from }); + await tx.send({ wait: NO_WAIT }); + } // Ensure chain progresses const targetCheckpointNumber = CheckpointNumber(lastInvalidatedCheckpointNumber! + 2); @@ -236,11 +263,13 @@ describe('e2e_epochs/epochs_invalidate_block', () => { 0.5, ); + await test.assertMultipleBlocksPerSlot(2); + logger.warn(`Test succeeded '${expect.getState().currentTestName}'`); }); // Regression for Joe's Q42025 London attack. Same as above but with an invalid signature instead of insufficient ones. - it('chain progresses if a block with an invalid attestation is invalidated with an invalid one', async () => { + it('chain progresses if a checkpoint with an invalid attestation is invalidated with an invalid one', async () => { // Configure all sequencers to skip collecting attestations before starting and always build blocks logger.warn('Configuring all sequencers to inject one invalid attestation'); const sequencers = nodes.map(node => node.getSequencer()!); @@ -270,7 +299,7 @@ describe('e2e_epochs/epochs_invalidate_block', () => { invalidatePromise.promise, ]); - // Disable injectFakeAttestation + // Disable injectFakeAttestations sequencers.forEach(sequencer => { sequencer.updateConfig({ injectFakeAttestation: false }); }); @@ -297,11 +326,11 @@ describe('e2e_epochs/epochs_invalidate_block', () => { logger.warn(`Test succeeded '${expect.getState().currentTestName}'`); }); - // Here we disable invalidation checks from two of the proposers. Our goal is to get two invalid blocks + // Here we disable invalidation checks from two of the proposers. Our goal is to get two invalid checkpoints // in a row, so the third proposer invalidates the earliest one, and the chain progresses. Note that the - // second invalid block will also have invalid attestations, we are *not* testing the scenario where the - // committee is malicious (or incompetent) and attests for the descendent of an invalid block. - it('proposer invalidates multiple blocks', async () => { + // second invalid checkpoint will also have invalid attestations, we are *not* testing the scenario where the + // committee is malicious (or incompetent) and attests for the descendent of an invalid checkpoint. + it('proposer invalidates multiple checkpoints', async () => { const initialSlot = (await test.monitor.run()).l2SlotNumber; // Disable validation and attestation gathering for the proposers of two consecutive slots @@ -406,9 +435,9 @@ describe('e2e_epochs/epochs_invalidate_block', () => { logger.warn(`Test succeeded '${expect.getState().currentTestName}'`); }); - it('proposer invalidates previous block without publishing its own', async () => { + it('proposer invalidates previous checkpoint without publishing its own', async () => { const sequencers = nodes.map(node => node.getSequencer()!); - const initialBlockNumber = await nodes[0].getBlockNumber(); + const initialCheckpointNumber = (await nodes[0].getL2Tips()).checkpointed.checkpoint.number; // Configure all sequencers to skip collecting attestations before starting logger.warn('Configuring all sequencers to skip attestation collection and always publish blocks'); @@ -437,8 +466,8 @@ describe('e2e_epochs/epochs_invalidate_block', () => { toBlock: 'latest', }); - // The next proposer should invalidate the previous block - logger.warn('Waiting for next proposer to invalidate the previous block'); + // The next proposer should invalidate the previous checkpoint + logger.warn('Waiting for next proposer to invalidate the previous checkpoint'); // Wait for the CheckpointInvalidated event const checkpointInvalidatedEvents = await retryUntil( @@ -454,8 +483,7 @@ describe('e2e_epochs/epochs_invalidate_block', () => { // Verify the CheckpointInvalidated event was emitted and that the block was removed const [event] = checkpointInvalidatedEvents; logger.warn(`CheckpointInvalidated event emitted`, { event }); - expect(event.args.checkpointNumber).toBeGreaterThan(initialBlockNumber); - const initialCheckpointNumber = await getCheckpointNumberForBlock(nodes[0], initialBlockNumber); + expect(event.args.checkpointNumber).toBeGreaterThan(initialCheckpointNumber); expect(await test.rollup.getCheckpointNumber()).toEqual(initialCheckpointNumber); logger.warn(`Test succeeded '${expect.getState().currentTestName}'`); @@ -465,7 +493,7 @@ describe('e2e_epochs/epochs_invalidate_block', () => { // REFACTOR: Remove code duplication with above test (and others?) it('proposer invalidates previous block with shuffled attestations', async () => { const sequencers = nodes.map(node => node.getSequencer()!); - const initialBlockNumber = await nodes[0].getBlockNumber(); + const initialCheckpointNumber = (await nodes[0].getL2Tips()).checkpointed.checkpoint.number; // Configure all sequencers to shuffle attestations before starting logger.warn('Configuring all sequencers to shuffle attestations and always publish blocks'); @@ -494,8 +522,8 @@ describe('e2e_epochs/epochs_invalidate_block', () => { toBlock: 'latest', }); - // The next proposer should invalidate the previous block - logger.warn('Waiting for next proposer to invalidate the previous block'); + // The next proposer should invalidate the previous checkpoint + logger.warn('Waiting for next proposer to invalidate the previous checkpoint'); // Wait for the CheckpointInvalidated event const checkpointInvalidatedEvents = await retryUntil( @@ -511,8 +539,7 @@ describe('e2e_epochs/epochs_invalidate_block', () => { // Verify the CheckpointInvalidated event was emitted and that the block was removed const [event] = checkpointInvalidatedEvents; logger.warn(`CheckpointInvalidated event emitted`, { event }); - expect(event.args.checkpointNumber).toBeGreaterThan(initialBlockNumber); - const initialCheckpointNumber = await getCheckpointNumberForBlock(nodes[0], initialBlockNumber); + expect(event.args.checkpointNumber).toBeGreaterThan(initialCheckpointNumber); expect(await test.rollup.getCheckpointNumber()).toEqual(initialCheckpointNumber); logger.warn(`Test succeeded '${expect.getState().currentTestName}'`); @@ -520,7 +547,7 @@ describe('e2e_epochs/epochs_invalidate_block', () => { it('committee member invalidates a block if proposer does not come through', async () => { const sequencers = nodes.map(node => node.getSequencer()!); - const initialBlockNumber = await nodes[0].getBlockNumber(); + const initialCheckpointNumber = await nodes[0].getL2Tips().then(t => t.checkpointed.checkpoint.number); // Configure all sequencers to skip collecting attestations before starting logger.warn('Configuring all sequencers to skip attestation collection and invalidation as proposer'); @@ -560,8 +587,8 @@ describe('e2e_epochs/epochs_invalidate_block', () => { toBlock: 'latest', }); - // Some committee member should invalidate the previous block - logger.warn('Waiting for committee member to invalidate the previous block'); + // Some committee member should invalidate the previous checkpoint + logger.warn('Waiting for committee member to invalidate the previous checkpoint'); // Wait for the CheckpointInvalidated event const checkpointInvalidatedEvents = await retryUntil( @@ -577,7 +604,7 @@ describe('e2e_epochs/epochs_invalidate_block', () => { // Verify the CheckpointInvalidated event was emitted const [event] = checkpointInvalidatedEvents; logger.warn(`CheckpointInvalidated event emitted`, { event }); - expect(event.args.checkpointNumber).toBeGreaterThan(initialBlockNumber); + expect(event.args.checkpointNumber).toBeGreaterThan(initialCheckpointNumber); // And check that the invalidation happened at least after the specified timeout. // We use the checkpoint header timestamp (L2 timestamp) since that's what the sequencer uses @@ -585,20 +612,7 @@ describe('e2e_epochs/epochs_invalidate_block', () => { const invalidSlotTimestamp = getTimestampForSlot(invalidCheckpointSlotNumber!, test.constants); const { timestamp: invalidationTimestamp } = await l1Client.getBlock({ blockNumber: event.blockNumber }); expect(invalidationTimestamp).toBeGreaterThanOrEqual(invalidSlotTimestamp + BigInt(invalidationDelay)); + logger.warn(`Test succeeded '${expect.getState().currentTestName}'`); }); }); - -async function getCheckpointNumberForBlock( - node: AztecNodeService, - blockNumber: BlockNumber, -): Promise { - if (blockNumber === 0) { - return CheckpointNumber(0); - } - const block = await node.getBlock(blockNumber); - if (!block) { - throw new Error(`Block ${blockNumber} not found`); - } - return block.checkpointNumber; -} diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_l1_reorgs.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_l1_reorgs.parallel.test.ts similarity index 72% rename from yarn-project/end-to-end/src/e2e_epochs/epochs_l1_reorgs.test.ts rename to yarn-project/end-to-end/src/e2e_epochs/epochs_l1_reorgs.parallel.test.ts index 063287d2ec1b..776a9374a093 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_l1_reorgs.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_l1_reorgs.parallel.test.ts @@ -1,6 +1,7 @@ import type { Archiver } from '@aztec/archiver'; import type { AztecNodeService } from '@aztec/aztec-node'; import { AztecAddress } from '@aztec/aztec.js/addresses'; +import { NO_WAIT } from '@aztec/aztec.js/contracts'; import { Fr } from '@aztec/aztec.js/fields'; import type { Logger } from '@aztec/aztec.js/log'; import type { AztecNode } from '@aztec/aztec.js/node'; @@ -15,6 +16,7 @@ import { AbortError } from '@aztec/foundation/error'; import { retryUntil } from '@aztec/foundation/retry'; import { hexToBuffer } from '@aztec/foundation/string'; import { executeTimeout } from '@aztec/foundation/timer'; +import type { TestContract } from '@aztec/noir-test-contracts.js/Test'; import type { ProverNode } from '@aztec/prover-node'; import { jest } from '@jest/globals'; @@ -23,9 +25,10 @@ import { keccak256, parseTransaction } from 'viem'; import { sendL1ToL2Message } from '../fixtures/l1_to_l2_messaging.js'; import type { EndToEndContext } from '../fixtures/utils.js'; +import { proveInteraction } from '../test-wallet/utils.js'; import { EpochsTestContext } from './epochs_test.js'; -jest.setTimeout(1000 * 60 * 10); +jest.setTimeout(1000 * 60 * 20); describe('e2e_epochs/epochs_l1_reorgs', () => { let context: EndToEndContext; @@ -41,18 +44,44 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { let L2_SLOT_DURATION_IN_S: number; let test: EpochsTestContext; + let contract: TestContract; + let from: AztecAddress; + + // Number of txs to send at the start of each blocks test to trigger multi-block checkpoints. + const TX_COUNT = 8; + + /** Pre-proves and sends txs to generate L2 activity for multi-block checkpoints. */ + const sendTransactions = async (count: number, offset = 0) => { + logger.warn(`Pre-proving ${count} transactions`); + const txs = await timesAsync(count, i => + proveInteraction(context.wallet, contract.methods.emit_nullifier(new Fr(offset + i + 1)), { from }), + ); + const txHashes = await Promise.all(txs.map(tx => tx.send({ wait: NO_WAIT }))); + logger.warn(`Sent ${txHashes.length} transactions`); + return txHashes; + }; beforeEach(async () => { test = await EpochsTestContext.setup({ + numberOfAccounts: 1, maxSpeedUpAttempts: 0, // Do not speed up l1 txs, we dont want them to land cancelTxOnTimeout: false, - aztecEpochDuration: 8, // Bump epoch duration, epoch 0 is finishing before we had a chance to do anything - ethereumSlotDuration: process.env.L1_BLOCK_TIME ? parseInt(process.env.L1_BLOCK_TIME) : 4, // Got to speed these tests up for CI + aztecEpochDuration: 4, + ethereumSlotDuration: 4, + aztecSlotDuration: 36, + blockDurationMs: 8000, + l1PublishingTime: 2, + minTxsPerBlock: 0, + maxTxsPerBlock: 1, + enforceTimeTable: true, + aztecProofSubmissionEpochs: 1, }); ({ proverDelayer, sequencerDelayer, context, logger, monitor, L1_BLOCK_TIME_IN_S, L2_SLOT_DURATION_IN_S } = test); node = context.aztecNode; archiver = (node as AztecNodeService).getBlockSource() as Archiver; proverNode = context.proverNode!; + from = context.accounts[0]; + contract = await test.registerTestContract(context.wallet); }); afterEach(async () => { @@ -75,6 +104,12 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { const getProvenCheckpointNumber = (node: AztecNode) => node.getL2Tips().then(tips => tips.proven.checkpoint.number); it('prunes L2 blocks if a proof is removed due to an L1 reorg', async () => { + // Send txs to trigger multi-block checkpoints + await sendTransactions(TX_COUNT); + + // Capture initial chain state + const initialProvenCheckpoint = (await monitor.run(true)).provenCheckpointNumber; + // Wait until we have proven something and the nodes have caught up const epochDurationSeconds = test.constants.epochDuration * test.constants.slotDuration; logger.warn(`Waiting for initial proof to land`); @@ -82,7 +117,7 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { signal => { return new Promise<{ provenCheckpointNumber: number; l1BlockNumber: number }>((res, rej) => { const handleMsg = (...[ev]: ChainMonitorEventMap['checkpoint-proven']) => { - if (ev.provenCheckpointNumber !== 0) { + if (ev.provenCheckpointNumber > initialProvenCheckpoint) { res(ev); monitor.off('checkpoint-proven', handleMsg); } @@ -104,15 +139,18 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { // And remove the proof from L1 await context.cheatCodes.eth.reorgTo(provenBlockEvent.l1BlockNumber - 1); - expect((await monitor.run(true)).provenCheckpointNumber).toEqual(0); + expect((await monitor.run(true)).provenCheckpointNumber).toEqual(initialProvenCheckpoint); - // Wait until the end of the proof submission window for the first epoch - await test.waitUntilLastSlotOfProofSubmissionWindow(0); + // Wait until the end of the proof submission window for the epoch of the proven checkpoint + const provenCheckpointEpoch = await test.rollup.getEpochNumberForCheckpoint( + CheckpointNumber(provenBlockEvent.provenCheckpointNumber), + ); + await test.waitUntilLastSlotOfProofSubmissionWindow(provenCheckpointEpoch); // Ensure that a new node sees the reorg logger.warn(`Syncing new node to test reorg`); const newNode = await executeTimeout(() => test.createNonValidatorNode(), 10_000, `new node sync`); - expect(await newNode.getProvenBlockNumber()).toEqual(0); + expect(await getProvenCheckpointNumber(newNode)).toEqual(initialProvenCheckpoint); // Latest checkpointed block seen by the node may be from the current checkpoint, or one less if it was *just* mined. // This is because the call to createNonValidatorNode will block until the initial sync is completed, @@ -123,53 +161,97 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { // And check that the old node has processed the reorg as well logger.warn(`Testing old node after reorg`); - await retryUntil(() => node.getProvenBlockNumber().then(b => b === 0), 'prune', L2_SLOT_DURATION_IN_S * 4, 0.1); + await retryUntil( + () => getProvenCheckpointNumber(node).then(cp => cp === initialProvenCheckpoint), + 'prune', + L2_SLOT_DURATION_IN_S * 4, + 0.1, + ); expect(await getCheckpointNumber(node)).toBeWithin(monitor.checkpointNumber - 1, monitor.checkpointNumber + 1); + // Verify multi-block checkpoints were built + await test.assertMultipleBlocksPerSlot(2); + logger.warn(`Test succeeded`); await newNode.stop(); }); it('does not prune if a second proof lands within the submission window after the first one is reorged out', async () => { + // Send txs to trigger multi-block checkpoints + await sendTransactions(TX_COUNT); + + // Capture initial chain state + const initialProvenCheckpoint = (await monitor.run(true)).provenCheckpointNumber; + const targetProvenCheckpoint = CheckpointNumber(initialProvenCheckpoint + 1); + // Wait until we have proven something and the nodes have caught up + // Use a longer timeout since we need to wait for the epoch to complete (~288s) plus proving time + const epochDurationSeconds = test.constants.epochDuration * test.constants.slotDuration; logger.warn(`Waiting for initial proof to land`); - const provenCheckpoint = await test.waitUntilProvenCheckpointNumber(CheckpointNumber(1)); - const provenBlock = Number(provenCheckpoint); - await retryUntil(() => node.getProvenBlockNumber().then(p => p >= provenBlock), 'node sync', 10, 0.1); + const provenCheckpoint = await test.waitUntilProvenCheckpointNumber( + targetProvenCheckpoint, + epochDurationSeconds * 2, + ); + await retryUntil(() => getProvenCheckpointNumber(node).then(cp => cp >= provenCheckpoint), 'node sync', 10, 0.1); // Stop the prover node await proverNode.stop(); // Remove the proof from L1 but do not change the block number await context.cheatCodes.eth.reorgWithReplacement(1); - await expect(monitor.run(true).then(m => m.provenCheckpointNumber)).resolves.toEqual(0); + await expect(monitor.run(true).then(m => m.provenCheckpointNumber)).resolves.toEqual(initialProvenCheckpoint); // Create another prover node so it submits a proof and wait until it is submitted + // Use a longer timeout to allow the new prover to sync and generate a proof const newProverNode = await test.createProverNode(); - const provenCheckpointRetry = await test.waitUntilProvenCheckpointNumber(CheckpointNumber(1)); - await expect(monitor.run(true).then(m => m.provenCheckpointNumber)).resolves.toBeGreaterThanOrEqual(1); + const provenCheckpointRetry = await test.waitUntilProvenCheckpointNumber( + targetProvenCheckpoint, + epochDurationSeconds, + ); + await expect(monitor.run(true).then(m => m.provenCheckpointNumber)).resolves.toBeGreaterThanOrEqual( + targetProvenCheckpoint, + ); // Check that the node has followed along logger.warn(`Testing old node`); - const provenBlockRetry = Number(provenCheckpointRetry); - await retryUntil(() => node.getProvenBlockNumber().then(b => b >= provenBlockRetry), 'proof sync', 10, 0.1); + await retryUntil( + () => getProvenCheckpointNumber(node).then(cp => cp >= provenCheckpointRetry), + 'proof sync', + 10, + 0.1, + ); expect(await getCheckpointNumber(node)).toBeWithin(monitor.checkpointNumber - 1, monitor.checkpointNumber + 1); + // Verify multi-block checkpoints were built + await test.assertMultipleBlocksPerSlot(2); + logger.warn(`Test succeeded`); await newProverNode.stop(); }); it('restores L2 blocks if a proof is added due to an L1 reorg', async () => { + // Send txs to trigger multi-block checkpoints + await sendTransactions(TX_COUNT); + + // Capture initial chain state + const initialProvenCheckpoint = (await monitor.run(true)).provenCheckpointNumber; + const initialCheckpoint = monitor.checkpointNumber; + // Next proof shall not land proverDelayer.cancelNextTx(); // Expect pending chain to advance, so there's something to be pruned - await retryUntil(() => node.getBlockNumber().then(b => b > 1), 'node sync', 60, 0.1); + await retryUntil(() => getCheckpointNumber(node).then(cp => cp > initialCheckpoint), 'node sync', 60, 0.1); - // Wait until the end of the proof submission window for the first epoch - await test.waitUntilLastSlotOfProofSubmissionWindow(0); + // Wait until the end of the proof submission window for the first unproven epoch + const firstUnprovenCheckpoint = CheckpointNumber(initialProvenCheckpoint + 1); + await test.waitUntilCheckpointNumber(firstUnprovenCheckpoint, 60); + const epochToWaitFor = await test.rollup.getEpochNumberForCheckpoint(firstUnprovenCheckpoint); + await test.waitUntilLastSlotOfProofSubmissionWindow(epochToWaitFor); await monitor.run(true); - logger.warn(`End of epoch 0 submission window (L1 block ${await monitor.run(true).then(m => m.l1BlockNumber)}).`); + logger.warn( + `End of epoch ${epochToWaitFor} submission window (L1 block ${await monitor.run(true).then(m => m.l1BlockNumber)}).`, + ); // Grab the prover's tx to submit it later as part of a reorg and stop the prover const [proofTx] = proverDelayer.getCancelledTxs(); @@ -179,9 +261,14 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { // Wait for the node to prune const syncTimeout = L2_SLOT_DURATION_IN_S * 2; - await retryUntil(() => node.getBlockNumber().then(b => b <= 1), 'node prune', syncTimeout, 0.1); - expect(monitor.provenCheckpointNumber).toEqual(0); - expect(await node.getProvenBlockNumber()).toEqual(0); + await retryUntil( + () => getCheckpointNumber(node).then(cp => cp <= initialProvenCheckpoint + 1), + 'node prune', + syncTimeout, + 0.1, + ); + expect(monitor.provenCheckpointNumber).toEqual(initialProvenCheckpoint); + expect(await getProvenCheckpointNumber(node)).toEqual(initialProvenCheckpoint); // But not all is lost, for a reorg gets the proof back on chain! logger.warn(`Reorging proof back (L1 block ${await monitor.run(true).then(m => m.l1BlockNumber)}).`); @@ -191,8 +278,8 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { // Monitor should update to see the proof const { checkpointNumber, provenCheckpointNumber } = await monitor.run(true); - expect(checkpointNumber).toBeGreaterThan(1); - expect(provenCheckpointNumber).toBeGreaterThan(0); + expect(checkpointNumber).toBeGreaterThan(initialCheckpoint); + expect(provenCheckpointNumber).toBeGreaterThan(initialProvenCheckpoint); // And so the node undoes its reorg await retryUntil( @@ -208,18 +295,30 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { 0.1, ); + // Verify multi-block checkpoints were built + await test.assertMultipleBlocksPerSlot(2); + logger.warn(`Test succeeded`); }); it('prunes blocks from pending chain removed from L1 due to an L1 reorg', async () => { + // Send txs to trigger multi-block checkpoints + await sendTransactions(TX_COUNT); + + // Capture initial chain state + const initialCheckpoint = (await monitor.run(true)).checkpointNumber; + // Wait until CHECKPOINT_NUMBER is mined and node synced, and stop the sequencer - const CHECKPOINT_NUMBER = CheckpointNumber(3); - await test.waitUntilCheckpointNumber(CHECKPOINT_NUMBER, L2_SLOT_DURATION_IN_S * (CHECKPOINT_NUMBER + 4)); + const CHECKPOINT_NUMBER = CheckpointNumber(initialCheckpoint + 3); + await test.waitUntilCheckpointNumber(CHECKPOINT_NUMBER, L2_SLOT_DURATION_IN_S * 7); expect(monitor.checkpointNumber).toEqual(CHECKPOINT_NUMBER); const l1BlockNumber = monitor.l1BlockNumber; // Wait for node to sync to the checkpoint. await retryUntil(() => getCheckpointNumber(node).then(b => b === CHECKPOINT_NUMBER), 'node sync', 10, 0.1); + // Verify multi-block checkpoints were built before we do the reorg + await test.assertMultipleBlocksPerSlot(2); + logger.warn(`Reached checkpoint ${CHECKPOINT_NUMBER}. Stopping block production.`); await context.aztecNodeAdmin.setConfig({ minTxsPerBlock: 100 }); @@ -237,14 +336,23 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { }); it('sees new blocks added in an L1 reorg', async () => { + // Send txs to trigger multi-block checkpoints + await sendTransactions(TX_COUNT); + + // Capture initial chain state + const initialCheckpoint = (await monitor.run(true)).checkpointNumber; + // Wait until the checkpoint *before* CHECKPOINT_NUMBER is mined and node synced - const CHECKPOINT_NUMBER = CheckpointNumber(3); + const CHECKPOINT_NUMBER = CheckpointNumber(initialCheckpoint + 3); const prevCheckpointNumber = CheckpointNumber(CHECKPOINT_NUMBER - 1); - await test.waitUntilCheckpointNumber(prevCheckpointNumber, L2_SLOT_DURATION_IN_S * (CHECKPOINT_NUMBER + 4)); + await test.waitUntilCheckpointNumber(prevCheckpointNumber, L2_SLOT_DURATION_IN_S * 7); expect(monitor.checkpointNumber).toEqual(prevCheckpointNumber); // Wait for node to sync to the checkpoint await retryUntil(() => getCheckpointNumber(node).then(b => b === prevCheckpointNumber), 'node sync', 5, 0.1); + // Verify multi-block checkpoints were built before we do the reorg + await test.assertMultipleBlocksPerSlot(2); + // Cancel the next tx to be mined and pause the sequencer sequencerDelayer.cancelNextTx(); await retryUntil(() => sequencerDelayer.getCancelledTxs().length, 'next block', L2_SLOT_DURATION_IN_S * 2, 0.1); @@ -308,6 +416,9 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { ); it('updates L1 to L2 messages changed due to an L1 reorg', async () => { + // Send L2 txs to trigger multi-block checkpoints + await sendTransactions(TX_COUNT, 100); + // Send 3 messages and wait for archiver sync logger.warn(`Sending 3 cross chain messages`); const msgs = await timesAsync(3, async (i: number) => { @@ -335,9 +446,16 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { await retryUntil(() => node.isL1ToL2MessageSynced(newMsg.msgHash), 'new message sync', L1_BLOCK_TIME_IN_S * 6, 1); expect(await node.isL1ToL2MessageSynced(msgs[0].msgHash)).toBe(true); expect(await node.isL1ToL2MessageSynced(msgs.at(-1)!.msgHash)).toBe(false); + + // Verify multi-block checkpoints were built + await test.assertMultipleBlocksPerSlot(2); }); it('handles missed message inserted by an L1 reorg', async () => { + // Send L2 txs to trigger multi-block checkpoints and wait for them to land in a checkpoint + await sendTransactions(TX_COUNT, 200); + await test.waitUntilCheckpointNumber(CheckpointNumber(2), L2_SLOT_DURATION_IN_S * 4); + // Send a message and wait for node to sync it logger.warn(`Sending first cross chain message`); const firstMsg = await sendMessage(); @@ -369,6 +487,9 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { logger.warn(`Reorged-in second message on L1 block ${secondMsg.txReceipt.blockNumber}. Sending third message.`); const thirdMsg = await sendMessage(); await retryUntil(() => node.isL1ToL2MessageSynced(thirdMsg.msgHash), '3rd msg sync', L1_BLOCK_TIME_IN_S * 3, 1); + + // Verify multi-block checkpoints were built + await test.assertMultipleBlocksPerSlot(2); }); }); }); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_test.ts index 951e686db373..d7818b85be44 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_test.ts @@ -1,3 +1,4 @@ +import type { Archiver } from '@aztec/archiver'; import { type AztecNodeConfig, AztecNodeService } from '@aztec/aztec-node'; import { getTimestampRangeForEpoch } from '@aztec/aztec.js/block'; import { getContractInstanceFromInstantiationParams } from '@aztec/aztec.js/contracts'; @@ -319,7 +320,10 @@ export class EpochsTestContext { this.logger.info(`Waiting until last slot of submission window for epoch ${epochNumber} at ${date}`, { oneSlotBefore, }); - await waitUntilL1Timestamp(this.l1Client, oneSlotBefore); + // Use a timeout that accounts for the full proof submission window + const proofSubmissionWindowDuration = + this.constants.proofSubmissionEpochs * this.epochDuration * this.L2_SLOT_DURATION_IN_S; + await waitUntilL1Timestamp(this.l1Client, oneSlotBefore, undefined, proofSubmissionWindowDuration * 2); } /** Waits for the aztec node to sync to the target block number. */ @@ -394,6 +398,38 @@ export class EpochsTestContext { expect(result).toBe(expectedSuccess); } + /** Verifies at least one checkpoint has the target number of blocks (for MBPS validation). */ + public async assertMultipleBlocksPerSlot(targetBlockCount: number) { + const archiver = (this.context.aztecNode as AztecNodeService).getBlockSource() as Archiver; + const checkpoints = await archiver.getCheckpoints(CheckpointNumber(1), 50); + + this.logger.warn(`Retrieved ${checkpoints.length} checkpoints from archiver`, { + checkpoints: checkpoints.map(pc => pc.checkpoint.getStats()), + }); + + let expectedBlockNumber = checkpoints[0].checkpoint.blocks[0].number; + let targetFound = false; + + for (const checkpoint of checkpoints) { + const blockCount = checkpoint.checkpoint.blocks.length; + targetFound = targetFound || blockCount >= targetBlockCount; + + this.logger.verbose(`Checkpoint ${checkpoint.checkpoint.number} has ${blockCount} blocks`, { + checkpoint: checkpoint.checkpoint.getStats(), + }); + + for (let i = 0; i < blockCount; i++) { + const block = checkpoint.checkpoint.blocks[i]; + expect(block.indexWithinCheckpoint).toBe(i); + expect(block.checkpointNumber).toBe(checkpoint.checkpoint.number); + expect(block.number).toBe(expectedBlockNumber); + expectedBlockNumber++; + } + } + + expect(targetFound).toBe(true); + } + public watchSequencerEvents( sequencers: SequencerClient[], getMetadata: (i: number) => Record = () => ({}), diff --git a/yarn-project/end-to-end/src/e2e_p2p/multiple_validators_sentinel.parallel.test.ts b/yarn-project/end-to-end/src/e2e_p2p/multiple_validators_sentinel.parallel.test.ts index 7dc521775561..8747f2b7251d 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/multiple_validators_sentinel.parallel.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/multiple_validators_sentinel.parallel.test.ts @@ -10,17 +10,18 @@ import 'jest-extended'; import os from 'os'; import path from 'path'; +import { getBootNodeUdpPort } from '../fixtures/fixtures.js'; import { createNodes, createNonValidatorNode } from '../fixtures/setup_p2p_test.js'; import { P2PNetworkTest } from './p2p_network.js'; const NUM_NODES = 2; const VALIDATORS_PER_NODE = 3; const NUM_VALIDATORS = NUM_NODES * VALIDATORS_PER_NODE; -const BOOT_NODE_UDP_PORT = 4500; +const BOOT_NODE_UDP_PORT = getBootNodeUdpPort(); const SLOT_COUNT = 3; const EPOCH_DURATION = 2; -const ETHEREUM_SLOT_DURATION = 4; -const AZTEC_SLOT_DURATION = 8; +const ETHEREUM_SLOT_DURATION = 8; +const AZTEC_SLOT_DURATION = 36; const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'validators-sentinel-')); @@ -46,6 +47,9 @@ describe('e2e_p2p_multiple_validators_sentinel', () => { aztecTargetCommitteeSize: NUM_VALIDATORS, aztecSlotDuration: AZTEC_SLOT_DURATION, ethereumSlotDuration: ETHEREUM_SLOT_DURATION, + blockDurationMs: 6000, + l1PublishingTime: 8, + enforceTimeTable: true, aztecProofSubmissionEpochs: 1024, // effectively do not reorg listenAddress: '127.0.0.1', minTxsPerBlock: 0, diff --git a/yarn-project/end-to-end/src/e2e_p2p/reqresp/utils.ts b/yarn-project/end-to-end/src/e2e_p2p/reqresp/utils.ts index b5860060ba5f..05fe3e3429be 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/reqresp/utils.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/reqresp/utils.ts @@ -3,24 +3,24 @@ import { createLogger } from '@aztec/aztec.js/log'; import { waitForTx } from '@aztec/aztec.js/node'; import { Tx } from '@aztec/aztec.js/tx'; import { RollupContract } from '@aztec/ethereum/contracts'; -import { SlotNumber } from '@aztec/foundation/branded-types'; +import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types'; import { timesAsync } from '@aztec/foundation/collection'; import { retryUntil } from '@aztec/foundation/retry'; -import { jest } from '@jest/globals'; +import { expect, jest } from '@jest/globals'; import fs from 'fs'; import os from 'os'; import path from 'path'; -import { shouldCollectMetrics } from '../../fixtures/fixtures.js'; +import { getBootNodeUdpPort, shouldCollectMetrics } from '../../fixtures/fixtures.js'; import { createNodes } from '../../fixtures/setup_p2p_test.js'; -import { P2PNetworkTest, SHORTENED_BLOCK_TIME_CONFIG_NO_PRUNES, WAIT_FOR_TX_TIMEOUT } from '../p2p_network.js'; +import { P2PNetworkTest, WAIT_FOR_TX_TIMEOUT } from '../p2p_network.js'; import { prepareTransactions } from '../shared.js'; // Don't set this to a higher value than 9 because each node will use a different L1 publisher account and anvil seeds export const NUM_VALIDATORS = 6; -export const NUM_TXS_PER_NODE = 2; -export const BOOT_NODE_UDP_PORT = 4500; +export const NUM_TXS_PER_NODE = 4; +export const BOOT_NODE_UDP_PORT = getBootNodeUdpPort(); export const createReqrespDataDir = () => fs.mkdtempSync(path.join(os.tmpdir(), 'reqresp-')); @@ -38,8 +38,14 @@ export async function createReqrespTest(options: ReqrespOptions = {}): Promise

= 2; + + for (let i = 0; i < blockCount; i++) { + const block = published.checkpoint.blocks[i]; + expect(block.indexWithinCheckpoint).toBe(i); + expect(block.checkpointNumber).toBe(published.checkpoint.number); + expect(block.number).toBe(expectedBlockNumber); + expectedBlockNumber++; + } + } + + expect(mbpsFound).toBe(true); return nodes; } diff --git a/yarn-project/end-to-end/src/fixtures/fixtures.ts b/yarn-project/end-to-end/src/fixtures/fixtures.ts index edf72bd67b53..6b2002c6ffcd 100644 --- a/yarn-project/end-to-end/src/fixtures/fixtures.ts +++ b/yarn-project/end-to-end/src/fixtures/fixtures.ts @@ -7,6 +7,16 @@ export const shouldCollectMetrics = () => { return undefined; }; +/** Returns the boot node UDP port from environment variable or default value. */ +export function getBootNodeUdpPort(): number { + return process.env.BOOT_NODE_UDP_PORT ? parseInt(process.env.BOOT_NODE_UDP_PORT, 10) : 4500; +} + +/** Returns the anvil port from environment variable or default value. */ +export function getAnvilPort(): number { + return process.env.ANVIL_PORT ? parseInt(process.env.ANVIL_PORT, 10) : 8545; +} + export const TEST_PEER_CHECK_INTERVAL_MS = 1000; export const TEST_MAX_PENDING_TX_POOL_COUNT = 10_000; // Number of max pending TXs ~ 1.56GB diff --git a/yarn-project/prover-node/src/factory.ts b/yarn-project/prover-node/src/factory.ts index ac79e73e22ff..db2c855ddf7f 100644 --- a/yarn-project/prover-node/src/factory.ts +++ b/yarn-project/prover-node/src/factory.ts @@ -231,5 +231,6 @@ export async function createProverNode( proverNodeConfig, telemetry, delayer, + dateProvider, ); } diff --git a/yarn-project/prover-node/src/prover-node.ts b/yarn-project/prover-node/src/prover-node.ts index 47f704581168..25f7bbb3eb60 100644 --- a/yarn-project/prover-node/src/prover-node.ts +++ b/yarn-project/prover-node/src/prover-node.ts @@ -56,7 +56,6 @@ type DataStoreOptions = Pick & Pick = new Map(); private config: ProverNodeOptions; @@ -81,6 +80,7 @@ export class ProverNode implements EpochMonitorHandler, ProverNodeApi, Traceable config: Partial = {}, protected readonly telemetryClient: TelemetryClient = getTelemetryClient(), private delayer?: Delayer, + private readonly dateProvider: DateProvider = new DateProvider(), ) { this.config = { proverNodePollingIntervalMs: 1_000, diff --git a/yarn-project/sequencer-client/src/config.ts b/yarn-project/sequencer-client/src/config.ts index 60ce42919779..a6004e0f88c9 100644 --- a/yarn-project/sequencer-client/src/config.ts +++ b/yarn-project/sequencer-client/src/config.ts @@ -205,6 +205,9 @@ export const sequencerConfigMappings: ConfigMappingsType = { description: 'Skip pushing proposed blocks to archiver (default: true)', ...booleanConfigHelper(DefaultSequencerConfig.skipPushProposedBlocksToArchiver), }, + minBlocksForCheckpoint: { + description: 'Minimum number of blocks required for a checkpoint proposal (test only)', + }, ...pickConfigMappings(p2pConfigMappings, ['txPublicSetupAllowList']), }; diff --git a/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.ts b/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.ts index e7b84bb9cddc..7987730d579b 100644 --- a/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.ts +++ b/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.ts @@ -248,6 +248,15 @@ export class CheckpointProposalJob implements Traceable { return undefined; } + const minBlocksForCheckpoint = this.config.minBlocksForCheckpoint; + if (minBlocksForCheckpoint !== undefined && blocksInCheckpoint.length < minBlocksForCheckpoint) { + this.log.warn( + `Checkpoint has fewer blocks than minimum (${blocksInCheckpoint.length} < ${minBlocksForCheckpoint}), skipping proposal`, + { slot: this.slot, blocksBuilt: blocksInCheckpoint.length, minBlocksForCheckpoint }, + ); + return undefined; + } + // Assemble and broadcast the checkpoint proposal, including the last block that was not // broadcasted yet, and wait to collect the committee attestations. this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot); diff --git a/yarn-project/stdlib/src/interfaces/configs.ts b/yarn-project/stdlib/src/interfaces/configs.ts index 2b94ba64f1db..d51c3b0383ea 100644 --- a/yarn-project/stdlib/src/interfaces/configs.ts +++ b/yarn-project/stdlib/src/interfaces/configs.ts @@ -69,6 +69,8 @@ export interface SequencerConfig { buildCheckpointIfEmpty?: boolean; /** Skip pushing proposed blocks to archiver (default: false) */ skipPushProposedBlocksToArchiver?: boolean; + /** Minimum number of blocks required for a checkpoint proposal (test only, defaults to undefined = no minimum) */ + minBlocksForCheckpoint?: number; } export const SequencerConfigSchema = zodFor()( @@ -103,6 +105,7 @@ export const SequencerConfigSchema = zodFor()( blockDurationMs: z.number().positive().optional(), buildCheckpointIfEmpty: z.boolean().optional(), skipPushProposedBlocksToArchiver: z.boolean().optional(), + minBlocksForCheckpoint: z.number().positive().optional(), }), ); @@ -117,7 +120,8 @@ type SequencerConfigOptionalKeys = | 'fakeThrowAfterProcessingTxCount' | 'l1PublishingTime' | 'txPublicSetupAllowList' - | 'minValidTxsPerBlock'; + | 'minValidTxsPerBlock' + | 'minBlocksForCheckpoint'; export type ResolvedSequencerConfig = Prettify< Required> & Pick