Skip to content

Commit f40f1cc

Browse files
authored
fix: align timestamps for sample values (#16849)
Please read [contributing guidelines](CONTRIBUTING.md) and remove this line. For audit-related pull requests, please use the [audit PR template](?expand=1&template=audit.md).
2 parents d77a75b + a29ccaa commit f40f1cc

File tree

25 files changed

+180
-56
lines changed

25 files changed

+180
-56
lines changed

l1-contracts/src/core/Rollup.sol

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,14 @@ contract Rollup is IStaking, IValidatorSelection, IRollup, RollupCore {
387387
return ValidatorOperationsExtLib.getSampleSeedAt(getEpochAt(_ts));
388388
}
389389

390+
function getSamplingSizeAt(Timestamp _ts) external view override(IValidatorSelection) returns (uint256) {
391+
return ValidatorOperationsExtLib.getSamplingSizeAt(getEpochAt(_ts));
392+
}
393+
394+
function getLagInEpochs() external view override(IValidatorSelection) returns (uint256) {
395+
return ValidatorOperationsExtLib.getLagInEpochs();
396+
}
397+
390398
/**
391399
* @notice Get the sample seed for the current epoch
392400
*

l1-contracts/src/core/RollupCore.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ contract RollupCore is EIP712("Aztec Rollup", "1"), Ownable, IStakingCore, IVali
272272
StakingLib.initialize(
273273
_stakingAsset, _gse, exitDelay, address(slasher), _config.stakingQueueConfig, _config.localEjectionThreshold
274274
);
275-
ValidatorOperationsExtLib.initializeValidatorSelection(_config.targetCommitteeSize);
275+
ValidatorOperationsExtLib.initializeValidatorSelection(_config.targetCommitteeSize, _config.lagInEpochs);
276276

277277
// If no booster is specifically provided, deploy one.
278278
if (address(_config.rewardConfig.booster) == address(0)) {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ struct RollupConfigInput {
5757
uint256 aztecSlotDuration;
5858
uint256 aztecEpochDuration;
5959
uint256 targetCommitteeSize;
60+
uint256 lagInEpochs;
6061
uint256 aztecProofSubmissionEpochs;
6162
uint256 slashingQuorum;
6263
uint256 slashingRoundSize;

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ struct ValidatorSelectionStorage {
1111
mapping(Epoch => bytes32 committeeCommitment) committeeCommitments;
1212
// Checkpointed map of epoch -> randao value
1313
Checkpoints.Trace224 randaos;
14-
uint256 targetCommitteeSize;
14+
uint32 targetCommitteeSize;
15+
uint32 lagInEpochs;
1516
}
1617

1718
interface IValidatorSelectionCore {
@@ -36,6 +37,8 @@ interface IValidatorSelection is IValidatorSelectionCore, IEmperor {
3637
function getTimestampForSlot(Slot _slotNumber) external view returns (Timestamp);
3738

3839
function getSampleSeedAt(Timestamp _ts) external view returns (uint256);
40+
function getSamplingSizeAt(Timestamp _ts) external view returns (uint256);
41+
function getLagInEpochs() external view returns (uint256);
3942
function getCurrentSampleSeed() external view returns (uint256);
4043

4144
function getEpochAt(Timestamp _ts) external view returns (Epoch);

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ library ValidatorOperationsExtLib {
6666
StakingLib.finalizeWithdraw(_attester);
6767
}
6868

69-
function initializeValidatorSelection(uint256 _targetCommitteeSize) external {
70-
ValidatorSelectionLib.initialize(_targetCommitteeSize);
69+
function initializeValidatorSelection(uint256 _targetCommitteeSize, uint256 _lagInEpochs) external {
70+
ValidatorSelectionLib.initialize(_targetCommitteeSize, _lagInEpochs);
7171
}
7272

7373
function setupEpoch() external {
@@ -125,6 +125,14 @@ library ValidatorOperationsExtLib {
125125
return ValidatorSelectionLib.getSampleSeed(_epoch);
126126
}
127127

128+
function getSamplingSizeAt(Epoch _epoch) external view returns (uint256) {
129+
return ValidatorSelectionLib.getSamplingSize(_epoch);
130+
}
131+
132+
function getLagInEpochs() external view returns (uint256) {
133+
return ValidatorSelectionLib.getLagInEpochs();
134+
}
135+
128136
function getTargetCommitteeSize() external view returns (uint256) {
129137
return ValidatorSelectionLib.getStorage().targetCommitteeSize;
130138
}

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

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,12 @@ library ValidatorSelectionLib {
130130
* The first two epochs use maximum seed values for startup.
131131
* @param _targetCommitteeSize The desired number of validators in each epoch's committee
132132
*/
133-
function initialize(uint256 _targetCommitteeSize) internal {
133+
function initialize(uint256 _targetCommitteeSize, uint256 _lagInEpochs) internal {
134134
ValidatorSelectionStorage storage store = getStorage();
135-
store.targetCommitteeSize = _targetCommitteeSize;
135+
store.targetCommitteeSize = _targetCommitteeSize.toUint32();
136+
store.lagInEpochs = _lagInEpochs.toUint32();
136137

137-
// Set the initial randao
138-
store.randaos.push(0, uint224(block.prevrandao));
138+
checkpointRandao(Epoch.wrap(0));
139139
}
140140

141141
/**
@@ -453,33 +453,22 @@ library ValidatorSelectionLib {
453453

454454
/**
455455
* @notice Checkpoints randao value for future usage
456-
* @dev Checks if already stored before computing and storing the randao value.
457-
* Offset the epoch by 2 to maintain the two-epoch advance requirement.
458-
* This ensures randomness are set well in advance to prevent manipulation.
459-
* @param _epoch The current epoch (randao will be set for _epoch + 2)
460-
* Passed to reduce recomputation
456+
* @dev Checks if already stored before storing the randao value.
457+
* @param _epoch The current epoch
461458
*/
462459
function checkpointRandao(Epoch _epoch) internal {
463460
ValidatorSelectionStorage storage store = getStorage();
464461

465-
// Compute the offset
466-
uint32 epoch = Epoch.unwrap(_epoch).toUint32() + 2;
467-
468462
// Check if the latest checkpoint is for the next epoch
469463
// It should be impossible that zero epoch snapshots exist, as in the genesis state we push the first values
470464
// into the store
471-
(, uint32 mostRecentEpoch,) = store.randaos.latestCheckpoint();
472-
473-
// If the randao for the next epoch is already set, we can skip the computation
474-
if (mostRecentEpoch == epoch) {
475-
return;
476-
}
465+
(, uint32 mostRecentTs,) = store.randaos.latestCheckpoint();
466+
uint32 ts = Timestamp.unwrap(_epoch.toTimestamp()).toUint32();
477467

478468
// If the most recently stored epoch is less than the epoch we are querying, then we need to store randao for
479-
// later use
480-
if (mostRecentEpoch < epoch) {
481-
// Truncate the randao to be used for future sampling.
482-
store.randaos.push(epoch, uint224(block.prevrandao));
469+
// later use. We truncate to save storage costs.
470+
if (mostRecentTs < ts) {
471+
store.randaos.push(ts, uint224(block.prevrandao));
483472
}
484473
}
485474

@@ -539,22 +528,16 @@ library ValidatorSelectionLib {
539528
* @notice Converts an epoch number to the timestamp used for validator set sampling
540529
* @dev Calculates the sampling timestamp by:
541530
* 1. Taking the epoch start timestamp
542-
* 2. Subtracting one full epoch duration to ensure stability
543-
* 3. Subtracting 1 second to get end-of-block state
531+
* 2. Subtracting `lagInEpochs` full epoch duration to ensure stability
544532
*
545533
* This ensures validator set sampling uses stable historical data that won't be
546534
* affected by last-minute changes or L1 reorgs during synchronization.
547535
* @param _epoch The epoch to calculate sampling time for
548536
* @return The Unix timestamp (uint32) to use for validator set sampling
549537
*/
550538
function epochToSampleTime(Epoch _epoch) internal view returns (uint32) {
551-
// We do -1, as the snapshots practically happen at the end of the block, e.g.,
552-
// a tx manipulating the set in at $t$ would be visible already at lookup $t$ if after that
553-
// transactions. But reading at $t-1$ would be the state at the end of $t-1$ which is the state
554-
// as we "start" time $t$. We then shift that back by an entire L2 epoch to guarantee
555-
// we are not hit by last-minute changes or L1 reorgs when syncing validators from our clients.
556-
557-
return Timestamp.unwrap(_epoch.toTimestamp()).toUint32() - uint32(TimeLib.getEpochDurationInSeconds()) - 1;
539+
uint32 sub = getStorage().lagInEpochs * TimeLib.getEpochDurationInSeconds().toUint32();
540+
return Timestamp.unwrap(_epoch.toTimestamp()).toUint32() - sub;
558541
}
559542

560543
/**
@@ -566,7 +549,17 @@ library ValidatorSelectionLib {
566549
*/
567550
function getSampleSeed(Epoch _epoch) internal view returns (uint256) {
568551
ValidatorSelectionStorage storage store = getStorage();
569-
return uint256(keccak256(abi.encode(_epoch, store.randaos.upperLookup(Epoch.unwrap(_epoch).toUint32()))));
552+
uint32 ts = epochToSampleTime(_epoch);
553+
return uint256(keccak256(abi.encode(_epoch, store.randaos.upperLookup(ts))));
554+
}
555+
556+
function getSamplingSize(Epoch _epoch) internal view returns (uint256) {
557+
uint32 ts = epochToSampleTime(_epoch);
558+
return StakingLib.getAttesterCountAtTime(Timestamp.wrap(ts));
559+
}
560+
561+
function getLagInEpochs() internal view returns (uint256) {
562+
return getStorage().lagInEpochs;
570563
}
571564

572565
/**

l1-contracts/test/harnesses/TestConstants.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ library TestConstants {
1818
uint256 internal constant AZTEC_SLOT_DURATION = 36;
1919
uint256 internal constant AZTEC_EPOCH_DURATION = 32;
2020
uint256 internal constant AZTEC_TARGET_COMMITTEE_SIZE = 48;
21+
uint256 internal constant AZTEC_LAG_IN_EPOCHS = 2;
2122
uint256 internal constant AZTEC_PROOF_SUBMISSION_EPOCHS = 1;
2223
uint256 internal constant AZTEC_SLASHING_QUORUM = 6;
2324
uint256 internal constant AZTEC_SLASHING_ROUND_SIZE = 10;
@@ -95,6 +96,7 @@ library TestConstants {
9596
aztecEpochDuration: AZTEC_EPOCH_DURATION,
9697
aztecProofSubmissionEpochs: AZTEC_PROOF_SUBMISSION_EPOCHS,
9798
targetCommitteeSize: AZTEC_TARGET_COMMITTEE_SIZE,
99+
lagInEpochs: AZTEC_LAG_IN_EPOCHS,
98100
slashingQuorum: AZTEC_SLASHING_QUORUM,
99101
slashingRoundSize: AZTEC_SLASHING_ROUND_SIZE,
100102
slashingLifetimeInRounds: AZTEC_SLASHING_LIFETIME_IN_ROUNDS,

l1-contracts/test/staking/TimeCheater.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ contract TimeCheater {
4242
return Epoch.wrap(currentSlot / epochDuration);
4343
}
4444

45+
function getCurrentSlot() public view returns (uint256) {
46+
return currentSlot;
47+
}
48+
4549
function cheat__setTimeStorage(TimeStorage memory _timeStorage) public {
4650
vm.store(
4751
target,
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright 2025 Aztec Labs.
3+
pragma solidity >=0.8.27;
4+
5+
import {ValidatorSelectionTestBase, CheatDepositArgs} from "./ValidatorSelectionBase.sol";
6+
import {Epoch, Timestamp, TimeLib} from "@aztec/core/libraries/TimeLib.sol";
7+
import {Checkpoints} from "@oz/utils/structs/Checkpoints.sol";
8+
import {IValidatorSelection} from "@aztec/core/interfaces/IValidatorSelection.sol";
9+
import {TestConstants} from "../harnesses/TestConstants.sol";
10+
import {BN254Lib, G1Point, G2Point} from "@aztec/shared/libraries/BN254Lib.sol";
11+
import {GSE} from "@aztec/governance/GSE.sol";
12+
13+
contract SeedAndSizeSnapshotsTest is ValidatorSelectionTestBase {
14+
using TimeLib for Timestamp;
15+
using TimeLib for Epoch;
16+
17+
uint256 internal $currentsize = 4;
18+
mapping(uint256 slot => uint256 size) internal $sizes;
19+
mapping(uint256 slot => uint256 randao) internal $randaos;
20+
21+
function test_seedAndSizeSnapshots() public setup(4, 4) {
22+
// We set up the initial
23+
$sizes[timeCheater.getCurrentSlot()] = $currentsize;
24+
$randaos[timeCheater.getCurrentSlot()] = block.prevrandao;
25+
26+
uint256 endEpoch = Epoch.unwrap(timeCheater.getCurrentEpoch()) + 10;
27+
28+
GSE gse = rollup.getGSE();
29+
30+
vm.prank(address(testERC20.owner()));
31+
testERC20.mint(address(rollup), type(uint128).max);
32+
vm.prank(address(rollup));
33+
testERC20.approve(address(gse), type(uint128).max);
34+
35+
while (Epoch.unwrap(timeCheater.getCurrentEpoch()) < endEpoch) {
36+
timeCheater.cheat__progressSlot();
37+
38+
uint256 nextRandao = uint256(keccak256(abi.encode(block.prevrandao)));
39+
vm.prevrandao(nextRandao);
40+
41+
rollup.checkpointRandao();
42+
43+
vm.prank(address(rollup));
44+
gse.deposit(
45+
address(uint160(nextRandao)),
46+
address(uint160(nextRandao)),
47+
BN254Lib.g1Zero(),
48+
BN254Lib.g2Zero(),
49+
BN254Lib.g1Zero(),
50+
true
51+
);
52+
53+
$currentsize += 1;
54+
$sizes[timeCheater.getCurrentSlot()] = $currentsize;
55+
$randaos[timeCheater.getCurrentSlot()] = nextRandao;
56+
57+
// We will add one node to the GSE for rollup (impersonate rollup to avoid the queue).
58+
// We want to see that the lag between current values are the same between randaos and size values
59+
60+
uint256 epochIndex = Epoch.unwrap(timeCheater.getCurrentEpoch());
61+
if (epochIndex >= 2) {
62+
(uint256 seed, uint256 size) = getValues();
63+
uint256 slot = (epochIndex - 2) * timeCheater.epochDuration();
64+
65+
assertEq(size, $sizes[slot], "invalid size");
66+
assertEq(seed, uint256(keccak256(abi.encode(epochIndex, uint224($randaos[slot])))), "invalid seed");
67+
}
68+
}
69+
}
70+
71+
function getValues() internal view returns (uint256 sampleSeed, uint256 size) {
72+
// We are always using for an epoch, so we are going to do that here as well
73+
Timestamp ts = Timestamp.wrap(block.timestamp);
74+
75+
sampleSeed = rollup.getSampleSeedAt(ts);
76+
size = rollup.getSamplingSizeAt(ts);
77+
}
78+
}

spartan/environments/scenario.local.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ AZTEC_EJECTION_THRESHOLD=50000000000000000000
1818
AZTEC_SLASH_AMOUNT_SMALL=5000000000000000000
1919
AZTEC_SLASH_AMOUNT_MEDIUM=10000000000000000000
2020
AZTEC_SLASH_AMOUNT_LARGE=15000000000000000000
21+
AZTEC_LAG_IN_EPOCHS=0
2122

2223
# The following need to be set manually
2324
# AZTEC_DOCKER_IMAGE=aztecprotocol/aztec:whatever

0 commit comments

Comments
 (0)