Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion yarn-project/aztec/src/testing/anvil_test_watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export class AnvilTestWatcher {
return;
}

const l1Time = (await this.cheatcodes.timestamp()) * 1000;
const l1Time = (await this.cheatcodes.lastBlockTimestamp()) * 1000;
const wallTime = this.dateProvider.now();
if (l1Time > wallTime) {
this.logger.warn(`L1 is ahead of wall time. Syncing wall time to L1 time`);
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/aztec/src/testing/cheat_codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export class CheatCodes {
* @param duration - The duration to advance time by (in seconds)
*/
async warpL2TimeAtLeastBy(sequencerClient: SequencerClient, node: AztecNode, duration: bigint | number) {
const currentTimestamp = await this.eth.timestamp();
const currentTimestamp = await this.eth.lastBlockTimestamp();
const targetTimestamp = BigInt(currentTimestamp) + BigInt(duration);
await this.warpL2TimeAtLeastTo(sequencerClient, node, targetTimestamp);
}
Expand Down
8 changes: 4 additions & 4 deletions yarn-project/end-to-end/src/e2e_cheat_codes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,19 @@ describe('e2e_cheat_codes', () => {

it.each([100, 42, 99])(`setNextBlockTimestamp by %i`, async increment => {
const blockNumber = await ethCheatCodes.blockNumber();
const timestamp = await ethCheatCodes.timestamp();
const timestamp = await ethCheatCodes.lastBlockTimestamp();
await ethCheatCodes.setNextBlockTimestamp(timestamp + increment);

expect(await ethCheatCodes.timestamp()).toBe(timestamp);
expect(await ethCheatCodes.lastBlockTimestamp()).toBe(timestamp);

await ethCheatCodes.mine();

expect(await ethCheatCodes.blockNumber()).toBe(blockNumber + 1);
expect(await ethCheatCodes.timestamp()).toBe(timestamp + increment);
expect(await ethCheatCodes.lastBlockTimestamp()).toBe(timestamp + increment);
});

it('setNextBlockTimestamp to a past timestamp throws', async () => {
const timestamp = await ethCheatCodes.timestamp();
const timestamp = await ethCheatCodes.lastBlockTimestamp();
const pastTimestamp = timestamp - 1000;
await expect(async () => await ethCheatCodes.setNextBlockTimestamp(pastTimestamp)).rejects.toThrow(
'Timestamp error',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe('e2e_crowdfunding_and_claim', () => {
} = await setup(3));

// We set the deadline to a week from now
deadline = (await cheatCodes.eth.timestamp()) + 7 * 24 * 60 * 60;
deadline = (await cheatCodes.eth.lastBlockTimestamp()) + 7 * 24 * 60 * 60;

({ contract: donationToken } = await TokenContract.deploy(
wallet,
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ describe('e2e_p2p_add_rollup', () => {
const futureEpoch = EpochNumber.fromBigInt(500n + BigInt(await newRollup.getCurrentEpochNumber()));
const futureSlot = SlotNumber.fromBigInt(BigInt(futureEpoch) * BigInt(t.ctx.aztecNodeConfig.aztecEpochDuration));
const time = await newRollup.getTimestampForSlot(futureSlot);
if (time > BigInt(await t.ctx.cheatCodes.eth.timestamp())) {
if (time > BigInt(await t.ctx.cheatCodes.eth.lastBlockTimestamp())) {
await t.ctx.cheatCodes.eth.warp(Number(time));
await waitL1Block();
}
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/end-to-end/src/e2e_synching.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ describe('e2e_synching', () => {
for (const checkpoint of checkpoints) {
const lastBlock = checkpoint.blocks.at(-1)!;
const targetTime = Number(lastBlock.header.globalVariables.timestamp) - ETHEREUM_SLOT_DURATION;
while ((await cheatCodes.eth.timestamp()) < targetTime) {
while ((await cheatCodes.eth.lastBlockTimestamp()) < targetTime) {
await cheatCodes.eth.mine();
}
// If it breaks here, first place you should look is the pruning.
Expand Down
16 changes: 9 additions & 7 deletions yarn-project/end-to-end/src/fixtures/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,8 @@ export async function setup(
config.dataDirectory = directoryToCleanup;
}

const dateProvider = new TestDateProvider();

if (!config.l1RpcUrls?.length) {
if (!isAnvilTestChain(chain.id)) {
throw new Error(`No ETHEREUM_HOSTS set but non anvil chain requested`);
Expand All @@ -311,6 +313,7 @@ export async function setup(
accounts: opts.anvilAccounts,
port: opts.anvilPort ?? (process.env.ANVIL_PORT ? parseInt(process.env.ANVIL_PORT) : undefined),
slotsInAnEpoch: opts.anvilSlotsInAnEpoch,
dateProvider,
});
anvil = res.anvil;
config.l1RpcUrls = [res.rpcUrl];
Expand All @@ -322,8 +325,6 @@ export async function setup(
logger.info(`Logging metrics to ${filename}`);
setupMetricsLogger(filename);
}

const dateProvider = new TestDateProvider();
const ethCheatCodes = new EthCheatCodesWithState(config.l1RpcUrls, dateProvider);

if (opts.stateLoad) {
Expand Down Expand Up @@ -419,11 +420,12 @@ export async function setup(
await ethCheatCodes.setIntervalMining(config.ethereumSlotDuration);
}

// Always sync dateProvider to L1 time after deploying L1 contracts, regardless of mining mode.
// In compose mode, L1 time may have drifted ahead of system time due to the local-network watcher
// warping time forward on each filled slot. Without this sync, the sequencer computes the wrong
// slot from its dateProvider and cannot propose blocks.
dateProvider.setTime((await ethCheatCodes.timestamp()) * 1000);
// In compose mode (no local anvil), sync dateProvider to L1 time since it may have drifted
// ahead of system time due to the local-network watcher warping time forward on each filled slot.
// When running with a local anvil, the dateProvider is kept in sync via the stdout listener.
if (!anvil) {
dateProvider.setTime((await ethCheatCodes.lastBlockTimestamp()) * 1000);
}
Comment on lines +426 to +428
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This meant that we were setting the wall time to what the time was when the last block was mined, which could have been seconds ago.


if (opts.l2StartTime) {
await ethCheatCodes.warp(opts.l2StartTime, { resetBlockInterval: true });
Expand Down
6 changes: 4 additions & 2 deletions yarn-project/end-to-end/src/simulators/lending_simulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ export class LendingSimulator {

async prepare() {
this.accumulator = BASE;
const slot = await this.rollup.getSlotAt(BigInt(await this.cc.eth.timestamp()) + BigInt(this.ethereumSlotDuration));
const slot = await this.rollup.getSlotAt(
BigInt(await this.cc.eth.lastBlockTimestamp()) + BigInt(this.ethereumSlotDuration),
);
this.time = Number(await this.rollup.getTimestampForSlot(slot));
}

Expand All @@ -103,7 +105,7 @@ export class LendingSimulator {
return;
}

const slot = await this.rollup.getSlotAt(BigInt(await this.cc.eth.timestamp()));
const slot = await this.rollup.getSlotAt(BigInt(await this.cc.eth.lastBlockTimestamp()));
const targetSlot = SlotNumber(slot + diff);
const ts = Number(await this.rollup.getTimestampForSlot(targetSlot));
const timeDiff = ts - this.time;
Expand Down
10 changes: 6 additions & 4 deletions yarn-project/ethereum/src/test/eth_cheat_codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,12 @@ export class EthCheatCodes {
}

/**
* Get the current timestamp
* @returns The current timestamp
* Get the timestamp of the latest mined L1 block.
* Note: this is NOT the current time — it's the discrete timestamp of the last block.
* Between blocks, the actual chain time advances but no new block reflects it.
* @returns The latest block timestamp in seconds
*/
public async timestamp(): Promise<number> {
public async lastBlockTimestamp(): Promise<number> {
const res = await this.doRpcCall('eth_getBlockByNumber', ['latest', true]);
return parseInt(res.timestamp, 16);
}
Expand Down Expand Up @@ -552,7 +554,7 @@ export class EthCheatCodes {
}

public async syncDateProvider() {
const timestamp = await this.timestamp();
const timestamp = await this.lastBlockTimestamp();
if ('setTime' in this.dateProvider) {
this.dateProvider.setTime(timestamp * 1000);
}
Expand Down
37 changes: 37 additions & 0 deletions yarn-project/ethereum/src/test/start_anvil.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type Logger, createLogger } from '@aztec/foundation/log';
import { sleep } from '@aztec/foundation/sleep';
import { TestDateProvider } from '@aztec/foundation/timer';

import { createPublicClient, http, parseAbiItem } from 'viem';

Expand Down Expand Up @@ -52,4 +53,40 @@ describe('start_anvil', () => {
stopWatching();
await sleep(100);
});

it('syncs dateProvider to anvil block time on each mined block', async () => {
// Stop the default anvil instance (no dateProvider).
await anvil.stop();

const dateProvider = new TestDateProvider();
const res = await startAnvil({ dateProvider });
anvil = res.anvil;
rpcUrl = res.rpcUrl;

const publicClient = createPublicClient({ transport: http(rpcUrl, { batch: false }) });

// Mine a block so anvil emits a "Block Time" line.
await publicClient.request({ method: 'evm_mine', params: [] } as any);
// Give the stdout listener time to fire.
await sleep(200);

const block = await publicClient.getBlock({ blockTag: 'latest' });
const blockTimeMs = Number(block.timestamp) * 1000;
// The dateProvider should now be within 2 seconds of the anvil block time.
// TestDateProvider.now() = Date.now() + offset, and setTime sets offset = blockTimeMs - Date.now(),
// so subsequent now() calls return blockTimeMs + elapsed. We check the difference is small.
expect(Math.abs(dateProvider.now() - blockTimeMs)).toBeLessThan(2000);

// Warp anvil forward by 1000 seconds and verify the dateProvider follows.
const futureTimestamp = Number(block.timestamp) + 1000;
await publicClient.request({
method: 'evm_setNextBlockTimestamp',
params: [futureTimestamp],
} as any);
await publicClient.request({ method: 'evm_mine', params: [] } as any);
await sleep(200);

const futureTimeMs = futureTimestamp * 1000;
expect(Math.abs(dateProvider.now() - futureTimeMs)).toBeLessThan(2000);
});
});
27 changes: 25 additions & 2 deletions yarn-project/ethereum/src/test/start_anvil.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createLogger } from '@aztec/foundation/log';
import { makeBackoff, retry } from '@aztec/foundation/retry';
import type { TestDateProvider } from '@aztec/foundation/timer';
import { fileURLToPath } from '@aztec/foundation/url';

import { type ChildProcess, spawn } from 'child_process';
Expand Down Expand Up @@ -33,6 +34,12 @@ export async function startAnvil(
* L1-finality-based logic work without needing hundreds of mined blocks.
*/
slotsInAnEpoch?: number;
/**
* If provided, the date provider will be synced to anvil's block time on every mined block.
* This keeps the dateProvider in lockstep with anvil's chain time, avoiding drift between
* the wall clock and the L1 chain when computing L1 slot timestamps.
*/
dateProvider?: TestDateProvider;
} = {},
): Promise<{ anvil: Anvil; methodCalls?: string[]; rpcUrl: string; stop: () => Promise<void> }> {
const anvilBinary = resolve(dirname(fileURLToPath(import.meta.url)), '../../', 'scripts/anvil_kill_wrapper.sh');
Expand Down Expand Up @@ -108,12 +115,15 @@ export async function startAnvil(
child.once('close', onClose);
});

// Continue piping for logging / method-call capture after startup.
if (logger || opts.captureMethodCalls) {
// Continue piping for logging, method-call capture, and/or dateProvider sync after startup.
if (logger || opts.captureMethodCalls || opts.dateProvider) {
child.stdout?.on('data', (data: Buffer) => {
const text = data.toString();
logger?.debug(text.trim());
methodCalls?.push(...(text.match(/eth_[^\s]+/g) || []));
if (opts.dateProvider) {
syncDateProviderFromAnvilOutput(text, opts.dateProvider);
}
});
child.stderr?.on('data', (data: Buffer) => {
logger?.debug(data.toString().trim());
Expand Down Expand Up @@ -160,6 +170,19 @@ export async function startAnvil(
return { anvil: anvilObj, methodCalls, stop, rpcUrl: `http://127.0.0.1:${port}` };
}

/** Extracts block time from anvil stdout and syncs the dateProvider. */
function syncDateProviderFromAnvilOutput(text: string, dateProvider: TestDateProvider): void {
// Anvil logs mined blocks as:
// Block Time: "Fri, 20 Mar 2026 02:10:46 +0000"
const match = text.match(/Block Time:\s*"([^"]+)"/);
if (match) {
const blockTimeMs = new Date(match[1]).getTime();
if (!isNaN(blockTimeMs)) {
dateProvider.setTime(blockTimeMs);
}
}
}

/** Send SIGTERM, wait up to 5 s, then SIGKILL. All timers are always cleared. */
function killChild(child: ChildProcess): Promise<void> {
return new Promise<void>(resolve => {
Expand Down
Loading