From c93cd8e52a54398abc658a6ba0e7733636950c83 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] 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 223f4d1e29ea..9cf7edf6e0d8 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.test.ts @@ -189,6 +189,7 @@ describe('aztec node', () => { epochCache, getPackageVersion() ?? '', new TestCircuitVerifier(), + new DateProvider(), ); }); @@ -596,6 +597,7 @@ describe('aztec node', () => { epochCache, getPackageVersion() ?? '', new TestCircuitVerifier(), + new DateProvider(), undefined, undefined, undefined, @@ -784,6 +786,7 @@ describe('aztec node', () => { epochCache, getPackageVersion() ?? '', 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 203d2b4f866a..34cb88dbdeb9 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -8,12 +8,14 @@ import { createEthereumChain } from '@aztec/ethereum/chain'; import { getPublicClient } from '@aztec/ethereum/client'; import { RegistryContract, RollupContract } from '@aztec/ethereum/contracts'; import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses'; +import { EthCheatCodes } from '@aztec/ethereum/test'; import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; import { 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'; @@ -57,6 +59,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 { @@ -126,6 +129,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; @@ -151,6 +155,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { protected readonly epochCache: EpochCacheInterface, protected readonly packageVersion: string, private proofVerifier: ClientProtocolCircuitVerifier, + private dateProvider: DateProvider, private telemetry: TelemetryClient = getTelemetryClient(), private log = createLogger('node'), private blobClient?: BlobClientInterface, @@ -576,6 +581,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { epochCache, packageVersion, proofVerifier, + dateProvider, telemetry, log, blobClient, @@ -1602,6 +1608,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 27b0bf124dfc..b313c9b85075 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 22e1be967576..c5a8c4dd63fc 100644 --- a/yarn-project/sequencer-client/src/client/sequencer-client.ts +++ b/yarn-project/sequencer-client/src/client/sequencer-client.ts @@ -224,6 +224,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 d75788ea3cf4..0662916ae8e9 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -143,6 +143,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 bcf4b881603f..76b51cc83b3f 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 { @@ -198,4 +210,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 5976e9f346a6..32e9784a6abb 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'; @@ -59,6 +60,7 @@ export class TXEStateMachine { new MockEpochCache(), getPackageVersion() ?? '', new TestCircuitVerifier(), + new DateProvider(), undefined, log, );