|
| 1 | +// SPDX-License-Identifier: UNLICENSED |
| 2 | +pragma solidity ^0.8.13; |
| 3 | + |
| 4 | +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; |
| 5 | +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; |
| 6 | +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; |
| 7 | + |
| 8 | +contract KeyManager is Initializable, OwnableUpgradeable, UUPSUpgradeable { |
| 9 | + struct CommitteeMember { |
| 10 | + /// @notice public key for consensus votes, also used as the primary label for a node |
| 11 | + bytes sigKey; |
| 12 | + /// @notice DH public key used for authenticated network messages |
| 13 | + bytes dhKey; |
| 14 | + /// @notice public key for encrypting DKG-specific payloads |
| 15 | + bytes dkgKey; |
| 16 | + /// @notice a network address: `ip:port` or `hostname:port` |
| 17 | + string networkAddress; |
| 18 | + } |
| 19 | + |
| 20 | + /// @notice The consensus committee rotates with each epoch, registered by contract `manager`. |
| 21 | + /// @notice Timeboost makes the simplifying decision that this committee is exactly the keyset |
| 22 | + struct Committee { |
| 23 | + /// @notice unique identifier for the committee, assigned by this contract |
| 24 | + uint64 id; |
| 25 | + /// @notice wall clock time since unix epoch for this committee to be active |
| 26 | + uint64 effectiveTimestamp; |
| 27 | + /// @notice constituting members and their key materials |
| 28 | + CommitteeMember[] members; |
| 29 | + } |
| 30 | + |
| 31 | + /// @notice Emitted when a committee is created. |
| 32 | + /// @param id The id of the committee. |
| 33 | + event CommitteeCreated(uint64 indexed id); |
| 34 | + |
| 35 | + /// @notice Emitted when the threshold encryption key is set. |
| 36 | + /// @param thresholdEncryptionKey The threshold encryption key. |
| 37 | + event ThresholdEncryptionKeyUpdated(bytes thresholdEncryptionKey); |
| 38 | + |
| 39 | + /// @notice Emitted when the manager is changed. |
| 40 | + /// @param oldManager The old manager. |
| 41 | + /// @param newManager The new manager. |
| 42 | + event ManagerChanged(address indexed oldManager, address indexed newManager); |
| 43 | + |
| 44 | + /// @notice Emitted when a committee is removed. |
| 45 | + /// @param fromId The id of the first committee to prune. |
| 46 | + /// @param toId The id of the last committee to prune. |
| 47 | + event CommitteesPruned(uint64 indexed fromId, uint64 indexed toId); |
| 48 | + |
| 49 | + /// @notice Thrown when the caller is not the manager. |
| 50 | + /// @param caller The address that called the function. |
| 51 | + error NotManager(address caller); |
| 52 | + |
| 53 | + /// @notice Thrown when the address is invalid. |
| 54 | + error InvalidAddress(); |
| 55 | + |
| 56 | + /// @notice Thrown when the threshold encryption key is already set. |
| 57 | + error ThresholdEncryptionKeyAlreadySet(); |
| 58 | + |
| 59 | + /// @notice Thrown when the committee id does not exist. |
| 60 | + /// @param committeeId The id of the committee. |
| 61 | + error CommitteeIdDoesNotExist(uint64 committeeId); |
| 62 | + /// @notice Thrown when the committee is empty. |
| 63 | + error EmptyCommitteeMembers(); |
| 64 | + /// @notice Thrown when the effective timestamp is invalid. |
| 65 | + error InvalidEffectiveTimestamp(uint64 effectiveTimestamp, uint64 lastEffectiveTimestamp); |
| 66 | + /// @notice Thrown when there is no committee scheduled. |
| 67 | + error NoCommitteeScheduled(); |
| 68 | + /// @notice Thrown when the committee id overflows. |
| 69 | + error CommitteeIdOverflow(); |
| 70 | + /// @notice Thrown when the committee is too recent to remove. |
| 71 | + error CannotRemoveRecentCommittees(); |
| 72 | + /// @notice Thrown when pruning with invalid range. |
| 73 | + error InvalidPruneRange(uint64 upToCommitteeId, uint64 oldestStored, uint64 nextCommitteeId); |
| 74 | + |
| 75 | + /// @notice The threshold encryption key for the committee. |
| 76 | + bytes public thresholdEncryptionKey; |
| 77 | + /// @notice The mapping of committee ids to committees. |
| 78 | + mapping(uint64 => Committee) public committees; |
| 79 | + /// @notice The manager of the contract. |
| 80 | + address public manager; |
| 81 | + /// @notice The next committee id. |
| 82 | + uint64 public nextCommitteeId; |
| 83 | + /// @notice The oldest committee id still stored in the mapping |
| 84 | + uint64 private _oldestStoredCommitteeId; |
| 85 | + /// @notice The gap for future upgrades. |
| 86 | + uint256[48] private __gap; |
| 87 | + |
| 88 | + /// @notice Modifier to check if the caller is the manager. |
| 89 | + modifier onlyManager() { |
| 90 | + _onlyManager(); |
| 91 | + _; |
| 92 | + } |
| 93 | + |
| 94 | + /// @notice Internal function to check if the caller is the manager. |
| 95 | + function _onlyManager() internal view { |
| 96 | + if (msg.sender != manager) { |
| 97 | + revert NotManager(msg.sender); |
| 98 | + } |
| 99 | + } |
| 100 | + |
| 101 | + constructor() { |
| 102 | + _disableInitializers(); |
| 103 | + } |
| 104 | + |
| 105 | + /** |
| 106 | + * @notice This function is used to initialize the contract. |
| 107 | + * @dev Reverts if the manager is the zero address. |
| 108 | + * @dev Assumes that the manager is valid. |
| 109 | + * @dev This must be called once when the contract is first deployed. |
| 110 | + * @param initialManager The initial manager. |
| 111 | + */ |
| 112 | + function initialize(address initialManager) external initializer { |
| 113 | + if (initialManager == address(0)) { |
| 114 | + revert InvalidAddress(); |
| 115 | + } |
| 116 | + __Ownable_init(msg.sender); |
| 117 | + __UUPSUpgradeable_init(); |
| 118 | + manager = initialManager; |
| 119 | + } |
| 120 | + |
| 121 | + /** |
| 122 | + * @notice This function is used to authorize the upgrade of the contract. |
| 123 | + * @dev Reverts if the caller is not the owner. |
| 124 | + * @dev Assumes that the new implementation is valid. |
| 125 | + * @param newImplementation The new implementation. |
| 126 | + */ |
| 127 | + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} |
| 128 | + |
| 129 | + /** |
| 130 | + * @notice This function is used to set the manager. |
| 131 | + * @dev Reverts if the manager is the zero address or the same as the current manager. |
| 132 | + * @dev Reverts if the caller is not the owner. |
| 133 | + * @dev Assumes that the manager is valid. |
| 134 | + * @param newManager The new manager. |
| 135 | + */ |
| 136 | + function setManager(address newManager) external virtual onlyOwner { |
| 137 | + if (newManager == address(0) || newManager == manager) { |
| 138 | + revert InvalidAddress(); |
| 139 | + } |
| 140 | + address oldManager = manager; |
| 141 | + manager = newManager; |
| 142 | + emit ManagerChanged(oldManager, newManager); |
| 143 | + } |
| 144 | + |
| 145 | + /** |
| 146 | + * @notice This function is used to set the threshold encryption key. |
| 147 | + * @dev Reverts if the threshold encryption key is already set. |
| 148 | + * @dev Reverts if the caller is not the manager. |
| 149 | + * @dev Assumes that the threshold encryption key is valid. |
| 150 | + * @param newThresholdEncryptionKey The threshold encryption key. |
| 151 | + */ |
| 152 | + function setThresholdEncryptionKey(bytes calldata newThresholdEncryptionKey) external virtual onlyManager { |
| 153 | + if (thresholdEncryptionKey.length > 0) { |
| 154 | + revert ThresholdEncryptionKeyAlreadySet(); |
| 155 | + } |
| 156 | + thresholdEncryptionKey = newThresholdEncryptionKey; |
| 157 | + emit ThresholdEncryptionKeyUpdated(newThresholdEncryptionKey); |
| 158 | + } |
| 159 | + |
| 160 | + /** |
| 161 | + * @notice This function is used to set the next committee. |
| 162 | + * @dev Reverts if the members array is empty. |
| 163 | + * @dev Reverts if the effective timestamp is less than the last effective timestamp. |
| 164 | + * @dev Reverts if the committees mapping is at uint64.max. |
| 165 | + * @dev Assumes that the committee members are valid. |
| 166 | + * @param effectiveTimestamp The effective timestamp of the committee. |
| 167 | + * @param members The committee members. |
| 168 | + * @return committeeId The id of the new committee. |
| 169 | + */ |
| 170 | + function setNextCommittee(uint64 effectiveTimestamp, CommitteeMember[] calldata members) |
| 171 | + external |
| 172 | + virtual |
| 173 | + onlyManager |
| 174 | + returns (uint64 committeeId) |
| 175 | + { |
| 176 | + if (members.length == 0) { |
| 177 | + revert EmptyCommitteeMembers(); |
| 178 | + } |
| 179 | + |
| 180 | + // ensure the effective timestamp is greater than the last effective timestamp |
| 181 | + if (nextCommitteeId > 0) { |
| 182 | + uint64 lastTimestamp = committees[nextCommitteeId - 1].effectiveTimestamp; |
| 183 | + if (effectiveTimestamp <= lastTimestamp) { |
| 184 | + revert InvalidEffectiveTimestamp(effectiveTimestamp, lastTimestamp); |
| 185 | + } |
| 186 | + } |
| 187 | + |
| 188 | + if (nextCommitteeId == type(uint64).max) revert CommitteeIdOverflow(); |
| 189 | + |
| 190 | + committees[nextCommitteeId] = |
| 191 | + Committee({id: nextCommitteeId, effectiveTimestamp: effectiveTimestamp, members: members}); |
| 192 | + |
| 193 | + nextCommitteeId++; |
| 194 | + |
| 195 | + emit CommitteeCreated(nextCommitteeId - 1); |
| 196 | + return nextCommitteeId - 1; |
| 197 | + } |
| 198 | + |
| 199 | + /** |
| 200 | + * @notice This function is used to get the committee by id. |
| 201 | + * @dev Reverts if the id is greater than the length of the committees mapping. |
| 202 | + * @dev Reverts if the id is less than the head committee id. |
| 203 | + * @param id The id of the committee. |
| 204 | + * @return committee The committee. |
| 205 | + */ |
| 206 | + function getCommitteeById(uint64 id) external view virtual returns (Committee memory committee) { |
| 207 | + if (id < _oldestStoredCommitteeId || committees[id].id != id) { |
| 208 | + revert CommitteeIdDoesNotExist(id); |
| 209 | + } |
| 210 | + |
| 211 | + return committees[id]; |
| 212 | + } |
| 213 | + |
| 214 | + /** |
| 215 | + * @notice This function is used to get the current committee id. |
| 216 | + * @dev Reverts if there is no committee scheduled at the current timestamp. |
| 217 | + * @dev Searches backwards through existing committees to find the active one. |
| 218 | + * @return committeeId The current committee id. |
| 219 | + */ |
| 220 | + function currentCommitteeId() public view virtual returns (uint64 committeeId) { |
| 221 | + uint64 currentTimestamp = uint64(block.timestamp); |
| 222 | + |
| 223 | + if (nextCommitteeId == 0 || _oldestStoredCommitteeId >= nextCommitteeId) { |
| 224 | + revert NoCommitteeScheduled(); |
| 225 | + } |
| 226 | + |
| 227 | + // Search backwards from most recent committee to oldest stored |
| 228 | + uint64 currCommitteeId = nextCommitteeId - 1; |
| 229 | + while (currCommitteeId >= _oldestStoredCommitteeId) { |
| 230 | + if (currentTimestamp >= committees[currCommitteeId].effectiveTimestamp) { |
| 231 | + return currCommitteeId; |
| 232 | + } |
| 233 | + |
| 234 | + if (currCommitteeId == 0) { |
| 235 | + break; |
| 236 | + } |
| 237 | + |
| 238 | + currCommitteeId--; |
| 239 | + } |
| 240 | + |
| 241 | + revert NoCommitteeScheduled(); |
| 242 | + } |
| 243 | + |
| 244 | + /** |
| 245 | + * @notice Prunes all committees from _oldestStoredCommitteeId up to and including upToCommitteeId. |
| 246 | + * @dev This matches timeboost's garbage collection behavior of removing old committees in bulk. |
| 247 | + * @dev Reverts if upToCommitteeId is not in a valid range for pruning. |
| 248 | + * @dev Reverts if any committee in the range became effective within the last 10 minutes. |
| 249 | + * @param upToCommitteeId The highest committee ID to prune (inclusive). |
| 250 | + */ |
| 251 | + function pruneUntil(uint64 upToCommitteeId) external virtual onlyManager { |
| 252 | + if (upToCommitteeId < _oldestStoredCommitteeId || upToCommitteeId >= nextCommitteeId) { |
| 253 | + revert InvalidPruneRange(upToCommitteeId, _oldestStoredCommitteeId, nextCommitteeId); |
| 254 | + } |
| 255 | + |
| 256 | + // Delete all committees in range |
| 257 | + uint64 cutOffTime = uint64(block.timestamp - 10 minutes); |
| 258 | + uint64 oldOldestStored = _oldestStoredCommitteeId; |
| 259 | + for (uint64 id = _oldestStoredCommitteeId; id <= upToCommitteeId; id++) { |
| 260 | + if (committees[id].effectiveTimestamp >= cutOffTime) { |
| 261 | + revert CannotRemoveRecentCommittees(); |
| 262 | + } |
| 263 | + delete committees[id]; |
| 264 | + } |
| 265 | + |
| 266 | + _oldestStoredCommitteeId = upToCommitteeId + 1; |
| 267 | + |
| 268 | + emit CommitteesPruned(oldOldestStored, upToCommitteeId); |
| 269 | + } |
| 270 | +} |
0 commit comments