Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion l1-contracts/script/InternalGov.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion l1-contracts/script/StakingAssetHandler.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
4 changes: 4 additions & 0 deletions l1-contracts/src/core/Rollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
10 changes: 10 additions & 0 deletions l1-contracts/src/core/RollupCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 7 additions & 1 deletion l1-contracts/src/core/interfaces/IValidatorSelection.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);

Expand Down
2 changes: 2 additions & 0 deletions l1-contracts/src/core/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -709,13 +745,21 @@ 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;

// Only clamp to a floor that is already sealed. GSE snapshots are keyed by timestamp and an
// equal-key checkpoint write overwrites in place, so the set "at" the current block is still
// mutable: anyone could exit after the committee was latched and change what the same sample
// time reports. A floor one block back can no longer be written to.
uint32 floor = store.validatorSetSampleFloor;
return (ts < floor && floor < block.timestamp) ? floor : ts;
}

/**
Expand Down
11 changes: 10 additions & 1 deletion l1-contracts/src/periphery/RegisterNewRollupVersionPayload.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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))});
Expand All @@ -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;
}

Expand Down
4 changes: 3 additions & 1 deletion l1-contracts/test/governance/scenario/AddRollup.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -45,6 +45,8 @@ contract BadRollup {
function getGSE() external view returns (GSE) {
return GSE(address(gse));
}

function setValidatorSetSampleFloor() external {}
}

contract AddRollupTest is TestBase {
Expand Down

This file was deleted.

Loading
Loading