@aztec/slasher 0.0.1-commit.001888fc
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 +228 -0
- package/dest/config.d.ts +6 -0
- package/dest/config.d.ts.map +1 -0
- package/dest/config.js +146 -0
- package/dest/empire_slasher_client.d.ts +190 -0
- package/dest/empire_slasher_client.d.ts.map +1 -0
- package/dest/empire_slasher_client.js +564 -0
- package/dest/factory/create_facade.d.ts +16 -0
- package/dest/factory/create_facade.d.ts.map +1 -0
- package/dest/factory/create_facade.js +46 -0
- package/dest/factory/create_implementation.d.ts +18 -0
- package/dest/factory/create_implementation.d.ts.map +1 -0
- package/dest/factory/create_implementation.js +77 -0
- package/dest/factory/get_settings.d.ts +4 -0
- package/dest/factory/get_settings.d.ts.map +1 -0
- package/dest/factory/get_settings.js +36 -0
- package/dest/factory/index.d.ts +3 -0
- package/dest/factory/index.d.ts.map +1 -0
- package/dest/factory/index.js +2 -0
- package/dest/generated/slasher-defaults.d.ts +21 -0
- package/dest/generated/slasher-defaults.d.ts.map +1 -0
- package/dest/generated/slasher-defaults.js +21 -0
- package/dest/index.d.ts +11 -0
- package/dest/index.d.ts.map +1 -0
- package/dest/index.js +10 -0
- package/dest/null_slasher_client.d.ts +17 -0
- package/dest/null_slasher_client.d.ts.map +1 -0
- package/dest/null_slasher_client.js +33 -0
- package/dest/slash_offenses_collector.d.ts +48 -0
- package/dest/slash_offenses_collector.d.ts.map +1 -0
- package/dest/slash_offenses_collector.js +94 -0
- package/dest/slash_round_monitor.d.ts +30 -0
- package/dest/slash_round_monitor.d.ts.map +1 -0
- package/dest/slash_round_monitor.js +52 -0
- package/dest/slasher_client_facade.d.ts +45 -0
- package/dest/slasher_client_facade.d.ts.map +1 -0
- package/dest/slasher_client_facade.js +78 -0
- package/dest/slasher_client_interface.d.ts +39 -0
- package/dest/slasher_client_interface.d.ts.map +1 -0
- package/dest/slasher_client_interface.js +4 -0
- package/dest/stores/offenses_store.d.ts +37 -0
- package/dest/stores/offenses_store.d.ts.map +1 -0
- package/dest/stores/offenses_store.js +107 -0
- package/dest/stores/payloads_store.d.ts +29 -0
- package/dest/stores/payloads_store.d.ts.map +1 -0
- package/dest/stores/payloads_store.js +128 -0
- package/dest/stores/schema_version.d.ts +2 -0
- package/dest/stores/schema_version.d.ts.map +1 -0
- package/dest/stores/schema_version.js +1 -0
- package/dest/tally_slasher_client.d.ts +125 -0
- package/dest/tally_slasher_client.d.ts.map +1 -0
- package/dest/tally_slasher_client.js +354 -0
- package/dest/test/dummy_watcher.d.ts +11 -0
- package/dest/test/dummy_watcher.d.ts.map +1 -0
- package/dest/test/dummy_watcher.js +14 -0
- package/dest/watcher.d.ts +21 -0
- package/dest/watcher.d.ts.map +1 -0
- package/dest/watcher.js +1 -0
- package/dest/watchers/attestations_block_watcher.d.ts +34 -0
- package/dest/watchers/attestations_block_watcher.d.ts.map +1 -0
- package/dest/watchers/attestations_block_watcher.js +142 -0
- package/dest/watchers/epoch_prune_watcher.d.ts +39 -0
- package/dest/watchers/epoch_prune_watcher.d.ts.map +1 -0
- package/dest/watchers/epoch_prune_watcher.js +176 -0
- package/package.json +92 -0
- package/src/config.ts +172 -0
- package/src/empire_slasher_client.ts +649 -0
- package/src/factory/create_facade.ts +82 -0
- package/src/factory/create_implementation.ts +184 -0
- package/src/factory/get_settings.ts +58 -0
- package/src/factory/index.ts +2 -0
- package/src/generated/slasher-defaults.ts +23 -0
- package/src/index.ts +10 -0
- package/src/null_slasher_client.ts +41 -0
- package/src/slash_offenses_collector.ts +123 -0
- package/src/slash_round_monitor.ts +62 -0
- package/src/slasher_client_facade.ts +103 -0
- package/src/slasher_client_interface.ts +46 -0
- package/src/stores/offenses_store.ts +147 -0
- package/src/stores/payloads_store.ts +149 -0
- package/src/stores/schema_version.ts +1 -0
- package/src/tally_slasher_client.ts +448 -0
- package/src/test/dummy_watcher.ts +21 -0
- package/src/watcher.ts +27 -0
- package/src/watchers/attestations_block_watcher.ts +194 -0
- package/src/watchers/epoch_prune_watcher.ts +253 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
+
import { RegistryContract, RollupContract } from '@aztec/ethereum/contracts';
|
|
3
|
+
import type { L1ReaderConfig } from '@aztec/ethereum/l1-reader';
|
|
4
|
+
import type { ViemClient } from '@aztec/ethereum/types';
|
|
5
|
+
import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
6
|
+
import { unique } from '@aztec/foundation/collection';
|
|
7
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
8
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
9
|
+
import { DateProvider } from '@aztec/foundation/timer';
|
|
10
|
+
import { createStore } from '@aztec/kv-store/lmdb-v2';
|
|
11
|
+
import { getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
12
|
+
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
|
|
13
|
+
import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
|
|
14
|
+
|
|
15
|
+
import { SlasherClientFacade } from '../slasher_client_facade.js';
|
|
16
|
+
import type { SlasherClientInterface } from '../slasher_client_interface.js';
|
|
17
|
+
import { SCHEMA_VERSION } from '../stores/schema_version.js';
|
|
18
|
+
import type { Watcher } from '../watcher.js';
|
|
19
|
+
|
|
20
|
+
/** Creates a slasher client facade that updates itself whenever the rollup slasher changes */
|
|
21
|
+
export async function createSlasherFacade(
|
|
22
|
+
config: SlasherConfig & DataStoreConfig & { ethereumSlotDuration: number },
|
|
23
|
+
l1Contracts: Pick<L1ReaderConfig['l1Contracts'], 'rollupAddress' | 'slashFactoryAddress' | 'registryAddress'>,
|
|
24
|
+
l1Client: ViemClient,
|
|
25
|
+
watchers: Watcher[],
|
|
26
|
+
dateProvider: DateProvider,
|
|
27
|
+
epochCache: EpochCache,
|
|
28
|
+
/** List of own validator addresses to add to the slashValidatorNever list unless slashSelfAllowed is true */
|
|
29
|
+
validatorAddresses: EthAddress[] = [],
|
|
30
|
+
logger = createLogger('slasher'),
|
|
31
|
+
): Promise<SlasherClientInterface> {
|
|
32
|
+
if (!l1Contracts.rollupAddress || l1Contracts.rollupAddress.equals(EthAddress.ZERO)) {
|
|
33
|
+
throw new Error('Cannot initialize SlasherClient without a Rollup address');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const kvStore = await createStore('slasher', SCHEMA_VERSION, config, logger.getBindings());
|
|
37
|
+
const rollup = new RollupContract(l1Client, l1Contracts.rollupAddress);
|
|
38
|
+
|
|
39
|
+
// Compute and cache the L2 slot at which the rollup was registered as canonical
|
|
40
|
+
const settingsMap = kvStore.openMap<string, number>('slasher-settings');
|
|
41
|
+
const cacheKey = `registeredSlot:${l1Contracts.rollupAddress}`;
|
|
42
|
+
let rollupRegisteredAtL2Slot = (await settingsMap.getAsync(cacheKey)) as SlotNumber | undefined;
|
|
43
|
+
|
|
44
|
+
if (rollupRegisteredAtL2Slot === undefined) {
|
|
45
|
+
const registry = new RegistryContract(l1Client, l1Contracts.registryAddress);
|
|
46
|
+
const l1StartBlock = await rollup.getL1StartBlock();
|
|
47
|
+
const registrationTimestamp = await registry.getCanonicalRollupRegistrationTimestamp(
|
|
48
|
+
l1Contracts.rollupAddress,
|
|
49
|
+
l1StartBlock,
|
|
50
|
+
);
|
|
51
|
+
if (registrationTimestamp !== undefined) {
|
|
52
|
+
const l1GenesisTime = await rollup.getL1GenesisTime();
|
|
53
|
+
const slotDuration = await rollup.getSlotDuration();
|
|
54
|
+
rollupRegisteredAtL2Slot = getSlotAtTimestamp(registrationTimestamp, {
|
|
55
|
+
l1GenesisTime,
|
|
56
|
+
slotDuration: Number(slotDuration),
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
rollupRegisteredAtL2Slot = SlotNumber(0);
|
|
60
|
+
}
|
|
61
|
+
await settingsMap.set(cacheKey, rollupRegisteredAtL2Slot);
|
|
62
|
+
logger.info(`Canonical rollup registered at L2 slot ${rollupRegisteredAtL2Slot}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const slashValidatorsNever = config.slashSelfAllowed
|
|
66
|
+
? config.slashValidatorsNever
|
|
67
|
+
: unique([...config.slashValidatorsNever, ...validatorAddresses].map(a => a.toString())).map(EthAddress.fromString);
|
|
68
|
+
const updatedConfig = { ...config, slashValidatorsNever };
|
|
69
|
+
|
|
70
|
+
return new SlasherClientFacade(
|
|
71
|
+
updatedConfig,
|
|
72
|
+
rollup,
|
|
73
|
+
l1Client,
|
|
74
|
+
l1Contracts.slashFactoryAddress,
|
|
75
|
+
watchers,
|
|
76
|
+
epochCache,
|
|
77
|
+
dateProvider,
|
|
78
|
+
kvStore,
|
|
79
|
+
rollupRegisteredAtL2Slot,
|
|
80
|
+
logger,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
+
import {
|
|
3
|
+
EmpireSlashingProposerContract,
|
|
4
|
+
RollupContract,
|
|
5
|
+
TallySlashingProposerContract,
|
|
6
|
+
} from '@aztec/ethereum/contracts';
|
|
7
|
+
import type { ViemClient } from '@aztec/ethereum/types';
|
|
8
|
+
import type { SlotNumber } from '@aztec/foundation/branded-types';
|
|
9
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
10
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
11
|
+
import { DateProvider } from '@aztec/foundation/timer';
|
|
12
|
+
import { AztecLMDBStoreV2 } from '@aztec/kv-store/lmdb-v2';
|
|
13
|
+
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
|
|
14
|
+
import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
|
|
15
|
+
import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
|
|
16
|
+
|
|
17
|
+
import { EmpireSlasherClient, type EmpireSlasherSettings } from '../empire_slasher_client.js';
|
|
18
|
+
import { NullSlasherClient } from '../null_slasher_client.js';
|
|
19
|
+
import { SlasherOffensesStore } from '../stores/offenses_store.js';
|
|
20
|
+
import { SlasherPayloadsStore } from '../stores/payloads_store.js';
|
|
21
|
+
import { TallySlasherClient } from '../tally_slasher_client.js';
|
|
22
|
+
import type { Watcher } from '../watcher.js';
|
|
23
|
+
import { getTallySlasherSettings } from './get_settings.js';
|
|
24
|
+
|
|
25
|
+
/** Creates a slasher client implementation (either tally or empire) based on the slasher proposer type in the rollup */
|
|
26
|
+
export async function createSlasherImplementation(
|
|
27
|
+
config: SlasherConfig & DataStoreConfig & { ethereumSlotDuration: number },
|
|
28
|
+
rollup: RollupContract,
|
|
29
|
+
l1Client: ViemClient,
|
|
30
|
+
slashFactoryAddress: EthAddress | undefined,
|
|
31
|
+
watchers: Watcher[],
|
|
32
|
+
epochCache: EpochCache,
|
|
33
|
+
dateProvider: DateProvider,
|
|
34
|
+
kvStore: AztecLMDBStoreV2,
|
|
35
|
+
rollupRegisteredAtL2Slot: SlotNumber,
|
|
36
|
+
logger = createLogger('slasher'),
|
|
37
|
+
) {
|
|
38
|
+
const proposer = await rollup.getSlashingProposer();
|
|
39
|
+
if (!proposer) {
|
|
40
|
+
return new NullSlasherClient(config);
|
|
41
|
+
} else if (proposer.type === 'tally') {
|
|
42
|
+
return createTallySlasher(
|
|
43
|
+
config,
|
|
44
|
+
rollup,
|
|
45
|
+
proposer,
|
|
46
|
+
watchers,
|
|
47
|
+
dateProvider,
|
|
48
|
+
epochCache,
|
|
49
|
+
kvStore,
|
|
50
|
+
rollupRegisteredAtL2Slot,
|
|
51
|
+
logger,
|
|
52
|
+
);
|
|
53
|
+
} else {
|
|
54
|
+
if (!slashFactoryAddress || slashFactoryAddress.equals(EthAddress.ZERO)) {
|
|
55
|
+
throw new Error('Cannot initialize an empire-based SlasherClient without a SlashFactory address');
|
|
56
|
+
}
|
|
57
|
+
const slashFactory = new SlashFactoryContract(l1Client, slashFactoryAddress.toString());
|
|
58
|
+
return createEmpireSlasher(
|
|
59
|
+
config,
|
|
60
|
+
rollup,
|
|
61
|
+
proposer,
|
|
62
|
+
slashFactory,
|
|
63
|
+
watchers,
|
|
64
|
+
dateProvider,
|
|
65
|
+
kvStore,
|
|
66
|
+
rollupRegisteredAtL2Slot,
|
|
67
|
+
logger,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function createEmpireSlasher(
|
|
73
|
+
config: SlasherConfig & DataStoreConfig & { ethereumSlotDuration: number },
|
|
74
|
+
rollup: RollupContract,
|
|
75
|
+
slashingProposer: EmpireSlashingProposerContract,
|
|
76
|
+
slashFactoryContract: SlashFactoryContract,
|
|
77
|
+
watchers: Watcher[],
|
|
78
|
+
dateProvider: DateProvider,
|
|
79
|
+
kvStore: AztecLMDBStoreV2,
|
|
80
|
+
rollupRegisteredAtL2Slot: SlotNumber,
|
|
81
|
+
logger = createLogger('slasher'),
|
|
82
|
+
): Promise<EmpireSlasherClient> {
|
|
83
|
+
if (slashingProposer.type !== 'empire') {
|
|
84
|
+
throw new Error('Slashing proposer contract is not of type Empire');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const [
|
|
88
|
+
slashingExecutionDelayInRounds,
|
|
89
|
+
slashingPayloadLifetimeInRounds,
|
|
90
|
+
slashingRoundSize,
|
|
91
|
+
slashingQuorumSize,
|
|
92
|
+
epochDuration,
|
|
93
|
+
proofSubmissionEpochs,
|
|
94
|
+
l1GenesisTime,
|
|
95
|
+
slotDuration,
|
|
96
|
+
l1StartBlock,
|
|
97
|
+
slasher,
|
|
98
|
+
] = await Promise.all([
|
|
99
|
+
slashingProposer.getExecutionDelayInRounds(),
|
|
100
|
+
slashingProposer.getLifetimeInRounds(),
|
|
101
|
+
slashingProposer.getRoundSize(),
|
|
102
|
+
slashingProposer.getQuorumSize(),
|
|
103
|
+
rollup.getEpochDuration(),
|
|
104
|
+
rollup.getProofSubmissionEpochs(),
|
|
105
|
+
rollup.getL1GenesisTime(),
|
|
106
|
+
rollup.getSlotDuration(),
|
|
107
|
+
rollup.getL1StartBlock(),
|
|
108
|
+
rollup.getSlasherContract(),
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
const settings: EmpireSlasherSettings = {
|
|
112
|
+
slashingExecutionDelayInRounds: Number(slashingExecutionDelayInRounds),
|
|
113
|
+
slashingPayloadLifetimeInRounds: Number(slashingPayloadLifetimeInRounds),
|
|
114
|
+
slashingRoundSize: Number(slashingRoundSize),
|
|
115
|
+
slashingQuorumSize: Number(slashingQuorumSize),
|
|
116
|
+
epochDuration: Number(epochDuration),
|
|
117
|
+
proofSubmissionEpochs: Number(proofSubmissionEpochs),
|
|
118
|
+
l1GenesisTime: l1GenesisTime,
|
|
119
|
+
slotDuration: Number(slotDuration),
|
|
120
|
+
l1StartBlock,
|
|
121
|
+
ethereumSlotDuration: config.ethereumSlotDuration,
|
|
122
|
+
slashingAmounts: undefined,
|
|
123
|
+
rollupRegisteredAtL2Slot,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const payloadsStore = new SlasherPayloadsStore(kvStore, {
|
|
127
|
+
slashingPayloadLifetimeInRounds: settings.slashingPayloadLifetimeInRounds,
|
|
128
|
+
});
|
|
129
|
+
const offensesStore = new SlasherOffensesStore(kvStore, {
|
|
130
|
+
...settings,
|
|
131
|
+
slashOffenseExpirationRounds: config.slashOffenseExpirationRounds,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return new EmpireSlasherClient(
|
|
135
|
+
config,
|
|
136
|
+
settings,
|
|
137
|
+
slashFactoryContract,
|
|
138
|
+
slashingProposer,
|
|
139
|
+
slasher!,
|
|
140
|
+
rollup,
|
|
141
|
+
watchers,
|
|
142
|
+
dateProvider,
|
|
143
|
+
offensesStore,
|
|
144
|
+
payloadsStore,
|
|
145
|
+
logger,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function createTallySlasher(
|
|
150
|
+
config: SlasherConfig & DataStoreConfig,
|
|
151
|
+
rollup: RollupContract,
|
|
152
|
+
slashingProposer: TallySlashingProposerContract,
|
|
153
|
+
watchers: Watcher[],
|
|
154
|
+
dateProvider: DateProvider,
|
|
155
|
+
epochCache: EpochCache,
|
|
156
|
+
kvStore: AztecLMDBStoreV2,
|
|
157
|
+
rollupRegisteredAtL2Slot: SlotNumber,
|
|
158
|
+
logger = createLogger('slasher'),
|
|
159
|
+
): Promise<TallySlasherClient> {
|
|
160
|
+
if (slashingProposer.type !== 'tally') {
|
|
161
|
+
throw new Error('Slashing proposer contract is not of type tally');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const settings = { ...(await getTallySlasherSettings(rollup, slashingProposer)), rollupRegisteredAtL2Slot };
|
|
165
|
+
const slasher = await rollup.getSlasherContract();
|
|
166
|
+
|
|
167
|
+
const offensesStore = new SlasherOffensesStore(kvStore, {
|
|
168
|
+
...settings,
|
|
169
|
+
slashOffenseExpirationRounds: config.slashOffenseExpirationRounds,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return new TallySlasherClient(
|
|
173
|
+
config,
|
|
174
|
+
settings,
|
|
175
|
+
slashingProposer,
|
|
176
|
+
slasher!,
|
|
177
|
+
rollup,
|
|
178
|
+
watchers,
|
|
179
|
+
epochCache,
|
|
180
|
+
dateProvider,
|
|
181
|
+
offensesStore,
|
|
182
|
+
logger,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { RollupContract, TallySlashingProposerContract } from '@aztec/ethereum/contracts';
|
|
2
|
+
|
|
3
|
+
import type { TallySlasherSettings } from '../tally_slasher_client.js';
|
|
4
|
+
|
|
5
|
+
export async function getTallySlasherSettings(
|
|
6
|
+
rollup: RollupContract,
|
|
7
|
+
slashingProposer?: TallySlashingProposerContract,
|
|
8
|
+
): Promise<Omit<TallySlasherSettings, 'rollupRegisteredAtL2Slot'>> {
|
|
9
|
+
if (!slashingProposer) {
|
|
10
|
+
const rollupSlashingProposer = await rollup.getSlashingProposer();
|
|
11
|
+
if (!rollupSlashingProposer || rollupSlashingProposer.type !== 'tally') {
|
|
12
|
+
throw new Error('Rollup slashing proposer is not of type tally');
|
|
13
|
+
}
|
|
14
|
+
slashingProposer = rollupSlashingProposer;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const [
|
|
18
|
+
slashingExecutionDelayInRounds,
|
|
19
|
+
slashingRoundSize,
|
|
20
|
+
slashingRoundSizeInEpochs,
|
|
21
|
+
slashingLifetimeInRounds,
|
|
22
|
+
slashingOffsetInRounds,
|
|
23
|
+
slashingAmounts,
|
|
24
|
+
slashingQuorumSize,
|
|
25
|
+
epochDuration,
|
|
26
|
+
l1GenesisTime,
|
|
27
|
+
slotDuration,
|
|
28
|
+
targetCommitteeSize,
|
|
29
|
+
] = await Promise.all([
|
|
30
|
+
slashingProposer.getExecutionDelayInRounds(),
|
|
31
|
+
slashingProposer.getRoundSize(),
|
|
32
|
+
slashingProposer.getRoundSizeInEpochs(),
|
|
33
|
+
slashingProposer.getLifetimeInRounds(),
|
|
34
|
+
slashingProposer.getSlashOffsetInRounds(),
|
|
35
|
+
slashingProposer.getSlashingAmounts(),
|
|
36
|
+
slashingProposer.getQuorumSize(),
|
|
37
|
+
rollup.getEpochDuration(),
|
|
38
|
+
rollup.getL1GenesisTime(),
|
|
39
|
+
rollup.getSlotDuration(),
|
|
40
|
+
rollup.getTargetCommitteeSize(),
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const settings: Omit<TallySlasherSettings, 'rollupRegisteredAtL2Slot'> = {
|
|
44
|
+
slashingExecutionDelayInRounds: Number(slashingExecutionDelayInRounds),
|
|
45
|
+
slashingRoundSize: Number(slashingRoundSize),
|
|
46
|
+
slashingRoundSizeInEpochs: Number(slashingRoundSizeInEpochs),
|
|
47
|
+
slashingLifetimeInRounds: Number(slashingLifetimeInRounds),
|
|
48
|
+
slashingQuorumSize: Number(slashingQuorumSize),
|
|
49
|
+
epochDuration: Number(epochDuration),
|
|
50
|
+
l1GenesisTime: l1GenesisTime,
|
|
51
|
+
slotDuration: Number(slotDuration),
|
|
52
|
+
slashingOffsetInRounds: Number(slashingOffsetInRounds),
|
|
53
|
+
slashingAmounts,
|
|
54
|
+
targetCommitteeSize: Number(targetCommitteeSize),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return settings;
|
|
58
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Auto-generated from spartan/environments/network-defaults.yml
|
|
2
|
+
// Do not edit manually - run yarn generate to regenerate
|
|
3
|
+
|
|
4
|
+
/** Default slasher configuration values from network-defaults.yml */
|
|
5
|
+
export const slasherDefaultEnv = {
|
|
6
|
+
SLASH_MIN_PENALTY_PERCENTAGE: 0.5,
|
|
7
|
+
SLASH_MAX_PENALTY_PERCENTAGE: 2,
|
|
8
|
+
SLASH_OFFENSE_EXPIRATION_ROUNDS: 4,
|
|
9
|
+
SLASH_MAX_PAYLOAD_SIZE: 80,
|
|
10
|
+
SLASH_EXECUTE_ROUNDS_LOOK_BACK: 4,
|
|
11
|
+
SLASH_PRUNE_PENALTY: 10000000000000000000,
|
|
12
|
+
SLASH_DATA_WITHHOLDING_PENALTY: 10000000000000000000,
|
|
13
|
+
SLASH_INACTIVITY_TARGET_PERCENTAGE: 0.9,
|
|
14
|
+
SLASH_INACTIVITY_CONSECUTIVE_EPOCH_THRESHOLD: 1,
|
|
15
|
+
SLASH_INACTIVITY_PENALTY: 10000000000000000000,
|
|
16
|
+
SLASH_PROPOSE_INVALID_ATTESTATIONS_PENALTY: 10000000000000000000,
|
|
17
|
+
SLASH_ATTEST_DESCENDANT_OF_INVALID_PENALTY: 10000000000000000000,
|
|
18
|
+
SLASH_DUPLICATE_PROPOSAL_PENALTY: 0,
|
|
19
|
+
SLASH_DUPLICATE_ATTESTATION_PENALTY: 0,
|
|
20
|
+
SLASH_UNKNOWN_PENALTY: 10000000000000000000,
|
|
21
|
+
SLASH_INVALID_BLOCK_PENALTY: 10000000000000000000,
|
|
22
|
+
SLASH_GRACE_PERIOD_L2_SLOTS: 0,
|
|
23
|
+
} as const;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './config.js';
|
|
2
|
+
export * from './watchers/epoch_prune_watcher.js';
|
|
3
|
+
export * from './watchers/attestations_block_watcher.js';
|
|
4
|
+
export * from './empire_slasher_client.js';
|
|
5
|
+
export * from './tally_slasher_client.js';
|
|
6
|
+
export * from './slash_offenses_collector.js';
|
|
7
|
+
export * from './slasher_client_interface.js';
|
|
8
|
+
export * from './factory/index.js';
|
|
9
|
+
export * from './watcher.js';
|
|
10
|
+
export * from '@aztec/stdlib/slashing';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { SlotNumber } from '@aztec/foundation/branded-types';
|
|
2
|
+
import type { Offense, ProposerSlashAction, SlashPayloadRound } from '@aztec/stdlib/slashing';
|
|
3
|
+
|
|
4
|
+
import type { SlasherConfig } from './config.js';
|
|
5
|
+
import type { SlasherClientInterface } from './slasher_client_interface.js';
|
|
6
|
+
|
|
7
|
+
export class NullSlasherClient implements SlasherClientInterface {
|
|
8
|
+
constructor(private config: SlasherConfig) {}
|
|
9
|
+
|
|
10
|
+
public start(): Promise<void> {
|
|
11
|
+
return Promise.resolve();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public stop(): Promise<void> {
|
|
15
|
+
return Promise.resolve();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public getSlashPayloads(): Promise<SlashPayloadRound[]> {
|
|
19
|
+
return Promise.resolve([]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public gatherOffensesForRound(_round?: bigint): Promise<Offense[]> {
|
|
23
|
+
return Promise.resolve([]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public getPendingOffenses(): Promise<Offense[]> {
|
|
27
|
+
return Promise.resolve([]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public updateConfig(config: Partial<SlasherConfig>): void {
|
|
31
|
+
this.config = { ...this.config, ...config };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public getProposerActions(_slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
|
|
35
|
+
return Promise.resolve([]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public getConfig(): SlasherConfig {
|
|
39
|
+
return this.config;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { SlotNumber } from '@aztec/foundation/branded-types';
|
|
2
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
3
|
+
import type { Prettify } from '@aztec/foundation/types';
|
|
4
|
+
import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
|
|
5
|
+
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
|
|
6
|
+
import { type Offense, type OffenseIdentifier, getSlotForOffense } from '@aztec/stdlib/slashing';
|
|
7
|
+
|
|
8
|
+
import type { SlasherOffensesStore } from './stores/offenses_store.js';
|
|
9
|
+
import { WANT_TO_SLASH_EVENT, type WantToSlashArgs, type Watcher } from './watcher.js';
|
|
10
|
+
|
|
11
|
+
export type SlashOffensesCollectorConfig = Prettify<Pick<SlasherConfig, 'slashGracePeriodL2Slots'>>;
|
|
12
|
+
export type SlashOffensesCollectorSettings = Prettify<
|
|
13
|
+
Pick<L1RollupConstants, 'epochDuration'> & {
|
|
14
|
+
slashingAmounts: [bigint, bigint, bigint] | undefined;
|
|
15
|
+
/** L2 slot at which the rollup was registered as canonical in the Registry. Used to anchor the slash grace period. */
|
|
16
|
+
rollupRegisteredAtL2Slot: SlotNumber;
|
|
17
|
+
}
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Collects and manages slashable offenses from watchers.
|
|
22
|
+
* This class handles the common logic for subscribing to slash watcher events,
|
|
23
|
+
* storing offenses, and retrieving pending offenses for slashing.
|
|
24
|
+
*/
|
|
25
|
+
export class SlashOffensesCollector {
|
|
26
|
+
private readonly unwatchCallbacks: (() => void)[] = [];
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
private readonly config: SlashOffensesCollectorConfig,
|
|
30
|
+
private readonly settings: SlashOffensesCollectorSettings,
|
|
31
|
+
private readonly watchers: Watcher[],
|
|
32
|
+
private readonly offensesStore: SlasherOffensesStore,
|
|
33
|
+
private readonly log = createLogger('slasher:offenses-collector'),
|
|
34
|
+
) {}
|
|
35
|
+
|
|
36
|
+
public start() {
|
|
37
|
+
this.log.debug('Starting SlashOffensesCollector...');
|
|
38
|
+
|
|
39
|
+
// Subscribe to watchers WANT_TO_SLASH_EVENT
|
|
40
|
+
for (const watcher of this.watchers) {
|
|
41
|
+
const wantToSlashCallback = (args: WantToSlashArgs[]) =>
|
|
42
|
+
void this.handleWantToSlash(args).catch(err => this.log.error('Error handling wantToSlash', err));
|
|
43
|
+
watcher.on(WANT_TO_SLASH_EVENT, wantToSlashCallback);
|
|
44
|
+
this.unwatchCallbacks.push(() => watcher.removeListener(WANT_TO_SLASH_EVENT, wantToSlashCallback));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.log.info('Started SlashOffensesCollector');
|
|
48
|
+
return Promise.resolve();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public stop() {
|
|
52
|
+
this.log.debug('Stopping SlashOffensesCollector...');
|
|
53
|
+
|
|
54
|
+
for (const unwatchCallback of this.unwatchCallbacks) {
|
|
55
|
+
unwatchCallback();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.log.info('SlashOffensesCollector stopped');
|
|
59
|
+
return Promise.resolve();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Called when a slash watcher emits WANT_TO_SLASH_EVENT.
|
|
64
|
+
* Stores pending offenses instead of creating payloads immediately.
|
|
65
|
+
* @param args - the arguments from the watcher, including the validators, amounts, and offenses
|
|
66
|
+
*/
|
|
67
|
+
public async handleWantToSlash(args: WantToSlashArgs[]) {
|
|
68
|
+
for (const arg of args) {
|
|
69
|
+
const pendingOffense: Offense = {
|
|
70
|
+
validator: arg.validator,
|
|
71
|
+
amount: arg.amount,
|
|
72
|
+
offenseType: arg.offenseType,
|
|
73
|
+
epochOrSlot: arg.epochOrSlot,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (this.shouldSkipOffense(pendingOffense)) {
|
|
77
|
+
this.log.verbose('Skipping offense during grace period', pendingOffense);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (await this.offensesStore.hasOffense(pendingOffense)) {
|
|
82
|
+
this.log.debug('Skipping repeated offense', pendingOffense);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (this.settings.slashingAmounts) {
|
|
87
|
+
const minSlash = this.settings.slashingAmounts[0];
|
|
88
|
+
if (arg.amount < minSlash) {
|
|
89
|
+
this.log.warn(`Offense amount ${arg.amount} is below minimum slashing amount ${minSlash}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.log.info(`Adding pending offense for validator ${arg.validator}`, pendingOffense);
|
|
94
|
+
await this.offensesStore.addPendingOffense(pendingOffense);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Triggered on a time basis when we enter a new slashing round.
|
|
100
|
+
* Clears expired offenses from stores.
|
|
101
|
+
*/
|
|
102
|
+
public async handleNewRound(round: bigint) {
|
|
103
|
+
const cleared = await this.offensesStore.clearExpiredOffenses(round);
|
|
104
|
+
if (cleared && cleared > 0) {
|
|
105
|
+
this.log.debug(`Cleared ${cleared} expired offenses for round ${round}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Marks offenses as slashed (no longer pending)
|
|
111
|
+
* @param offenses - The offenses to mark as slashed
|
|
112
|
+
*/
|
|
113
|
+
public markAsSlashed(offenses: OffenseIdentifier[]) {
|
|
114
|
+
this.log.verbose(`Marking offenses as slashed`, { offenses });
|
|
115
|
+
return this.offensesStore.markAsSlashed(offenses);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Returns whether to skip an offense if it happened during the grace period after the network upgrade */
|
|
119
|
+
private shouldSkipOffense(offense: Offense): boolean {
|
|
120
|
+
const offenseSlot = getSlotForOffense(offense, this.settings);
|
|
121
|
+
return offenseSlot < this.settings.rollupRegisteredAtL2Slot + this.config.slashGracePeriodL2Slots;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
2
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
3
|
+
import type { DateProvider } from '@aztec/foundation/timer';
|
|
4
|
+
import type { Prettify } from '@aztec/foundation/types';
|
|
5
|
+
import { type L1RollupConstants, getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
6
|
+
import { getRoundForSlot } from '@aztec/stdlib/slashing';
|
|
7
|
+
|
|
8
|
+
export type SlashRoundMonitorSettings = Prettify<
|
|
9
|
+
Pick<L1RollupConstants, 'epochDuration' | 'l1GenesisTime' | 'slotDuration'> & { slashingRoundSize: number }
|
|
10
|
+
>;
|
|
11
|
+
|
|
12
|
+
export class SlashRoundMonitor {
|
|
13
|
+
private currentRound: bigint = 0n;
|
|
14
|
+
private intervalId: NodeJS.Timeout | undefined = undefined;
|
|
15
|
+
private handler: ((round: bigint) => Promise<void>) | undefined = undefined;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
private settings: SlashRoundMonitorSettings,
|
|
19
|
+
private dateProvider: DateProvider,
|
|
20
|
+
private log = createLogger('slasher:round-monitor'),
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
public start() {
|
|
24
|
+
// Check for round changes
|
|
25
|
+
this.currentRound = this.getCurrentRound().round;
|
|
26
|
+
this.intervalId = setInterval(() => {
|
|
27
|
+
const round = this.getCurrentRound().round;
|
|
28
|
+
if (round !== this.currentRound) {
|
|
29
|
+
this.currentRound = round;
|
|
30
|
+
if (this.handler) {
|
|
31
|
+
void this.handler(round).catch(err => this.log.error('Error handling new round', err));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}, 500);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public stop() {
|
|
38
|
+
if (this.intervalId) {
|
|
39
|
+
clearInterval(this.intervalId);
|
|
40
|
+
this.intervalId = undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public listenToNewRound(handler: (round: bigint) => Promise<void>): () => void {
|
|
45
|
+
this.handler = handler;
|
|
46
|
+
return () => {
|
|
47
|
+
this.handler = undefined;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Returns the slashing round number and the voting slot within the round based on the L2 chain slot */
|
|
52
|
+
public getRoundForSlot(slotNumber: SlotNumber): { round: bigint; votingSlot: SlotNumber } {
|
|
53
|
+
return getRoundForSlot(slotNumber, this.settings);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Returns the current slashing round and voting slot within the round */
|
|
57
|
+
public getCurrentRound(): { round: bigint; votingSlot: SlotNumber } {
|
|
58
|
+
const now = this.dateProvider.nowInSeconds();
|
|
59
|
+
const currentSlot = getSlotAtTimestamp(BigInt(now), this.settings);
|
|
60
|
+
return this.getRoundForSlot(currentSlot);
|
|
61
|
+
}
|
|
62
|
+
}
|