diff --git a/yarn-project/end-to-end/src/e2e_p2p/broadcasted_invalid_block_proposal_slash.test.ts b/yarn-project/end-to-end/src/e2e_p2p/broadcasted_invalid_block_proposal_slash.test.ts index 8dc7dd33c2b9..02dd223b3b86 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/broadcasted_invalid_block_proposal_slash.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/broadcasted_invalid_block_proposal_slash.test.ts @@ -62,6 +62,8 @@ describe('e2e_p2p_broadcasted_invalid_block_proposal_slash', () => { ethereumSlotDuration: ETHEREUM_SLOT_DURATION, aztecSlotDuration: AZTEC_SLOT_DURATION, aztecTargetCommitteeSize: COMMITTEE_SIZE, + enableProposerPipelining: true, + inboxLag: 2, aztecProofSubmissionEpochs: 1024, // effectively do not reorg slashInactivityConsecutiveEpochThreshold: 32, // effectively do not slash for inactivity minTxsPerBlock: 0, // always be building diff --git a/yarn-project/end-to-end/src/e2e_p2p/data_withholding_slash.test.ts b/yarn-project/end-to-end/src/e2e_p2p/data_withholding_slash.test.ts index 84171f697fb2..bd63a95eda13 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/data_withholding_slash.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/data_withholding_slash.test.ts @@ -73,6 +73,8 @@ describe('e2e_p2p_data_withholding_slash', () => { slashAmountLarge: slashingUnit * 3n, slashSelfAllowed: true, minTxsPerBlock: 0, + enableProposerPipelining: true, + inboxLag: 2, }, }); @@ -165,6 +167,11 @@ describe('e2e_p2p_data_withholding_slash', () => { // Re-create the nodes. // ASSUMING they sync in the middle of the epoch, they will "see" the reorg, and try to slash. + // Reset minTxsPerBlock to 0 so re-created validators build empty checkpoints. Under proposer + // pipelining, the vote-offenses signature is bound to the target slot and the multicall is only + // delayed to the target slot start when a checkpoint is being proposed; without a proposal, + // votes would mine in the current wall-clock slot, causing the EIP-712 signature verification to fail. + t.ctx.aztecNodeConfig.minTxsPerBlock = 0; t.logger.warn('Re-creating nodes'); nodes = await createNodes( t.ctx.aztecNodeConfig, diff --git a/yarn-project/end-to-end/src/e2e_p2p/duplicate_attestation_slash.test.ts b/yarn-project/end-to-end/src/e2e_p2p/duplicate_attestation_slash.test.ts index 0f6cd6c0f9c3..ce4a8f706999 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/duplicate_attestation_slash.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/duplicate_attestation_slash.test.ts @@ -89,6 +89,8 @@ describe('e2e_p2p_duplicate_attestation_slash', () => { slashDuplicateProposalPenalty: slashingUnit, slashDuplicateAttestationPenalty: slashingUnit, slashingOffsetInRounds: 1, + enableProposerPipelining: true, + inboxLag: 2, }, }); diff --git a/yarn-project/end-to-end/src/e2e_p2p/duplicate_proposal_slash.test.ts b/yarn-project/end-to-end/src/e2e_p2p/duplicate_proposal_slash.test.ts index 51b8e4b4083f..697e2f539039 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/duplicate_proposal_slash.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/duplicate_proposal_slash.test.ts @@ -2,6 +2,7 @@ import type { AztecNodeService } from '@aztec/aztec-node'; import type { TestAztecNodeService } from '@aztec/aztec-node/test'; import { EthAddress } from '@aztec/aztec.js/addresses'; import { EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; +import { retryUntil } from '@aztec/foundation/retry'; import { bufferToHex } from '@aztec/foundation/string'; import { OffenseType } from '@aztec/slasher'; import { TopicType } from '@aztec/stdlib/p2p'; @@ -79,6 +80,8 @@ describe('e2e_p2p_duplicate_proposal_slash', () => { blockDurationMs: BLOCK_DURATION * 1000, slashDuplicateProposalPenalty: slashingUnit, slashingOffsetInRounds: 1, + enableProposerPipelining: true, + inboxLag: 2, }, }); @@ -220,28 +223,47 @@ describe('e2e_p2p_duplicate_proposal_slash', () => { t.logger.warn('Starting all sequencers'); await Promise.all(nodes.map(n => n.getSequencer()!.start())); - // Now warp to the target epoch — sequencers are already running - t.logger.warn(`Advancing to target epoch ${targetEpoch}`); - await t.ctx.cheatCodes.rollup.advanceToEpoch(targetEpoch); + // Now warp to one slot before the target epoch — sequencers are already running. + // Under proposer pipelining, the malicious proposers begin building for the first + // slot of the target epoch one slot earlier; warping to the start of the epoch + // would force both AVM-heavy duplicate proposals to serialize past the slot + // boundary, after which honest receivers reject them as late. + t.logger.warn(`Advancing to one slot before target epoch ${targetEpoch}`); + await t.ctx.cheatCodes.rollup.advanceToEpoch(targetEpoch, { offset: -AZTEC_SLOT_DURATION }); - // Wait for offense to be detected - // The honest nodes should detect the duplicate proposal from the malicious validator + // Wait for offense to be detected. Under proposer pipelining, checkpoint proposals are broadcast + // at the slot boundary while the receivers' wall clocks may have already advanced past the build + // slot — when that happens, honest nodes reject the gossip with "invalid slot number" before + // duplicate detection runs, so DUPLICATE_PROPOSAL is only observed by whichever node managed to + // process both proposals while still in the build slot (often the other malicious node, since + // they receive each other's broadcasts immediately). We therefore collect offenses from every + // node in the network and assert that at least one of them recorded the duplicate proposal. t.logger.warn('Waiting for duplicate proposal offense to be detected...'); - const offenses = await awaitOffenseDetected({ + await awaitOffenseDetected({ epochDuration: t.ctx.aztecNodeConfig.aztecEpochDuration, logger: t.logger, - nodeAdmin: honestNode1, // Use honest node to check for offenses + nodeAdmin: honestNode1, slashingRoundSize, waitUntilOffenseCount: 1, timeoutSeconds: AZTEC_SLOT_DURATION * 16, }); - t.logger.warn(`Collected offenses`, { offenses }); + // Poll every node for DUPLICATE_PROPOSAL offenses, retrying briefly so any node that detected + // the duplicate after the initial offense was collected has time to flush it through the + // slasher's offenses-collector. + const proposalOffenses = await retryUntil( + async () => { + const allOffenses = (await Promise.all(nodes.map(n => n.getSlashOffenses('all')))).flat(); + const filtered = allOffenses.filter(o => o.offenseType === OffenseType.DUPLICATE_PROPOSAL); + if (filtered.length > 0) { + return filtered; + } + }, + 'duplicate proposal offense', + AZTEC_SLOT_DURATION * 4, + ); - // Filter to only DUPLICATE_PROPOSAL offenses. The two malicious nodes sharing the same key - // will also each self-attest to their own (different) checkpoint proposals, which causes honest - // nodes to detect a DUPLICATE_ATTESTATION as well. We only care about proposals here. - const proposalOffenses = offenses.filter(o => o.offenseType === OffenseType.DUPLICATE_PROPOSAL); + t.logger.warn(`Collected duplicate proposal offenses`, { proposalOffenses }); expect(proposalOffenses.length).toBeGreaterThan(0); for (const offense of proposalOffenses) { expect(offense.validator.toString()).toEqual(maliciousValidatorAddress.toString()); diff --git a/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts b/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts index 2ff865324b5f..aad5e52e0631 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts @@ -69,6 +69,8 @@ describe('e2e_p2p_network', () => { slashingRoundSizeInEpochs: 2, slashingQuorum: 5, listenAddress: '127.0.0.1', + enableProposerPipelining: true, + inboxLag: 2, }, }); diff --git a/yarn-project/end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts b/yarn-project/end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts index 242a2fef3e16..9fa76164243d 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts @@ -68,6 +68,8 @@ describe('e2e_p2p_network', () => { // Without this, no blocks are built until txs arrive, and a failed checkpoint during tx // submission causes block pruning that invalidates tx references. minTxsPerBlock: 0, + enableProposerPipelining: true, + inboxLag: 2, }, }); diff --git a/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_test.ts b/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_test.ts index efcc6d4833a5..20fff9244f6d 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_test.ts @@ -58,6 +58,8 @@ export class P2PInactivityTest { basePort: BOOT_NODE_UDP_PORT, startProverNode: true, initialConfig: { + enableProposerPipelining: true, + inboxLag: 2, anvilSlotsInAnEpoch: 4, proverNodeConfig: { proverNodeEpochProvingDelayMs: AZTEC_SLOT_DURATION * 1000 }, aztecTargetCommitteeSize: COMMITTEE_SIZE, diff --git a/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_with_consecutive_epochs.test.ts b/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_with_consecutive_epochs.test.ts index 93513f4b879f..78af77bc07d6 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_with_consecutive_epochs.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_with_consecutive_epochs.test.ts @@ -55,7 +55,7 @@ describe('e2e_p2p_inactivity_slash_with_consecutive_epochs', () => { return offenses.length > 0 ? offenses : undefined; }, 'slash offenses', - slashInactivityConsecutiveEpochThreshold * aztecEpochDuration * aztecSlotDuration * 2, + slashInactivityConsecutiveEpochThreshold * aztecEpochDuration * aztecSlotDuration * 4, ); expect(unique(offenses.map(o => o.validator.toString()))).toEqual([offlineValidator.toString()]); expect(unique(offenses.map(o => o.offenseType))).toEqual([OffenseType.INACTIVITY]); diff --git a/yarn-project/end-to-end/src/e2e_p2p/multiple_validators_sentinel.parallel.test.ts b/yarn-project/end-to-end/src/e2e_p2p/multiple_validators_sentinel.parallel.test.ts index e90f0a918d46..b270212db706 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/multiple_validators_sentinel.parallel.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/multiple_validators_sentinel.parallel.test.ts @@ -58,6 +58,8 @@ describe('e2e_p2p_multiple_validators_sentinel', () => { slashingRoundSizeInEpochs: 2, sentinelEnabled: true, slashInactivityPenalty: 0n, // Set to 0 to disable + enableProposerPipelining: true, + inboxLag: 2, }, }); @@ -103,7 +105,20 @@ describe('e2e_p2p_multiple_validators_sentinel', () => { }); it('collects attestations for all validators on a node', async () => { + // Ensure all nodes see each other, especially the sentinel, before starting slot counting + await t.waitForP2PMeshConnectivity([...nodes, sentinel]); + + // Wait until validator nodes have advanced past their first proposed slot so that the + // pipelining warm-up period (where some attestations may be missed) is behind us. await t.monitor.run(); + const warmupSlot = Number(t.monitor.l2SlotNumber) + 1; + t.logger.info(`Waiting for warmup slot ${warmupSlot} before establishing initial slot`); + await retryUntil( + async () => (await t.monitor.run()).l2SlotNumber >= warmupSlot, + 'warmup slot', + AZTEC_SLOT_DURATION * 3, + ); + const { checkpointNumber: initialBlock, l2SlotNumber: initialSlot } = t.monitor; const timeout = AZTEC_SLOT_DURATION * SLOT_COUNT * 4; diff --git a/yarn-project/end-to-end/src/e2e_p2p/preferred_gossip_network.test.ts b/yarn-project/end-to-end/src/e2e_p2p/preferred_gossip_network.test.ts index 03bd23ebb57e..409d8580e684 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/preferred_gossip_network.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/preferred_gossip_network.test.ts @@ -142,6 +142,9 @@ describe('e2e_p2p_preferred_network', () => { p2pDisableStatusHandshake: false, // Just for testing be aggressive here, don't allow any auth handshake failures p2pMaxFailedAuthAttemptsAllowed: 0, + minTxsPerBlock: 0, + enableProposerPipelining: true, + inboxLag: 2, }, }); diff --git a/yarn-project/end-to-end/src/e2e_p2p/rediscovery.test.ts b/yarn-project/end-to-end/src/e2e_p2p/rediscovery.test.ts index 527750cc83e8..e4ddef8157f5 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/rediscovery.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/rediscovery.test.ts @@ -35,6 +35,8 @@ describe('e2e_p2p_rediscovery', () => { ...SHORTENED_BLOCK_TIME_CONFIG_NO_PRUNES, aztecSlotDuration: 24, listenAddress: '127.0.0.1', + enableProposerPipelining: true, + inboxLag: 2, }, }); await t.setup(); diff --git a/yarn-project/end-to-end/src/e2e_p2p/reex.test.ts b/yarn-project/end-to-end/src/e2e_p2p/reex.test.ts index 47f81edb57d4..aa1369606450 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/reex.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/reex.test.ts @@ -47,6 +47,8 @@ describe('e2e_p2p_reex', () => { txTimeoutMs: 30_000, listenAddress: '127.0.0.1', aztecProofSubmissionEpochs: 1024, // effectively do not reorg + enableProposerPipelining: true, + inboxLag: 2, }, }); diff --git a/yarn-project/end-to-end/src/e2e_p2p/shared.ts b/yarn-project/end-to-end/src/e2e_p2p/shared.ts index 2b454d59845e..4c46ec6e5c57 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/shared.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/shared.ts @@ -281,7 +281,10 @@ export async function awaitCommitteeKicked({ expect(attesterInfo.status).toEqual(1); // Validating } - const timeout = slashingRoundSize * 2 * aztecSlotDuration + 30; + // Allow up to four round-lengths so that under proposer pipelining, where individual rounds + // sometimes fail to gather quorum because parts of the committee miss votes due to chain-state + // races, we still see a later round execute the slash. + const timeout = slashingRoundSize * 4 * aztecSlotDuration + 30; logger.info(`Waiting for slash to be executed (timeout ${timeout}s)`); await awaitProposalExecution(slashingProposer, timeout, logger); diff --git a/yarn-project/end-to-end/src/e2e_p2p/slash_veto_demo.test.ts b/yarn-project/end-to-end/src/e2e_p2p/slash_veto_demo.test.ts index f6ffbe12c371..c7fa39824224 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/slash_veto_demo.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/slash_veto_demo.test.ts @@ -76,6 +76,8 @@ describe('veto slash', () => { aztecProofSubmissionEpochs: 1024, // effectively do not reorg listenAddress: '127.0.0.1', minTxsPerBlock: 0, + enableProposerPipelining: true, + inboxLag: 2, aztecEpochDuration: EPOCH_DURATION, sentinelEnabled: true, slashSelfAllowed: true, diff --git a/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts b/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts index f4b5fa4d3acb..205899701ace 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts @@ -54,6 +54,9 @@ describe('e2e_p2p_governance_proposer', () => { governanceProposerRoundSize: 10, activationThreshold: 10n ** 22n, ejectionThreshold: 5n ** 22n, + enableProposerPipelining: true, + inboxLag: 2, + minTxsPerBlock: 0, }, }); diff --git a/yarn-project/end-to-end/src/e2e_p2p/valid_epoch_pruned_slash.test.ts b/yarn-project/end-to-end/src/e2e_p2p/valid_epoch_pruned_slash.test.ts index e0245c2d6e2e..c9129bf2f3d6 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/valid_epoch_pruned_slash.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/valid_epoch_pruned_slash.test.ts @@ -69,6 +69,8 @@ describe('e2e_p2p_valid_epoch_pruned_slash', () => { slashAmountMedium: slashingUnit * 2n, slashAmountLarge: slashingUnit * 3n, aztecTargetCommitteeSize: COMMITTEE_SIZE, + enableProposerPipelining: true, + inboxLag: 2, }, }); diff --git a/yarn-project/end-to-end/src/e2e_p2p/validators_sentinel.test.ts b/yarn-project/end-to-end/src/e2e_p2p/validators_sentinel.test.ts index 4d0b38d533dd..189404f772a9 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/validators_sentinel.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/validators_sentinel.test.ts @@ -50,6 +50,8 @@ describe('e2e_p2p_validators_sentinel', () => { slashingRoundSizeInEpochs: 2, sentinelEnabled: true, slashInactivityPenalty: 0n, // Set to 0 to disable + enableProposerPipelining: true, + inboxLag: 2, }, }); @@ -85,6 +87,20 @@ describe('e2e_p2p_validators_sentinel', () => { describe('with an offline validator', () => { let stats: ValidatorsStats; beforeAll(async () => { + // Ensure all running validator nodes see each other before starting block counting. + await t.waitForP2PMeshConnectivity(nodes); + + // Wait until validator nodes have advanced past their first proposed slot so that the + // pipelining warm-up period (where some attestations may be missed) is behind us. + await t.monitor.run(); + const warmupSlot = Number(t.monitor.l2SlotNumber) + 1; + t.logger.info(`Waiting for warmup slot ${warmupSlot} before establishing initial block`); + await retryUntil( + async () => (await t.monitor.run()).l2SlotNumber >= warmupSlot, + 'warmup slot', + AZTEC_SLOT_DURATION * 3, + ); + const currentBlock = t.monitor.checkpointNumber; const blockCount = BLOCK_COUNT; const timeout = AZTEC_SLOT_DURATION * blockCount * 8;