Skip to content

Commit ec95e32

Browse files
committed
[key-server] handle committee mode
1 parent 0206f54 commit ec95e32

File tree

11 files changed

+523
-38
lines changed

11 files changed

+523
-38
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/dkg-cli/src/main.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,12 @@ async fn main() -> Result<()> {
182182
};
183183

184184
// Fetch current committee from onchain.
185-
let mut grpc_client = create_grpc_client(&network)?;
185+
let network_enum = match network.to_lowercase().as_str() {
186+
"mainnet" => seal_committee::Network::Mainnet,
187+
"testnet" => seal_committee::Network::Testnet,
188+
_ => anyhow::bail!("Invalid network: {}. Use 'mainnet' or 'testnet'", network),
189+
};
190+
let mut grpc_client = create_grpc_client(network_enum)?;
186191
let committee = fetch_committee_data(&committee_id, &mut grpc_client).await?;
187192

188193
// Validate committee state is in Init state and contains my address.

crates/key-server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ hyper-util = "0.1.10"
5050
http-body-util = "0.1.2"
5151
futures = "0.3"
5252
seal-sdk = { path = "../seal-sdk" }
53+
seal-committee = { path = "../seal-committee" }
5354

5455
# sui grpc dep
5556
sui-rpc = { workspace = true }

crates/key-server/key-server-config.yaml

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
# network: !Custom
88
# node_url: 'url_to_node_endpoint' # Optional - see below
99
# use_default_mainnet_for_mvr: true # Optional, default to true if not present,
10-
set this to false for mvr to resolve on testnet.
10+
# set this to false for mvr to resolve on testnet.
1111
#
1212
# Use Custom for production deployments. You must provide the node URL
1313
# in exactly one of these ways:
@@ -19,20 +19,24 @@ set this to false for mvr to resolve on testnet.
1919
network: Testnet
2020

2121

22-
# Server mode. The server can either work in Open/permissionless mode that
23-
# supports all packages, or in Permissioned mode that only supports
24-
# whitelisted packages .
22+
# Server mode: Open, Permissioned or Committee.
23+
# Open: Supports all policy packages.
24+
# Permissioned: Supports only allowed policy packages.
25+
# Committee: Supports all packages. Contains committee ID and next
26+
# committee ID. Note that the MASTER_KEY for Committee mode is the
27+
# output of a DKG ceremony that you participated in.
2528
#
26-
# For the Open mode, set the registered key server object ID to be used, e.g.,:
29+
#### OPEN MODE ####
30+
# Set the registered key server object ID to be used, e.g.,:
2731
# server_mode: !Open
2832
# key_server_object_id: '0x0000000000000000000000000000000000000000000000000000000000000000'
2933
#
3034
# Example:
31-
server_mode: !Open
32-
key_server_object_id: '0x0000000000000000000000000000000000000000000000000000000000000000'
35+
# server_mode: !Open
36+
# key_server_object_id: '0x0000000000000000000000000000000000000000000000000000000000000000'
3337

34-
#
35-
# For the Permissioned mode, set the allowed clients and their configurations.
38+
#### PERMISSIONED MODE ####
39+
# Set the allowed clients and their configurations.
3640
# A client config contains:
3741
# - Client name - for debugging purposes only, can always be modified.
3842
# - The associated key server object id. Should never change.
@@ -75,6 +79,24 @@ server_mode: !Open
7579
# package_ids:
7680
# - "0x3333333333333333333333333333333333333333333333333333333333333333"
7781

82+
#### COMMITTEE MODE ####
83+
# The config contains the committee ID and member address, and the server expects a MASTER_KEY provided
84+
# as an environment variable. The server validates the committee is finalized and serve request with
85+
# MASTER_KEY. During key rotation, the config can also contain the next committee ID and the server
86+
# expects NEXT_MASTER_KEY. The server fetches the onchain state periodically:
87+
# - If the committee still exists and the next committee is in Init or PostDKG state, still serve
88+
# requests with MASTER_KEY.
89+
# - If the committee is deleted and the next committee is finalized, serve requests with
90+
# NEXT_MASTER_KEY.
91+
# Example:
92+
# server_mode: !Committee
93+
# member_address: '0x0000000000000000000000000000000000000000000000000000000000000000'
94+
# committee_id: '0x0000000000000000000000000000000000000000000000000000000000000001'
95+
# next_committee_id: '0x0000000000000000000000000000000000000000000000000000000000000002'
96+
#
97+
server_mode: !Committee
98+
member_address: '0x223762117ab21a439f0f3f3b0577e838b8b26a37d9a1723a4be311243f4461b9'
99+
committee_id: '0xd80885d60e2d86be6ab936afd1c717dc0d459406d4fda0cee583dee429947784'
78100

79101
#### Optional advanced configurations ####
80102
#

crates/key-server/src/key_server_options.rs

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use duration_str::deserialize_duration;
99
use semver::VersionReq;
1010
use serde::{Deserialize, Serialize};
1111
use std::time::Duration;
12+
use sui_sdk_types::Address;
1213
use sui_types::base_types::ObjectID;
1314
use tracing::info;
1415

@@ -47,8 +48,12 @@ pub enum ServerMode {
4748
// Master key is expected to by 32 byte HKDF seed
4849
client_configs: Vec<ClientConfig>,
4950
},
51+
Committee {
52+
member_address: Address,
53+
committee_id: ObjectID,
54+
next_committee_id: Option<ObjectID>,
55+
},
5056
}
51-
5257
/// Configuration for the RPC client.
5358
#[derive(Debug, Clone, Serialize, Deserialize)]
5459
pub struct RpcConfig {
@@ -197,6 +202,30 @@ impl KeyServerOptions {
197202
serde_yaml::to_string(self).expect("should serialize")
198203
);
199204

205+
// Validate Committee mode specific requirements
206+
if let ServerMode::Committee {
207+
next_committee_id, ..
208+
} = &self.server_mode
209+
{
210+
// MASTER_KEY must always be present in Committee mode
211+
if std::env::var("MASTER_KEY").is_err() {
212+
return Err(anyhow!(
213+
"In Committee mode, MASTER_KEY environment variable must be set"
214+
));
215+
}
216+
217+
let next_master_key_exists = std::env::var("NEXT_MASTER_KEY").is_ok();
218+
219+
// Both NEXT_MASTER_KEY and next_committee_id must be provided together or both absent
220+
if next_master_key_exists != next_committee_id.is_some() {
221+
return Err(anyhow!(
222+
"In Committee mode, NEXT_MASTER_KEY environment variable and next_committee_id must both be provided or both be absent. Found: NEXT_MASTER_KEY={}, next_committee_id={}",
223+
next_master_key_exists,
224+
next_committee_id.is_some()
225+
));
226+
}
227+
}
228+
200229
if let ServerMode::Permissioned { client_configs } = &self.server_mode {
201230
let mut names = std::collections::HashSet::new();
202231
let mut derivation_indices = std::collections::HashSet::new();
@@ -256,13 +285,18 @@ impl KeyServerOptions {
256285
Ok(())
257286
}
258287

259-
pub(crate) fn get_supported_key_server_object_ids(&self) -> Vec<ObjectID> {
288+
#[cfg(test)]
289+
pub(crate) fn get_key_server_object_ids_for_testing(&self) -> Vec<ObjectID> {
260290
match &self.server_mode {
261291
ServerMode::Open {
262292
key_server_object_id,
263293
} => {
264294
vec![*key_server_object_id]
265295
}
296+
ServerMode::Committee { .. } => {
297+
// Committee mode fetches key_server_id from chain
298+
vec![]
299+
}
266300
ServerMode::Permissioned { client_configs } => client_configs
267301
.iter()
268302
.filter(|c| {
@@ -354,6 +388,43 @@ server_mode: !Open
354388

355389
let unknown_option = "a_complete_unknown: 'a rolling stone'\n";
356390
assert!(serde_yaml::from_str::<KeyServerOptions>(unknown_option).is_err());
391+
392+
// test committee mode
393+
let valid_configuration_committee = r#"
394+
network: Mainnet
395+
server_mode: !Committee
396+
member_address: '0x0000000000000000000000000000000000000000000000000000000000000000'
397+
committee_id: '0x1'
398+
next_committee_id: '0x2'
399+
"#;
400+
let options: KeyServerOptions = serde_yaml::from_str(valid_configuration_committee)
401+
.expect("Failed to parse valid configuration");
402+
assert_eq!(
403+
options.server_mode,
404+
ServerMode::Committee {
405+
member_address: Address::ZERO,
406+
committee_id: ObjectID::from_str("0x1").unwrap(),
407+
next_committee_id: Some(ObjectID::from_str("0x2").unwrap()),
408+
}
409+
);
410+
411+
// test committee mode without next_committee_id
412+
let valid_configuration_committee_no_next = r#"
413+
network: Mainnet
414+
server_mode: !Committee
415+
member_address: '0x0000000000000000000000000000000000000000000000000000000000000000'
416+
committee_id: '0x1'
417+
"#;
418+
let options: KeyServerOptions = serde_yaml::from_str(valid_configuration_committee_no_next)
419+
.expect("Failed to parse valid configuration");
420+
assert_eq!(
421+
options.server_mode,
422+
ServerMode::Committee {
423+
member_address: Address::ZERO,
424+
committee_id: ObjectID::from_str("0x1").unwrap(),
425+
next_committee_id: None,
426+
}
427+
);
357428
}
358429

359430
#[test]
@@ -416,8 +487,9 @@ session_key_ttl_max: '60s'
416487
let options: KeyServerOptions =
417488
serde_yaml::from_str(valid_configuration).expect("Failed to parse valid configuration");
418489

490+
// Verify that the HashMap would contain keys for alice and bob (not carol since Exported)
419491
assert_eq!(
420-
options.get_supported_key_server_object_ids(),
492+
options.get_key_server_object_ids_for_testing(),
421493
vec![
422494
ObjectID::from_str(
423495
"0xaaaa000000000000000000000000000000000000000000000000000000000001"

crates/key-server/src/master_keys.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const MASTER_KEY_ENV_VAR: &str = "MASTER_KEY";
2020
/// Represents the set of master keys held by a key server.
2121
#[derive(Clone)]
2222
pub enum MasterKeys {
23-
/// In open mode, the key server has a single master key used for all packages.
23+
/// In open or committee mode, the key server has a single master key used for all packages.
2424
Open { master_key: IbeMasterKey },
2525
/// In permissioned mode, the key server has a mapping of package IDs to master keys.
2626
Permissioned {
@@ -33,7 +33,7 @@ impl MasterKeys {
3333
pub(crate) fn load(options: &KeyServerOptions) -> anyhow::Result<Self> {
3434
info!("Loading keys from env variables");
3535
match &options.server_mode {
36-
ServerMode::Open { .. } => {
36+
ServerMode::Open { .. } | ServerMode::Committee { .. } => {
3737
let master_key = match decode_master_key::<DefaultEncoding>(MASTER_KEY_ENV_VAR) {
3838
Ok(master_key) => master_key,
3939

crates/key-server/src/server.rs

Lines changed: 110 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::errors::InternalError::{
44
DeprecatedSDKVersion, InvalidSDKVersion, MissingRequiredHeader,
55
};
66
use crate::externals::get_reference_gas_price;
7-
use crate::key_server_options::ServerMode;
7+
use crate::key_server_options::{ClientKeyType, ServerMode};
88
use crate::metrics::{call_with_duration, observation_callback, status_callback, Metrics};
99
use crate::metrics_push::create_push_client;
1010
use crate::mvr::mvr_forward_resolution;
@@ -155,17 +155,7 @@ impl Server {
155155
panic!("Failed to load master keys: {}", e);
156156
});
157157

158-
let key_server_oid_to_pop = options
159-
.get_supported_key_server_object_ids()
160-
.into_iter()
161-
.map(|ks_oid| {
162-
let key = master_keys
163-
.get_key_for_key_server(&ks_oid)
164-
.expect("checked already");
165-
let pop = create_proof_of_possession(key, &ks_oid.into_bytes());
166-
(ks_oid, pop)
167-
})
168-
.collect();
158+
let key_server_oid_to_pop = Self::build_key_server_pop_map(&options, &master_keys).await;
169159

170160
Server {
171161
sui_rpc_client,
@@ -175,6 +165,114 @@ impl Server {
175165
}
176166
}
177167

168+
/// Build the key_server_oid -> PoP HashMap for all server modes.
169+
/// Handles fetching from chain for Committee mode.
170+
async fn build_key_server_pop_map(
171+
options: &KeyServerOptions,
172+
master_keys: &MasterKeys,
173+
) -> HashMap<ObjectID, MasterKeyPOP> {
174+
match &options.server_mode {
175+
ServerMode::Open {
176+
key_server_object_id,
177+
} => {
178+
let key = master_keys
179+
.get_key_for_key_server(key_server_object_id)
180+
.expect("checked already");
181+
let pop = create_proof_of_possession(key, &key_server_object_id.into_bytes());
182+
let mut map = HashMap::new();
183+
map.insert(*key_server_object_id, pop);
184+
map
185+
}
186+
ServerMode::Committee {
187+
member_address,
188+
committee_id,
189+
next_committee_id: _,
190+
} => {
191+
info!("Committee mode - validating committee at startup");
192+
193+
// Create gRPC client for seal-committee helpers
194+
let network = match options.network {
195+
Network::Mainnet => seal_committee::Network::Mainnet,
196+
Network::Testnet => seal_committee::Network::Testnet,
197+
_ => panic!("Unsupported network for Committee mode"),
198+
};
199+
let mut grpc_client = seal_committee::create_grpc_client(network)
200+
.expect("Failed to create gRPC client");
201+
202+
// Fetch and verify committee is finalized
203+
let committee_addr = sui_sdk_types::Address::from_bytes(committee_id.as_ref())
204+
.unwrap_or_else(|e| panic!("Invalid committee ID: {:?}", e));
205+
if !seal_committee::check_committee_finalized(&mut grpc_client, &committee_addr)
206+
.await
207+
.unwrap_or_else(|e| panic!("Failed to check committee finalized: {:?}", e))
208+
{
209+
panic!("Committee {} is not finalized", committee_id);
210+
}
211+
212+
// Fetch KeyServer from committee's dynamic field
213+
let key_server_id =
214+
seal_committee::fetch_key_server_id(&mut grpc_client, &committee_addr)
215+
.await
216+
.unwrap_or_else(|e| {
217+
panic!("Failed to fetch KeyServer from committee: {:?}", e)
218+
});
219+
220+
let key_server_oid = ObjectID::new(key_server_id.into_inner());
221+
222+
// Fetch PartialKeyServer info for this member
223+
let partial_key_server_info = seal_committee::fetch_partial_key_server_info(
224+
&mut grpc_client,
225+
&key_server_id,
226+
*member_address,
227+
)
228+
.await
229+
.unwrap_or_else(|e| panic!("Failed to fetch PartialKeyServer: {:?}", e))
230+
.unwrap_or_else(|| {
231+
panic!("No PartialKeyServer found for member {}", member_address)
232+
});
233+
234+
// Sign PoP over: key_server_oid || party_id || partial_pk
235+
let mut pop_message = Vec::new();
236+
pop_message.extend_from_slice(&key_server_oid.into_bytes());
237+
pop_message.extend_from_slice(&partial_key_server_info.party_id.to_le_bytes());
238+
pop_message.extend_from_slice(&partial_key_server_info.partial_pk);
239+
240+
// Extract master_key from already-loaded MasterKeys
241+
let master_key = match master_keys {
242+
MasterKeys::Open { master_key } => master_key,
243+
_ => panic!("Committee mode requires Open master key configuration"),
244+
};
245+
let pop = create_proof_of_possession(master_key, &pop_message);
246+
247+
info!(
248+
"Committee mode: KeyServer {} with party_id={} for POP with MASTER_KEY",
249+
key_server_oid, partial_key_server_info.party_id
250+
);
251+
252+
// Use KeyServer object ID as the key since PartialKeyServer is stored inline
253+
let mut map = HashMap::new();
254+
map.insert(key_server_oid, pop);
255+
map
256+
}
257+
ServerMode::Permissioned { client_configs } => client_configs
258+
.iter()
259+
.filter(|c| {
260+
matches!(
261+
c.client_master_key,
262+
ClientKeyType::Derived { .. } | ClientKeyType::Imported { .. }
263+
)
264+
})
265+
.map(|c| {
266+
let key = master_keys
267+
.get_key_for_key_server(&c.key_server_object_id)
268+
.expect("checked already");
269+
let pop = create_proof_of_possession(key, &c.key_server_object_id.into_bytes());
270+
(c.key_server_object_id, pop)
271+
})
272+
.collect(),
273+
}
274+
}
275+
178276
#[allow(clippy::too_many_arguments)]
179277
async fn check_signature(
180278
&self,

0 commit comments

Comments
 (0)