diff --git a/yarn-project/slasher/src/tally_slasher_client.test.ts b/yarn-project/slasher/src/tally_slasher_client.test.ts index 235ed7e016f6..9683d93958e3 100644 --- a/yarn-project/slasher/src/tally_slasher_client.test.ts +++ b/yarn-project/slasher/src/tally_slasher_client.test.ts @@ -399,6 +399,33 @@ describe('TallySlasherClient', () => { expect(tallySlashingProposer.getRound).toHaveBeenCalledWith(0n); expect(tallySlashingProposer.getRound).toHaveBeenCalledWith(1n); }); + + it('should respect lifetimeInRounds when computing oldestExecutableRound', async () => { + // Use a large lookBack to test that lifetimeInRounds constrains it + tallySlasherClient.updateConfig({ slashExecuteRoundsLookBack: 20 }); + + const currentRound = 15n; + const currentSlot = currentRound * BigInt(roundSize); + const slashingLifetimeInRounds = BigInt(settings.slashingLifetimeInRounds); // 10 + + // The oldest executable round should be currentRound - lifetimeInRounds = 15 - 10 = 5 + // NOT currentRound - executionDelay - 1 - lookBack = 15 - 2 - 1 - 20 = -8 (clamped to 0) + const expectedOldestRound = currentRound - slashingLifetimeInRounds; // 5 + + // Mock rounds 5-12 as executable (executableRound = currentRound - executionDelay - 1 = 15 - 2 - 1 = 12) + tallySlashingProposer.getRound.mockImplementation((round: bigint) => + Promise.resolve(round >= expectedOldestRound && round <= 12n ? executableRoundData : emptyRoundData), + ); + + const actions = await tallySlasherClient.getProposerActions(currentSlot); + + // Should execute the oldest round (5), not try to execute rounds before that + expect(actions).toHaveLength(1); + expectActionExecuteSlash(actions[0], expectedOldestRound); + + // Verify we didn't try to check rounds older than the lifetime allows + expect(tallySlashingProposer.getRound).not.toHaveBeenCalledWith(expectedOldestRound - 1n); + }); }); describe('multiple', () => { diff --git a/yarn-project/slasher/src/tally_slasher_client.ts b/yarn-project/slasher/src/tally_slasher_client.ts index b98de1e9ca72..d11d48adeb05 100644 --- a/yarn-project/slasher/src/tally_slasher_client.ts +++ b/yarn-project/slasher/src/tally_slasher_client.ts @@ -187,7 +187,14 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC const slashingExecutionDelayInRounds = BigInt(this.settings.slashingExecutionDelayInRounds); const executableRound = currentRound - slashingExecutionDelayInRounds - 1n; const lookBack = BigInt(this.config.slashExecuteRoundsLookBack); - const oldestExecutableRound = maxBigint(0n, executableRound - lookBack); + const slashingLifetimeInRounds = BigInt(this.settings.slashingLifetimeInRounds); + + // Compute the oldest executable round considering both lookBack and lifetimeInRounds + // A round is only executable if currentRound <= round + lifetimeInRounds + // So the oldest round we can execute is: currentRound - lifetimeInRounds + const oldestByLifetime = maxBigint(0n, currentRound - slashingLifetimeInRounds); + const oldestByLookBack = maxBigint(0n, executableRound - lookBack); + const oldestExecutableRound = maxBigint(oldestByLifetime, oldestByLookBack); // Check if slashing is enabled at all if (!(await this.slasher.isSlashingEnabled())) { @@ -199,10 +206,12 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC slotNumber, currentRound, oldestExecutableRound, + oldestByLifetime, + oldestByLookBack, executableRound, slashingExecutionDelayInRounds, lookBack, - slashingLifetimeInRounds: this.settings.slashingLifetimeInRounds, + slashingLifetimeInRounds, }); // Iterate over all rounds, starting from the oldest, until we find one that is executable @@ -220,6 +229,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC /** * 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. + * @param executableRound - The round to check for execution */ private async tryGetRoundExecuteAction(executableRound: bigint): Promise { let logData: Record = { executableRound };