@aztec/slasher 0.0.1-commit.7d4e6cd → 0.0.1-commit.7ffbba4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +24 -14
  2. package/dest/config.d.ts +1 -1
  3. package/dest/config.d.ts.map +1 -1
  4. package/dest/config.js +29 -17
  5. package/dest/empire_slasher_client.d.ts +1 -1
  6. package/dest/empire_slasher_client.d.ts.map +1 -1
  7. package/dest/empire_slasher_client.js +0 -8
  8. package/dest/factory/create_facade.d.ts +3 -3
  9. package/dest/factory/create_facade.d.ts.map +1 -1
  10. package/dest/factory/create_facade.js +26 -3
  11. package/dest/factory/create_implementation.d.ts +4 -3
  12. package/dest/factory/create_implementation.d.ts.map +1 -1
  13. package/dest/factory/create_implementation.js +11 -7
  14. package/dest/factory/get_settings.d.ts +2 -2
  15. package/dest/factory/get_settings.d.ts.map +1 -1
  16. package/dest/generated/slasher-defaults.d.ts +21 -0
  17. package/dest/generated/slasher-defaults.d.ts.map +1 -0
  18. package/dest/generated/slasher-defaults.js +21 -0
  19. package/dest/slash_offenses_collector.d.ts +5 -2
  20. package/dest/slash_offenses_collector.d.ts.map +1 -1
  21. package/dest/slash_offenses_collector.js +2 -2
  22. package/dest/slasher_client_facade.d.ts +4 -3
  23. package/dest/slasher_client_facade.d.ts.map +1 -1
  24. package/dest/slasher_client_facade.js +4 -2
  25. package/dest/stores/offenses_store.d.ts +1 -1
  26. package/dest/stores/offenses_store.d.ts.map +1 -1
  27. package/dest/stores/offenses_store.js +4 -2
  28. package/dest/stores/payloads_store.d.ts +1 -1
  29. package/dest/stores/payloads_store.d.ts.map +1 -1
  30. package/dest/stores/payloads_store.js +6 -3
  31. package/dest/tally_slasher_client.d.ts +2 -2
  32. package/dest/tally_slasher_client.d.ts.map +1 -1
  33. package/dest/tally_slasher_client.js +10 -5
  34. package/dest/watchers/attestations_block_watcher.d.ts +4 -3
  35. package/dest/watchers/attestations_block_watcher.d.ts.map +1 -1
  36. package/dest/watchers/attestations_block_watcher.js +5 -4
  37. package/dest/watchers/epoch_prune_watcher.d.ts +10 -8
  38. package/dest/watchers/epoch_prune_watcher.d.ts.map +1 -1
  39. package/dest/watchers/epoch_prune_watcher.js +55 -16
  40. package/package.json +14 -12
  41. package/src/config.ts +32 -17
  42. package/src/empire_slasher_client.ts +0 -8
  43. package/src/factory/create_facade.ts +33 -4
  44. package/src/factory/create_implementation.ts +29 -4
  45. package/src/factory/get_settings.ts +2 -2
  46. package/src/generated/slasher-defaults.ts +23 -0
  47. package/src/slash_offenses_collector.ts +8 -3
  48. package/src/slasher_client_facade.ts +3 -1
  49. package/src/stores/offenses_store.ts +4 -2
  50. package/src/stores/payloads_store.ts +7 -4
  51. package/src/tally_slasher_client.ts +12 -6
  52. package/src/watchers/attestations_block_watcher.ts +8 -3
  53. package/src/watchers/epoch_prune_watcher.ts +85 -26
@@ -1,14 +1,16 @@
1
1
  import { EpochCache } from '@aztec/epoch-cache';
2
- import { RollupContract } from '@aztec/ethereum/contracts';
2
+ import { RegistryContract, RollupContract } from '@aztec/ethereum/contracts';
3
3
  import type { L1ReaderConfig } from '@aztec/ethereum/l1-reader';
4
4
  import type { ViemClient } from '@aztec/ethereum/types';
5
+ import { SlotNumber } from '@aztec/foundation/branded-types';
5
6
  import { unique } from '@aztec/foundation/collection';
6
7
  import { EthAddress } from '@aztec/foundation/eth-address';
7
8
  import { createLogger } from '@aztec/foundation/log';
8
9
  import { DateProvider } from '@aztec/foundation/timer';
9
- import type { DataStoreConfig } from '@aztec/kv-store/config';
10
10
  import { createStore } from '@aztec/kv-store/lmdb-v2';
11
+ import { getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
11
12
  import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
13
+ import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
12
14
 
13
15
  import { SlasherClientFacade } from '../slasher_client_facade.js';
14
16
  import type { SlasherClientInterface } from '../slasher_client_interface.js';
@@ -18,7 +20,7 @@ import type { Watcher } from '../watcher.js';
18
20
  /** Creates a slasher client facade that updates itself whenever the rollup slasher changes */
19
21
  export async function createSlasherFacade(
20
22
  config: SlasherConfig & DataStoreConfig & { ethereumSlotDuration: number },
21
- l1Contracts: Pick<L1ReaderConfig['l1Contracts'], 'rollupAddress' | 'slashFactoryAddress'>,
23
+ l1Contracts: Pick<L1ReaderConfig['l1Contracts'], 'rollupAddress' | 'slashFactoryAddress' | 'registryAddress'>,
22
24
  l1Client: ViemClient,
23
25
  watchers: Watcher[],
24
26
  dateProvider: DateProvider,
@@ -31,9 +33,35 @@ export async function createSlasherFacade(
31
33
  throw new Error('Cannot initialize SlasherClient without a Rollup address');
32
34
  }
33
35
 
34
- const kvStore = await createStore('slasher', SCHEMA_VERSION, config, createLogger('slasher:lmdb'));
36
+ const kvStore = await createStore('slasher', SCHEMA_VERSION, config, logger.getBindings());
35
37
  const rollup = new RollupContract(l1Client, l1Contracts.rollupAddress);
36
38
 
39
+ // Compute and cache the L2 slot at which the rollup was registered as canonical
40
+ const settingsMap = kvStore.openMap<string, number>('slasher-settings');
41
+ const cacheKey = `registeredSlot:${l1Contracts.rollupAddress}`;
42
+ let rollupRegisteredAtL2Slot = (await settingsMap.getAsync(cacheKey)) as SlotNumber | undefined;
43
+
44
+ if (rollupRegisteredAtL2Slot === undefined) {
45
+ const registry = new RegistryContract(l1Client, l1Contracts.registryAddress);
46
+ const l1StartBlock = await rollup.getL1StartBlock();
47
+ const registrationTimestamp = await registry.getCanonicalRollupRegistrationTimestamp(
48
+ l1Contracts.rollupAddress,
49
+ l1StartBlock,
50
+ );
51
+ if (registrationTimestamp !== undefined) {
52
+ const l1GenesisTime = await rollup.getL1GenesisTime();
53
+ const slotDuration = await rollup.getSlotDuration();
54
+ rollupRegisteredAtL2Slot = getSlotAtTimestamp(registrationTimestamp, {
55
+ l1GenesisTime,
56
+ slotDuration: Number(slotDuration),
57
+ });
58
+ } else {
59
+ rollupRegisteredAtL2Slot = SlotNumber(0);
60
+ }
61
+ await settingsMap.set(cacheKey, rollupRegisteredAtL2Slot);
62
+ logger.info(`Canonical rollup registered at L2 slot ${rollupRegisteredAtL2Slot}`);
63
+ }
64
+
37
65
  const slashValidatorsNever = config.slashSelfAllowed
38
66
  ? config.slashValidatorsNever
39
67
  : unique([...config.slashValidatorsNever, ...validatorAddresses].map(a => a.toString())).map(EthAddress.fromString);
@@ -48,6 +76,7 @@ export async function createSlasherFacade(
48
76
  epochCache,
49
77
  dateProvider,
50
78
  kvStore,
79
+ rollupRegisteredAtL2Slot,
51
80
  logger,
52
81
  );
53
82
  }
@@ -5,12 +5,13 @@ import {
5
5
  TallySlashingProposerContract,
6
6
  } from '@aztec/ethereum/contracts';
7
7
  import type { ViemClient } from '@aztec/ethereum/types';
8
+ import type { SlotNumber } from '@aztec/foundation/branded-types';
8
9
  import { EthAddress } from '@aztec/foundation/eth-address';
9
10
  import { createLogger } from '@aztec/foundation/log';
10
11
  import { DateProvider } from '@aztec/foundation/timer';
11
- import type { DataStoreConfig } from '@aztec/kv-store/config';
12
12
  import { AztecLMDBStoreV2 } from '@aztec/kv-store/lmdb-v2';
13
13
  import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
14
+ import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
14
15
  import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
15
16
 
16
17
  import { EmpireSlasherClient, type EmpireSlasherSettings } from '../empire_slasher_client.js';
@@ -31,19 +32,40 @@ export async function createSlasherImplementation(
31
32
  epochCache: EpochCache,
32
33
  dateProvider: DateProvider,
33
34
  kvStore: AztecLMDBStoreV2,
35
+ rollupRegisteredAtL2Slot: SlotNumber,
34
36
  logger = createLogger('slasher'),
35
37
  ) {
36
38
  const proposer = await rollup.getSlashingProposer();
37
39
  if (!proposer) {
38
40
  return new NullSlasherClient(config);
39
41
  } else if (proposer.type === 'tally') {
40
- return createTallySlasher(config, rollup, proposer, watchers, dateProvider, epochCache, kvStore, logger);
42
+ return createTallySlasher(
43
+ config,
44
+ rollup,
45
+ proposer,
46
+ watchers,
47
+ dateProvider,
48
+ epochCache,
49
+ kvStore,
50
+ rollupRegisteredAtL2Slot,
51
+ logger,
52
+ );
41
53
  } else {
42
54
  if (!slashFactoryAddress || slashFactoryAddress.equals(EthAddress.ZERO)) {
43
55
  throw new Error('Cannot initialize an empire-based SlasherClient without a SlashFactory address');
44
56
  }
45
57
  const slashFactory = new SlashFactoryContract(l1Client, slashFactoryAddress.toString());
46
- return createEmpireSlasher(config, rollup, proposer, slashFactory, watchers, dateProvider, kvStore, logger);
58
+ return createEmpireSlasher(
59
+ config,
60
+ rollup,
61
+ proposer,
62
+ slashFactory,
63
+ watchers,
64
+ dateProvider,
65
+ kvStore,
66
+ rollupRegisteredAtL2Slot,
67
+ logger,
68
+ );
47
69
  }
48
70
  }
49
71
 
@@ -55,6 +77,7 @@ async function createEmpireSlasher(
55
77
  watchers: Watcher[],
56
78
  dateProvider: DateProvider,
57
79
  kvStore: AztecLMDBStoreV2,
80
+ rollupRegisteredAtL2Slot: SlotNumber,
58
81
  logger = createLogger('slasher'),
59
82
  ): Promise<EmpireSlasherClient> {
60
83
  if (slashingProposer.type !== 'empire') {
@@ -97,6 +120,7 @@ async function createEmpireSlasher(
97
120
  l1StartBlock,
98
121
  ethereumSlotDuration: config.ethereumSlotDuration,
99
122
  slashingAmounts: undefined,
123
+ rollupRegisteredAtL2Slot,
100
124
  };
101
125
 
102
126
  const payloadsStore = new SlasherPayloadsStore(kvStore, {
@@ -130,13 +154,14 @@ async function createTallySlasher(
130
154
  dateProvider: DateProvider,
131
155
  epochCache: EpochCache,
132
156
  kvStore: AztecLMDBStoreV2,
157
+ rollupRegisteredAtL2Slot: SlotNumber,
133
158
  logger = createLogger('slasher'),
134
159
  ): Promise<TallySlasherClient> {
135
160
  if (slashingProposer.type !== 'tally') {
136
161
  throw new Error('Slashing proposer contract is not of type tally');
137
162
  }
138
163
 
139
- const settings = await getTallySlasherSettings(rollup, slashingProposer);
164
+ const settings = { ...(await getTallySlasherSettings(rollup, slashingProposer)), rollupRegisteredAtL2Slot };
140
165
  const slasher = await rollup.getSlasherContract();
141
166
 
142
167
  const offensesStore = new SlasherOffensesStore(kvStore, {
@@ -5,7 +5,7 @@ import type { TallySlasherSettings } from '../tally_slasher_client.js';
5
5
  export async function getTallySlasherSettings(
6
6
  rollup: RollupContract,
7
7
  slashingProposer?: TallySlashingProposerContract,
8
- ): Promise<TallySlasherSettings> {
8
+ ): Promise<Omit<TallySlasherSettings, 'rollupRegisteredAtL2Slot'>> {
9
9
  if (!slashingProposer) {
10
10
  const rollupSlashingProposer = await rollup.getSlashingProposer();
11
11
  if (!rollupSlashingProposer || rollupSlashingProposer.type !== 'tally') {
@@ -40,7 +40,7 @@ export async function getTallySlasherSettings(
40
40
  rollup.getTargetCommitteeSize(),
41
41
  ]);
42
42
 
43
- const settings: TallySlasherSettings = {
43
+ const settings: Omit<TallySlasherSettings, 'rollupRegisteredAtL2Slot'> = {
44
44
  slashingExecutionDelayInRounds: Number(slashingExecutionDelayInRounds),
45
45
  slashingRoundSize: Number(slashingRoundSize),
46
46
  slashingRoundSizeInEpochs: Number(slashingRoundSizeInEpochs),
@@ -0,0 +1,23 @@
1
+ // Auto-generated from spartan/environments/network-defaults.yml
2
+ // Do not edit manually - run yarn generate to regenerate
3
+
4
+ /** Default slasher configuration values from network-defaults.yml */
5
+ export const slasherDefaultEnv = {
6
+ SLASH_MIN_PENALTY_PERCENTAGE: 0.5,
7
+ SLASH_MAX_PENALTY_PERCENTAGE: 2,
8
+ SLASH_OFFENSE_EXPIRATION_ROUNDS: 4,
9
+ SLASH_MAX_PAYLOAD_SIZE: 80,
10
+ SLASH_EXECUTE_ROUNDS_LOOK_BACK: 4,
11
+ SLASH_PRUNE_PENALTY: 10000000000000000000,
12
+ SLASH_DATA_WITHHOLDING_PENALTY: 10000000000000000000,
13
+ SLASH_INACTIVITY_TARGET_PERCENTAGE: 0.9,
14
+ SLASH_INACTIVITY_CONSECUTIVE_EPOCH_THRESHOLD: 1,
15
+ SLASH_INACTIVITY_PENALTY: 10000000000000000000,
16
+ SLASH_PROPOSE_INVALID_ATTESTATIONS_PENALTY: 10000000000000000000,
17
+ SLASH_ATTEST_DESCENDANT_OF_INVALID_PENALTY: 10000000000000000000,
18
+ SLASH_DUPLICATE_PROPOSAL_PENALTY: 0,
19
+ SLASH_DUPLICATE_ATTESTATION_PENALTY: 0,
20
+ SLASH_UNKNOWN_PENALTY: 10000000000000000000,
21
+ SLASH_INVALID_BLOCK_PENALTY: 10000000000000000000,
22
+ SLASH_GRACE_PERIOD_L2_SLOTS: 0,
23
+ } as const;
@@ -1,3 +1,4 @@
1
+ import type { SlotNumber } from '@aztec/foundation/branded-types';
1
2
  import { createLogger } from '@aztec/foundation/log';
2
3
  import type { Prettify } from '@aztec/foundation/types';
3
4
  import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
@@ -9,7 +10,11 @@ import { WANT_TO_SLASH_EVENT, type WantToSlashArgs, type Watcher } from './watch
9
10
 
10
11
  export type SlashOffensesCollectorConfig = Prettify<Pick<SlasherConfig, 'slashGracePeriodL2Slots'>>;
11
12
  export type SlashOffensesCollectorSettings = Prettify<
12
- Pick<L1RollupConstants, 'epochDuration'> & { slashingAmounts: [bigint, bigint, bigint] | undefined }
13
+ Pick<L1RollupConstants, 'epochDuration'> & {
14
+ slashingAmounts: [bigint, bigint, bigint] | undefined;
15
+ /** L2 slot at which the rollup was registered as canonical in the Registry. Used to anchor the slash grace period. */
16
+ rollupRegisteredAtL2Slot: SlotNumber;
17
+ }
13
18
  >;
14
19
 
15
20
  /**
@@ -110,9 +115,9 @@ export class SlashOffensesCollector {
110
115
  return this.offensesStore.markAsSlashed(offenses);
111
116
  }
112
117
 
113
- /** Returns whether to skip an offense if it happened during the grace period at the beginning of the chain */
118
+ /** Returns whether to skip an offense if it happened during the grace period after the network upgrade */
114
119
  private shouldSkipOffense(offense: Offense): boolean {
115
120
  const offenseSlot = getSlotForOffense(offense, this.settings);
116
- return offenseSlot < this.config.slashGracePeriodL2Slots;
121
+ return offenseSlot < this.settings.rollupRegisteredAtL2Slot + this.config.slashGracePeriodL2Slots;
117
122
  }
118
123
  }
@@ -5,9 +5,9 @@ import type { SlotNumber } from '@aztec/foundation/branded-types';
5
5
  import { EthAddress } from '@aztec/foundation/eth-address';
6
6
  import { createLogger } from '@aztec/foundation/log';
7
7
  import { DateProvider } from '@aztec/foundation/timer';
8
- import type { DataStoreConfig } from '@aztec/kv-store/config';
9
8
  import { AztecLMDBStoreV2 } from '@aztec/kv-store/lmdb-v2';
10
9
  import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
10
+ import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
11
11
  import type { Offense, ProposerSlashAction, SlashPayloadRound } from '@aztec/stdlib/slashing';
12
12
 
13
13
  import { createSlasherImplementation } from './factory/create_implementation.js';
@@ -32,6 +32,7 @@ export class SlasherClientFacade implements SlasherClientInterface {
32
32
  private epochCache: EpochCache,
33
33
  private dateProvider: DateProvider,
34
34
  private kvStore: AztecLMDBStoreV2,
35
+ private rollupRegisteredAtL2Slot: SlotNumber,
35
36
  private logger = createLogger('slasher'),
36
37
  ) {}
37
38
 
@@ -88,6 +89,7 @@ export class SlasherClientFacade implements SlasherClientInterface {
88
89
  this.epochCache,
89
90
  this.dateProvider,
90
91
  this.kvStore,
92
+ this.rollupRegisteredAtL2Slot,
91
93
  this.logger,
92
94
  );
93
95
  }
@@ -76,9 +76,11 @@ export class SlasherOffensesStore {
76
76
  /** Adds a new offense (defaults to pending, but will be slashed if markAsSlashed had been called for it) */
77
77
  public async addPendingOffense(offense: Offense): Promise<void> {
78
78
  const key = this.getOffenseKey(offense);
79
- await this.offenses.set(key, serializeOffense(offense));
80
79
  const round = getRoundForOffense(offense, this.settings);
81
- await this.roundsOffenses.set(this.getRoundKey(round), key);
80
+ await this.kvStore.transactionAsync(async () => {
81
+ await this.offenses.set(key, serializeOffense(offense));
82
+ await this.roundsOffenses.set(this.getRoundKey(round), key);
83
+ });
82
84
  this.log.trace(`Adding pending offense ${key} for round ${round}`);
83
85
  }
84
86
 
@@ -118,10 +118,13 @@ export class SlasherPayloadsStore {
118
118
 
119
119
  public async incrementPayloadVotes(payloadAddress: EthAddress, round: bigint): Promise<bigint> {
120
120
  const key = this.getPayloadVotesKey(round, payloadAddress);
121
- const currentVotes = (await this.roundPayloadVotes.getAsync(key)) || 0n;
122
- const newVotes = currentVotes + 1n;
123
- await this.roundPayloadVotes.set(key, newVotes);
124
- return newVotes;
121
+ let newVotes: bigint;
122
+ await this.kvStore.transactionAsync(async () => {
123
+ const currentVotes = (await this.roundPayloadVotes.getAsync(key)) || 0n;
124
+ newVotes = currentVotes + 1n;
125
+ await this.roundPayloadVotes.set(key, newVotes);
126
+ });
127
+ return newVotes!;
125
128
  }
126
129
 
127
130
  public async addPayload(payload: SlashPayloadRound): Promise<void> {
@@ -5,7 +5,6 @@ import { maxBigint } from '@aztec/foundation/bigint';
5
5
  import { SlotNumber } from '@aztec/foundation/branded-types';
6
6
  import { compactArray, partition, times } from '@aztec/foundation/collection';
7
7
  import { createLogger } from '@aztec/foundation/log';
8
- import { sleep } from '@aztec/foundation/sleep';
9
8
  import type { DateProvider } from '@aztec/foundation/timer';
10
9
  import type { Prettify } from '@aztec/foundation/types';
11
10
  import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
@@ -47,7 +46,10 @@ export type TallySlasherSettings = Prettify<
47
46
  >;
48
47
 
49
48
  export type TallySlasherClientConfig = SlashOffensesCollectorConfig &
50
- Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever' | 'slashExecuteRoundsLookBack'>;
49
+ Pick<
50
+ SlasherConfig,
51
+ 'slashValidatorsAlways' | 'slashValidatorsNever' | 'slashExecuteRoundsLookBack' | 'slashMaxPayloadSize'
52
+ >;
51
53
 
52
54
  /**
53
55
  * The Tally Slasher client is responsible for managing slashable offenses using
@@ -138,8 +140,6 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
138
140
  this.roundMonitor.stop();
139
141
  await this.offensesCollector.stop();
140
142
 
141
- // Sleeping to sidestep viem issue with unwatching events
142
- await sleep(2000);
143
143
  this.log.info('Tally Slasher client stopped');
144
144
  }
145
145
 
@@ -281,8 +281,12 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
281
281
  return undefined;
282
282
  }
283
283
 
284
+ const slashActionsWithAmounts = slashActions.map(action => ({
285
+ validator: action.validator.toString(),
286
+ slashAmount: action.slashAmount.toString(),
287
+ }));
284
288
  this.log.info(`Round ${executableRound} is ready to execute with ${slashActions.length} slashes`, {
285
- slashActions,
289
+ slashActions: slashActionsWithAmounts,
286
290
  payloadAddress: payload.address.toString(),
287
291
  ...logData,
288
292
  });
@@ -357,11 +361,13 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
357
361
 
358
362
  const committees = await this.collectCommitteesActiveDuringRound(slashedRound);
359
363
  const epochsForCommittees = getEpochsForRound(slashedRound, this.settings);
364
+ const { slashMaxPayloadSize } = this.config;
360
365
  const votes = getSlashConsensusVotesFromOffenses(
361
366
  offensesToSlash,
362
367
  committees,
363
368
  epochsForCommittees.map(e => BigInt(e)),
364
- this.settings,
369
+ { ...this.settings, maxSlashedValidators: slashMaxPayloadSize },
370
+ this.log,
365
371
  );
366
372
  if (votes.every(v => v === 0)) {
367
373
  this.log.warn(`Computed votes for offenses are all zero. Skipping vote.`, {
@@ -67,19 +67,23 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
67
67
  }
68
68
 
69
69
  public start() {
70
- this.l2BlockSource.on(L2BlockSourceEvents.InvalidAttestationsCheckpointDetected, this.boundHandleInvalidCheckpoint);
70
+ this.l2BlockSource.events.on(
71
+ L2BlockSourceEvents.InvalidAttestationsCheckpointDetected,
72
+ this.boundHandleInvalidCheckpoint,
73
+ );
71
74
  return Promise.resolve();
72
75
  }
73
76
 
74
77
  public stop() {
75
- this.l2BlockSource.removeListener(
78
+ this.l2BlockSource.events.removeListener(
76
79
  L2BlockSourceEvents.InvalidAttestationsCheckpointDetected,
77
80
  this.boundHandleInvalidCheckpoint,
78
81
  );
79
82
  return Promise.resolve();
80
83
  }
81
84
 
82
- private handleInvalidCheckpoint(event: InvalidCheckpointDetectedEvent): void {
85
+ /** Event handler for invalid checkpoints as reported by the archiver. Public for testing purposes. */
86
+ public handleInvalidCheckpoint(event: InvalidCheckpointDetectedEvent): void {
83
87
  const { validationResult } = event;
84
88
  const checkpoint = validationResult.checkpoint;
85
89
 
@@ -139,6 +143,7 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
139
143
  committee: validationResult.committee,
140
144
  seed: validationResult.seed,
141
145
  epoch: validationResult.epoch,
146
+ isEscapeHatchOpen: false,
142
147
  };
143
148
  const proposer = this.epochCache.getProposerFromEpochCommittee(epochCommitteeInfo, slot);
144
149
 
@@ -1,23 +1,26 @@
1
1
  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';
4
+ import type { Fr } from '@aztec/foundation/curves/bn254';
4
5
  import { type Logger, createLogger } from '@aztec/foundation/log';
5
6
  import {
6
7
  EthAddress,
7
- L2BlockNew,
8
- type L2BlockPruneEvent,
8
+ L2Block,
9
9
  type L2BlockSourceEventEmitter,
10
10
  L2BlockSourceEvents,
11
+ type L2PruneUnprovenEvent,
11
12
  } from '@aztec/stdlib/block';
12
13
  import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
13
14
  import type {
14
- IFullNodeBlockBuilder,
15
+ ICheckpointBlockBuilder,
16
+ ICheckpointsBuilder,
15
17
  ITxProvider,
16
18
  MerkleTreeWriteOperations,
17
19
  SlasherConfig,
18
20
  } from '@aztec/stdlib/interfaces/server';
19
- import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
21
+ import { type L1ToL2MessageSource, computeCheckpointOutHash } from '@aztec/stdlib/messaging';
20
22
  import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';
23
+ import type { CheckpointGlobalVariables } from '@aztec/stdlib/tx';
21
24
  import {
22
25
  ReExFailedTxsError,
23
26
  ReExStateMismatchError,
@@ -52,7 +55,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
52
55
  private l1ToL2MessageSource: L1ToL2MessageSource,
53
56
  private epochCache: EpochCache,
54
57
  private txProvider: Pick<ITxProvider, 'getAvailableTxs'>,
55
- private blockBuilder: IFullNodeBlockBuilder,
58
+ private checkpointsBuilder: ICheckpointsBuilder,
56
59
  penalties: EpochPruneWatcherPenalties,
57
60
  ) {
58
61
  super();
@@ -63,12 +66,12 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
63
66
  }
64
67
 
65
68
  public start() {
66
- this.l2BlockSource.on(L2BlockSourceEvents.L2PruneDetected, this.boundHandlePruneL2Blocks);
69
+ this.l2BlockSource.events.on(L2BlockSourceEvents.L2PruneUnproven, this.boundHandlePruneL2Blocks);
67
70
  return Promise.resolve();
68
71
  }
69
72
 
70
73
  public stop() {
71
- this.l2BlockSource.removeListener(L2BlockSourceEvents.L2PruneDetected, this.boundHandlePruneL2Blocks);
74
+ this.l2BlockSource.events.removeListener(L2BlockSourceEvents.L2PruneUnproven, this.boundHandlePruneL2Blocks);
72
75
  return Promise.resolve();
73
76
  }
74
77
 
@@ -77,7 +80,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
77
80
  this.log.verbose('EpochPruneWatcher config updated', this.penalties);
78
81
  }
79
82
 
80
- private handlePruneL2Blocks(event: L2BlockPruneEvent): void {
83
+ private handlePruneL2Blocks(event: L2PruneUnprovenEvent): void {
81
84
  const { blocks, epochNumber } = event;
82
85
  void this.processPruneL2Blocks(blocks, epochNumber).catch(err =>
83
86
  this.log.error('Error processing pruned L2 blocks', err, { epochNumber }),
@@ -95,7 +98,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
95
98
  this.emit(WANT_TO_SLASH_EVENT, args);
96
99
  }
97
100
 
98
- private async processPruneL2Blocks(blocks: L2BlockNew[], epochNumber: EpochNumber): Promise<void> {
101
+ private async processPruneL2Blocks(blocks: L2Block[], epochNumber: EpochNumber): Promise<void> {
99
102
  try {
100
103
  const l1Constants = this.epochCache.getL1Constants();
101
104
  const epochBlocks = blocks.filter(b => getEpochAtSlot(b.header.getSlot(), l1Constants) === epochNumber);
@@ -104,7 +107,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
104
107
  { blocks: epochBlocks.map(b => b.toBlockInfo()) },
105
108
  );
106
109
 
107
- await this.validateBlocks(epochBlocks);
110
+ await this.validateBlocks(epochBlocks, epochNumber);
108
111
  this.log.info(`Pruned epoch ${epochNumber} was valid. Want to slash committee for not having it proven.`);
109
112
  await this.emitSlashForEpoch(OffenseType.VALID_EPOCH_PRUNED, epochNumber);
110
113
  } catch (error) {
@@ -119,21 +122,83 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
119
122
  }
120
123
  }
121
124
 
122
- public async validateBlocks(blocks: L2BlockNew[]): Promise<void> {
125
+ public async validateBlocks(blocks: L2Block[], epochNumber: EpochNumber): Promise<void> {
123
126
  if (blocks.length === 0) {
124
127
  return;
125
128
  }
126
- const fork = await this.blockBuilder.getFork(BlockNumber(blocks[0].header.globalVariables.blockNumber - 1));
129
+
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.getCheckpointsDataForEpoch(epochNumber))
136
+ .filter(c => c.checkpointNumber < sortedBlocks[0].checkpointNumber)
137
+ .map(c => c.checkpointOutHash);
138
+ let previousCheckpointOutHashes: Fr[] = [...priorCheckpointOutHashes];
139
+
140
+ const fork = await this.checkpointsBuilder.getFork(
141
+ BlockNumber(sortedBlocks[0].header.globalVariables.blockNumber - 1),
142
+ );
127
143
  try {
128
- for (const block of blocks) {
129
- await this.validateBlock(block, fork);
144
+ for (const checkpointBlocks of blocksByCheckpoint) {
145
+ await this.validateCheckpoint(checkpointBlocks, previousCheckpointOutHashes, fork);
146
+
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
+ );
151
+ previousCheckpointOutHashes = [...previousCheckpointOutHashes, checkpointOutHash];
130
152
  }
131
153
  } finally {
132
154
  await fork.close();
133
155
  }
134
156
  }
135
157
 
136
- public async validateBlock(blockFromL1: L2BlockNew, fork: MerkleTreeWriteOperations): Promise<void> {
158
+ private async validateCheckpoint(
159
+ checkpointBlocks: L2Block[],
160
+ previousCheckpointOutHashes: Fr[],
161
+ fork: MerkleTreeWriteOperations,
162
+ ): Promise<void> {
163
+ const checkpointNumber = checkpointBlocks[0].checkpointNumber;
164
+ this.log.debug(`Validating pruned checkpoint ${checkpointNumber} with ${checkpointBlocks.length} blocks`);
165
+
166
+ // Get L1ToL2Messages once for the entire checkpoint
167
+ const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
168
+
169
+ // Build checkpoint constants from first block's global variables
170
+ const gv = checkpointBlocks[0].header.globalVariables;
171
+ const constants: CheckpointGlobalVariables = {
172
+ chainId: gv.chainId,
173
+ version: gv.version,
174
+ slotNumber: gv.slotNumber,
175
+ timestamp: gv.timestamp,
176
+ coinbase: gv.coinbase,
177
+ feeRecipient: gv.feeRecipient,
178
+ gasFees: gv.gasFees,
179
+ };
180
+
181
+ // Start checkpoint builder once for all blocks in this checkpoint
182
+ const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
183
+ checkpointNumber,
184
+ constants,
185
+ 0n, // feeAssetPriceModifier is not used for validation of the checkpoint content
186
+ l1ToL2Messages,
187
+ previousCheckpointOutHashes,
188
+ fork,
189
+ this.log.getBindings(),
190
+ );
191
+
192
+ // Validate all blocks in the checkpoint sequentially
193
+ for (const block of checkpointBlocks) {
194
+ await this.validateBlockInCheckpoint(block, checkpointBuilder);
195
+ }
196
+ }
197
+
198
+ private async validateBlockInCheckpoint(
199
+ blockFromL1: L2Block,
200
+ checkpointBuilder: ICheckpointBlockBuilder,
201
+ ): Promise<void> {
137
202
  this.log.debug(`Validating pruned block ${blockFromL1.header.globalVariables.blockNumber}`);
138
203
  const txHashes = blockFromL1.body.txEffects.map(txEffect => txEffect.txHash);
139
204
  // We load txs from the mempool directly, since the TxCollector running in the background has already been
@@ -145,15 +210,9 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
145
210
  throw new TransactionsNotAvailableError(missingTxs);
146
211
  }
147
212
 
148
- const checkpointNumber = CheckpointNumber.fromBlockNumber(blockFromL1.number);
149
- const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
150
- const { block, failedTxs, numTxs } = await this.blockBuilder.buildBlock(
151
- txs,
152
- l1ToL2Messages,
153
- blockFromL1.header.globalVariables,
154
- {},
155
- fork,
156
- );
213
+ const gv = blockFromL1.header.globalVariables;
214
+ const { block, failedTxs, numTxs } = await checkpointBuilder.buildBlock(txs, gv.blockNumber, gv.timestamp, {});
215
+
157
216
  if (numTxs !== txs.length) {
158
217
  // This should be detected by state mismatch, but this makes it easier to debug.
159
218
  throw new ValidatorError(`Built block with ${numTxs} txs, expected ${txs.length}`);