Skip to content

Commit 97da230

Browse files
spalladinoclaude
andcommitted
feat(ethereum): add mineUntilTimestamp to EthCheatCodes and use in rollup cheat codes
Adds mineUntilTimestamp which mines real L1 blocks (via hardhat_mine with a timestamp interval) so finalized block timestamps advance alongside latest. This prevents epoch-cache's finalized guard from rejecting committees after time advances in tests. The method derives the block interval from the last two block timestamps (to handle anvil_setBlockTimestampInterval overrides), stops interval mining before the burst, and leaves it stopped so the caller controls when to resume. Updates rollup cheat codes (advanceToEpoch, advanceToNextEpoch, advanceToNextSlot, advanceSlots) to use mineUntilTimestamp with automatic interval restore. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 769be08 commit 97da230

4 files changed

Lines changed: 130 additions & 15 deletions

File tree

yarn-project/epoch-cache/src/epoch_cache.integration.test.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,13 @@ describe('EpochCache Integration', () => {
6464
bn254SecretKey: new SecretValue(Fr.random().toBigInt()),
6565
}));
6666

67+
// Use short L2 epochs (6 slots * 24s = 144s) so mineUntilTimestamp doesn't
68+
// need to mine thousands of blocks. With ethereumSlotDuration=12 and
69+
// aztecSlotDuration=24, advancing one L2 epoch needs ceil(144/12) = 12 L1 blocks.
6770
const deployed = await deployAztecL1Contracts(rpcUrl, deployerPrivateKey, foundry.id, {
6871
...DefaultL1ContractsConfig,
72+
aztecSlotDuration: 24,
73+
aztecEpochDuration: 6,
6974
vkTreeRoot: Fr.random(),
7075
protocolContractsHash: Fr.random(),
7176
genesisArchiveRoot: Fr.random(),
@@ -74,7 +79,11 @@ describe('EpochCache Integration', () => {
7479
initialValidators,
7580
});
7681

77-
rollupCheatCodes = new RollupCheatCodes(cheatCodes, deployed.l1ContractAddresses);
82+
rollupCheatCodes = new RollupCheatCodes(
83+
cheatCodes,
84+
deployed.l1ContractAddresses,
85+
DefaultL1ContractsConfig.ethereumSlotDuration,
86+
);
7887

7988
const publicClient = getPublicClient({ l1RpcUrls: [rpcUrl], l1ChainId: foundry.id });
8089
rollup = new RollupContract(publicClient, deployed.l1ContractAddresses.rollupAddress.toString());
@@ -95,14 +104,17 @@ describe('EpochCache Integration', () => {
95104
// Advance past the validator set lag so the epoch's data is finalized.
96105
const lagEpochs = Math.max(constants.lagInEpochsForValidatorSet, constants.lagInEpochsForRandao);
97106
const targetEpoch = EpochNumber(lagEpochs + 2);
107+
// mineUntilTimestamp mines real blocks so finalized advances with them.
108+
// It stops interval mining, so we must restore it before setupEpoch (which
109+
// submits a transaction that needs to be mined).
98110
await rollupCheatCodes.advanceToEpoch(targetEpoch);
99111

100-
// Mine enough blocks so the finalized tag catches up (finalized = latest - 16 with slotsInAnEpoch=8).
101-
await cheatCodes.mine(20);
102-
103112
// Setup epoch so the committee commitment is stored on-chain.
104113
await rollupCheatCodes.setupEpoch();
105114

115+
// Stop interval mining to freeze the dateProvider while we run assertions.
116+
await cheatCodes.setIntervalMining(0);
117+
106118
const { committee, seed, epoch } = await epochCache.getCommittee('now');
107119

108120
expect(committee).toBeDefined();

yarn-project/ethereum/src/test/eth_cheat_codes.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,46 @@ describe('EthCheatCodes', () => {
212212
});
213213
});
214214

215+
describe('mineUntilTimestamp', () => {
216+
it('mines blocks until the target timestamp is reached and restores interval mining', async () => {
217+
const blockInterval = 2;
218+
await cheatCodes.setIntervalMining(blockInterval);
219+
220+
const currentTs = await cheatCodes.lastBlockTimestamp();
221+
const targetTs = currentTs + 20;
222+
223+
await cheatCodes.mineUntilTimestamp(targetTs, { blockTimestampInterval: blockInterval });
224+
225+
const latestTs = await cheatCodes.lastBlockTimestamp();
226+
expect(latestTs).toBeGreaterThanOrEqual(targetTs);
227+
228+
// The key difference from warp: finalized block timestamp also advances.
229+
const finalizedBlock = await cheatCodes.publicClient.getBlock({ blockTag: 'finalized' });
230+
expect(Number(finalizedBlock.timestamp)).toBeGreaterThan(currentTs);
231+
232+
// Interval mining should be restored.
233+
const intervalAfter = await cheatCodes.getIntervalMining();
234+
expect(intervalAfter).toBe(blockInterval);
235+
});
236+
237+
it('throws when blockTimestampInterval is not positive', async () => {
238+
const currentTs = await cheatCodes.lastBlockTimestamp();
239+
await expect(cheatCodes.mineUntilTimestamp(currentTs + 10, { blockTimestampInterval: 0 })).rejects.toThrow(
240+
'blockTimestampInterval must be a positive number',
241+
);
242+
});
243+
244+
it('does nothing if already past the target timestamp', async () => {
245+
const currentTs = await cheatCodes.lastBlockTimestamp();
246+
const blockNumberBefore = await cheatCodes.blockNumber();
247+
248+
await cheatCodes.mineUntilTimestamp(currentTs - 5, { blockTimestampInterval: 1 });
249+
250+
const blockNumberAfter = await cheatCodes.blockNumber();
251+
expect(blockNumberAfter).toBe(blockNumberBefore);
252+
});
253+
});
254+
215255
describe('mineEmptyBlock', () => {
216256
it('mines an empty block while preserving pending transactions', async () => {
217257
// Deploy a token first (with automine enabled)

yarn-project/ethereum/src/test/eth_cheat_codes.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,58 @@ export class EthCheatCodes {
279279
}
280280
}
281281

282+
/**
283+
* Advances L1 time to the target timestamp by mining blocks at the given interval.
284+
* Unlike `warp`, this mines real blocks so finalized block timestamps also advance.
285+
*
286+
* Stops interval mining for the burst, then restores it if it was previously enabled.
287+
*
288+
* @param timestamp - The target timestamp to advance to
289+
* @param opts - Must include `blockTimestampInterval` (seconds between block timestamps).
290+
*/
291+
public async mineUntilTimestamp(
292+
timestamp: number | bigint,
293+
opts: { blockTimestampInterval: number; silent?: boolean },
294+
): Promise<void> {
295+
const targetTimestamp = Number(timestamp);
296+
const blockInterval = opts.blockTimestampInterval;
297+
if (blockInterval <= 0) {
298+
throw new Error('blockTimestampInterval must be a positive number');
299+
}
300+
301+
const currentTimestamp = await this.lastBlockTimestamp();
302+
const blocksNeeded = Math.ceil((targetTimestamp - currentTimestamp) / blockInterval);
303+
if (blocksNeeded <= 0) {
304+
return;
305+
}
306+
307+
// Save and stop interval mining so Anvil doesn't auto-mine during the burst.
308+
const previousInterval = await this.getIntervalMining();
309+
if (previousInterval !== null && previousInterval > 0) {
310+
await this.setIntervalMining(0, { silent: true });
311+
}
312+
313+
try {
314+
// Mine all blocks at once with the correct interval between them.
315+
// hardhat_mine accepts (count, interval) where interval is seconds between blocks.
316+
await this.doRpcCall('hardhat_mine', [`0x${blocksNeeded.toString(16)}`, `0x${blockInterval.toString(16)}`]);
317+
} finally {
318+
// Restore interval mining if it was previously enabled.
319+
if (previousInterval !== null && previousInterval > 0) {
320+
await this.setIntervalMining(previousInterval, { silent: true });
321+
}
322+
}
323+
324+
// Query the actual last block timestamp (may overshoot the target due to rounding).
325+
const actualTimestamp = await this.lastBlockTimestamp();
326+
if ('setTime' in this.dateProvider) {
327+
this.dateProvider.setTime(actualTimestamp * 1000);
328+
}
329+
if (!opts.silent) {
330+
this.logger.warn(`Mined ${blocksNeeded} L1 blocks until timestamp ${actualTimestamp}`);
331+
}
332+
}
333+
282334
/**
283335
* Load the value at a storage slot of a contract address on eth
284336
* @param contract - The contract address

yarn-project/ethereum/src/test/rollup_cheat_codes.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export class RollupCheatCodes {
2929
constructor(
3030
private ethCheatCodes: EthCheatCodes,
3131
addresses: Pick<L1ContractAddresses, 'rollupAddress'>,
32+
private readonly ethereumSlotDuration: number = 12,
3233
) {
3334
this.client = createPublicClient({
3435
chain: ethCheatCodes.chain,
@@ -45,9 +46,10 @@ export class RollupCheatCodes {
4546
rpcUrls: string[],
4647
addresses: Pick<L1ContractAddresses, 'rollupAddress'>,
4748
dateProvider: DateProvider,
49+
ethereumSlotDuration: number = 12,
4850
): RollupCheatCodes {
4951
const ethCheatCodes = new EthCheatCodes(rpcUrls, dateProvider);
50-
return new RollupCheatCodes(ethCheatCodes, addresses);
52+
return new RollupCheatCodes(ethCheatCodes, addresses, ethereumSlotDuration);
5153
}
5254

5355
/** Returns the current slot */
@@ -136,47 +138,56 @@ export class RollupCheatCodes {
136138
const slotNumber = SlotNumber(Number(epoch) * Number(slotsInEpoch));
137139
const timestamp = (await this.rollup.read.getTimestampForSlot([BigInt(slotNumber)])) + BigInt(opts.offset ?? 0);
138140
try {
139-
await this.ethCheatCodes.warp(Number(timestamp), { ...opts, silent: true, resetBlockInterval: true });
140-
this.logger.warn(`Warped to epoch ${epoch}`, { offset: opts.offset, timestamp });
141+
await this.ethCheatCodes.mineUntilTimestamp(Number(timestamp), {
142+
blockTimestampInterval: this.ethereumSlotDuration,
143+
silent: true,
144+
});
145+
this.logger.warn(`Advanced to epoch ${epoch}`, { offset: opts.offset, timestamp });
141146
} catch (err) {
142-
this.logger.warn(`Warp to epoch ${epoch} failed: ${err}`);
147+
this.logger.warn(`Advance to epoch ${epoch} failed: ${err}`);
143148
}
144149
return timestamp;
145150
}
146151

147-
/** Warps time in L1 until the next epoch */
152+
/** Advances L1 time until the next epoch */
148153
public async advanceToNextEpoch() {
149154
const slot = await this.getSlot();
150155
const { epochDuration, slotDuration } = await this.getConfig();
151156
const slotsUntilNextEpoch = epochDuration - (BigInt(slot) % epochDuration) + 1n;
152157
const timeToNextEpoch = slotsUntilNextEpoch * BigInt(slotDuration);
153158
const l1Timestamp = BigInt((await this.client.getBlock()).timestamp);
154-
await this.ethCheatCodes.warp(Number(l1Timestamp + timeToNextEpoch), {
159+
await this.ethCheatCodes.mineUntilTimestamp(Number(l1Timestamp + timeToNextEpoch), {
160+
blockTimestampInterval: this.ethereumSlotDuration,
155161
silent: true,
156-
resetBlockInterval: true,
157162
});
158163
this.logger.warn(`Advanced to next epoch`);
159164
}
160165

161-
/** Warps time in L1 until the beginning of the next slot. */
166+
/** Advances L1 time until the beginning of the next slot. */
162167
public async advanceToNextSlot() {
163168
const currentSlot = await this.getSlot();
164169
const nextSlot = SlotNumber(currentSlot + 1);
165170
const timestamp = await this.rollup.read.getTimestampForSlot([BigInt(nextSlot)]);
166-
await this.ethCheatCodes.warp(Number(timestamp), { silent: true, resetBlockInterval: true });
171+
await this.ethCheatCodes.mineUntilTimestamp(Number(timestamp), {
172+
blockTimestampInterval: this.ethereumSlotDuration,
173+
silent: true,
174+
});
167175
this.logger.warn(`Advanced to slot ${nextSlot}`);
168176
return [timestamp, nextSlot];
169177
}
170178

171179
/**
172-
* Warps time in L1 equivalent to however many slots.
180+
* Advances L1 time by the given number of slots.
173181
* @param howMany - The number of slots to advance.
174182
*/
175183
public async advanceSlots(howMany: number) {
176184
const l1Timestamp = (await this.client.getBlock()).timestamp;
177185
const slotDuration = Number(await this.rollup.read.getSlotDuration());
178186
const timeToWarp = BigInt(howMany) * BigInt(slotDuration);
179-
await this.ethCheatCodes.warp(l1Timestamp + timeToWarp, { silent: true, resetBlockInterval: true });
187+
await this.ethCheatCodes.mineUntilTimestamp(Number(l1Timestamp + timeToWarp), {
188+
blockTimestampInterval: this.ethereumSlotDuration,
189+
silent: true,
190+
});
180191
const [slot, epoch] = await Promise.all([this.getSlot(), this.getEpoch()]);
181192
this.logger.warn(`Advanced ${howMany} slots up to slot ${slot} in epoch ${epoch}`);
182193
}

0 commit comments

Comments
 (0)