Skip to content

Commit 976368e

Browse files
committed
feat: Slash lists, store expiration, veto checks
Adds some missing features to slashing: - Adds support for the slashValidatorNever and slashValidatorAlways config settings (now EthAddress lists) so that validators in those lists are never/always slashed. All validators in the local keystore are automatically added to the "never" list. - Checks if a slash payload is vetoed before trying to execute it, to avoid running an unnecessary simulation. This is handled on the slasher client directly. - Adds expiration for offenses and payloads to avoid cluttering the local stores. Removes unused slashPayloadTtl setting. - Avoids posting committee addresses during executeRound for committees that are not slashed, in order to save calldata gas.
1 parent 9330fa3 commit 976368e

26 files changed

+959
-117
lines changed

yarn-project/aztec-node/src/aztec-node/server.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ import {
108108
getTelemetryClient,
109109
trackSpan,
110110
} from '@aztec/telemetry-client';
111-
import { ValidatorClient, createValidatorClient } from '@aztec/validator-client';
111+
import { NodeKeystoreAdapter, ValidatorClient, createValidatorClient } from '@aztec/validator-client';
112112
import { createWorldStateSynchronizer } from '@aztec/world-state';
113113

114114
import { createPublicClient, fallback, http } from 'viem';
@@ -373,13 +373,19 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
373373
if (!config.disableValidator) {
374374
// We create a slasher only if we have a sequencer, since all slashing actions go through the sequencer publisher
375375
// as they are executed when the node is selected as proposer.
376+
const validatorAddresses = keyStoreManager
377+
? NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager).getAddresses()
378+
: [];
379+
376380
slasherClient = await createSlasher(
377381
config,
378382
config.l1Contracts,
379383
getPublicClient(config),
380384
watchers,
381385
dateProvider,
382386
epochCache,
387+
validatorAddresses,
388+
undefined, // logger
383389
);
384390
await slasherClient.start();
385391

yarn-project/aztec-node/src/sentinel/sentinel.test.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,9 @@ describe('sentinel', () => {
4545
let epoch: bigint;
4646
let ts: bigint;
4747
let l1Constants: L1RollupConstants;
48-
const config: Pick<
49-
SlasherConfig,
50-
'slashInactivityTargetPercentage' | 'slashInactivityPenalty' | 'slashPayloadTtlSeconds'
51-
> = {
48+
const config: Pick<SlasherConfig, 'slashInactivityTargetPercentage' | 'slashInactivityPenalty'> = {
5249
slashInactivityPenalty: 100n,
5350
slashInactivityTargetPercentage: 0.8,
54-
slashPayloadTtlSeconds: 60 * 60,
5551
};
5652

5753
beforeEach(async () => {
@@ -466,10 +462,7 @@ class TestSentinel extends Sentinel {
466462
archiver: L2BlockSource,
467463
p2p: P2PClient,
468464
store: SentinelStore,
469-
config: Pick<
470-
SlasherConfig,
471-
'slashInactivityTargetPercentage' | 'slashInactivityPenalty' | 'slashPayloadTtlSeconds'
472-
>,
465+
config: Pick<SlasherConfig, 'slashInactivityTargetPercentage' | 'slashInactivityPenalty'>,
473466
protected override blockStream: L2BlockStream,
474467
) {
475468
super(epochCache, archiver, p2p, store, config);

yarn-project/aztec-node/src/sentinel/sentinel.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
4444
protected archiver: L2BlockSource,
4545
protected p2p: P2PClient,
4646
protected store: SentinelStore,
47-
protected config: Pick<
48-
SlasherConfig,
49-
'slashInactivityTargetPercentage' | 'slashInactivityPenalty' | 'slashPayloadTtlSeconds'
50-
>,
47+
protected config: Pick<SlasherConfig, 'slashInactivityTargetPercentage' | 'slashInactivityPenalty'>,
5148
protected logger = createLogger('node:sentinel'),
5249
) {
5350
super();

yarn-project/aztec/src/cli/chain_l2_config.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ export const testnetIgnitionL2ChainConfig: L2ChainConfig = {
6969
provingCostPerMana: 0n,
7070

7171
slasherFlavor: 'none',
72-
slashPayloadTtlSeconds: 0,
7372
slashAmountSmall: 0n,
7473
slashAmountMedium: 0n,
7574
slashAmountLarge: 0n,
@@ -153,7 +152,6 @@ export const alphaTestnetL2ChainConfig: L2ChainConfig = {
153152
slashAmountLarge: DefaultL1ContractsConfig.slashAmountLarge,
154153

155154
// Slashing stuff
156-
slashPayloadTtlSeconds: 36 * 32 * 6 * 6, // 6 rounds (a bit longer than lifetime)
157155
slashMinPenaltyPercentage: 0.5,
158156
slashMaxPenaltyPercentage: 2.0,
159157
slashInactivityTargetPercentage: 0.7,
@@ -325,7 +323,6 @@ export async function enrichEnvironmentWithChainConfig(networkName: NetworkNames
325323
enrichEthAddressVar('AZTEC_SLASHING_VETOER', config.slashingVetoer.toString());
326324

327325
// Slashing
328-
enrichVar('SLASH_PAYLOAD_TTL_SECONDS', config.slashPayloadTtlSeconds.toString());
329326
enrichVar('SLASH_MIN_PENALTY_PERCENTAGE', config.slashMinPenaltyPercentage.toString());
330327
enrichVar('SLASH_MAX_PENALTY_PERCENTAGE', config.slashMaxPenaltyPercentage.toString());
331328
enrichVar('SLASH_PRUNE_PENALTY', config.slashPrunePenalty.toString());

yarn-project/ethereum/src/contracts/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ export * from './registry.js';
1111
export * from './rollup.js';
1212
export * from './empire_slashing_proposer.js';
1313
export * from './tally_slashing_proposer.js';
14+
export * from './slasher_contract.js';

yarn-project/ethereum/src/contracts/rollup.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import type { ViemClient } from '../types.js';
2727
import { formatViemError } from '../utils.js';
2828
import { EmpireSlashingProposerContract } from './empire_slashing_proposer.js';
2929
import { GSEContract } from './gse.js';
30+
import { SlasherContract } from './slasher_contract.js';
3031
import { TallySlashingProposerContract } from './tally_slashing_proposer.js';
3132
import { checkBlockTag } from './utils.js';
3233

@@ -267,6 +268,14 @@ export class RollupContract {
267268
return this.rollup.read.getSlasher();
268269
}
269270

271+
/**
272+
* Returns a SlasherContract instance for interacting with the slasher contract.
273+
*/
274+
async getSlasherContract(): Promise<SlasherContract> {
275+
const slasherAddress = await this.getSlasher();
276+
return new SlasherContract(this.client, EthAddress.fromString(slasherAddress));
277+
}
278+
270279
getOwner() {
271280
return this.rollup.read.owner();
272281
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { EthAddress } from '@aztec/foundation/eth-address';
2+
import { createLogger } from '@aztec/foundation/log';
3+
import { SlasherAbi } from '@aztec/l1-artifacts/SlasherAbi';
4+
5+
import { type GetContractReturnType, getContract } from 'viem';
6+
7+
import type { ViemClient } from '../types.js';
8+
9+
/**
10+
* Typescript wrapper around the Slasher contract.
11+
*/
12+
export class SlasherContract {
13+
private contract: GetContractReturnType<typeof SlasherAbi, ViemClient>;
14+
15+
constructor(
16+
private readonly client: ViemClient,
17+
private readonly address: EthAddress,
18+
private readonly log = createLogger('slasher-contract'),
19+
) {
20+
this.contract = getContract({
21+
address: this.address.toString(),
22+
abi: SlasherAbi,
23+
client: this.client,
24+
});
25+
}
26+
27+
/**
28+
* Checks if a slash payload is vetoed.
29+
* @param payloadAddress - The address of the payload to check
30+
* @returns True if the payload is vetoed, false otherwise
31+
*/
32+
public async isPayloadVetoed(payloadAddress: EthAddress): Promise<boolean> {
33+
try {
34+
return await this.contract.read.vetoedPayloads([payloadAddress.toString()]);
35+
} catch (error) {
36+
this.log.error(`Error checking if payload ${payloadAddress} is vetoed`, error);
37+
throw error;
38+
}
39+
}
40+
41+
/**
42+
* Gets the current vetoer address.
43+
* @returns The vetoer address
44+
*/
45+
public async getVetoer(): Promise<EthAddress> {
46+
const vetoer = await this.contract.read.VETOER();
47+
return EthAddress.fromString(vetoer);
48+
}
49+
50+
/**
51+
* Gets the current governance address.
52+
* @returns The governance address
53+
*/
54+
public async getGovernance(): Promise<EthAddress> {
55+
const governance = await this.contract.read.GOVERNANCE();
56+
return EthAddress.fromString(governance);
57+
}
58+
59+
/**
60+
* Gets the current proposer address.
61+
* @returns The proposer address
62+
*/
63+
public async getProposer(): Promise<EthAddress> {
64+
const proposer = await this.contract.read.PROPOSER();
65+
return EthAddress.fromString(proposer);
66+
}
67+
}

yarn-project/ethereum/src/contracts/tally_slashing_proposer.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,21 +97,20 @@ export class TallySlashingProposerContract extends EventEmitter {
9797
public async getPayload(
9898
round: bigint,
9999
): Promise<{ actions: { slashAmount: bigint; validator: EthAddress }[]; address: EthAddress }> {
100-
const result = await this.getTallyFromContract(round);
101-
const address = await this.contract.read.getPayloadAddress([round, result]);
102-
const actions = this.mapSlashActions(result);
100+
const { result: committees } = await this.contract.simulate.getSlashTargetCommittees([round]);
101+
const tally = await this.contract.read.getTally([round, committees]);
102+
const address = await this.contract.read.getPayloadAddress([round, tally]);
103+
const actions = this.mapSlashActions(tally);
103104
return { actions, address: EthAddress.fromString(address) };
104105
}
105106

106107
/** Returns the slash actions to be executed for a given round based on votes */
107-
public async getTally(round: bigint): Promise<{ slashAmount: bigint; validator: EthAddress }[]> {
108-
const result = await this.getTallyFromContract(round);
109-
return this.mapSlashActions(result);
110-
}
111-
112-
private async getTallyFromContract(round: bigint): Promise<readonly { slashAmount: bigint; validator: Hex }[]> {
108+
public async getTally(
109+
round: bigint,
110+
): Promise<{ actions: { slashAmount: bigint; validator: EthAddress }[]; committees: EthAddress[][] }> {
113111
const { result: committees } = await this.contract.simulate.getSlashTargetCommittees([round]);
114-
return await this.contract.read.getTally([round, committees]);
112+
const tally = await this.contract.read.getTally([round, committees]);
113+
return { actions: this.mapSlashActions(tally), committees: committees.map(c => c.map(EthAddress.fromString)) };
115114
}
116115

117116
private mapSlashActions(

yarn-project/foundation/src/config/env_var.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,6 @@ export type EnvVar =
208208
| 'SLASH_INACTIVITY_TARGET_PERCENTAGE'
209209
| 'SLASH_INVALID_BLOCK_PENALTY'
210210
| 'SLASH_OVERRIDE_PAYLOAD'
211-
| 'SLASH_PAYLOAD_TTL_SECONDS'
212211
| 'SLASH_PROPOSE_INVALID_ATTESTATIONS_PENALTY'
213212
| 'SLASH_ATTEST_DESCENDANT_OF_INVALID_PENALTY'
214213
| 'SLASH_UNKNOWN_PENALTY'

yarn-project/slasher/README.md

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Key characteristics:
3232
- Requires quorum to execute slashing
3333
- L1 contract determines which offenses reach consensus
3434
- Execution happens after a delay period for review
35+
- Slash payloads can be vetoed during the execution delay period
3536

3637
### Empire Model
3738

@@ -56,13 +57,14 @@ Common interface implemented by both tally and empire clients. Provides methods
5657
#### SlashOffensesCollector
5758
Collects slashable offenses from watchers and stores them in the offenses store. Features:
5859
- Subscribes to `WANT_TO_SLASH_EVENT` from watchers
59-
- Manages offense lifecycle and expiration
60+
- Manages offense lifecycle and automatic expiration
6061

6162
#### SlasherOffensesStore
6263
Persistent storage for offenses. Tracks:
6364
- Pending offenses awaiting slashing
6465
- Executed offenses to prevent double slashing
6566
- Round-based offense organization
67+
- Automatic expiration of old offenses based on configurable rounds
6668

6769
#### SlashRoundMonitor
6870
Monitors slashing rounds and triggers actions on round transitions:
@@ -85,6 +87,15 @@ Actions returned by the slasher client to the SequencerPublisher:
8587
4. **Action Execution**: SequencerPublisher receives actions and executes them on L1
8688
5. **Round Monitoring**: SlashRoundMonitor tracks rounds and triggers execution when conditions are met
8789

90+
## Vetoing
91+
92+
The slashing system includes a veto mechanism that allows designated vetoers to block slash payloads during the execution delay period. When a slash payload is ready for execution, the system first checks if it has been vetoed before proceeding.
93+
94+
Key features:
95+
- Slash payloads can be vetoed by authorized addresses on the L1 slasher contract
96+
- Veto checks are performed automatically before execution attempts
97+
- The veto mechanism provides a safety valve for incorrectly proposed slashes
98+
8899
## Slashable Offenses
89100

90101
### DATA_WITHHOLDING
@@ -132,15 +143,27 @@ Actions returned by the slasher client to the SequencerPublisher:
132143

133144
## Configuration
134145

135-
### Slasher Configuration
136-
- `slashGracePeriodL2Slots`: Number of initial L2 slots where slashing is disabled
137-
- `slashMaxPayloadSize`: Maximum size of slash payloads (empire model)
138-
- `slashingRoundSize`: Number of slots per slashing round
146+
### L1 System Settings (L1ContractsConfig)
147+
These settings are deployed with the L1 contracts and apply system-wide to the protocol:
148+
149+
- `slashingRoundSize`: Number of slots per slashing round (default: 192, must be multiple of epochs)
139150
- `slashingQuorumSize`: Votes required to slash (tally model)
140151
- `slashingOffsetInRounds`: How many rounds to look back for offenses (tally model)
141152
- `slashingExecutionDelayInRounds`: Rounds to wait before execution
142153
- `slashingLifetimeInRounds`: Maximum age of executable rounds
143-
- `slashingUnit`: Base slashing amount per unit
154+
- `slashingAmounts`: Valid values for each individual slash (tally model)
155+
156+
### Local Node Configuration (SlasherConfig)
157+
These settings are configured locally on each validator node:
144158

145-
### Environment Variables
146-
- `SLASHER_CLIENT_TYPE`: Select between 'tally' or 'empire' (default: 'tally')
159+
- `slashGracePeriodL2Slots`: Number of initial L2 slots where slashing is disabled
160+
- `slashMaxPayloadSize`: Maximum size of slash payloads (empire model)
161+
- `slashOffenseExpirationRounds`: Number of rounds after which pending offenses expire
162+
- `slashValidatorsAlways`: Array of validator addresses that should always be slashed
163+
- `slashValidatorsNever`: Array of validator addresses that should never be slashed (own validator addresses are automatically added to this list)
164+
- `slashPrunePenalty`: Penalty for DATA_WITHHOLDING and VALID_EPOCH_PRUNED offenses
165+
- `slashInactivityPenalty`: Penalty for INACTIVITY offenses
166+
- `slashBroadcastedInvalidBlockPenalty`: Penalty for broadcasting invalid blocks
167+
- `slashProposeInvalidAttestationsPenalty`: Penalty for proposing with insufficient/incorrect attestations
168+
- `slashAttestDescendantOfInvalidPenalty`: Penalty for attesting to descendants of invalid blocks
169+
- `slashUnknownPenalty`: Default penalty for unknown offense types

0 commit comments

Comments
 (0)