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
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('slash inactivity test', () => {

let client: ViemPublicClient;
let rollup: RollupContract;
let slashSettings: TallySlasherSettings;
let slashSettings: Omit<TallySlasherSettings, 'rollupRegisteredAtL2Slot'>;
let constants: Omit<L1RollupConstants, 'ethereumSlotDuration'>;
let monitor: ChainMonitor;
let offlineValidator: EthAddress;
Expand Down
32 changes: 31 additions & 1 deletion yarn-project/ethereum/src/contracts/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createLogger } from '@aztec/foundation/log';
import { RegistryAbi } from '@aztec/l1-artifacts/RegistryAbi';
import { TestERC20Abi } from '@aztec/l1-artifacts/TestERC20Abi';

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

import type { L1ContractAddresses } from '../l1_contract_addresses.js';
import type { ViemClient } from '../types.js';
Expand Down Expand Up @@ -128,4 +128,34 @@ export class RegistryContract {
public async getRewardDistributor(): Promise<EthAddress> {
return EthAddress.fromString(await this.registry.read.getRewardDistributor());
}

/** Returns the L1 timestamp at which the given rollup was registered via addRollup(). */
public async getCanonicalRollupRegistrationTimestamp(
rollupAddress: EthAddress,
fromBlock?: bigint,
): Promise<bigint | undefined> {
const event = getAbiItem({ abi: RegistryAbi, name: 'CanonicalRollupUpdated' });
const start = fromBlock ?? 0n;
const latestBlock = await this.client.getBlockNumber();
const chunkSize = 1_000n;

for (let from = start; from <= latestBlock; from += chunkSize) {
const to = from + chunkSize - 1n > latestBlock ? latestBlock : from + chunkSize - 1n;
const logs = await this.client.getLogs({
address: this.address.toString(),
fromBlock: from,
toBlock: to,
strict: true,
event,
args: { instance: rollupAddress.toString() },
});

if (logs.length > 0) {
const block = await this.client.getBlock({ blockNumber: logs[0].blockNumber });
return block.timestamp;
}
}

return undefined;
}
}
3 changes: 2 additions & 1 deletion yarn-project/slasher/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
...numberConfigHelper(DefaultSlasherConfig.slashMaxPayloadSize),
},
slashGracePeriodL2Slots: {
description: 'Number of L2 slots to wait before considering a slashing offense expired.',
description:
'Number of L2 slots after the network upgrade during which slashing offenses are ignored. The upgrade time is determined from the CanonicalRollupUpdated event.',
env: 'SLASH_GRACE_PERIOD_L2_SLOTS',
...numberConfigHelper(DefaultSlasherConfig.slashGracePeriodL2Slots),
},
Expand Down
1 change: 1 addition & 0 deletions yarn-project/slasher/src/empire_slasher_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ describe('EmpireSlasherClient', () => {
slotDuration: 4,
ethereumSlotDuration: 12,
slashingAmounts: undefined,
rollupRegisteredAtL2Slot: SlotNumber(0),
};

const config: SlasherConfig = {
Expand Down
33 changes: 31 additions & 2 deletions yarn-project/slasher/src/factory/create_facade.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { EpochCache } from '@aztec/epoch-cache';
import { RollupContract } from '@aztec/ethereum/contracts';
import { RegistryContract, RollupContract } from '@aztec/ethereum/contracts';
import type { L1ReaderConfig } from '@aztec/ethereum/l1-reader';
import type { ViemClient } from '@aztec/ethereum/types';
import { SlotNumber } from '@aztec/foundation/branded-types';
import { unique } from '@aztec/foundation/collection';
import { EthAddress } from '@aztec/foundation/eth-address';
import { createLogger } from '@aztec/foundation/log';
import { DateProvider } from '@aztec/foundation/timer';
import type { DataStoreConfig } from '@aztec/kv-store/config';
import { createStore } from '@aztec/kv-store/lmdb-v2';
import { getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';

import { SlasherClientFacade } from '../slasher_client_facade.js';
Expand All @@ -18,7 +20,7 @@ import type { Watcher } from '../watcher.js';
/** Creates a slasher client facade that updates itself whenever the rollup slasher changes */
export async function createSlasherFacade(
config: SlasherConfig & DataStoreConfig & { ethereumSlotDuration: number },
l1Contracts: Pick<L1ReaderConfig['l1Contracts'], 'rollupAddress' | 'slashFactoryAddress'>,
l1Contracts: Pick<L1ReaderConfig['l1Contracts'], 'rollupAddress' | 'slashFactoryAddress' | 'registryAddress'>,
l1Client: ViemClient,
watchers: Watcher[],
dateProvider: DateProvider,
Expand All @@ -34,6 +36,32 @@ export async function createSlasherFacade(
const kvStore = await createStore('slasher', SCHEMA_VERSION, config, logger.getBindings());
const rollup = new RollupContract(l1Client, l1Contracts.rollupAddress);

// Compute and cache the L2 slot at which the rollup was registered as canonical
const settingsMap = kvStore.openMap<string, number>('slasher-settings');
const cacheKey = `registeredSlot:${l1Contracts.rollupAddress}`;
let rollupRegisteredAtL2Slot = (await settingsMap.getAsync(cacheKey)) as SlotNumber | undefined;

if (rollupRegisteredAtL2Slot === undefined) {
const registry = new RegistryContract(l1Client, l1Contracts.registryAddress);
const l1StartBlock = await rollup.getL1StartBlock();
const registrationTimestamp = await registry.getCanonicalRollupRegistrationTimestamp(
l1Contracts.rollupAddress,
l1StartBlock,
);
if (registrationTimestamp !== undefined) {
const l1GenesisTime = await rollup.getL1GenesisTime();
const slotDuration = await rollup.getSlotDuration();
rollupRegisteredAtL2Slot = getSlotAtTimestamp(registrationTimestamp, {
l1GenesisTime,
slotDuration: Number(slotDuration),
});
} else {
rollupRegisteredAtL2Slot = SlotNumber(0);
}
await settingsMap.set(cacheKey, rollupRegisteredAtL2Slot);
logger.info(`Canonical rollup registered at L2 slot ${rollupRegisteredAtL2Slot}`);
}

const slashValidatorsNever = config.slashSelfAllowed
? config.slashValidatorsNever
: unique([...config.slashValidatorsNever, ...validatorAddresses].map(a => a.toString())).map(EthAddress.fromString);
Expand All @@ -48,6 +76,7 @@ export async function createSlasherFacade(
epochCache,
dateProvider,
kvStore,
rollupRegisteredAtL2Slot,
logger,
);
}
31 changes: 28 additions & 3 deletions yarn-project/slasher/src/factory/create_implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
TallySlashingProposerContract,
} from '@aztec/ethereum/contracts';
import type { ViemClient } from '@aztec/ethereum/types';
import type { SlotNumber } from '@aztec/foundation/branded-types';
import { EthAddress } from '@aztec/foundation/eth-address';
import { createLogger } from '@aztec/foundation/log';
import { DateProvider } from '@aztec/foundation/timer';
Expand All @@ -31,19 +32,40 @@ export async function createSlasherImplementation(
epochCache: EpochCache,
dateProvider: DateProvider,
kvStore: AztecLMDBStoreV2,
rollupRegisteredAtL2Slot: SlotNumber,
logger = createLogger('slasher'),
) {
const proposer = await rollup.getSlashingProposer();
if (!proposer) {
return new NullSlasherClient(config);
} else if (proposer.type === 'tally') {
return createTallySlasher(config, rollup, proposer, watchers, dateProvider, epochCache, kvStore, logger);
return createTallySlasher(
config,
rollup,
proposer,
watchers,
dateProvider,
epochCache,
kvStore,
rollupRegisteredAtL2Slot,
logger,
);
} else {
if (!slashFactoryAddress || slashFactoryAddress.equals(EthAddress.ZERO)) {
throw new Error('Cannot initialize an empire-based SlasherClient without a SlashFactory address');
}
const slashFactory = new SlashFactoryContract(l1Client, slashFactoryAddress.toString());
return createEmpireSlasher(config, rollup, proposer, slashFactory, watchers, dateProvider, kvStore, logger);
return createEmpireSlasher(
config,
rollup,
proposer,
slashFactory,
watchers,
dateProvider,
kvStore,
rollupRegisteredAtL2Slot,
logger,
);
}
}

Expand All @@ -55,6 +77,7 @@ async function createEmpireSlasher(
watchers: Watcher[],
dateProvider: DateProvider,
kvStore: AztecLMDBStoreV2,
rollupRegisteredAtL2Slot: SlotNumber,
logger = createLogger('slasher'),
): Promise<EmpireSlasherClient> {
if (slashingProposer.type !== 'empire') {
Expand Down Expand Up @@ -97,6 +120,7 @@ async function createEmpireSlasher(
l1StartBlock,
ethereumSlotDuration: config.ethereumSlotDuration,
slashingAmounts: undefined,
rollupRegisteredAtL2Slot,
};

const payloadsStore = new SlasherPayloadsStore(kvStore, {
Expand Down Expand Up @@ -130,13 +154,14 @@ async function createTallySlasher(
dateProvider: DateProvider,
epochCache: EpochCache,
kvStore: AztecLMDBStoreV2,
rollupRegisteredAtL2Slot: SlotNumber,
logger = createLogger('slasher'),
): Promise<TallySlasherClient> {
if (slashingProposer.type !== 'tally') {
throw new Error('Slashing proposer contract is not of type tally');
}

const settings = await getTallySlasherSettings(rollup, slashingProposer);
const settings = { ...(await getTallySlasherSettings(rollup, slashingProposer)), rollupRegisteredAtL2Slot };
const slasher = await rollup.getSlasherContract();

const offensesStore = new SlasherOffensesStore(kvStore, {
Expand Down
4 changes: 2 additions & 2 deletions yarn-project/slasher/src/factory/get_settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { TallySlasherSettings } from '../tally_slasher_client.js';
export async function getTallySlasherSettings(
rollup: RollupContract,
slashingProposer?: TallySlashingProposerContract,
): Promise<TallySlasherSettings> {
): Promise<Omit<TallySlasherSettings, 'rollupRegisteredAtL2Slot'>> {
if (!slashingProposer) {
const rollupSlashingProposer = await rollup.getSlashingProposer();
if (!rollupSlashingProposer || rollupSlashingProposer.type !== 'tally') {
Expand Down Expand Up @@ -40,7 +40,7 @@ export async function getTallySlasherSettings(
rollup.getTargetCommitteeSize(),
]);

const settings: TallySlasherSettings = {
const settings: Omit<TallySlasherSettings, 'rollupRegisteredAtL2Slot'> = {
slashingExecutionDelayInRounds: Number(slashingExecutionDelayInRounds),
slashingRoundSize: Number(slashingRoundSize),
slashingRoundSizeInEpochs: Number(slashingRoundSizeInEpochs),
Expand Down
26 changes: 15 additions & 11 deletions yarn-project/slasher/src/slash_offenses_collector.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SlotNumber } from '@aztec/foundation/branded-types';
import { EthAddress } from '@aztec/foundation/eth-address';
import { type Logger, createLogger } from '@aztec/foundation/log';
import { openTmpStore } from '@aztec/kv-store/lmdb';
Expand All @@ -18,6 +19,7 @@ describe('SlashOffensesCollector', () => {
const settings: SlashOffensesCollectorSettings = {
epochDuration: 32,
slashingAmounts: [100n, 200n, 300n],
rollupRegisteredAtL2Slot: 100 as SlotNumber,
};

const config: SlasherConfig = {
Expand Down Expand Up @@ -90,27 +92,28 @@ describe('SlashOffensesCollector', () => {
});
});

it('should skip offenses that happen during grace period', async () => {
it('should skip offenses that happen during grace period after upgrade', async () => {
const validator1 = EthAddress.random();
const validator2 = EthAddress.random();

// Create offense during grace period (slot < slashGracePeriodL2Slots = 10)
// Grace period is registeredSlot (100) + gracePeriodL2Slots (10) = 110
// Create offense during grace period (slot 105 < 110)
const gracePeriodOffense: WantToSlashArgs[] = [
{
validator: validator1,
amount: 1000000000000000000n,
offenseType: OffenseType.PROPOSED_INSUFFICIENT_ATTESTATIONS, // Slot-based offense
epochOrSlot: 5n, // Within grace period (< 10)
epochOrSlot: 105n, // Within grace period (< 110)
},
];

// Create offense after grace period
// Create offense after grace period (slot 115 >= 110)
const validOffense: WantToSlashArgs[] = [
{
validator: validator2,
amount: 2000000000000000000n,
offenseType: OffenseType.PROPOSED_INSUFFICIENT_ATTESTATIONS, // Slot-based offense
epochOrSlot: 20n, // After grace period (>= 10)
epochOrSlot: 115n, // After grace period (>= 110)
},
];

Expand All @@ -134,25 +137,26 @@ describe('SlashOffensesCollector', () => {
const validator2 = EthAddress.random();
const validator3 = EthAddress.random();

// Create an event with multiple offenses in a single array
// Grace period ends at registeredSlot (100) + gracePeriod (10) = 110
// All offenses are after the grace period
const multipleOffensesArgs: WantToSlashArgs[] = [
{
validator: validator1,
amount: 1000000000000000000n,
offenseType: OffenseType.INACTIVITY,
epochOrSlot: 100n,
epochOrSlot: 100n, // epoch 100 → slot 3200, well past grace period
},
{
validator: validator2,
amount: 2000000000000000000n,
offenseType: OffenseType.PROPOSED_INSUFFICIENT_ATTESTATIONS,
epochOrSlot: 50n,
epochOrSlot: 150n, // slot 150 >= 110
},
{
validator: validator3,
amount: 1500000000000000000n,
offenseType: OffenseType.ATTESTED_DESCENDANT_OF_INVALID,
epochOrSlot: 75n,
epochOrSlot: 175n, // slot 175 >= 110
},
];

Expand Down Expand Up @@ -182,14 +186,14 @@ describe('SlashOffensesCollector', () => {
validator: validator2,
amount: 2000000000000000000n,
offenseType: OffenseType.PROPOSED_INSUFFICIENT_ATTESTATIONS,
epochOrSlot: 50n,
epochOrSlot: 150n,
});

expect(offensesByValidator[validator3.toString()]).toMatchObject({
validator: validator3,
amount: 1500000000000000000n,
offenseType: OffenseType.ATTESTED_DESCENDANT_OF_INVALID,
epochOrSlot: 75n,
epochOrSlot: 175n,
});
});
});
11 changes: 8 additions & 3 deletions yarn-project/slasher/src/slash_offenses_collector.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SlotNumber } from '@aztec/foundation/branded-types';
import { createLogger } from '@aztec/foundation/log';
import type { Prettify } from '@aztec/foundation/types';
import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
Expand All @@ -9,7 +10,11 @@ import { WANT_TO_SLASH_EVENT, type WantToSlashArgs, type Watcher } from './watch

export type SlashOffensesCollectorConfig = Prettify<Pick<SlasherConfig, 'slashGracePeriodL2Slots'>>;
export type SlashOffensesCollectorSettings = Prettify<
Pick<L1RollupConstants, 'epochDuration'> & { slashingAmounts: [bigint, bigint, bigint] | undefined }
Pick<L1RollupConstants, 'epochDuration'> & {
slashingAmounts: [bigint, bigint, bigint] | undefined;
/** L2 slot at which the rollup was registered as canonical in the Registry. Used to anchor the slash grace period. */
rollupRegisteredAtL2Slot: SlotNumber;
}
>;

/**
Expand Down Expand Up @@ -110,9 +115,9 @@ export class SlashOffensesCollector {
return this.offensesStore.markAsSlashed(offenses);
}

/** Returns whether to skip an offense if it happened during the grace period at the beginning of the chain */
/** Returns whether to skip an offense if it happened during the grace period after the network upgrade */
private shouldSkipOffense(offense: Offense): boolean {
const offenseSlot = getSlotForOffense(offense, this.settings);
return offenseSlot < this.config.slashGracePeriodL2Slots;
return offenseSlot < this.settings.rollupRegisteredAtL2Slot + this.config.slashGracePeriodL2Slots;
}
}
2 changes: 2 additions & 0 deletions yarn-project/slasher/src/slasher_client_facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export class SlasherClientFacade implements SlasherClientInterface {
private epochCache: EpochCache,
private dateProvider: DateProvider,
private kvStore: AztecLMDBStoreV2,
private rollupRegisteredAtL2Slot: SlotNumber,
private logger = createLogger('slasher'),
) {}

Expand Down Expand Up @@ -88,6 +89,7 @@ export class SlasherClientFacade implements SlasherClientInterface {
this.epochCache,
this.dateProvider,
this.kvStore,
this.rollupRegisteredAtL2Slot,
this.logger,
);
}
Expand Down
1 change: 1 addition & 0 deletions yarn-project/slasher/src/tally_slasher_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ describe('TallySlasherClient', () => {
l1GenesisTime: BigInt(Math.floor(Date.now() / 1000) - 10000),
slotDuration: 4,
slashingQuorumSize: 110,
rollupRegisteredAtL2Slot: SlotNumber(0),
};

const config: SlasherConfig = {
Expand Down
Loading