diff --git a/yarn-project/archiver/package.json b/yarn-project/archiver/package.json index ff0dd170380f..86f129da24cf 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": { @@ -106,4 +107,4 @@ "engines": { "node": ">=18" } -} +} \ No newline at end of file diff --git a/yarn-project/archiver/src/archiver/epoch_helpers.ts b/yarn-project/archiver/src/archiver/epoch_helpers.ts index f84bafeee38a..55fe28e2f0ec 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 c71e811edc04..097851f343a6 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -55,7 +55,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'; @@ -70,7 +70,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'; @@ -132,10 +132,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) { @@ -165,16 +169,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 41aba729aebe..e1688b791985 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 f1afd126b827..cd2a799df92f 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/end-to-end/src/e2e_epochs.test.ts b/yarn-project/end-to-end/src/e2e_epochs.test.ts new file mode 100644 index 000000000000..89dd54f271b1 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_epochs.test.ts @@ -0,0 +1,127 @@ +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'; + +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; + 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 () => { + 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); + + 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; + + const newL2BlockNumber = Number(await rollup.getBlockNumber()); + if (l2BlockNumber !== newL2BlockNumber) { + logger.info(`Mined new L2 block ${newL2BlockNumber} at L1 block ${newL1BlockNumber} (timestamp ${timestamp})`); + l2BlockNumber = newL2BlockNumber; + } else { + logger.info(`Mined new L1 block ${newL1BlockNumber} (timestamp ${timestamp})`); + } + }, 200); + + proverDelayer = (context.proverNode as any).publisher.delayer; + sequencerDelayer = (context.sequencer as any).sequencer.publisher.delayer; + expect(proverDelayer).toBeDefined(); + expect(sequencerDelayer).toBeDefined(); + + 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 before L1 timestamp ${start} is reached as the start of epoch ${epoch}`); + await waitUntilL1Timestamp(l1Client, start - 1n); + return start; + }; + + const waitUntilL2BlockNumber = async (target: number) => { + await retryUntil(() => Promise.resolve(target === l2BlockNumber), `Wait until L2 block ${l2BlockNumber}`, 60, 0.1); + }; + + /** Asserts the current L2 block number against the rollup contract directly. */ + const expectL2BlockNumber = async (blockNumber: number) => { + const currentBlock = await rollup.getBlockNumber(); + expect(currentBlock).toEqual(BigInt(blockNumber)); + }; + + it('does not allow submitting proof after epoch end', async () => { + await waitUntilEpochStarts(1); + const initialBlockNumber = Number(await rollup.getBlockNumber()); + logger.info(`Starting epoch 1 after L2 block ${initialBlockNumber}`); + + // Hold off prover tx until end of current epoch + const [nextEpochStart] = getTimestampRangeForEpoch(2n, constants); + proverDelayer.pauseNextTxUntilTimestamp(nextEpochStart); + + // Wait until the last block of epoch 1 is published and hold off the sequencer + await waitUntilL2BlockNumber(initialBlockNumber + EPOCH_DURATION); + sequencerDelayer.pauseNextTxUntilTimestamp(nextEpochStart + BigInt(L1_BLOCK_TIME)); + + // Next sequencer to publish a block should trigger a rollback, let's give it time to publish + await waitUntilL1Timestamp(l1Client, nextEpochStart + BigInt(L1_BLOCK_TIME * L2_SLOT_DURATION_IN_L1_BLOCKS)); + await expectL2BlockNumber(initialBlockNumber + 1); + + // The prover tx should have been rejected, and mined strictly before the one that triggered the rollback + const [hash] = proverDelayer.getTxs(); + const receipt = await l1Client.getTransactionReceipt({ hash }); + expect(receipt.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(receipt.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 15abecfb4467..01a6d9f96ea4 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 529e27762948..8a2c0dbaab80 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 743c780bda86..2a81a74257e3 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 46994fbfd7f8..6f81b43d7144 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 ee16764f278e..ec9697b46a06 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 afee02eac0a6..d95fc8b5b6d1 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" @@ -35,6 +39,8 @@ "@jest/globals": "^29.5.0", "@types/jest": "^29.5.0", "@types/node": "^18.14.6", + "@viem/anvil": "^0.0.10", + "get-port": "^7.1.0", "jest": "^29.5.0", "ts-node": "^10.9.1", "typescript": "^5.0.4" @@ -82,4 +88,4 @@ "engines": { "node": ">=18" } -} +} \ No newline at end of file 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 000000000000..f35e118a5a16 --- /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 000000000000..9219a4730088 --- /dev/null +++ b/yarn-project/ethereum/src/contracts/rollup.ts @@ -0,0 +1,51 @@ +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(); + } + + 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/test/index.ts b/yarn-project/ethereum/src/test/index.ts new file mode 100644 index 000000000000..e6e7d745ad64 --- /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 000000000000..8efdff4452b3 --- /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 000000000000..b8c287681b31 --- /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 000000000000..2f1d4a76f90c --- /dev/null +++ b/yarn-project/ethereum/src/test/tx_delayer.test.ts @@ -0,0 +1,55 @@ +import { type DebugLogger, createDebugLogger } from '@aztec/foundation/log'; + +import { type Anvil } from '@viem/anvil'; +import { createWalletClient, http, publicActions } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { foundry } from 'viem/chains'; + +import { startAnvil } from './start_anvil.js'; +import { withDelayer } from './tx_delayer.js'; + +describe('tx_delayer', () => { + let anvil: Anvil; + let rpcUrl: string; + let logger: DebugLogger; + + beforeAll(async () => { + ({ anvil, rpcUrl } = await startAnvil(1)); + logger = createDebugLogger('aztec:ethereum:test:tx_delayer'); + }); + + it('delays a transaction until a given L1 block number', async () => { + const transport = http(rpcUrl); + const account = privateKeyToAccount('0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'); + const { client, delayer } = withDelayer( + createWalletClient({ transport, chain: foundry, account }).extend(publicActions), + ); + + const hash = await client.sendTransaction({ to: account.address }); + const receipt = await client.waitForTransactionReceipt({ hash }); + expect(receipt).toBeDefined(); + logger.info(`Non-delayed transaction mined in block ${receipt.blockNumber}`); + + const blockNumber = await client.getBlockNumber({ cacheTime: 0 }); + logger.info(`Current block is ${blockNumber}`); + + 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( + expect.objectContaining({ name: 'TransactionReceiptNotFoundError' }), + ); + + logger.info(`Delayed tx sent. Awaiting receipt.`); + const delayedTxReceipt = await client.waitForTransactionReceipt({ hash: delayedTxHash }); + expect(delayedTxReceipt.blockNumber).toEqual(blockNumber + 3n); + + logger.info(`Receipt received. Sending another non-delayed tx.`); + await client.waitForTransactionReceipt({ hash: await client.sendTransaction({ to: account.address }) }); + }, 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 000000000000..5b845198b1ac --- /dev/null +++ b/yarn-project/ethereum/src/test/tx_delayer.ts @@ -0,0 +1,124 @@ +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 { + 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): { client: T; delayer: Delayer } { + const logger = createDebugLogger('aztec:ethereum:tx_delayer'); + const delayer = new DelayerImpl(); + const extended = client + .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 - 1n, logger); + + void wait + .then(async () => { + logger.info(`Sending delayed tx to land on ${inspect(waitUntil)}`); + const txHash = await client.sendRawTransaction(...args); + delayer.txs.push(txHash); + }) + .catch(err => logger.error(`Error sending tx after delay`, err)); + + const { serializedTransaction } = args[0]; + return Promise.resolve(keccak256(serializedTransaction)); + } else { + const txHash = await client.sendRawTransaction(...args); + delayer.txs.push(txHash); + return txHash; + } + }, + })) + .extend(client => ({ sendTransaction: walletActions(client).sendTransaction })) as T; + + return { client: extended, delayer }; +} diff --git a/yarn-project/prover-node/src/factory.ts b/yarn-project/prover-node/src/factory.ts index d722c4e1e032..12ac2e0de920 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/sequencer-client/src/client/sequencer-client.ts b/yarn-project/sequencer-client/src/client/sequencer-client.ts index 1cccefe6050f..404b062696a3 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 97e14e962627..5590025020a3 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 229ba4f7d752..672c4ff51421 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 000000000000..f45327f86ec2 --- /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); + this.delayer = delayer; + return client; + } +} diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 196a74d9b036..39fb9e98b629 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 @@ -5452,6 +5454,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"