@aztec/slasher 0.0.1-commit.f146247c → 0.0.1-commit.f224bb98b

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 (37) hide show
  1. package/README.md +24 -14
  2. package/dest/config.d.ts +1 -1
  3. package/dest/config.d.ts.map +1 -1
  4. package/dest/config.js +13 -1
  5. package/dest/factory/create_facade.d.ts +3 -3
  6. package/dest/factory/create_facade.d.ts.map +1 -1
  7. package/dest/factory/create_facade.js +25 -2
  8. package/dest/factory/create_implementation.d.ts +4 -3
  9. package/dest/factory/create_implementation.d.ts.map +1 -1
  10. package/dest/factory/create_implementation.js +11 -7
  11. package/dest/factory/get_settings.d.ts +2 -2
  12. package/dest/factory/get_settings.d.ts.map +1 -1
  13. package/dest/generated/slasher-defaults.d.ts +4 -2
  14. package/dest/generated/slasher-defaults.d.ts.map +1 -1
  15. package/dest/generated/slasher-defaults.js +3 -1
  16. package/dest/slash_offenses_collector.d.ts +5 -2
  17. package/dest/slash_offenses_collector.d.ts.map +1 -1
  18. package/dest/slash_offenses_collector.js +3 -7
  19. package/dest/slasher_client_facade.d.ts +4 -3
  20. package/dest/slasher_client_facade.d.ts.map +1 -1
  21. package/dest/slasher_client_facade.js +4 -2
  22. package/dest/tally_slasher_client.d.ts +2 -2
  23. package/dest/tally_slasher_client.d.ts.map +1 -1
  24. package/dest/tally_slasher_client.js +6 -6
  25. package/dest/watchers/epoch_prune_watcher.d.ts +6 -5
  26. package/dest/watchers/epoch_prune_watcher.d.ts.map +1 -1
  27. package/dest/watchers/epoch_prune_watcher.js +43 -25
  28. package/package.json +9 -9
  29. package/src/config.ts +15 -1
  30. package/src/factory/create_facade.ts +32 -3
  31. package/src/factory/create_implementation.ts +29 -4
  32. package/src/factory/get_settings.ts +2 -2
  33. package/src/generated/slasher-defaults.ts +3 -1
  34. package/src/slash_offenses_collector.ts +9 -8
  35. package/src/slasher_client_facade.ts +3 -1
  36. package/src/tally_slasher_client.ts +8 -7
  37. package/src/watchers/epoch_prune_watcher.ts +57 -25
@@ -1,7 +1,7 @@
1
1
  import { EpochCache } from '@aztec/epoch-cache';
2
- import type { Fr } from '@aztec/foundation/curves/bn254';
2
+ import { EpochNumber } from '@aztec/foundation/branded-types';
3
3
  import { L2Block, type L2BlockSourceEventEmitter } from '@aztec/stdlib/block';
4
- import type { ICheckpointsBuilder, ITxProvider, MerkleTreeWriteOperations, SlasherConfig } from '@aztec/stdlib/interfaces/server';
4
+ import type { ICheckpointsBuilder, ITxProvider, SlasherConfig } from '@aztec/stdlib/interfaces/server';
5
5
  import { type L1ToL2MessageSource } from '@aztec/stdlib/messaging';
6
6
  import { type Watcher, type WatcherEmitter } from '../watcher.js';
7
7
  declare const EpochPruneWatcherPenaltiesConfigKeys: readonly ["slashPrunePenalty", "slashDataWithholdingPenalty"];
@@ -29,10 +29,11 @@ export declare class EpochPruneWatcher extends EpochPruneWatcher_base implements
29
29
  private handlePruneL2Blocks;
30
30
  private emitSlashForEpoch;
31
31
  private processPruneL2Blocks;
32
- validateBlocks(blocks: L2Block[]): Promise<void>;
33
- validateBlock(blockFromL1: L2Block, previousCheckpointOutHashes: Fr[], fork: MerkleTreeWriteOperations): Promise<void>;
32
+ validateBlocks(blocks: L2Block[], epochNumber: EpochNumber): Promise<void>;
33
+ private validateCheckpoint;
34
+ private validateBlockInCheckpoint;
34
35
  private getValidatorsForEpoch;
35
36
  private validatorsToSlashingArgs;
36
37
  }
37
38
  export {};
38
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXBvY2hfcHJ1bmVfd2F0Y2hlci5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3dhdGNoZXJzL2Vwb2NoX3BydW5lX3dhdGNoZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBR2hELE9BQU8sS0FBSyxFQUFFLEVBQUUsRUFBRSxNQUFNLGdDQUFnQyxDQUFDO0FBRXpELE9BQU8sRUFFTCxPQUFPLEVBQ1AsS0FBSyx5QkFBeUIsRUFHL0IsTUFBTSxxQkFBcUIsQ0FBQztBQUU3QixPQUFPLEtBQUssRUFDVixtQkFBbUIsRUFDbkIsV0FBVyxFQUNYLHlCQUF5QixFQUN6QixhQUFhLEVBQ2QsTUFBTSxpQ0FBaUMsQ0FBQztBQUN6QyxPQUFPLEVBQUUsS0FBSyxtQkFBbUIsRUFBNEIsTUFBTSx5QkFBeUIsQ0FBQztBQVk3RixPQUFPLEVBQTZDLEtBQUssT0FBTyxFQUFFLEtBQUssY0FBYyxFQUFFLE1BQU0sZUFBZSxDQUFDO0FBRTdHLFFBQUEsTUFBTSxvQ0FBb0MsK0RBQWdFLENBQUM7QUFFM0csS0FBSywwQkFBMEIsR0FBRyxJQUFJLENBQUMsYUFBYSxFQUFFLENBQUMsT0FBTyxvQ0FBb0MsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUM7O0FBRTdHOzs7OztHQUtHO0FBQ0gscUJBQWEsaUJBQWtCLFNBQVEsc0JBQTJDLFlBQVcsT0FBTztJQVNoRyxPQUFPLENBQUMsYUFBYTtJQUNyQixPQUFPLENBQUMsbUJBQW1CO0lBQzNCLE9BQU8sQ0FBQyxVQUFVO0lBQ2xCLE9BQU8sQ0FBQyxVQUFVO0lBQ2xCLE9BQU8sQ0FBQyxrQkFBa0I7SUFaNUIsT0FBTyxDQUFDLEdBQUcsQ0FBK0M7SUFHMUQsT0FBTyxDQUFDLHdCQUF3QixDQUF1QztJQUV2RSxPQUFPLENBQUMsU0FBUyxDQUE2QjtJQUU5QyxZQUNVLGFBQWEsRUFBRSx5QkFBeUIsRUFDeEMsbUJBQW1CLEVBQUUsbUJBQW1CLEVBQ3hDLFVBQVUsRUFBRSxVQUFVLEVBQ3RCLFVBQVUsRUFBRSxJQUFJLENBQUMsV0FBVyxFQUFFLGlCQUFpQixDQUFDLEVBQ2hELGtCQUFrQixFQUFFLG1CQUFtQixFQUMvQyxTQUFTLEVBQUUsMEJBQTBCLEVBT3RDO0lBRU0sS0FBSyxrQkFHWDtJQUVNLElBQUksa0JBR1Y7SUFFTSxZQUFZLENBQUMsTUFBTSxFQUFFLE9BQU8sQ0FBQyxhQUFhLENBQUMsR0FBRyxJQUFJLENBR3hEO0lBRUQsT0FBTyxDQUFDLG1CQUFtQjtZQU9iLGlCQUFpQjtZQVdqQixvQkFBb0I7SUF3QnJCLGNBQWMsQ0FBQyxNQUFNLEVBQUUsT0FBTyxFQUFFLEdBQUcsT0FBTyxDQUFDLElBQUksQ0FBQyxDQWtCNUQ7SUFFWSxhQUFhLENBQ3hCLFdBQVcsRUFBRSxPQUFPLEVBQ3BCLDJCQUEyQixFQUFFLEVBQUUsRUFBRSxFQUNqQyxJQUFJLEVBQUUseUJBQXlCLEdBQzlCLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0E4Q2Y7WUFFYSxxQkFBcUI7SUFTbkMsT0FBTyxDQUFDLHdCQUF3QjtDQWdCakMifQ==
39
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXBvY2hfcHJ1bmVfd2F0Y2hlci5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3dhdGNoZXJzL2Vwb2NoX3BydW5lX3dhdGNoZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQ2hELE9BQU8sRUFBZSxXQUFXLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQztBQUkzRSxPQUFPLEVBRUwsT0FBTyxFQUNQLEtBQUsseUJBQXlCLEVBRy9CLE1BQU0scUJBQXFCLENBQUM7QUFFN0IsT0FBTyxLQUFLLEVBRVYsbUJBQW1CLEVBQ25CLFdBQVcsRUFFWCxhQUFhLEVBQ2QsTUFBTSxpQ0FBaUMsQ0FBQztBQUN6QyxPQUFPLEVBQUUsS0FBSyxtQkFBbUIsRUFBNEIsTUFBTSx5QkFBeUIsQ0FBQztBQVk3RixPQUFPLEVBQTZDLEtBQUssT0FBTyxFQUFFLEtBQUssY0FBYyxFQUFFLE1BQU0sZUFBZSxDQUFDO0FBRTdHLFFBQUEsTUFBTSxvQ0FBb0MsK0RBQWdFLENBQUM7QUFFM0csS0FBSywwQkFBMEIsR0FBRyxJQUFJLENBQUMsYUFBYSxFQUFFLENBQUMsT0FBTyxvQ0FBb0MsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUM7O0FBRTdHOzs7OztHQUtHO0FBQ0gscUJBQWEsaUJBQWtCLFNBQVEsc0JBQTJDLFlBQVcsT0FBTztJQVNoRyxPQUFPLENBQUMsYUFBYTtJQUNyQixPQUFPLENBQUMsbUJBQW1CO0lBQzNCLE9BQU8sQ0FBQyxVQUFVO0lBQ2xCLE9BQU8sQ0FBQyxVQUFVO0lBQ2xCLE9BQU8sQ0FBQyxrQkFBa0I7SUFaNUIsT0FBTyxDQUFDLEdBQUcsQ0FBK0M7SUFHMUQsT0FBTyxDQUFDLHdCQUF3QixDQUF1QztJQUV2RSxPQUFPLENBQUMsU0FBUyxDQUE2QjtJQUU5QyxZQUNVLGFBQWEsRUFBRSx5QkFBeUIsRUFDeEMsbUJBQW1CLEVBQUUsbUJBQW1CLEVBQ3hDLFVBQVUsRUFBRSxVQUFVLEVBQ3RCLFVBQVUsRUFBRSxJQUFJLENBQUMsV0FBVyxFQUFFLGlCQUFpQixDQUFDLEVBQ2hELGtCQUFrQixFQUFFLG1CQUFtQixFQUMvQyxTQUFTLEVBQUUsMEJBQTBCLEVBT3RDO0lBRU0sS0FBSyxrQkFHWDtJQUVNLElBQUksa0JBR1Y7SUFFTSxZQUFZLENBQUMsTUFBTSxFQUFFLE9BQU8sQ0FBQyxhQUFhLENBQUMsR0FBRyxJQUFJLENBR3hEO0lBRUQsT0FBTyxDQUFDLG1CQUFtQjtZQU9iLGlCQUFpQjtZQVdqQixvQkFBb0I7SUF3QnJCLGNBQWMsQ0FBQyxNQUFNLEVBQUUsT0FBTyxFQUFFLEVBQUUsV0FBVyxFQUFFLFdBQVcsR0FBRyxPQUFPLENBQUMsSUFBSSxDQUFDLENBK0J0RjtZQUVhLGtCQUFrQjtZQXdDbEIseUJBQXlCO1lBOEJ6QixxQkFBcUI7SUFTbkMsT0FBTyxDQUFDLHdCQUF3QjtDQWdCakMifQ==
@@ -1 +1 @@
1
- {"version":3,"file":"epoch_prune_watcher.d.ts","sourceRoot":"","sources":["../../src/watchers/epoch_prune_watcher.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,gCAAgC,CAAC;AAEzD,OAAO,EAEL,OAAO,EACP,KAAK,yBAAyB,EAG/B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EACV,mBAAmB,EACnB,WAAW,EACX,yBAAyB,EACzB,aAAa,EACd,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,KAAK,mBAAmB,EAA4B,MAAM,yBAAyB,CAAC;AAY7F,OAAO,EAA6C,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAE7G,QAAA,MAAM,oCAAoC,+DAAgE,CAAC;AAE3G,KAAK,0BAA0B,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC,OAAO,oCAAoC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;;AAE7G;;;;;GAKG;AACH,qBAAa,iBAAkB,SAAQ,sBAA2C,YAAW,OAAO;IAShG,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,mBAAmB;IAC3B,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,kBAAkB;IAZ5B,OAAO,CAAC,GAAG,CAA+C;IAG1D,OAAO,CAAC,wBAAwB,CAAuC;IAEvE,OAAO,CAAC,SAAS,CAA6B;IAE9C,YACU,aAAa,EAAE,yBAAyB,EACxC,mBAAmB,EAAE,mBAAmB,EACxC,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,IAAI,CAAC,WAAW,EAAE,iBAAiB,CAAC,EAChD,kBAAkB,EAAE,mBAAmB,EAC/C,SAAS,EAAE,0BAA0B,EAOtC;IAEM,KAAK,kBAGX;IAEM,IAAI,kBAGV;IAEM,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI,CAGxD;IAED,OAAO,CAAC,mBAAmB;YAOb,iBAAiB;YAWjB,oBAAoB;IAwBrB,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAkB5D;IAEY,aAAa,CACxB,WAAW,EAAE,OAAO,EACpB,2BAA2B,EAAE,EAAE,EAAE,EACjC,IAAI,EAAE,yBAAyB,GAC9B,OAAO,CAAC,IAAI,CAAC,CA8Cf;YAEa,qBAAqB;IASnC,OAAO,CAAC,wBAAwB;CAgBjC"}
1
+ {"version":3,"file":"epoch_prune_watcher.d.ts","sourceRoot":"","sources":["../../src/watchers/epoch_prune_watcher.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAe,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAI3E,OAAO,EAEL,OAAO,EACP,KAAK,yBAAyB,EAG/B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAEV,mBAAmB,EACnB,WAAW,EAEX,aAAa,EACd,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,KAAK,mBAAmB,EAA4B,MAAM,yBAAyB,CAAC;AAY7F,OAAO,EAA6C,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAE7G,QAAA,MAAM,oCAAoC,+DAAgE,CAAC;AAE3G,KAAK,0BAA0B,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC,OAAO,oCAAoC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;;AAE7G;;;;;GAKG;AACH,qBAAa,iBAAkB,SAAQ,sBAA2C,YAAW,OAAO;IAShG,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,mBAAmB;IAC3B,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,kBAAkB;IAZ5B,OAAO,CAAC,GAAG,CAA+C;IAG1D,OAAO,CAAC,wBAAwB,CAAuC;IAEvE,OAAO,CAAC,SAAS,CAA6B;IAE9C,YACU,aAAa,EAAE,yBAAyB,EACxC,mBAAmB,EAAE,mBAAmB,EACxC,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,IAAI,CAAC,WAAW,EAAE,iBAAiB,CAAC,EAChD,kBAAkB,EAAE,mBAAmB,EAC/C,SAAS,EAAE,0BAA0B,EAOtC;IAEM,KAAK,kBAGX;IAEM,IAAI,kBAGV;IAEM,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI,CAGxD;IAED,OAAO,CAAC,mBAAmB;YAOb,iBAAiB;YAWjB,oBAAoB;IAwBrB,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA+BtF;YAEa,kBAAkB;YAwClB,yBAAyB;YA8BzB,qBAAqB;IASnC,OAAO,CAAC,wBAAwB;CAgBjC"}
@@ -1,5 +1,5 @@
1
- import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
2
- import { merge, pick } from '@aztec/foundation/collection';
1
+ import { BlockNumber } from '@aztec/foundation/branded-types';
2
+ import { chunkBy, merge, pick } from '@aztec/foundation/collection';
3
3
  import { createLogger } from '@aztec/foundation/log';
4
4
  import { L2BlockSourceEvents } from '@aztec/stdlib/block';
5
5
  import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
@@ -67,7 +67,7 @@ const EpochPruneWatcherPenaltiesConfigKeys = [
67
67
  this.log.info(`Detected chain prune. Validating epoch ${epochNumber} with blocks ${epochBlocks[0]?.number} to ${epochBlocks[epochBlocks.length - 1]?.number}.`, {
68
68
  blocks: epochBlocks.map((b)=>b.toBlockInfo())
69
69
  });
70
- await this.validateBlocks(epochBlocks);
70
+ await this.validateBlocks(epochBlocks, epochNumber);
71
71
  this.log.info(`Pruned epoch ${epochNumber} was valid. Want to slash committee for not having it proven.`);
72
72
  await this.emitSlashForEpoch(OffenseType.VALID_EPOCH_PRUNED, epochNumber);
73
73
  } catch (error) {
@@ -81,19 +81,26 @@ const EpochPruneWatcherPenaltiesConfigKeys = [
81
81
  }
82
82
  }
83
83
  }
84
- async validateBlocks(blocks) {
84
+ async validateBlocks(blocks, epochNumber) {
85
85
  if (blocks.length === 0) {
86
86
  return;
87
87
  }
88
- let previousCheckpointOutHashes = [];
89
- const fork = await this.checkpointsBuilder.getFork(BlockNumber(blocks[0].header.globalVariables.blockNumber - 1));
88
+ // Sort blocks by block number and group by checkpoint
89
+ const sortedBlocks = [
90
+ ...blocks
91
+ ].sort((a, b)=>a.number - b.number);
92
+ const blocksByCheckpoint = chunkBy(sortedBlocks, (b)=>b.checkpointNumber);
93
+ // Get prior checkpoints in the epoch (in case this was a partial prune) to extract the out hashes
94
+ const priorCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(epochNumber)).filter((c)=>c.checkpointNumber < sortedBlocks[0].checkpointNumber).map((c)=>c.checkpointOutHash);
95
+ let previousCheckpointOutHashes = [
96
+ ...priorCheckpointOutHashes
97
+ ];
98
+ const fork = await this.checkpointsBuilder.getFork(BlockNumber(sortedBlocks[0].header.globalVariables.blockNumber - 1));
90
99
  try {
91
- for (const block of blocks){
92
- await this.validateBlock(block, previousCheckpointOutHashes, fork);
93
- // TODO(mbps): This assumes one block per checkpoint, which is only true for now.
94
- const checkpointOutHash = computeCheckpointOutHash([
95
- block.body.txEffects.map((tx)=>tx.l2ToL1Msgs)
96
- ]);
100
+ for (const checkpointBlocks of blocksByCheckpoint){
101
+ await this.validateCheckpoint(checkpointBlocks, previousCheckpointOutHashes, fork);
102
+ // Compute checkpoint out hash from all blocks in this checkpoint
103
+ const checkpointOutHash = computeCheckpointOutHash(checkpointBlocks.map((b)=>b.body.txEffects.map((tx)=>tx.l2ToL1Msgs)));
97
104
  previousCheckpointOutHashes = [
98
105
  ...previousCheckpointOutHashes,
99
106
  checkpointOutHash
@@ -103,7 +110,30 @@ const EpochPruneWatcherPenaltiesConfigKeys = [
103
110
  await fork.close();
104
111
  }
105
112
  }
106
- async validateBlock(blockFromL1, previousCheckpointOutHashes, fork) {
113
+ async validateCheckpoint(checkpointBlocks, previousCheckpointOutHashes, fork) {
114
+ const checkpointNumber = checkpointBlocks[0].checkpointNumber;
115
+ this.log.debug(`Validating pruned checkpoint ${checkpointNumber} with ${checkpointBlocks.length} blocks`);
116
+ // Get L1ToL2Messages once for the entire checkpoint
117
+ const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
118
+ // Build checkpoint constants from first block's global variables
119
+ const gv = checkpointBlocks[0].header.globalVariables;
120
+ const constants = {
121
+ chainId: gv.chainId,
122
+ version: gv.version,
123
+ slotNumber: gv.slotNumber,
124
+ timestamp: gv.timestamp,
125
+ coinbase: gv.coinbase,
126
+ feeRecipient: gv.feeRecipient,
127
+ gasFees: gv.gasFees
128
+ };
129
+ // Start checkpoint builder once for all blocks in this checkpoint
130
+ const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(checkpointNumber, constants, 0n, l1ToL2Messages, previousCheckpointOutHashes, fork, this.log.getBindings());
131
+ // Validate all blocks in the checkpoint sequentially
132
+ for (const block of checkpointBlocks){
133
+ await this.validateBlockInCheckpoint(block, checkpointBuilder);
134
+ }
135
+ }
136
+ async validateBlockInCheckpoint(blockFromL1, checkpointBuilder) {
107
137
  this.log.debug(`Validating pruned block ${blockFromL1.header.globalVariables.blockNumber}`);
108
138
  const txHashes = blockFromL1.body.txEffects.map((txEffect)=>txEffect.txHash);
109
139
  // We load txs from the mempool directly, since the TxCollector running in the background has already been
@@ -113,19 +143,7 @@ const EpochPruneWatcherPenaltiesConfigKeys = [
113
143
  if (missingTxs && missingTxs.length > 0) {
114
144
  throw new TransactionsNotAvailableError(missingTxs);
115
145
  }
116
- const checkpointNumber = CheckpointNumber.fromBlockNumber(blockFromL1.number);
117
- const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
118
146
  const gv = blockFromL1.header.globalVariables;
119
- const constants = {
120
- chainId: gv.chainId,
121
- version: gv.version,
122
- slotNumber: gv.slotNumber,
123
- coinbase: gv.coinbase,
124
- feeRecipient: gv.feeRecipient,
125
- gasFees: gv.gasFees
126
- };
127
- // Use checkpoint builder to validate the block
128
- const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(checkpointNumber, constants, l1ToL2Messages, previousCheckpointOutHashes, fork, this.log.getBindings());
129
147
  const { block, failedTxs, numTxs } = await checkpointBuilder.buildBlock(txs, gv.blockNumber, gv.timestamp, {});
130
148
  if (numTxs !== txs.length) {
131
149
  // This should be detected by state mismatch, but this makes it easier to debug.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/slasher",
3
- "version": "0.0.1-commit.f146247c",
3
+ "version": "0.0.1-commit.f224bb98b",
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.f146247c",
60
- "@aztec/ethereum": "0.0.1-commit.f146247c",
61
- "@aztec/foundation": "0.0.1-commit.f146247c",
62
- "@aztec/kv-store": "0.0.1-commit.f146247c",
63
- "@aztec/l1-artifacts": "0.0.1-commit.f146247c",
64
- "@aztec/stdlib": "0.0.1-commit.f146247c",
65
- "@aztec/telemetry-client": "0.0.1-commit.f146247c",
59
+ "@aztec/epoch-cache": "0.0.1-commit.f224bb98b",
60
+ "@aztec/ethereum": "0.0.1-commit.f224bb98b",
61
+ "@aztec/foundation": "0.0.1-commit.f224bb98b",
62
+ "@aztec/kv-store": "0.0.1-commit.f224bb98b",
63
+ "@aztec/l1-artifacts": "0.0.1-commit.f224bb98b",
64
+ "@aztec/stdlib": "0.0.1-commit.f224bb98b",
65
+ "@aztec/telemetry-client": "0.0.1-commit.f224bb98b",
66
66
  "source-map-support": "^0.5.21",
67
67
  "tslib": "^2.4.0",
68
68
  "viem": "npm:@aztec/viem@2.38.2",
69
69
  "zod": "^3.23.8"
70
70
  },
71
71
  "devDependencies": {
72
- "@aztec/aztec.js": "0.0.1-commit.f146247c",
72
+ "@aztec/aztec.js": "0.0.1-commit.f224bb98b",
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
@@ -23,6 +23,8 @@ export const DefaultSlasherConfig: SlasherConfig = {
23
23
  slashInactivityTargetPercentage: slasherDefaultEnv.SLASH_INACTIVITY_TARGET_PERCENTAGE,
24
24
  slashInactivityConsecutiveEpochThreshold: slasherDefaultEnv.SLASH_INACTIVITY_CONSECUTIVE_EPOCH_THRESHOLD,
25
25
  slashBroadcastedInvalidBlockPenalty: BigInt(slasherDefaultEnv.SLASH_INVALID_BLOCK_PENALTY),
26
+ slashDuplicateProposalPenalty: BigInt(slasherDefaultEnv.SLASH_DUPLICATE_PROPOSAL_PENALTY),
27
+ slashDuplicateAttestationPenalty: BigInt(slasherDefaultEnv.SLASH_DUPLICATE_ATTESTATION_PENALTY),
26
28
  slashInactivityPenalty: BigInt(slasherDefaultEnv.SLASH_INACTIVITY_PENALTY),
27
29
  slashProposeInvalidAttestationsPenalty: BigInt(slasherDefaultEnv.SLASH_PROPOSE_INVALID_ATTESTATIONS_PENALTY),
28
30
  slashAttestDescendantOfInvalidPenalty: BigInt(slasherDefaultEnv.SLASH_ATTEST_DESCENDANT_OF_INVALID_PENALTY),
@@ -88,6 +90,17 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
88
90
  description: 'Penalty amount for slashing a validator for an invalid block proposed via p2p.',
89
91
  ...bigintConfigHelper(DefaultSlasherConfig.slashBroadcastedInvalidBlockPenalty),
90
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:
@@ -142,7 +155,8 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
142
155
  ...numberConfigHelper(DefaultSlasherConfig.slashMaxPayloadSize),
143
156
  },
144
157
  slashGracePeriodL2Slots: {
145
- description: 'Number of L2 slots to wait before considering a slashing offense expired.',
158
+ description:
159
+ 'Number of L2 slots after the network upgrade during which slashing offenses are ignored. The upgrade time is determined from the CanonicalRollupUpdated event.',
146
160
  env: 'SLASH_GRACE_PERIOD_L2_SLOTS',
147
161
  ...numberConfigHelper(DefaultSlasherConfig.slashGracePeriodL2Slots),
148
162
  },
@@ -1,14 +1,16 @@
1
1
  import { EpochCache } from '@aztec/epoch-cache';
2
- import { RollupContract } from '@aztec/ethereum/contracts';
2
+ import { RegistryContract, RollupContract } from '@aztec/ethereum/contracts';
3
3
  import type { L1ReaderConfig } from '@aztec/ethereum/l1-reader';
4
4
  import type { ViemClient } from '@aztec/ethereum/types';
5
+ import { SlotNumber } from '@aztec/foundation/branded-types';
5
6
  import { unique } from '@aztec/foundation/collection';
6
7
  import { EthAddress } from '@aztec/foundation/eth-address';
7
8
  import { createLogger } from '@aztec/foundation/log';
8
9
  import { DateProvider } from '@aztec/foundation/timer';
9
- import type { DataStoreConfig } from '@aztec/kv-store/config';
10
10
  import { createStore } from '@aztec/kv-store/lmdb-v2';
11
+ import { getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
11
12
  import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
13
+ import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
12
14
 
13
15
  import { SlasherClientFacade } from '../slasher_client_facade.js';
14
16
  import type { SlasherClientInterface } from '../slasher_client_interface.js';
@@ -18,7 +20,7 @@ import type { Watcher } from '../watcher.js';
18
20
  /** Creates a slasher client facade that updates itself whenever the rollup slasher changes */
19
21
  export async function createSlasherFacade(
20
22
  config: SlasherConfig & DataStoreConfig & { ethereumSlotDuration: number },
21
- l1Contracts: Pick<L1ReaderConfig['l1Contracts'], 'rollupAddress' | 'slashFactoryAddress'>,
23
+ l1Contracts: Pick<L1ReaderConfig['l1Contracts'], 'rollupAddress' | 'slashFactoryAddress' | 'registryAddress'>,
22
24
  l1Client: ViemClient,
23
25
  watchers: Watcher[],
24
26
  dateProvider: DateProvider,
@@ -34,6 +36,32 @@ export async function createSlasherFacade(
34
36
  const kvStore = await createStore('slasher', SCHEMA_VERSION, config, logger.getBindings());
35
37
  const rollup = new RollupContract(l1Client, l1Contracts.rollupAddress);
36
38
 
39
+ // Compute and cache the L2 slot at which the rollup was registered as canonical
40
+ const settingsMap = kvStore.openMap<string, number>('slasher-settings');
41
+ const cacheKey = `registeredSlot:${l1Contracts.rollupAddress}`;
42
+ let rollupRegisteredAtL2Slot = (await settingsMap.getAsync(cacheKey)) as SlotNumber | undefined;
43
+
44
+ if (rollupRegisteredAtL2Slot === undefined) {
45
+ const registry = new RegistryContract(l1Client, l1Contracts.registryAddress);
46
+ const l1StartBlock = await rollup.getL1StartBlock();
47
+ const registrationTimestamp = await registry.getCanonicalRollupRegistrationTimestamp(
48
+ l1Contracts.rollupAddress,
49
+ l1StartBlock,
50
+ );
51
+ if (registrationTimestamp !== undefined) {
52
+ const l1GenesisTime = await rollup.getL1GenesisTime();
53
+ const slotDuration = await rollup.getSlotDuration();
54
+ rollupRegisteredAtL2Slot = getSlotAtTimestamp(registrationTimestamp, {
55
+ l1GenesisTime,
56
+ slotDuration: Number(slotDuration),
57
+ });
58
+ } else {
59
+ rollupRegisteredAtL2Slot = SlotNumber(0);
60
+ }
61
+ await settingsMap.set(cacheKey, rollupRegisteredAtL2Slot);
62
+ logger.info(`Canonical rollup registered at L2 slot ${rollupRegisteredAtL2Slot}`);
63
+ }
64
+
37
65
  const slashValidatorsNever = config.slashSelfAllowed
38
66
  ? config.slashValidatorsNever
39
67
  : unique([...config.slashValidatorsNever, ...validatorAddresses].map(a => a.toString())).map(EthAddress.fromString);
@@ -48,6 +76,7 @@ export async function createSlasherFacade(
48
76
  epochCache,
49
77
  dateProvider,
50
78
  kvStore,
79
+ rollupRegisteredAtL2Slot,
51
80
  logger,
52
81
  );
53
82
  }
@@ -5,12 +5,13 @@ import {
5
5
  TallySlashingProposerContract,
6
6
  } from '@aztec/ethereum/contracts';
7
7
  import type { ViemClient } from '@aztec/ethereum/types';
8
+ import type { SlotNumber } from '@aztec/foundation/branded-types';
8
9
  import { EthAddress } from '@aztec/foundation/eth-address';
9
10
  import { createLogger } from '@aztec/foundation/log';
10
11
  import { DateProvider } from '@aztec/foundation/timer';
11
- import type { DataStoreConfig } from '@aztec/kv-store/config';
12
12
  import { AztecLMDBStoreV2 } from '@aztec/kv-store/lmdb-v2';
13
13
  import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
14
+ import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
14
15
  import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
15
16
 
16
17
  import { EmpireSlasherClient, type EmpireSlasherSettings } from '../empire_slasher_client.js';
@@ -31,19 +32,40 @@ export async function createSlasherImplementation(
31
32
  epochCache: EpochCache,
32
33
  dateProvider: DateProvider,
33
34
  kvStore: AztecLMDBStoreV2,
35
+ rollupRegisteredAtL2Slot: SlotNumber,
34
36
  logger = createLogger('slasher'),
35
37
  ) {
36
38
  const proposer = await rollup.getSlashingProposer();
37
39
  if (!proposer) {
38
40
  return new NullSlasherClient(config);
39
41
  } else if (proposer.type === 'tally') {
40
- return createTallySlasher(config, rollup, proposer, watchers, dateProvider, epochCache, kvStore, logger);
42
+ return createTallySlasher(
43
+ config,
44
+ rollup,
45
+ proposer,
46
+ watchers,
47
+ dateProvider,
48
+ epochCache,
49
+ kvStore,
50
+ rollupRegisteredAtL2Slot,
51
+ logger,
52
+ );
41
53
  } else {
42
54
  if (!slashFactoryAddress || slashFactoryAddress.equals(EthAddress.ZERO)) {
43
55
  throw new Error('Cannot initialize an empire-based SlasherClient without a SlashFactory address');
44
56
  }
45
57
  const slashFactory = new SlashFactoryContract(l1Client, slashFactoryAddress.toString());
46
- return createEmpireSlasher(config, rollup, proposer, slashFactory, watchers, dateProvider, kvStore, logger);
58
+ return createEmpireSlasher(
59
+ config,
60
+ rollup,
61
+ proposer,
62
+ slashFactory,
63
+ watchers,
64
+ dateProvider,
65
+ kvStore,
66
+ rollupRegisteredAtL2Slot,
67
+ logger,
68
+ );
47
69
  }
48
70
  }
49
71
 
@@ -55,6 +77,7 @@ async function createEmpireSlasher(
55
77
  watchers: Watcher[],
56
78
  dateProvider: DateProvider,
57
79
  kvStore: AztecLMDBStoreV2,
80
+ rollupRegisteredAtL2Slot: SlotNumber,
58
81
  logger = createLogger('slasher'),
59
82
  ): Promise<EmpireSlasherClient> {
60
83
  if (slashingProposer.type !== 'empire') {
@@ -97,6 +120,7 @@ async function createEmpireSlasher(
97
120
  l1StartBlock,
98
121
  ethereumSlotDuration: config.ethereumSlotDuration,
99
122
  slashingAmounts: undefined,
123
+ rollupRegisteredAtL2Slot,
100
124
  };
101
125
 
102
126
  const payloadsStore = new SlasherPayloadsStore(kvStore, {
@@ -130,13 +154,14 @@ async function createTallySlasher(
130
154
  dateProvider: DateProvider,
131
155
  epochCache: EpochCache,
132
156
  kvStore: AztecLMDBStoreV2,
157
+ rollupRegisteredAtL2Slot: SlotNumber,
133
158
  logger = createLogger('slasher'),
134
159
  ): Promise<TallySlasherClient> {
135
160
  if (slashingProposer.type !== 'tally') {
136
161
  throw new Error('Slashing proposer contract is not of type tally');
137
162
  }
138
163
 
139
- const settings = await getTallySlasherSettings(rollup, slashingProposer);
164
+ const settings = { ...(await getTallySlasherSettings(rollup, slashingProposer)), rollupRegisteredAtL2Slot };
140
165
  const slasher = await rollup.getSlasherContract();
141
166
 
142
167
  const offensesStore = new SlasherOffensesStore(kvStore, {
@@ -5,7 +5,7 @@ import type { TallySlasherSettings } from '../tally_slasher_client.js';
5
5
  export async function getTallySlasherSettings(
6
6
  rollup: RollupContract,
7
7
  slashingProposer?: TallySlashingProposerContract,
8
- ): Promise<TallySlasherSettings> {
8
+ ): Promise<Omit<TallySlasherSettings, 'rollupRegisteredAtL2Slot'>> {
9
9
  if (!slashingProposer) {
10
10
  const rollupSlashingProposer = await rollup.getSlashingProposer();
11
11
  if (!rollupSlashingProposer || rollupSlashingProposer.type !== 'tally') {
@@ -40,7 +40,7 @@ export async function getTallySlasherSettings(
40
40
  rollup.getTargetCommitteeSize(),
41
41
  ]);
42
42
 
43
- const settings: TallySlasherSettings = {
43
+ const settings: Omit<TallySlasherSettings, 'rollupRegisteredAtL2Slot'> = {
44
44
  slashingExecutionDelayInRounds: Number(slashingExecutionDelayInRounds),
45
45
  slashingRoundSize: Number(slashingRoundSize),
46
46
  slashingRoundSizeInEpochs: Number(slashingRoundSizeInEpochs),
@@ -6,7 +6,7 @@ export const slasherDefaultEnv = {
6
6
  SLASH_MIN_PENALTY_PERCENTAGE: 0.5,
7
7
  SLASH_MAX_PENALTY_PERCENTAGE: 2,
8
8
  SLASH_OFFENSE_EXPIRATION_ROUNDS: 4,
9
- SLASH_MAX_PAYLOAD_SIZE: 50,
9
+ SLASH_MAX_PAYLOAD_SIZE: 80,
10
10
  SLASH_EXECUTE_ROUNDS_LOOK_BACK: 4,
11
11
  SLASH_PRUNE_PENALTY: 10000000000000000000,
12
12
  SLASH_DATA_WITHHOLDING_PENALTY: 10000000000000000000,
@@ -15,6 +15,8 @@ export const slasherDefaultEnv = {
15
15
  SLASH_INACTIVITY_PENALTY: 10000000000000000000,
16
16
  SLASH_PROPOSE_INVALID_ATTESTATIONS_PENALTY: 10000000000000000000,
17
17
  SLASH_ATTEST_DESCENDANT_OF_INVALID_PENALTY: 10000000000000000000,
18
+ SLASH_DUPLICATE_PROPOSAL_PENALTY: 0,
19
+ SLASH_DUPLICATE_ATTESTATION_PENALTY: 0,
18
20
  SLASH_UNKNOWN_PENALTY: 10000000000000000000,
19
21
  SLASH_INVALID_BLOCK_PENALTY: 10000000000000000000,
20
22
  SLASH_GRACE_PERIOD_L2_SLOTS: 0,
@@ -1,3 +1,4 @@
1
+ import type { SlotNumber } from '@aztec/foundation/branded-types';
1
2
  import { createLogger } from '@aztec/foundation/log';
2
3
  import type { Prettify } from '@aztec/foundation/types';
3
4
  import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
@@ -9,7 +10,11 @@ import { WANT_TO_SLASH_EVENT, type WantToSlashArgs, type Watcher } from './watch
9
10
 
10
11
  export type SlashOffensesCollectorConfig = Prettify<Pick<SlasherConfig, 'slashGracePeriodL2Slots'>>;
11
12
  export type SlashOffensesCollectorSettings = Prettify<
12
- Pick<L1RollupConstants, 'epochDuration'> & { slashingAmounts: [bigint, bigint, bigint] | undefined }
13
+ Pick<L1RollupConstants, 'epochDuration'> & {
14
+ slashingAmounts: [bigint, bigint, bigint] | undefined;
15
+ /** L2 slot at which the rollup was registered as canonical in the Registry. Used to anchor the slash grace period. */
16
+ rollupRegisteredAtL2Slot: SlotNumber;
17
+ }
13
18
  >;
14
19
 
15
20
  /**
@@ -85,11 +90,7 @@ export class SlashOffensesCollector {
85
90
  }
86
91
  }
87
92
 
88
- this.log.info(`Adding pending offense for validator ${arg.validator}`, {
89
- ...pendingOffense,
90
- epochOrSlot: pendingOffense.epochOrSlot.toString(),
91
- amount: pendingOffense.amount.toString(),
92
- });
93
+ this.log.info(`Adding pending offense for validator ${arg.validator}`, pendingOffense);
93
94
  await this.offensesStore.addPendingOffense(pendingOffense);
94
95
  }
95
96
  }
@@ -114,9 +115,9 @@ export class SlashOffensesCollector {
114
115
  return this.offensesStore.markAsSlashed(offenses);
115
116
  }
116
117
 
117
- /** Returns whether to skip an offense if it happened during the grace period at the beginning of the chain */
118
+ /** Returns whether to skip an offense if it happened during the grace period after the network upgrade */
118
119
  private shouldSkipOffense(offense: Offense): boolean {
119
120
  const offenseSlot = getSlotForOffense(offense, this.settings);
120
- return offenseSlot < this.config.slashGracePeriodL2Slots;
121
+ return offenseSlot < this.settings.rollupRegisteredAtL2Slot + this.config.slashGracePeriodL2Slots;
121
122
  }
122
123
  }
@@ -5,9 +5,9 @@ import type { SlotNumber } from '@aztec/foundation/branded-types';
5
5
  import { EthAddress } from '@aztec/foundation/eth-address';
6
6
  import { createLogger } from '@aztec/foundation/log';
7
7
  import { DateProvider } from '@aztec/foundation/timer';
8
- import type { DataStoreConfig } from '@aztec/kv-store/config';
9
8
  import { AztecLMDBStoreV2 } from '@aztec/kv-store/lmdb-v2';
10
9
  import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
10
+ import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
11
11
  import type { Offense, ProposerSlashAction, SlashPayloadRound } from '@aztec/stdlib/slashing';
12
12
 
13
13
  import { createSlasherImplementation } from './factory/create_implementation.js';
@@ -32,6 +32,7 @@ export class SlasherClientFacade implements SlasherClientInterface {
32
32
  private epochCache: EpochCache,
33
33
  private dateProvider: DateProvider,
34
34
  private kvStore: AztecLMDBStoreV2,
35
+ private rollupRegisteredAtL2Slot: SlotNumber,
35
36
  private logger = createLogger('slasher'),
36
37
  ) {}
37
38
 
@@ -88,6 +89,7 @@ export class SlasherClientFacade implements SlasherClientInterface {
88
89
  this.epochCache,
89
90
  this.dateProvider,
90
91
  this.kvStore,
92
+ this.rollupRegisteredAtL2Slot,
91
93
  this.logger,
92
94
  );
93
95
  }
@@ -46,7 +46,10 @@ export type TallySlasherSettings = Prettify<
46
46
  >;
47
47
 
48
48
  export type TallySlasherClientConfig = SlashOffensesCollectorConfig &
49
- Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever' | 'slashExecuteRoundsLookBack'>;
49
+ Pick<
50
+ SlasherConfig,
51
+ 'slashValidatorsAlways' | 'slashValidatorsNever' | 'slashExecuteRoundsLookBack' | 'slashMaxPayloadSize'
52
+ >;
50
53
 
51
54
  /**
52
55
  * The Tally Slasher client is responsible for managing slashable offenses using
@@ -349,24 +352,22 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
349
352
  return undefined;
350
353
  }
351
354
 
352
- const offensesToSlashLog = offensesToSlash.map(offense => ({
353
- ...offense,
354
- amount: offense.amount.toString(),
355
- }));
356
355
  this.log.info(`Voting to slash ${offensesToSlash.length} offenses`, {
357
356
  slotNumber,
358
357
  currentRound,
359
358
  slashedRound,
360
- offensesToSlash: offensesToSlashLog,
359
+ offensesToSlash,
361
360
  });
362
361
 
363
362
  const committees = await this.collectCommitteesActiveDuringRound(slashedRound);
364
363
  const epochsForCommittees = getEpochsForRound(slashedRound, this.settings);
364
+ const { slashMaxPayloadSize } = this.config;
365
365
  const votes = getSlashConsensusVotesFromOffenses(
366
366
  offensesToSlash,
367
367
  committees,
368
368
  epochsForCommittees.map(e => BigInt(e)),
369
- this.settings,
369
+ { ...this.settings, maxSlashedValidators: slashMaxPayloadSize },
370
+ this.log,
370
371
  );
371
372
  if (votes.every(v => v === 0)) {
372
373
  this.log.warn(`Computed votes for offenses are all zero. Skipping vote.`, {