From 194a788b575491bcd544bf5d6cba0e4c9ba8abb4 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Tue, 19 Nov 2024 18:57:23 -0300 Subject: [PATCH 1/5] chore: Fast epoch building test --- yarn-project/archiver/package.json | 1 + .../archiver/src/archiver/archiver.ts | 16 +- .../archiver/src/archiver/epoch_helpers.ts | 36 ++++- .../aztec-node/src/aztec-node/server.ts | 26 +-- yarn-project/aztec-node/src/bin/index.ts | 3 +- yarn-project/aztec/src/sandbox.ts | 2 +- .../l2_block_downloader/l2_block_stream.ts | 9 +- .../end-to-end/scripts/e2e_test_config.yml | 1 + .../end-to-end/src/e2e_epochs.test.ts | 146 +++++++++++++++++ .../src/e2e_l1_with_wall_time.test.ts | 2 +- .../end-to-end/src/e2e_p2p/p2p_network.ts | 2 +- .../end-to-end/src/e2e_synching.test.ts | 11 +- .../end-to-end/src/fixtures/setup_p2p_test.ts | 9 +- .../src/fixtures/snapshot_manager.ts | 64 ++------ yarn-project/end-to-end/src/fixtures/utils.ts | 139 +++++++++------- yarn-project/ethereum/package.json | 8 +- .../scripts/anvil_kill_wrapper.sh | 0 yarn-project/ethereum/src/contracts/index.ts | 1 + yarn-project/ethereum/src/contracts/rollup.ts | 60 +++++++ yarn-project/ethereum/src/index.ts | 1 + yarn-project/ethereum/src/test/index.ts | 2 + .../ethereum/src/test/start_anvil.test.ts | 16 ++ yarn-project/ethereum/src/test/start_anvil.ts | 39 +++++ .../ethereum/src/test/tx_delayer.test.ts | 97 +++++++++++ yarn-project/ethereum/src/test/tx_delayer.ts | 150 ++++++++++++++++++ yarn-project/ethereum/src/types.ts | 22 +++ yarn-project/foundation/src/string/index.ts | 8 + yarn-project/prover-node/src/factory.ts | 3 +- yarn-project/prover-node/src/prover-node.ts | 14 +- .../src/client/sequencer-client.ts | 28 +++- .../sequencer-client/src/publisher/index.ts | 1 + .../src/publisher/l1-publisher.ts | 38 +++-- .../src/publisher/test-l1-publisher.ts | 20 +++ yarn-project/yarn.lock | 14 ++ 34 files changed, 802 insertions(+), 187 deletions(-) create mode 100644 yarn-project/end-to-end/src/e2e_epochs.test.ts rename yarn-project/{end-to-end => ethereum}/scripts/anvil_kill_wrapper.sh (100%) create mode 100644 yarn-project/ethereum/src/contracts/index.ts create mode 100644 yarn-project/ethereum/src/contracts/rollup.ts create mode 100644 yarn-project/ethereum/src/test/index.ts create mode 100644 yarn-project/ethereum/src/test/start_anvil.test.ts create mode 100644 yarn-project/ethereum/src/test/start_anvil.ts create mode 100644 yarn-project/ethereum/src/test/tx_delayer.test.ts create mode 100644 yarn-project/ethereum/src/test/tx_delayer.ts create mode 100644 yarn-project/ethereum/src/types.ts create mode 100644 yarn-project/sequencer-client/src/publisher/test-l1-publisher.ts diff --git a/yarn-project/archiver/package.json b/yarn-project/archiver/package.json index 8ce9df4a632..3366d4b3f1b 100644 --- a/yarn-project/archiver/package.json +++ b/yarn-project/archiver/package.json @@ -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": { diff --git a/yarn-project/archiver/src/archiver/archiver.ts b/yarn-project/archiver/src/archiver/archiver.ts index cd1c78cb3b2..1cb3d874d7e 100644 --- a/yarn-project/archiver/src/archiver/archiver.ts +++ b/yarn-project/archiver/src/archiver/archiver.ts @@ -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'; @@ -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); @@ -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) { diff --git a/yarn-project/archiver/src/archiver/epoch_helpers.ts b/yarn-project/archiver/src/archiver/epoch_helpers.ts index f84bafeee38..55fe28e2f0e 100644 --- a/yarn-project/archiver/src/archiver/epoch_helpers.ts +++ b/yarn-project/archiver/src/archiver/epoch_helpers.ts @@ -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) { +export function getSlotAtTimestamp(ts: bigint, constants: Pick) { 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, +) { 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) { +/** Returns the range of L2 slots (inclusive) for a given epoch number. */ +export function getSlotRangeForEpoch(epochNumber: bigint, constants: Pick) { 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, +) { 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, +) { + const epochDurationInL1Blocks = BigInt(constants.epochDuration) * BigInt(constants.slotDuration); + return [ + epochNumber * epochDurationInL1Blocks + constants.l1GenesisBlock, + (epochNumber + 1n) * epochDurationInL1Blocks + constants.l1GenesisBlock - 1n, + ]; +} diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 13d1051a623..279638eb734 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -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'; @@ -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'; @@ -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 { - 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) { @@ -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, diff --git a/yarn-project/aztec-node/src/bin/index.ts b/yarn-project/aztec-node/src/bin/index.ts index 41aba729aeb..e1688b79198 100644 --- a/yarn-project/aztec-node/src/bin/index.ts +++ b/yarn-project/aztec-node/src/bin/index.ts @@ -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'; @@ -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); } /** diff --git a/yarn-project/aztec/src/sandbox.ts b/yarn-project/aztec/src/sandbox.ts index f1afd126b82..cd2a799df92 100644 --- a/yarn-project/aztec/src/sandbox.ts +++ b/yarn-project/aztec/src/sandbox.ts @@ -171,7 +171,7 @@ export async function createSandbox(config: Partial = {}) { */ export async function createAztecNode(config: Partial = {}, telemetryClient?: TelemetryClient) { const aztecNodeConfig: AztecNodeConfig = { ...getConfigEnvVars(), ...config }; - const node = await AztecNodeService.createAndSync(aztecNodeConfig, telemetryClient); + const node = await AztecNodeService.createAndSync(aztecNodeConfig, { telemetry: telemetryClient }); return node; } diff --git a/yarn-project/circuit-types/src/l2_block_downloader/l2_block_stream.ts b/yarn-project/circuit-types/src/l2_block_downloader/l2_block_stream.ts index 50608b6c0ea..41eb4581346 100644 --- a/yarn-project/circuit-types/src/l2_block_downloader/l2_block_stream.ts +++ b/yarn-project/circuit-types/src/l2_block_downloader/l2_block_stream.ts @@ -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; @@ -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; } diff --git a/yarn-project/end-to-end/scripts/e2e_test_config.yml b/yarn-project/end-to-end/scripts/e2e_test_config.yml index 85d30516ee7..f3c8e91ddf6 100644 --- a/yarn-project/end-to-end/scripts/e2e_test_config.yml +++ b/yarn-project/end-to-end/scripts/e2e_test_config.yml @@ -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' diff --git a/yarn-project/end-to-end/src/e2e_epochs.test.ts b/yarn-project/end-to-end/src/e2e_epochs.test.ts new file mode 100644 index 00000000000..b0d87ad741c --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_epochs.test.ts @@ -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); + }); +}); diff --git a/yarn-project/end-to-end/src/e2e_l1_with_wall_time.test.ts b/yarn-project/end-to-end/src/e2e_l1_with_wall_time.test.ts index 15abecfb446..01a6d9f96ea 100644 --- a/yarn-project/end-to-end/src/e2e_l1_with_wall_time.test.ts +++ b/yarn-project/end-to-end/src/e2e_l1_with_wall_time.test.ts @@ -20,7 +20,7 @@ describe('e2e_l1_with_wall_time', () => { ({ teardown, logger, pxe } = await setup(0, { initialValidators, - l1BlockTime: ethereumSlotDuration, + ethereumSlotDuration, salt: 420, })); }); diff --git a/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts b/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts index 529e2776294..8a2c0dbaab8 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts @@ -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, diff --git a/yarn-project/end-to-end/src/e2e_synching.test.ts b/yarn-project/end-to-end/src/e2e_synching.test.ts index 743c780bda8..2a81a74257e 100644 --- a/yarn-project/end-to-end/src/e2e_synching.test.ts +++ b/yarn-project/end-to-end/src/e2e_synching.test.ts @@ -419,10 +419,7 @@ describe('e2e_synching', () => { async (opts: Partial, variant: TestVariant) => { // All the blocks have been "re-played" and we are now to simply get a new node up to speed const timer = new Timer(); - const freshNode = await AztecNodeService.createAndSync( - { ...opts.config!, disableValidator: true }, - new NoopTelemetryClient(), - ); + const freshNode = await AztecNodeService.createAndSync({ ...opts.config!, disableValidator: true }); const syncTime = timer.s(); const blockNumber = await freshNode.getBlockNumber(); @@ -468,7 +465,7 @@ describe('e2e_synching', () => { ); await watcher.start(); - const aztecNode = await AztecNodeService.createAndSync(opts.config!, new NoopTelemetryClient()); + const aztecNode = await AztecNodeService.createAndSync(opts.config!); const sequencer = aztecNode.getSequencer(); const { pxe } = await setupPXEService(aztecNode!); @@ -579,7 +576,7 @@ describe('e2e_synching', () => { const pendingBlockNumber = await rollup.read.getPendingBlockNumber(); await rollup.write.setAssumeProvenThroughBlockNumber([pendingBlockNumber - BigInt(variant.blockCount) / 2n]); - const aztecNode = await AztecNodeService.createAndSync(opts.config!, new NoopTelemetryClient()); + const aztecNode = await AztecNodeService.createAndSync(opts.config!); const sequencer = aztecNode.getSequencer(); const blockBeforePrune = await aztecNode.getBlockNumber(); @@ -660,7 +657,7 @@ describe('e2e_synching', () => { await watcher.start(); // The sync here could likely be avoided by using the node we just synched. - const aztecNode = await AztecNodeService.createAndSync(opts.config!, new NoopTelemetryClient()); + const aztecNode = await AztecNodeService.createAndSync(opts.config!); const sequencer = aztecNode.getSequencer(); const { pxe } = await setupPXEService(aztecNode!); diff --git a/yarn-project/end-to-end/src/fixtures/setup_p2p_test.ts b/yarn-project/end-to-end/src/fixtures/setup_p2p_test.ts index 46994fbfd7f..6f81b43d714 100644 --- a/yarn-project/end-to-end/src/fixtures/setup_p2p_test.ts +++ b/yarn-project/end-to-end/src/fixtures/setup_p2p_test.ts @@ -83,11 +83,10 @@ export async function createNode( const telemetryClient = await getEndToEndTestTelemetryClient(metricsPort, /*serviceName*/ `node:${tcpPort}`); - return await AztecNodeService.createAndSync( - validatorConfig, - telemetryClient, - createDebugLogger(`aztec:node-${tcpPort}`), - ); + return await AztecNodeService.createAndSync(validatorConfig, { + telemetry: telemetryClient, + logger: createDebugLogger(`aztec:node-${tcpPort}`), + }); } export async function createValidatorConfig( diff --git a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts index 36504fac84d..9479ec40744 100644 --- a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts +++ b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts @@ -1,10 +1,8 @@ import { SchnorrAccountContractArtifact, getSchnorrAccount } from '@aztec/accounts/schnorr'; -import { type Archiver, createArchiver } from '@aztec/archiver'; import { type AztecNodeConfig, AztecNodeService, getConfigEnvVars } from '@aztec/aztec-node'; import { AnvilTestWatcher, type AztecAddress, - type AztecNode, BatchCall, CheatCodes, type CompleteAddress, @@ -18,18 +16,17 @@ import { } from '@aztec/aztec.js'; import { deployInstance, registerContractClass } from '@aztec/aztec.js/deployment'; import { type DeployL1ContractsArgs, createL1Clients, getL1ContractsConfigEnvVars, l1Artifacts } from '@aztec/ethereum'; +import { startAnvil } from '@aztec/ethereum/test'; import { asyncMap } from '@aztec/foundation/async-map'; import { type Logger, createDebugLogger } from '@aztec/foundation/log'; import { resolver, reviver } from '@aztec/foundation/serialize'; -import { type ProverNode, type ProverNodeConfig, createProverNode } from '@aztec/prover-node'; +import { type ProverNode } from '@aztec/prover-node'; import { type PXEService, createPXEService, getPXEServiceConfig } from '@aztec/pxe'; -import { NoopTelemetryClient } from '@aztec/telemetry-client/noop'; import { createAndStartTelemetryClient, getConfigEnvVars as getTelemetryConfig } from '@aztec/telemetry-client/start'; -import { type Anvil, createAnvil } from '@viem/anvil'; +import { type Anvil } from '@viem/anvil'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; import { copySync, removeSync } from 'fs-extra/esm'; -import getPort from 'get-port'; import { join } from 'path'; import { type Hex, getContract } from 'viem'; import { mnemonicToAccount } from 'viem/accounts'; @@ -38,7 +35,7 @@ import { MNEMONIC } from './fixtures.js'; import { getACVMConfig } from './get_acvm_config.js'; import { getBBConfig } from './get_bb_config.js'; import { setupL1Contracts } from './setup_l1_contracts.js'; -import { type SetupOptions, getPrivateKeyFromIndex, startAnvil } from './utils.js'; +import { type SetupOptions, createAndSyncProverNode, getPrivateKeyFromIndex } from './utils.js'; import { getEndToEndTestTelemetryClient } from './with_telemetry_utils.js'; export type SubsystemsContext = { @@ -252,47 +249,6 @@ async function teardown(context: SubsystemsContext | undefined) { await context.watcher.stop(); } -async function createAndSyncProverNode( - proverNodePrivateKey: `0x${string}`, - aztecNodeConfig: AztecNodeConfig, - aztecNode: AztecNode, -) { - // Disable stopping the aztec node as the prover coordination test will kill it otherwise - // This is only required when stopping the prover node for testing - const aztecNodeWithoutStop = { - addEpochProofQuote: aztecNode.addEpochProofQuote.bind(aztecNode), - getTxByHash: aztecNode.getTxByHash.bind(aztecNode), - stop: () => Promise.resolve(), - }; - - // Creating temp store and archiver for simulated prover node - const archiverConfig = { ...aztecNodeConfig, dataDirectory: undefined }; - const archiver = await createArchiver(archiverConfig, new NoopTelemetryClient(), { blockUntilSync: true }); - - // Prover node config is for simulated proofs - const proverConfig: ProverNodeConfig = { - ...aztecNodeConfig, - proverCoordinationNodeUrl: undefined, - dataDirectory: undefined, - proverId: new Fr(42), - realProofs: false, - proverAgentConcurrency: 2, - publisherPrivateKey: proverNodePrivateKey, - proverNodeMaxPendingJobs: 10, - proverNodePollingIntervalMs: 200, - quoteProviderBasisPointFee: 100, - quoteProviderBondAmount: 1000n, - proverMinimumEscrowAmount: 1000n, - proverTargetEscrowAmount: 2000n, - }; - const proverNode = await createProverNode(proverConfig, { - aztecNodeTxProvider: aztecNodeWithoutStop, - archiver: archiver as Archiver, - }); - await proverNode.start(); - return proverNode; -} - /** * Initializes a fresh set of subsystems. * If given a statePath, the state will be written to the path. @@ -315,7 +271,7 @@ async function setupFromFresh( // Start anvil. We go via a wrapper script to ensure if the parent dies, anvil dies. logger.verbose('Starting anvil...'); - const res = await startAnvil(opts.l1BlockTime); + const res = await startAnvil(opts.ethereumSlotDuration); const anvil = res.anvil; aztecNodeConfig.l1RpcUrl = res.rpcUrl; @@ -391,7 +347,7 @@ async function setupFromFresh( const telemetry = await getEndToEndTestTelemetryClient(opts.metricsPort, /*serviceName*/ 'basenode'); logger.verbose('Creating and synching an aztec node...'); - const aztecNode = await AztecNodeService.createAndSync(aztecNodeConfig, telemetry); + const aztecNode = await AztecNodeService.createAndSync(aztecNodeConfig, { telemetry }); let proverNode: ProverNode | undefined = undefined; if (opts.startProverNode) { @@ -443,10 +399,8 @@ async function setupFromState(statePath: string, logger: Logger): Promise = {}, chain: Chain = foundry, ) => { const l1Data = await deployL1Contracts(l1RpcUrl, account, chain, logger, { @@ -104,6 +109,7 @@ export const setupL1Contracts = async ( initialValidators: args.initialValidators, assumeProvenThrough: args.assumeProvenThrough, ...getL1ContractsConfigEnvVars(), + ...args, }); return l1Data; @@ -213,7 +219,7 @@ async function setupWithRemoteEnvironment( return { aztecNode, sequencer: undefined, - prover: undefined, + proverNode: undefined, pxe: pxeClient, deployL1ContractsValues, accounts: await pxeClient!.getRegisteredAccounts(), @@ -241,8 +247,6 @@ export type SetupOptions = { salt?: number; /** An initial set of validators */ initialValidators?: EthAddress[]; - /** Anvil block time (interval) */ - l1BlockTime?: number; /** Anvil Start time */ l1StartTime?: number; /** The anvil time where we should at the earliest be seeing L2 blocks */ @@ -259,6 +263,8 @@ export type SetupOptions = { export type EndToEndContext = { /** The Aztec Node service or client a connected to it. */ aztecNode: AztecNode; + /** The prover node service (only set if startProverNode is true) */ + proverNode: ProverNode | undefined; /** A client to the sequencer service (undefined if connected to remote environment) */ sequencer: SequencerClient | undefined; /** The Private eXecution Environment (PXE). */ @@ -310,7 +316,7 @@ export async function setup( ); } - const res = await startAnvil(opts.l1BlockTime); + const res = await startAnvil(opts.ethereumSlotDuration); anvil = res.anvil; config.l1RpcUrl = res.rpcUrl; } @@ -355,14 +361,7 @@ export async function setup( } const deployL1ContractsValues = - opts.deployL1ContractsValues ?? - (await setupL1Contracts( - config.l1RpcUrl, - publisherHdAccount!, - logger, - { salt: opts.salt, initialValidators: opts.initialValidators, assumeProvenThrough: opts.assumeProvenThrough }, - chain, - )); + opts.deployL1ContractsValues ?? (await setupL1Contracts(config.l1RpcUrl, publisherHdAccount!, logger, opts, chain)); config.l1Contracts = deployL1ContractsValues.l1ContractAddresses; @@ -419,11 +418,19 @@ export async function setup( config.l1PublishRetryIntervalMS = 100; const telemetry = await telemetryPromise; - const aztecNode = await AztecNodeService.createAndSync(config, telemetry); + const publisher = new TestL1Publisher(config, telemetry); + const aztecNode = await AztecNodeService.createAndSync(config, { telemetry, publisher }); const sequencer = aztecNode.getSequencer(); - logger.verbose('Creating a pxe...'); + let proverNode: ProverNode | undefined = undefined; + if (opts.startProverNode) { + logger.verbose('Creating and syncing a simulated prover node...'); + const proverNodePrivateKey = getPrivateKeyFromIndex(2); + const proverNodePrivateKeyHex: Hex = `0x${proverNodePrivateKey!.toString('hex')}`; + proverNode = await createAndSyncProverNode(proverNodePrivateKeyHex, config, aztecNode); + } + logger.verbose('Creating a pxe...'); const { pxe } = await setupPXEService(aztecNode!, pxeOpts, logger); if (!config.skipProtocolContracts) { @@ -456,6 +463,7 @@ export async function setup( return { aztecNode, + proverNode, pxe, deployL1ContractsValues, config, @@ -479,37 +487,6 @@ export function getL1WalletClient(rpcUrl: string, index: number) { }); } -/** - * Ensures there's a running Anvil instance and returns the RPC URL. - * @returns - */ -export async function startAnvil(l1BlockTime?: number): Promise<{ anvil: Anvil; rpcUrl: string }> { - let rpcUrl: string | undefined = undefined; - - // Start anvil. - // We go via a wrapper script to ensure if the parent dies, anvil dies. - const anvil = await retry( - async () => { - const ethereumHostPort = await getPort(); - rpcUrl = `http://127.0.0.1:${ethereumHostPort}`; - const anvil = createAnvil({ - anvilBinary: './scripts/anvil_kill_wrapper.sh', - port: ethereumHostPort, - blockTime: l1BlockTime, - }); - await anvil.start(); - return anvil; - }, - 'Start anvil', - makeBackoff([5, 5, 5]), - ); - - if (!rpcUrl) { - throw new Error('Failed to start anvil'); - } - - return { anvil, rpcUrl }; -} /** * Registers the contract class used for test accounts and publicly deploys the instances requested. * Use this when you need to make a public call to an account contract, such as for requesting a public authwit. @@ -699,3 +676,49 @@ export async function waitForProvenChain(node: AztecNode, targetBlock?: number, intervalSec, ); } + +export async function createAndSyncProverNode( + proverNodePrivateKey: `0x${string}`, + aztecNodeConfig: AztecNodeConfig, + aztecNode: AztecNode, +) { + // Disable stopping the aztec node as the prover coordination test will kill it otherwise + // This is only required when stopping the prover node for testing + const aztecNodeWithoutStop = { + addEpochProofQuote: aztecNode.addEpochProofQuote.bind(aztecNode), + getTxByHash: aztecNode.getTxByHash.bind(aztecNode), + stop: () => Promise.resolve(), + }; + + // Creating temp store and archiver for simulated prover node + const archiverConfig = { ...aztecNodeConfig, dataDirectory: undefined }; + const archiver = await createArchiver(archiverConfig, new NoopTelemetryClient(), { blockUntilSync: true }); + + // Prover node config is for simulated proofs + const proverConfig: ProverNodeConfig = { + ...aztecNodeConfig, + proverCoordinationNodeUrl: undefined, + dataDirectory: undefined, + proverId: new Fr(42), + realProofs: false, + proverAgentConcurrency: 2, + publisherPrivateKey: proverNodePrivateKey, + proverNodeMaxPendingJobs: 10, + proverNodePollingIntervalMs: 200, + quoteProviderBasisPointFee: 100, + quoteProviderBondAmount: 1000n, + proverMinimumEscrowAmount: 1000n, + proverTargetEscrowAmount: 2000n, + }; + + // Use testing l1 publisher + const publisher = new TestL1Publisher(proverConfig, new NoopTelemetryClient()); + + const proverNode = await createProverNode(proverConfig, { + aztecNodeTxProvider: aztecNodeWithoutStop, + archiver: archiver as Archiver, + publisher, + }); + await proverNode.start(); + return proverNode; +} diff --git a/yarn-project/ethereum/package.json b/yarn-project/ethereum/package.json index afee02eac0a..887ad01645d 100644 --- a/yarn-project/ethereum/package.json +++ b/yarn-project/ethereum/package.json @@ -2,7 +2,11 @@ "name": "@aztec/ethereum", "version": "0.1.0", "type": "module", - "exports": "./dest/index.js", + "exports": { + ".": "./dest/index.js", + "./test": "./dest/test/index.js", + "./contracts": "./dest/contracts/index.js" + }, "typedocOptions": { "entryPoints": [ "./src/index.ts" @@ -26,7 +30,9 @@ "dependencies": { "@aztec/foundation": "workspace:^", "@aztec/l1-artifacts": "workspace:^", + "@viem/anvil": "^0.0.10", "dotenv": "^16.0.3", + "get-port": "^7.1.0", "tslib": "^2.4.0", "viem": "^2.7.15", "zod": "^3.23.8" diff --git a/yarn-project/end-to-end/scripts/anvil_kill_wrapper.sh b/yarn-project/ethereum/scripts/anvil_kill_wrapper.sh similarity index 100% rename from yarn-project/end-to-end/scripts/anvil_kill_wrapper.sh rename to yarn-project/ethereum/scripts/anvil_kill_wrapper.sh diff --git a/yarn-project/ethereum/src/contracts/index.ts b/yarn-project/ethereum/src/contracts/index.ts new file mode 100644 index 00000000000..f35e118a5a1 --- /dev/null +++ b/yarn-project/ethereum/src/contracts/index.ts @@ -0,0 +1 @@ +export * from './rollup.js'; diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts new file mode 100644 index 00000000000..98e3ac29fe2 --- /dev/null +++ b/yarn-project/ethereum/src/contracts/rollup.ts @@ -0,0 +1,60 @@ +import { memoize } from '@aztec/foundation/decorators'; +import { RollupAbi } from '@aztec/l1-artifacts'; + +import { + type Chain, + type GetContractReturnType, + type Hex, + type HttpTransport, + type PublicClient, + createPublicClient, + getContract, + http, +} from 'viem'; + +import { createEthereumChain } from '../ethereum_chain.js'; +import { type L1ReaderConfig } from '../l1_reader.js'; + +export class RollupContract { + private readonly rollup: GetContractReturnType>; + + constructor(client: PublicClient, address: Hex) { + this.rollup = getContract({ address, abi: RollupAbi, client }); + } + + @memoize + getL1StartBlock() { + return this.rollup.read.L1_BLOCK_AT_GENESIS(); + } + + @memoize + getL1GenesisTime() { + return this.rollup.read.GENESIS_TIME(); + } + + getBlockNumber() { + return this.rollup.read.getPendingBlockNumber(); + } + + getProvenBlockNumber() { + return this.rollup.read.getProvenBlockNumber(); + } + + getSlotNumber() { + return this.rollup.read.getCurrentSlot(); + } + + async getEpochNumber(blockNumber?: bigint) { + blockNumber ??= await this.getBlockNumber(); + return this.rollup.read.getEpochForBlock([BigInt(blockNumber)]); + } + + static getFromConfig(config: L1ReaderConfig) { + const client = createPublicClient({ + transport: http(config.l1RpcUrl), + chain: createEthereumChain(config.l1RpcUrl, config.l1ChainId).chainInfo, + }); + const address = config.l1Contracts.rollupAddress.toString(); + return new RollupContract(client, address); + } +} diff --git a/yarn-project/ethereum/src/index.ts b/yarn-project/ethereum/src/index.ts index 80bc84bcbc1..30a990db651 100644 --- a/yarn-project/ethereum/src/index.ts +++ b/yarn-project/ethereum/src/index.ts @@ -5,3 +5,4 @@ export * from './l1_reader.js'; export * from './ethereum_chain.js'; export * from './utils.js'; export * from './config.js'; +export * from './types.js'; diff --git a/yarn-project/ethereum/src/test/index.ts b/yarn-project/ethereum/src/test/index.ts new file mode 100644 index 00000000000..e6e7d745ad6 --- /dev/null +++ b/yarn-project/ethereum/src/test/index.ts @@ -0,0 +1,2 @@ +export * from './start_anvil.js'; +export * from './tx_delayer.js'; diff --git a/yarn-project/ethereum/src/test/start_anvil.test.ts b/yarn-project/ethereum/src/test/start_anvil.test.ts new file mode 100644 index 00000000000..8efdff4452b --- /dev/null +++ b/yarn-project/ethereum/src/test/start_anvil.test.ts @@ -0,0 +1,16 @@ +import { createPublicClient, http } from 'viem'; + +import { startAnvil } from './start_anvil.js'; + +describe('start_anvil', () => { + it('starts anvil on a free port', async () => { + const { anvil, rpcUrl } = await startAnvil(); + const publicClient = createPublicClient({ transport: http(rpcUrl) }); + const chainId = await publicClient.getChainId(); + expect(chainId).toEqual(31337); + expect(anvil.status).toEqual('listening'); + + await anvil.stop(); + expect(anvil.status).toEqual('idle'); + }); +}); diff --git a/yarn-project/ethereum/src/test/start_anvil.ts b/yarn-project/ethereum/src/test/start_anvil.ts new file mode 100644 index 00000000000..b8c287681b3 --- /dev/null +++ b/yarn-project/ethereum/src/test/start_anvil.ts @@ -0,0 +1,39 @@ +import { makeBackoff, retry } from '@aztec/foundation/retry'; +import { fileURLToPath } from '@aztec/foundation/url'; + +import { type Anvil, createAnvil } from '@viem/anvil'; +import getPort from 'get-port'; +import { dirname, resolve } from 'path'; + +/** + * Ensures there's a running Anvil instance and returns the RPC URL. + */ +export async function startAnvil(l1BlockTime?: number): Promise<{ anvil: Anvil; rpcUrl: string }> { + let ethereumHostPort: number | undefined; + + const anvilBinary = resolve(dirname(fileURLToPath(import.meta.url)), '../../', 'scripts/anvil_kill_wrapper.sh'); + + // Start anvil. + // We go via a wrapper script to ensure if the parent dies, anvil dies. + const anvil = await retry( + async () => { + ethereumHostPort = await getPort(); + const anvil = createAnvil({ + anvilBinary, + port: ethereumHostPort, + blockTime: l1BlockTime, + }); + await anvil.start(); + return anvil; + }, + 'Start anvil', + makeBackoff([5, 5, 5]), + ); + + if (!ethereumHostPort) { + throw new Error('Failed to start anvil'); + } + + const rpcUrl = `http://127.0.0.1:${ethereumHostPort}`; + return { anvil, rpcUrl }; +} diff --git a/yarn-project/ethereum/src/test/tx_delayer.test.ts b/yarn-project/ethereum/src/test/tx_delayer.test.ts new file mode 100644 index 00000000000..f85bcd453cf --- /dev/null +++ b/yarn-project/ethereum/src/test/tx_delayer.test.ts @@ -0,0 +1,97 @@ +import { type DebugLogger, createDebugLogger } from '@aztec/foundation/log'; +import { TestERC20Abi, TestERC20Bytecode } from '@aztec/l1-artifacts'; + +import { type Anvil } from '@viem/anvil'; +import { type PrivateKeyAccount, createWalletClient, getContract, http, publicActions } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { foundry } from 'viem/chains'; + +import { type ViemClient } from '../types.js'; +import { startAnvil } from './start_anvil.js'; +import { type Delayer, withDelayer } from './tx_delayer.js'; + +describe('tx_delayer', () => { + let anvil: Anvil; + let rpcUrl: string; + let logger: DebugLogger; + let account: PrivateKeyAccount; + let client: ViemClient; + let delayer: Delayer; + + const ETHEREUM_SLOT_DURATION = 2; + + beforeAll(async () => { + ({ anvil, rpcUrl } = await startAnvil(ETHEREUM_SLOT_DURATION)); + logger = createDebugLogger('aztec:ethereum:test:tx_delayer'); + }); + + beforeEach(() => { + const transport = http(rpcUrl); + account = privateKeyToAccount('0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'); + ({ client, delayer } = withDelayer( + createWalletClient({ transport, chain: foundry, account }).extend(publicActions), + { ethereumSlotDuration: ETHEREUM_SLOT_DURATION }, + )); + }); + + const receiptNotFound = expect.objectContaining({ name: 'TransactionReceiptNotFoundError' }); + + it('sends a regular tx', async () => { + const blockNumber = await client.getBlockNumber({ cacheTime: 0 }); + const hash = await client.sendTransaction({ to: account.address }); + const receipt = await client.waitForTransactionReceipt({ hash }); + expect(receipt).toBeDefined(); + expect(receipt.blockNumber).toEqual(blockNumber + 1n); + }); + + it('delays a transaction until a given L1 block number', async () => { + const blockNumber = await client.getBlockNumber({ cacheTime: 0 }); + delayer.pauseNextTxUntilBlock(blockNumber + 3n); + logger.info(`Pausing next tx until block ${blockNumber + 3n}`); + + const delayedTxHash = await client.sendTransaction({ to: account.address }); + await expect(client.getTransactionReceipt({ hash: delayedTxHash })).rejects.toThrow(receiptNotFound); + + logger.info(`Delayed tx sent. Awaiting receipt.`); + const delayedTxReceipt = await client.waitForTransactionReceipt({ hash: delayedTxHash }); + expect(delayedTxReceipt.blockNumber).toEqual(blockNumber + 3n); + }, 20000); + + it('delays a transaction until a given L1 timestamp', async () => { + const block = await client.getBlock({ includeTransactions: false }); + const timestamp = block.timestamp; + delayer.pauseNextTxUntilTimestamp(timestamp + 6n); + logger.info(`Pausing next tx until timestamp ${timestamp + 6n}`); + + const delayedTxHash = await client.sendTransaction({ to: account.address }); + await expect(client.getTransactionReceipt({ hash: delayedTxHash })).rejects.toThrow(receiptNotFound); + + logger.info(`Delayed tx sent. Awaiting receipt.`); + const delayedTxReceipt = await client.waitForTransactionReceipt({ hash: delayedTxHash }); + expect(delayedTxReceipt.blockNumber).toEqual(block.number + 3n); + }, 20000); + + it('delays a tx sent through a contract', async () => { + const deployTxHash = await client.deployContract({ abi: TestERC20Abi, bytecode: TestERC20Bytecode, args: [] }); + const { contractAddress, blockNumber } = await client.waitForTransactionReceipt({ + hash: deployTxHash, + pollingInterval: 100, + }); + logger.info(`Deployed contract at ${contractAddress} on block ${blockNumber}`); + + delayer.pauseNextTxUntilBlock(blockNumber + 3n); + logger.info(`Pausing next tx until block ${blockNumber + 3n}`); + + const contract = getContract({ address: contractAddress!, abi: TestERC20Abi, client }); + const delayedTxHash = await contract.write.mint([account.address, 100n]); + await expect(client.getTransactionReceipt({ hash: delayedTxHash })).rejects.toThrow(receiptNotFound); + + logger.info(`Delayed tx sent. Awaiting receipt.`); + const delayedTxReceipt = await client.waitForTransactionReceipt({ hash: delayedTxHash }); + expect(delayedTxReceipt.blockNumber).toEqual(blockNumber + 3n); + }, 20000); + + afterAll(async () => { + await anvil.stop(); + }); +}); diff --git a/yarn-project/ethereum/src/test/tx_delayer.ts b/yarn-project/ethereum/src/test/tx_delayer.ts new file mode 100644 index 00000000000..220823692e1 --- /dev/null +++ b/yarn-project/ethereum/src/test/tx_delayer.ts @@ -0,0 +1,150 @@ +import { type DebugLogger, createDebugLogger } from '@aztec/foundation/log'; +import { retryUntil } from '@aztec/foundation/retry'; + +import { inspect } from 'util'; +import { + type Client, + type Hex, + type PublicClient, + type WalletClient, + keccak256, + publicActions, + walletActions, +} from 'viem'; + +export function waitUntilBlock(client: T, blockNumber: number | bigint, logger?: DebugLogger) { + const publicClient = + 'getBlockNumber' in client && typeof client.getBlockNumber === 'function' + ? (client as unknown as PublicClient) + : client.extend(publicActions); + + return retryUntil( + async () => { + const currentBlockNumber = await publicClient.getBlockNumber({ cacheTime: 0 }); + logger?.debug(`Block number is ${currentBlockNumber} (waiting until ${blockNumber})`); + return currentBlockNumber >= BigInt(blockNumber); + }, + `Wait until L1 block ${blockNumber}`, + 60, + 0.1, + ); +} + +export function waitUntilL1Timestamp(client: T, timestamp: number | bigint, logger?: DebugLogger) { + const publicClient = + 'getBlockNumber' in client && typeof client.getBlockNumber === 'function' + ? (client as unknown as PublicClient) + : client.extend(publicActions); + + let lastBlock: bigint | undefined = undefined; + return retryUntil( + async () => { + const currentBlockNumber = await publicClient.getBlockNumber({ cacheTime: 0 }); + if (currentBlockNumber === lastBlock) { + return false; + } + lastBlock = currentBlockNumber; + const currentBlock = await publicClient.getBlock({ includeTransactions: false, blockNumber: currentBlockNumber }); + const currentTs = currentBlock.timestamp; + logger?.debug(`Block timstamp is ${currentTs} (waiting until ${timestamp})`); + return currentTs >= BigInt(timestamp); + }, + `Wait until L1 timestamp ${timestamp}`, + 60, + 0.1, + ); +} + +export interface Delayer { + /** Returns the list of all txs (not just the delayed ones) sent through the attached client. */ + getTxs(): Hex[]; + /** Delays the next tx to be sent so it lands on the given L1 block number. */ + pauseNextTxUntilBlock(l1BlockNumber: number | bigint | undefined): void; + /** Delays the next tx to be sent so it lands on the given timestamp. */ + pauseNextTxUntilTimestamp(l1Timestamp: number | bigint | undefined): void; +} + +class DelayerImpl implements Delayer { + constructor(opts: { ethereumSlotDuration: bigint | number }) { + this.ethereumSlotDuration = BigInt(opts.ethereumSlotDuration); + } + + public ethereumSlotDuration: bigint; + public nextWait: { l1Timestamp: bigint } | { l1BlockNumber: bigint } | undefined = undefined; + public txs: Hex[] = []; + + getTxs() { + return this.txs; + } + + pauseNextTxUntilBlock(l1BlockNumber: number | bigint) { + this.nextWait = { l1BlockNumber: BigInt(l1BlockNumber) }; + } + + pauseNextTxUntilTimestamp(l1Timestamp: number | bigint) { + this.nextWait = { l1Timestamp: BigInt(l1Timestamp) }; + } +} + +/** + * Returns a new client (without modifying the one passed in) with an injected tx delayer. + * The delayer can be used to hold off the next tx to be sent until a given block number. + */ +export function withDelayer( + client: T, + opts: { ethereumSlotDuration: bigint | number }, +): { client: T; delayer: Delayer } { + const logger = createDebugLogger('aztec:ethereum:tx_delayer'); + const delayer = new DelayerImpl(opts); + const extended = client + // Tweak sendRawTransaction so it uses the delay defined in the delayer. + // Note that this will only work with local accounts (ie accounts for which we have the private key). + // Transactions signed by the node will not be delayed since they use sendTransaction directly, + // but we do not use them in our codebase at all. + .extend(client => ({ + async sendRawTransaction(...args) { + if (delayer.nextWait !== undefined) { + const waitUntil = delayer.nextWait; + delayer.nextWait = undefined; + + const publicClient = client as unknown as PublicClient; + const wait = + 'l1BlockNumber' in waitUntil + ? waitUntilBlock(publicClient, waitUntil.l1BlockNumber - 1n, logger) + : waitUntilL1Timestamp(publicClient, waitUntil.l1Timestamp - delayer.ethereumSlotDuration, logger); + + // Compute the tx hash manually so we emulate sendRawTransaction response + const { serializedTransaction } = args[0]; + const txHash = keccak256(serializedTransaction); + logger.info(`Delaying tx ${txHash} until ${inspect(waitUntil)}`); + + // Do not await here so we can return the tx hash immediately as if it had been sent on the spot. + // Instead, delay it so it lands on the desired block number or timestamp, assuming anvil will + // mine it immediately. + void wait + .then(async () => { + const txHash = await client.sendRawTransaction(...args); + logger.info(`Sent previously delayed tx ${txHash} to land on ${inspect(waitUntil)}`); + delayer.txs.push(txHash); + }) + .catch(err => logger.error(`Error sending tx after delay`, err)); + + return Promise.resolve(txHash); + } else { + const txHash = await client.sendRawTransaction(...args); + logger.debug(`Sent tx immediately ${txHash}`); + delayer.txs.push(txHash); + return txHash; + } + }, + })) + // Re-extend with sendTransaction so it uses the modified sendRawTransaction. + .extend(client => ({ sendTransaction: walletActions(client).sendTransaction })) + // And with the actions that depend on the modified sendTransaction + .extend(client => ({ + writeContract: walletActions(client).writeContract, + deployContract: walletActions(client).deployContract, + })) as T; + + return { client: extended, delayer }; +} diff --git a/yarn-project/ethereum/src/types.ts b/yarn-project/ethereum/src/types.ts new file mode 100644 index 00000000000..2dfb9dc591c --- /dev/null +++ b/yarn-project/ethereum/src/types.ts @@ -0,0 +1,22 @@ +import { + type Chain, + type Client, + type HttpTransport, + type PrivateKeyAccount, + type PublicActions, + type PublicRpcSchema, + type WalletActions, + type WalletRpcSchema, +} from 'viem'; + +/** + * Type for a viem wallet and public client using a local private key. + * Created as: `createWalletClient({ account: privateKeyToAccount(key), transport: http(url), chain }).extend(publicActions)` + */ +export type ViemClient = Client< + HttpTransport, + Chain, + PrivateKeyAccount, + [...PublicRpcSchema, ...WalletRpcSchema], + PublicActions & WalletActions +>; diff --git a/yarn-project/foundation/src/string/index.ts b/yarn-project/foundation/src/string/index.ts index 0d1fd95cb07..250b3c02581 100644 --- a/yarn-project/foundation/src/string/index.ts +++ b/yarn-project/foundation/src/string/index.ts @@ -13,3 +13,11 @@ export function isHex(str: string): boolean { export function hexToBuffer(str: string): Buffer { return Buffer.from(withoutHexPrefix(str), 'hex'); } + +export function pluralize(str: string, count: number | bigint, plural?: string): string { + return count === 1 || count === 1n ? str : plural ?? `${str}s`; +} + +export function count(count: number | bigint, str: string, plural?: string): string { + return `${count} ${pluralize(str, count, plural)}`; +} diff --git a/yarn-project/prover-node/src/factory.ts b/yarn-project/prover-node/src/factory.ts index d722c4e1e03..12ac2e0de92 100644 --- a/yarn-project/prover-node/src/factory.ts +++ b/yarn-project/prover-node/src/factory.ts @@ -31,6 +31,7 @@ export async function createProverNode( log?: DebugLogger; aztecNodeTxProvider?: ProverCoordination; archiver?: Archiver; + publisher?: L1Publisher; } = {}, ) { const telemetry = deps.telemetry ?? new NoopTelemetryClient(); @@ -45,7 +46,7 @@ export async function createProverNode( const prover = await createProverClient(config, telemetry); // REFACTOR: Move publisher out of sequencer package and into an L1-related package - const publisher = new L1Publisher(config, telemetry); + const publisher = deps.publisher ?? new L1Publisher(config, telemetry); // If config.p2pEnabled is true, createProverCoordination will create a p2p client where quotes will be shared and tx's requested // If config.p2pEnabled is false, createProverCoordination request information from the AztecNode diff --git a/yarn-project/prover-node/src/prover-node.ts b/yarn-project/prover-node/src/prover-node.ts index 7f3ea2eb35e..cff56201098 100644 --- a/yarn-project/prover-node/src/prover-node.ts +++ b/yarn-project/prover-node/src/prover-node.ts @@ -142,7 +142,11 @@ export class ProverNode implements ClaimsMonitorHandler, EpochMonitorHandler, Pr const signed = await this.quoteSigner.sign(quote); // Send it to the coordinator - await this.sendEpochProofQuote(signed); + this.log.info( + `Sending quote for epoch ${epochNumber} with blocks ${blocks[0].number} to ${blocks.at(-1)!.number}`, + quote.toViemArgs(), + ); + await this.doSendEpochProofQuote(signed); } catch (err) { this.log.error(`Error handling epoch completed`, err); } @@ -177,11 +181,13 @@ export class ProverNode implements ClaimsMonitorHandler, EpochMonitorHandler, Pr this.log.info('Stopped ProverNode'); } - /** - * Sends an epoch proof quote to the coordinator. - */ + /** Sends an epoch proof quote to the coordinator. */ public sendEpochProofQuote(quote: EpochProofQuote): Promise { this.log.info(`Sending quote for epoch`, quote.toViemArgs().quote); + return this.doSendEpochProofQuote(quote); + } + + private doSendEpochProofQuote(quote: EpochProofQuote) { return this.coordination.addEpochProofQuote(quote); } diff --git a/yarn-project/sequencer-client/src/client/sequencer-client.ts b/yarn-project/sequencer-client/src/client/sequencer-client.ts index 1cccefe6050..404b062696a 100644 --- a/yarn-project/sequencer-client/src/client/sequencer-client.ts +++ b/yarn-project/sequencer-client/src/client/sequencer-client.ts @@ -34,15 +34,27 @@ export class SequencerClient { */ public static async new( config: SequencerClientConfig, - validatorClient: ValidatorClient | undefined, // allowed to be undefined while we migrate - p2pClient: P2P, - worldStateSynchronizer: WorldStateSynchronizer, - contractDataSource: ContractDataSource, - l2BlockSource: L2BlockSource, - l1ToL2MessageSource: L1ToL2MessageSource, - telemetryClient: TelemetryClient, + deps: { + validatorClient: ValidatorClient | undefined; // allowed to be undefined while we migrate + p2pClient: P2P; + worldStateSynchronizer: WorldStateSynchronizer; + contractDataSource: ContractDataSource; + l2BlockSource: L2BlockSource; + l1ToL2MessageSource: L1ToL2MessageSource; + telemetry: TelemetryClient; + publisher?: L1Publisher; + }, ) { - const publisher = new L1Publisher(config, telemetryClient); + const { + validatorClient, + p2pClient, + worldStateSynchronizer, + contractDataSource, + l2BlockSource, + l1ToL2MessageSource, + telemetry: telemetryClient, + } = deps; + const publisher = deps.publisher ?? new L1Publisher(config, telemetryClient); const globalsBuilder = new GlobalVariableBuilder(config); const publicProcessorFactory = new PublicProcessorFactory(contractDataSource, telemetryClient); diff --git a/yarn-project/sequencer-client/src/publisher/index.ts b/yarn-project/sequencer-client/src/publisher/index.ts index 97e14e96262..5590025020a 100644 --- a/yarn-project/sequencer-client/src/publisher/index.ts +++ b/yarn-project/sequencer-client/src/publisher/index.ts @@ -1,2 +1,3 @@ export { L1Publisher, L1SubmitEpochProofArgs } from './l1-publisher.js'; +export * from './test-l1-publisher.js'; export * from './config.js'; diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index a1e5a5799ee..9226059ab6c 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -17,7 +17,7 @@ import { type Proof, type RootRollupPublicInputs, } from '@aztec/circuits.js'; -import { type L1ContractsConfig, createEthereumChain } from '@aztec/ethereum'; +import { type EthereumChain, type L1ContractsConfig, createEthereumChain } from '@aztec/ethereum'; import { makeTuple } from '@aztec/foundation/array'; import { areArraysEqual, compactArray, times } from '@aztec/foundation/collection'; import { type Signature } from '@aztec/foundation/eth-signature'; @@ -58,7 +58,6 @@ import { publicActions, } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; -import type * as chains from 'viem/chains'; import { type PublisherConfig, type TxSenderConfig } from './config.js'; import { L1PublisherMetrics } from './l1-publisher-metrics.js'; @@ -145,19 +144,19 @@ export class L1Publisher { protected log = createDebugLogger('aztec:sequencer:publisher'); - private rollupContract: GetContractReturnType< + protected rollupContract: GetContractReturnType< typeof RollupAbi, - WalletClient + WalletClient >; - private governanceProposerContract?: GetContractReturnType< + protected governanceProposerContract?: GetContractReturnType< typeof GovernanceProposerAbi, - WalletClient + WalletClient > = undefined; - private publicClient: PublicClient; - private walletClient: WalletClient; - private account: PrivateKeyAccount; - private ethereumSlotDuration: bigint; + protected publicClient: PublicClient; + protected walletClient: WalletClient; + protected account: PrivateKeyAccount; + protected ethereumSlotDuration: bigint; public static PROPOSE_GAS_GUESS: bigint = 12_000_000n; public static PROPOSE_AND_CLAIM_GAS_GUESS: bigint = this.PROPOSE_GAS_GUESS + 100_000n; @@ -175,11 +174,7 @@ export class L1Publisher { this.account = privateKeyToAccount(publisherPrivateKey); this.log.debug(`Publishing from address ${this.account.address}`); - this.walletClient = createWalletClient({ - account: this.account, - chain: chain.chainInfo, - transport: http(chain.rpcUrl), - }); + this.walletClient = this.createWalletClient(this.account, chain); this.publicClient = createPublicClient({ chain: chain.chainInfo, @@ -202,6 +197,17 @@ export class L1Publisher { } } + protected createWalletClient( + account: PrivateKeyAccount, + chain: EthereumChain, + ): WalletClient { + return createWalletClient({ + account, + chain: chain.chainInfo, + transport: http(chain.rpcUrl), + }); + } + public getPayLoad() { return this.payload; } @@ -226,7 +232,7 @@ export class L1Publisher { public getRollupContract(): GetContractReturnType< typeof RollupAbi, - WalletClient + WalletClient > { return this.rollupContract; } diff --git a/yarn-project/sequencer-client/src/publisher/test-l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/test-l1-publisher.ts new file mode 100644 index 00000000000..f60dd608138 --- /dev/null +++ b/yarn-project/sequencer-client/src/publisher/test-l1-publisher.ts @@ -0,0 +1,20 @@ +import { type EthereumChain } from '@aztec/ethereum'; +import { type Delayer, withDelayer } from '@aztec/ethereum/test'; + +import { type Chain, type HttpTransport, type PrivateKeyAccount, type WalletClient } from 'viem'; + +import { L1Publisher } from './l1-publisher.js'; + +export class TestL1Publisher extends L1Publisher { + public delayer: Delayer | undefined; + + protected override createWalletClient( + account: PrivateKeyAccount, + chain: EthereumChain, + ): WalletClient { + const baseClient = super.createWalletClient(account, chain); + const { client, delayer } = withDelayer(baseClient, { ethereumSlotDuration: this.ethereumSlotDuration }); + this.delayer = delayer; + return client; + } +} diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index fe8d29ab2cc..5acda1df8ca 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -623,7 +623,9 @@ __metadata: "@jest/globals": ^29.5.0 "@types/jest": ^29.5.0 "@types/node": ^18.14.6 + "@viem/anvil": ^0.0.10 dotenv: ^16.0.3 + get-port: ^7.1.0 jest: ^29.5.0 ts-node: ^10.9.1 tslib: ^2.4.0 @@ -5454,6 +5456,18 @@ __metadata: languageName: node linkType: hard +"@viem/anvil@npm:^0.0.10": + version: 0.0.10 + resolution: "@viem/anvil@npm:0.0.10" + dependencies: + execa: ^7.1.1 + get-port: ^6.1.2 + http-proxy: ^1.18.1 + ws: ^8.13.0 + checksum: fb475055f36c753cea26fa0c02a0278301dddcdc8003418576395cfc31e97ba5a236fbc66ff093bd8d39ea05286487adb86b7499308e446b6cfe90dc08089b38 + languageName: node + linkType: hard + "@viem/anvil@npm:^0.0.9": version: 0.0.9 resolution: "@viem/anvil@npm:0.0.9" From 7bf4dea2266a9d697dbd1c4d244b9496d071dc4e Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Wed, 20 Nov 2024 18:09:27 -0300 Subject: [PATCH 2/5] chore: Add LOG_ELAPSED_TIME to track global time in debug logs --- yarn-project/foundation/src/config/env_var.ts | 1 + yarn-project/foundation/src/log/logger.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index c62e2a03c1d..c114dafd698 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -53,6 +53,7 @@ export type EnvVar = | 'L1_CHAIN_ID' | 'L1_PRIVATE_KEY' | 'L2_QUEUE_SIZE' + | 'LOG_ELAPSED_TIME' | 'LOG_JSON' | 'LOG_LEVEL' | 'MNEMONIC' diff --git a/yarn-project/foundation/src/log/logger.ts b/yarn-project/foundation/src/log/logger.ts index f771af9851c..782ff1d4f90 100644 --- a/yarn-project/foundation/src/log/logger.ts +++ b/yarn-project/foundation/src/log/logger.ts @@ -20,6 +20,9 @@ function getLogLevel() { export let currentLevel = getLogLevel(); +const logElapsedTime = ['1', 'true'].includes(process.env.LOG_ELAPSED_TIME ?? ''); +const firstTimestamp: number = Date.now(); + function filterNegativePatterns(debugString: string): string { return debugString .split(',') @@ -141,7 +144,12 @@ function logWithDebug(debug: debug.Debugger, level: LogLevel, msg: string, data? msg = data ? `${msg} ${fmtLogData(data)}` : msg; if (debug.enabled && LogLevels.indexOf(level) <= LogLevels.indexOf(currentLevel)) { - debug('[%s] %s', level.toUpperCase(), msg); + if (logElapsedTime) { + const ts = ((Date.now() - firstTimestamp) / 1000).toFixed(3); + debug('%ss [%s] %s', ts, level.toUpperCase(), msg); + } else { + debug('[%s] %s', level.toUpperCase(), msg); + } } } From ec0c936dab3841d910b2a10af0d0a96e0f2212f2 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Wed, 20 Nov 2024 19:47:14 -0300 Subject: [PATCH 3/5] Run test on every PR --- scripts/ci/get_e2e_jobs.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/ci/get_e2e_jobs.sh b/scripts/ci/get_e2e_jobs.sh index 37ee194d2e8..6671e546e20 100755 --- a/scripts/ci/get_e2e_jobs.sh +++ b/scripts/ci/get_e2e_jobs.sh @@ -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" From 93f59aa8cd55f34fa3fbe7dc5ba065c8abb5138b Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 21 Nov 2024 09:42:43 -0300 Subject: [PATCH 4/5] Fix --- yarn-project/kv-store/src/stores/l2_tips_store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/kv-store/src/stores/l2_tips_store.ts b/yarn-project/kv-store/src/stores/l2_tips_store.ts index 36180272967..8141d804ce0 100644 --- a/yarn-project/kv-store/src/stores/l2_tips_store.ts +++ b/yarn-project/kv-store/src/stores/l2_tips_store.ts @@ -34,7 +34,7 @@ export class L2TipsStore implements L2BlockStreamEventHandler, L2BlockStreamLoca private getL2Tip(tag: L2BlockTag): L2BlockId { const blockNumber = this.l2TipsStore.get(tag); - if (blockNumber === undefined) { + if (blockNumber === undefined || blockNumber === 0) { return { number: 0, hash: undefined }; } const blockHash = this.l2BlockHashesStore.get(blockNumber); From 7a8a8bd4e4237dc25ff833af7d6419c56bfde435 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 21 Nov 2024 10:43:31 -0300 Subject: [PATCH 5/5] Bump l1 block time to see if it fixes CI --- yarn-project/end-to-end/src/e2e_epochs.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/end-to-end/src/e2e_epochs.test.ts b/yarn-project/end-to-end/src/e2e_epochs.test.ts index b0d87ad741c..3ac4e07afd8 100644 --- a/yarn-project/end-to-end/src/e2e_epochs.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs.test.ts @@ -25,7 +25,7 @@ describe('e2e_epochs', () => { let handle: NodeJS.Timeout; const EPOCH_DURATION = 4; - const L1_BLOCK_TIME = 3; + const L1_BLOCK_TIME = 5; const L2_SLOT_DURATION_IN_L1_BLOCKS = 2; beforeAll(async () => {