diff --git a/packages/evm/contracts/Enclave.sol b/packages/evm/contracts/Enclave.sol index 644cc14b..74d0b20f 100644 --- a/packages/evm/contracts/Enclave.sol +++ b/packages/evm/contracts/Enclave.sol @@ -106,7 +106,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { //////////////////////////////////////////////////////////// function request( - address[] memory pools, // TODO: should we allow for multiple pools? + address filter, uint32[2] calldata threshold, // TODO: do we also need a start block/time? Would it be possible to have computations where inputs are //published before the request is made? This kind of assumes the cypher nodes have already been selected @@ -169,7 +169,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { e3s[e3Id] = e3; require( - cyphernodeRegistry.selectCommittee(e3Id, pools, threshold), + cyphernodeRegistry.requestCommittee(e3Id, filter, threshold), CommitteeSelectionFailed() ); // TODO: validate that the selected pool accepts both the computation and execution modules. @@ -177,7 +177,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit E3Requested( e3Id, e3s[e3Id], - pools, + filter, computationModule, executionModule ); @@ -190,7 +190,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { require(e3.expiration == 0, E3AlreadyActivated(e3Id)); bytes memory committeePublicKey = cyphernodeRegistry - .getCommitteePublicKey(e3Id); + .committeePublicKey(e3Id); // Note: This check feels weird require(committeePublicKey.length > 0, CommitteeSelectionFailed()); diff --git a/packages/evm/contracts/interfaces/ICommitteeCoordinator.sol b/packages/evm/contracts/interfaces/ICommitteeCoordinator.sol new file mode 100644 index 00000000..8177745f --- /dev/null +++ b/packages/evm/contracts/interfaces/ICommitteeCoordinator.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.26; + +interface ICommitteeCoordinator { + /// @notice This event MUST be emitted when a committee is selected for an E3. + /// @param e3Id ID of the E3 for which the committee was selected. + /// @param threshold The M/N threshold for the committee. + event CommitteeRequested(uint256 indexed e3Id, uint32[2] threshold); + + /// @notice This event MUST be emitted when a committee is selected for an E3. + /// @param e3Id ID of the E3 for which the committee was selected. + /// @param publicKey Public key of the committee. + event CommitteeAssembled(uint256 indexed e3Id, bytes publicKey); + + /// @notice This function should be called by the Enclave contract to select a node committee. + /// @param e3Id ID of the E3 for which to select the committee. + /// @param threshold The M/N threshold for the committee. + /// @return success True if committee selection was successfully initiated. + function requestCommittee( + uint256 e3Id, + uint32[2] calldata threshold + ) external returns (bool success); + + /// @notice This function should be called by the Enclave contract to get the public key of a committee. + /// @dev This function MUST revert if no committee has been requested for the given E3. + /// @dev This function MUST revert if the committee has not yet published a public key. + /// @param e3Id ID of the E3 for which to get the committee public key. + /// @return publicKey The public key of the committee. + function committeePublicKey( + uint256 e3Id + ) external view returns (bytes memory); +} diff --git a/packages/evm/contracts/interfaces/ICyphernodeRegistry.sol b/packages/evm/contracts/interfaces/ICyphernodeRegistry.sol index f69a1d28..3d6da67e 100644 --- a/packages/evm/contracts/interfaces/ICyphernodeRegistry.sol +++ b/packages/evm/contracts/interfaces/ICyphernodeRegistry.sol @@ -4,48 +4,40 @@ pragma solidity >=0.8.26; interface ICyphernodeRegistry { /// @notice This event MUST be emitted when a committee is selected for an E3. /// @param e3Id ID of the E3 for which the committee was selected. - /// @param pools Addresses of the pools of nodes from which the committee was selected. + /// @param filter Address of the contract that will coordinate committee selection. /// @param threshold The M/N threshold for the committee. event CommitteeRequested( uint256 indexed e3Id, - address[] pools, + address filter, uint32[2] threshold ); /// @notice This event MUST be emitted when a committee is selected for an E3. /// @param e3Id ID of the E3 for which the committee was selected. - /// @param nodes Addresses of the nodes in the committee. - /// @param merkleRoots Merkle roots of the nodes in the committee. /// @param publicKey Public key of the committee. - event CommitteeSelected( - uint256 indexed e3Id, - address[] nodes, - bytes32[] merkleRoots, - bytes publicKey - ); + event CommitteeSelected(uint256 indexed e3Id, bytes publicKey); - /// @notice This event MUST be emitted when a node is added to the registry. - /// @param nodeId ID of the node. - /// @param node Address of the node. - event NodeAdded(uint256 indexed nodeId, address indexed node); + /// @notice This event MUST be emitted when a filter is added to the registry. + event FilterAdded(address indexed filter); - /// @notice This event MUST be emitted when a node is removed from the registry. - /// @param nodeId ID of the node. - /// @param node Address of the node. - event NodeRemoved(uint256 indexed nodeId, address indexed node); + /// @notice This event MUST be emitted when a filter is removed from the registry. + event FilterRemoved(address indexed filter); /// @notice This event MUST be emitted when `encalve` is set. /// @param enclave Address of the enclave contract. event EnclaveSet(address indexed enclave); + function addFilter(address filter) external; + + function removeFilter(address filter) external; + /// @notice This function should be called by the Enclave contract to select a node committee. /// @param e3Id ID of the E3 for which to select the committee. - /// @param pools IDs of the pool of nodes from which to select the committee. /// @param threshold The M/N threshold for the committee. /// @return success True if committee selection was successfully initiated. - function selectCommittee( + function requestCommittee( uint256 e3Id, - address[] memory pools, + address filter, uint32[2] calldata threshold ) external returns (bool success); @@ -54,7 +46,7 @@ interface ICyphernodeRegistry { /// @dev This function MUST revert if the committee has not yet published a public key. /// @param e3Id ID of the E3 for which to get the committee public key. /// @return publicKey The public key of the committee. - function getCommitteePublicKey( + function committeePublicKey( uint256 e3Id - ) external view returns (bytes memory publicKey); + ) external view returns (bytes memory); } diff --git a/packages/evm/contracts/interfaces/IEnclave.sol b/packages/evm/contracts/interfaces/IEnclave.sol index bf7e4f5e..d1b48177 100644 --- a/packages/evm/contracts/interfaces/IEnclave.sol +++ b/packages/evm/contracts/interfaces/IEnclave.sol @@ -13,13 +13,13 @@ interface IEnclave { /// @notice This event MUST be emitted when an Encrypted Execution Environment (E3) is successfully requested. /// @param e3Id ID of the E3. /// @param e3 Details of the E3. - /// @param pool Address of the pool of nodes from which the Cypher Node committee was selected. + /// @param filter Address of the pool of nodes from which the Cypher Node committee was selected. /// @param computationModule Address of the Computation module selected. /// @param executionModule Address of the execution module selected. event E3Requested( uint256 e3Id, E3 e3, - address[] pool, + address filter, IComputationModule indexed computationModule, IExecutionModule indexed executionModule ); @@ -87,7 +87,7 @@ interface IEnclave { /// @notice This function should be called to request a computation within an Encrypted Execution Environment (E3). /// @dev This function MUST emit the E3Requested event. - /// @param pools IDs of the pool of nodes from which to select the committee. + /// @param filter IDs of the pool of nodes from which to select the committee. /// @param threshold The M/N threshold for the committee. /// @param duration The duration of the computation in seconds. /// @param computationModule Address of the computation module. @@ -97,7 +97,7 @@ interface IEnclave { /// @return e3Id ID of the E3. /// @return e3 The E3 struct. function request( - address[] memory pools, + address filter, uint32[2] calldata threshold, uint256 duration, IComputationModule computationModule, diff --git a/packages/evm/contracts/interfaces/INodePool.sol b/packages/evm/contracts/interfaces/INodePool.sol deleted file mode 100644 index bf1a9100..00000000 --- a/packages/evm/contracts/interfaces/INodePool.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -pragma solidity >=0.8.26; - -interface INodePool { - /// @notice This function MUST return the Merkle root of this contract's curated pool of nodes. - function merkleRoot() external returns (bytes32 merkleRoot); -} diff --git a/packages/evm/contracts/registry/CyphernodeRegistryOwnable.sol b/packages/evm/contracts/registry/CyphernodeRegistryOwnable.sol index 69a6ec24..b84e215d 100644 --- a/packages/evm/contracts/registry/CyphernodeRegistryOwnable.sol +++ b/packages/evm/contracts/registry/CyphernodeRegistryOwnable.sol @@ -2,26 +2,12 @@ pragma solidity >=0.8.26; import { ICyphernodeRegistry } from "../interfaces/ICyphernodeRegistry.sol"; +import { ICommitteeCoordinator } from "../interfaces/ICommitteeCoordinator.sol"; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; contract CyphernodeRegistryOwnable is ICyphernodeRegistry, OwnableUpgradeable { - struct Committee { - address[] nodes; - uint32[2] threshold; - address[] pools; - bytes32[] merkleRoots; - bytes publicKey; - } - - struct Node { - bool eligible; - // Number of duties the node has not yet completed. - // Incremented each time a duty is added, decremented each time a duty is completed. - uint256 outstandingDuties; - } - //////////////////////////////////////////////////////////// // // // Storage Variables // @@ -30,8 +16,9 @@ contract CyphernodeRegistryOwnable is ICyphernodeRegistry, OwnableUpgradeable { address public enclave; - mapping(uint256 e3 => Committee committee) public committees; - mapping(address nodeId => Node node) public nodes; + mapping(address filter => bool enabled) public filters; + + mapping(uint256 e3 => ICommitteeCoordinator coordinator) public requests; //////////////////////////////////////////////////////////// // // @@ -39,6 +26,7 @@ contract CyphernodeRegistryOwnable is ICyphernodeRegistry, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + error CommitteeAlreadyRequested(); error CommitteeAlreadyExists(); error CommitteeAlreadyPublished(); error CommitteeDoesNotExist(); @@ -78,36 +66,21 @@ contract CyphernodeRegistryOwnable is ICyphernodeRegistry, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// - function selectCommittee( + function requestCommittee( uint256 e3Id, - address[] memory pools, + address filter, uint32[2] calldata threshold ) external onlyEnclave returns (bool success) { - Committee storage committee = committees[e3Id]; - require(committee.threshold.length == 0, CommitteeAlreadyExists()); - committee.threshold = threshold; - committee.pools = pools; - success = true; - - emit CommitteeRequested(e3Id, pools, threshold); - } - - function publishCommittee( - uint256 e3Id, - address[] memory _nodes, - bytes32[] memory merkleRoots, - bytes memory publicKey - ) external onlyOwner { - Committee storage committee = committees[e3Id]; require( - keccak256(committee.publicKey) == keccak256(hex""), - CommitteeAlreadyPublished() + requests[e3Id] == ICommitteeCoordinator(address(0)), + CommitteeAlreadyRequested() ); - committee.nodes = _nodes; - committee.merkleRoots = merkleRoots; - committee.publicKey = publicKey; + requests[e3Id] = ICommitteeCoordinator(filter); - emit CommitteeSelected(e3Id, _nodes, merkleRoots, publicKey); + ICommitteeCoordinator(filter).requestCommittee(e3Id, threshold); + + emit CommitteeRequested(e3Id, filter, threshold); + success = true; } //////////////////////////////////////////////////////////// @@ -121,23 +94,28 @@ contract CyphernodeRegistryOwnable is ICyphernodeRegistry, OwnableUpgradeable { emit EnclaveSet(_enclave); } + function addFilter(address filter) external { + filters[filter] = true; + emit FilterAdded(filter); + } + + function removeFilter(address filter) external onlyOwner { + filters[filter] = false; + emit FilterRemoved(filter); + } + //////////////////////////////////////////////////////////// // // // Get Functions // // // //////////////////////////////////////////////////////////// - function getCommitteePublicKey( + function committeePublicKey( uint256 e3Id ) external view returns (bytes memory publicKey) { - publicKey = committees[e3Id].publicKey; - require(publicKey.length > 0, NoPublicKeyPublished()); - } + require(requests[e3Id] != ICommitteeCoordinator(address(0))); - function getCommittee( - uint256 e3Id - ) external view returns (Committee memory committee) { - committee = committees[e3Id]; - require(committees[e3Id].threshold.length > 0, CommitteeDoesNotExist()); + publicKey = requests[e3Id].committeePublicKey(e3Id); + require(publicKey.length > 0, NoPublicKeyPublished()); } } diff --git a/packages/evm/contracts/registry/NaiveCommitteeCoordinator.sol b/packages/evm/contracts/registry/NaiveCommitteeCoordinator.sol new file mode 100644 index 00000000..1a6e2031 --- /dev/null +++ b/packages/evm/contracts/registry/NaiveCommitteeCoordinator.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.26; + +import { ICommitteeCoordinator } from "../interfaces/ICommitteeCoordinator.sol"; +import { + OwnableUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +contract NaiveCommitteeCoordinator is + ICommitteeCoordinator, + OwnableUpgradeable +{ + struct Committee { + address[] nodes; + uint32[2] threshold; + bytes publicKey; + } + + struct Node { + bool eligible; + // Number of duties the node has not yet completed. + // Incremented each time a duty is added, decremented each time a duty is completed. + uint256 outstandingDuties; + } + + //////////////////////////////////////////////////////////// + // // + // Storage Variables // + // // + //////////////////////////////////////////////////////////// + + address public registry; + + mapping(uint256 e3 => Committee committee) public committees; + + //////////////////////////////////////////////////////////// + // // + // Errors // + // // + //////////////////////////////////////////////////////////// + + error CommitteeAlreadyExists(); + error CommitteeAlreadyPublished(); + error CommitteeDoesNotExist(); + error NoPublicKeyPublished(); + error OnlyRegistry(); + + //////////////////////////////////////////////////////////// + // // + // Modifiers // + // // + //////////////////////////////////////////////////////////// + + modifier onlyRegistry() { + require(msg.sender == registry, OnlyRegistry()); + _; + } + + //////////////////////////////////////////////////////////// + // // + // Initialization // + // // + //////////////////////////////////////////////////////////// + + constructor(address _owner, address _enclave) { + initialize(_owner, _enclave); + } + + function initialize(address _owner, address _registry) public initializer { + __Ownable_init(msg.sender); + setRegistry(_registry); + transferOwnership(_owner); + } + + //////////////////////////////////////////////////////////// + // // + // Core Entrypoints // + // // + //////////////////////////////////////////////////////////// + + function requestCommittee( + uint256 e3Id, + uint32[2] calldata threshold + ) external onlyRegistry returns (bool success) { + Committee storage committee = committees[e3Id]; + require(committee.threshold.length == 0, CommitteeAlreadyExists()); + committee.threshold = threshold; + success = true; + } + + function publishCommittee( + uint256 e3Id, + address[] memory _nodes, + bytes memory publicKey + ) external onlyOwner { + Committee storage committee = committees[e3Id]; + require( + keccak256(committee.publicKey) == keccak256(hex""), + CommitteeAlreadyPublished() + ); + committee.nodes = _nodes; + committee.publicKey = publicKey; + } + + //////////////////////////////////////////////////////////// + // // + // Set Functions // + // // + //////////////////////////////////////////////////////////// + + function setRegistry(address _registry) public onlyOwner { + registry = _registry; + } + + //////////////////////////////////////////////////////////// + // // + // Get Functions // + // // + //////////////////////////////////////////////////////////// + + function committeePublicKey( + uint256 e3Id + ) external view returns (bytes memory publicKey) { + publicKey = committees[e3Id].publicKey; + require(publicKey.length > 0, NoPublicKeyPublished()); + } + + /* + * This naive registry does make store and make nodes available on chain + * other more sophisticated registry may keep these on an off-chain tree + * and store the root + * + * The idea is to not include this speficics on the main interfaces for + * enclave for now + */ + function getCommittee( + uint256 e3Id + ) external view returns (Committee memory committee) { + committee = committees[e3Id]; + require(committees[e3Id].threshold.length > 0, CommitteeDoesNotExist()); + } +} diff --git a/packages/evm/contracts/test/MockCyphernodeRegistry.sol b/packages/evm/contracts/test/MockCyphernodeRegistry.sol index 484eae7e..8da724d8 100644 --- a/packages/evm/contracts/test/MockCyphernodeRegistry.sol +++ b/packages/evm/contracts/test/MockCyphernodeRegistry.sol @@ -4,19 +4,19 @@ pragma solidity >=0.8.26; import { ICyphernodeRegistry } from "../interfaces/ICyphernodeRegistry.sol"; contract MockCyphernodeRegistry is ICyphernodeRegistry { - function selectCommittee( + function requestCommittee( uint256, - address[] memory pools, + address filter, uint32[2] calldata ) external pure returns (bool success) { - if (pools[0] == address(2)) { + if (filter == address(2)) { success = false; } else { success = true; } } - function getCommitteePublicKey( + function committeePublicKey( uint256 e3Id ) external pure returns (bytes memory) { if (e3Id == type(uint256).max) { @@ -25,24 +25,30 @@ contract MockCyphernodeRegistry is ICyphernodeRegistry { return abi.encodePacked(keccak256(abi.encode(e3Id))); } } + + function addFilter(address filter) external {} + + function removeFilter(address filter) external {} } contract MockCyphernodeRegistryEmptyKey is ICyphernodeRegistry { - function selectCommittee( + function requestCommittee( uint256, - address[] memory pools, + address filter, uint32[2] calldata ) external pure returns (bool success) { - if (pools[0] == address(2)) { + if (filter == address(2)) { success = false; } else { success = true; } } - function getCommitteePublicKey( - uint256 - ) external pure returns (bytes memory) { + function committeePublicKey(uint256) external pure returns (bytes memory) { return hex""; } + + function addFilter(address filter) external {} + + function removeFilter(address filter) external {} }