From 4ccf5a7c9f51bb5b48aeefdf744b56198afce5f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Wed, 25 Mar 2026 17:13:55 +0000 Subject: [PATCH 1/8] sketching timetravel rpc --- .../aztec-node/src/aztec-node/server.test.ts | 3 + .../aztec-node/src/aztec-node/server.ts | 84 +++++++++++++++++++ .../end-to-end/src/e2e_cheat_codes.test.ts | 51 +++++++++++ .../src/client/sequencer-client.ts | 5 ++ .../src/sequencer/sequencer.ts | 5 ++ .../src/interfaces/aztec-node-admin.test.ts | 21 +++++ .../stdlib/src/interfaces/aztec-node-admin.ts | 12 +++ yarn-project/txe/src/state_machine/index.ts | 2 + 8 files changed, 183 insertions(+) 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..79232fc78438 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.test.ts @@ -200,6 +200,7 @@ describe('aztec node', () => { getPackageVersion() ?? '', new TestCircuitVerifier(), new TestCircuitVerifier(), + new DateProvider(), ); }); @@ -742,6 +743,7 @@ describe('aztec node', () => { getPackageVersion() ?? '', new TestCircuitVerifier(), new TestCircuitVerifier(), + new DateProvider(), undefined, undefined, undefined, @@ -931,6 +933,7 @@ describe('aztec node', () => { getPackageVersion() ?? '', new TestCircuitVerifier(), new TestCircuitVerifier(), + new DateProvider(), undefined, undefined, undefined, diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index eecd89eff75a..314b9cce059e 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 { @@ -129,6 +132,7 @@ import { NodeMetrics } from './node_metrics.js'; export class AztecNodeService implements AztecNode, AztecNodeAdmin, 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 +159,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 +621,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { packageVersion, peerProofVerifier, rpcProofVerifier, + dateProvider, telemetry, log, blobClient, @@ -1643,6 +1649,84 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { this.log.info('Keystore reloaded: coinbase, feeRecipient, and attester keys updated'); } + private 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 L1 timestamp, if it supports time manipulation. */ + private updateDateProvider(timestampInSeconds: number): void { + if ('setTime' in this.dateProvider) { + (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.updateDateProvider(timestamp); + } + + public async advanceNextBlockTimestampBy(duration: number): Promise { + const ethCheatCodes = this.getEthCheatCodes(); + const currentTimestamp = await ethCheatCodes.timestamp(); + await ethCheatCodes.setNextBlockTimestamp(currentTimestamp + duration); + this.updateDateProvider(currentTimestamp + 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.timestamp()); + const currentSlot = getSlotAtTimestamp(currentL1Timestamp, l1Constants); + + const latestBlock = await this.getBlock('latest'); + const lastBlockSlot = latestBlock ? BigInt(latestBlock.header.globalVariables.slotNumber) : 0n; + + if (BigInt(currentSlot) <= lastBlockSlot) { + const nextSlotTimestamp = getTimestampForSlot(SlotNumber(Number(lastBlockSlot) + 1), l1Constants); + await ethCheatCodes.warp(Number(nextSlotTimestamp)); + } + + // Update dateProvider to match L1 time + const newTimestamp = await ethCheatCodes.timestamp(); + this.updateDateProvider(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/end-to-end/src/e2e_cheat_codes.test.ts b/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts index 5a4646542537..5198a6ee26af 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, AztecNodeAdmin } 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,53 @@ describe('e2e_cheat_codes', () => { } }); }); + + describe('L2 admin time manipulation', () => { + let context: EndToEndContext; + let aztecNode: AztecNode; + let aztecNodeAdmin: AztecNodeAdmin; + + beforeAll(async () => { + context = await setup(0); + aztecNode = context.aztecNode; + aztecNodeAdmin = context.aztecNodeAdmin; + }); + + 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 aztecNodeAdmin.setNextBlockTimestamp(targetTimestamp); + await aztecNodeAdmin.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 aztecNodeAdmin.advanceNextBlockTimestampBy(advancement); + await aztecNodeAdmin.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 blockNumberBefore = await aztecNode.getBlockNumber(); + await aztecNodeAdmin.mineBlock(); + const blockNumberAfter = await aztecNode.getBlockNumber(); + expect(blockNumberAfter).toBeGreaterThan(blockNumberBefore); + }); + }); }); 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-admin.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node-admin.test.ts index 6387858d12da..0177f19b50ba 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node-admin.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node-admin.test.ts @@ -89,6 +89,18 @@ describe('AztecNodeAdminApiSchema', () => { it('reloadKeystore', async () => { await context.client.reloadKeystore(); }); + + it('setNextBlockTimestamp', async () => { + await context.client.setNextBlockTimestamp(1000000); + }); + + it('advanceNextBlockTimestampBy', async () => { + await context.client.advanceNextBlockTimestampBy(60); + }); + + it('mineBlock', async () => { + await context.client.mineBlock(); + }); }); class MockAztecNodeAdmin implements AztecNodeAdmin { @@ -197,4 +209,13 @@ class MockAztecNodeAdmin implements AztecNodeAdmin { reloadKeystore(): Promise { return Promise.resolve(); } + 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-admin.ts b/yarn-project/stdlib/src/interfaces/aztec-node-admin.ts index 8c3f41786dce..a9d0d6ab7b71 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node-admin.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node-admin.ts @@ -70,6 +70,15 @@ export interface AztecNodeAdmin { * A validator with an unknown publisher key will cause the reload to be rejected. */ reloadKeystore(): Promise; + + /** 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; } // L1 contracts are not mutable via admin updates. @@ -109,6 +118,9 @@ export const AztecNodeAdminApiSchema: ApiSchemaFor = { .args(z.union([z.bigint(), z.literal('all'), z.literal('current')])) .returns(z.array(OffenseSchema)), reloadKeystore: z.function().returns(z.void()), + 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 createAztecNodeAdminClient( 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, ); From 471ff1ff7c587f08c705c30609acb7714040a303 Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 27 Mar 2026 05:26:17 +0000 Subject: [PATCH 2/8] compilation fixes and readability improvements --- .../aztec-node/src/aztec-node/server.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 314b9cce059e..efc76bddf6f5 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -1649,7 +1649,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { this.log.info('Keystore reloaded: coinbase, feeRecipient, and attester keys updated'); } - private getEthCheatCodes(): EthCheatCodes { + #getEthCheatCodes(): EthCheatCodes { if (!this.ethCheatCodes) { this.ethCheatCodes = new EthCheatCodes(this.config.l1RpcUrls, this.dateProvider); } @@ -1657,23 +1657,23 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { } /** Updates the date provider to match the given L1 timestamp, if it supports time manipulation. */ - private updateDateProvider(timestampInSeconds: number): void { + #updateDateProvider(timestampInSeconds: number): void { if ('setTime' in this.dateProvider) { (this.dateProvider as { setTime(ms: number): void }).setTime(timestampInSeconds * 1000); } } public async setNextBlockTimestamp(timestamp: number): Promise { - const ethCheatCodes = this.getEthCheatCodes(); + const ethCheatCodes = this.#getEthCheatCodes(); await ethCheatCodes.setNextBlockTimestamp(timestamp); - this.updateDateProvider(timestamp); + this.#updateDateProvider(timestamp); } public async advanceNextBlockTimestampBy(duration: number): Promise { - const ethCheatCodes = this.getEthCheatCodes(); - const currentTimestamp = await ethCheatCodes.timestamp(); - await ethCheatCodes.setNextBlockTimestamp(currentTimestamp + duration); - this.updateDateProvider(currentTimestamp + duration); + const ethCheatCodes = this.#getEthCheatCodes(); + const currentL1Timestamp = await ethCheatCodes.lastBlockTimestamp(); + await ethCheatCodes.setNextBlockTimestamp(currentL1Timestamp + duration); + this.#updateDateProvider(currentL1Timestamp + duration); } public async mineBlock(): Promise { @@ -1684,12 +1684,12 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { const currentBlockNumber = await this.getBlockNumber(); // Mine one L1 block — this uses any pending evm_setNextBlockTimestamp - const ethCheatCodes = this.getEthCheatCodes(); + 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.timestamp()); + const currentL1Timestamp = BigInt(await ethCheatCodes.lastBlockTimestamp()); const currentSlot = getSlotAtTimestamp(currentL1Timestamp, l1Constants); const latestBlock = await this.getBlock('latest'); @@ -1701,8 +1701,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { } // Update dateProvider to match L1 time - const newTimestamp = await ethCheatCodes.timestamp(); - this.updateDateProvider(newTimestamp); + const newTimestamp = await ethCheatCodes.lastBlockTimestamp(); + this.#updateDateProvider(newTimestamp); // Temporarily set minTxsPerBlock to 0 so the sequencer produces a block even with no txs const originalMinTxsPerBlock = this.sequencer.getSequencer().getConfig().minTxsPerBlock; From 13792be19467bd1bb2ccf648d3b89405f26e4a72 Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 27 Mar 2026 06:05:58 +0000 Subject: [PATCH 3/8] wip --- yarn-project/aztec-node/src/aztec-node/server.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index efc76bddf6f5..df53bcdb04d4 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -1656,24 +1656,26 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { return this.ethCheatCodes; } - /** Updates the date provider to match the given L1 timestamp, if it supports time manipulation. */ - #updateDateProvider(timestampInSeconds: number): void { - if ('setTime' in this.dateProvider) { - (this.dateProvider as { setTime(ms: number): void }).setTime(timestampInSeconds * 1000); + /** 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.#updateDateProvider(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.#updateDateProvider(currentL1Timestamp + duration); + this.#updateDateProviderTimestampTo(currentL1Timestamp + duration); } public async mineBlock(): Promise { @@ -1702,7 +1704,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { // Update dateProvider to match L1 time const newTimestamp = await ethCheatCodes.lastBlockTimestamp(); - this.#updateDateProvider(newTimestamp); + 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; From a4ab47ba0da89b6f9fd1f78673f623df233319a6 Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 30 Mar 2026 05:01:57 +0000 Subject: [PATCH 4/8] erroring out if current slot is less than lat block slot --- .../aztec-node/src/aztec-node/server.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index df53bcdb04d4..4184545f5c7b 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -1695,10 +1695,25 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { const currentSlot = getSlotAtTimestamp(currentL1Timestamp, l1Constants); const latestBlock = await this.getBlock('latest'); - const lastBlockSlot = latestBlock ? BigInt(latestBlock.header.globalVariables.slotNumber) : 0n; + const lastBlockSlot = latestBlock ? BigInt(latestBlock.header.globalVariables.slotNumber) : SlotNumber(0); - if (BigInt(currentSlot) <= lastBlockSlot) { + 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 ${BigInt(lastBlockSlot) + 1n}`, + ); + // warp mines a block - hence we don't need to do it manually here await ethCheatCodes.warp(Number(nextSlotTimestamp)); } From f9dc604c2bbd4875fc447acecfe715bb471ce8bf Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 30 Mar 2026 05:54:12 +0000 Subject: [PATCH 5/8] cleanup --- yarn-project/aztec-node/src/aztec-node/server.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 4184545f5c7b..1b50f19e990d 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -1695,7 +1695,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { const currentSlot = getSlotAtTimestamp(currentL1Timestamp, l1Constants); const latestBlock = await this.getBlock('latest'); - const lastBlockSlot = latestBlock ? BigInt(latestBlock.header.globalVariables.slotNumber) : SlotNumber(0); + 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 @@ -1710,9 +1710,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { 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 ${BigInt(lastBlockSlot) + 1n}`, - ); + 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)); } From f480d5465624388e264ceedb41aee8b25500a9e4 Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 30 Mar 2026 13:37:42 +0000 Subject: [PATCH 6/8] wip --- .../aztec-node/src/aztec-node/server.test.ts | 169 +++++++++++++++++- .../end-to-end/src/e2e_cheat_codes.test.ts | 20 ++- 2 files changed, 184 insertions(+), 5 deletions(-) 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 79232fc78438..1088a4e788aa 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'; @@ -961,6 +962,170 @@ 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); + // TestDateProvider uses offsets from wall clock, so allow small drift + const targetMs = targetTimestamp * 1000; + expect(testDateProvider.now()).toBeGreaterThanOrEqual(targetMs); + expect(testDateProvider.now()).toBeLessThan(targetMs + 100); + }); + }); + + 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()).toBe(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/end-to-end/src/e2e_cheat_codes.test.ts b/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts index 5198a6ee26af..1911489c162b 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 @@ -179,10 +179,24 @@ describe('e2e_cheat_codes', () => { }); it('mineBlock without setting timestamp still produces a new block', async () => { - const blockNumberBefore = await aztecNode.getBlockNumber(); + const firstBlockNumber = await aztecNode.getBlockNumber(); + const firstBlock = await aztecNode.getBlock(firstBlockNumber); + const firstSlot = firstBlock!.header.globalVariables.slotNumber; + await aztecNodeAdmin.mineBlock(); - const blockNumberAfter = await aztecNode.getBlockNumber(); - expect(blockNumberAfter).toBeGreaterThan(blockNumberBefore); + 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 aztecNodeAdmin.mineBlock(); + const thirdBlockNumber = await aztecNode.getBlockNumber(); + const thirdBlock = await aztecNode.getBlock(thirdBlockNumber); + const thirdSlot = thirdBlock!.header.globalVariables.slotNumber; + + expect(thirdSlot).toBeGreaterThan(secondSlot); }); }); }); From 2a964152bdf3512112769b75e84937f334f387fe Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 30 Mar 2026 14:57:49 +0000 Subject: [PATCH 7/8] fixes --- yarn-project/aztec-node/src/aztec-node/server.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 1088a4e788aa..931b30f91a3a 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.test.ts @@ -1061,7 +1061,7 @@ describe('aztec node', () => { 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 }); + expect(updateCalls[1][0]).toEqual({ minTxsPerBlock: INITIAL_MIN_TXS_PER_BLOCK }); }); }); @@ -1073,10 +1073,8 @@ describe('aztec node', () => { await nodeWithSequencer.setNextBlockTimestamp(targetTimestamp); expect(mockEthCheatCodes.setNextBlockTimestamp).toHaveBeenCalledWith(targetTimestamp); - // TestDateProvider uses offsets from wall clock, so allow small drift const targetMs = targetTimestamp * 1000; expect(testDateProvider.now()).toBeGreaterThanOrEqual(targetMs); - expect(testDateProvider.now()).toBeLessThan(targetMs + 100); }); }); @@ -1088,7 +1086,7 @@ describe('aztec node', () => { await nodeWithSequencer.advanceNextBlockTimestampBy(100); expect(mockEthCheatCodes.setNextBlockTimestamp).toHaveBeenCalledWith(600); - expect(testDateProvider.now()).toBe(600_000); + expect(testDateProvider.now()).toBeGreaterThanOrEqual(600_000); }); }); From b715407e7a2e3d76dcc30ceb109dd98b1401a34f Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 2 Apr 2026 10:53:05 +0000 Subject: [PATCH 8/8] AztecNodeDebug --- .../aztec-node/src/aztec-node/server.ts | 3 +- .../aztec/src/cli/aztec_start_action.ts | 3 +- .../end-to-end/src/e2e_cheat_codes.test.ts | 20 ++++---- .../src/interfaces/aztec-node-admin.test.ts | 21 -------- .../stdlib/src/interfaces/aztec-node-admin.ts | 12 ----- .../src/interfaces/aztec-node-debug.test.ts | 49 +++++++++++++++++++ .../stdlib/src/interfaces/aztec-node-debug.ts | 40 +++++++++++++++ yarn-project/stdlib/src/interfaces/client.ts | 1 + 8 files changed, 104 insertions(+), 45 deletions(-) create mode 100644 yarn-project/stdlib/src/interfaces/aztec-node-debug.test.ts create mode 100644 yarn-project/stdlib/src/interfaces/aztec-node-debug.ts diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 1b50f19e990d..d3bdcefd2571 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -69,6 +69,7 @@ import { type AztecNodeAdmin, type AztecNodeAdminConfig, AztecNodeAdminConfigSchema, + type AztecNodeDebug, type GetContractClassLogsResponse, type GetPublicLogsResponse, } from '@aztec/stdlib/interfaces/client'; @@ -129,7 +130,7 @@ 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; 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 1911489c162b..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,7 +4,7 @@ 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, AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; +import type { AztecNode, AztecNodeDebug } from '@aztec/stdlib/interfaces/client'; import { parseEther } from 'viem'; import { mnemonicToAccount } from 'viem/accounts'; @@ -137,15 +137,15 @@ describe('e2e_cheat_codes', () => { }); }); - describe('L2 admin time manipulation', () => { + describe('L2 debug time manipulation', () => { let context: EndToEndContext; let aztecNode: AztecNode; - let aztecNodeAdmin: AztecNodeAdmin; + let aztecNodeDebug: AztecNodeDebug; beforeAll(async () => { context = await setup(0); aztecNode = context.aztecNode; - aztecNodeAdmin = context.aztecNodeAdmin; + aztecNodeDebug = context.aztecNodeService; }); afterAll(async () => { @@ -154,8 +154,8 @@ describe('e2e_cheat_codes', () => { it('setNextBlockTimestamp + mineBlock produces a block with the target timestamp', async () => { const targetTimestamp = Math.floor(Date.now() / 1000) + 1000; - await aztecNodeAdmin.setNextBlockTimestamp(targetTimestamp); - await aztecNodeAdmin.mineBlock(); + await aztecNodeDebug.setNextBlockTimestamp(targetTimestamp); + await aztecNodeDebug.mineBlock(); const blockNumber = await aztecNode.getBlockNumber(); const block = await aztecNode.getBlock(blockNumber); @@ -168,8 +168,8 @@ describe('e2e_cheat_codes', () => { const timestampBefore = Number(blockBeforeAdvance!.header.globalVariables.timestamp); const advancement = 100; - await aztecNodeAdmin.advanceNextBlockTimestampBy(advancement); - await aztecNodeAdmin.mineBlock(); + await aztecNodeDebug.advanceNextBlockTimestampBy(advancement); + await aztecNodeDebug.mineBlock(); const blockNumber = await aztecNode.getBlockNumber(); const block = await aztecNode.getBlock(blockNumber); @@ -183,7 +183,7 @@ describe('e2e_cheat_codes', () => { const firstBlock = await aztecNode.getBlock(firstBlockNumber); const firstSlot = firstBlock!.header.globalVariables.slotNumber; - await aztecNodeAdmin.mineBlock(); + await aztecNodeDebug.mineBlock(); const secondBlockNumber = await aztecNode.getBlockNumber(); const secondBlock = await aztecNode.getBlock(secondBlockNumber); const secondSlot = secondBlock!.header.globalVariables.slotNumber; @@ -191,7 +191,7 @@ describe('e2e_cheat_codes', () => { expect(secondSlot).toBeGreaterThan(firstSlot); // Second call is still in the same slot, so mineBlock must warp to the next slot - await aztecNodeAdmin.mineBlock(); + await aztecNodeDebug.mineBlock(); const thirdBlockNumber = await aztecNode.getBlockNumber(); const thirdBlock = await aztecNode.getBlock(thirdBlockNumber); const thirdSlot = thirdBlock!.header.globalVariables.slotNumber; diff --git a/yarn-project/stdlib/src/interfaces/aztec-node-admin.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node-admin.test.ts index 0177f19b50ba..6387858d12da 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node-admin.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node-admin.test.ts @@ -89,18 +89,6 @@ describe('AztecNodeAdminApiSchema', () => { it('reloadKeystore', async () => { await context.client.reloadKeystore(); }); - - it('setNextBlockTimestamp', async () => { - await context.client.setNextBlockTimestamp(1000000); - }); - - it('advanceNextBlockTimestampBy', async () => { - await context.client.advanceNextBlockTimestampBy(60); - }); - - it('mineBlock', async () => { - await context.client.mineBlock(); - }); }); class MockAztecNodeAdmin implements AztecNodeAdmin { @@ -209,13 +197,4 @@ class MockAztecNodeAdmin implements AztecNodeAdmin { reloadKeystore(): Promise { return Promise.resolve(); } - 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-admin.ts b/yarn-project/stdlib/src/interfaces/aztec-node-admin.ts index a9d0d6ab7b71..8c3f41786dce 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node-admin.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node-admin.ts @@ -70,15 +70,6 @@ export interface AztecNodeAdmin { * A validator with an unknown publisher key will cause the reload to be rejected. */ reloadKeystore(): Promise; - - /** 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; } // L1 contracts are not mutable via admin updates. @@ -118,9 +109,6 @@ export const AztecNodeAdminApiSchema: ApiSchemaFor = { .args(z.union([z.bigint(), z.literal('all'), z.literal('current')])) .returns(z.array(OffenseSchema)), reloadKeystore: z.function().returns(z.void()), - 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 createAztecNodeAdminClient( 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';