@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
@@ -1,12 +1,14 @@
1
+ import type { SlotNumber } from '@aztec/foundation/branded-types';
1
2
  import type { Prettify } from '@aztec/foundation/types';
2
3
  import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
3
4
  import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
4
- import { type OffenseIdentifier } from '@aztec/stdlib/slashing';
5
5
  import type { SlasherOffensesStore } from './stores/offenses_store.js';
6
- import { type WantToSlashArgs, type Watcher } from './watcher.js';
6
+ import { type WantToClearSlashArgs, type WantToSlashArgs, type Watcher } from './watcher.js';
7
7
  export type SlashOffensesCollectorConfig = Prettify<Pick<SlasherConfig, 'slashGracePeriodL2Slots'>>;
8
8
  export type SlashOffensesCollectorSettings = Prettify<Pick<L1RollupConstants, 'epochDuration'> & {
9
9
  slashingAmounts: [bigint, bigint, bigint] | undefined;
10
+ /** L2 slot at which the rollup was registered as canonical in the Registry. Used to anchor the slash grace period. */
11
+ rollupRegisteredAtL2Slot: SlotNumber;
10
12
  }>;
11
13
  /**
12
14
  * Collects and manages slashable offenses from watchers.
@@ -20,6 +22,7 @@ export declare class SlashOffensesCollector {
20
22
  private readonly offensesStore;
21
23
  private readonly log;
22
24
  private readonly unwatchCallbacks;
25
+ private readonly storeMutationQueue;
23
26
  constructor(config: SlashOffensesCollectorConfig, settings: SlashOffensesCollectorSettings, watchers: Watcher[], offensesStore: SlasherOffensesStore, log?: import("@aztec/foundation/log").Logger);
24
27
  start(): Promise<void>;
25
28
  stop(): Promise<void>;
@@ -29,17 +32,15 @@ export declare class SlashOffensesCollector {
29
32
  * @param args - the arguments from the watcher, including the validators, amounts, and offenses
30
33
  */
31
34
  handleWantToSlash(args: WantToSlashArgs[]): Promise<void>;
35
+ handleWantToClearSlash(args: WantToClearSlashArgs[]): Promise<void>;
32
36
  /**
33
37
  * Triggered on a time basis when we enter a new slashing round.
34
38
  * Clears expired offenses from stores.
35
39
  */
36
40
  handleNewRound(round: bigint): Promise<void>;
37
- /**
38
- * Marks offenses as slashed (no longer pending)
39
- * @param offenses - The offenses to mark as slashed
40
- */
41
- markAsSlashed(offenses: OffenseIdentifier[]): Promise<void>;
42
- /** Returns whether to skip an offense if it happened during the grace period at the beginning of the chain */
41
+ /** Returns whether to skip an offense if it happened during the grace period after the network upgrade */
43
42
  private shouldSkipOffense;
43
+ private getOffenseLogData;
44
+ private enqueueStoreMutation;
44
45
  }
45
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhc2hfb2ZmZW5zZXNfY29sbGVjdG9yLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvc2xhc2hfb2ZmZW5zZXNfY29sbGVjdG9yLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sS0FBSyxFQUFFLFFBQVEsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQ3hELE9BQU8sS0FBSyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sNkJBQTZCLENBQUM7QUFDckUsT0FBTyxLQUFLLEVBQUUsYUFBYSxFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDckUsT0FBTyxFQUFnQixLQUFLLGlCQUFpQixFQUFxQixNQUFNLHdCQUF3QixDQUFDO0FBRWpHLE9BQU8sS0FBSyxFQUFFLG9CQUFvQixFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFDdkUsT0FBTyxFQUF1QixLQUFLLGVBQWUsRUFBRSxLQUFLLE9BQU8sRUFBRSxNQUFNLGNBQWMsQ0FBQztBQUV2RixNQUFNLE1BQU0sNEJBQTRCLEdBQUcsUUFBUSxDQUFDLElBQUksQ0FBQyxhQUFhLEVBQUUseUJBQXlCLENBQUMsQ0FBQyxDQUFDO0FBQ3BHLE1BQU0sTUFBTSw4QkFBOEIsR0FBRyxRQUFRLENBQ25ELElBQUksQ0FBQyxpQkFBaUIsRUFBRSxlQUFlLENBQUMsR0FBRztJQUFFLGVBQWUsRUFBRSxDQUFDLE1BQU0sRUFBRSxNQUFNLEVBQUUsTUFBTSxDQUFDLEdBQUcsU0FBUyxDQUFBO0NBQUUsQ0FDckcsQ0FBQztBQUVGOzs7O0dBSUc7QUFDSCxxQkFBYSxzQkFBc0I7SUFJL0IsT0FBTyxDQUFDLFFBQVEsQ0FBQyxNQUFNO0lBQ3ZCLE9BQU8sQ0FBQyxRQUFRLENBQUMsUUFBUTtJQUN6QixPQUFPLENBQUMsUUFBUSxDQUFDLFFBQVE7SUFDekIsT0FBTyxDQUFDLFFBQVEsQ0FBQyxhQUFhO0lBQzlCLE9BQU8sQ0FBQyxRQUFRLENBQUMsR0FBRztJQVB0QixPQUFPLENBQUMsUUFBUSxDQUFDLGdCQUFnQixDQUFzQjtJQUV2RCxZQUNtQixNQUFNLEVBQUUsNEJBQTRCLEVBQ3BDLFFBQVEsRUFBRSw4QkFBOEIsRUFDeEMsUUFBUSxFQUFFLE9BQU8sRUFBRSxFQUNuQixhQUFhLEVBQUUsb0JBQW9CLEVBQ25DLEdBQUcseUNBQTZDLEVBQy9EO0lBRUcsS0FBSyxrQkFhWDtJQUVNLElBQUksa0JBU1Y7SUFFRDs7OztPQUlHO0lBQ1UsaUJBQWlCLENBQUMsSUFBSSxFQUFFLGVBQWUsRUFBRSxpQkFpQ3JEO0lBRUQ7OztPQUdHO0lBQ1UsY0FBYyxDQUFDLEtBQUssRUFBRSxNQUFNLGlCQUt4QztJQUVEOzs7T0FHRztJQUNJLGFBQWEsQ0FBQyxRQUFRLEVBQUUsaUJBQWlCLEVBQUUsaUJBR2pEO0lBRUQsOEdBQThHO0lBQzlHLE9BQU8sQ0FBQyxpQkFBaUI7Q0FJMUIifQ==
46
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhc2hfb2ZmZW5zZXNfY29sbGVjdG9yLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvc2xhc2hfb2ZmZW5zZXNfY29sbGVjdG9yLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxFQUFFLFVBQVUsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBR2xFLE9BQU8sS0FBSyxFQUFFLFFBQVEsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQ3hELE9BQU8sS0FBSyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sNkJBQTZCLENBQUM7QUFDckUsT0FBTyxLQUFLLEVBQUUsYUFBYSxFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFHckUsT0FBTyxLQUFLLEVBQUUsb0JBQW9CLEVBQUUsTUFBTSw0QkFBNEIsQ0FBQztBQUN2RSxPQUFPLEVBR0wsS0FBSyxvQkFBb0IsRUFDekIsS0FBSyxlQUFlLEVBQ3BCLEtBQUssT0FBTyxFQUNiLE1BQU0sY0FBYyxDQUFDO0FBRXRCLE1BQU0sTUFBTSw0QkFBNEIsR0FBRyxRQUFRLENBQUMsSUFBSSxDQUFDLGFBQWEsRUFBRSx5QkFBeUIsQ0FBQyxDQUFDLENBQUM7QUFDcEcsTUFBTSxNQUFNLDhCQUE4QixHQUFHLFFBQVEsQ0FDbkQsSUFBSSxDQUFDLGlCQUFpQixFQUFFLGVBQWUsQ0FBQyxHQUFHO0lBQ3pDLGVBQWUsRUFBRSxDQUFDLE1BQU0sRUFBRSxNQUFNLEVBQUUsTUFBTSxDQUFDLEdBQUcsU0FBUyxDQUFDO0lBQ3RELHNIQUFzSDtJQUN0SCx3QkFBd0IsRUFBRSxVQUFVLENBQUM7Q0FDdEMsQ0FDRixDQUFDO0FBRUY7Ozs7R0FJRztBQUNILHFCQUFhLHNCQUFzQjtJQUsvQixPQUFPLENBQUMsUUFBUSxDQUFDLE1BQU07SUFDdkIsT0FBTyxDQUFDLFFBQVEsQ0FBQyxRQUFRO0lBQ3pCLE9BQU8sQ0FBQyxRQUFRLENBQUMsUUFBUTtJQUN6QixPQUFPLENBQUMsUUFBUSxDQUFDLGFBQWE7SUFDOUIsT0FBTyxDQUFDLFFBQVEsQ0FBQyxHQUFHO0lBUnRCLE9BQU8sQ0FBQyxRQUFRLENBQUMsZ0JBQWdCLENBQXNCO0lBQ3ZELE9BQU8sQ0FBQyxRQUFRLENBQUMsa0JBQWtCLENBQXFCO0lBRXhELFlBQ21CLE1BQU0sRUFBRSw0QkFBNEIsRUFDcEMsUUFBUSxFQUFFLDhCQUE4QixFQUN4QyxRQUFRLEVBQUUsT0FBTyxFQUFFLEVBQ25CLGFBQWEsRUFBRSxvQkFBb0IsRUFDbkMsR0FBRyx5Q0FBNkMsRUFDL0Q7SUFFRyxLQUFLLGtCQW1CWDtJQUVZLElBQUksa0JBVWhCO0lBRUQ7Ozs7T0FJRztJQUNVLGlCQUFpQixDQUFDLElBQUksRUFBRSxlQUFlLEVBQUUsaUJBK0JyRDtJQUVZLHNCQUFzQixDQUFDLElBQUksRUFBRSxvQkFBb0IsRUFBRSxpQkFXL0Q7SUFFRDs7O09BR0c7SUFDVSxjQUFjLENBQUMsS0FBSyxFQUFFLE1BQU0saUJBS3hDO0lBRUQsMEdBQTBHO0lBQzFHLE9BQU8sQ0FBQyxpQkFBaUI7SUFLekIsT0FBTyxDQUFDLGlCQUFpQjtJQVF6QixPQUFPLENBQUMsb0JBQW9CO0NBRzdCIn0=
@@ -1 +1 @@
1
- {"version":3,"file":"slash_offenses_collector.d.ts","sourceRoot":"","sources":["../src/slash_offenses_collector.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAgB,KAAK,iBAAiB,EAAqB,MAAM,wBAAwB,CAAC;AAEjG,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,EAAuB,KAAK,eAAe,EAAE,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvF,MAAM,MAAM,4BAA4B,GAAG,QAAQ,CAAC,IAAI,CAAC,aAAa,EAAE,yBAAyB,CAAC,CAAC,CAAC;AACpG,MAAM,MAAM,8BAA8B,GAAG,QAAQ,CACnD,IAAI,CAAC,iBAAiB,EAAE,eAAe,CAAC,GAAG;IAAE,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAA;CAAE,CACrG,CAAC;AAEF;;;;GAIG;AACH,qBAAa,sBAAsB;IAI/B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG;IAPtB,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAsB;IAEvD,YACmB,MAAM,EAAE,4BAA4B,EACpC,QAAQ,EAAE,8BAA8B,EACxC,QAAQ,EAAE,OAAO,EAAE,EACnB,aAAa,EAAE,oBAAoB,EACnC,GAAG,yCAA6C,EAC/D;IAEG,KAAK,kBAaX;IAEM,IAAI,kBASV;IAED;;;;OAIG;IACU,iBAAiB,CAAC,IAAI,EAAE,eAAe,EAAE,iBAiCrD;IAED;;;OAGG;IACU,cAAc,CAAC,KAAK,EAAE,MAAM,iBAKxC;IAED;;;OAGG;IACI,aAAa,CAAC,QAAQ,EAAE,iBAAiB,EAAE,iBAGjD;IAED,8GAA8G;IAC9G,OAAO,CAAC,iBAAiB;CAI1B"}
1
+ {"version":3,"file":"slash_offenses_collector.d.ts","sourceRoot":"","sources":["../src/slash_offenses_collector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAGlE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAGrE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,EAGL,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,OAAO,EACb,MAAM,cAAc,CAAC;AAEtB,MAAM,MAAM,4BAA4B,GAAG,QAAQ,CAAC,IAAI,CAAC,aAAa,EAAE,yBAAyB,CAAC,CAAC,CAAC;AACpG,MAAM,MAAM,8BAA8B,GAAG,QAAQ,CACnD,IAAI,CAAC,iBAAiB,EAAE,eAAe,CAAC,GAAG;IACzC,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IACtD,sHAAsH;IACtH,wBAAwB,EAAE,UAAU,CAAC;CACtC,CACF,CAAC;AAEF;;;;GAIG;AACH,qBAAa,sBAAsB;IAK/B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG;IARtB,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAsB;IACvD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IAExD,YACmB,MAAM,EAAE,4BAA4B,EACpC,QAAQ,EAAE,8BAA8B,EACxC,QAAQ,EAAE,OAAO,EAAE,EACnB,aAAa,EAAE,oBAAoB,EACnC,GAAG,yCAA6C,EAC/D;IAEG,KAAK,kBAmBX;IAEY,IAAI,kBAUhB;IAED;;;;OAIG;IACU,iBAAiB,CAAC,IAAI,EAAE,eAAe,EAAE,iBA+BrD;IAEY,sBAAsB,CAAC,IAAI,EAAE,oBAAoB,EAAE,iBAW/D;IAED;;;OAGG;IACU,cAAc,CAAC,KAAK,EAAE,MAAM,iBAKxC;IAED,0GAA0G;IAC1G,OAAO,CAAC,iBAAiB;IAKzB,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,oBAAoB;CAG7B"}
@@ -1,6 +1,7 @@
1
1
  import { createLogger } from '@aztec/foundation/log';
2
- import { getSlotForOffense } from '@aztec/stdlib/slashing';
3
- import { WANT_TO_SLASH_EVENT } from './watcher.js';
2
+ import { SerialQueue } from '@aztec/foundation/queue';
3
+ import { getOffenseTypeName, getSlotForOffense } from '@aztec/stdlib/slashing';
4
+ import { WANT_TO_CLEAR_SLASH_EVENT, WANT_TO_SLASH_EVENT } from './watcher.js';
4
5
  /**
5
6
  * Collects and manages slashable offenses from watchers.
6
7
  * This class handles the common logic for subscribing to slash watcher events,
@@ -12,6 +13,7 @@ import { WANT_TO_SLASH_EVENT } from './watcher.js';
12
13
  offensesStore;
13
14
  log;
14
15
  unwatchCallbacks;
16
+ storeMutationQueue;
15
17
  constructor(config, settings, watchers, offensesStore, log = createLogger('slasher:offenses-collector')){
16
18
  this.config = config;
17
19
  this.settings = settings;
@@ -19,25 +21,30 @@ import { WANT_TO_SLASH_EVENT } from './watcher.js';
19
21
  this.offensesStore = offensesStore;
20
22
  this.log = log;
21
23
  this.unwatchCallbacks = [];
24
+ this.storeMutationQueue = new SerialQueue();
22
25
  }
23
26
  start() {
24
27
  this.log.debug('Starting SlashOffensesCollector...');
25
- // Subscribe to watchers WANT_TO_SLASH_EVENT
28
+ this.storeMutationQueue.start();
29
+ // Subscribe to watcher slashing events.
26
30
  for (const watcher of this.watchers){
27
- const wantToSlashCallback = (args)=>void this.handleWantToSlash(args).catch((err)=>this.log.error('Error handling wantToSlash', err));
31
+ const wantToSlashCallback = (args)=>this.enqueueStoreMutation('wantToSlash', ()=>this.handleWantToSlash(args));
28
32
  watcher.on(WANT_TO_SLASH_EVENT, wantToSlashCallback);
29
33
  this.unwatchCallbacks.push(()=>watcher.removeListener(WANT_TO_SLASH_EVENT, wantToSlashCallback));
34
+ const wantToClearSlashCallback = (args)=>this.enqueueStoreMutation('wantToClearSlash', ()=>this.handleWantToClearSlash(args));
35
+ watcher.on(WANT_TO_CLEAR_SLASH_EVENT, wantToClearSlashCallback);
36
+ this.unwatchCallbacks.push(()=>watcher.removeListener(WANT_TO_CLEAR_SLASH_EVENT, wantToClearSlashCallback));
30
37
  }
31
38
  this.log.info('Started SlashOffensesCollector');
32
39
  return Promise.resolve();
33
40
  }
34
- stop() {
41
+ async stop() {
35
42
  this.log.debug('Stopping SlashOffensesCollector...');
36
43
  for (const unwatchCallback of this.unwatchCallbacks){
37
44
  unwatchCallback();
38
45
  }
46
+ await this.storeMutationQueue.end();
39
47
  this.log.info('SlashOffensesCollector stopped');
40
- return Promise.resolve();
41
48
  }
42
49
  /**
43
50
  * Called when a slash watcher emits WANT_TO_SLASH_EVENT.
@@ -45,32 +52,40 @@ import { WANT_TO_SLASH_EVENT } from './watcher.js';
45
52
  * @param args - the arguments from the watcher, including the validators, amounts, and offenses
46
53
  */ async handleWantToSlash(args) {
47
54
  for (const arg of args){
48
- const pendingOffense = {
55
+ const offense = {
49
56
  validator: arg.validator,
50
57
  amount: arg.amount,
51
58
  offenseType: arg.offenseType,
52
59
  epochOrSlot: arg.epochOrSlot
53
60
  };
54
- if (this.shouldSkipOffense(pendingOffense)) {
55
- this.log.verbose('Skipping offense during grace period', pendingOffense);
56
- continue;
57
- }
58
- if (await this.offensesStore.hasOffense(pendingOffense)) {
59
- this.log.debug('Skipping repeated offense', pendingOffense);
61
+ if (this.shouldSkipOffense(offense)) {
62
+ this.log.verbose('Skipping offense during grace period', this.getOffenseLogData(offense));
60
63
  continue;
61
64
  }
62
- if (this.settings.slashingAmounts) {
63
- const minSlash = this.settings.slashingAmounts[0];
64
- if (arg.amount < minSlash) {
65
- this.log.warn(`Offense amount ${arg.amount} is below minimum slashing amount ${minSlash}`);
65
+ const added = await this.offensesStore.addOffense(offense);
66
+ if (added) {
67
+ if (this.settings.slashingAmounts) {
68
+ const minSlash = this.settings.slashingAmounts[0];
69
+ if (arg.amount < minSlash) {
70
+ this.log.warn(`Offense amount ${arg.amount} is below minimum slashing amount ${minSlash}`, this.getOffenseLogData(offense));
71
+ }
66
72
  }
73
+ this.log.info(`Adding pending offense for validator ${arg.validator}`, this.getOffenseLogData(offense));
74
+ } else {
75
+ this.log.debug('Skipping repeated offense', this.getOffenseLogData(offense));
76
+ }
77
+ }
78
+ }
79
+ async handleWantToClearSlash(args) {
80
+ for (const arg of args){
81
+ const cleared = await this.offensesStore.clearOffenses(arg);
82
+ if (cleared > 0) {
83
+ this.log.info(`Cleared ${cleared} pending offenses`, {
84
+ offenseType: getOffenseTypeName(arg.offenseType),
85
+ epochOrSlot: arg.epochOrSlot,
86
+ validators: arg.validators?.map((validator)=>validator.toString())
87
+ });
67
88
  }
68
- this.log.info(`Adding pending offense for validator ${arg.validator}`, {
69
- ...pendingOffense,
70
- epochOrSlot: pendingOffense.epochOrSlot.toString(),
71
- amount: pendingOffense.amount.toString()
72
- });
73
- await this.offensesStore.addPendingOffense(pendingOffense);
74
89
  }
75
90
  }
76
91
  /**
@@ -82,17 +97,18 @@ import { WANT_TO_SLASH_EVENT } from './watcher.js';
82
97
  this.log.debug(`Cleared ${cleared} expired offenses for round ${round}`);
83
98
  }
84
99
  }
85
- /**
86
- * Marks offenses as slashed (no longer pending)
87
- * @param offenses - The offenses to mark as slashed
88
- */ markAsSlashed(offenses) {
89
- this.log.verbose(`Marking offenses as slashed`, {
90
- offenses
91
- });
92
- return this.offensesStore.markAsSlashed(offenses);
93
- }
94
- /** Returns whether to skip an offense if it happened during the grace period at the beginning of the chain */ shouldSkipOffense(offense) {
100
+ /** Returns whether to skip an offense if it happened during the grace period after the network upgrade */ shouldSkipOffense(offense) {
95
101
  const offenseSlot = getSlotForOffense(offense, this.settings);
96
- return offenseSlot < this.config.slashGracePeriodL2Slots;
102
+ return offenseSlot < this.settings.rollupRegisteredAtL2Slot + this.config.slashGracePeriodL2Slots;
103
+ }
104
+ getOffenseLogData(offense) {
105
+ return {
106
+ ...offense,
107
+ validator: offense.validator.toString(),
108
+ offenseType: getOffenseTypeName(offense.offenseType)
109
+ };
110
+ }
111
+ enqueueStoreMutation(label, callback) {
112
+ void this.storeMutationQueue.put(callback).catch((err)=>this.log.error(`Error handling ${label}`, err));
97
113
  }
98
114
  }
@@ -0,0 +1,118 @@
1
+ import { EthAddress } from '@aztec/aztec.js/addresses';
2
+ import type { EpochCache } from '@aztec/epoch-cache';
3
+ import { RollupContract, SlasherContract, SlashingProposerContract } from '@aztec/ethereum/contracts';
4
+ import { SlotNumber } from '@aztec/foundation/branded-types';
5
+ import type { DateProvider } from '@aztec/foundation/timer';
6
+ import type { Prettify } from '@aztec/foundation/types';
7
+ import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
8
+ import { type Offense, type ProposerSlashAction, type ProposerSlashActionProvider } from '@aztec/stdlib/slashing';
9
+ import type { Hex } from 'viem';
10
+ import { SlasherMetrics } from './metrics.js';
11
+ import { OwnValidatorSlashMonitor } from './own_validator_slash_monitor.js';
12
+ import { SlashOffensesCollector, type SlashOffensesCollectorConfig, type SlashOffensesCollectorSettings } from './slash_offenses_collector.js';
13
+ import { SlashRoundMonitor, type SlashRoundMonitorSettings } from './slash_round_monitor.js';
14
+ import type { SlasherClientInterface } from './slasher_client_interface.js';
15
+ import type { SlasherOffensesStore } from './stores/offenses_store.js';
16
+ import type { Watcher } from './watcher.js';
17
+ /** Settings used in the slasher client, loaded from the L1 contracts during initialization */
18
+ export type SlasherSettings = Prettify<SlashRoundMonitorSettings & SlashOffensesCollectorSettings & {
19
+ slashingLifetimeInRounds: number;
20
+ slashingExecutionDelayInRounds: number;
21
+ slashingRoundSizeInEpochs: number;
22
+ slashingOffsetInRounds: number;
23
+ slashingQuorumSize: number;
24
+ slashingAmounts: [bigint, bigint, bigint];
25
+ /** Committee size for block proposal */
26
+ targetCommitteeSize: number;
27
+ }>;
28
+ export type SlasherClientConfig = SlashOffensesCollectorConfig & Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever' | 'slashExecuteRoundsLookBack' | 'slashMaxPayloadSize'>;
29
+ /**
30
+ * The Slasher client is responsible for managing slashable offenses using
31
+ * the consensus-based slashing model where proposers vote on individual validator offenses.
32
+ *
33
+ * The client subscribes to several slash watchers that emit offenses and tracks them. When the slasher is the
34
+ * proposer, it votes for which validators from past epochs should be slashed based on collected offenses.
35
+ * Voting is handled by the sequencer publisher, the slasher client does not interact with L1 directly.
36
+ * The client also monitors rounds and executes slashing when rounds become executable after reaching quorum.
37
+ *
38
+ * Voting and offense collection
39
+ * - Time is divided into rounds (ROUND_SIZE slots each). During each round, block proposers can submit votes
40
+ * indicating which validators from SLASH_OFFSET_IN_ROUNDS rounds ago should be slashed.
41
+ * - Votes are encoded as bytes where each validator's vote is represented by 2 bits indicating the slash amount (0-3 slash units)
42
+ * for the validator in the committee being slashed.
43
+ * - When gathering offenses for round N, the system looks at offenses from round N-2 (where 2 is the hardcoded
44
+ * offset), giving time to detect offenses and vote on them in a later round.
45
+ * - Each offense carries an epoch or block identifier to differentiate multiple offenses by the same validator.
46
+ *
47
+ * Quorum and execution
48
+ * - After a round ends, there is an execution delay period for review so the VETOER in the Slasher can veto
49
+ * if needed.
50
+ * - Once the delay passes, anyone can call executeRound() to tally votes and execute slashing.
51
+ * - Validators that reach the quorum threshold are slashed. A vote for slashing N units is also considered
52
+ * a vote for slashing N-1, N-2, ..., 1 units. The system slashes for the largest amount that reaches quorum.
53
+ * - The client monitors executable rounds and triggers execution when appropriate.
54
+ */
55
+ export declare class SlasherClient implements ProposerSlashActionProvider, SlasherClientInterface {
56
+ private config;
57
+ private settings;
58
+ private slashingProposer;
59
+ private slasher;
60
+ private rollup;
61
+ private epochCache;
62
+ private dateProvider;
63
+ private offensesStore;
64
+ private readonly ownValidators;
65
+ private log;
66
+ private readonly metrics;
67
+ protected unwatchCallbacks: (() => void)[];
68
+ protected roundMonitor: SlashRoundMonitor;
69
+ protected offensesCollector: SlashOffensesCollector;
70
+ protected readonly ownValidatorMonitor: OwnValidatorSlashMonitor;
71
+ constructor(config: SlasherClientConfig, settings: SlasherSettings, slashingProposer: SlashingProposerContract, slasher: SlasherContract, rollup: RollupContract, watchers: Watcher[], epochCache: EpochCache, dateProvider: DateProvider, offensesStore: SlasherOffensesStore, ownValidators?: EthAddress[], log?: import("@aztec/foundation/log").Logger, metrics?: SlasherMetrics);
72
+ start(): Promise<void>;
73
+ /** Stop the slasher client */
74
+ stop(): Promise<void>;
75
+ /** Returns the current config */
76
+ getConfig(): SlasherConfig;
77
+ /** Update the config of the slasher client */
78
+ updateConfig(config: Partial<SlasherConfig>): void;
79
+ /** Triggered on a time basis when we enter a new slashing round. Clears expired offenses. */
80
+ protected handleNewRound(round: bigint): Promise<void>;
81
+ /** Called when we see a RoundExecuted event on the SlashingProposer (just for logging and metrics). */
82
+ protected handleRoundExecuted(round: bigint, slashCount: bigint, l1BlockHash: Hex): Promise<void>;
83
+ /**
84
+ * Get the actions the proposer should take for slashing
85
+ * @param slotNumber - The current slot number
86
+ * @returns The actions to take
87
+ */
88
+ getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]>;
89
+ /**
90
+ * Returns an execute slash action if there are any rounds ready to be executed.
91
+ * Returns the oldest slash action if there are multiple rounds pending execution.
92
+ */
93
+ protected getExecuteSlashAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined>;
94
+ private tryGetRoundExecuteAction;
95
+ /** Returns a vote action based on offenses from the target round (with offset applied) */
96
+ protected getVoteOffensesAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined>;
97
+ /** Returns the committees that were active during the timespan of a given round */
98
+ private collectCommitteesActiveDuringRound;
99
+ /**
100
+ * Gather offenses to be slashed on a given round.
101
+ * Round N slashes validators from round N - slashOffsetInRounds.
102
+ * @param round - The round to get offenses for, defaults to current round
103
+ * @returns Array of pending offenses for the round with offset applied
104
+ */
105
+ gatherOffensesForRound(round?: bigint): Promise<Offense[]>;
106
+ /** Returns all offenses stored */
107
+ getOffenses(): Promise<Offense[]>;
108
+ /**
109
+ * Returns the round to be slashed given the current round by applying the slash offset.
110
+ * During round N, we cannot slash the validators from the epochs of the same round, since the round is not over,
111
+ * and besides we would be asking the current validators to vote to slash themselves. So during round N we look at the
112
+ * epochs spanned during round N - SLASH_OFFSET_IN_ROUNDS. This offset means that the epochs we slash are complete,
113
+ * and also gives nodes time to detect any misbehavior (eg slashing for prunes requires the proof submission window to
114
+ * pass).
115
+ */
116
+ private getSlashedRound;
117
+ }
118
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhc2hlcl9jbGllbnQuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9zbGFzaGVyX2NsaWVudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFDdkQsT0FBTyxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDckQsT0FBTyxFQUFFLGNBQWMsRUFBRSxlQUFlLEVBQUUsd0JBQXdCLEVBQUUsTUFBTSwyQkFBMkIsQ0FBQztBQUV0RyxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFHN0QsT0FBTyxLQUFLLEVBQUUsWUFBWSxFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDNUQsT0FBTyxLQUFLLEVBQUUsUUFBUSxFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDeEQsT0FBTyxLQUFLLEVBQUUsYUFBYSxFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDckUsT0FBTyxFQUNMLEtBQUssT0FBTyxFQUVaLEtBQUssbUJBQW1CLEVBQ3hCLEtBQUssMkJBQTJCLEVBSWpDLE1BQU0sd0JBQXdCLENBQUM7QUFHaEMsT0FBTyxLQUFLLEVBQUUsR0FBRyxFQUFFLE1BQU0sTUFBTSxDQUFDO0FBRWhDLE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFDOUMsT0FBTyxFQUFFLHdCQUF3QixFQUFFLE1BQU0sa0NBQWtDLENBQUM7QUFDNUUsT0FBTyxFQUNMLHNCQUFzQixFQUN0QixLQUFLLDRCQUE0QixFQUNqQyxLQUFLLDhCQUE4QixFQUNwQyxNQUFNLCtCQUErQixDQUFDO0FBQ3ZDLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxLQUFLLHlCQUF5QixFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDN0YsT0FBTyxLQUFLLEVBQUUsc0JBQXNCLEVBQUUsTUFBTSwrQkFBK0IsQ0FBQztBQUM1RSxPQUFPLEtBQUssRUFBRSxvQkFBb0Is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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slasher_client.d.ts","sourceRoot":"","sources":["../src/slasher_client.ts"],"names":[],"mappings":"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"}
@@ -2,12 +2,14 @@ import { EthAddress } from '@aztec/aztec.js/addresses';
2
2
  import { maxBigint } from '@aztec/foundation/bigint';
3
3
  import { compactArray, partition, times } from '@aztec/foundation/collection';
4
4
  import { createLogger } from '@aztec/foundation/log';
5
- import { sleep } from '@aztec/foundation/sleep';
6
- import { OffenseType, getEpochsForRound, getSlashConsensusVotesFromOffenses } from '@aztec/stdlib/slashing';
5
+ import { OffenseType, getEpochsForRound, getOffenseTypeName, getSlashConsensusVotesFromOffenses } from '@aztec/stdlib/slashing';
6
+ import { getTelemetryClient } from '@aztec/telemetry-client';
7
+ import { SlasherMetrics } from './metrics.js';
8
+ import { OwnValidatorSlashMonitor } from './own_validator_slash_monitor.js';
7
9
  import { SlashOffensesCollector } from './slash_offenses_collector.js';
8
10
  import { SlashRoundMonitor } from './slash_round_monitor.js';
9
11
  /**
10
- * The Tally Slasher client is responsible for managing slashable offenses using
12
+ * The Slasher client is responsible for managing slashable offenses using
11
13
  * the consensus-based slashing model where proposers vote on individual validator offenses.
12
14
  *
13
15
  * The client subscribes to several slash watchers that emit offenses and tracks them. When the slasher is the
@@ -31,62 +33,65 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
31
33
  * - Validators that reach the quorum threshold are slashed. A vote for slashing N units is also considered
32
34
  * a vote for slashing N-1, N-2, ..., 1 units. The system slashes for the largest amount that reaches quorum.
33
35
  * - The client monitors executable rounds and triggers execution when appropriate.
34
- *
35
- * Differences from Empire model
36
- * - No fixed slash payloads - votes are for individual validator offenses encoded in bytes
37
- * - The L1 contract determines which offenses reach quorum rather than nodes agreeing on a payload
38
- * - Proposers vote directly on which validators to slash and by how much
39
- * - Uses a slash offset to vote on validators from past rounds (e.g., round N votes on round N-2)
40
- */ export class TallySlasherClient {
36
+ */ export class SlasherClient {
41
37
  config;
42
38
  settings;
43
- tallySlashingProposer;
39
+ slashingProposer;
44
40
  slasher;
45
41
  rollup;
46
42
  epochCache;
47
43
  dateProvider;
48
44
  offensesStore;
45
+ ownValidators;
49
46
  log;
47
+ metrics;
50
48
  unwatchCallbacks;
51
49
  roundMonitor;
52
50
  offensesCollector;
53
- constructor(config, settings, tallySlashingProposer, slasher, rollup, watchers, epochCache, dateProvider, offensesStore, log = createLogger('slasher:consensus')){
51
+ ownValidatorMonitor;
52
+ constructor(config, settings, slashingProposer, slasher, rollup, watchers, epochCache, dateProvider, offensesStore, ownValidators = [], log = createLogger('slasher:consensus'), metrics = new SlasherMetrics(getTelemetryClient())){
54
53
  this.config = config;
55
54
  this.settings = settings;
56
- this.tallySlashingProposer = tallySlashingProposer;
55
+ this.slashingProposer = slashingProposer;
57
56
  this.slasher = slasher;
58
57
  this.rollup = rollup;
59
58
  this.epochCache = epochCache;
60
59
  this.dateProvider = dateProvider;
61
60
  this.offensesStore = offensesStore;
61
+ this.ownValidators = ownValidators;
62
62
  this.log = log;
63
+ this.metrics = metrics;
63
64
  this.unwatchCallbacks = [];
64
65
  this.roundMonitor = new SlashRoundMonitor(settings, dateProvider);
65
66
  this.offensesCollector = new SlashOffensesCollector(config, settings, watchers, offensesStore);
67
+ this.ownValidatorMonitor = new OwnValidatorSlashMonitor(slashingProposer, settings, ownValidators, this.metrics);
66
68
  }
67
69
  async start() {
68
- this.log.debug('Starting Tally Slasher client...');
70
+ this.log.debug('Starting slasher client...');
69
71
  this.roundMonitor.start();
70
72
  await this.offensesCollector.start();
71
73
  // Listen for RoundExecuted events
72
- this.unwatchCallbacks.push(this.tallySlashingProposer.listenToRoundExecuted(({ round, slashCount, l1BlockHash })=>void this.handleRoundExecuted(round, slashCount, l1BlockHash).catch((err)=>this.log.error('Error handling round executed', err))));
74
+ this.unwatchCallbacks.push(this.slashingProposer.listenToRoundExecuted(({ round, slashCount, l1BlockHash })=>void this.handleRoundExecuted(round, slashCount, l1BlockHash).catch((err)=>this.log.error('Error handling round executed', err))));
73
75
  // Check for round changes
74
76
  this.unwatchCallbacks.push(this.roundMonitor.listenToNewRound((round)=>this.handleNewRound(round)));
75
- this.log.info(`Started tally slasher client`);
77
+ // Warn early when a slashing vote names one of our own validators. Skipped entirely when running none, so
78
+ // those nodes pay no extra L1 subscription.
79
+ if (this.ownValidators.length > 0) {
80
+ this.ownValidatorMonitor.start(this.roundMonitor.getCurrentRound().round);
81
+ this.unwatchCallbacks.push(this.slashingProposer.listenToVoteCast(({ round, voteIndex })=>void this.ownValidatorMonitor.handleVoteCast(round, voteIndex)));
82
+ }
83
+ this.log.info(`Started slasher client`);
76
84
  return Promise.resolve();
77
85
  }
78
- /**
79
- * Stop the tally slasher client
80
- */ async stop() {
81
- this.log.debug('Stopping Tally Slasher client...');
86
+ /** Stop the slasher client */ async stop() {
87
+ this.log.debug('Stopping slasher client...');
82
88
  for (const unwatchCallback of this.unwatchCallbacks){
83
89
  unwatchCallback();
84
90
  }
85
91
  this.roundMonitor.stop();
92
+ await this.ownValidatorMonitor.stop();
86
93
  await this.offensesCollector.stop();
87
- // Sleeping to sidestep viem issue with unwatching events
88
- await sleep(2000);
89
- this.log.info('Tally Slasher client stopped');
94
+ this.log.info('Slasher client stopped');
90
95
  }
91
96
  /** Returns the current config */ getConfig() {
92
97
  return this.config;
@@ -98,14 +103,17 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
98
103
  };
99
104
  }
100
105
  /** Triggered on a time basis when we enter a new slashing round. Clears expired offenses. */ async handleNewRound(round) {
101
- this.log.info(`Starting new tally slashing round ${round}`);
106
+ this.log.info(`Starting new slashing round ${round}`);
107
+ this.ownValidatorMonitor.handleNewRound(round);
102
108
  await this.offensesCollector.handleNewRound(round);
103
109
  }
104
- /** Called when we see a RoundExecuted event on the TallySlashingProposer (just for logging). */ async handleRoundExecuted(round, slashCount, l1BlockHash) {
110
+ /** Called when we see a RoundExecuted event on the SlashingProposer (just for logging and metrics). */ async handleRoundExecuted(round, slashCount, l1BlockHash) {
111
+ this.metrics.recordRoundExecuted();
105
112
  const slashes = await this.rollup.getSlashEvents(l1BlockHash);
106
113
  this.log.info(`Slashing round ${round} has been executed with ${slashCount} slashes`, {
107
114
  slashes
108
115
  });
116
+ this.ownValidatorMonitor.handleSlashes(round, slashes, l1BlockHash);
109
117
  }
110
118
  /**
111
119
  * Get the actions the proposer should take for slashing
@@ -173,7 +181,7 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
173
181
  };
174
182
  this.log.debug(`Testing if slashing round ${executableRound} is executable`, logData);
175
183
  try {
176
- const roundInfo = await this.tallySlashingProposer.getRound(executableRound);
184
+ const roundInfo = await this.slashingProposer.getRound(executableRound);
177
185
  logData = {
178
186
  ...logData,
179
187
  roundInfo
@@ -189,19 +197,19 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
189
197
  return undefined;
190
198
  }
191
199
  // Check if round is ready to execute at the given slot
192
- const isReadyToExecute = await this.tallySlashingProposer.isRoundReadyToExecute(executableRound, slotNumber);
200
+ const isReadyToExecute = await this.slashingProposer.isRoundReadyToExecute(executableRound, slotNumber);
193
201
  if (!isReadyToExecute) {
194
202
  this.log.warn(`Round ${executableRound} is not ready to execute at slot ${slotNumber} according to contract check`, logData);
195
203
  return undefined;
196
204
  }
197
205
  // Check if the round yields any slashing at all
198
- const { actions: slashActions, committees } = await this.tallySlashingProposer.getTally(executableRound);
206
+ const { actions: slashActions, committees } = await this.slashingProposer.getTally(executableRound);
199
207
  if (slashActions.length === 0) {
200
208
  this.log.verbose(`Round ${executableRound} does not resolve in any slashing`, logData);
201
209
  return undefined;
202
210
  }
203
211
  // Check if the slash payload is vetoed
204
- const payload = await this.tallySlashingProposer.getPayload(executableRound);
212
+ const payload = await this.slashingProposer.getPayload(executableRound);
205
213
  const isVetoed = await this.slasher.isPayloadVetoed(payload.address);
206
214
  if (isVetoed) {
207
215
  this.log.warn(`Round ${executableRound} payload is vetoed (skipping execution)`, {
@@ -259,7 +267,7 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
259
267
  slotNumber,
260
268
  currentRound,
261
269
  slashedRound,
262
- offensesToForgive,
270
+ offensesFromAlwaysSlash: offensesFromAlwaysSlash.map(getOffenseLogData),
263
271
  slashValidatorsAlways: this.config.slashValidatorsAlways
264
272
  });
265
273
  }
@@ -268,7 +276,7 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
268
276
  slotNumber,
269
277
  currentRound,
270
278
  slashedRound,
271
- offensesToForgive,
279
+ offensesToForgive: offensesToForgive.map(getOffenseLogData),
272
280
  slashValidatorsNever: this.config.slashValidatorsNever
273
281
  });
274
282
  }
@@ -280,29 +288,36 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
280
288
  });
281
289
  return undefined;
282
290
  }
283
- const offensesToSlashLog = offensesToSlash.map((offense)=>({
284
- ...offense,
285
- amount: offense.amount.toString()
286
- }));
287
- this.log.info(`Voting to slash ${offensesToSlash.length} offenses`, {
291
+ this.log.debug(`Computing slash votes for ${offensesToSlash.length} offenses`, {
288
292
  slotNumber,
289
293
  currentRound,
290
294
  slashedRound,
291
- offensesToSlash: offensesToSlashLog
295
+ offensesToSlash: offensesToSlash.map(getOffenseLogData)
292
296
  });
293
297
  const committees = await this.collectCommitteesActiveDuringRound(slashedRound);
294
298
  const epochsForCommittees = getEpochsForRound(slashedRound, this.settings);
295
- const votes = getSlashConsensusVotesFromOffenses(offensesToSlash, committees, epochsForCommittees.map((e)=>BigInt(e)), this.settings);
299
+ const { slashMaxPayloadSize } = this.config;
300
+ const votes = getSlashConsensusVotesFromOffenses(offensesToSlash, committees, epochsForCommittees.map((e)=>BigInt(e)), {
301
+ ...this.settings,
302
+ maxSlashedValidators: slashMaxPayloadSize
303
+ }, this.log);
296
304
  if (votes.every((v)=>v === 0)) {
297
305
  this.log.warn(`Computed votes for offenses are all zero. Skipping vote.`, {
298
306
  slotNumber,
299
307
  currentRound,
300
308
  slashedRound,
301
- offensesToSlash,
309
+ offensesToSlash: offensesToSlash.map(getOffenseLogData),
302
310
  committees
303
311
  });
304
312
  return undefined;
305
313
  }
314
+ this.log.info(`Voting to slash ${offensesToSlash.length} offenses`, {
315
+ slotNumber,
316
+ slashedRound,
317
+ currentRound,
318
+ votes,
319
+ offensesToSlash: offensesToSlash.map(getOffenseLogData)
320
+ });
306
321
  this.log.debug(`Computed votes for slashing ${offensesToSlash.length} offenses`, {
307
322
  slashedRound,
308
323
  currentRound,
@@ -323,14 +338,8 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
323
338
  return Promise.all(epochsToSlash.map((epoch)=>this.epochCache.getCommitteeForEpoch(epoch).then((c)=>c.committee ?? emptyCommittee)));
324
339
  }
325
340
  /**
326
- * Get slash payloads is NOT SUPPORTED in tally model
327
- * @throws Error indicating this operation is not supported
328
- */ getSlashPayloads() {
329
- return Promise.reject(new Error('Tally slashing model does not support slash payloads'));
330
- }
331
- /**
332
341
  * Gather offenses to be slashed on a given round.
333
- * In tally slashing, round N slashes validators from round N - slashOffsetInRounds.
342
+ * Round N slashes validators from round N - slashOffsetInRounds.
334
343
  * @param round - The round to get offenses for, defaults to current round
335
344
  * @returns Array of pending offenses for the round with offset applied
336
345
  */ async gatherOffensesForRound(round) {
@@ -340,8 +349,8 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
340
349
  }
341
350
  return await this.offensesStore.getOffensesForRound(targetRound);
342
351
  }
343
- /** Returns all pending offenses stored */ getPendingOffenses() {
344
- return this.offensesStore.getPendingOffenses();
352
+ /** Returns all offenses stored */ getOffenses() {
353
+ return this.offensesStore.getOffenses();
345
354
  }
346
355
  /**
347
356
  * Returns the round to be slashed given the current round by applying the slash offset.
@@ -355,3 +364,10 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
355
364
  return round - BigInt(this.settings.slashingOffsetInRounds);
356
365
  }
357
366
  }
367
+ function getOffenseLogData(offense) {
368
+ return {
369
+ ...offense,
370
+ validator: offense.validator.toString(),
371
+ offenseType: getOffenseTypeName(offense.offenseType)
372
+ };
373
+ }