diff --git a/l1-contracts/src/core/RollupCore.sol b/l1-contracts/src/core/RollupCore.sol index 969fc2a76eac..53132bb4ef5f 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/src/core/interfaces/IValidatorSelection.sol b/l1-contracts/src/core/interfaces/IValidatorSelection.sol index 51825e4db508..b11251714870 100644 --- a/l1-contracts/src/core/interfaces/IValidatorSelection.sol +++ b/l1-contracts/src/core/interfaces/IValidatorSelection.sol @@ -3,25 +3,24 @@ pragma solidity >=0.8.27; import {Timestamp, Slot, Epoch} from "@aztec/core/libraries/TimeLib.sol"; - +import {Checkpoints} from "@oz/utils/structs/Checkpoints.sol"; /** * @notice The data structure for an epoch * @param committee - The attesters for the epoch * @param sampleSeed - The seed used to sample the attesters of the epoch * @param nextSeed - The seed used to influence the NEXT epoch */ + struct EpochData { // TODO: remove in favor of commitment to comittee address[] committee; - uint256 sampleSeed; - uint256 nextSeed; } struct ValidatorSelectionStorage { // A mapping to snapshots of the validator set mapping(Epoch => EpochData) epochs; - // The last stored randao value, same value as `seed` in the last inserted epoch - uint256 lastSeed; + // Checkpointed map of epoch -> sample seed + Checkpoints.Trace224 seeds; uint256 targetCommitteeSize; } diff --git a/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol b/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol index 8a0ffe4e26d9..67e78b377f03 100644 --- a/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ExtRollupLib.sol @@ -3,15 +3,18 @@ pragma solidity >=0.8.27; import {SubmitEpochRootProofArgs, PublicInputArgs} from "@aztec/core/interfaces/IRollup.sol"; +import {Epoch, Timestamp, TimeLib} from "@aztec/core/libraries/TimeLib.sol"; import {StakingLib} from "./../staking/StakingLib.sol"; import {ValidatorSelectionLib} from "./../validator-selection/ValidatorSelectionLib.sol"; import {BlobLib} from "./BlobLib.sol"; import {EpochProofLib} from "./EpochProofLib.sol"; import {ProposeLib, ProposeArgs, Signature} from "./ProposeLib.sol"; - // We are using this library such that we can more easily "link" just a larger external library // instead of a few smaller ones. + library ExtRollupLib { + using TimeLib for Timestamp; + function submitEpochRootProof(SubmitEpochRootProofArgs calldata _args) external { EpochProofLib.submitEpochRootProof(_args); } @@ -30,7 +33,8 @@ library ExtRollupLib { } function setupEpoch() external { - ValidatorSelectionLib.setupEpoch(StakingLib.getStorage()); + Epoch currentEpoch = Timestamp.wrap(block.timestamp).epochFromTimestamp(); + ValidatorSelectionLib.setupEpoch(StakingLib.getStorage(), currentEpoch); } function getEpochProofPublicInputs( diff --git a/l1-contracts/src/core/libraries/rollup/ProposeLib.sol b/l1-contracts/src/core/libraries/rollup/ProposeLib.sol index 21717af2ad91..a40c085f48a1 100644 --- a/l1-contracts/src/core/libraries/rollup/ProposeLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ProposeLib.sol @@ -99,10 +99,10 @@ library ProposeLib { BlobLib.validateBlobs(_blobInput, _checkBlob); Header memory header = HeaderLib.decode(_args.header); - v.headerHash = HeaderLib.hash(_args.header); - ValidatorSelectionLib.setupEpoch(StakingLib.getStorage()); + Epoch currentEpoch = Timestamp.wrap(block.timestamp).epochFromTimestamp(); + ValidatorSelectionLib.setupEpoch(StakingLib.getStorage(), currentEpoch); ManaBaseFeeComponents memory components = getManaBaseFeeComponentsAt(Timestamp.wrap(block.timestamp), true); diff --git a/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol b/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol index b58395276d9f..4b90f4201cb1 100644 --- a/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol +++ b/l1-contracts/src/core/libraries/validator-selection/ValidatorSelectionLib.sol @@ -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"); @@ -31,27 +35,33 @@ 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); } /** * @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]; - if (epoch.sampleSeed == 0) { - epoch.sampleSeed = getSampleSeed(epochNumber); - epoch.nextSeed = store.lastSeed = computeNextSeed(epochNumber); - epoch.committee = sampleValidators(_stakingStore, epochNumber, epoch.sampleSeed); + //################ Seeds ################ + // Get the sample seed for this current epoch. + uint224 sampleSeed = getSampleSeed(_epochNumber); + + // 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); } } @@ -159,7 +169,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) { @@ -187,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) @@ -194,23 +211,39 @@ 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); - } + /** + * @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 + // 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(); - // Emulate a sampling of the validators - uint256 sampleSeed = getSampleSeed(_epochNumber); + // If the sample seed for the next epoch is already set, we can skip the computation + if (mostRecentSeedEpoch == epoch) { + return; + } - return sampleValidators(_stakingStore, _epochNumber, sampleSeed); + // 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 +260,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; + uint224 sampleSeed = store.seeds.upperLookup(Epoch.unwrap(_epoch).toUint32()); + if (sampleSeed == 0) { + sampleSeed = type(uint224).max; } - - sampleSeed = store.epochs[_epoch - Epoch.wrap(1)].nextSeed; - if (sampleSeed != 0) { - return sampleSeed; - } - - return store.lastSeed; + return sampleSeed; } function getStorage() internal pure returns (ValidatorSelectionStorage storage storageStruct) { @@ -262,8 +286,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 30120c97ddd0..61a8503cbf8d 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol @@ -11,132 +11,37 @@ 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 {HeaderLib} from "@aztec/core/libraries/rollup/HeaderLib.sol"; import { ProposeArgs, - ProposePayload, OracleInput, - ProposeLib + 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 {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/slashing/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.slotNumber; - uint256 initialTime = - full.block.decodedHeader.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); @@ -159,7 +64,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 @@ -181,7 +86,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 @@ -195,11 +100,20 @@ 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 + assertEq(preCommittee, postCommittee, "Committee elements have changed"); } - 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); @@ -212,12 +126,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. @@ -251,11 +165,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..b179538605b0 --- /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/slashing/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 Setup contracts needed for the tests with the a given number of validators + */ + modifier setup(uint256 _validatorCount) { + string memory _name = "mixed_block_1"; + { + DecoderBase.Full memory full = load(_name); + uint256 slotNumber = full.block.decodedHeader.slotNumber; + uint256 initialTime = + full.block.decodedHeader.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..c62f556972ae --- /dev/null +++ b/l1-contracts/test/validator-selection/setupEpoch.t.sol @@ -0,0 +1,205 @@ +// 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" + ); + } + + 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" + ); + 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); + + // Sample the validator set for the current epoch + address[] memory committeeAfterAddingExtraValidators = + IValidatorSelection(address(rollup)).getCurrentEpochCommittee(); + assertEq(committeeAfterAddingExtraValidators, initialCommittee, "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)); + assertEq(committeeAfterJumpingIntoFuture, initialCommittee, "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..f8be8b7dd80b --- /dev/null +++ b/l1-contracts/test/validator-selection/setupEpoch.tree @@ -0,0 +1,15 @@ +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 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 +