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
Expand Up @@ -24,7 +24,12 @@ describe('e2e_epochs/epochs_long_proving_time', () => {
const { aztecSlotDuration } = EpochsTestContext.getSlotDurations({ aztecEpochDuration });
const epochDurationInSeconds = aztecSlotDuration * aztecEpochDuration;
const proverTestDelayMs = (epochDurationInSeconds * 1000 * 3) / 4;
test = await EpochsTestContext.setup({ aztecEpochDuration, aztecProofSubmissionEpochs: 8, proverTestDelayMs });
test = await EpochsTestContext.setup({
aztecEpochDuration,
aztecProofSubmissionEpochs: 1000, // Effectively don't re-org
proverTestDelayMs,
proverNodeMaxPendingJobs: 1, // We test for only a single job at once
});
({ logger, monitor, L1_BLOCK_TIME_IN_S } = test);
logger.warn(`Initialized with prover delay set to ${proverTestDelayMs}ms (epoch is ${epochDurationInSeconds}s)`);
});
Expand All @@ -34,7 +39,7 @@ describe('e2e_epochs/epochs_long_proving_time', () => {
await test.teardown();
});

it.skip('generates proof over multiple epochs', async () => {
it('generates proof over multiple epochs', async () => {
const targetProvenEpochs = process.env.TARGET_PROVEN_EPOCHS ? parseInt(process.env.TARGET_PROVEN_EPOCHS) : 1;
const targetProvenBlockNumber = targetProvenEpochs * test.epochDuration;
logger.info(`Waiting for ${targetProvenEpochs} epochs to be proven at ${targetProvenBlockNumber} L2 blocks`);
Expand Down
1 change: 1 addition & 0 deletions yarn-project/foundation/src/config/env_var.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ export type EnvVar =
| 'SEQ_L1_PUBLISHING_TIME_ALLOWANCE_IN_SLOT'
| 'SEQ_ATTESTATION_PROPAGATION_TIME'
| 'SEQ_BLOCK_DURATION_MS'
| 'SEQ_EXPECTED_BLOCK_PROPOSALS_PER_SLOT'
| 'SEQ_BUILD_CHECKPOINT_IF_EMPTY'
| 'SEQ_SECONDS_BEFORE_INVALIDATING_BLOCK_AS_COMMITTEE_MEMBER'
| 'SEQ_SECONDS_BEFORE_INVALIDATING_BLOCK_AS_NON_COMMITTEE_MEMBER'
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/p2p/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface P2PConfig
ChainConfig,
TxCollectionConfig,
TxFileStoreConfig,
Pick<SequencerConfig, 'blockDurationMs'> {
Pick<SequencerConfig, 'blockDurationMs' | 'expectedBlockProposalsPerSlot'> {
/** A flag dictating whether the P2P subsystem should be enabled. */
p2pEnabled: boolean;

Expand Down
43 changes: 29 additions & 14 deletions yarn-project/p2p/src/services/gossipsub/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ We configure all parameters (P1-P4) with values calculated dynamically from netw
| P3b: meshFailurePenalty | -34 per topic | Sticky penalty after pruning |
| P4: invalidMessageDeliveries | -20 per message | Attack detection |

**Important:** P1 and P2 are only enabled on topics with P3 enabled (block_proposal, checkpoint_proposal, checkpoint_attestation). The tx topic has all scoring disabled except P4, to prevent free positive score accumulation that would offset penalties from other topics.
**Important:** P1 and P2 are only enabled on topics with P3 enabled. By default, P3 is enabled for checkpoint_proposal and checkpoint_attestation (2 topics). Block proposal scoring is controlled by `expectedBlockProposalsPerSlot` (current default: `0`, including when env var is unset, so disabled) - see [Block Proposals](#block-proposals-block_proposal) for details. The tx topic has all scoring disabled except P4, to prevent free positive score accumulation that would offset penalties from other topics.

## Exponential Decay

Expand Down Expand Up @@ -217,7 +217,21 @@ Transactions are submitted unpredictably by users, so we cannot set meaningful d

### Block Proposals (block_proposal)

In Multi-Block-Per-Slot (MBPS) mode, N-1 block proposals are gossiped per slot (the last block is bundled with the checkpoint). In single-block mode, this is 0.
Block proposal scoring is controlled by the `expectedBlockProposalsPerSlot` config (`SEQ_EXPECTED_BLOCK_PROPOSALS_PER_SLOT` env var):

| Config Value | Behavior |
|-------------|----------|
| `0` (current default) | Block proposal P3 scoring is **disabled** |
| Positive number | Uses the provided value as expected proposals per slot |
| `undefined` | Falls back to `blocksPerSlot - 1` (MBPS mode: N-1, single block: 0) |

**Current behavior note:** In the current implementation, if `SEQ_EXPECTED_BLOCK_PROPOSALS_PER_SLOT` is not set, config mapping applies `0` by default (scoring disabled). The `undefined` fallback above is currently reachable only if the value is explicitly provided as `undefined` in code.

**Future intent:** Once throughput is stable, we may change env parsing/defaults so an unset env var resolves to `undefined` again (re-enabling automatic fallback to `blocksPerSlot - 1`).

**Why disabled by default?** In MBPS mode, gossipsub expects N-1 block proposals per slot. When transaction throughput is low (as expected at launch), fewer blocks are actually built, causing peers to be incorrectly penalized for under-delivering block proposals. The default of 0 disables this scoring. Set to a positive value when throughput increases and block production is consistent.

In MBPS mode (when enabled), N-1 block proposals are gossiped per slot (the last block is bundled with the checkpoint). In single-block mode, this is 0.

### Checkpoint Proposals (checkpoint_proposal)

Expand All @@ -241,6 +255,7 @@ The scoring parameters depend on:
| `targetCommitteeSize` | L1RollupConstants | 48 |
| `heartbeatInterval` | P2PConfig.gossipsubInterval | 700ms |
| `blockDurationMs` | P2PConfig.blockDurationMs | undefined (single block) |
| `expectedBlockProposalsPerSlot` | P2PConfig.expectedBlockProposalsPerSlot | 0 (disabled; current unset-env behavior) |

## Invalid Message Handling (P4)

Expand Down Expand Up @@ -320,9 +335,9 @@ Conversely, if topic scores are low, a peer slightly above the disconnect thresh

Topic scores provide **burst response** to attacks, while app score provides **stable baseline**:

- P1 (time in mesh): Max +8 per topic (+24 across 3 topics)
- P2 (first deliveries): Max +25 per topic (+75 across 3 topics, but decays fast)
- P3 (under-delivery): Max -34 per topic (-102 across 3 topics in MBPS; -68 in single-block mode)
- P1 (time in mesh): Max +8 per topic (+16 default, +24 with block proposal scoring enabled)
- P2 (first deliveries): Max +25 per topic (+50 default, +75 with block proposal scoring, but decays fast)
- P3 (under-delivery): Max -34 per topic (-68 default with 2 topics, -102 with block proposal scoring enabled)
- P4 (invalid messages): -20 per invalid message, can spike to -2000+ during attacks

Example attack scenario:
Expand Down Expand Up @@ -373,21 +388,21 @@ When a peer is pruned from the mesh:
3. **P3b captures the penalty**: The P3 deficit at prune time becomes P3b, which decays slowly

After pruning, the peer's score consists mainly of P3b:
- **Total P3b across 3 topics: -102** (max)
- **Total P3b: -68** (default, 2 topics) or **-102** (with block proposal scoring enabled, 3 topics)
- **Recovery time**: P3b decays to ~1% over one decay window (2-5 slots = 2-6 minutes)
- **Grafting eligibility**: Peer can be grafted when score ≥ 0, but asymptotic decay means recovery is slow

### Why Non-Contributors Aren't Disconnected

With P3b capped at -102 total after pruning (MBPS mode). In single-block mode, the cap is -68:
With P3b capped at -68 (default, 2 topics) or -102 (with block proposal scoring, 3 topics) after pruning:

| Threshold | Value | P3b Score | Triggered? |
|-----------|-------|-----------|------------|
| gossipThreshold | -500 | -102 (MBPS) / -68 (single) | No |
| publishThreshold | -1000 | -102 (MBPS) / -68 (single) | No |
| graylistThreshold | -2000 | -102 (MBPS) / -68 (single) | No |
| gossipThreshold | -500 | -68 (default) / -102 (block scoring on) | No |
| publishThreshold | -1000 | -68 (default) / -102 (block scoring on) | No |
| graylistThreshold | -2000 | -68 (default) / -102 (block scoring on) | No |

**A score of -102 (MBPS) or -68 (single-block) is well above -500**, so non-contributing peers:
**A score of -68 or -102 is well above -500**, so non-contributing peers:
- Are pruned from mesh (good - stops them slowing propagation)
- Still receive gossip (can recover by reconnecting/restarting)
- Are NOT disconnected unless they also have application-level penalties
Expand Down Expand Up @@ -547,7 +562,7 @@ What happens when a peer experiences a network outage and stops delivering messa
While the peer is disconnected:

1. **P3 penalty accumulates**: The message delivery counter decays toward 0, causing increasing P3 penalty
2. **Max P3 penalty reached**: Once counter drops below threshold, penalty hits -34 per topic (-102 total in MBPS; -68 single-block)
2. **Max P3 penalty reached**: Once counter drops below threshold, penalty hits -34 per topic (-68 default, -102 with block proposal scoring)
3. **Mesh pruning**: Topic score goes negative → peer is pruned from mesh
4. **P3b captures penalty**: The P3 deficit at prune time becomes P3b (sticky penalty)

Expand All @@ -569,13 +584,13 @@ Note: If the peer just joined the mesh, P3 penalties only start after
During a network outage, the peer:
- **Does NOT send invalid messages** → No P4 penalty
- **Does NOT violate protocols** → No application-level penalty
- **Only accumulates topic-level penalties** → Max -102 (P3b, MBPS) or -68 (single-block)
- **Only accumulates topic-level penalties** → Max -68 (default) or -102 (with block proposal scoring)

This is the crucial difference from malicious behavior:

| Scenario | App Score | Topic Score | Total | Threshold Hit |
|----------|-----------|-------------|-------|---------------|
| Network outage | 0 | -102 (MBPS) / -68 (single) | -102 / -68 | None |
| Network outage | 0 | -68 (default) / -102 (block scoring on) | -68 / -102 | None |
| Validation failure | -50 | -20 | -520 | gossipThreshold |
| Malicious peer | -100 | -2000+ | -2100+ | graylistThreshold |

Expand Down
92 changes: 78 additions & 14 deletions yarn-project/p2p/src/services/gossipsub/topic_score_params.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
createAllTopicScoreParams,
createTopicScoreParamsForTopic,
getDecayWindowSlots,
getEffectiveBlockProposalsPerSlot,
getExpectedMessagesPerSlot,
} from './topic_score_params.js';

Expand Down Expand Up @@ -148,18 +149,47 @@ describe('Topic Score Params', () => {
});
});

describe('getEffectiveBlockProposalsPerSlot', () => {
it('returns undefined when override is 0 (disabled)', () => {
expect(getEffectiveBlockProposalsPerSlot(5, 0)).toBeUndefined();
});

it('returns override value when positive', () => {
expect(getEffectiveBlockProposalsPerSlot(5, 3)).toBe(3);
expect(getEffectiveBlockProposalsPerSlot(1, 7)).toBe(7);
});

it('falls back to blocksPerSlot - 1 when override is undefined', () => {
expect(getEffectiveBlockProposalsPerSlot(5, undefined)).toBe(4);
expect(getEffectiveBlockProposalsPerSlot(3, undefined)).toBe(2);
});

it('returns undefined when override is undefined and single block mode', () => {
expect(getEffectiveBlockProposalsPerSlot(1, undefined)).toBeUndefined();
});
});

describe('getExpectedMessagesPerSlot', () => {
it('returns undefined for tx topic (unpredictable)', () => {
expect(getExpectedMessagesPerSlot(TopicType.tx, 48, 5)).toBeUndefined();
});

it('returns N-1 for block_proposal in MBPS mode', () => {
it('returns N-1 for block_proposal when override is undefined (fallback)', () => {
expect(getExpectedMessagesPerSlot(TopicType.block_proposal, 48, 5)).toBe(4);
expect(getExpectedMessagesPerSlot(TopicType.block_proposal, 48, 3)).toBe(2);
});

it('returns 0 for block_proposal in single block mode', () => {
expect(getExpectedMessagesPerSlot(TopicType.block_proposal, 48, 1)).toBe(0);
it('returns undefined for block_proposal in single block mode without override', () => {
expect(getExpectedMessagesPerSlot(TopicType.block_proposal, 48, 1)).toBeUndefined();
});

it('returns undefined for block_proposal when override is 0 (disabled)', () => {
expect(getExpectedMessagesPerSlot(TopicType.block_proposal, 48, 5, 0)).toBeUndefined();
});

it('returns override value for block_proposal when positive', () => {
expect(getExpectedMessagesPerSlot(TopicType.block_proposal, 48, 1, 3)).toBe(3);
expect(getExpectedMessagesPerSlot(TopicType.block_proposal, 48, 5, 7)).toBe(7);
});

it('returns 1 for checkpoint_proposal', () => {
Expand Down Expand Up @@ -207,10 +237,35 @@ describe('Topic Score Params', () => {
expect(params.meshFailurePenaltyWeight).toBe(0);
});

it('enables P3/P3b for block_proposal in MBPS mode', () => {
it('disables P3/P3b for block_proposal in MBPS mode when expectedBlockProposalsPerSlot is 0', () => {
const factory = new TopicScoreParamsFactory({
...standardParams,
blockDurationMs: 10000,
expectedBlockProposalsPerSlot: 0,
});
const params = factory.createForTopic(TopicType.block_proposal);

expect(params.meshMessageDeliveriesWeight).toBe(0);
expect(params.meshFailurePenaltyWeight).toBe(0);
});

it('enables P3/P3b for block_proposal when expectedBlockProposalsPerSlot is positive', () => {
const factory = new TopicScoreParamsFactory({
...standardParams,
blockDurationMs: 10000,
expectedBlockProposalsPerSlot: 3,
});
const params = factory.createForTopic(TopicType.block_proposal);

expect(params.meshMessageDeliveriesWeight).toBeLessThan(0);
expect(params.meshFailurePenaltyWeight).toBeLessThan(0);
});

it('falls back to blocksPerSlot - 1 for block_proposal when expectedBlockProposalsPerSlot is undefined', () => {
const factory = new TopicScoreParamsFactory({ ...standardParams, blockDurationMs: 10000 });
const params = factory.createForTopic(TopicType.block_proposal);

// MBPS mode with no override: falls back to blocksPerSlot - 1 > 0, so P3 is enabled
expect(params.meshMessageDeliveriesWeight).toBeLessThan(0);
expect(params.meshFailurePenaltyWeight).toBeLessThan(0);
});
Expand Down Expand Up @@ -447,33 +502,42 @@ describe('Topic Score Params', () => {
expect(Math.abs(maxP3)).toBeGreaterThan(maxP1 + maxP2);
});

it('total P3b across all topics is approximately -102', () => {
const factory = new TopicScoreParamsFactory(standardParams);
it('total P3b is -102 when block proposal scoring is enabled (3 topics)', () => {
const factory = new TopicScoreParamsFactory({
...standardParams,
blockDurationMs: 4000,
expectedBlockProposalsPerSlot: 3,
});

// Topics with P3 enabled: checkpoint_proposal, checkpoint_attestation, block_proposal (in MBPS)
const mbpsParams = { ...standardParams, blockDurationMs: 4000 };
const mbpsFactory = new TopicScoreParamsFactory(mbpsParams);
expect(factory.numP3EnabledTopics).toBe(3);
expect(factory.totalMaxP3bPenalty).toBeCloseTo(-102, 0);

const checkpointParams = factory.createForTopic(TopicType.checkpoint_proposal);
const attestationParams = factory.createForTopic(TopicType.checkpoint_attestation);
const blockParams = mbpsFactory.createForTopic(TopicType.block_proposal);
const blockParams = factory.createForTopic(TopicType.block_proposal);

// Calculate max P3 for each topic
const p3Checkpoint =
checkpointParams.meshMessageDeliveriesThreshold ** 2 * checkpointParams.meshMessageDeliveriesWeight;
const p3Attestation =
attestationParams.meshMessageDeliveriesThreshold ** 2 * attestationParams.meshMessageDeliveriesWeight;
const p3Block = blockParams.meshMessageDeliveriesThreshold ** 2 * blockParams.meshMessageDeliveriesWeight;

// Each should be approximately -34
expect(p3Checkpoint).toBeCloseTo(-34, 0);
expect(p3Attestation).toBeCloseTo(-34, 0);
expect(p3Block).toBeCloseTo(-34, 0);

// Total should be approximately -102
expect(p3Checkpoint + p3Attestation + p3Block).toBeCloseTo(-102, 0);
});

it('total P3b is -68 when block proposal scoring is disabled (2 topics)', () => {
const factory = new TopicScoreParamsFactory({
...standardParams,
expectedBlockProposalsPerSlot: 0,
});

expect(factory.numP3EnabledTopics).toBe(2);
expect(factory.totalMaxP3bPenalty).toBeCloseTo(-68, 0);
});

it('non-contributing peer has negative topic score and gets pruned', () => {
const factory = new TopicScoreParamsFactory(standardParams);
const params = factory.createForTopic(TopicType.checkpoint_proposal);
Expand Down
Loading
Loading