Skip to content

Commit 82b8d2c

Browse files
authored
chore: update get sample seed (#16391)
Updating the sample logic such that we store only the truncated randao values in the checkpoints and then at retrieval time computes the sample seed using the epoch as well. Means that we don't have the "stale seed" issue to the same degree, but still allowing checkpointing the randao.
2 parents cfc0c6e + d0194a4 commit 82b8d2c

File tree

8 files changed

+92
-100
lines changed

8 files changed

+92
-100
lines changed

l1-contracts/src/core/RollupCore.sol

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -526,8 +526,8 @@ contract RollupCore is EIP712("Aztec Rollup", "1"), Ownable, IStakingCore, IVali
526526
* `ExtRollupLib.propose(...)` -> `ProposeLib.propose(...)` -> `ValidatorSelectionLib.setupEpoch(...)`).
527527
*
528528
* If there are missed proposals then setupEpoch does not get called automatically. Since the next committee
529-
* selection is computed based on the latest stored seed and the epoch number, we would only fail to get a
530-
* fresh seed if:
529+
* selection is computed based on the stored randao and the epoch number, failing to update the randao stored
530+
* will keep the committee predictable longer into the future. We would only fail to get a fresh randao if:
531531
* 1. All the proposals in the epoch were missed
532532
* 2. Nobody called setupEpoch on the Rollup contract
533533
*
@@ -540,13 +540,13 @@ contract RollupCore is EIP712("Aztec Rollup", "1"), Ownable, IStakingCore, IVali
540540
}
541541

542542
/**
543-
* @notice Captures the seed for validator selection in the next epoch
544-
* @dev Can be called by anyone. Takes a snapshot of the current state to ensure
545-
* unpredictable but deterministic validator selection. Automatically called
546-
* from setupEpoch.
543+
* @notice Captures the randao for future validator selection
544+
* @dev Can be called by anyone. Takes a snapshot of the current randao to ensure unpredictable but deterministic
545+
* validator selection. Automatically called from setupEpoch. Can be used as a cheaper alternative to
546+
* `setupEpoch` to update the randao checkpoints.
547547
*/
548-
function setupSeedSnapshotForNextEpoch() public override(IValidatorSelectionCore) {
549-
ExtRollupLib2.setupSeedSnapshotForNextEpoch();
548+
function checkpointRandao() public override(IValidatorSelectionCore) {
549+
ExtRollupLib2.checkpointRandao();
550550
}
551551

552552
/**

l1-contracts/src/core/interfaces/IValidatorSelection.sol

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ import {Checkpoints} from "@oz/utils/structs/Checkpoints.sol";
99
struct ValidatorSelectionStorage {
1010
// A mapping to snapshots of the validator set
1111
mapping(Epoch => bytes32 committeeCommitment) committeeCommitments;
12-
// Checkpointed map of epoch -> sample seed
13-
Checkpoints.Trace224 seeds;
12+
// Checkpointed map of epoch -> randao value
13+
Checkpoints.Trace224 randaos;
1414
uint256 targetCommitteeSize;
1515
}
1616

1717
interface IValidatorSelectionCore {
1818
function setupEpoch() external;
19-
function setupSeedSnapshotForNextEpoch() external;
19+
function checkpointRandao() external;
2020
}
2121

2222
interface IValidatorSelection is IValidatorSelectionCore, IEmperor {

l1-contracts/src/core/libraries/rollup/ExtRollupLib2.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ library ExtRollupLib2 {
7171
ValidatorSelectionLib.setupEpoch(currentEpoch);
7272
}
7373

74-
function setupSeedSnapshotForNextEpoch() external {
74+
function checkpointRandao() external {
7575
Epoch currentEpoch = Timestamp.wrap(block.timestamp).epochFromTimestamp();
76-
ValidatorSelectionLib.setSampleSeedForNextEpoch(currentEpoch);
76+
ValidatorSelectionLib.checkpointRandao(currentEpoch);
7777
}
7878

7979
function updateStakingQueueConfig(StakingQueueConfig memory _config) external {

l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol

Lines changed: 35 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ import {TransientSlot} from "@oz/utils/TransientSlot.sol";
6060
* 4. Seed Management:
6161
* - Sample seeds determine committee and proposer selection for each epoch
6262
* - Seeds use prevrandao from L1 blocks combined with epoch number for unpredictability
63-
* - Seeds are set 2 epochs in advance to prevent last-minute manipulation and provide L1-reorg resistance
64-
* - First two epochs use maximum seed values (type(uint224).max) for bootstrap (this results in the committee
63+
* - Prevrandao are set 2 epochs in advance to prevent last-minute manipulation and provide L1-reorg resistance
64+
* - First two epochs use randao values (type(uint224).max) for bootstrap (this results in the committee
6565
* being predictable in the first 2 epochs which is considered acceptable when bootstrapping the network)
6666
*
6767
* 5. Caching and Optimization:
@@ -134,9 +134,8 @@ library ValidatorSelectionLib {
134134
ValidatorSelectionStorage storage store = getStorage();
135135
store.targetCommitteeSize = _targetCommitteeSize;
136136

137-
// Set the sample seed for the first 2 epochs to max
138-
store.seeds.push(0, type(uint224).max);
139-
store.seeds.push(1, type(uint224).max);
137+
// Set the initial randao
138+
store.randaos.push(0, uint224(block.prevrandao));
140139
}
141140

142141
/**
@@ -155,11 +154,11 @@ library ValidatorSelectionLib {
155154

156155
//################ Seeds ################
157156
// Get the sample seed for this current epoch.
158-
uint224 sampleSeed = getSampleSeed(_epochNumber);
157+
uint256 sampleSeed = getSampleSeed(_epochNumber);
159158

160-
// Set the sample seed for the next epoch if required
159+
// Checkpoint randao for future sampling if required
161160
// function handles the case where it is already set
162-
setSampleSeedForNextEpoch(_epochNumber);
161+
checkpointRandao(_epochNumber);
163162

164163
//################ Committee ################
165164
// If the committee is not set for this epoch, we need to sample it
@@ -228,7 +227,7 @@ library ValidatorSelectionLib {
228227
}
229228

230229
// Get the proposer from the committee based on the epoch, slot, and sample seed
231-
uint224 sampleSeed = getSampleSeed(_epochNumber);
230+
uint256 sampleSeed = getSampleSeed(_epochNumber);
232231
proposerIndex = computeProposerIndex(_epochNumber, _slot, sampleSeed, committeeSize);
233232
proposer = committee[proposerIndex];
234233

@@ -384,7 +383,7 @@ library ValidatorSelectionLib {
384383

385384
Epoch epochNumber = _slot.epochFromSlot();
386385

387-
uint224 sampleSeed = getSampleSeed(epochNumber);
386+
uint256 sampleSeed = getSampleSeed(epochNumber);
388387
(uint32 ts, uint256[] memory indices) = sampleValidatorsIndices(epochNumber, sampleSeed);
389388
uint256 committeeSize = indices.length;
390389
if (committeeSize == 0) {
@@ -402,7 +401,7 @@ library ValidatorSelectionLib {
402401
* @param _seed The cryptographic seed for sampling randomness
403402
* @return The array of validator addresses selected for the committee
404403
*/
405-
function sampleValidators(Epoch _epoch, uint224 _seed) internal returns (address[] memory) {
404+
function sampleValidators(Epoch _epoch, uint256 _seed) internal returns (address[] memory) {
406405
(uint32 ts, uint256[] memory indices) = sampleValidatorsIndices(_epoch, _seed);
407406
return StakingLib.getAttestersFromIndicesAtTime(Timestamp.wrap(ts), indices);
408407
}
@@ -415,7 +414,7 @@ library ValidatorSelectionLib {
415414
* @return The array of committee member addresses for the epoch
416415
*/
417416
function getCommitteeAt(Epoch _epochNumber) internal returns (address[] memory) {
418-
uint224 seed = getSampleSeed(_epochNumber);
417+
uint256 seed = getSampleSeed(_epochNumber);
419418
return sampleValidators(_epochNumber, seed);
420419
}
421420

@@ -444,41 +443,34 @@ library ValidatorSelectionLib {
444443
}
445444

446445
/**
447-
* @notice Sets the sample seed for the epoch two epochs in the future
448-
* @dev Calls setSampleSeedForEpoch with _epoch + 2 to maintain the two-epoch advance requirement.
449-
* This ensures randomness seeds are set well in advance to prevent manipulation.
450-
* @param _epoch The current epoch (seed will be set for _epoch + 2)
446+
* @notice Checkpoints randao value for future usage
447+
* @dev Checks if already stored before computing and storing the randao value.
448+
* Offset the epoch by 2 to maintain the two-epoch advance requirement.
449+
* This ensures randomness are set well in advance to prevent manipulation.
450+
* @param _epoch The current epoch (randao will be set for _epoch + 2)
451+
* Passed to reduce recomputation
451452
*/
452-
function setSampleSeedForNextEpoch(Epoch _epoch) internal {
453-
setSampleSeedForEpoch(_epoch + Epoch.wrap(2));
454-
}
455-
456-
/**
457-
* @notice Sets the sample seed for a specific epoch if not already set
458-
* @dev Checks if the seed is already stored before computing and storing a new one.
459-
* Uses computeNextSeed() to generate cryptographically secure randomness.
460-
* @param _epoch The epoch to set the sample seed for
461-
*/
462-
function setSampleSeedForEpoch(Epoch _epoch) internal {
453+
function checkpointRandao(Epoch _epoch) internal {
463454
ValidatorSelectionStorage storage store = getStorage();
464-
uint32 epoch = Epoch.unwrap(_epoch).toUint32();
455+
456+
// Compute the offset
457+
uint32 epoch = Epoch.unwrap(_epoch).toUint32() + 2;
465458

466459
// Check if the latest checkpoint is for the next epoch
467-
// It should be impossible that zero epoch snapshots exist, as in the genesis state we push the first sample seed
460+
// It should be impossible that zero epoch snapshots exist, as in the genesis state we push the first values
468461
// into the store
469-
(, uint32 mostRecentSeedEpoch,) = store.seeds.latestCheckpoint();
462+
(, uint32 mostRecentEpoch,) = store.randaos.latestCheckpoint();
470463

471-
// If the sample seed for the next epoch is already set, we can skip the computation
472-
if (mostRecentSeedEpoch == epoch) {
464+
// If the randao for the next epoch is already set, we can skip the computation
465+
if (mostRecentEpoch == epoch) {
473466
return;
474467
}
475468

476-
// If the most recently stored seed is less than the epoch we are querying, then we need to compute its seed for
469+
// If the most recently stored epoch is less than the epoch we are querying, then we need to store randao for
477470
// later use
478-
if (mostRecentSeedEpoch < epoch) {
479-
// Compute the sample seed for the next epoch
480-
uint224 nextSeed = computeNextSeed(_epoch);
481-
store.seeds.push(epoch, nextSeed);
471+
if (mostRecentEpoch < epoch) {
472+
// Truncate the randao to be used for future sampling.
473+
store.randaos.push(epoch, uint224(block.prevrandao));
482474
}
483475
}
484476

@@ -558,15 +550,14 @@ library ValidatorSelectionLib {
558550

559551
/**
560552
* @notice Gets the cryptographic sample seed for an epoch
561-
* @dev Retrieves the seed from the checkpointed seeds mapping using upperLookup.
562-
* The seed is computed as keccak256(epoch, block.prevrandao) during epoch setup.
563-
* Never called for epoch 0 or future epochs - only for current/past epochs.
553+
* @dev Retrieves the randao from the checkpointed randaos mapping using upperLookup.
554+
* Then computes the sample seed using keccak256(epoch, randao)
564555
* @param _epoch The epoch to get the sample seed for
565-
* @return The 224-bit sample seed used for validator selection randomness
556+
* @return The sample seed used for validator selection randomness
566557
*/
567-
function getSampleSeed(Epoch _epoch) internal view returns (uint224) {
558+
function getSampleSeed(Epoch _epoch) internal view returns (uint256) {
568559
ValidatorSelectionStorage storage store = getStorage();
569-
return store.seeds.upperLookup(Epoch.unwrap(_epoch).toUint32());
560+
return uint256(keccak256(abi.encode(_epoch, store.randaos.upperLookup(Epoch.unwrap(_epoch).toUint32()))));
570561
}
571562

572563
/**
@@ -607,7 +598,7 @@ library ValidatorSelectionLib {
607598
* @return indices Array of validator indices selected for the committee
608599
* @custom:reverts Errors.ValidatorSelection__InsufficientCommitteeSize if not enough validators available
609600
*/
610-
function sampleValidatorsIndices(Epoch _epoch, uint224 _seed) private returns (uint32, uint256[] memory) {
601+
function sampleValidatorsIndices(Epoch _epoch, uint256 _seed) private returns (uint32, uint256[] memory) {
611602
ValidatorSelectionStorage storage store = getStorage();
612603
uint32 ts = epochToSampleTime(_epoch);
613604
uint256 validatorSetSize = StakingLib.getAttesterCountAtTime(Timestamp.wrap(ts));
@@ -625,18 +616,6 @@ library ValidatorSelectionLib {
625616
return (ts, SampleLib.computeCommittee(targetCommitteeSize, validatorSetSize, _seed));
626617
}
627618

628-
/**
629-
* @notice Computes the cryptographic seed for an epoch using L1 randomness
630-
* @dev Combines epoch number with block.prevrandao to create unpredictable but deterministic seed.
631-
* Including epoch prevents foundry testing issues where prevrandao might be zero.
632-
* @param _epoch The epoch to compute the seed for
633-
* @return The computed 224-bit seed (truncated from 256-bit keccak output)
634-
*/
635-
function computeNextSeed(Epoch _epoch) private view returns (uint224) {
636-
// Allow for unsafe (lossy) downcast as we do not care if we loose bits
637-
return uint224(uint256(keccak256(abi.encode(_epoch, block.prevrandao))));
638-
}
639-
640619
/**
641620
* @notice Computes the keccak256 commitment hash for a committee member array
642621
* @dev Creates a cryptographic commitment to the committee composition that can be verified later.

l1-contracts/test/validator-selection/ValidatorSelection.t.sol

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,10 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase {
136136
assertEq(expectedProposer, actualProposer, "Invalid proposer");
137137
}
138138

139-
function testCommitteeForNonSetupEpoch(uint8 _epochsToJump) public setup(4, 4) progressEpochs(2) {
139+
function testCommitteeForNonSetupEpoch() public setup(8, 4) progressEpochs(2) {
140140
Epoch pre = rollup.getCurrentEpoch();
141-
vm.warp(block.timestamp + uint256(_epochsToJump) * rollup.getEpochDuration() * rollup.getSlotDuration());
141+
// Jump 8 epochs into the future to ensure that it haven't been setup.
142+
vm.warp(block.timestamp + 8 * rollup.getEpochDuration() * rollup.getSlotDuration());
142143

143144
Epoch post = rollup.getCurrentEpoch();
144145

@@ -151,8 +152,8 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase {
151152
assertEq(preCommittee.length, expectedSize, "Invalid committee size");
152153
assertEq(postCommittee.length, expectedSize, "Invalid committee size");
153154

154-
// Elements in the committee should be the same
155-
assertEq(preCommittee, postCommittee, "Committee elements have changed");
155+
// Elements in the committee should **not** be the same, as the epoch is mixed into the seed
156+
assertNotEq(preCommittee, postCommittee, "Committee elements have not changed");
156157
}
157158

158159
function testStableCommittee(uint8 _timeToJump) public setup(4, 4) progressEpochs(2) {

0 commit comments

Comments
 (0)