Skip to content

Commit 7a080c9

Browse files
authored
[committee] add seal_testnet and committee move code (#336)
1 parent 5109bd2 commit 7a080c9

File tree

6 files changed

+1514
-1
lines changed

6 files changed

+1514
-1
lines changed

.github/workflows/move.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
platform: ubuntu
3232
arch: x86_64
3333
extension-matching: true
34-
tag: testnet-v1.52.1
34+
tag: testnet-v1.57.2
3535
3636
- name: Run move tests
3737
run: |

move/committee/Move.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "seal_committee"
3+
edition = "2024.beta"
4+
5+
[dependencies]
6+
seal_testnet = { local = "../seal_testnet" }
7+
8+
[addresses]
9+
seal_committee = "0x0"
10+
11+
[dev-dependencies]
12+
13+
[dev-addresses]
Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
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

Comments
 (0)