@aztec/slasher 2.0.3-rc.17 → 2.0.3-rc.19
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/dest/config.d.ts.map +1 -1
- package/dest/config.js +7 -1
- package/dest/tally_slasher_client.d.ts +10 -2
- package/dest/tally_slasher_client.d.ts.map +1 -1
- package/dest/tally_slasher_client.js +39 -8
- package/dest/watchers/epoch_prune_watcher.d.ts +2 -0
- package/dest/watchers/epoch_prune_watcher.d.ts.map +1 -1
- package/dest/watchers/epoch_prune_watcher.js +28 -31
- package/package.json +9 -9
- package/src/config.ts +7 -1
- package/src/tally_slasher_client.ts +46 -9
- package/src/watchers/epoch_prune_watcher.ts +41 -46
package/dest/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAQnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAErE,YAAY,EAAE,aAAa,EAAE,CAAC;AAE9B,eAAO,MAAM,oBAAoB,EAAE,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAQnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAErE,YAAY,EAAE,aAAa,EAAE,CAAC;AAE9B,eAAO,MAAM,oBAAoB,EAAE,aAoBlC,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,kBAAkB,CAAC,aAAa,CAyHnE,CAAC"}
|
package/dest/config.js
CHANGED
|
@@ -19,6 +19,7 @@ export const DefaultSlasherConfig = {
|
|
|
19
19
|
slashOffenseExpirationRounds: 4,
|
|
20
20
|
slashMaxPayloadSize: 50,
|
|
21
21
|
slashGracePeriodL2Slots: 0,
|
|
22
|
+
slashExecuteRoundsLookBack: 4,
|
|
22
23
|
slashSelfAllowed: false
|
|
23
24
|
};
|
|
24
25
|
export const slasherConfigMappings = {
|
|
@@ -62,7 +63,7 @@ export const slasherConfigMappings = {
|
|
|
62
63
|
},
|
|
63
64
|
slashBroadcastedInvalidBlockPenalty: {
|
|
64
65
|
env: 'SLASH_INVALID_BLOCK_PENALTY',
|
|
65
|
-
description: 'Penalty amount for slashing a validator for an invalid block.',
|
|
66
|
+
description: 'Penalty amount for slashing a validator for an invalid block proposed via p2p.',
|
|
66
67
|
...bigintConfigHelper(DefaultSlasherConfig.slashBroadcastedInvalidBlockPenalty)
|
|
67
68
|
},
|
|
68
69
|
slashInactivityTargetPercentage: {
|
|
@@ -121,6 +122,11 @@ export const slasherConfigMappings = {
|
|
|
121
122
|
env: 'SLASH_GRACE_PERIOD_L2_SLOTS',
|
|
122
123
|
...numberConfigHelper(DefaultSlasherConfig.slashGracePeriodL2Slots)
|
|
123
124
|
},
|
|
125
|
+
slashExecuteRoundsLookBack: {
|
|
126
|
+
env: 'SLASH_EXECUTE_ROUNDS_LOOK_BACK',
|
|
127
|
+
description: 'How many rounds to look back when searching for a round to execute.',
|
|
128
|
+
...numberConfigHelper(DefaultSlasherConfig.slashExecuteRoundsLookBack)
|
|
129
|
+
},
|
|
124
130
|
slashSelfAllowed: {
|
|
125
131
|
description: 'Whether to allow slashes to own validators',
|
|
126
132
|
...booleanConfigHelper(DefaultSlasherConfig.slashSelfAllowed)
|
|
@@ -21,7 +21,7 @@ export type TallySlasherSettings = Prettify<SlashRoundMonitorSettings & SlashOff
|
|
|
21
21
|
/** Committee size for block proposal */
|
|
22
22
|
targetCommitteeSize: number;
|
|
23
23
|
}>;
|
|
24
|
-
export type TallySlasherClientConfig = SlashOffensesCollectorConfig & Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever'>;
|
|
24
|
+
export type TallySlasherClientConfig = SlashOffensesCollectorConfig & Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever' | 'slashExecuteRoundsLookBack'>;
|
|
25
25
|
/**
|
|
26
26
|
* The Tally Slasher client is responsible for managing slashable offenses using
|
|
27
27
|
* the consensus-based slashing model where proposers vote on individual validator offenses.
|
|
@@ -87,8 +87,16 @@ export declare class TallySlasherClient implements ProposerSlashActionProvider,
|
|
|
87
87
|
* @returns The actions to take
|
|
88
88
|
*/
|
|
89
89
|
getProposerActions(slotNumber: bigint): Promise<ProposerSlashAction[]>;
|
|
90
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Returns an execute slash action if there are any rounds ready to be executed.
|
|
92
|
+
* Returns the oldest slash action if there are multiple rounds pending execution.
|
|
93
|
+
*/
|
|
91
94
|
protected getExecuteSlashAction(slotNumber: bigint): Promise<ProposerSlashAction | undefined>;
|
|
95
|
+
/**
|
|
96
|
+
* Checks if a given round is executable and returns an execute-slash action for it if so.
|
|
97
|
+
* Assumes round number has already been checked against lifetime and execution delay.
|
|
98
|
+
*/
|
|
99
|
+
private tryGetRoundExecuteAction;
|
|
92
100
|
/** Returns a vote action based on offenses from the target round (with offset applied) */
|
|
93
101
|
protected getVoteOffensesAction(slotNumber: bigint): Promise<ProposerSlashAction | undefined>;
|
|
94
102
|
/** Returns the committees that were active during the timespan of a given round */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tally_slasher_client.d.ts","sourceRoot":"","sources":["../src/tally_slasher_client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,6BAA6B,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"tally_slasher_client.d.ts","sourceRoot":"","sources":["../src/tally_slasher_client.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EthAddress } from '@aztec/aztec.js';
|
|
2
|
+
import { maxBigint } from '@aztec/foundation/bigint';
|
|
2
3
|
import { compactArray, partition, times } from '@aztec/foundation/collection';
|
|
3
4
|
import { createLogger } from '@aztec/foundation/log';
|
|
4
5
|
import { sleep } from '@aztec/foundation/sleep';
|
|
@@ -120,19 +121,51 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
|
120
121
|
voteAction
|
|
121
122
|
]);
|
|
122
123
|
}
|
|
123
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* Returns an execute slash action if there are any rounds ready to be executed.
|
|
126
|
+
* Returns the oldest slash action if there are multiple rounds pending execution.
|
|
127
|
+
*/ async getExecuteSlashAction(slotNumber) {
|
|
124
128
|
const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
|
|
125
129
|
const slashingExecutionDelayInRounds = BigInt(this.settings.slashingExecutionDelayInRounds);
|
|
126
130
|
const executableRound = currentRound - slashingExecutionDelayInRounds - 1n;
|
|
127
|
-
|
|
131
|
+
const lookBack = BigInt(this.config.slashExecuteRoundsLookBack);
|
|
132
|
+
const oldestExecutableRound = maxBigint(0n, executableRound - lookBack);
|
|
133
|
+
// Check if slashing is enabled at all
|
|
134
|
+
if (!await this.slasher.isSlashingEnabled()) {
|
|
135
|
+
this.log.warn(`Slashing is disabled in the Slasher contract (skipping execution)`);
|
|
128
136
|
return undefined;
|
|
129
137
|
}
|
|
130
|
-
|
|
138
|
+
this.log.debug(`Checking slashing rounds ${oldestExecutableRound} to ${executableRound} to execute`, {
|
|
139
|
+
slotNumber,
|
|
131
140
|
currentRound,
|
|
141
|
+
oldestExecutableRound,
|
|
132
142
|
executableRound,
|
|
133
|
-
|
|
143
|
+
slashingExecutionDelayInRounds,
|
|
144
|
+
lookBack,
|
|
145
|
+
slashingLifetimeInRounds: this.settings.slashingLifetimeInRounds
|
|
146
|
+
});
|
|
147
|
+
// Iterate over all rounds, starting from the oldest, until we find one that is executable
|
|
148
|
+
for(let roundToCheck = oldestExecutableRound; roundToCheck <= executableRound; roundToCheck++){
|
|
149
|
+
const action = await this.tryGetRoundExecuteAction(roundToCheck);
|
|
150
|
+
if (action) {
|
|
151
|
+
return action;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// And return nothing if none are found
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Checks if a given round is executable and returns an execute-slash action for it if so.
|
|
159
|
+
* Assumes round number has already been checked against lifetime and execution delay.
|
|
160
|
+
*/ async tryGetRoundExecuteAction(executableRound) {
|
|
161
|
+
let logData = {
|
|
162
|
+
executableRound
|
|
134
163
|
};
|
|
164
|
+
this.log.debug(`Testing if slashing round ${executableRound} is executable`, logData);
|
|
135
165
|
try {
|
|
166
|
+
// Note we do not check isReadyToExecute here, since we already know that based on the
|
|
167
|
+
// executableRound number. Not just that, but it may be that we are building for the given slot number
|
|
168
|
+
// that is in the future, so the contract may think it's not yet ready to execute, whereas it is.
|
|
136
169
|
const roundInfo = await this.tallySlashingProposer.getRound(executableRound);
|
|
137
170
|
logData = {
|
|
138
171
|
...logData,
|
|
@@ -141,9 +174,6 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
|
141
174
|
if (roundInfo.isExecuted) {
|
|
142
175
|
this.log.verbose(`Round ${executableRound} has already been executed`, logData);
|
|
143
176
|
return undefined;
|
|
144
|
-
} else if (!roundInfo.readyToExecute) {
|
|
145
|
-
this.log.verbose(`Round ${executableRound} is not ready to execute yet`, logData);
|
|
146
|
-
return undefined;
|
|
147
177
|
} else if (roundInfo.voteCount === 0n) {
|
|
148
178
|
this.log.debug(`Round ${executableRound} received no votes`, logData);
|
|
149
179
|
return undefined;
|
|
@@ -151,6 +181,7 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
|
151
181
|
this.log.verbose(`Round ${executableRound} does not have enough votes to execute`, logData);
|
|
152
182
|
return undefined;
|
|
153
183
|
}
|
|
184
|
+
// Check if the round yields any slashing at all
|
|
154
185
|
const { actions: slashActions, committees } = await this.tallySlashingProposer.getTally(executableRound);
|
|
155
186
|
if (slashActions.length === 0) {
|
|
156
187
|
this.log.verbose(`Round ${executableRound} does not resolve in any slashing`, logData);
|
|
@@ -184,8 +215,8 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
|
184
215
|
};
|
|
185
216
|
} catch (error) {
|
|
186
217
|
this.log.error(`Error checking round to execute ${executableRound}`, error);
|
|
218
|
+
return undefined;
|
|
187
219
|
}
|
|
188
|
-
return undefined;
|
|
189
220
|
}
|
|
190
221
|
/** Returns a vote action based on offenses from the target round (with offset applied) */ async getVoteOffensesAction(slotNumber) {
|
|
191
222
|
// Compute what round we are in based on the slot number and what round will be slashed
|
|
@@ -26,6 +26,8 @@ export declare class EpochPruneWatcher extends EpochPruneWatcher_base implements
|
|
|
26
26
|
stop(): Promise<void>;
|
|
27
27
|
updateConfig(config: Partial<SlasherConfig>): void;
|
|
28
28
|
private handlePruneL2Blocks;
|
|
29
|
+
private emitSlashForEpoch;
|
|
30
|
+
private processPruneL2Blocks;
|
|
29
31
|
validateBlocks(blocks: L2Block[]): Promise<void>;
|
|
30
32
|
validateBlock(blockFromL1: L2Block, fork: MerkleTreeWriteOperations): Promise<void>;
|
|
31
33
|
private getValidatorsForEpoch;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"epoch_prune_watcher.d.ts","sourceRoot":"","sources":["../../src/watchers/epoch_prune_watcher.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"epoch_prune_watcher.d.ts","sourceRoot":"","sources":["../../src/watchers/epoch_prune_watcher.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,OAAO,EAEL,OAAO,EAEP,KAAK,yBAAyB,EAE/B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EACV,qBAAqB,EACrB,WAAW,EACX,yBAAyB,EACzB,aAAa,EACd,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAWnE,OAAO,EAA6C,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAE7G,QAAA,MAAM,oCAAoC,+DAAgE,CAAC;AAE3G,KAAK,0BAA0B,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC,OAAO,oCAAoC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;sCAQrD,UAAU,cAAc;AANhF;;;;;GAKG;AACH,qBAAa,iBAAkB,SAAQ,sBAA2C,YAAW,OAAO;IAShG,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,mBAAmB;IAC3B,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,YAAY;IAZtB,OAAO,CAAC,GAAG,CAA+C;IAG1D,OAAO,CAAC,wBAAwB,CAAuC;IAEvE,OAAO,CAAC,SAAS,CAA6B;gBAGpC,aAAa,EAAE,yBAAyB,EACxC,mBAAmB,EAAE,mBAAmB,EACxC,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,IAAI,CAAC,WAAW,EAAE,iBAAiB,CAAC,EAChD,YAAY,EAAE,qBAAqB,EAC3C,SAAS,EAAE,0BAA0B;IAShC,KAAK;IAKL,IAAI;IAKJ,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI;IAKzD,OAAO,CAAC,mBAAmB;YAOb,iBAAiB;YAWjB,oBAAoB;IAwBrB,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAchD,aAAa,CAAC,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC;YAgClF,qBAAqB;IASnC,OAAO,CAAC,wBAAwB;CAgBjC"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { merge, pick } from '@aztec/foundation/collection';
|
|
2
2
|
import { createLogger } from '@aztec/foundation/log';
|
|
3
3
|
import { L2BlockSourceEvents } from '@aztec/stdlib/block';
|
|
4
|
-
import {
|
|
4
|
+
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
5
|
+
import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';
|
|
5
6
|
import { ReExFailedTxsError, ReExStateMismatchError, TransactionsNotAvailableError, ValidatorError } from '@aztec/stdlib/validators';
|
|
6
7
|
import EventEmitter from 'node:events';
|
|
7
8
|
import { WANT_TO_SLASH_EVENT } from '../watcher.js';
|
|
@@ -43,44 +44,40 @@ const EpochPruneWatcherPenaltiesConfigKeys = [
|
|
|
43
44
|
}
|
|
44
45
|
handlePruneL2Blocks(event) {
|
|
45
46
|
const { blocks, epochNumber } = event;
|
|
46
|
-
this.log.
|
|
47
|
-
|
|
47
|
+
void this.processPruneL2Blocks(blocks, epochNumber).catch((err)=>this.log.error('Error processing pruned L2 blocks', err, {
|
|
48
|
+
epochNumber
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
async emitSlashForEpoch(offense, epochNumber) {
|
|
52
|
+
const validators = await this.getValidatorsForEpoch(epochNumber);
|
|
53
|
+
if (validators.length === 0) {
|
|
54
|
+
this.log.warn(`No validators found for epoch ${epochNumber} (cannot slash for ${getOffenseTypeName(offense)})`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const args = this.validatorsToSlashingArgs(validators, offense, BigInt(epochNumber));
|
|
58
|
+
this.log.verbose(`Created slash for ${getOffenseTypeName(offense)} at epoch ${epochNumber}`, args);
|
|
59
|
+
this.emit(WANT_TO_SLASH_EVENT, args);
|
|
60
|
+
}
|
|
61
|
+
async processPruneL2Blocks(blocks, epochNumber) {
|
|
62
|
+
try {
|
|
63
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
64
|
+
const epochBlocks = blocks.filter((b)=>getEpochAtSlot(b.slot, l1Constants) === epochNumber);
|
|
65
|
+
this.log.info(`Detected chain prune. Validating epoch ${epochNumber} with blocks ${epochBlocks[0]?.number} to ${epochBlocks[epochBlocks.length - 1]?.number}.`, {
|
|
66
|
+
blocks: epochBlocks.map((b)=>b.toBlockInfo())
|
|
67
|
+
});
|
|
68
|
+
await this.validateBlocks(epochBlocks);
|
|
48
69
|
this.log.info(`Pruned epoch ${epochNumber} was valid. Want to slash committee for not having it proven.`);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const result = {
|
|
52
|
-
validators,
|
|
53
|
-
offense: OffenseType.VALID_EPOCH_PRUNED
|
|
54
|
-
};
|
|
55
|
-
return result;
|
|
56
|
-
}).catch(async (error)=>{
|
|
70
|
+
await this.emitSlashForEpoch(OffenseType.VALID_EPOCH_PRUNED, epochNumber);
|
|
71
|
+
} catch (error) {
|
|
57
72
|
if (error instanceof TransactionsNotAvailableError) {
|
|
58
73
|
this.log.info(`Data for pruned epoch ${epochNumber} was not available. Will want to slash.`, {
|
|
59
74
|
message: error.message
|
|
60
75
|
});
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
validators,
|
|
64
|
-
offense: OffenseType.DATA_WITHHOLDING
|
|
65
|
-
};
|
|
76
|
+
await this.emitSlashForEpoch(OffenseType.DATA_WITHHOLDING, epochNumber);
|
|
66
77
|
} else {
|
|
67
78
|
this.log.error(`Error while validating pruned epoch ${epochNumber}. Will not want to slash.`, error);
|
|
68
|
-
return {
|
|
69
|
-
validators: [],
|
|
70
|
-
offense: undefined
|
|
71
|
-
};
|
|
72
79
|
}
|
|
73
|
-
}
|
|
74
|
-
if (validators.length === 0 || offense === undefined) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
const args = this.validatorsToSlashingArgs(validators, offense, BigInt(epochNumber));
|
|
78
|
-
this.log.info(`Slash for epoch ${epochNumber} created`, args);
|
|
79
|
-
this.emit(WANT_TO_SLASH_EVENT, args);
|
|
80
|
-
}).catch((error)=>{
|
|
81
|
-
// This can happen if we fail to get the validators for the epoch.
|
|
82
|
-
this.log.error('Error while creating slash for epoch', error);
|
|
83
|
-
});
|
|
80
|
+
}
|
|
84
81
|
}
|
|
85
82
|
async validateBlocks(blocks) {
|
|
86
83
|
if (blocks.length === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/slasher",
|
|
3
|
-
"version": "2.0.3-rc.
|
|
3
|
+
"version": "2.0.3-rc.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dest/index.js",
|
|
@@ -54,20 +54,20 @@
|
|
|
54
54
|
]
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@aztec/epoch-cache": "2.0.3-rc.
|
|
58
|
-
"@aztec/ethereum": "2.0.3-rc.
|
|
59
|
-
"@aztec/foundation": "2.0.3-rc.
|
|
60
|
-
"@aztec/kv-store": "2.0.3-rc.
|
|
61
|
-
"@aztec/l1-artifacts": "2.0.3-rc.
|
|
62
|
-
"@aztec/stdlib": "2.0.3-rc.
|
|
63
|
-
"@aztec/telemetry-client": "2.0.3-rc.
|
|
57
|
+
"@aztec/epoch-cache": "2.0.3-rc.19",
|
|
58
|
+
"@aztec/ethereum": "2.0.3-rc.19",
|
|
59
|
+
"@aztec/foundation": "2.0.3-rc.19",
|
|
60
|
+
"@aztec/kv-store": "2.0.3-rc.19",
|
|
61
|
+
"@aztec/l1-artifacts": "2.0.3-rc.19",
|
|
62
|
+
"@aztec/stdlib": "2.0.3-rc.19",
|
|
63
|
+
"@aztec/telemetry-client": "2.0.3-rc.19",
|
|
64
64
|
"source-map-support": "^0.5.21",
|
|
65
65
|
"tslib": "^2.4.0",
|
|
66
66
|
"viem": "2.23.7",
|
|
67
67
|
"zod": "^3.23.8"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@aztec/aztec.js": "2.0.3-rc.
|
|
70
|
+
"@aztec/aztec.js": "2.0.3-rc.19",
|
|
71
71
|
"@jest/globals": "^30.0.0",
|
|
72
72
|
"@types/jest": "^30.0.0",
|
|
73
73
|
"@types/node": "^22.15.17",
|
package/src/config.ts
CHANGED
|
@@ -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,6 +1,7 @@
|
|
|
1
1
|
import { EthAddress } from '@aztec/aztec.js';
|
|
2
2
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
3
3
|
import { RollupContract, SlasherContract, TallySlashingProposerContract } from '@aztec/ethereum/contracts';
|
|
4
|
+
import { maxBigint } from '@aztec/foundation/bigint';
|
|
4
5
|
import { compactArray, partition, times } from '@aztec/foundation/collection';
|
|
5
6
|
import { createLogger } from '@aztec/foundation/log';
|
|
6
7
|
import { sleep } from '@aztec/foundation/sleep';
|
|
@@ -45,7 +46,7 @@ export type TallySlasherSettings = Prettify<
|
|
|
45
46
|
>;
|
|
46
47
|
|
|
47
48
|
export type TallySlasherClientConfig = SlashOffensesCollectorConfig &
|
|
48
|
-
Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever'>;
|
|
49
|
+
Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever' | 'slashExecuteRoundsLookBack'>;
|
|
49
50
|
|
|
50
51
|
/**
|
|
51
52
|
* The Tally Slasher client is responsible for managing slashable offenses using
|
|
@@ -177,26 +178,62 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
177
178
|
return compactArray<ProposerSlashAction>([executeAction, voteAction]);
|
|
178
179
|
}
|
|
179
180
|
|
|
180
|
-
/**
|
|
181
|
+
/**
|
|
182
|
+
* Returns an execute slash action if there are any rounds ready to be executed.
|
|
183
|
+
* Returns the oldest slash action if there are multiple rounds pending execution.
|
|
184
|
+
*/
|
|
181
185
|
protected async getExecuteSlashAction(slotNumber: bigint): Promise<ProposerSlashAction | undefined> {
|
|
182
186
|
const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
|
|
183
187
|
const slashingExecutionDelayInRounds = BigInt(this.settings.slashingExecutionDelayInRounds);
|
|
184
188
|
const executableRound = currentRound - slashingExecutionDelayInRounds - 1n;
|
|
185
|
-
|
|
189
|
+
const lookBack = BigInt(this.config.slashExecuteRoundsLookBack);
|
|
190
|
+
const oldestExecutableRound = maxBigint(0n, executableRound - lookBack);
|
|
191
|
+
|
|
192
|
+
// Check if slashing is enabled at all
|
|
193
|
+
if (!(await this.slasher.isSlashingEnabled())) {
|
|
194
|
+
this.log.warn(`Slashing is disabled in the Slasher contract (skipping execution)`);
|
|
186
195
|
return undefined;
|
|
187
196
|
}
|
|
188
197
|
|
|
189
|
-
|
|
198
|
+
this.log.debug(`Checking slashing rounds ${oldestExecutableRound} to ${executableRound} to execute`, {
|
|
199
|
+
slotNumber,
|
|
200
|
+
currentRound,
|
|
201
|
+
oldestExecutableRound,
|
|
202
|
+
executableRound,
|
|
203
|
+
slashingExecutionDelayInRounds,
|
|
204
|
+
lookBack,
|
|
205
|
+
slashingLifetimeInRounds: this.settings.slashingLifetimeInRounds,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Iterate over all rounds, starting from the oldest, until we find one that is executable
|
|
209
|
+
for (let roundToCheck = oldestExecutableRound; roundToCheck <= executableRound; roundToCheck++) {
|
|
210
|
+
const action = await this.tryGetRoundExecuteAction(roundToCheck);
|
|
211
|
+
if (action) {
|
|
212
|
+
return action;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// And return nothing if none are found
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Checks if a given round is executable and returns an execute-slash action for it if so.
|
|
222
|
+
* Assumes round number has already been checked against lifetime and execution delay.
|
|
223
|
+
*/
|
|
224
|
+
private async tryGetRoundExecuteAction(executableRound: bigint): Promise<ProposerSlashAction | undefined> {
|
|
225
|
+
let logData: Record<string, unknown> = { executableRound };
|
|
226
|
+
this.log.debug(`Testing if slashing round ${executableRound} is executable`, logData);
|
|
190
227
|
|
|
191
228
|
try {
|
|
229
|
+
// Note we do not check isReadyToExecute here, since we already know that based on the
|
|
230
|
+
// executableRound number. Not just that, but it may be that we are building for the given slot number
|
|
231
|
+
// that is in the future, so the contract may think it's not yet ready to execute, whereas it is.
|
|
192
232
|
const roundInfo = await this.tallySlashingProposer.getRound(executableRound);
|
|
193
233
|
logData = { ...logData, roundInfo };
|
|
194
234
|
if (roundInfo.isExecuted) {
|
|
195
235
|
this.log.verbose(`Round ${executableRound} has already been executed`, logData);
|
|
196
236
|
return undefined;
|
|
197
|
-
} else if (!roundInfo.readyToExecute) {
|
|
198
|
-
this.log.verbose(`Round ${executableRound} is not ready to execute yet`, logData);
|
|
199
|
-
return undefined;
|
|
200
237
|
} else if (roundInfo.voteCount === 0n) {
|
|
201
238
|
this.log.debug(`Round ${executableRound} received no votes`, logData);
|
|
202
239
|
return undefined;
|
|
@@ -205,6 +242,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
205
242
|
return undefined;
|
|
206
243
|
}
|
|
207
244
|
|
|
245
|
+
// Check if the round yields any slashing at all
|
|
208
246
|
const { actions: slashActions, committees } = await this.tallySlashingProposer.getTally(executableRound);
|
|
209
247
|
if (slashActions.length === 0) {
|
|
210
248
|
this.log.verbose(`Round ${executableRound} does not resolve in any slashing`, logData);
|
|
@@ -239,9 +277,8 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
239
277
|
return { type: 'execute-slash', round: executableRound, committees: slashedCommittees };
|
|
240
278
|
} catch (error) {
|
|
241
279
|
this.log.error(`Error checking round to execute ${executableRound}`, error);
|
|
280
|
+
return undefined;
|
|
242
281
|
}
|
|
243
|
-
|
|
244
|
-
return undefined;
|
|
245
282
|
}
|
|
246
283
|
|
|
247
284
|
/** Returns a vote action based on offenses from the target round (with offset applied) */
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Tx } from '@aztec/aztec.js';
|
|
2
1
|
import { EpochCache } from '@aztec/epoch-cache';
|
|
3
2
|
import { merge, pick } from '@aztec/foundation/collection';
|
|
4
3
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
@@ -9,6 +8,7 @@ import {
|
|
|
9
8
|
type L2BlockSourceEventEmitter,
|
|
10
9
|
L2BlockSourceEvents,
|
|
11
10
|
} from '@aztec/stdlib/block';
|
|
11
|
+
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
12
12
|
import type {
|
|
13
13
|
IFullNodeBlockBuilder,
|
|
14
14
|
ITxProvider,
|
|
@@ -16,7 +16,7 @@ import type {
|
|
|
16
16
|
SlasherConfig,
|
|
17
17
|
} from '@aztec/stdlib/interfaces/server';
|
|
18
18
|
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
19
|
-
import { OffenseType } from '@aztec/stdlib/slashing';
|
|
19
|
+
import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';
|
|
20
20
|
import {
|
|
21
21
|
ReExFailedTxsError,
|
|
22
22
|
ReExStateMismatchError,
|
|
@@ -78,49 +78,44 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
78
78
|
|
|
79
79
|
private handlePruneL2Blocks(event: L2BlockPruneEvent): void {
|
|
80
80
|
const { blocks, epochNumber } = event;
|
|
81
|
-
this.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
})
|
|
120
|
-
.catch(error => {
|
|
121
|
-
// This can happen if we fail to get the validators for the epoch.
|
|
122
|
-
this.log.error('Error while creating slash for epoch', error);
|
|
123
|
-
});
|
|
81
|
+
void this.processPruneL2Blocks(blocks, epochNumber).catch(err =>
|
|
82
|
+
this.log.error('Error processing pruned L2 blocks', err, { epochNumber }),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async emitSlashForEpoch(offense: OffenseType, epochNumber: bigint): Promise<void> {
|
|
87
|
+
const validators = await this.getValidatorsForEpoch(epochNumber);
|
|
88
|
+
if (validators.length === 0) {
|
|
89
|
+
this.log.warn(`No validators found for epoch ${epochNumber} (cannot slash for ${getOffenseTypeName(offense)})`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const args = this.validatorsToSlashingArgs(validators, offense, BigInt(epochNumber));
|
|
93
|
+
this.log.verbose(`Created slash for ${getOffenseTypeName(offense)} at epoch ${epochNumber}`, args);
|
|
94
|
+
this.emit(WANT_TO_SLASH_EVENT, args);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private async processPruneL2Blocks(blocks: L2Block[], epochNumber: bigint): Promise<void> {
|
|
98
|
+
try {
|
|
99
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
100
|
+
const epochBlocks = blocks.filter(b => getEpochAtSlot(b.slot, l1Constants) === epochNumber);
|
|
101
|
+
this.log.info(
|
|
102
|
+
`Detected chain prune. Validating epoch ${epochNumber} with blocks ${epochBlocks[0]?.number} to ${epochBlocks[epochBlocks.length - 1]?.number}.`,
|
|
103
|
+
{ blocks: epochBlocks.map(b => b.toBlockInfo()) },
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
await this.validateBlocks(epochBlocks);
|
|
107
|
+
this.log.info(`Pruned epoch ${epochNumber} was valid. Want to slash committee for not having it proven.`);
|
|
108
|
+
await this.emitSlashForEpoch(OffenseType.VALID_EPOCH_PRUNED, epochNumber);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error instanceof TransactionsNotAvailableError) {
|
|
111
|
+
this.log.info(`Data for pruned epoch ${epochNumber} was not available. Will want to slash.`, {
|
|
112
|
+
message: error.message,
|
|
113
|
+
});
|
|
114
|
+
await this.emitSlashForEpoch(OffenseType.DATA_WITHHOLDING, epochNumber);
|
|
115
|
+
} else {
|
|
116
|
+
this.log.error(`Error while validating pruned epoch ${epochNumber}. Will not want to slash.`, error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
124
119
|
}
|
|
125
120
|
|
|
126
121
|
public async validateBlocks(blocks: L2Block[]): Promise<void> {
|
|
@@ -151,7 +146,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
151
146
|
|
|
152
147
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockFromL1.number);
|
|
153
148
|
const { block, failedTxs, numTxs } = await this.blockBuilder.buildBlock(
|
|
154
|
-
txs
|
|
149
|
+
txs,
|
|
155
150
|
l1ToL2Messages,
|
|
156
151
|
blockFromL1.header.globalVariables,
|
|
157
152
|
{},
|