diff --git a/yarn-project/aztec-node/src/aztec-node/server.test.ts b/yarn-project/aztec-node/src/aztec-node/server.test.ts index f7547a5159ba..931b30f91a3a 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.test.ts @@ -1,19 +1,20 @@ import { TestCircuitVerifier } from '@aztec/bb-prover'; import { EpochCache } from '@aztec/epoch-cache'; import type { RollupContract } from '@aztec/ethereum/contracts'; +import type { EthCheatCodes } from '@aztec/ethereum/test'; import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { EthAddress } from '@aztec/foundation/eth-address'; import { BadRequestError } from '@aztec/foundation/json-rpc'; import type { Hex } from '@aztec/foundation/string'; -import { DateProvider } from '@aztec/foundation/timer'; +import { DateProvider, TestDateProvider } from '@aztec/foundation/timer'; import { unfreeze } from '@aztec/foundation/types'; import { type KeyStore, KeystoreManager, RemoteSigner, type ValidatorKeyStore } from '@aztec/node-keystore'; import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree'; import type { P2P } from '@aztec/p2p'; import { protocolContractsHash } from '@aztec/protocol-contracts'; import { computeFeePayerBalanceLeafSlot } from '@aztec/protocol-contracts/fee-juice'; -import type { GlobalVariableBuilder, SequencerClient } from '@aztec/sequencer-client'; +import type { GlobalVariableBuilder, Sequencer, SequencerClient } from '@aztec/sequencer-client'; import type { SlasherClientInterface } from '@aztec/slasher'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { BlockHash, type BlockParameter, CheckpointedL2Block, L2Block, type L2BlockSource } from '@aztec/stdlib/block'; @@ -200,6 +201,7 @@ describe('aztec node', () => { getPackageVersion() ?? '', new TestCircuitVerifier(), new TestCircuitVerifier(), + new DateProvider(), ); }); @@ -742,6 +744,7 @@ describe('aztec node', () => { getPackageVersion() ?? '', new TestCircuitVerifier(), new TestCircuitVerifier(), + new DateProvider(), undefined, undefined, undefined, @@ -931,6 +934,7 @@ describe('aztec node', () => { getPackageVersion() ?? '', new TestCircuitVerifier(), new TestCircuitVerifier(), + new DateProvider(), undefined, undefined, undefined, @@ -958,6 +962,168 @@ describe('aztec node', () => { }); }); + describe('time manipulation', () => { + const INITIAL_MIN_TXS_PER_BLOCK = 1; + + let mockEthCheatCodes: MockProxy; + let sequencerClient: MockProxy; + let sequencer: MockProxy; + let testDateProvider: TestDateProvider; + let nodeWithSequencer: AztecNodeService; + + beforeEach(() => { + mockEthCheatCodes = mock(); + sequencer = mock(); + sequencer.getConfig.mockReturnValue({ minTxsPerBlock: INITIAL_MIN_TXS_PER_BLOCK } as any); + + sequencerClient = mock(); + sequencerClient.getSequencer.mockReturnValue(sequencer); + sequencerClient.trigger.mockReturnValue(Promise.resolve()); + + testDateProvider = new TestDateProvider(); + + nodeWithSequencer = new AztecNodeService( + nodeConfig, + p2p, + l2BlockSource, + mock(), + mock(), + mock(), + worldState, + sequencerClient, + undefined, + undefined, + undefined, + undefined, + 12345, + rollupVersion.toNumber(), + globalVariablesBuilder, + epochCache, + getPackageVersion() ?? '', + new TestCircuitVerifier(), + new TestCircuitVerifier(), + testDateProvider, + ); + + // Pre-inject mock to avoid #getEthCheatCodes() creating a real EthCheatCodes with HTTP clients + (nodeWithSequencer as any).ethCheatCodes = mockEthCheatCodes; + }); + + // Slot calculation: slot = (timestamp - l1GenesisTime) / slotDuration + // With genesis=1000, slotDuration=12: timestamp 1060 → slot 5, timestamp 1120 → slot 10 + const l1Constants = { ...EmptyL1RollupConstants, l1GenesisTime: 1000n, slotDuration: 12 }; + + const makeBlockInSlot = (slot: number) => + L2Block.empty( + BlockHeader.empty({ + globalVariables: GlobalVariables.empty({ slotNumber: SlotNumber(slot) }), + }), + ); + + /** Simulates block number advancing from `from` to `to` after the first call. */ + const mockBlockNumberAdvancing = (from: number, to: number) => { + let callCount = 0; + l2BlockSource.getBlockNumber.mockImplementation(() => { + callCount++; + return Promise.resolve(callCount > 1 ? BlockNumber(to) : BlockNumber(from)); + }); + }; + + describe('mineBlock', () => { + it('throws when no sequencer is running', async () => { + // The default `node` has no sequencer (undefined) + (node as any).ethCheatCodes = mockEthCheatCodes; + await expect(node.mineBlock()).rejects.toThrow('Cannot mine block: no sequencer is running'); + }); + + // mineBlock slot-handling logic (new slot and same slot) is tested in e2e_cheat_codes.test.ts + + it('throws when currentSlot is behind lastBlockSlot', async () => { + mockEthCheatCodes.evmMine.mockResolvedValue(); + // Timestamp 1036 → slot (1036-1000)/12 = 3, behind latest block's slot 5 + mockEthCheatCodes.lastBlockTimestamp.mockResolvedValue(1036); + l2BlockSource.getL1Constants.mockResolvedValue(l1Constants); + l2BlockSource.getL2Block.mockResolvedValue(makeBlockInSlot(5)); + l2BlockSource.getBlockNumber.mockResolvedValue(BlockNumber(5)); + + await expect(nodeWithSequencer.mineBlock()).rejects.toThrow("Current slot 3 is behind the last block's slot 5"); + }); + + it('restores minTxsPerBlock after successful block production', async () => { + mockEthCheatCodes.evmMine.mockResolvedValue(); + mockEthCheatCodes.lastBlockTimestamp.mockResolvedValue(1120); + l2BlockSource.getL1Constants.mockResolvedValue(l1Constants); + l2BlockSource.getL2Block.mockResolvedValue(makeBlockInSlot(5)); + mockBlockNumberAdvancing(5, 6); + + await nodeWithSequencer.mineBlock(); + + const updateCalls = sequencerClient.updateConfig.mock.calls; + expect(updateCalls[0][0]).toEqual({ minTxsPerBlock: 0 }); + // Last call to update calls should revert the value to the original + expect(updateCalls[1][0]).toEqual({ minTxsPerBlock: INITIAL_MIN_TXS_PER_BLOCK }); + }); + }); + + describe('setNextBlockTimestamp', () => { + it('sets timestamp on ethCheatCodes and updates dateProvider', async () => { + mockEthCheatCodes.setNextBlockTimestamp.mockResolvedValue(); + + const targetTimestamp = 1_000_000; + await nodeWithSequencer.setNextBlockTimestamp(targetTimestamp); + + expect(mockEthCheatCodes.setNextBlockTimestamp).toHaveBeenCalledWith(targetTimestamp); + const targetMs = targetTimestamp * 1000; + expect(testDateProvider.now()).toBeGreaterThanOrEqual(targetMs); + }); + }); + + describe('advanceNextBlockTimestampBy', () => { + it('advances timestamp by duration from current L1 timestamp', async () => { + mockEthCheatCodes.lastBlockTimestamp.mockResolvedValue(500); + mockEthCheatCodes.setNextBlockTimestamp.mockResolvedValue(); + + await nodeWithSequencer.advanceNextBlockTimestampBy(100); + + expect(mockEthCheatCodes.setNextBlockTimestamp).toHaveBeenCalledWith(600); + expect(testDateProvider.now()).toBeGreaterThanOrEqual(600_000); + }); + }); + + describe('updateDateProviderTimestampTo', () => { + it('throws when dateProvider does not support setTime', async () => { + const nodeWithPlainDateProvider = new AztecNodeService( + nodeConfig, + p2p, + l2BlockSource, + mock(), + mock(), + mock(), + worldState, + sequencerClient, + undefined, + undefined, + undefined, + undefined, + 12345, + rollupVersion.toNumber(), + globalVariablesBuilder, + epochCache, + getPackageVersion() ?? '', + new TestCircuitVerifier(), + new TestCircuitVerifier(), + new DateProvider(), + ); + (nodeWithPlainDateProvider as any).ethCheatCodes = mockEthCheatCodes; + mockEthCheatCodes.setNextBlockTimestamp.mockResolvedValue(); + + await expect(nodeWithPlainDateProvider.setNextBlockTimestamp(1000)).rejects.toThrow( + 'Date provider does not support direct time manipulation', + ); + }); + }); + }); + describe('getL2ToL1Messages', () => { const makeCheckpointedBlock = (slotNumber: number, l2ToL1MsgsByTx: Fr[][]): CheckpointedL2Block => { const block = L2Block.empty( diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index eecd89eff75a..d3bdcefd2571 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -10,12 +10,14 @@ import { getPublicClient, makeL1HttpTransport } from '@aztec/ethereum/client'; import { RegistryContract, RollupContract } from '@aztec/ethereum/contracts'; import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses'; import type { L1TxUtils } from '@aztec/ethereum/l1-tx-utils'; +import { EthCheatCodes } from '@aztec/ethereum/test'; import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; import { chunkBy, compactArray, pick, unique } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/curves/bn254'; import { EthAddress } from '@aztec/foundation/eth-address'; import { BadRequestError } from '@aztec/foundation/json-rpc'; import { type Logger, createLogger } from '@aztec/foundation/log'; +import { retryUntil } from '@aztec/foundation/retry'; import { count } from '@aztec/foundation/string'; import { DateProvider, Timer } from '@aztec/foundation/timer'; import { MembershipWitness, SiblingPath } from '@aztec/foundation/trees'; @@ -59,6 +61,7 @@ import type { NodeInfo, ProtocolContractAddresses, } from '@aztec/stdlib/contract'; +import { getSlotAtTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers'; import { GasFees } from '@aztec/stdlib/gas'; import { computePublicDataTreeLeafSlot } from '@aztec/stdlib/hash'; import { @@ -66,6 +69,7 @@ import { type AztecNodeAdmin, type AztecNodeAdminConfig, AztecNodeAdminConfigSchema, + type AztecNodeDebug, type GetContractClassLogsResponse, type GetPublicLogsResponse, } from '@aztec/stdlib/interfaces/client'; @@ -126,9 +130,10 @@ import { NodeMetrics } from './node_metrics.js'; /** * The aztec node. */ -export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { +export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDebug, Traceable { private metrics: NodeMetrics; private initialHeaderHashPromise: Promise | undefined = undefined; + private ethCheatCodes: EthCheatCodes | undefined; // Prevent two snapshot operations to happen simultaneously private isUploadingSnapshot = false; @@ -155,6 +160,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { protected readonly packageVersion: string, private peerProofVerifier: ClientProtocolCircuitVerifier, private rpcProofVerifier: ClientProtocolCircuitVerifier, + private dateProvider: DateProvider, private telemetry: TelemetryClient = getTelemetryClient(), private log = createLogger('node'), private blobClient?: BlobClientInterface, @@ -616,6 +622,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { packageVersion, peerProofVerifier, rpcProofVerifier, + dateProvider, telemetry, log, blobClient, @@ -1643,6 +1650,99 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { this.log.info('Keystore reloaded: coinbase, feeRecipient, and attester keys updated'); } + #getEthCheatCodes(): EthCheatCodes { + if (!this.ethCheatCodes) { + this.ethCheatCodes = new EthCheatCodes(this.config.l1RpcUrls, this.dateProvider); + } + return this.ethCheatCodes; + } + + /** Updates the date provider to match the given timestamp, if it supports time manipulation. */ + #updateDateProviderTimestampTo(timestampInSeconds: number): void { + if (!('setTime' in this.dateProvider)) { + throw new Error('Date provider does not support direct time manipulation.'); + } + + (this.dateProvider as { setTime(ms: number): void }).setTime(timestampInSeconds * 1000); + } + + public async setNextBlockTimestamp(timestamp: number): Promise { + const ethCheatCodes = this.#getEthCheatCodes(); + await ethCheatCodes.setNextBlockTimestamp(timestamp); + this.#updateDateProviderTimestampTo(timestamp); + } + + public async advanceNextBlockTimestampBy(duration: number): Promise { + const ethCheatCodes = this.#getEthCheatCodes(); + const currentL1Timestamp = await ethCheatCodes.lastBlockTimestamp(); + await ethCheatCodes.setNextBlockTimestamp(currentL1Timestamp + duration); + this.#updateDateProviderTimestampTo(currentL1Timestamp + duration); + } + + public async mineBlock(): Promise { + if (!this.sequencer) { + throw new BadRequestError('Cannot mine block: no sequencer is running'); + } + + const currentBlockNumber = await this.getBlockNumber(); + + // Mine one L1 block — this uses any pending evm_setNextBlockTimestamp + const ethCheatCodes = this.#getEthCheatCodes(); + await ethCheatCodes.evmMine(); + + // Check if we're in a new L2 slot. If not, warp to the next slot's timestamp. + const l1Constants = await this.blockSource.getL1Constants(); + const currentL1Timestamp = BigInt(await ethCheatCodes.lastBlockTimestamp()); + const currentSlot = getSlotAtTimestamp(currentL1Timestamp, l1Constants); + + const latestBlock = await this.getBlock('latest'); + const lastBlockSlot = latestBlock ? latestBlock.header.globalVariables.slotNumber : SlotNumber(0); + + if (currentSlot < lastBlockSlot) { + // Both Anvil and Hardhat enforce that evm_setNextBlockTimestamp only accepts timestamps strictly greater than + // the current block's timestamp, so L1 time cannot go backwards. If the current slot is behind the last block's + // slot, something has gone wrong. + throw new Error( + `Current slot ${currentSlot} is behind the last block's slot ${lastBlockSlot}. ` + + `L1 time cannot be warped backwards.`, + ); + } + + if (currentSlot === lastBlockSlot) { + // A block was already produced in this slot. Warp L1 time forward to the next slot so we can mine another block. + const nextSlotTimestamp = getTimestampForSlot(SlotNumber(Number(lastBlockSlot) + 1), l1Constants); + this.log.info(`Current slot ${currentSlot} already has a block, warping L1 time to slot ${lastBlockSlot + 1}`); + // warp mines a block - hence we don't need to do it manually here + await ethCheatCodes.warp(Number(nextSlotTimestamp)); + } + + // Update dateProvider to match L1 time + const newTimestamp = await ethCheatCodes.lastBlockTimestamp(); + this.#updateDateProviderTimestampTo(newTimestamp); + + // Temporarily set minTxsPerBlock to 0 so the sequencer produces a block even with no txs + const originalMinTxsPerBlock = this.sequencer.getSequencer().getConfig().minTxsPerBlock; + this.sequencer.updateConfig({ minTxsPerBlock: 0 }); + + try { + // Trigger the sequencer to produce a block immediately + void this.sequencer.trigger(); + + // Wait for the new L2 block to appear + await retryUntil( + async () => { + const newBlockNumber = await this.getBlockNumber(); + return newBlockNumber > currentBlockNumber ? true : undefined; + }, + 'mineBlock', + 30, + 0.1, + ); + } finally { + this.sequencer.updateConfig({ minTxsPerBlock: originalMinTxsPerBlock }); + } + } + #getInitialHeaderHash(): Promise { if (!this.initialHeaderHashPromise) { this.initialHeaderHashPromise = this.worldStateSynchronizer.getCommitted().getInitialHeader().hash(); diff --git a/yarn-project/aztec/src/cli/aztec_start_action.ts b/yarn-project/aztec/src/cli/aztec_start_action.ts index f7c3c7c5ff07..f7f62eb6a594 100644 --- a/yarn-project/aztec/src/cli/aztec_start_action.ts +++ b/yarn-project/aztec/src/cli/aztec_start_action.ts @@ -7,7 +7,7 @@ import { } from '@aztec/foundation/json-rpc/server'; import type { LogFn, Logger } from '@aztec/foundation/log'; import type { ChainConfig } from '@aztec/stdlib/config'; -import { AztecNodeAdminApiSchema, AztecNodeApiSchema } from '@aztec/stdlib/interfaces/client'; +import { AztecNodeAdminApiSchema, AztecNodeApiSchema, AztecNodeDebugApiSchema } from '@aztec/stdlib/interfaces/client'; import { getPackageVersion } from '@aztec/stdlib/update-checker'; import { getVersioningMiddleware } from '@aztec/stdlib/versioning'; import { getOtelJsonRpcPropagationMiddleware } from '@aztec/telemetry-client'; @@ -51,6 +51,7 @@ export async function aztecStart(options: any, userLog: LogFn, debugLogger: Logg signalHandlers.push(stop); services.node = [node, AztecNodeApiSchema]; adminServices.node = [node, AztecNodeAdminApiSchema]; + adminServices.nodeDebug = [node, AztecNodeDebugApiSchema]; } else { // Route --prover-node through startNode if (options.proverNode && !options.node) { diff --git a/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts b/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts index 5a4646542537..bfd0c87b7b88 100644 --- a/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts +++ b/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts @@ -4,12 +4,14 @@ import { createExtendedL1Client } from '@aztec/ethereum/client'; import type { Anvil } from '@aztec/ethereum/test'; import type { ExtendedViemWalletClient } from '@aztec/ethereum/types'; import { DateProvider } from '@aztec/foundation/timer'; +import type { AztecNode, AztecNodeDebug } from '@aztec/stdlib/interfaces/client'; import { parseEther } from 'viem'; import { mnemonicToAccount } from 'viem/accounts'; import { foundry } from 'viem/chains'; import { MNEMONIC } from './fixtures/fixtures.js'; +import { type EndToEndContext, setup } from './fixtures/setup.js'; import { getLogger, startAnvil } from './fixtures/utils.js'; describe('e2e_cheat_codes', () => { @@ -134,4 +136,67 @@ describe('e2e_cheat_codes', () => { } }); }); + + describe('L2 debug time manipulation', () => { + let context: EndToEndContext; + let aztecNode: AztecNode; + let aztecNodeDebug: AztecNodeDebug; + + beforeAll(async () => { + context = await setup(0); + aztecNode = context.aztecNode; + aztecNodeDebug = context.aztecNodeService; + }); + + afterAll(async () => { + await context.teardown(); + }); + + it('setNextBlockTimestamp + mineBlock produces a block with the target timestamp', async () => { + const targetTimestamp = Math.floor(Date.now() / 1000) + 1000; + await aztecNodeDebug.setNextBlockTimestamp(targetTimestamp); + await aztecNodeDebug.mineBlock(); + + const blockNumber = await aztecNode.getBlockNumber(); + const block = await aztecNode.getBlock(blockNumber); + expect(block).toBeDefined(); + expect(Number(block!.header.globalVariables.timestamp)).toBeGreaterThanOrEqual(targetTimestamp); + }); + + it('advanceNextBlockTimestampBy + mineBlock advances time', async () => { + const blockBeforeAdvance = await aztecNode.getBlock(await aztecNode.getBlockNumber()); + const timestampBefore = Number(blockBeforeAdvance!.header.globalVariables.timestamp); + + const advancement = 100; + await aztecNodeDebug.advanceNextBlockTimestampBy(advancement); + await aztecNodeDebug.mineBlock(); + + const blockNumber = await aztecNode.getBlockNumber(); + const block = await aztecNode.getBlock(blockNumber); + expect(block).toBeDefined(); + const timestampAfter = Number(block!.header.globalVariables.timestamp); + expect(timestampAfter).toBeGreaterThanOrEqual(timestampBefore + advancement); + }); + + it('mineBlock without setting timestamp still produces a new block', async () => { + const firstBlockNumber = await aztecNode.getBlockNumber(); + const firstBlock = await aztecNode.getBlock(firstBlockNumber); + const firstSlot = firstBlock!.header.globalVariables.slotNumber; + + await aztecNodeDebug.mineBlock(); + const secondBlockNumber = await aztecNode.getBlockNumber(); + const secondBlock = await aztecNode.getBlock(secondBlockNumber); + const secondSlot = secondBlock!.header.globalVariables.slotNumber; + + expect(secondSlot).toBeGreaterThan(firstSlot); + + // Second call is still in the same slot, so mineBlock must warp to the next slot + await aztecNodeDebug.mineBlock(); + const thirdBlockNumber = await aztecNode.getBlockNumber(); + const thirdBlock = await aztecNode.getBlock(thirdBlockNumber); + const thirdSlot = thirdBlock!.header.globalVariables.slotNumber; + + expect(thirdSlot).toBeGreaterThan(secondSlot); + }); + }); }); diff --git a/yarn-project/sequencer-client/src/client/sequencer-client.ts b/yarn-project/sequencer-client/src/client/sequencer-client.ts index 361f7f1008f0..5003f9310fed 100644 --- a/yarn-project/sequencer-client/src/client/sequencer-client.ts +++ b/yarn-project/sequencer-client/src/client/sequencer-client.ts @@ -209,6 +209,11 @@ export class SequencerClient { this.l1Metrics?.stop(); } + /** Triggers an immediate run of the sequencer, bypassing the polling interval. */ + public trigger() { + return this.sequencer.trigger(); + } + public getSequencer(): Sequencer { return this.sequencer; } diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index a881f6b9eb97..28e1ffe587e7 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -146,6 +146,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter { this.log.info(`Stopping sequencer`); diff --git a/yarn-project/stdlib/src/interfaces/aztec-node-debug.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node-debug.test.ts new file mode 100644 index 000000000000..8a0603cf812a --- /dev/null +++ b/yarn-project/stdlib/src/interfaces/aztec-node-debug.test.ts @@ -0,0 +1,49 @@ +import { type JsonRpcTestContext, createJsonRpcTestSetup } from '@aztec/foundation/json-rpc/test'; + +import { type AztecNodeDebug, AztecNodeDebugApiSchema } from './aztec-node-debug.js'; + +describe('AztecNodeDebugApiSchema', () => { + let handler: MockAztecNodeDebug; + let context: JsonRpcTestContext; + + const tested: Set = new Set(); + + beforeEach(async () => { + handler = new MockAztecNodeDebug(); + context = await createJsonRpcTestSetup(handler, AztecNodeDebugApiSchema); + }); + + afterEach(() => { + tested.add(/^AztecNodeDebugApiSchema\s+([^(]+)/.exec(expect.getState().currentTestName!)![1]); + context.httpServer.close(); + }); + + afterAll(() => { + const all = Object.keys(AztecNodeDebugApiSchema); + expect([...tested].sort()).toEqual(all.sort()); + }); + + it('setNextBlockTimestamp', async () => { + await context.client.setNextBlockTimestamp(1000000); + }); + + it('advanceNextBlockTimestampBy', async () => { + await context.client.advanceNextBlockTimestampBy(60); + }); + + it('mineBlock', async () => { + await context.client.mineBlock(); + }); +}); + +class MockAztecNodeDebug implements AztecNodeDebug { + setNextBlockTimestamp(_timestamp: number): Promise { + return Promise.resolve(); + } + advanceNextBlockTimestampBy(_duration: number): Promise { + return Promise.resolve(); + } + mineBlock(): Promise { + return Promise.resolve(); + } +} diff --git a/yarn-project/stdlib/src/interfaces/aztec-node-debug.ts b/yarn-project/stdlib/src/interfaces/aztec-node-debug.ts new file mode 100644 index 000000000000..1c3cd4c6f813 --- /dev/null +++ b/yarn-project/stdlib/src/interfaces/aztec-node-debug.ts @@ -0,0 +1,40 @@ +import { createSafeJsonRpcClient, defaultFetch } from '@aztec/foundation/json-rpc/client'; + +import { z } from 'zod'; + +import type { ApiSchemaFor } from '../schemas/schemas.js'; +import { type ComponentsVersions, getVersioningResponseHandler } from '../versioning/index.js'; + +/** + * Debug interface for Aztec node available in sandbox/local-network mode. + */ +export interface AztecNodeDebug { + /** Sets the L1 timestamp for the next block via `evm_setNextBlockTimestamp`. Does not mine. */ + setNextBlockTimestamp(timestamp: number): Promise; + + /** Advances the L1 timestamp by the given duration (in seconds) via `evm_setNextBlockTimestamp`. Does not mine. */ + advanceNextBlockTimestampBy(duration: number): Promise; + + /** Mines an L1 block, ensures we're in a new L2 slot, and forces the sequencer to produce an L2 block. */ + mineBlock(): Promise; +} + +export const AztecNodeDebugApiSchema: ApiSchemaFor = { + setNextBlockTimestamp: z.function().args(z.number()).returns(z.void()), + advanceNextBlockTimestampBy: z.function().args(z.number()).returns(z.void()), + mineBlock: z.function().returns(z.void()), +}; + +export function createAztecNodeDebugClient( + url: string, + versions: Partial = {}, + fetch = defaultFetch, + apiKey?: string, +): AztecNodeDebug { + return createSafeJsonRpcClient(url, AztecNodeDebugApiSchema, { + namespaceMethods: 'nodeDebug', + fetch, + onResponse: getVersioningResponseHandler(versions), + ...(apiKey ? { extraHeaders: { 'x-api-key': apiKey } } : {}), + }); +} diff --git a/yarn-project/stdlib/src/interfaces/client.ts b/yarn-project/stdlib/src/interfaces/client.ts index 46f03caed0ee..78b3d5d82d08 100644 --- a/yarn-project/stdlib/src/interfaces/client.ts +++ b/yarn-project/stdlib/src/interfaces/client.ts @@ -1,5 +1,6 @@ export * from './aztec-node.js'; export * from './aztec-node-admin.js'; +export * from './aztec-node-debug.js'; export * from './private_kernel_prover.js'; export * from './get_logs_response.js'; export * from './api_limit.js'; diff --git a/yarn-project/txe/src/state_machine/index.ts b/yarn-project/txe/src/state_machine/index.ts index ca44362beee2..f303addb9a8f 100644 --- a/yarn-project/txe/src/state_machine/index.ts +++ b/yarn-project/txe/src/state_machine/index.ts @@ -3,6 +3,7 @@ import { TestCircuitVerifier } from '@aztec/bb-prover/test'; import { CheckpointNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { createLogger } from '@aztec/foundation/log'; +import { DateProvider } from '@aztec/foundation/timer'; import { type AnchorBlockStore, type ContractStore, ContractSyncService, type NoteStore } from '@aztec/pxe/server'; import { MessageContextService } from '@aztec/pxe/simulator'; import { L2Block } from '@aztec/stdlib/block'; @@ -60,6 +61,7 @@ export class TXEStateMachine { getPackageVersion() ?? '', new TestCircuitVerifier(), new TestCircuitVerifier(), + new DateProvider(), undefined, log, );