Skip to content

Commit 403c840

Browse files
committed
fix(slasher): handle multiple blocks per slot in epoch prune watcher
Update to e2e test pending.
1 parent 8e7e4a4 commit 403c840

File tree

3 files changed

+66
-26
lines changed

3 files changed

+66
-26
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'valid-epoch-pruned-slash
2929
* We don't need to do anything special for this test other than to run it without a prover node
3030
* (which is the default), and this will produce pruned epochs that could have been proven. But we do
3131
* need to send a tx to make sure that the slash is due to valid epoch prune and not data withholding.
32+
*
33+
* TODO(palla/mbps): Add tests for 1) out messages and 2) partial epoch prunes
3234
*/
3335
describe('e2e_p2p_valid_epoch_pruned_slash', () => {
3436
let t: P2PNetworkTest;

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { EpochCache } from '@aztec/epoch-cache';
2-
import { BlockNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
2+
import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
33
import { EthAddress } from '@aztec/foundation/eth-address';
44
import { sleep } from '@aztec/foundation/sleep';
55
import { L2Block, type L2BlockSourceEventEmitter, L2BlockSourceEvents } from '@aztec/stdlib/block';
@@ -76,12 +76,14 @@ describe('EpochPruneWatcher', () => {
7676
it('should emit WANT_TO_SLASH_EVENT when a validator is in a pruned epoch when data is unavailable', async () => {
7777
const emitSpy = jest.spyOn(watcher, 'emit');
7878
const epochNumber = EpochNumber(1);
79+
const checkpointNumber = CheckpointNumber(1);
7980

8081
const block = await L2Block.random(
8182
BlockNumber(12), // block number
8283
{
8384
txsPerBlock: 4,
8485
slotNumber: SlotNumber(10),
86+
checkpointNumber,
8587
},
8688
);
8789
txProvider.getAvailableTxs.mockResolvedValue({ txs: [], missingTxs: [block.body.txEffects[0].txHash] });
@@ -124,12 +126,14 @@ describe('EpochPruneWatcher', () => {
124126

125127
it('should slash if the data is available and the epoch could have been proven', async () => {
126128
const emitSpy = jest.spyOn(watcher, 'emit');
129+
const checkpointNumber = CheckpointNumber(1);
127130

128131
const block = await L2Block.random(
129132
BlockNumber(12), // block number
130133
{
131134
txsPerBlock: 4,
132135
slotNumber: SlotNumber(10),
136+
checkpointNumber,
133137
},
134138
);
135139
const tx = Tx.random();
@@ -186,12 +190,14 @@ describe('EpochPruneWatcher', () => {
186190

187191
it('should not slash if the data is available but the epoch could not have been proven', async () => {
188192
const emitSpy = jest.spyOn(watcher, 'emit');
193+
const checkpointNumber = CheckpointNumber(1);
189194

190195
const blockFromL1 = await L2Block.random(
191196
BlockNumber(12), // block number
192197
{
193198
txsPerBlock: 1,
194199
slotNumber: SlotNumber(10),
200+
checkpointNumber,
195201
},
196202
);
197203

@@ -200,6 +206,7 @@ describe('EpochPruneWatcher', () => {
200206
{
201207
txsPerBlock: 1,
202208
slotNumber: SlotNumber(10),
209+
checkpointNumber,
203210
},
204211
);
205212
const tx = Tx.random();
@@ -244,6 +251,7 @@ describe('EpochPruneWatcher', () => {
244251

245252
class MockL2BlockSource {
246253
public readonly events = new EventEmitter();
254+
public getCheckpointsForEpoch = () => [];
247255

248256
constructor() {}
249257
}

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

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { EpochCache } from '@aztec/epoch-cache';
2-
import { BlockNumber, CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
3-
import { merge, pick } from '@aztec/foundation/collection';
2+
import { BlockNumber, EpochNumber } from '@aztec/foundation/branded-types';
3+
import { chunkBy, merge, pick } from '@aztec/foundation/collection';
44
import type { Fr } from '@aztec/foundation/curves/bn254';
55
import { type Logger, createLogger } from '@aztec/foundation/log';
66
import {
@@ -12,6 +12,7 @@ import {
1212
} from '@aztec/stdlib/block';
1313
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
1414
import type {
15+
ICheckpointBlockBuilder,
1516
ICheckpointsBuilder,
1617
ITxProvider,
1718
MerkleTreeWriteOperations,
@@ -106,7 +107,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
106107
{ blocks: epochBlocks.map(b => b.toBlockInfo()) },
107108
);
108109

109-
await this.validateBlocks(epochBlocks);
110+
await this.validateBlocks(epochBlocks, epochNumber);
110111
this.log.info(`Pruned epoch ${epochNumber} was valid. Want to slash committee for not having it proven.`);
111112
await this.emitSlashForEpoch(OffenseType.VALID_EPOCH_PRUNED, epochNumber);
112113
} catch (error) {
@@ -121,45 +122,52 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
121122
}
122123
}
123124

124-
public async validateBlocks(blocks: L2Block[]): Promise<void> {
125+
public async validateBlocks(blocks: L2Block[], epochNumber: EpochNumber): Promise<void> {
125126
if (blocks.length === 0) {
126127
return;
127128
}
128129

129-
let previousCheckpointOutHashes: Fr[] = [];
130-
const fork = await this.checkpointsBuilder.getFork(BlockNumber(blocks[0].header.globalVariables.blockNumber - 1));
130+
// Sort blocks by block number and group by checkpoint
131+
const sortedBlocks = [...blocks].sort((a, b) => a.number - b.number);
132+
const blocksByCheckpoint = chunkBy(sortedBlocks, b => b.checkpointNumber);
133+
134+
// Get prior checkpoints in the epoch (in case this was a partial prune) to extract the out hashes
135+
const priorCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsForEpoch(epochNumber))
136+
.filter(c => c.number < sortedBlocks[0].checkpointNumber)
137+
.map(c => c.getCheckpointOutHash());
138+
let previousCheckpointOutHashes: Fr[] = [...priorCheckpointOutHashes];
139+
140+
const fork = await this.checkpointsBuilder.getFork(
141+
BlockNumber(sortedBlocks[0].header.globalVariables.blockNumber - 1),
142+
);
131143
try {
132-
for (const block of blocks) {
133-
await this.validateBlock(block, previousCheckpointOutHashes, fork);
144+
for (const checkpointBlocks of blocksByCheckpoint) {
145+
await this.validateCheckpoint(checkpointBlocks, previousCheckpointOutHashes, fork);
134146

135-
// TODO(mbps): This assumes one block per checkpoint, which is only true for now.
136-
const checkpointOutHash = computeCheckpointOutHash([block.body.txEffects.map(tx => tx.l2ToL1Msgs)]);
147+
// Compute checkpoint out hash from all blocks in this checkpoint
148+
const checkpointOutHash = computeCheckpointOutHash(
149+
checkpointBlocks.map(b => b.body.txEffects.map(tx => tx.l2ToL1Msgs)),
150+
);
137151
previousCheckpointOutHashes = [...previousCheckpointOutHashes, checkpointOutHash];
138152
}
139153
} finally {
140154
await fork.close();
141155
}
142156
}
143157

144-
public async validateBlock(
145-
blockFromL1: L2Block,
158+
private async validateCheckpoint(
159+
checkpointBlocks: L2Block[],
146160
previousCheckpointOutHashes: Fr[],
147161
fork: MerkleTreeWriteOperations,
148162
): Promise<void> {
149-
this.log.debug(`Validating pruned block ${blockFromL1.header.globalVariables.blockNumber}`);
150-
const txHashes = blockFromL1.body.txEffects.map(txEffect => txEffect.txHash);
151-
// We load txs from the mempool directly, since the TxCollector running in the background has already been
152-
// trying to fetch them from nodes or via reqresp. If we haven't managed to collect them by now,
153-
// it's likely that they are not available in the network at all.
154-
const { txs, missingTxs } = await this.txProvider.getAvailableTxs(txHashes);
155-
156-
if (missingTxs && missingTxs.length > 0) {
157-
throw new TransactionsNotAvailableError(missingTxs);
158-
}
163+
const checkpointNumber = checkpointBlocks[0].checkpointNumber;
164+
this.log.debug(`Validating pruned checkpoint ${checkpointNumber} with ${checkpointBlocks.length} blocks`);
159165

160-
const checkpointNumber = CheckpointNumber.fromBlockNumber(blockFromL1.number);
166+
// Get L1ToL2Messages once for the entire checkpoint
161167
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
162-
const gv = blockFromL1.header.globalVariables;
168+
169+
// Build checkpoint constants from first block's global variables
170+
const gv = checkpointBlocks[0].header.globalVariables;
163171
const constants: CheckpointGlobalVariables = {
164172
chainId: gv.chainId,
165173
version: gv.version,
@@ -169,7 +177,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
169177
gasFees: gv.gasFees,
170178
};
171179

172-
// Use checkpoint builder to validate the block
180+
// Start checkpoint builder once for all blocks in this checkpoint
173181
const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
174182
checkpointNumber,
175183
constants,
@@ -179,6 +187,28 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
179187
this.log.getBindings(),
180188
);
181189

190+
// Validate all blocks in the checkpoint sequentially
191+
for (const block of checkpointBlocks) {
192+
await this.validateBlockInCheckpoint(block, checkpointBuilder);
193+
}
194+
}
195+
196+
private async validateBlockInCheckpoint(
197+
blockFromL1: L2Block,
198+
checkpointBuilder: ICheckpointBlockBuilder,
199+
): Promise<void> {
200+
this.log.debug(`Validating pruned block ${blockFromL1.header.globalVariables.blockNumber}`);
201+
const txHashes = blockFromL1.body.txEffects.map(txEffect => txEffect.txHash);
202+
// We load txs from the mempool directly, since the TxCollector running in the background has already been
203+
// trying to fetch them from nodes or via reqresp. If we haven't managed to collect them by now,
204+
// it's likely that they are not available in the network at all.
205+
const { txs, missingTxs } = await this.txProvider.getAvailableTxs(txHashes);
206+
207+
if (missingTxs && missingTxs.length > 0) {
208+
throw new TransactionsNotAvailableError(missingTxs);
209+
}
210+
211+
const gv = blockFromL1.header.globalVariables;
182212
const { block, failedTxs, numTxs } = await checkpointBuilder.buildBlock(txs, gv.blockNumber, gv.timestamp, {});
183213

184214
if (numTxs !== txs.length) {

0 commit comments

Comments
 (0)