@aztec-labs/slasher 6.0.0-nightly.20260829

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 (98) hide show
  1. package/README.md +232 -0
  2. package/dest/config.d.ts +6 -0
  3. package/dest/config.d.ts.map +1 -0
  4. package/dest/config.js +146 -0
  5. package/dest/factory/create_facade.d.ts +19 -0
  6. package/dest/factory/create_facade.d.ts.map +1 -0
  7. package/dest/factory/create_facade.js +49 -0
  8. package/dest/factory/create_implementation.d.ts +17 -0
  9. package/dest/factory/create_implementation.d.ts.map +1 -0
  10. package/dest/factory/create_implementation.js +25 -0
  11. package/dest/factory/get_settings.d.ts +4 -0
  12. package/dest/factory/get_settings.d.ts.map +1 -0
  13. package/dest/factory/get_settings.js +36 -0
  14. package/dest/factory/index.d.ts +3 -0
  15. package/dest/factory/index.d.ts.map +1 -0
  16. package/dest/factory/index.js +2 -0
  17. package/dest/generated/slasher-defaults.d.ts +21 -0
  18. package/dest/generated/slasher-defaults.d.ts.map +1 -0
  19. package/dest/generated/slasher-defaults.js +21 -0
  20. package/dest/index.d.ts +13 -0
  21. package/dest/index.d.ts.map +1 -0
  22. package/dest/index.js +12 -0
  23. package/dest/metrics.d.ts +23 -0
  24. package/dest/metrics.d.ts.map +1 -0
  25. package/dest/metrics.js +38 -0
  26. package/dest/null_slasher_client.d.ts +16 -0
  27. package/dest/null_slasher_client.d.ts.map +1 -0
  28. package/dest/null_slasher_client.js +30 -0
  29. package/dest/own_validator_slash_monitor.d.ts +66 -0
  30. package/dest/own_validator_slash_monitor.d.ts.map +1 -0
  31. package/dest/own_validator_slash_monitor.js +163 -0
  32. package/dest/slash_offenses_collector.d.ts +46 -0
  33. package/dest/slash_offenses_collector.d.ts.map +1 -0
  34. package/dest/slash_offenses_collector.js +114 -0
  35. package/dest/slash_round_monitor.d.ts +30 -0
  36. package/dest/slash_round_monitor.d.ts.map +1 -0
  37. package/dest/slash_round_monitor.js +52 -0
  38. package/dest/slasher_client.d.ts +118 -0
  39. package/dest/slasher_client.d.ts.map +1 -0
  40. package/dest/slasher_client.js +373 -0
  41. package/dest/slasher_client_facade.d.ts +44 -0
  42. package/dest/slasher_client_facade.d.ts.map +1 -0
  43. package/dest/slasher_client_facade.js +75 -0
  44. package/dest/slasher_client_interface.d.ts +25 -0
  45. package/dest/slasher_client_interface.d.ts.map +1 -0
  46. package/dest/slasher_client_interface.js +1 -0
  47. package/dest/stores/offenses_store.d.ts +37 -0
  48. package/dest/stores/offenses_store.d.ts.map +1 -0
  49. package/dest/stores/offenses_store.js +130 -0
  50. package/dest/stores/schema_version.d.ts +2 -0
  51. package/dest/stores/schema_version.d.ts.map +1 -0
  52. package/dest/stores/schema_version.js +1 -0
  53. package/dest/test/dummy_watcher.d.ts +11 -0
  54. package/dest/test/dummy_watcher.d.ts.map +1 -0
  55. package/dest/test/dummy_watcher.js +14 -0
  56. package/dest/watcher.d.ts +28 -0
  57. package/dest/watcher.d.ts.map +1 -0
  58. package/dest/watcher.js +2 -0
  59. package/dest/watchers/attestations_block_watcher.d.ts +47 -0
  60. package/dest/watchers/attestations_block_watcher.d.ts.map +1 -0
  61. package/dest/watchers/attestations_block_watcher.js +157 -0
  62. package/dest/watchers/attested_invalid_proposal_watcher.d.ts +42 -0
  63. package/dest/watchers/attested_invalid_proposal_watcher.d.ts.map +1 -0
  64. package/dest/watchers/attested_invalid_proposal_watcher.js +117 -0
  65. package/dest/watchers/broadcasted_invalid_checkpoint_proposal_watcher.d.ts +38 -0
  66. package/dest/watchers/broadcasted_invalid_checkpoint_proposal_watcher.d.ts.map +1 -0
  67. package/dest/watchers/broadcasted_invalid_checkpoint_proposal_watcher.js +138 -0
  68. package/dest/watchers/checkpoint_equivocation_watcher.d.ts +30 -0
  69. package/dest/watchers/checkpoint_equivocation_watcher.d.ts.map +1 -0
  70. package/dest/watchers/checkpoint_equivocation_watcher.js +69 -0
  71. package/dest/watchers/data_withholding_watcher.d.ts +69 -0
  72. package/dest/watchers/data_withholding_watcher.d.ts.map +1 -0
  73. package/dest/watchers/data_withholding_watcher.js +223 -0
  74. package/package.json +90 -0
  75. package/src/config.ts +177 -0
  76. package/src/factory/create_facade.ts +85 -0
  77. package/src/factory/create_implementation.ts +83 -0
  78. package/src/factory/get_settings.ts +58 -0
  79. package/src/factory/index.ts +2 -0
  80. package/src/generated/slasher-defaults.ts +23 -0
  81. package/src/index.ts +12 -0
  82. package/src/metrics.ts +58 -0
  83. package/src/null_slasher_client.ts +37 -0
  84. package/src/own_validator_slash_monitor.ts +183 -0
  85. package/src/slash_offenses_collector.ts +156 -0
  86. package/src/slash_round_monitor.ts +62 -0
  87. package/src/slasher_client.ts +477 -0
  88. package/src/slasher_client_facade.ts +99 -0
  89. package/src/slasher_client_interface.ts +31 -0
  90. package/src/stores/offenses_store.ts +173 -0
  91. package/src/stores/schema_version.ts +1 -0
  92. package/src/test/dummy_watcher.ts +20 -0
  93. package/src/watcher.ts +35 -0
  94. package/src/watchers/attestations_block_watcher.ts +199 -0
  95. package/src/watchers/attested_invalid_proposal_watcher.ts +167 -0
  96. package/src/watchers/broadcasted_invalid_checkpoint_proposal_watcher.ts +191 -0
  97. package/src/watchers/checkpoint_equivocation_watcher.ts +95 -0
  98. package/src/watchers/data_withholding_watcher.ts +256 -0
@@ -0,0 +1,52 @@
1
+ import { createLogger } from '@aztec-labs/foundation/log';
2
+ import { getSlotAtTimestamp } from '@aztec-labs/stdlib/epoch-helpers';
3
+ import { getRoundForSlot } from '@aztec-labs/stdlib/slashing';
4
+ export class SlashRoundMonitor {
5
+ settings;
6
+ dateProvider;
7
+ log;
8
+ currentRound;
9
+ intervalId;
10
+ handler;
11
+ constructor(settings, dateProvider, log = createLogger('slasher:round-monitor')){
12
+ this.settings = settings;
13
+ this.dateProvider = dateProvider;
14
+ this.log = log;
15
+ this.currentRound = 0n;
16
+ this.intervalId = undefined;
17
+ this.handler = undefined;
18
+ }
19
+ start() {
20
+ // Check for round changes
21
+ this.currentRound = this.getCurrentRound().round;
22
+ this.intervalId = setInterval(()=>{
23
+ const round = this.getCurrentRound().round;
24
+ if (round !== this.currentRound) {
25
+ this.currentRound = round;
26
+ if (this.handler) {
27
+ void this.handler(round).catch((err)=>this.log.error('Error handling new round', err));
28
+ }
29
+ }
30
+ }, 500);
31
+ }
32
+ stop() {
33
+ if (this.intervalId) {
34
+ clearInterval(this.intervalId);
35
+ this.intervalId = undefined;
36
+ }
37
+ }
38
+ listenToNewRound(handler) {
39
+ this.handler = handler;
40
+ return ()=>{
41
+ this.handler = undefined;
42
+ };
43
+ }
44
+ /** Returns the slashing round number and the voting slot within the round based on the L2 chain slot */ getRoundForSlot(slotNumber) {
45
+ return getRoundForSlot(slotNumber, this.settings);
46
+ }
47
+ /** Returns the current slashing round and voting slot within the round */ getCurrentRound() {
48
+ const now = this.dateProvider.nowInSeconds();
49
+ const currentSlot = getSlotAtTimestamp(BigInt(now), this.settings);
50
+ return this.getRoundForSlot(currentSlot);
51
+ }
52
+ }
@@ -0,0 +1,118 @@
1
+ import type { EpochCache } from '@aztec-labs/epoch-cache';
2
+ import { RollupContract, SlasherContract, SlashingProposerContract } from '@aztec-labs/ethereum/contracts';
3
+ import { SlotNumber } from '@aztec-labs/foundation/branded-types';
4
+ import { EthAddress } from '@aztec-labs/foundation/eth-address';
5
+ import type { DateProvider } from '@aztec-labs/foundation/timer';
6
+ import type { Prettify } from '@aztec-labs/foundation/types';
7
+ import type { SlasherConfig } from '@aztec-labs/stdlib/interfaces/server';
8
+ import { type Offense, type ProposerSlashAction, type ProposerSlashActionProvider } from '@aztec-labs/stdlib/slashing';
9
+ import type { Hex } from 'viem';
10
+ import { SlasherMetrics } from './metrics.js';
11
+ import { OwnValidatorSlashMonitor } from './own_validator_slash_monitor.js';
12
+ import { SlashOffensesCollector, type SlashOffensesCollectorConfig, type SlashOffensesCollectorSettings } from './slash_offenses_collector.js';
13
+ import { SlashRoundMonitor, type SlashRoundMonitorSettings } from './slash_round_monitor.js';
14
+ import type { SlasherClientInterface } from './slasher_client_interface.js';
15
+ import type { SlasherOffensesStore } from './stores/offenses_store.js';
16
+ import type { Watcher } from './watcher.js';
17
+ /** Settings used in the slasher client, loaded from the L1 contracts during initialization */
18
+ export type SlasherSettings = Prettify<SlashRoundMonitorSettings & SlashOffensesCollectorSettings & {
19
+ slashingLifetimeInRounds: number;
20
+ slashingExecutionDelayInRounds: number;
21
+ slashingRoundSizeInEpochs: number;
22
+ slashingOffsetInRounds: number;
23
+ slashingQuorumSize: number;
24
+ slashingAmounts: [bigint, bigint, bigint];
25
+ /** Committee size for block proposal */
26
+ targetCommitteeSize: number;
27
+ }>;
28
+ export type SlasherClientConfig = SlashOffensesCollectorConfig & Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever' | 'slashExecuteRoundsLookBack' | 'slashMaxPayloadSize'>;
29
+ /**
30
+ * The Slasher client is responsible for managing slashable offenses using
31
+ * the consensus-based slashing model where proposers vote on individual validator offenses.
32
+ *
33
+ * The client subscribes to several slash watchers that emit offenses and tracks them. When the slasher is the
34
+ * proposer, it votes for which validators from past epochs should be slashed based on collected offenses.
35
+ * Voting is handled by the sequencer publisher, the slasher client does not interact with L1 directly.
36
+ * The client also monitors rounds and executes slashing when rounds become executable after reaching quorum.
37
+ *
38
+ * Voting and offense collection
39
+ * - Time is divided into rounds (ROUND_SIZE slots each). During each round, block proposers can submit votes
40
+ * indicating which validators from SLASH_OFFSET_IN_ROUNDS rounds ago should be slashed.
41
+ * - Votes are encoded as bytes where each validator's vote is represented by 2 bits indicating the slash amount (0-3 slash units)
42
+ * for the validator in the committee being slashed.
43
+ * - When gathering offenses for round N, the system looks at offenses from round N-2 (where 2 is the hardcoded
44
+ * offset), giving time to detect offenses and vote on them in a later round.
45
+ * - Each offense carries an epoch or block identifier to differentiate multiple offenses by the same validator.
46
+ *
47
+ * Quorum and execution
48
+ * - After a round ends, there is an execution delay period for review so the VETOER in the Slasher can veto
49
+ * if needed.
50
+ * - Once the delay passes, anyone can call executeRound() to tally votes and execute slashing.
51
+ * - Validators that reach the quorum threshold are slashed. A vote for slashing N units is also considered
52
+ * a vote for slashing N-1, N-2, ..., 1 units. The system slashes for the largest amount that reaches quorum.
53
+ * - The client monitors executable rounds and triggers execution when appropriate.
54
+ */
55
+ export declare class SlasherClient implements ProposerSlashActionProvider, SlasherClientInterface {
56
+ private config;
57
+ private settings;
58
+ private slashingProposer;
59
+ private slasher;
60
+ private rollup;
61
+ private epochCache;
62
+ private dateProvider;
63
+ private offensesStore;
64
+ private readonly ownValidators;
65
+ private log;
66
+ private readonly metrics;
67
+ protected unwatchCallbacks: (() => void)[];
68
+ protected roundMonitor: SlashRoundMonitor;
69
+ protected offensesCollector: SlashOffensesCollector;
70
+ protected readonly ownValidatorMonitor: OwnValidatorSlashMonitor;
71
+ constructor(config: SlasherClientConfig, settings: SlasherSettings, slashingProposer: SlashingProposerContract, slasher: SlasherContract, rollup: RollupContract, watchers: Watcher[], epochCache: EpochCache, dateProvider: DateProvider, offensesStore: SlasherOffensesStore, ownValidators?: EthAddress[], log?: import("@aztec-labs/foundation/log").Logger, metrics?: SlasherMetrics);
72
+ start(): Promise<void>;
73
+ /** Stop the slasher client */
74
+ stop(): Promise<void>;
75
+ /** Returns the current config */
76
+ getConfig(): SlasherConfig;
77
+ /** Update the config of the slasher client */
78
+ updateConfig(config: Partial<SlasherConfig>): void;
79
+ /** Triggered on a time basis when we enter a new slashing round. Clears expired offenses. */
80
+ protected handleNewRound(round: bigint): Promise<void>;
81
+ /** Called when we see a RoundExecuted event on the SlashingProposer (just for logging and metrics). */
82
+ protected handleRoundExecuted(round: bigint, slashCount: bigint, l1BlockHash: Hex): Promise<void>;
83
+ /**
84
+ * Get the actions the proposer should take for slashing
85
+ * @param slotNumber - The current slot number
86
+ * @returns The actions to take
87
+ */
88
+ getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]>;
89
+ /**
90
+ * Returns an execute slash action if there are any rounds ready to be executed.
91
+ * Returns the oldest slash action if there are multiple rounds pending execution.
92
+ */
93
+ protected getExecuteSlashAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined>;
94
+ private tryGetRoundExecuteAction;
95
+ /** Returns a vote action based on offenses from the target round (with offset applied) */
96
+ protected getVoteOffensesAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined>;
97
+ /** Returns the committees that were active during the timespan of a given round */
98
+ private collectCommitteesActiveDuringRound;
99
+ /**
100
+ * Gather offenses to be slashed on a given round.
101
+ * Round N slashes validators from round N - slashOffsetInRounds.
102
+ * @param round - The round to get offenses for, defaults to current round
103
+ * @returns Array of pending offenses for the round with offset applied
104
+ */
105
+ gatherOffensesForRound(round?: bigint): Promise<Offense[]>;
106
+ /** Returns all offenses stored */
107
+ getOffenses(): Promise<Offense[]>;
108
+ /**
109
+ * Returns the round to be slashed given the current round by applying the slash offset.
110
+ * During round N, we cannot slash the validators from the epochs of the same round, since the round is not over,
111
+ * and besides we would be asking the current validators to vote to slash themselves. So during round N we look at the
112
+ * epochs spanned during round N - SLASH_OFFSET_IN_ROUNDS. This offset means that the epochs we slash are complete,
113
+ * and also gives nodes time to detect any misbehavior (eg slashing for prunes requires the proof submission window to
114
+ * pass).
115
+ */
116
+ private getSlashedRound;
117
+ }
118
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhc2hlcl9jbGllbnQuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9zbGFzaGVyX2NsaWVudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssRUFBRSxVQUFVLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUMxRCxPQUFPLEVBQUUsY0FBYyxFQUFFLGVBQWUsRUFBRSx3QkFBd0IsRUFBRSxNQUFNLGdDQUFnQyxDQUFDO0FBRTNHLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxzQ0FBc0MsQ0FBQztBQUVsRSxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sb0NBQW9DLENBQUM7QUFFaEUsT0FBTyxLQUFLLEVBQUUsWUFBWSxFQUFFLE1BQU0sOEJBQThCLENBQUM7QUFDakUsT0FBTyxLQUFLLEVBQUUsUUFBUSxFQUFFLE1BQU0sOEJBQThCLENBQUM7QUFDN0QsT0FBTyxLQUFLLEVBQUUsYUFBYSxFQUFFLE1BQU0sc0NBQXNDLENBQUM7QUFDMUUsT0FBTyxFQUNMLEtBQUssT0FBTyxFQUVaLEtBQUssbUJBQW1CLEVBQ3hCLEtBQUssMkJBQTJCLEVBSWpDLE1BQU0sNkJBQTZCLENBQUM7QUFFckMsT0FBTyxLQUFLLEVBQUUsR0FBRyxFQUFFLE1BQU0sTUFBTSxDQUFDO0FBRWhDLE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFDOUMsT0FBTyxFQUFFLHdCQUF3QixFQUFFLE1BQU0sa0NBQWtDLENBQUM7QUFDNUUsT0FBTyxFQUNMLHNCQUFzQixFQUN0QixLQUFLLDRCQUE0QixFQUNqQyxLQUFLLDhCQUE4QixFQUNwQyxNQUFNLCtCQUErQixDQUFDO0FBQ3ZDLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxLQUFLLHlCQUF5QixFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDN0YsT0FBTyxLQUFLLEVBQUUsc0JBQXNCLEVBQUUsTUFBTSwrQkFBK0IsQ0FBQztBQUM1RSxPQUFPLEtBQUssRUFBRSxvQkFBb0Is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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slasher_client.d.ts","sourceRoot":"","sources":["../src/slasher_client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,373 @@
1
+ import { maxBigint } from '@aztec-labs/foundation/bigint';
2
+ import { compactArray, partition, times } from '@aztec-labs/foundation/collection';
3
+ import { EthAddress } from '@aztec-labs/foundation/eth-address';
4
+ import { createLogger } from '@aztec-labs/foundation/log';
5
+ import { OffenseType, getEpochsForRound, getOffenseTypeName, getSlashConsensusVotesFromOffenses } from '@aztec-labs/stdlib/slashing';
6
+ import { getTelemetryClient } from '@aztec-labs/telemetry-client';
7
+ import { SlasherMetrics } from './metrics.js';
8
+ import { OwnValidatorSlashMonitor } from './own_validator_slash_monitor.js';
9
+ import { SlashOffensesCollector } from './slash_offenses_collector.js';
10
+ import { SlashRoundMonitor } from './slash_round_monitor.js';
11
+ /**
12
+ * The Slasher client is responsible for managing slashable offenses using
13
+ * the consensus-based slashing model where proposers vote on individual validator offenses.
14
+ *
15
+ * The client subscribes to several slash watchers that emit offenses and tracks them. When the slasher is the
16
+ * proposer, it votes for which validators from past epochs should be slashed based on collected offenses.
17
+ * Voting is handled by the sequencer publisher, the slasher client does not interact with L1 directly.
18
+ * The client also monitors rounds and executes slashing when rounds become executable after reaching quorum.
19
+ *
20
+ * Voting and offense collection
21
+ * - Time is divided into rounds (ROUND_SIZE slots each). During each round, block proposers can submit votes
22
+ * indicating which validators from SLASH_OFFSET_IN_ROUNDS rounds ago should be slashed.
23
+ * - Votes are encoded as bytes where each validator's vote is represented by 2 bits indicating the slash amount (0-3 slash units)
24
+ * for the validator in the committee being slashed.
25
+ * - When gathering offenses for round N, the system looks at offenses from round N-2 (where 2 is the hardcoded
26
+ * offset), giving time to detect offenses and vote on them in a later round.
27
+ * - Each offense carries an epoch or block identifier to differentiate multiple offenses by the same validator.
28
+ *
29
+ * Quorum and execution
30
+ * - After a round ends, there is an execution delay period for review so the VETOER in the Slasher can veto
31
+ * if needed.
32
+ * - Once the delay passes, anyone can call executeRound() to tally votes and execute slashing.
33
+ * - Validators that reach the quorum threshold are slashed. A vote for slashing N units is also considered
34
+ * a vote for slashing N-1, N-2, ..., 1 units. The system slashes for the largest amount that reaches quorum.
35
+ * - The client monitors executable rounds and triggers execution when appropriate.
36
+ */ export class SlasherClient {
37
+ config;
38
+ settings;
39
+ slashingProposer;
40
+ slasher;
41
+ rollup;
42
+ epochCache;
43
+ dateProvider;
44
+ offensesStore;
45
+ ownValidators;
46
+ log;
47
+ metrics;
48
+ unwatchCallbacks;
49
+ roundMonitor;
50
+ offensesCollector;
51
+ ownValidatorMonitor;
52
+ constructor(config, settings, slashingProposer, slasher, rollup, watchers, epochCache, dateProvider, offensesStore, ownValidators = [], log = createLogger('slasher:consensus'), metrics = new SlasherMetrics(getTelemetryClient())){
53
+ this.config = config;
54
+ this.settings = settings;
55
+ this.slashingProposer = slashingProposer;
56
+ this.slasher = slasher;
57
+ this.rollup = rollup;
58
+ this.epochCache = epochCache;
59
+ this.dateProvider = dateProvider;
60
+ this.offensesStore = offensesStore;
61
+ this.ownValidators = ownValidators;
62
+ this.log = log;
63
+ this.metrics = metrics;
64
+ this.unwatchCallbacks = [];
65
+ this.roundMonitor = new SlashRoundMonitor(settings, dateProvider);
66
+ this.offensesCollector = new SlashOffensesCollector(config, settings, watchers, offensesStore);
67
+ this.ownValidatorMonitor = new OwnValidatorSlashMonitor(slashingProposer, settings, ownValidators, this.metrics);
68
+ }
69
+ async start() {
70
+ this.log.debug('Starting slasher client...');
71
+ this.roundMonitor.start();
72
+ await this.offensesCollector.start();
73
+ // Listen for RoundExecuted events
74
+ this.unwatchCallbacks.push(this.slashingProposer.listenToRoundExecuted(({ round, slashCount, l1BlockHash })=>void this.handleRoundExecuted(round, slashCount, l1BlockHash).catch((err)=>this.log.error('Error handling round executed', err))));
75
+ // Check for round changes
76
+ this.unwatchCallbacks.push(this.roundMonitor.listenToNewRound((round)=>this.handleNewRound(round)));
77
+ // Warn early when a slashing vote names one of our own validators. Skipped entirely when running none, so
78
+ // those nodes pay no extra L1 subscription.
79
+ if (this.ownValidators.length > 0) {
80
+ this.ownValidatorMonitor.start(this.roundMonitor.getCurrentRound().round);
81
+ this.unwatchCallbacks.push(this.slashingProposer.listenToVoteCast(({ round, voteIndex })=>void this.ownValidatorMonitor.handleVoteCast(round, voteIndex)));
82
+ }
83
+ this.log.info(`Started slasher client`);
84
+ return Promise.resolve();
85
+ }
86
+ /** Stop the slasher client */ async stop() {
87
+ this.log.debug('Stopping slasher client...');
88
+ for (const unwatchCallback of this.unwatchCallbacks){
89
+ unwatchCallback();
90
+ }
91
+ this.roundMonitor.stop();
92
+ await this.ownValidatorMonitor.stop();
93
+ await this.offensesCollector.stop();
94
+ this.log.info('Slasher client stopped');
95
+ }
96
+ /** Returns the current config */ getConfig() {
97
+ return this.config;
98
+ }
99
+ /** Update the config of the slasher client */ updateConfig(config) {
100
+ this.config = {
101
+ ...this.config,
102
+ ...config
103
+ };
104
+ }
105
+ /** Triggered on a time basis when we enter a new slashing round. Clears expired offenses. */ async handleNewRound(round) {
106
+ this.log.info(`Starting new slashing round ${round}`);
107
+ this.ownValidatorMonitor.handleNewRound(round);
108
+ await this.offensesCollector.handleNewRound(round);
109
+ }
110
+ /** Called when we see a RoundExecuted event on the SlashingProposer (just for logging and metrics). */ async handleRoundExecuted(round, slashCount, l1BlockHash) {
111
+ this.metrics.recordRoundExecuted();
112
+ const slashes = await this.rollup.getSlashEvents(l1BlockHash);
113
+ this.log.info(`Slashing round ${round} has been executed with ${slashCount} slashes`, {
114
+ slashes
115
+ });
116
+ this.ownValidatorMonitor.handleSlashes(round, slashes, l1BlockHash);
117
+ }
118
+ /**
119
+ * Get the actions the proposer should take for slashing
120
+ * @param slotNumber - The current slot number
121
+ * @returns The actions to take
122
+ */ async getProposerActions(slotNumber) {
123
+ const [executeAction, voteAction] = await Promise.all([
124
+ this.getExecuteSlashAction(slotNumber),
125
+ this.getVoteOffensesAction(slotNumber)
126
+ ]);
127
+ return compactArray([
128
+ executeAction,
129
+ voteAction
130
+ ]);
131
+ }
132
+ /**
133
+ * Returns an execute slash action if there are any rounds ready to be executed.
134
+ * Returns the oldest slash action if there are multiple rounds pending execution.
135
+ */ async getExecuteSlashAction(slotNumber) {
136
+ const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
137
+ const slashingExecutionDelayInRounds = BigInt(this.settings.slashingExecutionDelayInRounds);
138
+ const executableRound = currentRound - slashingExecutionDelayInRounds - 1n;
139
+ const lookBack = BigInt(this.config.slashExecuteRoundsLookBack);
140
+ const slashingLifetimeInRounds = BigInt(this.settings.slashingLifetimeInRounds);
141
+ // Compute the oldest executable round considering both lookBack and lifetimeInRounds
142
+ // A round is only executable if currentRound <= round + lifetimeInRounds
143
+ // So the oldest round we can execute is: currentRound - lifetimeInRounds
144
+ const oldestByLifetime = maxBigint(0n, currentRound - slashingLifetimeInRounds);
145
+ const oldestByLookBack = maxBigint(0n, executableRound - lookBack);
146
+ const oldestExecutableRound = maxBigint(oldestByLifetime, oldestByLookBack);
147
+ // Check if slashing is enabled at all
148
+ if (!await this.slasher.isSlashingEnabled()) {
149
+ this.log.warn(`Slashing is disabled in the Slasher contract (skipping execution)`);
150
+ return undefined;
151
+ }
152
+ this.log.debug(`Checking slashing rounds ${oldestExecutableRound} to ${executableRound} to execute`, {
153
+ slotNumber,
154
+ currentRound,
155
+ oldestExecutableRound,
156
+ oldestByLifetime,
157
+ oldestByLookBack,
158
+ executableRound,
159
+ slashingExecutionDelayInRounds,
160
+ lookBack,
161
+ slashingLifetimeInRounds
162
+ });
163
+ // Iterate over all rounds, starting from the oldest, until we find one that is executable
164
+ for(let roundToCheck = oldestExecutableRound; roundToCheck <= executableRound; roundToCheck++){
165
+ const action = await this.tryGetRoundExecuteAction(roundToCheck, slotNumber);
166
+ if (action) {
167
+ return action;
168
+ }
169
+ }
170
+ // And return nothing if none are found
171
+ return undefined;
172
+ }
173
+ /**
174
+ * Checks if a given round is executable and returns an execute-slash action for it if so.
175
+ * Assumes round number has already been checked against lifetime and execution delay.
176
+ * @param executableRound - The round to check for execution
177
+ */ async tryGetRoundExecuteAction(executableRound, slotNumber) {
178
+ let logData = {
179
+ executableRound,
180
+ slotNumber
181
+ };
182
+ this.log.debug(`Testing if slashing round ${executableRound} is executable`, logData);
183
+ try {
184
+ const roundInfo = await this.slashingProposer.getRound(executableRound);
185
+ logData = {
186
+ ...logData,
187
+ roundInfo
188
+ };
189
+ if (roundInfo.isExecuted) {
190
+ this.log.verbose(`Round ${executableRound} has already been executed`, logData);
191
+ return undefined;
192
+ } else if (roundInfo.voteCount === 0n) {
193
+ this.log.debug(`Round ${executableRound} received no votes`, logData);
194
+ return undefined;
195
+ } else if (roundInfo.voteCount < this.settings.slashingQuorumSize) {
196
+ this.log.verbose(`Round ${executableRound} does not have enough votes to execute`, logData);
197
+ return undefined;
198
+ }
199
+ // Check if round is ready to execute at the given slot
200
+ const isReadyToExecute = await this.slashingProposer.isRoundReadyToExecute(executableRound, slotNumber);
201
+ if (!isReadyToExecute) {
202
+ this.log.warn(`Round ${executableRound} is not ready to execute at slot ${slotNumber} according to contract check`, logData);
203
+ return undefined;
204
+ }
205
+ // Check if the round yields any slashing at all
206
+ const { actions: slashActions, committees } = await this.slashingProposer.getTally(executableRound);
207
+ if (slashActions.length === 0) {
208
+ this.log.verbose(`Round ${executableRound} does not resolve in any slashing`, logData);
209
+ return undefined;
210
+ }
211
+ // Check if the slash payload is vetoed
212
+ const payload = await this.slashingProposer.getPayload(executableRound);
213
+ const isVetoed = await this.slasher.isPayloadVetoed(payload.address);
214
+ if (isVetoed) {
215
+ this.log.warn(`Round ${executableRound} payload is vetoed (skipping execution)`, {
216
+ payloadAddress: payload.address.toString(),
217
+ ...logData
218
+ });
219
+ return undefined;
220
+ }
221
+ const slashActionsWithAmounts = slashActions.map((action)=>({
222
+ validator: action.validator.toString(),
223
+ slashAmount: action.slashAmount.toString()
224
+ }));
225
+ this.log.info(`Round ${executableRound} is ready to execute with ${slashActions.length} slashes`, {
226
+ slashActions: slashActionsWithAmounts,
227
+ payloadAddress: payload.address.toString(),
228
+ ...logData
229
+ });
230
+ // We only need to post committees that are actually slashed
231
+ const slashedCommittees = committees.map((c)=>c.some((validator)=>slashActions.some((action)=>action.validator.equals(validator))) ? c : []);
232
+ this.log.debug(`Collected ${committees.length} committees for executing round ${executableRound}`, {
233
+ slashedCommittees,
234
+ ...logData
235
+ });
236
+ return {
237
+ type: 'execute-slash',
238
+ round: executableRound,
239
+ committees: slashedCommittees
240
+ };
241
+ } catch (error) {
242
+ this.log.error(`Error checking round to execute ${executableRound}`, error);
243
+ return undefined;
244
+ }
245
+ }
246
+ /** Returns a vote action based on offenses from the target round (with offset applied) */ async getVoteOffensesAction(slotNumber) {
247
+ // Compute what round we are in based on the slot number and what round will be slashed
248
+ const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
249
+ const slashedRound = this.getSlashedRound(currentRound);
250
+ if (slashedRound < 0n) {
251
+ return undefined;
252
+ }
253
+ // Compute offenses to slash, by loading the offenses for this round, adding synthetic offenses
254
+ // for validators that should always be slashed, and removing the ones that should never be slashed.
255
+ const offensesForRound = await this.gatherOffensesForRound(currentRound);
256
+ const offensesFromAlwaysSlash = (this.config.slashValidatorsAlways ?? []).map((validator)=>({
257
+ validator,
258
+ amount: this.settings.slashingAmounts[2],
259
+ offenseType: OffenseType.UNKNOWN
260
+ }));
261
+ const [offensesToForgive, offensesToSlash] = partition([
262
+ ...offensesForRound,
263
+ ...offensesFromAlwaysSlash
264
+ ], (offense)=>this.config.slashValidatorsNever?.some((v)=>v.equals(offense.validator)));
265
+ if (offensesFromAlwaysSlash.length > 0) {
266
+ this.log.verbose(`Slashing ${offensesFromAlwaysSlash.length} validators due to always-slash config`, {
267
+ slotNumber,
268
+ currentRound,
269
+ slashedRound,
270
+ offensesFromAlwaysSlash: offensesFromAlwaysSlash.map(getOffenseLogData),
271
+ slashValidatorsAlways: this.config.slashValidatorsAlways
272
+ });
273
+ }
274
+ if (offensesToForgive.length > 0) {
275
+ this.log.verbose(`Skipping slashing of ${offensesToForgive.length} offenses`, {
276
+ slotNumber,
277
+ currentRound,
278
+ slashedRound,
279
+ offensesToForgive: offensesToForgive.map(getOffenseLogData),
280
+ slashValidatorsNever: this.config.slashValidatorsNever
281
+ });
282
+ }
283
+ if (offensesToSlash.length === 0) {
284
+ this.log.debug(`No offenses to slash for round ${slashedRound}`, {
285
+ currentRound,
286
+ slotNumber,
287
+ slashedRound
288
+ });
289
+ return undefined;
290
+ }
291
+ this.log.debug(`Computing slash votes for ${offensesToSlash.length} offenses`, {
292
+ slotNumber,
293
+ currentRound,
294
+ slashedRound,
295
+ offensesToSlash: offensesToSlash.map(getOffenseLogData)
296
+ });
297
+ const committees = await this.collectCommitteesActiveDuringRound(slashedRound);
298
+ const epochsForCommittees = getEpochsForRound(slashedRound, this.settings);
299
+ const { slashMaxPayloadSize } = this.config;
300
+ const votes = getSlashConsensusVotesFromOffenses(offensesToSlash, committees, epochsForCommittees.map((e)=>BigInt(e)), {
301
+ ...this.settings,
302
+ maxSlashedValidators: slashMaxPayloadSize
303
+ }, this.log);
304
+ if (votes.every((v)=>v === 0)) {
305
+ this.log.warn(`Computed votes for offenses are all zero. Skipping vote.`, {
306
+ slotNumber,
307
+ currentRound,
308
+ slashedRound,
309
+ offensesToSlash: offensesToSlash.map(getOffenseLogData),
310
+ committees
311
+ });
312
+ return undefined;
313
+ }
314
+ this.log.info(`Voting to slash ${offensesToSlash.length} offenses`, {
315
+ slotNumber,
316
+ slashedRound,
317
+ currentRound,
318
+ votes,
319
+ offensesToSlash: offensesToSlash.map(getOffenseLogData)
320
+ });
321
+ this.log.debug(`Computed votes for slashing ${offensesToSlash.length} offenses`, {
322
+ slashedRound,
323
+ currentRound,
324
+ votes,
325
+ committees,
326
+ settings: this.settings
327
+ });
328
+ return {
329
+ type: 'vote-offenses',
330
+ round: currentRound,
331
+ votes,
332
+ committees
333
+ };
334
+ }
335
+ /** Returns the committees that were active during the timespan of a given round */ collectCommitteesActiveDuringRound(round) {
336
+ const epochsToSlash = getEpochsForRound(round, this.settings);
337
+ const emptyCommittee = times(Number(this.settings.targetCommitteeSize), ()=>EthAddress.ZERO);
338
+ return Promise.all(epochsToSlash.map((epoch)=>this.epochCache.getCommitteeForEpoch(epoch).then((c)=>c.committee ?? emptyCommittee)));
339
+ }
340
+ /**
341
+ * Gather offenses to be slashed on a given round.
342
+ * Round N slashes validators from round N - slashOffsetInRounds.
343
+ * @param round - The round to get offenses for, defaults to current round
344
+ * @returns Array of pending offenses for the round with offset applied
345
+ */ async gatherOffensesForRound(round) {
346
+ const targetRound = this.getSlashedRound(round);
347
+ if (targetRound < 0n) {
348
+ return [];
349
+ }
350
+ return await this.offensesStore.getOffensesForRound(targetRound);
351
+ }
352
+ /** Returns all offenses stored */ getOffenses() {
353
+ return this.offensesStore.getOffenses();
354
+ }
355
+ /**
356
+ * Returns the round to be slashed given the current round by applying the slash offset.
357
+ * During round N, we cannot slash the validators from the epochs of the same round, since the round is not over,
358
+ * and besides we would be asking the current validators to vote to slash themselves. So during round N we look at the
359
+ * epochs spanned during round N - SLASH_OFFSET_IN_ROUNDS. This offset means that the epochs we slash are complete,
360
+ * and also gives nodes time to detect any misbehavior (eg slashing for prunes requires the proof submission window to
361
+ * pass).
362
+ */ getSlashedRound(round) {
363
+ round ??= this.roundMonitor.getCurrentRound().round;
364
+ return round - BigInt(this.settings.slashingOffsetInRounds);
365
+ }
366
+ }
367
+ function getOffenseLogData(offense) {
368
+ return {
369
+ ...offense,
370
+ validator: offense.validator.toString(),
371
+ offenseType: getOffenseTypeName(offense.offenseType)
372
+ };
373
+ }
@@ -0,0 +1,44 @@
1
+ import { EpochCache } from '@aztec-labs/epoch-cache';
2
+ import { RollupContract } from '@aztec-labs/ethereum/contracts';
3
+ import type { ViemClient } from '@aztec-labs/ethereum/types';
4
+ import type { SlotNumber } from '@aztec-labs/foundation/branded-types';
5
+ import type { EthAddress } from '@aztec-labs/foundation/eth-address';
6
+ import { DateProvider } from '@aztec-labs/foundation/timer';
7
+ import { AztecLMDBStoreV2 } from '@aztec-labs/kv-store/lmdb-v2';
8
+ import type { SlasherConfig } from '@aztec-labs/stdlib/interfaces/server';
9
+ import type { DataStoreConfig } from '@aztec-labs/stdlib/kv-store';
10
+ import type { Offense, ProposerSlashAction } from '@aztec-labs/stdlib/slashing';
11
+ import type { SlasherClientInterface } from './slasher_client_interface.js';
12
+ import type { Watcher } from './watcher.js';
13
+ /**
14
+ * Facade for the Slasher client. This class forwards all requests to the actual Slasher client implementation.
15
+ * This class also monitors via the rollup contract when the underlying slasher proposer contract changes, and when it
16
+ * does, it stops the current slasher client, recreates a new one with the new contract address, and starts it again.
17
+ */
18
+ export declare class SlasherClientFacade implements SlasherClientInterface {
19
+ private config;
20
+ private rollup;
21
+ private l1Client;
22
+ private watchers;
23
+ private epochCache;
24
+ private dateProvider;
25
+ private kvStore;
26
+ private rollupRegisteredAtL2Slot;
27
+ private ownValidators;
28
+ private logger;
29
+ private client;
30
+ private unwatch;
31
+ constructor(config: SlasherConfig & DataStoreConfig & {
32
+ ethereumSlotDuration: number;
33
+ }, rollup: RollupContract, l1Client: ViemClient, watchers: Watcher[], epochCache: EpochCache, dateProvider: DateProvider, kvStore: AztecLMDBStoreV2, rollupRegisteredAtL2Slot: SlotNumber, ownValidators?: EthAddress[], logger?: import("@aztec-labs/foundation/log").Logger);
34
+ start(): Promise<void>;
35
+ stop(): Promise<void>;
36
+ getConfig(): SlasherConfig;
37
+ updateConfig(config: Partial<SlasherConfig>): void;
38
+ gatherOffensesForRound(round?: bigint): Promise<Offense[]>;
39
+ getOffenses(): Promise<Offense[]>;
40
+ getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]>;
41
+ private createSlasherClient;
42
+ private handleSlasherChange;
43
+ }
44
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhc2hlcl9jbGllbnRfZmFjYWRlLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvc2xhc2hlcl9jbGllbnRfZmFjYWRlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUNyRCxPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0sZ0NBQWdDLENBQUM7QUFDaEUsT0FBTyxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFDN0QsT0FBTyxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0sc0NBQXNDLENBQUM7QUFDdkUsT0FBTyxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0sb0NBQW9DLENBQUM7QUFFckUsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBQzVELE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBQ2hFLE9BQU8sS0FBSyxFQUFFLGFBQWEsRUFBRSxNQUFNLHNDQUFzQyxDQUFDO0FBQzFFLE9BQU8sS0FBSyxFQUFFLGVBQWUsRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBQ25FLE9BQU8sS0FBSyxFQUFFLE9BQU8sRUFBRSxtQkFBbUIsRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBR2hGLE9BQU8sS0FBSyxFQUFFLHNCQUFzQixFQUFFLE1BQU0sK0JBQStCLENBQUM7QUFDNUUsT0FBTyxLQUFLLEVBQUUsT0FBTyxFQUFFLE1BQU0sY0FBYyxDQUFDO0FBRTVDOzs7O0dBSUc7QUFDSCxxQkFBYSxtQkFBb0IsWUFBVyxzQkFBc0I7SUFLOUQsT0FBTyxDQUFDLE1BQU07SUFDZCxPQUFPLENBQUMsTUFBTTtJQUNkLE9BQU8sQ0FBQyxRQUFRO0lBQ2hCLE9BQU8sQ0FBQyxRQUFRO0lBQ2hCLE9BQU8sQ0FBQyxVQUFVO0lBQ2xCLE9BQU8sQ0FBQyxZQUFZO0lBQ3BCLE9BQU8sQ0FBQyxPQUFPO0lBQ2YsT0FBTyxDQUFDLHdCQUF3QjtJQUNoQyxPQUFPLENBQUMsYUFBYTtJQUNyQixPQUFPLENBQUMsTUFBTTtJQWJoQixPQUFPLENBQUMsTUFBTSxDQUFxQztJQUNuRCxPQUFPLENBQUMsT0FBTyxDQUEyQjtJQUUxQyxZQUNVLE1BQU0sRUFBRSxhQUFhLEdBQUcsZUFBZSxHQUFHO1FBQUUsb0JBQW9CLEVBQUUsTUFBTSxDQUFBO0tBQUUsRUFDMUUsTUFBTSxFQUFFLGNBQWMsRUFDdEIsUUFBUSxFQUFFLFVBQVUsRUFDcEIsUUFBUSxFQUFFLE9BQU8sRUFBRSxFQUNuQixVQUFVLEVBQUUsVUFBVSxFQUN0QixZQUFZLEVBQUUsWUFBWSxFQUMxQixPQUFPLEVBQUUsZ0JBQWdCLEVBQ3pCLHdCQUF3QixFQUFFLFVBQVUsRUFDcEMsYUFBYSxHQUFFLFVBQVUsRUFBTyxFQUNoQyxNQUFNLDhDQUEwQixFQUN0QztJQUVTLEtBQUssSUFBSSxPQUFPLENBQUMsSUFBSSxDQUFDLENBU2xDO0lBRVksSUFBSSxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FJakM7SUFFTSxTQUFTLElBQUksYUFBYSxDQUVoQztJQUVNLFlBQVksQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLGFBQWEsQ0FBQyxHQUFHLElBQUksQ0FJeEQ7SUFFTSxzQkFBc0IsQ0FBQyxLQUFLLENBQUMsRUFBRSxNQUFNLEdBQUcsT0FBTyxDQUFDLE9BQU8sRUFBRSxDQUFDLENBRWhFO0lBRU0sV0FBVyxJQUFJLE9BQU8sQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUV2QztJQUVNLGtCQUFrQixDQUFDLFVBQVUsRUFBRSxVQUFVLEdBQUcsT0FBTyxDQUFDLG1CQUFtQixFQUFFLENBQUMsQ0FFaEY7SUFFRCxPQUFPLENBQUMsbUJBQW1CO1lBZWIsbUJBQW1CO0NBTWxDIn0=
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slasher_client_facade.d.ts","sourceRoot":"","sources":["../src/slasher_client_facade.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAErE,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAC1E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAGhF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC5E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C;;;;GAIG;AACH,qBAAa,mBAAoB,YAAW,sBAAsB;IAK9D,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,wBAAwB;IAChC,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,MAAM;IAbhB,OAAO,CAAC,MAAM,CAAqC;IACnD,OAAO,CAAC,OAAO,CAA2B;IAE1C,YACU,MAAM,EAAE,aAAa,GAAG,eAAe,GAAG;QAAE,oBAAoB,EAAE,MAAM,CAAA;KAAE,EAC1E,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,UAAU,EACpB,QAAQ,EAAE,OAAO,EAAE,EACnB,UAAU,EAAE,UAAU,EACtB,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,gBAAgB,EACzB,wBAAwB,EAAE,UAAU,EACpC,aAAa,GAAE,UAAU,EAAO,EAChC,MAAM,8CAA0B,EACtC;IAES,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CASlC;IAEY,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAIjC;IAEM,SAAS,IAAI,aAAa,CAEhC;IAEM,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI,CAIxD;IAEM,sBAAsB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAEhE;IAEM,WAAW,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,CAEvC;IAEM,kBAAkB,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAEhF;IAED,OAAO,CAAC,mBAAmB;YAeb,mBAAmB;CAMlC"}