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
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<BlockProductionWithProverFixture> {
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<AztecNodeConfig>),
);
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
Expand Down Expand Up @@ -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
Expand Down
41 changes: 35 additions & 6 deletions yarn-project/end-to-end/src/multi-node/block-production/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AztecNodeConfig> & { 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<MultiNodeTestOpts>;
nodeOpts?: Partial<AztecNodeConfig> & { dontStartSequencer?: boolean };
nodeOpts?: ValidatorNodeOpts | ((index: number) => ValidatorNodeOpts);
}): Promise<SimpleBlockProductionFixture> {
const validators = buildMockGossipValidators(opts.nodeCount);

Expand All @@ -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()) });

Expand Down Expand Up @@ -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<BlockProductionWithProverFixture> {
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.
Expand All @@ -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;
Expand Down
120 changes: 12 additions & 108 deletions yarn-project/end-to-end/src/multi-node/governance/add_rollup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -17,7 +17,6 @@ import { retryUntil } from '@aztec/foundation/retry';
import { sleep } from '@aztec/foundation/sleep';
import {
GovernanceAbi,
GovernanceProposerAbi,
OutboxAbi,
RegisterNewRollupVersionPayloadAbi,
RegisterNewRollupVersionPayloadBytecode,
Expand All @@ -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;
Expand Down Expand Up @@ -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]`.
Expand Down Expand Up @@ -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(
Expand 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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
Loading
Loading