diff --git a/l1-contracts/script/deploy/DeployRollupForUpgrade.s.sol b/l1-contracts/script/deploy/DeployRollupForUpgrade.s.sol index 97f1ea3ac2c..e09e8bf3ece 100644 --- a/l1-contracts/script/deploy/DeployRollupForUpgrade.s.sol +++ b/l1-contracts/script/deploy/DeployRollupForUpgrade.s.sol @@ -73,9 +73,14 @@ contract DeployRollupForUpgrade is Script { console.log("JSON DEPLOY RESULT:", finalJson); } + function _requireRegistryHasCode(Registry _registry) internal view { + require(address(_registry).code.length > 0, "DeployRollupForUpgrade: REGISTRY_ADDRESS has no code on this chain"); + } + /// @notice Parse existing L1 infrastructure from environment variables function _getRollupAddressInput() internal returns (RollupAddressInput memory) { Registry registry = Registry(vm.envAddress("REGISTRY_ADDRESS")); + _requireRegistryHasCode(registry); // Load existing addresses from the registry and canonical rollup. Governance governance = Governance(registry.getGovernance()); diff --git a/l1-contracts/script/deploy/DeployRollupLib.sol b/l1-contracts/script/deploy/DeployRollupLib.sol index d6347408a21..e10e5aa100f 100644 --- a/l1-contracts/script/deploy/DeployRollupLib.sol +++ b/l1-contracts/script/deploy/DeployRollupLib.sol @@ -85,12 +85,47 @@ library DeployRollupLib { GenesisState memory genesisState = config.getGenesisState(); RollupConfigInput memory rollupConfigInput = config.getRollupConfiguration(IRewardDistributor(address(input.rewardDistributor))); + validateRollupConfig(input, rollupConfigInput); return new Rollup( input.feeAsset, input.stakingAsset, input.gse, verifier, input.deployer, genesisState, rollupConfigInput ); } + /// @notice Reject configuration the Rollup constructor accepts but that leaves the deployed instance unusable. + /// @dev Every value here is fixed at construction, so a bad one can only be corrected by redeploying. A zero + /// `targetCommitteeSize` is not rejected: `ValidatorSelectionLib` treats it as "no committee", which local + /// networks and end-to-end tests rely on. An epoch longer than `MAX_CHECKPOINTS_PER_EPOCH` is not rejected + /// either: end-to-end tests use long epochs to keep a run inside epoch 0. + function validateRollupConfig(RollupAddressInput memory input, RollupConfigInput memory config) internal view { + require(config.ethereumSlotDuration > 0, "DeployRollupLib: ethereumSlotDuration is zero"); + require(config.aztecSlotDuration > 0, "DeployRollupLib: aztecSlotDuration is zero"); + require(config.aztecEpochDuration > 0, "DeployRollupLib: aztecEpochDuration is zero"); + // TimeLib multiplies the two uint32 durations without widening. + uint256 epochDurationSeconds = config.aztecSlotDuration * config.aztecEpochDuration; + require(epochDurationSeconds <= type(uint32).max, "DeployRollupLib: epoch duration in seconds overflows uint32"); + require(config.exitDelaySeconds > 0, "DeployRollupLib: exitDelaySeconds is zero"); + // A zero RANDAO lag samples the same epoch it seeds, and the sample-time subtraction is done in uint32. + require(config.lagInEpochsForRandao >= 1, "DeployRollupLib: lagInEpochsForRandao is zero"); + require( + config.lagInEpochsForValidatorSet >= config.lagInEpochsForRandao, + "DeployRollupLib: lagInEpochsForValidatorSet below lagInEpochsForRandao" + ); + require( + config.lagInEpochsForValidatorSet * epochDurationSeconds <= type(uint32).max, + "DeployRollupLib: lag in seconds overflows uint32" + ); + // SlashingProposer encodes each amount as uint96 when it builds a payload. + for (uint256 i = 0; i < config.slashAmounts.length; i++) { + require(config.slashAmounts[i] <= type(uint96).max, "DeployRollupLib: slash amount exceeds uint96"); + } + require(address(config.rewardConfig.rewardDistributor) != address(0), "DeployRollupLib: rewardDistributor is zero"); + // The GSE only accounts stake in its own asset. + require( + address(input.stakingAsset) == address(input.gse.ASSET()), "DeployRollupLib: stakingAsset is not GSE.ASSET()" + ); + } + function _maybeMintInitialFeeAsset(RollupAddressInput memory input, Rollup rollup, IRollupConfiguration config) private { diff --git a/l1-contracts/script/deploy/RollupConfiguration.sol b/l1-contracts/script/deploy/RollupConfiguration.sol index f6730887098..7dc3753cfca 100644 --- a/l1-contracts/script/deploy/RollupConfiguration.sol +++ b/l1-contracts/script/deploy/RollupConfiguration.sol @@ -38,22 +38,43 @@ contract RollupConfiguration is IRollupConfiguration, Test { function loadConfig() external { networkName = vm.envOr("NETWORK", string("local")); validatorsJson = vm.envOr("INITIAL_VALIDATORS", string("[]")); + + _requireNetworkMatchesChain(networkName, block.chainid); + } + + /// @dev NETWORK selects the contract defaults and the chain id selects RPC and keys; a mismatch means one of + /// them points at the wrong network. + function _requireNetworkMatchesChain(string memory _networkName, uint256 _chainId) internal pure { + bool isMainnetName = keccak256(bytes(_networkName)) == keccak256("mainnet"); + require(!isMainnetName || _chainId == 1, "RollupConfiguration: NETWORK=mainnet requires chain id 1"); + require(_chainId != 1 || isMainnetName, "RollupConfiguration: chain id 1 requires NETWORK=mainnet"); } + /// @dev A stub verifier accepts any proof, so it must be requested explicitly with REAL_VERIFIER=false. function useRealVerifier() external view returns (bool) { - return vm.envOr("REAL_VERIFIER", false); + return vm.envOr("REAL_VERIFIER", true); } function getFeeJuicePortalInitialBalance() external view returns (uint256) { return vm.envOr("FEE_JUICE_PORTAL_INITIAL_BALANCE", uint256(0)); } + /// @dev The genesis roots are immutable once the rollup is constructed and a zero root cannot match any node's + /// view of the chain, so each one must be supplied explicitly. function getGenesisState() external view returns (GenesisState memory) { - return GenesisState({ + GenesisState memory genesisState = GenesisState({ vkTreeRoot: bytes32(vm.envOr("VK_TREE_ROOT", uint256(0))), protocolContractsHash: bytes32(vm.envOr("PROTOCOL_CONTRACTS_HASH", uint256(0))), genesisArchiveRoot: bytes32(vm.envOr("GENESIS_ARCHIVE_ROOT", uint256(0))) }); + _requireGenesisState(genesisState); + return genesisState; + } + + function _requireGenesisState(GenesisState memory _genesisState) internal pure { + require(_genesisState.vkTreeRoot != bytes32(0), "RollupConfiguration: VK_TREE_ROOT is unset"); + require(_genesisState.protocolContractsHash != bytes32(0), "RollupConfiguration: PROTOCOL_CONTRACTS_HASH is unset"); + require(_genesisState.genesisArchiveRoot != bytes32(0), "RollupConfiguration: GENESIS_ARCHIVE_ROOT is unset"); } function getRewardConfiguration(IRewardDistributor _rewardDistributor) external pure returns (RewardConfig memory) { diff --git a/l1-contracts/scripts/run_rollup_upgrade.sh b/l1-contracts/scripts/run_rollup_upgrade.sh index 3cabda3ecf0..1d3e09fd17c 100755 --- a/l1-contracts/scripts/run_rollup_upgrade.sh +++ b/l1-contracts/scripts/run_rollup_upgrade.sh @@ -20,7 +20,6 @@ echo "=== Deploying rollup upgrade ===" echo "Registry: $registry_address" REGISTRY_ADDRESS="$registry_address" \ -REAL_VERIFIER="${REAL_VERIFIER:-true}" \ ./scripts/forge_broadcast.js \ script/deploy/DeployRollupForUpgrade.s.sol:DeployRollupForUpgrade \ --rpc-url "$L1_RPC_URL" \ diff --git a/l1-contracts/scripts/stress_test_deploy.sh b/l1-contracts/scripts/stress_test_deploy.sh index 7207ba420b7..5ce1f5b94ff 100755 --- a/l1-contracts/scripts/stress_test_deploy.sh +++ b/l1-contracts/scripts/stress_test_deploy.sh @@ -21,6 +21,12 @@ echo "=== Results dir: $RESULTS_DIR ===" source ./scripts/load_network_defaults.sh +# The deployer requires explicit genesis roots. Only 31 random bytes (top byte zero) so each value is always +# below the BN254 scalar field modulus. +export VK_TREE_ROOT="0x00$(openssl rand -hex 31)" +export PROTOCOL_CONTRACTS_HASH="0x00$(openssl rand -hex 31)" +export GENESIS_ARCHIVE_ROOT="0x00$(openssl rand -hex 31)" + PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" # Pre-compile so workers skip compilation. diff --git a/l1-contracts/scripts/test_rollup_upgrade.sh b/l1-contracts/scripts/test_rollup_upgrade.sh index 209ae0bb0e2..775cc04fb3e 100755 --- a/l1-contracts/scripts/test_rollup_upgrade.sh +++ b/l1-contracts/scripts/test_rollup_upgrade.sh @@ -20,6 +20,12 @@ trap cleanup EXIT # Clean stale broadcast artifacts from previous runs to avoid nonce conflicts. rm -rf broadcast/ +# The deployer requires explicit genesis roots. Only 31 random bytes (top byte zero) so each value is always +# below the BN254 scalar field modulus. +export VK_TREE_ROOT="0x00$(openssl rand -hex 31)" +export PROTOCOL_CONTRACTS_HASH="0x00$(openssl rand -hex 31)" +export GENESIS_ARCHIVE_ROOT="0x00$(openssl rand -hex 31)" + # Fixed port — this test runs with ISOLATE=1 so no conflicts. ANVIL_PORT="${ANVIL_PORT:-8545}" @@ -51,8 +57,7 @@ if [[ -z "$registry_address" || "$registry_address" == "null" ]]; then fi echo "=== Testing run_rollup_upgrade.sh ===" -# Use a different genesis to get a different rollup version. Only 31 random bytes (top byte zero) so the value is -# always below the BN254 scalar field modulus; the rollup rejects a genesis archive root >= the field modulus. +# Use a different genesis to get a different rollup version. export GENESIS_ARCHIVE_ROOT="0x00$(openssl rand -hex 31)" ./scripts/run_rollup_upgrade.sh "$registry_address" diff --git a/l1-contracts/test/script/DeployAztecL1Contracts.t.sol b/l1-contracts/test/script/DeployAztecL1Contracts.t.sol index 599b0178524..bbf10a3c154 100644 --- a/l1-contracts/test/script/DeployAztecL1Contracts.t.sol +++ b/l1-contracts/test/script/DeployAztecL1Contracts.t.sol @@ -36,6 +36,11 @@ contract DeployAztecL1ContractsTest is Test { string memory path = string.concat(root, "/scripts/network-defaults.json"); string memory json = vm.readFile(path); + // Genesis roots are required by the deployer; any non-zero field element will do here. + vm.setEnv("VK_TREE_ROOT", vm.toString(uint256(keccak256("vk_tree_root")) >> 8)); + vm.setEnv("PROTOCOL_CONTRACTS_HASH", vm.toString(uint256(keccak256("protocol_contracts_hash")) >> 8)); + vm.setEnv("GENESIS_ARCHIVE_ROOT", vm.toString(uint256(keccak256("genesis_archive_root")) >> 8)); + // Timing config vm.setEnv("ETHEREUM_SLOT_DURATION", vm.toString(json.readUint(".ETHEREUM_SLOT_DURATION"))); vm.setEnv("AZTEC_SLOT_DURATION", vm.toString(json.readUint(".AZTEC_SLOT_DURATION"))); diff --git a/l1-contracts/test/script/DeployConfigValidation.t.sol b/l1-contracts/test/script/DeployConfigValidation.t.sol new file mode 100644 index 00000000000..7c827ce088a --- /dev/null +++ b/l1-contracts/test/script/DeployConfigValidation.t.sol @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.27; + +import {Test} from "forge-std/Test.sol"; + +import {DeployRollupForUpgrade} from "../../script/deploy/DeployRollupForUpgrade.s.sol"; +import {DeployRollupLib, RollupAddressInput} from "../../script/deploy/DeployRollupLib.sol"; +import {RollupConfiguration} from "../../script/deploy/RollupConfiguration.sol"; +import {GenesisState, RollupConfigInput} from "@aztec/core/interfaces/IRollup.sol"; +import {RewardConfig, Bps} from "@aztec/core/libraries/rollup/RewardLib.sol"; +import {IBoosterCore} from "@aztec/core/reward-boost/RewardBooster.sol"; +import {IRewardDistributor} from "@aztec/governance/interfaces/IRewardDistributor.sol"; +import {Registry} from "@aztec/governance/Registry.sol"; +import {GSE} from "@aztec/governance/GSE.sol"; +import {Governance} from "@aztec/governance/Governance.sol"; +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; + +contract RollupConfigurationHarness is RollupConfiguration { + function requireGenesisState(GenesisState memory _genesisState) external pure { + _requireGenesisState(_genesisState); + } + + function requireNetworkMatchesChain(string memory _networkName, uint256 _chainId) external pure { + _requireNetworkMatchesChain(_networkName, _chainId); + } +} + +contract DeployRollupForUpgradeHarness is DeployRollupForUpgrade { + function requireRegistryHasCode(Registry _registry) external view { + _requireRegistryHasCode(_registry); + } +} + +contract DeployRollupLibHarness { + function validateRollupConfig(RollupAddressInput memory _input, RollupConfigInput memory _config) external view { + DeployRollupLib.validateRollupConfig(_input, _config); + } +} + +/// @dev Only the asset binding is consulted by the deployer's checks. +contract GseAssetStub { + IERC20 public immutable ASSET; + + constructor(IERC20 _asset) { + ASSET = _asset; + } +} + +/** + * @title DeployConfigValidationTest + * @notice The env-driven deployer refuses configuration that the Rollup constructor would accept but that leaves + * the deployed instance unusable or mis-bound. The checks are exercised on values built in the test; the + * suite never writes environment variables, because forge runs tests in parallel and the process + * environment is shared with the other script tests. + */ +contract DeployConfigValidationTest is Test { + RollupConfigurationHarness internal configuration; + DeployRollupLibHarness internal lib; + RollupAddressInput internal input; + IRewardDistributor internal rewardDistributor = IRewardDistributor(makeAddr("rewardDistributor")); + IERC20 internal stakingAsset = IERC20(makeAddr("stakingAsset")); + + function setUp() public { + configuration = new RollupConfigurationHarness(); + lib = new DeployRollupLibHarness(); + input = RollupAddressInput({ + deployer: address(this), + registry: Registry(makeAddr("registry")), + gse: GSE(address(new GseAssetStub(stakingAsset))), + governance: Governance(makeAddr("governance")), + feeAsset: IERC20(makeAddr("feeAsset")), + stakingAsset: stakingAsset, + rewardDistributor: rewardDistributor + }); + } + + /// @dev The mainnet timing and slashing values; the fields the deployer does not check are left zeroed. + function _baseConfig() internal view returns (RollupConfigInput memory config) { + config.ethereumSlotDuration = 12; + config.aztecSlotDuration = 72; + config.aztecEpochDuration = 32; + config.targetCommitteeSize = 48; + config.lagInEpochsForValidatorSet = 2; + config.lagInEpochsForRandao = 1; + config.exitDelaySeconds = 345_600; + config.slashAmounts = [uint256(2000e18), 5000e18, 5000e18]; + config.rewardConfig = RewardConfig({ + rewardDistributor: rewardDistributor, + sequencerBps: Bps.wrap(7000), + booster: IBoosterCore(address(0)), + checkpointReward: 500e18 + }); + } + + function _validGenesis() internal pure returns (GenesisState memory) { + return GenesisState({ + vkTreeRoot: bytes32(uint256(keccak256("vk_tree_root")) >> 8), + protocolContractsHash: bytes32(uint256(keccak256("protocol_contracts_hash")) >> 8), + genesisArchiveRoot: bytes32(uint256(keccak256("genesis_archive_root")) >> 8) + }); + } + + function _expectInvalid(RollupConfigInput memory _config, string memory _reason) internal { + vm.expectRevert(bytes(_reason)); + lib.validateRollupConfig(input, _config); + } + + function test_BaselineIsValid() public view { + lib.validateRollupConfig(input, _baseConfig()); + configuration.requireGenesisState(_validGenesis()); + } + + function test_RealVerifierUnlessDisabled() public view { + assertTrue(configuration.useRealVerifier()); + } + + function test_RevertWhenGenesisRootUnset() public { + GenesisState memory genesisState = _validGenesis(); + genesisState.vkTreeRoot = bytes32(0); + vm.expectRevert(bytes("RollupConfiguration: VK_TREE_ROOT is unset")); + configuration.requireGenesisState(genesisState); + } + + function test_RevertWhenProtocolContractsHashUnset() public { + GenesisState memory genesisState = _validGenesis(); + genesisState.protocolContractsHash = bytes32(0); + vm.expectRevert(bytes("RollupConfiguration: PROTOCOL_CONTRACTS_HASH is unset")); + configuration.requireGenesisState(genesisState); + } + + function test_RevertWhenGenesisArchiveRootUnset() public { + GenesisState memory genesisState = _validGenesis(); + genesisState.genesisArchiveRoot = bytes32(0); + vm.expectRevert(bytes("RollupConfiguration: GENESIS_ARCHIVE_ROOT is unset")); + configuration.requireGenesisState(genesisState); + } + + function test_NetworkAndChainIdMustAgree() public { + configuration.requireNetworkMatchesChain("mainnet", 1); + configuration.requireNetworkMatchesChain("testnet", 11_155_111); + configuration.requireNetworkMatchesChain("local", 31_337); + + vm.expectRevert(bytes("RollupConfiguration: chain id 1 requires NETWORK=mainnet")); + configuration.requireNetworkMatchesChain("testnet", 1); + + vm.expectRevert(bytes("RollupConfiguration: NETWORK=mainnet requires chain id 1")); + configuration.requireNetworkMatchesChain("mainnet", 11_155_111); + } + + function test_RevertWhenRegistryHasNoCode() public { + DeployRollupForUpgradeHarness upgrade = new DeployRollupForUpgradeHarness(); + vm.expectRevert(bytes("DeployRollupForUpgrade: REGISTRY_ADDRESS has no code on this chain")); + upgrade.requireRegistryHasCode(Registry(makeAddr("not-a-registry"))); + } + + function test_ZeroCommitteeSizeIsAllowed() public view { + RollupConfigInput memory config = _baseConfig(); + config.targetCommitteeSize = 0; + lib.validateRollupConfig(input, config); + } + + function test_LongEpochIsAllowed() public view { + RollupConfigInput memory config = _baseConfig(); + config.aztecEpochDuration = 1000; + lib.validateRollupConfig(input, config); + } + + function test_RevertWhenExitDelayZero() public { + RollupConfigInput memory config = _baseConfig(); + config.exitDelaySeconds = 0; + _expectInvalid(config, "DeployRollupLib: exitDelaySeconds is zero"); + } + + function test_RevertWhenDurationsZero() public { + RollupConfigInput memory config = _baseConfig(); + config.ethereumSlotDuration = 0; + _expectInvalid(config, "DeployRollupLib: ethereumSlotDuration is zero"); + + config = _baseConfig(); + config.aztecSlotDuration = 0; + _expectInvalid(config, "DeployRollupLib: aztecSlotDuration is zero"); + + config = _baseConfig(); + config.aztecEpochDuration = 0; + _expectInvalid(config, "DeployRollupLib: aztecEpochDuration is zero"); + } + + function test_RevertWhenEpochSecondsOverflowUint32() public { + RollupConfigInput memory config = _baseConfig(); + // 32 checkpoints of 2^27 seconds is exactly 2^32 seconds. + config.aztecSlotDuration = uint256(1) << 27; + _expectInvalid(config, "DeployRollupLib: epoch duration in seconds overflows uint32"); + } + + function test_RevertWhenRandaoLagZero() public { + RollupConfigInput memory config = _baseConfig(); + config.lagInEpochsForRandao = 0; + _expectInvalid(config, "DeployRollupLib: lagInEpochsForRandao is zero"); + } + + function test_RevertWhenValidatorSetLagBelowRandaoLag() public { + RollupConfigInput memory config = _baseConfig(); + config.lagInEpochsForRandao = 2; + config.lagInEpochsForValidatorSet = 1; + _expectInvalid(config, "DeployRollupLib: lagInEpochsForValidatorSet below lagInEpochsForRandao"); + } + + function test_RevertWhenLagSecondsOverflowUint32() public { + RollupConfigInput memory config = _baseConfig(); + // 2304 seconds per epoch (72 x 32); 1_864_136 epochs of lag is just over 2^32 seconds. + config.lagInEpochsForValidatorSet = 1_864_136; + _expectInvalid(config, "DeployRollupLib: lag in seconds overflows uint32"); + } + + function test_RevertWhenSlashAmountExceedsUint96() public { + RollupConfigInput memory config = _baseConfig(); + config.slashAmounts[2] = uint256(type(uint96).max) + 1; + _expectInvalid(config, "DeployRollupLib: slash amount exceeds uint96"); + } + + function test_RevertWhenRewardDistributorZero() public { + RollupConfigInput memory config = _baseConfig(); + config.rewardConfig.rewardDistributor = IRewardDistributor(address(0)); + _expectInvalid(config, "DeployRollupLib: rewardDistributor is zero"); + } + + function test_RevertWhenStakingAssetIsNotGseAsset() public { + RollupAddressInput memory other = input; + other.stakingAsset = IERC20(makeAddr("other-asset")); + RollupConfigInput memory config = _baseConfig(); + vm.expectRevert(bytes("DeployRollupLib: stakingAsset is not GSE.ASSET()")); + lib.validateRollupConfig(other, config); + } +} diff --git a/l1-contracts/test/script/DeployRollupForUpgrade.t.sol b/l1-contracts/test/script/DeployRollupForUpgrade.t.sol index 0a0b93da073..5d17d25e829 100644 --- a/l1-contracts/test/script/DeployRollupForUpgrade.t.sol +++ b/l1-contracts/test/script/DeployRollupForUpgrade.t.sol @@ -38,6 +38,11 @@ contract DeployRollupForUpgradeTest is Test { string memory path = string.concat(root, "/scripts/network-defaults.json"); string memory json = vm.readFile(path); + // Genesis roots are required by the deployer; any non-zero field element will do here. + vm.setEnv("VK_TREE_ROOT", vm.toString(uint256(keccak256("vk_tree_root")) >> 8)); + vm.setEnv("PROTOCOL_CONTRACTS_HASH", vm.toString(uint256(keccak256("protocol_contracts_hash")) >> 8)); + vm.setEnv("GENESIS_ARCHIVE_ROOT", vm.toString(uint256(keccak256("genesis_archive_root")) >> 8)); + // Timing config vm.setEnv("ETHEREUM_SLOT_DURATION", vm.toString(json.readUint(".ETHEREUM_SLOT_DURATION"))); vm.setEnv("AZTEC_SLOT_DURATION", vm.toString(json.readUint(".AZTEC_SLOT_DURATION")));