@aztec/slasher 4.0.0-nightly.20250907 → 4.0.0-nightly.20260107
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 +60 -11
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +8 -2
- package/dest/empire_slasher_client.d.ts +8 -6
- package/dest/empire_slasher_client.d.ts.map +1 -1
- package/dest/empire_slasher_client.js +11 -5
- package/dest/factory/create_facade.d.ts +3 -2
- package/dest/factory/create_facade.d.ts.map +1 -1
- package/dest/factory/create_implementation.d.ts +3 -3
- package/dest/factory/create_implementation.d.ts.map +1 -1
- package/dest/factory/create_implementation.js +6 -4
- package/dest/factory/get_settings.d.ts +2 -2
- package/dest/factory/get_settings.d.ts.map +1 -1
- package/dest/factory/index.d.ts +1 -1
- package/dest/index.d.ts +1 -1
- package/dest/null_slasher_client.d.ts +3 -2
- package/dest/null_slasher_client.d.ts.map +1 -1
- package/dest/slash_offenses_collector.d.ts +1 -1
- package/dest/slash_offenses_collector.d.ts.map +1 -1
- package/dest/slash_offenses_collector.js +1 -2
- package/dest/slash_round_monitor.d.ts +5 -4
- package/dest/slash_round_monitor.d.ts.map +1 -1
- package/dest/slasher_client_facade.d.ts +4 -3
- package/dest/slasher_client_facade.d.ts.map +1 -1
- package/dest/slasher_client_facade.js +1 -0
- package/dest/slasher_client_interface.d.ts +3 -2
- package/dest/slasher_client_interface.d.ts.map +1 -1
- package/dest/stores/offenses_store.d.ts +1 -1
- package/dest/stores/offenses_store.d.ts.map +1 -1
- package/dest/stores/offenses_store.js +1 -1
- package/dest/stores/payloads_store.d.ts +2 -2
- package/dest/stores/payloads_store.d.ts.map +1 -1
- package/dest/stores/schema_version.d.ts +1 -1
- package/dest/tally_slasher_client.d.ts +14 -8
- package/dest/tally_slasher_client.d.ts.map +1 -1
- package/dest/tally_slasher_client.js +57 -12
- 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 +3 -1
- package/dest/watcher.d.ts.map +1 -1
- package/dest/watchers/attestations_block_watcher.d.ts +6 -3
- package/dest/watchers/attestations_block_watcher.d.ts.map +1 -1
- package/dest/watchers/attestations_block_watcher.js +37 -22
- package/dest/watchers/epoch_prune_watcher.d.ts +8 -7
- package/dest/watchers/epoch_prune_watcher.d.ts.map +1 -1
- package/dest/watchers/epoch_prune_watcher.js +48 -37
- package/package.json +13 -12
- package/src/config.ts +8 -2
- package/src/empire_slasher_client.ts +15 -8
- package/src/factory/create_facade.ts +2 -1
- package/src/factory/create_implementation.ts +6 -1
- package/src/factory/get_settings.ts +1 -1
- package/src/null_slasher_client.ts +2 -1
- package/src/slash_offenses_collector.ts +1 -2
- package/src/slash_round_monitor.ts +3 -2
- package/src/slasher_client_facade.ts +4 -2
- package/src/slasher_client_interface.ts +2 -1
- package/src/stores/offenses_store.ts +1 -1
- package/src/tally_slasher_client.ts +80 -17
- package/src/test/dummy_watcher.ts +21 -0
- package/src/watcher.ts +4 -1
- package/src/watchers/attestations_block_watcher.ts +44 -26
- package/src/watchers/epoch_prune_watcher.ts +67 -55
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
|
|
2
|
+
import { merge, pick } from '@aztec/foundation/collection';
|
|
1
3
|
import { createLogger } from '@aztec/foundation/log';
|
|
2
4
|
import { L2BlockSourceEvents } from '@aztec/stdlib/block';
|
|
3
|
-
import {
|
|
5
|
+
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
6
|
+
import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';
|
|
4
7
|
import { ReExFailedTxsError, ReExStateMismatchError, TransactionsNotAvailableError, ValidatorError } from '@aztec/stdlib/validators';
|
|
5
8
|
import EventEmitter from 'node:events';
|
|
6
9
|
import { WANT_TO_SLASH_EVENT } from '../watcher.js';
|
|
10
|
+
const EpochPruneWatcherPenaltiesConfigKeys = [
|
|
11
|
+
'slashPrunePenalty',
|
|
12
|
+
'slashDataWithholdingPenalty'
|
|
13
|
+
];
|
|
7
14
|
/**
|
|
8
15
|
* This watcher is responsible for detecting chain prunes and creating slashing arguments for the committee.
|
|
9
16
|
* It only wants to slash if:
|
|
@@ -15,12 +22,13 @@ import { WANT_TO_SLASH_EVENT } from '../watcher.js';
|
|
|
15
22
|
epochCache;
|
|
16
23
|
txProvider;
|
|
17
24
|
blockBuilder;
|
|
18
|
-
penalties;
|
|
19
25
|
log;
|
|
20
26
|
// Store bound function reference for proper listener removal
|
|
21
27
|
boundHandlePruneL2Blocks;
|
|
28
|
+
penalties;
|
|
22
29
|
constructor(l2BlockSource, l1ToL2MessageSource, epochCache, txProvider, blockBuilder, penalties){
|
|
23
|
-
super(), this.l2BlockSource = l2BlockSource, this.l1ToL2MessageSource = l1ToL2MessageSource, this.epochCache = epochCache, this.txProvider = txProvider, this.blockBuilder = blockBuilder, this.
|
|
30
|
+
super(), this.l2BlockSource = l2BlockSource, this.l1ToL2MessageSource = l1ToL2MessageSource, this.epochCache = epochCache, this.txProvider = txProvider, this.blockBuilder = blockBuilder, this.log = createLogger('epoch-prune-watcher'), this.boundHandlePruneL2Blocks = this.handlePruneL2Blocks.bind(this);
|
|
31
|
+
this.penalties = pick(penalties, ...EpochPruneWatcherPenaltiesConfigKeys);
|
|
24
32
|
this.log.verbose(`EpochPruneWatcher initialized with penalties: valid epoch pruned=${penalties.slashPrunePenalty} data withholding=${penalties.slashDataWithholdingPenalty}`);
|
|
25
33
|
}
|
|
26
34
|
start() {
|
|
@@ -31,50 +39,52 @@ import { WANT_TO_SLASH_EVENT } from '../watcher.js';
|
|
|
31
39
|
this.l2BlockSource.removeListener(L2BlockSourceEvents.L2PruneDetected, this.boundHandlePruneL2Blocks);
|
|
32
40
|
return Promise.resolve();
|
|
33
41
|
}
|
|
42
|
+
updateConfig(config) {
|
|
43
|
+
this.penalties = merge(this.penalties, pick(config, ...EpochPruneWatcherPenaltiesConfigKeys));
|
|
44
|
+
this.log.verbose('EpochPruneWatcher config updated', this.penalties);
|
|
45
|
+
}
|
|
34
46
|
handlePruneL2Blocks(event) {
|
|
35
47
|
const { blocks, epochNumber } = event;
|
|
36
|
-
this.log.
|
|
37
|
-
|
|
48
|
+
void this.processPruneL2Blocks(blocks, epochNumber).catch((err)=>this.log.error('Error processing pruned L2 blocks', err, {
|
|
49
|
+
epochNumber
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
async emitSlashForEpoch(offense, epochNumber) {
|
|
53
|
+
const validators = await this.getValidatorsForEpoch(epochNumber);
|
|
54
|
+
if (validators.length === 0) {
|
|
55
|
+
this.log.warn(`No validators found for epoch ${epochNumber} (cannot slash for ${getOffenseTypeName(offense)})`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const args = this.validatorsToSlashingArgs(validators, offense, epochNumber);
|
|
59
|
+
this.log.verbose(`Created slash for ${getOffenseTypeName(offense)} at epoch ${epochNumber}`, args);
|
|
60
|
+
this.emit(WANT_TO_SLASH_EVENT, args);
|
|
61
|
+
}
|
|
62
|
+
async processPruneL2Blocks(blocks, epochNumber) {
|
|
63
|
+
try {
|
|
64
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
65
|
+
const epochBlocks = blocks.filter((b)=>getEpochAtSlot(b.slot, l1Constants) === epochNumber);
|
|
66
|
+
this.log.info(`Detected chain prune. Validating epoch ${epochNumber} with blocks ${epochBlocks[0]?.number} to ${epochBlocks[epochBlocks.length - 1]?.number}.`, {
|
|
67
|
+
blocks: epochBlocks.map((b)=>b.toBlockInfo())
|
|
68
|
+
});
|
|
69
|
+
await this.validateBlocks(epochBlocks);
|
|
38
70
|
this.log.info(`Pruned epoch ${epochNumber} was valid. Want to slash committee for not having it proven.`);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const result = {
|
|
42
|
-
validators,
|
|
43
|
-
offense: OffenseType.VALID_EPOCH_PRUNED
|
|
44
|
-
};
|
|
45
|
-
return result;
|
|
46
|
-
}).catch(async (error)=>{
|
|
71
|
+
await this.emitSlashForEpoch(OffenseType.VALID_EPOCH_PRUNED, epochNumber);
|
|
72
|
+
} catch (error) {
|
|
47
73
|
if (error instanceof TransactionsNotAvailableError) {
|
|
48
|
-
this.log.info(`Data for pruned epoch ${epochNumber} was not available. Will want to slash.`,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
offense: OffenseType.DATA_WITHHOLDING
|
|
53
|
-
};
|
|
74
|
+
this.log.info(`Data for pruned epoch ${epochNumber} was not available. Will want to slash.`, {
|
|
75
|
+
message: error.message
|
|
76
|
+
});
|
|
77
|
+
await this.emitSlashForEpoch(OffenseType.DATA_WITHHOLDING, epochNumber);
|
|
54
78
|
} else {
|
|
55
79
|
this.log.error(`Error while validating pruned epoch ${epochNumber}. Will not want to slash.`, error);
|
|
56
|
-
return {
|
|
57
|
-
validators: [],
|
|
58
|
-
offense: undefined
|
|
59
|
-
};
|
|
60
80
|
}
|
|
61
|
-
}
|
|
62
|
-
if (validators.length === 0 || offense === undefined) {
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
const args = this.validatorsToSlashingArgs(validators, offense, BigInt(epochNumber));
|
|
66
|
-
this.log.info(`Slash for epoch ${epochNumber} created`, args);
|
|
67
|
-
this.emit(WANT_TO_SLASH_EVENT, args);
|
|
68
|
-
}).catch((error)=>{
|
|
69
|
-
// This can happen if we fail to get the validators for the epoch.
|
|
70
|
-
this.log.error('Error while creating slash for epoch', error);
|
|
71
|
-
});
|
|
81
|
+
}
|
|
72
82
|
}
|
|
73
83
|
async validateBlocks(blocks) {
|
|
74
84
|
if (blocks.length === 0) {
|
|
75
85
|
return;
|
|
76
86
|
}
|
|
77
|
-
const fork = await this.blockBuilder.getFork(blocks[0].header.globalVariables.blockNumber - 1);
|
|
87
|
+
const fork = await this.blockBuilder.getFork(BlockNumber(blocks[0].header.globalVariables.blockNumber - 1));
|
|
78
88
|
try {
|
|
79
89
|
for (const block of blocks){
|
|
80
90
|
await this.validateBlock(block, fork);
|
|
@@ -93,7 +103,8 @@ import { WANT_TO_SLASH_EVENT } from '../watcher.js';
|
|
|
93
103
|
if (missingTxs && missingTxs.length > 0) {
|
|
94
104
|
throw new TransactionsNotAvailableError(missingTxs);
|
|
95
105
|
}
|
|
96
|
-
const
|
|
106
|
+
const checkpointNumber = CheckpointNumber.fromBlockNumber(blockFromL1.number);
|
|
107
|
+
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
97
108
|
const { block, failedTxs, numTxs } = await this.blockBuilder.buildBlock(txs, l1ToL2Messages, blockFromL1.header.globalVariables, {}, fork);
|
|
98
109
|
if (numTxs !== txs.length) {
|
|
99
110
|
// This should be detected by state mismatch, but this makes it easier to debug.
|
|
@@ -120,7 +131,7 @@ import { WANT_TO_SLASH_EVENT } from '../watcher.js';
|
|
|
120
131
|
validator: v,
|
|
121
132
|
amount: penalty,
|
|
122
133
|
offenseType,
|
|
123
|
-
epochOrSlot
|
|
134
|
+
epochOrSlot: BigInt(epochOrSlot)
|
|
124
135
|
}));
|
|
125
136
|
}
|
|
126
137
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/slasher",
|
|
3
|
-
"version": "4.0.0-nightly.
|
|
3
|
+
"version": "4.0.0-nightly.20260107",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dest/index.js",
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"../package.common.json"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build": "yarn clean && tsc
|
|
14
|
-
"build:dev": "tsc
|
|
13
|
+
"build": "yarn clean && ../scripts/tsc.sh",
|
|
14
|
+
"build:dev": "../scripts/tsc.sh --watch",
|
|
15
15
|
"clean": "rm -rf ./dest .tsbuildinfo",
|
|
16
16
|
"bb": "node --no-warnings ./dest/bb/index.js",
|
|
17
17
|
"test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}"
|
|
@@ -54,24 +54,25 @@
|
|
|
54
54
|
]
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@aztec/epoch-cache": "4.0.0-nightly.
|
|
58
|
-
"@aztec/ethereum": "4.0.0-nightly.
|
|
59
|
-
"@aztec/foundation": "4.0.0-nightly.
|
|
60
|
-
"@aztec/kv-store": "4.0.0-nightly.
|
|
61
|
-
"@aztec/l1-artifacts": "4.0.0-nightly.
|
|
62
|
-
"@aztec/stdlib": "4.0.0-nightly.
|
|
63
|
-
"@aztec/telemetry-client": "4.0.0-nightly.
|
|
57
|
+
"@aztec/epoch-cache": "4.0.0-nightly.20260107",
|
|
58
|
+
"@aztec/ethereum": "4.0.0-nightly.20260107",
|
|
59
|
+
"@aztec/foundation": "4.0.0-nightly.20260107",
|
|
60
|
+
"@aztec/kv-store": "4.0.0-nightly.20260107",
|
|
61
|
+
"@aztec/l1-artifacts": "4.0.0-nightly.20260107",
|
|
62
|
+
"@aztec/stdlib": "4.0.0-nightly.20260107",
|
|
63
|
+
"@aztec/telemetry-client": "4.0.0-nightly.20260107",
|
|
64
64
|
"source-map-support": "^0.5.21",
|
|
65
65
|
"tslib": "^2.4.0",
|
|
66
|
-
"viem": "2.
|
|
66
|
+
"viem": "npm:@aztec/viem@2.38.2",
|
|
67
67
|
"zod": "^3.23.8"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@aztec/aztec.js": "4.0.0-nightly.
|
|
70
|
+
"@aztec/aztec.js": "4.0.0-nightly.20260107",
|
|
71
71
|
"@jest/globals": "^30.0.0",
|
|
72
72
|
"@types/jest": "^30.0.0",
|
|
73
73
|
"@types/node": "^22.15.17",
|
|
74
74
|
"@types/source-map-support": "^0.5.10",
|
|
75
|
+
"@typescript/native-preview": "7.0.0-dev.20251126.1",
|
|
75
76
|
"jest": "^30.0.0",
|
|
76
77
|
"jest-mock-extended": "^4.0.0",
|
|
77
78
|
"ts-node": "^10.9.1",
|
package/src/config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DefaultL1ContractsConfig } from '@aztec/ethereum';
|
|
1
|
+
import { DefaultL1ContractsConfig } from '@aztec/ethereum/config';
|
|
2
2
|
import type { ConfigMappingsType } from '@aztec/foundation/config';
|
|
3
3
|
import {
|
|
4
4
|
bigintConfigHelper,
|
|
@@ -29,6 +29,7 @@ export const DefaultSlasherConfig: SlasherConfig = {
|
|
|
29
29
|
slashOffenseExpirationRounds: 4,
|
|
30
30
|
slashMaxPayloadSize: 50,
|
|
31
31
|
slashGracePeriodL2Slots: 0,
|
|
32
|
+
slashExecuteRoundsLookBack: 4,
|
|
32
33
|
slashSelfAllowed: false,
|
|
33
34
|
};
|
|
34
35
|
|
|
@@ -83,7 +84,7 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
|
|
|
83
84
|
},
|
|
84
85
|
slashBroadcastedInvalidBlockPenalty: {
|
|
85
86
|
env: 'SLASH_INVALID_BLOCK_PENALTY',
|
|
86
|
-
description: 'Penalty amount for slashing a validator for an invalid block.',
|
|
87
|
+
description: 'Penalty amount for slashing a validator for an invalid block proposed via p2p.',
|
|
87
88
|
...bigintConfigHelper(DefaultSlasherConfig.slashBroadcastedInvalidBlockPenalty),
|
|
88
89
|
},
|
|
89
90
|
slashInactivityTargetPercentage: {
|
|
@@ -144,6 +145,11 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
|
|
|
144
145
|
env: 'SLASH_GRACE_PERIOD_L2_SLOTS',
|
|
145
146
|
...numberConfigHelper(DefaultSlasherConfig.slashGracePeriodL2Slots),
|
|
146
147
|
},
|
|
148
|
+
slashExecuteRoundsLookBack: {
|
|
149
|
+
env: 'SLASH_EXECUTE_ROUNDS_LOOK_BACK',
|
|
150
|
+
description: 'How many rounds to look back when searching for a round to execute.',
|
|
151
|
+
...numberConfigHelper(DefaultSlasherConfig.slashExecuteRoundsLookBack),
|
|
152
|
+
},
|
|
147
153
|
slashSelfAllowed: {
|
|
148
154
|
description: 'Whether to allow slashes to own validators',
|
|
149
155
|
...booleanConfigHelper(DefaultSlasherConfig.slashSelfAllowed),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { EmpireSlashingProposerContract, RollupContract } from '@aztec/ethereum';
|
|
1
|
+
import { EmpireSlashingProposerContract, RollupContract, SlasherContract } from '@aztec/ethereum/contracts';
|
|
2
2
|
import { sumBigint } from '@aztec/foundation/bigint';
|
|
3
|
+
import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
3
4
|
import { compactArray, filterAsync, maxBy, pick } from '@aztec/foundation/collection';
|
|
4
5
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
5
6
|
import { createLogger } from '@aztec/foundation/log';
|
|
@@ -121,6 +122,7 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
|
|
|
121
122
|
private settings: EmpireSlasherSettings,
|
|
122
123
|
private slashFactoryContract: SlashFactoryContract,
|
|
123
124
|
private slashingProposer: EmpireSlashingProposerContract,
|
|
125
|
+
private slasher: SlasherContract,
|
|
124
126
|
private rollup: RollupContract,
|
|
125
127
|
watchers: Watcher[],
|
|
126
128
|
private dateProvider: DateProvider,
|
|
@@ -368,7 +370,7 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
|
|
|
368
370
|
* @param slotNumber - The current slot number
|
|
369
371
|
* @returns The actions to take
|
|
370
372
|
*/
|
|
371
|
-
public async getProposerActions(slotNumber:
|
|
373
|
+
public async getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
|
|
372
374
|
const [executeAction, proposePayloadActions] = await Promise.all([
|
|
373
375
|
this.getExecutePayloadAction(slotNumber),
|
|
374
376
|
this.getProposePayloadActions(slotNumber),
|
|
@@ -378,7 +380,7 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
|
|
|
378
380
|
}
|
|
379
381
|
|
|
380
382
|
/** Returns an execute payload action if there are any payloads ready to be executed */
|
|
381
|
-
protected async getExecutePayloadAction(slotNumber:
|
|
383
|
+
protected async getExecutePayloadAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
|
|
382
384
|
const { round } = this.roundMonitor.getRoundForSlot(slotNumber);
|
|
383
385
|
const toRemove: PayloadWithRound[] = [];
|
|
384
386
|
|
|
@@ -403,12 +405,17 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
|
|
|
403
405
|
continue;
|
|
404
406
|
}
|
|
405
407
|
|
|
408
|
+
// Check if slashing is enabled at all
|
|
409
|
+
if (!(await this.slasher.isSlashingEnabled())) {
|
|
410
|
+
this.log.warn(`Slashing is disabled in the Slasher contract (skipping execution)`);
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
|
|
406
414
|
// Check if the slash payload is vetoed
|
|
407
|
-
const
|
|
408
|
-
const isVetoed = await slasherContract.isPayloadVetoed(payload.payload);
|
|
415
|
+
const isVetoed = await this.slasher.isPayloadVetoed(payload.payload);
|
|
409
416
|
|
|
410
417
|
if (isVetoed) {
|
|
411
|
-
this.log.info(`Payload ${payload.payload} from round ${payload.round} is vetoed
|
|
418
|
+
this.log.info(`Payload ${payload.payload} from round ${payload.round} is vetoed (skipping execution)`);
|
|
412
419
|
toRemove.push(payload);
|
|
413
420
|
continue;
|
|
414
421
|
}
|
|
@@ -424,7 +431,7 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
|
|
|
424
431
|
}
|
|
425
432
|
|
|
426
433
|
/** Returns a vote or create payload action based on payload scoring */
|
|
427
|
-
protected async getProposePayloadActions(slotNumber:
|
|
434
|
+
protected async getProposePayloadActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
|
|
428
435
|
// Compute what round we are in based on the slot number
|
|
429
436
|
const { round, votingSlot } = this.roundMonitor.getRoundForSlot(slotNumber);
|
|
430
437
|
const { slashingRoundSize: roundSize, slashingQuorumSize: quorumSize } = this.settings;
|
|
@@ -467,7 +474,7 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
|
|
|
467
474
|
// Find the best existing payload. We filter out those that have no chance of winning given how many voting
|
|
468
475
|
// slots are left in the round, and then filter by those we agree with.
|
|
469
476
|
const feasiblePayloads = existingPayloads.filter(
|
|
470
|
-
p => BigInt(quorumSize) - p.votes <= BigInt(roundSize) - votingSlot,
|
|
477
|
+
p => BigInt(quorumSize) - p.votes <= BigInt(roundSize) - BigInt(votingSlot),
|
|
471
478
|
);
|
|
472
479
|
const requiredOffenses = await this.getPendingUncontroversialOffensesForRound(round);
|
|
473
480
|
const agreedPayloads = await filterAsync(feasiblePayloads, p => this.agreeWithPayload(p, round, requiredOffenses));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
-
import type { L1ReaderConfig, ViemClient } from '@aztec/ethereum';
|
|
3
2
|
import { RollupContract } from '@aztec/ethereum/contracts';
|
|
3
|
+
import type { L1ReaderConfig } from '@aztec/ethereum/l1-reader';
|
|
4
|
+
import type { ViemClient } from '@aztec/ethereum/types';
|
|
4
5
|
import { unique } from '@aztec/foundation/collection';
|
|
5
6
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
6
7
|
import { createLogger } from '@aztec/foundation/log';
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
-
import type { ViemClient } from '@aztec/ethereum';
|
|
3
2
|
import {
|
|
4
3
|
EmpireSlashingProposerContract,
|
|
5
4
|
RollupContract,
|
|
6
5
|
TallySlashingProposerContract,
|
|
7
6
|
} from '@aztec/ethereum/contracts';
|
|
7
|
+
import type { ViemClient } from '@aztec/ethereum/types';
|
|
8
8
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
9
9
|
import { createLogger } from '@aztec/foundation/log';
|
|
10
10
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
@@ -71,6 +71,7 @@ async function createEmpireSlasher(
|
|
|
71
71
|
l1GenesisTime,
|
|
72
72
|
slotDuration,
|
|
73
73
|
l1StartBlock,
|
|
74
|
+
slasher,
|
|
74
75
|
] = await Promise.all([
|
|
75
76
|
slashingProposer.getExecutionDelayInRounds(),
|
|
76
77
|
slashingProposer.getLifetimeInRounds(),
|
|
@@ -81,6 +82,7 @@ async function createEmpireSlasher(
|
|
|
81
82
|
rollup.getL1GenesisTime(),
|
|
82
83
|
rollup.getSlotDuration(),
|
|
83
84
|
rollup.getL1StartBlock(),
|
|
85
|
+
rollup.getSlasherContract(),
|
|
84
86
|
]);
|
|
85
87
|
|
|
86
88
|
const settings: EmpireSlasherSettings = {
|
|
@@ -110,6 +112,7 @@ async function createEmpireSlasher(
|
|
|
110
112
|
settings,
|
|
111
113
|
slashFactoryContract,
|
|
112
114
|
slashingProposer,
|
|
115
|
+
slasher!,
|
|
113
116
|
rollup,
|
|
114
117
|
watchers,
|
|
115
118
|
dateProvider,
|
|
@@ -134,6 +137,7 @@ async function createTallySlasher(
|
|
|
134
137
|
}
|
|
135
138
|
|
|
136
139
|
const settings = await getTallySlasherSettings(rollup, slashingProposer);
|
|
140
|
+
const slasher = await rollup.getSlasherContract();
|
|
137
141
|
|
|
138
142
|
const offensesStore = new SlasherOffensesStore(kvStore, {
|
|
139
143
|
...settings,
|
|
@@ -144,6 +148,7 @@ async function createTallySlasher(
|
|
|
144
148
|
config,
|
|
145
149
|
settings,
|
|
146
150
|
slashingProposer,
|
|
151
|
+
slasher!,
|
|
147
152
|
rollup,
|
|
148
153
|
watchers,
|
|
149
154
|
epochCache,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SlotNumber } from '@aztec/foundation/branded-types';
|
|
1
2
|
import type { Offense, ProposerSlashAction, SlashPayloadRound } from '@aztec/stdlib/slashing';
|
|
2
3
|
|
|
3
4
|
import type { SlasherConfig } from './config.js';
|
|
@@ -30,7 +31,7 @@ export class NullSlasherClient implements SlasherClientInterface {
|
|
|
30
31
|
this.config = { ...this.config, ...config };
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
public getProposerActions(_slotNumber:
|
|
34
|
+
public getProposerActions(_slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
|
|
34
35
|
return Promise.resolve([]);
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -95,10 +95,9 @@ export class SlashOffensesCollector {
|
|
|
95
95
|
* Clears expired offenses from stores.
|
|
96
96
|
*/
|
|
97
97
|
public async handleNewRound(round: bigint) {
|
|
98
|
-
this.log.verbose(`Clearing expired offenses for new slashing round ${round}`);
|
|
99
98
|
const cleared = await this.offensesStore.clearExpiredOffenses(round);
|
|
100
99
|
if (cleared && cleared > 0) {
|
|
101
|
-
this.log.
|
|
100
|
+
this.log.debug(`Cleared ${cleared} expired offenses for round ${round}`);
|
|
102
101
|
}
|
|
103
102
|
}
|
|
104
103
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
1
2
|
import { createLogger } from '@aztec/foundation/log';
|
|
2
3
|
import type { DateProvider } from '@aztec/foundation/timer';
|
|
3
4
|
import type { Prettify } from '@aztec/foundation/types';
|
|
@@ -48,12 +49,12 @@ export class SlashRoundMonitor {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
/** Returns the slashing round number and the voting slot within the round based on the L2 chain slot */
|
|
51
|
-
public getRoundForSlot(slotNumber:
|
|
52
|
+
public getRoundForSlot(slotNumber: SlotNumber): { round: bigint; votingSlot: SlotNumber } {
|
|
52
53
|
return getRoundForSlot(slotNumber, this.settings);
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
/** Returns the current slashing round and voting slot within the round */
|
|
56
|
-
public getCurrentRound(): { round: bigint; votingSlot:
|
|
57
|
+
public getCurrentRound(): { round: bigint; votingSlot: SlotNumber } {
|
|
57
58
|
const now = this.dateProvider.nowInSeconds();
|
|
58
59
|
const currentSlot = getSlotAtTimestamp(BigInt(now), this.settings);
|
|
59
60
|
return this.getRoundForSlot(currentSlot);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
-
import type { ViemClient } from '@aztec/ethereum';
|
|
3
2
|
import { RollupContract } from '@aztec/ethereum/contracts';
|
|
3
|
+
import type { ViemClient } from '@aztec/ethereum/types';
|
|
4
|
+
import type { SlotNumber } from '@aztec/foundation/branded-types';
|
|
4
5
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
5
6
|
import { createLogger } from '@aztec/foundation/log';
|
|
6
7
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
@@ -58,6 +59,7 @@ export class SlasherClientFacade implements SlasherClientInterface {
|
|
|
58
59
|
public updateConfig(config: Partial<SlasherConfig>): void {
|
|
59
60
|
this.config = { ...this.config, ...config };
|
|
60
61
|
this.client?.updateConfig(config);
|
|
62
|
+
this.watchers.forEach(watcher => watcher.updateConfig?.(config));
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
public getSlashPayloads(): Promise<SlashPayloadRound[]> {
|
|
@@ -72,7 +74,7 @@ export class SlasherClientFacade implements SlasherClientInterface {
|
|
|
72
74
|
return this.client?.getPendingOffenses() ?? Promise.reject(new Error('Slasher client not initialized'));
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
public getProposerActions(slotNumber:
|
|
77
|
+
public getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
|
|
76
78
|
return this.client?.getProposerActions(slotNumber) ?? Promise.reject(new Error('Slasher client not initialized'));
|
|
77
79
|
}
|
|
78
80
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SlotNumber } from '@aztec/foundation/branded-types';
|
|
1
2
|
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
|
|
2
3
|
import type { Offense, ProposerSlashAction, SlashPayloadRound } from '@aztec/stdlib/slashing';
|
|
3
4
|
|
|
@@ -38,7 +39,7 @@ export interface SlasherClientInterface {
|
|
|
38
39
|
* @param slotNumber - The current slot number
|
|
39
40
|
* @returns The actions to take
|
|
40
41
|
*/
|
|
41
|
-
getProposerActions(slotNumber:
|
|
42
|
+
getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]>;
|
|
42
43
|
|
|
43
44
|
/** Returns the current config */
|
|
44
45
|
getConfig(): SlasherConfig;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { EthAddress } from '@aztec/aztec.js';
|
|
1
|
+
import { EthAddress } from '@aztec/aztec.js/addresses';
|
|
2
2
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
3
|
-
import { RollupContract, TallySlashingProposerContract } from '@aztec/ethereum/contracts';
|
|
3
|
+
import { RollupContract, SlasherContract, TallySlashingProposerContract } from '@aztec/ethereum/contracts';
|
|
4
|
+
import { maxBigint } from '@aztec/foundation/bigint';
|
|
5
|
+
import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
4
6
|
import { compactArray, partition, times } from '@aztec/foundation/collection';
|
|
5
7
|
import { createLogger } from '@aztec/foundation/log';
|
|
6
8
|
import { sleep } from '@aztec/foundation/sleep';
|
|
@@ -45,7 +47,7 @@ export type TallySlasherSettings = Prettify<
|
|
|
45
47
|
>;
|
|
46
48
|
|
|
47
49
|
export type TallySlasherClientConfig = SlashOffensesCollectorConfig &
|
|
48
|
-
Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever'>;
|
|
50
|
+
Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever' | 'slashExecuteRoundsLookBack'>;
|
|
49
51
|
|
|
50
52
|
/**
|
|
51
53
|
* The Tally Slasher client is responsible for managing slashable offenses using
|
|
@@ -88,6 +90,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
88
90
|
private config: TallySlasherClientConfig,
|
|
89
91
|
private settings: TallySlasherSettings,
|
|
90
92
|
private tallySlashingProposer: TallySlashingProposerContract,
|
|
93
|
+
private slasher: SlasherContract,
|
|
91
94
|
private rollup: RollupContract,
|
|
92
95
|
watchers: Watcher[],
|
|
93
96
|
private epochCache: EpochCache,
|
|
@@ -167,7 +170,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
167
170
|
* @param slotNumber - The current slot number
|
|
168
171
|
* @returns The actions to take
|
|
169
172
|
*/
|
|
170
|
-
public async getProposerActions(slotNumber:
|
|
173
|
+
public async getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
|
|
171
174
|
const [executeAction, voteAction] = await Promise.all([
|
|
172
175
|
this.getExecuteSlashAction(slotNumber),
|
|
173
176
|
this.getVoteOffensesAction(slotNumber),
|
|
@@ -176,16 +179,65 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
176
179
|
return compactArray<ProposerSlashAction>([executeAction, voteAction]);
|
|
177
180
|
}
|
|
178
181
|
|
|
179
|
-
/**
|
|
180
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Returns an execute slash action if there are any rounds ready to be executed.
|
|
184
|
+
* Returns the oldest slash action if there are multiple rounds pending execution.
|
|
185
|
+
*/
|
|
186
|
+
protected async getExecuteSlashAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
|
|
181
187
|
const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
|
|
182
188
|
const slashingExecutionDelayInRounds = BigInt(this.settings.slashingExecutionDelayInRounds);
|
|
183
189
|
const executableRound = currentRound - slashingExecutionDelayInRounds - 1n;
|
|
184
|
-
|
|
190
|
+
const lookBack = BigInt(this.config.slashExecuteRoundsLookBack);
|
|
191
|
+
const slashingLifetimeInRounds = BigInt(this.settings.slashingLifetimeInRounds);
|
|
192
|
+
|
|
193
|
+
// Compute the oldest executable round considering both lookBack and lifetimeInRounds
|
|
194
|
+
// A round is only executable if currentRound <= round + lifetimeInRounds
|
|
195
|
+
// So the oldest round we can execute is: currentRound - lifetimeInRounds
|
|
196
|
+
const oldestByLifetime = maxBigint(0n, currentRound - slashingLifetimeInRounds);
|
|
197
|
+
const oldestByLookBack = maxBigint(0n, executableRound - lookBack);
|
|
198
|
+
const oldestExecutableRound = maxBigint(oldestByLifetime, oldestByLookBack);
|
|
199
|
+
|
|
200
|
+
// Check if slashing is enabled at all
|
|
201
|
+
if (!(await this.slasher.isSlashingEnabled())) {
|
|
202
|
+
this.log.warn(`Slashing is disabled in the Slasher contract (skipping execution)`);
|
|
185
203
|
return undefined;
|
|
186
204
|
}
|
|
187
205
|
|
|
188
|
-
|
|
206
|
+
this.log.debug(`Checking slashing rounds ${oldestExecutableRound} to ${executableRound} to execute`, {
|
|
207
|
+
slotNumber,
|
|
208
|
+
currentRound,
|
|
209
|
+
oldestExecutableRound,
|
|
210
|
+
oldestByLifetime,
|
|
211
|
+
oldestByLookBack,
|
|
212
|
+
executableRound,
|
|
213
|
+
slashingExecutionDelayInRounds,
|
|
214
|
+
lookBack,
|
|
215
|
+
slashingLifetimeInRounds,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Iterate over all rounds, starting from the oldest, until we find one that is executable
|
|
219
|
+
for (let roundToCheck = oldestExecutableRound; roundToCheck <= executableRound; roundToCheck++) {
|
|
220
|
+
const action = await this.tryGetRoundExecuteAction(roundToCheck, slotNumber);
|
|
221
|
+
if (action) {
|
|
222
|
+
return action;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// And return nothing if none are found
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Checks if a given round is executable and returns an execute-slash action for it if so.
|
|
232
|
+
* Assumes round number has already been checked against lifetime and execution delay.
|
|
233
|
+
* @param executableRound - The round to check for execution
|
|
234
|
+
*/
|
|
235
|
+
private async tryGetRoundExecuteAction(
|
|
236
|
+
executableRound: bigint,
|
|
237
|
+
slotNumber: SlotNumber,
|
|
238
|
+
): Promise<ProposerSlashAction | undefined> {
|
|
239
|
+
let logData: Record<string, unknown> = { executableRound, slotNumber };
|
|
240
|
+
this.log.debug(`Testing if slashing round ${executableRound} is executable`, logData);
|
|
189
241
|
|
|
190
242
|
try {
|
|
191
243
|
const roundInfo = await this.tallySlashingProposer.getRound(executableRound);
|
|
@@ -193,9 +245,6 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
193
245
|
if (roundInfo.isExecuted) {
|
|
194
246
|
this.log.verbose(`Round ${executableRound} has already been executed`, logData);
|
|
195
247
|
return undefined;
|
|
196
|
-
} else if (!roundInfo.readyToExecute) {
|
|
197
|
-
this.log.verbose(`Round ${executableRound} is not ready to execute yet`, logData);
|
|
198
|
-
return undefined;
|
|
199
248
|
} else if (roundInfo.voteCount === 0n) {
|
|
200
249
|
this.log.debug(`Round ${executableRound} received no votes`, logData);
|
|
201
250
|
return undefined;
|
|
@@ -204,6 +253,17 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
204
253
|
return undefined;
|
|
205
254
|
}
|
|
206
255
|
|
|
256
|
+
// Check if round is ready to execute at the given slot
|
|
257
|
+
const isReadyToExecute = await this.tallySlashingProposer.isRoundReadyToExecute(executableRound, slotNumber);
|
|
258
|
+
if (!isReadyToExecute) {
|
|
259
|
+
this.log.warn(
|
|
260
|
+
`Round ${executableRound} is not ready to execute at slot ${slotNumber} according to contract check`,
|
|
261
|
+
logData,
|
|
262
|
+
);
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Check if the round yields any slashing at all
|
|
207
267
|
const { actions: slashActions, committees } = await this.tallySlashingProposer.getTally(executableRound);
|
|
208
268
|
if (slashActions.length === 0) {
|
|
209
269
|
this.log.verbose(`Round ${executableRound} does not resolve in any slashing`, logData);
|
|
@@ -212,8 +272,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
212
272
|
|
|
213
273
|
// Check if the slash payload is vetoed
|
|
214
274
|
const payload = await this.tallySlashingProposer.getPayload(executableRound);
|
|
215
|
-
const
|
|
216
|
-
const isVetoed = await slasherContract.isPayloadVetoed(payload.address);
|
|
275
|
+
const isVetoed = await this.slasher.isPayloadVetoed(payload.address);
|
|
217
276
|
if (isVetoed) {
|
|
218
277
|
this.log.warn(`Round ${executableRound} payload is vetoed (skipping execution)`, {
|
|
219
278
|
payloadAddress: payload.address.toString(),
|
|
@@ -239,13 +298,12 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
239
298
|
return { type: 'execute-slash', round: executableRound, committees: slashedCommittees };
|
|
240
299
|
} catch (error) {
|
|
241
300
|
this.log.error(`Error checking round to execute ${executableRound}`, error);
|
|
301
|
+
return undefined;
|
|
242
302
|
}
|
|
243
|
-
|
|
244
|
-
return undefined;
|
|
245
303
|
}
|
|
246
304
|
|
|
247
305
|
/** Returns a vote action based on offenses from the target round (with offset applied) */
|
|
248
|
-
protected async getVoteOffensesAction(slotNumber:
|
|
306
|
+
protected async getVoteOffensesAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
|
|
249
307
|
// Compute what round we are in based on the slot number and what round will be slashed
|
|
250
308
|
const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
|
|
251
309
|
const slashedRound = this.getSlashedRound(currentRound);
|
|
@@ -299,7 +357,12 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
299
357
|
|
|
300
358
|
const committees = await this.collectCommitteesActiveDuringRound(slashedRound);
|
|
301
359
|
const epochsForCommittees = getEpochsForRound(slashedRound, this.settings);
|
|
302
|
-
const votes = getSlashConsensusVotesFromOffenses(
|
|
360
|
+
const votes = getSlashConsensusVotesFromOffenses(
|
|
361
|
+
offensesToSlash,
|
|
362
|
+
committees,
|
|
363
|
+
epochsForCommittees.map(e => BigInt(e)),
|
|
364
|
+
this.settings,
|
|
365
|
+
);
|
|
303
366
|
if (votes.every(v => v === 0)) {
|
|
304
367
|
this.log.warn(`Computed votes for offenses are all zero. Skipping vote.`, {
|
|
305
368
|
slotNumber,
|