From e8cfed75cce442e4e7a1a7a41df3ccc95b1ff2b7 Mon Sep 17 00:00:00 2001 From: Lasse Herskind <16536249+LHerskind@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:11:08 +0000 Subject: [PATCH] feat!: leonidas library (#11596) Fixes #11521, slaying the king. Pushes a bunch of the functions that lived in the "rollup" our from the core, such that there now is a `RollupCore` with the logic that we need to run the rollup and `Rollup` extending it with a bunch of helpers and such. TBH, the `RollupCore` is still pretty disgusting to look at, but some of this should be helped as we split the state transition function checks further from the sequencer selection. Will alos allow us to get rid of some of the existing bundling libraries and improve clarity. Makes a tiny update in the forwarder and one of the tests for it. --- .../guides/developer_guides/js_apps/test.md | 2 +- l1-contracts/src/core/Rollup.sol | 1056 ++++++----------- l1-contracts/src/core/RollupCore.sol | 782 ++++++++++++ l1-contracts/src/core/ValidatorSelection.sol | 453 ------- l1-contracts/src/core/interfaces/IRollup.sol | 72 +- l1-contracts/src/core/interfaces/IStaking.sol | 4 +- .../core/interfaces/IValidatorSelection.sol | 9 +- l1-contracts/src/core/libraries/Errors.sol | 1 + .../libraries/RollupLibs/ValidationLib.sol | 4 +- .../ValidatorSelectionLib.sol | 226 ++-- .../src/core/libraries/staking/StakingLib.sol | 16 +- l1-contracts/src/periphery/Forwarder.sol | 6 +- l1-contracts/src/periphery/SlashPayload.sol | 4 +- l1-contracts/test/Forwarder.t.sol | 5 +- l1-contracts/test/Rollup.t.sol | 17 +- l1-contracts/test/base/Base.sol | 2 +- l1-contracts/test/fees/FeeRollup.t.sol | 1 - .../governance-proposer/executeProposal.t.sol | 8 +- .../governance-proposer/mocks/Fakerollup.sol | 42 + .../governance/governance-proposer/vote.t.sol | 8 +- .../test/harnesses/ValidatorSelection.sol | 21 - l1-contracts/test/portals/UniswapPortal.t.sol | 5 + l1-contracts/test/staking/deposit.t.sol | 4 +- .../test/staking/finaliseWithdraw.t.sol | 4 +- .../test/staking/initiateWithdraw.t.sol | 6 +- l1-contracts/test/staking/slash.t.sol | 8 +- .../ValidatorSelection.t.sol | 19 +- yarn-project/epoch-cache/src/epoch_cache.ts | 2 +- yarn-project/ethereum/src/contracts/rollup.ts | 2 +- 29 files changed, 1387 insertions(+), 1402 deletions(-) create mode 100644 l1-contracts/src/core/RollupCore.sol delete mode 100644 l1-contracts/src/core/ValidatorSelection.sol create mode 100644 l1-contracts/test/governance/governance-proposer/mocks/Fakerollup.sol delete mode 100644 l1-contracts/test/harnesses/ValidatorSelection.sol diff --git a/docs/docs/guides/developer_guides/js_apps/test.md b/docs/docs/guides/developer_guides/js_apps/test.md index 19f10a7c56d..d2f90b39668 100644 --- a/docs/docs/guides/developer_guides/js_apps/test.md +++ b/docs/docs/guides/developer_guides/js_apps/test.md @@ -134,7 +134,7 @@ The [`CheatCodes`](../../../reference/developer_references/sandbox_reference/che ### Set next block timestamp Since the rollup time is dependent on what "slot" the block is included in, time can be progressed by progressing slots. -The duration of a slot is available by calling `getSlotDuration()` on the Rollup (code in Leonidas.sol). +The duration of a slot is available by calling `getSlotDuration()` on the Rollup (code in Rollup.sol). You can then use the `warp` function on the EthCheatCodes to progress the underlying chain. diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 98811e1c8b6..cd5345c95d4 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -2,102 +2,57 @@ // Copyright 2024 Aztec Labs. pragma solidity >=0.8.27; -import {IFeeJuicePortal} from "@aztec/core/interfaces/IFeeJuicePortal.sol"; -import {IProofCommitmentEscrow} from "@aztec/core/interfaces/IProofCommitmentEscrow.sol"; +import {IRollup, ChainTips} from "@aztec/core/interfaces/IRollup.sol"; import { - IRollup, - ITestRollup, - CheatDepositArgs, + IStaking, + ValidatorInfo, + Exit, + OperatorInfo, + EnumerableSet +} from "@aztec/core/interfaces/IStaking.sol"; +import {IValidatorSelection} from "@aztec/core/interfaces/IValidatorSelection.sol"; + +// We allow the unused imports here as they make it much simpler to import the Rollup later +// solhint-disable no-unused-import +import { + RollupCore, + Config, + IRewardDistributor, + IFeeJuicePortal, + IERC20, + BlockLog, FeeHeader, ManaBaseFeeComponents, - BlockLog, - ChainTips, - RollupStore, - L1GasOracleValues, + SubmitEpochRootProofArgs, L1FeeData, - SubmitEpochRootProofArgs -} from "@aztec/core/interfaces/IRollup.sol"; -import {IVerifier} from "@aztec/core/interfaces/IVerifier.sol"; -import {IInbox} from "@aztec/core/interfaces/messagebridge/IInbox.sol"; -import {IOutbox} from "@aztec/core/interfaces/messagebridge/IOutbox.sol"; -import {Constants} from "@aztec/core/libraries/ConstantsGen.sol"; -import {MerkleLib} from "@aztec/core/libraries/crypto/MerkleLib.sol"; -import {Signature} from "@aztec/core/libraries/crypto/SignatureLib.sol"; -import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; -import {Errors} from "@aztec/core/libraries/Errors.sol"; -import { + ValidatorSelectionLib, + StakingLib, + TimeLib, + Slot, + Epoch, + Timestamp, + Errors, + Signature, + DataStructures, ExtRollupLib, - ValidateHeaderArgs, - Header, - SignedEpochProofQuote, - SubmitEpochRootProofInterimValues -} from "@aztec/core/libraries/RollupLibs/ExtRollupLib.sol"; -import {IntRollupLib, EpochProofQuote} from "@aztec/core/libraries/RollupLibs/IntRollupLib.sol"; -import {ProposeArgs, ProposeLib} from "@aztec/core/libraries/RollupLibs/ProposeLib.sol"; -import {Timestamp, Slot, Epoch, TimeLib} from "@aztec/core/libraries/TimeLib.sol"; -import {Inbox} from "@aztec/core/messagebridge/Inbox.sol"; -import {Outbox} from "@aztec/core/messagebridge/Outbox.sol"; -import {ProofCommitmentEscrow} from "@aztec/core/ProofCommitmentEscrow.sol"; -import {ValidatorSelection} from "@aztec/core/ValidatorSelection.sol"; -import {IRewardDistributor} from "@aztec/governance/interfaces/IRewardDistributor.sol"; -import {MockVerifier} from "@aztec/mock/MockVerifier.sol"; -import {Ownable} from "@oz/access/Ownable.sol"; -import {IERC20} from "@oz/token/ERC20/IERC20.sol"; -import {EIP712} from "@oz/utils/cryptography/EIP712.sol"; - -struct Config { - uint256 aztecSlotDuration; - uint256 aztecEpochDuration; - uint256 targetCommitteeSize; - uint256 aztecEpochProofClaimWindowInL2Slots; - uint256 minimumStake; - uint256 slashingQuorum; - uint256 slashingRoundSize; -} + IntRollupLib +} from "./RollupCore.sol"; +// solhint-enable no-unused-import /** * @title Rollup * @author Aztec Labs - * @notice Rollup contract that is concerned about readability and velocity of development - * not giving a damn about gas costs. - * @dev WARNING: This contract is VERY close to the size limit (500B at time of writing). + * @notice A wrapper contract around the RollupCore which provides additional view functions + * which are not needed by the rollup itself to function, but makes it easy to reason + * about the state of the rollup and test it. */ -contract Rollup is EIP712("Aztec Rollup", "1"), Ownable, ValidatorSelection, IRollup, ITestRollup { - using ProposeLib for ProposeArgs; - using IntRollupLib for uint256; - using IntRollupLib for ManaBaseFeeComponents; - - Slot public constant LIFETIME = Slot.wrap(5); - Slot public constant LAG = Slot.wrap(2); - - // See https://github.com/AztecProtocol/engineering-designs/blob/main/in-progress/8401-proof-timeliness/proof-timeliness.ipynb - // for justification of CLAIM_DURATION_IN_L2_SLOTS. - uint256 public constant PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST = 1000; - - // A Cuauhxicalli [kʷaːʍʃiˈkalːi] ("eagle gourd bowl") is a ceremonial Aztec vessel or altar used to hold offerings, - // such as sacrificial hearts, during rituals performed within temples. - address public constant CUAUHXICALLI = address(bytes20("CUAUHXICALLI")); - - address public constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); - bool public immutable IS_FOUNDRY_TEST; - // @note Always true, exists to override to false for testing only - bool public checkBlob = true; - - uint256 public immutable CLAIM_DURATION_IN_L2_SLOTS; - uint256 public immutable L1_BLOCK_AT_GENESIS; - IInbox public immutable INBOX; - IOutbox public immutable OUTBOX; - IProofCommitmentEscrow public immutable PROOF_COMMITMENT_ESCROW; - uint256 public immutable VERSION; - IFeeJuicePortal public immutable FEE_JUICE_PORTAL; - IRewardDistributor public immutable REWARD_DISTRIBUTOR; - IERC20 public immutable ASSET; +contract Rollup is IStaking, IValidatorSelection, IRollup, RollupCore { + using EnumerableSet for EnumerableSet.AddressSet; - RollupStore internal rollupStore; - - // @note Assume that all blocks up to this value (inclusive) are automatically proven. Speeds up bootstrapping. - // Testing only. This should be removed eventually. - uint256 private assumeProvenThroughBlockNumber; + using TimeLib for Timestamp; + using TimeLib for Slot; + using TimeLib for Epoch; + using IntRollupLib for ManaBaseFeeComponents; constructor( IFeeJuicePortal _fpcJuicePortal, @@ -108,207 +63,51 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Ownable, ValidatorSelection, IRo address _ares, Config memory _config ) - Ownable(_ares) - ValidatorSelection( + RollupCore( + _fpcJuicePortal, + _rewardDistributor, _stakingAsset, - _config.minimumStake, - _config.slashingQuorum, - _config.slashingRoundSize, - _config.aztecSlotDuration, - _config.aztecEpochDuration, - _config.targetCommitteeSize + _vkTreeRoot, + _protocolContractTreeRoot, + _ares, + _config ) - { - FEE_JUICE_PORTAL = _fpcJuicePortal; - REWARD_DISTRIBUTOR = _rewardDistributor; - ASSET = _fpcJuicePortal.UNDERLYING(); - PROOF_COMMITMENT_ESCROW = new ProofCommitmentEscrow( - ASSET, address(this), _config.aztecSlotDuration, _config.aztecEpochDuration - ); - INBOX = IInbox(address(new Inbox(address(this), Constants.L1_TO_L2_MSG_SUBTREE_HEIGHT))); - OUTBOX = IOutbox(address(new Outbox(address(this)))); - VERSION = 1; - L1_BLOCK_AT_GENESIS = block.number; - CLAIM_DURATION_IN_L2_SLOTS = _config.aztecEpochProofClaimWindowInL2Slots; - - IS_FOUNDRY_TEST = VM_ADDRESS.code.length > 0; - - rollupStore.epochProofVerifier = new MockVerifier(); - rollupStore.vkTreeRoot = _vkTreeRoot; - rollupStore.protocolContractTreeRoot = _protocolContractTreeRoot; - - // Genesis block - rollupStore.blocks[0] = BlockLog({ - feeHeader: FeeHeader({ - excessMana: 0, - feeAssetPriceNumerator: 0, - manaUsed: 0, - provingCostPerManaNumerator: 0, - congestionCost: 0 - }), - archive: bytes32(Constants.GENESIS_ARCHIVE_ROOT), - blockHash: bytes32(Constants.GENESIS_BLOCK_HASH), - slotNumber: Slot.wrap(0) - }); - rollupStore.l1GasOracleValues = L1GasOracleValues({ - pre: L1FeeData({baseFee: 1 gwei, blobFee: 1}), - post: L1FeeData({baseFee: block.basefee, blobFee: ExtRollupLib.getBlobBaseFee(VM_ADDRESS)}), - slotOfChange: LIFETIME - }); - } - - function cheat__InitialiseValidatorSet(CheatDepositArgs[] memory _args) - external - override(ITestRollup) - onlyOwner - { - for (uint256 i = 0; i < _args.length; i++) { - _cheat__Deposit(_args[i].attester, _args[i].proposer, _args[i].withdrawer, _args[i].amount); - } - setupEpoch(); + {} + + function getTargetCommitteeSize() external view override(IValidatorSelection) returns (uint256) { + return ValidatorSelectionLib.getStorage().targetCommitteeSize; } - /** - * @notice Prune the pending chain up to the last proven block - * - * @dev Will revert if there is nothing to prune or if the chain is not ready to be pruned - */ - function prune() external override(IRollup) { - require(canPrune(), Errors.Rollup__NothingToPrune()); - _prune(); + function getGenesisTime() external view override(IValidatorSelection) returns (Timestamp) { + return Timestamp.wrap(TimeLib.getStorage().genesisTime); } - /** - * Sets the assumeProvenThroughBlockNumber. Only the contract deployer can set it. - * @param _blockNumber - New value. - */ - function setAssumeProvenThroughBlockNumber(uint256 _blockNumber) - external - override(ITestRollup) - onlyOwner - { - _fakeBlockNumberAsProven(_blockNumber); - assumeProvenThroughBlockNumber = _blockNumber; + function getSlotDuration() external view override(IValidatorSelection) returns (uint256) { + return TimeLib.getStorage().slotDuration; } - /** - * @notice Set the verifier contract - * - * @dev This is only needed for testing, and should be removed - * - * @param _verifier - The new verifier contract - */ - function setEpochVerifier(address _verifier) external override(ITestRollup) onlyOwner { - rollupStore.epochProofVerifier = IVerifier(_verifier); + function getEpochDuration() external view override(IValidatorSelection) returns (uint256) { + return TimeLib.getStorage().epochDuration; } - /** - * @notice Set the vkTreeRoot - * - * @dev This is only needed for testing, and should be removed - * - * @param _vkTreeRoot - The new vkTreeRoot to be used by proofs - */ - function setVkTreeRoot(bytes32 _vkTreeRoot) external override(ITestRollup) onlyOwner { - rollupStore.vkTreeRoot = _vkTreeRoot; + function getSlasher() external view override(IStaking) returns (address) { + return StakingLib.getStorage().slasher; } - /** - * @notice Set the protocolContractTreeRoot - * - * @dev This is only needed for testing, and should be removed - * - * @param _protocolContractTreeRoot - The new protocolContractTreeRoot to be used by proofs - */ - function setProtocolContractTreeRoot(bytes32 _protocolContractTreeRoot) - external - override(ITestRollup) - onlyOwner - { - rollupStore.protocolContractTreeRoot = _protocolContractTreeRoot; + function getStakingAsset() external view override(IStaking) returns (IERC20) { + return StakingLib.getStorage().stakingAsset; } - /** - * @notice Publishes the body and propose the block - * @dev `eth_log_handlers` rely on this function - * - * @param _args - The arguments to propose the block - * @param _signatures - Signatures from the validators - * // TODO(#9101): The below _body should be removed once we can extract blobs. It's only here so the archiver can extract tx effects. - * @param _body - The body of the L2 block - * @param _blobInput - The blob evaluation KZG proof, challenge, and opening required for the precompile. - */ - function proposeAndClaim( - ProposeArgs calldata _args, - Signature[] memory _signatures, - bytes calldata _body, - bytes calldata _blobInput, - SignedEpochProofQuote calldata _quote - ) external override(IRollup) { - propose(_args, _signatures, _body, _blobInput); - claimEpochProofRight(_quote); + function getMinimumStake() external view override(IStaking) returns (uint256) { + return StakingLib.getStorage().minimumStake; } - /** - * @notice Submit a proof for an epoch in the pending chain - * - * @dev Will emit `L2ProofVerified` if the proof is valid - * - * @dev Will throw if: - * - The block number is past the pending chain - * - The last archive root of the header does not match the archive root of parent block - * - The archive root of the header does not match the archive root of the proposed block - * - The proof is invalid - * - * @dev We provide the `_archive` and `_blockHash` even if it could be read from storage itself because it allow for - * better error messages. Without passing it, we would just have a proof verification failure. - * - * @param _args - The arguments to submit the epoch root proof: - * _epochSize - The size of the epoch (to be promoted to a constant) - * _args - Array of public inputs to the proof (previousArchive, endArchive, previousBlockHash, endBlockHash, endTimestamp, outHash, proverId) - * _fees - Array of recipient-value pairs with fees to be distributed for the epoch - * _blobPublicInputs - The blob public inputs for the proof - * _aggregationObject - The aggregation object for the proof - * _proof - The proof to verify - */ - function submitEpochRootProof(SubmitEpochRootProofArgs calldata _args) external override(IRollup) { - if (canPrune()) { - _prune(); - } - - // We want to compute the two epoch values before hand. Could we do partial interim? - // We compute these in here to avoid a lot of pain with linking libraries and passing - // external functions into internal functions as args. - SubmitEpochRootProofInterimValues memory interimValues; - interimValues.previousBlockNumber = rollupStore.tips.provenBlockNumber; - interimValues.endBlockNumber = interimValues.previousBlockNumber + _args.epochSize; - - // @note The _getEpochForBlock is expected to revert if the block is beyond pending. - // If this changes you are gonna get so rekt you won't believe it. - // I mean proving blocks that have been pruned rekt. - interimValues.startEpoch = getEpochForBlock(interimValues.previousBlockNumber + 1); - interimValues.epochToProve = getEpochForBlock(interimValues.endBlockNumber); - - uint256 endBlockNumber = ExtRollupLib.submitEpochRootProof( - rollupStore, - _args, - interimValues, - PROOF_COMMITMENT_ESCROW, - FEE_JUICE_PORTAL, - REWARD_DISTRIBUTOR, - ASSET, - CUAUHXICALLI - ); - emit L2ProofVerified(endBlockNumber, _args.args[6]); + function getExitDelay() external view override(IStaking) returns (Timestamp) { + return StakingLib.getStorage().exitDelay; } - function getProofClaim() - external - view - override(IRollup) - returns (DataStructures.EpochProofClaim memory) - { - return rollupStore.proofClaim; + function getActiveAttesterCount() external view override(IStaking) returns (uint256) { + return StakingLib.getStorage().attesters.length(); } function getTips() external view override(IRollup) returns (ChainTips memory) { @@ -338,6 +137,15 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Ownable, ValidatorSelection, IRo ); } + function getProofClaim() + external + view + override(IRollup) + returns (DataStructures.EpochProofClaim memory) + { + return rollupStore.proofClaim; + } + /** * @notice Returns the computed public inputs for the given epoch proof. * @@ -363,40 +171,22 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Ownable, ValidatorSelection, IRo } /** - * @notice Check if msg.sender can propose at a given time - * - * @param _ts - The timestamp to check - * @param _archive - The archive to check (should be the latest archive) - * - * @return uint256 - The slot at the given timestamp - * @return uint256 - The block number at the given timestamp + * @notice Get the next epoch that can be claimed + * @dev Will revert if the epoch has already been claimed or if there is no epoch to prove */ - function canProposeAtTime(Timestamp _ts, bytes32 _archive) - external - view - override(IRollup) - returns (Slot, uint256) - { - Slot slot = getSlotAt(_ts); - - // Consider if a prune will hit in this slot - uint256 pendingBlockNumber = - canPruneAtTime(_ts) ? rollupStore.tips.provenBlockNumber : rollupStore.tips.pendingBlockNumber; - - Slot lastSlot = rollupStore.blocks[pendingBlockNumber].slotNumber; - - require(slot > lastSlot, Errors.Rollup__SlotAlreadyInChain(lastSlot, slot)); - - // Make sure that the proposer is up to date and on the right chain (ie no reorgs) - bytes32 tipArchive = rollupStore.blocks[pendingBlockNumber].archive; - require(tipArchive == _archive, Errors.Rollup__InvalidArchive(tipArchive, _archive)); - - Signature[] memory sigs = new Signature[](0); - DataStructures.ExecutionFlags memory flags = - DataStructures.ExecutionFlags({ignoreDA: true, ignoreSignatures: true}); - _validateValidatorSelection(slot, sigs, _archive, flags); - - return (slot, pendingBlockNumber + 1); + function getClaimableEpoch() external view override(IRollup) returns (Epoch) { + Epoch epochToProve = getEpochToProve(); + require( + // If the epoch has been claimed, it cannot be claimed again + rollupStore.proofClaim.epochToProve != epochToProve + // Edge case for if no claim has been made yet. + // We know that the bondProvider is always set, + // Since otherwise the claimEpochProofRight would have reverted, + // because the zero address cannot have deposited funds into escrow. + || rollupStore.proofClaim.bondProvider == address(0), + Errors.Rollup__ProofRightAlreadyClaimed() + ); + return epochToProve; } /** @@ -409,7 +199,7 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Ownable, ValidatorSelection, IRo * @param _signatures - The signatures to validate * @param _digest - The digest to validate * @param _currentTime - The current time - * @param _blobsHashesCommitment - The blobs hash for this block + * @param _blobsHash - The blobs hash for this block * @param _flags - The flags to validate */ function validateHeader( @@ -417,7 +207,7 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Ownable, ValidatorSelection, IRo Signature[] memory _signatures, bytes32 _digest, Timestamp _currentTime, - bytes32 _blobsHashesCommitment, + bytes32 _blobsHash, DataStructures.ExecutionFlags memory _flags ) external view override(IRollup) { _validateHeader( @@ -426,13 +216,13 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Ownable, ValidatorSelection, IRo _digest, _currentTime, getManaBaseFeeAt(_currentTime, true), - _blobsHashesCommitment, + _blobsHash, _flags ); } /** - * @notice Validate blob transactions against given inputs + * @notice Validate blob transactions against given inputs. * @dev Only exists here for gas estimation. */ function validateBlobs(bytes calldata _blobsInput) @@ -445,316 +235,281 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Ownable, ValidatorSelection, IRo } /** - * @notice Get the next epoch that can be claimed - * @dev Will revert if the epoch has already been claimed or if there is no epoch to prove + * @notice Get the current archive root + * + * @return bytes32 - The current archive root */ - function getClaimableEpoch() external view override(IRollup) returns (Epoch) { - Epoch epochToProve = getEpochToProve(); + function archive() external view override(IRollup) returns (bytes32) { + return rollupStore.blocks[rollupStore.tips.pendingBlockNumber].archive; + } + + function getProvenBlockNumber() external view override(IRollup) returns (uint256) { + return rollupStore.tips.provenBlockNumber; + } + + function getPendingBlockNumber() external view override(IRollup) returns (uint256) { + return rollupStore.tips.pendingBlockNumber; + } + + function getBlock(uint256 _blockNumber) external view override(IRollup) returns (BlockLog memory) { require( - // If the epoch has been claimed, it cannot be claimed again - rollupStore.proofClaim.epochToProve != epochToProve - // Edge case for if no claim has been made yet. - // We know that the bondProvider is always set, - // Since otherwise the claimEpochProofRight would have reverted, - // because the zero address cannot have deposited funds into escrow. - || rollupStore.proofClaim.bondProvider == address(0), - Errors.Rollup__ProofRightAlreadyClaimed() + _blockNumber <= rollupStore.tips.pendingBlockNumber, + Errors.Rollup__InvalidBlockNumber(rollupStore.tips.pendingBlockNumber, _blockNumber) ); - return epochToProve; + return rollupStore.blocks[_blockNumber]; } - function claimEpochProofRight(SignedEpochProofQuote calldata _quote) public override(IRollup) { - validateEpochProofRightClaimAtTime(Timestamp.wrap(block.timestamp), _quote); + function getBlobPublicInputsHash(uint256 _blockNumber) + external + view + override(IRollup) + returns (bytes32) + { + return rollupStore.blobPublicInputsHashes[_blockNumber]; + } - Slot currentSlot = getCurrentSlot(); - Epoch epochToProve = getEpochToProve(); + function getProposerForAttester(address _attester) + external + view + override(IStaking) + returns (address) + { + return StakingLib.getStorage().info[_attester].proposer; + } - // We don't currently unstake, - // but we will as part of https://github.com/AztecProtocol/aztec-packages/issues/8652. - // Blocked on submitting epoch proofs to this contract. - PROOF_COMMITMENT_ESCROW.stakeBond(_quote.quote.prover, _quote.quote.bondAmount); - - rollupStore.proofClaim = DataStructures.EpochProofClaim({ - epochToProve: epochToProve, - basisPointFee: _quote.quote.basisPointFee, - bondAmount: _quote.quote.bondAmount, - bondProvider: _quote.quote.prover, - proposerClaimant: msg.sender - }); - - emit ProofRightClaimed( - epochToProve, _quote.quote.prover, msg.sender, _quote.quote.bondAmount, currentSlot - ); + function getAttesterAtIndex(uint256 _index) external view override(IStaking) returns (address) { + return StakingLib.getStorage().attesters.at(_index); } - /** - * @notice Publishes the body and propose the block - * @dev `eth_log_handlers` rely on this function - * - * @param _args - The arguments to propose the block - * @param _signatures - Signatures from the validators - * // TODO(#9101): The below _body should be removed once we can extract blobs. It's only here so the archiver can extract tx effects. - * @param - The body of the L2 block - * @param _blobInput - The blob evaluation KZG proof, challenge, and opening required for the precompile. - */ - function propose( - ProposeArgs calldata _args, - Signature[] memory _signatures, - // TODO(#9101): Extract blobs from beacon chain => remove below body input - bytes calldata, - bytes calldata _blobInput - ) public override(IRollup) { - if (canPrune()) { - _prune(); - } - updateL1GasFeeOracle(); - - // Since an invalid blob hash here would fail the consensus checks of - // the header, the `blobInput` is implicitly accepted by consensus as well. - (bytes32[] memory blobHashes, bytes32 blobsHashesCommitment, bytes32 blobPublicInputsHash) = - ExtRollupLib.validateBlobs(_blobInput, checkBlob); - - // Decode and validate header - Header memory header = ExtRollupLib.decodeHeader(_args.header); - - setupEpoch(); - ManaBaseFeeComponents memory components = - getManaBaseFeeComponentsAt(Timestamp.wrap(block.timestamp), true); - uint256 manaBaseFee = components.summedBaseFee(); - _validateHeader({ - _header: header, - _signatures: _signatures, - _digest: _args.digest(), - _currentTime: Timestamp.wrap(block.timestamp), - _manaBaseFee: manaBaseFee, - _blobsHashesCommitment: blobsHashesCommitment, - _flags: DataStructures.ExecutionFlags({ignoreDA: false, ignoreSignatures: false}) - }); - - uint256 blockNumber = ++rollupStore.tips.pendingBlockNumber; - - { - rollupStore.blocks[blockNumber] = _toBlockLog(_args, blockNumber, components.congestionCost); - } - - rollupStore.blobPublicInputsHashes[blockNumber] = blobPublicInputsHash; - - // @note The block number here will always be >=1 as the genesis block is at 0 - { - bytes32 inHash = INBOX.consume(blockNumber); - require( - header.contentCommitment.inHash == inHash, - Errors.Rollup__InvalidInHash(inHash, header.contentCommitment.inHash) - ); - } - - // TODO(#7218): Revert to fixed height tree for outbox, currently just providing min as interim - // Min size = smallest path of the rollup tree + 1 - (uint256 min,) = MerkleLib.computeMinMaxPathLength(header.contentCommitment.numTxs); - OUTBOX.insert(blockNumber, header.contentCommitment.outHash, min + 1); - - emit L2BlockProposed(blockNumber, _args.archive, blobHashes); - - // Automatically flag the block as proven if we have cheated and set assumeProvenThroughBlockNumber. - if (blockNumber <= assumeProvenThroughBlockNumber) { - _fakeBlockNumberAsProven(blockNumber); - - bool isFeeCanonical = address(this) == FEE_JUICE_PORTAL.canonicalRollup(); - bool isRewardDistributorCanonical = address(this) == REWARD_DISTRIBUTOR.canonicalRollup(); - - if (isFeeCanonical && header.globalVariables.coinbase != address(0) && header.totalFees > 0) { - // @note This will currently fail if there are insufficient funds in the bridge - // which WILL happen for the old version after an upgrade where the bridge follow. - // Consider allowing a failure. See #7938. - FEE_JUICE_PORTAL.distributeFees(header.globalVariables.coinbase, header.totalFees); - } - if (isRewardDistributorCanonical && header.globalVariables.coinbase != address(0)) { - REWARD_DISTRIBUTOR.claim(header.globalVariables.coinbase); - } - - emit L2ProofVerified(blockNumber, "CHEAT"); - } + function getProposerAtIndex(uint256 _index) external view override(IStaking) returns (address) { + return StakingLib.getStorage().info[StakingLib.getStorage().attesters.at(_index)].proposer; } - /** - * @notice Updates the l1 gas fee oracle - * @dev This function is called by the `propose` function - */ - function updateL1GasFeeOracle() public override(IRollup) { - Slot slot = getCurrentSlot(); - // The slot where we find a new queued value acceptable - Slot acceptableSlot = rollupStore.l1GasOracleValues.slotOfChange + (LIFETIME - LAG); + function getInfo(address _attester) + external + view + override(IStaking) + returns (ValidatorInfo memory) + { + return StakingLib.getStorage().info[_attester]; + } - if (slot < acceptableSlot) { - return; - } + function getExit(address _attester) external view override(IStaking) returns (Exit memory) { + return StakingLib.getStorage().exits[_attester]; + } - rollupStore.l1GasOracleValues.pre = rollupStore.l1GasOracleValues.post; - rollupStore.l1GasOracleValues.post = - L1FeeData({baseFee: block.basefee, blobFee: ExtRollupLib.getBlobBaseFee(VM_ADDRESS)}); - rollupStore.l1GasOracleValues.slotOfChange = slot + LAG; + function getOperatorAtIndex(uint256 _index) + external + view + override(IStaking) + returns (OperatorInfo memory) + { + address attester = StakingLib.getStorage().attesters.at(_index); + return + OperatorInfo({proposer: StakingLib.getStorage().info[attester].proposer, attester: attester}); } /** - * @notice Gets the fee asset price as fee_asset / eth with 1e9 precision + * @notice Get the validator set for a given epoch + * + * @dev Consider removing this to replace with a `size` and individual getter. + * + * @param _epoch The epoch number to get the validator set for * - * @return The fee asset price + * @return The validator set for the given epoch */ - function getFeeAssetPrice() public view override(IRollup) returns (uint256) { - return IntRollupLib.feeAssetPriceModifier( - rollupStore.blocks[rollupStore.tips.pendingBlockNumber].feeHeader.feeAssetPriceNumerator - ); + function getEpochCommittee(Epoch _epoch) + external + view + override(IValidatorSelection) + returns (address[] memory) + { + return ValidatorSelectionLib.getStorage().epochs[_epoch].committee; } - function getL1FeesAt(Timestamp _timestamp) - public + /** + * @notice Get the validator set for the current epoch + * @return The validator set for the current epoch + */ + function getCurrentEpochCommittee() + external view - override(IRollup) - returns (L1FeeData memory) + override(IValidatorSelection) + returns (address[] memory) { - return getSlotAt(_timestamp) < rollupStore.l1GasOracleValues.slotOfChange - ? rollupStore.l1GasOracleValues.pre - : rollupStore.l1GasOracleValues.post; + return ValidatorSelectionLib.getCommitteeAt(StakingLib.getStorage(), getCurrentEpoch()); } /** - * @notice Gets the mana base fee + * @notice Get the committee for a given timestamp * - * @param _inFeeAsset - Whether to return the fee in the fee asset or ETH + * @param _ts - The timestamp to get the committee for * - * @return The mana base fee + * @return The committee for the given timestamp */ - function getManaBaseFeeAt(Timestamp _timestamp, bool _inFeeAsset) - public + function getCommitteeAt(Timestamp _ts) + external view - override(IRollup) - returns (uint256) + override(IValidatorSelection) + returns (address[] memory) { - return getManaBaseFeeComponentsAt(_timestamp, _inFeeAsset).summedBaseFee(); + return ValidatorSelectionLib.getCommitteeAt(StakingLib.getStorage(), getEpochAt(_ts)); } /** - * @notice Gets the mana base fee components - * For more context, consult: - * https://github.com/AztecProtocol/engineering-designs/blob/main/in-progress/8757-fees/design.md + * @notice Get the sample seed for a given timestamp * - * @dev TODO #10004 - As part of the refactor, will likely get rid of this function or make it private - * keeping it public for now makes it simpler to test. + * @param _ts - The timestamp to get the sample seed for * - * @param _inFeeAsset - Whether to return the fee in the fee asset or ETH - * - * @return The mana base fee components + * @return The sample seed for the given timestamp */ - function getManaBaseFeeComponentsAt(Timestamp _timestamp, bool _inFeeAsset) - public + function getSampleSeedAt(Timestamp _ts) + external view - override(ITestRollup) - returns (ManaBaseFeeComponents memory) + override(IValidatorSelection) + returns (uint256) { - // If we can prune, we use the proven block, otherwise the pending block - uint256 blockOfInterest = canPruneAtTime(_timestamp) - ? rollupStore.tips.provenBlockNumber - : rollupStore.tips.pendingBlockNumber; - - return ExtRollupLib.getManaBaseFeeComponentsAt( - rollupStore.blocks[blockOfInterest].feeHeader, - getL1FeesAt(_timestamp), - _inFeeAsset ? getFeeAssetPrice() : 1e9, - TimeLib.getStorage().epochDuration - ); + return ValidatorSelectionLib.getSampleSeed(getEpochAt(_ts)); } - function quoteToDigest(EpochProofQuote memory _quote) - public - view - override(IRollup) - returns (bytes32) - { - return _hashTypedDataV4(IntRollupLib.computeQuoteHash(_quote)); + /** + * @notice Get the sample seed for the current epoch + * + * @return The sample seed for the current epoch + */ + function getCurrentSampleSeed() external view override(IValidatorSelection) returns (uint256) { + return ValidatorSelectionLib.getSampleSeed(getCurrentEpoch()); } - function validateEpochProofRightClaimAtTime(Timestamp _ts, SignedEpochProofQuote calldata _quote) - public - view - override(IRollup) - { - Slot currentSlot = getSlotAt(_ts); - address currentProposer = getProposerAt(_ts); - Epoch epochToProve = getEpochToProve(); - uint256 posInEpoch = TimeLib.positionInEpoch(currentSlot); - bytes32 digest = quoteToDigest(_quote.quote); - - ExtRollupLib.validateEpochProofRightClaimAtTime( - currentSlot, - currentProposer, - epochToProve, - posInEpoch, - _quote, - digest, - rollupStore.proofClaim, - CLAIM_DURATION_IN_L2_SLOTS, - PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST, - PROOF_COMMITMENT_ESCROW - ); + /** + * @notice Get the attester set + * + * @dev Consider removing this to replace with a `size` and individual getter. + * + * @return The validator set + */ + function getAttesters() external view override(IValidatorSelection) returns (address[] memory) { + return StakingLib.getStorage().attesters.values(); } /** - * @notice Get the current archive root + * @notice Get the current slot number * - * @return bytes32 - The current archive root + * @return The current slot number */ - function archive() public view override(IRollup) returns (bytes32) { - return rollupStore.blocks[rollupStore.tips.pendingBlockNumber].archive; + function getCurrentSlot() external view override(IValidatorSelection) returns (Slot) { + return Timestamp.wrap(block.timestamp).slotFromTimestamp(); } - function getProvenBlockNumber() public view override(IRollup) returns (uint256) { - return rollupStore.tips.provenBlockNumber; + /** + * @notice Get the timestamp for a given slot + * + * @param _slotNumber - The slot number to get the timestamp for + * + * @return The timestamp for the given slot + */ + function getTimestampForSlot(Slot _slotNumber) + external + view + override(IValidatorSelection) + returns (Timestamp) + { + return _slotNumber.toTimestamp(); } - function getPendingBlockNumber() public view override(IRollup) returns (uint256) { - return rollupStore.tips.pendingBlockNumber; + /** + * @notice Get the proposer for the current slot + * + * @dev Calls `getCurrentProposer(uint256)` with the current timestamp + * + * @return The address of the proposer + */ + function getCurrentProposer() external view override(IValidatorSelection) returns (address) { + return getProposerAt(Timestamp.wrap(block.timestamp)); } - function getBlock(uint256 _blockNumber) public view override(IRollup) returns (BlockLog memory) { - require( - _blockNumber <= rollupStore.tips.pendingBlockNumber, - Errors.Rollup__InvalidBlockNumber(rollupStore.tips.pendingBlockNumber, _blockNumber) - ); - return rollupStore.blocks[_blockNumber]; + /** + * @notice Computes the slot at a specific time + * + * @param _ts - The timestamp to compute the slot for + * + * @return The computed slot + */ + function getSlotAt(Timestamp _ts) external view override(IValidatorSelection) returns (Slot) { + return _ts.slotFromTimestamp(); } - function getBlobPublicInputsHash(uint256 _blockNumber) - public + /** + * @notice Computes the epoch at a specific slot + * + * @param _slotNumber - The slot number to compute the epoch for + * + * @return The computed epoch + */ + function getEpochAtSlot(Slot _slotNumber) + external view - override(IRollup) - returns (bytes32) + override(IValidatorSelection) + returns (Epoch) { - return rollupStore.blobPublicInputsHashes[_blockNumber]; + return _slotNumber.epochFromSlot(); } - function getEpochForBlock(uint256 _blockNumber) public view override(IRollup) returns (Epoch) { - require( - _blockNumber <= rollupStore.tips.pendingBlockNumber, - Errors.Rollup__InvalidBlockNumber(rollupStore.tips.pendingBlockNumber, _blockNumber) + /** + * @notice Check if msg.sender can propose at a given time + * + * @param _ts - The timestamp to check + * @param _archive - The archive to check (should be the latest archive) + * + * @return uint256 - The slot at the given timestamp + * @return uint256 - The block number at the given timestamp + */ + function canProposeAtTime(Timestamp _ts, bytes32 _archive) + external + view + override(IRollup) + returns (Slot, uint256) + { + Slot slot = _ts.slotFromTimestamp(); + + // Consider if a prune will hit in this slot + uint256 pendingBlockNumber = + canPruneAtTime(_ts) ? rollupStore.tips.provenBlockNumber : rollupStore.tips.pendingBlockNumber; + + Slot lastSlot = rollupStore.blocks[pendingBlockNumber].slotNumber; + + require(slot > lastSlot, Errors.Rollup__SlotAlreadyInChain(lastSlot, slot)); + + // Make sure that the proposer is up to date and on the right chain (ie no reorgs) + bytes32 tipArchive = rollupStore.blocks[pendingBlockNumber].archive; + require(tipArchive == _archive, Errors.Rollup__InvalidArchive(tipArchive, _archive)); + + Signature[] memory sigs = new Signature[](0); + DataStructures.ExecutionFlags memory flags = + DataStructures.ExecutionFlags({ignoreDA: true, ignoreSignatures: true}); + + Epoch currentEpoch = slot.epochFromSlot(); + ValidatorSelectionLib.validateValidatorSelection( + StakingLib.getStorage(), slot, currentEpoch, sigs, _archive, flags ); - return getEpochAt(getTimestampForSlot(rollupStore.blocks[_blockNumber].slotNumber)); + + return (slot, pendingBlockNumber + 1); } /** - * @notice Get the epoch that should be proven + * @notice Gets the mana base fee * - * @dev This is the epoch that should be proven. It does so by getting the epoch of the block - * following the last proven block. If there is no such block (i.e. the pending chain is - * the same as the proven chain), then revert. + * @param _inFeeAsset - Whether to return the fee in the fee asset or ETH * - * @return uint256 - The epoch to prove + * @return The mana base fee */ - function getEpochToProve() public view override(IRollup) returns (Epoch) { - require( - rollupStore.tips.provenBlockNumber != rollupStore.tips.pendingBlockNumber, - Errors.Rollup__NoEpochToProve() - ); - return getEpochForBlock(rollupStore.tips.provenBlockNumber + 1); + function getManaBaseFeeAt(Timestamp _timestamp, bool _inFeeAsset) + public + view + override(IRollup) + returns (uint256) + { + return getManaBaseFeeComponentsAt(_timestamp, _inFeeAsset).summedBaseFee(); } /** @@ -770,185 +525,50 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Ownable, ValidatorSelection, IRo : bytes32(0); } - function canPrune() public view override(IRollup) returns (bool) { - return canPruneAtTime(Timestamp.wrap(block.timestamp)); - } - - function canPruneAtTime(Timestamp _ts) public view override(IRollup) returns (bool) { - if ( - rollupStore.tips.pendingBlockNumber == rollupStore.tips.provenBlockNumber - || rollupStore.tips.pendingBlockNumber <= assumeProvenThroughBlockNumber - ) { - return false; - } - - Slot currentSlot = getSlotAt(_ts); - Epoch oldestPendingEpoch = getEpochForBlock(rollupStore.tips.provenBlockNumber + 1); - Slot startSlotOfPendingEpoch = TimeLib.toSlots(oldestPendingEpoch); - - // suppose epoch 1 is proven, epoch 2 is pending, epoch 3 is the current epoch. - // we prune the pending chain back to the end of epoch 1 if: - // - the proof claim phase of epoch 3 has ended without a claim to prove epoch 2 (or proof of epoch 2) - // - we reach epoch 4 without a proof of epoch 2 (regardless of whether a proof claim was submitted) - bool inClaimPhase = currentSlot - < startSlotOfPendingEpoch + TimeLib.toSlots(Epoch.wrap(1)) - + Slot.wrap(CLAIM_DURATION_IN_L2_SLOTS); - - bool claimExists = currentSlot < startSlotOfPendingEpoch + TimeLib.toSlots(Epoch.wrap(2)) - && rollupStore.proofClaim.epochToProve == oldestPendingEpoch - && rollupStore.proofClaim.proposerClaimant != address(0); - - if (inClaimPhase || claimExists) { - // If we are in the claim phase, do not prune - return false; - } - return true; - } - - function _prune() internal { - // TODO #8656 - delete rollupStore.proofClaim; - - uint256 pending = rollupStore.tips.pendingBlockNumber; - - // @note We are not deleting the blocks, but we are "winding back" the pendingTip to the last block that was proven. - // We can do because any new block proposed will overwrite a previous block in the block log, - // so no values should "survive". - // People must therefore read the chain using the pendingTip as a boundary. - rollupStore.tips.pendingBlockNumber = rollupStore.tips.provenBlockNumber; - - emit PrunedPending(rollupStore.tips.provenBlockNumber, pending); + /** + * @notice Computes the epoch at a specific time + * + * @param _ts - The timestamp to compute the epoch for + * + * @return The computed epoch + */ + function getEpochAt(Timestamp _ts) public view override(IValidatorSelection) returns (Epoch) { + return _ts.epochFromTimestamp(); } /** - * @notice Validates the header for submission - * - * @param _header - The proposed block header - * @param _signatures - The signatures for the attestations - * @param _digest - The digest that signatures signed - * @param _currentTime - The time of execution - * @param _blobsHashesCommitment - The blobs hash for this block - * @dev - This value is provided to allow for simple simulation of future - * @param _flags - Flags specific to the execution, whether certain checks should be skipped + * @notice Get the current epoch number + * + * @return The current epoch number */ - function _validateHeader( - Header memory _header, - Signature[] memory _signatures, - bytes32 _digest, - Timestamp _currentTime, - uint256 _manaBaseFee, - bytes32 _blobsHashesCommitment, - DataStructures.ExecutionFlags memory _flags - ) internal view { - uint256 pendingBlockNumber = canPruneAtTime(_currentTime) - ? rollupStore.tips.provenBlockNumber - : rollupStore.tips.pendingBlockNumber; - - ExtRollupLib.validateHeaderForSubmissionBase( - ValidateHeaderArgs({ - header: _header, - currentTime: _currentTime, - manaBaseFee: _manaBaseFee, - blobsHashesCommitment: _blobsHashesCommitment, - pendingBlockNumber: pendingBlockNumber, - flags: _flags, - version: VERSION, - feeJuicePortal: FEE_JUICE_PORTAL, - getTimestampForSlot: this.getTimestampForSlot - }), - rollupStore.blocks - ); - _validateHeaderForSubmissionSequencerSelection( - Slot.wrap(_header.globalVariables.slotNumber), _signatures, _digest, _currentTime, _flags - ); + function getCurrentEpoch() public view override(IValidatorSelection) returns (Epoch) { + return Timestamp.wrap(block.timestamp).epochFromTimestamp(); } /** - * @notice Validate a header for submission to the pending chain (sequencer selection checks) + * @notice Get the proposer for the slot at a specific timestamp * - * These validation checks are directly related to ValidatorSelection. - * Note that while these checks are strict, they can be relaxed with some changes to - * message boxes. + * @dev This function is very useful for off-chain usage, as it easily allow a client to + * determine who will be the proposer at the NEXT ethereum block. + * Should not be trusted when moving beyond the current epoch, since changes to the + * validator set might not be reflected when we actually reach that epoch (more changes + * might have happened). * - * Each of the following validation checks must pass, otherwise an error is thrown and we revert. - * - The slot MUST be the current slot - * This might be relaxed for allow consensus set to better handle short-term bursts of L1 congestion - * - The slot MUST be in the current epoch + * @dev The proposer is selected from the validator set of the current epoch. * - * @param _slot - The slot of the header to validate - * @param _signatures - The signatures to validate - * @param _digest - The digest that signatures sign over + * @dev Should only be access on-chain if epoch is setup, otherwise very expensive. + * + * @dev A return value of address(0) means that the proposer is "open" and can be anyone. + * + * @dev If the current epoch is the first epoch, returns address(0) + * If the current epoch is setup, we will return the proposer for the current slot + * If the current epoch is not setup, we will perform a sample as if it was (gas heavy) + * + * @return The address of the proposer */ - function _validateHeaderForSubmissionSequencerSelection( - Slot _slot, - Signature[] memory _signatures, - bytes32 _digest, - Timestamp _currentTime, - DataStructures.ExecutionFlags memory _flags - ) internal view { - // Ensure that the slot proposed is NOT in the future - Slot currentSlot = getSlotAt(_currentTime); - require(_slot == currentSlot, Errors.HeaderLib__InvalidSlotNumber(currentSlot, _slot)); - - // @note We are currently enforcing that the slot is in the current epoch - // If this is not the case, there could potentially be a weird reorg - // of an entire epoch if no-one from the new epoch committee have seen - // those blocks or behaves as if they did not. - - Epoch epochNumber = getEpochAt(getTimestampForSlot(_slot)); - Epoch currentEpoch = getEpochAt(_currentTime); - require(epochNumber == currentEpoch, Errors.Rollup__InvalidEpoch(currentEpoch, epochNumber)); - - _validateValidatorSelection(_slot, _signatures, _digest, _flags); - } - - // Helper to avoid stack too deep - function _toBlockLog(ProposeArgs calldata _args, uint256 _blockNumber, uint256 _congestionCost) - internal - view - returns (BlockLog memory) - { - FeeHeader memory parentFeeHeader = rollupStore.blocks[_blockNumber - 1].feeHeader; - return BlockLog({ - archive: _args.archive, - blockHash: _args.blockHash, - slotNumber: Slot.wrap(uint256(bytes32(_args.header[0x0194:0x01b4]))), - feeHeader: FeeHeader({ - excessMana: IntRollupLib.computeExcessMana(parentFeeHeader), - feeAssetPriceNumerator: parentFeeHeader.feeAssetPriceNumerator.clampedAdd( - _args.oracleInput.feeAssetPriceModifier - ), - manaUsed: uint256(bytes32(_args.header[0x0268:0x0288])), - provingCostPerManaNumerator: parentFeeHeader.provingCostPerManaNumerator.clampedAdd( - _args.oracleInput.provingCostModifier - ), - congestionCost: _congestionCost - }) - }); - } - - function _fakeBlockNumberAsProven(uint256 _blockNumber) private { - if ( - _blockNumber > rollupStore.tips.provenBlockNumber - && _blockNumber <= rollupStore.tips.pendingBlockNumber - ) { - rollupStore.tips.provenBlockNumber = _blockNumber; - - // If this results on a new epoch, create a fake claim for it - // Otherwise nextEpochToProve will report an old epoch - Epoch epoch = getEpochForBlock(_blockNumber); - if ( - Epoch.unwrap(epoch) == 0 - || Epoch.unwrap(epoch) > Epoch.unwrap(rollupStore.proofClaim.epochToProve) - ) { - rollupStore.proofClaim = DataStructures.EpochProofClaim({ - epochToProve: epoch, - basisPointFee: 0, - bondAmount: 0, - bondProvider: address(0), - proposerClaimant: msg.sender - }); - } - } + function getProposerAt(Timestamp _ts) public view override(IValidatorSelection) returns (address) { + Slot slot = _ts.slotFromTimestamp(); + Epoch epochNumber = slot.epochFromSlot(); + return ValidatorSelectionLib.getProposerAt(StakingLib.getStorage(), slot, epochNumber); } } diff --git a/l1-contracts/src/core/RollupCore.sol b/l1-contracts/src/core/RollupCore.sol new file mode 100644 index 00000000000..34a5d6d0100 --- /dev/null +++ b/l1-contracts/src/core/RollupCore.sol @@ -0,0 +1,782 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.27; + +import {IFeeJuicePortal} from "@aztec/core/interfaces/IFeeJuicePortal.sol"; +import {IProofCommitmentEscrow} from "@aztec/core/interfaces/IProofCommitmentEscrow.sol"; +import { + IRollupCore, + ITestRollup, + CheatDepositArgs, + FeeHeader, + ManaBaseFeeComponents, + BlockLog, + RollupStore, + L1GasOracleValues, + L1FeeData, + SubmitEpochRootProofArgs +} from "@aztec/core/interfaces/IRollup.sol"; +import {IStakingCore} from "@aztec/core/interfaces/IStaking.sol"; +import {IValidatorSelectionCore} from "@aztec/core/interfaces/IValidatorSelection.sol"; +import {IVerifier} from "@aztec/core/interfaces/IVerifier.sol"; +import {IInbox} from "@aztec/core/interfaces/messagebridge/IInbox.sol"; +import {IOutbox} from "@aztec/core/interfaces/messagebridge/IOutbox.sol"; +import {Constants} from "@aztec/core/libraries/ConstantsGen.sol"; +import {MerkleLib} from "@aztec/core/libraries/crypto/MerkleLib.sol"; +import {Signature} from "@aztec/core/libraries/crypto/SignatureLib.sol"; +import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; +import {Errors} from "@aztec/core/libraries/Errors.sol"; +import { + ExtRollupLib, + ValidateHeaderArgs, + Header, + SignedEpochProofQuote, + SubmitEpochRootProofInterimValues +} from "@aztec/core/libraries/RollupLibs/ExtRollupLib.sol"; +import {IntRollupLib, EpochProofQuote} from "@aztec/core/libraries/RollupLibs/IntRollupLib.sol"; +import {ProposeArgs, ProposeLib} from "@aztec/core/libraries/RollupLibs/ProposeLib.sol"; +import {StakingLib} from "@aztec/core/libraries/staking/StakingLib.sol"; +import {Timestamp, Slot, Epoch, TimeLib} from "@aztec/core/libraries/TimeLib.sol"; +import {ValidatorSelectionLib} from + "@aztec/core/libraries/ValidatorSelectionLib/ValidatorSelectionLib.sol"; +import {Inbox} from "@aztec/core/messagebridge/Inbox.sol"; +import {Outbox} from "@aztec/core/messagebridge/Outbox.sol"; +import {ProofCommitmentEscrow} from "@aztec/core/ProofCommitmentEscrow.sol"; +import {Slasher} from "@aztec/core/staking/Slasher.sol"; +import {IRewardDistributor} from "@aztec/governance/interfaces/IRewardDistributor.sol"; +import {MockVerifier} from "@aztec/mock/MockVerifier.sol"; +import {Ownable} from "@oz/access/Ownable.sol"; +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; +import {EIP712} from "@oz/utils/cryptography/EIP712.sol"; + +struct Config { + uint256 aztecSlotDuration; + uint256 aztecEpochDuration; + uint256 targetCommitteeSize; + uint256 aztecEpochProofClaimWindowInL2Slots; + uint256 minimumStake; + uint256 slashingQuorum; + uint256 slashingRoundSize; +} + +/** + * @title Rollup + * @author Aztec Labs + * @notice Rollup contract that is concerned about readability and velocity of development + * not giving a damn about gas costs. + * @dev WARNING: This contract is VERY close to the size limit (500B at time of writing). + */ +contract RollupCore is + EIP712("Aztec Rollup", "1"), + Ownable, + IStakingCore, + IValidatorSelectionCore, + IRollupCore, + ITestRollup +{ + using ProposeLib for ProposeArgs; + using IntRollupLib for uint256; + using IntRollupLib for ManaBaseFeeComponents; + + using TimeLib for Timestamp; + using TimeLib for Slot; + using TimeLib for Epoch; + + Slot public constant LIFETIME = Slot.wrap(5); + Slot public constant LAG = Slot.wrap(2); + + // See https://github.com/AztecProtocol/engineering-designs/blob/main/in-progress/8401-proof-timeliness/proof-timeliness.ipynb + // for justification of CLAIM_DURATION_IN_L2_SLOTS. + uint256 public constant PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST = 1000; + + // A Cuauhxicalli [kʷaːʍʃiˈkalːi] ("eagle gourd bowl") is a ceremonial Aztec vessel or altar used to hold offerings, + // such as sacrificial hearts, during rituals performed within temples. + address public constant CUAUHXICALLI = address(bytes20("CUAUHXICALLI")); + + address public constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); + bool public immutable IS_FOUNDRY_TEST; + + uint256 public immutable CLAIM_DURATION_IN_L2_SLOTS; + uint256 public immutable L1_BLOCK_AT_GENESIS; + IInbox public immutable INBOX; + IOutbox public immutable OUTBOX; + IProofCommitmentEscrow public immutable PROOF_COMMITMENT_ESCROW; + uint256 public immutable VERSION; + IFeeJuicePortal public immutable FEE_JUICE_PORTAL; + IRewardDistributor public immutable REWARD_DISTRIBUTOR; + IERC20 public immutable ASSET; + + // To push checkblock into its own slot so we don't have the trouble of being in the middle of a slot + uint256 private gap = 0; + + // @note Always true, exists to override to false for testing only + bool public checkBlob = true; + + RollupStore internal rollupStore; + + // @note Assume that all blocks up to this value (inclusive) are automatically proven. Speeds up bootstrapping. + // Testing only. This should be removed eventually. + uint256 private assumeProvenThroughBlockNumber; + + constructor( + IFeeJuicePortal _fpcJuicePortal, + IRewardDistributor _rewardDistributor, + IERC20 _stakingAsset, + bytes32 _vkTreeRoot, + bytes32 _protocolContractTreeRoot, + address _ares, + Config memory _config + ) Ownable(_ares) { + TimeLib.initialize(block.timestamp, _config.aztecSlotDuration, _config.aztecEpochDuration); + + Timestamp exitDelay = Timestamp.wrap(60 * 60 * 24); + Slasher slasher = new Slasher(_config.slashingQuorum, _config.slashingRoundSize); + StakingLib.initialize(_stakingAsset, _config.minimumStake, exitDelay, address(slasher)); + ValidatorSelectionLib.initialize(_config.targetCommitteeSize); + + FEE_JUICE_PORTAL = _fpcJuicePortal; + REWARD_DISTRIBUTOR = _rewardDistributor; + ASSET = _fpcJuicePortal.UNDERLYING(); + PROOF_COMMITMENT_ESCROW = new ProofCommitmentEscrow( + ASSET, address(this), _config.aztecSlotDuration, _config.aztecEpochDuration + ); + INBOX = IInbox(address(new Inbox(address(this), Constants.L1_TO_L2_MSG_SUBTREE_HEIGHT))); + OUTBOX = IOutbox(address(new Outbox(address(this)))); + VERSION = 1; + L1_BLOCK_AT_GENESIS = block.number; + CLAIM_DURATION_IN_L2_SLOTS = _config.aztecEpochProofClaimWindowInL2Slots; + + IS_FOUNDRY_TEST = VM_ADDRESS.code.length > 0; + + rollupStore.epochProofVerifier = new MockVerifier(); + rollupStore.vkTreeRoot = _vkTreeRoot; + rollupStore.protocolContractTreeRoot = _protocolContractTreeRoot; + + // Genesis block + rollupStore.blocks[0] = BlockLog({ + feeHeader: FeeHeader({ + excessMana: 0, + feeAssetPriceNumerator: 0, + manaUsed: 0, + provingCostPerManaNumerator: 0, + congestionCost: 0 + }), + archive: bytes32(Constants.GENESIS_ARCHIVE_ROOT), + blockHash: bytes32(Constants.GENESIS_BLOCK_HASH), + slotNumber: Slot.wrap(0) + }); + rollupStore.l1GasOracleValues = L1GasOracleValues({ + pre: L1FeeData({baseFee: 1 gwei, blobFee: 1}), + post: L1FeeData({baseFee: block.basefee, blobFee: ExtRollupLib.getBlobBaseFee(VM_ADDRESS)}), + slotOfChange: LIFETIME + }); + } + + function deposit(address _attester, address _proposer, address _withdrawer, uint256 _amount) + external + override(IStakingCore) + { + setupEpoch(); + StakingLib.deposit(_attester, _proposer, _withdrawer, _amount); + } + + function initiateWithdraw(address _attester, address _recipient) + external + override(IStakingCore) + returns (bool) + { + // @note The attester might be chosen for the epoch, so the delay must be long enough + // to allow for that. + setupEpoch(); + return StakingLib.initiateWithdraw(_attester, _recipient); + } + + function finaliseWithdraw(address _attester) external override(IStakingCore) { + StakingLib.finaliseWithdraw(_attester); + } + + function slash(address _attester, uint256 _amount) external override(IStakingCore) { + StakingLib.slash(_attester, _amount); + } + + function cheat__InitialiseValidatorSet(CheatDepositArgs[] memory _args) + external + override(ITestRollup) + onlyOwner + { + for (uint256 i = 0; i < _args.length; i++) { + StakingLib.deposit(_args[i].attester, _args[i].proposer, _args[i].withdrawer, _args[i].amount); + } + setupEpoch(); + } + + /** + * @notice Prune the pending chain up to the last proven block + * + * @dev Will revert if there is nothing to prune or if the chain is not ready to be pruned + */ + function prune() external override(IRollupCore) { + require(canPrune(), Errors.Rollup__NothingToPrune()); + _prune(); + } + + /** + * Sets the assumeProvenThroughBlockNumber. Only the contract deployer can set it. + * @param _blockNumber - New value. + */ + function setAssumeProvenThroughBlockNumber(uint256 _blockNumber) + external + override(ITestRollup) + onlyOwner + { + _fakeBlockNumberAsProven(_blockNumber); + assumeProvenThroughBlockNumber = _blockNumber; + } + + /** + * @notice Set the verifier contract + * + * @dev This is only needed for testing, and should be removed + * + * @param _verifier - The new verifier contract + */ + function setEpochVerifier(address _verifier) external override(ITestRollup) onlyOwner { + rollupStore.epochProofVerifier = IVerifier(_verifier); + } + + /** + * @notice Set the vkTreeRoot + * + * @dev This is only needed for testing, and should be removed + * + * @param _vkTreeRoot - The new vkTreeRoot to be used by proofs + */ + function setVkTreeRoot(bytes32 _vkTreeRoot) external override(ITestRollup) onlyOwner { + rollupStore.vkTreeRoot = _vkTreeRoot; + } + + /** + * @notice Set the protocolContractTreeRoot + * + * @dev This is only needed for testing, and should be removed + * + * @param _protocolContractTreeRoot - The new protocolContractTreeRoot to be used by proofs + */ + function setProtocolContractTreeRoot(bytes32 _protocolContractTreeRoot) + external + override(ITestRollup) + onlyOwner + { + rollupStore.protocolContractTreeRoot = _protocolContractTreeRoot; + } + + /** + * @notice Publishes the body and propose the block + * @dev `eth_log_handlers` rely on this function + * + * @param _args - The arguments to propose the block + * @param _signatures - Signatures from the validators + * // TODO(#9101): The below _body should be removed once we can extract blobs. It's only here so the archiver can extract tx effects. + * @param _body - The body of the L2 block + * @param _blobInput - The blob evaluation KZG proof, challenge, and opening required for the precompile. + */ + function proposeAndClaim( + ProposeArgs calldata _args, + Signature[] memory _signatures, + bytes calldata _body, + bytes calldata _blobInput, + SignedEpochProofQuote calldata _quote + ) external override(IRollupCore) { + propose(_args, _signatures, _body, _blobInput); + claimEpochProofRight(_quote); + } + + /** + * @notice Submit a proof for an epoch in the pending chain + * + * @dev Will emit `L2ProofVerified` if the proof is valid + * + * @dev Will throw if: + * - The block number is past the pending chain + * - The last archive root of the header does not match the archive root of parent block + * - The archive root of the header does not match the archive root of the proposed block + * - The proof is invalid + * + * @dev We provide the `_archive` and `_blockHash` even if it could be read from storage itself because it allow for + * better error messages. Without passing it, we would just have a proof verification failure. + * + * @param _args - The arguments to submit the epoch root proof: + * _epochSize - The size of the epoch (to be promoted to a constant) + * _args - Array of public inputs to the proof (previousArchive, endArchive, previousBlockHash, endBlockHash, endTimestamp, outHash, proverId) + * _fees - Array of recipient-value pairs with fees to be distributed for the epoch + * _blobPublicInputs - The blob public inputs for the proof + * _aggregationObject - The aggregation object for the proof + * _proof - The proof to verify + */ + function submitEpochRootProof(SubmitEpochRootProofArgs calldata _args) + external + override(IRollupCore) + { + if (canPrune()) { + _prune(); + } + + // We want to compute the two epoch values before hand. Could we do partial interim? + // We compute these in here to avoid a lot of pain with linking libraries and passing + // external functions into internal functions as args. + SubmitEpochRootProofInterimValues memory interimValues; + interimValues.previousBlockNumber = rollupStore.tips.provenBlockNumber; + interimValues.endBlockNumber = interimValues.previousBlockNumber + _args.epochSize; + + // @note The _getEpochForBlock is expected to revert if the block is beyond pending. + // If this changes you are gonna get so rekt you won't believe it. + // I mean proving blocks that have been pruned rekt. + interimValues.startEpoch = getEpochForBlock(interimValues.previousBlockNumber + 1); + interimValues.epochToProve = getEpochForBlock(interimValues.endBlockNumber); + + uint256 endBlockNumber = ExtRollupLib.submitEpochRootProof( + rollupStore, + _args, + interimValues, + PROOF_COMMITMENT_ESCROW, + FEE_JUICE_PORTAL, + REWARD_DISTRIBUTOR, + ASSET, + CUAUHXICALLI + ); + emit L2ProofVerified(endBlockNumber, _args.args[6]); + } + + function setupEpoch() public override(IValidatorSelectionCore) { + ValidatorSelectionLib.setupEpoch(StakingLib.getStorage()); + } + + function claimEpochProofRight(SignedEpochProofQuote calldata _quote) public override(IRollupCore) { + validateEpochProofRightClaimAtTime(Timestamp.wrap(block.timestamp), _quote); + + Slot currentSlot = Timestamp.wrap(block.timestamp).slotFromTimestamp(); + Epoch epochToProve = getEpochToProve(); + + // We don't currently unstake, + // but we will as part of https://github.com/AztecProtocol/aztec-packages/issues/8652. + // Blocked on submitting epoch proofs to this contract. + PROOF_COMMITMENT_ESCROW.stakeBond(_quote.quote.prover, _quote.quote.bondAmount); + + rollupStore.proofClaim = DataStructures.EpochProofClaim({ + epochToProve: epochToProve, + basisPointFee: _quote.quote.basisPointFee, + bondAmount: _quote.quote.bondAmount, + bondProvider: _quote.quote.prover, + proposerClaimant: msg.sender + }); + + emit ProofRightClaimed( + epochToProve, _quote.quote.prover, msg.sender, _quote.quote.bondAmount, currentSlot + ); + } + + /** + * @notice Publishes the body and propose the block + * @dev `eth_log_handlers` rely on this function + * + * @param _args - The arguments to propose the block + * @param _signatures - Signatures from the validators + * // TODO(#9101): The below _body should be removed once we can extract blobs. It's only here so the archiver can extract tx effects. + * @param - The body of the L2 block + * @param _blobInput - The blob evaluation KZG proof, challenge, and opening required for the precompile. + */ + function propose( + ProposeArgs calldata _args, + Signature[] memory _signatures, + // TODO(#9101): Extract blobs from beacon chain => remove below body input + bytes calldata, + bytes calldata _blobInput + ) public override(IRollupCore) { + if (canPrune()) { + _prune(); + } + updateL1GasFeeOracle(); + + // Since an invalid blob hash here would fail the consensus checks of + // the header, the `blobInput` is implicitly accepted by consensus as well. + (bytes32[] memory blobHashes, bytes32 blobsHashesCommitment, bytes32 blobPublicInputsHash) = + ExtRollupLib.validateBlobs(_blobInput, checkBlob); + + // Decode and validate header + Header memory header = ExtRollupLib.decodeHeader(_args.header); + + setupEpoch(); + ManaBaseFeeComponents memory components = + getManaBaseFeeComponentsAt(Timestamp.wrap(block.timestamp), true); + uint256 manaBaseFee = components.summedBaseFee(); + _validateHeader({ + _header: header, + _signatures: _signatures, + _digest: _args.digest(), + _currentTime: Timestamp.wrap(block.timestamp), + _manaBaseFee: manaBaseFee, + _blobsHashesCommitment: blobsHashesCommitment, + _flags: DataStructures.ExecutionFlags({ignoreDA: false, ignoreSignatures: false}) + }); + + uint256 blockNumber = ++rollupStore.tips.pendingBlockNumber; + + { + rollupStore.blocks[blockNumber] = _toBlockLog(_args, blockNumber, components.congestionCost); + } + + rollupStore.blobPublicInputsHashes[blockNumber] = blobPublicInputsHash; + + // @note The block number here will always be >=1 as the genesis block is at 0 + { + bytes32 inHash = INBOX.consume(blockNumber); + require( + header.contentCommitment.inHash == inHash, + Errors.Rollup__InvalidInHash(inHash, header.contentCommitment.inHash) + ); + } + + // TODO(#7218): Revert to fixed height tree for outbox, currently just providing min as interim + // Min size = smallest path of the rollup tree + 1 + (uint256 min,) = MerkleLib.computeMinMaxPathLength(header.contentCommitment.numTxs); + OUTBOX.insert(blockNumber, header.contentCommitment.outHash, min + 1); + + emit L2BlockProposed(blockNumber, _args.archive, blobHashes); + + // Automatically flag the block as proven if we have cheated and set assumeProvenThroughBlockNumber. + if (blockNumber <= assumeProvenThroughBlockNumber) { + _fakeBlockNumberAsProven(blockNumber); + + bool isFeeCanonical = address(this) == FEE_JUICE_PORTAL.canonicalRollup(); + bool isRewardDistributorCanonical = address(this) == REWARD_DISTRIBUTOR.canonicalRollup(); + + if (isFeeCanonical && header.globalVariables.coinbase != address(0) && header.totalFees > 0) { + // @note This will currently fail if there are insufficient funds in the bridge + // which WILL happen for the old version after an upgrade where the bridge follow. + // Consider allowing a failure. See #7938. + FEE_JUICE_PORTAL.distributeFees(header.globalVariables.coinbase, header.totalFees); + } + if (isRewardDistributorCanonical && header.globalVariables.coinbase != address(0)) { + REWARD_DISTRIBUTOR.claim(header.globalVariables.coinbase); + } + + emit L2ProofVerified(blockNumber, "CHEAT"); + } + } + + /** + * @notice Updates the l1 gas fee oracle + * @dev This function is called by the `propose` function + */ + function updateL1GasFeeOracle() public override(IRollupCore) { + Slot slot = Timestamp.wrap(block.timestamp).slotFromTimestamp(); + // The slot where we find a new queued value acceptable + Slot acceptableSlot = rollupStore.l1GasOracleValues.slotOfChange + (LIFETIME - LAG); + + if (slot < acceptableSlot) { + return; + } + + rollupStore.l1GasOracleValues.pre = rollupStore.l1GasOracleValues.post; + rollupStore.l1GasOracleValues.post = + L1FeeData({baseFee: block.basefee, blobFee: ExtRollupLib.getBlobBaseFee(VM_ADDRESS)}); + rollupStore.l1GasOracleValues.slotOfChange = slot + LAG; + } + + /** + * @notice Gets the fee asset price as fee_asset / eth with 1e9 precision + * + * @return The fee asset price + */ + function getFeeAssetPrice() public view override(IRollupCore) returns (uint256) { + return IntRollupLib.feeAssetPriceModifier( + rollupStore.blocks[rollupStore.tips.pendingBlockNumber].feeHeader.feeAssetPriceNumerator + ); + } + + function getL1FeesAt(Timestamp _timestamp) + public + view + override(IRollupCore) + returns (L1FeeData memory) + { + return _timestamp.slotFromTimestamp() < rollupStore.l1GasOracleValues.slotOfChange + ? rollupStore.l1GasOracleValues.pre + : rollupStore.l1GasOracleValues.post; + } + + /** + * @notice Gets the mana base fee components + * For more context, consult: + * https://github.com/AztecProtocol/engineering-designs/blob/main/in-progress/8757-fees/design.md + * + * @dev TODO #10004 - As part of the refactor, will likely get rid of this function or make it private + * keeping it public for now makes it simpler to test. + * + * @param _inFeeAsset - Whether to return the fee in the fee asset or ETH + * + * @return The mana base fee components + */ + function getManaBaseFeeComponentsAt(Timestamp _timestamp, bool _inFeeAsset) + public + view + override(ITestRollup) + returns (ManaBaseFeeComponents memory) + { + // If we can prune, we use the proven block, otherwise the pending block + uint256 blockOfInterest = canPruneAtTime(_timestamp) + ? rollupStore.tips.provenBlockNumber + : rollupStore.tips.pendingBlockNumber; + + return ExtRollupLib.getManaBaseFeeComponentsAt( + rollupStore.blocks[blockOfInterest].feeHeader, + getL1FeesAt(_timestamp), + _inFeeAsset ? getFeeAssetPrice() : 1e9, + TimeLib.getStorage().epochDuration + ); + } + + function quoteToDigest(EpochProofQuote memory _quote) + public + view + override(IRollupCore) + returns (bytes32) + { + return _hashTypedDataV4(IntRollupLib.computeQuoteHash(_quote)); + } + + function validateEpochProofRightClaimAtTime(Timestamp _ts, SignedEpochProofQuote calldata _quote) + public + view + override(IRollupCore) + { + Slot currentSlot = _ts.slotFromTimestamp(); + address currentProposer = ValidatorSelectionLib.getProposerAt( + StakingLib.getStorage(), currentSlot, currentSlot.epochFromSlot() + ); + Epoch epochToProve = getEpochToProve(); + uint256 posInEpoch = TimeLib.positionInEpoch(currentSlot); + bytes32 digest = quoteToDigest(_quote.quote); + + ExtRollupLib.validateEpochProofRightClaimAtTime( + currentSlot, + currentProposer, + epochToProve, + posInEpoch, + _quote, + digest, + rollupStore.proofClaim, + CLAIM_DURATION_IN_L2_SLOTS, + PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST, + PROOF_COMMITMENT_ESCROW + ); + } + + function getEpochForBlock(uint256 _blockNumber) public view override(IRollupCore) returns (Epoch) { + require( + _blockNumber <= rollupStore.tips.pendingBlockNumber, + Errors.Rollup__InvalidBlockNumber(rollupStore.tips.pendingBlockNumber, _blockNumber) + ); + return rollupStore.blocks[_blockNumber].slotNumber.epochFromSlot(); + } + + /** + * @notice Get the epoch that should be proven + * + * @dev This is the epoch that should be proven. It does so by getting the epoch of the block + * following the last proven block. If there is no such block (i.e. the pending chain is + * the same as the proven chain), then revert. + * + * @return uint256 - The epoch to prove + */ + function getEpochToProve() public view override(IRollupCore) returns (Epoch) { + require( + rollupStore.tips.provenBlockNumber != rollupStore.tips.pendingBlockNumber, + Errors.Rollup__NoEpochToProve() + ); + return getEpochForBlock(rollupStore.tips.provenBlockNumber + 1); + } + + function canPrune() public view override(IRollupCore) returns (bool) { + return canPruneAtTime(Timestamp.wrap(block.timestamp)); + } + + function canPruneAtTime(Timestamp _ts) public view override(IRollupCore) returns (bool) { + if ( + rollupStore.tips.pendingBlockNumber == rollupStore.tips.provenBlockNumber + || rollupStore.tips.pendingBlockNumber <= assumeProvenThroughBlockNumber + ) { + return false; + } + + Slot currentSlot = _ts.slotFromTimestamp(); + Epoch oldestPendingEpoch = getEpochForBlock(rollupStore.tips.provenBlockNumber + 1); + Slot startSlotOfPendingEpoch = oldestPendingEpoch.toSlots(); + + // suppose epoch 1 is proven, epoch 2 is pending, epoch 3 is the current epoch. + // we prune the pending chain back to the end of epoch 1 if: + // - the proof claim phase of epoch 3 has ended without a claim to prove epoch 2 (or proof of epoch 2) + // - we reach epoch 4 without a proof of epoch 2 (regardless of whether a proof claim was submitted) + bool inClaimPhase = currentSlot + < startSlotOfPendingEpoch + TimeLib.toSlots(Epoch.wrap(1)) + + Slot.wrap(CLAIM_DURATION_IN_L2_SLOTS); + + bool claimExists = currentSlot < startSlotOfPendingEpoch + TimeLib.toSlots(Epoch.wrap(2)) + && rollupStore.proofClaim.epochToProve == oldestPendingEpoch + && rollupStore.proofClaim.proposerClaimant != address(0); + + if (inClaimPhase || claimExists) { + // If we are in the claim phase, do not prune + return false; + } + return true; + } + + function _prune() internal { + // TODO #8656 + delete rollupStore.proofClaim; + + uint256 pending = rollupStore.tips.pendingBlockNumber; + + // @note We are not deleting the blocks, but we are "winding back" the pendingTip to the last block that was proven. + // We can do because any new block proposed will overwrite a previous block in the block log, + // so no values should "survive". + // People must therefore read the chain using the pendingTip as a boundary. + rollupStore.tips.pendingBlockNumber = rollupStore.tips.provenBlockNumber; + + emit PrunedPending(rollupStore.tips.provenBlockNumber, pending); + } + + /** + * @notice Validates the header for submission + * + * @param _header - The proposed block header + * @param _signatures - The signatures for the attestations + * @param _digest - The digest that signatures signed + * @param _currentTime - The time of execution + * @param _blobsHashesCommitment - The blobs hash for this block + * @dev - This value is provided to allow for simple simulation of future + * @param _flags - Flags specific to the execution, whether certain checks should be skipped + */ + function _validateHeader( + Header memory _header, + Signature[] memory _signatures, + bytes32 _digest, + Timestamp _currentTime, + uint256 _manaBaseFee, + bytes32 _blobsHashesCommitment, + DataStructures.ExecutionFlags memory _flags + ) internal view { + uint256 pendingBlockNumber = canPruneAtTime(_currentTime) + ? rollupStore.tips.provenBlockNumber + : rollupStore.tips.pendingBlockNumber; + + ExtRollupLib.validateHeaderForSubmissionBase( + ValidateHeaderArgs({ + header: _header, + currentTime: _currentTime, + manaBaseFee: _manaBaseFee, + blobsHashesCommitment: _blobsHashesCommitment, + pendingBlockNumber: pendingBlockNumber, + flags: _flags, + version: VERSION, + feeJuicePortal: FEE_JUICE_PORTAL + }), + rollupStore.blocks + ); + _validateHeaderForSubmissionSequencerSelection( + Slot.wrap(_header.globalVariables.slotNumber), _signatures, _digest, _currentTime, _flags + ); + } + + /** + * @notice Validate a header for submission to the pending chain (sequencer selection checks) + * + * These validation checks are directly related to sequencer selection. + * Note that while these checks are strict, they can be relaxed with some changes to + * message boxes. + * + * Each of the following validation checks must pass, otherwise an error is thrown and we revert. + * - The slot MUST be the current slot + * This might be relaxed for allow consensus set to better handle short-term bursts of L1 congestion + * - The slot MUST be in the current epoch + * + * @param _slot - The slot of the header to validate + * @param _signatures - The signatures to validate + * @param _digest - The digest that signatures sign over + */ + function _validateHeaderForSubmissionSequencerSelection( + Slot _slot, + Signature[] memory _signatures, + bytes32 _digest, + Timestamp _currentTime, + DataStructures.ExecutionFlags memory _flags + ) internal view { + // Ensure that the slot proposed is NOT in the future + Slot currentSlot = _currentTime.slotFromTimestamp(); + require(_slot == currentSlot, Errors.HeaderLib__InvalidSlotNumber(currentSlot, _slot)); + + // @note We are currently enforcing that the slot is in the current epoch + // If this is not the case, there could potentially be a weird reorg + // of an entire epoch if no-one from the new epoch committee have seen + // those blocks or behaves as if they did not. + + Epoch epochNumber = _slot.epochFromSlot(); + Epoch currentEpoch = _currentTime.epochFromTimestamp(); + require(epochNumber == currentEpoch, Errors.Rollup__InvalidEpoch(currentEpoch, epochNumber)); + + ValidatorSelectionLib.validateValidatorSelection( + StakingLib.getStorage(), _slot, epochNumber, _signatures, _digest, _flags + ); + } + + // Helper to avoid stack too deep + function _toBlockLog(ProposeArgs calldata _args, uint256 _blockNumber, uint256 _congestionCost) + internal + view + returns (BlockLog memory) + { + FeeHeader memory parentFeeHeader = rollupStore.blocks[_blockNumber - 1].feeHeader; + return BlockLog({ + archive: _args.archive, + blockHash: _args.blockHash, + slotNumber: Slot.wrap(uint256(bytes32(_args.header[0x0194:0x01b4]))), + feeHeader: FeeHeader({ + excessMana: IntRollupLib.computeExcessMana(parentFeeHeader), + feeAssetPriceNumerator: parentFeeHeader.feeAssetPriceNumerator.clampedAdd( + _args.oracleInput.feeAssetPriceModifier + ), + manaUsed: uint256(bytes32(_args.header[0x0268:0x0288])), + provingCostPerManaNumerator: parentFeeHeader.provingCostPerManaNumerator.clampedAdd( + _args.oracleInput.provingCostModifier + ), + congestionCost: _congestionCost + }) + }); + } + + function _fakeBlockNumberAsProven(uint256 _blockNumber) private { + if ( + _blockNumber > rollupStore.tips.provenBlockNumber + && _blockNumber <= rollupStore.tips.pendingBlockNumber + ) { + rollupStore.tips.provenBlockNumber = _blockNumber; + + // If this results on a new epoch, create a fake claim for it + // Otherwise nextEpochToProve will report an old epoch + Epoch epoch = getEpochForBlock(_blockNumber); + if ( + Epoch.unwrap(epoch) == 0 + || Epoch.unwrap(epoch) > Epoch.unwrap(rollupStore.proofClaim.epochToProve) + ) { + rollupStore.proofClaim = DataStructures.EpochProofClaim({ + epochToProve: epoch, + basisPointFee: 0, + bondAmount: 0, + bondProvider: address(0), + proposerClaimant: msg.sender + }); + } + } + } +} diff --git a/l1-contracts/src/core/ValidatorSelection.sol b/l1-contracts/src/core/ValidatorSelection.sol deleted file mode 100644 index 9dc6b172652..00000000000 --- a/l1-contracts/src/core/ValidatorSelection.sol +++ /dev/null @@ -1,453 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2024 Aztec Labs. -pragma solidity >=0.8.27; - -import {IStaking, ValidatorInfo, Exit, OperatorInfo} from "@aztec/core/interfaces/IStaking.sol"; -import { - IValidatorSelection, - EpochData, - ValidatorSelectionStorage -} from "@aztec/core/interfaces/IValidatorSelection.sol"; -import {Signature} from "@aztec/core/libraries/crypto/SignatureLib.sol"; -import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; -import {Errors} from "@aztec/core/libraries/Errors.sol"; -import {StakingLib} from "@aztec/core/libraries/staking/StakingLib.sol"; -import { - Timestamp, Slot, Epoch, SlotLib, EpochLib, TimeLib -} from "@aztec/core/libraries/TimeLib.sol"; -import {ValidatorSelectionLib} from - "@aztec/core/libraries/ValidatorSelectionLib/ValidatorSelectionLib.sol"; -import {Slasher} from "@aztec/core/staking/Slasher.sol"; -import {IERC20} from "@oz/token/ERC20/IERC20.sol"; -import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol"; - -/** - * @title Validator Selection - * - * @dev Validator Selection has one thing in mind, he provide a reference of the LOGIC going on for the spartan selection. - * It is a reference implementation, it is not optimized for gas. - * - */ -contract ValidatorSelection is IValidatorSelection, IStaking { - using EnumerableSet for EnumerableSet.AddressSet; - - using SlotLib for Slot; - using EpochLib for Epoch; - using TimeLib for Timestamp; - using TimeLib for Slot; - using TimeLib for Epoch; - - // The target number of validators in a committee - // @todo #8021 - uint256 public immutable TARGET_COMMITTEE_SIZE; - - ValidatorSelectionStorage private validatorSelectionStore; - - constructor( - IERC20 _stakingAsset, - uint256 _minimumStake, - uint256 _slashingQuorum, - uint256 _roundSize, - uint256 _slotDuration, - uint256 _epochDuration, - uint256 _targetCommitteeSize - ) { - TARGET_COMMITTEE_SIZE = _targetCommitteeSize; - - TimeLib.initialize(block.timestamp, _slotDuration, _epochDuration); - - Timestamp exitDelay = Timestamp.wrap(60 * 60 * 24); - Slasher slasher = new Slasher(_slashingQuorum, _roundSize); - StakingLib.initialize(_stakingAsset, _minimumStake, exitDelay, address(slasher)); - } - - function deposit(address _attester, address _proposer, address _withdrawer, uint256 _amount) - external - override(IStaking) - { - setupEpoch(); - require( - _attester != address(0) && _proposer != address(0), - Errors.ValidatorSelection__InvalidDeposit(_attester, _proposer) - ); - StakingLib.deposit(_attester, _proposer, _withdrawer, _amount); - } - - function initiateWithdraw(address _attester, address _recipient) - external - override(IStaking) - returns (bool) - { - // @note The attester might be chosen for the epoch, so the delay must be long enough - // to allow for that. - setupEpoch(); - return StakingLib.initiateWithdraw(_attester, _recipient); - } - - function finaliseWithdraw(address _attester) external override(IStaking) { - StakingLib.finaliseWithdraw(_attester); - } - - function slash(address _attester, uint256 _amount) external override(IStaking) { - StakingLib.slash(_attester, _amount); - } - - function getGenesisTime() external view override(IValidatorSelection) returns (Timestamp) { - return Timestamp.wrap(TimeLib.getStorage().genesisTime); - } - - function getSlotDuration() external view override(IValidatorSelection) returns (uint256) { - return TimeLib.getStorage().slotDuration; - } - - function getEpochDuration() external view override(IValidatorSelection) returns (uint256) { - return TimeLib.getStorage().epochDuration; - } - - function getSlasher() external view override(IStaking) returns (address) { - return StakingLib.getStorage().slasher; - } - - function getStakingAsset() external view override(IStaking) returns (IERC20) { - return StakingLib.getStorage().stakingAsset; - } - - function getMinimumStake() external view override(IStaking) returns (uint256) { - return StakingLib.getStorage().minimumStake; - } - - function getExitDelay() external view override(IStaking) returns (Timestamp) { - return StakingLib.getStorage().exitDelay; - } - - function getActiveAttesterCount() external view override(IStaking) returns (uint256) { - return StakingLib.getStorage().attesters.length(); - } - - function getProposerForAttester(address _attester) - external - view - override(IStaking) - returns (address) - { - return StakingLib.getStorage().info[_attester].proposer; - } - - function getAttesterAtIndex(uint256 _index) external view override(IStaking) returns (address) { - return StakingLib.getStorage().attesters.at(_index); - } - - function getProposerAtIndex(uint256 _index) external view override(IStaking) returns (address) { - return StakingLib.getStorage().info[StakingLib.getStorage().attesters.at(_index)].proposer; - } - - function getInfo(address _attester) - external - view - override(IStaking) - returns (ValidatorInfo memory) - { - return StakingLib.getStorage().info[_attester]; - } - - function getExit(address _attester) external view override(IStaking) returns (Exit memory) { - return StakingLib.getStorage().exits[_attester]; - } - - function getOperatorAtIndex(uint256 _index) - external - view - override(IStaking) - returns (OperatorInfo memory) - { - address attester = StakingLib.getStorage().attesters.at(_index); - return - OperatorInfo({proposer: StakingLib.getStorage().info[attester].proposer, attester: attester}); - } - - /** - * @notice Get the validator set for a given epoch - * - * @dev Consider removing this to replace with a `size` and individual getter. - * - * @param _epoch The epoch number to get the validator set for - * - * @return The validator set for the given epoch - */ - function getEpochCommittee(Epoch _epoch) - external - view - override(IValidatorSelection) - returns (address[] memory) - { - return validatorSelectionStore.epochs[_epoch].committee; - } - - /** - * @notice Get the validator set for the current epoch - * @return The validator set for the current epoch - */ - function getCurrentEpochCommittee() - external - view - override(IValidatorSelection) - returns (address[] memory) - { - return ValidatorSelectionLib.getCommitteeAt( - validatorSelectionStore, StakingLib.getStorage(), getCurrentEpoch(), TARGET_COMMITTEE_SIZE - ); - } - - /** - * @notice Get the committee for a given timestamp - * - * @param _ts - The timestamp to get the committee for - * - * @return The committee for the given timestamp - */ - function getCommitteeAt(Timestamp _ts) - external - view - override(IValidatorSelection) - returns (address[] memory) - { - return ValidatorSelectionLib.getCommitteeAt( - validatorSelectionStore, StakingLib.getStorage(), getEpochAt(_ts), TARGET_COMMITTEE_SIZE - ); - } - - /** - * @notice Get the sample seed for a given timestamp - * - * @param _ts - The timestamp to get the sample seed for - * - * @return The sample seed for the given timestamp - */ - function getSampleSeedAt(Timestamp _ts) - external - view - override(IValidatorSelection) - returns (uint256) - { - return ValidatorSelectionLib.getSampleSeed(validatorSelectionStore, getEpochAt(_ts)); - } - - /** - * @notice Get the sample seed for the current epoch - * - * @return The sample seed for the current epoch - */ - function getCurrentSampleSeed() external view override(IValidatorSelection) returns (uint256) { - return ValidatorSelectionLib.getSampleSeed(validatorSelectionStore, getCurrentEpoch()); - } - - /** - * @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 ValidatorSelectionStore 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 - */ - function setupEpoch() public override(IValidatorSelection) { - Epoch epochNumber = getCurrentEpoch(); - EpochData storage epoch = validatorSelectionStore.epochs[epochNumber]; - - if (epoch.sampleSeed == 0) { - epoch.sampleSeed = ValidatorSelectionLib.getSampleSeed(validatorSelectionStore, epochNumber); - epoch.nextSeed = validatorSelectionStore.lastSeed = _computeNextSeed(epochNumber); - epoch.committee = ValidatorSelectionLib.sampleValidators( - StakingLib.getStorage(), epoch.sampleSeed, TARGET_COMMITTEE_SIZE - ); - } - } - - /** - * @notice Get the attester set - * - * @dev Consider removing this to replace with a `size` and individual getter. - * - * @return The validator set - */ - function getAttesters() public view override(IValidatorSelection) returns (address[] memory) { - return StakingLib.getStorage().attesters.values(); - } - - /** - * @notice Get the current epoch number - * - * @return The current epoch number - */ - function getCurrentEpoch() public view override(IValidatorSelection) returns (Epoch) { - return getEpochAt(Timestamp.wrap(block.timestamp)); - } - - /** - * @notice Get the current slot number - * - * @return The current slot number - */ - function getCurrentSlot() public view override(IValidatorSelection) returns (Slot) { - return getSlotAt(Timestamp.wrap(block.timestamp)); - } - - /** - * @notice Get the timestamp for a given slot - * - * @param _slotNumber - The slot number to get the timestamp for - * - * @return The timestamp for the given slot - */ - function getTimestampForSlot(Slot _slotNumber) - public - view - override(IValidatorSelection) - returns (Timestamp) - { - return _slotNumber.toTimestamp(); - } - - /** - * @notice Get the proposer for the current slot - * - * @dev Calls `getCurrentProposer(uint256)` with the current timestamp - * - * @return The address of the proposer - */ - function getCurrentProposer() public view override(IValidatorSelection) returns (address) { - return getProposerAt(Timestamp.wrap(block.timestamp)); - } - - /** - * @notice Get the proposer for the slot at a specific timestamp - * - * @dev This function is very useful for off-chain usage, as it easily allow a client to - * determine who will be the proposer at the NEXT ethereum block. - * Should not be trusted when moving beyond the current epoch, since changes to the - * validator set might not be reflected when we actually reach that epoch (more changes - * might have happened). - * - * @dev The proposer is selected from the validator set of the current epoch. - * - * @dev Should only be access on-chain if epoch is setup, otherwise very expensive. - * - * @dev A return value of address(0) means that the proposer is "open" and can be anyone. - * - * @dev If the current epoch is the first epoch, returns address(0) - * If the current epoch is setup, we will return the proposer for the current slot - * If the current epoch is not setup, we will perform a sample as if it was (gas heavy) - * - * @return The address of the proposer - */ - function getProposerAt(Timestamp _ts) public view override(IValidatorSelection) returns (address) { - Slot slot = getSlotAt(_ts); - Epoch epochNumber = getEpochAtSlot(slot); - return ValidatorSelectionLib.getProposerAt( - validatorSelectionStore, StakingLib.getStorage(), slot, epochNumber, TARGET_COMMITTEE_SIZE - ); - } - - /** - * @notice Computes the epoch at a specific time - * - * @param _ts - The timestamp to compute the epoch for - * - * @return The computed epoch - */ - function getEpochAt(Timestamp _ts) public view override(IValidatorSelection) returns (Epoch) { - return _ts.epochFromTimestamp(); - } - - /** - * @notice Computes the slot at a specific time - * - * @param _ts - The timestamp to compute the slot for - * - * @return The computed slot - */ - function getSlotAt(Timestamp _ts) public view override(IValidatorSelection) returns (Slot) { - return _ts.slotFromTimestamp(); - } - - /** - * @notice Computes the epoch at a specific slot - * - * @param _slotNumber - The slot number to compute the epoch for - * - * @return The computed epoch - */ - function getEpochAtSlot(Slot _slotNumber) - public - view - override(IValidatorSelection) - returns (Epoch) - { - return _slotNumber.epochFromSlot(); - } - - // Can be used to add validators without setting up the epoch, useful for the initial set. - function _cheat__Deposit( - address _attester, - address _proposer, - address _withdrawer, - uint256 _amount - ) internal { - require( - _attester != address(0) && _proposer != address(0), - Errors.ValidatorSelection__InvalidDeposit(_attester, _proposer) - ); - - StakingLib.deposit(_attester, _proposer, _withdrawer, _amount); - } - - /** - * @notice Propose a pending block from the point-of-view of sequencer selection. Will: - * - Setup the epoch if needed (if epoch committee is empty skips the rest) - * - Validate that the proposer is the proposer of the slot - * - Validate that the signatures for attestations are indeed from the validatorset - * - Validate that the number of valid attestations is sufficient - * - * @dev Cases where errors are thrown: - * - If the epoch is not setup - * - If the proposer is not the real proposer AND the proposer is not open - * - If the number of valid attestations is insufficient - * - * @param _slot - The slot of the block - * @param _signatures - The signatures of the committee members - * @param _digest - The digest of the block - */ - function _validateValidatorSelection( - Slot _slot, - Signature[] memory _signatures, - bytes32 _digest, - DataStructures.ExecutionFlags memory _flags - ) internal view { - Epoch epochNumber = getEpochAtSlot(_slot); - ValidatorSelectionLib.validateValidatorSelection( - validatorSelectionStore, - StakingLib.getStorage(), - _slot, - epochNumber, - _signatures, - _digest, - _flags, - TARGET_COMMITTEE_SIZE - ); - } - - /** - * @notice Computes the nextSeed for an epoch - * - * @dev We include the `_epoch` instead of using the randao directly to avoid issues with foundry testing - * where randao == 0. - * - * @param _epoch - The epoch to compute the seed for - * - * @return The computed seed - */ - function _computeNextSeed(Epoch _epoch) private view returns (uint256) { - return uint256(keccak256(abi.encode(_epoch, block.prevrandao))); - } -} diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index c8216166cf0..edda9efec3c 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -76,7 +76,7 @@ interface ITestRollup { returns (ManaBaseFeeComponents memory); } -interface IRollup { +interface IRollupCore { event L2BlockProposed( uint256 indexed blockNumber, bytes32 indexed archive, bytes32[] versionedBlobHashes ); @@ -112,17 +112,6 @@ interface IRollup { function submitEpochRootProof(SubmitEpochRootProofArgs calldata _args) external; - function canProposeAtTime(Timestamp _ts, bytes32 _archive) external view returns (Slot, uint256); - - function validateHeader( - bytes calldata _header, - Signature[] memory _signatures, - bytes32 _digest, - Timestamp _currentTime, - bytes32 _blobsHash, - DataStructures.ExecutionFlags memory _flags - ) external view; - // solhint-disable-next-line func-name-mixedcase function INBOX() external view returns (IInbox); @@ -132,7 +121,22 @@ interface IRollup { // solhint-disable-next-line func-name-mixedcase function L1_BLOCK_AT_GENESIS() external view returns (uint256); - function getProofClaim() external view returns (DataStructures.EpochProofClaim memory); + function quoteToDigest(EpochProofQuote memory _quote) external view returns (bytes32); + + function getFeeAssetPrice() external view returns (uint256); + function getL1FeesAt(Timestamp _timestamp) external view returns (L1FeeData memory); + + function canPrune() external view returns (bool); + function canPruneAtTime(Timestamp _ts) external view returns (bool); + function getEpochToProve() external view returns (Epoch); + + function validateEpochProofRightClaimAtTime(Timestamp _ts, SignedEpochProofQuote calldata _quote) + external + view; + function getEpochForBlock(uint256 _blockNumber) external view returns (Epoch); +} + +interface IRollup is IRollupCore { function getTips() external view returns (ChainTips memory); function status(uint256 _myHeaderBlockNumber) @@ -147,25 +151,8 @@ interface IRollup { Epoch provenEpochNumber ); - function quoteToDigest(EpochProofQuote memory _quote) external view returns (bytes32); - function getBlock(uint256 _blockNumber) external view returns (BlockLog memory); - function getFeeAssetPrice() external view returns (uint256); - function getManaBaseFeeAt(Timestamp _timestamp, bool _inFeeAsset) external view returns (uint256); - function getL1FeesAt(Timestamp _timestamp) external view returns (L1FeeData memory); + function getProofClaim() external view returns (DataStructures.EpochProofClaim memory); - function archive() external view returns (bytes32); - function archiveAt(uint256 _blockNumber) external view returns (bytes32); - function canPrune() external view returns (bool); - function canPruneAtTime(Timestamp _ts) external view returns (bool); - function getProvenBlockNumber() external view returns (uint256); - function getPendingBlockNumber() external view returns (uint256); - function getBlobPublicInputsHash(uint256 _blockNumber) external view returns (bytes32); - function getEpochToProve() external view returns (Epoch); - function getClaimableEpoch() external view returns (Epoch); - function validateEpochProofRightClaimAtTime(Timestamp _ts, SignedEpochProofQuote calldata _quote) - external - view; - function getEpochForBlock(uint256 _blockNumber) external view returns (Epoch); function getEpochProofPublicInputs( uint256 _epochSize, bytes32[7] calldata _args, @@ -173,8 +160,31 @@ interface IRollup { bytes calldata _blobPublicInputs, bytes calldata _aggregationObject ) external view returns (bytes32[] memory); + + function getClaimableEpoch() external view returns (Epoch); + + function validateHeader( + bytes calldata _header, + Signature[] memory _signatures, + bytes32 _digest, + Timestamp _currentTime, + bytes32 _blobsHash, + DataStructures.ExecutionFlags memory _flags + ) external view; + + function canProposeAtTime(Timestamp _ts, bytes32 _archive) external view returns (Slot, uint256); + function validateBlobs(bytes calldata _blobsInputs) external view returns (bytes32[] memory, bytes32, bytes32); + + function getManaBaseFeeAt(Timestamp _timestamp, bool _inFeeAsset) external view returns (uint256); + + function archive() external view returns (bytes32); + function archiveAt(uint256 _blockNumber) external view returns (bytes32); + function getProvenBlockNumber() external view returns (uint256); + function getPendingBlockNumber() external view returns (uint256); + function getBlock(uint256 _blockNumber) external view returns (BlockLog memory); + function getBlobPublicInputsHash(uint256 _blockNumber) external view returns (bytes32); } diff --git a/l1-contracts/src/core/interfaces/IStaking.sol b/l1-contracts/src/core/interfaces/IStaking.sol index a3a7414f605..3460af47db1 100644 --- a/l1-contracts/src/core/interfaces/IStaking.sol +++ b/l1-contracts/src/core/interfaces/IStaking.sol @@ -45,7 +45,7 @@ struct StakingStorage { mapping(address attester => Exit) exits; } -interface IStaking { +interface IStakingCore { event Deposit( address indexed attester, address indexed proposer, address indexed withdrawer, uint256 amount ); @@ -58,7 +58,9 @@ interface IStaking { function initiateWithdraw(address _attester, address _recipient) external returns (bool); function finaliseWithdraw(address _attester) external; function slash(address _attester, uint256 _amount) external; +} +interface IStaking is IStakingCore { function getInfo(address _attester) external view returns (ValidatorInfo memory); function getExit(address _attester) external view returns (Exit memory); function getActiveAttesterCount() external view returns (uint256); diff --git a/l1-contracts/src/core/interfaces/IValidatorSelection.sol b/l1-contracts/src/core/interfaces/IValidatorSelection.sol index 07dab165693..387abe16b66 100644 --- a/l1-contracts/src/core/interfaces/IValidatorSelection.sol +++ b/l1-contracts/src/core/interfaces/IValidatorSelection.sol @@ -21,11 +21,15 @@ struct ValidatorSelectionStorage { mapping(Epoch => EpochData) epochs; // The last stored randao value, same value as `seed` in the last inserted epoch uint256 lastSeed; + uint256 targetCommitteeSize; } -interface IValidatorSelection { - // Likely changing to optimize in Pleistarchus +interface IValidatorSelectionCore { function setupEpoch() external; +} + +interface IValidatorSelection is IValidatorSelectionCore { + // Likely changing to optimize in Pleistarchus function getCurrentProposer() external view returns (address); function getProposerAt(Timestamp _ts) external view returns (address); @@ -53,4 +57,5 @@ interface IValidatorSelection { function getGenesisTime() external view returns (Timestamp); function getSlotDuration() external view returns (uint256); function getEpochDuration() external view returns (uint256); + function getTargetCommitteeSize() external view returns (uint256); } diff --git a/l1-contracts/src/core/libraries/Errors.sol b/l1-contracts/src/core/libraries/Errors.sol index 9084c90fa4b..afd700cc04a 100644 --- a/l1-contracts/src/core/libraries/Errors.sol +++ b/l1-contracts/src/core/libraries/Errors.sol @@ -109,6 +109,7 @@ library Errors { error Staking__AlreadyRegistered(address); // 0x18047699 error Staking__CannotSlashExitedStake(address); // 0x45bf4940 error Staking__FailedToRemove(address); // 0xa7d7baab + error Staking__InvalidDeposit(address attester, address proposer); // 0xf33fe8c6 error Staking__InsufficientStake(uint256, uint256); // 0x903aee24 error Staking__NoOneToSlash(address); // 0x7e2f7f1c error Staking__NotExiting(address); // 0xef566ee0 diff --git a/l1-contracts/src/core/libraries/RollupLibs/ValidationLib.sol b/l1-contracts/src/core/libraries/RollupLibs/ValidationLib.sol index 3738b5f7920..72931ba060e 100644 --- a/l1-contracts/src/core/libraries/RollupLibs/ValidationLib.sol +++ b/l1-contracts/src/core/libraries/RollupLibs/ValidationLib.sol @@ -9,6 +9,7 @@ import {SignatureLib} from "@aztec/core/libraries/crypto/SignatureLib.sol"; import {DataStructures} from "./../DataStructures.sol"; import {Errors} from "./../Errors.sol"; import {Timestamp, Slot, Epoch} from "./../TimeLib.sol"; +import {TimeLib} from "./../TimeLib.sol"; import {SignedEpochProofQuote} from "./EpochProofQuoteLib.sol"; import {Header} from "./HeaderLib.sol"; @@ -21,7 +22,6 @@ struct ValidateHeaderArgs { DataStructures.ExecutionFlags flags; uint256 version; IFeeJuicePortal feeJuicePortal; - function(Slot) external view returns (Timestamp) getTimestampForSlot; } library ValidationLib { @@ -56,7 +56,7 @@ library ValidationLib { Slot lastSlot = _blocks[_args.pendingBlockNumber].slotNumber; require(slot > lastSlot, Errors.Rollup__SlotAlreadyInChain(lastSlot, slot)); - Timestamp timestamp = _args.getTimestampForSlot(slot); + Timestamp timestamp = TimeLib.toTimestamp(slot); require( Timestamp.wrap(_args.header.globalVariables.timestamp) == timestamp, Errors.Rollup__InvalidTimestamp( diff --git a/l1-contracts/src/core/libraries/ValidatorSelectionLib/ValidatorSelectionLib.sol b/l1-contracts/src/core/libraries/ValidatorSelectionLib/ValidatorSelectionLib.sol index 981b7d92b2e..d2830f4a8cd 100644 --- a/l1-contracts/src/core/libraries/ValidatorSelectionLib/ValidatorSelectionLib.sol +++ b/l1-contracts/src/core/libraries/ValidatorSelectionLib/ValidatorSelectionLib.sol @@ -10,7 +10,7 @@ import {SampleLib} from "@aztec/core/libraries/crypto/SampleLib.sol"; import {SignatureLib, Signature} from "@aztec/core/libraries/crypto/SignatureLib.sol"; import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; -import {Slot, Epoch} from "@aztec/core/libraries/TimeLib.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"; @@ -18,43 +18,31 @@ library ValidatorSelectionLib { using EnumerableSet for EnumerableSet.AddressSet; using MessageHashUtils for bytes32; using SignatureLib for Signature; + using TimeLib for Timestamp; + + bytes32 private constant VALIDATOR_SELECTION_STORAGE_POSITION = + keccak256("aztec.validator_selection.storage"); /** - * @notice Samples a validator set for a specific epoch - * - * @dev Only used internally, should never be called for anything but the "next" epoch - * Allowing us to always use `lastSeed`. + * @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 * - * @return The validators for the given epoch + * @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 */ - function sampleValidators( - StakingStorage storage _stakingStore, - uint256 _seed, - uint256 _targetCommitteeSize - ) external view returns (address[] memory) { - return _sampleValidators(_stakingStore, _seed, _targetCommitteeSize); - } + function setupEpoch(StakingStorage storage _stakingStore) external { + Epoch epochNumber = Timestamp.wrap(block.timestamp).epochFromTimestamp(); + ValidatorSelectionStorage storage store = getStorage(); + EpochData storage epoch = store.epochs[epochNumber]; - function getProposerAt( - ValidatorSelectionStorage storage _validatorSelectionStore, - StakingStorage storage _stakingStore, - Slot _slot, - Epoch _epochNumber, - uint256 _targetCommitteeSize - ) external view returns (address) { - return _getProposerAt( - _validatorSelectionStore, _stakingStore, _slot, _epochNumber, _targetCommitteeSize - ); - } - - function getCommitteeAt( - ValidatorSelectionStorage storage _validatorSelectionStore, - StakingStorage storage _stakingStore, - Epoch _epochNumber, - uint256 _targetCommitteeSize - ) external view returns (address[] memory) { - return - _getCommitteeAt(_validatorSelectionStore, _stakingStore, _epochNumber, _targetCommitteeSize); + if (epoch.sampleSeed == 0) { + epoch.sampleSeed = getSampleSeed(epochNumber); + epoch.nextSeed = store.lastSeed = computeNextSeed(epochNumber); + epoch.committee = sampleValidators(_stakingStore, epoch.sampleSeed); + } } /** @@ -74,23 +62,20 @@ library ValidatorSelectionLib { * @param _digest - The digest of the block */ function validateValidatorSelection( - ValidatorSelectionStorage storage _validatorSelectionStore, StakingStorage storage _stakingStore, Slot _slot, Epoch _epochNumber, Signature[] memory _signatures, bytes32 _digest, - DataStructures.ExecutionFlags memory _flags, - uint256 _targetCommitteeSize + DataStructures.ExecutionFlags memory _flags ) external view { // Same logic as we got in getProposerAt // Done do avoid duplicate computing the committee - address[] memory committee = - _getCommitteeAt(_validatorSelectionStore, _stakingStore, _epochNumber, _targetCommitteeSize); + address[] memory committee = getCommitteeAt(_stakingStore, _epochNumber); address attester = committee.length == 0 ? address(0) : committee[computeProposerIndex( - _epochNumber, _slot, getSampleSeed(_validatorSelectionStore, _epochNumber), committee.length + _epochNumber, _slot, getSampleSeed(_epochNumber), committee.length )]; address proposer = _stakingStore.info[attester].proposer; @@ -137,39 +122,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(ValidatorSelectionStorage storage _validatorSelectionStore, Epoch _epoch) - internal + function getProposerAt(StakingStorage storage _stakingStore, Slot _slot, Epoch _epochNumber) + external view - returns (uint256) + returns (address) { - if (Epoch.unwrap(_epoch) == 0) { - return type(uint256).max; - } - uint256 sampleSeed = _validatorSelectionStore.epochs[_epoch].sampleSeed; - if (sampleSeed != 0) { - return sampleSeed; + // @note this is deliberately "bad" for the simple reason of code reduction. + // it does not need to actually return the full committee and then draw from it + // it can just return the proposer directly, but then we duplicate the code + // which we just don't have room for right now... + address[] memory committee = getCommitteeAt(_stakingStore, _epochNumber); + if (committee.length == 0) { + return address(0); } - sampleSeed = _validatorSelectionStore.epochs[_epoch - Epoch.wrap(1)].nextSeed; - if (sampleSeed != 0) { - return sampleSeed; - } + address attester = committee[computeProposerIndex( + _epochNumber, _slot, getSampleSeed(_epochNumber), committee.length + )]; - return _validatorSelectionStore.lastSeed; + return _stakingStore.info[attester].proposer; } /** @@ -180,62 +151,41 @@ library ValidatorSelectionLib { * * @return The validators for the given epoch */ - function _sampleValidators( - StakingStorage storage _stakingStore, - uint256 _seed, - uint256 _targetCommitteeSize - ) private view returns (address[] memory) { + function sampleValidators(StakingStorage storage _stakingStore, uint256 _seed) + public + view + returns (address[] memory) + { uint256 validatorSetSize = _stakingStore.attesters.length(); if (validatorSetSize == 0) { return new address[](0); } + ValidatorSelectionStorage storage store = getStorage(); + uint256 targetCommitteeSize = store.targetCommitteeSize; + // If we have less validators than the target committee size, we just return the full set - if (validatorSetSize <= _targetCommitteeSize) { + if (validatorSetSize <= targetCommitteeSize) { return _stakingStore.attesters.values(); } uint256[] memory indices = - SampleLib.computeCommitteeClever(_targetCommitteeSize, validatorSetSize, _seed); + SampleLib.computeCommitteeClever(targetCommitteeSize, validatorSetSize, _seed); - address[] memory committee = new address[](_targetCommitteeSize); - for (uint256 i = 0; i < _targetCommitteeSize; i++) { + address[] memory committee = new address[](targetCommitteeSize); + for (uint256 i = 0; i < targetCommitteeSize; i++) { committee[i] = _stakingStore.attesters.at(indices[i]); } return committee; } - function _getProposerAt( - ValidatorSelectionStorage storage _validatorSelectionStore, - StakingStorage storage _stakingStore, - Slot _slot, - Epoch _epochNumber, - uint256 _targetCommitteeSize - ) private view returns (address) { - // @note this is deliberately "bad" for the simple reason of code reduction. - // it does not need to actually return the full committee and then draw from it - // it can just return the proposer directly, but then we duplicate the code - // which we just don't have room for right now... - address[] memory committee = - _getCommitteeAt(_validatorSelectionStore, _stakingStore, _epochNumber, _targetCommitteeSize); - if (committee.length == 0) { - return address(0); - } - - address attester = committee[computeProposerIndex( - _epochNumber, _slot, getSampleSeed(_validatorSelectionStore, _epochNumber), committee.length - )]; - - return _stakingStore.info[attester].proposer; - } - - function _getCommitteeAt( - ValidatorSelectionStorage storage _validatorSelectionStore, - StakingStorage storage _stakingStore, - Epoch _epochNumber, - uint256 _targetCommitteeSize - ) private view returns (address[] memory) { - EpochData storage epoch = _validatorSelectionStore.epochs[_epochNumber]; + function getCommitteeAt(StakingStorage storage _stakingStore, Epoch _epochNumber) + public + view + returns (address[] memory) + { + ValidatorSelectionStorage storage store = getStorage(); + EpochData storage epoch = store.epochs[_epochNumber]; if (epoch.sampleSeed != 0) { uint256 committeeSize = epoch.committee.length; @@ -251,8 +201,66 @@ library ValidatorSelectionLib { } // Emulate a sampling of the validators - uint256 sampleSeed = getSampleSeed(_validatorSelectionStore, _epochNumber); - return _sampleValidators(_stakingStore, sampleSeed, _targetCommitteeSize); + uint256 sampleSeed = getSampleSeed(_epochNumber); + return sampleValidators(_stakingStore, sampleSeed); + } + + function initialize(uint256 _targetCommitteeSize) internal { + ValidatorSelectionStorage storage store = getStorage(); + store.targetCommitteeSize = _targetCommitteeSize; + } + + /** + * @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 (uint256) { + if (Epoch.unwrap(_epoch) == 0) { + return type(uint256).max; + } + ValidatorSelectionStorage storage store = getStorage(); + uint256 sampleSeed = store.epochs[_epoch].sampleSeed; + if (sampleSeed != 0) { + return sampleSeed; + } + + sampleSeed = store.epochs[_epoch - Epoch.wrap(1)].nextSeed; + if (sampleSeed != 0) { + return sampleSeed; + } + + return store.lastSeed; + } + + function getStorage() internal pure returns (ValidatorSelectionStorage storage storageStruct) { + bytes32 position = VALIDATOR_SELECTION_STORAGE_POSITION; + assembly { + storageStruct.slot := position + } + } + + /** + * @notice Computes the nextSeed for an epoch + * + * @dev We include the `_epoch` instead of using the randao directly to avoid issues with foundry testing + * where randao == 0. + * + * @param _epoch - The epoch to compute the seed for + * + * @return The computed seed + */ + function computeNextSeed(Epoch _epoch) private view returns (uint256) { + return uint256(keccak256(abi.encode(_epoch, block.prevrandao))); } /** diff --git a/l1-contracts/src/core/libraries/staking/StakingLib.sol b/l1-contracts/src/core/libraries/staking/StakingLib.sol index 0071f09b626..6c02dfdf54d 100644 --- a/l1-contracts/src/core/libraries/staking/StakingLib.sol +++ b/l1-contracts/src/core/libraries/staking/StakingLib.sol @@ -7,9 +7,9 @@ import { ValidatorInfo, Exit, Timestamp, - StakingStorage + StakingStorage, + IStakingCore } from "@aztec/core/interfaces/IStaking.sol"; -import {IStaking} from "@aztec/core/interfaces/IStaking.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; import {IERC20} from "@oz/token/ERC20/IERC20.sol"; import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; @@ -53,7 +53,7 @@ library StakingLib { store.stakingAsset.transfer(recipient, amount); - emit IStaking.WithdrawFinalised(_attester, recipient, amount); + emit IStakingCore.WithdrawFinalised(_attester, recipient, amount); } function slash(address _attester, uint256 _amount) external { @@ -81,12 +81,16 @@ library StakingLib { validator.status = Status.LIVING; } - emit IStaking.Slashed(_attester, _amount); + emit IStakingCore.Slashed(_attester, _amount); } function deposit(address _attester, address _proposer, address _withdrawer, uint256 _amount) external { + require( + _attester != address(0) && _proposer != address(0), + Errors.Staking__InvalidDeposit(_attester, _proposer) + ); StakingStorage storage store = getStorage(); require( _amount >= store.minimumStake, Errors.Staking__InsufficientStake(_amount, store.minimumStake) @@ -106,7 +110,7 @@ library StakingLib { status: Status.VALIDATING }); - emit IStaking.Deposit(_attester, _proposer, _withdrawer, _amount); + emit IStakingCore.Deposit(_attester, _proposer, _withdrawer, _amount); } function initiateWithdraw(address _attester, address _recipient) external returns (bool) { @@ -131,7 +135,7 @@ library StakingLib { Exit({exitableAt: Timestamp.wrap(block.timestamp) + store.exitDelay, recipient: _recipient}); validator.status = Status.EXITING; - emit IStaking.WithdrawInitiated(_attester, _recipient, validator.stake); + emit IStakingCore.WithdrawInitiated(_attester, _recipient, validator.stake); return true; } diff --git a/l1-contracts/src/periphery/Forwarder.sol b/l1-contracts/src/periphery/Forwarder.sol index a921b2ab500..0f3673336e2 100644 --- a/l1-contracts/src/periphery/Forwarder.sol +++ b/l1-contracts/src/periphery/Forwarder.sol @@ -11,7 +11,11 @@ contract Forwarder is Ownable, IForwarder { constructor(address __owner) Ownable(__owner) {} - function forward(address[] calldata _to, bytes[] calldata _data) external override onlyOwner { + function forward(address[] calldata _to, bytes[] calldata _data) + external + override(IForwarder) + onlyOwner + { require( _to.length == _data.length, IForwarder.ForwarderLengthMismatch(_to.length, _data.length) ); diff --git a/l1-contracts/src/periphery/SlashPayload.sol b/l1-contracts/src/periphery/SlashPayload.sol index b39fcb8301c..e9c429dc5e8 100644 --- a/l1-contracts/src/periphery/SlashPayload.sol +++ b/l1-contracts/src/periphery/SlashPayload.sol @@ -2,7 +2,7 @@ // Copyright 2024 Aztec Labs. pragma solidity >=0.8.27; -import {IStaking} from "@aztec/core/interfaces/IStaking.sol"; +import {IStakingCore} from "@aztec/core/interfaces/IStaking.sol"; import {IValidatorSelection} from "@aztec/core/interfaces/IValidatorSelection.sol"; import {Epoch} from "@aztec/core/libraries/TimeLib.sol"; import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; @@ -28,7 +28,7 @@ contract SlashPayload is IPayload { for (uint256 i = 0; i < attesters.length; i++) { actions[i] = IPayload.Action({ target: address(VALIDATOR_SELECTION), - data: abi.encodeWithSelector(IStaking.slash.selector, attesters[i], AMOUNT) + data: abi.encodeWithSelector(IStakingCore.slash.selector, attesters[i], AMOUNT) }); } diff --git a/l1-contracts/test/Forwarder.t.sol b/l1-contracts/test/Forwarder.t.sol index 8599dcca51f..0470dbb4c8f 100644 --- a/l1-contracts/test/Forwarder.t.sol +++ b/l1-contracts/test/Forwarder.t.sol @@ -66,9 +66,8 @@ contract ForwarderTest is Test { } function testRevertWhenCallToInvalidAddress(address _invalidAddress) public { - vm.assume(_invalidAddress != address(token1)); - vm.assume(_invalidAddress != address(token2)); - vm.assume(_invalidAddress != address(forwarder)); + vm.assume(_invalidAddress.code.length == 0); + vm.assume(uint160(_invalidAddress) > uint160(0x0a)); address[] memory targets = new address[](1); targets[0] = _invalidAddress; diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index ad454786e91..3439bd957cf 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -18,10 +18,9 @@ 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 {Rollup} from "./harnesses/Rollup.sol"; -import {IRollup, BlockLog, SubmitEpochRootProofArgs} from "@aztec/core/interfaces/IRollup.sol"; +import {IRollupCore, BlockLog, SubmitEpochRootProofArgs} from "@aztec/core/interfaces/IRollup.sol"; import {IProofCommitmentEscrow} from "@aztec/core/interfaces/IProofCommitmentEscrow.sol"; import {FeeJuicePortal} from "@aztec/core/FeeJuicePortal.sol"; -import {ValidatorSelection} from "@aztec/core/ValidatorSelection.sol"; import {NaiveMerkle} from "./merkle/Naive.sol"; import {MerkleTestUtil} from "./merkle/TestUtil.sol"; import {TestERC20} from "@aztec/mock/TestERC20.sol"; @@ -54,7 +53,6 @@ contract RollupTest is DecoderBase { Inbox internal inbox; Outbox internal outbox; Rollup internal rollup; - ValidatorSelection internal leo; MerkleTestUtil internal merkleTestUtil; TestERC20 internal testERC20; FeeJuicePortal internal feeJuicePortal; @@ -86,15 +84,6 @@ contract RollupTest is DecoderBase { { testERC20 = new TestERC20("test", "TEST", address(this)); - leo = new ValidatorSelection( - testERC20, - TestConstants.AZTEC_MINIMUM_STAKE, - TestConstants.AZTEC_SLASHING_QUORUM, - TestConstants.AZTEC_SLASHING_ROUND_SIZE, - TestConstants.AZTEC_SLOT_DURATION, - TestConstants.AZTEC_EPOCH_DURATION, - TestConstants.AZTEC_TARGET_COMMITTEE_SIZE - ); DecoderBase.Full memory full = load(_name); uint256 slotNumber = full.block.decodedHeader.globalVariables.slotNumber; uint256 initialTime = @@ -273,7 +262,7 @@ contract RollupTest is DecoderBase { _testBlock("mixed_block_1", false, 1); vm.expectEmit(true, true, true, true); - emit IRollup.ProofRightClaimed( + emit IRollupCore.ProofRightClaimed( quote.epochToProve, quote.prover, address(this), quote.bondAmount, Slot.wrap(1) ); rollup.claimEpochProofRight(signedQuote); @@ -455,7 +444,7 @@ contract RollupTest is DecoderBase { signedQuote = _quoteToSignedQuote(quote); vm.expectEmit(true, true, true, true); - emit IRollup.ProofRightClaimed( + emit IRollupCore.ProofRightClaimed( quote.epochToProve, quote.prover, address(this), quote.bondAmount, Epoch.wrap(3).toSlots() ); rollup.claimEpochProofRight(signedQuote); diff --git a/l1-contracts/test/base/Base.sol b/l1-contracts/test/base/Base.sol index 52d175faeab..5d646d20cea 100644 --- a/l1-contracts/test/base/Base.sol +++ b/l1-contracts/test/base/Base.sol @@ -230,6 +230,6 @@ contract TestBase is Test { // and looking in the logs. Interesting. // Alternative, run forge inspect src/core/Rollup.sol:Rollup storageLayout --pretty // uint256 slot = stdstore.target(address(rollup)).sig("checkBlob()").find(); - vm.store(address(rollup), bytes32(uint256(5)), bytes32(uint256(0))); + vm.store(address(rollup), bytes32(uint256(4)), bytes32(uint256(0))); } } diff --git a/l1-contracts/test/fees/FeeRollup.t.sol b/l1-contracts/test/fees/FeeRollup.t.sol index c79eaa05032..8093c87f261 100644 --- a/l1-contracts/test/fees/FeeRollup.t.sol +++ b/l1-contracts/test/fees/FeeRollup.t.sol @@ -26,7 +26,6 @@ import { import {IRollup} from "@aztec/core/interfaces/IRollup.sol"; import {IProofCommitmentEscrow} from "@aztec/core/interfaces/IProofCommitmentEscrow.sol"; import {FeeJuicePortal} from "@aztec/core/FeeJuicePortal.sol"; -import {ValidatorSelection} from "@aztec/core/ValidatorSelection.sol"; import {NaiveMerkle} from "../merkle/Naive.sol"; import {MerkleTestUtil} from "../merkle/TestUtil.sol"; import {TestERC20} from "@aztec/mock/TestERC20.sol"; diff --git a/l1-contracts/test/governance/governance-proposer/executeProposal.t.sol b/l1-contracts/test/governance/governance-proposer/executeProposal.t.sol index 9ccdcc234ef..817269c4c20 100644 --- a/l1-contracts/test/governance/governance-proposer/executeProposal.t.sol +++ b/l1-contracts/test/governance/governance-proposer/executeProposal.t.sol @@ -4,17 +4,17 @@ pragma solidity >=0.8.27; import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; import {IGovernanceProposer} from "@aztec/governance/interfaces/IGovernanceProposer.sol"; import {GovernanceProposerBase} from "./Base.t.sol"; -import {ValidatorSelection} from "../../harnesses/ValidatorSelection.sol"; import {Errors} from "@aztec/governance/libraries/Errors.sol"; import {Slot, SlotLib, Timestamp} from "@aztec/core/libraries/TimeLib.sol"; import {FaultyGovernance} from "./mocks/FaultyGovernance.sol"; import {FalsyGovernance} from "./mocks/FalsyGovernance.sol"; +import {Fakerollup} from "./mocks/Fakerollup.sol"; contract ExecuteProposalTest is GovernanceProposerBase { using SlotLib for Slot; - ValidatorSelection internal validatorSelection; + Fakerollup internal validatorSelection; IPayload internal proposal = IPayload(address(this)); address internal proposer = address(0); @@ -30,7 +30,7 @@ contract ExecuteProposalTest is GovernanceProposerBase { } modifier givenCanonicalInstanceHoldCode() { - validatorSelection = new ValidatorSelection(); + validatorSelection = new Fakerollup(); vm.prank(registry.getGovernance()); registry.upgrade(address(validatorSelection)); @@ -221,7 +221,7 @@ contract ExecuteProposalTest is GovernanceProposerBase { // it revert // When using a new registry we change the governanceProposer's interpetation of time :O - ValidatorSelection freshInstance = new ValidatorSelection(); + Fakerollup freshInstance = new Fakerollup(); vm.prank(registry.getGovernance()); registry.upgrade(address(freshInstance)); diff --git a/l1-contracts/test/governance/governance-proposer/mocks/Fakerollup.sol b/l1-contracts/test/governance/governance-proposer/mocks/Fakerollup.sol new file mode 100644 index 00000000000..cc4ee196a75 --- /dev/null +++ b/l1-contracts/test/governance/governance-proposer/mocks/Fakerollup.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.27; + +import { + Timestamp, Slot, Epoch, SlotLib, EpochLib, TimeLib +} from "@aztec/core/libraries/TimeLib.sol"; +import {TestConstants} from "../../../harnesses/TestConstants.sol"; + +contract Fakerollup { + using TimeLib for Slot; + using TimeLib for Timestamp; + + constructor() { + TimeLib.initialize( + block.timestamp, TestConstants.AZTEC_SLOT_DURATION, TestConstants.AZTEC_EPOCH_DURATION + ); + } + + function getTimestampForSlot(Slot _slot) external view returns (Timestamp) { + return _slot.toTimestamp(); + } + + function getCurrentSlot() external view returns (Slot) { + return Timestamp.wrap(block.timestamp).slotFromTimestamp(); + } + + function getGenesisTime() external view returns (Timestamp) { + return Timestamp.wrap(TimeLib.getStorage().genesisTime); + } + + function getSlotDuration() external view returns (uint256) { + return TimeLib.getStorage().slotDuration; + } + + function getEpochDuration() external view returns (uint256) { + return TimeLib.getStorage().epochDuration; + } + + function getCurrentProposer() external pure returns (address) { + return address(0); + } +} diff --git a/l1-contracts/test/governance/governance-proposer/vote.t.sol b/l1-contracts/test/governance/governance-proposer/vote.t.sol index 00144b652b5..89ba7e4a6ad 100644 --- a/l1-contracts/test/governance/governance-proposer/vote.t.sol +++ b/l1-contracts/test/governance/governance-proposer/vote.t.sol @@ -4,16 +4,16 @@ pragma solidity >=0.8.27; import {IPayload} from "@aztec/governance/interfaces/IPayload.sol"; import {IGovernanceProposer} from "@aztec/governance/interfaces/IGovernanceProposer.sol"; import {GovernanceProposerBase} from "./Base.t.sol"; -import {ValidatorSelection} from "../../harnesses/ValidatorSelection.sol"; import {Errors} from "@aztec/governance/libraries/Errors.sol"; import {Slot, SlotLib, Timestamp} from "@aztec/core/libraries/TimeLib.sol"; +import {Fakerollup} from "./mocks/Fakerollup.sol"; contract VoteTest is GovernanceProposerBase { using SlotLib for Slot; IPayload internal proposal = IPayload(address(0xdeadbeef)); address internal proposer = address(0); - ValidatorSelection internal validatorSelection; + Fakerollup internal validatorSelection; // Skipping this test since the it matches the for now skipped check in `EmpireBase::vote` function skip__test_WhenProposalHoldNoCode() external { @@ -40,7 +40,7 @@ contract VoteTest is GovernanceProposerBase { } modifier givenCanonicalRollupHoldCode() { - validatorSelection = new ValidatorSelection(); + validatorSelection = new Fakerollup(); vm.prank(registry.getGovernance()); registry.upgrade(address(validatorSelection)); @@ -144,7 +144,7 @@ contract VoteTest is GovernanceProposerBase { uint256 yeaBefore = governanceProposer.yeaCount(address(validatorSelection), validatorSelectionRound, proposal); - ValidatorSelection freshInstance = new ValidatorSelection(); + Fakerollup freshInstance = new Fakerollup(); vm.prank(registry.getGovernance()); registry.upgrade(address(freshInstance)); diff --git a/l1-contracts/test/harnesses/ValidatorSelection.sol b/l1-contracts/test/harnesses/ValidatorSelection.sol deleted file mode 100644 index 1ff93546cfa..00000000000 --- a/l1-contracts/test/harnesses/ValidatorSelection.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2024 Aztec Labs. -pragma solidity >=0.8.27; - -import {ValidatorSelection as RealValidatorSelection} from "@aztec/core/ValidatorSelection.sol"; -import {TestConstants} from "./TestConstants.sol"; -import {TestERC20} from "@aztec/mock/TestERC20.sol"; - -contract ValidatorSelection is RealValidatorSelection { - constructor() - RealValidatorSelection( - new TestERC20("test", "TEST", address(this)), - 100e18, - TestConstants.AZTEC_SLASHING_QUORUM, - TestConstants.AZTEC_SLASHING_ROUND_SIZE, - TestConstants.AZTEC_SLOT_DURATION, - TestConstants.AZTEC_EPOCH_DURATION, - TestConstants.AZTEC_TARGET_COMMITTEE_SIZE - ) - {} -} diff --git a/l1-contracts/test/portals/UniswapPortal.t.sol b/l1-contracts/test/portals/UniswapPortal.t.sol index 7f4060be639..b74fab88761 100644 --- a/l1-contracts/test/portals/UniswapPortal.t.sol +++ b/l1-contracts/test/portals/UniswapPortal.t.sol @@ -22,7 +22,10 @@ import {UniswapPortal} from "./UniswapPortal.sol"; import {MockFeeJuicePortal} from "@aztec/mock/MockFeeJuicePortal.sol"; import {RewardDistributor} from "@aztec/governance/RewardDistributor.sol"; +import {stdStorage, StdStorage} from "forge-std/Test.sol"; + contract UniswapPortalTest is Test { + using stdStorage for StdStorage; using Hash for DataStructures.L2ToL1Msg; IERC20 public constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); @@ -70,6 +73,8 @@ contract UniswapPortalTest is Test { // Modify the proven block count vm.store(address(rollup), bytes32(uint256(13)), bytes32(l2BlockNumber + 1)); + + stdstore.target(address(rollup)).sig("getProvenBlockNumber()").checked_write(l2BlockNumber + 1); assertEq(rollup.getProvenBlockNumber(), l2BlockNumber + 1); // have DAI locked in portal that can be moved when funds are withdrawn diff --git a/l1-contracts/test/staking/deposit.t.sol b/l1-contracts/test/staking/deposit.t.sol index ac2f51be3ec..0c743693492 100644 --- a/l1-contracts/test/staking/deposit.t.sol +++ b/l1-contracts/test/staking/deposit.t.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.27; import {StakingBase} from "./base.t.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; import {IERC20Errors} from "@oz/interfaces/draft-IERC6093.sol"; -import {IStaking, Status, ValidatorInfo} from "@aztec/core/interfaces/IStaking.sol"; +import {IStakingCore, Status, ValidatorInfo} from "@aztec/core/interfaces/IStaking.sol"; contract DepositTest is StakingBase { uint256 internal depositAmount; @@ -147,7 +147,7 @@ contract DepositTest is StakingBase { assertEq(stakingAsset.balanceOf(address(staking)), 0); vm.expectEmit(true, true, true, true, address(staking)); - emit IStaking.Deposit(ATTESTER, PROPOSER, WITHDRAWER, depositAmount); + emit IStakingCore.Deposit(ATTESTER, PROPOSER, WITHDRAWER, depositAmount); staking.deposit({ _attester: ATTESTER, diff --git a/l1-contracts/test/staking/finaliseWithdraw.t.sol b/l1-contracts/test/staking/finaliseWithdraw.t.sol index 1cdde4fcddb..4dc11259cae 100644 --- a/l1-contracts/test/staking/finaliseWithdraw.t.sol +++ b/l1-contracts/test/staking/finaliseWithdraw.t.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.27; import {StakingBase} from "./base.t.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; import { - Timestamp, Status, ValidatorInfo, Exit, IStaking + Timestamp, Status, ValidatorInfo, Exit, IStakingCore } from "@aztec/core/interfaces/IStaking.sol"; contract FinaliseWithdrawTest is StakingBase { @@ -69,7 +69,7 @@ contract FinaliseWithdrawTest is StakingBase { vm.warp(Timestamp.unwrap(exit.exitableAt)); vm.expectEmit(true, true, true, true, address(staking)); - emit IStaking.WithdrawFinalised(ATTESTER, RECIPIENT, MINIMUM_STAKE); + emit IStakingCore.WithdrawFinalised(ATTESTER, RECIPIENT, MINIMUM_STAKE); staking.finaliseWithdraw(ATTESTER); exit = staking.getExit(ATTESTER); diff --git a/l1-contracts/test/staking/initiateWithdraw.t.sol b/l1-contracts/test/staking/initiateWithdraw.t.sol index fe69c47c7d1..cb93ad8ed3f 100644 --- a/l1-contracts/test/staking/initiateWithdraw.t.sol +++ b/l1-contracts/test/staking/initiateWithdraw.t.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.27; import {StakingBase} from "./base.t.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; import { - Timestamp, Status, ValidatorInfo, Exit, IStaking + Timestamp, Status, ValidatorInfo, Exit, IStakingCore } from "@aztec/core/interfaces/IStaking.sol"; contract InitiateWithdrawTest is StakingBase { @@ -105,7 +105,7 @@ contract InitiateWithdrawTest is StakingBase { assertEq(staking.getActiveAttesterCount(), 1); vm.expectEmit(true, true, true, true, address(staking)); - emit IStaking.WithdrawInitiated(ATTESTER, RECIPIENT, MINIMUM_STAKE); + emit IStakingCore.WithdrawInitiated(ATTESTER, RECIPIENT, MINIMUM_STAKE); vm.prank(WITHDRAWER); staking.initiateWithdraw(ATTESTER, RECIPIENT); @@ -138,7 +138,7 @@ contract InitiateWithdrawTest is StakingBase { assertEq(staking.getActiveAttesterCount(), 0); vm.expectEmit(true, true, true, true, address(staking)); - emit IStaking.WithdrawInitiated(ATTESTER, RECIPIENT, MINIMUM_STAKE); + emit IStakingCore.WithdrawInitiated(ATTESTER, RECIPIENT, MINIMUM_STAKE); vm.prank(WITHDRAWER); staking.initiateWithdraw(ATTESTER, RECIPIENT); diff --git a/l1-contracts/test/staking/slash.t.sol b/l1-contracts/test/staking/slash.t.sol index d0cfe2e4364..1d3279ac7a8 100644 --- a/l1-contracts/test/staking/slash.t.sol +++ b/l1-contracts/test/staking/slash.t.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.27; import {StakingBase} from "./base.t.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; import { - IStaking, Status, ValidatorInfo, Exit, Timestamp + IStakingCore, Status, ValidatorInfo, Exit, Timestamp } from "@aztec/core/interfaces/IStaking.sol"; contract SlashTest is StakingBase { @@ -83,7 +83,7 @@ contract SlashTest is StakingBase { assertTrue(info.status == Status.EXITING); vm.expectEmit(true, true, true, true, address(staking)); - emit IStaking.Slashed(ATTESTER, 1); + emit IStakingCore.Slashed(ATTESTER, 1); vm.prank(SLASHER); staking.slash(ATTESTER, 1); @@ -113,7 +113,7 @@ contract SlashTest is StakingBase { uint256 balance = info.stake; vm.expectEmit(true, true, true, true, address(staking)); - emit IStaking.Slashed(ATTESTER, 1); + emit IStakingCore.Slashed(ATTESTER, 1); vm.prank(SLASHER); staking.slash(ATTESTER, 1); @@ -163,7 +163,7 @@ contract SlashTest is StakingBase { uint256 balance = info.stake; vm.expectEmit(true, true, true, true, address(staking)); - emit IStaking.Slashed(ATTESTER, slashingAmount); + emit IStakingCore.Slashed(ATTESTER, slashingAmount); vm.prank(SLASHER); staking.slash(ATTESTER, slashingAmount); diff --git a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol index adcf5ceef4e..46fd1bea36c 100644 --- a/l1-contracts/test/validator-selection/ValidatorSelection.t.sol +++ b/l1-contracts/test/validator-selection/ValidatorSelection.t.sol @@ -13,7 +13,6 @@ 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, Config} from "@aztec/core/Rollup.sol"; -import {ValidatorSelection} from "@aztec/core/ValidatorSelection.sol"; import {NaiveMerkle} from "../merkle/Naive.sol"; import {MerkleTestUtil} from "../merkle/TestUtil.sol"; import {TestERC20} from "@aztec/mock/TestERC20.sol"; @@ -69,20 +68,10 @@ contract ValidatorSelectionTest is DecoderBase { modifier setup(uint256 _validatorCount) { string memory _name = "mixed_block_1"; { - ValidatorSelection validatorSelection = new ValidatorSelection( - testERC20, - TestConstants.AZTEC_MINIMUM_STAKE, - TestConstants.AZTEC_SLASHING_QUORUM, - TestConstants.AZTEC_SLASHING_ROUND_SIZE, - TestConstants.AZTEC_SLOT_DURATION, - TestConstants.AZTEC_EPOCH_DURATION, - TestConstants.AZTEC_TARGET_COMMITTEE_SIZE - ); - DecoderBase.Full memory full = load(_name); uint256 slotNumber = full.block.decodedHeader.globalVariables.slotNumber; uint256 initialTime = full.block.decodedHeader.globalVariables.timestamp - - slotNumber * validatorSelection.getSlotDuration(); + - slotNumber * TestConstants.AZTEC_SLOT_DURATION; vm.warp(initialTime); } @@ -187,14 +176,14 @@ contract ValidatorSelectionTest is DecoderBase { } function testValidatorSetLargerThanCommittee(bool _insufficientSigs) public setup(100) { - assertGt(rollup.getAttesters().length, rollup.TARGET_COMMITTEE_SIZE(), "Not enough validators"); - uint256 committeeSize = rollup.TARGET_COMMITTEE_SIZE() * 2 / 3 + (_insufficientSigs ? 0 : 1); + assertGt(rollup.getAttesters().length, rollup.getTargetCommitteeSize(), "Not enough validators"); + uint256 committeeSize = rollup.getTargetCommitteeSize() * 2 / 3 + (_insufficientSigs ? 0 : 1); _testBlock("mixed_block_1", _insufficientSigs, committeeSize, false); assertEq( rollup.getEpochCommittee(rollup.getCurrentEpoch()).length, - rollup.TARGET_COMMITTEE_SIZE(), + rollup.getTargetCommitteeSize(), "Invalid committee size" ); } diff --git a/yarn-project/epoch-cache/src/epoch_cache.ts b/yarn-project/epoch-cache/src/epoch_cache.ts index 1cbe315c2bb..eaa2fca5d45 100644 --- a/yarn-project/epoch-cache/src/epoch_cache.ts +++ b/yarn-project/epoch-cache/src/epoch_cache.ts @@ -142,7 +142,7 @@ export class EpochCache extends EventEmitter<{ committeeChanged: [EthAddress[], } /** - * Get the ABI encoding of the proposer index - see Leonidas.sol _computeProposerIndex + * Get the ABI encoding of the proposer index - see ValidatorSelectionLib.sol computeProposerIndex */ getProposerIndexEncoding(epoch: bigint, slot: bigint, seed: bigint): `0x${string}` { return encodeAbiParameters( diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts index 45013f3b30a..6fc021d316d 100644 --- a/yarn-project/ethereum/src/contracts/rollup.ts +++ b/yarn-project/ethereum/src/contracts/rollup.ts @@ -113,7 +113,7 @@ export class RollupContract { @memoize getTargetCommitteeSize() { - return this.rollup.read.TARGET_COMMITTEE_SIZE(); + return this.rollup.read.getTargetCommitteeSize(); } @memoize