diff --git a/yarn-project/end-to-end/src/multi-node/block-production/blob_promotion.test.ts b/yarn-project/end-to-end/src/multi-node/block-production/blob_promotion.test.ts index 10dc50a6e584..d2f5a935e767 100644 --- a/yarn-project/end-to-end/src/multi-node/block-production/blob_promotion.test.ts +++ b/yarn-project/end-to-end/src/multi-node/block-production/blob_promotion.test.ts @@ -1,20 +1,14 @@ -import type { Archiver } from '@aztec/archiver'; -import type { AztecNodeConfig } from '@aztec/aztec-node'; import { Fr } from '@aztec/aztec.js/fields'; import { waitForTx } from '@aztec/aztec.js/node'; -import { asyncMap } from '@aztec/foundation/async-map'; import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; import { retryUntil } from '@aztec/foundation/retry'; import { executeTimeout } from '@aztec/foundation/timer'; -import type { TestWallet } from '../../test-wallet/test_wallet.js'; import { proveAndSendTxs } from '../../test-wallet/utils.js'; -import { MultiNodeTestContext, buildMockGossipValidators } from '../multi_node_test_context.js'; import { type BlockProductionWithProverFixture, - NODE_COUNT, - WIDE_SLOT_TIMING, jest, + setupBlockProductionWithProver, waitForProvenCheckpoint, } from './setup.js'; @@ -36,57 +30,6 @@ describe('multi-node/block-production/blob_promotion', () => { await fixture?.test?.teardown(); }); - /** - * Sets up the pipelining wide-slot context: same timing profile as {@link setupBlockProductionWithProver} plus 500ms mock - * gossip latency, a tighter `maxTxsPerCheckpoint`, and node-0 with checkpoint promotion disabled so - * the blob-promotion behavior of the other nodes can be asserted against it. - */ - async function setupBlobPromotion(): Promise { - const validators = buildMockGossipValidators(NODE_COUNT); - - const test = await MultiNodeTestContext.setup({ - ...WIDE_SLOT_TIMING, - numberOfAccounts: 0, - initialValidators: validators, - mockGossipSubNetwork: true, - mockGossipSubNetworkLatency: 500, // adverse network conditions - startProverNode: true, - maxTxsPerCheckpoint: 24, - inboxLag: 2, - minTxsPerBlock: 1, - maxTxsPerBlock: PIPELINE_MAX_TXS_PER_BLOCK, - pxeOpts: { syncChainTip: 'checkpointed' }, - skipInitialSequencer: true, - }); - - const { context, logger, rollup } = test; - const wallet = context.wallet as TestWallet; - const from = context.accounts[0]; // auto-created by setup - - logger.warn(`Initial setup complete. Starting ${NODE_COUNT} validator nodes.`); - // Clear inherited coinbase so each validator derives coinbase from its own attester key - const nodes = await asyncMap(validators, ({ privateKey }, i) => - test.createValidatorNode([privateKey], { - dontStartSequencer: true, - coinbase: undefined, - // Disable checkpoint promotion on the first node so it always fetches blobs, - // allowing us to assert that other nodes skip blob fetching via promotion. - ...(i === 0 ? { skipPromoteProposedCheckpointDuringL1Sync: true } : {}), - } as Partial), - ); - logger.warn(`Started ${NODE_COUNT} validator nodes.`, { validators: validators.map(v => v.attester.toString()) }); - - wallet.updateNode(nodes[0]); - const archiver = nodes[0].getBlockSource() as Archiver; - - const contract = await test.registerTestContract(wallet); - logger.warn(`Test setup completed.`, { validators: validators.map(v => v.attester.toString()) }); - - const { failEvents } = test.watchNodeSequencerEvents(nodes); - - return { test, context, logger, rollup, archiver, validators, nodes, contract, wallet, from, failEvents }; - } - /** * Waits until the archiver's checkpointed chain tip has reached `targetBlockNumber`, then retrieves all * checkpoints and returns the number of the first one with at least `targetBlockCount` blocks. Used to @@ -121,7 +64,18 @@ describe('multi-node/block-production/blob_promotion', () => { // mined, then verifies node-0 (promotion disabled) fetches blobs while nodes 1-3 (promotion enabled) // skip blob fetching entirely, and that a high-block-count checkpoint built under load still proves. it('promotion-disabled node fetches blobs while peers skip them, and the checkpoint proves', async () => { - fixture = await setupBlobPromotion(); + // Same wide-slot prover-backed cluster as the rest of this directory, plus adverse gossip latency, a + // tighter maxTxsPerCheckpoint, and node 0 with checkpoint promotion disabled so its blob-fetching can + // be contrasted with the promotion-enabled peers. + fixture = await setupBlockProductionWithProver({ + syncChainTip: 'checkpointed', + minTxsPerBlock: 1, + maxTxsPerBlock: PIPELINE_MAX_TXS_PER_BLOCK, + maxTxsPerCheckpoint: 24, + mockGossipSubNetworkLatency: 500, + clearInheritedCoinbase: true, + disableCheckpointPromotionOnFirstNode: true, + }); const { test, context, logger, nodes, contract, from } = fixture; // Spy on getBlobSidecar on all validator nodes before sequencers start, so we check that nodes diff --git a/yarn-project/end-to-end/src/multi-node/block-production/setup.ts b/yarn-project/end-to-end/src/multi-node/block-production/setup.ts index d04826ca89cf..20a4bec1ca73 100644 --- a/yarn-project/end-to-end/src/multi-node/block-production/setup.ts +++ b/yarn-project/end-to-end/src/multi-node/block-production/setup.ts @@ -76,11 +76,14 @@ export type BlockProductionWithProverFixture = { failEvents: TrackedSequencerEvent[]; }; +/** Per-validator node config, or a function deriving it from the validator's 0-based index. */ +type ValidatorNodeOpts = Partial & { dontStartSequencer?: boolean }; + /** Shared spine: builds N mock-gossip validators, sets up the context, spawns one node per validator. */ async function buildValidatorCluster(opts: { nodeCount: number; setupOpts: Partial; - nodeOpts?: Partial & { dontStartSequencer?: boolean }; + nodeOpts?: ValidatorNodeOpts | ((index: number) => ValidatorNodeOpts); }): Promise { const validators = buildMockGossipValidators(opts.nodeCount); @@ -93,8 +96,11 @@ async function buildValidatorCluster(opts: { const from = context.accounts[0]; logger.warn(`Initial setup complete. Starting ${opts.nodeCount} validator nodes.`); - const nodes = await asyncMap(validators, ({ privateKey }) => - test.createValidatorNode([privateKey], { ...opts.nodeOpts }), + const nodes = await asyncMap(validators, ({ privateKey }, i) => + test.createValidatorNode( + [privateKey], + typeof opts.nodeOpts === 'function' ? opts.nodeOpts(i) : { ...opts.nodeOpts }, + ), ); logger.warn(`Started ${opts.nodeCount} validator nodes.`, { validators: validators.map(v => v.attester.toString()) }); @@ -124,16 +130,33 @@ export function setupSimpleBlockProduction(opts: { /** * Creates validators and sets up a wide-slot test context with the pipelining timing profile and a prover * node, then starts (paused) validator nodes and points the wallet at node 0. Mirrors the per-test - * setup from the dissolved `mbps.parallel` file. + * setup from the dissolved `mbps.parallel` file. The blob-promotion and pipeline-prune suites layer their + * adverse-network shape on via `mockGossipSubNetworkLatency`, `maxTxsPerCheckpoint`, `clearInheritedCoinbase`, + * and `disableCheckpointPromotionOnFirstNode`. */ export async function setupBlockProductionWithProver(opts: { syncChainTip: 'proposed' | 'checkpointed'; minTxsPerBlock?: number; maxTxsPerBlock?: number; + maxTxsPerCheckpoint?: number; buildCheckpointIfEmpty?: boolean; skipPushProposedBlocksToArchiver?: boolean; + /** Injects artificial mock-gossip propagation latency (ms) to model adverse network conditions. */ + mockGossipSubNetworkLatency?: number; + /** Clears each validator node's inherited coinbase so it derives one from its own attester key. */ + clearInheritedCoinbase?: boolean; + /** + * Disables checkpoint promotion on node 0 (`skipPromoteProposedCheckpointDuringL1Sync`), so node 0 fetches + * blobs during L1 sync while its peers promote their own proposed checkpoints and skip the blob fetch. + */ + disableCheckpointPromotionOnFirstNode?: boolean; }): Promise { - const { syncChainTip = 'checkpointed', ...setupOpts } = opts; + const { + syncChainTip = 'checkpointed', + clearInheritedCoinbase = false, + disableCheckpointPromotionOnFirstNode = false, + ...setupOpts + } = opts; // WIDE_SLOT_TIMING is the wide 72s/12s pipelining cadence (see A-914 on why the tighter 36s/4s breaks // non-proposer nodes); the JSDoc on the profile carries the full rationale. @@ -149,7 +172,13 @@ export async function setupBlockProductionWithProver(opts: { skipInitialSequencer: true, inboxLag: 2, }, - nodeOpts: { dontStartSequencer: true }, + nodeOpts: (index: number) => ({ + dontStartSequencer: true, + ...(clearInheritedCoinbase ? { coinbase: undefined } : {}), + ...(disableCheckpointPromotionOnFirstNode && index === 0 + ? { skipPromoteProposedCheckpointDuringL1Sync: true } + : {}), + }), }); const { rollup } = test; diff --git a/yarn-project/end-to-end/src/multi-node/governance/add_rollup.test.ts b/yarn-project/end-to-end/src/multi-node/governance/add_rollup.test.ts index 21272c08e4a7..a81fff2c73f8 100644 --- a/yarn-project/end-to-end/src/multi-node/governance/add_rollup.test.ts +++ b/yarn-project/end-to-end/src/multi-node/governance/add_rollup.test.ts @@ -6,7 +6,7 @@ import { generateClaimSecret } from '@aztec/aztec.js/ethereum'; import { Fr } from '@aztec/aztec.js/fields'; import { waitForL1ToL2MessageReady } from '@aztec/aztec.js/messaging'; import { RollupCheatCodes } from '@aztec/aztec/testing'; -import { FeeAssetHandlerContract, RegistryContract, RollupContract } from '@aztec/ethereum/contracts'; +import { FeeAssetHandlerContract, RegistryContract } from '@aztec/ethereum/contracts'; import { deployRollupForUpgrade } from '@aztec/ethereum/deploy-aztec-l1-contracts'; import { deployL1Contract } from '@aztec/ethereum/deploy-l1-contract'; import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses'; @@ -17,7 +17,6 @@ import { retryUntil } from '@aztec/foundation/retry'; import { sleep } from '@aztec/foundation/sleep'; import { GovernanceAbi, - GovernanceProposerAbi, OutboxAbi, RegisterNewRollupVersionPayloadAbi, RegisterNewRollupVersionPayloadBytecode, @@ -42,7 +41,7 @@ import { MultiNodeTestContext, buildMockGossipValidators, } from '../multi_node_test_context.js'; -import { GOVERNANCE_TIMING, jest } from './setup.js'; +import { GOVERNANCE_TIMING, createGovernanceTestDriver, driveGovernanceRound, jest } from './setup.js'; // Don't set this to a higher value than 9 because each node will use a different L1 publisher account and anvil seeds const NUM_VALIDATORS = 4; @@ -112,44 +111,16 @@ describe('multi-node/governance/add_rollup', () => { it('Should cast votes to add new rollup to registry', async () => { const { context, logger } = test; + const driver = await createGovernanceTestDriver(test, l1TxUtils); + const { governance, governanceProposer, rollup } = driver; + const registry = getContract({ address: getAddress(context.deployL1ContractsValues.l1ContractAddresses.registryAddress.toString()), abi: RegistryAbi, client: context.deployL1ContractsValues.l1Client, }); - const governanceProposer = getContract({ - address: getAddress(context.deployL1ContractsValues.l1ContractAddresses.governanceProposerAddress.toString()), - abi: GovernanceProposerAbi, - client: context.deployL1ContractsValues.l1Client, - }); - - const roundSize = await governanceProposer.read.ROUND_SIZE(); - - const governance = getContract({ - address: getAddress(context.deployL1ContractsValues.l1ContractAddresses.governanceAddress.toString()), - abi: GovernanceAbi, - client: context.deployL1ContractsValues.l1Client, - }); - - const rollup = new RollupContract( - context.deployL1ContractsValues.l1Client, - context.deployL1ContractsValues.l1ContractAddresses.rollupAddress, - ); - - const emperor = context.deployL1ContractsValues.l1Client.account; - - const waitL1Block = async () => { - await l1TxUtils.sendAndMonitorTransaction({ - to: emperor.address, - value: 1n, - }); - }; - - const currentSlot = await rollup.getSlotNumber(); - const nextRoundSlot = SlotNumber.fromBigInt((BigInt(currentSlot) / roundSize) * roundSize + roundSize); - const nextRoundTimestamp = await rollup.getTimestampForSlot(nextRoundSlot); - await context.cheatCodes.eth.warp(Number(nextRoundTimestamp)); + await driver.warpToNextRound(); // Build the new rollup's genesis from the same funded-account set the context used (the additionally // funded accounts plus the sponsored FPC), so the second bridging step can fund `fundedAccounts[1]`. @@ -225,27 +196,7 @@ describe('multi-node/governance/add_rollup', () => { [context.deployL1ContractsValues.l1ContractAddresses.registryAddress.toString(), newRollup.address], ); - const govInfo = async () => { - const bn = await context.cheatCodes.eth.blockNumber(); - const slot = await rollup.getSlotNumber(); - const round = await governanceProposer.read.computeRound([BigInt(slot)]); - - const info = await governanceProposer.read.getRoundData([ - context.deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(), - round, - ]); - const leaderVotes = await governanceProposer.read.signalCount([ - context.deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(), - round, - info.payloadWithMostSignals, - ]); - logger.info( - `Governance stats for round ${round} (Slot: ${slot}, BN: ${bn}). Leader: ${info.payloadWithMostSignals} have ${leaderVotes} signals`, - ); - return { bn, slot, round, info, leaderVotes }; - }; - - await waitL1Block(); + await driver.waitL1Block(); logger.info('Creating nodes'); nodes = await Promise.all( @@ -262,7 +213,7 @@ describe('multi-node/governance/add_rollup', () => { logger.info('Start progressing time to cast votes'); const quorumSize = await governanceProposer.read.QUORUM_SIZE(); - logger.info(`Quorum size: ${quorumSize}, round size: ${await governanceProposer.read.ROUND_SIZE()}`); + logger.info(`Quorum size: ${quorumSize}, round size: ${driver.roundSize}`); const bridging = async ( node: AztecNodeService, @@ -429,56 +380,9 @@ describe('multi-node/governance/add_rollup', () => { context.aztecNodeConfig.l1RpcUrls, ); - // Poll once per L2 slot for the round leader to reach quorum, since validators signal once per slot - const govData = await retryUntil( - async () => { - const govData = await govInfo(); - if (govData.leaderVotes >= quorumSize) { - return govData; - } - }, - 'governance leader reaches quorum', - 600, - context.aztecNodeConfig.aztecSlotDuration, - ); - - const currentSlot2 = await rollup.getSlotNumber(); - const nextRoundSlot2 = SlotNumber.fromBigInt((BigInt(currentSlot2) / roundSize) * roundSize + roundSize); - const nextRoundTimestamp2 = await rollup.getTimestampForSlot(nextRoundSlot2); - logger.info(`Warpping to ${nextRoundTimestamp2}`); - await context.cheatCodes.eth.warp(Number(nextRoundTimestamp2)); - - await waitL1Block(); - - logger.info(`Executing proposal ${govData.round}`); - - await l1TxUtils.sendAndMonitorTransaction({ - to: governanceProposer.address, - data: encodeFunctionData({ - abi: GovernanceProposerAbi, - functionName: 'submitRoundWinner', - args: [govData.round], - }), - }); - logger.info(`Submitted winner for round ${govData.round}`); - - const proposal = await governance.read.getProposal([0n]); - - const timeToActive = proposal.creation + proposal.config.votingDelay; - logger.info(`Warping to ${timeToActive + 1n}`); - await context.cheatCodes.eth.warp(Number(timeToActive + 1n)); - logger.info(`Warped to ${timeToActive + 1n}`); - await waitL1Block(); - - logger.info(`Voting`); - await rollup.vote(l1TxUtils, 0n); - logger.info(`Voted`); - - const timeToExecutable = timeToActive + proposal.config.votingDuration + proposal.config.executionDelay + 1n; - logger.info(`Warping to ${timeToExecutable}`); - await context.cheatCodes.eth.warp(Number(timeToExecutable)); - logger.info(`Warped to ${timeToExecutable}`); - await waitL1Block(); + // Drive the signaled payload to quorum and vote it through to an executable proposal. The 600s quorum + // timeout covers the many slots validators need to accumulate signals while bridging runs concurrently. + await driveGovernanceRound(driver, { quorumTimeoutSeconds: 600 }); const canonicalBefore = EthAddress.fromString(await registry.read.getCanonicalRollup()); expect(canonicalBefore.equals(EthAddress.fromString(rollup.address))).toBe(true); @@ -534,7 +438,7 @@ describe('multi-node/governance/add_rollup', () => { const time = await newRollup.getTimestampForSlot(futureSlot); if (time > BigInt(await context.cheatCodes.eth.lastBlockTimestamp())) { await context.cheatCodes.eth.warp(Number(time)); - await waitL1Block(); + await driver.waitL1Block(); } const newVersion = await newRollup.getVersion(); diff --git a/yarn-project/end-to-end/src/multi-node/governance/setup.ts b/yarn-project/end-to-end/src/multi-node/governance/setup.ts index 186cdb7874a7..1f6124909054 100644 --- a/yarn-project/end-to-end/src/multi-node/governance/setup.ts +++ b/yarn-project/end-to-end/src/multi-node/governance/setup.ts @@ -1,4 +1,14 @@ +import { RollupContract } from '@aztec/ethereum/contracts'; +import type { L1TxUtils } from '@aztec/ethereum/l1-tx-utils'; +import type { ExtendedViemWalletClient } from '@aztec/ethereum/types'; +import { SlotNumber } from '@aztec/foundation/branded-types'; +import { retryUntil } from '@aztec/foundation/retry'; +import { GovernanceAbi, GovernanceProposerAbi } from '@aztec/l1-artifacts'; + import { jest } from '@jest/globals'; +import { type GetContractReturnType, encodeFunctionData, getAddress, getContract } from 'viem'; + +import type { MultiNodeTestContext } from '../multi_node_test_context.js'; jest.setTimeout(1000 * 60 * 10); @@ -14,4 +24,149 @@ export const GOVERNANCE_TIMING = { aztecProofSubmissionEpochs: 640, } as const; +/** The current round leader's payload and its signal count, as observed by {@link GovernanceTestDriver.govInfo}. */ +type GovRoundState = { round: bigint; leaderVotes: bigint }; + +/** + * Wraps the L1 governance-proposer / governance / rollup contracts and the round-driving mechanics shared by + * the governance suites (`add_rollup`, `upgrade_governance_proposer`): warping to round boundaries, polling + * round state, waiting for signal quorum, submitting the round winner, and voting a proposal through to + * executable. Scenario-specific payload construction, node signaling, and post-execution assertions stay in + * each test. + */ +export interface GovernanceTestDriver { + governanceProposer: GetContractReturnType; + governance: GetContractReturnType; + rollup: RollupContract; + roundSize: bigint; + emperor: ExtendedViemWalletClient['account']; + /** Sends a 1-wei self-transfer to mine a fresh L1 block, so warped time takes effect. */ + waitL1Block: () => Promise; + /** Reads the current round's leading payload and its signal count. */ + govInfo: () => Promise; + /** Warps L1 to the start of the next governance-proposer round. */ + warpToNextRound: () => Promise; + /** Polls once per L2 slot until the round leader reaches quorum, returning the round state at that point. */ + waitForQuorum: (quorumSize: bigint, timeoutSeconds: number) => Promise; + /** Submits the winning payload of `round` to the governance proposer, creating the on-chain proposal. */ + submitRoundWinner: (round: bigint) => Promise; + /** + * Warps past the proposal's voting delay, casts a yes vote (asserting it lands), then warps past the + * voting duration and execution delay so the proposal becomes executable. + */ + voteToExecutable: () => Promise; +} + +/** Builds a {@link GovernanceTestDriver} over the context's L1 governance contracts. */ +export async function createGovernanceTestDriver( + test: MultiNodeTestContext, + l1TxUtils: L1TxUtils, +): Promise { + const { l1Client, l1ContractAddresses } = test.context.deployL1ContractsValues; + const rollupAddress = l1ContractAddresses.rollupAddress.toString(); + + const governanceProposer = getContract({ + address: getAddress(l1ContractAddresses.governanceProposerAddress.toString()), + abi: GovernanceProposerAbi, + client: l1Client, + }); + const governance = getContract({ + address: getAddress(l1ContractAddresses.governanceAddress.toString()), + abi: GovernanceAbi, + client: l1Client, + }); + const rollup = new RollupContract(l1Client, l1ContractAddresses.rollupAddress); + const roundSize = await governanceProposer.read.ROUND_SIZE(); + const emperor = l1Client.account; + + const waitL1Block = async () => { + await l1TxUtils.sendAndMonitorTransaction({ to: emperor.address, value: 1n }); + }; + + const govInfo = async (): Promise => { + const bn = await test.context.cheatCodes.eth.blockNumber(); + const slot = await rollup.getSlotNumber(); + const round = await governanceProposer.read.computeRound([BigInt(slot)]); + const info = await governanceProposer.read.getRoundData([rollupAddress, round]); + const leaderVotes = await governanceProposer.read.signalCount([rollupAddress, round, info.payloadWithMostSignals]); + test.logger.info( + `Governance stats for round ${round} (Slot: ${slot}, BN: ${bn}). Leader: ${info.payloadWithMostSignals} have ${leaderVotes} signals`, + ); + return { round, leaderVotes }; + }; + + const warpToNextRound = async () => { + const currentSlot = await rollup.getSlotNumber(); + const nextRoundSlot = SlotNumber.fromBigInt((BigInt(currentSlot) / roundSize) * roundSize + roundSize); + const nextRoundTimestamp = await rollup.getTimestampForSlot(nextRoundSlot); + await test.context.cheatCodes.eth.warp(Number(nextRoundTimestamp)); + }; + + const waitForQuorum = (quorumSize: bigint, timeoutSeconds: number) => + retryUntil( + async () => { + const data = await govInfo(); + return data.leaderVotes >= quorumSize ? data : undefined; + }, + 'governance leader reaches quorum', + timeoutSeconds, + GOVERNANCE_TIMING.aztecSlotDuration, + ); + + const submitRoundWinner = async (round: bigint) => { + await l1TxUtils.sendAndMonitorTransaction({ + to: governanceProposer.address, + data: encodeFunctionData({ abi: GovernanceProposerAbi, functionName: 'submitRoundWinner', args: [round] }), + }); + }; + + const voteToExecutable = async () => { + const proposal = await governance.read.getProposal([0n]); + + const timeToActive = proposal.creation + proposal.config.votingDelay; + await test.context.cheatCodes.eth.warp(Number(timeToActive + 1n)); + await waitL1Block(); + + const voteTx = await rollup.vote(l1TxUtils, 0n); + expect(voteTx.receipt?.status).toBe('success'); + + const timeToExecutable = timeToActive + proposal.config.votingDuration + proposal.config.executionDelay + 1n; + await test.context.cheatCodes.eth.warp(Number(timeToExecutable)); + await waitL1Block(); + }; + + return { + governanceProposer, + governance, + rollup, + roundSize, + emperor, + waitL1Block, + govInfo, + warpToNextRound, + waitForQuorum, + submitRoundWinner, + voteToExecutable, + }; +} + +/** + * Drives one governance round to an executable proposal: waits for the signaled payload to reach quorum, + * warps to the next round, submits the round winner, then votes the resulting proposal through its voting + * and execution delays. Returns the round state observed at quorum; the caller performs the scenario-specific + * execution and assertions. + */ +export async function driveGovernanceRound( + driver: GovernanceTestDriver, + opts: { quorumTimeoutSeconds: number }, +): Promise<{ govData: GovRoundState }> { + const quorumSize = await driver.governanceProposer.read.QUORUM_SIZE(); + const govData = await driver.waitForQuorum(quorumSize, opts.quorumTimeoutSeconds); + await driver.warpToNextRound(); + await driver.waitL1Block(); + await driver.submitRoundWinner(govData.round); + await driver.voteToExecutable(); + return { govData }; +} + export { jest }; diff --git a/yarn-project/end-to-end/src/multi-node/governance/upgrade_governance_proposer.test.ts b/yarn-project/end-to-end/src/multi-node/governance/upgrade_governance_proposer.test.ts index 59bbcc337c55..36a6dd07bf23 100644 --- a/yarn-project/end-to-end/src/multi-node/governance/upgrade_governance_proposer.test.ts +++ b/yarn-project/end-to-end/src/multi-node/governance/upgrade_governance_proposer.test.ts @@ -1,24 +1,16 @@ -import { RollupContract } from '@aztec/ethereum/contracts'; import { deployL1Contract } from '@aztec/ethereum/deploy-l1-contract'; import { L1TxUtils, createL1TxUtils } from '@aztec/ethereum/l1-tx-utils'; -import { SlotNumber } from '@aztec/foundation/branded-types'; -import { retryUntil } from '@aztec/foundation/retry'; import { sleep } from '@aztec/foundation/sleep'; -import { - GovernanceAbi, - GovernanceProposerAbi, - NewGovernanceProposerPayloadAbi, - NewGovernanceProposerPayloadBytecode, -} from '@aztec/l1-artifacts'; +import { NewGovernanceProposerPayloadAbi, NewGovernanceProposerPayloadBytecode } from '@aztec/l1-artifacts'; -import { encodeFunctionData, getAddress, getContract } from 'viem'; +import { getAddress } from 'viem'; import { MOCK_GOSSIP_MULTI_VALIDATOR_OPTS, MultiNodeTestContext, buildMockGossipValidators, } from '../multi_node_test_context.js'; -import { GOVERNANCE_TIMING, jest } from './setup.js'; +import { GOVERNANCE_TIMING, createGovernanceTestDriver, driveGovernanceRound, jest } from './setup.js'; // Don't set this to a higher value than 9 because each node will use a different L1 publisher account and anvil seeds const NUM_VALIDATORS = 4; @@ -62,40 +54,12 @@ describe('multi-node/governance/upgrade_governance_proposer', () => { // warps past round boundary, submits the round winner, then drives the full governance lifecycle // (vote, execution delay, execute). Asserts the governance contract's governanceProposer changes. it('should cast votes to upgrade governanceProposer', async () => { - const governanceProposer = getContract({ - address: getAddress( - test.context.deployL1ContractsValues.l1ContractAddresses.governanceProposerAddress.toString(), - ), - abi: GovernanceProposerAbi, - client: test.context.deployL1ContractsValues.l1Client, - }); - - const roundSize = await governanceProposer.read.ROUND_SIZE(); - - const governance = getContract({ - address: getAddress(test.context.deployL1ContractsValues.l1ContractAddresses.governanceAddress.toString()), - abi: GovernanceAbi, - client: test.context.deployL1ContractsValues.l1Client, - }); - - const rollup = new RollupContract( - test.context.deployL1ContractsValues.l1Client, - test.context.deployL1ContractsValues.l1ContractAddresses.rollupAddress, - ); + const driver = await createGovernanceTestDriver(test, l1TxUtils); + const { governance, rollup } = driver; const gseAddress = await rollup.getGSE(); - const waitL1Block = async () => { - await l1TxUtils.sendAndMonitorTransaction({ - to: emperor.address, - value: 1n, - }); - }; - - const currentSlot = await rollup.getSlotNumber(); - const nextRoundSlot = SlotNumber.fromBigInt((BigInt(currentSlot) / roundSize) * roundSize + roundSize); - const nextRoundTimestamp = await rollup.getTimestampForSlot(nextRoundSlot); - await test.context.cheatCodes.eth.warp(Number(nextRoundTimestamp)); + await driver.warpToNextRound(); const { address: newPayloadAddress } = await deployL1Contract( test.context.deployL1ContractsValues.l1Client, @@ -103,34 +67,11 @@ describe('multi-node/governance/upgrade_governance_proposer', () => { NewGovernanceProposerPayloadBytecode, [test.context.deployL1ContractsValues.l1ContractAddresses.registryAddress.toString(), gseAddress.toString()], ); - test.logger.info(`Deployed new payload at ${newPayloadAddress}`); - const emperor = test.context.deployL1ContractsValues.l1Client.account; - - const govInfo = async () => { - const bn = await test.context.cheatCodes.eth.blockNumber(); - const slot = await rollup.getSlotNumber(); - const round = await governanceProposer.read.computeRound([BigInt(slot)]); - - const info = await governanceProposer.read.getRoundData([ - test.context.deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(), - round, - ]); - const leaderVotes = await governanceProposer.read.signalCount([ - test.context.deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(), - round, - info.payloadWithMostSignals, - ]); - test.logger.info( - `Governance stats for round ${round} (Slot: ${slot}, BN: ${bn}). Leader: ${info.payloadWithMostSignals} have ${leaderVotes} signals`, - ); - return { bn, slot, round, info, leaderVotes }; - }; - - await waitL1Block(); + await driver.waitL1Block(); - const govBefore = await govInfo(); + const govBefore = await driver.govInfo(); test.logger.info('Creating nodes'); // Nodes are torn down by test.teardown(); they only need to be running to signal the payload. @@ -143,78 +84,29 @@ describe('multi-node/governance/upgrade_governance_proposer', () => { await sleep(4000); test.logger.info('Start progressing time to cast signals'); - const quorumSize = await governanceProposer.read.QUORUM_SIZE(); - test.logger.info(`Quorum size: ${quorumSize}, round size: ${await governanceProposer.read.ROUND_SIZE()}`); - - const govData = await retryUntil( - async () => { - const data = await govInfo(); - return data.leaderVotes >= quorumSize ? data : undefined; - }, - 'quorum of signals', - Number(quorumSize) * GOVERNANCE_TIMING.aztecSlotDuration * 3, - GOVERNANCE_TIMING.aztecSlotDuration, - ); - - expect(govData.leaderVotes).toBeGreaterThan(govBefore.leaderVotes); - - const currentSlot2 = await rollup.getSlotNumber(); - const nextRoundSlot2 = SlotNumber.fromBigInt((BigInt(currentSlot2) / roundSize) * roundSize + roundSize); - const nextRoundTimestamp2 = await rollup.getTimestampForSlot(nextRoundSlot2); - test.logger.info(`Warping to ${nextRoundTimestamp2}`); - await test.context.cheatCodes.eth.warp(Number(nextRoundTimestamp2)); + const quorumSize = await driver.governanceProposer.read.QUORUM_SIZE(); + test.logger.info(`Quorum size: ${quorumSize}, round size: ${driver.roundSize}`); - await waitL1Block(); - - test.logger.info(`Submitting winner of round ${govData.round}`); - - await l1TxUtils.sendAndMonitorTransaction({ - to: governanceProposer.address, - data: encodeFunctionData({ - abi: GovernanceProposerAbi, - functionName: 'submitRoundWinner', - args: [govData.round], - }), + const { govData } = await driveGovernanceRound(driver, { + quorumTimeoutSeconds: Number(quorumSize) * GOVERNANCE_TIMING.aztecSlotDuration * 3, }); - test.logger.info(`Submitted winner of round ${govData.round}`); - - const proposal = await governance.read.getProposal([0n]); - - const timeToActive = proposal.creation + proposal.config.votingDelay; - test.logger.info(`Warping to ${timeToActive + 1n}`); - await test.context.cheatCodes.eth.warp(Number(timeToActive + 1n)); - test.logger.info(`Warped to ${timeToActive + 1n}`); - await waitL1Block(); - - test.logger.info(`Voting`); - const voteTx = await rollup.vote(l1TxUtils, 0n); - expect(voteTx.receipt?.status).toBe('success'); - test.logger.info(`Voted`); - - const proposalState = await governance.read.getProposal([0n]); - test.logger.info(`Proposal state`, proposalState); + expect(govData.leaderVotes).toBeGreaterThan(govBefore.leaderVotes); - const timeToExecutable = timeToActive + proposal.config.votingDuration + proposal.config.executionDelay + 1n; - test.logger.info(`Warping to ${timeToExecutable}`); - await test.context.cheatCodes.eth.warp(Number(timeToExecutable)); - test.logger.info(`Warped to ${timeToExecutable}`); - await waitL1Block(); + const governanceProposerAddress = getAddress( + test.context.deployL1ContractsValues.l1ContractAddresses.governanceProposerAddress.toString(), + ); test.logger.info(`Checking governance proposer`); - expect(await governance.read.governanceProposer()).toEqual( - getAddress(test.context.deployL1ContractsValues.l1ContractAddresses.governanceProposerAddress.toString()), - ); + expect(await governance.read.governanceProposer()).toEqual(governanceProposerAddress); test.logger.info(`Governance proposer is correct`); test.logger.info(`Executing proposal`); - const executeTx = await governance.write.execute([0n], { account: emperor }); + const executeTx = await governance.write.execute([0n], { account: driver.emperor }); await test.context.deployL1ContractsValues.l1Client.waitForTransactionReceipt({ hash: executeTx }); test.logger.info(`Executed proposal`); const newGovernanceProposer = await governance.read.governanceProposer(); - expect(newGovernanceProposer).not.toEqual( - getAddress(test.context.deployL1ContractsValues.l1ContractAddresses.governanceProposerAddress.toString()), - ); + expect(newGovernanceProposer).not.toEqual(governanceProposerAddress); expect(await governance.read.getProposalState([0n])).toEqual(5); test.logger.info(`Governance proposer is correct`); }, 1_000_000); diff --git a/yarn-project/end-to-end/src/multi-node/recovery/pipeline_prune.test.ts b/yarn-project/end-to-end/src/multi-node/recovery/pipeline_prune.test.ts index 296aa8e06537..9ed928bbdddd 100644 --- a/yarn-project/end-to-end/src/multi-node/recovery/pipeline_prune.test.ts +++ b/yarn-project/end-to-end/src/multi-node/recovery/pipeline_prune.test.ts @@ -1,33 +1,22 @@ -import type { Archiver } from '@aztec/archiver'; -import type { AztecNodeService } from '@aztec/aztec-node'; -import type { AztecAddress, EthAddress } from '@aztec/aztec.js/addresses'; +import type { EthAddress } from '@aztec/aztec.js/addresses'; import { Fr } from '@aztec/aztec.js/fields'; -import type { Logger } from '@aztec/aztec.js/log'; import { waitForTx } from '@aztec/aztec.js/node'; import type { EpochCacheInterface } from '@aztec/epoch-cache'; -import { asyncMap } from '@aztec/foundation/async-map'; import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types'; import { executeTimeout } from '@aztec/foundation/timer'; -import type { TestContract } from '@aztec/noir-test-contracts.js/Test'; import type { SequencerEvents } from '@aztec/sequencer-client'; import { L2BlockSourceEvents } from '@aztec/stdlib/block'; -import { jest } from '@jest/globals'; - -import type { EndToEndContext } from '../../fixtures/utils.js'; -import type { TestWallet } from '../../test-wallet/test_wallet.js'; import { proveAndSendTxs } from '../../test-wallet/utils.js'; import { + type BlockProductionWithProverFixture, type BlockProposedEvent, - MultiNodeTestContext, - type RegisteredValidator, - WIDE_SLOT_TIMING, - buildMockGossipValidators, -} from '../multi_node_test_context.js'; + jest, + setupBlockProductionWithProver, +} from '../block-production/setup.js'; jest.setTimeout(1000 * 60 * 20); -const NODE_COUNT = 4; const EXPECTED_BLOCKS_PER_CHECKPOINT = 8; // Send enough transactions to trigger multiple blocks within a checkpoint assuming 2 txs per block. @@ -45,68 +34,11 @@ const TX_COUNT = 34; * with mockGossipSubNetwork and no initial sequencer. */ describe('multi-node/recovery/pipeline_prune', () => { - let context: EndToEndContext; - let logger: Logger; - let archiver: Archiver; - - let test: MultiNodeTestContext; - let validators: RegisteredValidator[]; - let nodes: AztecNodeService[]; - let contract: TestContract; - let wallet: TestWallet; - let from: AztecAddress; - - /** Creates validators and sets up the test context with MBPS and proposer pipelining. */ - async function setupTest(opts: { - syncChainTip: 'proposed' | 'checkpointed'; - minTxsPerBlock?: number; - maxTxsPerBlock?: number; - }) { - const { syncChainTip = 'checkpointed', ...setupOpts } = opts; - - validators = buildMockGossipValidators(NODE_COUNT); - - test = await MultiNodeTestContext.setup({ - ...WIDE_SLOT_TIMING, - numberOfAccounts: 0, - initialValidators: validators, - mockGossipSubNetwork: true, - mockGossipSubNetworkLatency: 500, // adverse network conditions - startProverNode: true, - maxTxsPerCheckpoint: 24, - inboxLag: 2, - ...setupOpts, - pxeOpts: { syncChainTip }, - skipInitialSequencer: true, - }); - - ({ context, logger } = test); - wallet = context.wallet as TestWallet; - from = context.accounts[0]; // auto-created by setup - - logger.warn(`Initial setup complete. Starting ${NODE_COUNT} validator nodes.`); - // Clear inherited coinbase so each validator derives coinbase from its own attester key - nodes = await asyncMap(validators, ({ privateKey }, i) => - test.createValidatorNode([privateKey], { - dontStartSequencer: true, - coinbase: undefined, - // Disable checkpoint promotion on the first node so it always fetches blobs, - // allowing us to assert that other nodes skip blob fetching via promotion. - ...(i === 0 ? { skipPromoteProposedCheckpointDuringL1Sync: true } : {}), - }), - ); - logger.warn(`Started ${NODE_COUNT} validator nodes.`, { validators: validators.map(v => v.attester.toString()) }); - - wallet.updateNode(nodes[0]); - archiver = nodes[0].getBlockSource() as Archiver; - - contract = await test.registerTestContract(wallet); - logger.warn(`Test setup completed.`, { validators: validators.map(v => v.attester.toString()) }); - } + let fixture: BlockProductionWithProverFixture; afterEach(async () => { jest.restoreAllMocks(); - await test?.teardown(); + await fixture?.test?.teardown(); }); // Establishes a baseline at checkpoint 1. Identifies the next proposer and disables its @@ -114,7 +46,18 @@ describe('multi-node/recovery/pipeline_prune', () => { // re-enables publishing. Waits for all txs to be mined, asserts a MBPS checkpoint exists, // verifies the pipelining offset, and checks recovery blockNumber > baseline. it('prunes uncheckpointed blocks when proposer fails to deliver', async () => { - await setupTest({ syncChainTip: 'checkpointed', minTxsPerBlock: 1, maxTxsPerBlock: 2 }); + // Same wide-slot prover-backed cluster as block-production, under adverse gossip latency with node 0's + // checkpoint promotion disabled (see setupBlockProductionWithProver). + fixture = await setupBlockProductionWithProver({ + syncChainTip: 'checkpointed', + minTxsPerBlock: 1, + maxTxsPerBlock: 2, + maxTxsPerCheckpoint: 24, + mockGossipSubNetworkLatency: 500, + clearInheritedCoinbase: true, + disableCheckpointPromotionOnFirstNode: true, + }); + const { test, context, logger, archiver, validators, nodes, contract, from } = fixture; const blockProposedEvents: BlockProposedEvent[] = []; const sequencers = test.getSequencers(nodes);