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: 8 additions & 5 deletions l1-contracts/src/core/slashing/TallySlashingProposer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -594,13 +594,16 @@ contract TallySlashingProposer is EIP712 {
function getVotes(SlashRound _round, uint256 _index) external view returns (bytes memory) {
uint256 expectedLength = COMMITTEE_SIZE * ROUND_SIZE_IN_EPOCHS / 4;

// _getRoundVotes reverts if _round is out of the roundabout range.
// But within-range the storage slot may still hold stale data from a
// previous round that mapped to the same circular index. Guard against
// that by checking the authoritative round metadata first.
// _getRoundData reverts if _round is out of the roundabout range and
// returns empty metadata if this circular slot still contains round data
// from an older round number.
SlashRound currentRound = getCurrentRound();
RoundData memory roundData = _getRoundData(_round, currentRound);
if (roundData.voteCount == 0) {

// Vote storage is not cleared when a circular slot is reused. If this
// round has fewer votes than a previous one that shared the same slot,
// indices >= voteCount would otherwise return stale vote bytes.
if (_index >= roundData.voteCount) {
return new bytes(expectedLength);
}

Expand Down
46 changes: 43 additions & 3 deletions l1-contracts/test/slashing/TallySlashingProposer.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -605,9 +605,8 @@ contract TallySlashingProposerTest is TestBase {

function test_getVotesRevertsForOutOfRangeRound() public {
// Test that getVotes reverts for rounds outside the valid roundabout range.
// Before the range check was added to _getRoundVotes, getVotes would silently
// return whatever stale data was at the storage slot - potentially data from
// a completely different round that mapped to the same circular index.
// getVotes routes through _getRoundData for range validation before loading
// vote slots, so out-of-range round reads should always revert.
_jumpToSlashRound(10);
SlashRound baseRound = slashingProposer.getCurrentRound();

Expand Down Expand Up @@ -670,6 +669,47 @@ contract TallySlashingProposerTest is TestBase {
assertEq(result, emptyVote, "getVotes should return empty bytes for stale round");
}

function test_getVotesReturnsEmptyForUnwrittenIndexInCurrentRound() public {
// Populate two vote indices in a base round.
_jumpToSlashRound(10);
SlashRound baseRound = slashingProposer.getCurrentRound();

uint8[] memory firstVote = new uint8[](COMMITTEE_SIZE * ROUND_SIZE_IN_EPOCHS);
firstVote[0] = 3;
_castVote(firstVote);

timeCheater.cheat__progressSlot();

uint8[] memory secondVote = new uint8[](COMMITTEE_SIZE * ROUND_SIZE_IN_EPOCHS);
secondVote[1] = 2;
secondVote[2] = 1;
_castVote(secondVote);

bytes memory secondVoteData = _createVoteData(secondVote);
assertEq(slashingProposer.getVotes(baseRound, 1), secondVoteData, "Base round index 1 should be populated");

// Jump to a round that maps to the same circular slot and cast only one vote.
uint256 roundaboutSize = slashingProposer.ROUNDABOUT_SIZE();
SlashRound overlappingRound = SlashRound.wrap(SlashRound.unwrap(baseRound) + roundaboutSize);
_jumpToSlashRound(SlashRound.unwrap(overlappingRound));

uint8[] memory overlappingVote = new uint8[](COMMITTEE_SIZE * ROUND_SIZE_IN_EPOCHS);
overlappingVote[0] = 1;
_castVote(overlappingVote);

(, uint256 voteCount) = slashingProposer.getRound(overlappingRound);
assertEq(voteCount, 1, "Overlapping round should have exactly one vote");

// Reading index 1 must return empty bytes, not stale bytes from baseRound index 1.
uint256 expectedLength = COMMITTEE_SIZE * ROUND_SIZE_IN_EPOCHS / 4;
bytes memory emptyVote = new bytes(expectedLength);
assertEq(
slashingProposer.getVotes(overlappingRound, 1),
emptyVote,
"getVotes should return empty bytes for unwritten vote index"
);
}

function test_getVotesRevertsForFutureRound() public {
// getVotes should revert when asked about a round in the future
_jumpToSlashRound(10);
Expand Down
Loading