Skip to content

Commit

Permalink
chore: Fast epoch building test
Browse files Browse the repository at this point in the history
  • Loading branch information
spalladino committed Nov 20, 2024
1 parent 375482f commit e000b09
Show file tree
Hide file tree
Showing 34 changed files with 802 additions and 187 deletions.
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 = 3;
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

0 comments on commit e000b09

Please sign in to comment.