Skip to content

Commit 78594d7

Browse files
authored
test(e2e): dedupe multi-node block-production and governance test setups (#24498)
Round-2 e2e consolidation, PR 7 of 9. Pure dedup/refactor of multi-node test setups — no timing or config values change; every asserted behavior is preserved. ## What changed - **block-production/blob_promotion + recovery/pipeline_prune → shared `setupBlockProductionWithProver`.** Both files reimplemented the same 4-validator + prover + `WIDE_SLOT_TIMING` + `mockGossipSubNetworkLatency: 500` cluster locally (`setupBlobPromotion` / `setupTest`). Those local helpers are deleted and both now call the shared helper, which gained four opt-in options: `mockGossipSubNetworkLatency`, `maxTxsPerCheckpoint`, `clearInheritedCoinbase` (per-attester coinbase), and `disableCheckpointPromotionOnFirstNode`. `buildValidatorCluster` now accepts a per-index `nodeOpts` function so node 0 can be configured distinctly. **The resolved setup options and per-node config are identical to the originals** (same keys/values; existing callers `cross_chain_messages`/`deploy_and_call_ordering`/`proposed_chain` are unaffected because both new booleans default to false). One benign side effect: pipeline_prune now also installs `watchNodeSequencerEvents` via the helper — instrumentation only, no assertions consume it. (recovery → block-production cross-directory import, as permitted by the plan.) - **governance mechanics dedup.** Extracted `createGovernanceTestDriver` + `driveGovernanceRound` into `governance/setup.ts`, wrapping the `govInfo` / round-boundary warp / quorum wait / submit-winner / vote-through-delays mechanics duplicated by `add_rollup` and `upgrade_governance_proposer`. Both files adopt it; their scenario-specific parts (payload construction, node signaling, node migration + bridging in add_rollup, pre/post-execute assertions) stay in place. Each caller passes its own quorum timeout (`upgrade`: `quorumSize * aztecSlotDuration * 3`; `add_rollup`: `600`) so no timing value changes. The vote-success assertion now lives in the driver and covers both callers. ## Dropped assertions None. The `upgrade_governance_proposer` vote-success assertion is preserved (moved into the driver's `voteToExecutable`, where it now also covers `add_rollup`). ## Local test runs (all pass) - `upgrade_governance_proposer` — 1 passed (116s) - `block-production/blob_promotion` — 1 passed (293s) - `recovery/pipeline_prune` — 1 passed (367s) - `governance/add_rollup` — 1 passed (327s locally); also green in CI.
1 parent 842cb8d commit 78594d7

6 files changed

Lines changed: 253 additions & 376 deletions

File tree

yarn-project/end-to-end/src/multi-node/block-production/blob_promotion.test.ts

Lines changed: 13 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
1-
import type { Archiver } from '@aztec/archiver';
2-
import type { AztecNodeConfig } from '@aztec/aztec-node';
31
import { Fr } from '@aztec/aztec.js/fields';
42
import { waitForTx } from '@aztec/aztec.js/node';
5-
import { asyncMap } from '@aztec/foundation/async-map';
63
import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
74
import { retryUntil } from '@aztec/foundation/retry';
85
import { executeTimeout } from '@aztec/foundation/timer';
96

10-
import type { TestWallet } from '../../test-wallet/test_wallet.js';
117
import { proveAndSendTxs } from '../../test-wallet/utils.js';
12-
import { MultiNodeTestContext, buildMockGossipValidators } from '../multi_node_test_context.js';
138
import {
149
type BlockProductionWithProverFixture,
15-
NODE_COUNT,
16-
WIDE_SLOT_TIMING,
1710
jest,
11+
setupBlockProductionWithProver,
1812
waitForProvenCheckpoint,
1913
} from './setup.js';
2014

@@ -36,57 +30,6 @@ describe('multi-node/block-production/blob_promotion', () => {
3630
await fixture?.test?.teardown();
3731
});
3832

39-
/**
40-
* Sets up the pipelining wide-slot context: same timing profile as {@link setupBlockProductionWithProver} plus 500ms mock
41-
* gossip latency, a tighter `maxTxsPerCheckpoint`, and node-0 with checkpoint promotion disabled so
42-
* the blob-promotion behavior of the other nodes can be asserted against it.
43-
*/
44-
async function setupBlobPromotion(): Promise<BlockProductionWithProverFixture> {
45-
const validators = buildMockGossipValidators(NODE_COUNT);
46-
47-
const test = await MultiNodeTestContext.setup({
48-
...WIDE_SLOT_TIMING,
49-
numberOfAccounts: 0,
50-
initialValidators: validators,
51-
mockGossipSubNetwork: true,
52-
mockGossipSubNetworkLatency: 500, // adverse network conditions
53-
startProverNode: true,
54-
maxTxsPerCheckpoint: 24,
55-
inboxLag: 2,
56-
minTxsPerBlock: 1,
57-
maxTxsPerBlock: PIPELINE_MAX_TXS_PER_BLOCK,
58-
pxeOpts: { syncChainTip: 'checkpointed' },
59-
skipInitialSequencer: true,
60-
});
61-
62-
const { context, logger, rollup } = test;
63-
const wallet = context.wallet as TestWallet;
64-
const from = context.accounts[0]; // auto-created by setup
65-
66-
logger.warn(`Initial setup complete. Starting ${NODE_COUNT} validator nodes.`);
67-
// Clear inherited coinbase so each validator derives coinbase from its own attester key
68-
const nodes = await asyncMap(validators, ({ privateKey }, i) =>
69-
test.createValidatorNode([privateKey], {
70-
dontStartSequencer: true,
71-
coinbase: undefined,
72-
// Disable checkpoint promotion on the first node so it always fetches blobs,
73-
// allowing us to assert that other nodes skip blob fetching via promotion.
74-
...(i === 0 ? { skipPromoteProposedCheckpointDuringL1Sync: true } : {}),
75-
} as Partial<AztecNodeConfig>),
76-
);
77-
logger.warn(`Started ${NODE_COUNT} validator nodes.`, { validators: validators.map(v => v.attester.toString()) });
78-
79-
wallet.updateNode(nodes[0]);
80-
const archiver = nodes[0].getBlockSource() as Archiver;
81-
82-
const contract = await test.registerTestContract(wallet);
83-
logger.warn(`Test setup completed.`, { validators: validators.map(v => v.attester.toString()) });
84-
85-
const { failEvents } = test.watchNodeSequencerEvents(nodes);
86-
87-
return { test, context, logger, rollup, archiver, validators, nodes, contract, wallet, from, failEvents };
88-
}
89-
9033
/**
9134
* Waits until the archiver's checkpointed chain tip has reached `targetBlockNumber`, then retrieves all
9235
* 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', () => {
12164
// mined, then verifies node-0 (promotion disabled) fetches blobs while nodes 1-3 (promotion enabled)
12265
// skip blob fetching entirely, and that a high-block-count checkpoint built under load still proves.
12366
it('promotion-disabled node fetches blobs while peers skip them, and the checkpoint proves', async () => {
124-
fixture = await setupBlobPromotion();
67+
// Same wide-slot prover-backed cluster as the rest of this directory, plus adverse gossip latency, a
68+
// tighter maxTxsPerCheckpoint, and node 0 with checkpoint promotion disabled so its blob-fetching can
69+
// be contrasted with the promotion-enabled peers.
70+
fixture = await setupBlockProductionWithProver({
71+
syncChainTip: 'checkpointed',
72+
minTxsPerBlock: 1,
73+
maxTxsPerBlock: PIPELINE_MAX_TXS_PER_BLOCK,
74+
maxTxsPerCheckpoint: 24,
75+
mockGossipSubNetworkLatency: 500,
76+
clearInheritedCoinbase: true,
77+
disableCheckpointPromotionOnFirstNode: true,
78+
});
12579
const { test, context, logger, nodes, contract, from } = fixture;
12680

12781
// Spy on getBlobSidecar on all validator nodes before sequencers start, so we check that nodes

yarn-project/end-to-end/src/multi-node/block-production/setup.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,14 @@ export type BlockProductionWithProverFixture = {
7676
failEvents: TrackedSequencerEvent[];
7777
};
7878

79+
/** Per-validator node config, or a function deriving it from the validator's 0-based index. */
80+
type ValidatorNodeOpts = Partial<AztecNodeConfig> & { dontStartSequencer?: boolean };
81+
7982
/** Shared spine: builds N mock-gossip validators, sets up the context, spawns one node per validator. */
8083
async function buildValidatorCluster(opts: {
8184
nodeCount: number;
8285
setupOpts: Partial<MultiNodeTestOpts>;
83-
nodeOpts?: Partial<AztecNodeConfig> & { dontStartSequencer?: boolean };
86+
nodeOpts?: ValidatorNodeOpts | ((index: number) => ValidatorNodeOpts);
8487
}): Promise<SimpleBlockProductionFixture> {
8588
const validators = buildMockGossipValidators(opts.nodeCount);
8689

@@ -93,8 +96,11 @@ async function buildValidatorCluster(opts: {
9396
const from = context.accounts[0];
9497

9598
logger.warn(`Initial setup complete. Starting ${opts.nodeCount} validator nodes.`);
96-
const nodes = await asyncMap(validators, ({ privateKey }) =>
97-
test.createValidatorNode([privateKey], { ...opts.nodeOpts }),
99+
const nodes = await asyncMap(validators, ({ privateKey }, i) =>
100+
test.createValidatorNode(
101+
[privateKey],
102+
typeof opts.nodeOpts === 'function' ? opts.nodeOpts(i) : { ...opts.nodeOpts },
103+
),
98104
);
99105
logger.warn(`Started ${opts.nodeCount} validator nodes.`, { validators: validators.map(v => v.attester.toString()) });
100106

@@ -124,16 +130,33 @@ export function setupSimpleBlockProduction(opts: {
124130
/**
125131
* Creates validators and sets up a wide-slot test context with the pipelining timing profile and a prover
126132
* node, then starts (paused) validator nodes and points the wallet at node 0. Mirrors the per-test
127-
* setup from the dissolved `mbps.parallel` file.
133+
* setup from the dissolved `mbps.parallel` file. The blob-promotion and pipeline-prune suites layer their
134+
* adverse-network shape on via `mockGossipSubNetworkLatency`, `maxTxsPerCheckpoint`, `clearInheritedCoinbase`,
135+
* and `disableCheckpointPromotionOnFirstNode`.
128136
*/
129137
export async function setupBlockProductionWithProver(opts: {
130138
syncChainTip: 'proposed' | 'checkpointed';
131139
minTxsPerBlock?: number;
132140
maxTxsPerBlock?: number;
141+
maxTxsPerCheckpoint?: number;
133142
buildCheckpointIfEmpty?: boolean;
134143
skipPushProposedBlocksToArchiver?: boolean;
144+
/** Injects artificial mock-gossip propagation latency (ms) to model adverse network conditions. */
145+
mockGossipSubNetworkLatency?: number;
146+
/** Clears each validator node's inherited coinbase so it derives one from its own attester key. */
147+
clearInheritedCoinbase?: boolean;
148+
/**
149+
* Disables checkpoint promotion on node 0 (`skipPromoteProposedCheckpointDuringL1Sync`), so node 0 fetches
150+
* blobs during L1 sync while its peers promote their own proposed checkpoints and skip the blob fetch.
151+
*/
152+
disableCheckpointPromotionOnFirstNode?: boolean;
135153
}): Promise<BlockProductionWithProverFixture> {
136-
const { syncChainTip = 'checkpointed', ...setupOpts } = opts;
154+
const {
155+
syncChainTip = 'checkpointed',
156+
clearInheritedCoinbase = false,
157+
disableCheckpointPromotionOnFirstNode = false,
158+
...setupOpts
159+
} = opts;
137160

138161
// WIDE_SLOT_TIMING is the wide 72s/12s pipelining cadence (see A-914 on why the tighter 36s/4s breaks
139162
// non-proposer nodes); the JSDoc on the profile carries the full rationale.
@@ -149,7 +172,13 @@ export async function setupBlockProductionWithProver(opts: {
149172
skipInitialSequencer: true,
150173
inboxLag: 2,
151174
},
152-
nodeOpts: { dontStartSequencer: true },
175+
nodeOpts: (index: number) => ({
176+
dontStartSequencer: true,
177+
...(clearInheritedCoinbase ? { coinbase: undefined } : {}),
178+
...(disableCheckpointPromotionOnFirstNode && index === 0
179+
? { skipPromoteProposedCheckpointDuringL1Sync: true }
180+
: {}),
181+
}),
153182
});
154183

155184
const { rollup } = test;

yarn-project/end-to-end/src/multi-node/governance/add_rollup.test.ts

Lines changed: 12 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { generateClaimSecret } from '@aztec/aztec.js/ethereum';
66
import { Fr } from '@aztec/aztec.js/fields';
77
import { waitForL1ToL2MessageReady } from '@aztec/aztec.js/messaging';
88
import { RollupCheatCodes } from '@aztec/aztec/testing';
9-
import { FeeAssetHandlerContract, RegistryContract, RollupContract } from '@aztec/ethereum/contracts';
9+
import { FeeAssetHandlerContract, RegistryContract } from '@aztec/ethereum/contracts';
1010
import { deployRollupForUpgrade } from '@aztec/ethereum/deploy-aztec-l1-contracts';
1111
import { deployL1Contract } from '@aztec/ethereum/deploy-l1-contract';
1212
import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses';
@@ -17,7 +17,6 @@ import { retryUntil } from '@aztec/foundation/retry';
1717
import { sleep } from '@aztec/foundation/sleep';
1818
import {
1919
GovernanceAbi,
20-
GovernanceProposerAbi,
2120
OutboxAbi,
2221
RegisterNewRollupVersionPayloadAbi,
2322
RegisterNewRollupVersionPayloadBytecode,
@@ -42,7 +41,7 @@ import {
4241
MultiNodeTestContext,
4342
buildMockGossipValidators,
4443
} from '../multi_node_test_context.js';
45-
import { GOVERNANCE_TIMING, jest } from './setup.js';
44+
import { GOVERNANCE_TIMING, createGovernanceTestDriver, driveGovernanceRound, jest } from './setup.js';
4645

4746
// Don't set this to a higher value than 9 because each node will use a different L1 publisher account and anvil seeds
4847
const NUM_VALIDATORS = 4;
@@ -112,44 +111,16 @@ describe('multi-node/governance/add_rollup', () => {
112111
it('Should cast votes to add new rollup to registry', async () => {
113112
const { context, logger } = test;
114113

114+
const driver = await createGovernanceTestDriver(test, l1TxUtils);
115+
const { governance, governanceProposer, rollup } = driver;
116+
115117
const registry = getContract({
116118
address: getAddress(context.deployL1ContractsValues.l1ContractAddresses.registryAddress.toString()),
117119
abi: RegistryAbi,
118120
client: context.deployL1ContractsValues.l1Client,
119121
});
120122

121-
const governanceProposer = getContract({
122-
address: getAddress(context.deployL1ContractsValues.l1ContractAddresses.governanceProposerAddress.toString()),
123-
abi: GovernanceProposerAbi,
124-
client: context.deployL1ContractsValues.l1Client,
125-
});
126-
127-
const roundSize = await governanceProposer.read.ROUND_SIZE();
128-
129-
const governance = getContract({
130-
address: getAddress(context.deployL1ContractsValues.l1ContractAddresses.governanceAddress.toString()),
131-
abi: GovernanceAbi,
132-
client: context.deployL1ContractsValues.l1Client,
133-
});
134-
135-
const rollup = new RollupContract(
136-
context.deployL1ContractsValues.l1Client,
137-
context.deployL1ContractsValues.l1ContractAddresses.rollupAddress,
138-
);
139-
140-
const emperor = context.deployL1ContractsValues.l1Client.account;
141-
142-
const waitL1Block = async () => {
143-
await l1TxUtils.sendAndMonitorTransaction({
144-
to: emperor.address,
145-
value: 1n,
146-
});
147-
};
148-
149-
const currentSlot = await rollup.getSlotNumber();
150-
const nextRoundSlot = SlotNumber.fromBigInt((BigInt(currentSlot) / roundSize) * roundSize + roundSize);
151-
const nextRoundTimestamp = await rollup.getTimestampForSlot(nextRoundSlot);
152-
await context.cheatCodes.eth.warp(Number(nextRoundTimestamp));
123+
await driver.warpToNextRound();
153124

154125
// Build the new rollup's genesis from the same funded-account set the context used (the additionally
155126
// 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', () => {
225196
[context.deployL1ContractsValues.l1ContractAddresses.registryAddress.toString(), newRollup.address],
226197
);
227198

228-
const govInfo = async () => {
229-
const bn = await context.cheatCodes.eth.blockNumber();
230-
const slot = await rollup.getSlotNumber();
231-
const round = await governanceProposer.read.computeRound([BigInt(slot)]);
232-
233-
const info = await governanceProposer.read.getRoundData([
234-
context.deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(),
235-
round,
236-
]);
237-
const leaderVotes = await governanceProposer.read.signalCount([
238-
context.deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(),
239-
round,
240-
info.payloadWithMostSignals,
241-
]);
242-
logger.info(
243-
`Governance stats for round ${round} (Slot: ${slot}, BN: ${bn}). Leader: ${info.payloadWithMostSignals} have ${leaderVotes} signals`,
244-
);
245-
return { bn, slot, round, info, leaderVotes };
246-
};
247-
248-
await waitL1Block();
199+
await driver.waitL1Block();
249200

250201
logger.info('Creating nodes');
251202
nodes = await Promise.all(
@@ -262,7 +213,7 @@ describe('multi-node/governance/add_rollup', () => {
262213

263214
logger.info('Start progressing time to cast votes');
264215
const quorumSize = await governanceProposer.read.QUORUM_SIZE();
265-
logger.info(`Quorum size: ${quorumSize}, round size: ${await governanceProposer.read.ROUND_SIZE()}`);
216+
logger.info(`Quorum size: ${quorumSize}, round size: ${driver.roundSize}`);
266217

267218
const bridging = async (
268219
node: AztecNodeService,
@@ -429,56 +380,9 @@ describe('multi-node/governance/add_rollup', () => {
429380
context.aztecNodeConfig.l1RpcUrls,
430381
);
431382

432-
// Poll once per L2 slot for the round leader to reach quorum, since validators signal once per slot
433-
const govData = await retryUntil(
434-
async () => {
435-
const govData = await govInfo();
436-
if (govData.leaderVotes >= quorumSize) {
437-
return govData;
438-
}
439-
},
440-
'governance leader reaches quorum',
441-
600,
442-
context.aztecNodeConfig.aztecSlotDuration,
443-
);
444-
445-
const currentSlot2 = await rollup.getSlotNumber();
446-
const nextRoundSlot2 = SlotNumber.fromBigInt((BigInt(currentSlot2) / roundSize) * roundSize + roundSize);
447-
const nextRoundTimestamp2 = await rollup.getTimestampForSlot(nextRoundSlot2);
448-
logger.info(`Warpping to ${nextRoundTimestamp2}`);
449-
await context.cheatCodes.eth.warp(Number(nextRoundTimestamp2));
450-
451-
await waitL1Block();
452-
453-
logger.info(`Executing proposal ${govData.round}`);
454-
455-
await l1TxUtils.sendAndMonitorTransaction({
456-
to: governanceProposer.address,
457-
data: encodeFunctionData({
458-
abi: GovernanceProposerAbi,
459-
functionName: 'submitRoundWinner',
460-
args: [govData.round],
461-
}),
462-
});
463-
logger.info(`Submitted winner for round ${govData.round}`);
464-
465-
const proposal = await governance.read.getProposal([0n]);
466-
467-
const timeToActive = proposal.creation + proposal.config.votingDelay;
468-
logger.info(`Warping to ${timeToActive + 1n}`);
469-
await context.cheatCodes.eth.warp(Number(timeToActive + 1n));
470-
logger.info(`Warped to ${timeToActive + 1n}`);
471-
await waitL1Block();
472-
473-
logger.info(`Voting`);
474-
await rollup.vote(l1TxUtils, 0n);
475-
logger.info(`Voted`);
476-
477-
const timeToExecutable = timeToActive + proposal.config.votingDuration + proposal.config.executionDelay + 1n;
478-
logger.info(`Warping to ${timeToExecutable}`);
479-
await context.cheatCodes.eth.warp(Number(timeToExecutable));
480-
logger.info(`Warped to ${timeToExecutable}`);
481-
await waitL1Block();
383+
// Drive the signaled payload to quorum and vote it through to an executable proposal. The 600s quorum
384+
// timeout covers the many slots validators need to accumulate signals while bridging runs concurrently.
385+
await driveGovernanceRound(driver, { quorumTimeoutSeconds: 600 });
482386

483387
const canonicalBefore = EthAddress.fromString(await registry.read.getCanonicalRollup());
484388
expect(canonicalBefore.equals(EthAddress.fromString(rollup.address))).toBe(true);
@@ -534,7 +438,7 @@ describe('multi-node/governance/add_rollup', () => {
534438
const time = await newRollup.getTimestampForSlot(futureSlot);
535439
if (time > BigInt(await context.cheatCodes.eth.lastBlockTimestamp())) {
536440
await context.cheatCodes.eth.warp(Number(time));
537-
await waitL1Block();
441+
await driver.waitL1Block();
538442
}
539443

540444
const newVersion = await newRollup.getVersion();

0 commit comments

Comments
 (0)