@aztec/slasher 0.0.1-commit.e3c1de76 → 0.0.1-commit.e558bd1c

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.
package/README.md CHANGED
@@ -137,9 +137,15 @@ List of all slashable offenses in the system:
137
137
  **Time Unit**: Slot-based offense.
138
138
 
139
139
  ### ATTESTED_DESCENDANT_OF_INVALID
140
- **Description**: A committee member attested to a block built on top of an invalid ancestor.
141
- **Detection**: AttestationsBlockWatcher tracks invalid blocks and their descendants.
142
- **Target**: Committee members who attested to the descendant block.
140
+ **Description**: A committee member attested to a block built on top of an invalid ancestor.
141
+ **Detection**: AttestationsBlockWatcher tracks invalid blocks and their descendants.
142
+ **Target**: Committee members who attested to the descendant block.
143
+ **Time Unit**: Slot-based offense.
144
+
145
+ ### DUPLICATE_PROPOSAL
146
+ **Description**: A proposer sent multiple block or checkpoint proposals for the same position (slot and indexWithinCheckpoint for blocks, or slot for checkpoints) with different content. Since each slot has exactly one designated proposer, sending conflicting proposals is equivocation.
147
+ **Detection**: Detected in the P2P layer when proposals are received. The AttestationPool tracks proposals by position; when a second proposal arrives for the same position with a different archive, it flags the duplicate. The first duplicate is propagated (Accept) so other validators can witness the offense.
148
+ **Target**: Proposer who broadcast the duplicate proposal.
143
149
  **Time Unit**: Slot-based offense.
144
150
 
145
151
  ## Configuration
@@ -175,6 +181,7 @@ These settings are configured locally on each validator node:
175
181
  - `slashDataWithholdingPenalty`: Penalty for DATA_WITHHOLDING
176
182
  - `slashInactivityPenalty`: Penalty for INACTIVITY
177
183
  - `slashBroadcastedInvalidBlockPenalty`: Penalty for BROADCASTED_INVALID_BLOCK_PROPOSAL
184
+ - `slashDuplicateProposalPenalty`: Penalty for DUPLICATE_PROPOSAL
178
185
  - `slashProposeInvalidAttestationsPenalty`: Penalty for PROPOSED_INSUFFICIENT_ATTESTATIONS and PROPOSED_INCORRECT_ATTESTATIONS
179
186
  - `slashAttestDescendantOfInvalidPenalty`: Penalty for ATTESTED_DESCENDANT_OF_INVALID
180
187
  - `slashUnknownPenalty`: Default penalty for unknown offense types
package/dest/config.d.ts CHANGED
@@ -3,4 +3,4 @@ import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
3
3
  export type { SlasherConfig };
4
4
  export declare const DefaultSlasherConfig: SlasherConfig;
5
5
  export declare const slasherConfigMappings: ConfigMappingsType<SlasherConfig>;
6
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uZmlnLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvY29uZmlnLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFRbkUsT0FBTyxLQUFLLEVBQUUsYUFBYSxFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFJckUsWUFBWSxFQUFFLGFBQWEsRUFBRSxDQUFDO0FBRTlCLGVBQU8sTUFBTSxvQkFBb0IsRUFBRSxhQW9CbEMsQ0FBQztBQUVGLGVBQU8sTUFBTSxxQkFBcUIsRUFBRSxrQkFBa0IsQ0FBQyxhQUFhLENBeUhuRSxDQUFDIn0=
6
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uZmlnLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvY29uZmlnLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFRbkUsT0FBTyxLQUFLLEVBQUUsYUFBYSxFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFJckUsWUFBWSxFQUFFLGFBQWEsRUFBRSxDQUFDO0FBRTlCLGVBQU8sTUFBTSxvQkFBb0IsRUFBRSxhQXFCbEMsQ0FBQztBQUVGLGVBQU8sTUFBTSxxQkFBcUIsRUFBRSxrQkFBa0IsQ0FBQyxhQUFhLENBOEhuRSxDQUFDIn0=
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAQnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAIrE,YAAY,EAAE,aAAa,EAAE,CAAC;AAE9B,eAAO,MAAM,oBAAoB,EAAE,aAoBlC,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,kBAAkB,CAAC,aAAa,CAyHnE,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAQnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAIrE,YAAY,EAAE,aAAa,EAAE,CAAC;AAE9B,eAAO,MAAM,oBAAoB,EAAE,aAqBlC,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,kBAAkB,CAAC,aAAa,CA8HnE,CAAC"}
package/dest/config.js CHANGED
@@ -12,6 +12,7 @@ export const DefaultSlasherConfig = {
12
12
  slashInactivityTargetPercentage: slasherDefaultEnv.SLASH_INACTIVITY_TARGET_PERCENTAGE,
13
13
  slashInactivityConsecutiveEpochThreshold: slasherDefaultEnv.SLASH_INACTIVITY_CONSECUTIVE_EPOCH_THRESHOLD,
14
14
  slashBroadcastedInvalidBlockPenalty: BigInt(slasherDefaultEnv.SLASH_INVALID_BLOCK_PENALTY),
15
+ slashDuplicateProposalPenalty: BigInt(slasherDefaultEnv.SLASH_DUPLICATE_PROPOSAL_PENALTY),
15
16
  slashInactivityPenalty: BigInt(slasherDefaultEnv.SLASH_INACTIVITY_PENALTY),
16
17
  slashProposeInvalidAttestationsPenalty: BigInt(slasherDefaultEnv.SLASH_PROPOSE_INVALID_ATTESTATIONS_PENALTY),
17
18
  slashAttestDescendantOfInvalidPenalty: BigInt(slasherDefaultEnv.SLASH_ATTEST_DESCENDANT_OF_INVALID_PENALTY),
@@ -66,6 +67,11 @@ export const slasherConfigMappings = {
66
67
  description: 'Penalty amount for slashing a validator for an invalid block proposed via p2p.',
67
68
  ...bigintConfigHelper(DefaultSlasherConfig.slashBroadcastedInvalidBlockPenalty)
68
69
  },
70
+ slashDuplicateProposalPenalty: {
71
+ env: 'SLASH_DUPLICATE_PROPOSAL_PENALTY',
72
+ description: 'Penalty amount for slashing a validator for sending duplicate proposals.',
73
+ ...bigintConfigHelper(DefaultSlasherConfig.slashDuplicateProposalPenalty)
74
+ },
69
75
  slashInactivityTargetPercentage: {
70
76
  env: 'SLASH_INACTIVITY_TARGET_PERCENTAGE',
71
77
  description: 'Missed attestation percentage to trigger creation of inactivity slash payload (0, 1]. Must be greater than 0',
@@ -12,8 +12,9 @@ export declare const slasherDefaultEnv: {
12
12
  readonly SLASH_INACTIVITY_PENALTY: 10000000000000000000;
13
13
  readonly SLASH_PROPOSE_INVALID_ATTESTATIONS_PENALTY: 10000000000000000000;
14
14
  readonly SLASH_ATTEST_DESCENDANT_OF_INVALID_PENALTY: 10000000000000000000;
15
+ readonly SLASH_DUPLICATE_PROPOSAL_PENALTY: 10000000000000000000;
15
16
  readonly SLASH_UNKNOWN_PENALTY: 10000000000000000000;
16
17
  readonly SLASH_INVALID_BLOCK_PENALTY: 10000000000000000000;
17
18
  readonly SLASH_GRACE_PERIOD_L2_SLOTS: 0;
18
19
  };
19
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhc2hlci1kZWZhdWx0cy5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2dlbmVyYXRlZC9zbGFzaGVyLWRlZmF1bHRzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUdBLHFFQUFxRTtBQUNyRSxlQUFPLE1BQU0saUJBQWlCOzs7Ozs7Ozs7Ozs7Ozs7O0NBZ0JwQixDQUFDIn0=
20
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhc2hlci1kZWZhdWx0cy5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2dlbmVyYXRlZC9zbGFzaGVyLWRlZmF1bHRzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUdBLHFFQUFxRTtBQUNyRSxlQUFPLE1BQU0saUJBQWlCOzs7Ozs7Ozs7Ozs7Ozs7OztDQWlCcEIsQ0FBQyJ9
@@ -1 +1 @@
1
- {"version":3,"file":"slasher-defaults.d.ts","sourceRoot":"","sources":["../../src/generated/slasher-defaults.ts"],"names":[],"mappings":"AAGA,qEAAqE;AACrE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;CAgBpB,CAAC"}
1
+ {"version":3,"file":"slasher-defaults.d.ts","sourceRoot":"","sources":["../../src/generated/slasher-defaults.ts"],"names":[],"mappings":"AAGA,qEAAqE;AACrE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;CAiBpB,CAAC"}
@@ -13,6 +13,7 @@
13
13
  SLASH_INACTIVITY_PENALTY: 10000000000000000000,
14
14
  SLASH_PROPOSE_INVALID_ATTESTATIONS_PENALTY: 10000000000000000000,
15
15
  SLASH_ATTEST_DESCENDANT_OF_INVALID_PENALTY: 10000000000000000000,
16
+ SLASH_DUPLICATE_PROPOSAL_PENALTY: 10000000000000000000,
16
17
  SLASH_UNKNOWN_PENALTY: 10000000000000000000,
17
18
  SLASH_INVALID_BLOCK_PENALTY: 10000000000000000000,
18
19
  SLASH_GRACE_PERIOD_L2_SLOTS: 0
@@ -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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXBvY2hfcHJ1bmVfd2F0Y2hlci5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3dhdGNoZXJzL2Vwb2NoX3BydW5lX3dhdGNoZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQ2hELE9BQU8sRUFBZSxXQUFXLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQztBQUkzRSxPQUFPLEVBRUwsT0FBTyxFQUNQLEtBQUsseUJBQXlCLEVBRy9CLE1BQU0scUJBQXFCLENBQUM7QUFFN0IsT0FBTyxLQUFLLEVBRVYsbUJBQW1CLEVBQ25CLFdBQVcsRUFFWCxhQUFhLEVBQ2QsTUFBTSxpQ0FBaUMsQ0FBQztBQUN6QyxPQUFPLEVBQUUsS0FBSyxtQkFBbUIsRUFBNEIsTUFBTSx5QkFBeUIsQ0FBQztBQVk3RixPQUFPLEVBQTZDLEtBQUssT0FBTyxFQUFFLEtBQUssY0FBYyxFQUFFLE1BQU0sZUFBZSxDQUFDO0FBRTdHLFFBQUEsTUFBTSxvQ0FBb0MsK0RBQWdFLENBQUM7QUFFM0csS0FBSywwQkFBMEIsR0FBRyxJQUFJLENBQUMsYUFBYSxFQUFFLENBQUMsT0FBTyxvQ0FBb0MsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUM7O0FBRTdHOzs7OztHQUtHO0FBQ0gscUJBQWEsaUJBQWtCLFNBQVEsc0JBQTJDLFlBQVcsT0FBTztJQVNoRyxPQUFPLENBQUMsYUFBYTtJQUNyQixPQUFPLENBQUMsbUJBQW1CO0lBQzNCLE9BQU8sQ0FBQyxVQUFVO0lBQ2xCLE9BQU8sQ0FBQyxVQUFVO0lBQ2xCLE9BQU8sQ0FBQyxrQkFBa0I7SUFaNUIsT0FBTyxDQUFDLEdBQUcsQ0FBK0M7SUFHMUQsT0FBTyxDQUFDLHdCQUF3QixDQUF1QztJQUV2RSxPQUFPLENBQUMsU0FBUyxDQUE2QjtJQUU5QyxZQUNVLGFBQWEsRUFBRSx5QkFBeUIsRUFDeEMsbUJBQW1CLEVBQUUsbUJBQW1CLEVBQ3hDLFVBQVUsRUFBRSxVQUFVLEVBQ3RCLFVBQVUsRUFBRSxJQUFJLENBQUMsV0FBVyxFQUFFLGlCQUFpQixDQUFDLEVBQ2hELGtCQUFrQixFQUFFLG1CQUFtQixFQUMvQyxTQUFTLEVBQUUsMEJBQTBCLEVBT3RDO0lBRU0sS0FBSyxrQkFHWDtJQUVNLElBQUksa0JBR1Y7SUFFTSxZQUFZLENBQUMsTUFBTSxFQUFFLE9BQU8sQ0FBQyxhQUFhLENBQUMsR0FBRyxJQUFJLENBR3hEO0lBRUQsT0FBTyxDQUFDLG1CQUFtQjtZQU9iLGlCQUFpQjtZQVdqQixvQkFBb0I7SUF3QnJCLGNBQWMsQ0FBQyxNQUFNLEVBQUUsT0FBTyxFQUFFLEVBQUUsV0FBVyxFQUFFLFdBQVcsR0FBRyxPQUFPLENBQUMsSUFBSSxDQUFDLENBK0J0RjtZQUVhLGtCQUFrQjtZQXNDbEIseUJBQXlCO1lBOEJ6QixxQkFBcUI7SUFTbkMsT0FBTyxDQUFDLHdCQUF3QjtDQWdCakMifQ==
@@ -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;YAsClB,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.getCheckpointsForEpoch(epochNumber)).filter((c)=>c.number < sortedBlocks[0].checkpointNumber).map((c)=>c.getCheckpointOutHash());
95
+ let previousCheckpointOutHashes = [
96
+ ...priorCheckpointOutHashes
97
+ ];
98
+ const fork = await this.checkpointsBuilder.getFork(BlockNumber(sortedBlocks[0].header.globalVariables.blockNumber - 1));
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,19 +110,13 @@ const EpochPruneWatcherPenaltiesConfigKeys = [
103
110
  await fork.close();
104
111
  }
105
112
  }
106
- async validateBlock(blockFromL1, previousCheckpointOutHashes, fork) {
107
- this.log.debug(`Validating pruned block ${blockFromL1.header.globalVariables.blockNumber}`);
108
- const txHashes = blockFromL1.body.txEffects.map((txEffect)=>txEffect.txHash);
109
- // We load txs from the mempool directly, since the TxCollector running in the background has already been
110
- // trying to fetch them from nodes or via reqresp. If we haven't managed to collect them by now,
111
- // it's likely that they are not available in the network at all.
112
- const { txs, missingTxs } = await this.txProvider.getAvailableTxs(txHashes);
113
- if (missingTxs && missingTxs.length > 0) {
114
- throw new TransactionsNotAvailableError(missingTxs);
115
- }
116
- const checkpointNumber = CheckpointNumber.fromBlockNumber(blockFromL1.number);
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
117
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
118
- const gv = blockFromL1.header.globalVariables;
118
+ // Build checkpoint constants from first block's global variables
119
+ const gv = checkpointBlocks[0].header.globalVariables;
119
120
  const constants = {
120
121
  chainId: gv.chainId,
121
122
  version: gv.version,
@@ -124,8 +125,24 @@ const EpochPruneWatcherPenaltiesConfigKeys = [
124
125
  feeRecipient: gv.feeRecipient,
125
126
  gasFees: gv.gasFees
126
127
  };
127
- // Use checkpoint builder to validate the block
128
+ // Start checkpoint builder once for all blocks in this checkpoint
128
129
  const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(checkpointNumber, constants, l1ToL2Messages, previousCheckpointOutHashes, fork, this.log.getBindings());
130
+ // Validate all blocks in the checkpoint sequentially
131
+ for (const block of checkpointBlocks){
132
+ await this.validateBlockInCheckpoint(block, checkpointBuilder);
133
+ }
134
+ }
135
+ async validateBlockInCheckpoint(blockFromL1, checkpointBuilder) {
136
+ this.log.debug(`Validating pruned block ${blockFromL1.header.globalVariables.blockNumber}`);
137
+ const txHashes = blockFromL1.body.txEffects.map((txEffect)=>txEffect.txHash);
138
+ // We load txs from the mempool directly, since the TxCollector running in the background has already been
139
+ // trying to fetch them from nodes or via reqresp. If we haven't managed to collect them by now,
140
+ // it's likely that they are not available in the network at all.
141
+ const { txs, missingTxs } = await this.txProvider.getAvailableTxs(txHashes);
142
+ if (missingTxs && missingTxs.length > 0) {
143
+ throw new TransactionsNotAvailableError(missingTxs);
144
+ }
145
+ const gv = blockFromL1.header.globalVariables;
129
146
  const { block, failedTxs, numTxs } = await checkpointBuilder.buildBlock(txs, gv.blockNumber, gv.timestamp, {});
130
147
  if (numTxs !== txs.length) {
131
148
  // 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.e3c1de76",
3
+ "version": "0.0.1-commit.e558bd1c",
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.e3c1de76",
60
- "@aztec/ethereum": "0.0.1-commit.e3c1de76",
61
- "@aztec/foundation": "0.0.1-commit.e3c1de76",
62
- "@aztec/kv-store": "0.0.1-commit.e3c1de76",
63
- "@aztec/l1-artifacts": "0.0.1-commit.e3c1de76",
64
- "@aztec/stdlib": "0.0.1-commit.e3c1de76",
65
- "@aztec/telemetry-client": "0.0.1-commit.e3c1de76",
59
+ "@aztec/epoch-cache": "0.0.1-commit.e558bd1c",
60
+ "@aztec/ethereum": "0.0.1-commit.e558bd1c",
61
+ "@aztec/foundation": "0.0.1-commit.e558bd1c",
62
+ "@aztec/kv-store": "0.0.1-commit.e558bd1c",
63
+ "@aztec/l1-artifacts": "0.0.1-commit.e558bd1c",
64
+ "@aztec/stdlib": "0.0.1-commit.e558bd1c",
65
+ "@aztec/telemetry-client": "0.0.1-commit.e558bd1c",
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.e3c1de76",
72
+ "@aztec/aztec.js": "0.0.1-commit.e558bd1c",
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,7 @@ 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),
26
27
  slashInactivityPenalty: BigInt(slasherDefaultEnv.SLASH_INACTIVITY_PENALTY),
27
28
  slashProposeInvalidAttestationsPenalty: BigInt(slasherDefaultEnv.SLASH_PROPOSE_INVALID_ATTESTATIONS_PENALTY),
28
29
  slashAttestDescendantOfInvalidPenalty: BigInt(slasherDefaultEnv.SLASH_ATTEST_DESCENDANT_OF_INVALID_PENALTY),
@@ -88,6 +89,11 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
88
89
  description: 'Penalty amount for slashing a validator for an invalid block proposed via p2p.',
89
90
  ...bigintConfigHelper(DefaultSlasherConfig.slashBroadcastedInvalidBlockPenalty),
90
91
  },
92
+ slashDuplicateProposalPenalty: {
93
+ env: 'SLASH_DUPLICATE_PROPOSAL_PENALTY',
94
+ description: 'Penalty amount for slashing a validator for sending duplicate proposals.',
95
+ ...bigintConfigHelper(DefaultSlasherConfig.slashDuplicateProposalPenalty),
96
+ },
91
97
  slashInactivityTargetPercentage: {
92
98
  env: 'SLASH_INACTIVITY_TARGET_PERCENTAGE',
93
99
  description:
@@ -15,6 +15,7 @@ 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: 10000000000000000000,
18
19
  SLASH_UNKNOWN_PENALTY: 10000000000000000000,
19
20
  SLASH_INVALID_BLOCK_PENALTY: 10000000000000000000,
20
21
  SLASH_GRACE_PERIOD_L2_SLOTS: 0,
@@ -1,6 +1,6 @@
1
1
  import { EpochCache } from '@aztec/epoch-cache';
2
- import { BlockNumber, CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
3
- import { merge, pick } from '@aztec/foundation/collection';
2
+ import { BlockNumber, EpochNumber } from '@aztec/foundation/branded-types';
3
+ import { chunkBy, merge, pick } from '@aztec/foundation/collection';
4
4
  import type { Fr } from '@aztec/foundation/curves/bn254';
5
5
  import { type Logger, createLogger } from '@aztec/foundation/log';
6
6
  import {
@@ -12,6 +12,7 @@ import {
12
12
  } from '@aztec/stdlib/block';
13
13
  import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
14
14
  import type {
15
+ ICheckpointBlockBuilder,
15
16
  ICheckpointsBuilder,
16
17
  ITxProvider,
17
18
  MerkleTreeWriteOperations,
@@ -106,7 +107,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
106
107
  { blocks: epochBlocks.map(b => b.toBlockInfo()) },
107
108
  );
108
109
 
109
- await this.validateBlocks(epochBlocks);
110
+ await this.validateBlocks(epochBlocks, epochNumber);
110
111
  this.log.info(`Pruned epoch ${epochNumber} was valid. Want to slash committee for not having it proven.`);
111
112
  await this.emitSlashForEpoch(OffenseType.VALID_EPOCH_PRUNED, epochNumber);
112
113
  } catch (error) {
@@ -121,19 +122,32 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
121
122
  }
122
123
  }
123
124
 
124
- public async validateBlocks(blocks: L2Block[]): Promise<void> {
125
+ public async validateBlocks(blocks: L2Block[], epochNumber: EpochNumber): Promise<void> {
125
126
  if (blocks.length === 0) {
126
127
  return;
127
128
  }
128
129
 
129
- let previousCheckpointOutHashes: Fr[] = [];
130
- const fork = await this.checkpointsBuilder.getFork(BlockNumber(blocks[0].header.globalVariables.blockNumber - 1));
130
+ // Sort blocks by block number and group by checkpoint
131
+ const sortedBlocks = [...blocks].sort((a, b) => a.number - b.number);
132
+ const blocksByCheckpoint = chunkBy(sortedBlocks, b => b.checkpointNumber);
133
+
134
+ // Get prior checkpoints in the epoch (in case this was a partial prune) to extract the out hashes
135
+ const priorCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsForEpoch(epochNumber))
136
+ .filter(c => c.number < sortedBlocks[0].checkpointNumber)
137
+ .map(c => c.getCheckpointOutHash());
138
+ let previousCheckpointOutHashes: Fr[] = [...priorCheckpointOutHashes];
139
+
140
+ const fork = await this.checkpointsBuilder.getFork(
141
+ BlockNumber(sortedBlocks[0].header.globalVariables.blockNumber - 1),
142
+ );
131
143
  try {
132
- for (const block of blocks) {
133
- await this.validateBlock(block, previousCheckpointOutHashes, fork);
144
+ for (const checkpointBlocks of blocksByCheckpoint) {
145
+ await this.validateCheckpoint(checkpointBlocks, previousCheckpointOutHashes, fork);
134
146
 
135
- // TODO(mbps): This assumes one block per checkpoint, which is only true for now.
136
- const checkpointOutHash = computeCheckpointOutHash([block.body.txEffects.map(tx => tx.l2ToL1Msgs)]);
147
+ // Compute checkpoint out hash from all blocks in this checkpoint
148
+ const checkpointOutHash = computeCheckpointOutHash(
149
+ checkpointBlocks.map(b => b.body.txEffects.map(tx => tx.l2ToL1Msgs)),
150
+ );
137
151
  previousCheckpointOutHashes = [...previousCheckpointOutHashes, checkpointOutHash];
138
152
  }
139
153
  } finally {
@@ -141,25 +155,19 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
141
155
  }
142
156
  }
143
157
 
144
- public async validateBlock(
145
- blockFromL1: L2Block,
158
+ private async validateCheckpoint(
159
+ checkpointBlocks: L2Block[],
146
160
  previousCheckpointOutHashes: Fr[],
147
161
  fork: MerkleTreeWriteOperations,
148
162
  ): Promise<void> {
149
- this.log.debug(`Validating pruned block ${blockFromL1.header.globalVariables.blockNumber}`);
150
- const txHashes = blockFromL1.body.txEffects.map(txEffect => txEffect.txHash);
151
- // We load txs from the mempool directly, since the TxCollector running in the background has already been
152
- // trying to fetch them from nodes or via reqresp. If we haven't managed to collect them by now,
153
- // it's likely that they are not available in the network at all.
154
- const { txs, missingTxs } = await this.txProvider.getAvailableTxs(txHashes);
155
-
156
- if (missingTxs && missingTxs.length > 0) {
157
- throw new TransactionsNotAvailableError(missingTxs);
158
- }
163
+ const checkpointNumber = checkpointBlocks[0].checkpointNumber;
164
+ this.log.debug(`Validating pruned checkpoint ${checkpointNumber} with ${checkpointBlocks.length} blocks`);
159
165
 
160
- const checkpointNumber = CheckpointNumber.fromBlockNumber(blockFromL1.number);
166
+ // Get L1ToL2Messages once for the entire checkpoint
161
167
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
162
- const gv = blockFromL1.header.globalVariables;
168
+
169
+ // Build checkpoint constants from first block's global variables
170
+ const gv = checkpointBlocks[0].header.globalVariables;
163
171
  const constants: CheckpointGlobalVariables = {
164
172
  chainId: gv.chainId,
165
173
  version: gv.version,
@@ -169,7 +177,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
169
177
  gasFees: gv.gasFees,
170
178
  };
171
179
 
172
- // Use checkpoint builder to validate the block
180
+ // Start checkpoint builder once for all blocks in this checkpoint
173
181
  const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
174
182
  checkpointNumber,
175
183
  constants,
@@ -179,6 +187,28 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
179
187
  this.log.getBindings(),
180
188
  );
181
189
 
190
+ // Validate all blocks in the checkpoint sequentially
191
+ for (const block of checkpointBlocks) {
192
+ await this.validateBlockInCheckpoint(block, checkpointBuilder);
193
+ }
194
+ }
195
+
196
+ private async validateBlockInCheckpoint(
197
+ blockFromL1: L2Block,
198
+ checkpointBuilder: ICheckpointBlockBuilder,
199
+ ): Promise<void> {
200
+ this.log.debug(`Validating pruned block ${blockFromL1.header.globalVariables.blockNumber}`);
201
+ const txHashes = blockFromL1.body.txEffects.map(txEffect => txEffect.txHash);
202
+ // We load txs from the mempool directly, since the TxCollector running in the background has already been
203
+ // trying to fetch them from nodes or via reqresp. If we haven't managed to collect them by now,
204
+ // it's likely that they are not available in the network at all.
205
+ const { txs, missingTxs } = await this.txProvider.getAvailableTxs(txHashes);
206
+
207
+ if (missingTxs && missingTxs.length > 0) {
208
+ throw new TransactionsNotAvailableError(missingTxs);
209
+ }
210
+
211
+ const gv = blockFromL1.header.globalVariables;
182
212
  const { block, failedTxs, numTxs } = await checkpointBuilder.buildBlock(txs, gv.blockNumber, gv.timestamp, {});
183
213
 
184
214
  if (numTxs !== txs.length) {