|
| 1 | +// Copyright (c), Mysten Labs, Inc. |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +/// Implementation of committee based key server operations. The admin that initializes the |
| 5 | +/// committee should deploy this package itself, so that the committee can manage its own upgrade |
| 6 | +/// and the key rotation. The key server object is owned by the committee. |
| 7 | + |
| 8 | +module seal_committee::seal_committee; |
| 9 | + |
| 10 | +use seal_testnet::key_server::{ |
| 11 | + KeyServer, |
| 12 | + create_partial_key_server, |
| 13 | + create_committee_v2, |
| 14 | + PartialKeyServer |
| 15 | +}; |
| 16 | +use std::string::String; |
| 17 | +use sui::{dynamic_object_field as dof, vec_map::{Self, VecMap}, vec_set::{Self, VecSet}}; |
| 18 | + |
| 19 | +// ===== Errors ===== |
| 20 | + |
| 21 | +const ENotMember: u64 = 0; |
| 22 | +const EInvalidMembers: u64 = 1; |
| 23 | +const EInvalidThreshold: u64 = 2; |
| 24 | +const EInsufficientOldMembers: u64 = 3; |
| 25 | +const EAlreadyRegistered: u64 = 4; |
| 26 | +const ENotRegistered: u64 = 5; |
| 27 | +const EAlreadyProposed: u64 = 6; |
| 28 | +const EInvalidProposal: u64 = 7; |
| 29 | +const EInvalidState: u64 = 8; |
| 30 | + |
| 31 | +// ===== Structs ===== |
| 32 | + |
| 33 | +/// Member information to register with two public keys and the key server URL. |
| 34 | +public struct MemberInfo has copy, drop, store { |
| 35 | + /// ECIES encryption public key, used during offchain DKG. |
| 36 | + enc_pk: vector<u8>, |
| 37 | + /// Signing PK, used during offchain DKG. |
| 38 | + signing_pk: vector<u8>, |
| 39 | + /// URL that the partial key server is running at. |
| 40 | + url: String, |
| 41 | +} |
| 42 | + |
| 43 | +/// Valid states of the committee that holds state specific infos. |
| 44 | +public enum State has drop, store { |
| 45 | + Init { |
| 46 | + members_info: VecMap<address, MemberInfo>, |
| 47 | + }, |
| 48 | + PostDKG { |
| 49 | + members_info: VecMap<address, MemberInfo>, |
| 50 | + partial_pks: vector<vector<u8>>, |
| 51 | + pk: vector<u8>, |
| 52 | + approvals: VecSet<address>, |
| 53 | + }, |
| 54 | + Finalized, |
| 55 | +} |
| 56 | + |
| 57 | +/// MPC committee with defined threshold and members with its state. |
| 58 | +public struct Committee has key { |
| 59 | + id: UID, |
| 60 | + threshold: u16, |
| 61 | + /// The members of the committee. The 'party_id' used in the DKG protocol is the index of this |
| 62 | + /// vector. |
| 63 | + members: vector<address>, |
| 64 | + state: State, |
| 65 | + /// Old committee ID that this committee rotates from. |
| 66 | + old_committee_id: Option<ID>, |
| 67 | +} |
| 68 | + |
| 69 | +// ===== Public Functions ===== |
| 70 | + |
| 71 | +/// Create a committee for fresh DKG with a list of members and threshold. The committee is in Init |
| 72 | +/// state with empty members_info. |
| 73 | +public fun init_committee(threshold: u16, members: vector<address>, ctx: &mut TxContext) { |
| 74 | + init_internal(threshold, members, option::none(), ctx) |
| 75 | +} |
| 76 | + |
| 77 | +/// Create a committee for rotation from an existing finalized old committee. The new committee must |
| 78 | +/// contain an old threshold of the old committee members. |
| 79 | +public fun init_rotation( |
| 80 | + old_committee: &Committee, |
| 81 | + threshold: u16, |
| 82 | + members: vector<address>, |
| 83 | + ctx: &mut TxContext, |
| 84 | +) { |
| 85 | + // Verify the old committee is finalized for rotation. |
| 86 | + assert!(old_committee.is_finalized(), EInvalidState); |
| 87 | + |
| 88 | + // Check that new committee has at least the threshold of old committee members. |
| 89 | + let mut continuing_members = 0; |
| 90 | + members.do!(|member| if (old_committee.members.contains(&member)) { |
| 91 | + continuing_members = continuing_members + 1; |
| 92 | + }); |
| 93 | + assert!(continuing_members >= (old_committee.threshold), EInsufficientOldMembers); |
| 94 | + |
| 95 | + init_internal(threshold, members, option::some(object::id(old_committee)), ctx); |
| 96 | +} |
| 97 | + |
| 98 | +/// Register a member with ecies pk, signing pk and URL. Append it to members_info. |
| 99 | +public fun register( |
| 100 | + committee: &mut Committee, |
| 101 | + enc_pk: vector<u8>, |
| 102 | + signing_pk: vector<u8>, |
| 103 | + url: String, |
| 104 | + ctx: &mut TxContext, |
| 105 | +) { |
| 106 | + // TODO: add checks for enc_pk, signing_pk to be valid elements, maybe PoP. |
| 107 | + assert!(committee.members.contains(&ctx.sender()), ENotMember); |
| 108 | + match (&mut committee.state) { |
| 109 | + State::Init { members_info } => { |
| 110 | + let sender = ctx.sender(); |
| 111 | + assert!(!members_info.contains(&sender), EAlreadyRegistered); |
| 112 | + members_info.insert(sender, MemberInfo { enc_pk, signing_pk, url }); |
| 113 | + }, |
| 114 | + _ => abort EInvalidState, |
| 115 | + } |
| 116 | +} |
| 117 | + |
| 118 | +/// Propose a fresh DKG committee with a list partial pks (in the order of committee's members list) |
| 119 | +/// and master pk. Add the caller to approvals list. If already in PostDKG state, check the submitted |
| 120 | +/// partial_pks and pk are consistent with the onchain state, then add the caller to approvals list. |
| 121 | +/// If all members have approved, finalize the committee by creating a KeyServerV2 and transfer it |
| 122 | +/// to the committee. |
| 123 | +public fun propose( |
| 124 | + committee: &mut Committee, |
| 125 | + partial_pks: vector<vector<u8>>, |
| 126 | + pk: vector<u8>, |
| 127 | + ctx: &mut TxContext, |
| 128 | +) { |
| 129 | + // For fresh DKG committee only. |
| 130 | + assert!(committee.old_committee_id.is_none(), EInvalidState); |
| 131 | + committee.propose_internal(partial_pks, pk, ctx); |
| 132 | + committee.try_finalize(ctx); |
| 133 | +} |
| 134 | + |
| 135 | +/// Propose a rotation from old committee to new one with a list of partial pks. Add the caller to |
| 136 | +/// approvals list. If already in PostDKG state, checks that submitted partial_pks are consistent |
| 137 | +/// with the onchain state, then add the caller to approvals list. |
| 138 | +public fun propose_for_rotation( |
| 139 | + committee: &mut Committee, |
| 140 | + partial_pks: vector<vector<u8>>, |
| 141 | + mut old_committee: Committee, |
| 142 | + ctx: &mut TxContext, |
| 143 | +) { |
| 144 | + committee.check_rotation_consistency(&old_committee); |
| 145 | + let old_committee_id = object::id(&old_committee); |
| 146 | + let key_server = dof::remove<ID, KeyServer>(&mut old_committee.id, old_committee_id); |
| 147 | + key_server.assert_committee_server_v2(); |
| 148 | + committee.propose_internal(partial_pks, *key_server.pk(), ctx); |
| 149 | + committee.try_finalize_for_rotation(old_committee, key_server); |
| 150 | +} |
| 151 | + |
| 152 | +/// Update the url of the partial key server object corresponding to the sender. |
| 153 | +public fun update_member_url(committee: &mut Committee, url: String, ctx: &mut TxContext) { |
| 154 | + assert!(committee.members.contains(&ctx.sender()), ENotMember); |
| 155 | + let committee_id = object::id(committee); |
| 156 | + let key_server = dof::borrow_mut<ID, KeyServer>(&mut committee.id, committee_id); |
| 157 | + key_server.update_member_url(url, ctx.sender()); |
| 158 | +} |
| 159 | + |
| 160 | +// TODO: handle package upgrade with threshold approvals of the committee. |
| 161 | + |
| 162 | +/// Helper function to check if a committee is finalized. |
| 163 | +public(package) fun is_finalized(committee: &Committee): bool { |
| 164 | + match (&committee.state) { |
| 165 | + State::Finalized => true, |
| 166 | + _ => false, |
| 167 | + } |
| 168 | +} |
| 169 | + |
| 170 | +// ===== Internal Functions ===== |
| 171 | + |
| 172 | +/// Internal function to initialize a shared committee object with optional old committee id. |
| 173 | +fun init_internal( |
| 174 | + threshold: u16, |
| 175 | + members: vector<address>, |
| 176 | + old_committee_id: Option<ID>, |
| 177 | + ctx: &mut TxContext, |
| 178 | +) { |
| 179 | + assert!(threshold > 0, EInvalidThreshold); |
| 180 | + assert!(members.length() as u16 < std::u16::max_value!(), EInvalidMembers); |
| 181 | + assert!(members.length() as u16 >= threshold, EInvalidThreshold); |
| 182 | + |
| 183 | + // Throws EKeyAlreadyExists if duplicate members are found. |
| 184 | + let _ = vec_set::from_keys(members); |
| 185 | + |
| 186 | + transfer::share_object(Committee { |
| 187 | + id: object::new(ctx), |
| 188 | + threshold, |
| 189 | + members, |
| 190 | + state: State::Init { members_info: vec_map::empty() }, |
| 191 | + old_committee_id, |
| 192 | + }); |
| 193 | +} |
| 194 | + |
| 195 | +/// Internal function to handle propose logic for both fresh DKG and rotation. |
| 196 | +fun propose_internal( |
| 197 | + committee: &mut Committee, |
| 198 | + partial_pks: vector<vector<u8>>, |
| 199 | + pk: vector<u8>, |
| 200 | + ctx: &TxContext, |
| 201 | +) { |
| 202 | + // TODO: add sanity check for partial pks and pk as valid elements. |
| 203 | + assert!(committee.members.contains(&ctx.sender()), ENotMember); |
| 204 | + assert!(partial_pks.length() == committee.members.length(), EInvalidProposal); |
| 205 | + |
| 206 | + match (&mut committee.state) { |
| 207 | + State::Init { members_info } => { |
| 208 | + // Check that all members have registered. |
| 209 | + assert!(members_info.length() == committee.members.length(), ENotRegistered); |
| 210 | + |
| 211 | + // Move to PostDKG state with the proposal and the caller as the first approval. |
| 212 | + committee.state = |
| 213 | + State::PostDKG { |
| 214 | + members_info: *members_info, |
| 215 | + approvals: vec_set::singleton(ctx.sender()), |
| 216 | + partial_pks, |
| 217 | + pk, |
| 218 | + }; |
| 219 | + }, |
| 220 | + State::PostDKG { |
| 221 | + approvals, |
| 222 | + members_info: _, |
| 223 | + partial_pks: existing_partial_pks, |
| 224 | + pk: existing_pk, |
| 225 | + } => { |
| 226 | + // Check that submitted partial_pks and pk are consistent. |
| 227 | + assert!(partial_pks == *existing_partial_pks, EInvalidProposal); |
| 228 | + assert!(pk == *existing_pk, EInvalidProposal); |
| 229 | + |
| 230 | + // Insert approval and make sure if approval was not inserted before. |
| 231 | + assert!(!approvals.contains(&ctx.sender()), EAlreadyProposed); |
| 232 | + approvals.insert(ctx.sender()); |
| 233 | + }, |
| 234 | + _ => abort EInvalidState, |
| 235 | + }; |
| 236 | +} |
| 237 | + |
| 238 | +/// Helper function to finalize the committee for a fresh DKG, creates a new KeyServer and TTO to |
| 239 | +/// the committee. |
| 240 | +fun try_finalize(committee: &mut Committee, ctx: &mut TxContext) { |
| 241 | + // Sanity check, only for fresh DKG committee. |
| 242 | + assert!(committee.old_committee_id.is_none(), EInvalidState); |
| 243 | + |
| 244 | + match (&committee.state) { |
| 245 | + State::PostDKG { approvals, members_info, partial_pks, pk } => { |
| 246 | + // Approvals count not reached, exit immediately. |
| 247 | + if (approvals.length() != committee.members.length()) { |
| 248 | + return |
| 249 | + }; |
| 250 | + |
| 251 | + // Build partial key servers from PostDKG state. |
| 252 | + let partial_key_servers = committee.build_partial_key_servers( |
| 253 | + members_info, |
| 254 | + partial_pks, |
| 255 | + ); |
| 256 | + // Create the KeyServerV2 object and attach it to the committee as dynamic object field. |
| 257 | + let ks = create_committee_v2( |
| 258 | + committee.id.to_address().to_string(), |
| 259 | + committee.threshold, |
| 260 | + *pk, |
| 261 | + partial_key_servers, |
| 262 | + ctx, |
| 263 | + ); |
| 264 | + let committee_id = object::id(committee); |
| 265 | + dof::add<ID, KeyServer>(&mut committee.id, committee_id, ks); |
| 266 | + committee.state = State::Finalized; |
| 267 | + }, |
| 268 | + _ => abort EInvalidState, |
| 269 | + } |
| 270 | +} |
| 271 | + |
| 272 | +/// Helper function to finalize rotation for the committee. Transfer the KeyServer from old |
| 273 | +/// committee to the new committee and destroys the old committee object. Add all new partial key |
| 274 | +/// server as df to key server. |
| 275 | +fun try_finalize_for_rotation( |
| 276 | + committee: &mut Committee, |
| 277 | + mut old_committee: Committee, |
| 278 | + mut key_server: KeyServer, |
| 279 | +) { |
| 280 | + committee.check_rotation_consistency(&old_committee); |
| 281 | + |
| 282 | + match (&committee.state) { |
| 283 | + State::PostDKG { approvals, members_info, partial_pks, .. } => { |
| 284 | + // Approvals count not reached, return key server back to old committee as dynamic object field. |
| 285 | + if (approvals.length() != committee.members.length()) { |
| 286 | + let old_committee_id = object::id(&old_committee); |
| 287 | + dof::add<ID, KeyServer>(&mut old_committee.id, old_committee_id, key_server); |
| 288 | + transfer::share_object(old_committee); |
| 289 | + return |
| 290 | + }; |
| 291 | + |
| 292 | + // Build partial key servers from PostDKG state and update in key server object. |
| 293 | + let partial_key_servers = committee.build_partial_key_servers( |
| 294 | + members_info, |
| 295 | + partial_pks, |
| 296 | + ); |
| 297 | + key_server.set_partial_key_servers(partial_key_servers); |
| 298 | + let committee_id = object::id(committee); |
| 299 | + dof::add<ID, KeyServer>(&mut committee.id, committee_id, key_server); |
| 300 | + committee.state = State::Finalized; |
| 301 | + |
| 302 | + // Destroy the old committee object. |
| 303 | + let Committee { id, .. } = old_committee; |
| 304 | + id.delete(); |
| 305 | + }, |
| 306 | + _ => abort EInvalidState, |
| 307 | + } |
| 308 | +} |
| 309 | + |
| 310 | +/// Helper function to build the partial key servers VecMap for the list of committee members. |
| 311 | +fun build_partial_key_servers( |
| 312 | + committee: &Committee, |
| 313 | + members_info: &VecMap<address, MemberInfo>, |
| 314 | + partial_pks: &vector<vector<u8>>, |
| 315 | +): VecMap<address, PartialKeyServer> { |
| 316 | + let members = committee.members; |
| 317 | + assert!(members.length() > 0, EInvalidMembers); |
| 318 | + assert!(members.length() == partial_pks.length(), EInvalidMembers); |
| 319 | + assert!(members.length() == members_info.length(), EInvalidMembers); |
| 320 | + |
| 321 | + let mut partial_key_servers = vec_map::empty(); |
| 322 | + let mut i = 0; |
| 323 | + members.do!(|member| { |
| 324 | + partial_key_servers.insert( |
| 325 | + member, |
| 326 | + create_partial_key_server( |
| 327 | + partial_pks[i], |
| 328 | + members_info.get(&member).url, |
| 329 | + i as u16, |
| 330 | + ), |
| 331 | + ); |
| 332 | + i = i + 1; |
| 333 | + }); |
| 334 | + partial_key_servers |
| 335 | +} |
| 336 | + |
| 337 | +/// Helper function to check committee and old committee state for rotation. |
| 338 | +fun check_rotation_consistency(self: &Committee, old_committee: &Committee) { |
| 339 | + assert!(self.old_committee_id.is_some(), EInvalidState); |
| 340 | + assert!(object::id(old_committee) == *self.old_committee_id.borrow(), EInvalidState); |
| 341 | + assert!(old_committee.is_finalized(), EInvalidState); |
| 342 | +} |
| 343 | + |
| 344 | +/// Test-only function to borrow the KeyServer dynamic object field. |
| 345 | +#[test_only] |
| 346 | +public(package) fun borrow_key_server(committee: &Committee): &KeyServer { |
| 347 | + dof::borrow<ID, KeyServer>(&committee.id, object::id(committee)) |
| 348 | +} |
0 commit comments