@aztec/slasher 0.0.1-commit.f504929 → 0.0.1-commit.f5a9928

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 (99) hide show
  1. package/README.md +83 -79
  2. package/dest/config.d.ts +1 -1
  3. package/dest/config.d.ts.map +1 -1
  4. package/dest/config.js +28 -28
  5. package/dest/factory/create_facade.d.ts +7 -4
  6. package/dest/factory/create_facade.d.ts.map +1 -1
  7. package/dest/factory/create_facade.js +5 -2
  8. package/dest/factory/create_implementation.d.ts +6 -7
  9. package/dest/factory/create_implementation.d.ts.map +1 -1
  10. package/dest/factory/create_implementation.js +7 -59
  11. package/dest/factory/get_settings.d.ts +4 -4
  12. package/dest/factory/get_settings.d.ts.map +1 -1
  13. package/dest/factory/get_settings.js +3 -3
  14. package/dest/factory/index.d.ts +2 -2
  15. package/dest/factory/index.d.ts.map +1 -1
  16. package/dest/factory/index.js +1 -1
  17. package/dest/generated/slasher-defaults.d.ts +4 -4
  18. package/dest/generated/slasher-defaults.js +4 -4
  19. package/dest/index.d.ts +6 -4
  20. package/dest/index.d.ts.map +1 -1
  21. package/dest/index.js +5 -3
  22. package/dest/metrics.d.ts +23 -0
  23. package/dest/metrics.d.ts.map +1 -0
  24. package/dest/metrics.js +38 -0
  25. package/dest/null_slasher_client.d.ts +3 -4
  26. package/dest/null_slasher_client.d.ts.map +1 -1
  27. package/dest/null_slasher_client.js +1 -4
  28. package/dest/own_validator_slash_monitor.d.ts +66 -0
  29. package/dest/own_validator_slash_monitor.d.ts.map +1 -0
  30. package/dest/own_validator_slash_monitor.js +172 -0
  31. package/dest/slash_offenses_collector.d.ts +6 -8
  32. package/dest/slash_offenses_collector.d.ts.map +1 -1
  33. package/dest/slash_offenses_collector.js +48 -28
  34. package/dest/slasher_client.d.ts +118 -0
  35. package/dest/slasher_client.d.ts.map +1 -0
  36. package/dest/{tally_slasher_client.js → slasher_client.js} +62 -49
  37. package/dest/slasher_client_facade.d.ts +7 -8
  38. package/dest/slasher_client_facade.d.ts.map +1 -1
  39. package/dest/slasher_client_facade.js +6 -9
  40. package/dest/slasher_client_interface.d.ts +7 -21
  41. package/dest/slasher_client_interface.d.ts.map +1 -1
  42. package/dest/slasher_client_interface.js +1 -4
  43. package/dest/stores/offenses_store.d.ts +12 -12
  44. package/dest/stores/offenses_store.d.ts.map +1 -1
  45. package/dest/stores/offenses_store.js +61 -38
  46. package/dest/watcher.d.ts +8 -1
  47. package/dest/watcher.d.ts.map +1 -1
  48. package/dest/watcher.js +1 -0
  49. package/dest/watchers/attestations_block_watcher.d.ts +26 -13
  50. package/dest/watchers/attestations_block_watcher.d.ts.map +1 -1
  51. package/dest/watchers/attestations_block_watcher.js +76 -61
  52. package/dest/watchers/attested_invalid_proposal_watcher.d.ts +42 -0
  53. package/dest/watchers/attested_invalid_proposal_watcher.d.ts.map +1 -0
  54. package/dest/watchers/attested_invalid_proposal_watcher.js +117 -0
  55. package/dest/watchers/broadcasted_invalid_checkpoint_proposal_watcher.d.ts +38 -0
  56. package/dest/watchers/broadcasted_invalid_checkpoint_proposal_watcher.d.ts.map +1 -0
  57. package/dest/watchers/broadcasted_invalid_checkpoint_proposal_watcher.js +138 -0
  58. package/dest/watchers/checkpoint_equivocation_watcher.d.ts +30 -0
  59. package/dest/watchers/checkpoint_equivocation_watcher.d.ts.map +1 -0
  60. package/dest/watchers/checkpoint_equivocation_watcher.js +69 -0
  61. package/dest/watchers/data_withholding_watcher.d.ts +69 -0
  62. package/dest/watchers/data_withholding_watcher.d.ts.map +1 -0
  63. package/dest/watchers/data_withholding_watcher.js +223 -0
  64. package/package.json +10 -10
  65. package/src/config.ts +33 -28
  66. package/src/factory/create_facade.ts +7 -4
  67. package/src/factory/create_implementation.ts +17 -118
  68. package/src/factory/get_settings.ts +8 -8
  69. package/src/factory/index.ts +1 -1
  70. package/src/generated/slasher-defaults.ts +4 -4
  71. package/src/index.ts +5 -3
  72. package/src/metrics.ts +59 -0
  73. package/src/null_slasher_client.ts +2 -6
  74. package/src/own_validator_slash_monitor.ts +195 -0
  75. package/src/slash_offenses_collector.ts +62 -29
  76. package/src/{tally_slasher_client.ts → slasher_client.ts} +79 -55
  77. package/src/slasher_client_facade.ts +7 -11
  78. package/src/slasher_client_interface.ts +6 -21
  79. package/src/stores/offenses_store.ts +73 -47
  80. package/src/watcher.ts +8 -0
  81. package/src/watchers/attestations_block_watcher.ts +88 -82
  82. package/src/watchers/attested_invalid_proposal_watcher.ts +168 -0
  83. package/src/watchers/broadcasted_invalid_checkpoint_proposal_watcher.ts +192 -0
  84. package/src/watchers/checkpoint_equivocation_watcher.ts +96 -0
  85. package/src/watchers/data_withholding_watcher.ts +257 -0
  86. package/dest/empire_slasher_client.d.ts +0 -190
  87. package/dest/empire_slasher_client.d.ts.map +0 -1
  88. package/dest/empire_slasher_client.js +0 -564
  89. package/dest/stores/payloads_store.d.ts +0 -29
  90. package/dest/stores/payloads_store.d.ts.map +0 -1
  91. package/dest/stores/payloads_store.js +0 -128
  92. package/dest/tally_slasher_client.d.ts +0 -125
  93. package/dest/tally_slasher_client.d.ts.map +0 -1
  94. package/dest/watchers/epoch_prune_watcher.d.ts +0 -39
  95. package/dest/watchers/epoch_prune_watcher.d.ts.map +0 -1
  96. package/dest/watchers/epoch_prune_watcher.js +0 -176
  97. package/src/empire_slasher_client.ts +0 -649
  98. package/src/stores/payloads_store.ts +0 -149
  99. package/src/watchers/epoch_prune_watcher.ts +0 -253
@@ -0,0 +1,223 @@
1
+ import { CheckpointProposalHash, SlotNumber } from '@aztec/foundation/branded-types';
2
+ import { compactArray, merge, pick } from '@aztec/foundation/collection';
3
+ import { createLogger } from '@aztec/foundation/log';
4
+ import { RunningPromise } from '@aztec/foundation/promise';
5
+ import { getAttestationInfoFromPublishedCheckpoint } from '@aztec/stdlib/block';
6
+ import { ConsensusPayload } from '@aztec/stdlib/p2p';
7
+ import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';
8
+ import EventEmitter from 'node:events';
9
+ import { WANT_TO_SLASH_EVENT } from '../watcher.js';
10
+ const DataWithholdingWatcherConfigKeys = [
11
+ 'slashDataWithholdingPenalty',
12
+ 'slashDataWithholdingToleranceSlots'
13
+ ];
14
+ /**
15
+ * Detects data-withholding offenses by probing the local mempool for the txs in published
16
+ * checkpoints once they are old enough that an honest node should have collected them.
17
+ *
18
+ * Per AZIP-7: once `slashDataWithholdingToleranceSlots` full slots have elapsed after the
19
+ * checkpoint's slot — i.e. at `slotStart(checkpoint.slot + slashDataWithholdingToleranceSlots
20
+ * + 1)` — if any tx from the checkpoint's blocks is still missing locally, the checkpoint's
21
+ * attesters are considered at fault for not making the data available, and we emit a slash
22
+ * for them.
23
+ *
24
+ * The watcher ticks at quarter-eth-slot cadence (matching the Sentinel template). On boot it
25
+ * floors processing at the current slot — restart-time gaps are accepted and not back-filled,
26
+ * matching the Sentinel approach.
27
+ *
28
+ * Since the evidence for an offense is the absence of txs in the local pool, the probe is gated on
29
+ * p2p connectivity: while there are no connected peers, slots are skipped instead of probed, since
30
+ * missing txs then say nothing about the attesters. Nodes running with p2p disabled do not run this
31
+ * watcher at all.
32
+ */ export class DataWithholdingWatcher extends EventEmitter {
33
+ epochCache;
34
+ l2BlockSource;
35
+ txProvider;
36
+ p2p;
37
+ reexecutionTracker;
38
+ signatureContext;
39
+ log;
40
+ runningPromise;
41
+ initialSlot;
42
+ lastCheckedSlot;
43
+ config;
44
+ gossipDegraded;
45
+ constructor(epochCache, l2BlockSource, txProvider, p2p, reexecutionTracker, signatureContext, config, log = createLogger('data-withholding-watcher')){
46
+ super(), this.epochCache = epochCache, this.l2BlockSource = l2BlockSource, this.txProvider = txProvider, this.p2p = p2p, this.reexecutionTracker = reexecutionTracker, this.signatureContext = signatureContext, this.log = log, this.gossipDegraded = false;
47
+ this.config = pick(config, ...DataWithholdingWatcherConfigKeys);
48
+ const interval = epochCache.getL1Constants().ethereumSlotDuration * 1000 / 4;
49
+ this.runningPromise = new RunningPromise(this.work.bind(this), log, interval);
50
+ this.log.verbose(`DataWithholdingWatcher initialized`, this.config);
51
+ }
52
+ async start() {
53
+ // Floor processing at the archiver's synced slot rather than the wallclock — restart-time
54
+ // gaps before the archiver catches up are accepted and not back-filled. Falls back to the
55
+ // wallclock if the archiver isn't ready yet (cold start).
56
+ const syncedSlot = await this.l2BlockSource.getSyncedL2SlotNumber();
57
+ this.initialSlot = syncedSlot ?? this.epochCache.getSlotNow();
58
+ this.log.info(`Starting data-withholding watcher with initial slot ${this.initialSlot}`);
59
+ this.runningPromise.start();
60
+ }
61
+ stop() {
62
+ return this.runningPromise.stop();
63
+ }
64
+ updateConfig(config) {
65
+ this.config = merge(this.config, pick(config, ...DataWithholdingWatcherConfigKeys));
66
+ this.log.verbose('DataWithholdingWatcher config updated', this.config);
67
+ }
68
+ /**
69
+ * Runs every tick. Walks newly-eligible slots and probes their checkpoints for data
70
+ * availability; emits a DATA_WITHHOLDING slash for any checkpoint whose txs are missing.
71
+ */ async work() {
72
+ if (this.initialSlot === undefined) {
73
+ return;
74
+ }
75
+ // tolerance is the number of full slots that must elapse after the checkpoint's slot
76
+ // before we declare its data missing. For checkpoint slot S, we therefore process S
77
+ // only once we are in slot `S + tolerance + 1` or later. Drive this off the archiver's
78
+ // synced slot rather than the wallclock so we don't make claims about slots we haven't
79
+ // fully ingested yet (archiver may lag behind L1).
80
+ const tolerance = this.config.slashDataWithholdingToleranceSlots;
81
+ const currentSlot = await this.l2BlockSource.getSyncedL2SlotNumber() ?? this.epochCache.getSlotNow();
82
+ if (currentSlot <= tolerance) {
83
+ return;
84
+ }
85
+ const targetSlot = SlotNumber(currentSlot - tolerance - 1);
86
+ if (targetSlot <= this.initialSlot) {
87
+ return;
88
+ }
89
+ // Missing txs are only evidence of withholding if we could have received them. Slots probed
90
+ // while gossip is down are skipped for good rather than deferred: once peers return, txs from
91
+ // those checkpoints may already have been evicted from the pool as mined, so a late probe would
92
+ // still report them missing. An unknown must not turn into an offense. Note this deliberately
93
+ // ignores whether p2p is enabled: a node with no p2p stack sees even less than a peerless one.
94
+ const connectivity = await this.p2p.getP2PConnectivity();
95
+ if (connectivity.connectedPeers === 0) {
96
+ if (!this.gossipDegraded) {
97
+ this.gossipDegraded = true;
98
+ this.log.warn(`Skipping data-withholding checks while no peers are connected`, {
99
+ targetSlot,
100
+ lastCheckedSlot: this.lastCheckedSlot
101
+ });
102
+ }
103
+ this.lastCheckedSlot = targetSlot;
104
+ return;
105
+ }
106
+ if (this.gossipDegraded) {
107
+ this.gossipDegraded = false;
108
+ this.log.info(`Resuming data-withholding checks now that peers are connected`, {
109
+ targetSlot,
110
+ connectedPeers: connectivity.connectedPeers
111
+ });
112
+ }
113
+ const startSlot = this.lastCheckedSlot === undefined ? this.initialSlot : this.lastCheckedSlot;
114
+ for(let slot = SlotNumber(startSlot + 1); slot <= targetSlot; slot = SlotNumber(slot + 1)){
115
+ try {
116
+ await this.processSlot(slot);
117
+ } catch (err) {
118
+ this.log.error(`Error processing slot ${slot} for data-withholding check`, err, {
119
+ slot
120
+ });
121
+ }
122
+ this.lastCheckedSlot = slot;
123
+ }
124
+ }
125
+ /** Probes the checkpoint at the given slot, if any, and emits a slash on missing txs. */ async processSlot(slot) {
126
+ const published = await this.l2BlockSource.getCheckpoint({
127
+ slot
128
+ });
129
+ if (!published) {
130
+ this.log.trace(`No published checkpoint at slot ${slot}`, {
131
+ slot
132
+ });
133
+ return;
134
+ }
135
+ const checkpointNumber = published.checkpoint.number;
136
+ // Per-block tx-collection records (true | false | undefined) for every block in this
137
+ // published checkpoint. Captured by the validator's proposal handler at the moment of
138
+ // tx collection (i.e. by the *re-execution* deadline). Used as a positive short-circuit
139
+ // only: a `true` for every block means we know the data was available locally, so this
140
+ // checkpoint cannot be a data-withholding offense. A `false` does *not* trigger a slash
141
+ // on its own — the re-execution deadline is much earlier than the data-withholding
142
+ // tolerance window, so missing txs at that earlier deadline may still arrive in time.
143
+ // Anything other than all-true falls through to the mempool probe, which respects the
144
+ // tolerance window.
145
+ const collectionRecords = published.checkpoint.blocks.map((block, idx)=>this.reexecutionTracker.getTxsCollectedRecord(block.header.getSlot(), idx));
146
+ if (collectionRecords.every((r)=>r === true)) {
147
+ this.log.trace(`All blocks for checkpoint at slot ${slot} were collected locally; skipping`, {
148
+ slot,
149
+ checkpointNumber
150
+ });
151
+ return;
152
+ }
153
+ const txHashes = published.checkpoint.blocks.flatMap((block)=>block.body.txEffects.map((txEffect)=>txEffect.txHash));
154
+ if (txHashes.length === 0) {
155
+ this.log.trace(`Checkpoint at slot ${slot} has no txs`, {
156
+ slot
157
+ });
158
+ return;
159
+ }
160
+ const availability = await this.txProvider.hasTxs(txHashes);
161
+ const missingTxs = txHashes.filter((_, i)=>!availability[i]);
162
+ if (missingTxs.length === 0) {
163
+ this.log.trace(`All ${txHashes.length} txs available for checkpoint at slot ${slot}`, {
164
+ slot
165
+ });
166
+ return;
167
+ }
168
+ const attesters = await this.extractAttesters(published);
169
+ if (attesters.length === 0) {
170
+ this.log.warn(`Detected data withholding at slot ${slot} but no recoverable attesters`, {
171
+ slot,
172
+ checkpointNumber,
173
+ missingTxs: missingTxs.map((h)=>h.toString()),
174
+ records: collectionRecords
175
+ });
176
+ return;
177
+ }
178
+ this.log.info(`Detected data withholding offense at slot ${slot}`, {
179
+ slot,
180
+ checkpointNumber,
181
+ amount: this.config.slashDataWithholdingPenalty,
182
+ offenseType: getOffenseTypeName(OffenseType.DATA_WITHHOLDING),
183
+ missingTxs: missingTxs.map((h)=>h.toString()),
184
+ records: collectionRecords,
185
+ attesters: attesters.map((a)=>a.toString())
186
+ });
187
+ const args = attesters.map((validator)=>({
188
+ validator,
189
+ amount: this.config.slashDataWithholdingPenalty,
190
+ offenseType: OffenseType.DATA_WITHHOLDING,
191
+ epochOrSlot: BigInt(slot)
192
+ }));
193
+ this.emit(WANT_TO_SLASH_EVENT, args);
194
+ }
195
+ /**
196
+ * Returns the union of:
197
+ * 1. attesters whose signatures landed in the published checkpoint on L1, and
198
+ * 2. attesters we observed signing the same proposal on p2p (the proposer publishes as
199
+ * soon as it has hit committee quorum, so honest peer attestations that arrive after
200
+ * that point are dropped — but they still vouched for the data and
201
+ * should be slashed for withholding it).
202
+ *
203
+ *
204
+ * Exposed as protected so tests can substitute a deterministic recovery without having
205
+ * to construct real secp256k1 signatures.
206
+ */ async extractAttesters(published) {
207
+ const fromL1 = getAttestationInfoFromPublishedCheckpoint(published, this.signatureContext).filter((info)=>info.status === 'recovered-from-signature').map((info)=>info.address);
208
+ const slot = published.checkpoint.header.slotNumber;
209
+ const proposalPayloadHash = CheckpointProposalHash.fromBuffer(ConsensusPayload.fromCheckpoint(published.checkpoint, this.signatureContext).getPayloadHash());
210
+ const fromP2p = await this.p2p.getCheckpointAttestationsForSlot(slot, proposalPayloadHash).then((attestations)=>attestations.map((a)=>a.getSender()));
211
+ // Dedupe
212
+ const all = new Map();
213
+ for (const addr of compactArray([
214
+ ...fromL1,
215
+ ...fromP2p
216
+ ])){
217
+ all.set(addr.toString(), addr);
218
+ }
219
+ return [
220
+ ...all.values()
221
+ ];
222
+ }
223
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/slasher",
3
- "version": "0.0.1-commit.f504929",
3
+ "version": "0.0.1-commit.f5a9928",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/index.js",
@@ -56,20 +56,20 @@
56
56
  ]
57
57
  },
58
58
  "dependencies": {
59
- "@aztec/epoch-cache": "0.0.1-commit.f504929",
60
- "@aztec/ethereum": "0.0.1-commit.f504929",
61
- "@aztec/foundation": "0.0.1-commit.f504929",
62
- "@aztec/kv-store": "0.0.1-commit.f504929",
63
- "@aztec/l1-artifacts": "0.0.1-commit.f504929",
64
- "@aztec/stdlib": "0.0.1-commit.f504929",
65
- "@aztec/telemetry-client": "0.0.1-commit.f504929",
59
+ "@aztec/epoch-cache": "0.0.1-commit.f5a9928",
60
+ "@aztec/ethereum": "0.0.1-commit.f5a9928",
61
+ "@aztec/foundation": "0.0.1-commit.f5a9928",
62
+ "@aztec/kv-store": "0.0.1-commit.f5a9928",
63
+ "@aztec/l1-artifacts": "0.0.1-commit.f5a9928",
64
+ "@aztec/stdlib": "0.0.1-commit.f5a9928",
65
+ "@aztec/telemetry-client": "0.0.1-commit.f5a9928",
66
66
  "source-map-support": "^0.5.21",
67
67
  "tslib": "^2.4.0",
68
68
  "viem": "npm:@aztec/viem@2.38.2",
69
- "zod": "^3.23.8"
69
+ "zod": "^4"
70
70
  },
71
71
  "devDependencies": {
72
- "@aztec/aztec.js": "0.0.1-commit.f504929",
72
+ "@aztec/aztec.js": "0.0.1-commit.f5a9928",
73
73
  "@jest/globals": "^30.0.0",
74
74
  "@types/jest": "^30.0.0",
75
75
  "@types/node": "^22.15.17",
package/src/config.ts CHANGED
@@ -14,20 +14,24 @@ export type { SlasherConfig };
14
14
 
15
15
  export const DefaultSlasherConfig: SlasherConfig = {
16
16
  slashOverridePayload: undefined,
17
- slashMinPenaltyPercentage: slasherDefaultEnv.SLASH_MIN_PENALTY_PERCENTAGE,
18
- slashMaxPenaltyPercentage: slasherDefaultEnv.SLASH_MAX_PENALTY_PERCENTAGE,
19
17
  slashValidatorsAlways: [], // Empty by default
20
18
  slashValidatorsNever: [], // Empty by default
21
- slashPrunePenalty: BigInt(slasherDefaultEnv.SLASH_PRUNE_PENALTY),
22
19
  slashDataWithholdingPenalty: BigInt(slasherDefaultEnv.SLASH_DATA_WITHHOLDING_PENALTY),
20
+ slashDataWithholdingToleranceSlots: slasherDefaultEnv.SLASH_DATA_WITHHOLDING_TOLERANCE_SLOTS,
23
21
  slashInactivityTargetPercentage: slasherDefaultEnv.SLASH_INACTIVITY_TARGET_PERCENTAGE,
24
22
  slashInactivityConsecutiveEpochThreshold: slasherDefaultEnv.SLASH_INACTIVITY_CONSECUTIVE_EPOCH_THRESHOLD,
25
23
  slashBroadcastedInvalidBlockPenalty: BigInt(slasherDefaultEnv.SLASH_INVALID_BLOCK_PENALTY),
24
+ slashBroadcastedInvalidCheckpointProposalPenalty: BigInt(slasherDefaultEnv.SLASH_INVALID_CHECKPOINT_PROPOSAL_PENALTY),
26
25
  slashDuplicateProposalPenalty: BigInt(slasherDefaultEnv.SLASH_DUPLICATE_PROPOSAL_PENALTY),
27
26
  slashDuplicateAttestationPenalty: BigInt(slasherDefaultEnv.SLASH_DUPLICATE_ATTESTATION_PENALTY),
28
27
  slashInactivityPenalty: BigInt(slasherDefaultEnv.SLASH_INACTIVITY_PENALTY),
29
28
  slashProposeInvalidAttestationsPenalty: BigInt(slasherDefaultEnv.SLASH_PROPOSE_INVALID_ATTESTATIONS_PENALTY),
30
- slashAttestDescendantOfInvalidPenalty: BigInt(slasherDefaultEnv.SLASH_ATTEST_DESCENDANT_OF_INVALID_PENALTY),
29
+ slashProposeDescendantOfCheckpointWithInvalidAttestationsPenalty: BigInt(
30
+ slasherDefaultEnv.SLASH_PROPOSE_DESCENDANT_OF_CHECKPOINT_WITH_INVALID_ATTESTATIONS_PENALTY,
31
+ ),
32
+ slashAttestInvalidCheckpointProposalPenalty: BigInt(
33
+ slasherDefaultEnv.SLASH_ATTEST_INVALID_CHECKPOINT_PROPOSAL_PENALTY,
34
+ ),
31
35
  slashUnknownPenalty: BigInt(slasherDefaultEnv.SLASH_UNKNOWN_PENALTY),
32
36
  slashOffenseExpirationRounds: slasherDefaultEnv.SLASH_OFFENSE_EXPIRATION_ROUNDS,
33
37
  slashMaxPayloadSize: slasherDefaultEnv.SLASH_MAX_PAYLOAD_SIZE,
@@ -40,19 +44,9 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
40
44
  slashOverridePayload: {
41
45
  env: 'SLASH_OVERRIDE_PAYLOAD',
42
46
  description: 'An Ethereum address for a slash payload to vote for unconditionally.',
43
- parseEnv: (val: string) => (val ? EthAddress.fromString(val) : undefined),
47
+ parseEnv: (val: string) => EthAddress.fromString(val),
44
48
  defaultValue: DefaultSlasherConfig.slashOverridePayload,
45
49
  },
46
- slashMinPenaltyPercentage: {
47
- env: 'SLASH_MIN_PENALTY_PERCENTAGE',
48
- description: 'Minimum penalty percentage for slashing offenses (0.1 is 10%).',
49
- ...floatConfigHelper(DefaultSlasherConfig.slashMinPenaltyPercentage),
50
- },
51
- slashMaxPenaltyPercentage: {
52
- env: 'SLASH_MAX_PENALTY_PERCENTAGE',
53
- description: 'Maximum penalty percentage for slashing offenses (2.0 is 200%).',
54
- ...floatConfigHelper(DefaultSlasherConfig.slashMaxPenaltyPercentage),
55
- },
56
50
  slashValidatorsAlways: {
57
51
  env: 'SLASH_VALIDATORS_ALWAYS',
58
52
  description: 'Comma-separated list of validator addresses that should always be slashed.',
@@ -75,21 +69,27 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
75
69
  .map(addr => EthAddress.fromString(addr)),
76
70
  defaultValue: DefaultSlasherConfig.slashValidatorsNever,
77
71
  },
78
- slashPrunePenalty: {
79
- env: 'SLASH_PRUNE_PENALTY',
80
- description: 'Penalty amount for slashing validators of a valid pruned epoch (set to 0 to disable).',
81
- ...bigintConfigHelper(DefaultSlasherConfig.slashPrunePenalty),
82
- },
83
72
  slashDataWithholdingPenalty: {
84
73
  env: 'SLASH_DATA_WITHHOLDING_PENALTY',
85
- description: 'Penalty amount for slashing validators for data withholding (set to 0 to disable).',
74
+ description: 'Penalty for data withholding (0 records offenses without slash votes).',
86
75
  ...bigintConfigHelper(DefaultSlasherConfig.slashDataWithholdingPenalty),
87
76
  },
77
+ slashDataWithholdingToleranceSlots: {
78
+ env: 'SLASH_DATA_WITHHOLDING_TOLERANCE_SLOTS',
79
+ description:
80
+ 'Number of full L2 slots that must elapse after a checkpoint slot before declaring its txs missing and slashing its attesters for data withholding.',
81
+ ...numberConfigHelper(DefaultSlasherConfig.slashDataWithholdingToleranceSlots),
82
+ },
88
83
  slashBroadcastedInvalidBlockPenalty: {
89
84
  env: 'SLASH_INVALID_BLOCK_PENALTY',
90
85
  description: 'Penalty amount for slashing a validator for an invalid block proposed via p2p.',
91
86
  ...bigintConfigHelper(DefaultSlasherConfig.slashBroadcastedInvalidBlockPenalty),
92
87
  },
88
+ slashBroadcastedInvalidCheckpointProposalPenalty: {
89
+ env: 'SLASH_INVALID_CHECKPOINT_PROPOSAL_PENALTY',
90
+ description: 'Penalty amount for slashing a validator for an invalid checkpoint proposal proposed via p2p.',
91
+ ...bigintConfigHelper(DefaultSlasherConfig.slashBroadcastedInvalidCheckpointProposalPenalty),
92
+ },
93
93
  slashDuplicateProposalPenalty: {
94
94
  env: 'SLASH_DUPLICATE_PROPOSAL_PENALTY',
95
95
  description: 'Penalty amount for slashing a validator for sending duplicate proposals.',
@@ -125,23 +125,28 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
125
125
  },
126
126
  slashInactivityPenalty: {
127
127
  env: 'SLASH_INACTIVITY_PENALTY',
128
- description: 'Penalty amount for slashing an inactive validator (set to 0 to disable).',
128
+ description: 'Penalty for an inactive validator (0 records offenses without slash votes).',
129
129
  ...bigintConfigHelper(DefaultSlasherConfig.slashInactivityPenalty),
130
130
  },
131
131
  slashProposeInvalidAttestationsPenalty: {
132
132
  env: 'SLASH_PROPOSE_INVALID_ATTESTATIONS_PENALTY',
133
- description: 'Penalty amount for slashing a proposer that proposed invalid attestations (set to 0 to disable).',
133
+ description: 'Penalty for proposing invalid attestations (0 records offenses without slash votes).',
134
134
  ...bigintConfigHelper(DefaultSlasherConfig.slashProposeInvalidAttestationsPenalty),
135
135
  },
136
- slashAttestDescendantOfInvalidPenalty: {
137
- env: 'SLASH_ATTEST_DESCENDANT_OF_INVALID_PENALTY',
136
+ slashProposeDescendantOfCheckpointWithInvalidAttestationsPenalty: {
137
+ env: 'SLASH_PROPOSE_DESCENDANT_OF_CHECKPOINT_WITH_INVALID_ATTESTATIONS_PENALTY',
138
138
  description:
139
- 'Penalty amount for slashing a validator that attested to a descendant of an invalid block (set to 0 to disable).',
140
- ...bigintConfigHelper(DefaultSlasherConfig.slashAttestDescendantOfInvalidPenalty),
139
+ 'Penalty for publishing a checkpoint building on an invalid checkpoint (0 records offenses without slash votes).',
140
+ ...bigintConfigHelper(DefaultSlasherConfig.slashProposeDescendantOfCheckpointWithInvalidAttestationsPenalty),
141
+ },
142
+ slashAttestInvalidCheckpointProposalPenalty: {
143
+ env: 'SLASH_ATTEST_INVALID_CHECKPOINT_PROPOSAL_PENALTY',
144
+ description: 'Penalty for attesting to an invalid checkpoint proposal (0 records offenses without slash votes).',
145
+ ...bigintConfigHelper(DefaultSlasherConfig.slashAttestInvalidCheckpointProposalPenalty),
141
146
  },
142
147
  slashUnknownPenalty: {
143
148
  env: 'SLASH_UNKNOWN_PENALTY',
144
- description: 'Penalty amount for slashing a validator for an unknown offense (set to 0 to disable).',
149
+ description: 'Penalty for an unknown offense (0 records offenses without slash votes).',
145
150
  ...bigintConfigHelper(DefaultSlasherConfig.slashUnknownPenalty),
146
151
  },
147
152
  slashOffenseExpirationRounds: {
@@ -7,10 +7,10 @@ import { unique } from '@aztec/foundation/collection';
7
7
  import { EthAddress } from '@aztec/foundation/eth-address';
8
8
  import { createLogger } from '@aztec/foundation/log';
9
9
  import { DateProvider } from '@aztec/foundation/timer';
10
- import type { DataStoreConfig } from '@aztec/kv-store/config';
11
10
  import { createStore } from '@aztec/kv-store/lmdb-v2';
12
11
  import { getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
13
12
  import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
13
+ import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
14
14
 
15
15
  import { SlasherClientFacade } from '../slasher_client_facade.js';
16
16
  import type { SlasherClientInterface } from '../slasher_client_interface.js';
@@ -20,12 +20,15 @@ import type { Watcher } from '../watcher.js';
20
20
  /** Creates a slasher client facade that updates itself whenever the rollup slasher changes */
21
21
  export async function createSlasherFacade(
22
22
  config: SlasherConfig & DataStoreConfig & { ethereumSlotDuration: number },
23
- l1Contracts: Pick<L1ReaderConfig['l1Contracts'], 'rollupAddress' | 'slashFactoryAddress' | 'registryAddress'>,
23
+ l1Contracts: Pick<L1ReaderConfig, 'rollupAddress' | 'registryAddress'>,
24
24
  l1Client: ViemClient,
25
25
  watchers: Watcher[],
26
26
  dateProvider: DateProvider,
27
27
  epochCache: EpochCache,
28
- /** List of own validator addresses to add to the slashValidatorNever list unless slashSelfAllowed is true */
28
+ /**
29
+ * List of own validator addresses. Added to the slashValidatorNever list unless slashSelfAllowed is true, and used
30
+ * (independently of slashSelfAllowed) to warn and record metrics when they are targeted by onchain slashing.
31
+ */
29
32
  validatorAddresses: EthAddress[] = [],
30
33
  logger = createLogger('slasher'),
31
34
  ): Promise<SlasherClientInterface> {
@@ -71,12 +74,12 @@ export async function createSlasherFacade(
71
74
  updatedConfig,
72
75
  rollup,
73
76
  l1Client,
74
- l1Contracts.slashFactoryAddress,
75
77
  watchers,
76
78
  epochCache,
77
79
  dateProvider,
78
80
  kvStore,
79
81
  rollupRegisteredAtL2Slot,
82
+ validatorAddresses,
80
83
  logger,
81
84
  );
82
85
  }
@@ -1,167 +1,65 @@
1
1
  import { EpochCache } from '@aztec/epoch-cache';
2
- import {
3
- EmpireSlashingProposerContract,
4
- RollupContract,
5
- TallySlashingProposerContract,
6
- } from '@aztec/ethereum/contracts';
2
+ import { RollupContract, SlashingProposerContract } from '@aztec/ethereum/contracts';
7
3
  import type { ViemClient } from '@aztec/ethereum/types';
8
4
  import type { SlotNumber } from '@aztec/foundation/branded-types';
9
- import { EthAddress } from '@aztec/foundation/eth-address';
5
+ import type { EthAddress } from '@aztec/foundation/eth-address';
10
6
  import { createLogger } from '@aztec/foundation/log';
11
7
  import { DateProvider } from '@aztec/foundation/timer';
12
- import type { DataStoreConfig } from '@aztec/kv-store/config';
13
8
  import { AztecLMDBStoreV2 } from '@aztec/kv-store/lmdb-v2';
14
9
  import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
15
- import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
10
+ import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
16
11
 
17
- import { EmpireSlasherClient, type EmpireSlasherSettings } from '../empire_slasher_client.js';
18
12
  import { NullSlasherClient } from '../null_slasher_client.js';
13
+ import { SlasherClient } from '../slasher_client.js';
19
14
  import { SlasherOffensesStore } from '../stores/offenses_store.js';
20
- import { SlasherPayloadsStore } from '../stores/payloads_store.js';
21
- import { TallySlasherClient } from '../tally_slasher_client.js';
22
15
  import type { Watcher } from '../watcher.js';
23
- import { getTallySlasherSettings } from './get_settings.js';
16
+ import { getSlasherSettings } from './get_settings.js';
24
17
 
25
- /** Creates a slasher client implementation (either tally or empire) based on the slasher proposer type in the rollup */
18
+ /** Creates a slasher client implementation based on the slasher proposer type in the rollup */
26
19
  export async function createSlasherImplementation(
27
20
  config: SlasherConfig & DataStoreConfig & { ethereumSlotDuration: number },
28
21
  rollup: RollupContract,
29
22
  l1Client: ViemClient,
30
- slashFactoryAddress: EthAddress | undefined,
31
23
  watchers: Watcher[],
32
24
  epochCache: EpochCache,
33
25
  dateProvider: DateProvider,
34
26
  kvStore: AztecLMDBStoreV2,
35
27
  rollupRegisteredAtL2Slot: SlotNumber,
28
+ ownValidators: EthAddress[] = [],
36
29
  logger = createLogger('slasher'),
37
30
  ) {
38
31
  const proposer = await rollup.getSlashingProposer();
39
32
  if (!proposer) {
40
33
  return new NullSlasherClient(config);
41
- } else if (proposer.type === 'tally') {
42
- return createTallySlasher(
43
- config,
44
- rollup,
45
- proposer,
46
- watchers,
47
- dateProvider,
48
- epochCache,
49
- kvStore,
50
- rollupRegisteredAtL2Slot,
51
- logger,
52
- );
53
34
  } else {
54
- if (!slashFactoryAddress || slashFactoryAddress.equals(EthAddress.ZERO)) {
55
- throw new Error('Cannot initialize an empire-based SlasherClient without a SlashFactory address');
56
- }
57
- const slashFactory = new SlashFactoryContract(l1Client, slashFactoryAddress.toString());
58
- return createEmpireSlasher(
35
+ return createSlasher(
59
36
  config,
60
37
  rollup,
61
38
  proposer,
62
- slashFactory,
63
39
  watchers,
64
40
  dateProvider,
41
+ epochCache,
65
42
  kvStore,
66
43
  rollupRegisteredAtL2Slot,
44
+ ownValidators,
67
45
  logger,
68
46
  );
69
47
  }
70
48
  }
71
49
 
72
- async function createEmpireSlasher(
73
- config: SlasherConfig & DataStoreConfig & { ethereumSlotDuration: number },
74
- rollup: RollupContract,
75
- slashingProposer: EmpireSlashingProposerContract,
76
- slashFactoryContract: SlashFactoryContract,
77
- watchers: Watcher[],
78
- dateProvider: DateProvider,
79
- kvStore: AztecLMDBStoreV2,
80
- rollupRegisteredAtL2Slot: SlotNumber,
81
- logger = createLogger('slasher'),
82
- ): Promise<EmpireSlasherClient> {
83
- if (slashingProposer.type !== 'empire') {
84
- throw new Error('Slashing proposer contract is not of type Empire');
85
- }
86
-
87
- const [
88
- slashingExecutionDelayInRounds,
89
- slashingPayloadLifetimeInRounds,
90
- slashingRoundSize,
91
- slashingQuorumSize,
92
- epochDuration,
93
- proofSubmissionEpochs,
94
- l1GenesisTime,
95
- slotDuration,
96
- l1StartBlock,
97
- slasher,
98
- ] = await Promise.all([
99
- slashingProposer.getExecutionDelayInRounds(),
100
- slashingProposer.getLifetimeInRounds(),
101
- slashingProposer.getRoundSize(),
102
- slashingProposer.getQuorumSize(),
103
- rollup.getEpochDuration(),
104
- rollup.getProofSubmissionEpochs(),
105
- rollup.getL1GenesisTime(),
106
- rollup.getSlotDuration(),
107
- rollup.getL1StartBlock(),
108
- rollup.getSlasherContract(),
109
- ]);
110
-
111
- const settings: EmpireSlasherSettings = {
112
- slashingExecutionDelayInRounds: Number(slashingExecutionDelayInRounds),
113
- slashingPayloadLifetimeInRounds: Number(slashingPayloadLifetimeInRounds),
114
- slashingRoundSize: Number(slashingRoundSize),
115
- slashingQuorumSize: Number(slashingQuorumSize),
116
- epochDuration: Number(epochDuration),
117
- proofSubmissionEpochs: Number(proofSubmissionEpochs),
118
- l1GenesisTime: l1GenesisTime,
119
- slotDuration: Number(slotDuration),
120
- l1StartBlock,
121
- ethereumSlotDuration: config.ethereumSlotDuration,
122
- slashingAmounts: undefined,
123
- rollupRegisteredAtL2Slot,
124
- };
125
-
126
- const payloadsStore = new SlasherPayloadsStore(kvStore, {
127
- slashingPayloadLifetimeInRounds: settings.slashingPayloadLifetimeInRounds,
128
- });
129
- const offensesStore = new SlasherOffensesStore(kvStore, {
130
- ...settings,
131
- slashOffenseExpirationRounds: config.slashOffenseExpirationRounds,
132
- });
133
-
134
- return new EmpireSlasherClient(
135
- config,
136
- settings,
137
- slashFactoryContract,
138
- slashingProposer,
139
- slasher!,
140
- rollup,
141
- watchers,
142
- dateProvider,
143
- offensesStore,
144
- payloadsStore,
145
- logger,
146
- );
147
- }
148
-
149
- async function createTallySlasher(
50
+ async function createSlasher(
150
51
  config: SlasherConfig & DataStoreConfig,
151
52
  rollup: RollupContract,
152
- slashingProposer: TallySlashingProposerContract,
53
+ slashingProposer: SlashingProposerContract,
153
54
  watchers: Watcher[],
154
55
  dateProvider: DateProvider,
155
56
  epochCache: EpochCache,
156
57
  kvStore: AztecLMDBStoreV2,
157
58
  rollupRegisteredAtL2Slot: SlotNumber,
59
+ ownValidators: EthAddress[] = [],
158
60
  logger = createLogger('slasher'),
159
- ): Promise<TallySlasherClient> {
160
- if (slashingProposer.type !== 'tally') {
161
- throw new Error('Slashing proposer contract is not of type tally');
162
- }
163
-
164
- const settings = { ...(await getTallySlasherSettings(rollup, slashingProposer)), rollupRegisteredAtL2Slot };
61
+ ): Promise<SlasherClient> {
62
+ const settings = { ...(await getSlasherSettings(rollup, slashingProposer)), rollupRegisteredAtL2Slot };
165
63
  const slasher = await rollup.getSlasherContract();
166
64
 
167
65
  const offensesStore = new SlasherOffensesStore(kvStore, {
@@ -169,7 +67,7 @@ async function createTallySlasher(
169
67
  slashOffenseExpirationRounds: config.slashOffenseExpirationRounds,
170
68
  });
171
69
 
172
- return new TallySlasherClient(
70
+ return new SlasherClient(
173
71
  config,
174
72
  settings,
175
73
  slashingProposer,
@@ -179,6 +77,7 @@ async function createTallySlasher(
179
77
  epochCache,
180
78
  dateProvider,
181
79
  offensesStore,
80
+ ownValidators,
182
81
  logger,
183
82
  );
184
83
  }
@@ -1,15 +1,15 @@
1
- import type { RollupContract, TallySlashingProposerContract } from '@aztec/ethereum/contracts';
1
+ import type { RollupContract, SlashingProposerContract } from '@aztec/ethereum/contracts';
2
2
 
3
- import type { TallySlasherSettings } from '../tally_slasher_client.js';
3
+ import type { SlasherSettings } from '../slasher_client.js';
4
4
 
5
- export async function getTallySlasherSettings(
5
+ export async function getSlasherSettings(
6
6
  rollup: RollupContract,
7
- slashingProposer?: TallySlashingProposerContract,
8
- ): Promise<Omit<TallySlasherSettings, 'rollupRegisteredAtL2Slot'>> {
7
+ slashingProposer?: SlashingProposerContract,
8
+ ): Promise<Omit<SlasherSettings, 'rollupRegisteredAtL2Slot'>> {
9
9
  if (!slashingProposer) {
10
10
  const rollupSlashingProposer = await rollup.getSlashingProposer();
11
- if (!rollupSlashingProposer || rollupSlashingProposer.type !== 'tally') {
12
- throw new Error('Rollup slashing proposer is not of type tally');
11
+ if (!rollupSlashingProposer) {
12
+ throw new Error('Rollup slashing proposer not found');
13
13
  }
14
14
  slashingProposer = rollupSlashingProposer;
15
15
  }
@@ -40,7 +40,7 @@ export async function getTallySlasherSettings(
40
40
  rollup.getTargetCommitteeSize(),
41
41
  ]);
42
42
 
43
- const settings: Omit<TallySlasherSettings, 'rollupRegisteredAtL2Slot'> = {
43
+ const settings: Omit<SlasherSettings, 'rollupRegisteredAtL2Slot'> = {
44
44
  slashingExecutionDelayInRounds: Number(slashingExecutionDelayInRounds),
45
45
  slashingRoundSize: Number(slashingRoundSize),
46
46
  slashingRoundSizeInEpochs: Number(slashingRoundSizeInEpochs),
@@ -1,2 +1,2 @@
1
1
  export { createSlasherFacade as createSlasher } from './create_facade.js';
2
- export { getTallySlasherSettings } from './get_settings.js';
2
+ export { getSlasherSettings } from './get_settings.js';