From ea11e5e65d8b844055d74ea5befc08b67d938f24 Mon Sep 17 00:00:00 2001 From: Amin Sammara Date: Mon, 5 Oct 2026 13:54:14 +0700 Subject: [PATCH 1/2] feat(l1): let governance pin a validator-set sample floor Committee selection samples the validator set as it stood `lagInEpochsForValidatorSet` epochs before an epoch starts. The GSE credits the shared bonus bucket to whichever rollup was its latest *at the queried timestamp*, and never backdates that attribution when a new rollup is added. So a rollup that governance has just made canonical owns every inherited validator for a query at `now`, but sees an empty set at the lagged timestamp sampling actually reads. `setupEpoch` reverts with `InsufficientValidatorSetSize(0, targetCommitteeSize)` for the cutover epoch and the next `lag` epochs, which is also every ordinary `propose` in them. On mainnet values (lag 2, 2304s epochs, 3059 attesters all in the bonus bucket) that is a bounded but automatic ~1h40m stall at every upgrade. `setValidatorSetSampleFloor` lets governance pin the timestamp at which the inheritance took effect, and sampling clamps to it. The floor is unset (zero) by default and a zero floor never clamps, so deployed rollups and deploy scripts are unaffected. The floor is pinned in the same transaction that makes the rollup canonical, rather than latched lazily on the first `setupEpoch`: - `setupEpoch` is permissionless, so whoever called it first would choose the cutover epoch's committee from a menu of inclusion timestamps. - `propose` carries committee attestations gathered off-chain beforehand, which is impossible for a committee that is not determined until the transaction lands. Both are avoided by establishing the floor atomically with the event that creates the need for it. The setter is one-shot and refuses to pin a floor at which the GSE does not already report this rollup as latest, so a payload that orders it before `GSE.addRollup` reverts rather than storing a floor that resolves to the previous rollup. The clamp is strictly more conservative than the lag it stands in for: the sample still predates the epoch being sampled, so no one can stake after the upgrade and be selected. Clamped epochs share a validator set but still mix a per-epoch randao, so their committees differ. --- l1-contracts/src/core/Rollup.sol | 4 + l1-contracts/src/core/RollupCore.sol | 10 + .../core/interfaces/IValidatorSelection.sol | 8 +- l1-contracts/src/core/libraries/Errors.sol | 2 + .../rollup/ValidatorOperationsExtLib.sol | 9 + .../rollup/ValidatorSelectionLib.sol | 45 +++- .../scenario/ValidatorSetSampleFloor.t.sol | 218 ++++++++++++++++++ 7 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 l1-contracts/test/governance/scenario/ValidatorSetSampleFloor.t.sol diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 62f344221f3..2ef3925190a 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -450,6 +450,10 @@ contract Rollup is IStaking, IValidatorSelection, IRollup, RollupCore { return ValidatorOperationsExtLib.getLagInEpochsForValidatorSet(); } + function getValidatorSetSampleFloor() external view override(IValidatorSelection) returns (uint256) { + return ValidatorOperationsExtLib.getValidatorSetSampleFloor(); + } + function getLagInEpochsForRandao() external view override(IValidatorSelection) returns (uint256) { return ValidatorOperationsExtLib.getLagInEpochsForRandao(); } diff --git a/l1-contracts/src/core/RollupCore.sol b/l1-contracts/src/core/RollupCore.sol index 56527e4d2ff..410f889c480 100644 --- a/l1-contracts/src/core/RollupCore.sol +++ b/l1-contracts/src/core/RollupCore.sol @@ -405,6 +405,16 @@ contract RollupCore is EIP712("Aztec Rollup", "1"), Ownable, IStakingCore, IVali ValidatorOperationsExtLib.setEscapeHatch(_escapeHatch); } + /** + * @notice Pins the earliest timestamp that validator-set sampling may read from + * @dev Called by governance in the transaction that makes this rollup canonical, after + * `GSE.addRollup`. One-shot, and rejected while the GSE's latest rollup is not this one. + * See {ValidatorSelectionLib.setValidatorSetSampleFloor} for why the floor is needed. + */ + function setValidatorSetSampleFloor() external override(IValidatorSelectionCore) onlyOwner { + ValidatorOperationsExtLib.setValidatorSetSampleFloor(); + } + /** * @notice Claims accumulated rewards for a sequencer (checkpoint proposer) * @dev Transfers all accumulated rewards to the recipient. diff --git a/l1-contracts/src/core/interfaces/IValidatorSelection.sol b/l1-contracts/src/core/interfaces/IValidatorSelection.sol index 8607c43b026..b9a4519c9a5 100644 --- a/l1-contracts/src/core/interfaces/IValidatorSelection.sol +++ b/l1-contracts/src/core/interfaces/IValidatorSelection.sol @@ -12,20 +12,25 @@ struct ValidatorSelectionStorage { mapping(Epoch => bytes32 committeeCommitment) committeeCommitments; // Checkpointed map of epoch -> randao value Checkpoints.Trace224 randaos; - // The following 3 uint32s pack into a single slot (12 bytes) + // The following 4 uint32s pack into a single slot (16 bytes) uint32 targetCommitteeSize; uint32 lagInEpochsForValidatorSet; uint32 lagInEpochsForRandao; + // Lower bound on the validator-set sample timestamp. Zero until governance pins it, and zero + // leaves sampling untouched. See ValidatorSelectionLib.setValidatorSetSampleFloor. + uint32 validatorSetSampleFloor; // Checkpointed escape hatch addresses (key = timestamp, value = address as uint160) Checkpoints.Trace160 escapeHatchCheckpoints; } interface IValidatorSelectionCore { event EscapeHatchSet(address escapeHatch); + event ValidatorSetSampleFloorSet(uint32 floor); function setupEpoch() external; function checkpointRandao() external; function setEscapeHatch(address _escapeHatch) external; + function setValidatorSetSampleFloor() external; } interface IValidatorSelection is IValidatorSelectionCore, IEmperor { @@ -48,6 +53,7 @@ interface IValidatorSelection is IValidatorSelectionCore, IEmperor { function getSampleSeedAt(Timestamp _ts) external view returns (uint256); function getSamplingSizeAt(Timestamp _ts) external view returns (uint256); function getLagInEpochsForValidatorSet() external view returns (uint256); + function getValidatorSetSampleFloor() external view returns (uint256); function getLagInEpochsForRandao() external view returns (uint256); function getCurrentSampleSeed() external view returns (uint256); diff --git a/l1-contracts/src/core/libraries/Errors.sol b/l1-contracts/src/core/libraries/Errors.sol index b3d0b82f55b..5431eaf205d 100644 --- a/l1-contracts/src/core/libraries/Errors.sol +++ b/l1-contracts/src/core/libraries/Errors.sol @@ -150,6 +150,8 @@ library Errors { error ValidatorSelection__EscapeHatchAlreadySet(); error ValidatorSelection__EscapeHatchCannotBeZero(); error ValidatorSelection__EscapeHatchRollupMismatch(address expected, address actual); + error ValidatorSelection__SampleFloorAlreadySet(); + error ValidatorSelection__NotLatestRollupInGSE(address expected, address actual); // Staking error Staking__AlreadyQueued(address _attester); diff --git a/l1-contracts/src/core/libraries/rollup/ValidatorOperationsExtLib.sol b/l1-contracts/src/core/libraries/rollup/ValidatorOperationsExtLib.sol index 737b764855f..038bb9b6efe 100644 --- a/l1-contracts/src/core/libraries/rollup/ValidatorOperationsExtLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ValidatorOperationsExtLib.sol @@ -101,6 +101,11 @@ library ValidatorOperationsExtLib { emit IValidatorSelectionCore.EscapeHatchSet(_escapeHatch); } + function setValidatorSetSampleFloor() external { + ValidatorSelectionLib.setValidatorSetSampleFloor(); + emit IValidatorSelectionCore.ValidatorSetSampleFloorSet(uint32(ValidatorSelectionLib.getValidatorSetSampleFloor())); + } + function invalidateBadAttestation( uint256 _checkpointNumber, CommitteeAttestations memory _attestations, @@ -150,6 +155,10 @@ library ValidatorOperationsExtLib { return ValidatorSelectionLib.getLagInEpochsForValidatorSet(); } + function getValidatorSetSampleFloor() external view returns (uint256) { + return ValidatorSelectionLib.getValidatorSetSampleFloor(); + } + function getLagInEpochsForRandao() external view returns (uint256) { return ValidatorSelectionLib.getLagInEpochsForRandao(); } diff --git a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol index c57f4319d73..a3901f019a5 100644 --- a/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ValidatorSelectionLib.sol @@ -178,6 +178,38 @@ library ValidatorSelectionLib { store.escapeHatchCheckpoints.push(nextEpochTs, uint160(_escapeHatch)); } + /** + * @notice Pins the earliest timestamp that validator-set sampling may read from + * @dev Committee selection reads the validator set as it stood `lagInEpochsForValidatorSet` epochs + * before the epoch starts. The GSE credits the shared "bonus" validators to whichever rollup + * was its latest at the queried timestamp, and that attribution is historical: it is not + * backdated when a new rollup is added. So for the first `lag + 1` epochs after an upgrade the + * lagged timestamp still resolves to the previous rollup, this rollup's own bucket is empty, + * and sampling reverts with `InsufficientValidatorSetSize` even though every validator has + * already been inherited. + * + * Pinning a floor here lets those epochs sample at the moment the inheritance took effect + * instead. The floor is in the past, so the committee for any given epoch is still fixed and + * agreed by every observer. It is also strictly more conservative than the lag it replaces -- + * the sample predates the epoch being sampled, so no one can stake after the upgrade and be + * selected. + * + * Governance calls this in the transaction that makes the rollup canonical, after + * `GSE.addRollup`. Ordering it earlier pins a timestamp that still resolves to the previous + * rollup, which is why that case is rejected rather than silently stored. + * @custom:reverts Errors.ValidatorSelection__SampleFloorAlreadySet if a floor is already pinned + * @custom:reverts Errors.ValidatorSelection__NotLatestRollupInGSE if the GSE's latest is not us + */ + function setValidatorSetSampleFloor() internal { + ValidatorSelectionStorage storage store = getStorage(); + require(store.validatorSetSampleFloor == 0, Errors.ValidatorSelection__SampleFloorAlreadySet()); + + address latest = StakingLib.getStorage().gse.getLatestRollup(); + require(latest == address(this), Errors.ValidatorSelection__NotLatestRollupInGSE(address(this), latest)); + + store.validatorSetSampleFloor = block.timestamp.toUint32(); + } + /** * @notice Performs epoch setup by sampling the committee and setting future seeds * @dev This function handles the epoch transition by: @@ -597,6 +629,10 @@ library ValidatorSelectionLib { return getStorage().lagInEpochsForValidatorSet; } + function getValidatorSetSampleFloor() internal view returns (uint256) { + return getStorage().validatorSetSampleFloor; + } + function getLagInEpochsForRandao() internal view returns (uint256) { return getStorage().lagInEpochsForRandao; } @@ -709,13 +745,18 @@ library ValidatorSelectionLib { } function stableEpochToValidatorSetSampleTime(Epoch _epoch) private view returns (uint32) { - uint32 sub = getStorage().lagInEpochsForValidatorSet * TimeLib.getEpochDurationInSeconds().toUint32(); + ValidatorSelectionStorage storage store = getStorage(); + uint32 sub = store.lagInEpochsForValidatorSet * TimeLib.getEpochDurationInSeconds().toUint32(); uint32 ts = Timestamp.unwrap(_epoch.toTimestamp()).toUint32() - sub; + // Stability is judged on the unclamped time, so an epoch is stable exactly when it was before. require( ts <= block.timestamp, Errors.ValidatorSelection__EpochNotStable(uint256(Epoch.unwrap(_epoch)), uint32(block.timestamp)) ); - return ts; + + // The floor is only ever a past timestamp, so this cannot raise `ts` above the current block. + uint32 floor = store.validatorSetSampleFloor; + return ts < floor ? floor : ts; } /** diff --git a/l1-contracts/test/governance/scenario/ValidatorSetSampleFloor.t.sol b/l1-contracts/test/governance/scenario/ValidatorSetSampleFloor.t.sol new file mode 100644 index 00000000000..4b0fd1201e6 --- /dev/null +++ b/l1-contracts/test/governance/scenario/ValidatorSetSampleFloor.t.sol @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: UNLICENSED +// solhint-disable +pragma solidity >=0.8.27; + +import {TestBase} from "@test/base/Base.sol"; +import {Rollup} from "@aztec/core/Rollup.sol"; +import {Registry} from "@aztec/governance/Registry.sol"; +import {GSE} from "@aztec/governance/GSE.sol"; +import {TestERC20} from "@aztec/mock/TestERC20.sol"; +import {RollupConfigInput} from "@aztec/core/interfaces/IRollup.sol"; +import {IHaveVersion} from "@aztec/governance/interfaces/IRegistry.sol"; +import {Errors} from "@aztec/core/libraries/Errors.sol"; +import {Epoch, Timestamp} from "@aztec/core/libraries/TimeLib.sol"; +import {StakingQueueConfig} from "@aztec/core/libraries/compressed-data/StakingQueueConfig.sol"; +import {MultiAdder, CheatDepositArgs} from "@aztec/mock/MultiAdder.sol"; +import {BN254Lib} from "@aztec/shared/libraries/BN254Lib.sol"; +import {Ownable} from "@oz/access/Ownable.sol"; +import {RollupBuilder} from "../../builder/RollupBuilder.sol"; +import {TestConstants} from "../../harnesses/TestConstants.sol"; + +/** + * Every validator here deposits into the GSE's shared "bonus" bucket, which is how mainnet is + * staked. That bucket is credited to whichever rollup the GSE considers latest *at the timestamp + * being queried*, and the attribution is never backdated. So a rollup that has just been made + * canonical owns the whole validator set for a query at `now`, yet sees an empty set at the lagged + * timestamp its committee sampling actually reads from. + */ +contract ValidatorSetSampleFloorTest is TestBase { + uint256 internal constant VALIDATOR_COUNT = 48; + + Registry internal registry; + GSE internal gse; + TestERC20 internal token; + Rollup internal oldRollup; + Rollup internal newRollup; + + uint256 internal targetCommitteeSize; + uint256 internal lag; + uint256 internal epochSeconds; + + function setUp() external { + vm.warp(1_000_000); + + StakingQueueConfig memory queueConfig = TestConstants.getStakingQueueConfig(); + queueConfig.normalFlushSizeMin = VALIDATOR_COUNT; + + RollupBuilder builder = new RollupBuilder(address(this)).setStakingQueueConfig(queueConfig) + .setValidators(_validators("validator", VALIDATOR_COUNT)); + builder.deploy(); + + oldRollup = Rollup(address(builder.getConfig().rollup)); + registry = builder.getConfig().registry; + gse = builder.getConfig().gse; + token = builder.getConfig().testERC20; + + // The version is a hash of the config, so the successor needs a config that differs somewhere or + // the Registry rejects it as already registered. `setMakeGovernance(false)` keeps the successor + // from standing up a second Governance and repointing the GSE at it, which would orphan the + // stake; `setUpdateOwnerships` then leaves this contract owning the rollup and the GSE, standing + // in for governance. + RollupConfigInput memory successorConfig = TestConstants.getRollupConfigInput(); + successorConfig.version = successorConfig.version + 1; + + RollupBuilder successorBuilder = new RollupBuilder(address(this)).setRegistry(registry).setGSE(gse) + .setTestERC20(token).setRollupConfigInput(successorConfig).setStakingQueueConfig(queueConfig) + .setMakeCanonical(false).setMakeGovernance(false); + successorBuilder.deploy(); + newRollup = Rollup(address(successorBuilder.getConfig().rollup)); + + targetCommitteeSize = oldRollup.getTargetCommitteeSize(); + lag = oldRollup.getLagInEpochsForValidatorSet(); + epochSeconds = oldRollup.getEpochDuration() * oldRollup.getSlotDuration(); + + assertGt(targetCommitteeSize, 0, "fixture needs a committee"); + assertGt(lag, 0, "fixture needs a lag"); + + // Let enough history accumulate that the outgoing rollup samples normally, and land partway + // into an epoch rather than on a boundary. On a boundary the sample time of epoch + // `cutover + lag` lands exactly on the cutover and already resolves to the successor, so the + // stall is one epoch shorter; a governance execution is not going to be that well aligned. + vm.warp(block.timestamp + (lag + 1) * epochSeconds + epochSeconds / 3); + + assertEq(gse.supplyOf(address(oldRollup)), 0, "validators must sit in the bonus bucket"); + assertEq( + oldRollup.getEpochCommittee(oldRollup.getCurrentEpoch()).length, + targetCommitteeSize, + "outgoing rollup should have a committee" + ); + } + + function _validators(string memory _salt, uint256 _count) internal pure returns (CheatDepositArgs[] memory) { + CheatDepositArgs[] memory out = new CheatDepositArgs[](_count); + for (uint256 i = 0; i < _count; i++) { + address validator = vm.addr(uint256(keccak256(abi.encode(_salt, i)))); + out[i] = CheatDepositArgs({ + attester: validator, + withdrawer: validator, + publicKeyInG1: BN254Lib.g1Zero(), + publicKeyInG2: BN254Lib.g2Zero(), + proofOfPossession: BN254Lib.g1Zero() + }); + } + return out; + } + + /// The two actions every upgrade payload has always had, and nothing else. + function _cutover() internal { + registry.addRollup(IHaveVersion(address(newRollup))); + gse.addRollup(address(newRollup)); + } + + function test_WhenNoFloorIsPinned_TheSuccessorCannotFormACommittee() external { + _cutover(); + + assertEq( + gse.getAttesterCountAtTime(address(newRollup), Timestamp.wrap(block.timestamp)), + VALIDATOR_COUNT, + "the successor has inherited every validator as of now" + ); + + for (uint256 k = 0; k <= lag; k++) { + Epoch epoch = newRollup.getCurrentEpoch(); + vm.expectRevert( + abi.encodeWithSelector(Errors.ValidatorSelection__InsufficientValidatorSetSize.selector, 0, targetCommitteeSize) + ); + newRollup.getEpochCommittee(epoch); + vm.warp(block.timestamp + epochSeconds); + } + + // The stall ends on its own once the lagged sample time passes the cutover. + assertEq(newRollup.getEpochCommittee(newRollup.getCurrentEpoch()).length, targetCommitteeSize); + } + + function test_WhenAFloorIsPinned_TheSuccessorFormsACommitteeImmediately() external { + _cutover(); + newRollup.setValidatorSetSampleFloor(); + + for (uint256 k = 0; k <= lag; k++) { + assertEq( + newRollup.getEpochCommittee(newRollup.getCurrentEpoch()).length, + targetCommitteeSize, + "successor should have a committee from the cutover epoch onwards" + ); + vm.warp(block.timestamp + epochSeconds); + } + } + + /// The clamped epochs read one validator set but mix a different randao, so their committees must + /// not collapse onto each other. + function test_ClampedEpochsDoNotShareACommittee() external { + _cutover(); + newRollup.setValidatorSetSampleFloor(); + + bytes32 first = keccak256(abi.encode(newRollup.getEpochCommittee(newRollup.getCurrentEpoch()))); + vm.warp(block.timestamp + epochSeconds); + bytes32 second = keccak256(abi.encode(newRollup.getEpochCommittee(newRollup.getCurrentEpoch()))); + + assertNotEq(first, second, "clamped epochs should still sample distinct committees"); + } + + /// Once the lagged sample time overtakes the floor the clamp has to stop mattering, so validators + /// who joined after the floor must show up exactly as they would have without it. + function test_TheClampBecomesInertOnceTheLagClears() external { + _cutover(); + newRollup.setValidatorSetSampleFloor(); + assertEq(newRollup.getSamplingSizeAt(Timestamp.wrap(block.timestamp)), VALIDATOR_COUNT); + + CheatDepositArgs[] memory extra = _validators("late-validator", 4); + MultiAdder adder = new MultiAdder(address(newRollup), address(this)); + vm.prank(token.owner()); + token.addMinter(address(this)); + token.mint(address(adder), gse.ACTIVATION_THRESHOLD() * extra.length); + adder.addValidators(extra); + + vm.warp(block.timestamp + (lag + 2) * epochSeconds); + + assertEq( + newRollup.getSamplingSizeAt(Timestamp.wrap(block.timestamp)), + VALIDATOR_COUNT + extra.length, + "sampling must track the validator set again once the floor is behind the lagged time" + ); + } + + function test_WhenOrderedBeforeTheGseAction_ItReverts() external { + registry.addRollup(IHaveVersion(address(newRollup))); + + vm.expectRevert( + abi.encodeWithSelector( + Errors.ValidatorSelection__NotLatestRollupInGSE.selector, address(newRollup), address(oldRollup) + ) + ); + newRollup.setValidatorSetSampleFloor(); + } + + function test_ItIsOneShot() external { + _cutover(); + newRollup.setValidatorSetSampleFloor(); + + vm.expectRevert(abi.encodeWithSelector(Errors.ValidatorSelection__SampleFloorAlreadySet.selector)); + newRollup.setValidatorSetSampleFloor(); + } + + function test_ItIsOwnerOnly() external { + _cutover(); + + address stranger = address(uint160(bytes20("stranger"))); + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, stranger)); + newRollup.setValidatorSetSampleFloor(); + } + + function test_TheFloorIsReadableAfterwards() external { + assertEq(newRollup.getValidatorSetSampleFloor(), 0, "unset by default"); + _cutover(); + newRollup.setValidatorSetSampleFloor(); + assertEq(newRollup.getValidatorSetSampleFloor(), block.timestamp, "floor is the cutover timestamp"); + } +} From 2534dc61eaa01adfbe340a3589385d3a866cb50d Mon Sep 17 00:00:00 2001 From: Amin Sammara Date: Mon, 5 Oct 2026 13:54:25 +0700 Subject: [PATCH 2/2] feat(l1): pin the sample floor from RegisterNewRollupVersionPayload Every rollup upgrade runs the same two governance actions, `Registry.addRollup` and `GSE.addRollup`, so the committee stall they cause is best fixed once here rather than re-remembered in each bespoke upgrade payload. The new action has to follow `GSE.addRollup`: until that runs, the GSE still reports the previous rollup as its latest and the setter refuses the floor. All three actions execute in one transaction, so the pinned floor is exactly the cutover timestamp wherever it sits in the list. `test/governance/scenario/RegisterNewRollupVersionPayload.sol` was a verbatim copy of the production payload, and the two would now drift. Dropped in favour of the real one, which puts the new action through the full governance lifecycle in `AddRollup.t.sol` -- signalling, voting, queueing and `Governance.execute` -- instead of leaving it covered only by direct calls. `InternalGov.s.sol` and `StakingAssetHandler.s.sol` imported the test copy and now take the production one too. --- l1-contracts/script/InternalGov.s.sol | 2 +- l1-contracts/script/StakingAssetHandler.s.sol | 2 +- .../RegisterNewRollupVersionPayload.sol | 11 +++++- .../test/governance/scenario/AddRollup.t.sol | 4 +- .../RegisterNewRollupVersionPayload.sol | 39 ------------------- 5 files changed, 15 insertions(+), 43 deletions(-) delete mode 100644 l1-contracts/test/governance/scenario/RegisterNewRollupVersionPayload.sol diff --git a/l1-contracts/script/InternalGov.s.sol b/l1-contracts/script/InternalGov.s.sol index 233226c77e5..cbb18fff1fd 100644 --- a/l1-contracts/script/InternalGov.s.sol +++ b/l1-contracts/script/InternalGov.s.sol @@ -12,7 +12,7 @@ import {IRollup} from "@aztec/core/interfaces/IRollup.sol"; import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol"; import {IValidatorSelection} from "@aztec/core/interfaces/IValidatorSelection.sol"; import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; -import {RegisterNewRollupVersionPayload} from "../test/governance/scenario/RegisterNewRollupVersionPayload.sol"; +import {RegisterNewRollupVersionPayload} from "@aztec/periphery/RegisterNewRollupVersionPayload.sol"; import {Ownable} from "@oz/access/Ownable.sol"; import {Fakerollup} from "../test/governance/governance-proposer/mocks/Fakerollup.sol"; import {StakingAssetHandler} from "../src/mock/StakingAssetHandler.sol"; diff --git a/l1-contracts/script/StakingAssetHandler.s.sol b/l1-contracts/script/StakingAssetHandler.s.sol index ab9aa6f6aab..465812a4a8d 100644 --- a/l1-contracts/script/StakingAssetHandler.s.sol +++ b/l1-contracts/script/StakingAssetHandler.s.sol @@ -11,7 +11,7 @@ import {IRollup} from "@aztec/core/interfaces/IRollup.sol"; import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol"; import {IValidatorSelection} from "@aztec/core/interfaces/IValidatorSelection.sol"; import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; -import {RegisterNewRollupVersionPayload} from "../test/governance/scenario/RegisterNewRollupVersionPayload.sol"; +import {RegisterNewRollupVersionPayload} from "@aztec/periphery/RegisterNewRollupVersionPayload.sol"; import {ProposalLib} from "@aztec/governance/libraries/ProposalLib.sol"; import {Ownable} from "@oz/access/Ownable.sol"; import {Fakerollup} from "../test/governance/governance-proposer/mocks/Fakerollup.sol"; diff --git a/l1-contracts/src/periphery/RegisterNewRollupVersionPayload.sol b/l1-contracts/src/periphery/RegisterNewRollupVersionPayload.sol index 96264266587..5d376bf4772 100644 --- a/l1-contracts/src/periphery/RegisterNewRollupVersionPayload.sol +++ b/l1-contracts/src/periphery/RegisterNewRollupVersionPayload.sol @@ -3,6 +3,7 @@ pragma solidity >=0.8.27; import {IInstance} from "@aztec/core/interfaces/IInstance.sol"; +import {IValidatorSelectionCore} from "@aztec/core/interfaces/IValidatorSelection.sol"; import {IGSECore} from "@aztec/governance/GSE.sol"; import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol"; @@ -34,7 +35,7 @@ contract RegisterNewRollupVersionPayload is IPayload { * @return The array of actions to execute */ function getActions() external view override(IPayload) returns (IPayload.Action[] memory) { - IPayload.Action[] memory res = new IPayload.Action[](2); + IPayload.Action[] memory res = new IPayload.Action[](3); res[0] = Action({target: address(REGISTRY), data: abi.encodeWithSelector(IRegistry.addRollup.selector, address(ROLLUP))}); @@ -43,6 +44,14 @@ contract RegisterNewRollupVersionPayload is IPayload { target: address(ROLLUP.getGSE()), data: abi.encodeWithSelector(IGSECore.addRollup.selector, address(ROLLUP)) }); + // Pins the validator-set sample floor to this transaction's timestamp, so the rollup can form a + // committee from its inherited validators immediately instead of stalling for `lag + 1` epochs. + // MUST stay after `GSE.addRollup`: until that action runs the GSE still reports the previous + // rollup as its latest, and the setter rejects a floor pinned at such a timestamp. + res[2] = Action({ + target: address(ROLLUP), data: abi.encodeWithSelector(IValidatorSelectionCore.setValidatorSetSampleFloor.selector) + }); + return res; } diff --git a/l1-contracts/test/governance/scenario/AddRollup.t.sol b/l1-contracts/test/governance/scenario/AddRollup.t.sol index 181235762e8..9a30cc8950d 100644 --- a/l1-contracts/test/governance/scenario/AddRollup.t.sol +++ b/l1-contracts/test/governance/scenario/AddRollup.t.sol @@ -25,7 +25,7 @@ import {RollupBuilder} from "../../builder/RollupBuilder.sol"; import {IGSE, GSE} from "@aztec/governance/GSE.sol"; import {GSEPayload} from "@aztec/governance/GSEPayload.sol"; import {FakeRollup} from "../governance/TestPayloads.sol"; -import {RegisterNewRollupVersionPayload} from "./RegisterNewRollupVersionPayload.sol"; +import {RegisterNewRollupVersionPayload} from "@aztec/periphery/RegisterNewRollupVersionPayload.sol"; import {IInstance} from "@aztec/core/interfaces/IInstance.sol"; import {StakingQueueConfig} from "@aztec/core/libraries/compressed-data/StakingQueueConfig.sol"; import {BN254Lib, G1Point, G2Point} from "@aztec/shared/libraries/BN254Lib.sol"; @@ -45,6 +45,8 @@ contract BadRollup { function getGSE() external view returns (GSE) { return GSE(address(gse)); } + + function setValidatorSetSampleFloor() external {} } contract AddRollupTest is TestBase { diff --git a/l1-contracts/test/governance/scenario/RegisterNewRollupVersionPayload.sol b/l1-contracts/test/governance/scenario/RegisterNewRollupVersionPayload.sol deleted file mode 100644 index 8e97954dd27..00000000000 --- a/l1-contracts/test/governance/scenario/RegisterNewRollupVersionPayload.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.27; - -import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; -import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol"; -import {IInstance} from "@aztec/core/interfaces/IInstance.sol"; -import {IGSECore} from "@aztec/governance/GSE.sol"; - -/** - * @title RegisterNewRollupVersionPayload - * @author Aztec Labs - * @notice A payload that registers a new rollup version. - */ -contract RegisterNewRollupVersionPayload is IPayload { - IRegistry public immutable REGISTRY; - IInstance public immutable ROLLUP; - - constructor(IRegistry _registry, IInstance _rollup) { - REGISTRY = _registry; - ROLLUP = _rollup; - } - - function getActions() external view override(IPayload) returns (IPayload.Action[] memory) { - IPayload.Action[] memory res = new IPayload.Action[](2); - - res[0] = - Action({target: address(REGISTRY), data: abi.encodeWithSelector(IRegistry.addRollup.selector, address(ROLLUP))}); - - res[1] = Action({ - target: address(ROLLUP.getGSE()), data: abi.encodeWithSelector(IGSECore.addRollup.selector, address(ROLLUP)) - }); - - return res; - } - - function getURI() external pure override(IPayload) returns (string memory) { - return "RegisterNewRollupVersionPayload"; - } -}