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
13 changes: 11 additions & 2 deletions yarn-project/aztec-node/src/sentinel/sentinel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
type WantToSlashArgs,
type Watcher,
type WatcherEmitter,
getOffenseTypeName,
} from '@aztec/slasher';
import type { SlasherConfig } from '@aztec/slasher/config';
import {
Expand Down Expand Up @@ -358,9 +359,17 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
}));

if (criminals.length > 0) {
this.logger.verbose(
this.logger.info(
`Identified ${criminals.length} inactivity offenses in at least ${epochThreshold} consecutive epochs`,
{ offenses: args, epochThreshold },
{
offenses: args.map(arg => ({
validator: arg.validator.toString(),
amount: arg.amount,
offenseType: getOffenseTypeName(arg.offenseType),
epochOrSlot: arg.epochOrSlot,
})),
epochThreshold,
},
);
this.emit(WANT_TO_SLASH_EVENT, args);
}
Expand Down
23 changes: 17 additions & 6 deletions yarn-project/slasher/src/slash_offenses_collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { SerialQueue } from '@aztec/foundation/queue';
import type { Prettify } from '@aztec/foundation/types';
import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
import { type Offense, getSlotForOffense } from '@aztec/stdlib/slashing';
import { type Offense, getOffenseTypeName, getSlotForOffense } from '@aztec/stdlib/slashing';

import type { SlasherOffensesStore } from './stores/offenses_store.js';
import {
Expand Down Expand Up @@ -89,7 +89,7 @@ export class SlashOffensesCollector {
};

if (this.shouldSkipOffense(offense)) {
this.log.verbose('Skipping offense during grace period', offense);
this.log.verbose('Skipping offense during grace period', this.getOffenseLogData(offense));
continue;
}

Expand All @@ -98,13 +98,16 @@ export class SlashOffensesCollector {
if (this.settings.slashingAmounts) {
const minSlash = this.settings.slashingAmounts[0];
if (arg.amount < minSlash) {
this.log.warn(`Offense amount ${arg.amount} is below minimum slashing amount ${minSlash}`);
this.log.warn(
`Offense amount ${arg.amount} is below minimum slashing amount ${minSlash}`,
this.getOffenseLogData(offense),
);
}
}

this.log.info(`Adding pending offense for validator ${arg.validator}`, offense);
this.log.info(`Adding pending offense for validator ${arg.validator}`, this.getOffenseLogData(offense));
} else {
this.log.debug('Skipping repeated offense', offense);
this.log.debug('Skipping repeated offense', this.getOffenseLogData(offense));
}
}
}
Expand All @@ -114,7 +117,7 @@ export class SlashOffensesCollector {
const cleared = await this.offensesStore.clearOffenses(arg);
if (cleared > 0) {
this.log.info(`Cleared ${cleared} pending offenses`, {
offenseType: arg.offenseType,
offenseType: getOffenseTypeName(arg.offenseType),
epochOrSlot: arg.epochOrSlot,
validators: arg.validators?.map(validator => validator.toString()),
});
Expand All @@ -139,6 +142,14 @@ export class SlashOffensesCollector {
return offenseSlot < this.settings.rollupRegisteredAtL2Slot + this.config.slashGracePeriodL2Slots;
}

private getOffenseLogData(offense: Offense) {
return {
...offense,
validator: offense.validator.toString(),
offenseType: getOffenseTypeName(offense.offenseType),
};
}

private enqueueStoreMutation(label: string, callback: () => Promise<void>) {
void this.storeMutationQueue.put(callback).catch(err => this.log.error(`Error handling ${label}`, err));
}
Expand Down
29 changes: 23 additions & 6 deletions yarn-project/slasher/src/slasher_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
type ProposerSlashAction,
type ProposerSlashActionProvider,
getEpochsForRound,
getOffenseTypeName,
getSlashConsensusVotesFromOffenses,
} from '@aztec/stdlib/slashing';

Expand Down Expand Up @@ -50,6 +51,14 @@ export type SlasherClientConfig = SlashOffensesCollectorConfig &
'slashValidatorsAlways' | 'slashValidatorsNever' | 'slashExecuteRoundsLookBack' | 'slashMaxPayloadSize'
>;

type AlwaysSlashOffense = {
validator: EthAddress;
amount: bigint;
offenseType: OffenseType.UNKNOWN;
};

type SlashVoteOffense = Offense | AlwaysSlashOffense;

/**
* The Slasher client is responsible for managing slashable offenses using
* the consensus-based slashing model where proposers vote on individual validator offenses.
Expand Down Expand Up @@ -309,7 +318,7 @@ export class SlasherClient implements ProposerSlashActionProvider, SlasherClient
// Compute offenses to slash, by loading the offenses for this round, adding synthetic offenses
// for validators that should always be slashed, and removing the ones that should never be slashed.
const offensesForRound = await this.gatherOffensesForRound(currentRound);
const offensesFromAlwaysSlash = (this.config.slashValidatorsAlways ?? []).map(validator => ({
const offensesFromAlwaysSlash: AlwaysSlashOffense[] = (this.config.slashValidatorsAlways ?? []).map(validator => ({
validator,
amount: this.settings.slashingAmounts[2],
offenseType: OffenseType.UNKNOWN,
Expand All @@ -323,7 +332,7 @@ export class SlasherClient implements ProposerSlashActionProvider, SlasherClient
slotNumber,
currentRound,
slashedRound,
offensesToForgive,
offensesFromAlwaysSlash: offensesFromAlwaysSlash.map(getOffenseLogData),
slashValidatorsAlways: this.config.slashValidatorsAlways,
});
}
Expand All @@ -333,7 +342,7 @@ export class SlasherClient implements ProposerSlashActionProvider, SlasherClient
slotNumber,
currentRound,
slashedRound,
offensesToForgive,
offensesToForgive: offensesToForgive.map(getOffenseLogData),
slashValidatorsNever: this.config.slashValidatorsNever,
});
}
Expand All @@ -347,7 +356,7 @@ export class SlasherClient implements ProposerSlashActionProvider, SlasherClient
slotNumber,
currentRound,
slashedRound,
offensesToSlash,
offensesToSlash: offensesToSlash.map(getOffenseLogData),
});

const committees = await this.collectCommitteesActiveDuringRound(slashedRound);
Expand All @@ -365,7 +374,7 @@ export class SlasherClient implements ProposerSlashActionProvider, SlasherClient
slotNumber,
currentRound,
slashedRound,
offensesToSlash,
offensesToSlash: offensesToSlash.map(getOffenseLogData),
committees,
});
return undefined;
Expand All @@ -376,7 +385,7 @@ export class SlasherClient implements ProposerSlashActionProvider, SlasherClient
slashedRound,
currentRound,
votes,
offensesToSlash,
offensesToSlash: offensesToSlash.map(getOffenseLogData),
});

this.log.debug(`Computed votes for slashing ${offensesToSlash.length} offenses`, {
Expand Down Expand Up @@ -437,3 +446,11 @@ export class SlasherClient implements ProposerSlashActionProvider, SlasherClient
return round - BigInt(this.settings.slashingOffsetInRounds);
}
}

function getOffenseLogData(offense: SlashVoteOffense) {
return {
...offense,
validator: offense.validator.toString(),
offenseType: getOffenseTypeName(offense.offenseType),
};
}
23 changes: 16 additions & 7 deletions yarn-project/slasher/src/watchers/attestations_block_watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
type ValidateCheckpointNegativeResult,
} from '@aztec/stdlib/block';
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
import { OffenseType } from '@aztec/stdlib/slashing';
import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';

import EventEmitter from 'node:events';

Expand Down Expand Up @@ -134,9 +134,13 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
epochOrSlot: BigInt(slot),
};

this.log.info(`Want to slash proposer of checkpoint ${checkpointNumber} due to ${reason}`, {
this.log.info(`Detected invalid attestations checkpoint proposer offense`, {
...checkpoint,
...args,
reason,
validator: args.validator.toString(),
amount: args.amount,
offenseType: getOffenseTypeName(args.offenseType),
epochOrSlot: args.epochOrSlot,
});

this.emit(WANT_TO_SLASH_EVENT, [args]);
Expand Down Expand Up @@ -168,10 +172,15 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
epochOrSlot: BigInt(slot),
};

this.log.info(
`Want to slash proposer of checkpoint ${checkpoint.checkpointNumber} built on invalid checkpoint ${ancestorCheckpointNumber}`,
{ ...checkpoint, ancestorArchiveRoot: ancestorArchiveRoot.toString(), ...args },
);
this.log.info(`Detected invalid descendant checkpoint proposer offense`, {
...checkpoint,
ancestorCheckpointNumber,
ancestorArchiveRoot: ancestorArchiveRoot.toString(),
validator: args.validator.toString(),
amount: args.amount,
offenseType: getOffenseTypeName(args.offenseType),
epochOrSlot: args.epochOrSlot,
});

this.emit(WANT_TO_SLASH_EVENT, [args]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,20 +119,6 @@ describe('AttestedInvalidProposalWatcher', () => {
expect(handler).toHaveBeenCalledTimes(1);
});

it('deduplicates repeated scans for the same offense when the penalty changes', async () => {
const slot = SlotNumber(10);
invalidProposalSlots.add(slot);
const attestation = await makeAttestation(slot);
p2pClient.getCheckpointAttestationsForSlot.mockResolvedValue([attestation]);

watcher.updateConfig({ slashAttestInvalidCheckpointProposalPenalty: 0n });
await watcher.scanSlot(slot);
watcher.updateConfig({ slashAttestInvalidCheckpointProposalPenalty: 13n });
await watcher.scanSlot(slot);

expect(handler).toHaveBeenCalledTimes(1);
});

it('scans only marked invalid proposal slots once they are past the scan lag', async () => {
watcher = new AttestedInvalidProposalWatcher(
p2pClient,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { RunningPromise } from '@aztec/foundation/running-promise';
import type { L2BlockSource } from '@aztec/stdlib/block';
import type { P2PClient, SlasherConfig } from '@aztec/stdlib/interfaces/server';
import type { CheckpointAttestation } from '@aztec/stdlib/p2p';
import { OffenseType } from '@aztec/stdlib/slashing';
import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';

import EventEmitter from 'node:events';

Expand Down Expand Up @@ -127,12 +127,12 @@ export class AttestedInvalidProposalWatcher extends (EventEmitter as new () => W
return;
}

this.log.warn('Detected attestations to invalid checkpoint proposal', {
this.log.info('Detected attestations to invalid checkpoint proposal', {
slot,
offenses: slashArgs.map(args => ({
validator: args.validator.toString(),
amount: args.amount,
offenseType: args.offenseType,
offenseType: getOffenseTypeName(args.offenseType),
epochOrSlot: args.epochOrSlot,
})),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,21 +228,6 @@ describe('BroadcastedInvalidCheckpointProposalWatcher', () => {
expect(handler).toHaveBeenCalledTimes(1);
});

it('deduplicates repeated scans for the same offense when the penalty changes', async () => {
const signer = Secp256k1Signer.random();
const slot = SlotNumber(10);
const blocks = await makeBlocks(signer, slot, 4);
const checkpoint = await makeCheckpointCore(signer, slot, blocks[1]);
mockProposals(slot, blocks, [checkpoint]);

watcher.updateConfig({ slashBroadcastedInvalidCheckpointProposalPenalty: 0n });
await watcher.scanSlot(slot);
watcher.updateConfig({ slashBroadcastedInvalidCheckpointProposalPenalty: 11n });
await watcher.scanSlot(slot);

expect(handler).toHaveBeenCalledTimes(1);
});

it('scans a lookback of closed slots', async () => {
const signer = Secp256k1Signer.random();
const slot = SlotNumber(10);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { RunningPromise } from '@aztec/foundation/running-promise';
import type { L2BlockSource } from '@aztec/stdlib/block';
import type { P2PClient, SlasherConfig } from '@aztec/stdlib/interfaces/server';
import type { BlockProposal, CheckpointProposalCore } from '@aztec/stdlib/p2p';
import { OffenseType } from '@aztec/stdlib/slashing';
import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';

import EventEmitter from 'node:events';

Expand Down Expand Up @@ -118,7 +118,7 @@ export class BroadcastedInvalidCheckpointProposalWatcher
offenses: slashArgs.map(args => ({
validator: args.validator.toString(),
amount: args.amount,
offenseType: args.offenseType,
offenseType: getOffenseTypeName(args.offenseType),
epochOrSlot: args.epochOrSlot,
})),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
L2BlockSourceEvents,
} from '@aztec/stdlib/block';
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
import { OffenseType } from '@aztec/stdlib/slashing';
import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';

import EventEmitter from 'node:events';

Expand Down Expand Up @@ -86,7 +86,7 @@ export class CheckpointEquivocationWatcher extends (EventEmitter as new () => Wa
slotNumber: event.slotNumber,
checkpointNumber: event.checkpointNumber,
amount: slashArgs.amount,
offenseType: slashArgs.offenseType,
offenseType: getOffenseTypeName(slashArgs.offenseType),
l1ArchiveRoot: event.l1ArchiveRoot.toString(),
proposedArchiveRoot: event.proposedArchiveRoot.toString(),
validator: proposer.toString(),
Expand Down
6 changes: 3 additions & 3 deletions yarn-project/slasher/src/watchers/data_withholding_watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { getAttestationInfoFromPublishedCheckpoint } from '@aztec/stdlib/block';
import type { CheckpointReexecutionTracker, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
import type { ITxProvider, P2PApi, SlasherConfig } from '@aztec/stdlib/interfaces/server';
import { ConsensusPayload, type CoordinationSignatureContext } from '@aztec/stdlib/p2p';
import { OffenseType } from '@aztec/stdlib/slashing';
import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';
import type { TxHash } from '@aztec/stdlib/tx';

import EventEmitter from 'node:events';
Expand Down Expand Up @@ -171,11 +171,11 @@ export class DataWithholdingWatcher extends (EventEmitter as new () => WatcherEm
return;
}

this.log.warn(`Detected data withholding offense at slot ${slot}`, {
this.log.info(`Detected data withholding offense at slot ${slot}`, {
slot,
checkpointNumber,
amount: this.config.slashDataWithholdingPenalty,
offenseType: OffenseType.DATA_WITHHOLDING,
offenseType: getOffenseTypeName(OffenseType.DATA_WITHHOLDING),
missingTxs: missingTxs.map(h => h.toString()),
records: collectionRecords,
attesters: attesters.map(a => a.toString()),
Expand Down
Loading
Loading