Skip to content

Commit 5d263f1

Browse files
authored
fix: Do not reexecute all pruned blocks in the prune watcher (#17178)
When deciding whether to slash committee members for an epoch, the epoch prune watcher tries reexecuting all blocks in the pruned epoch. It also uses that to decide whether to slash for data withholding, if not all data is available. However, the blocks being reexecuted (and the txs being gathered) were from ALL pruned epochs, which could be more than one if the proof submission window was long enough. So we ended up slashing committee members from the first epoch for data withholding offenses perpetrated by members in future epochs.
2 parents 5909591 + 59f2d78 commit 5d263f1

File tree

2 files changed

+34
-10
lines changed

2 files changed

+34
-10
lines changed

yarn-project/slasher/src/watchers/epoch_prune_watcher.test.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { EpochCache } from '@aztec/epoch-cache';
22
import { EthAddress } from '@aztec/foundation/eth-address';
33
import { sleep } from '@aztec/foundation/sleep';
44
import { L2Block, type L2BlockSourceEventEmitter, L2BlockSourceEvents } from '@aztec/stdlib/block';
5+
import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
56
import type {
67
BuildBlockResult,
78
IFullNodeBlockBuilder,
@@ -28,6 +29,10 @@ describe('EpochPruneWatcher', () => {
2829
let txProvider: MockProxy<Pick<ITxProvider, 'getAvailableTxs'>>;
2930
let blockBuilder: MockProxy<IFullNodeBlockBuilder>;
3031
let fork: MockProxy<MerkleTreeWriteOperations>;
32+
33+
let ts: bigint;
34+
let l1Constants: L1RollupConstants;
35+
3136
const validEpochPrunedPenalty = BigInt(1000000000000000000n);
3237
const dataWithholdingPenalty = BigInt(2000000000000000000n);
3338

@@ -41,6 +46,18 @@ describe('EpochPruneWatcher', () => {
4146
fork = mock<MerkleTreeWriteOperations>();
4247
blockBuilder.getFork.mockResolvedValue(fork);
4348

49+
ts = BigInt(Math.ceil(Date.now() / 1000));
50+
l1Constants = {
51+
l1StartBlock: 1n,
52+
l1GenesisTime: ts,
53+
slotDuration: 24,
54+
epochDuration: 8,
55+
ethereumSlotDuration: 12,
56+
proofSubmissionEpochs: 1,
57+
};
58+
59+
epochCache.getL1Constants.mockReturnValue(l1Constants);
60+
4461
watcher = new EpochPruneWatcher(l2BlockSource, l1ToL2MessageSource, epochCache, txProvider, blockBuilder, {
4562
slashPrunePenalty: validEpochPrunedPenalty,
4663
slashDataWithholdingPenalty: dataWithholdingPenalty,
@@ -54,9 +71,10 @@ describe('EpochPruneWatcher', () => {
5471

5572
it('should emit WANT_TO_SLASH_EVENT when a validator is in a pruned epoch when data is unavailable', async () => {
5673
const emitSpy = jest.spyOn(watcher, 'emit');
74+
const epochNumber = 1n;
5775

5876
const block = await L2Block.random(
59-
1, // block number
77+
12, // block number
6078
4, // txs per block
6179
);
6280
txProvider.getAvailableTxs.mockResolvedValue({ txs: [], missingTxs: [block.body.txEffects[0].txHash] });
@@ -68,11 +86,11 @@ describe('EpochPruneWatcher', () => {
6886
epochCache.getCommitteeForEpoch.mockResolvedValue({
6987
committee: committee.map(EthAddress.fromString),
7088
seed: 0n,
71-
epoch: 1n,
89+
epoch: epochNumber,
7290
});
7391

7492
l2BlockSource.emit(L2BlockSourceEvents.L2PruneDetected, {
75-
epochNumber: 1n,
93+
epochNumber,
7694
blocks: [block],
7795
type: L2BlockSourceEvents.L2PruneDetected,
7896
});
@@ -85,13 +103,13 @@ describe('EpochPruneWatcher', () => {
85103
validator: EthAddress.fromString(committee[0]),
86104
amount: dataWithholdingPenalty,
87105
offenseType: OffenseType.DATA_WITHHOLDING,
88-
epochOrSlot: 1n,
106+
epochOrSlot: epochNumber,
89107
},
90108
{
91109
validator: EthAddress.fromString(committee[1]),
92110
amount: dataWithholdingPenalty,
93111
offenseType: OffenseType.DATA_WITHHOLDING,
94-
epochOrSlot: 1n,
112+
epochOrSlot: epochNumber,
95113
},
96114
] satisfies WantToSlashArgs[]);
97115
});
@@ -100,7 +118,7 @@ describe('EpochPruneWatcher', () => {
100118
const emitSpy = jest.spyOn(watcher, 'emit');
101119

102120
const block = await L2Block.random(
103-
1, // block number
121+
12, // block number
104122
4, // txs per block
105123
);
106124
const tx = Tx.random();
@@ -152,11 +170,11 @@ describe('EpochPruneWatcher', () => {
152170
const emitSpy = jest.spyOn(watcher, 'emit');
153171

154172
const blockFromL1 = await L2Block.random(
155-
1, // block number
173+
12, // block number
156174
1, // txs per block
157175
);
158176
const blockFromBuilder = await L2Block.random(
159-
2, // block number
177+
13, // block number
160178
1, // txs per block
161179
);
162180
const tx = Tx.random();

yarn-project/slasher/src/watchers/epoch_prune_watcher.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
type L2BlockSourceEventEmitter,
1010
L2BlockSourceEvents,
1111
} from '@aztec/stdlib/block';
12+
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
1213
import type {
1314
IFullNodeBlockBuilder,
1415
ITxProvider,
@@ -78,9 +79,14 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
7879

7980
private handlePruneL2Blocks(event: L2BlockPruneEvent): void {
8081
const { blocks, epochNumber } = event;
81-
this.log.info(`Detected chain prune. Validating epoch ${epochNumber}`);
82+
const l1Constants = this.epochCache.getL1Constants();
83+
const epochBlocks = blocks.filter(b => getEpochAtSlot(b.slot, l1Constants) === epochNumber);
84+
this.log.info(
85+
`Detected chain prune. Validating epoch ${epochNumber} with blocks ${epochBlocks[0]?.number} to ${epochBlocks[epochBlocks.length - 1]?.number}.`,
86+
{ blocks: epochBlocks.map(b => b.toBlockInfo()) },
87+
);
8288

83-
this.validateBlocks(blocks)
89+
this.validateBlocks(epochBlocks)
8490
.then(async () => {
8591
this.log.info(`Pruned epoch ${epochNumber} was valid. Want to slash committee for not having it proven.`);
8692
const validators = await this.getValidatorsForEpoch(epochNumber);

0 commit comments

Comments
 (0)