diff --git a/l1-contracts/script/deploy/RollupConfiguration.sol b/l1-contracts/script/deploy/RollupConfiguration.sol index 9d6f4cc38190..a2ffb3a02823 100644 --- a/l1-contracts/script/deploy/RollupConfiguration.sol +++ b/l1-contracts/script/deploy/RollupConfiguration.sol @@ -127,9 +127,6 @@ contract RollupConfiguration is IRollupConfiguration, Test { config.rewardBoostConfig = this.getRewardBoostConfiguration(); config.stakingQueueConfig = this.getStakingQueueConfiguration(); - // Compute version as first 4 bytes of hash(abi.encode(config, genesisState)) - config.version = _computeConfigVersion(config, this.getGenesisState()); - return config; } @@ -156,7 +153,6 @@ contract RollupConfiguration is IRollupConfiguration, Test { config.slashingDisableDuration = vm.envUint("AZTEC_SLASHING_DISABLE_DURATION"); config.manaTarget = vm.envUint("AZTEC_MANA_TARGET"); config.exitDelaySeconds = vm.envUint("AZTEC_EXIT_DELAY_SECONDS"); - config.version = 0; // Computed below config.provingCostPerMana = EthValue.wrap(vm.envUint("AZTEC_PROVING_COST_PER_MANA")); config.initialEthPerFeeAsset = EthPerFeeAssetE12.wrap(vm.envUint("AZTEC_INITIAL_ETH_PER_FEE_ASSET")); config.registryRewardOverrides[0] = _getRegistryRewardOverride("AZTEC_REGISTRY_REWARD_OVERRIDE_0"); @@ -179,21 +175,6 @@ contract RollupConfiguration is IRollupConfiguration, Test { registryRewardOverride.sequencerReward = vm.parseUint(fields[1]).toUint96(); } - /// @notice Compute rollup config version by hashing config + genesis state - /// @dev Version is the first 4 bytes (uint32) of keccak256(abi.encode(rollupConfig, genesisState)) - /// This DOES NOT match the TS implementation: keccak256(jsonStringify({rollupConfigArgs, genesisStateArgs})) - function _computeConfigVersion(RollupConfigInput memory _config, GenesisState memory _genesisState) - private - pure - returns (uint32) - { - bytes32 hash = keccak256(abi.encode(_config, _genesisState)); - // Extract first 4 bytes as uint32 (big-endian) - // Casting to bytes4 is intentional because the config version is defined as the first 4 bytes of the hash. - // forge-lint: disable-next-line(unsafe-typecast) - return uint32(bytes4(hash)); - } - function _getSlashingOffset() private view returns (uint256) { return vm.envUint("AZTEC_SLASHING_OFFSET_IN_ROUNDS"); } diff --git a/l1-contracts/src/core/RollupCore.sol b/l1-contracts/src/core/RollupCore.sol index c49b562f39be..6fd823d7cdaf 100644 --- a/l1-contracts/src/core/RollupCore.sol +++ b/l1-contracts/src/core/RollupCore.sol @@ -242,6 +242,13 @@ contract RollupCore is EIP712("Aztec Rollup", "1"), Ownable, IStakingCore, IVali GenesisState memory _genesisState, RollupConfigInput memory _config ) Ownable(_governance) { + // Hashed before anything below can modify `_config` in memory. Including this contract's address means the + // deployer cannot choose the version and two deployments do not share one. The 32-bit truncation is still + // reachable by grinding CREATE2 salts, so it separates deployments rather than identifying them. + // Casting to bytes4 is intentional because the version is defined as the first 4 bytes of the hash. + // forge-lint: disable-next-line(unsafe-typecast) + VERSION = uint32(bytes4(keccak256(abi.encode(block.chainid, address(this), _config, _genesisState)))); + StakingLib.assertValidQueueConfig(_config.stakingQueueConfig); // queueSetSlasher schedules the replacement slasher to land at `block.timestamp + @@ -294,13 +301,12 @@ contract RollupCore is EIP712("Aztec Rollup", "1"), Ownable, IStakingCore, IVali // factored out into a helper the way the slasher and reward setup are. VK_TREE_ROOT = _genesisState.vkTreeRoot; PROTOCOL_CONTRACTS_HASH = _genesisState.protocolContractsHash; - VERSION = _config.version; FEE_ASSET = _feeAsset; EPOCH_PROOF_VERIFIER = _epochProofVerifier; - IInbox inbox = IInbox(address(new Inbox(address(this), _feeAsset, _config.version, INBOX_BUCKET_RING_SIZE))); + IInbox inbox = IInbox(address(new Inbox(address(this), _feeAsset, VERSION, INBOX_BUCKET_RING_SIZE))); INBOX = inbox; - OUTBOX = IOutbox(address(new Outbox(address(this), _config.version))); + OUTBOX = IOutbox(address(new Outbox(address(this), VERSION))); FEE_ASSET_PORTAL = IFeeJuicePortal(inbox.getFeeAssetPortal()); STFLib.initialize(_genesisState); diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index 1c5b7114d8d3..ca821656df2e 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -113,7 +113,6 @@ struct RollupConfigInput { uint256 slashingDisableDuration; uint256 manaTarget; uint256 exitDelaySeconds; - uint32 version; EthValue provingCostPerMana; EthPerFeeAssetE12 initialEthPerFeeAsset; RewardConfig rewardConfig; diff --git a/l1-contracts/src/governance/Registry.sol b/l1-contracts/src/governance/Registry.sol index cc14ac4e8965..90b6bc1577c7 100644 --- a/l1-contracts/src/governance/Registry.sol +++ b/l1-contracts/src/governance/Registry.sol @@ -11,8 +11,8 @@ import {RewardDistributor, IRewardDistributor} from "./RewardDistributor.sol"; struct RegistryStorage { /** * @notice Mapping from version to rollup instance - * @dev As implemented today, the version is a truncated hash of identifiers of the rollup instance - * See RollupCore.sol for the implementation. + * @dev The version is a truncated hash of the chain id, the rollup's address, its configuration and its + * genesis state, computed by the rollup itself at construction. See RollupCore.sol for the implementation. * @dev updated when a new rollup instance is added, which becomes the new canonical rollup */ mapping(uint256 version => IHaveVersion rollup) versionToRollup; diff --git a/l1-contracts/test/RollupVersion.t.sol b/l1-contracts/test/RollupVersion.t.sol new file mode 100644 index 000000000000..3adf4dd953bb --- /dev/null +++ b/l1-contracts/test/RollupVersion.t.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +// solhint-disable func-name-mixedcase +pragma solidity >=0.8.27; + +import {Test} from "forge-std/Test.sol"; +import {RollupBuilder} from "./builder/RollupBuilder.sol"; +import {Rollup} from "@aztec/core/Rollup.sol"; +import {Inbox} from "@aztec/core/messagebridge/Inbox.sol"; +import {Outbox} from "@aztec/core/messagebridge/Outbox.sol"; +import {Registry} from "@aztec/governance/Registry.sol"; +import {IHaveVersion} from "@aztec/governance/interfaces/IRegistry.sol"; + +contract RollupVersionTest is Test { + function test_versionIsDerivedFromChainIdAddressConfigAndGenesis() external { + RollupBuilder builder = new RollupBuilder(address(this)).deploy(); + Rollup rollup = Rollup(address(builder.getConfig().rollup)); + + uint256 expected = uint32( + bytes4( + keccak256( + abi.encode( + block.chainid, address(rollup), builder.getConfig().rollupConfigInput, builder.getConfig().genesisState + ) + ) + ) + ); + assertEq(rollup.getVersion(), expected, "version"); + assertEq(Inbox(address(rollup.getInbox())).VERSION(), expected, "inbox version"); + assertEq(Outbox(address(rollup.getOutbox())).VERSION(), expected, "outbox version"); + } + + function test_identicallyConfiguredRollupsHaveDistinctVersions() external { + RollupBuilder first = new RollupBuilder(address(this)).deploy(); + Registry registry = first.getConfig().registry; + Rollup firstRollup = Rollup(address(first.getConfig().rollup)); + + // Same registry, GSE, assets, genesis state and rollup config as the first rollup. + RollupBuilder second = new RollupBuilder(address(this)).setGSE(first.getConfig().gse) + .setTestERC20(first.getConfig().testERC20).setRegistry(registry).setMakeCanonical(false).setMakeGovernance(false) + .setUpdateOwnerships(false).deploy(); + Rollup secondRollup = Rollup(address(second.getConfig().rollup)); + + assertNotEq(firstRollup.getVersion(), secondRollup.getVersion(), "versions collide"); + + // Both can be registered side by side, since the Registry is keyed by version. + vm.prank(registry.owner()); + registry.addRollup(IHaveVersion(address(secondRollup))); + assertEq(address(registry.getRollup(firstRollup.getVersion())), address(firstRollup), "first rollup"); + assertEq(address(registry.getRollup(secondRollup.getVersion())), address(secondRollup), "second rollup"); + } +} diff --git a/l1-contracts/test/builder/RollupBuilder.sol b/l1-contracts/test/builder/RollupBuilder.sol index 7ed6cfc15e17..92890810ee43 100644 --- a/l1-contracts/test/builder/RollupBuilder.sol +++ b/l1-contracts/test/builder/RollupBuilder.sol @@ -124,7 +124,6 @@ contract RollupBuilder is Test { target.slashingDisableDuration = _source.slashingDisableDuration; target.manaTarget = _source.manaTarget; target.exitDelaySeconds = _source.exitDelaySeconds; - target.version = _source.version; target.provingCostPerMana = _source.provingCostPerMana; target.initialEthPerFeeAsset = _source.initialEthPerFeeAsset; target.rewardConfig = _source.rewardConfig; diff --git a/l1-contracts/test/harnesses/TestConstants.sol b/l1-contracts/test/harnesses/TestConstants.sol index 881dfa54743f..059598566752 100644 --- a/l1-contracts/test/harnesses/TestConstants.sol +++ b/l1-contracts/test/harnesses/TestConstants.sol @@ -125,7 +125,6 @@ library TestConstants { config.exitDelaySeconds = AZTEC_EXIT_DELAY_SECONDS; config.provingCostPerMana = AZTEC_PROVING_COST_PER_MANA; config.initialEthPerFeeAsset = AZTEC_INITIAL_ETH_PER_FEE_ASSET; - config.version = 0; config.rewardConfig = rewardConfig; config.rewardBoostConfig = rewardBoostConfig; config.stakingQueueConfig = stakingQueueConfig; @@ -134,12 +133,6 @@ library TestConstants { config.localEjectionThreshold = 0; config.ethereumSlotDuration = ETHEREUM_SLOT_DURATION; - // For the version we derive it based on the config (with a 0 version) - // TODO(https://linear.app/aztec-labs/issue/TMNT-139/version-at-deployment) - uint32 version = - uint32(uint256(keccak256(abi.encode(bytes("aztec_rollup"), block.chainid, getGenesisState(), config)))); - config.version = version; - return config; } } diff --git a/l1-contracts/test/staking/initiateWithdrawByAttester.t.sol b/l1-contracts/test/staking/initiateWithdrawByAttester.t.sol index ec40bafad4c5..8b6ad55cc2a6 100644 --- a/l1-contracts/test/staking/initiateWithdrawByAttester.t.sol +++ b/l1-contracts/test/staking/initiateWithdrawByAttester.t.sol @@ -97,7 +97,6 @@ contract InitiateWithdrawByAttesterTest is StakingBase { RollupBuilder builder = new RollupBuilder(address(this)).setGSE(gse).setTestERC20(stakingAsset) .setRegistry(registry).setMakeCanonical(false).setMakeGovernance(false).setUpdateOwnerships(false); RollupConfigInput memory config = builder.getConfig().rollupConfigInput; - config.version = uint32(IHaveVersion(address(staking)).getVersion() + 1); builder.setRollupConfigInput(config).deploy(); nextRollup = IStaking(address(builder.getConfig().rollup));