diff --git a/yarn-project/cli/src/config/chain_l2_config.ts b/yarn-project/cli/src/config/chain_l2_config.ts index 3c0c2b0ed0c1..a4fd5c12a542 100644 --- a/yarn-project/cli/src/config/chain_l2_config.ts +++ b/yarn-project/cli/src/config/chain_l2_config.ts @@ -68,6 +68,7 @@ const DefaultSlashConfig = { slashGracePeriodL2Slots: 32 * 2, // Two epochs from genesis slashOffenseExpirationRounds: 8, sentinelEnabled: true, + slashExecuteRoundsLookBack: 4, } satisfies Partial; export const stagingIgnitionL2ChainConfig: L2ChainConfig = { @@ -148,6 +149,7 @@ export const stagingIgnitionL2ChainConfig: L2ChainConfig = { slashOffenseExpirationRounds: 8, sentinelEnabled: true, slashingDisableDuration: 5 * 24 * 60 * 60, + slashExecuteRoundsLookBack: 4, }; export const stagingPublicL2ChainConfig: L2ChainConfig = { diff --git a/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_with_consecutive_epochs.test.ts b/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_with_consecutive_epochs.test.ts index 1d9e7b23c882..16089a4cf66b 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_with_consecutive_epochs.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_with_consecutive_epochs.test.ts @@ -1,5 +1,6 @@ -import type { EthAddress } from '@aztec/aztec.js'; +import { type EthAddress, retryUntil } from '@aztec/aztec.js'; import { unique } from '@aztec/foundation/collection'; +import { OffenseType } from '@aztec/slasher'; import { jest } from '@jest/globals'; import 'jest-extended'; @@ -26,8 +27,14 @@ describe('e2e_p2p_inactivity_slash_with_consecutive_epochs', () => { it('only slashes validator inactive for N consecutive epochs', async () => { const [offlineValidator, reenabledValidator] = test.offlineValidators; - const { aztecEpochDuration, slashingExecutionDelayInRounds, slashingOffsetInRounds, slashingRoundSizeInEpochs } = - test.ctx.aztecNodeConfig; + + const { + aztecEpochDuration, + slashingExecutionDelayInRounds, + slashingOffsetInRounds, + slashingRoundSizeInEpochs, + aztecSlotDuration, + } = test.ctx.aztecNodeConfig; const initialEpoch = Number(test.test.monitor.l2EpochNumber) + 1; test.logger.warn(`Waiting until end of epoch ${initialEpoch} to reenable validator ${reenabledValidator}`); @@ -38,6 +45,18 @@ describe('e2e_p2p_inactivity_slash_with_consecutive_epochs', () => { expect(reenabledNode.getSequencer()!.validatorAddresses![0].toString()).toEqual(reenabledValidator.toString()); await reenabledNode.getSequencer()!.start(); + test.logger.warn(`Waiting until offenses are created for ${offlineValidator}`); + const offenses = await retryUntil( + async () => { + const offenses = await test.activeNodes[0].getSlashOffenses('all'); + return offenses.length > 0 ? offenses : undefined; + }, + 'slash offenses', + slashInactivityConsecutiveEpochThreshold * aztecEpochDuration * aztecSlotDuration * 2, + ); + expect(unique(offenses.map(o => o.validator.toString()))).toEqual([offlineValidator.toString()]); + expect(unique(offenses.map(o => o.offenseType))).toEqual([OffenseType.INACTIVITY]); + test.logger.warn(`Expecting offline validator ${offlineValidator} to be slashed but not ${reenabledValidator}`); const slashed: EthAddress[] = []; test.rollup.listenToSlash(args => { @@ -51,7 +70,7 @@ describe('e2e_p2p_inactivity_slash_with_consecutive_epochs', () => { slashInactivityConsecutiveEpochThreshold + (slashingExecutionDelayInRounds + slashingOffsetInRounds) * slashingRoundSizeInEpochs + 5; - test.logger.warn(`Waiting until slot ${aztecEpochDuration * targetEpoch} (epoch ${targetEpoch})`); + test.logger.warn(`Waiting until slot ${aztecEpochDuration * targetEpoch} (epoch ${targetEpoch}) for slash`); await test.test.monitor.waitUntilL2Slot(aztecEpochDuration * targetEpoch); expect(unique(slashed.map(addr => addr.toString()))).toEqual([offlineValidator.toString()]); }); diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index 91b3e3ea53f2..c2010f11c758 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -209,6 +209,7 @@ export type EnvVar = | 'SLASH_GRACE_PERIOD_L2_SLOTS' | 'SLASH_OFFENSE_EXPIRATION_ROUNDS' | 'SLASH_MAX_PAYLOAD_SIZE' + | 'SLASH_EXECUTE_ROUNDS_LOOK_BACK' | 'SYNC_MODE' | 'SYNC_SNAPSHOTS_URL' | 'TELEMETRY' diff --git a/yarn-project/slasher/src/config.ts b/yarn-project/slasher/src/config.ts index 114de841c75e..a3b85ec0ba26 100644 --- a/yarn-project/slasher/src/config.ts +++ b/yarn-project/slasher/src/config.ts @@ -29,6 +29,7 @@ export const DefaultSlasherConfig: SlasherConfig = { slashOffenseExpirationRounds: 4, slashMaxPayloadSize: 50, slashGracePeriodL2Slots: 0, + slashExecuteRoundsLookBack: 4, slashSelfAllowed: false, }; @@ -144,6 +145,11 @@ export const slasherConfigMappings: ConfigMappingsType = { env: 'SLASH_GRACE_PERIOD_L2_SLOTS', ...numberConfigHelper(DefaultSlasherConfig.slashGracePeriodL2Slots), }, + slashExecuteRoundsLookBack: { + env: 'SLASH_EXECUTE_ROUNDS_LOOK_BACK', + description: 'How many rounds to look back when searching for a round to execute.', + ...numberConfigHelper(DefaultSlasherConfig.slashExecuteRoundsLookBack), + }, slashSelfAllowed: { description: 'Whether to allow slashes to own validators', ...booleanConfigHelper(DefaultSlasherConfig.slashSelfAllowed), diff --git a/yarn-project/slasher/src/tally_slasher_client.test.ts b/yarn-project/slasher/src/tally_slasher_client.test.ts index 1613766a1cbd..235ed7e016f6 100644 --- a/yarn-project/slasher/src/tally_slasher_client.test.ts +++ b/yarn-project/slasher/src/tally_slasher_client.test.ts @@ -55,6 +55,25 @@ describe('TallySlasherClient', () => { ...DefaultSlasherConfig, slashGracePeriodL2Slots: 10, slashMaxPayloadSize: 100, + slashExecuteRoundsLookBack: 0, + }; + + const executableRoundData = { + isExecuted: false, + readyToExecute: true, + voteCount: 150n, + }; + + const executedRoundData = { + isExecuted: true, + readyToExecute: false, + voteCount: 150n, + }; + + const emptyRoundData = { + isExecuted: false, + readyToExecute: false, + voteCount: 0n, }; const createOffense = ( @@ -127,7 +146,7 @@ describe('TallySlasherClient', () => { slasherContract = mockDeep(); // Setup mock responses - tallySlashingProposer.getRound.mockResolvedValue({ isExecuted: false, readyToExecute: false, voteCount: 0n }); + tallySlashingProposer.getRound.mockResolvedValue({ ...emptyRoundData }); tallySlashingProposer.getTally.mockResolvedValue({ actions: [{ validator: committee[0], slashAmount: slashingUnit }], committees: [committee], @@ -171,7 +190,7 @@ describe('TallySlasherClient', () => { it('should return vote-offenses action when offenses are available for the target round', async () => { // Round 5 votes on round 3 (offset of 2) const currentRound = 5n; - const currentSlot = currentRound * BigInt(roundSize); // Round 5 + const currentSlot = currentRound * BigInt(roundSize); const targetRound = 3n; // Add slot-based offenses for the target round (slots 576-767 are in round 3) @@ -203,7 +222,7 @@ describe('TallySlasherClient', () => { it('should not vote for offenses outside the target round', async () => { const currentRound = 5n; - const currentSlot = currentRound * BigInt(roundSize); // Round 5 + const currentSlot = currentRound * BigInt(roundSize); const wrongRound = 4n; // Round 5 should vote on round 3, not 4 await offensesStore.addPendingOffense( @@ -220,7 +239,7 @@ describe('TallySlasherClient', () => { it('should handle early rounds where offset cannot be applied', async () => { const currentRound = 0n; - const currentSlot = currentRound * BigInt(roundSize) + 50n; // Round 0 (any slot in round 0) + const currentSlot = currentRound * BigInt(roundSize) + 50n; const action = await tallySlasherClient.getVoteOffensesAction(currentSlot); @@ -229,7 +248,7 @@ describe('TallySlasherClient', () => { it('should use empty committees when epoch cache returns undefined', async () => { const currentRound = 5n; - const currentSlot = currentRound * BigInt(roundSize); // Round 5 + const currentSlot = currentRound * BigInt(roundSize); const targetRound = 3n; await addPendingOffense({ @@ -284,14 +303,10 @@ describe('TallySlasherClient', () => { describe('execute-slash', () => { it('should return execute-slash action when round is ready to execute', async () => { const currentRound = 5n; - const currentSlot = currentRound * BigInt(roundSize); // Round 5 + const currentSlot = currentRound * BigInt(roundSize); const executableRound = 2n; // After execution delay of 2: currentRound - delay - 1 = 5 - 2 - 1 = 2 - tallySlashingProposer.getRound.mockResolvedValueOnce({ - isExecuted: false, - readyToExecute: true, - voteCount: 120n, - }); + tallySlashingProposer.getRound.mockResolvedValueOnce(executableRoundData); const actions = await tallySlasherClient.getProposerActions(currentSlot); @@ -302,28 +317,9 @@ describe('TallySlasherClient', () => { it('should not execute rounds that have already been executed', async () => { const currentRound = 5n; - const currentSlot = currentRound * BigInt(roundSize); // Round 5 - - tallySlashingProposer.getRound.mockResolvedValueOnce({ - isExecuted: true, - readyToExecute: true, - voteCount: 120n, - }); - - const actions = await tallySlasherClient.getProposerActions(currentSlot); - - expect(actions).toEqual([]); - }); - - it('should not execute rounds not ready to execute', async () => { - const currentRound = 5n; - const currentSlot = currentRound * BigInt(roundSize); // Round 5 + const currentSlot = currentRound * BigInt(roundSize); - tallySlashingProposer.getRound.mockResolvedValueOnce({ - isExecuted: false, - readyToExecute: false, - voteCount: 120n, - }); + tallySlashingProposer.getRound.mockResolvedValueOnce(executedRoundData); const actions = await tallySlasherClient.getProposerActions(currentSlot); @@ -332,13 +328,9 @@ describe('TallySlasherClient', () => { it('should not execute rounds with not enough votes', async () => { const currentRound = 5n; - const currentSlot = currentRound * BigInt(roundSize); // Round 5 + const currentSlot = currentRound * BigInt(roundSize); - tallySlashingProposer.getRound.mockResolvedValueOnce({ - isExecuted: false, - readyToExecute: true, - voteCount: 10n, - }); + tallySlashingProposer.getRound.mockResolvedValueOnce({ ...executableRoundData, voteCount: 10n }); const actions = await tallySlasherClient.getProposerActions(currentSlot); @@ -347,13 +339,9 @@ describe('TallySlasherClient', () => { it('should not execute rounds with no slash actions', async () => { const currentRound = 5n; - const currentSlot = currentRound * BigInt(roundSize); // Round 5 + const currentSlot = currentRound * BigInt(roundSize); - tallySlashingProposer.getRound.mockResolvedValueOnce({ - isExecuted: false, - readyToExecute: true, - voteCount: 120n, - }); + tallySlashingProposer.getRound.mockResolvedValueOnce(executableRoundData); tallySlashingProposer.getTally.mockResolvedValueOnce({ actions: [], committees: [committee] }); @@ -364,14 +352,10 @@ describe('TallySlasherClient', () => { it('should not execute vetoed rounds', async () => { const currentRound = 5n; - const currentSlot = currentRound * BigInt(roundSize); // Round 5 + const currentSlot = currentRound * BigInt(roundSize); const executableRound = 2n; // After execution delay of 2: currentRound - delay - 1 = 5 - 2 - 1 = 2 - tallySlashingProposer.getRound.mockResolvedValueOnce({ - isExecuted: false, - readyToExecute: true, - voteCount: 120n, - }); + tallySlashingProposer.getRound.mockResolvedValueOnce(executableRoundData); const payloadAddress = EthAddress.random(); tallySlashingProposer.getPayload.mockResolvedValue({ @@ -389,13 +373,32 @@ describe('TallySlasherClient', () => { it('should not execute when slashing is disabled', async () => { const currentRound = 5n; - const currentSlot = currentRound * BigInt(roundSize); // Round 5 + const currentSlot = currentRound * BigInt(roundSize); slasherContract.isSlashingEnabled.mockResolvedValue(false); const actions = await tallySlasherClient.getProposerActions(currentSlot); expect(actions).toHaveLength(0); }); + + it('should return earliest execute when multiple are available', async () => { + const currentRound = 5n; + const currentSlot = currentRound * BigInt(roundSize); + + tallySlasherClient.updateConfig({ slashExecuteRoundsLookBack: 5 }); + + tallySlashingProposer.getRound + .mockResolvedValueOnce({ ...executedRoundData }) // round 0 + .mockResolvedValueOnce({ ...executableRoundData }); // round 1 + + const actions = await tallySlasherClient.getProposerActions(currentSlot); + + expect(actions).toHaveLength(1); + expectActionExecuteSlash(actions[0], 1n); + expect(tallySlashingProposer.getRound).toHaveBeenCalledTimes(2); + expect(tallySlashingProposer.getRound).toHaveBeenCalledWith(0n); + expect(tallySlashingProposer.getRound).toHaveBeenCalledWith(1n); + }); }); describe('multiple', () => { @@ -643,11 +646,7 @@ describe('TallySlasherClient', () => { const executionRound = 7n; const executionSlot = executionRound * BigInt(roundSize); const executableRound = executionRound - BigInt(settings.slashingExecutionDelayInRounds) - 1n; // 7 - 2 - 1 = 4 - tallySlashingProposer.getRound.mockResolvedValueOnce({ - isExecuted: false, - readyToExecute: true, - voteCount: 150n, - }); + tallySlashingProposer.getRound.mockResolvedValueOnce(executableRoundData); const executeActions = await tallySlasherClient.getProposerActions(executionSlot); @@ -655,16 +654,86 @@ describe('TallySlasherClient', () => { expectActionExecuteSlash(executeActions[0], executableRound); // Verify that if round is marked as executed it won't be executed again - tallySlashingProposer.getRound.mockResolvedValueOnce({ - isExecuted: true, - readyToExecute: true, - voteCount: 150n, - }); + tallySlashingProposer.getRound.mockResolvedValueOnce(executedRoundData); const postExecuteActions = await tallySlasherClient.getProposerActions(executionSlot); expect(postExecuteActions).toEqual([]); }); + it('should handle missed execution', async () => { + tallySlasherClient.updateConfig({ slashExecuteRoundsLookBack: 3 }); + await tallySlasherClient.start(); + + // Round 3: An offense occurs + const offenseRound = 3n; + const validator = committee[0]; + const offense: WantToSlashArgs = { + validator, + amount: settings.slashingAmounts[1], + offenseType: OffenseType.PROPOSED_INSUFFICIENT_ATTESTATIONS, // slot-based + epochOrSlot: offenseRound * BigInt(roundSize), + }; + dummyWatcher.triggerSlash([offense]); + await sleep(100); + + // Round 4: Another offense! + const offenseRound4 = 4n; + const offense4: WantToSlashArgs = { + validator, + amount: settings.slashingAmounts[1], + offenseType: OffenseType.PROPOSED_INSUFFICIENT_ATTESTATIONS, // slot-based + epochOrSlot: offenseRound4 * BigInt(roundSize), + }; + dummyWatcher.triggerSlash([offense4]); + await sleep(100); + + // Round 5: Proposers vote on round 3 offenses + const votingSlot = 5n * BigInt(roundSize); + const voteActions = await tallySlasherClient.getProposerActions(votingSlot); + expect(voteActions).toHaveLength(1); + expectActionVoteOffenses(voteActions[0], 5n, []); + + // Round 6: Proposers vote on round 4 offenses + const votingSlot6 = 6n * BigInt(roundSize); + const voteActions6 = await tallySlasherClient.getProposerActions(votingSlot6); + expect(voteActions6).toHaveLength(1); + expectActionVoteOffenses(voteActions6[0], 6n, []); + + // Assume everything after round 4 inclusive is executable + tallySlashingProposer.getRound.mockImplementation((round: bigint) => + Promise.resolve(round >= 4n ? executableRoundData : emptyRoundData), + ); + + // Round 7: Can execute round 4 + const executionRound = 7n; + const executionSlot = executionRound * BigInt(roundSize); + const executableRound = executionRound - BigInt(settings.slashingExecutionDelayInRounds) - 1n; // 7 - 2 - 1 = 4 + expect(executableRound).toBe(4n); + const executeActions = await tallySlasherClient.getProposerActions(executionSlot); + expect(executeActions).toHaveLength(1); + expectActionExecuteSlash(executeActions[0], executableRound); + + // Round 8.0: Assuming no execution on round 7, we should get another chance to execute round 4 + const nextExecutionRound = 8n; + const nextExecutionSlot = nextExecutionRound * BigInt(roundSize); + const nextExecuteActions = await tallySlasherClient.getProposerActions(nextExecutionSlot); + expect(nextExecuteActions).toHaveLength(1); + expectActionExecuteSlash(nextExecuteActions[0], executableRound); + + // Round 8.1: But if there was execution, then we move onto executing round 5 + tallySlashingProposer.getRound.mockImplementation((round: bigint) => + Promise.resolve(round >= 5n ? executableRoundData : emptyRoundData), + ); + const executeActionsRound5 = await tallySlasherClient.getProposerActions(nextExecutionSlot + 1n); + expect(executeActionsRound5).toHaveLength(1); + expectActionExecuteSlash(executeActionsRound5[0], 5n); + + // Round 8.2: And if round 5 is executed as well, then nothing left to do + tallySlashingProposer.getRound.mockResolvedValue(executedRoundData); + const noExecuteActions = await tallySlasherClient.getProposerActions(nextExecutionSlot + 1n); + expect(noExecuteActions).toHaveLength(0); + }); + it('should handle multiple offenses with different slash amounts', async () => { const currentRound = 5n; const currentSlot = currentRound * BigInt(roundSize); // Round 5 diff --git a/yarn-project/slasher/src/tally_slasher_client.ts b/yarn-project/slasher/src/tally_slasher_client.ts index c035c0e45c82..b98de1e9ca72 100644 --- a/yarn-project/slasher/src/tally_slasher_client.ts +++ b/yarn-project/slasher/src/tally_slasher_client.ts @@ -1,6 +1,7 @@ import { EthAddress } from '@aztec/aztec.js'; import type { EpochCache } from '@aztec/epoch-cache'; import { RollupContract, SlasherContract, TallySlashingProposerContract } from '@aztec/ethereum/contracts'; +import { maxBigint } from '@aztec/foundation/bigint'; import { compactArray, partition, times } from '@aztec/foundation/collection'; import { createLogger } from '@aztec/foundation/log'; import { sleep } from '@aztec/foundation/sleep'; @@ -45,7 +46,7 @@ export type TallySlasherSettings = Prettify< >; export type TallySlasherClientConfig = SlashOffensesCollectorConfig & - Pick; + Pick; /** * The Tally Slasher client is responsible for managing slashable offenses using @@ -177,32 +178,62 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC return compactArray([executeAction, voteAction]); } - /** Returns an execute slash action if there are any rounds ready to be executed */ + /** + * Returns an execute slash action if there are any rounds ready to be executed. + * Returns the oldest slash action if there are multiple rounds pending execution. + */ protected async getExecuteSlashAction(slotNumber: bigint): Promise { const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber); const slashingExecutionDelayInRounds = BigInt(this.settings.slashingExecutionDelayInRounds); const executableRound = currentRound - slashingExecutionDelayInRounds - 1n; - if (executableRound < 0n) { + const lookBack = BigInt(this.config.slashExecuteRoundsLookBack); + const oldestExecutableRound = maxBigint(0n, executableRound - lookBack); + + // Check if slashing is enabled at all + if (!(await this.slasher.isSlashingEnabled())) { + this.log.warn(`Slashing is disabled in the Slasher contract (skipping execution)`); return undefined; } - let logData: Record = { currentRound, executableRound, slotNumber }; + this.log.debug(`Checking slashing rounds ${oldestExecutableRound} to ${executableRound} to execute`, { + slotNumber, + currentRound, + oldestExecutableRound, + executableRound, + slashingExecutionDelayInRounds, + lookBack, + slashingLifetimeInRounds: this.settings.slashingLifetimeInRounds, + }); - try { - // Check if slashing is enabled at all - if (!(await this.slasher.isSlashingEnabled())) { - this.log.warn(`Slashing is disabled in the Slasher contract (skipping execution)`, logData); - return undefined; + // Iterate over all rounds, starting from the oldest, until we find one that is executable + for (let roundToCheck = oldestExecutableRound; roundToCheck <= executableRound; roundToCheck++) { + const action = await this.tryGetRoundExecuteAction(roundToCheck); + if (action) { + return action; } + } + + // And return nothing if none are found + return undefined; + } + + /** + * Checks if a given round is executable and returns an execute-slash action for it if so. + * Assumes round number has already been checked against lifetime and execution delay. + */ + private async tryGetRoundExecuteAction(executableRound: bigint): Promise { + let logData: Record = { executableRound }; + this.log.debug(`Testing if slashing round ${executableRound} is executable`, logData); + try { + // Note we do not check isReadyToExecute here, since we already know that based on the + // executableRound number. Not just that, but it may be that we are building for the given slot number + // that is in the future, so the contract may think it's not yet ready to execute, whereas it is. const roundInfo = await this.tallySlashingProposer.getRound(executableRound); logData = { ...logData, roundInfo }; if (roundInfo.isExecuted) { this.log.verbose(`Round ${executableRound} has already been executed`, logData); return undefined; - } else if (!roundInfo.readyToExecute) { - this.log.verbose(`Round ${executableRound} is not ready to execute yet`, logData); - return undefined; } else if (roundInfo.voteCount === 0n) { this.log.debug(`Round ${executableRound} received no votes`, logData); return undefined; @@ -211,6 +242,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC return undefined; } + // Check if the round yields any slashing at all const { actions: slashActions, committees } = await this.tallySlashingProposer.getTally(executableRound); if (slashActions.length === 0) { this.log.verbose(`Round ${executableRound} does not resolve in any slashing`, logData); @@ -245,9 +277,8 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC return { type: 'execute-slash', round: executableRound, committees: slashedCommittees }; } catch (error) { this.log.error(`Error checking round to execute ${executableRound}`, error); + return undefined; } - - return undefined; } /** Returns a vote action based on offenses from the target round (with offset applied) */ diff --git a/yarn-project/stdlib/src/interfaces/aztec-node-admin.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node-admin.test.ts index 034ff8d55767..a33126a57de1 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node-admin.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node-admin.test.ts @@ -157,6 +157,7 @@ class MockAztecNodeAdmin implements AztecNodeAdmin { slashMaxPayloadSize: 50, slashUnknownPenalty: 1000n, slashGracePeriodL2Slots: 0, + slashExecuteRoundsLookBack: 4, slasherClientType: 'tally' as const, disableValidator: false, disabledValidators: [], diff --git a/yarn-project/stdlib/src/interfaces/slasher.ts b/yarn-project/stdlib/src/interfaces/slasher.ts index 01daaf34d442..74fda5dd844d 100644 --- a/yarn-project/stdlib/src/interfaces/slasher.ts +++ b/yarn-project/stdlib/src/interfaces/slasher.ts @@ -24,6 +24,7 @@ export interface SlasherConfig { slashOffenseExpirationRounds: number; // Number of rounds after which pending offenses expire slashMaxPayloadSize: number; // Maximum number of offenses to include in a single slash payload slashGracePeriodL2Slots: number; // Number of L2 slots to wait after genesis before slashing for most offenses + slashExecuteRoundsLookBack: number; // How many rounds to look back when searching for a round to execute } export const SlasherConfigSchema = z.object({ @@ -44,5 +45,6 @@ export const SlasherConfigSchema = z.object({ slashMaxPayloadSize: z.number(), slashGracePeriodL2Slots: z.number(), slashBroadcastedInvalidBlockPenalty: schemas.BigInt, + slashExecuteRoundsLookBack: z.number(), slashSelfAllowed: z.boolean().optional(), }) satisfies ZodFor;