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
15 changes: 10 additions & 5 deletions yarn-project/end-to-end/src/automine/contracts/state_vars.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
35 changes: 20 additions & 15 deletions yarn-project/end-to-end/src/automine/effects/pruned_blocks.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 0 additions & 2 deletions yarn-project/end-to-end/src/p2p/rediscovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
62 changes: 39 additions & 23 deletions yarn-project/end-to-end/src/single-node/fees/fee_settings.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<GasFees> {
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.
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions yarn-project/end-to-end/src/single-node/fees/fees_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<EpochNumber, SlotNumber>();
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()) {
Expand Down
Loading
Loading