Skip to content

Commit f9dcfd7

Browse files
committed
test(e2e): enable pipelining on p2p tests
1 parent 0b96c13 commit f9dcfd7

18 files changed

Lines changed: 131 additions & 17 deletions

yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { EthAddress } from '@aztec/aztec.js/addresses';
55
import { waitForProven } from '@aztec/aztec.js/contracts';
66
import { generateClaimSecret } from '@aztec/aztec.js/ethereum';
77
import { Fr } from '@aztec/aztec.js/fields';
8+
import { waitForL1ToL2MessageReady } from '@aztec/aztec.js/messaging';
89
import { RollupCheatCodes } from '@aztec/aztec/testing';
910
import { FeeAssetHandlerContract, RegistryContract, RollupContract } from '@aztec/ethereum/contracts';
1011
import { deployRollupForUpgrade } from '@aztec/ethereum/deploy-aztec-l1-contracts';
@@ -13,7 +14,6 @@ import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses'
1314
import { L1TxUtils, createL1TxUtils } from '@aztec/ethereum/l1-tx-utils';
1415
import type { ExtendedViemWalletClient } from '@aztec/ethereum/types';
1516
import { CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
16-
import { retryUntil } from '@aztec/foundation/retry';
1717
import { sleep } from '@aztec/foundation/sleep';
1818
import {
1919
GovernanceAbi,
@@ -53,7 +53,7 @@ const BOOT_NODE_UDP_PORT = 4500;
5353
const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'add-rollup-old-'));
5454
const DATA_DIR_NEW = fs.mkdtempSync(path.join(os.tmpdir(), 'add-rollup-new-'));
5555

56-
jest.setTimeout(1000 * 60 * 10);
56+
jest.setTimeout(1000 * 60 * 20);
5757

5858
/**
5959
* This test emulates the addition of a new rollup to the registry and tests that cross-chain messages work.
@@ -80,6 +80,14 @@ describe('e2e_p2p_add_rollup', () => {
8080
...SHORTENED_BLOCK_TIME_CONFIG_NO_PRUNES,
8181
listenAddress: '127.0.0.1',
8282
governanceProposerRoundSize: 10,
83+
enableProposerPipelining: true,
84+
// Allow validators to build empty checkpoints under pipelining so the chain keeps
85+
// advancing while we wait for L1->L2 messages to land in the next checkpoint's inbox tree.
86+
minTxsPerBlock: 0,
87+
// Pipelining starts cycle for checkpoint N+1 during slot N, but the inbox tree for
88+
// checkpoint N is only sealed when checkpoint N is published. inboxLag: 2 sources
89+
// L1->L2 messages from checkpoint N-1 (already sealed), avoiding L1ToL2MessagesNotReadyError.
90+
inboxLag: 2,
8391
},
8492
startProverNode: false, // Start one later using p2p.
8593
});
@@ -307,8 +315,10 @@ describe('e2e_p2p_add_rollup', () => {
307315
});
308316

309317
const makeMessageConsumable = async (msgHash: Fr) => {
310-
// We poll isL1ToL2MessageSynced endpoint until the message is available
311-
await retryUntil(async () => await node.isL1ToL2MessageSynced(msgHash), 'message sync', 10);
318+
// Wait until the message is ready to be consumed (the rollup has reached the message's checkpoint).
319+
// Using waitForL1ToL2MessageReady rather than isL1ToL2MessageSynced because with `inboxLag > 0`
320+
// a synced message is not yet present in the latest checkpoint's inbox tree.
321+
await waitForL1ToL2MessageReady(node, msgHash, { timeoutSeconds: 120 });
312322

313323
const { receipt } = await testContract.methods
314324
.create_l2_to_l1_message_arbitrary_recipient_private(contentOutFromRollup, ethRecipient)

yarn-project/end-to-end/src/e2e_p2p/broadcasted_invalid_block_proposal_slash.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ describe('e2e_p2p_broadcasted_invalid_block_proposal_slash', () => {
6262
ethereumSlotDuration: ETHEREUM_SLOT_DURATION,
6363
aztecSlotDuration: AZTEC_SLOT_DURATION,
6464
aztecTargetCommitteeSize: COMMITTEE_SIZE,
65+
enableProposerPipelining: true,
66+
inboxLag: 2,
6567
aztecProofSubmissionEpochs: 1024, // effectively do not reorg
6668
slashInactivityConsecutiveEpochThreshold: 32, // effectively do not slash for inactivity
6769
minTxsPerBlock: 0, // always be building

yarn-project/end-to-end/src/e2e_p2p/data_withholding_slash.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ describe('e2e_p2p_data_withholding_slash', () => {
7373
slashAmountLarge: slashingUnit * 3n,
7474
slashSelfAllowed: true,
7575
minTxsPerBlock: 0,
76+
enableProposerPipelining: true,
77+
inboxLag: 2,
7678
},
7779
});
7880

@@ -165,6 +167,11 @@ describe('e2e_p2p_data_withholding_slash', () => {
165167

166168
// Re-create the nodes.
167169
// ASSUMING they sync in the middle of the epoch, they will "see" the reorg, and try to slash.
170+
// Reset minTxsPerBlock to 0 so re-created validators build empty checkpoints. Under proposer
171+
// pipelining, the vote-offenses signature is bound to the target slot and the multicall is only
172+
// delayed to the target slot start when a checkpoint is being proposed; without a proposal,
173+
// votes would mine in the current wall-clock slot, causing the EIP-712 signature verification to fail.
174+
t.ctx.aztecNodeConfig.minTxsPerBlock = 0;
168175
t.logger.warn('Re-creating nodes');
169176
nodes = await createNodes(
170177
t.ctx.aztecNodeConfig,

yarn-project/end-to-end/src/e2e_p2p/duplicate_attestation_slash.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ describe('e2e_p2p_duplicate_attestation_slash', () => {
8989
slashDuplicateProposalPenalty: slashingUnit,
9090
slashDuplicateAttestationPenalty: slashingUnit,
9191
slashingOffsetInRounds: 1,
92+
enableProposerPipelining: true,
93+
inboxLag: 2,
9294
},
9395
});
9496

yarn-project/end-to-end/src/e2e_p2p/duplicate_proposal_slash.test.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { AztecNodeService } from '@aztec/aztec-node';
22
import type { TestAztecNodeService } from '@aztec/aztec-node/test';
33
import { EthAddress } from '@aztec/aztec.js/addresses';
44
import { EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
5+
import { retryUntil } from '@aztec/foundation/retry';
56
import { bufferToHex } from '@aztec/foundation/string';
67
import { OffenseType } from '@aztec/slasher';
78
import { TopicType } from '@aztec/stdlib/p2p';
@@ -79,6 +80,8 @@ describe('e2e_p2p_duplicate_proposal_slash', () => {
7980
blockDurationMs: BLOCK_DURATION * 1000,
8081
slashDuplicateProposalPenalty: slashingUnit,
8182
slashingOffsetInRounds: 1,
83+
enableProposerPipelining: true,
84+
inboxLag: 2,
8285
},
8386
});
8487

@@ -224,24 +227,39 @@ describe('e2e_p2p_duplicate_proposal_slash', () => {
224227
t.logger.warn(`Advancing to target epoch ${targetEpoch}`);
225228
await t.ctx.cheatCodes.rollup.advanceToEpoch(targetEpoch);
226229

227-
// Wait for offense to be detected
228-
// The honest nodes should detect the duplicate proposal from the malicious validator
230+
// Wait for offense to be detected. Under proposer pipelining, checkpoint proposals are broadcast
231+
// at the slot boundary while the receivers' wall clocks may have already advanced past the build
232+
// slot — when that happens, honest nodes reject the gossip with "invalid slot number" before
233+
// duplicate detection runs, so DUPLICATE_PROPOSAL is only observed by whichever node managed to
234+
// process both proposals while still in the build slot (often the other malicious node, since
235+
// they receive each other's broadcasts immediately). We therefore collect offenses from every
236+
// node in the network and assert that at least one of them recorded the duplicate proposal.
229237
t.logger.warn('Waiting for duplicate proposal offense to be detected...');
230-
const offenses = await awaitOffenseDetected({
238+
await awaitOffenseDetected({
231239
epochDuration: t.ctx.aztecNodeConfig.aztecEpochDuration,
232240
logger: t.logger,
233-
nodeAdmin: honestNode1, // Use honest node to check for offenses
241+
nodeAdmin: honestNode1,
234242
slashingRoundSize,
235243
waitUntilOffenseCount: 1,
236244
timeoutSeconds: AZTEC_SLOT_DURATION * 16,
237245
});
238246

239-
t.logger.warn(`Collected offenses`, { offenses });
247+
// Poll every node for DUPLICATE_PROPOSAL offenses, retrying briefly so any node that detected
248+
// the duplicate after the initial offense was collected has time to flush it through the
249+
// slasher's offenses-collector.
250+
const proposalOffenses = await retryUntil(
251+
async () => {
252+
const allOffenses = (await Promise.all(nodes.map(n => n.getSlashOffenses('all')))).flat();
253+
const filtered = allOffenses.filter(o => o.offenseType === OffenseType.DUPLICATE_PROPOSAL);
254+
if (filtered.length > 0) {
255+
return filtered;
256+
}
257+
},
258+
'duplicate proposal offense',
259+
AZTEC_SLOT_DURATION * 4,
260+
);
240261

241-
// Filter to only DUPLICATE_PROPOSAL offenses. The two malicious nodes sharing the same key
242-
// will also each self-attest to their own (different) checkpoint proposals, which causes honest
243-
// nodes to detect a DUPLICATE_ATTESTATION as well. We only care about proposals here.
244-
const proposalOffenses = offenses.filter(o => o.offenseType === OffenseType.DUPLICATE_PROPOSAL);
262+
t.logger.warn(`Collected duplicate proposal offenses`, { proposalOffenses });
245263
expect(proposalOffenses.length).toBeGreaterThan(0);
246264
for (const offense of proposalOffenses) {
247265
expect(offense.validator.toString()).toEqual(maliciousValidatorAddress.toString());

yarn-project/end-to-end/src/e2e_p2p/fee_asset_price_oracle_gossip.test.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ describe('e2e_p2p_network', () => {
6363
slashingRoundSizeInEpochs: 2,
6464
slashingQuorum: 5,
6565
listenAddress: '127.0.0.1',
66+
enableProposerPipelining: true,
67+
inboxLag: 2,
6668
},
6769
});
6870

@@ -205,11 +207,23 @@ describe('e2e_p2p_network', () => {
205207
expect(validatorAddresses).toContain(signer);
206208
}
207209

210+
// Allow a tolerance up to the per-checkpoint modifier cap (±100 bps): under proposer pipelining,
211+
// the modifier for checkpoint N is computed in slot N-1 from the price visible at prep time, but
212+
// the on-chain new price is the parent fee header (one checkpoint ahead) modified by that value.
213+
// This stale-read effect causes the price to oscillate around the oracle target with up to ~1%
214+
// amplitude rather than land exactly on it. The test still verifies that the price tracks the
215+
// oracle within the cap.
216+
const TOLERANCE_BPS = 100n;
217+
const absDiffBps = (a: bigint, b: bigint) => {
218+
const d = diffInBps(a, b);
219+
return d < 0n ? -d : d;
220+
};
221+
208222
await retryUntil(
209223
async () => {
210224
const currentPrice = await rollup.getEthPerFeeAsset();
211225
t.logger.info(`Current on-chain price: ${currentPrice}, waiting for: ${targetOraclePrice}`);
212-
return diffInBps(currentPrice, targetOraclePrice) == 0n;
226+
return absDiffBps(currentPrice, targetOraclePrice) <= TOLERANCE_BPS;
213227
},
214228
'price convergence toward oracle',
215229
120, // timeout in seconds
@@ -225,7 +239,7 @@ describe('e2e_p2p_network', () => {
225239
async () => {
226240
const currentPrice = await rollup.getEthPerFeeAsset();
227241
t.logger.info(`Current on-chain price: ${currentPrice}, waiting for: ${targetOraclePrice2}`);
228-
return diffInBps(currentPrice, targetOraclePrice2) == 0n;
242+
return absDiffBps(currentPrice, targetOraclePrice2) <= TOLERANCE_BPS;
229243
},
230244
'price convergence toward oracle',
231245
120, // timeout in seconds
@@ -237,6 +251,6 @@ describe('e2e_p2p_network', () => {
237251

238252
// Verify the price moved toward the oracle price
239253
expect(finalPrice).toBeGreaterThan(initialOnChainPrice);
240-
expect(diffInBps(finalPrice, targetOraclePrice2)).toBe(0n);
254+
expect(absDiffBps(finalPrice, targetOraclePrice2)).toBeLessThanOrEqual(TOLERANCE_BPS);
241255
});
242256
});

yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ describe('e2e_p2p_network', () => {
6969
slashingRoundSizeInEpochs: 2,
7070
slashingQuorum: 5,
7171
listenAddress: '127.0.0.1',
72+
enableProposerPipelining: true,
73+
inboxLag: 2,
7274
},
7375
});
7476

yarn-project/end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ describe('e2e_p2p_network', () => {
6868
// Without this, no blocks are built until txs arrive, and a failed checkpoint during tx
6969
// submission causes block pruning that invalidates tx references.
7070
minTxsPerBlock: 0,
71+
enableProposerPipelining: true,
72+
inboxLag: 2,
7173
},
7274
});
7375

yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ export class P2PInactivityTest {
5858
basePort: BOOT_NODE_UDP_PORT,
5959
startProverNode: true,
6060
initialConfig: {
61+
enableProposerPipelining: true,
62+
inboxLag: 2,
6163
anvilSlotsInAnEpoch: 4,
6264
proverNodeConfig: { proverNodeEpochProvingDelayMs: AZTEC_SLOT_DURATION * 1000 },
6365
aztecTargetCommitteeSize: COMMITTEE_SIZE,

yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_with_consecutive_epochs.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ describe('e2e_p2p_inactivity_slash_with_consecutive_epochs', () => {
5555
return offenses.length > 0 ? offenses : undefined;
5656
},
5757
'slash offenses',
58-
slashInactivityConsecutiveEpochThreshold * aztecEpochDuration * aztecSlotDuration * 2,
58+
slashInactivityConsecutiveEpochThreshold * aztecEpochDuration * aztecSlotDuration * 4,
5959
);
6060
expect(unique(offenses.map(o => o.validator.toString()))).toEqual([offlineValidator.toString()]);
6161
expect(unique(offenses.map(o => o.offenseType))).toEqual([OffenseType.INACTIVITY]);

0 commit comments

Comments
 (0)