@aztec/slasher 0.0.1-commit.c31f2472 → 0.0.1-commit.c52d6e7

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 (100) hide show
  1. package/README.md +90 -76
  2. package/dest/config.d.ts +1 -1
  3. package/dest/config.d.ts.map +1 -1
  4. package/dest/config.js +41 -29
  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 +29 -3
  8. package/dest/factory/create_implementation.d.ts +7 -7
  9. package/dest/factory/create_implementation.d.ts.map +1 -1
  10. package/dest/factory/create_implementation.js +8 -56
  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 +8 -6
  18. package/dest/generated/slasher-defaults.d.ts.map +1 -1
  19. package/dest/generated/slasher-defaults.js +7 -5
  20. package/dest/index.d.ts +6 -4
  21. package/dest/index.d.ts.map +1 -1
  22. package/dest/index.js +5 -3
  23. package/dest/metrics.d.ts +23 -0
  24. package/dest/metrics.d.ts.map +1 -0
  25. package/dest/metrics.js +38 -0
  26. package/dest/null_slasher_client.d.ts +3 -4
  27. package/dest/null_slasher_client.d.ts.map +1 -1
  28. package/dest/null_slasher_client.js +1 -4
  29. package/dest/own_validator_slash_monitor.d.ts +66 -0
  30. package/dest/own_validator_slash_monitor.d.ts.map +1 -0
  31. package/dest/own_validator_slash_monitor.js +163 -0
  32. package/dest/slash_offenses_collector.d.ts +10 -9
  33. package/dest/slash_offenses_collector.d.ts.map +1 -1
  34. package/dest/slash_offenses_collector.js +50 -34
  35. package/dest/slasher_client.d.ts +118 -0
  36. package/dest/slasher_client.d.ts.map +1 -0
  37. package/dest/{tally_slasher_client.js → slasher_client.js} +64 -48
  38. package/dest/slasher_client_facade.d.ts +8 -8
  39. package/dest/slasher_client_facade.d.ts.map +1 -1
  40. package/dest/slasher_client_facade.js +8 -9
  41. package/dest/slasher_client_interface.d.ts +7 -21
  42. package/dest/slasher_client_interface.d.ts.map +1 -1
  43. package/dest/slasher_client_interface.js +1 -4
  44. package/dest/stores/offenses_store.d.ts +12 -12
  45. package/dest/stores/offenses_store.d.ts.map +1 -1
  46. package/dest/stores/offenses_store.js +64 -39
  47. package/dest/watcher.d.ts +8 -1
  48. package/dest/watcher.d.ts.map +1 -1
  49. package/dest/watcher.js +1 -0
  50. package/dest/watchers/attestations_block_watcher.d.ts +26 -13
  51. package/dest/watchers/attestations_block_watcher.d.ts.map +1 -1
  52. package/dest/watchers/attestations_block_watcher.js +76 -61
  53. package/dest/watchers/attested_invalid_proposal_watcher.d.ts +42 -0
  54. package/dest/watchers/attested_invalid_proposal_watcher.d.ts.map +1 -0
  55. package/dest/watchers/attested_invalid_proposal_watcher.js +117 -0
  56. package/dest/watchers/broadcasted_invalid_checkpoint_proposal_watcher.d.ts +38 -0
  57. package/dest/watchers/broadcasted_invalid_checkpoint_proposal_watcher.d.ts.map +1 -0
  58. package/dest/watchers/broadcasted_invalid_checkpoint_proposal_watcher.js +138 -0
  59. package/dest/watchers/checkpoint_equivocation_watcher.d.ts +30 -0
  60. package/dest/watchers/checkpoint_equivocation_watcher.d.ts.map +1 -0
  61. package/dest/watchers/checkpoint_equivocation_watcher.js +69 -0
  62. package/dest/watchers/data_withholding_watcher.d.ts +63 -0
  63. package/dest/watchers/data_withholding_watcher.d.ts.map +1 -0
  64. package/dest/watchers/data_withholding_watcher.js +193 -0
  65. package/package.json +10 -10
  66. package/src/config.ts +48 -29
  67. package/src/factory/create_facade.ts +37 -5
  68. package/src/factory/create_implementation.ts +29 -105
  69. package/src/factory/get_settings.ts +8 -8
  70. package/src/factory/index.ts +1 -1
  71. package/src/generated/slasher-defaults.ts +7 -5
  72. package/src/index.ts +5 -3
  73. package/src/metrics.ts +59 -0
  74. package/src/null_slasher_client.ts +2 -6
  75. package/src/own_validator_slash_monitor.ts +184 -0
  76. package/src/slash_offenses_collector.ts +70 -36
  77. package/src/{tally_slasher_client.ts → slasher_client.ts} +85 -57
  78. package/src/slasher_client_facade.ts +9 -11
  79. package/src/slasher_client_interface.ts +6 -21
  80. package/src/stores/offenses_store.ts +76 -48
  81. package/src/watcher.ts +8 -0
  82. package/src/watchers/attestations_block_watcher.ts +88 -82
  83. package/src/watchers/attested_invalid_proposal_watcher.ts +168 -0
  84. package/src/watchers/broadcasted_invalid_checkpoint_proposal_watcher.ts +192 -0
  85. package/src/watchers/checkpoint_equivocation_watcher.ts +96 -0
  86. package/src/watchers/data_withholding_watcher.ts +225 -0
  87. package/dest/empire_slasher_client.d.ts +0 -190
  88. package/dest/empire_slasher_client.d.ts.map +0 -1
  89. package/dest/empire_slasher_client.js +0 -572
  90. package/dest/stores/payloads_store.d.ts +0 -29
  91. package/dest/stores/payloads_store.d.ts.map +0 -1
  92. package/dest/stores/payloads_store.js +0 -125
  93. package/dest/tally_slasher_client.d.ts +0 -125
  94. package/dest/tally_slasher_client.d.ts.map +0 -1
  95. package/dest/watchers/epoch_prune_watcher.d.ts +0 -38
  96. package/dest/watchers/epoch_prune_watcher.d.ts.map +0 -1
  97. package/dest/watchers/epoch_prune_watcher.js +0 -158
  98. package/src/empire_slasher_client.ts +0 -657
  99. package/src/stores/payloads_store.ts +0 -146
  100. package/src/watchers/epoch_prune_watcher.ts +0 -221
@@ -0,0 +1,69 @@
1
+ import { merge, pick } from '@aztec/foundation/collection';
2
+ import { createLogger } from '@aztec/foundation/log';
3
+ import { L2BlockSourceEvents } from '@aztec/stdlib/block';
4
+ import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';
5
+ import EventEmitter from 'node:events';
6
+ import { WANT_TO_SLASH_EVENT } from '../watcher.js';
7
+ const CheckpointEquivocationWatcherConfigKeys = [
8
+ 'slashDuplicateProposalPenalty'
9
+ ];
10
+ /**
11
+ * Slashes the slot proposer for DUPLICATE_PROPOSAL when the archiver detects that a
12
+ * locally-stored proposed checkpoint disagrees with the L1-confirmed checkpoint at the
13
+ * same slot. Both are signed by the slot proposer (the proposed one by accepting it via
14
+ * P2P or building it locally; the L1 one by submission), so the proposer equivocated.
15
+ */ export class CheckpointEquivocationWatcher extends EventEmitter {
16
+ l2BlockSource;
17
+ epochCache;
18
+ log;
19
+ handler;
20
+ config;
21
+ constructor(l2BlockSource, epochCache, config){
22
+ super(), this.l2BlockSource = l2BlockSource, this.epochCache = epochCache, this.log = createLogger('checkpoint-equivocation-watcher');
23
+ this.config = pick(config, ...CheckpointEquivocationWatcherConfigKeys);
24
+ this.handler = (event)=>{
25
+ this.onEquivocationDetected(event).catch((err)=>this.log.error('Failed to handle checkpoint equivocation event', err));
26
+ };
27
+ this.log.info('CheckpointEquivocationWatcher initialized');
28
+ }
29
+ updateConfig(config) {
30
+ this.config = merge(this.config, pick(config, ...CheckpointEquivocationWatcherConfigKeys));
31
+ this.log.verbose('CheckpointEquivocationWatcher config updated', this.config);
32
+ }
33
+ start() {
34
+ this.l2BlockSource.events.on(L2BlockSourceEvents.CheckpointEquivocationDetected, this.handler);
35
+ return Promise.resolve();
36
+ }
37
+ stop() {
38
+ this.l2BlockSource.events.off(L2BlockSourceEvents.CheckpointEquivocationDetected, this.handler);
39
+ return Promise.resolve();
40
+ }
41
+ /** Public for tests. */ async onEquivocationDetected(event) {
42
+ const proposer = await this.epochCache.getProposerAttesterAddressInSlot(event.slotNumber);
43
+ if (!proposer) {
44
+ this.log.warn(`Cannot attribute checkpoint equivocation: no proposer for slot ${event.slotNumber}`, {
45
+ slotNumber: event.slotNumber,
46
+ checkpointNumber: event.checkpointNumber
47
+ });
48
+ return;
49
+ }
50
+ const slashArgs = {
51
+ validator: proposer,
52
+ amount: this.config.slashDuplicateProposalPenalty,
53
+ offenseType: OffenseType.DUPLICATE_PROPOSAL,
54
+ epochOrSlot: BigInt(event.slotNumber)
55
+ };
56
+ this.log.info(`Detected checkpoint equivocation offense`, {
57
+ slotNumber: event.slotNumber,
58
+ checkpointNumber: event.checkpointNumber,
59
+ amount: slashArgs.amount,
60
+ offenseType: getOffenseTypeName(slashArgs.offenseType),
61
+ l1ArchiveRoot: event.l1ArchiveRoot.toString(),
62
+ proposedArchiveRoot: event.proposedArchiveRoot.toString(),
63
+ validator: proposer.toString()
64
+ });
65
+ this.emit(WANT_TO_SLASH_EVENT, [
66
+ slashArgs
67
+ ]);
68
+ }
69
+ }
@@ -0,0 +1,63 @@
1
+ import type { EpochCache } from '@aztec/epoch-cache';
2
+ import type { EthAddress } from '@aztec/foundation/eth-address';
3
+ import { type Logger } from '@aztec/foundation/log';
4
+ import type { L2BlockSource } from '@aztec/stdlib/block';
5
+ import type { CheckpointReexecutionTracker, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
6
+ import type { ITxProvider, P2PApi, SlasherConfig } from '@aztec/stdlib/interfaces/server';
7
+ import { type CoordinationSignatureContext } from '@aztec/stdlib/p2p';
8
+ import { type Watcher, type WatcherEmitter } from '../watcher.js';
9
+ declare const DataWithholdingWatcherConfigKeys: readonly ["slashDataWithholdingPenalty", "slashDataWithholdingToleranceSlots"];
10
+ type DataWithholdingWatcherConfig = Pick<SlasherConfig, (typeof DataWithholdingWatcherConfigKeys)[number]>;
11
+ declare const DataWithholdingWatcher_base: new () => WatcherEmitter;
12
+ /**
13
+ * Detects data-withholding offenses by probing the local mempool for the txs in published
14
+ * checkpoints once they are old enough that an honest node should have collected them.
15
+ *
16
+ * Per AZIP-7: once `slashDataWithholdingToleranceSlots` full slots have elapsed after the
17
+ * checkpoint's slot — i.e. at `slotStart(checkpoint.slot + slashDataWithholdingToleranceSlots
18
+ * + 1)` — if any tx from the checkpoint's blocks is still missing locally, the checkpoint's
19
+ * attesters are considered at fault for not making the data available, and we emit a slash
20
+ * for them.
21
+ *
22
+ * The watcher ticks at quarter-eth-slot cadence (matching the Sentinel template). On boot it
23
+ * floors processing at the current slot — restart-time gaps are accepted and not back-filled,
24
+ * matching the Sentinel approach.
25
+ */
26
+ export declare class DataWithholdingWatcher extends DataWithholdingWatcher_base implements Watcher {
27
+ private readonly epochCache;
28
+ private readonly l2BlockSource;
29
+ private readonly txProvider;
30
+ private readonly p2p;
31
+ private readonly reexecutionTracker;
32
+ private readonly signatureContext;
33
+ private readonly log;
34
+ private runningPromise;
35
+ private initialSlot;
36
+ private lastCheckedSlot;
37
+ private config;
38
+ constructor(epochCache: EpochCache, l2BlockSource: Pick<L2BlockSource, 'getCheckpoint' | 'getSyncedL2SlotNumber'>, txProvider: Pick<ITxProvider, 'hasTxs'>, p2p: Pick<P2PApi, 'getCheckpointAttestationsForSlot'>, reexecutionTracker: Pick<CheckpointReexecutionTracker, 'getTxsCollectedRecord'>, signatureContext: CoordinationSignatureContext, config: DataWithholdingWatcherConfig, log?: Logger);
39
+ start(): Promise<void>;
40
+ stop(): Promise<void>;
41
+ updateConfig(config: Partial<SlasherConfig>): void;
42
+ /**
43
+ * Runs every tick. Walks newly-eligible slots and probes their checkpoints for data
44
+ * availability; emits a DATA_WITHHOLDING slash for any checkpoint whose txs are missing.
45
+ */
46
+ work(): Promise<void>;
47
+ private processSlot;
48
+ /**
49
+ * Returns the union of:
50
+ * 1. attesters whose signatures landed in the published checkpoint on L1, and
51
+ * 2. attesters we observed signing the same proposal on p2p (the proposer publishes as
52
+ * soon as it has hit committee quorum, so honest peer attestations that arrive after
53
+ * that point are dropped — but they still vouched for the data and
54
+ * should be slashed for withholding it).
55
+ *
56
+ *
57
+ * Exposed as protected so tests can substitute a deterministic recovery without having
58
+ * to construct real secp256k1 signatures.
59
+ */
60
+ protected extractAttesters(published: PublishedCheckpoint): Promise<EthAddress[]>;
61
+ }
62
+ export {};
63
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGF0YV93aXRoaG9sZGluZ193YXRjaGVyLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvd2F0Y2hlcnMvZGF0YV93aXRoaG9sZGluZ193YXRjaGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxFQUFFLFVBQVUsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBR3JELE9BQU8sS0FBSyxFQUFFLFVBQVUsRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBQ2hFLE9BQU8sRUFBRSxLQUFLLE1BQU0sRUFBZ0IsTUFBTSx1QkFBdUIsQ0FBQztBQUVsRSxPQUFPLEtBQUssRUFBRSxhQUFhLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUV6RCxPQUFPLEtBQUssRUFBRSw0QkFBNEIsRUFBRSxtQkFBbUIsRUFBRSxNQUFNLDBCQUEwQixDQUFDO0FBQ2xHLE9BQU8sS0FBSyxFQUFFLFdBQVcsRUFBRSxNQUFNLEVBQUUsYUFBYSxFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDMUYsT0FBTyxFQUFvQixLQUFLLDRCQUE0QixFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFNeEYsT0FBTyxFQUE2QyxLQUFLLE9BQU8sRUFBRSxLQUFLLGNBQWMsRUFBRSxNQUFNLGVBQWUsQ0FBQztBQUU3RyxRQUFBLE1BQU0sZ0NBQWdDLGdGQUFpRixDQUFDO0FBRXhILEtBQUssNEJBQTRCLEdBQUcsSUFBSSxDQUFDLGFBQWEsRUFBRSxDQUFDLE9BQU8sZ0NBQWdDLENBQUMsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDOztBQUUzRzs7Ozs7Ozs7Ozs7OztHQWFHO0FBQ0gscUJBQWEsc0JBQXVCLFNBQVEsMkJBQTJDLFlBQVcsT0FBTztJQU9yRyxPQUFPLENBQUMsUUFBUSxDQUFDLFVBQVU7SUFDM0IsT0FBTyxDQUFDLFFBQVEsQ0FBQyxhQUFhO0lBQzlCLE9BQU8sQ0FBQyxRQUFRLENBQUMsVUFBVTtJQUMzQixPQUFPLENBQUMsUUFBUSxDQUFDLEdBQUc7SUFDcEIsT0FBTyxDQUFDLFFBQVEsQ0FBQyxrQkFBa0I7SUFDbkMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxnQkFBZ0I7SUFFakMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxHQUFHO0lBYnRCLE9BQU8sQ0FBQyxjQUFjLENBQWlCO0lBQ3ZDLE9BQU8sQ0FBQyxXQUFXLENBQXlCO0lBQzVDLE9BQU8sQ0FBQyxlQUFlLENBQXlCO0lBQ2hELE9BQU8sQ0FBQyxNQUFNLENBQStCO0lBRTdDLFlBQ21CLFVBQVUsRUFBRSxVQUFVLEVBQ3RCLGFBQWEsRUFBRSxJQUFJLENBQUMsYUFBYSxFQUFFLGVBQWUsR0FBRyx1QkFBdUIsQ0FBQyxFQUM3RSxVQUFVLEVBQUUsSUFBSSxDQUFDLFdBQVcsRUFBRSxRQUFRLENBQUMsRUFDdkMsR0FBRyxFQUFFLElBQUksQ0FBQyxNQUFNLEVBQUUsa0NBQWtDLENBQUMsRUFDckQsa0JBQWtCLEVBQUUsSUFBSSxDQUFDLDRCQUE0QixFQUFFLHVCQUF1QixDQUFDLEVBQy9FLGdCQUFnQixFQUFFLDRCQUE0QixFQUMvRCxNQUFNLEVBQUUsNEJBQTRCLEVBQ25CLEdBQUcsR0FBRSxNQUFpRCxFQU94RTtJQUVZLEtBQUssSUFBSSxPQUFPLENBQUMsSUFBSSxDQUFDLENBUWxDO0lBRU0sSUFBSSxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FFM0I7SUFFTSxZQUFZLENBQUMsTUFBTSxFQUFFLE9BQU8sQ0FBQyxhQUFhLENBQUMsR0FBRyxJQUFJLENBR3hEO0lBRUQ7OztPQUdHO0lBQ1UsSUFBSSxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0E4QmpDO1lBR2EsV0FBVztJQTZFekI7Ozs7Ozs7Ozs7O09BV0c7SUFDSCxVQUFnQixnQkFBZ0IsQ0FBQyxTQUFTLEVBQUUsbUJBQW1CLEdBQUcsT0FBTyxDQUFDLFVBQVUsRUFBRSxDQUFDLENBbUJ0RjtDQUNGIn0=
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data_withholding_watcher.d.ts","sourceRoot":"","sources":["../../src/watchers/data_withholding_watcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,KAAK,MAAM,EAAgB,MAAM,uBAAuB,CAAC;AAElE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,4BAA4B,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAClG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAC1F,OAAO,EAAoB,KAAK,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AAMxF,OAAO,EAA6C,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAE7G,QAAA,MAAM,gCAAgC,gFAAiF,CAAC;AAExH,KAAK,4BAA4B,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC,OAAO,gCAAgC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;;AAE3G;;;;;;;;;;;;;GAaG;AACH,qBAAa,sBAAuB,SAAQ,2BAA2C,YAAW,OAAO;IAOrG,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IACnC,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IAEjC,OAAO,CAAC,QAAQ,CAAC,GAAG;IAbtB,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,WAAW,CAAyB;IAC5C,OAAO,CAAC,eAAe,CAAyB;IAChD,OAAO,CAAC,MAAM,CAA+B;IAE7C,YACmB,UAAU,EAAE,UAAU,EACtB,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE,eAAe,GAAG,uBAAuB,CAAC,EAC7E,UAAU,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,EACvC,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,kCAAkC,CAAC,EACrD,kBAAkB,EAAE,IAAI,CAAC,4BAA4B,EAAE,uBAAuB,CAAC,EAC/E,gBAAgB,EAAE,4BAA4B,EAC/D,MAAM,EAAE,4BAA4B,EACnB,GAAG,GAAE,MAAiD,EAOxE;IAEY,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAQlC;IAEM,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAE3B;IAEM,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI,CAGxD;IAED;;;OAGG;IACU,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CA8BjC;YAGa,WAAW;IA6EzB;;;;;;;;;;;OAWG;IACH,UAAgB,gBAAgB,CAAC,SAAS,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAmBtF;CACF"}
@@ -0,0 +1,193 @@
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
+ */ export class DataWithholdingWatcher extends EventEmitter {
28
+ epochCache;
29
+ l2BlockSource;
30
+ txProvider;
31
+ p2p;
32
+ reexecutionTracker;
33
+ signatureContext;
34
+ log;
35
+ runningPromise;
36
+ initialSlot;
37
+ lastCheckedSlot;
38
+ config;
39
+ constructor(epochCache, l2BlockSource, txProvider, p2p, reexecutionTracker, signatureContext, config, log = createLogger('data-withholding-watcher')){
40
+ super(), this.epochCache = epochCache, this.l2BlockSource = l2BlockSource, this.txProvider = txProvider, this.p2p = p2p, this.reexecutionTracker = reexecutionTracker, this.signatureContext = signatureContext, this.log = log;
41
+ this.config = pick(config, ...DataWithholdingWatcherConfigKeys);
42
+ const interval = epochCache.getL1Constants().ethereumSlotDuration * 1000 / 4;
43
+ this.runningPromise = new RunningPromise(this.work.bind(this), log, interval);
44
+ this.log.verbose(`DataWithholdingWatcher initialized`, this.config);
45
+ }
46
+ async start() {
47
+ // Floor processing at the archiver's synced slot rather than the wallclock — restart-time
48
+ // gaps before the archiver catches up are accepted and not back-filled. Falls back to the
49
+ // wallclock if the archiver isn't ready yet (cold start).
50
+ const syncedSlot = await this.l2BlockSource.getSyncedL2SlotNumber();
51
+ this.initialSlot = syncedSlot ?? this.epochCache.getSlotNow();
52
+ this.log.info(`Starting data-withholding watcher with initial slot ${this.initialSlot}`);
53
+ this.runningPromise.start();
54
+ }
55
+ stop() {
56
+ return this.runningPromise.stop();
57
+ }
58
+ updateConfig(config) {
59
+ this.config = merge(this.config, pick(config, ...DataWithholdingWatcherConfigKeys));
60
+ this.log.verbose('DataWithholdingWatcher config updated', this.config);
61
+ }
62
+ /**
63
+ * Runs every tick. Walks newly-eligible slots and probes their checkpoints for data
64
+ * availability; emits a DATA_WITHHOLDING slash for any checkpoint whose txs are missing.
65
+ */ async work() {
66
+ if (this.initialSlot === undefined) {
67
+ return;
68
+ }
69
+ // tolerance is the number of full slots that must elapse after the checkpoint's slot
70
+ // before we declare its data missing. For checkpoint slot S, we therefore process S
71
+ // only once we are in slot `S + tolerance + 1` or later. Drive this off the archiver's
72
+ // synced slot rather than the wallclock so we don't make claims about slots we haven't
73
+ // fully ingested yet (archiver may lag behind L1).
74
+ const tolerance = this.config.slashDataWithholdingToleranceSlots;
75
+ const currentSlot = await this.l2BlockSource.getSyncedL2SlotNumber() ?? this.epochCache.getSlotNow();
76
+ if (currentSlot <= tolerance) {
77
+ return;
78
+ }
79
+ const targetSlot = SlotNumber(currentSlot - tolerance - 1);
80
+ if (targetSlot <= this.initialSlot) {
81
+ return;
82
+ }
83
+ const startSlot = this.lastCheckedSlot === undefined ? this.initialSlot : this.lastCheckedSlot;
84
+ for(let slot = SlotNumber(startSlot + 1); slot <= targetSlot; slot = SlotNumber(slot + 1)){
85
+ try {
86
+ await this.processSlot(slot);
87
+ } catch (err) {
88
+ this.log.error(`Error processing slot ${slot} for data-withholding check`, err, {
89
+ slot
90
+ });
91
+ }
92
+ this.lastCheckedSlot = slot;
93
+ }
94
+ }
95
+ /** Probes the checkpoint at the given slot, if any, and emits a slash on missing txs. */ async processSlot(slot) {
96
+ const published = await this.l2BlockSource.getCheckpoint({
97
+ slot
98
+ });
99
+ if (!published) {
100
+ this.log.trace(`No published checkpoint at slot ${slot}`, {
101
+ slot
102
+ });
103
+ return;
104
+ }
105
+ const checkpointNumber = published.checkpoint.number;
106
+ // Per-block tx-collection records (true | false | undefined) for every block in this
107
+ // published checkpoint. Captured by the validator's proposal handler at the moment of
108
+ // tx collection (i.e. by the *re-execution* deadline). Used as a positive short-circuit
109
+ // only: a `true` for every block means we know the data was available locally, so this
110
+ // checkpoint cannot be a data-withholding offense. A `false` does *not* trigger a slash
111
+ // on its own — the re-execution deadline is much earlier than the data-withholding
112
+ // tolerance window, so missing txs at that earlier deadline may still arrive in time.
113
+ // Anything other than all-true falls through to the mempool probe, which respects the
114
+ // tolerance window.
115
+ const collectionRecords = published.checkpoint.blocks.map((block, idx)=>this.reexecutionTracker.getTxsCollectedRecord(block.header.getSlot(), idx));
116
+ if (collectionRecords.every((r)=>r === true)) {
117
+ this.log.trace(`All blocks for checkpoint at slot ${slot} were collected locally; skipping`, {
118
+ slot,
119
+ checkpointNumber
120
+ });
121
+ return;
122
+ }
123
+ const txHashes = published.checkpoint.blocks.flatMap((block)=>block.body.txEffects.map((txEffect)=>txEffect.txHash));
124
+ if (txHashes.length === 0) {
125
+ this.log.trace(`Checkpoint at slot ${slot} has no txs`, {
126
+ slot
127
+ });
128
+ return;
129
+ }
130
+ const availability = await this.txProvider.hasTxs(txHashes);
131
+ const missingTxs = txHashes.filter((_, i)=>!availability[i]);
132
+ if (missingTxs.length === 0) {
133
+ this.log.trace(`All ${txHashes.length} txs available for checkpoint at slot ${slot}`, {
134
+ slot
135
+ });
136
+ return;
137
+ }
138
+ const attesters = await this.extractAttesters(published);
139
+ if (attesters.length === 0) {
140
+ this.log.warn(`Detected data withholding at slot ${slot} but no recoverable attesters`, {
141
+ slot,
142
+ checkpointNumber,
143
+ missingTxs: missingTxs.map((h)=>h.toString()),
144
+ records: collectionRecords
145
+ });
146
+ return;
147
+ }
148
+ this.log.info(`Detected data withholding offense at slot ${slot}`, {
149
+ slot,
150
+ checkpointNumber,
151
+ amount: this.config.slashDataWithholdingPenalty,
152
+ offenseType: getOffenseTypeName(OffenseType.DATA_WITHHOLDING),
153
+ missingTxs: missingTxs.map((h)=>h.toString()),
154
+ records: collectionRecords,
155
+ attesters: attesters.map((a)=>a.toString())
156
+ });
157
+ const args = attesters.map((validator)=>({
158
+ validator,
159
+ amount: this.config.slashDataWithholdingPenalty,
160
+ offenseType: OffenseType.DATA_WITHHOLDING,
161
+ epochOrSlot: BigInt(slot)
162
+ }));
163
+ this.emit(WANT_TO_SLASH_EVENT, args);
164
+ }
165
+ /**
166
+ * Returns the union of:
167
+ * 1. attesters whose signatures landed in the published checkpoint on L1, and
168
+ * 2. attesters we observed signing the same proposal on p2p (the proposer publishes as
169
+ * soon as it has hit committee quorum, so honest peer attestations that arrive after
170
+ * that point are dropped — but they still vouched for the data and
171
+ * should be slashed for withholding it).
172
+ *
173
+ *
174
+ * Exposed as protected so tests can substitute a deterministic recovery without having
175
+ * to construct real secp256k1 signatures.
176
+ */ async extractAttesters(published) {
177
+ const fromL1 = getAttestationInfoFromPublishedCheckpoint(published, this.signatureContext).filter((info)=>info.status === 'recovered-from-signature').map((info)=>info.address);
178
+ const slot = published.checkpoint.header.slotNumber;
179
+ const proposalPayloadHash = CheckpointProposalHash.fromBuffer(ConsensusPayload.fromCheckpoint(published.checkpoint, this.signatureContext).getPayloadHash());
180
+ const fromP2p = await this.p2p.getCheckpointAttestationsForSlot(slot, proposalPayloadHash).then((attestations)=>attestations.map((a)=>a.getSender()));
181
+ // Dedupe
182
+ const all = new Map();
183
+ for (const addr of compactArray([
184
+ ...fromL1,
185
+ ...fromP2p
186
+ ])){
187
+ all.set(addr.toString(), addr);
188
+ }
189
+ return [
190
+ ...all.values()
191
+ ];
192
+ }
193
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/slasher",
3
- "version": "0.0.1-commit.c31f2472",
3
+ "version": "0.0.1-commit.c52d6e7",
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.c31f2472",
60
- "@aztec/ethereum": "0.0.1-commit.c31f2472",
61
- "@aztec/foundation": "0.0.1-commit.c31f2472",
62
- "@aztec/kv-store": "0.0.1-commit.c31f2472",
63
- "@aztec/l1-artifacts": "0.0.1-commit.c31f2472",
64
- "@aztec/stdlib": "0.0.1-commit.c31f2472",
65
- "@aztec/telemetry-client": "0.0.1-commit.c31f2472",
59
+ "@aztec/epoch-cache": "0.0.1-commit.c52d6e7",
60
+ "@aztec/ethereum": "0.0.1-commit.c52d6e7",
61
+ "@aztec/foundation": "0.0.1-commit.c52d6e7",
62
+ "@aztec/kv-store": "0.0.1-commit.c52d6e7",
63
+ "@aztec/l1-artifacts": "6.0.0-nightly.20260807",
64
+ "@aztec/stdlib": "0.0.1-commit.c52d6e7",
65
+ "@aztec/telemetry-client": "0.0.1-commit.c52d6e7",
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.c31f2472",
72
+ "@aztec/aztec.js": "0.0.1-commit.c52d6e7",
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,18 +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),
25
+ slashDuplicateProposalPenalty: BigInt(slasherDefaultEnv.SLASH_DUPLICATE_PROPOSAL_PENALTY),
26
+ slashDuplicateAttestationPenalty: BigInt(slasherDefaultEnv.SLASH_DUPLICATE_ATTESTATION_PENALTY),
26
27
  slashInactivityPenalty: BigInt(slasherDefaultEnv.SLASH_INACTIVITY_PENALTY),
27
28
  slashProposeInvalidAttestationsPenalty: BigInt(slasherDefaultEnv.SLASH_PROPOSE_INVALID_ATTESTATIONS_PENALTY),
28
- 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
+ ),
29
35
  slashUnknownPenalty: BigInt(slasherDefaultEnv.SLASH_UNKNOWN_PENALTY),
30
36
  slashOffenseExpirationRounds: slasherDefaultEnv.SLASH_OFFENSE_EXPIRATION_ROUNDS,
31
37
  slashMaxPayloadSize: slasherDefaultEnv.SLASH_MAX_PAYLOAD_SIZE,
@@ -38,19 +44,9 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
38
44
  slashOverridePayload: {
39
45
  env: 'SLASH_OVERRIDE_PAYLOAD',
40
46
  description: 'An Ethereum address for a slash payload to vote for unconditionally.',
41
- parseEnv: (val: string) => (val ? EthAddress.fromString(val) : undefined),
47
+ parseEnv: (val: string) => EthAddress.fromString(val),
42
48
  defaultValue: DefaultSlasherConfig.slashOverridePayload,
43
49
  },
44
- slashMinPenaltyPercentage: {
45
- env: 'SLASH_MIN_PENALTY_PERCENTAGE',
46
- description: 'Minimum penalty percentage for slashing offenses (0.1 is 10%).',
47
- ...floatConfigHelper(DefaultSlasherConfig.slashMinPenaltyPercentage),
48
- },
49
- slashMaxPenaltyPercentage: {
50
- env: 'SLASH_MAX_PENALTY_PERCENTAGE',
51
- description: 'Maximum penalty percentage for slashing offenses (2.0 is 200%).',
52
- ...floatConfigHelper(DefaultSlasherConfig.slashMaxPenaltyPercentage),
53
- },
54
50
  slashValidatorsAlways: {
55
51
  env: 'SLASH_VALIDATORS_ALWAYS',
56
52
  description: 'Comma-separated list of validator addresses that should always be slashed.',
@@ -73,21 +69,38 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
73
69
  .map(addr => EthAddress.fromString(addr)),
74
70
  defaultValue: DefaultSlasherConfig.slashValidatorsNever,
75
71
  },
76
- slashPrunePenalty: {
77
- env: 'SLASH_PRUNE_PENALTY',
78
- description: 'Penalty amount for slashing validators of a valid pruned epoch (set to 0 to disable).',
79
- ...bigintConfigHelper(DefaultSlasherConfig.slashPrunePenalty),
80
- },
81
72
  slashDataWithholdingPenalty: {
82
73
  env: 'SLASH_DATA_WITHHOLDING_PENALTY',
83
- 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).',
84
75
  ...bigintConfigHelper(DefaultSlasherConfig.slashDataWithholdingPenalty),
85
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
+ },
86
83
  slashBroadcastedInvalidBlockPenalty: {
87
84
  env: 'SLASH_INVALID_BLOCK_PENALTY',
88
85
  description: 'Penalty amount for slashing a validator for an invalid block proposed via p2p.',
89
86
  ...bigintConfigHelper(DefaultSlasherConfig.slashBroadcastedInvalidBlockPenalty),
90
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
+ 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
+ },
91
104
  slashInactivityTargetPercentage: {
92
105
  env: 'SLASH_INACTIVITY_TARGET_PERCENTAGE',
93
106
  description:
@@ -112,23 +125,28 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
112
125
  },
113
126
  slashInactivityPenalty: {
114
127
  env: 'SLASH_INACTIVITY_PENALTY',
115
- 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).',
116
129
  ...bigintConfigHelper(DefaultSlasherConfig.slashInactivityPenalty),
117
130
  },
118
131
  slashProposeInvalidAttestationsPenalty: {
119
132
  env: 'SLASH_PROPOSE_INVALID_ATTESTATIONS_PENALTY',
120
- 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).',
121
134
  ...bigintConfigHelper(DefaultSlasherConfig.slashProposeInvalidAttestationsPenalty),
122
135
  },
123
- slashAttestDescendantOfInvalidPenalty: {
124
- env: 'SLASH_ATTEST_DESCENDANT_OF_INVALID_PENALTY',
136
+ slashProposeDescendantOfCheckpointWithInvalidAttestationsPenalty: {
137
+ env: 'SLASH_PROPOSE_DESCENDANT_OF_CHECKPOINT_WITH_INVALID_ATTESTATIONS_PENALTY',
125
138
  description:
126
- 'Penalty amount for slashing a validator that attested to a descendant of an invalid block (set to 0 to disable).',
127
- ...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),
128
146
  },
129
147
  slashUnknownPenalty: {
130
148
  env: 'SLASH_UNKNOWN_PENALTY',
131
- 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).',
132
150
  ...bigintConfigHelper(DefaultSlasherConfig.slashUnknownPenalty),
133
151
  },
134
152
  slashOffenseExpirationRounds: {
@@ -142,7 +160,8 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
142
160
  ...numberConfigHelper(DefaultSlasherConfig.slashMaxPayloadSize),
143
161
  },
144
162
  slashGracePeriodL2Slots: {
145
- description: 'Number of L2 slots to wait before considering a slashing offense expired.',
163
+ description:
164
+ 'Number of L2 slots after the network upgrade during which slashing offenses are ignored. The upgrade time is determined from the CanonicalRollupUpdated event.',
146
165
  env: 'SLASH_GRACE_PERIOD_L2_SLOTS',
147
166
  ...numberConfigHelper(DefaultSlasherConfig.slashGracePeriodL2Slots),
148
167
  },
@@ -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,12 +20,15 @@ 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, 'rollupAddress' | 'registryAddress'>,
22
24
  l1Client: ViemClient,
23
25
  watchers: Watcher[],
24
26
  dateProvider: DateProvider,
25
27
  epochCache: EpochCache,
26
- /** 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
+ */
27
32
  validatorAddresses: EthAddress[] = [],
28
33
  logger = createLogger('slasher'),
29
34
  ): Promise<SlasherClientInterface> {
@@ -34,6 +39,32 @@ export async function createSlasherFacade(
34
39
  const kvStore = await createStore('slasher', SCHEMA_VERSION, config, logger.getBindings());
35
40
  const rollup = new RollupContract(l1Client, l1Contracts.rollupAddress);
36
41
 
42
+ // Compute and cache the L2 slot at which the rollup was registered as canonical
43
+ const settingsMap = kvStore.openMap<string, number>('slasher-settings');
44
+ const cacheKey = `registeredSlot:${l1Contracts.rollupAddress}`;
45
+ let rollupRegisteredAtL2Slot = (await settingsMap.getAsync(cacheKey)) as SlotNumber | undefined;
46
+
47
+ if (rollupRegisteredAtL2Slot === undefined) {
48
+ const registry = new RegistryContract(l1Client, l1Contracts.registryAddress);
49
+ const l1StartBlock = await rollup.getL1StartBlock();
50
+ const registrationTimestamp = await registry.getCanonicalRollupRegistrationTimestamp(
51
+ l1Contracts.rollupAddress,
52
+ l1StartBlock,
53
+ );
54
+ if (registrationTimestamp !== undefined) {
55
+ const l1GenesisTime = await rollup.getL1GenesisTime();
56
+ const slotDuration = await rollup.getSlotDuration();
57
+ rollupRegisteredAtL2Slot = getSlotAtTimestamp(registrationTimestamp, {
58
+ l1GenesisTime,
59
+ slotDuration: Number(slotDuration),
60
+ });
61
+ } else {
62
+ rollupRegisteredAtL2Slot = SlotNumber(0);
63
+ }
64
+ await settingsMap.set(cacheKey, rollupRegisteredAtL2Slot);
65
+ logger.info(`Canonical rollup registered at L2 slot ${rollupRegisteredAtL2Slot}`);
66
+ }
67
+
37
68
  const slashValidatorsNever = config.slashSelfAllowed
38
69
  ? config.slashValidatorsNever
39
70
  : unique([...config.slashValidatorsNever, ...validatorAddresses].map(a => a.toString())).map(EthAddress.fromString);
@@ -43,11 +74,12 @@ export async function createSlasherFacade(
43
74
  updatedConfig,
44
75
  rollup,
45
76
  l1Client,
46
- l1Contracts.slashFactoryAddress,
47
77
  watchers,
48
78
  epochCache,
49
79
  dateProvider,
50
80
  kvStore,
81
+ rollupRegisteredAtL2Slot,
82
+ validatorAddresses,
51
83
  logger,
52
84
  );
53
85
  }