Skip to content

Commit 1a96abd

Browse files
committed
feat: Change slash votes from 4 to 2 bits
To reduce gas costs. Also bumps settings in tests to get better benchmarks. ``` $ FORGE_GAS_REPORT=true forge test --mp test/governance/scenario/slashing/TallySlashing.t.sol --mt Small | grep executeRound 1614818 ```
1 parent 880075b commit 1a96abd

File tree

6 files changed

+111
-75
lines changed

6 files changed

+111
-75
lines changed

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

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ import {SafeCast} from "@oz/utils/math/SafeCast.sol";
3636
* 1. Time is divided into rounds (ROUND_SIZE slots each).
3737
* 2. During each round, block proposers can submit votes indicating which validators from the epochs that span
3838
* SLASH_OFFSET_IN_ROUNDS rounds ago should be slashed.
39-
* 3. Votes are encoded as bytes where the i-th nibble (4 bits) represents the slash amount (0-15 slash units) for
40-
* the i-th validator slashed in the round.
39+
* 3. Votes are encoded as bytes where each 2-bit pair represents the slash amount (0-3 slash units) for
40+
* the corresponding validator slashed in the round.
4141
* 4. After a round ends, there is an execution delay period for review so the VETOER in the Slasher can veto the
4242
* expected payload address if needed.
4343
* 5. Once the delay passes, anyone can call executeRound() to tally votes and execute slashing.
@@ -121,7 +121,7 @@ contract TallySlashingProposer is EIP712 {
121121
/**
122122
* @notice Contains all vote data for a single round
123123
* @dev Stores up to MAX_ROUND_SIZE votes as bytes arrays. Each vote encodes slash amounts
124-
* for all validators in the round using 4-bit nibbles per validator.
124+
* for all validators in the round using 2 bits per validator.
125125
* @param votes Array of encoded vote data, one entry per proposer vote in the round
126126
*/
127127
struct RoundVotes {
@@ -180,7 +180,7 @@ contract TallySlashingProposer is EIP712 {
180180

181181
/**
182182
* @notice Base amount of stake to slash per slashing unit (in wei)
183-
* @dev Validators can be voted to be slashed by 1-15 units, multiplied by this base amount
183+
* @dev Validators can be voted to be slashed by 1-3 units, multiplied by this base amount
184184
*/
185185
uint256 public immutable SLASHING_UNIT;
186186

@@ -323,12 +323,12 @@ contract TallySlashingProposer is EIP712 {
323323
/**
324324
* @notice Submit a vote for slashing validators from SLASH_OFFSET_IN_ROUNDS rounds ago
325325
* @dev Only the current block proposer can submit votes, enforced via EIP-712 signature verification.
326-
* Each byte in the votes encodes slash amounts for 2 validators using 4-bit nibbles (0-15 units each).
326+
* Each byte in the votes encodes slash amounts for 4 validators using 2 bits each (0-3 units each).
327327
* The vote includes the current slot number to prevent replay attacks.
328328
*
329-
* @param _votes Encoded voting data where each byte represents slash amounts for 2 validators.
330-
* Lower 4 bits for first validator, upper 4 bits for second validator in each byte.
331-
* Length must equal (COMMITTEE_SIZE * ROUND_SIZE_IN_EPOCHS) / 2 bytes.
329+
* @param _votes Encoded voting data where each byte represents slash amounts for 4 validators.
330+
* Bits 0-1 for first validator, bits 2-3 for second, bits 4-5 for third, bits 6-7 for fourth.
331+
* Length must equal (COMMITTEE_SIZE * ROUND_SIZE_IN_EPOCHS) / 4 bytes.
332332
* @param _sig EIP-712 signature from the current proposer proving authorization to vote.
333333
* Signature covers the vote data and current slot number.
334334
*
@@ -356,8 +356,8 @@ contract TallySlashingProposer is EIP712 {
356356
bytes32 digest = getVoteSignatureDigest(_votes, slot);
357357
require(_sig.verify(proposer, digest), Errors.TallySlashingProposer__InvalidSignature());
358358

359-
// Each byte encodes 2 validators (4 bits each), so each validator is represented as a nibble in the byte array.
360-
uint256 expectedLength = COMMITTEE_SIZE * ROUND_SIZE_IN_EPOCHS / 2;
359+
// Each byte encodes 4 validators (2 bits each), so each validator is represented as 2 bits in the byte array.
360+
uint256 expectedLength = COMMITTEE_SIZE * ROUND_SIZE_IN_EPOCHS / 4;
361361
require(
362362
_votes.length == expectedLength, Errors.TallySlashingProposer__InvalidVoteLength(expectedLength, _votes.length)
363363
);
@@ -676,28 +676,41 @@ contract TallySlashingProposer is EIP712 {
676676

677677
// Create a 2D voting tally matrix: tallyMatrix[validatorIndex][slashAmountInUnits] = voteCount
678678
// - First dimension: validator index (0 to COMMITTEE_SIZE * ROUND_SIZE_IN_EPOCHS - 1)
679-
// - Second dimension: slash amount in units (0-15, where 0 means no slash)
680-
// Each validator can be voted to be slashed by 1-15 units (0 represents no slashing)
681-
uint256[16][] memory tallyMatrix = new uint256[16][](COMMITTEE_SIZE * ROUND_SIZE_IN_EPOCHS);
679+
// - Second dimension: slash amount in units (0-3, where 0 means no slash)
680+
// Each validator can be voted to be slashed by 1-3 units (0 represents no slashing)
681+
uint256[4][] memory tallyMatrix = new uint256[4][](COMMITTEE_SIZE * ROUND_SIZE_IN_EPOCHS);
682682

683683
// Process all votes cast during this round to populate the tally matrix
684-
// Vote encoding: each byte contains 2 validator votes using 4-bit nibbles
685-
// - Lower 4 bits (0x0F): slash amount for validator at index (j * 2)
686-
// - Upper 4 bits (0xF0): slash amount for validator at index (j * 2 + 1)
684+
// Vote encoding: each byte contains 4 validator votes using 2 bits each
685+
// - Bits 0-1 (0x03): slash amount for validator at index (j * 4)
686+
// - Bits 2-3 (0x0C): slash amount for validator at index (j * 4 + 1)
687+
// - Bits 4-5 (0x30): slash amount for validator at index (j * 4 + 2)
688+
// - Bits 6-7 (0xC0): slash amount for validator at index (j * 4 + 3)
687689
for (uint256 i = 0; i < voteCount; i++) {
688690
// Load the i-th votes from this round from storage into memory
689691
bytes memory currentVote = _getRoundVotes(roundNumber).votes[i];
690692
for (uint256 j = 0; j < currentVote.length; j++) {
691-
// Extract lower 4 bits for first validator in the byte
692-
uint8 validatorSlash = uint8(currentVote[j]) & 0x0F;
693-
if (validatorSlash > 0) {
694-
tallyMatrix[j * 2][validatorSlash]++;
693+
uint8 currentByte = uint8(currentVote[j]);
694+
695+
// Extract 2 bits for each of the 4 validators in this byte
696+
uint8 validatorSlash0 = currentByte & 0x03;
697+
if (validatorSlash0 > 0) {
698+
tallyMatrix[j * 4][validatorSlash0]++;
699+
}
700+
701+
uint8 validatorSlash1 = (currentByte >> 2) & 0x03;
702+
if (validatorSlash1 > 0) {
703+
tallyMatrix[j * 4 + 1][validatorSlash1]++;
704+
}
705+
706+
uint8 validatorSlash2 = (currentByte >> 4) & 0x03;
707+
if (validatorSlash2 > 0) {
708+
tallyMatrix[j * 4 + 2][validatorSlash2]++;
695709
}
696710

697-
// Extract upper 4 bits for second validator in the byte
698-
validatorSlash = (uint8(currentVote[j]) >> 4) & 0x0F;
699-
if (validatorSlash > 0) {
700-
tallyMatrix[j * 2 + 1][validatorSlash]++;
711+
uint8 validatorSlash3 = (currentByte >> 6) & 0x03;
712+
if (validatorSlash3 > 0) {
713+
tallyMatrix[j * 4 + 3][validatorSlash3]++;
701714
}
702715
}
703716
}
@@ -712,10 +725,10 @@ contract TallySlashingProposer is EIP712 {
712725
for (uint256 i = 0; i < tallyMatrix.length; i++) {
713726
uint256 voteCountForValidator = 0;
714727

715-
// Check slash amounts from highest (15 units) to lowest (1 unit)
728+
// Check slash amounts from highest (3 units) to lowest (1 unit)
716729
// Cumulative voting: a vote for N units counts as votes for N-1, N-2, ..., 1 units
717-
// This means if someone votes to slash 5 units, it also counts as votes for 1-4 units
718-
for (uint256 j = 15; j > 0; j--) {
730+
// This means if someone votes to slash 3 units, it also counts as votes for 1-2 units
731+
for (uint256 j = 3; j > 0; j--) {
719732
voteCountForValidator += tallyMatrix[i][j];
720733

721734
// Check if this slash amount has reached quorum

l1-contracts/test/benchmark/happy.t.sol

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -391,16 +391,18 @@ contract BenchmarkRollupTest is FeeModelTestPoints, DecoderBase {
391391
* @return Encoded vote data
392392
*/
393393
function createTallyVoteData(uint256 _size) internal returns (bytes memory) {
394-
require(_size % 2 == 0, "Vote data must have even number of validators");
394+
require(_size % 4 == 0, "Vote data must have multiple of 4 validators");
395395

396396
bytes32 seed = keccak256(abi.encode(_size, block.timestamp));
397397

398-
bytes memory voteData = new bytes(_size / 2);
398+
bytes memory voteData = new bytes(_size / 4);
399399

400-
for (uint256 i = 0; i < _size; i += 2) {
401-
uint8 firstValidator = uint8(uint256(keccak256(abi.encode(seed, i)))) & 0x0F;
402-
uint8 secondValidator = uint8(uint256(keccak256(abi.encode(seed, i + 1)))) & 0x0F;
403-
voteData[i / 2] = bytes1((secondValidator << 4) | firstValidator);
400+
for (uint256 i = 0; i < _size; i += 4) {
401+
uint8 validator0 = uint8(uint256(keccak256(abi.encode(seed, i)))) & 0x03; // 2 bits
402+
uint8 validator1 = uint8(uint256(keccak256(abi.encode(seed, i + 1)))) & 0x03; // 2 bits
403+
uint8 validator2 = uint8(uint256(keccak256(abi.encode(seed, i + 2)))) & 0x03; // 2 bits
404+
uint8 validator3 = uint8(uint256(keccak256(abi.encode(seed, i + 3)))) & 0x03; // 2 bits
405+
voteData[i / 4] = bytes1((validator3 << 6) | (validator2 << 4) | (validator1 << 2) | validator0);
404406
}
405407

406408
return voteData;

l1-contracts/test/builder/RollupBuilder.sol

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,16 @@ contract RollupBuilder is Test {
224224
return this;
225225
}
226226

227+
function setEntryQueueFlushSizeMin(uint256 _flushSizeMin) public returns (RollupBuilder) {
228+
config.rollupConfigInput.stakingQueueConfig.normalFlushSizeMin = _flushSizeMin;
229+
return this;
230+
}
231+
232+
function setEntryQueueFlushSizeQuotient(uint256 _flushSizeQuotient) public returns (RollupBuilder) {
233+
config.rollupConfigInput.stakingQueueConfig.normalFlushSizeQuotient = _flushSizeQuotient;
234+
return this;
235+
}
236+
227237
function setValidators(CheatDepositArgs[] memory _validators) public returns (RollupBuilder) {
228238
for (uint256 i = 0; i < _validators.length; i++) {
229239
config.validators.push(_validators[i]);

l1-contracts/test/governance/scenario/slashing/TallySlashing.t.sol

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@ contract SlashingTest is TestBase {
5050
uint256[] internal validatorKeys;
5151
address[] internal validatorAddresses;
5252

53-
uint256 constant VALIDATOR_COUNT = 4;
54-
uint256 constant COMMITTEE_SIZE = 4;
55-
uint256 constant HOW_MANY_SLASHED = 4;
56-
uint256 constant ROUND_SIZE_IN_EPOCHS = 1;
53+
uint256 constant VALIDATOR_COUNT = 128;
54+
uint256 constant COMMITTEE_SIZE = 48;
55+
uint256 constant HOW_MANY_SLASHED = 1;
56+
uint256 constant ROUND_SIZE_IN_EPOCHS = 2;
57+
uint256 constant EPOCH_DURATION = 32;
5758
uint256 constant INITIAL_EPOCH = 6 + ROUND_SIZE_IN_EPOCHS;
5859

5960
function _getProposerKey() internal returns (uint256) {
@@ -88,22 +89,24 @@ contract SlashingTest is TestBase {
8889
SlashRound votingRound = slashingProposer.getCurrentRound();
8990

9091
// Create votes - for tally slashing we need to encode votes as bytes
91-
// Each validator gets a slash amount between 1-15 units
92+
// Each validator gets a slash amount between 1-3 units
9293
// For simplicity, we'll vote to slash all validators by the same amount
9394
uint256 slashUnits = _slashAmount / slashingProposer.SLASHING_UNIT();
9495
if (slashUnits == 0) slashUnits = 1; // Minimum 1 unit
95-
if (slashUnits > 15) slashUnits = 15; // Maximum 15 units
96+
if (slashUnits > 3) slashUnits = 3; // Maximum 3 units
9697

97-
// Calculate expected vote length: (COMMITTEE_SIZE * ROUND_SIZE_IN_EPOCHS) / 2
98+
// Calculate expected vote length: (COMMITTEE_SIZE * ROUND_SIZE_IN_EPOCHS) / 4
9899
uint256 totalValidators = slashingProposer.COMMITTEE_SIZE() * slashingProposer.ROUND_SIZE_IN_EPOCHS();
99-
uint256 voteLength = totalValidators / 2;
100+
uint256 voteLength = totalValidators / 4;
100101
bytes memory votes = new bytes(voteLength);
101102

102-
// Encode votes: each byte contains 2 validator votes (4 bits each)
103+
// Encode votes: each byte contains 4 validator votes (2 bits each)
103104
for (uint256 i = 0; i < voteLength; i++) {
104-
uint8 vote1 = (i * 2 < _howMany) ? uint8(slashUnits) : 0;
105-
uint8 vote2 = (i * 2 + 1 < _howMany) ? uint8(slashUnits) : 0;
106-
votes[i] = bytes1((vote2 << 4) | vote1);
105+
uint8 vote1 = (i * 4 < _howMany) ? uint8(slashUnits) : 0;
106+
uint8 vote2 = (i * 4 + 1 < _howMany) ? uint8(slashUnits) : 0;
107+
uint8 vote3 = (i * 4 + 2 < _howMany) ? uint8(slashUnits) : 0;
108+
uint8 vote4 = (i * 4 + 3 < _howMany) ? uint8(slashUnits) : 0;
109+
votes[i] = bytes1((vote4 << 6) | (vote3 << 4) | (vote2 << 2) | vote1);
107110
}
108111

109112
// Cast votes in multiple slots to reach quorum
@@ -160,7 +163,7 @@ contract SlashingTest is TestBase {
160163
).setSlashingLifetimeInRounds(_slashingLifetimeInRounds).setSlashingExecutionDelayInRounds(
161164
_slashingExecutionDelayInRounds
162165
).setSlasherFlavor(SlasherFlavor.TALLY).setSlashingRoundSize(roundSize).setSlashingQuorum(roundSize / 2 + 1)
163-
.setSlashingOffsetInRounds(2);
166+
.setSlashingOffsetInRounds(2).setEpochDuration(EPOCH_DURATION).setEntryQueueFlushSizeMin(VALIDATOR_COUNT);
164167
builder.deploy();
165168

166169
rollup = builder.getConfig().rollup;
@@ -174,7 +177,7 @@ contract SlashingTest is TestBase {
174177
address(rollup),
175178
block.timestamp,
176179
TestConstants.AZTEC_SLOT_DURATION,
177-
TestConstants.AZTEC_EPOCH_DURATION,
180+
EPOCH_DURATION,
178181
TestConstants.AZTEC_PROOF_SUBMISSION_EPOCHS
179182
);
180183

@@ -194,7 +197,7 @@ contract SlashingTest is TestBase {
194197

195198
_setupCommitteeForSlashing(_lifetimeInRounds, _executionDelayInRounds);
196199
address[] memory attesters = rollup.getEpochCommittee(Epoch.wrap(INITIAL_EPOCH));
197-
uint96 slashAmount = 10e18;
200+
uint96 slashAmount = 20e18;
198201
SlashRound firstSlashingRound = _createSlashingVotes(slashAmount, attesters.length);
199202

200203
uint256 firstExecutableSlot =
@@ -221,9 +224,8 @@ contract SlashingTest is TestBase {
221224
_lifetimeInRounds = bound(_lifetimeInRounds, _executionDelayInRounds + 1, 127); // Must be < ROUNDABOUT_SIZE
222225

223226
_setupCommitteeForSlashing(_lifetimeInRounds, _executionDelayInRounds);
224-
address[] memory attesters = rollup.getEpochCommittee(Epoch.wrap(INITIAL_EPOCH));
225-
uint96 slashAmount = 10e18;
226-
SlashRound firstSlashingRound = _createSlashingVotes(slashAmount, attesters.length);
227+
uint96 slashAmount = 20e18;
228+
SlashRound firstSlashingRound = _createSlashingVotes(slashAmount, COMMITTEE_SIZE);
227229

228230
uint256 firstExecutableSlot =
229231
(SlashRound.unwrap(firstSlashingRound) + _executionDelayInRounds + 1) * slashingProposer.ROUND_SIZE();
@@ -232,8 +234,9 @@ contract SlashingTest is TestBase {
232234
_jumpToSlot = bound(_jumpToSlot, firstExecutableSlot, lastExecutableSlot);
233235

234236
timeCheater.cheat__jumpToSlot(_jumpToSlot);
235-
uint256[] memory stakes = new uint256[](attesters.length);
236-
for (uint256 i = 0; i < attesters.length; i++) {
237+
address[] memory attesters = rollup.getEpochCommittee(slashingProposer.getSlashTargetEpoch(firstSlashingRound, 0));
238+
uint256[] memory stakes = new uint256[](COMMITTEE_SIZE);
239+
for (uint256 i = 0; i < COMMITTEE_SIZE; i++) {
237240
AttesterView memory attesterView = rollup.getAttesterView(attesters[i]);
238241
stakes[i] = attesterView.effectiveBalance;
239242
assertTrue(attesterView.status == Status.VALIDATING, "Invalid status");
@@ -264,7 +267,7 @@ contract SlashingTest is TestBase {
264267

265268
_setupCommitteeForSlashing(_lifetimeInRounds, _executionDelayInRounds);
266269
address[] memory attesters = rollup.getEpochCommittee(Epoch.wrap(INITIAL_EPOCH));
267-
uint96 slashAmount = 10e18;
270+
uint96 slashAmount = 20e18;
268271
SlashRound firstSlashingRound = _createSlashingVotes(slashAmount, attesters.length);
269272

270273
// For tally slashing, we need to predict the payload address and veto it
@@ -327,10 +330,16 @@ contract SlashingTest is TestBase {
327330
// Execute the slash
328331
slashingProposer.executeRound(firstSlashingRound, committees);
329332

333+
// Calculate actual slash amount (limited by max 3 units)
334+
uint256 slashUnits = slashAmount1 / slashingProposer.SLASHING_UNIT();
335+
if (slashUnits == 0) slashUnits = 1; // Minimum 1 unit
336+
if (slashUnits > 3) slashUnits = 3; // Maximum 3 units
337+
uint256 actualSlashAmount = slashUnits * slashingProposer.SLASHING_UNIT();
338+
330339
// Check balances
331340
for (uint256 i = 0; i < howManyToSlash; i++) {
332341
AttesterView memory attesterView = rollup.getAttesterView(attesters[i]);
333-
assertEq(attesterView.effectiveBalance, stakes[i] - slashAmount1);
342+
assertEq(attesterView.effectiveBalance, stakes[i] - actualSlashAmount);
334343
assertEq(attesterView.exit.amount, 0, "Invalid stake");
335344
assertTrue(attesterView.status == Status.VALIDATING, "Invalid status");
336345
}

l1-contracts/test/harnesses/TestConstants.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ library TestConstants {
2525
uint256 internal constant AZTEC_SLASHING_EXECUTION_DELAY_IN_ROUNDS = 0;
2626
uint256 internal constant AZTEC_SLASHING_OFFSET_IN_ROUNDS = 2;
2727
address internal constant AZTEC_SLASHING_VETOER = address(0);
28-
uint256 internal constant AZTEC_SLASHING_UNIT = 5e18;
28+
uint256 internal constant AZTEC_SLASHING_UNIT = 20e18;
2929
uint256 internal constant AZTEC_MANA_TARGET = 100_000_000;
3030
uint256 internal constant AZTEC_ENTRY_QUEUE_FLUSH_SIZE_MIN = 4;
3131
uint256 internal constant AZTEC_ENTRY_QUEUE_FLUSH_SIZE_QUOTIENT = 2;

0 commit comments

Comments
 (0)