diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index fa7904296cc..c1c56911834 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -58,7 +58,7 @@ import { type ContractArtifact } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { padArrayEnd } from '@aztec/foundation/collection'; import { type Logger, createLogger } from '@aztec/foundation/log'; -import { Timer } from '@aztec/foundation/timer'; +import { DateProvider, Timer } from '@aztec/foundation/timer'; import { type AztecKVStore } from '@aztec/kv-store'; import { openTmpStore } from '@aztec/kv-store/lmdb'; import { SHA256Trunc, StandardTree, UnbalancedTree } from '@aztec/merkle-tree'; @@ -136,10 +136,12 @@ export class AztecNodeService implements AztecNode { telemetry?: TelemetryClient; logger?: Logger; publisher?: L1Publisher; + dateProvider?: DateProvider; } = {}, ): Promise { const telemetry = deps.telemetry ?? new NoopTelemetryClient(); const log = deps.logger ?? createLogger('node'); + const dateProvider = deps.dateProvider ?? new DateProvider(); const ethereumChain = createEthereumChain(config.l1RpcUrl, config.l1ChainId); //validate that the actual chain id matches that specified in configuration if (config.l1ChainId !== ethereumChain.chainInfo.id) { @@ -152,7 +154,8 @@ export class AztecNodeService implements AztecNode { // we identify the P2P transaction protocol by using the rollup contract address. // this may well change in future - config.transactionProtocol = `/aztec/tx/${config.l1Contracts.rollupAddress.toString()}`; + const rollupAddress = config.l1Contracts.rollupAddress; + config.transactionProtocol = `/aztec/tx/${rollupAddress.toString()}`; // now create the merkle trees and the world state synchronizer const worldStateSynchronizer = await createWorldStateSynchronizer(config, archiver, telemetry); @@ -167,7 +170,7 @@ export class AztecNodeService implements AztecNode { // start both and wait for them to sync from the block source await Promise.all([p2pClient.start(), worldStateSynchronizer.start()]); - const validatorClient = await createValidatorClient(config, config.l1Contracts.rollupAddress, p2pClient, telemetry); + const validatorClient = await createValidatorClient(config, rollupAddress, { p2pClient, telemetry, dateProvider }); // now create the sequencer const sequencer = config.disableValidator diff --git a/yarn-project/end-to-end/package.json b/yarn-project/end-to-end/package.json index c51b89150bb..4a31d3e2ee1 100644 --- a/yarn-project/end-to-end/package.json +++ b/yarn-project/end-to-end/package.json @@ -98,7 +98,6 @@ "devDependencies": { "0x": "^5.7.0", "@jest/globals": "^29.5.0", - "@sinonjs/fake-timers": "^13.0.5", "@types/jest": "^29.5.0", "@types/js-yaml": "^4.0.9", "@types/lodash.chunk": "^4.2.9", diff --git a/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts b/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts index afa5cdbb1cd..7a74cb96163 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts @@ -76,6 +76,7 @@ describe('e2e_p2p_network', () => { t.logger.info('Creating nodes'); nodes = await createNodes( t.ctx.aztecNodeConfig, + t.ctx.dateProvider, t.bootstrapNodeEnr, NUM_NODES, BOOT_NODE_UDP_PORT, 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 09d282c4f43..a218e4e7fe3 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 @@ -93,7 +93,7 @@ export class P2PNetworkTest { */ public async syncMockSystemTime() { this.logger.info('Syncing mock system time'); - const { timer, deployL1ContractsValues } = this.ctx!; + const { dateProvider, deployL1ContractsValues } = this.ctx!; // Send a tx and only update the time after the tx is mined, as eth time is not continuous const tx = await deployL1ContractsValues.walletClient.sendTransaction({ to: this.baseAccount.address, @@ -104,8 +104,7 @@ export class P2PNetworkTest { hash: tx, }); const timestamp = await deployL1ContractsValues.publicClient.getBlock({ blockNumber: receipt.blockNumber }); - timer.setSystemTime(Number(timestamp.timestamp) * 1000); - this.logger.info(`Synced mock system time to ${timestamp.timestamp * 1000n}`); + dateProvider.setTime(Number(timestamp.timestamp) * 1000); } static async create({ @@ -133,7 +132,7 @@ export class P2PNetworkTest { async applyBaseSnapshots() { await this.snapshotManager.snapshot( 'add-validators', - async ({ deployL1ContractsValues, aztecNodeConfig, timer }) => { + async ({ deployL1ContractsValues, aztecNodeConfig, dateProvider }) => { const rollup = getContract({ address: deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(), abi: RollupAbi, @@ -203,7 +202,7 @@ export class P2PNetworkTest { // Set the system time in the node, only after we have warped the time and waited for a block // Time is only set in the NEXT block - timer.setSystemTime(Number(timestamp) * 1000); + dateProvider.setTime(Number(timestamp) * 1000); }, ); } @@ -244,7 +243,7 @@ export class P2PNetworkTest { async removeInitialNode() { await this.snapshotManager.snapshot( 'remove-inital-validator', - async ({ deployL1ContractsValues, aztecNode, timer }) => { + async ({ deployL1ContractsValues, aztecNode, dateProvider }) => { // Send and await a tx to make sure we mine a block for the warp to correctly progress. const receipt = await deployL1ContractsValues.publicClient.waitForTransactionReceipt({ hash: await deployL1ContractsValues.walletClient.sendTransaction({ @@ -256,7 +255,7 @@ export class P2PNetworkTest { const block = await deployL1ContractsValues.publicClient.getBlock({ blockNumber: receipt.blockNumber, }); - timer.setSystemTime(Number(block.timestamp) * 1000); + dateProvider.setTime(Number(block.timestamp) * 1000); await aztecNode.stop(); }, diff --git a/yarn-project/end-to-end/src/e2e_p2p/rediscovery.test.ts b/yarn-project/end-to-end/src/e2e_p2p/rediscovery.test.ts index 81d069b65c7..f7a8ffbfb4a 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/rediscovery.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/rediscovery.test.ts @@ -46,6 +46,7 @@ describe('e2e_p2p_rediscovery', () => { const contexts: NodeContext[] = []; nodes = await createNodes( t.ctx.aztecNodeConfig, + t.ctx.dateProvider, t.bootstrapNodeEnr, NUM_NODES, BOOT_NODE_UDP_PORT, @@ -72,6 +73,7 @@ describe('e2e_p2p_rediscovery', () => { const newNode = await createNode( t.ctx.aztecNodeConfig, + t.ctx.dateProvider, i + 1 + BOOT_NODE_UDP_PORT, undefined, i, diff --git a/yarn-project/end-to-end/src/e2e_p2p/reex.test.ts b/yarn-project/end-to-end/src/e2e_p2p/reex.test.ts index 10b12d4da59..be5475755c0 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/reex.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/reex.test.ts @@ -65,6 +65,7 @@ describe('e2e_p2p_reex', () => { nodes = await createNodes( t.ctx.aztecNodeConfig, + t.ctx.dateProvider, t.bootstrapNodeEnr, NUM_NODES, BOOT_NODE_UDP_PORT, diff --git a/yarn-project/end-to-end/src/e2e_p2p/reqresp.test.ts b/yarn-project/end-to-end/src/e2e_p2p/reqresp.test.ts index d9be627e861..fed938743d4 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/reqresp.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/reqresp.test.ts @@ -65,6 +65,7 @@ describe('e2e_p2p_reqresp_tx', () => { t.logger.info('Creating nodes'); nodes = await createNodes( t.ctx.aztecNodeConfig, + t.ctx.dateProvider, t.bootstrapNodeEnr, NUM_NODES, BOOT_NODE_UDP_PORT, diff --git a/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts b/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts index f7ef6b05195..9499bce2eef 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts @@ -130,6 +130,7 @@ describe('e2e_p2p_governance_proposer', () => { t.logger.info('Creating nodes'); nodes = await createNodes( { ...t.ctx.aztecNodeConfig, governanceProposerPayload: newPayloadAddress }, + t.ctx.dateProvider, t.bootstrapNodeEnr, NUM_NODES, BOOT_NODE_UDP_PORT, 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 b69ae1c69e0..1d31d41ab6c 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 @@ -4,6 +4,7 @@ import { type AztecNodeConfig, AztecNodeService } from '@aztec/aztec-node'; import { type SentTx, createLogger } from '@aztec/aztec.js'; import { type AztecAddress } from '@aztec/circuits.js'; +import { type DateProvider } from '@aztec/foundation/timer'; import { type PXEService } from '@aztec/pxe'; import getPort from 'get-port'; @@ -34,6 +35,7 @@ export function generatePrivateKeys(startIndex: number, numberOfKeys: number): ` export function createNodes( config: AztecNodeConfig, + dateProvider: DateProvider, bootstrapNodeEnr: string, numNodes: number, bootNodePort: number, @@ -46,7 +48,7 @@ export function createNodes( const port = bootNodePort + i + 1; const dataDir = dataDirectory ? `${dataDirectory}-${i}` : undefined; - const nodePromise = createNode(config, port, bootstrapNodeEnr, i, dataDir, metricsPort); + const nodePromise = createNode(config, dateProvider, port, bootstrapNodeEnr, i, dataDir, metricsPort); nodePromises.push(nodePromise); } return Promise.all(nodePromises); @@ -55,6 +57,7 @@ export function createNodes( // creates a P2P enabled instance of Aztec Node Service export async function createNode( config: AztecNodeConfig, + dateProvider: DateProvider, tcpPort: number, bootstrapNode: string | undefined, accountIndex: number, @@ -68,6 +71,7 @@ export async function createNode( return await AztecNodeService.createAndSync(validatorConfig, { telemetry: telemetryClient, logger: createLogger(`node:${tcpPort}`), + dateProvider, }); } 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 6743a7ef2c4..5581397e60c 100644 --- a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts +++ b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts @@ -20,11 +20,11 @@ import { startAnvil } from '@aztec/ethereum/test'; import { asyncMap } from '@aztec/foundation/async-map'; import { createLogger } from '@aztec/foundation/log'; import { resolver, reviver } from '@aztec/foundation/serialize'; +import { TestDateProvider } from '@aztec/foundation/timer'; import { type ProverNode } from '@aztec/prover-node'; import { type PXEService, createPXEService, getPXEServiceConfig } from '@aztec/pxe'; import { createAndStartTelemetryClient, getConfigEnvVars as getTelemetryConfig } from '@aztec/telemetry-client/start'; -import { type InstalledClock, install } from '@sinonjs/fake-timers'; import { type Anvil } from '@viem/anvil'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; import { copySync, removeSync } from 'fs-extra/esm'; @@ -50,7 +50,7 @@ export type SubsystemsContext = { proverNode?: ProverNode; watcher: AnvilTestWatcher; cheatCodes: CheatCodes; - timer: InstalledClock; + dateProvider: TestDateProvider; }; type SnapshotEntry = { @@ -250,7 +250,6 @@ async function teardown(context: SubsystemsContext | undefined) { await context.acvmConfig?.cleanup(); await context.anvil.stop(); await context.watcher.stop(); - context.timer?.uninstall(); } catch (err) { getLogger().error('Error during teardown', err); } @@ -272,9 +271,6 @@ async function setupFromFresh( ): Promise { logger.verbose(`Initializing state...`); - // Use sinonjs fake timers - const timer = install({ shouldAdvanceTime: true, advanceTimeDelta: 20, toFake: ['Date'] }); - // Fetch the AztecNode config. // TODO: For some reason this is currently the union of a bunch of subsystems. That needs fixing. const aztecNodeConfig: AztecNodeConfig & SetupOptions = { ...getConfigEnvVars(), ...opts }; @@ -358,7 +354,8 @@ async function setupFromFresh( const telemetry = await getEndToEndTestTelemetryClient(opts.metricsPort); logger.verbose('Creating and synching an aztec node...'); - const aztecNode = await AztecNodeService.createAndSync(aztecNodeConfig, { telemetry }); + const dateProvider = new TestDateProvider(); + const aztecNode = await AztecNodeService.createAndSync(aztecNodeConfig, { telemetry, dateProvider }); let proverNode: ProverNode | undefined = undefined; if (opts.startProverNode) { @@ -392,7 +389,7 @@ async function setupFromFresh( proverNode, watcher, cheatCodes, - timer, + dateProvider, }; } @@ -402,9 +399,6 @@ async function setupFromFresh( async function setupFromState(statePath: string, logger: Logger): Promise { logger.verbose(`Initializing with saved state at ${statePath}...`); - // TODO: make one function - const timer = install({ shouldAdvanceTime: true, advanceTimeDelta: 20, toFake: ['Date'] }); - // TODO: For some reason this is currently the union of a bunch of subsystems. That needs fixing. const aztecNodeConfig: AztecNodeConfig & SetupOptions = JSON.parse( readFileSync(`${statePath}/aztec_node_config.json`, 'utf-8'), @@ -445,7 +439,8 @@ async function setupFromState(statePath: string, logger: Logger): Promise Promise; }; @@ -415,7 +419,8 @@ export async function setup( const telemetry = await telemetryPromise; const publisher = new TestL1Publisher(config, telemetry); - const aztecNode = await AztecNodeService.createAndSync(config, { telemetry, publisher }); + const dateProvider = new TestDateProvider(); + const aztecNode = await AztecNodeService.createAndSync(config, { telemetry, publisher, dateProvider }); const sequencer = aztecNode.getSequencer(); let proverNode: ProverNode | undefined = undefined; @@ -466,6 +471,7 @@ export async function setup( cheatCodes, sequencer, watcher, + dateProvider, teardown, }; } diff --git a/yarn-project/epoch-cache/src/epoch_cache.ts b/yarn-project/epoch-cache/src/epoch_cache.ts index b0c019c6b63..1cbe315c2bb 100644 --- a/yarn-project/epoch-cache/src/epoch_cache.ts +++ b/yarn-project/epoch-cache/src/epoch_cache.ts @@ -7,6 +7,7 @@ import { import { RollupContract, createEthereumChain } from '@aztec/ethereum'; import { EthAddress } from '@aztec/foundation/eth-address'; import { type Logger, createLogger } from '@aztec/foundation/log'; +import { DateProvider } from '@aztec/foundation/timer'; import { EventEmitter } from 'node:events'; import { createPublicClient, encodeAbiParameters, http, keccak256 } from 'viem'; @@ -40,6 +41,7 @@ export class EpochCache extends EventEmitter<{ committeeChanged: [EthAddress[], initialValidators: EthAddress[] = [], initialSampleSeed: bigint = 0n, private readonly l1constants: L1RollupConstants = EmptyL1RollupConstants, + private readonly dateProvider: DateProvider = new DateProvider(), ) { super(); this.committee = initialValidators; @@ -47,10 +49,14 @@ export class EpochCache extends EventEmitter<{ committeeChanged: [EthAddress[], this.log.debug(`Initialized EpochCache with constants and validators`, { l1constants, initialValidators }); - this.cachedEpoch = getEpochNumberAtTimestamp(BigInt(Math.floor(Date.now() / 1000)), this.l1constants); + this.cachedEpoch = getEpochNumberAtTimestamp(this.nowInSeconds(), this.l1constants); } - static async create(rollupAddress: EthAddress, config?: EpochCacheConfig) { + static async create( + rollupAddress: EthAddress, + config?: EpochCacheConfig, + deps: { dateProvider?: DateProvider } = {}, + ) { config = config ?? getEpochCacheConfigEnvVars(); const chain = createEthereumChain(config.l1RpcUrl, config.l1ChainId); @@ -81,16 +87,20 @@ export class EpochCache extends EventEmitter<{ committeeChanged: [EthAddress[], initialValidators.map(v => EthAddress.fromString(v)), sampleSeed, l1RollupConstants, + deps.dateProvider, ); } + private nowInSeconds(): bigint { + return BigInt(Math.floor(this.dateProvider.now() / 1000)); + } + getEpochAndSlotNow(): EpochAndSlot { - const now = BigInt(Math.floor(Date.now() / 1000)); - return this.getEpochAndSlotAtTimestamp(now); + return this.getEpochAndSlotAtTimestamp(this.nowInSeconds()); } getEpochAndSlotInNextSlot(): EpochAndSlot { - const nextSlotTs = BigInt(Math.floor(Date.now() / 1000) + this.l1constants.slotDuration); + const nextSlotTs = this.nowInSeconds() + BigInt(this.l1constants.slotDuration); return this.getEpochAndSlotAtTimestamp(nextSlotTs); } diff --git a/yarn-project/foundation/src/timer/date.test.ts b/yarn-project/foundation/src/timer/date.test.ts new file mode 100644 index 00000000000..55aeac96d55 --- /dev/null +++ b/yarn-project/foundation/src/timer/date.test.ts @@ -0,0 +1,33 @@ +import { sleep } from '../sleep/index.js'; +import { TestDateProvider } from './date.js'; + +describe('TestDateProvider', () => { + let dateProvider: TestDateProvider; + beforeEach(() => { + dateProvider = new TestDateProvider(); + }); + + it('should return the current datetime', () => { + const currentTime = Date.now(); + const result = dateProvider.now(); + expect(result).toBeGreaterThanOrEqual(currentTime); + expect(result).toBeLessThan(currentTime + 100); + }); + + it('should return the overridden datetime', () => { + const overriddenTime = Date.now() + 1000; + dateProvider.setTime(overriddenTime); + const result = dateProvider.now(); + expect(result).toBeGreaterThanOrEqual(overriddenTime); + expect(result).toBeLessThan(overriddenTime + 100); + }); + + it('should keep ticking after overriding', async () => { + const overriddenTime = Date.now() + 1000; + dateProvider.setTime(overriddenTime); + await sleep(510); + const result = dateProvider.now(); + expect(result).toBeGreaterThanOrEqual(overriddenTime + 500); + expect(result).toBeLessThan(overriddenTime + 600); + }); +}); diff --git a/yarn-project/foundation/src/timer/date.ts b/yarn-project/foundation/src/timer/date.ts new file mode 100644 index 00000000000..f1ed1ee4361 --- /dev/null +++ b/yarn-project/foundation/src/timer/date.ts @@ -0,0 +1,24 @@ +import { createLogger } from '../log/pino-logger.js'; + +/** Returns current datetime. */ +export class DateProvider { + public now(): number { + return Date.now(); + } +} + +/** Returns current datetime and allows to override it. */ +export class TestDateProvider implements DateProvider { + private offset = 0; + + constructor(private readonly logger = createLogger('foundation:test-date-provider')) {} + + public now(): number { + return Date.now() + this.offset; + } + + public setTime(timeMs: number) { + this.offset = timeMs - Date.now(); + this.logger.warn(`Time set to ${timeMs}`); + } +} diff --git a/yarn-project/foundation/src/timer/index.ts b/yarn-project/foundation/src/timer/index.ts index 972248c8f41..afb200bb32f 100644 --- a/yarn-project/foundation/src/timer/index.ts +++ b/yarn-project/foundation/src/timer/index.ts @@ -1,3 +1,4 @@ export { TimeoutTask, executeTimeoutWithCustomError } from './timeout.js'; export { Timer } from './timer.js'; export { elapsed, elapsedSync } from './elapsed.js'; +export * from './date.js'; diff --git a/yarn-project/validator-client/src/factory.ts b/yarn-project/validator-client/src/factory.ts index 7ea8b09e8d6..3382163f408 100644 --- a/yarn-project/validator-client/src/factory.ts +++ b/yarn-project/validator-client/src/factory.ts @@ -1,5 +1,6 @@ import { EpochCache, type EpochCacheConfig } from '@aztec/epoch-cache'; import { type EthAddress } from '@aztec/foundation/eth-address'; +import { type DateProvider } from '@aztec/foundation/timer'; import { type P2P } from '@aztec/p2p'; import { type TelemetryClient } from '@aztec/telemetry-client'; @@ -11,8 +12,11 @@ import { ValidatorClient } from './validator.js'; export async function createValidatorClient( config: ValidatorClientConfig & EpochCacheConfig, rollupAddress: EthAddress, - p2pClient: P2P, - telemetry: TelemetryClient, + deps: { + p2pClient: P2P; + telemetry: TelemetryClient; + dateProvider: DateProvider; + }, ) { if (config.disableValidator) { return undefined; @@ -22,7 +26,7 @@ export async function createValidatorClient( } // Create the epoch cache - const epochCache = await EpochCache.create(rollupAddress, /*l1TimestampSource,*/ config); + const epochCache = await EpochCache.create(rollupAddress, config, deps); - return ValidatorClient.new(config, epochCache, p2pClient, telemetry); + return ValidatorClient.new(config, epochCache, deps.p2pClient, deps.telemetry); } diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 9a83f7b8135..b14f188618a 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -550,7 +550,6 @@ __metadata: "@aztec/world-state": "workspace:^" "@jest/globals": ^29.5.0 "@noble/curves": ^1.0.0 - "@sinonjs/fake-timers": ^13.0.5 "@swc/core": ^1.4.11 "@swc/jest": ^0.2.36 "@types/fs-extra": ^11.0.2 @@ -4536,7 +4535,7 @@ __metadata: languageName: node linkType: hard -"@sinonjs/commons@npm:^3.0.0, @sinonjs/commons@npm:^3.0.1": +"@sinonjs/commons@npm:^3.0.0": version: 3.0.1 resolution: "@sinonjs/commons@npm:3.0.1" dependencies: @@ -4554,15 +4553,6 @@ __metadata: languageName: node linkType: hard -"@sinonjs/fake-timers@npm:^13.0.5": - version: 13.0.5 - resolution: "@sinonjs/fake-timers@npm:13.0.5" - dependencies: - "@sinonjs/commons": ^3.0.1 - checksum: b1c6ba87fadb7666d3aa126c9e8b4ac32b2d9e84c9e5fd074aa24cab3c8342fd655459de014b08e603be1e6c24c9f9716d76d6d2a36c50f59bb0091be61601dd - languageName: node - linkType: hard - "@swc/core-darwin-arm64@npm:1.5.5": version: 1.5.5 resolution: "@swc/core-darwin-arm64@npm:1.5.5"