diff --git a/yarn-project/end-to-end/src/automine/contracts/state_vars.test.ts b/yarn-project/end-to-end/src/automine/contracts/state_vars.test.ts index b808114e2925..6695d135d630 100644 --- a/yarn-project/end-to-end/src/automine/contracts/state_vars.test.ts +++ b/yarn-project/end-to-end/src/automine/contracts/state_vars.test.ts @@ -406,6 +406,15 @@ describe('automine/contracts/state_vars', () => { } }); + // Drives the chain forward by sending a no-op tx per iteration (rather than warping wall-clock time) + // until the latest block's timestamp reaches `target`. Under the forced 12s slot duration a fixed + // block-count delay cannot count for the schedule, so we poll the real block timestamp instead. + const advanceChainToTimestamp = async (target: bigint) => { + while ((await aztecNode.getBlockData('latest'))!.header.globalVariables.timestamp < target) { + await authContract.methods.get_authorized().send({ from: defaultAccountAddress }); + } + }; + // Changes the authorized delay from 5 slots (360s) to 2 slots, advances the chain past the // scheduled timestamp_of_change by sending no-op txs, then proves the private read and asserts // the expirationTimestamp equals anchorTimestamp + newDelay - 1. @@ -436,11 +445,7 @@ describe('automine/contracts/state_vars', () => { // forces aztecSlotDuration=12s under pipelining (see fixtures/setup.ts), so a fixed // `delay(N blocks)` cannot count for the schedule — block timestamp polling is the // slot-duration-agnostic way to know we have crossed the schedule. - // REFACTOR: hand-rolled loop advancing the chain by sending no-op txs until a target timestamp is - // crossed; a DSL helper like advanceChainToTimestamp(node, timestampOfChange) should replace this. - while ((await aztecNode.getBlockData('latest'))!.header.globalVariables.timestamp < timestampOfChange) { - await authContract.methods.get_authorized().send({ from: defaultAccountAddress }); - } + await advanceChainToTimestamp(timestampOfChange); // We now call our AuthContract to see if the change in expiration timestamp has reflected our delay change. // expirationTimestamp is `anchor.timestamp + effective_minimum_delay`, where the anchor is the diff --git a/yarn-project/end-to-end/src/automine/effects/offchain_payment.parallel.test.ts b/yarn-project/end-to-end/src/automine/effects/offchain_payment.parallel.test.ts index 567d2749e780..6413f8b4318c 100644 --- a/yarn-project/end-to-end/src/automine/effects/offchain_payment.parallel.test.ts +++ b/yarn-project/end-to-end/src/automine/effects/offchain_payment.parallel.test.ts @@ -57,6 +57,20 @@ describe('automine/effects/offchain_payment', () => { logger.info(`Empty block mined. New L2 block: ${await aztecNode.getBlockNumber()}`); } + // Polls the PXE note view until `owner`'s balance equals `expected`. The PXE syncs asynchronously from the + // archiver, so the balance may lag briefly after a block is mined. + function waitForNoteBalance(owner: AztecAddress, expected: bigint) { + return retryUntil( + async () => { + const { result } = await contract.methods.get_balance(owner).simulate({ from: owner }); + return result === expected; + }, + `note balance of ${owner} to reach ${expected}`, + 30, + 0.1, + ); + } + // Reverts the chain to `checkpointBeforeTx`. Pauses the AutomineSequencer first: reverting restores the // un-mined transfer tx to the pending pool, and the sequencer's mempool poller would otherwise re-mine it // within ~50ms, racing the post-reorg balance assertions. Pausing only gates the poller; explicit ops @@ -211,18 +225,7 @@ describe('automine/effects/offchain_payment', () => { await forceEmptyBlock(); // Wait for the PXE to process the re-mined block and update its note view. - // The PXE syncs asynchronously from the archiver, so the balance may lag briefly. - // REFACTOR: hand-rolled poll waiting for PXE to reprocess re-mined offchain notes; a DSL helper - // (e.g. waitForNoteBalance or waitForPXESync) should replace this retryUntil loop. - await retryUntil( - async () => { - const { result } = await contract.methods.get_balance(bob).simulate({ from: bob }); - return result === paymentAmount; - }, - 'Bob balance restored after re-mine', - 30, - 0.1, - ); + await waitForNoteBalance(bob, paymentAmount); // Check that the message was reprocessed and Bob has his payment again. // Notice what we want to test here is that the offchain effects don't need to be re-enqueued diff --git a/yarn-project/end-to-end/src/automine/effects/pruned_blocks.test.ts b/yarn-project/end-to-end/src/automine/effects/pruned_blocks.test.ts index 35c25090df7f..223ccabdf5b6 100644 --- a/yarn-project/end-to-end/src/automine/effects/pruned_blocks.test.ts +++ b/yarn-project/end-to-end/src/automine/effects/pruned_blocks.test.ts @@ -1,8 +1,10 @@ import type { AztecAddress } from '@aztec/aztec.js/addresses'; +import type { Fr } from '@aztec/aztec.js/fields'; import type { Logger } from '@aztec/aztec.js/log'; import { MerkleTreeId } from '@aztec/aztec.js/trees'; import type { Wallet } from '@aztec/aztec.js/wallet'; import { CheatCodes } from '@aztec/aztec/testing'; +import type { BlockNumber } from '@aztec/foundation/branded-types'; import { retryUntil } from '@aztec/foundation/retry'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; import type { AztecNode, AztecNodeDebug } from '@aztec/stdlib/interfaces/client'; @@ -73,6 +75,23 @@ describe('automine/effects/pruned_blocks', () => { } } + // Polls the historical leaf query until it starts throwing "Unable to find leaf", which is how a + // pruned world-state block surfaces to callers once the prune has propagated. + const waitForWorldStatePrune = (blockNumber: BlockNumber, note: Fr) => + retryUntil( + async () => { + try { + await aztecNode.findLeavesIndexes(blockNumber, MerkleTreeId.NOTE_HASH_TREE, [note]); + return false; + } catch (error) { + return (error as Error).message.includes('Unable to find leaf'); + } + }, + 'waiting for pruning', + 60, + 0.5, + ); + // Mints half the token amount (tx1), mines enough empty blocks to make that block eligible for pruning, // calls markAsProven + extra L1 blocks to finalize the prune, polls until the archive query on tx1's // block fails, then mints the other half and transfers the full amount. Asserts final balances. @@ -114,21 +133,7 @@ describe('automine/effects/pruned_blocks', () => { // The same historical query we performed before should now fail since this block is not available anymore. We poll // the node for a bit until it processes the blocks we marked as proven, causing the historical query to fail. logger.warn(`Awaiting 'unable to find leaf' error from node due to pruned history`); - // REFACTOR: hand-rolled poll waiting for world-state prune to propagate; a DSL helper such as - // waitForWorldStatePrune(node, blockNumber) should replace this retryUntil loop. - await retryUntil( - async () => { - try { - await aztecNode.findLeavesIndexes(firstMintReceipt.blockNumber!, MerkleTreeId.NOTE_HASH_TREE, [mintedNote!]); - return false; - } catch (error) { - return (error as Error).message.includes('Unable to find leaf'); - } - }, - 'waiting for pruning', - 60, - 0.5, - ); + await waitForWorldStatePrune(firstMintReceipt.blockNumber!, mintedNote!); // We've completed the setup we were interested in, and can now simply mint the second half of the amount, transfer // the full amount to the recipient (which will require the sender to discover and prove both the old and new notes) diff --git a/yarn-project/end-to-end/src/p2p/preferred_gossip_network.test.ts b/yarn-project/end-to-end/src/p2p/preferred_gossip_network.test.ts index 86d5ddb6f6b9..9e481e80fffc 100644 --- a/yarn-project/end-to-end/src/p2p/preferred_gossip_network.test.ts +++ b/yarn-project/end-to-end/src/p2p/preferred_gossip_network.test.ts @@ -180,8 +180,6 @@ describe('e2e_p2p_preferred_network', () => { // Creates a 7-node topology (2 regular + 2 preferred + 2 validators + 1 no-discovery validator), // installs gossip monitors to verify no-discovery validators only receive traffic from preferred nodes, // submits txs from regular nodes, and asserts all txs mine with attestations from all validators. - // REFACTOR: peer-count polling loop in waitForNodeToAcquirePeers is hand-rolled; consider - // using t.waitForP2PMeshConnectivity with a peer-count predicate it('should rollup txs from all peers', async () => { // create the bootstrap node for the network if (!t.bootstrapNodeEnr) { diff --git a/yarn-project/end-to-end/src/p2p/rediscovery.test.ts b/yarn-project/end-to-end/src/p2p/rediscovery.test.ts index 39f446176746..134fef4b82a5 100644 --- a/yarn-project/end-to-end/src/p2p/rediscovery.test.ts +++ b/yarn-project/end-to-end/src/p2p/rediscovery.test.ts @@ -58,8 +58,6 @@ describe('e2e_p2p_rediscovery', () => { // Forms an initial 4-node mesh, stops the bootstrap node, then restarts each validator from its data // directory without any bootstrap ENR. Submits txs to each restarted node and asserts they mine, // proving that discv5 peer-store entries are sufficient for re-discovery. - // REFACTOR: sequential sleep(2500) between node restarts is hand-rolled; the delay exists to avoid - // port conflicts but should be replaced with a port-readiness check or staggered createNode calls it('should re-discover stored peers without bootstrap node', async () => { const txsSentViaDifferentNodes: TxHash[][] = []; nodes = await createNodes( diff --git a/yarn-project/end-to-end/src/single-node/cross-chain/l1_to_l2.parallel.test.ts b/yarn-project/end-to-end/src/single-node/cross-chain/l1_to_l2.parallel.test.ts index 73ae303a2c83..d9c27d6be37c 100644 --- a/yarn-project/end-to-end/src/single-node/cross-chain/l1_to_l2.parallel.test.ts +++ b/yarn-project/end-to-end/src/single-node/cross-chain/l1_to_l2.parallel.test.ts @@ -134,9 +134,8 @@ describe('single-node/cross-chain/l1_to_l2', () => { } }; - // Waits until the message is fetched by the archiver of the node and returns the msg target checkpoint - // REFACTOR: hand-rolled retryUntil loop that also advances blocks on each retry; replace with a - // waitForL1ToL2MessageIndexed(node, msgHash, advanceBlock) helper in the e2e fixture or harness. + // Waits until the message is fetched by the archiver of the node and returns the msg target checkpoint. + // Advances a block on each retry because an L1->L2 message is only indexed once further L2 blocks build. const waitForMessageFetched = async (msgHash: Fr) => { log.warn(`Waiting until the message is fetched by the node`); return await retryUntil( diff --git a/yarn-project/end-to-end/src/single-node/fees/bridging_race.notest.ts b/yarn-project/end-to-end/src/single-node/fees/bridging_race.notest.ts index 9da3127852d6..c8c0ff7893de 100644 --- a/yarn-project/end-to-end/src/single-node/fees/bridging_race.notest.ts +++ b/yarn-project/end-to-end/src/single-node/fees/bridging_race.notest.ts @@ -54,6 +54,14 @@ describe('single-node/fees/bridging_race', () => { bobsAddress = bobsAccountManager.address; }); + // Sleeps until 500ms before the current L2 slot ends, so the subsequent bridge lands right at the slot + // boundary (this is what reproduces the "message not in state" race the test guards against). + const sleepUntilNearSlotEnd = async () => { + const sleepTime = (Number(t.monitor.checkpointTimestamp) + AZTEC_SLOT_DURATION) * 1000 - Date.now() - 500; + logger.info(`Sleeping for ${sleepTime}ms until near end of L2 slot before sending L1 fee juice to L2 inbox`); + await sleep(sleepTime); + }; + // Reproduces a timing race where an L1→L2 fee-juice bridge message lands just before the end of an // L2 slot, causing the archiver to miss it. The fix was to wait for the archiver to see the message // before waiting for the required two-block confirmation. The sleep injected into approve() simulates @@ -65,11 +73,7 @@ describe('single-node/fees/bridging_race', () => { const origApprove = l1TokenManager.approve.bind(l1TokenManager); l1TokenManager.approve = async (amount: bigint, address: Hex, addressName = '') => { await origApprove(amount, address, addressName); - const sleepTime = (Number(t.monitor.checkpointTimestamp) + AZTEC_SLOT_DURATION) * 1000 - Date.now() - 500; - logger.info(`Sleeping for ${sleepTime}ms until near end of L2 slot before sending L1 fee juice to L2 inbox`); - // REFACTOR: hand-rolled slot-boundary sleep; replace with a timing helper that derives the remaining - // slot time from the chain monitor's slot boundaries rather than computing it inline. - await sleep(sleepTime); + await sleepUntilNearSlotEnd(); }; // Waiting for the archiver to sync the message _before_ waiting for the mandatory 2 L2 blocks to pass fixed it diff --git a/yarn-project/end-to-end/src/single-node/fees/failures.test.ts b/yarn-project/end-to-end/src/single-node/fees/failures.test.ts index 4f44952b04b9..ef9e336ca787 100644 --- a/yarn-project/end-to-end/src/single-node/fees/failures.test.ts +++ b/yarn-project/end-to-end/src/single-node/fees/failures.test.ts @@ -97,10 +97,7 @@ describe('single-node/fees/failures', () => { await expectMapping(t.getGasBalanceFn, [aliceAddress, bananaFPC.address], [initialAliceGas, initialFPCGas]); // We wait until the proven chain is caught up so all previous fees are paid out. - // REFACTOR: manual advanceToNextEpoch + catchUpProvenChain sequence; replace with a single - // waitForEpochProven() helper on FeesTest that encapsulates this pattern. - await t.cheatCodes.rollup.advanceToNextEpoch(); - await t.catchUpProvenChain(); + await t.waitForEpochProven(); const currentSequencerRewards = await t.getCoinbaseSequencerRewards(); const provenCheckpointBefore = await t.rollupContract.getProvenCheckpointNumber(); diff --git a/yarn-project/end-to-end/src/single-node/fees/fee_settings.test.ts b/yarn-project/end-to-end/src/single-node/fees/fee_settings.test.ts index d2bb0c9411a6..333efc4067f3 100644 --- a/yarn-project/end-to-end/src/single-node/fees/fee_settings.test.ts +++ b/yarn-project/end-to-end/src/single-node/fees/fee_settings.test.ts @@ -1,4 +1,5 @@ import type { AztecAddress } from '@aztec/aztec.js/addresses'; +import type { Logger } from '@aztec/aztec.js/log'; import type { AztecNode } from '@aztec/aztec.js/node'; import { CheatCodes } from '@aztec/aztec/testing'; import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; @@ -17,6 +18,41 @@ import type { TestWallet } from '../../test-wallet/test_wallet.js'; import { proveInteraction } from '../../test-wallet/utils.js'; import { FeesTest } from './fees_test.js'; +/** + * Repeatedly bumps the L1 base fee, mines an L1 block, and rotates the gas-fee oracle until the node's + * min L2 fee (`feePerL2Gas`) reaches `minRiseTarget`, then returns that fee snapshot. The oracle rotation + * deadband (LIFETIME - LAG = 3 L2 slots between successful rotations, see FeeLib.sol) silently no-ops + * `updateL1GasFeeOracle` until the window opens, so the throw is swallowed and the loop retries. + */ +async function spikeL1BaseFeeUntilMinFee( + cheatCodes: CheatCodes, + aztecNode: AztecNode, + targetL1BaseFee: bigint, + minRiseTarget: bigint, + opts: { timeout?: number; interval?: number; logger?: Logger } = {}, +): Promise { + return await retryUntil( + async () => { + await cheatCodes.eth.setNextBlockBaseFeePerGas(targetL1BaseFee); + await cheatCodes.eth.mine(); + try { + await cheatCodes.rollup.updateL1GasFeeOracle(); + } catch { + // Rotation deadband closed — try again on the next iteration. + } + const after = await aztecNode.getCurrentMinFees(); + opts.logger?.info(`L2 min fees are now ${inspect(after)}`, { + minFeesAfter: after.toInspect(), + minRiseTarget: minRiseTarget.toString(), + }); + return after.feePerL2Gas >= minRiseTarget ? after : undefined; + }, + 'L2 min fee organic increase (L1 base fee bump) above reference', + opts.timeout ?? 90, + opts.interval ?? 1, + ); +} + // Fee oracle and wallet fee-padding behaviour under L1 base-fee spikes and governance fee-config bumps. // Uses FeesTest with a custom timing preset (ethSlot=4s, aztecSlot=12s, inboxLag=2, minTxsPerBlock=0, // aztecProofSubmissionEpochs=640, manaTarget=4M, walletMinFeePadding=30) and fake in-proc prover node. @@ -118,29 +154,9 @@ describe('single-node/fees/fee_settings', () => { const targetL1BaseFee = referenceDerivedL1BaseFee > 0n ? referenceDerivedL1BaseFee : 1n; t.logger.info(`Targeting L1 base fee ${targetL1BaseFee} (current ${currentL1BaseFee})`); - // REFACTOR: hand-rolled retryUntil loop that mines L1 blocks and rotates the oracle; replace with - // a helper on RollupCheatCodes that abstracts the L1-base-fee-spike + oracle-rotation retry. - return await retryUntil( - async () => { - await cheatCodes.eth.setNextBlockBaseFeePerGas(targetL1BaseFee); - await cheatCodes.eth.mine(); - try { - await cheatCodes.rollup.updateL1GasFeeOracle(); - } catch { - // Rotation deadband closed — try again on the next iteration. - } - const after = await aztecNode.getCurrentMinFees(); - t.logger.info(`L2 min fees are now ${inspect(after)}`, { - minFeesBefore: beforeAtCall.toInspect(), - minFeesAfter: after.toInspect(), - minRiseTarget: minRiseTarget.toString(), - }); - return after.feePerL2Gas >= minRiseTarget ? after : undefined; - }, - 'L2 min fee organic increase (L1 base fee bump) above reference', - 90, - 1, - ); + return await spikeL1BaseFeeUntilMinFee(cheatCodes, aztecNode, targetL1BaseFee, minRiseTarget, { + logger: t.logger, + }); }; // Pick a baseline from the post-checkpoint chain state. The prove step itself is diff --git a/yarn-project/end-to-end/src/single-node/fees/fees_test.ts b/yarn-project/end-to-end/src/single-node/fees/fees_test.ts index 4da5003cef30..12ecbe89c029 100644 --- a/yarn-project/end-to-end/src/single-node/fees/fees_test.ts +++ b/yarn-project/end-to-end/src/single-node/fees/fees_test.ts @@ -136,6 +136,12 @@ export class FeesTest extends SingleNodeTestContext { } } + /** Advances to the next epoch and waits for the proven chain to catch up, so all prior fees are paid out. */ + async waitForEpochProven() { + await this.cheatCodes.rollup.advanceToNextEpoch(); + await this.catchUpProvenChain(); + } + async getBlockRewards() { const blockReward = await this.rollupContract.getCheckpointReward(); const rewardConfig = await this.rollupContract.getRewardConfig(); diff --git a/yarn-project/end-to-end/src/single-node/fees/private_payments.parallel.test.ts b/yarn-project/end-to-end/src/single-node/fees/private_payments.parallel.test.ts index 7f530f728b5c..d5773e556ee9 100644 --- a/yarn-project/end-to-end/src/single-node/fees/private_payments.parallel.test.ts +++ b/yarn-project/end-to-end/src/single-node/fees/private_payments.parallel.test.ts @@ -45,8 +45,7 @@ describe('single-node/fees/private_payments', () => { ({ wallet, aliceAddress, bobAddress, sequencerAddress, bananaCoin, bananaFPC, gasSettings, aztecNode } = t); // Prove up until the current state by advancing the epoch and waiting for the prover node. - await t.cheatCodes.rollup.advanceToNextEpoch(); - await t.catchUpProvenChain(); + await t.waitForEpochProven(); }); afterAll(async () => { diff --git a/yarn-project/end-to-end/src/single-node/proving/long_proving_time.test.ts b/yarn-project/end-to-end/src/single-node/proving/long_proving_time.test.ts index 5254deb40cd5..7a50e9a8f006 100644 --- a/yarn-project/end-to-end/src/single-node/proving/long_proving_time.test.ts +++ b/yarn-project/end-to-end/src/single-node/proving/long_proving_time.test.ts @@ -60,6 +60,22 @@ describe('single-node/proving/long_proving_time', () => { await test.teardown(); }); + // Waits until the proven checkpoint reaches `target` while sampling the prover job queue on every + // tick and returning the peak parallelism observed over the whole proving window (the value the + // MAX_JOB_COUNT assertion depends on — a one-shot snapshot could not capture the peak). + const sampleMaxJobCountUntilProven = async (target: number) => { + let maxJobCount = 0; + while (monitor.provenCheckpointNumber === undefined || monitor.provenCheckpointNumber < target) { + const jobs = await test.proverNodes[0].getProverNode()!.getJobs(); + if (jobs.length > maxJobCount) { + maxJobCount = jobs.length; + logger.info(`Updated max job count to ${maxJobCount}`, jobs); + } + await sleep((L1_BLOCK_TIME_IN_S * 1000) / 2); + } + return maxJobCount; + }; + // Polls the prover node's job queue until provenCheckpointNumber reaches targetProvenEpochs. // Asserts that checkpointNumber advanced at least 3× the proven epoch count, confirming proving // lagged behind block production. Asserts maxJobCount stays within MAX_JOB_COUNT (20), confirming @@ -69,19 +85,7 @@ describe('single-node/proving/long_proving_time', () => { const targetProvenBlockNumber = targetProvenEpochs * test.epochDuration; logger.info(`Waiting for ${targetProvenEpochs} epochs to be proven at ${targetProvenBlockNumber} L2 blocks`); - // Wait until we hit the target proven block number, and keep an eye on how many proving jobs are run in parallel. - let maxJobCount = 0; - // REFACTOR: hand-rolled sleep loop polling provenCheckpointNumber; replace with - // test.waitUntilProvenCheckpointNumber(targetProvenBlockNumber, timeout) and check job count - // separately via a one-time snapshot rather than updating inside the loop. - while (monitor.provenCheckpointNumber === undefined || monitor.provenCheckpointNumber < targetProvenBlockNumber) { - const jobs = await test.proverNodes[0].getProverNode()!.getJobs(); - if (jobs.length > maxJobCount) { - maxJobCount = jobs.length; - logger.info(`Updated max job count to ${maxJobCount}`, jobs); - } - await sleep((L1_BLOCK_TIME_IN_S * 1000) / 2); - } + const maxJobCount = await sampleMaxJobCountUntilProven(targetProvenBlockNumber); // At least 3 epochs should have passed after the proven one (though we add a -1 just in case) expect(monitor.checkpointNumber).toBeGreaterThanOrEqual(targetProvenEpochs * test.epochDuration * 3 - 1); diff --git a/yarn-project/end-to-end/src/single-node/proving/optimistic.parallel.test.ts b/yarn-project/end-to-end/src/single-node/proving/optimistic.parallel.test.ts index 704acfdbb7ba..98b9cffe1485 100644 --- a/yarn-project/end-to-end/src/single-node/proving/optimistic.parallel.test.ts +++ b/yarn-project/end-to-end/src/single-node/proving/optimistic.parallel.test.ts @@ -112,8 +112,6 @@ describe('single-node/proving/optimistic', () => { /** epoch -> lowest checkpoint header slot of any CheckpointProver observed for that epoch. */ const lowestProvenSlotByEpoch = new Map(); let stopped = false; - // REFACTOR: hand-rolled setTimeout sampler loop with a `stopped` flag — a polling/observe helper - // (e.g. a sampler that records earliest-observed values per key until disposed) should replace it. const loop = (async () => { while (!stopped) { for (const prover of proverNode.getCheckpointStore().listAll()) { diff --git a/yarn-project/end-to-end/src/single-node/proving/proof_fails.parallel.test.ts b/yarn-project/end-to-end/src/single-node/proving/proof_fails.parallel.test.ts index 008b924727a3..e1670002f7bf 100644 --- a/yarn-project/end-to-end/src/single-node/proving/proof_fails.parallel.test.ts +++ b/yarn-project/end-to-end/src/single-node/proving/proof_fails.parallel.test.ts @@ -7,7 +7,6 @@ import { ChainMonitor } from '@aztec/ethereum/test'; import type { ViemClient } from '@aztec/ethereum/types'; import { CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types'; import { promiseWithResolvers } from '@aztec/foundation/promise'; -import { retryUntil } from '@aztec/foundation/retry'; import { sleep } from '@aztec/foundation/sleep'; import type { TestProverNode } from '@aztec/prover-node/test'; import type { SequencerEvents } from '@aztec/sequencer-client'; @@ -108,15 +107,10 @@ describe('single-node/proving/proof_fails', () => { await test.warpToEpochStart(2); // Wait until the prune is processed and a new checkpoint mined. - const checkpointAfterRollback = await retryUntil( - async () => { - const checkpoint = await rollup.getCheckpointNumber(); - return checkpoint > 0 && checkpoint < checkpointBeforeRollback ? checkpoint : undefined; - }, - 'rollup rolled back', - L2_SLOT_DURATION_IN_S * 4, - 0.2, - ); + const checkpointAfterRollback = await context.cheatCodes.rollup.waitForCheckpointBelow(checkpointBeforeRollback, { + timeout: L2_SLOT_DURATION_IN_S * 4, + interval: 0.2, + }); // The post-rollback chain tip should be in epoch 2, since the rollback-triggering propose // was made during epoch 2, after the deadline. diff --git a/yarn-project/end-to-end/src/single-node/sequencer/gov_proposal.parallel.test.ts b/yarn-project/end-to-end/src/single-node/sequencer/gov_proposal.parallel.test.ts index 19b76439fa0b..07c5306c4deb 100644 --- a/yarn-project/end-to-end/src/single-node/sequencer/gov_proposal.parallel.test.ts +++ b/yarn-project/end-to-end/src/single-node/sequencer/gov_proposal.parallel.test.ts @@ -15,7 +15,6 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { TimeoutError } from '@aztec/foundation/error'; import { EthAddress } from '@aztec/foundation/eth-address'; import type { Logger } from '@aztec/foundation/log'; -import { retryUntil } from '@aztec/foundation/retry'; import { sleep } from '@aztec/foundation/sleep'; import { bufferToHex } from '@aztec/foundation/string'; import type { TestDateProvider } from '@aztec/foundation/timer'; @@ -242,15 +241,9 @@ describe('single-node/sequencer/gov_proposal', () => { // Check that the checkpoint number has indeed increased on L1 so sequencers cant pass the sync check. // Allow another slot for any in-flight L1 propose to mine, since the work loop above hits its wait timeout the // moment the tx misses L2 sync, not the moment the L1 tx lands. - // REFACTOR: retryUntil polling ChainMonitor should be replaced with a ChainMonitor.waitForCheckpoint helper - const checkpointAfterBlobDisable = await retryUntil( - async () => { - const snapshot = await monitor.run(); - return snapshot.checkpointNumber > lastCheckpointOnL1 ? snapshot : undefined; - }, - 'L1 checkpoint to advance after disabling blob client', - AZTEC_SLOT_DURATION + 5, - 1, + const checkpointAfterBlobDisable = await monitor.waitForCheckpoint( + event => event.checkpointNumber > lastCheckpointOnL1, + { timeout: (AZTEC_SLOT_DURATION + 5) * 1000, checkCurrentCheckpoint: true }, ); expect(checkpointAfterBlobDisable.checkpointNumber).toBeGreaterThan(lastCheckpointOnL1); logger.warn(`L1 checkpoint number has increased`, { diff --git a/yarn-project/end-to-end/src/single-node/sequencer/publisher_funding_multi.test.ts b/yarn-project/end-to-end/src/single-node/sequencer/publisher_funding_multi.test.ts index ee3ed2b6ede0..7bb271bea103 100644 --- a/yarn-project/end-to-end/src/single-node/sequencer/publisher_funding_multi.test.ts +++ b/yarn-project/end-to-end/src/single-node/sequencer/publisher_funding_multi.test.ts @@ -123,6 +123,27 @@ describe('single-node/sequencer/publisher_funding_multi', () => { await rm(keyStoreDirectory, { recursive: true, force: true }); }); + // Polls until every address in `accounts` has an L1 balance strictly above `threshold`. + const waitForBalancesAbove = (accounts: EthAddress[], threshold: bigint) => + retryUntil( + async () => { + const balances = await Promise.all(accounts.map(account => ethCheatCodes.getBalance(account))); + return balances.every(balance => balance > threshold) || undefined; + }, + `all balances above ${threshold}`, + 180, + 1, + ); + + // Polls until `funder`'s L1 spend since `before` reaches at least `amount`. + const waitForFunderSpend = (funder: EthAddress, before: bigint, amount: bigint) => + retryUntil( + async () => before - (await ethCheatCodes.getBalance(funder)) >= amount || undefined, + `funder to spend at least ${amount}`, + 180, + 1, + ); + // Sets both publisher L1 balances below the funding threshold via ethCheatCodes, drives the // PublisherManager's funding loop to top them both up (round 1), then drains one publisher again // and drives a second funding round to confirm the loop is still healthy. @@ -156,18 +177,7 @@ describe('single-node/sequencer/publisher_funding_multi', () => { // publishers were topped up. await fundingPromise!.trigger(); - // REFACTOR: hand-rolled poll waiting for PublisherManager funding cycle; a helper like - // waitForPublisherBalancesAbove(publisherManager, threshold) should replace this retryUntil. - await retryUntil( - async () => { - const balance1 = await ethCheatCodes.getBalance(publisher1Address); - const balance2 = await ethCheatCodes.getBalance(publisher2Address); - return balance1 > LOW_BALANCE && balance2 > LOW_BALANCE ? true : undefined; - }, - 'waiting for both publishers to be funded', - 180, - 1, - ); + await waitForBalancesAbove([publisher1Address, publisher2Address], LOW_BALANCE); const publisher1BalanceAfter = await ethCheatCodes.getBalance(publisher1Address); const publisher2BalanceAfter = await ethCheatCodes.getBalance(publisher2Address); @@ -197,17 +207,7 @@ describe('single-node/sequencer/publisher_funding_multi', () => { // Force a second funding cycle rather than waiting for the next 2-minute poll. await fundingPromise!.trigger(); - // REFACTOR: hand-rolled poll waiting for a second PublisherManager funding cycle; same helper - // as above should cover this site. - await retryUntil( - async () => { - const spent = funderBalanceBefore2 - (await ethCheatCodes.getBalance(funderAddress)); - return spent >= FUNDING_AMOUNT ? true : undefined; - }, - 'waiting for second funding round', - 180, - 1, - ); + await waitForFunderSpend(funderAddress, funderBalanceBefore2, FUNDING_AMOUNT); const funderSpent2 = funderBalanceBefore2 - (await ethCheatCodes.getBalance(funderAddress)); logger.info(`Second funding round: funder spent ${funderSpent2} (expected ~${FUNDING_AMOUNT})`); diff --git a/yarn-project/end-to-end/src/single-node/sync/snapshot_sync.test.ts b/yarn-project/end-to-end/src/single-node/sync/snapshot_sync.test.ts index 1296d0c06bb8..5a2ba3a376a3 100644 --- a/yarn-project/end-to-end/src/single-node/sync/snapshot_sync.test.ts +++ b/yarn-project/end-to-end/src/single-node/sync/snapshot_sync.test.ts @@ -75,6 +75,9 @@ describe('e2e_snapshot_sync', () => { ); }; + const waitForSnapshotFiles = (dir: string) => + retryUntil(() => readdir(dir).then(files => files.length > 0), 'snapshot-created', 90, 1); + const expectNodeSyncedToL2Block = async (node: AztecNode, blockNumber: number) => { const tips = await node.getChainTips(); expect(tips.proposed.number).toBeGreaterThanOrEqual(blockNumber); @@ -86,9 +89,7 @@ describe('e2e_snapshot_sync', () => { // enough chain history for the subsequent snapshot tests. it('waits until a few checkpoints have been mined', async () => { log.warn(`Waiting for checkpoints to be mined`); - // REFACTOR: hand-rolled poll on ChainMonitor.checkpointNumber; EpochsTestContext.waitUntilCheckpointNumber - // or a shared helper should replace this retryUntil. - await retryUntil(() => monitor.checkpointNumber > TARGET_CHECKPOINT_NUMBER, 'checkpoints-mined', 90, 1); + await monitor.waitUntilCheckpoint(CheckpointNumber(TARGET_CHECKPOINT_NUMBER + 1)); log.warn(`Checkpoint height is now ${monitor.checkpointNumber}.`); }); @@ -97,9 +98,7 @@ describe('e2e_snapshot_sync', () => { it('creates a snapshot', async () => { log.warn(`Creating snapshot`); await context.aztecNodeAdmin.startSnapshotUpload(snapshotLocation); - // REFACTOR: hand-rolled poll waiting for snapshot files to appear; a helper like - // waitForSnapshotUpload(adminNode, snapshotDir) should replace this. - await retryUntil(() => readdir(snapshotDir).then(files => files.length > 0), 'snapshot-created', 90, 1); + await waitForSnapshotFiles(snapshotDir); log.warn(`Snapshot created`); }); diff --git a/yarn-project/ethereum/src/test/chain_monitor.ts b/yarn-project/ethereum/src/test/chain_monitor.ts index b36f697f09fe..ab09a1713cdb 100644 --- a/yarn-project/ethereum/src/test/chain_monitor.ts +++ b/yarn-project/ethereum/src/test/chain_monitor.ts @@ -278,11 +278,32 @@ export class ChainMonitor extends EventEmitter { * {@link waitUntilCheckpoint} (which waits for a target number), this lets callers wait for an * arbitrary checkpoint property (e.g. one published in the first half of its slot). Rejects after * `opts.timeout` ms if provided; otherwise waits indefinitely. + * + * By default this is purely event-driven and only resolves on the *next* matching checkpoint that + * arrives after the call. Set `checkCurrentCheckpoint` to also test the current checkpoint first (via + * a fresh {@link run} snapshot) and short-circuit if it already satisfies `match`. Use it only for + * latching state predicates (e.g. "checkpoint number has passed N"), where an already-satisfied + * result is valid and you want to avoid missing an advance that landed before the listener attached. + * Do NOT set it when the predicate depends on observing the checkpoint live (e.g. one published + * mid-slot that the caller then times against wall-clock), since it may return a checkpoint whose + * slot has already elapsed. */ - public waitForCheckpoint( + public async waitForCheckpoint( match: (event: ChainMonitorEventMap['checkpoint'][0]) => boolean, - opts: { timeout?: number } = {}, + opts: { timeout?: number; checkCurrentCheckpoint?: boolean } = {}, ): Promise { + if (opts.checkCurrentCheckpoint) { + await this.run(); + const current: ChainMonitorEventMap['checkpoint'][0] = { + checkpointNumber: this.checkpointNumber, + l1BlockNumber: this.l1BlockNumber, + l2SlotNumber: this.l2SlotNumber, + timestamp: this.checkpointTimestamp, + }; + if (match(current)) { + return current; + } + } return new Promise((resolve, reject) => { let timer: NodeJS.Timeout | undefined; const listener = (event: ChainMonitorEventMap['checkpoint'][0]) => { diff --git a/yarn-project/ethereum/src/test/rollup_cheat_codes.ts b/yarn-project/ethereum/src/test/rollup_cheat_codes.ts index ac5d2f33e048..9aa17c44994d 100644 --- a/yarn-project/ethereum/src/test/rollup_cheat_codes.ts +++ b/yarn-project/ethereum/src/test/rollup_cheat_codes.ts @@ -271,6 +271,30 @@ export class RollupCheatCodes { ); } + /** + * Polls the rollup until its pending checkpoint settles below `checkpoint` on a freshly mined, non-zero + * checkpoint, and returns that new pending checkpoint number. Reads the L1 rollup contract directly + * rather than a node, since a rollback lands on L1 first. + * + * A prune can momentarily drop the pending checkpoint to 0 before the post-deadline propose mines its + * replacement, so a caller detecting a rollback wants the new lower checkpoint, not that transient + * empty state — hence the non-zero guard. + */ + public async waitForCheckpointBelow( + checkpoint: CheckpointNumber, + opts: { timeout?: number; interval?: number } = {}, + ): Promise { + return await retryUntil( + async () => { + const { pending } = await this.getTips(); + return pending > 0 && pending < checkpoint ? pending : undefined; + }, + `rollup checkpoint in (0, ${checkpoint})`, + opts.timeout ?? 60, + opts.interval ?? 1, + ); + } + /** * Overrides the inProgress field of the Inbox contract state * @param howMuch - How many checkpoints to move it forward