Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: Fast epoch building test #10045

Merged
merged 5 commits into from
Nov 22, 2024
Merged
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
1 change: 1 addition & 0 deletions scripts/ci/get_e2e_jobs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ allow_list=(
"e2e_cross_chain_messaging"
"e2e_crowdfunding_and_claim"
"e2e_deploy_contract"
"e2e_epochs"
"e2e_fees"
"e2e_fees_failures"
"e2e_fees_gas_estimation"
Expand Down
1 change: 1 addition & 0 deletions yarn-project/archiver/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"exports": {
".": "./dest/index.js",
"./data-retrieval": "./dest/archiver/data_retrieval.js",
"./epoch": "./dest/archiver/epoch_helpers.js",
"./test": "./dest/test/index.js"
},
"typedocOptions": {
Expand Down
16 changes: 8 additions & 8 deletions yarn-project/archiver/src/archiver/archiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { type EthAddress } from '@aztec/foundation/eth-address';
import { Fr } from '@aztec/foundation/fields';
import { type DebugLogger, createDebugLogger } from '@aztec/foundation/log';
import { RunningPromise } from '@aztec/foundation/running-promise';
import { count } from '@aztec/foundation/string';
import { Timer } from '@aztec/foundation/timer';
import { InboxAbi, RollupAbi } from '@aztec/l1-artifacts';
import { ProtocolContractAddress } from '@aztec/protocol-contracts';
Expand Down Expand Up @@ -285,12 +286,14 @@ export class Archiver implements ArchiveSource {
(await this.rollup.read.canPruneAtTime([time], { blockNumber: currentL1BlockNumber }));

if (canPrune) {
this.log.verbose(`L2 prune will occur on next submission. Rolling back to last proven block.`);
const blocksToUnwind = localPendingBlockNumber - provenBlockNumber;
this.log.verbose(
`Unwinding ${blocksToUnwind} block${blocksToUnwind > 1n ? 's' : ''} from block ${localPendingBlockNumber}`,
`L2 prune will occur on next submission. ` +
`Unwinding ${count(blocksToUnwind, 'block')} from block ${localPendingBlockNumber} ` +
`to the last proven block ${provenBlockNumber}.`,
);
await this.store.unwindBlocks(Number(localPendingBlockNumber), Number(blocksToUnwind));
this.log.verbose(`Unwound ${count(blocksToUnwind, 'block')}. New L2 block is ${await this.getBlockNumber()}.`);
// TODO(palla/reorg): Do we need to set the block synched L1 block number here?
// Seems like the next iteration should handle this.
// await this.store.setBlockSynchedL1BlockNumber(currentL1BlockNumber);
Expand Down Expand Up @@ -585,14 +588,11 @@ export class Archiver implements ArchiveSource {
if (number === 'latest') {
number = await this.store.getSynchedL2BlockNumber();
}
try {
const headers = await this.store.getBlockHeaders(number, 1);
return headers.length === 0 ? undefined : headers[0];
} catch (e) {
// If the latest is 0, then getBlockHeaders will throw an error
this.log.error(`getBlockHeader: error fetching block number: ${number}`);
if (number === 0) {
return undefined;
}
const headers = await this.store.getBlockHeaders(number, 1);
return headers.length === 0 ? undefined : headers[0];
}

public getTxEffect(txHash: TxHash) {
Expand Down
36 changes: 30 additions & 6 deletions yarn-project/archiver/src/archiver/epoch_helpers.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,54 @@
type TimeConstants = {
// REFACTOR: This file should go in a package lower in the dependency graph.

export type EpochConstants = {
l1GenesisBlock: bigint;
l1GenesisTime: bigint;
epochDuration: number;
slotDuration: number;
};

/** Returns the slot number for a given timestamp. */
export function getSlotAtTimestamp(ts: bigint, constants: Pick<TimeConstants, 'l1GenesisTime' | 'slotDuration'>) {
export function getSlotAtTimestamp(ts: bigint, constants: Pick<EpochConstants, 'l1GenesisTime' | 'slotDuration'>) {
return ts < constants.l1GenesisTime ? 0n : (ts - constants.l1GenesisTime) / BigInt(constants.slotDuration);
}

/** Returns the epoch number for a given timestamp. */
export function getEpochNumberAtTimestamp(ts: bigint, constants: TimeConstants) {
export function getEpochNumberAtTimestamp(
ts: bigint,
constants: Pick<EpochConstants, 'epochDuration' | 'slotDuration' | 'l1GenesisTime'>,
) {
return getSlotAtTimestamp(ts, constants) / BigInt(constants.epochDuration);
}

/** Returns the range of slots (inclusive) for a given epoch number. */
export function getSlotRangeForEpoch(epochNumber: bigint, constants: Pick<TimeConstants, 'epochDuration'>) {
/** Returns the range of L2 slots (inclusive) for a given epoch number. */
export function getSlotRangeForEpoch(epochNumber: bigint, constants: Pick<EpochConstants, 'epochDuration'>) {
const startSlot = epochNumber * BigInt(constants.epochDuration);
return [startSlot, startSlot + BigInt(constants.epochDuration) - 1n];
}

/** Returns the range of L1 timestamps (inclusive) for a given epoch number. */
export function getTimestampRangeForEpoch(epochNumber: bigint, constants: TimeConstants) {
export function getTimestampRangeForEpoch(
epochNumber: bigint,
constants: Pick<EpochConstants, 'l1GenesisTime' | 'slotDuration' | 'epochDuration'>,
) {
const [startSlot, endSlot] = getSlotRangeForEpoch(epochNumber, constants);
return [
constants.l1GenesisTime + startSlot * BigInt(constants.slotDuration),
constants.l1GenesisTime + endSlot * BigInt(constants.slotDuration),
];
}

/**
* Returns the range of L1 blocks (inclusive) for a given epoch number.
* @remarks This assumes no time warp has happened.
*/
export function getL1BlockRangeForEpoch(
epochNumber: bigint,
constants: Pick<EpochConstants, 'l1GenesisBlock' | 'epochDuration' | 'slotDuration'>,
) {
const epochDurationInL1Blocks = BigInt(constants.epochDuration) * BigInt(constants.slotDuration);
return [
epochNumber * epochDurationInL1Blocks + constants.l1GenesisBlock,
(epochNumber + 1n) * epochDurationInL1Blocks + constants.l1GenesisBlock - 1n,
];
}
26 changes: 15 additions & 11 deletions yarn-project/aztec-node/src/aztec-node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import { type L1ContractAddresses, createEthereumChain } from '@aztec/ethereum';
import { type ContractArtifact } from '@aztec/foundation/abi';
import { AztecAddress } from '@aztec/foundation/aztec-address';
import { padArrayEnd } from '@aztec/foundation/collection';
import { createDebugLogger } from '@aztec/foundation/log';
import { type DebugLogger, createDebugLogger } from '@aztec/foundation/log';
import { Timer } from '@aztec/foundation/timer';
import { type AztecKVStore } from '@aztec/kv-store';
import { openTmpStore } from '@aztec/kv-store/utils';
Expand All @@ -72,7 +72,7 @@ import {
createP2PClient,
} from '@aztec/p2p';
import { ProtocolContractAddress } from '@aztec/protocol-contracts';
import { GlobalVariableBuilder, SequencerClient } from '@aztec/sequencer-client';
import { GlobalVariableBuilder, type L1Publisher, SequencerClient } from '@aztec/sequencer-client';
import { PublicProcessorFactory } from '@aztec/simulator';
import { type TelemetryClient } from '@aztec/telemetry-client';
import { NoopTelemetryClient } from '@aztec/telemetry-client/noop';
Expand Down Expand Up @@ -139,10 +139,14 @@ export class AztecNodeService implements AztecNode {
*/
public static async createAndSync(
config: AztecNodeConfig,
telemetry?: TelemetryClient,
log = createDebugLogger('aztec:node'),
deps: {
telemetry?: TelemetryClient;
logger?: DebugLogger;
publisher?: L1Publisher;
} = {},
): Promise<AztecNodeService> {
telemetry ??= new NoopTelemetryClient();
const telemetry = deps.telemetry ?? new NoopTelemetryClient();
const log = deps.logger ?? createDebugLogger('aztec:node');
const ethereumChain = createEthereumChain(config.l1RpcUrl, config.l1ChainId);
//validate that the actual chain id matches that specified in configuration
if (config.l1ChainId !== ethereumChain.chainInfo.id) {
Expand Down Expand Up @@ -172,16 +176,16 @@ export class AztecNodeService implements AztecNode {
// now create the sequencer
const sequencer = config.disableValidator
? undefined
: await SequencerClient.new(
config,
: await SequencerClient.new(config, {
validatorClient,
p2pClient,
worldStateSynchronizer,
archiver,
archiver,
archiver,
contractDataSource: archiver,
l2BlockSource: archiver,
l1ToL2MessageSource: archiver,
telemetry,
);
...deps,
});

return new AztecNodeService(
config,
Expand Down
3 changes: 1 addition & 2 deletions yarn-project/aztec-node/src/bin/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#!/usr/bin/env -S node --no-warnings
import { createDebugLogger } from '@aztec/foundation/log';
import { NoopTelemetryClient } from '@aztec/telemetry-client/noop';

import http from 'http';

Expand All @@ -16,7 +15,7 @@ const logger = createDebugLogger('aztec:node');
async function createAndDeployAztecNode() {
const aztecNodeConfig: AztecNodeConfig = { ...getConfigEnvVars() };

return await AztecNodeService.createAndSync(aztecNodeConfig, new NoopTelemetryClient());
return await AztecNodeService.createAndSync(aztecNodeConfig);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/aztec/src/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export async function createSandbox(config: Partial<SandboxConfig> = {}) {
*/
export async function createAztecNode(config: Partial<AztecNodeConfig> = {}, telemetryClient?: TelemetryClient) {
const aztecNodeConfig: AztecNodeConfig = { ...getConfigEnvVars(), ...config };
const node = await AztecNodeService.createAndSync(aztecNodeConfig, telemetryClient);
const node = await AztecNodeService.createAndSync(aztecNodeConfig, { telemetry: telemetryClient });
return node;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { RunningPromise } from '@aztec/foundation/running-promise';
import { type L2Block } from '../l2_block.js';
import { type L2BlockId, type L2BlockSource, type L2Tips } from '../l2_block_source.js';

/** Creates a stream of events for new blocks, chain tips updates, and reorgs, out of polling an archiver. */
/** Creates a stream of events for new blocks, chain tips updates, and reorgs, out of polling an archiver or a node. */
export class L2BlockStream {
private readonly runningPromise: RunningPromise;

Expand Down Expand Up @@ -119,7 +119,12 @@ export class L2BlockStream {
const sourceBlockHash =
args.sourceCache.find(id => id.number === blockNumber && id.hash)?.hash ??
(await this.l2BlockSource.getBlockHeader(blockNumber).then(h => h?.hash().toString()));
this.log.debug(`Comparing block hashes for block ${blockNumber}`, { localBlockHash, sourceBlockHash });
this.log.debug(`Comparing block hashes for block ${blockNumber}`, {
localBlockHash,
sourceBlockHash,
sourceCacheNumber: args.sourceCache[0]?.number,
sourceCacheHash: args.sourceCache[0]?.hash,
});
return localBlockHash === sourceBlockHash;
}

Expand Down
1 change: 1 addition & 0 deletions yarn-project/end-to-end/scripts/e2e_test_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ tests:
e2e_devnet_smoke: {}
docs_examples:
use_compose: true
e2e_epochs: {}
e2e_escrow_contract: {}
e2e_fees_account_init:
test_path: 'e2e_fees/account_init.test.ts'
Expand Down
146 changes: 146 additions & 0 deletions yarn-project/end-to-end/src/e2e_epochs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { type EpochConstants, getTimestampRangeForEpoch } from '@aztec/archiver/epoch';
import { type DebugLogger, retryUntil } from '@aztec/aztec.js';
import { RollupContract } from '@aztec/ethereum/contracts';
import { type Delayer, waitUntilL1Timestamp } from '@aztec/ethereum/test';

import { type PublicClient } from 'viem';

import { type EndToEndContext, setup } from './fixtures/utils.js';

// Tests building of epochs using fast block times and short epochs.
// Spawns an aztec node and a prover node with fake proofs.
// Sequencer is allowed to build empty blocks.
describe('e2e_epochs', () => {
let context: EndToEndContext;
let l1Client: PublicClient;
let rollup: RollupContract;
let constants: EpochConstants;
let logger: DebugLogger;
let proverDelayer: Delayer;
let sequencerDelayer: Delayer;

let l2BlockNumber: number = 0;
let l2ProvenBlockNumber: number = 0;
let l1BlockNumber: number;
let handle: NodeJS.Timeout;

const EPOCH_DURATION = 4;
const L1_BLOCK_TIME = 5;
const L2_SLOT_DURATION_IN_L1_BLOCKS = 2;

beforeAll(async () => {
// Set up system without any account nor protocol contracts
// and with faster block times and shorter epochs.
context = await setup(0, {
assumeProvenThrough: undefined,
skipProtocolContracts: true,
salt: 1,
aztecEpochDuration: EPOCH_DURATION,
aztecSlotDuration: L1_BLOCK_TIME * L2_SLOT_DURATION_IN_L1_BLOCKS,
ethereumSlotDuration: L1_BLOCK_TIME,
aztecEpochProofClaimWindowInL2Slots: EPOCH_DURATION / 2,
minTxsPerBlock: 0,
realProofs: false,
startProverNode: true,
});

logger = context.logger;
l1Client = context.deployL1ContractsValues.publicClient;
rollup = RollupContract.getFromConfig(context.config);

// Loop that tracks L1 and L2 block numbers and logs whenever there's a new one.
// We could refactor this out to an utility if we want to use this in other tests.
handle = setInterval(async () => {
const newL1BlockNumber = Number(await l1Client.getBlockNumber({ cacheTime: 0 }));
if (l1BlockNumber === newL1BlockNumber) {
return;
}
const block = await l1Client.getBlock({ blockNumber: BigInt(newL1BlockNumber), includeTransactions: false });
const timestamp = block.timestamp;
l1BlockNumber = newL1BlockNumber;

let msg = `L1 block ${newL1BlockNumber} mined at ${timestamp}`;

const newL2BlockNumber = Number(await rollup.getBlockNumber());
if (l2BlockNumber !== newL2BlockNumber) {
const epochNumber = await rollup.getEpochNumber(BigInt(newL2BlockNumber));
msg += ` with new L2 block ${newL2BlockNumber} for epoch ${epochNumber}`;
l2BlockNumber = newL2BlockNumber;
}

const newL2ProvenBlockNumber = Number(await rollup.getProvenBlockNumber());
if (l2ProvenBlockNumber !== newL2ProvenBlockNumber) {
const epochNumber = await rollup.getEpochNumber(BigInt(newL2ProvenBlockNumber));
msg += ` with proof up to L2 block ${newL2ProvenBlockNumber} for epoch ${epochNumber}`;
l2ProvenBlockNumber = newL2ProvenBlockNumber;
}
logger.info(msg);
}, 200);

// The "as any" cast sucks, but it saves us from having to define test-only types for the provernode
// and sequencer that are exactly like the real ones but with the publisher exposed. We should
// do it if we see the this pattern popping up in more places.
proverDelayer = (context.proverNode as any).publisher.delayer;
sequencerDelayer = (context.sequencer as any).sequencer.publisher.delayer;
expect(proverDelayer).toBeDefined();
expect(sequencerDelayer).toBeDefined();

// Constants used for time calculation
constants = {
epochDuration: EPOCH_DURATION,
slotDuration: L1_BLOCK_TIME * L2_SLOT_DURATION_IN_L1_BLOCKS,
l1GenesisBlock: await rollup.getL1StartBlock(),
l1GenesisTime: await rollup.getL1GenesisTime(),
};

logger.info(`L2 genesis at L1 block ${constants.l1GenesisBlock} (timestamp ${constants.l1GenesisTime})`);
});

afterAll(async () => {
clearInterval(handle);
await context.teardown();
});

/** Waits until the epoch begins (ie until the immediately previous L1 block is mined). */
const waitUntilEpochStarts = async (epoch: number) => {
const [start] = getTimestampRangeForEpoch(BigInt(epoch), constants);
logger.info(`Waiting until L1 timestamp ${start} is reached as the start of epoch ${epoch}`);
await waitUntilL1Timestamp(l1Client, start - BigInt(L1_BLOCK_TIME));
return start;
};

/** Waits until the given L2 block number is mined. */
const waitUntilL2BlockNumber = async (target: number) => {
await retryUntil(() => Promise.resolve(target === l2BlockNumber), `Wait until L2 block ${l2BlockNumber}`, 60, 0.1);
};

it('does not allow submitting proof after epoch end', async () => {
await waitUntilEpochStarts(1);
const blockNumberAtEndOfEpoch0 = Number(await rollup.getBlockNumber());
logger.info(`Starting epoch 1 after L2 block ${blockNumberAtEndOfEpoch0}`);

// Hold off prover tx until end of next epoch!
const [epoch2Start] = getTimestampRangeForEpoch(2n, constants);
proverDelayer.pauseNextTxUntilTimestamp(epoch2Start);
logger.info(`Delayed prover tx until epoch 2 starts at ${epoch2Start}`);

// Wait until the last block of epoch 1 is published and then hold off the sequencer
await waitUntilL2BlockNumber(blockNumberAtEndOfEpoch0 + EPOCH_DURATION);
sequencerDelayer.pauseNextTxUntilTimestamp(epoch2Start + BigInt(L1_BLOCK_TIME));

// Next sequencer to publish a block should trigger a rollback to block 1
await waitUntilL1Timestamp(l1Client, epoch2Start + BigInt(L1_BLOCK_TIME));
expect(await rollup.getBlockNumber()).toEqual(1n);
expect(await rollup.getSlotNumber()).toEqual(8n);

// The prover tx should have been rejected, and mined strictly before the one that triggered the rollback
const lastProverTxHash = proverDelayer.getTxs().at(-1);
const lastProverTxReceipt = await l1Client.getTransactionReceipt({ hash: lastProverTxHash! });
expect(lastProverTxReceipt.status).toEqual('reverted');

const lastL2BlockTxHash = sequencerDelayer.getTxs().at(-1);
const lastL2BlockTxReceipt = await l1Client.getTransactionReceipt({ hash: lastL2BlockTxHash! });
expect(lastL2BlockTxReceipt.status).toEqual('success');
expect(lastL2BlockTxReceipt.blockNumber).toBeGreaterThan(lastProverTxReceipt!.blockNumber);
});
});
2 changes: 1 addition & 1 deletion yarn-project/end-to-end/src/e2e_l1_with_wall_time.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe('e2e_l1_with_wall_time', () => {

({ teardown, logger, pxe } = await setup(0, {
initialValidators,
l1BlockTime: ethereumSlotDuration,
ethereumSlotDuration,
salt: 420,
}));
});
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class P2PNetworkTest {

this.snapshotManager = createSnapshotManager(`e2e_p2p_network/${testName}`, process.env.E2E_DATA_PATH, {
...initialValidatorConfig,
l1BlockTime: l1ContractsConfig.ethereumSlotDuration,
ethereumSlotDuration: l1ContractsConfig.ethereumSlotDuration,
salt: 420,
initialValidators,
metricsPort: metricsPort,
Expand Down
Loading
Loading