@aztec/stdlib 2.0.0-nightly.20250822 → 2.0.0-nightly.20250823

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 (56) hide show
  1. package/dest/block/l2_block.d.ts +2 -0
  2. package/dest/block/l2_block.d.ts.map +1 -1
  3. package/dest/block/l2_block.js +6 -0
  4. package/dest/interfaces/aztec-node-admin.d.ts +29 -15
  5. package/dest/interfaces/aztec-node-admin.d.ts.map +1 -1
  6. package/dest/interfaces/aztec-node-admin.js +7 -2
  7. package/dest/interfaces/epoch-prover.d.ts +1 -1
  8. package/dest/interfaces/slasher.d.ts +33 -17
  9. package/dest/interfaces/slasher.d.ts.map +1 -1
  10. package/dest/interfaces/slasher.js +8 -4
  11. package/dest/interfaces/world_state.d.ts +4 -4
  12. package/dest/interfaces/world_state.js +1 -1
  13. package/dest/l1-contracts/index.d.ts +2 -0
  14. package/dest/l1-contracts/index.d.ts.map +1 -0
  15. package/dest/l1-contracts/index.js +1 -0
  16. package/dest/l1-contracts/slash_factory.d.ts +44 -0
  17. package/dest/l1-contracts/slash_factory.d.ts.map +1 -0
  18. package/dest/l1-contracts/slash_factory.js +157 -0
  19. package/dest/slashing/empire.d.ts +31 -0
  20. package/dest/slashing/empire.d.ts.map +1 -0
  21. package/dest/slashing/empire.js +84 -0
  22. package/dest/slashing/helpers.d.ts +31 -0
  23. package/dest/slashing/helpers.d.ts.map +1 -0
  24. package/dest/slashing/helpers.js +62 -0
  25. package/dest/slashing/index.d.ts +6 -50
  26. package/dest/slashing/index.d.ts.map +1 -1
  27. package/dest/slashing/index.js +6 -54
  28. package/dest/slashing/interfaces.d.ts +11 -0
  29. package/dest/slashing/interfaces.d.ts.map +1 -0
  30. package/dest/slashing/interfaces.js +1 -0
  31. package/dest/slashing/serialization.d.ts +8 -0
  32. package/dest/slashing/serialization.d.ts.map +1 -0
  33. package/dest/slashing/serialization.js +78 -0
  34. package/dest/slashing/tally.d.ts +17 -0
  35. package/dest/slashing/tally.d.ts.map +1 -0
  36. package/dest/slashing/tally.js +36 -0
  37. package/dest/slashing/types.d.ts +161 -0
  38. package/dest/slashing/types.d.ts.map +1 -0
  39. package/dest/slashing/types.js +66 -0
  40. package/dest/stats/stats.d.ts +2 -2
  41. package/package.json +10 -8
  42. package/src/block/l2_block.ts +8 -0
  43. package/src/interfaces/aztec-node-admin.ts +11 -4
  44. package/src/interfaces/epoch-prover.ts +1 -1
  45. package/src/interfaces/slasher.ts +18 -9
  46. package/src/interfaces/world_state.ts +2 -2
  47. package/src/l1-contracts/index.ts +1 -0
  48. package/src/l1-contracts/slash_factory.ts +177 -0
  49. package/src/slashing/empire.ts +100 -0
  50. package/src/slashing/helpers.ts +87 -0
  51. package/src/slashing/index.ts +6 -74
  52. package/src/slashing/interfaces.ts +11 -0
  53. package/src/slashing/serialization.ts +103 -0
  54. package/src/slashing/tally.ts +51 -0
  55. package/src/slashing/types.ts +129 -0
  56. package/src/stats/stats.ts +2 -2
@@ -0,0 +1,66 @@
1
+ import { z } from 'zod';
2
+ import { schemas } from '../schemas/index.js';
3
+ export var OffenseType = /*#__PURE__*/ function(OffenseType) {
4
+ OffenseType[OffenseType["UNKNOWN"] = 0] = "UNKNOWN";
5
+ /** The data for proving an epoch was not publicly available, we slash its committee */ OffenseType[OffenseType["DATA_WITHHOLDING"] = 1] = "DATA_WITHHOLDING";
6
+ /** An epoch was not successfully proven in time, we slash its committee */ OffenseType[OffenseType["VALID_EPOCH_PRUNED"] = 2] = "VALID_EPOCH_PRUNED";
7
+ /** A proposer failed to attest or propose during an epoch according to the Sentinel */ OffenseType[OffenseType["INACTIVITY"] = 3] = "INACTIVITY";
8
+ /** A proposer sent an invalid block proposal over the p2p network to the committee */ OffenseType[OffenseType["BROADCASTED_INVALID_BLOCK_PROPOSAL"] = 4] = "BROADCASTED_INVALID_BLOCK_PROPOSAL";
9
+ /** A proposer pushed to L1 a block with insufficient committee attestations */ OffenseType[OffenseType["PROPOSED_INSUFFICIENT_ATTESTATIONS"] = 5] = "PROPOSED_INSUFFICIENT_ATTESTATIONS";
10
+ /** A proposer pushed to L1 a block with incorrect committee attestations (ie signature from a non-committee member) */ OffenseType[OffenseType["PROPOSED_INCORRECT_ATTESTATIONS"] = 6] = "PROPOSED_INCORRECT_ATTESTATIONS";
11
+ /** A committee member attested to a block that was built as a descendent of an invalid block (as in a block with invalid attestations) */ OffenseType[OffenseType["ATTESTED_DESCENDANT_OF_INVALID"] = 7] = "ATTESTED_DESCENDANT_OF_INVALID";
12
+ return OffenseType;
13
+ }({});
14
+ export const OffenseTypeSchema = z.nativeEnum(OffenseType);
15
+ export const OffenseToBigInt = {
16
+ [0]: 0n,
17
+ [1]: 1n,
18
+ [2]: 2n,
19
+ [3]: 3n,
20
+ [4]: 4n,
21
+ [5]: 5n,
22
+ [6]: 6n,
23
+ [7]: 7n
24
+ };
25
+ export function bigIntToOffense(offense) {
26
+ switch(offense){
27
+ case 0n:
28
+ return 0;
29
+ case 1n:
30
+ return 1;
31
+ case 2n:
32
+ return 2;
33
+ case 3n:
34
+ return 3;
35
+ case 4n:
36
+ return 4;
37
+ case 5n:
38
+ return 5;
39
+ case 6n:
40
+ return 6;
41
+ case 7n:
42
+ return 7;
43
+ default:
44
+ throw new Error(`Unknown offense: ${offense}`);
45
+ }
46
+ }
47
+ export const OffenseSchema = z.object({
48
+ validator: schemas.EthAddress,
49
+ amount: schemas.BigInt,
50
+ offenseType: OffenseTypeSchema,
51
+ epochOrSlot: schemas.BigInt
52
+ });
53
+ export const SlashPayloadRoundSchema = z.object({
54
+ address: schemas.EthAddress,
55
+ timestamp: schemas.BigInt,
56
+ votes: schemas.BigInt,
57
+ round: schemas.BigInt,
58
+ slashes: z.array(z.object({
59
+ validator: schemas.EthAddress,
60
+ amount: schemas.BigInt,
61
+ offenses: z.array(z.object({
62
+ offenseType: OffenseTypeSchema,
63
+ epochOrSlot: schemas.BigInt
64
+ }))
65
+ }))
66
+ });
@@ -164,9 +164,9 @@ export type L2BlockHandledStats = {
164
164
  /** Total duration in ms. */
165
165
  duration: number;
166
166
  /** Pending block number. */
167
- unfinalisedBlockNumber: bigint;
167
+ unfinalizedBlockNumber: bigint;
168
168
  /** Proven block number. */
169
- finalisedBlockNumber: bigint;
169
+ finalizedBlockNumber: bigint;
170
170
  /** Oldest historic block number. */
171
171
  oldestHistoricBlock: bigint;
172
172
  } & L2BlockStats;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/stdlib",
3
- "version": "2.0.0-nightly.20250822",
3
+ "version": "2.0.0-nightly.20250823",
4
4
  "type": "module",
5
5
  "inherits": [
6
6
  "../package.common.json",
@@ -51,7 +51,8 @@
51
51
  "./snapshots": "./dest/snapshots/index.js",
52
52
  "./update-checker": "./dest/update-checker/index.js",
53
53
  "./zkpassport": "./dest/zkpassport/index.js",
54
- "./slashing": "./dest/slashing/index.js"
54
+ "./slashing": "./dest/slashing/index.js",
55
+ "./l1-contracts": "./dest/l1-contracts/index.js"
55
56
  },
56
57
  "typedocOptions": {
57
58
  "entryPoints": [
@@ -68,12 +69,13 @@
68
69
  "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}"
69
70
  },
70
71
  "dependencies": {
71
- "@aztec/bb.js": "2.0.0-nightly.20250822",
72
- "@aztec/blob-lib": "2.0.0-nightly.20250822",
73
- "@aztec/constants": "2.0.0-nightly.20250822",
74
- "@aztec/ethereum": "2.0.0-nightly.20250822",
75
- "@aztec/foundation": "2.0.0-nightly.20250822",
76
- "@aztec/noir-noirc_abi": "2.0.0-nightly.20250822",
72
+ "@aztec/bb.js": "2.0.0-nightly.20250823",
73
+ "@aztec/blob-lib": "2.0.0-nightly.20250823",
74
+ "@aztec/constants": "2.0.0-nightly.20250823",
75
+ "@aztec/ethereum": "2.0.0-nightly.20250823",
76
+ "@aztec/foundation": "2.0.0-nightly.20250823",
77
+ "@aztec/l1-artifacts": "2.0.0-nightly.20250823",
78
+ "@aztec/noir-noirc_abi": "2.0.0-nightly.20250823",
77
79
  "@google-cloud/storage": "^7.15.0",
78
80
  "axios": "^1.9.0",
79
81
  "json-stringify-deterministic": "1.0.12",
@@ -110,6 +110,14 @@ export class L2Block {
110
110
  return this.header.getBlockNumber();
111
111
  }
112
112
 
113
+ get slot(): bigint {
114
+ return this.header.getSlot();
115
+ }
116
+
117
+ get timestamp(): bigint {
118
+ return this.header.globalVariables.timestamp;
119
+ }
120
+
113
121
  /**
114
122
  * Returns the block's hash (hash of block header).
115
123
  * @returns The block's hash.
@@ -3,7 +3,7 @@ import { createSafeJsonRpcClient, defaultFetch } from '@aztec/foundation/json-rp
3
3
  import { z } from 'zod';
4
4
 
5
5
  import type { ApiSchemaFor } from '../schemas/schemas.js';
6
- import { type MonitoredSlashPayload, MonitoredSlashPayloadSchema } from '../slashing/index.js';
6
+ import { type Offense, OffenseSchema, type SlashPayloadRound, SlashPayloadRoundSchema } from '../slashing/index.js';
7
7
  import { type ComponentsVersions, getVersioningResponseHandler } from '../versioning/index.js';
8
8
  import { type SequencerConfig, SequencerConfigSchema } from './configs.js';
9
9
  import { type ProverConfig, ProverConfigSchema } from './prover-client.js';
@@ -43,8 +43,11 @@ export interface AztecNodeAdmin {
43
43
  /** Resumes archiver and world state syncing. */
44
44
  resumeSync(): Promise<void>;
45
45
 
46
- /** Returns all monitored payloads by the slasher. */
47
- getSlasherMonitoredPayloads(): Promise<MonitoredSlashPayload[]>;
46
+ /** Returns all monitored payloads by the slasher for the current round. */
47
+ getSlashPayloads(): Promise<SlashPayloadRound[]>;
48
+
49
+ /** Returns all offenses applicable for the given round. */
50
+ getSlashOffenses(round: bigint | 'all' | 'current'): Promise<Offense[]>;
48
51
  }
49
52
 
50
53
  export type AztecNodeAdminConfig = SequencerConfig & ProverConfig & SlasherConfig & { maxTxPoolSize: number };
@@ -60,7 +63,11 @@ export const AztecNodeAdminApiSchema: ApiSchemaFor<AztecNodeAdmin> = {
60
63
  rollbackTo: z.function().args(z.number()).returns(z.void()),
61
64
  pauseSync: z.function().returns(z.void()),
62
65
  resumeSync: z.function().returns(z.void()),
63
- getSlasherMonitoredPayloads: z.function().returns(z.array(MonitoredSlashPayloadSchema)),
66
+ getSlashPayloads: z.function().returns(z.array(SlashPayloadRoundSchema)),
67
+ getSlashOffenses: z
68
+ .function()
69
+ .args(z.union([z.bigint(), z.literal('all'), z.literal('current')]))
70
+ .returns(z.array(OffenseSchema)),
64
71
  };
65
72
 
66
73
  export function createAztecNodeAdminClient(
@@ -33,7 +33,7 @@ export interface EpochProver extends Omit<IBlockFactory, 'setBlockCompleted'> {
33
33
  setBlockCompleted(blockNumber: number, expectedBlockHeader?: BlockHeader): Promise<L2Block>;
34
34
 
35
35
  /** Pads the epoch with empty block roots if needed and blocks until proven. Throws if proving has failed. */
36
- finaliseEpoch(): Promise<{ publicInputs: RootRollupPublicInputs; proof: Proof; batchedBlobInputs: BatchedBlob }>;
36
+ finalizeEpoch(): Promise<{ publicInputs: RootRollupPublicInputs; proof: Proof; batchedBlobInputs: BatchedBlob }>;
37
37
 
38
38
  /** Cancels all proving jobs. */
39
39
  cancel(): void;
@@ -1,18 +1,19 @@
1
- import type { SecretValue } from '@aztec/foundation/config';
2
1
  import type { EthAddress } from '@aztec/foundation/eth-address';
3
2
  import { type ZodFor, schemas } from '@aztec/foundation/schemas';
4
3
 
5
4
  import { z } from 'zod';
6
5
 
6
+ export type SlasherClientType = 'empire' | 'tally';
7
+
7
8
  export interface SlasherConfig {
8
9
  slashOverridePayload?: EthAddress;
9
10
  slashPayloadTtlSeconds: number; // TTL for payloads, in seconds
10
11
  slashPruneEnabled: boolean;
11
12
  slashPrunePenalty: bigint;
12
13
  slashPruneMaxPenalty: bigint;
13
- slashInvalidBlockEnabled: boolean;
14
- slashInvalidBlockPenalty: bigint;
15
- slashInvalidBlockMaxPenalty: bigint;
14
+ slashBroadcastedInvalidBlockEnabled: boolean;
15
+ slashBroadcastedInvalidBlockPenalty: bigint;
16
+ slashBroadcastedInvalidBlockMaxPenalty: bigint;
16
17
  slashInactivityEnabled: boolean;
17
18
  slashInactivityCreateTargetPercentage: number; // 0-1, 0.9 means 90%. Must be greater than 0
18
19
  slashInactivitySignalTargetPercentage: number; // 0-1, 0.6 means 60%. Must be greater than 0
@@ -23,7 +24,11 @@ export interface SlasherConfig {
23
24
  slashProposeInvalidAttestationsMaxPenalty: bigint;
24
25
  slashAttestDescendantOfInvalidPenalty: bigint;
25
26
  slashAttestDescendantOfInvalidMaxPenalty: bigint;
26
- slasherPrivateKey: SecretValue<string | undefined>; // Private key of the slasher account used for creating slash payloads
27
+ slashUnknownPenalty: bigint;
28
+ slashUnknownMaxPenalty: bigint;
29
+ slashOffenseExpirationRounds: number; // Number of rounds after which pending offenses expire
30
+ slashMaxPayloadSize: number; // Maximum number of offenses to include in a single slash payload
31
+ slashGracePeriodL2Slots: number; // Number of L2 slots to wait after genesis before slashing for most offenses
27
32
  }
28
33
 
29
34
  export const SlasherConfigSchema = z.object({
@@ -32,9 +37,9 @@ export const SlasherConfigSchema = z.object({
32
37
  slashPruneEnabled: z.boolean(),
33
38
  slashPrunePenalty: schemas.BigInt,
34
39
  slashPruneMaxPenalty: schemas.BigInt,
35
- slashInvalidBlockEnabled: z.boolean(),
36
- slashInvalidBlockPenalty: schemas.BigInt,
37
- slashInvalidBlockMaxPenalty: schemas.BigInt,
40
+ slashBroadcastedInvalidBlockEnabled: z.boolean(),
41
+ slashBroadcastedInvalidBlockPenalty: schemas.BigInt,
42
+ slashBroadcastedInvalidBlockMaxPenalty: schemas.BigInt,
38
43
  slashInactivityEnabled: z.boolean(),
39
44
  slashInactivityCreateTargetPercentage: z.number(),
40
45
  slashInactivitySignalTargetPercentage: z.number(),
@@ -45,5 +50,9 @@ export const SlasherConfigSchema = z.object({
45
50
  slashProposeInvalidAttestationsMaxPenalty: schemas.BigInt,
46
51
  slashAttestDescendantOfInvalidPenalty: schemas.BigInt,
47
52
  slashAttestDescendantOfInvalidMaxPenalty: schemas.BigInt,
48
- slasherPrivateKey: schemas.SecretValue(z.string().optional()),
53
+ slashUnknownPenalty: schemas.BigInt,
54
+ slashUnknownMaxPenalty: schemas.BigInt,
55
+ slashOffenseExpirationRounds: z.number(),
56
+ slashMaxPayloadSize: z.number(),
57
+ slashGracePeriodL2Slots: z.number(),
49
58
  }) satisfies ZodFor<SlasherConfig>;
@@ -20,7 +20,7 @@ export enum WorldStateRunningState {
20
20
  export interface WorldStateSyncStatus {
21
21
  latestBlockNumber: number;
22
22
  latestBlockHash: string;
23
- finalisedBlockNumber: number;
23
+ finalizedBlockNumber: number;
24
24
  oldestHistoricBlockNumber: number;
25
25
  treesAreSynched: boolean;
26
26
  }
@@ -84,7 +84,7 @@ export interface WorldStateSynchronizer extends ForkMerkleTreeOperations {
84
84
  }
85
85
 
86
86
  export const WorldStateSyncStatusSchema = z.object({
87
- finalisedBlockNumber: z.number().int().nonnegative(),
87
+ finalizedBlockNumber: z.number().int().nonnegative(),
88
88
  latestBlockNumber: z.number().int().nonnegative(),
89
89
  latestBlockHash: z.string(),
90
90
  oldestHistoricBlockNumber: z.number().int().nonnegative(),
@@ -0,0 +1 @@
1
+ export * from './slash_factory.js';
@@ -0,0 +1,177 @@
1
+ import { type L1TxRequest, type ViemClient, tryExtractEvent } from '@aztec/ethereum';
2
+ import { maxBigint } from '@aztec/foundation/bigint';
3
+ import { EthAddress } from '@aztec/foundation/eth-address';
4
+ import { createLogger } from '@aztec/foundation/log';
5
+ import { SlashFactoryAbi } from '@aztec/l1-artifacts/SlashFactoryAbi';
6
+
7
+ import { type GetContractReturnType, type Hex, type Log, encodeFunctionData, getContract } from 'viem';
8
+
9
+ import type { L1RollupConstants } from '../epoch-helpers/index.js';
10
+ import {
11
+ OffenseToBigInt,
12
+ type SlashPayload,
13
+ type ValidatorSlash,
14
+ type ValidatorSlashOffense,
15
+ bigIntToOffense,
16
+ } from '../slashing/index.js';
17
+
18
+ export class SlashFactoryContract {
19
+ private readonly logger = createLogger('contracts:slash_factory');
20
+ private readonly contract: GetContractReturnType<typeof SlashFactoryAbi, ViemClient>;
21
+
22
+ constructor(
23
+ public readonly client: ViemClient,
24
+ address: Hex | EthAddress,
25
+ ) {
26
+ this.contract = getContract({
27
+ address: typeof address === 'string' ? address : address.toString(),
28
+ abi: SlashFactoryAbi,
29
+ client,
30
+ });
31
+ }
32
+
33
+ public get address() {
34
+ return EthAddress.fromString(this.contract.address);
35
+ }
36
+
37
+ public buildCreatePayloadRequest(slashes: ValidatorSlash[]): L1TxRequest {
38
+ const sorted = this.sortSlashes(slashes);
39
+
40
+ return {
41
+ to: this.contract.address,
42
+ data: encodeFunctionData({
43
+ abi: SlashFactoryAbi,
44
+ functionName: 'createSlashPayload',
45
+ args: [
46
+ sorted.map(d => d.validator.toString()),
47
+ sorted.map(d => d.amount),
48
+ sorted.map(d => d.offenses.map(packValidatorSlashOffense)),
49
+ ],
50
+ }),
51
+ };
52
+ }
53
+
54
+ /** Tries to extract a SlashPayloadCreated event from the given logs. */
55
+ public tryExtractSlashPayloadCreatedEvent(logs: Log[]) {
56
+ return tryExtractEvent(logs, this.address.toString(), SlashFactoryAbi, 'SlashPayloadCreated');
57
+ }
58
+
59
+ public async getSlashPayloadCreatedEvents(): Promise<SlashPayload[]> {
60
+ const events = await this.contract.getEvents.SlashPayloadCreated();
61
+ return Promise.all(
62
+ events.map(async event => {
63
+ const { validators, amounts, offenses } = event.args;
64
+ const slashes: ValidatorSlash[] = validators!.map((validator, i) => ({
65
+ validator: EthAddress.fromString(validator),
66
+ amount: amounts![i],
67
+ offenses: offenses![i].map(unpackValidatorSlashOffense),
68
+ }));
69
+
70
+ const block = await this.client.getBlock({ blockNumber: event.blockNumber, includeTransactions: false });
71
+ return { address: EthAddress.fromString(event.args.payloadAddress!), slashes, timestamp: block.timestamp };
72
+ }),
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Searches for a slash payload in the events emitted by the contract.
78
+ * This method cannot query for historical payload events, it queries for payloads that have not yet expired.
79
+ * @param payloadAddress The address of the payload to search for.
80
+ * @param constants The L1 rollup constants needed for time calculations.
81
+ */
82
+ public async getSlashPayloadFromEvents(
83
+ payloadAddress: EthAddress,
84
+ settings: {
85
+ logsBatchSize?: number;
86
+ slashingRoundSize: number;
87
+ slashingPayloadLifetimeInRounds: number;
88
+ } & Pick<L1RollupConstants, 'slotDuration' | 'ethereumSlotDuration'>,
89
+ ): Promise<Omit<SlashPayload, 'votes'> | undefined> {
90
+ // We query for the log where the payload was emitted walking backwards until we go past payload expiration time
91
+ // Note that all log queries require a block range, and RPC providers cap the max range (eg quicknode is 10k blocks).
92
+ const { slashingRoundSize, slashingPayloadLifetimeInRounds, slotDuration, ethereumSlotDuration } = settings;
93
+ const currentBlockNumber = await this.client.getBlockNumber({ cacheTime: 0 });
94
+
95
+ // Why the +1 below? Just for good measure. Better err on the safe side.
96
+ const earliestBlockNumber = maxBigint(
97
+ 0n,
98
+ currentBlockNumber -
99
+ ((BigInt(slashingPayloadLifetimeInRounds) + 1n) * BigInt(slashingRoundSize) * BigInt(slotDuration)) /
100
+ BigInt(ethereumSlotDuration),
101
+ );
102
+
103
+ this.logger.trace(
104
+ `Starting search for slash payload ${payloadAddress} from block ${currentBlockNumber} with earliest block ${earliestBlockNumber}`,
105
+ );
106
+ const batchSize = BigInt(settings.logsBatchSize ?? 10000);
107
+ let toBlock = currentBlockNumber;
108
+
109
+ do {
110
+ const fromBlock = maxBigint(earliestBlockNumber, toBlock - batchSize);
111
+ this.logger.trace(`Searching for slash payload ${payloadAddress} in blocks ${fromBlock} to ${toBlock}`);
112
+ const logs = await this.contract.getEvents.SlashPayloadCreated(
113
+ { payloadAddress: payloadAddress.toString() },
114
+ { fromBlock, toBlock, strict: true },
115
+ );
116
+
117
+ // We found the payload, return it
118
+ if (logs.length > 0) {
119
+ const log = logs[0];
120
+ const { validators, amounts, offenses } = log.args;
121
+
122
+ // Convert the data to our internal types
123
+ const slashes: ValidatorSlash[] = validators!.map((validator, i) => ({
124
+ validator: EthAddress.fromString(validator),
125
+ amount: amounts![i],
126
+ offenses: offenses![i].map(unpackValidatorSlashOffense),
127
+ }));
128
+
129
+ // Get the timestamp from the block
130
+ const block = await this.client.getBlock({ blockNumber: log.blockNumber, includeTransactions: false });
131
+
132
+ return { address: payloadAddress, slashes, timestamp: block.timestamp };
133
+ }
134
+
135
+ // If not found, we go back one batch
136
+ toBlock -= batchSize;
137
+ } while (toBlock > earliestBlockNumber);
138
+
139
+ return undefined;
140
+ }
141
+
142
+ public async getAddressAndIsDeployed(
143
+ slashes: ValidatorSlash[],
144
+ ): Promise<{ address: EthAddress; salt: Hex; isDeployed: boolean }> {
145
+ const sortedSlashes = this.sortSlashes(slashes);
146
+ const [address, salt, isDeployed] = await this.contract.read.getAddressAndIsDeployed([
147
+ sortedSlashes.map(s => s.validator.toString()),
148
+ sortedSlashes.map(s => s.amount),
149
+ sortedSlashes.map(s => s.offenses.map(packValidatorSlashOffense)),
150
+ ]);
151
+ return { address: EthAddress.fromString(address), salt, isDeployed };
152
+ }
153
+
154
+ private sortSlashes(slashes: ValidatorSlash[]): ValidatorSlash[] {
155
+ const offenseSorter = (a: ValidatorSlashOffense, b: ValidatorSlashOffense) => {
156
+ return a.epochOrSlot === b.epochOrSlot ? a.offenseType - b.offenseType : Number(a.epochOrSlot - b.epochOrSlot);
157
+ };
158
+ return [...slashes]
159
+ .map(slash => ({ ...slash, offenses: [...slash.offenses].sort(offenseSorter) }))
160
+ .sort((a, b) => a.validator.toString().localeCompare(b.validator.toString()));
161
+ }
162
+ }
163
+
164
+ export function packValidatorSlashOffense(offense: ValidatorSlashOffense): bigint {
165
+ const offenseId = OffenseToBigInt[offense.offenseType];
166
+ if (offenseId > (1 << 8) - 1) {
167
+ throw new Error(`Offense type ${offense.offenseType} cannot be packed into 8 bits`);
168
+ }
169
+ return (offenseId << 120n) + offense.epochOrSlot;
170
+ }
171
+
172
+ export function unpackValidatorSlashOffense(packed: bigint): ValidatorSlashOffense {
173
+ const offenseId = (packed >> 120n) & 0xffn;
174
+ const epochOrSlot = packed & ((1n << 120n) - 1n);
175
+ const offenseType = bigIntToOffense(offenseId);
176
+ return { epochOrSlot, offenseType };
177
+ }
@@ -0,0 +1,100 @@
1
+ import { getRoundForSlot, getRoundsForEpoch } from './helpers.js';
2
+ import type { Offense, OffenseIdentifier, SlashPayload, SlashPayloadRound, ValidatorSlash } from './types.js';
3
+ import { OffenseType } from './types.js';
4
+
5
+ /**
6
+ * Returns true if the offense is uncontroversial as in it can be verified via L1 data alone,
7
+ * and does not depend on the local view of the node of the L2 p2p network.
8
+ * @param offense - The offense type to check
9
+ */
10
+ export function isOffenseUncontroversial(offense: OffenseType): boolean {
11
+ return (
12
+ offense === OffenseType.PROPOSED_INSUFFICIENT_ATTESTATIONS ||
13
+ offense === OffenseType.PROPOSED_INCORRECT_ATTESTATIONS ||
14
+ offense === OffenseType.ATTESTED_DESCENDANT_OF_INVALID
15
+ );
16
+ }
17
+
18
+ /** Extracts offense identifiers (validator, epoch, offense type) from an Empire-based SlashPayload */
19
+ export function getOffenseIdentifiersFromPayload(payload: SlashPayload | SlashPayloadRound): OffenseIdentifier[] {
20
+ return payload.slashes.flatMap((slash: ValidatorSlash) =>
21
+ slash.offenses.map(o => ({
22
+ validator: slash.validator,
23
+ offenseType: o.offenseType,
24
+ epochOrSlot: o.epochOrSlot,
25
+ })),
26
+ );
27
+ }
28
+
29
+ /** Creates ValidatorSlashes used to create an Empire-based SlashPayload from a set of Offenses */
30
+ export function offensesToValidatorSlash(offenses: Offense[]): ValidatorSlash[] {
31
+ return offenses.map(offense => ({
32
+ validator: offense.validator,
33
+ amount: offense.amount,
34
+ offenses: [{ epochOrSlot: offense.epochOrSlot, offenseType: offense.offenseType }],
35
+ }));
36
+ }
37
+
38
+ /**
39
+ * Sorts offense data by:
40
+ * - Uncontroversial offenses first
41
+ * - Slash amount (descending)
42
+ * - Epoch or slot (ascending, ie oldest first)
43
+ * - Validator address (ascending)
44
+ * - Offense type (descending)
45
+ */
46
+ export function offenseDataComparator(a: Offense, b: Offense): number {
47
+ return (
48
+ Number(isOffenseUncontroversial(b.offenseType)) - Number(isOffenseUncontroversial(a.offenseType)) ||
49
+ Number(b.amount - a.amount) ||
50
+ Number(a.epochOrSlot - b.epochOrSlot) ||
51
+ a.validator.toString().localeCompare(b.validator.toString()) ||
52
+ Number(b.offenseType) - Number(a.offenseType)
53
+ );
54
+ }
55
+
56
+ /**
57
+ * Returns the first round in which the offense is eligible for being included in an Empire-based slash payload.
58
+ * Should be equal to to the first round that starts strictly after the offense becomes detectable.
59
+ */
60
+ export function getFirstEligibleRoundForOffense(
61
+ offense: OffenseIdentifier,
62
+ constants: { slashingRoundSize: number; epochDuration: number; proofSubmissionEpochs: number },
63
+ ): bigint {
64
+ // TODO(palla/slash): Check for off-by-ones
65
+ switch (offense.offenseType) {
66
+ // Inactivity is detected at the end of the epoch, so we flag it as detected in the first fresh round for the next epoch
67
+ case OffenseType.INACTIVITY: {
68
+ const epoch = offense.epochOrSlot;
69
+ const detectedEpoch = epoch + 1n;
70
+ return getRoundsForEpoch(detectedEpoch, constants)[0] + 1n;
71
+ }
72
+ // These offenses are detected once an epoch is pruned, which happens after the proof submission window
73
+ case OffenseType.VALID_EPOCH_PRUNED:
74
+ case OffenseType.DATA_WITHHOLDING: {
75
+ // TODO(palla/slash): Check for off-by-ones especially here
76
+ const epoch = offense.epochOrSlot;
77
+ const detectedEpoch = epoch + BigInt(constants.proofSubmissionEpochs);
78
+ return getRoundsForEpoch(detectedEpoch, constants)[0] + 1n;
79
+ }
80
+ // These offenses are detected immediately in the slot they occur, so we assume they are detected in the first round for the following slot
81
+ case OffenseType.PROPOSED_INSUFFICIENT_ATTESTATIONS:
82
+ case OffenseType.PROPOSED_INCORRECT_ATTESTATIONS:
83
+ case OffenseType.ATTESTED_DESCENDANT_OF_INVALID:
84
+ case OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL: {
85
+ const slot = offense.epochOrSlot;
86
+ const detectedSlot = slot + 1n;
87
+ return getRoundForSlot(detectedSlot, constants).round + 1n;
88
+ }
89
+ // Assume these are epoch-based offenses, even though we should never have to process these
90
+ case OffenseType.UNKNOWN: {
91
+ const epoch = offense.epochOrSlot;
92
+ const detectedEpoch = epoch + 1n;
93
+ return getRoundsForEpoch(detectedEpoch, constants)[0] + 1n;
94
+ }
95
+ default: {
96
+ const _: never = offense.offenseType;
97
+ throw new Error(`Unknown offense type: ${offense.offenseType}`);
98
+ }
99
+ }
100
+ }
@@ -0,0 +1,87 @@
1
+ import { type L1RollupConstants, getEpochAtSlot, getSlotRangeForEpoch } from '../epoch-helpers/index.js';
2
+ import { type Offense, OffenseType } from './types.js';
3
+
4
+ /** Returns the voting round number and voting slot within the round for a given L2 slot. */
5
+ export function getRoundForSlot(
6
+ slot: bigint,
7
+ constants: { slashingRoundSize: number },
8
+ ): { round: bigint; votingSlot: bigint } {
9
+ const roundSize = BigInt(constants.slashingRoundSize);
10
+ const round = slot / roundSize;
11
+ const votingSlot = slot % roundSize;
12
+ return { round, votingSlot };
13
+ }
14
+
15
+ /** Returns the voting round(s) lower and upper bounds (inclusive) covered by the given epoch */
16
+ export function getRoundsForEpoch(
17
+ epoch: bigint,
18
+ constants: { slashingRoundSize: number; epochDuration: number },
19
+ ): [bigint, bigint] {
20
+ const [start, end] = getSlotRangeForEpoch(epoch, constants);
21
+ const startRound = getRoundForSlot(start, constants).round;
22
+ const endRound = getRoundForSlot(end, constants).round;
23
+ return [startRound, endRound];
24
+ }
25
+
26
+ /** Returns the epochs spanned during a given slashing round */
27
+ export function getEpochsForRound(
28
+ round: bigint,
29
+ constants: { slashingRoundSize: number; epochDuration: number },
30
+ ): bigint[] {
31
+ const epochs: bigint[] = [];
32
+ const firstSlot = round * BigInt(constants.slashingRoundSize);
33
+ const lastSlot = firstSlot + BigInt(constants.slashingRoundSize) - 1n;
34
+ const startEpoch = getEpochAtSlot(firstSlot, constants);
35
+ const endEpoch = getEpochAtSlot(lastSlot, constants);
36
+ for (let epoch = startEpoch; epoch <= endEpoch; epoch++) {
37
+ epochs.push(epoch);
38
+ }
39
+ return epochs;
40
+ }
41
+
42
+ /** Returns whether the `epochOrSlot` field for an offense references an epoch or a slot */
43
+ export function getTimeUnitForOffense(offense: OffenseType): 'epoch' | 'slot' {
44
+ switch (offense) {
45
+ case OffenseType.ATTESTED_DESCENDANT_OF_INVALID:
46
+ case OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL:
47
+ case OffenseType.PROPOSED_INCORRECT_ATTESTATIONS:
48
+ case OffenseType.PROPOSED_INSUFFICIENT_ATTESTATIONS:
49
+ return 'slot';
50
+ case OffenseType.INACTIVITY:
51
+ case OffenseType.DATA_WITHHOLDING:
52
+ case OffenseType.UNKNOWN:
53
+ case OffenseType.VALID_EPOCH_PRUNED:
54
+ return 'epoch';
55
+ default: {
56
+ const _: never = offense;
57
+ throw new Error(`Unknown offense type: ${offense}`);
58
+ }
59
+ }
60
+ }
61
+
62
+ /** Returns the slot for a given offense. If the offense references an epoch, returns the first slot of the epoch. */
63
+ export function getSlotForOffense(
64
+ offense: Pick<Offense, 'epochOrSlot' | 'offenseType'>,
65
+ constants: Pick<L1RollupConstants, 'epochDuration'>,
66
+ ): bigint {
67
+ const { epochOrSlot, offenseType } = offense;
68
+ return getTimeUnitForOffense(offenseType) === 'epoch' ? epochOrSlot * BigInt(constants.epochDuration) : epochOrSlot;
69
+ }
70
+
71
+ /** Returns the epoch for a given offense. */
72
+ export function getEpochForOffense(
73
+ offense: Pick<Offense, 'epochOrSlot' | 'offenseType'>,
74
+ constants: Pick<L1RollupConstants, 'epochDuration'>,
75
+ ): bigint {
76
+ const { epochOrSlot, offenseType } = offense;
77
+ return getTimeUnitForOffense(offenseType) === 'epoch' ? epochOrSlot : epochOrSlot / BigInt(constants.epochDuration);
78
+ }
79
+
80
+ /** Returns the slashing round in which a given offense occurred. */
81
+ export function getRoundForOffense(
82
+ offense: Pick<Offense, 'epochOrSlot' | 'offenseType'>,
83
+ constants: { slashingRoundSize: number; epochDuration: number },
84
+ ): bigint {
85
+ const slot = getSlotForOffense(offense, constants);
86
+ return getRoundForSlot(slot, constants).round;
87
+ }