Skip to content

Commit b760540

Browse files
committed
fix: skip empty epochs in tally slasher
1 parent 3fdb9af commit b760540

2 files changed

Lines changed: 57 additions & 3 deletions

File tree

l1-contracts/src/core/slashing/TallySlashingProposer.sol

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -883,8 +883,17 @@ contract TallySlashingProposer is EIP712 {
883883

884884
unchecked {
885885
for (uint256 i; i < totalValidators; ++i) {
886+
uint256 epochIndex = i / COMMITTEE_SIZE;
887+
886888
// Skip validators that belong to escape-hatch epochs
887-
if (escapeHatchEpochs[i / COMMITTEE_SIZE]) {
889+
if (escapeHatchEpochs[epochIndex]) {
890+
continue;
891+
}
892+
893+
// Skip validators for epochs without a valid committee (e.g. early epochs
894+
// before the validator set was sampled). Without this check, indexing into
895+
// an empty committee array would revert and block execution of the round.
896+
if (_committees[epochIndex].length != COMMITTEE_SIZE) {
888897
continue;
889898
}
890899

@@ -918,11 +927,11 @@ contract TallySlashingProposer is EIP712 {
918927

919928
// Record the slashing action
920929
actions[actionCount] =
921-
SlashAction({validator: _committees[i / COMMITTEE_SIZE][i % COMMITTEE_SIZE], slashAmount: slashAmount});
930+
SlashAction({validator: _committees[epochIndex][i % COMMITTEE_SIZE], slashAmount: slashAmount});
922931
++actionCount;
923932

924933
// Mark this committee as having at least one slashed validator
925-
committeesWithSlashes[i / COMMITTEE_SIZE] = true;
934+
committeesWithSlashes[epochIndex] = true;
926935

927936
// Only slash each validator once at the highest amount that reached quorum
928937
break;

l1-contracts/test/slashing/TallySlashingProposer.t.sol

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,51 @@ contract TallySlashingProposerTest is TestBase {
870870
}
871871
}
872872

873+
function test_executeRoundWithEmptyCommittee() public {
874+
// Round FIRST_SLASH_ROUND targets epochs 0 and 1, which have no committees
875+
// because they precede the validator set sampling lag.
876+
//
877+
// Before the fix, casting votes that reach quorum for validator slots in these
878+
// committee-less epochs would cause executeRound (and getTally) to revert with
879+
// an array out-of-bounds access when indexing _committees[epochIndex][validatorIndex].
880+
_jumpToSlashRound(FIRST_SLASH_ROUND);
881+
SlashRound targetRound = slashingProposer.getCurrentRound();
882+
883+
// Cast QUORUM votes with max slash for ALL validator slots, including those
884+
// corresponding to epochs without valid committees.
885+
uint8[] memory slashAmounts = new uint8[](COMMITTEE_SIZE * ROUND_SIZE_IN_EPOCHS);
886+
for (uint256 i = 0; i < slashAmounts.length; i++) {
887+
slashAmounts[i] = 3;
888+
}
889+
890+
for (uint256 i = 0; i < QUORUM; i++) {
891+
_castVote(slashAmounts);
892+
if (i < QUORUM - 1) {
893+
timeCheater.cheat__progressSlot();
894+
}
895+
}
896+
897+
// Jump past execution delay
898+
uint256 targetSlot = (SlashRound.unwrap(targetRound) + EXECUTION_DELAY_IN_ROUNDS + 1) * ROUND_SIZE;
899+
timeCheater.cheat__jumpToSlot(targetSlot);
900+
901+
// Verify that both targeted epochs have empty committees
902+
address[][] memory committees = slashingProposer.getSlashTargetCommittees(targetRound);
903+
assertEq(committees[0].length, 0, "Epoch 0 should have empty committee");
904+
assertEq(committees[1].length, 0, "Epoch 1 should have empty committee");
905+
906+
// getTally should not revert and should return 0 actions
907+
TallySlashingProposer.SlashAction[] memory actions = slashingProposer.getTally(targetRound, committees);
908+
assertEq(actions.length, 0, "Should have no slash actions for empty committees");
909+
910+
// executeRound should also succeed
911+
slashingProposer.executeRound(targetRound, committees);
912+
913+
// Verify round is marked as executed
914+
(bool isExecuted,) = slashingProposer.getRound(targetRound);
915+
assertTrue(isExecuted, "Round should be marked as executed");
916+
}
917+
873918
function test_getSlashTargetCommitteesEarlyEpochs() public {
874919
// Test that getSlashTargetCommittees handles epochs 0 and 1 without throwing
875920
// when ValidatorSelection__InsufficientValidatorSetSize is thrown

0 commit comments

Comments
 (0)