diff --git a/contracts-abi/abi/AlwaysFalseRegistry.abi b/contracts-abi/abi/AlwaysFalseRegistry.abi new file mode 100644 index 000000000..c89c5cb76 --- /dev/null +++ b/contracts-abi/abi/AlwaysFalseRegistry.abi @@ -0,0 +1,21 @@ +[ + { + "type": "function", + "name": "isValidatorOptedIn", + "inputs": [ + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "pure" + } +] diff --git a/contracts-abi/abi/ValidatorOptInHub.abi b/contracts-abi/abi/ValidatorOptInHub.abi new file mode 100644 index 000000000..12d73c31c --- /dev/null +++ b/contracts-abi/abi/ValidatorOptInHub.abi @@ -0,0 +1,516 @@ +[ + { + "type": "constructor", + "inputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "fallback", + "stateMutability": "payable" + }, + { + "type": "receive", + "stateMutability": "payable" + }, + { + "type": "function", + "name": "UPGRADE_INTERFACE_VERSION", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "acceptOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addRegistry", + "inputs": [ + { + "name": "registry", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "areValidatorsOptedIn", + "inputs": [ + { + "name": "valBLSPubKeys", + "type": "bytes[]", + "internalType": "bytes[]" + } + ], + "outputs": [ + { + "name": "", + "type": "bool[]", + "internalType": "bool[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "areValidatorsOptedInList", + "inputs": [ + { + "name": "valBLSPubKeys", + "type": "bytes[]", + "internalType": "bytes[]" + } + ], + "outputs": [ + { + "name": "", + "type": "bool[][]", + "internalType": "bool[][]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "_registries", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "_owner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "isValidatorOptedIn", + "inputs": [ + { + "name": "valPubKey", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isValidatorOptedInList", + "inputs": [ + { + "name": "valPubKey", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "bool[]", + "internalType": "bool[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pendingOwner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "proxiableUUID", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "registries", + "inputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IRegistry" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "removeRegistry", + "inputs": [ + { + "name": "index", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "registry", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "updateRegistry", + "inputs": [ + { + "name": "index", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "oldRegistry", + "type": "address", + "internalType": "address" + }, + { + "name": "newRegistry", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "upgradeToAndCall", + "inputs": [ + { + "name": "newImplementation", + "type": "address", + "internalType": "address" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferStarted", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RegistryAdded", + "inputs": [ + { + "name": "index", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "registry", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RegistryRemoved", + "inputs": [ + { + "name": "index", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "registry", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RegistryReplaced", + "inputs": [ + { + "name": "index", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "oldRegistry", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newRegistry", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Upgraded", + "inputs": [ + { + "name": "implementation", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC1967InvalidImplementation", + "inputs": [ + { + "name": "implementation", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC1967NonPayable", + "inputs": [] + }, + { + "type": "error", + "name": "FailedInnerCall", + "inputs": [] + }, + { + "type": "error", + "name": "IndexRegistryMismatch", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidFallback", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidIndex", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidInitialization", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidReceive", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidRegistry", + "inputs": [] + }, + { + "type": "error", + "name": "NotInitializing", + "inputs": [] + }, + { + "type": "error", + "name": "OwnableInvalidOwner", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "OwnableUnauthorizedAccount", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "UUPSUnauthorizedCallContext", + "inputs": [] + }, + { + "type": "error", + "name": "UUPSUnsupportedProxiableUUID", + "inputs": [ + { + "name": "slot", + "type": "bytes32", + "internalType": "bytes32" + } + ] + }, + { + "type": "error", + "name": "ZeroAddress", + "inputs": [] + } +] diff --git a/contracts-abi/script.sh b/contracts-abi/script.sh index 0806383fb..0a627e00f 100755 --- a/contracts-abi/script.sh +++ b/contracts-abi/script.sh @@ -52,6 +52,10 @@ extract_and_save_abi "$BASE_DIR/out/RewardDistributor.sol/RewardDistributor.json extract_and_save_abi "$BASE_DIR/out/BlockRewardManager.sol/BlockRewardManager.json" "$ABI_DIR/BlockRewardManager.abi" +extract_and_save_abi "$BASE_DIR/out/ValidatorOptInHub.sol/ValidatorOptInHub.json" "$ABI_DIR/ValidatorOptInHub.abi" + +extract_and_save_abi "$BASE_DIR/out/AlwaysFalseRegistry.sol/AlwaysFalseRegistry.json" "$ABI_DIR/AlwaysFalseRegistry.abi" + echo "ABI files extracted successfully." GO_CODE_BASE_DIR="./clients" @@ -123,6 +127,10 @@ generate_go_code "$ABI_DIR/RewardDistributor.abi" "RewardDistributor" "rewarddis generate_go_code "$ABI_DIR/BlockRewardManager.abi" "BlockRewardManager" "blockrewardmanager" +generate_go_code "$ABI_DIR/ValidatorOptInHub.abi" "ValidatorOptInHub" "validatoroptinhub" + +generate_go_code "$ABI_DIR/AlwaysFalseRegistry.abi" "AlwaysFalseRegistry" "alwaysfalseregistry" + echo "External ABI downloaded and processed successfully." echo "Go code generated successfully in separate folders." diff --git a/contracts/README.md b/contracts/README.md index 49cc07a94..7d97d2ff2 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -56,11 +56,13 @@ This changelog tracks deployments of **Hoodi Testnet** contracts. This changelog | Contract | Proxy Address | Initial Commit | |-----------------------|----------------------------------------------|---------------------| +| ValidatorOptInHub | `0x953c2a669493A126fd50E9f56306f254B4e49709` | `35664894728008c34cf6e24cbe77ce91f091144b` in 'optinrouter-new-registry-support' | | ValidatorOptInRouter | `0xa380ba6d6083a4Cb2a3B62b0a81Ea8727861c13e` | `13cf068477e6efdbb5c4fe5ce53a11af30bf8b47` in 'main' | | VanillaRegistry | `0x536f0792c5d5ed592e67a9260606c85f59c312f0` | `13cf068477e6efdbb5c4fe5ce53a11af30bf8b47` in 'main' | | MevCommitAVS | `0xdF8649d298ad05f019eE4AdBD6210867B8AB225F` | `13cf068477e6efdbb5c4fe5ce53a11af30bf8b47` in 'main' | | MevCommitMiddleware | `0x8E847EC4a36c8332652aB3b2B7D5c54dE29c7fde` | `13cf068477e6efdbb5c4fe5ce53a11af30bf8b47` in 'main' | + ### Upgrade History | Timestamp (UTC) | Contract | New Impl Version | Commmit | diff --git a/contracts/contracts/interfaces/IRegistry.sol b/contracts/contracts/interfaces/IRegistry.sol new file mode 100644 index 000000000..a8937428d --- /dev/null +++ b/contracts/contracts/interfaces/IRegistry.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BSL 1.1 +pragma solidity 0.8.26; + +interface IRegistry { + /// @notice Returns an array of OptInStatus structs indicating whether each validator pubkey is opted in to mev-commit. + function isValidatorOptedIn(bytes calldata valPubKey) external view returns (bool); +} \ No newline at end of file diff --git a/contracts/contracts/interfaces/IValidatorOptInHub.sol b/contracts/contracts/interfaces/IValidatorOptInHub.sol new file mode 100644 index 000000000..63ef1d163 --- /dev/null +++ b/contracts/contracts/interfaces/IValidatorOptInHub.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: BSL 1.1 +pragma solidity 0.8.26; + +interface IValidatorOptInHub { + + /// @notice Emitted when a registry is added. + event RegistryAdded(uint256 indexed index, address indexed registry); + + /// @notice Emitted when a registry is replaced. + event RegistryReplaced(uint256 indexed index, address indexed oldRegistry, address indexed newRegistry); + + /// @notice Emitted when a registry is removed. + event RegistryRemoved(uint256 indexed index, address indexed registry); + + + error InvalidRegistry(); + error InvalidIndex(); + error ZeroAddress(); + error IndexRegistryMismatch(); + + /// @notice Initializes the contract with the validator registry and mev-commit AVS contracts. + function initialize( + address[] calldata _registries, + address _owner + ) external; + + + /// @notice Adds a registry to the contract. + function addRegistry(address registry) external; + + /// @notice Replaces a registry with a new one. + function updateRegistry(uint256 index, address oldRegistry, address newRegistry) external; + + /// @notice Removes a registry from the contract. + function removeRegistry(uint256 index, address registry) external; + + /// @notice Returns an array of bool lists indicating whether each validator pubkey is opted in to mev-commit. + function areValidatorsOptedInList(bytes[] calldata valBLSPubKeys) external view returns (bool[][] memory optInStatuses); + + /// @notice Returns a bool list indicating whether a validator pubkey is opted in to mev-commit with any of the registries. + function areValidatorsOptedIn(bytes[] calldata valBLSPubKeys) external view returns (bool[] memory optInStatuses); + + /// @notice Returns a bool list indicating whether a validator pubkey is opted in to mev-commit. + function isValidatorOptedInList(bytes calldata valPubKey) external view returns (bool[] memory optInStatus); + + /// @notice Returns a bool indicating whether a validator pubkey is opted in to mev-commit with any of the registries. + function isValidatorOptedIn(bytes calldata valPubKey) external view returns (bool); +} \ No newline at end of file diff --git a/contracts/contracts/validator-registry/README.md b/contracts/contracts/validator-registry/README.md index c94132430..316784233 100644 --- a/contracts/contracts/validator-registry/README.md +++ b/contracts/contracts/validator-registry/README.md @@ -6,7 +6,7 @@ Validators are able to _opt-in to mev-commit_ in one of three ways: 2. Restaking with the `MevCommitMiddleware` contract. 3. Simple staking with the `VanillaRegistry` contract. -The `ValidatorOptInRouter` contract acts as a query router between all three solutions, allowing any actor to query whether a group of validator pubkeys is opted-in to mev-commit. +The `ValidatorOptInHub` contract acts as a query router between all three solutions, allowing any actor to query whether a group of validator pubkeys is opted-in to mev-commit. This is an updated version of the ValidatorOptInRouter contract for ease of use and to add future registry support. ## Mev-commit AVS - Eigenlayer Restaking Solution diff --git a/contracts/contracts/validator-registry/ValidatorOptInHub.sol b/contracts/contracts/validator-registry/ValidatorOptInHub.sol new file mode 100644 index 000000000..99f7cea5f --- /dev/null +++ b/contracts/contracts/validator-registry/ValidatorOptInHub.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: BSL 1.1 +pragma solidity 0.8.26; + +import {ValidatorOptInHubStorage} from "./ValidatorOptInHubStorage.sol"; +import {IValidatorOptInHub} from "../interfaces/IValidatorOptInHub.sol"; +import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {Errors} from "../utils/Errors.sol"; +import {IRegistry} from "../interfaces/IRegistry.sol"; + +/// @title ValidatorOptInHub +/// @notice This contract acts as the top level source of truth for whether a validator +/// is opted in to mev-commit from any of the registries. +contract ValidatorOptInHub is IValidatorOptInHub, ValidatorOptInHubStorage, + Ownable2StepUpgradeable, UUPSUpgradeable { + + /// @dev See https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#initializing_the_implementation_contract + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @dev Receive function is disabled for this contract to prevent unintended interactions. + receive() external payable { + revert Errors.InvalidReceive(); + } + + /// @dev Fallback function to revert all calls, ensuring no unintended interactions. + fallback() external payable { + revert Errors.InvalidFallback(); + } + + /// @notice Initializes the contract with the validator registry and mev-commit AVS contracts. + function initialize( + address[] calldata _registries, + address _owner + ) external initializer { + __Ownable_init(_owner); + __UUPSUpgradeable_init(); + uint256 len = _registries.length; + for (uint256 i = 0; i < len; ++i) { + _validateRegistry(_registries[i]); + registries.push(IRegistry(_registries[i])); + } + } + + // --- admin --- + function addRegistry(address registry) external onlyOwner { + _validateRegistry(registry); + registries.push(IRegistry(registry)); + emit RegistryAdded(registries.length - 1, registry); + } + + /// Pass in index and old registry address for safer replacement. + function updateRegistry(uint256 index, address oldRegistry, address newRegistry) external onlyOwner { + require(index < registries.length, InvalidIndex()); + require(oldRegistry != address(0), ZeroAddress()); + _validateRegistry(newRegistry); + if (address(registries[index]) == oldRegistry) { + registries[index] = IRegistry(newRegistry); + emit RegistryReplaced(index, oldRegistry, newRegistry); + return; + } + revert IndexRegistryMismatch(); + } + + /// Pass in index and registry address for safer removal. + function removeRegistry(uint256 index, address registry) external onlyOwner { + require(registry != address(0), ZeroAddress()); + require(index < registries.length, InvalidIndex()); + if (address(registries[index]) == registry) { + registries[index] = IRegistry(address(0)); + emit RegistryRemoved(index, registry); + return; + } + revert IndexRegistryMismatch(); + } + + /// @notice Returns an array of bool lists indicating whether each validator pubkey is opted in to mev-commit. + function areValidatorsOptedInList(bytes[] calldata valBLSPubKeys) external view returns (bool[][] memory) { + uint256 len = valBLSPubKeys.length; + bool[][] memory _optInStatuses = new bool[][](len); + for (uint256 i = 0; i < len; ++i) { + _optInStatuses[i] = _isValidatorOptedInList(valBLSPubKeys[i]); + } + return _optInStatuses; + } + + function areValidatorsOptedIn(bytes[] calldata valBLSPubKeys) external view returns (bool[] memory) { + uint256 len = valBLSPubKeys.length; + bool[] memory _optInStatuses = new bool[](len); + for (uint256 i = 0; i < len; ++i) { + _optInStatuses[i] = _isValidatorOptedIn(valBLSPubKeys[i]); + } + return _optInStatuses; + } + + function isValidatorOptedInList(bytes calldata valPubKey) external view returns (bool[] memory) { + return _isValidatorOptedInList(valPubKey); + } + + function isValidatorOptedIn(bytes calldata valPubKey) external view returns (bool) { + return _isValidatorOptedIn(valPubKey); + } + + // solhint-disable-next-line no-empty-blocks + function _authorizeUpgrade(address) internal override onlyOwner {} + + //Sanity check for owner operations + function _validateRegistry(address registry) internal view { + require(registry != address(0), ZeroAddress()); + if (registry == address(0)) revert IValidatorOptInHub.ZeroAddress(); + + // 1) Must be a contract + uint256 size; + assembly { size := extcodesize(registry) } + if (size == 0) revert IValidatorOptInHub.InvalidRegistry(); + + // 2) Must implement isValidatorOptedIn(bytes) -> bool and be STATICCALL-safe + (bool ok, bytes memory returnData) = registry.staticcall( + abi.encodeWithSelector(IRegistry.isValidatorOptedIn.selector, bytes("")) + ); + // ok must be true and return data must be exactly 32 bytes (bool) + if (!ok || returnData.length != 32) revert IValidatorOptInHub.InvalidRegistry(); + } + + /// @notice Internal function to check if a validator is opted in to mev-commit with any of the registries. + /// @return bool list indicating whether the validator is opted in to each registry. + function _isValidatorOptedInList(bytes calldata valPubKey) internal view returns (bool[] memory) { + bool[] memory _optInStatuses = new bool[](registries.length); + uint256 len = registries.length; + for (uint256 i = 0; i < len; ++i) { + if (address(registries[i]) == address(0)) { + _optInStatuses[i] = false; + } else { + _optInStatuses[i] = registries[i].isValidatorOptedIn(valPubKey); + } + } + return _optInStatuses; + } + + function _isValidatorOptedIn(bytes calldata valPubKey) internal view returns (bool) { + uint256 len = registries.length; + for (uint256 i = 0; i < len; ++i) { + if (address(registries[i]) != address(0)) { + if (registries[i].isValidatorOptedIn(valPubKey)) { + return true; + } + } + } + return false; + } +} diff --git a/contracts/contracts/validator-registry/ValidatorOptInHubStorage.sol b/contracts/contracts/validator-registry/ValidatorOptInHubStorage.sol new file mode 100644 index 000000000..7b3542eb7 --- /dev/null +++ b/contracts/contracts/validator-registry/ValidatorOptInHubStorage.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: BSL 1.1 +pragma solidity 0.8.26; + +import {IRegistry} from "../interfaces/IRegistry.sol"; + +/// @title ValidatorOptInHubStorage +/// @notice Storage components of the ValidatorOptInHub contract. +contract ValidatorOptInHubStorage { + + IRegistry[] public registries; + + /// @dev See https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#storage-gaps + uint256[48] private __gap; +} \ No newline at end of file diff --git a/contracts/contracts/validator-registry/falseRegistry/AlwaysFalseRegistry.sol b/contracts/contracts/validator-registry/falseRegistry/AlwaysFalseRegistry.sol new file mode 100644 index 000000000..8aaf8091b --- /dev/null +++ b/contracts/contracts/validator-registry/falseRegistry/AlwaysFalseRegistry.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: BSL 1.1 + +pragma solidity 0.8.26; + +contract AlwaysFalseRegistry { + function isValidatorOptedIn(bytes calldata) external pure returns (bool) { + return false; + } +} \ No newline at end of file diff --git a/contracts/l1-deployer-cli.sh b/contracts/l1-deployer-cli.sh index cb67c27a3..53dbfc48d 100755 --- a/contracts/l1-deployer-cli.sh +++ b/contracts/l1-deployer-cli.sh @@ -4,7 +4,7 @@ deploy_all_flag=false deploy_vanilla_flag=false deploy_avs_flag=false deploy_middleware_flag=false -deploy_router_flag=false +deploy_opt_in_hub_flag=false deploy_block_rewards_flag=false deploy_reward_distributor_flag=false skip_release_verification_flag=false @@ -21,11 +21,11 @@ help() { echo " $0 --chain [optional options]" echo echo "Commands (one required):" - echo " deploy-all Deploy all components (vanilla, AVS, middleware, router)." + echo " deploy-all Deploy all components (vanilla, AVS, middleware, opt-in-hub)." echo " deploy-vanilla Deploy and verify the VanillaRegistry contract to L1." echo " deploy-avs Deploy and verify the MevCommitAVS contract to L1." echo " deploy-middleware Deploy and verify the MevCommitMiddleware contract to L1." - echo " deploy-router Deploy and verify the ValidatorOptInRouter contract to L1." + echo " deploy-opt-in-hub Deploy and verify the ValidatorOptInHub contract to L1." echo " deploy-block-rewards Deploy and verify the BlockRewardManager contract to L1." echo " deploy-reward-distributor Deploy and verify the RewardDistributor contract to L1." echo @@ -122,8 +122,8 @@ parse_args() { deploy_middleware_flag=true shift ;; - deploy-router) - deploy_router_flag=true + deploy-opt-in-hub) + deploy_opt_in_hub_flag=true shift ;; deploy-block-rewards) @@ -215,7 +215,7 @@ parse_args() { fi commands_specified=0 - for flag in deploy_all_flag deploy_vanilla_flag deploy_avs_flag deploy_middleware_flag deploy_router_flag deploy_block_rewards_flag deploy_reward_distributor_flag; do + for flag in deploy_all_flag deploy_vanilla_flag deploy_avs_flag deploy_middleware_flag deploy_opt_in_hub_flag deploy_block_rewards_flag deploy_reward_distributor_flag; do if [[ "${!flag}" == true ]]; then ((commands_specified++)) fi @@ -394,8 +394,8 @@ deploy_middleware() { deploy_contract_generic "scripts/validator-registry/middleware/DeployMiddleware.s.sol" } -deploy_router() { - deploy_contract_generic "scripts/validator-registry/DeployValidatorOptInRouter.s.sol" +deploy_opt_in_hub() { + deploy_contract_generic "scripts/validator-registry/DeployValidatorOptInHub.s.sol" } deploy_block_rewards() { @@ -420,15 +420,15 @@ main() { deploy_vanilla deploy_avs deploy_middleware - deploy_router + deploy_opt_in_hub elif [[ "${deploy_vanilla_flag}" == true ]]; then deploy_vanilla elif [[ "${deploy_avs_flag}" == true ]]; then deploy_avs elif [[ "${deploy_middleware_flag}" == true ]]; then deploy_middleware - elif [[ "${deploy_router_flag}" == true ]]; then - deploy_router + elif [[ "${deploy_opt_in_hub_flag}" == true ]]; then + deploy_opt_in_hub elif [[ "${deploy_block_rewards_flag}" == true ]]; then deploy_block_rewards elif [[ "${deploy_reward_distributor_flag}" == true ]]; then diff --git a/contracts/scripts/validator-registry/DeployValidatorOptInHub.s.sol b/contracts/scripts/validator-registry/DeployValidatorOptInHub.s.sol new file mode 100644 index 000000000..e52afb6ff --- /dev/null +++ b/contracts/scripts/validator-registry/DeployValidatorOptInHub.s.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: BSL 1.1 + +// solhint-disable no-console +// solhint-disable one-contract-per-file + +pragma solidity 0.8.26; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; +import {ValidatorOptInHub} from "../../contracts/validator-registry/ValidatorOptInHub.sol"; +import {ValidatorOptInRouter} from "../../contracts/validator-registry/ValidatorOptInRouter.sol"; +import {AlwaysFalseRegistry} from "../../contracts/validator-registry/falseRegistry/AlwaysFalseRegistry.sol"; +import {IMevCommitAVS} from "../../contracts/interfaces/IMevCommitAVS.sol"; +import {IMevCommitMiddleware} from "../../contracts/interfaces/IMevCommitMiddleware.sol"; +import {IVanillaRegistry} from "../../contracts/interfaces/IVanillaRegistry.sol"; +import {MainnetConstants} from "../MainnetConstants.sol"; + +contract BaseDeploy is Script { + function deployValidatorOptInHub( + address[] memory registries, + address owner, + address optinRouter + ) public returns (address) { + console.log("Deploying ValidatorOptInHub on chain:", block.chainid); + address proxy = Upgrades.deployUUPSProxy( + "ValidatorOptInHub.sol", + abi.encodeCall( + ValidatorOptInHub.initialize, + (registries, owner) + ) + ); + console.log("ValidatorOptInHub UUPS proxy deployed to:", address(proxy)); + ValidatorOptInHub hub = ValidatorOptInHub(payable(proxy)); + console.log("ValidatorOptInHub owner:", hub.owner()); + + AlwaysFalseRegistry alwaysFalse = new AlwaysFalseRegistry(); + console.log("AlwaysFalseRegistry deployed at:", address(alwaysFalse)); + + address alwaysFalseAddress = address(alwaysFalse); + address hubAddress = address(hub); + + // Make router backwards compatible by getting data from the hub + ValidatorOptInRouter router = ValidatorOptInRouter(payable(optinRouter)); + router.setVanillaRegistry(IVanillaRegistry(hubAddress)); + router.setMevCommitAVS(IMevCommitAVS(alwaysFalseAddress)); + router.setMevCommitMiddleware(IMevCommitMiddleware(alwaysFalseAddress)); + console.log("ValidatorOptInRouter wired to hub"); + + return proxy; + } +} + +contract DeployMainnet is BaseDeploy { + address constant public MEV_COMMIT_MIDDLEWARE = 0x21fD239311B050bbeE7F32850d99ADc224761382; + address constant public MEV_COMMIT_AVS = 0xBc77233855e3274E1903771675Eb71E602D9DC2e; + address constant public VANILLA_REGISTRY = 0x47afdcB2B089C16CEe354811EA1Bbe0DB7c335E9; + + address constant public OWNER = MainnetConstants.PRIMEV_TEAM_MULTISIG; + + address constant public OPTIN_ROUTER = 0x821798d7b9d57dF7Ed7616ef9111A616aB19ed64; + + address[] public registries = [VANILLA_REGISTRY, MEV_COMMIT_AVS, MEV_COMMIT_MIDDLEWARE]; + + function run() external { + require(block.chainid == 1, "must deploy on mainnet"); + vm.startBroadcast(); + + deployValidatorOptInHub( + registries, + OWNER, + OPTIN_ROUTER + ); + vm.stopBroadcast(); + } +} + +contract DeployHoodi is BaseDeploy { + address constant public MEV_COMMIT_MIDDLEWARE = 0x8E847EC4a36c8332652aB3b2B7D5c54dE29c7fde; + address constant public MEV_COMMIT_AVS = 0xdF8649d298ad05f019eE4AdBD6210867B8AB225F; + address constant public VANILLA_REGISTRY = 0x536F0792c5D5Ed592e67a9260606c85F59C312F0; + + //This is the most important field. On mainnet it'll be the primev multisig. + address constant public OWNER = 0x1623fE21185c92BB43bD83741E226288B516134a; + address constant public OPTIN_ROUTER = 0xa380ba6d6083a4Cb2a3B62b0a81Ea8727861c13e; + + address[] public registries = [VANILLA_REGISTRY, MEV_COMMIT_AVS, MEV_COMMIT_MIDDLEWARE]; + + + function run() external { + require(block.chainid == 560048, "must deploy on Hoodi"); + + vm.startBroadcast(); + deployValidatorOptInHub( + registries, + OWNER, + OPTIN_ROUTER + ); + vm.stopBroadcast(); + } +} diff --git a/contracts/scripts/validator-registry/DeployValidatorOptInRouter.s.sol b/contracts/scripts/validator-registry/DeployValidatorOptInRouter.s.sol deleted file mode 100644 index 0a23b9a10..000000000 --- a/contracts/scripts/validator-registry/DeployValidatorOptInRouter.s.sol +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-License-Identifier: BSL 1.1 - -// solhint-disable no-console -// solhint-disable one-contract-per-file - -pragma solidity 0.8.26; - -import {Script} from "forge-std/Script.sol"; -import {console} from "forge-std/console.sol"; -import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; -import {ValidatorOptInRouter} from "../../contracts/validator-registry/ValidatorOptInRouter.sol"; -import {MainnetConstants} from "../MainnetConstants.sol"; - -contract BaseDeploy is Script { - function deployValidatorOptInRouter( - address vanillaRegistry, - address mevCommitAVS, - address mevCommitMiddleware, - address owner - ) public returns (address) { - console.log("Deploying ValidatorOptInRouter on chain:", block.chainid); - address proxy = Upgrades.deployUUPSProxy( - "ValidatorOptInRouter.sol", - abi.encodeCall( - ValidatorOptInRouter.initialize, - (vanillaRegistry, mevCommitAVS, mevCommitMiddleware, owner) - ) - ); - console.log("ValidatorOptInRouter UUPS proxy deployed to:", address(proxy)); - ValidatorOptInRouter router = ValidatorOptInRouter(payable(proxy)); - console.log("ValidatorOptInRouter owner:", router.owner()); - return proxy; - } -} - -contract DeployMainnet is BaseDeploy { - address constant public VANILLA_REGISTRY = 0x47afdcB2B089C16CEe354811EA1Bbe0DB7c335E9; - address constant public MEV_COMMIT_AVS = 0xBc77233855e3274E1903771675Eb71E602D9DC2e; - address constant public MEV_COMMIT_MIDDLEWARE = 0x21fD239311B050bbeE7F32850d99ADc224761382; - address constant public OWNER = MainnetConstants.PRIMEV_TEAM_MULTISIG; - - function run() external { - require(block.chainid == 1, "must deploy on mainnet"); - vm.startBroadcast(); - - deployValidatorOptInRouter( - VANILLA_REGISTRY, - MEV_COMMIT_AVS, - MEV_COMMIT_MIDDLEWARE, - OWNER - ); - vm.stopBroadcast(); - } -} - -contract DeployHolesky is BaseDeploy { - address constant public VANILLA_REGISTRY = 0x87D5F694fAD0b6C8aaBCa96277DE09451E277Bcf; - address constant public MEV_COMMIT_AVS = 0xEDEDB8ed37A43Fd399108A44646B85b780D85DD4; - address constant public MEV_COMMIT_MIDDLEWARE = 0x0D5A6dd3Ba8C6385ecA623B56199b7FFC490792a; - - // This is the most important field. On mainnet it'll be the primev multisig. - address constant public OWNER = 0x4535bd6fF24860b5fd2889857651a85fb3d3C6b1; - - function run() external { - require(block.chainid == 17000, "must deploy on Holesky"); - - vm.startBroadcast(); - deployValidatorOptInRouter( - VANILLA_REGISTRY, - MEV_COMMIT_AVS, - MEV_COMMIT_MIDDLEWARE, - OWNER - ); - vm.stopBroadcast(); - } -} - -contract DeployHoodi is BaseDeploy { - address constant public VANILLA_REGISTRY = 0x536F0792c5D5Ed592e67a9260606c85F59C312F0; - address constant public MEV_COMMIT_AVS = 0xdF8649d298ad05f019eE4AdBD6210867B8AB225F; - address constant public MEV_COMMIT_MIDDLEWARE = 0x8E847EC4a36c8332652aB3b2B7D5c54dE29c7fde; - - //This is the most important field. On mainnet it'll be the primev multisig. - address constant public OWNER = 0x1623fE21185c92BB43bD83741E226288B516134a; - - function run() external { - require(block.chainid == 560048, "must deploy on Hoodi"); - - vm.startBroadcast(); - deployValidatorOptInRouter( - VANILLA_REGISTRY, - MEV_COMMIT_AVS, - MEV_COMMIT_MIDDLEWARE, - OWNER - ); - vm.stopBroadcast(); - } -} diff --git a/contracts/test/validator-registry/ValidatorOptInHubTest.sol b/contracts/test/validator-registry/ValidatorOptInHubTest.sol new file mode 100644 index 000000000..b68f7a31a --- /dev/null +++ b/contracts/test/validator-registry/ValidatorOptInHubTest.sol @@ -0,0 +1,368 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import "forge-std/Test.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +// Adjust these paths to your repo layout: +import {ValidatorOptInHub} from "../../contracts/validator-registry/ValidatorOptInHub.sol"; +import {IValidatorOptInHub} from "../../contracts/interfaces/IValidatorOptInHub.sol"; +import {IRegistry} from "../../contracts/interfaces/IRegistry.sol"; +import {Errors} from "../../contracts/utils/Errors.sol"; + +contract ValidatorOptInHubTest is Test { + ValidatorOptInHub hubImplementation; + ValidatorOptInHub hub; + + MockRegistry registryA; + MockRegistry registryB; + MockRegistry registryC; + MockRegistry registryD; // used when testing addRegistry on top of 3 initial ones + + address badRegistry = address(0xBAD); + address contractOwner = address(this); + + // Sample validator public keys + bytes validatorPublicKeyOne = hex"01"; + bytes validatorPublicKeyTwo = hex"02"; + bytes validatorPublicKeyThree = hex"03"; + + function setUp() public { + // --- Deploy registry mocks --- + registryA = new MockRegistry(); + registryB = new MockRegistry(); + registryC = new MockRegistry(); + registryD = new MockRegistry(); + + // Seed sample statuses: + // - registryA: pk1=true, pk2=false + // - registryB: pk1=false, pk2=true + // - registryC/D: defaults to false + registryA.setOptIn(validatorPublicKeyOne, true); + registryA.setOptIn(validatorPublicKeyTwo, false); + registryB.setOptIn(validatorPublicKeyOne, false); + registryB.setOptIn(validatorPublicKeyTwo, true); + + // --- Deploy hub implementation (logic) --- + hubImplementation = new ValidatorOptInHub(); + + // Prepare initial registries for initializer (NOW 3) + address[] memory initialRegistries = new address[](3); + initialRegistries[0] = address(registryA); + initialRegistries[1] = address(registryB); + initialRegistries[2] = address(registryC); + + // --- Deploy UUPS proxy with initializer call --- + bytes memory initializerCalldata = + abi.encodeCall(ValidatorOptInHub.initialize, (initialRegistries, contractOwner)); + ERC1967Proxy proxy = new ERC1967Proxy(address(hubImplementation), initializerCalldata); + + // Contract has payable receive/fallback → cast via payable(address) + hub = ValidatorOptInHub(payable(address(proxy))); + } + + // --------------------------- + // Internal assertion helpers + // --------------------------- + + function _expectCustomError(bytes4 selector) internal { + vm.expectRevert(abi.encodeWithSelector(selector)); + } + + function _assertEqualBoolArray(bool[] memory actual, bool[] memory expected) internal { + assertEq(actual.length, expected.length, "length mismatch"); + for (uint256 index = 0; index < actual.length; ++index) { + assertEq(actual[index], expected[index], "element mismatch"); + } + } + + function _assertEqualBoolMatrix(bool[][] memory actual, bool[][] memory expected) internal { + assertEq(actual.length, expected.length, "row count mismatch"); + for (uint256 row = 0; row < actual.length; ++row) { + _assertEqualBoolArray(actual[row], expected[row]); + } + } + + // --------------- + // Core behaviours + // --------------- + + function test_initialize_and_queryMatrix() public { + // Query two validator keys + bytes[] memory validatorPublicKeys = new bytes[](2); + validatorPublicKeys[0] = validatorPublicKeyOne; + validatorPublicKeys[1] = validatorPublicKeyTwo; + + bool[][] memory actualResultsMatrix = hub.areValidatorsOptedInList(validatorPublicKeys); + + // Expected columns NOW: [registryA, registryB, registryC] + bool[][] memory expectedResultsMatrix = new bool[][](2); + expectedResultsMatrix; + expectedResultsMatrix[0] = new bool[](3); + expectedResultsMatrix[1] = new bool[](3); + + // pk1: A=true, B=false, C=false + expectedResultsMatrix[0][0] = true; + expectedResultsMatrix[0][1] = false; + expectedResultsMatrix[0][2] = false; + + // pk2: A=false, B=true, C=false + expectedResultsMatrix[1][0] = false; + expectedResultsMatrix[1][1] = true; + expectedResultsMatrix[1][2] = false; + + _assertEqualBoolMatrix(actualResultsMatrix, expectedResultsMatrix); + + // “Any” aggregate checks + assertTrue(hub.isValidatorOptedIn(validatorPublicKeyOne)); + assertTrue(hub.isValidatorOptedIn(validatorPublicKeyTwo)); + assertFalse(hub.isValidatorOptedIn(validatorPublicKeyThree)); + } + + function test_addRegistry_appends_and_affectsResults() public { + // Add registryD (defaults false for all keys) on top of 3 initial ones + hub.addRegistry(address(registryD)); + + // pk1 remains true (registryA says true) + assertTrue(hub.isValidatorOptedIn(validatorPublicKeyOne)); + + // pk3 remains false (all registries false) + assertFalse(hub.isValidatorOptedIn(validatorPublicKeyThree)); + + // Matrix now has 4 columns + bytes[] memory validatorPublicKeys = new bytes[](1); + validatorPublicKeys[0] = validatorPublicKeyOne; + + bool[][] memory actualResultsMatrix = hub.areValidatorsOptedInList(validatorPublicKeys); + assertEq(actualResultsMatrix[0].length, 4, "expected 4 registries"); + assertEq(actualResultsMatrix[0][0], true); // A + assertEq(actualResultsMatrix[0][1], false); // B + assertEq(actualResultsMatrix[0][2], false); // C + assertEq(actualResultsMatrix[0][3], false); // D (added) + } + + function test_addRegistry_zeroAddress_reverts() public { + _expectCustomError(IValidatorOptInHub.ZeroAddress.selector); + hub.addRegistry(address(0)); + } + + function test_addRegistry_invalidRegistry_reverts_onProbe() public { + // EOA / non-contract → probe should fail decoding and revert + _expectCustomError(IValidatorOptInHub.InvalidRegistry.selector); + hub.addRegistry(badRegistry); + } + + function test_addRegistry_invalidRegistry_reverts_onProbe_wrongSelector() public { + // Contract exists but does NOT implement isValidatorOptedIn(bytes) → staticcall ok with empty/invalid returndata + // → ret.length != 32 → InvalidRegistry + MockWrongSigRegistry wrong = new MockWrongSigRegistry(); + _expectCustomError(IValidatorOptInHub.InvalidRegistry.selector); + hub.addRegistry(address(wrong)); + } + + function test_addRegistry_invalidRegistry_reverts_onProbe_mutating() public { + // Contract implements the function but writes state → STATICCALL will fail (ok == false) → InvalidRegistry + MockMutatingRegistry bad = new MockMutatingRegistry(); + _expectCustomError(IValidatorOptInHub.InvalidRegistry.selector); + hub.addRegistry(address(bad)); + } + + function test_updateRegistry_replacesIndex_stably() public { + // We start at [A, B, C]; replace index 1 (B) with C → [A, C, C] + hub.updateRegistry(1, address(registryB), address(registryC)); + + // pk2 was true on B, false on C → col1 should now be false + bytes[] memory validatorPublicKeys = new bytes[](1); + validatorPublicKeys[0] = validatorPublicKeyTwo; + + bool[][] memory actualResultsMatrix = hub.areValidatorsOptedInList(validatorPublicKeys); + assertEq(actualResultsMatrix[0].length, 3); + assertEq(actualResultsMatrix[0][0], false); // A says false for pk2 + assertEq(actualResultsMatrix[0][1], false); // replaced with C + assertEq(actualResultsMatrix[0][2], false); // original C + } + + function test_updateRegistry_invalidIndex_reverts() public { + _expectCustomError(IValidatorOptInHub.InvalidIndex.selector); + hub.updateRegistry(99, address(registryA), address(registryC)); + } + + function test_updateRegistry_zeroAddress_reverts() public { + _expectCustomError(IValidatorOptInHub.ZeroAddress.selector); + hub.updateRegistry(0, address(0), address(registryC)); + } + + function test_updateRegistry_indexRegistryMismatch_reverts() public { + // Index 0 holds registryA; pass the wrong oldRegistry (registryB) + _expectCustomError(IValidatorOptInHub.IndexRegistryMismatch.selector); + hub.updateRegistry(0, address(registryB), address(registryC)); + } + + function test_removeRegistry_zeroesSlot_preservingIndexing() public { + // “Logical delete” index 0 (registryA) to preserve column indices + hub.removeRegistry(0, address(registryA)); + + // pk1 used to be true at column 0; now zeroed → false + bytes[] memory validatorPublicKeys = new bytes[](1); + validatorPublicKeys[0] = validatorPublicKeyOne; + + bool[][] memory actualResultsMatrix = hub.areValidatorsOptedInList(validatorPublicKeys); + assertEq(actualResultsMatrix[0][0], false); + // Column 1 remains registryB and still reports false for pk1 + assertEq(actualResultsMatrix[0][1], false); + // Column 2 remains registryC and reports false for pk1 + assertEq(actualResultsMatrix[0][2], false); + } + + function test_removeRegistry_invalidIndex_reverts() public { + _expectCustomError(IValidatorOptInHub.InvalidIndex.selector); + hub.removeRegistry(42, address(registryA)); + } + + function test_removeRegistry_zeroAddress_reverts() public { + _expectCustomError(IValidatorOptInHub.ZeroAddress.selector); + hub.removeRegistry(0, address(0)); + } + + function test_removeRegistry_indexRegistryMismatch_reverts() public { + _expectCustomError(IValidatorOptInHub.IndexRegistryMismatch.selector); + hub.removeRegistry(1, address(registryA)); // slot 1 is registryB at setup + } + + + function test_receive_reverts() public { + (bool ok, bytes memory r) = address(hub).call{value: 1 ether}(""); + assertFalse(ok, "receive should revert"); + assertGe(r.length, 4); + assertEq(bytes4(r), Errors.InvalidReceive.selector); + } + + function test_fallback_reverts() public { + (bool ok, bytes memory r) = address(hub).call(hex"deadbeef"); + assertFalse(ok, "fallback should revert"); + assertGe(r.length, 4); + assertEq(bytes4(r), Errors.InvalidFallback.selector); + } + + function test_isValidatorOptedInList_returnsPerRegistryFlags_forEachValidator_onInitialSetup() public { + // validatorPublicKeyOne is true on registryA, false on registryB/C + bool[] memory perRegistryFlagsForValidatorOne = hub.isValidatorOptedInList(validatorPublicKeyOne); + bool[] memory expectedFlagsForValidatorOne = new bool[](3); + expectedFlagsForValidatorOne[0] = true; // A + expectedFlagsForValidatorOne[1] = false; // B + expectedFlagsForValidatorOne[2] = false; // C + _assertEqualBoolArray(perRegistryFlagsForValidatorOne, expectedFlagsForValidatorOne); + + // validatorPublicKeyTwo is false on registryA, true on registryB, false on registryC + bool[] memory perRegistryFlagsForValidatorTwo = hub.isValidatorOptedInList(validatorPublicKeyTwo); + bool[] memory expectedFlagsForValidatorTwo = new bool[](3); + expectedFlagsForValidatorTwo[0] = false; // A + expectedFlagsForValidatorTwo[1] = true; // B + expectedFlagsForValidatorTwo[2] = false; // C + _assertEqualBoolArray(perRegistryFlagsForValidatorTwo, expectedFlagsForValidatorTwo); + + // validatorPublicKeyThree is false everywhere + bool[] memory perRegistryFlagsForValidatorThree = hub.isValidatorOptedInList(validatorPublicKeyThree); + bool[] memory expectedFlagsForValidatorThree = new bool[](3); + expectedFlagsForValidatorThree[0] = false; + expectedFlagsForValidatorThree[1] = false; + expectedFlagsForValidatorThree[2] = false; + _assertEqualBoolArray(perRegistryFlagsForValidatorThree, expectedFlagsForValidatorThree); + } + + function test_areValidatorsOptedIn_returnsAnyAggregation_forBatch_onInitialSetup() public { + bytes[] memory batchOfValidatorPublicKeys = new bytes[](3); + batchOfValidatorPublicKeys[0] = validatorPublicKeyOne; // true on A + batchOfValidatorPublicKeys[1] = validatorPublicKeyTwo; // true on B + batchOfValidatorPublicKeys[2] = validatorPublicKeyThree; // false everywhere + + bool[] memory anyAggregationStatuses = hub.areValidatorsOptedIn(batchOfValidatorPublicKeys); + + bool[] memory expectedAnyAggregationStatuses = new bool[](3); + expectedAnyAggregationStatuses[0] = true; // pk1 → true (A) + expectedAnyAggregationStatuses[1] = true; // pk2 → true (B) + expectedAnyAggregationStatuses[2] = false; // pk3 → false + _assertEqualBoolArray(anyAggregationStatuses, expectedAnyAggregationStatuses); + } + + function test_areValidatorsOptedIn_reflectsChanges_afterUpdateRegistryReplacement() public { + // Replace index 1 (registryB) with registryC; pk2 was true on B, but false on C + hub.updateRegistry(1, address(registryB), address(registryC)); + + bytes[] memory batchOfValidatorPublicKeys = new bytes[](2); + batchOfValidatorPublicKeys[0] = validatorPublicKeyOne; // still true on A + batchOfValidatorPublicKeys[1] = validatorPublicKeyTwo; // should become false (B replaced) + + bool[] memory anyAggregationStatuses = hub.areValidatorsOptedIn(batchOfValidatorPublicKeys); + + bool[] memory expectedAnyAggregationStatuses = new bool[](2); + expectedAnyAggregationStatuses[0] = true; // pk1 true via A + expectedAnyAggregationStatuses[1] = false; // pk2 now false (B→C) + _assertEqualBoolArray(anyAggregationStatuses, expectedAnyAggregationStatuses); + } + + function test_isValidatorOptedInList_expandsLength_afterAddingAdditionalRegistry() public { + // Add a fourth registry (registryD) that returns false for all keys by default + hub.addRegistry(address(registryD)); + + // Query per-registry flags for validatorPublicKeyOne; should now have length 4 + bool[] memory perRegistryFlagsForValidatorOne = hub.isValidatorOptedInList(validatorPublicKeyOne); + assertEq(perRegistryFlagsForValidatorOne.length, 4, "expected per-registry list to include the newly added registry"); + assertEq(perRegistryFlagsForValidatorOne[0], true); // A + assertEq(perRegistryFlagsForValidatorOne[1], false); // B + assertEq(perRegistryFlagsForValidatorOne[2], false); // C + assertEq(perRegistryFlagsForValidatorOne[3], false); // D (newly added defaults to false) + } + + function test_areValidatorsOptedIn_returnsEmpty_whenInputArrayIsEmpty() public { + bytes[] memory emptyBatchOfValidatorPublicKeys = new bytes[](0); + bool[] memory anyAggregationStatuses = hub.areValidatorsOptedIn(emptyBatchOfValidatorPublicKeys); + assertEq(anyAggregationStatuses.length, 0, "empty input should produce empty result"); + } + + function test_isValidatorOptedInList_allFalse_forUnknownValidatorKey() public { + bool[] memory perRegistryFlagsForUnknownKey = hub.isValidatorOptedInList(validatorPublicKeyThree); + bool[] memory expectedFlagsForUnknownKey = new bool[](3); + expectedFlagsForUnknownKey[0] = false; + expectedFlagsForUnknownKey[1] = false; + expectedFlagsForUnknownKey[2] = false; + _assertEqualBoolArray(perRegistryFlagsForUnknownKey, expectedFlagsForUnknownKey); + } + +} + + +// --------------------- +// Local mock registry +// --------------------- +contract MockRegistry is IRegistry { + mapping(bytes32 => bool) private optedInByKeyHash; + + function setOptIn(bytes memory validatorPublicKey, bool isOptedIn) external { + optedInByKeyHash[keccak256(validatorPublicKey)] = isOptedIn; + } + + function isValidatorOptedIn(bytes calldata validatorPublicKey) + external + view + returns (bool) + { + return optedInByKeyHash[keccak256(validatorPublicKey)]; + } +} + +contract MockWrongSigRegistry { + // Wrong arg type → selector mismatch with IRegistry.isValidatorOptedIn(bytes) + function isValidatorOptedIn(bytes32 /*validatorPublicKey*/) external pure returns (bool) { + return true; + } +} + +contract MockMutatingRegistry { + uint256 internal writes; + // Not marked view and performs SSTORE → will revert under STATICCALL during validation probe + function isValidatorOptedIn(bytes calldata /*validatorPublicKey*/) external returns (bool) { + writes += 1; + return true; + } +} \ No newline at end of file