@aztec/slasher 0.0.1-commit.7d4e6cd → 0.0.1-commit.858058eac

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 (40) hide show
  1. package/README.md +10 -3
  2. package/dest/config.d.ts +1 -1
  3. package/dest/config.d.ts.map +1 -1
  4. package/dest/config.js +28 -16
  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.js +1 -1
  9. package/dest/generated/slasher-defaults.d.ts +21 -0
  10. package/dest/generated/slasher-defaults.d.ts.map +1 -0
  11. package/dest/generated/slasher-defaults.js +21 -0
  12. package/dest/slash_offenses_collector.d.ts +1 -1
  13. package/dest/slash_offenses_collector.d.ts.map +1 -1
  14. package/dest/slash_offenses_collector.js +5 -1
  15. package/dest/stores/offenses_store.d.ts +1 -1
  16. package/dest/stores/offenses_store.d.ts.map +1 -1
  17. package/dest/stores/offenses_store.js +4 -2
  18. package/dest/stores/payloads_store.d.ts +1 -1
  19. package/dest/stores/payloads_store.d.ts.map +1 -1
  20. package/dest/stores/payloads_store.js +6 -3
  21. package/dest/tally_slasher_client.d.ts +1 -1
  22. package/dest/tally_slasher_client.d.ts.map +1 -1
  23. package/dest/tally_slasher_client.js +10 -5
  24. package/dest/watchers/attestations_block_watcher.d.ts +4 -3
  25. package/dest/watchers/attestations_block_watcher.d.ts.map +1 -1
  26. package/dest/watchers/attestations_block_watcher.js +5 -4
  27. package/dest/watchers/epoch_prune_watcher.d.ts +10 -8
  28. package/dest/watchers/epoch_prune_watcher.d.ts.map +1 -1
  29. package/dest/watchers/epoch_prune_watcher.js +54 -16
  30. package/package.json +14 -12
  31. package/src/config.ts +30 -16
  32. package/src/empire_slasher_client.ts +0 -8
  33. package/src/factory/create_facade.ts +1 -1
  34. package/src/generated/slasher-defaults.ts +23 -0
  35. package/src/slash_offenses_collector.ts +5 -1
  36. package/src/stores/offenses_store.ts +4 -2
  37. package/src/stores/payloads_store.ts +7 -4
  38. package/src/tally_slasher_client.ts +10 -5
  39. package/src/watchers/attestations_block_watcher.ts +8 -3
  40. package/src/watchers/epoch_prune_watcher.ts +83 -26
@@ -1,8 +1,9 @@
1
- import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
2
- import { merge, pick } from '@aztec/foundation/collection';
1
+ import { BlockNumber } from '@aztec/foundation/branded-types';
2
+ import { chunkBy, merge, pick } from '@aztec/foundation/collection';
3
3
  import { createLogger } from '@aztec/foundation/log';
4
4
  import { L2BlockSourceEvents } from '@aztec/stdlib/block';
5
5
  import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
6
+ import { computeCheckpointOutHash } from '@aztec/stdlib/messaging';
6
7
  import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';
7
8
  import { ReExFailedTxsError, ReExStateMismatchError, TransactionsNotAvailableError, ValidatorError } from '@aztec/stdlib/validators';
8
9
  import EventEmitter from 'node:events';
@@ -21,22 +22,22 @@ const EpochPruneWatcherPenaltiesConfigKeys = [
21
22
  l1ToL2MessageSource;
22
23
  epochCache;
23
24
  txProvider;
24
- blockBuilder;
25
+ checkpointsBuilder;
25
26
  log;
26
27
  // Store bound function reference for proper listener removal
27
28
  boundHandlePruneL2Blocks;
28
29
  penalties;
29
- constructor(l2BlockSource, l1ToL2MessageSource, epochCache, txProvider, blockBuilder, penalties){
30
- super(), this.l2BlockSource = l2BlockSource, this.l1ToL2MessageSource = l1ToL2MessageSource, this.epochCache = epochCache, this.txProvider = txProvider, this.blockBuilder = blockBuilder, this.log = createLogger('epoch-prune-watcher'), this.boundHandlePruneL2Blocks = this.handlePruneL2Blocks.bind(this);
30
+ constructor(l2BlockSource, l1ToL2MessageSource, epochCache, txProvider, checkpointsBuilder, penalties){
31
+ super(), this.l2BlockSource = l2BlockSource, this.l1ToL2MessageSource = l1ToL2MessageSource, this.epochCache = epochCache, this.txProvider = txProvider, this.checkpointsBuilder = checkpointsBuilder, this.log = createLogger('epoch-prune-watcher'), this.boundHandlePruneL2Blocks = this.handlePruneL2Blocks.bind(this);
31
32
  this.penalties = pick(penalties, ...EpochPruneWatcherPenaltiesConfigKeys);
32
33
  this.log.verbose(`EpochPruneWatcher initialized with penalties: valid epoch pruned=${penalties.slashPrunePenalty} data withholding=${penalties.slashDataWithholdingPenalty}`);
33
34
  }
34
35
  start() {
35
- this.l2BlockSource.on(L2BlockSourceEvents.L2PruneDetected, this.boundHandlePruneL2Blocks);
36
+ this.l2BlockSource.events.on(L2BlockSourceEvents.L2PruneUnproven, this.boundHandlePruneL2Blocks);
36
37
  return Promise.resolve();
37
38
  }
38
39
  stop() {
39
- this.l2BlockSource.removeListener(L2BlockSourceEvents.L2PruneDetected, this.boundHandlePruneL2Blocks);
40
+ this.l2BlockSource.events.removeListener(L2BlockSourceEvents.L2PruneUnproven, this.boundHandlePruneL2Blocks);
40
41
  return Promise.resolve();
41
42
  }
42
43
  updateConfig(config) {
@@ -66,7 +67,7 @@ const EpochPruneWatcherPenaltiesConfigKeys = [
66
67
  this.log.info(`Detected chain prune. Validating epoch ${epochNumber} with blocks ${epochBlocks[0]?.number} to ${epochBlocks[epochBlocks.length - 1]?.number}.`, {
67
68
  blocks: epochBlocks.map((b)=>b.toBlockInfo())
68
69
  });
69
- await this.validateBlocks(epochBlocks);
70
+ await this.validateBlocks(epochBlocks, epochNumber);
70
71
  this.log.info(`Pruned epoch ${epochNumber} was valid. Want to slash committee for not having it proven.`);
71
72
  await this.emitSlashForEpoch(OffenseType.VALID_EPOCH_PRUNED, epochNumber);
72
73
  } catch (error) {
@@ -80,20 +81,58 @@ const EpochPruneWatcherPenaltiesConfigKeys = [
80
81
  }
81
82
  }
82
83
  }
83
- async validateBlocks(blocks) {
84
+ async validateBlocks(blocks, epochNumber) {
84
85
  if (blocks.length === 0) {
85
86
  return;
86
87
  }
87
- const fork = await this.blockBuilder.getFork(BlockNumber(blocks[0].header.globalVariables.blockNumber - 1));
88
+ // Sort blocks by block number and group by checkpoint
89
+ const sortedBlocks = [
90
+ ...blocks
91
+ ].sort((a, b)=>a.number - b.number);
92
+ const blocksByCheckpoint = chunkBy(sortedBlocks, (b)=>b.checkpointNumber);
93
+ // Get prior checkpoints in the epoch (in case this was a partial prune) to extract the out hashes
94
+ const priorCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsForEpoch(epochNumber)).filter((c)=>c.number < sortedBlocks[0].checkpointNumber).map((c)=>c.getCheckpointOutHash());
95
+ let previousCheckpointOutHashes = [
96
+ ...priorCheckpointOutHashes
97
+ ];
98
+ const fork = await this.checkpointsBuilder.getFork(BlockNumber(sortedBlocks[0].header.globalVariables.blockNumber - 1));
88
99
  try {
89
- for (const block of blocks){
90
- await this.validateBlock(block, fork);
100
+ for (const checkpointBlocks of blocksByCheckpoint){
101
+ await this.validateCheckpoint(checkpointBlocks, previousCheckpointOutHashes, fork);
102
+ // Compute checkpoint out hash from all blocks in this checkpoint
103
+ const checkpointOutHash = computeCheckpointOutHash(checkpointBlocks.map((b)=>b.body.txEffects.map((tx)=>tx.l2ToL1Msgs)));
104
+ previousCheckpointOutHashes = [
105
+ ...previousCheckpointOutHashes,
106
+ checkpointOutHash
107
+ ];
91
108
  }
92
109
  } finally{
93
110
  await fork.close();
94
111
  }
95
112
  }
96
- async validateBlock(blockFromL1, fork) {
113
+ async validateCheckpoint(checkpointBlocks, previousCheckpointOutHashes, fork) {
114
+ const checkpointNumber = checkpointBlocks[0].checkpointNumber;
115
+ this.log.debug(`Validating pruned checkpoint ${checkpointNumber} with ${checkpointBlocks.length} blocks`);
116
+ // Get L1ToL2Messages once for the entire checkpoint
117
+ const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
118
+ // Build checkpoint constants from first block's global variables
119
+ const gv = checkpointBlocks[0].header.globalVariables;
120
+ const constants = {
121
+ chainId: gv.chainId,
122
+ version: gv.version,
123
+ slotNumber: gv.slotNumber,
124
+ coinbase: gv.coinbase,
125
+ feeRecipient: gv.feeRecipient,
126
+ gasFees: gv.gasFees
127
+ };
128
+ // Start checkpoint builder once for all blocks in this checkpoint
129
+ const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(checkpointNumber, constants, l1ToL2Messages, previousCheckpointOutHashes, fork, this.log.getBindings());
130
+ // Validate all blocks in the checkpoint sequentially
131
+ for (const block of checkpointBlocks){
132
+ await this.validateBlockInCheckpoint(block, checkpointBuilder);
133
+ }
134
+ }
135
+ async validateBlockInCheckpoint(blockFromL1, checkpointBuilder) {
97
136
  this.log.debug(`Validating pruned block ${blockFromL1.header.globalVariables.blockNumber}`);
98
137
  const txHashes = blockFromL1.body.txEffects.map((txEffect)=>txEffect.txHash);
99
138
  // We load txs from the mempool directly, since the TxCollector running in the background has already been
@@ -103,9 +142,8 @@ const EpochPruneWatcherPenaltiesConfigKeys = [
103
142
  if (missingTxs && missingTxs.length > 0) {
104
143
  throw new TransactionsNotAvailableError(missingTxs);
105
144
  }
106
- const checkpointNumber = CheckpointNumber.fromBlockNumber(blockFromL1.number);
107
- const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
108
- const { block, failedTxs, numTxs } = await this.blockBuilder.buildBlock(txs, l1ToL2Messages, blockFromL1.header.globalVariables, {}, fork);
145
+ const gv = blockFromL1.header.globalVariables;
146
+ const { block, failedTxs, numTxs } = await checkpointBuilder.buildBlock(txs, gv.blockNumber, gv.timestamp, {});
109
147
  if (numTxs !== txs.length) {
110
148
  // This should be detected by state mismatch, but this makes it easier to debug.
111
149
  throw new ValidatorError(`Built block with ${numTxs} txs, expected ${txs.length}`);
package/package.json CHANGED
@@ -1,20 +1,22 @@
1
1
  {
2
2
  "name": "@aztec/slasher",
3
- "version": "0.0.1-commit.7d4e6cd",
3
+ "version": "0.0.1-commit.858058eac",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/index.js",
7
7
  "./config": "./dest/config.js"
8
8
  },
9
9
  "inherits": [
10
- "../package.common.json"
10
+ "../package.common.json",
11
+ "./package.local.json"
11
12
  ],
12
13
  "scripts": {
13
14
  "build": "yarn clean && ../scripts/tsc.sh",
14
15
  "build:dev": "../scripts/tsc.sh --watch",
15
16
  "clean": "rm -rf ./dest .tsbuildinfo",
16
17
  "bb": "node --no-warnings ./dest/bb/index.js",
17
- "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}"
18
+ "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}",
19
+ "generate": "./scripts/generate.sh"
18
20
  },
19
21
  "jest": {
20
22
  "moduleNameMapper": {
@@ -54,25 +56,25 @@
54
56
  ]
55
57
  },
56
58
  "dependencies": {
57
- "@aztec/epoch-cache": "0.0.1-commit.7d4e6cd",
58
- "@aztec/ethereum": "0.0.1-commit.7d4e6cd",
59
- "@aztec/foundation": "0.0.1-commit.7d4e6cd",
60
- "@aztec/kv-store": "0.0.1-commit.7d4e6cd",
61
- "@aztec/l1-artifacts": "0.0.1-commit.7d4e6cd",
62
- "@aztec/stdlib": "0.0.1-commit.7d4e6cd",
63
- "@aztec/telemetry-client": "0.0.1-commit.7d4e6cd",
59
+ "@aztec/epoch-cache": "0.0.1-commit.858058eac",
60
+ "@aztec/ethereum": "0.0.1-commit.858058eac",
61
+ "@aztec/foundation": "0.0.1-commit.858058eac",
62
+ "@aztec/kv-store": "0.0.1-commit.858058eac",
63
+ "@aztec/l1-artifacts": "0.0.1-commit.858058eac",
64
+ "@aztec/stdlib": "0.0.1-commit.858058eac",
65
+ "@aztec/telemetry-client": "0.0.1-commit.858058eac",
64
66
  "source-map-support": "^0.5.21",
65
67
  "tslib": "^2.4.0",
66
68
  "viem": "npm:@aztec/viem@2.38.2",
67
69
  "zod": "^3.23.8"
68
70
  },
69
71
  "devDependencies": {
70
- "@aztec/aztec.js": "0.0.1-commit.7d4e6cd",
72
+ "@aztec/aztec.js": "0.0.1-commit.858058eac",
71
73
  "@jest/globals": "^30.0.0",
72
74
  "@types/jest": "^30.0.0",
73
75
  "@types/node": "^22.15.17",
74
76
  "@types/source-map-support": "^0.5.10",
75
- "@typescript/native-preview": "7.0.0-dev.20251126.1",
77
+ "@typescript/native-preview": "7.0.0-dev.20260113.1",
76
78
  "jest": "^30.0.0",
77
79
  "jest-mock-extended": "^4.0.0",
78
80
  "ts-node": "^10.9.1",
package/src/config.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { DefaultL1ContractsConfig } from '@aztec/ethereum/config';
2
1
  import type { ConfigMappingsType } from '@aztec/foundation/config';
3
2
  import {
4
3
  bigintConfigHelper,
@@ -9,27 +8,31 @@ import {
9
8
  import { EthAddress } from '@aztec/foundation/eth-address';
10
9
  import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
11
10
 
11
+ import { slasherDefaultEnv } from './generated/slasher-defaults.js';
12
+
12
13
  export type { SlasherConfig };
13
14
 
14
15
  export const DefaultSlasherConfig: SlasherConfig = {
15
16
  slashOverridePayload: undefined,
16
- slashMinPenaltyPercentage: 0.5, // 50% of penalty
17
- slashMaxPenaltyPercentage: 2.0, //2x of penalty
17
+ slashMinPenaltyPercentage: slasherDefaultEnv.SLASH_MIN_PENALTY_PERCENTAGE,
18
+ slashMaxPenaltyPercentage: slasherDefaultEnv.SLASH_MAX_PENALTY_PERCENTAGE,
18
19
  slashValidatorsAlways: [], // Empty by default
19
20
  slashValidatorsNever: [], // Empty by default
20
- slashPrunePenalty: DefaultL1ContractsConfig.slashAmountSmall,
21
- slashDataWithholdingPenalty: DefaultL1ContractsConfig.slashAmountSmall,
22
- slashInactivityTargetPercentage: 0.9,
23
- slashInactivityConsecutiveEpochThreshold: 1, // Default to 1 for backward compatibility
24
- slashBroadcastedInvalidBlockPenalty: DefaultL1ContractsConfig.slashAmountSmall,
25
- slashInactivityPenalty: DefaultL1ContractsConfig.slashAmountSmall,
26
- slashProposeInvalidAttestationsPenalty: DefaultL1ContractsConfig.slashAmountSmall,
27
- slashAttestDescendantOfInvalidPenalty: DefaultL1ContractsConfig.slashAmountSmall,
28
- slashUnknownPenalty: DefaultL1ContractsConfig.slashAmountSmall,
29
- slashOffenseExpirationRounds: 4,
30
- slashMaxPayloadSize: 50,
31
- slashGracePeriodL2Slots: 0,
32
- slashExecuteRoundsLookBack: 4,
21
+ slashPrunePenalty: BigInt(slasherDefaultEnv.SLASH_PRUNE_PENALTY),
22
+ slashDataWithholdingPenalty: BigInt(slasherDefaultEnv.SLASH_DATA_WITHHOLDING_PENALTY),
23
+ slashInactivityTargetPercentage: slasherDefaultEnv.SLASH_INACTIVITY_TARGET_PERCENTAGE,
24
+ slashInactivityConsecutiveEpochThreshold: slasherDefaultEnv.SLASH_INACTIVITY_CONSECUTIVE_EPOCH_THRESHOLD,
25
+ slashBroadcastedInvalidBlockPenalty: BigInt(slasherDefaultEnv.SLASH_INVALID_BLOCK_PENALTY),
26
+ slashDuplicateProposalPenalty: BigInt(slasherDefaultEnv.SLASH_DUPLICATE_PROPOSAL_PENALTY),
27
+ slashDuplicateAttestationPenalty: BigInt(slasherDefaultEnv.SLASH_DUPLICATE_ATTESTATION_PENALTY),
28
+ slashInactivityPenalty: BigInt(slasherDefaultEnv.SLASH_INACTIVITY_PENALTY),
29
+ slashProposeInvalidAttestationsPenalty: BigInt(slasherDefaultEnv.SLASH_PROPOSE_INVALID_ATTESTATIONS_PENALTY),
30
+ slashAttestDescendantOfInvalidPenalty: BigInt(slasherDefaultEnv.SLASH_ATTEST_DESCENDANT_OF_INVALID_PENALTY),
31
+ slashUnknownPenalty: BigInt(slasherDefaultEnv.SLASH_UNKNOWN_PENALTY),
32
+ slashOffenseExpirationRounds: slasherDefaultEnv.SLASH_OFFENSE_EXPIRATION_ROUNDS,
33
+ slashMaxPayloadSize: slasherDefaultEnv.SLASH_MAX_PAYLOAD_SIZE,
34
+ slashGracePeriodL2Slots: slasherDefaultEnv.SLASH_GRACE_PERIOD_L2_SLOTS,
35
+ slashExecuteRoundsLookBack: slasherDefaultEnv.SLASH_EXECUTE_ROUNDS_LOOK_BACK,
33
36
  slashSelfAllowed: false,
34
37
  };
35
38
 
@@ -87,6 +90,17 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
87
90
  description: 'Penalty amount for slashing a validator for an invalid block proposed via p2p.',
88
91
  ...bigintConfigHelper(DefaultSlasherConfig.slashBroadcastedInvalidBlockPenalty),
89
92
  },
93
+ slashDuplicateProposalPenalty: {
94
+ env: 'SLASH_DUPLICATE_PROPOSAL_PENALTY',
95
+ description: 'Penalty amount for slashing a validator for sending duplicate proposals.',
96
+ ...bigintConfigHelper(DefaultSlasherConfig.slashDuplicateProposalPenalty),
97
+ },
98
+ slashDuplicateAttestationPenalty: {
99
+ env: 'SLASH_DUPLICATE_ATTESTATION_PENALTY',
100
+ description:
101
+ 'Penalty amount for slashing a validator for signing attestations for different proposals at the same slot.',
102
+ ...bigintConfigHelper(DefaultSlasherConfig.slashDuplicateAttestationPenalty),
103
+ },
90
104
  slashInactivityTargetPercentage: {
91
105
  env: 'SLASH_INACTIVITY_TARGET_PERCENTAGE',
92
106
  description:
@@ -4,7 +4,6 @@ import { SlotNumber } from '@aztec/foundation/branded-types';
4
4
  import { compactArray, filterAsync, maxBy, pick } from '@aztec/foundation/collection';
5
5
  import { EthAddress } from '@aztec/foundation/eth-address';
6
6
  import { createLogger } from '@aztec/foundation/log';
7
- import { sleep } from '@aztec/foundation/sleep';
8
7
  import type { DateProvider } from '@aztec/foundation/timer';
9
8
  import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
10
9
  import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
@@ -196,13 +195,6 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
196
195
  this.roundMonitor.stop();
197
196
  await this.offensesCollector.stop();
198
197
 
199
- // Viem calls eth_uninstallFilter under the hood when uninstalling event watchers, but these calls are not awaited,
200
- // meaning that any error that happens during the uninstallation will not be caught. This causes errors during jest teardowns,
201
- // where we stop anvil after all other processes are stopped, so sometimes the eth_uninstallFilter call fails because anvil
202
- // is already stopped. We add a sleep here to give the uninstallation some time to complete, but the proper fix is for
203
- // viem to await the eth_uninstallFilter calls, or to catch any errors that happen during the uninstallation.
204
- // See https://github.com/wevm/viem/issues/3714.
205
- await sleep(2000);
206
198
  this.log.info('Empire Slasher client stopped');
207
199
  }
208
200
 
@@ -31,7 +31,7 @@ export async function createSlasherFacade(
31
31
  throw new Error('Cannot initialize SlasherClient without a Rollup address');
32
32
  }
33
33
 
34
- const kvStore = await createStore('slasher', SCHEMA_VERSION, config, createLogger('slasher:lmdb'));
34
+ const kvStore = await createStore('slasher', SCHEMA_VERSION, config, logger.getBindings());
35
35
  const rollup = new RollupContract(l1Client, l1Contracts.rollupAddress);
36
36
 
37
37
  const slashValidatorsNever = config.slashSelfAllowed
@@ -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: 50,
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: 10000000000000000000,
19
+ SLASH_DUPLICATE_ATTESTATION_PENALTY: 10000000000000000000,
20
+ SLASH_UNKNOWN_PENALTY: 10000000000000000000,
21
+ SLASH_INVALID_BLOCK_PENALTY: 10000000000000000000,
22
+ SLASH_GRACE_PERIOD_L2_SLOTS: 0,
23
+ } as const;
@@ -85,7 +85,11 @@ export class SlashOffensesCollector {
85
85
  }
86
86
  }
87
87
 
88
- this.log.info(`Adding pending offense for validator ${arg.validator}`, pendingOffense);
88
+ this.log.info(`Adding pending offense for validator ${arg.validator}`, {
89
+ ...pendingOffense,
90
+ epochOrSlot: pendingOffense.epochOrSlot.toString(),
91
+ amount: pendingOffense.amount.toString(),
92
+ });
89
93
  await this.offensesStore.addPendingOffense(pendingOffense);
90
94
  }
91
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';
@@ -138,8 +137,6 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
138
137
  this.roundMonitor.stop();
139
138
  await this.offensesCollector.stop();
140
139
 
141
- // Sleeping to sidestep viem issue with unwatching events
142
- await sleep(2000);
143
140
  this.log.info('Tally Slasher client stopped');
144
141
  }
145
142
 
@@ -281,8 +278,12 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
281
278
  return undefined;
282
279
  }
283
280
 
281
+ const slashActionsWithAmounts = slashActions.map(action => ({
282
+ validator: action.validator.toString(),
283
+ slashAmount: action.slashAmount.toString(),
284
+ }));
284
285
  this.log.info(`Round ${executableRound} is ready to execute with ${slashActions.length} slashes`, {
285
- slashActions,
286
+ slashActions: slashActionsWithAmounts,
286
287
  payloadAddress: payload.address.toString(),
287
288
  ...logData,
288
289
  });
@@ -348,11 +349,15 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
348
349
  return undefined;
349
350
  }
350
351
 
352
+ const offensesToSlashLog = offensesToSlash.map(offense => ({
353
+ ...offense,
354
+ amount: offense.amount.toString(),
355
+ }));
351
356
  this.log.info(`Voting to slash ${offensesToSlash.length} offenses`, {
352
357
  slotNumber,
353
358
  currentRound,
354
359
  slashedRound,
355
- offensesToSlash,
360
+ offensesToSlash: offensesToSlashLog,
356
361
  });
357
362
 
358
363
  const committees = await this.collectCommitteesActiveDuringRound(slashedRound);
@@ -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,81 @@ 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.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
+ );
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
+ coinbase: gv.coinbase,
176
+ feeRecipient: gv.feeRecipient,
177
+ gasFees: gv.gasFees,
178
+ };
179
+
180
+ // Start checkpoint builder once for all blocks in this checkpoint
181
+ const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
182
+ checkpointNumber,
183
+ constants,
184
+ l1ToL2Messages,
185
+ previousCheckpointOutHashes,
186
+ fork,
187
+ this.log.getBindings(),
188
+ );
189
+
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> {
137
200
  this.log.debug(`Validating pruned block ${blockFromL1.header.globalVariables.blockNumber}`);
138
201
  const txHashes = blockFromL1.body.txEffects.map(txEffect => txEffect.txHash);
139
202
  // We load txs from the mempool directly, since the TxCollector running in the background has already been
@@ -145,15 +208,9 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
145
208
  throw new TransactionsNotAvailableError(missingTxs);
146
209
  }
147
210
 
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
- );
211
+ const gv = blockFromL1.header.globalVariables;
212
+ const { block, failedTxs, numTxs } = await checkpointBuilder.buildBlock(txs, gv.blockNumber, gv.timestamp, {});
213
+
157
214
  if (numTxs !== txs.length) {
158
215
  // This should be detected by state mismatch, but this makes it easier to debug.
159
216
  throw new ValidatorError(`Built block with ${numTxs} txs, expected ${txs.length}`);