From 53abe8735720593c86b5f3d9b07d81bdbae01584 Mon Sep 17 00:00:00 2001 From: Maddiaa0 <47148561+Maddiaa0@users.noreply.github.com> Date: Tue, 15 Apr 2025 15:07:15 +0000 Subject: [PATCH 1/9] feat(rollup): seed snapshots --- .../core/interfaces/IValidatorSelection.sol | 8 +- .../core/libraries/rollup/ExtRollupLib.sol | 7 +- .../src/core/libraries/rollup/ProposeLib.sol | 3 +- .../ValidatorSelectionLib.sol | 104 ++++++++++-------- .../ValidatorSelection.t.sol | 11 +- 5 files changed, 80 insertions(+), 53 deletions(-) diff --git a/l1-contracts/src/core/interfaces/IValidatorSelection.sol b/l1-contracts/src/core/interfaces/IValidatorSelection.sol index 51825e4db508..5494273ed41c 100644 --- a/l1-contracts/src/core/interfaces/IValidatorSelection.sol +++ b/l1-contracts/src/core/interfaces/IValidatorSelection.sol @@ -3,7 +3,7 @@ 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 @@ -13,15 +13,13 @@ import {Timestamp, Slot, Epoch} from "@aztec/core/libraries/TimeLib.sol"; 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; } diff --git a/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol b/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol index 8a0ffe4e26d9..2653c28bcbb7 100644 --- a/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol @@ -8,10 +8,12 @@ import {ValidatorSelectionLib} from "./../validator-selection/ValidatorSelection import {BlobLib} from "./BlobLib.sol"; import {EpochProofLib} from "./EpochProofLib.sol"; import {ProposeLib, ProposeArgs, Signature} from "./ProposeLib.sol"; - +import {Epoch, Timestamp, TimeLib} from "@aztec/core/libraries/TimeLib.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); } @@ -30,7 +32,8 @@ library ExtRollupLib { } function setupEpoch() external { - ValidatorSelectionLib.setupEpoch(StakingLib.getStorage()); + Epoch currentEpoch = Timestamp.wrap(block.timestamp).epochFromTimestamp(); + ValidatorSelectionLib.setupEpoch(StakingLib.getStorage(), currentEpoch); } function getEpochProofPublicInputs( diff --git a/l1-contracts/src/core/libraries/rollup/ProposeLib.sol b/l1-contracts/src/core/libraries/rollup/ProposeLib.sol index 2ad05a1a704e..cb885be1bee9 100644 --- a/l1-contracts/src/core/libraries/rollup/ProposeLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ProposeLib.sol @@ -86,7 +86,8 @@ library ProposeLib { Header memory header = HeaderLib.decode(_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); diff --git a/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol b/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol index b58395276d9f..51c159509372 100644 --- a/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol +++ b/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol @@ -18,12 +18,17 @@ import {Timestamp, Slot, Epoch, TimeLib} from "@aztec/core/libraries/TimeLib.sol import {MessageHashUtils} from "@oz/utils/cryptography/MessageHashUtils.sol"; import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; +import {Checkpoints} from "@oz/utils/structs/Checkpoints.sol"; +import {SafeCast} from "@oz/utils/math/SafeCast.sol"; + library ValidatorSelectionLib { using EnumerableSet for EnumerableSet.AddressSet; using MessageHashUtils for bytes32; 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 +41,34 @@ 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 { ValidatorSelectionStorage storage store = getStorage(); - EpochData storage epoch = store.epochs[epochNumber]; + EpochData storage epoch = store.epochs[_epochNumber]; + + // Get the sample seed for this current epoch. + uint224 sampleSeed = getSampleSeed(_epochNumber); + + // If no sample seed is set, we are are in epoch 0, and we need to set the sample seed to max + if (sampleSeed == 0) { + sampleSeed = type(uint224).max; + store.seeds.push(Epoch.unwrap(_epochNumber).toUint32(), sampleSeed); + } - if (epoch.sampleSeed == 0) { - epoch.sampleSeed = getSampleSeed(epochNumber); - epoch.nextSeed = store.lastSeed = computeNextSeed(epochNumber); - epoch.committee = sampleValidators(_stakingStore, epochNumber, epoch.sampleSeed); + + // Set the sample seed for the next epoch if required + + // TODO(md): will this need to be max if nothing is found? + + // If the committee is not set for this epoch, we need to sample it + uint256 committeeLength = epoch.committee.length; + if (committeeLength == 0) { + epoch.committee = sampleValidators(_stakingStore, _epochNumber, sampleSeed); } + + // Compute the next seed, function handles the case where it is already set + setSampleSeedForEpoch(_epochNumber + Epoch.wrap(1)); } /** @@ -159,7 +175,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) { @@ -191,26 +207,18 @@ library ValidatorSelectionLib { internal returns (address[] memory) { + setupEpoch(_stakingStore, _epochNumber); + 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; - } - - // Allow anyone if there is no validator set - if (_stakingStore.attesters.length() == 0) { + // TODO(md): I imagine if the validator set for this epoch is empty, then we need to return an empty array + // otherwise we must calculate the epoch at this time + uint256 committeeSize = epoch.committee.length; + if (committeeSize == 0) { return new address[](0); } - - // Emulate a sampling of the validators - uint256 sampleSeed = getSampleSeed(_epochNumber); - - return sampleValidators(_stakingStore, _epochNumber, sampleSeed); + return epoch.committee; } /** @@ -227,22 +235,31 @@ 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; - } + return store.seeds.upperLookup(Epoch.unwrap(_epoch).toUint32()); + } + + function setSampleSeedForEpoch(Epoch _epoch) internal { + ValidatorSelectionStorage storage store = getStorage(); + uint32 epoch = Epoch.unwrap(_epoch).toUint32(); - sampleSeed = store.epochs[_epoch - Epoch.wrap(1)].nextSeed; - if (sampleSeed != 0) { - return sampleSeed; + // Check if the next epoch has a sample seed set + (bool exists, uint32 key, ) = store.seeds.latestCheckpoint(); + + // If the sample seed for the next epoch is already set, we can skip the computation + // TODO(md): if no checkpoint exists, then we are at the start, THIS should not happen???? think about this + uint32 mostRecentSeedEpoch = exists ? key : 0; + if (mostRecentSeedEpoch == epoch) { + return; } - return store.lastSeed; + // If the sample seed for the next epoch is not set, we need to compute it + if (mostRecentSeedEpoch < epoch) { + // Compute the sample seed for the next epoch + uint224 nextSeed = computeNextSeed(_epoch); + store.seeds.push(epoch, nextSeed); + } } function getStorage() internal pure returns (ValidatorSelectionStorage storage storageStruct) { @@ -262,8 +279,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)))); } /** diff --git a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol index 48e55f31fb5a..2352318710a9 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol @@ -189,8 +189,15 @@ contract ValidatorSelectionTest is DecoderBase { uint256 expectedSize = validatorSetSize > targetCommitteeSize ? targetCommitteeSize : validatorSetSize; - assertEq(rollup.getEpochCommittee(pre).length, expectedSize, "Invalid committee size"); - assertEq(rollup.getEpochCommittee(post).length, expectedSize, "Invalid committee size"); + address[] memory preCommittee = rollup.getEpochCommittee(pre); + address[] memory postCommittee = rollup.getEpochCommittee(post); + assertEq(preCommittee.length, expectedSize, "Invalid committee size"); + assertEq(postCommittee.length, expectedSize, "Invalid committee size"); + + // Elements in the committee should be the same + for (uint256 i = 0; i < expectedSize; i++) { + assertEq(preCommittee[i], postCommittee[i], "Committee element has changed"); + } } function testValidatorSetLargerThanCommittee(bool _insufficientSigs) public setup(100) { From 5a6fe8a3444c288c64cded55e14cf7978439c109 Mon Sep 17 00:00:00 2001 From: Maddiaa0 <47148561+Maddiaa0@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:07:04 +0000 Subject: [PATCH 2/9] chore: document --- .../ValidatorSelectionLib.sol | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol b/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol index 51c159509372..e64dac6e02ed 100644 --- a/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol +++ b/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol @@ -45,29 +45,25 @@ library ValidatorSelectionLib { */ function setupEpoch(StakingStorage storage _stakingStore, Epoch _epochNumber) internal { ValidatorSelectionStorage storage store = getStorage(); - EpochData storage epoch = store.epochs[_epochNumber]; // Get the sample seed for this current epoch. uint224 sampleSeed = getSampleSeed(_epochNumber); - // If no sample seed is set, we are are in epoch 0, and we need to set the sample seed to max + // 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) { sampleSeed = type(uint224).max; store.seeds.push(Epoch.unwrap(_epochNumber).toUint32(), sampleSeed); } - - // Set the sample seed for the next epoch if required - - // TODO(md): will this need to be max if nothing is found? - // 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); } - // Compute the next seed, function handles the case where it is already set + // Set the sample seed for the next epoch if required + // function handles the case where it is already set setSampleSeedForEpoch(_epochNumber + Epoch.wrap(1)); } @@ -207,16 +203,14 @@ library ValidatorSelectionLib { internal returns (address[] memory) { - setupEpoch(_stakingStore, _epochNumber); - ValidatorSelectionStorage storage store = getStorage(); EpochData storage epoch = store.epochs[_epochNumber]; - // TODO(md): I imagine if the validator set for this epoch is empty, then we need to return an empty array - // otherwise we must calculate the epoch at this time + // If no committee has been stored, then we need to setup the epoch uint256 committeeSize = epoch.committee.length; if (committeeSize == 0) { - return new address[](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; } @@ -244,17 +238,17 @@ library ValidatorSelectionLib { ValidatorSelectionStorage storage store = getStorage(); uint32 epoch = Epoch.unwrap(_epoch).toUint32(); - // Check if the next epoch has a sample seed set + // Check if the latest checkpoint is for the next epoch (bool exists, uint32 key, ) = store.seeds.latestCheckpoint(); // If the sample seed for the next epoch is already set, we can skip the computation - // TODO(md): if no checkpoint exists, then we are at the start, THIS should not happen???? think about this + // 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 sample seed for the next epoch is not set, we need to compute it + // 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); From 6bbadd4915c781bc39955e36cdc354d67086e9ec Mon Sep 17 00:00:00 2001 From: Maddiaa0 <47148561+Maddiaa0@users.noreply.github.com> Date: Tue, 15 Apr 2025 18:19:17 +0000 Subject: [PATCH 3/9] chore: add setupEpoch test suite --- l1-contracts/src/core/RollupCore.sol | 1 - .../ValidatorSelection.t.sol | 117 ++--------- .../ValidatorSelectionBase.sol | 134 +++++++++++++ .../test/validator-selection/setupEpoch.t.sol | 185 ++++++++++++++++++ .../test/validator-selection/setupEpoch.tree | 20 ++ 5 files changed, 350 insertions(+), 107 deletions(-) create mode 100644 l1-contracts/test/validator-selection/ValidatorSelectionBase.sol create mode 100644 l1-contracts/test/validator-selection/setupEpoch.t.sol create mode 100644 l1-contracts/test/validator-selection/setupEpoch.tree diff --git a/l1-contracts/src/core/RollupCore.sol b/l1-contracts/src/core/RollupCore.sol index 20dc0daf00f7..c93756f1e618 100644 --- a/l1-contracts/src/core/RollupCore.sol +++ b/l1-contracts/src/core/RollupCore.sol @@ -120,7 +120,6 @@ contract RollupCore is onlyOwner { CheatLib.cheat__InitialiseValidatorSet(_args); - setupEpoch(); } function setEpochVerifier(address _verifier) external override(ITestRollup) onlyOwner { diff --git a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol index 2352318710a9..051d2b4b82ad 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol @@ -11,126 +11,31 @@ import {Signature} from "@aztec/core/libraries/crypto/SignatureLib.sol"; import {Inbox} from "@aztec/core/messagebridge/Inbox.sol"; import {Outbox} from "@aztec/core/messagebridge/Outbox.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; -import {Registry} from "@aztec/governance/Registry.sol"; import {Rollup} from "@aztec/core/Rollup.sol"; import {NaiveMerkle} from "../merkle/Naive.sol"; import {MerkleTestUtil} from "../merkle/TestUtil.sol"; import {TestERC20} from "@aztec/mock/TestERC20.sol"; import {MessageHashUtils} from "@oz/utils/cryptography/MessageHashUtils.sol"; -import {MockFeeJuicePortal} from "@aztec/mock/MockFeeJuicePortal.sol"; import {ProposeArgs, OracleInput, ProposeLib} from "@aztec/core/libraries/rollup/ProposeLib.sol"; import {TestConstants} from "../harnesses/TestConstants.sol"; -import {CheatDepositArgs} from "@aztec/core/interfaces/IRollup.sol"; -import {Slot, Epoch, EpochLib, Timestamp} from "@aztec/core/libraries/TimeLib.sol"; -import {RewardDistributor} from "@aztec/governance/RewardDistributor.sol"; +import {Timestamp, EpochLib, Epoch} from "@aztec/core/libraries/TimeLib.sol"; import {SlashFactory} from "@aztec/periphery/SlashFactory.sol"; import {Slasher, IPayload} from "@aztec/core/staking/Slasher.sol"; -import {IValidatorSelection} from "@aztec/core/interfaces/IValidatorSelection.sol"; import {Status, ValidatorInfo} from "@aztec/core/interfaces/IStaking.sol"; -import {TimeCheater} from "../staking/TimeCheater.sol"; +import {ValidatorSelectionTestBase} from "./ValidatorSelectionBase.sol"; // solhint-disable comprehensive-interface /** * We are using the same blocks as from Rollup.t.sol. * The tests in this file is testing the sequencer selection */ -contract ValidatorSelectionTest is DecoderBase { +contract ValidatorSelectionTest is ValidatorSelectionTestBase { using MessageHashUtils for bytes32; using EpochLib for Epoch; - struct StructToAvoidDeepStacks { - uint256 needed; - address proposer; - bool shouldRevert; - } - - SlashFactory internal slashFactory; - Slasher internal slasher; - Inbox internal inbox; - Outbox internal outbox; - Rollup internal rollup; - MerkleTestUtil internal merkleTestUtil; - TestERC20 internal testERC20; - RewardDistributor internal rewardDistributor; - Signature internal emptySignature; - TimeCheater internal timeCheater; - mapping(address attester => uint256 privateKey) internal attesterPrivateKeys; - mapping(address proposer => uint256 privateKey) internal proposerPrivateKeys; - mapping(address proposer => address attester) internal proposerToAttester; - mapping(address => bool) internal _seenValidators; - mapping(address => bool) internal _seenCommittee; - - /** - * @notice Set up the contracts needed for the tests with time aligned to the provided block name - */ - modifier setup(uint256 _validatorCount) { - string memory _name = "mixed_block_1"; - { - DecoderBase.Full memory full = load(_name); - uint256 slotNumber = full.block.decodedHeader.globalVariables.slotNumber; - uint256 initialTime = full.block.decodedHeader.globalVariables.timestamp - - slotNumber * TestConstants.AZTEC_SLOT_DURATION; - - timeCheater = new TimeCheater( - address(rollup), - initialTime, - TestConstants.AZTEC_SLOT_DURATION, - TestConstants.AZTEC_EPOCH_DURATION - ); - vm.warp(initialTime); - } - - CheatDepositArgs[] memory initialValidators = new CheatDepositArgs[](_validatorCount); - - for (uint256 i = 1; i < _validatorCount + 1; i++) { - uint256 attesterPrivateKey = uint256(keccak256(abi.encode("attester", i))); - address attester = vm.addr(attesterPrivateKey); - attesterPrivateKeys[attester] = attesterPrivateKey; - uint256 proposerPrivateKey = uint256(keccak256(abi.encode("proposer", i))); - address proposer = vm.addr(proposerPrivateKey); - proposerPrivateKeys[proposer] = proposerPrivateKey; - - proposerToAttester[proposer] = attester; - - initialValidators[i - 1] = CheatDepositArgs({ - attester: attester, - proposer: proposer, - withdrawer: address(this), - amount: TestConstants.AZTEC_MINIMUM_STAKE - }); - } - - testERC20 = new TestERC20("test", "TEST", address(this)); - Registry registry = new Registry(address(this), testERC20); - rewardDistributor = RewardDistributor(address(registry.getRewardDistributor())); - rollup = new Rollup({ - _feeAsset: testERC20, - _rewardDistributor: rewardDistributor, - _stakingAsset: testERC20, - _governance: address(this), - _genesisState: TestConstants.getGenesisState(), - _config: TestConstants.getRollupConfigInput() - }); - slasher = Slasher(rollup.getSlasher()); - slashFactory = new SlashFactory(IValidatorSelection(address(rollup))); - - testERC20.mint(address(this), TestConstants.AZTEC_MINIMUM_STAKE * _validatorCount); - testERC20.approve(address(rollup), TestConstants.AZTEC_MINIMUM_STAKE * _validatorCount); - rollup.cheat__InitialiseValidatorSet(initialValidators); - - inbox = Inbox(address(rollup.getInbox())); - outbox = Outbox(address(rollup.getOutbox())); - - merkleTestUtil = new MerkleTestUtil(); - - // Progress into the next epoch for changes to take effect - timeCheater.cheat__progressEpoch(); - _; - } - - function testInitialCommitteeMatch() public setup(4) { + function testInitialCommitteeMatch() public setup(4) progressEpoch { address[] memory attesters = rollup.getAttesters(); address[] memory committee = rollup.getCurrentEpochCommittee(); assertEq(rollup.getCurrentEpoch(), 1); @@ -153,7 +58,7 @@ contract ValidatorSelectionTest is DecoderBase { assertTrue(_seenCommittee[proposerToAttester[proposer]]); } - function testProposerForNonSetupEpoch(uint8 _epochsToJump) public setup(4) { + function testProposerForNonSetupEpoch(uint8 _epochsToJump) public setup(4) progressEpoch { Epoch pre = rollup.getCurrentEpoch(); vm.warp( block.timestamp @@ -175,7 +80,7 @@ contract ValidatorSelectionTest is DecoderBase { assertEq(expectedProposer, actualProposer, "Invalid proposer"); } - function testCommitteeForNonSetupEpoch(uint8 _epochsToJump) public setup(4) { + function testCommitteeForNonSetupEpoch(uint8 _epochsToJump) public setup(4) progressEpoch { Epoch pre = rollup.getCurrentEpoch(); vm.warp( block.timestamp @@ -200,7 +105,7 @@ contract ValidatorSelectionTest is DecoderBase { } } - function testValidatorSetLargerThanCommittee(bool _insufficientSigs) public setup(100) { + function testValidatorSetLargerThanCommittee(bool _insufficientSigs) public setup(100) progressEpoch { assertGt(rollup.getAttesters().length, rollup.getTargetCommitteeSize(), "Not enough validators"); uint256 committeeSize = rollup.getTargetCommitteeSize() * 2 / 3 + (_insufficientSigs ? 0 : 1); @@ -213,12 +118,12 @@ contract ValidatorSelectionTest is DecoderBase { ); } - function testHappyPath() public setup(4) { + function testHappyPath() public setup(4) progressEpoch { _testBlock("mixed_block_1", false, 3, false); _testBlock("mixed_block_2", false, 3, false); } - function testNukeFromOrbit() public setup(4) { + function testNukeFromOrbit() public setup(4) progressEpoch { // We propose some blocks, and have a bunch of validators attest to them. // Then we slash EVERYONE that was in the committees because the epoch never // got finalised. @@ -252,11 +157,11 @@ contract ValidatorSelectionTest is DecoderBase { } } - function testInvalidProposer() public setup(4) { + function testInvalidProposer() public setup(4) progressEpoch { _testBlock("mixed_block_1", true, 3, true); } - function testInsufficientSigs() public setup(4) { + function testInsufficientSigs() public setup(4) progressEpoch { _testBlock("mixed_block_1", true, 2, false); } diff --git a/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol b/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol new file mode 100644 index 000000000000..67e8b7d10ea8 --- /dev/null +++ b/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 Aztec Labs. +pragma solidity >=0.8.27; + +import {DecoderBase} from "../base/DecoderBase.sol"; + +import {Signature} from "@aztec/core/libraries/crypto/SignatureLib.sol"; + +import {Inbox} from "@aztec/core/messagebridge/Inbox.sol"; +import {Outbox} from "@aztec/core/messagebridge/Outbox.sol"; +import {Registry} from "@aztec/governance/Registry.sol"; +import {Rollup} from "@aztec/core/Rollup.sol"; +import {MerkleTestUtil} from "../merkle/TestUtil.sol"; +import {TestERC20} from "@aztec/mock/TestERC20.sol"; +import {MessageHashUtils} from "@oz/utils/cryptography/MessageHashUtils.sol"; +import {TestConstants} from "../harnesses/TestConstants.sol"; +import {CheatDepositArgs} from "@aztec/core/interfaces/IRollup.sol"; + +import {Epoch, EpochLib, Timestamp} from "@aztec/core/libraries/TimeLib.sol"; +import {RewardDistributor} from "@aztec/governance/RewardDistributor.sol"; +import {SlashFactory} from "@aztec/periphery/SlashFactory.sol"; +import {Slasher} from "@aztec/core/staking/Slasher.sol"; +import {IValidatorSelection} from "@aztec/core/interfaces/IValidatorSelection.sol"; + +import {TimeCheater} from "../staking/TimeCheater.sol"; +// solhint-disable comprehensive-interface + +/** + * We are using the same blocks as from Rollup.t.sol. + * The tests in this file is testing the sequencer selection + */ +contract ValidatorSelectionTestBase is DecoderBase { + using MessageHashUtils for bytes32; + using EpochLib for Epoch; + + struct StructToAvoidDeepStacks { + uint256 needed; + address proposer; + bool shouldRevert; + } + + SlashFactory internal slashFactory; + Slasher internal slasher; + Inbox internal inbox; + Outbox internal outbox; + Rollup internal rollup; + MerkleTestUtil internal merkleTestUtil; + TestERC20 internal testERC20; + RewardDistributor internal rewardDistributor; + Signature internal emptySignature; + TimeCheater internal timeCheater; + mapping(address attester => uint256 privateKey) internal attesterPrivateKeys; + mapping(address proposer => uint256 privateKey) internal proposerPrivateKeys; + mapping(address proposer => address attester) internal proposerToAttester; + mapping(address => bool) internal _seenValidators; + mapping(address => bool) internal _seenCommittee; + + /** + * @notice Set up the contracts needed for the tests with time aligned to the provided block name + */ + modifier setup(uint256 _validatorCount) { + string memory _name = "mixed_block_1"; + { + DecoderBase.Full memory full = load(_name); + uint256 slotNumber = full.block.decodedHeader.globalVariables.slotNumber; + uint256 initialTime = full.block.decodedHeader.globalVariables.timestamp + - slotNumber * TestConstants.AZTEC_SLOT_DURATION; + + timeCheater = new TimeCheater( + address(rollup), + initialTime, + TestConstants.AZTEC_SLOT_DURATION, + TestConstants.AZTEC_EPOCH_DURATION + ); + vm.warp(initialTime); + } + + CheatDepositArgs[] memory initialValidators = new CheatDepositArgs[](_validatorCount); + + for (uint256 i = 1; i < _validatorCount + 1; i++) { + initialValidators[i - 1] = createDepositArgs(i); + } + + testERC20 = new TestERC20("test", "TEST", address(this)); + Registry registry = new Registry(address(this), testERC20); + rewardDistributor = RewardDistributor(address(registry.getRewardDistributor())); + rollup = new Rollup({ + _feeAsset: testERC20, + _rewardDistributor: rewardDistributor, + _stakingAsset: testERC20, + _governance: address(this), + _genesisState: TestConstants.getGenesisState(), + _config: TestConstants.getRollupConfigInput() + }); + slasher = Slasher(rollup.getSlasher()); + slashFactory = new SlashFactory(IValidatorSelection(address(rollup))); + + testERC20.mint(address(this), TestConstants.AZTEC_MINIMUM_STAKE * _validatorCount); + testERC20.approve(address(rollup), TestConstants.AZTEC_MINIMUM_STAKE * _validatorCount); + + if (_validatorCount > 0) { + rollup.cheat__InitialiseValidatorSet(initialValidators); + } + + inbox = Inbox(address(rollup.getInbox())); + outbox = Outbox(address(rollup.getOutbox())); + + merkleTestUtil = new MerkleTestUtil(); + _; + } + + modifier progressEpoch() { + // Progress into the next epoch for changes to take effect + timeCheater.cheat__progressEpoch(); + _; + } + + function createDepositArgs(uint256 _keySalt) internal returns (CheatDepositArgs memory) { + uint256 attesterPrivateKey = uint256(keccak256(abi.encode("attester", _keySalt))); + address attester = vm.addr(attesterPrivateKey); + attesterPrivateKeys[attester] = attesterPrivateKey; + uint256 proposerPrivateKey = uint256(keccak256(abi.encode("proposer", _keySalt))); + address proposer = vm.addr(proposerPrivateKey); + proposerPrivateKeys[proposer] = proposerPrivateKey; + proposerToAttester[proposer] = attester; + + return CheatDepositArgs({ + attester: attester, + proposer: proposer, + withdrawer: address(this), + amount: TestConstants.AZTEC_MINIMUM_STAKE + }); + } +} diff --git a/l1-contracts/test/validator-selection/setupEpoch.t.sol b/l1-contracts/test/validator-selection/setupEpoch.t.sol new file mode 100644 index 000000000000..f6b76e636af1 --- /dev/null +++ b/l1-contracts/test/validator-selection/setupEpoch.t.sol @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 Aztec Labs. +pragma solidity >=0.8.27; + +import {ValidatorSelectionTestBase, CheatDepositArgs} from "./ValidatorSelectionBase.sol"; +import {Epoch, Timestamp} from "@aztec/core/libraries/TimeLib.sol"; +import {Checkpoints} from "@oz/utils/structs/Checkpoints.sol"; +import {IValidatorSelection} from "@aztec/core/interfaces/IValidatorSelection.sol"; +import {TestConstants} from "../harnesses/TestConstants.sol"; + +contract SetupEpochTest is ValidatorSelectionTestBase { + using Checkpoints for Checkpoints.Trace224; + + modifier whenTheRollupIsInGenesisState() { + // Enforced with setup(0) modifier + _; + } + + function test_GivenTheRollupIsInGenesisState() external setup(0) whenTheRollupIsInGenesisState { + // it should set the sample seed to max + // it should set the sample seed for the next epoch + // it should not change the current epoch + + rollup.setupEpoch(); + + // Read the sample seed for epoch 0 through the rollup contract + uint256 sampleSeed = IValidatorSelection(address(rollup)).getCurrentSampleSeed(); + assertEq(sampleSeed, type(uint224).max, "Sample seed should be max in genesis state"); + + // Read the sample seed for epoch 1 through the rollup contract + Timestamp nextEpochTimestamp = Timestamp.wrap(block.timestamp + TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION); + uint256 nextEpochSeed = IValidatorSelection(address(rollup)).getSampleSeedAt(nextEpochTimestamp); + assertTrue(nextEpochSeed != 0, "Next epoch seed should be set"); + assertTrue(nextEpochSeed != sampleSeed, "Next epoch seed should be different from current epoch seed"); + + // Advance into the next epoch + vm.warp(Timestamp.unwrap(nextEpochTimestamp)); + + // Read the sample seed for epoch 1 now that we are in epoch 1, it should not change + uint256 epoch1SampleSeedInPast = IValidatorSelection(address(rollup)).getSampleSeedAt(nextEpochTimestamp); + assertEq(epoch1SampleSeedInPast, nextEpochSeed, "Next epoch seed should be the same as the current epoch seed"); + + // Call setupEpoch again, from the current epoch it should not change the sample seed + rollup.setupEpoch(); + assertEq(IValidatorSelection(address(rollup)).getCurrentSampleSeed(), nextEpochSeed, "Sample seed should not change"); + + // Seed for the next epoch should be set + Timestamp nextEpochTimestamp2 = Timestamp.wrap(block.timestamp + TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION); + uint256 nextEpochSeedInPast = IValidatorSelection(address(rollup)).getSampleSeedAt(nextEpochTimestamp2); + assertTrue(nextEpochSeedInPast != nextEpochSeed, "Next epoch seed should not be the same as the current epoch seed"); + } + + function test_WhenTheRollupIsNotInEpoch0ButIsInGenesisState() external setup(0) whenTheRollupIsInGenesisState { + // it should set the sample seed to max + // it should set the sample seed for the next epoch + // it should have the seed for the initial epoch be 0 + // it should not change the current epoch + + uint256 initialTimestamp = block.timestamp; + + // Advance into the next epoch + uint256 nextEpochTimestamp = block.timestamp + TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION; + vm.warp(nextEpochTimestamp); + + // Call setupEpoch, sample seed for initial epoch should be uint224.max + rollup.setupEpoch(); + assertEq(IValidatorSelection(address(rollup)).getCurrentSampleSeed(), type(uint224).max, "Sample seed should be max"); + + // Get sample seed for initial epoch + uint256 initialEpochSeed = IValidatorSelection(address(rollup)).getSampleSeedAt(Timestamp.wrap(initialTimestamp)); + assertEq(initialEpochSeed, 0, "Sample seed for initial epoch should be 0"); + } + + modifier whenTheRollupIsNotInGenesisState() { + // Enforced with setup(4) modifier + _; + } + + function test_GivenTheSeedHasBeenSampled() external setup(4) whenTheRollupIsNotInGenesisState { + // it should not change the sample seed + // it should be calcaulte the same committee when looking into the past + // it should not change the commitee even when validators are added or removed + // it should not change the next seed + + rollup.setupEpoch(); + + // Check that the initial epoch seed is set + uint256 initialEpochSeed = IValidatorSelection(address(rollup)).getSampleSeedAt(Timestamp.wrap(block.timestamp)); + assertEq(initialEpochSeed, type(uint224).max, "Sample seed for initial epoch should be max"); + + // Get the initial committee + address[] memory initialCommittee = IValidatorSelection(address(rollup)).getCurrentEpochCommittee(); + + // When setup epoch is called, nothing should change + rollup.setupEpoch(); + uint256 initialEpochSeedAfterSetup = IValidatorSelection(address(rollup)).getSampleSeedAt(Timestamp.wrap(block.timestamp)); + assertEq(initialEpochSeedAfterSetup, initialEpochSeed, "Sample seed should not change"); + + // Check that the committee is the same + address[] memory committeeAfterRepeatedSetup = IValidatorSelection(address(rollup)).getCurrentEpochCommittee(); + assertEq(committeeAfterRepeatedSetup.length, initialCommittee.length, "Committee should have the same length"); + for (uint256 i = 0; i < initialCommittee.length; i++) { + assertEq(committeeAfterRepeatedSetup[i], initialCommittee[i], "Committee should have the same validators"); + } + + // Add a couple of extra validators during this epoch, the sampled validator set should not change + addNumberOfValidators(420420, 2); + + // Sample the validator set for the current epoch + address[] memory committeeAfterAddingExtraValidators = IValidatorSelection(address(rollup)).getCurrentEpochCommittee(); + for (uint256 i = 0; i < committeeAfterAddingExtraValidators.length; i++) { + assertEq(committeeAfterAddingExtraValidators[i], initialCommittee[i], "Committee should be the same"); + } + + // Jump into the future and check the committee still does not change + uint256 savedTimestamp = block.timestamp; + vm.warp(savedTimestamp + TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION); + + address[] memory committeeAfterJumpingIntoFuture = IValidatorSelection(address(rollup)).getCommitteeAt(Timestamp.wrap(savedTimestamp)); + for (uint256 i = 0; i < committeeAfterJumpingIntoFuture.length; i++) { + assertEq(committeeAfterJumpingIntoFuture[i], initialCommittee[i], "Committee should be the same"); + } + } + + modifier whenItHasBeenALongTimeSinceTheLastSampleSeedWasSet() { + // Enforce validator set has been changed with the setup(50) modifier + _; + } + + function test_WhenItHasBeenALongTimeSinceTheLastSampleSeedWasSet() + external + setup(50) + whenItHasBeenALongTimeSinceTheLastSampleSeedWasSet + { + // it should use the most recent sample seed + rollup.setupEpoch(); + + // Check that the sample seed has been set for the next epoch + uint256 nextEpochTimestamp = block.timestamp + TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION; + uint256 nextEpochSeed = IValidatorSelection(address(rollup)).getSampleSeedAt(Timestamp.wrap(nextEpochTimestamp)); + assertGt(nextEpochSeed, 0, "Sample seed should be set for the next epoch"); + + // Jump into the future, looking back, the returned sample seed should be the same for the next range of epochs + uint256 savedTimestamp = block.timestamp; + vm.warp(savedTimestamp + 2*(TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION)); + + uint256 sampleSeedAfterJump = IValidatorSelection(address(rollup)).getCurrentSampleSeed(); + assertEq(sampleSeedAfterJump, nextEpochSeed, "Sample seed should be the same"); + + // Add some validators + addNumberOfValidators(420422, 2); + + // Jump further into the future + vm.warp(savedTimestamp + 4*(TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION)); + + // Check that the sample seed has not changed + assertEq(IValidatorSelection(address(rollup)).getCurrentSampleSeed(), nextEpochSeed, "Sample seed should not change"); + + // Call setupEpoch, the sample seed should not change + rollup.setupEpoch(); + assertEq(IValidatorSelection(address(rollup)).getCurrentSampleSeed(), nextEpochSeed, "Sample seed should not change"); + + // The sample seed for the next epoch should have changed + uint256 nextEpochTimestamp2 = block.timestamp + TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION; + uint256 nextEpochSeed2 = IValidatorSelection(address(rollup)).getSampleSeedAt(Timestamp.wrap(nextEpochTimestamp2)); + assertGt(nextEpochSeed2, nextEpochSeed, "Sample seed for the next epoch should have changed"); + + } + + function test_WhenNewSampleSeedsAreAdded() external whenItHasBeenALongTimeSinceTheLastSampleSeedWasSet { + // it should continue to use the snapshotted sample seed + // it should calcaulte the same committee + } + + function addNumberOfValidators(uint256 _saltStart, uint256 _numberOfValidators) internal { + CheatDepositArgs[] memory validators = new CheatDepositArgs[](_numberOfValidators); + for (uint256 i = 0; i < _numberOfValidators; i++) { + validators[i] = createDepositArgs(i + _saltStart); + } + + testERC20.mint(address(this), TestConstants.AZTEC_MINIMUM_STAKE * validators.length); + testERC20.approve(address(rollup), TestConstants.AZTEC_MINIMUM_STAKE * validators.length); + rollup.cheat__InitialiseValidatorSet(validators); + } +} diff --git a/l1-contracts/test/validator-selection/setupEpoch.tree b/l1-contracts/test/validator-selection/setupEpoch.tree new file mode 100644 index 000000000000..ca8ed25801fb --- /dev/null +++ b/l1-contracts/test/validator-selection/setupEpoch.tree @@ -0,0 +1,20 @@ +SetupEpochTest +├── when the rollup is in genesis state +│ ├── Given the rollup is in genesis state +│ │ ├── it should set the sample seed to max +│ │ ├── it should set the sample seed for the next epoch +│ │ └── it should not change the current epoch +│ └── When the rollup is not in epoch 0 but is in genesis state +│ ├── it should set the sample seed to max +│ ├── it should set the sample seed for the next epoch +│ ├── it should have the seed for the initial epoch be 0 +│ └── it should not change the current epoch +├── when the rollup is not in genesis state +│ └── Given the seed has been sampled +│ ├── it should not change the sample seed +│ ├── it should be calcaulte the same committee when looking into the past +│ ├── it should not change the commitee even when validators are added or removed +│ └── it should not change the next seed +└── when it has been a long time since the last sample seed was set + └── it should use the most recent sample seed + From 9724ef2d05df7a1c3c660d531a9aea81f8848761 Mon Sep 17 00:00:00 2001 From: Maddiaa0 <47148561+Maddiaa0@users.noreply.github.com> Date: Wed, 16 Apr 2025 21:54:55 +0000 Subject: [PATCH 4/9] fmt --- .../core/interfaces/IValidatorSelection.sol | 1 + .../core/libraries/rollup/ExtRollupLib.sol | 15 +- .../ValidatorSelectionLib.sol | 32 +- .../ValidatorSelection.t.sol | 6 +- .../ValidatorSelectionBase.sol | 28 +- .../test/validator-selection/setupEpoch.t.sol | 393 ++++++++++-------- 6 files changed, 270 insertions(+), 205 deletions(-) diff --git a/l1-contracts/src/core/interfaces/IValidatorSelection.sol b/l1-contracts/src/core/interfaces/IValidatorSelection.sol index 5494273ed41c..b11251714870 100644 --- a/l1-contracts/src/core/interfaces/IValidatorSelection.sol +++ b/l1-contracts/src/core/interfaces/IValidatorSelection.sol @@ -10,6 +10,7 @@ import {Checkpoints} from "@oz/utils/structs/Checkpoints.sol"; * @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; diff --git a/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol b/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol index 2653c28bcbb7..a6eb785bae76 100644 --- a/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol @@ -2,15 +2,16 @@ // Copyright 2024 Aztec Labs. pragma solidity >=0.8.27; -import {SubmitEpochRootProofArgs, PublicInputArgs} from "@aztec/core/interfaces/IRollup.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"; -import {Epoch, Timestamp, TimeLib} from "@aztec/core/libraries/TimeLib.sol"; +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; diff --git a/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol b/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol index e64dac6e02ed..2f24f2ba2508 100644 --- a/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol +++ b/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol @@ -2,24 +2,18 @@ // Copyright 2024 Aztec Labs. pragma solidity >=0.8.27; -import {BlockHeaderValidationFlags} from "@aztec/core/interfaces/IRollup.sol"; -import {StakingStorage} from "@aztec/core/interfaces/IStaking.sol"; -import { - EpochData, ValidatorSelectionStorage -} from "@aztec/core/interfaces/IValidatorSelection.sol"; -import {SampleLib} from "@aztec/core/libraries/crypto/SampleLib.sol"; -import {SignatureLib, Signature} from "@aztec/core/libraries/crypto/SignatureLib.sol"; -import {Errors} from "@aztec/core/libraries/Errors.sol"; -import { - AddressSnapshotLib, - SnapshottedAddressSet -} 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 {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; - -import {Checkpoints} from "@oz/utils/structs/Checkpoints.sol"; -import {SafeCast} from "@oz/utils/math/SafeCast.sol"; +import { BlockHeaderValidationFlags } from '@aztec/core/interfaces/IRollup.sol'; +import { StakingStorage } from '@aztec/core/interfaces/IStaking.sol'; +import { EpochData, ValidatorSelectionStorage } from '@aztec/core/interfaces/IValidatorSelection.sol'; +import { SampleLib } from '@aztec/core/libraries/crypto/SampleLib.sol'; +import { SignatureLib, Signature } from '@aztec/core/libraries/crypto/SignatureLib.sol'; +import { Errors } from '@aztec/core/libraries/Errors.sol'; +import { AddressSnapshotLib, SnapshottedAddressSet } 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 { using EnumerableSet for EnumerableSet.AddressSet; @@ -239,7 +233,7 @@ library ValidatorSelectionLib { uint32 epoch = Epoch.unwrap(_epoch).toUint32(); // Check if the latest checkpoint is for the next epoch - (bool exists, uint32 key, ) = store.seeds.latestCheckpoint(); + (bool exists, uint32 key,) = store.seeds.latestCheckpoint(); // 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 diff --git a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol index 051d2b4b82ad..091d868165ec 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol @@ -105,7 +105,11 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { } } - function testValidatorSetLargerThanCommittee(bool _insufficientSigs) public setup(100) progressEpoch { + function testValidatorSetLargerThanCommittee(bool _insufficientSigs) + public + setup(100) + progressEpoch + { assertGt(rollup.getAttesters().length, rollup.getTargetCommitteeSize(), "Not enough validators"); uint256 committeeSize = rollup.getTargetCommitteeSize() * 2 / 3 + (_insufficientSigs ? 0 : 1); diff --git a/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol b/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol index 67e8b7d10ea8..610fdf174318 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol @@ -116,19 +116,19 @@ contract ValidatorSelectionTestBase is DecoderBase { } function createDepositArgs(uint256 _keySalt) internal returns (CheatDepositArgs memory) { - uint256 attesterPrivateKey = uint256(keccak256(abi.encode("attester", _keySalt))); - address attester = vm.addr(attesterPrivateKey); - attesterPrivateKeys[attester] = attesterPrivateKey; - uint256 proposerPrivateKey = uint256(keccak256(abi.encode("proposer", _keySalt))); - address proposer = vm.addr(proposerPrivateKey); - proposerPrivateKeys[proposer] = proposerPrivateKey; - proposerToAttester[proposer] = attester; - - return CheatDepositArgs({ - attester: attester, - proposer: proposer, - withdrawer: address(this), - amount: TestConstants.AZTEC_MINIMUM_STAKE - }); + uint256 attesterPrivateKey = uint256(keccak256(abi.encode("attester", _keySalt))); + address attester = vm.addr(attesterPrivateKey); + attesterPrivateKeys[attester] = attesterPrivateKey; + uint256 proposerPrivateKey = uint256(keccak256(abi.encode("proposer", _keySalt))); + address proposer = vm.addr(proposerPrivateKey); + proposerPrivateKeys[proposer] = proposerPrivateKey; + proposerToAttester[proposer] = attester; + + return CheatDepositArgs({ + attester: attester, + proposer: proposer, + withdrawer: address(this), + amount: TestConstants.AZTEC_MINIMUM_STAKE + }); } } diff --git a/l1-contracts/test/validator-selection/setupEpoch.t.sol b/l1-contracts/test/validator-selection/setupEpoch.t.sol index f6b76e636af1..1553ccfe856b 100644 --- a/l1-contracts/test/validator-selection/setupEpoch.t.sol +++ b/l1-contracts/test/validator-selection/setupEpoch.t.sol @@ -9,177 +9,242 @@ import {IValidatorSelection} from "@aztec/core/interfaces/IValidatorSelection.so import {TestConstants} from "../harnesses/TestConstants.sol"; contract SetupEpochTest is ValidatorSelectionTestBase { - using Checkpoints for Checkpoints.Trace224; - - modifier whenTheRollupIsInGenesisState() { - // Enforced with setup(0) modifier - _; - } - - function test_GivenTheRollupIsInGenesisState() external setup(0) whenTheRollupIsInGenesisState { - // it should set the sample seed to max - // it should set the sample seed for the next epoch - // it should not change the current epoch - - rollup.setupEpoch(); - - // Read the sample seed for epoch 0 through the rollup contract - uint256 sampleSeed = IValidatorSelection(address(rollup)).getCurrentSampleSeed(); - assertEq(sampleSeed, type(uint224).max, "Sample seed should be max in genesis state"); - - // Read the sample seed for epoch 1 through the rollup contract - Timestamp nextEpochTimestamp = Timestamp.wrap(block.timestamp + TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION); - uint256 nextEpochSeed = IValidatorSelection(address(rollup)).getSampleSeedAt(nextEpochTimestamp); - assertTrue(nextEpochSeed != 0, "Next epoch seed should be set"); - assertTrue(nextEpochSeed != sampleSeed, "Next epoch seed should be different from current epoch seed"); - - // Advance into the next epoch - vm.warp(Timestamp.unwrap(nextEpochTimestamp)); - - // Read the sample seed for epoch 1 now that we are in epoch 1, it should not change - uint256 epoch1SampleSeedInPast = IValidatorSelection(address(rollup)).getSampleSeedAt(nextEpochTimestamp); - assertEq(epoch1SampleSeedInPast, nextEpochSeed, "Next epoch seed should be the same as the current epoch seed"); - - // Call setupEpoch again, from the current epoch it should not change the sample seed - rollup.setupEpoch(); - assertEq(IValidatorSelection(address(rollup)).getCurrentSampleSeed(), nextEpochSeed, "Sample seed should not change"); - - // Seed for the next epoch should be set - Timestamp nextEpochTimestamp2 = Timestamp.wrap(block.timestamp + TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION); - uint256 nextEpochSeedInPast = IValidatorSelection(address(rollup)).getSampleSeedAt(nextEpochTimestamp2); - assertTrue(nextEpochSeedInPast != nextEpochSeed, "Next epoch seed should not be the same as the current epoch seed"); - } - - function test_WhenTheRollupIsNotInEpoch0ButIsInGenesisState() external setup(0) whenTheRollupIsInGenesisState { - // it should set the sample seed to max - // it should set the sample seed for the next epoch - // it should have the seed for the initial epoch be 0 - // it should not change the current epoch - - uint256 initialTimestamp = block.timestamp; - - // Advance into the next epoch - uint256 nextEpochTimestamp = block.timestamp + TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION; - vm.warp(nextEpochTimestamp); - - // Call setupEpoch, sample seed for initial epoch should be uint224.max - rollup.setupEpoch(); - assertEq(IValidatorSelection(address(rollup)).getCurrentSampleSeed(), type(uint224).max, "Sample seed should be max"); - - // Get sample seed for initial epoch - uint256 initialEpochSeed = IValidatorSelection(address(rollup)).getSampleSeedAt(Timestamp.wrap(initialTimestamp)); - assertEq(initialEpochSeed, 0, "Sample seed for initial epoch should be 0"); - } - - modifier whenTheRollupIsNotInGenesisState() { - // Enforced with setup(4) modifier - _; + using Checkpoints for Checkpoints.Trace224; + + modifier whenTheRollupIsInGenesisState() { + // Enforced with setup(0) modifier + _; + } + + function test_GivenTheRollupIsInGenesisState() external setup(0) whenTheRollupIsInGenesisState { + // it should set the sample seed to max + // it should set the sample seed for the next epoch + // it should not change the current epoch + + rollup.setupEpoch(); + + // Read the sample seed for epoch 0 through the rollup contract + uint256 sampleSeed = IValidatorSelection(address(rollup)).getCurrentSampleSeed(); + assertEq(sampleSeed, type(uint224).max, "Sample seed should be max in genesis state"); + + // Read the sample seed for epoch 1 through the rollup contract + Timestamp nextEpochTimestamp = Timestamp.wrap( + block.timestamp + TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION + ); + uint256 nextEpochSeed = IValidatorSelection(address(rollup)).getSampleSeedAt(nextEpochTimestamp); + assertTrue(nextEpochSeed != 0, "Next epoch seed should be set"); + assertTrue( + nextEpochSeed != sampleSeed, "Next epoch seed should be different from current epoch seed" + ); + + // Advance into the next epoch + vm.warp(Timestamp.unwrap(nextEpochTimestamp)); + + // Read the sample seed for epoch 1 now that we are in epoch 1, it should not change + uint256 epoch1SampleSeedInPast = + IValidatorSelection(address(rollup)).getSampleSeedAt(nextEpochTimestamp); + assertEq( + epoch1SampleSeedInPast, + nextEpochSeed, + "Next epoch seed should be the same as the current epoch seed" + ); + + // Call setupEpoch again, from the current epoch it should not change the sample seed + rollup.setupEpoch(); + assertEq( + IValidatorSelection(address(rollup)).getCurrentSampleSeed(), + nextEpochSeed, + "Sample seed should not change" + ); + + // Seed for the next epoch should be set + Timestamp nextEpochTimestamp2 = Timestamp.wrap( + block.timestamp + TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION + ); + uint256 nextEpochSeedInPast = + IValidatorSelection(address(rollup)).getSampleSeedAt(nextEpochTimestamp2); + assertTrue( + nextEpochSeedInPast != nextEpochSeed, + "Next epoch seed should not be the same as the current epoch seed" + ); + } + + function test_WhenTheRollupIsNotInEpoch0ButIsInGenesisState() + external + setup(0) + whenTheRollupIsInGenesisState + { + // it should set the sample seed to max + // it should set the sample seed for the next epoch + // it should have the seed for the initial epoch be 0 + // it should not change the current epoch + + uint256 initialTimestamp = block.timestamp; + + // Advance into the next epoch + uint256 nextEpochTimestamp = + block.timestamp + TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION; + vm.warp(nextEpochTimestamp); + + // Call setupEpoch, sample seed for initial epoch should be uint224.max + rollup.setupEpoch(); + assertEq( + IValidatorSelection(address(rollup)).getCurrentSampleSeed(), + type(uint224).max, + "Sample seed should be max" + ); + + // Get sample seed for initial epoch + uint256 initialEpochSeed = + IValidatorSelection(address(rollup)).getSampleSeedAt(Timestamp.wrap(initialTimestamp)); + assertEq(initialEpochSeed, 0, "Sample seed for initial epoch should be 0"); + } + + modifier whenTheRollupIsNotInGenesisState() { + // Enforced with setup(4) modifier + _; + } + + function test_GivenTheSeedHasBeenSampled() external setup(4) whenTheRollupIsNotInGenesisState { + // it should not change the sample seed + // it should be calcaulte the same committee when looking into the past + // it should not change the commitee even when validators are added or removed + // it should not change the next seed + + rollup.setupEpoch(); + + // Check that the initial epoch seed is set + uint256 initialEpochSeed = + IValidatorSelection(address(rollup)).getSampleSeedAt(Timestamp.wrap(block.timestamp)); + assertEq(initialEpochSeed, type(uint224).max, "Sample seed for initial epoch should be max"); + + // Get the initial committee + address[] memory initialCommittee = + IValidatorSelection(address(rollup)).getCurrentEpochCommittee(); + + // When setup epoch is called, nothing should change + rollup.setupEpoch(); + uint256 initialEpochSeedAfterSetup = + IValidatorSelection(address(rollup)).getSampleSeedAt(Timestamp.wrap(block.timestamp)); + assertEq(initialEpochSeedAfterSetup, initialEpochSeed, "Sample seed should not change"); + + // Check that the committee is the same + address[] memory committeeAfterRepeatedSetup = + IValidatorSelection(address(rollup)).getCurrentEpochCommittee(); + assertEq( + committeeAfterRepeatedSetup.length, + initialCommittee.length, + "Committee should have the same length" + ); + for (uint256 i = 0; i < initialCommittee.length; i++) { + assertEq( + committeeAfterRepeatedSetup[i], + initialCommittee[i], + "Committee should have the same validators" + ); } - function test_GivenTheSeedHasBeenSampled() external setup(4) whenTheRollupIsNotInGenesisState { - // it should not change the sample seed - // it should be calcaulte the same committee when looking into the past - // it should not change the commitee even when validators are added or removed - // it should not change the next seed - - rollup.setupEpoch(); - - // Check that the initial epoch seed is set - uint256 initialEpochSeed = IValidatorSelection(address(rollup)).getSampleSeedAt(Timestamp.wrap(block.timestamp)); - assertEq(initialEpochSeed, type(uint224).max, "Sample seed for initial epoch should be max"); - - // Get the initial committee - address[] memory initialCommittee = IValidatorSelection(address(rollup)).getCurrentEpochCommittee(); - - // When setup epoch is called, nothing should change - rollup.setupEpoch(); - uint256 initialEpochSeedAfterSetup = IValidatorSelection(address(rollup)).getSampleSeedAt(Timestamp.wrap(block.timestamp)); - assertEq(initialEpochSeedAfterSetup, initialEpochSeed, "Sample seed should not change"); - - // Check that the committee is the same - address[] memory committeeAfterRepeatedSetup = IValidatorSelection(address(rollup)).getCurrentEpochCommittee(); - assertEq(committeeAfterRepeatedSetup.length, initialCommittee.length, "Committee should have the same length"); - for (uint256 i = 0; i < initialCommittee.length; i++) { - assertEq(committeeAfterRepeatedSetup[i], initialCommittee[i], "Committee should have the same validators"); - } - - // Add a couple of extra validators during this epoch, the sampled validator set should not change - addNumberOfValidators(420420, 2); - - // Sample the validator set for the current epoch - address[] memory committeeAfterAddingExtraValidators = IValidatorSelection(address(rollup)).getCurrentEpochCommittee(); - for (uint256 i = 0; i < committeeAfterAddingExtraValidators.length; i++) { - assertEq(committeeAfterAddingExtraValidators[i], initialCommittee[i], "Committee should be the same"); - } - - // Jump into the future and check the committee still does not change - uint256 savedTimestamp = block.timestamp; - vm.warp(savedTimestamp + TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION); - - address[] memory committeeAfterJumpingIntoFuture = IValidatorSelection(address(rollup)).getCommitteeAt(Timestamp.wrap(savedTimestamp)); - for (uint256 i = 0; i < committeeAfterJumpingIntoFuture.length; i++) { - assertEq(committeeAfterJumpingIntoFuture[i], initialCommittee[i], "Committee should be the same"); - } - } + // Add a couple of extra validators during this epoch, the sampled validator set should not change + addNumberOfValidators(420420, 2); - modifier whenItHasBeenALongTimeSinceTheLastSampleSeedWasSet() { - // Enforce validator set has been changed with the setup(50) modifier - _; + // Sample the validator set for the current epoch + address[] memory committeeAfterAddingExtraValidators = + IValidatorSelection(address(rollup)).getCurrentEpochCommittee(); + for (uint256 i = 0; i < committeeAfterAddingExtraValidators.length; i++) { + assertEq( + committeeAfterAddingExtraValidators[i], initialCommittee[i], "Committee should be the same" + ); } - function test_WhenItHasBeenALongTimeSinceTheLastSampleSeedWasSet() - external - setup(50) - whenItHasBeenALongTimeSinceTheLastSampleSeedWasSet - { - // it should use the most recent sample seed - rollup.setupEpoch(); - - // Check that the sample seed has been set for the next epoch - uint256 nextEpochTimestamp = block.timestamp + TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION; - uint256 nextEpochSeed = IValidatorSelection(address(rollup)).getSampleSeedAt(Timestamp.wrap(nextEpochTimestamp)); - assertGt(nextEpochSeed, 0, "Sample seed should be set for the next epoch"); - - // Jump into the future, looking back, the returned sample seed should be the same for the next range of epochs - uint256 savedTimestamp = block.timestamp; - vm.warp(savedTimestamp + 2*(TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION)); - - uint256 sampleSeedAfterJump = IValidatorSelection(address(rollup)).getCurrentSampleSeed(); - assertEq(sampleSeedAfterJump, nextEpochSeed, "Sample seed should be the same"); - - // Add some validators - addNumberOfValidators(420422, 2); - - // Jump further into the future - vm.warp(savedTimestamp + 4*(TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION)); - - // Check that the sample seed has not changed - assertEq(IValidatorSelection(address(rollup)).getCurrentSampleSeed(), nextEpochSeed, "Sample seed should not change"); - - // Call setupEpoch, the sample seed should not change - rollup.setupEpoch(); - assertEq(IValidatorSelection(address(rollup)).getCurrentSampleSeed(), nextEpochSeed, "Sample seed should not change"); - - // The sample seed for the next epoch should have changed - uint256 nextEpochTimestamp2 = block.timestamp + TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION; - uint256 nextEpochSeed2 = IValidatorSelection(address(rollup)).getSampleSeedAt(Timestamp.wrap(nextEpochTimestamp2)); - assertGt(nextEpochSeed2, nextEpochSeed, "Sample seed for the next epoch should have changed"); + // Jump into the future and check the committee still does not change + uint256 savedTimestamp = block.timestamp; + vm.warp(savedTimestamp + TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION); + address[] memory committeeAfterJumpingIntoFuture = + IValidatorSelection(address(rollup)).getCommitteeAt(Timestamp.wrap(savedTimestamp)); + for (uint256 i = 0; i < committeeAfterJumpingIntoFuture.length; i++) { + assertEq( + committeeAfterJumpingIntoFuture[i], initialCommittee[i], "Committee should be the same" + ); } - - function test_WhenNewSampleSeedsAreAdded() external whenItHasBeenALongTimeSinceTheLastSampleSeedWasSet { - // it should continue to use the snapshotted sample seed - // it should calcaulte the same committee + } + + modifier whenItHasBeenALongTimeSinceTheLastSampleSeedWasSet() { + // Enforce validator set has been changed with the setup(50) modifier + _; + } + + function test_WhenItHasBeenALongTimeSinceTheLastSampleSeedWasSet() + external + setup(50) + whenItHasBeenALongTimeSinceTheLastSampleSeedWasSet + { + // it should use the most recent sample seed + rollup.setupEpoch(); + + // Check that the sample seed has been set for the next epoch + uint256 nextEpochTimestamp = + block.timestamp + TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION; + uint256 nextEpochSeed = + IValidatorSelection(address(rollup)).getSampleSeedAt(Timestamp.wrap(nextEpochTimestamp)); + assertGt(nextEpochSeed, 0, "Sample seed should be set for the next epoch"); + + // Jump into the future, looking back, the returned sample seed should be the same for the next range of epochs + uint256 savedTimestamp = block.timestamp; + vm.warp( + savedTimestamp + 2 * (TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION) + ); + + uint256 sampleSeedAfterJump = IValidatorSelection(address(rollup)).getCurrentSampleSeed(); + assertEq(sampleSeedAfterJump, nextEpochSeed, "Sample seed should be the same"); + + // Add some validators + addNumberOfValidators(420422, 2); + + // Jump further into the future + vm.warp( + savedTimestamp + 4 * (TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION) + ); + + // Check that the sample seed has not changed + assertEq( + IValidatorSelection(address(rollup)).getCurrentSampleSeed(), + nextEpochSeed, + "Sample seed should not change" + ); + + // Call setupEpoch, the sample seed should not change + rollup.setupEpoch(); + assertEq( + IValidatorSelection(address(rollup)).getCurrentSampleSeed(), + nextEpochSeed, + "Sample seed should not change" + ); + + // The sample seed for the next epoch should have changed + uint256 nextEpochTimestamp2 = + block.timestamp + TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION; + uint256 nextEpochSeed2 = + IValidatorSelection(address(rollup)).getSampleSeedAt(Timestamp.wrap(nextEpochTimestamp2)); + assertGt(nextEpochSeed2, nextEpochSeed, "Sample seed for the next epoch should have changed"); + } + + function test_WhenNewSampleSeedsAreAdded() + external + whenItHasBeenALongTimeSinceTheLastSampleSeedWasSet + { + // it should continue to use the snapshotted sample seed + // it should calcaulte the same committee + } + + function addNumberOfValidators(uint256 _saltStart, uint256 _numberOfValidators) internal { + CheatDepositArgs[] memory validators = new CheatDepositArgs[](_numberOfValidators); + for (uint256 i = 0; i < _numberOfValidators; i++) { + validators[i] = createDepositArgs(i + _saltStart); } - function addNumberOfValidators(uint256 _saltStart, uint256 _numberOfValidators) internal { - CheatDepositArgs[] memory validators = new CheatDepositArgs[](_numberOfValidators); - for (uint256 i = 0; i < _numberOfValidators; i++) { - validators[i] = createDepositArgs(i + _saltStart); - } - - testERC20.mint(address(this), TestConstants.AZTEC_MINIMUM_STAKE * validators.length); - testERC20.approve(address(rollup), TestConstants.AZTEC_MINIMUM_STAKE * validators.length); - rollup.cheat__InitialiseValidatorSet(validators); - } + testERC20.mint(address(this), TestConstants.AZTEC_MINIMUM_STAKE * validators.length); + testERC20.approve(address(rollup), TestConstants.AZTEC_MINIMUM_STAKE * validators.length); + rollup.cheat__InitialiseValidatorSet(validators); + } } From b21238959bb2e65801008f124337db45273fbdc7 Mon Sep 17 00:00:00 2001 From: Maddiaa0 <47148561+Maddiaa0@users.noreply.github.com> Date: Wed, 16 Apr 2025 21:57:34 +0000 Subject: [PATCH 5/9] fmt: function order --- .../core/libraries/rollup/ExtRollupLib.sol | 14 ++-- .../ValidatorSelectionLib.sol | 67 ++++++++++--------- 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol b/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol index a6eb785bae76..67e78b377f03 100644 --- a/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol @@ -2,13 +2,13 @@ // Copyright 2024 Aztec Labs. 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'; +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. diff --git a/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol b/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol index 2f24f2ba2508..dd2b17fffcf3 100644 --- a/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol +++ b/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol @@ -2,18 +2,23 @@ // Copyright 2024 Aztec Labs. pragma solidity >=0.8.27; -import { BlockHeaderValidationFlags } from '@aztec/core/interfaces/IRollup.sol'; -import { StakingStorage } from '@aztec/core/interfaces/IStaking.sol'; -import { EpochData, ValidatorSelectionStorage } from '@aztec/core/interfaces/IValidatorSelection.sol'; -import { SampleLib } from '@aztec/core/libraries/crypto/SampleLib.sol'; -import { SignatureLib, Signature } from '@aztec/core/libraries/crypto/SignatureLib.sol'; -import { Errors } from '@aztec/core/libraries/Errors.sol'; -import { AddressSnapshotLib, SnapshottedAddressSet } 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'; +import {BlockHeaderValidationFlags} from "@aztec/core/interfaces/IRollup.sol"; +import {StakingStorage} from "@aztec/core/interfaces/IStaking.sol"; +import { + EpochData, ValidatorSelectionStorage +} from "@aztec/core/interfaces/IValidatorSelection.sol"; +import {SampleLib} from "@aztec/core/libraries/crypto/SampleLib.sol"; +import {SignatureLib, Signature} from "@aztec/core/libraries/crypto/SignatureLib.sol"; +import {Errors} from "@aztec/core/libraries/Errors.sol"; +import { + AddressSnapshotLib, + SnapshottedAddressSet +} 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 { using EnumerableSet for EnumerableSet.AddressSet; @@ -209,25 +214,6 @@ library ValidatorSelectionLib { return epoch.committee; } - /** - * @notice Get the sample seed for an epoch - * - * @dev This should behave as walking past the line, but it does not currently do that. - * If there are entire skips, e.g., 1, 2, 5 and we then go back and try executing - * for 4 we will get an invalid value because we will read lastSeed which is from 5. - * - * @dev The `_epoch` will never be 0 nor in the future - * - * @dev The return value will be equal to keccak256(n, block.prevrandao) for n being the last epoch - * setup. - * - * @return The sample seed for the epoch - */ - function getSampleSeed(Epoch _epoch) internal view returns (uint224) { - ValidatorSelectionStorage storage store = getStorage(); - return store.seeds.upperLookup(Epoch.unwrap(_epoch).toUint32()); - } - function setSampleSeedForEpoch(Epoch _epoch) internal { ValidatorSelectionStorage storage store = getStorage(); uint32 epoch = Epoch.unwrap(_epoch).toUint32(); @@ -250,6 +236,25 @@ library ValidatorSelectionLib { } } + /** + * @notice Get the sample seed for an epoch + * + * @dev This should behave as walking past the line, but it does not currently do that. + * If there are entire skips, e.g., 1, 2, 5 and we then go back and try executing + * for 4 we will get an invalid value because we will read lastSeed which is from 5. + * + * @dev The `_epoch` will never be 0 nor in the future + * + * @dev The return value will be equal to keccak256(n, block.prevrandao) for n being the last epoch + * setup. + * + * @return The sample seed for the epoch + */ + function getSampleSeed(Epoch _epoch) internal view returns (uint224) { + ValidatorSelectionStorage storage store = getStorage(); + return store.seeds.upperLookup(Epoch.unwrap(_epoch).toUint32()); + } + function getStorage() internal pure returns (ValidatorSelectionStorage storage storageStruct) { bytes32 position = VALIDATOR_SELECTION_STORAGE_POSITION; assembly { From 40967d9e10af39ed1f53cefa44f54d71a445c035 Mon Sep 17 00:00:00 2001 From: Maddiaa0 <47148561+Maddiaa0@users.noreply.github.com> Date: Mon, 28 Apr 2025 09:51:06 +0000 Subject: [PATCH 6/9] post merge fix --- .../test/validator-selection/ValidatorSelection.t.sol | 3 ++- .../test/validator-selection/ValidatorSelectionBase.sol | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol index c727fc63ee1e..147273c7c327 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol @@ -16,7 +16,8 @@ import {NaiveMerkle} from "../merkle/Naive.sol"; import {MerkleTestUtil} from "../merkle/TestUtil.sol"; import {TestERC20} from "@aztec/mock/TestERC20.sol"; import {MessageHashUtils} from "@oz/utils/cryptography/MessageHashUtils.sol"; -import {ProposeArgs, OracleInput, ProposeLib} from "@aztec/core/libraries/rollup/ProposeLib.sol"; +import {ProposeArgs, OracleInput, ProposeLib, ProposePayload} from "@aztec/core/libraries/rollup/ProposeLib.sol"; +import {HeaderLib} from "@aztec/core/libraries/rollup/HeaderLib.sol"; import {TestConstants} from "../harnesses/TestConstants.sol"; import {Timestamp, EpochLib, Epoch} from "@aztec/core/libraries/TimeLib.sol"; diff --git a/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol b/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol index 610fdf174318..9dad74cdb9fb 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol @@ -19,7 +19,7 @@ import {CheatDepositArgs} from "@aztec/core/interfaces/IRollup.sol"; import {Epoch, EpochLib, Timestamp} from "@aztec/core/libraries/TimeLib.sol"; import {RewardDistributor} from "@aztec/governance/RewardDistributor.sol"; import {SlashFactory} from "@aztec/periphery/SlashFactory.sol"; -import {Slasher} from "@aztec/core/staking/Slasher.sol"; +import {Slasher} from "@aztec/core/slashing/Slasher.sol"; import {IValidatorSelection} from "@aztec/core/interfaces/IValidatorSelection.sol"; import {TimeCheater} from "../staking/TimeCheater.sol"; @@ -62,8 +62,8 @@ contract ValidatorSelectionTestBase is DecoderBase { string memory _name = "mixed_block_1"; { DecoderBase.Full memory full = load(_name); - uint256 slotNumber = full.block.decodedHeader.globalVariables.slotNumber; - uint256 initialTime = full.block.decodedHeader.globalVariables.timestamp + uint256 slotNumber = full.block.decodedHeader.slotNumber; + uint256 initialTime = full.block.decodedHeader.timestamp - slotNumber * TestConstants.AZTEC_SLOT_DURATION; timeCheater = new TimeCheater( From 1ecec4ff76c53966dec30f519e007c855b72ad8e Mon Sep 17 00:00:00 2001 From: Maddiaa0 <47148561+Maddiaa0@users.noreply.github.com> Date: Mon, 28 Apr 2025 10:21:40 +0000 Subject: [PATCH 7/9] fmt --- .../test/validator-selection/ValidatorSelection.t.sol | 7 ++++++- .../test/validator-selection/ValidatorSelectionBase.sol | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol index 147273c7c327..c2a321294c37 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol @@ -16,7 +16,12 @@ import {NaiveMerkle} from "../merkle/Naive.sol"; import {MerkleTestUtil} from "../merkle/TestUtil.sol"; import {TestERC20} from "@aztec/mock/TestERC20.sol"; import {MessageHashUtils} from "@oz/utils/cryptography/MessageHashUtils.sol"; -import {ProposeArgs, OracleInput, ProposeLib, ProposePayload} from "@aztec/core/libraries/rollup/ProposeLib.sol"; +import { + ProposeArgs, + OracleInput, + ProposeLib, + ProposePayload +} from "@aztec/core/libraries/rollup/ProposeLib.sol"; import {HeaderLib} from "@aztec/core/libraries/rollup/HeaderLib.sol"; import {TestConstants} from "../harnesses/TestConstants.sol"; diff --git a/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol b/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol index 9dad74cdb9fb..ff397a1465b4 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol @@ -63,8 +63,8 @@ contract ValidatorSelectionTestBase is DecoderBase { { DecoderBase.Full memory full = load(_name); uint256 slotNumber = full.block.decodedHeader.slotNumber; - uint256 initialTime = full.block.decodedHeader.timestamp - - slotNumber * TestConstants.AZTEC_SLOT_DURATION; + uint256 initialTime = + full.block.decodedHeader.timestamp - slotNumber * TestConstants.AZTEC_SLOT_DURATION; timeCheater = new TimeCheater( address(rollup), From 08d11321c6dd3de8b28c09bf00c0d3d7ef3690d5 Mon Sep 17 00:00:00 2001 From: Maddiaa0 <47148561+Maddiaa0@users.noreply.github.com> Date: Mon, 28 Apr 2025 14:00:43 +0000 Subject: [PATCH 8/9] update getseed --- .../libraries/validator-selection/ValidatorSelectionLib.sol | 6 +++++- l1-contracts/test/validator-selection/setupEpoch.t.sol | 5 ----- l1-contracts/test/validator-selection/setupEpoch.tree | 1 - 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol b/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol index dd2b17fffcf3..fd384af26d19 100644 --- a/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol +++ b/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol @@ -252,7 +252,11 @@ library ValidatorSelectionLib { */ function getSampleSeed(Epoch _epoch) internal view returns (uint224) { ValidatorSelectionStorage storage store = getStorage(); - return store.seeds.upperLookup(Epoch.unwrap(_epoch).toUint32()); + uint224 sampleSeed = store.seeds.upperLookup(Epoch.unwrap(_epoch).toUint32()); + if (sampleSeed == 0) { + sampleSeed = type(uint224).max; + } + return sampleSeed; } function getStorage() internal pure returns (ValidatorSelectionStorage storage storageStruct) { diff --git a/l1-contracts/test/validator-selection/setupEpoch.t.sol b/l1-contracts/test/validator-selection/setupEpoch.t.sol index 1553ccfe856b..272611234299 100644 --- a/l1-contracts/test/validator-selection/setupEpoch.t.sol +++ b/l1-contracts/test/validator-selection/setupEpoch.t.sol @@ -93,11 +93,6 @@ contract SetupEpochTest is ValidatorSelectionTestBase { type(uint224).max, "Sample seed should be max" ); - - // Get sample seed for initial epoch - uint256 initialEpochSeed = - IValidatorSelection(address(rollup)).getSampleSeedAt(Timestamp.wrap(initialTimestamp)); - assertEq(initialEpochSeed, 0, "Sample seed for initial epoch should be 0"); } modifier whenTheRollupIsNotInGenesisState() { diff --git a/l1-contracts/test/validator-selection/setupEpoch.tree b/l1-contracts/test/validator-selection/setupEpoch.tree index ca8ed25801fb..9246b714067b 100644 --- a/l1-contracts/test/validator-selection/setupEpoch.tree +++ b/l1-contracts/test/validator-selection/setupEpoch.tree @@ -7,7 +7,6 @@ SetupEpochTest │ └── When the rollup is not in epoch 0 but is in genesis state │ ├── it should set the sample seed to max │ ├── it should set the sample seed for the next epoch -│ ├── it should have the seed for the initial epoch be 0 │ └── it should not change the current epoch ├── when the rollup is not in genesis state │ └── Given the seed has been sampled From 091c43eed127f6e4445a5cd33228ed25d4dbf55d Mon Sep 17 00:00:00 2001 From: Maddiaa0 <47148561+Maddiaa0@users.noreply.github.com> Date: Tue, 29 Apr 2025 09:11:00 +0000 Subject: [PATCH 9/9] chore: genesis state changes --- .../ValidatorSelectionLib.sol | 34 +++++++++----- .../ValidatorSelection.t.sol | 4 +- .../ValidatorSelectionBase.sol | 2 +- .../test/validator-selection/setupEpoch.t.sol | 46 ++----------------- .../test/validator-selection/setupEpoch.tree | 8 +--- 5 files changed, 29 insertions(+), 65 deletions(-) diff --git a/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol b/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol index fd384af26d19..4b90f4201cb1 100644 --- a/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol +++ b/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol @@ -35,6 +35,9 @@ library ValidatorSelectionLib { function initialize(uint256 _targetCommitteeSize) internal { ValidatorSelectionStorage storage store = getStorage(); store.targetCommitteeSize = _targetCommitteeSize; + + // Set the sample seed for the first epoch to max + store.seeds.push(0, type(uint224).max); } /** @@ -45,25 +48,21 @@ library ValidatorSelectionLib { function setupEpoch(StakingStorage storage _stakingStore, Epoch _epochNumber) internal { ValidatorSelectionStorage storage store = getStorage(); + //################ Seeds ################ // 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) { - sampleSeed = type(uint224).max; - store.seeds.push(Epoch.unwrap(_epochNumber).toUint32(), 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)); + //################ Committee ################ // 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)); } /** @@ -198,6 +197,13 @@ library ValidatorSelectionLib { return committee; } + /** + * @notice Get the committee for an epoch + * + * @param _epochNumber - The epoch to get the committee for + * + * @return The committee for the epoch + */ function getCommitteeAt(StakingStorage storage _stakingStore, Epoch _epochNumber) internal returns (address[] memory) @@ -214,16 +220,20 @@ library ValidatorSelectionLib { return epoch.committee; } + /** + * @notice Sets the sample seed for an epoch + * + * @param _epoch - The epoch to set the sample seed for + */ function setSampleSeedForEpoch(Epoch _epoch) internal { ValidatorSelectionStorage storage store = getStorage(); uint32 epoch = Epoch.unwrap(_epoch).toUint32(); // Check if the latest checkpoint is for the next epoch - (bool exists, uint32 key,) = store.seeds.latestCheckpoint(); + // 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,) = store.seeds.latestCheckpoint(); // 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; } diff --git a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol index c2a321294c37..61a8503cbf8d 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol @@ -106,9 +106,7 @@ contract ValidatorSelectionTest is ValidatorSelectionTestBase { assertEq(postCommittee.length, expectedSize, "Invalid committee size"); // Elements in the committee should be the same - for (uint256 i = 0; i < expectedSize; i++) { - assertEq(preCommittee[i], postCommittee[i], "Committee element has changed"); - } + assertEq(preCommittee, postCommittee, "Committee elements have changed"); } function testValidatorSetLargerThanCommittee(bool _insufficientSigs) diff --git a/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol b/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol index ff397a1465b4..b179538605b0 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelectionBase.sol @@ -56,7 +56,7 @@ contract ValidatorSelectionTestBase is DecoderBase { mapping(address => bool) internal _seenCommittee; /** - * @notice Set up the contracts needed for the tests with time aligned to the provided block name + * @notice Setup contracts needed for the tests with the a given number of validators */ modifier setup(uint256 _validatorCount) { string memory _name = "mixed_block_1"; diff --git a/l1-contracts/test/validator-selection/setupEpoch.t.sol b/l1-contracts/test/validator-selection/setupEpoch.t.sol index 272611234299..c62f556972ae 100644 --- a/l1-contracts/test/validator-selection/setupEpoch.t.sol +++ b/l1-contracts/test/validator-selection/setupEpoch.t.sol @@ -69,32 +69,6 @@ contract SetupEpochTest is ValidatorSelectionTestBase { ); } - function test_WhenTheRollupIsNotInEpoch0ButIsInGenesisState() - external - setup(0) - whenTheRollupIsInGenesisState - { - // it should set the sample seed to max - // it should set the sample seed for the next epoch - // it should have the seed for the initial epoch be 0 - // it should not change the current epoch - - uint256 initialTimestamp = block.timestamp; - - // Advance into the next epoch - uint256 nextEpochTimestamp = - block.timestamp + TestConstants.AZTEC_EPOCH_DURATION * TestConstants.AZTEC_SLOT_DURATION; - vm.warp(nextEpochTimestamp); - - // Call setupEpoch, sample seed for initial epoch should be uint224.max - rollup.setupEpoch(); - assertEq( - IValidatorSelection(address(rollup)).getCurrentSampleSeed(), - type(uint224).max, - "Sample seed should be max" - ); - } - modifier whenTheRollupIsNotInGenesisState() { // Enforced with setup(4) modifier _; @@ -131,13 +105,7 @@ contract SetupEpochTest is ValidatorSelectionTestBase { initialCommittee.length, "Committee should have the same length" ); - for (uint256 i = 0; i < initialCommittee.length; i++) { - assertEq( - committeeAfterRepeatedSetup[i], - initialCommittee[i], - "Committee should have the same validators" - ); - } + assertEq(committeeAfterRepeatedSetup, initialCommittee, "Committee should be the same"); // Add a couple of extra validators during this epoch, the sampled validator set should not change addNumberOfValidators(420420, 2); @@ -145,11 +113,7 @@ contract SetupEpochTest is ValidatorSelectionTestBase { // Sample the validator set for the current epoch address[] memory committeeAfterAddingExtraValidators = IValidatorSelection(address(rollup)).getCurrentEpochCommittee(); - for (uint256 i = 0; i < committeeAfterAddingExtraValidators.length; i++) { - assertEq( - committeeAfterAddingExtraValidators[i], initialCommittee[i], "Committee should be the same" - ); - } + assertEq(committeeAfterAddingExtraValidators, initialCommittee, "Committee should be the same"); // Jump into the future and check the committee still does not change uint256 savedTimestamp = block.timestamp; @@ -157,11 +121,7 @@ contract SetupEpochTest is ValidatorSelectionTestBase { address[] memory committeeAfterJumpingIntoFuture = IValidatorSelection(address(rollup)).getCommitteeAt(Timestamp.wrap(savedTimestamp)); - for (uint256 i = 0; i < committeeAfterJumpingIntoFuture.length; i++) { - assertEq( - committeeAfterJumpingIntoFuture[i], initialCommittee[i], "Committee should be the same" - ); - } + assertEq(committeeAfterJumpingIntoFuture, initialCommittee, "Committee should be the same"); } modifier whenItHasBeenALongTimeSinceTheLastSampleSeedWasSet() { diff --git a/l1-contracts/test/validator-selection/setupEpoch.tree b/l1-contracts/test/validator-selection/setupEpoch.tree index 9246b714067b..f8be8b7dd80b 100644 --- a/l1-contracts/test/validator-selection/setupEpoch.tree +++ b/l1-contracts/test/validator-selection/setupEpoch.tree @@ -1,15 +1,11 @@ SetupEpochTest ├── when the rollup is in genesis state -│ ├── Given the rollup is in genesis state -│ │ ├── it should set the sample seed to max -│ │ ├── it should set the sample seed for the next epoch -│ │ └── it should not change the current epoch -│ └── When the rollup is not in epoch 0 but is in genesis state +│ └── Given the rollup is in genesis state │ ├── it should set the sample seed to max │ ├── it should set the sample seed for the next epoch │ └── it should not change the current epoch ├── when the rollup is not in genesis state -│ └── Given the seed has been sampled +│ └── Given the seed has been sampled │ ├── it should not change the sample seed │ ├── it should be calcaulte the same committee when looking into the past │ ├── it should not change the commitee even when validators are added or removed