Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion yarn-project/aztec-node/src/aztec-node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ import {
getTelemetryClient,
trackSpan,
} from '@aztec/telemetry-client';
import { ValidatorClient, createValidatorClient } from '@aztec/validator-client';
import { NodeKeystoreAdapter, ValidatorClient, createValidatorClient } from '@aztec/validator-client';
import { createWorldStateSynchronizer } from '@aztec/world-state';

import { createPublicClient, fallback, http } from 'viem';
Expand Down Expand Up @@ -373,13 +373,19 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
if (!config.disableValidator) {
// We create a slasher only if we have a sequencer, since all slashing actions go through the sequencer publisher
// as they are executed when the node is selected as proposer.
const validatorAddresses = keyStoreManager
? NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager).getAddresses()
: [];

slasherClient = await createSlasher(
config,
config.l1Contracts,
getPublicClient(config),
watchers,
dateProvider,
epochCache,
validatorAddresses,
undefined, // logger
);
await slasherClient.start();

Expand Down
11 changes: 2 additions & 9 deletions yarn-project/aztec-node/src/sentinel/sentinel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,9 @@ describe('sentinel', () => {
let epoch: bigint;
let ts: bigint;
let l1Constants: L1RollupConstants;
const config: Pick<
SlasherConfig,
'slashInactivityTargetPercentage' | 'slashInactivityPenalty' | 'slashPayloadTtlSeconds'
> = {
const config: Pick<SlasherConfig, 'slashInactivityTargetPercentage' | 'slashInactivityPenalty'> = {
slashInactivityPenalty: 100n,
slashInactivityTargetPercentage: 0.8,
slashPayloadTtlSeconds: 60 * 60,
};

beforeEach(async () => {
Expand Down Expand Up @@ -466,10 +462,7 @@ class TestSentinel extends Sentinel {
archiver: L2BlockSource,
p2p: P2PClient,
store: SentinelStore,
config: Pick<
SlasherConfig,
'slashInactivityTargetPercentage' | 'slashInactivityPenalty' | 'slashPayloadTtlSeconds'
>,
config: Pick<SlasherConfig, 'slashInactivityTargetPercentage' | 'slashInactivityPenalty'>,
protected override blockStream: L2BlockStream,
) {
super(epochCache, archiver, p2p, store, config);
Expand Down
5 changes: 1 addition & 4 deletions yarn-project/aztec-node/src/sentinel/sentinel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
protected archiver: L2BlockSource,
protected p2p: P2PClient,
protected store: SentinelStore,
protected config: Pick<
SlasherConfig,
'slashInactivityTargetPercentage' | 'slashInactivityPenalty' | 'slashPayloadTtlSeconds'
>,
protected config: Pick<SlasherConfig, 'slashInactivityTargetPercentage' | 'slashInactivityPenalty'>,
protected logger = createLogger('node:sentinel'),
) {
super();
Expand Down
3 changes: 0 additions & 3 deletions yarn-project/aztec/src/cli/chain_l2_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ export const testnetIgnitionL2ChainConfig: L2ChainConfig = {
provingCostPerMana: 0n,

slasherFlavor: 'none',
slashPayloadTtlSeconds: 0,
slashAmountSmall: 0n,
slashAmountMedium: 0n,
slashAmountLarge: 0n,
Expand Down Expand Up @@ -153,7 +152,6 @@ export const alphaTestnetL2ChainConfig: L2ChainConfig = {
slashAmountLarge: DefaultL1ContractsConfig.slashAmountLarge,

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

// Slashing
enrichVar('SLASH_PAYLOAD_TTL_SECONDS', config.slashPayloadTtlSeconds.toString());
enrichVar('SLASH_MIN_PENALTY_PERCENTAGE', config.slashMinPenaltyPercentage.toString());
enrichVar('SLASH_MAX_PENALTY_PERCENTAGE', config.slashMaxPenaltyPercentage.toString());
enrichVar('SLASH_PRUNE_PENALTY', config.slashPrunePenalty.toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ describe('e2e_p2p_data_withholding_slash', () => {
slashAmountSmall: slashingUnit,
slashAmountMedium: slashingUnit * 2n,
slashAmountLarge: slashingUnit * 3n,
slashSelfAllowed: true,
minTxsPerBlock: 0,
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ describe('veto slash', () => {
aztecEpochDuration: EPOCH_DURATION,
validatorReexecute: false,
sentinelEnabled: true,
slashSelfAllowed: true,
slashingOffsetInRounds: SLASH_OFFSET_IN_ROUNDS,
slashAmountSmall: SLASHING_UNIT,
slashAmountMedium: SLASHING_UNIT * 2n,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe('e2e_p2p_valid_epoch_pruned', () => {
aztecProofSubmissionEpochs: 0, // reorg as soon as epoch ends
slashingQuorum,
slashingRoundSize,
slashSelfAllowed: true,
slashAmountSmall: slashingUnit,
slashAmountMedium: slashingUnit * 2n,
slashAmountLarge: slashingUnit * 3n,
Expand Down
1 change: 1 addition & 0 deletions yarn-project/ethereum/src/contracts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './registry.js';
export * from './rollup.js';
export * from './empire_slashing_proposer.js';
export * from './tally_slashing_proposer.js';
export * from './slasher_contract.js';
9 changes: 9 additions & 0 deletions yarn-project/ethereum/src/contracts/rollup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type { ViemClient } from '../types.js';
import { formatViemError } from '../utils.js';
import { EmpireSlashingProposerContract } from './empire_slashing_proposer.js';
import { GSEContract } from './gse.js';
import { SlasherContract } from './slasher_contract.js';
import { TallySlashingProposerContract } from './tally_slashing_proposer.js';
import { checkBlockTag } from './utils.js';

Expand Down Expand Up @@ -267,6 +268,14 @@ export class RollupContract {
return this.rollup.read.getSlasher();
}

/**
* Returns a SlasherContract instance for interacting with the slasher contract.
*/
async getSlasherContract(): Promise<SlasherContract> {
const slasherAddress = await this.getSlasher();
return new SlasherContract(this.client, EthAddress.fromString(slasherAddress));
}

getOwner() {
return this.rollup.read.owner();
}
Expand Down
67 changes: 67 additions & 0 deletions yarn-project/ethereum/src/contracts/slasher_contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { EthAddress } from '@aztec/foundation/eth-address';
import { createLogger } from '@aztec/foundation/log';
import { SlasherAbi } from '@aztec/l1-artifacts/SlasherAbi';

import { type GetContractReturnType, getContract } from 'viem';

import type { ViemClient } from '../types.js';

/**
* Typescript wrapper around the Slasher contract.
*/
export class SlasherContract {
private contract: GetContractReturnType<typeof SlasherAbi, ViemClient>;

constructor(
private readonly client: ViemClient,
private readonly address: EthAddress,
private readonly log = createLogger('slasher-contract'),
) {
this.contract = getContract({
address: this.address.toString(),
abi: SlasherAbi,
client: this.client,
});
}

/**
* Checks if a slash payload is vetoed.
* @param payloadAddress - The address of the payload to check
* @returns True if the payload is vetoed, false otherwise
*/
public async isPayloadVetoed(payloadAddress: EthAddress): Promise<boolean> {
try {
return await this.contract.read.vetoedPayloads([payloadAddress.toString()]);
} catch (error) {
this.log.error(`Error checking if payload ${payloadAddress} is vetoed`, error);
throw error;
}
}

/**
* Gets the current vetoer address.
* @returns The vetoer address
*/
public async getVetoer(): Promise<EthAddress> {
const vetoer = await this.contract.read.VETOER();
return EthAddress.fromString(vetoer);
}

/**
* Gets the current governance address.
* @returns The governance address
*/
public async getGovernance(): Promise<EthAddress> {
const governance = await this.contract.read.GOVERNANCE();
return EthAddress.fromString(governance);
}

/**
* Gets the current proposer address.
* @returns The proposer address
*/
public async getProposer(): Promise<EthAddress> {
const proposer = await this.contract.read.PROPOSER();
return EthAddress.fromString(proposer);
}
}
19 changes: 9 additions & 10 deletions yarn-project/ethereum/src/contracts/tally_slashing_proposer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,21 +97,20 @@ export class TallySlashingProposerContract extends EventEmitter {
public async getPayload(
round: bigint,
): Promise<{ actions: { slashAmount: bigint; validator: EthAddress }[]; address: EthAddress }> {
const result = await this.getTallyFromContract(round);
const address = await this.contract.read.getPayloadAddress([round, result]);
const actions = this.mapSlashActions(result);
const { result: committees } = await this.contract.simulate.getSlashTargetCommittees([round]);
const tally = await this.contract.read.getTally([round, committees]);
const address = await this.contract.read.getPayloadAddress([round, tally]);
const actions = this.mapSlashActions(tally);
return { actions, address: EthAddress.fromString(address) };
}

/** Returns the slash actions to be executed for a given round based on votes */
public async getTally(round: bigint): Promise<{ slashAmount: bigint; validator: EthAddress }[]> {
const result = await this.getTallyFromContract(round);
return this.mapSlashActions(result);
}

private async getTallyFromContract(round: bigint): Promise<readonly { slashAmount: bigint; validator: Hex }[]> {
public async getTally(
round: bigint,
): Promise<{ actions: { slashAmount: bigint; validator: EthAddress }[]; committees: EthAddress[][] }> {
const { result: committees } = await this.contract.simulate.getSlashTargetCommittees([round]);
return await this.contract.read.getTally([round, committees]);
const tally = await this.contract.read.getTally([round, committees]);
return { actions: this.mapSlashActions(tally), committees: committees.map(c => c.map(EthAddress.fromString)) };
}

private mapSlashActions(
Expand Down
38 changes: 38 additions & 0 deletions yarn-project/foundation/src/collection/array.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
maxBy,
mean,
median,
partition,
removeArrayPaddingEnd,
stdDev,
times,
Expand Down Expand Up @@ -261,3 +262,40 @@ describe('chunk', () => {
expect(result).toEqual([[1], [2], [3]]);
});
});

describe('partition', () => {
it('partitions an array into pass and fail arrays based on the predicate', () => {
const input = [1, 2, 3, 4, 5];
const [even, odd] = partition(input, x => x % 2 === 0);
expect(even).toEqual([2, 4]);
expect(odd).toEqual([1, 3, 5]);
});

it('returns all items in the first array if all pass the predicate', () => {
const input = [2, 4, 6];
const [pass, fail] = partition(input, x => x % 2 === 0);
expect(pass).toEqual([2, 4, 6]);
expect(fail).toEqual([]);
});

it('returns all items in the second array if none pass the predicate', () => {
const input = [1, 3, 5];
const [pass, fail] = partition(input, x => x % 2 === 0);
expect(pass).toEqual([]);
expect(fail).toEqual([1, 3, 5]);
});

it('handles an empty array', () => {
const input: number[] = [];
const [pass, fail] = partition(input, x => x > 0);
expect(pass).toEqual([]);
expect(fail).toEqual([]);
});

it('works with objects and custom predicates', () => {
const input = [{ a: 1 }, { a: 2 }, { a: 3 }];
const [even, odd] = partition(input, obj => obj.a % 2 === 0);
expect(even).toEqual([{ a: 2 }]);
expect(odd).toEqual([{ a: 1 }, { a: 3 }]);
});
});
14 changes: 14 additions & 0 deletions yarn-project/foundation/src/collection/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,17 @@ export function chunk<T>(items: T[], chunkSize: number): T[][] {
}
return chunks;
}

/** Partitions the given iterable into two arrays based on the predicate. */
export function partition<T>(items: T[], predicate: (item: T) => boolean): [T[], T[]] {
const pass: T[] = [];
const fail: T[] = [];
for (const item of items) {
if (predicate(item)) {
pass.push(item);
} else {
fail.push(item);
}
}
return [pass, fail];
}
1 change: 0 additions & 1 deletion yarn-project/foundation/src/config/env_var.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,6 @@ export type EnvVar =
| 'SLASH_INACTIVITY_TARGET_PERCENTAGE'
| 'SLASH_INVALID_BLOCK_PENALTY'
| 'SLASH_OVERRIDE_PAYLOAD'
| 'SLASH_PAYLOAD_TTL_SECONDS'
| 'SLASH_PROPOSE_INVALID_ATTESTATIONS_PENALTY'
| 'SLASH_ATTEST_DESCENDANT_OF_INVALID_PENALTY'
| 'SLASH_UNKNOWN_PENALTY'
Expand Down
39 changes: 31 additions & 8 deletions yarn-project/slasher/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Key characteristics:
- Requires quorum to execute slashing
- L1 contract determines which offenses reach consensus
- Execution happens after a delay period for review
- Slash payloads can be vetoed during the execution delay period

### Empire Model

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

#### SlasherOffensesStore
Persistent storage for offenses. Tracks:
- Pending offenses awaiting slashing
- Executed offenses to prevent double slashing
- Round-based offense organization
- Automatic expiration of old offenses based on configurable rounds

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

## Vetoing

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.

Key features:
- Slash payloads can be vetoed by authorized addresses on the L1 slasher contract
- Veto checks are performed automatically before execution attempts
- The veto mechanism provides a safety valve for incorrectly proposed slashes

## Slashable Offenses

### DATA_WITHHOLDING
Expand Down Expand Up @@ -132,15 +143,27 @@ Actions returned by the slasher client to the SequencerPublisher:

## Configuration

### Slasher Configuration
- `slashGracePeriodL2Slots`: Number of initial L2 slots where slashing is disabled
- `slashMaxPayloadSize`: Maximum size of slash payloads (empire model)
- `slashingRoundSize`: Number of slots per slashing round
### L1 System Settings (L1ContractsConfig)
These settings are deployed with the L1 contracts and apply system-wide to the protocol:

- `slashingRoundSize`: Number of slots per slashing round (default: 192, must be multiple of epochs)
- `slashingQuorumSize`: Votes required to slash (tally model)
- `slashingOffsetInRounds`: How many rounds to look back for offenses (tally model)
- `slashingExecutionDelayInRounds`: Rounds to wait before execution
- `slashingLifetimeInRounds`: Maximum age of executable rounds
- `slashingUnit`: Base slashing amount per unit
- `slashingAmounts`: Valid values for each individual slash (tally model)

### Local Node Configuration (SlasherConfig)
These settings are configured locally on each validator node:

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