Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
1 change: 0 additions & 1 deletion l1-contracts/src/core/RollupCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ contract RollupCore is
onlyOwner
{
CheatLib.cheat__InitialiseValidatorSet(_args);
setupEpoch();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one was leftover from last? 👀

}

function setEpochVerifier(address _verifier) external override(ITestRollup) onlyOwner {
Expand Down
9 changes: 4 additions & 5 deletions l1-contracts/src/core/interfaces/IValidatorSelection.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,24 @@
pragma solidity >=0.8.27;

import {Timestamp, Slot, Epoch} from "@aztec/core/libraries/TimeLib.sol";

import {Checkpoints} from "@oz/utils/structs/Checkpoints.sol";
/**
* @notice The data structure for an epoch
* @param committee - The attesters for the epoch
* @param sampleSeed - The seed used to sample the attesters of the epoch
* @param nextSeed - The seed used to influence the NEXT epoch
*/

struct EpochData {
// TODO: remove in favor of commitment to comittee
address[] committee;
uint256 sampleSeed;
uint256 nextSeed;
}

struct ValidatorSelectionStorage {
// A mapping to snapshots of the validator set
mapping(Epoch => EpochData) epochs;
// The last stored randao value, same value as `seed` in the last inserted epoch
uint256 lastSeed;
// Checkpointed map of epoch -> sample seed
Checkpoints.Trace224 seeds;
uint256 targetCommitteeSize;
}

Expand Down
8 changes: 6 additions & 2 deletions l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
pragma solidity >=0.8.27;

import {SubmitEpochRootProofArgs, PublicInputArgs} from "@aztec/core/interfaces/IRollup.sol";
import {Epoch, Timestamp, TimeLib} from "@aztec/core/libraries/TimeLib.sol";
import {StakingLib} from "./../staking/StakingLib.sol";
import {ValidatorSelectionLib} from "./../validator-selection/ValidatorSelectionLib.sol";
import {BlobLib} from "./BlobLib.sol";
import {EpochProofLib} from "./EpochProofLib.sol";
import {ProposeLib, ProposeArgs, Signature} from "./ProposeLib.sol";

// We are using this library such that we can more easily "link" just a larger external library
// instead of a few smaller ones.

library ExtRollupLib {
using TimeLib for Timestamp;

function submitEpochRootProof(SubmitEpochRootProofArgs calldata _args) external {
EpochProofLib.submitEpochRootProof(_args);
}
Expand All @@ -30,7 +33,8 @@ library ExtRollupLib {
}

function setupEpoch() external {
ValidatorSelectionLib.setupEpoch(StakingLib.getStorage());
Epoch currentEpoch = Timestamp.wrap(block.timestamp).epochFromTimestamp();
ValidatorSelectionLib.setupEpoch(StakingLib.getStorage(), currentEpoch);
}

function getEpochProofPublicInputs(
Expand Down
4 changes: 2 additions & 2 deletions l1-contracts/src/core/libraries/rollup/ProposeLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,10 @@ library ProposeLib {
BlobLib.validateBlobs(_blobInput, _checkBlob);

Header memory header = HeaderLib.decode(_args.header);

v.headerHash = HeaderLib.hash(_args.header);

ValidatorSelectionLib.setupEpoch(StakingLib.getStorage());
Epoch currentEpoch = Timestamp.wrap(block.timestamp).epochFromTimestamp();
ValidatorSelectionLib.setupEpoch(StakingLib.getStorage(), currentEpoch);

ManaBaseFeeComponents memory components =
getManaBaseFeeComponentsAt(Timestamp.wrap(block.timestamp), true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
} from "@aztec/core/libraries/staking/AddressSnapshotLib.sol";
import {Timestamp, Slot, Epoch, TimeLib} from "@aztec/core/libraries/TimeLib.sol";
import {MessageHashUtils} from "@oz/utils/cryptography/MessageHashUtils.sol";
import {SafeCast} from "@oz/utils/math/SafeCast.sol";
import {Checkpoints} from "@oz/utils/structs/Checkpoints.sol";
import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol";

library ValidatorSelectionLib {
Expand All @@ -24,6 +26,8 @@ library ValidatorSelectionLib {
using SignatureLib for Signature;
using TimeLib for Timestamp;
using AddressSnapshotLib for SnapshottedAddressSet;
using Checkpoints for Checkpoints.Trace224;
using SafeCast for *;

bytes32 private constant VALIDATOR_SELECTION_STORAGE_POSITION =
keccak256("aztec.validator_selection.storage");
Expand All @@ -36,23 +40,30 @@ library ValidatorSelectionLib {
/**
* @notice Performs a setup of an epoch if needed. The setup will
* - Sample the validator set for the epoch
* - Set the seed for the epoch
* - Update the last seed
*
* @dev Since this is a reference optimising for simplicity, we store the actual validator set in the epoch structure.
* This is very heavy on gas, so start crying because the gas here will melt the poles
* https://i.giphy.com/U1aN4HTfJ2SmgB2BBK.webp
* - Set the seed for the next epoch
*/
function setupEpoch(StakingStorage storage _stakingStore) internal {
Epoch epochNumber = Timestamp.wrap(block.timestamp).epochFromTimestamp();
function setupEpoch(StakingStorage storage _stakingStore, Epoch _epochNumber) internal {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note for my sanity, we use the case where _epochNumber != currentEpoch when we mught wanna look at a historical and not setup epoch as the sampleValidators are using it.

ValidatorSelectionStorage storage store = getStorage();
EpochData storage epoch = store.epochs[epochNumber];

if (epoch.sampleSeed == 0) {
epoch.sampleSeed = getSampleSeed(epochNumber);
epoch.nextSeed = store.lastSeed = computeNextSeed(epochNumber);
epoch.committee = sampleValidators(_stakingStore, epochNumber, epoch.sampleSeed);
// Get the sample seed for this current epoch.
uint224 sampleSeed = getSampleSeed(_epochNumber);

// If no sample seed is set, we are in a genesis state, we set the sample seed to max and push it into the store
if (sampleSeed == 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just set it up at genesis instead? As part of the construction? I might be forgetting something, but would that not at least get rid of this special case in the function that we will continously call? 🤷

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, lets

sampleSeed = type(uint224).max;
store.seeds.push(Epoch.unwrap(_epochNumber).toUint32(), sampleSeed);
}

// If the committee is not set for this epoch, we need to sample it
EpochData storage epoch = store.epochs[_epochNumber];
uint256 committeeLength = epoch.committee.length;
if (committeeLength == 0) {
epoch.committee = sampleValidators(_stakingStore, _epochNumber, sampleSeed);
}

// Set the sample seed for the next epoch if required
// function handles the case where it is already set
setSampleSeedForEpoch(_epochNumber + Epoch.wrap(1));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be slightly nicer separation if this is moved upwards, e.g., then you have things altering seeds and then things using seeds. Since it should alter in the future don't seems like it should cause an issue.

}

/**
Expand Down Expand Up @@ -159,7 +170,7 @@ library ValidatorSelectionLib {
*
* @return The validators for the given epoch
*/
function sampleValidators(StakingStorage storage _stakingStore, Epoch _epoch, uint256 _seed)
function sampleValidators(StakingStorage storage _stakingStore, Epoch _epoch, uint224 _seed)
internal
returns (address[] memory)
{
Expand Down Expand Up @@ -194,23 +205,35 @@ library ValidatorSelectionLib {
ValidatorSelectionStorage storage store = getStorage();
EpochData storage epoch = store.epochs[_epochNumber];

if (epoch.sampleSeed != 0) {
uint256 committeeSize = epoch.committee.length;
if (committeeSize == 0) {
return new address[](0);
}
return epoch.committee;
// If no committee has been stored, then we need to setup the epoch
uint256 committeeSize = epoch.committee.length;
if (committeeSize == 0) {
// This will set epoch.committee and the next sample seed in the store, meaning epoch.commitee on the line below will be set (storage reference)
setupEpoch(_stakingStore, _epochNumber);
}
return epoch.committee;
}

// Allow anyone if there is no validator set
if (_stakingStore.attesters.length() == 0) {
return new address[](0);
}
function setSampleSeedForEpoch(Epoch _epoch) internal {
ValidatorSelectionStorage storage store = getStorage();
uint32 epoch = Epoch.unwrap(_epoch).toUint32();

// Emulate a sampling of the validators
uint256 sampleSeed = getSampleSeed(_epochNumber);
// Check if the latest checkpoint is for the next epoch

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we set the initial using the constructor as well it might simplify us slightly here as well as we won't have the case where the last checkpoint does not exists so one less branch to deal with.

(bool exists, uint32 key,) = store.seeds.latestCheckpoint();

return sampleValidators(_stakingStore, _epochNumber, sampleSeed);
// If the sample seed for the next epoch is already set, we can skip the computation
// It should be impossible that zero epoch snapshots exist, as in the genesis state we push the first sample seed into the store
uint32 mostRecentSeedEpoch = exists ? key : 0;
if (mostRecentSeedEpoch == epoch) {
return;
}

// If the most recently stored seed is less than the epoch we are querying, then we need to compute it's seed for later use
if (mostRecentSeedEpoch < epoch) {
// Compute the sample seed for the next epoch
uint224 nextSeed = computeNextSeed(_epoch);
store.seeds.push(epoch, nextSeed);
}
}

/**
Expand All @@ -227,22 +250,13 @@ library ValidatorSelectionLib {
*
* @return The sample seed for the epoch
*/
function getSampleSeed(Epoch _epoch) internal view returns (uint256) {
if (Epoch.unwrap(_epoch) == 0) {
return type(uint256).max;
}
function getSampleSeed(Epoch _epoch) internal view returns (uint224) {
ValidatorSelectionStorage storage store = getStorage();
uint256 sampleSeed = store.epochs[_epoch].sampleSeed;
if (sampleSeed != 0) {
return sampleSeed;
}

sampleSeed = store.epochs[_epoch - Epoch.wrap(1)].nextSeed;
if (sampleSeed != 0) {
return sampleSeed;
uint224 sampleSeed = store.seeds.upperLookup(Epoch.unwrap(_epoch).toUint32());
if (sampleSeed == 0) {
sampleSeed = type(uint224).max;
}

return store.lastSeed;
return sampleSeed;
}

function getStorage() internal pure returns (ValidatorSelectionStorage storage storageStruct) {
Expand All @@ -262,8 +276,9 @@ library ValidatorSelectionLib {
*
* @return The computed seed
*/
function computeNextSeed(Epoch _epoch) private view returns (uint256) {
return uint256(keccak256(abi.encode(_epoch, block.prevrandao)));
function computeNextSeed(Epoch _epoch) private view returns (uint224) {
// Allow for unsafe (lossy) downcast as we do not care if we loose bits
return uint224(uint256(keccak256(abi.encode(_epoch, block.prevrandao))));
}

/**
Expand Down
Loading