-
Notifications
You must be signed in to change notification settings - Fork 615
feat(rollup): seed snapshots #13577
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(rollup): seed snapshots #13577
Changes from 10 commits
53abe87
5a6fe8a
6bbadd4
9724ef2
b212389
de29295
40967d9
4321429
1ecec4f
08d1132
091c43e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
|
@@ -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"); | ||
|
|
@@ -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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a note for my sanity, we use the case where |
||
| 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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? 🤷
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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) | ||
| { | ||
|
|
@@ -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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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) { | ||
|
|
@@ -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)))); | ||
| } | ||
|
|
||
| /** | ||
|
|
||
There was a problem hiding this comment.
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? 👀