diff --git a/crates/e2e-tests/src/e2e_flow.rs b/crates/e2e-tests/src/e2e_flow.rs index a777fbb6b..0b010633b 100644 --- a/crates/e2e-tests/src/e2e_flow.rs +++ b/crates/e2e-tests/src/e2e_flow.rs @@ -1345,6 +1345,131 @@ mod tests { Ok(()) } + #[tokio::test(flavor = "multi_thread")] + async fn test_varying_t_and_allowed_delta_across_epochs() -> Result<()> { + init_test_logging(); + + let mut networks = TestNetworksBuilder::new().with_nodes(4).build().await?; + + use hashi::onchain::types::DEFAULT_MPC_THRESHOLD_BASIS_POINTS; + use hashi::onchain::types::DEFAULT_MPC_WEIGHT_REDUCTION_ALLOWED_DELTA; + + // Wait for DKG (epoch 1 committee created with defaults). + let nodes = networks.hashi_network.nodes(); + let futs: Vec<_> = nodes + .iter() + .map(|n| n.wait_for_mpc_key(Duration::from_secs(120))) + .collect(); + for (i, r) in futures::future::join_all(futs) + .await + .into_iter() + .enumerate() + { + r.unwrap_or_else(|e| panic!("Node {i} DKG failed: {e}")); + } + + let initial_epoch = nodes[0].current_epoch().unwrap(); + let pk_before = nodes[0].hashi().mpc_handle().unwrap().public_key().unwrap(); + + // Verify epoch 1 committee has defaults. + let epoch1_committee = nodes[0] + .hashi() + .onchain_state() + .current_committee() + .unwrap(); + assert_eq!( + epoch1_committee.mpc_threshold_basis_points(), + DEFAULT_MPC_THRESHOLD_BASIS_POINTS + ); + assert_eq!( + epoch1_committee.mpc_weight_reduction_allowed_delta(), + DEFAULT_MPC_WEIGHT_REDUCTION_ALLOWED_DELTA + ); + + // Change config between epochs. + let new_threshold: u64 = 5000; + let new_delta: u64 = 1200; + crate::apply_onchain_config_overrides( + &mut networks, + &[ + ( + "mpc_threshold_basis_points".into(), + hashi_types::move_types::ConfigValue::U64(new_threshold), + ), + ( + "mpc_weight_reduction_allowed_delta".into(), + hashi_types::move_types::ConfigValue::U64(new_delta), + ), + ], + ) + .await?; + + // Force key rotation → epoch 2 committee created with new config. + let target_epoch = initial_epoch + 1; + networks.sui_network.force_close_epoch().await?; + let futs: Vec<_> = networks + .hashi_network() + .nodes() + .iter() + .map(|n| n.wait_for_epoch(target_epoch, Duration::from_secs(480))) + .collect(); + for (i, r) in futures::future::join_all(futs) + .await + .into_iter() + .enumerate() + { + r.unwrap_or_else(|e| panic!("Node {i} failed to reach epoch {target_epoch}: {e}")); + } + + // Verify key rotation succeeded: all nodes agree and key is preserved. + let nodes = networks.hashi_network().nodes(); + let pk_after = nodes[0].hashi().mpc_handle().unwrap().public_key().unwrap(); + assert_eq!( + pk_before, pk_after, + "MPC public key changed during rotation" + ); + for (i, node) in nodes.iter().enumerate().skip(1) { + let pk = node.hashi().mpc_handle().unwrap().public_key().unwrap(); + assert_eq!( + pk, pk_after, + "Node {i} MPC key differs from node 0 after rotation" + ); + } + + // Epoch 1 committee retains original defaults. + let state = networks.hashi_network.nodes()[0].hashi().onchain_state(); + let committees = { + let s = state.state(); + s.hashi().committees.committees().clone() + }; + let epoch1 = committees.get(&initial_epoch).expect("epoch 1 committee"); + assert_eq!( + epoch1.mpc_threshold_basis_points(), + DEFAULT_MPC_THRESHOLD_BASIS_POINTS, + "epoch {initial_epoch} committee should retain original threshold_basis_points" + ); + assert_eq!( + epoch1.mpc_weight_reduction_allowed_delta(), + DEFAULT_MPC_WEIGHT_REDUCTION_ALLOWED_DELTA, + "epoch {initial_epoch} committee should retain original allowed_delta" + ); + + // Epoch 2 committee has new values. + let epoch2 = committees.get(&target_epoch).expect("epoch 2 committee"); + assert_eq!( + epoch2.mpc_threshold_basis_points(), + new_threshold as u16, + "epoch {target_epoch} committee should have updated threshold_basis_points" + ); + assert_eq!( + epoch2.mpc_weight_reduction_allowed_delta(), + new_delta as u16, + "epoch {target_epoch} committee should have updated allowed_delta" + ); + + Ok(()) + } + /// Verify that a withdrawal can spend a change output whose producing /// transaction is mined on Bitcoin but not yet confirmed on Sui. The /// actual Bitcoin confirmation count must be queried from the node diff --git a/crates/e2e-tests/src/lib.rs b/crates/e2e-tests/src/lib.rs index 71b5fdd58..ad45bdec9 100644 --- a/crates/e2e-tests/src/lib.rs +++ b/crates/e2e-tests/src/lib.rs @@ -265,7 +265,7 @@ impl TestNetworksBuilder { /// Waits for DKG to complete first so the committee is ready to vote. /// All nodes vote on every proposal, ensuring quorum is always reached /// regardless of the number of nodes or their weight distribution. -async fn apply_onchain_config_overrides( +pub(crate) async fn apply_onchain_config_overrides( networks: &mut TestNetworks, overrides: &[(String, hashi_types::move_types::ConfigValue)], ) -> Result<()> { diff --git a/crates/hashi-types/proto/sui/hashi/v1alpha/signature.proto b/crates/hashi-types/proto/sui/hashi/v1alpha/signature.proto index dbab89d4f..35b7bb9f6 100644 --- a/crates/hashi-types/proto/sui/hashi/v1alpha/signature.proto +++ b/crates/hashi-types/proto/sui/hashi/v1alpha/signature.proto @@ -6,6 +6,8 @@ message Committee { optional uint64 epoch = 1; repeated CommitteeMember members = 2; optional uint64 total_weight = 3; + optional uint64 mpc_threshold_basis_points = 4; + optional uint64 mpc_weight_reduction_allowed_delta = 5; } message CommitteeMember { diff --git a/crates/hashi-types/src/committee.rs b/crates/hashi-types/src/committee.rs index ee1f6c743..b38625560 100644 --- a/crates/hashi-types/src/committee.rs +++ b/crates/hashi-types/src/committee.rs @@ -21,6 +21,14 @@ use serde::Serialize; use sui_crypto::SignatureError; use sui_sdk_types::Address; +/// Default MPC threshold in basis points. Mirrors `DEFAULT_THRESHOLD_BASIS_POINTS` in +/// `mpc_config.move`. +pub const DEFAULT_MPC_THRESHOLD_BASIS_POINTS: u16 = 3333; + +/// Default allowed delta for weight reduction. Mirrors `DEFAULT_WEIGHT_REDUCTION_ALLOWED_DELTA` in +/// `mpc_config.move`. +pub const DEFAULT_MPC_WEIGHT_REDUCTION_ALLOWED_DELTA: u16 = 800; + // TODO: Read threshold from on-chain config once it is made configurable. const THRESHOLD_NUMERATOR: u64 = 2; const THRESHOLD_DENOMINATOR: u64 = 3; @@ -85,6 +93,8 @@ pub struct Committee { members: Vec, address_to_index: HashMap, total_weight: u64, + mpc_threshold_basis_points: u16, + mpc_weight_reduction_allowed_delta: u16, } #[derive(Clone, PartialEq)] @@ -147,7 +157,12 @@ impl MemberSignature { } impl Committee { - pub fn new(members: Vec, epoch: u64) -> Self { + pub fn new( + members: Vec, + epoch: u64, + mpc_threshold_basis_points: u16, + mpc_weight_reduction_allowed_delta: u16, + ) -> Self { let total_weight = members.iter().map(|member| member.weight).sum(); let address_to_index = members .iter() @@ -159,6 +174,8 @@ impl Committee { members, address_to_index, total_weight, + mpc_threshold_basis_points, + mpc_weight_reduction_allowed_delta, } } @@ -175,6 +192,14 @@ impl Committee { self.total_weight } + pub fn mpc_threshold_basis_points(&self) -> u16 { + self.mpc_threshold_basis_points + } + + pub fn mpc_weight_reduction_allowed_delta(&self) -> u16 { + self.mpc_weight_reduction_allowed_delta + } + fn member(&self, address: &Address) -> Result<&CommitteeMember, SignatureError> { let index = self .address_to_index @@ -662,6 +687,9 @@ mod test { use fastcrypto::groups::FiatShamirChallenge; use fastcrypto::groups::bls12381::Scalar; use fastcrypto::serde_helpers::ToFromByteArray; + + const TEST_THRESHOLD_BASIS_POINTS: u16 = 3333; + const TEST_ALLOWED_DELTA: u16 = 0; use test_strategy::proptest; impl proptest::arbitrary::Arbitrary for Bls12381PrivateKey { @@ -719,7 +747,12 @@ mod test { weight: 1, }) .collect(); - let committee = Committee::new(members, epoch); + let committee = Committee::new( + members, + epoch, + TEST_THRESHOLD_BASIS_POINTS, + TEST_ALLOWED_DELTA, + ); let mut aggregator = BlsSignatureAggregator::new(&committee, message.clone()); @@ -816,7 +849,12 @@ mod test { weight: 1, }) .collect(); - let committee = Committee::new(members, epoch); + let committee = Committee::new( + members, + epoch, + TEST_THRESHOLD_BASIS_POINTS, + TEST_ALLOWED_DELTA, + ); let mut aggregator = BlsSignatureAggregator::new(&committee, message.clone()); @@ -858,6 +896,8 @@ mod test { }) .collect(), 999, // Different epoch + TEST_THRESHOLD_BASIS_POINTS, + TEST_ALLOWED_DELTA, ); assert!( certificate @@ -887,7 +927,12 @@ mod test { weight: 2500, // committee weight }) .collect(); - let committee = Committee::new(members, epoch); + let committee = Committee::new( + members, + epoch, + TEST_THRESHOLD_BASIS_POINTS, + TEST_ALLOWED_DELTA, + ); // Reduced weights: different from committee weights let reduced_weights: HashMap = @@ -970,7 +1015,12 @@ mod test { weight: 1, }) .collect(); - let committee = Committee::new(members, epoch); + let committee = Committee::new( + members, + epoch, + TEST_THRESHOLD_BASIS_POINTS, + TEST_ALLOWED_DELTA, + ); // Create a certificate via aggregator let mut aggregator = BlsSignatureAggregator::new(&committee, message.clone()); diff --git a/crates/hashi-types/src/guardian/proto_conversions.rs b/crates/hashi-types/src/guardian/proto_conversions.rs index 9d7795d9b..476f56ab5 100644 --- a/crates/hashi-types/src/guardian/proto_conversions.rs +++ b/crates/hashi-types/src/guardian/proto_conversions.rs @@ -54,6 +54,9 @@ use hpke::Serializable; use std::num::NonZeroU16; use std::str::FromStr; +use crate::committee::DEFAULT_MPC_THRESHOLD_BASIS_POINTS; +use crate::committee::DEFAULT_MPC_WEIGHT_REDUCTION_ALLOWED_DELTA; + // -------------------------------------------- // Proto -> Domain (deserialization) // -------------------------------------------- @@ -666,7 +669,20 @@ fn pb_to_hashi_committee(c: pb::Committee) -> GuardianResult { let total_weight = c.total_weight.ok_or_else(|| missing("total_weight"))?; - let committee = HashiCommittee::new(members, epoch); + let threshold_basis_points = c + .mpc_threshold_basis_points + .map(|v| v as u16) + .unwrap_or(DEFAULT_MPC_THRESHOLD_BASIS_POINTS); + let weight_reduction_allowed_delta = c + .mpc_weight_reduction_allowed_delta + .map(|v| v as u16) + .unwrap_or(DEFAULT_MPC_WEIGHT_REDUCTION_ALLOWED_DELTA); + let committee = HashiCommittee::new( + members, + epoch, + threshold_basis_points, + weight_reduction_allowed_delta, + ); if committee.total_weight() != total_weight { return Err(InvalidInputs(format!( @@ -687,6 +703,8 @@ fn hashi_committee_to_pb(c: HashiCommittee) -> pb::Committee { .map(|m| hashi_committee_member_to_pb(m.clone())) .collect(), total_weight: Some(c.total_weight()), + mpc_threshold_basis_points: Some(c.mpc_threshold_basis_points() as u64), + mpc_weight_reduction_allowed_delta: Some(c.mpc_weight_reduction_allowed_delta() as u64), } } diff --git a/crates/hashi-types/src/guardian/test_utils.rs b/crates/hashi-types/src/guardian/test_utils.rs index 0d2b2e482..72fe2555e 100644 --- a/crates/hashi-types/src/guardian/test_utils.rs +++ b/crates/hashi-types/src/guardian/test_utils.rs @@ -46,6 +46,10 @@ use hpke::Deserializable; use std::num::NonZeroU16; use sui_sdk_types::Address as SuiAddress; use sui_sdk_types::bcs::FromBcs; + +use crate::committee::DEFAULT_MPC_THRESHOLD_BASIS_POINTS; +use crate::committee::DEFAULT_MPC_WEIGHT_REDUCTION_ALLOWED_DELTA; + // ------------------------------- // Shared deterministic test values // ------------------------------- @@ -185,7 +189,12 @@ fn mock_committee_member() -> HashiCommitteeMember { } fn mock_committee_with_one_member(epoch: u64) -> HashiCommittee { - HashiCommittee::new(vec![mock_committee_member()], epoch) + HashiCommittee::new( + vec![mock_committee_member()], + epoch, + DEFAULT_MPC_THRESHOLD_BASIS_POINTS, + DEFAULT_MPC_WEIGHT_REDUCTION_ALLOWED_DELTA, + ) } impl ProvisionerInitState { diff --git a/crates/hashi-types/src/move_types/mod.rs b/crates/hashi-types/src/move_types/mod.rs index 85e320783..3947fdbee 100644 --- a/crates/hashi-types/src/move_types/mod.rs +++ b/crates/hashi-types/src/move_types/mod.rs @@ -156,6 +156,8 @@ pub struct Committee { pub members: Vec, /// Total voting weight of the committee. pub total_weight: u64, + pub mpc_threshold_basis_points: u64, + pub mpc_weight_reduction_allowed_delta: u64, } /// Rust version of the Move hashi::config::Config type. @@ -1101,6 +1103,8 @@ impl From<&crate::committee::Committee> for Committee { epoch: c.epoch(), members: c.members().iter().map(Into::into).collect(), total_weight: c.total_weight(), + mpc_threshold_basis_points: c.mpc_threshold_basis_points() as u64, + mpc_weight_reduction_allowed_delta: c.mpc_weight_reduction_allowed_delta() as u64, } } } @@ -1114,6 +1118,13 @@ impl TryFrom for crate::committee::Committee { .into_iter() .map(crate::committee::CommitteeMember::try_from) .collect::, _>>()?; - Ok(crate::committee::Committee::new(members, c.epoch)) + Ok(crate::committee::Committee::new( + members, + c.epoch, + u16::try_from(c.mpc_threshold_basis_points) + .expect("mpc_threshold_basis_points exceeds u16::MAX"), + u16::try_from(c.mpc_weight_reduction_allowed_delta) + .expect("mpc_weight_reduction_allowed_delta exceeds u16::MAX"), + )) } } diff --git a/crates/hashi-types/src/proto/generated/sui.hashi.v1alpha.fds.bin b/crates/hashi-types/src/proto/generated/sui.hashi.v1alpha.fds.bin index 5d90d92a4..198904930 100644 Binary files a/crates/hashi-types/src/proto/generated/sui.hashi.v1alpha.fds.bin and b/crates/hashi-types/src/proto/generated/sui.hashi.v1alpha.fds.bin differ diff --git a/crates/hashi-types/src/proto/generated/sui.hashi.v1alpha.rs b/crates/hashi-types/src/proto/generated/sui.hashi.v1alpha.rs index 274410159..11979504f 100644 --- a/crates/hashi-types/src/proto/generated/sui.hashi.v1alpha.rs +++ b/crates/hashi-types/src/proto/generated/sui.hashi.v1alpha.rs @@ -2822,6 +2822,10 @@ pub struct Committee { pub members: ::prost::alloc::vec::Vec, #[prost(uint64, optional, tag = "3")] pub total_weight: ::core::option::Option, + #[prost(uint64, optional, tag = "4")] + pub mpc_threshold_basis_points: ::core::option::Option, + #[prost(uint64, optional, tag = "5")] + pub mpc_weight_reduction_allowed_delta: ::core::option::Option, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct CommitteeMember { diff --git a/crates/hashi/src/lib.rs b/crates/hashi/src/lib.rs index 522e25db2..0f17b3000 100644 --- a/crates/hashi/src/lib.rs +++ b/crates/hashi/src/lib.rs @@ -219,8 +219,6 @@ impl Hashi { let state = self.onchain_state().state(); let hashi = state.hashi(); let committee_set = &hashi.committees; - let threshold_basis_points = hashi.config.mpc_threshold_basis_points(); - let weight_reduction_allowed_delta = hashi.config.mpc_weight_reduction_allowed_delta(); let session_id = mpc::SessionId::new(self.config.sui_chain_id(), epoch, &protocol_type); let encryption_key = self.config.encryption_private_key()?; self.db @@ -261,8 +259,6 @@ impl Hashi { encryption_key, signing_key, store, - threshold_basis_points, - weight_reduction_allowed_delta, chain_id, self.config.test_weight_divisor, batch_size_per_weight, diff --git a/crates/hashi/src/mpc/mpc_except_signing.rs b/crates/hashi/src/mpc/mpc_except_signing.rs index 391934a86..0bf019197 100644 --- a/crates/hashi/src/mpc/mpc_except_signing.rs +++ b/crates/hashi/src/mpc/mpc_except_signing.rs @@ -124,8 +124,6 @@ impl MpcManager { encryption_key: PrivateKey, signing_key: Bls12381PrivateKey, public_message_store: Box, - threshold_basis_points: u16, - weight_reduction_allowed_delta: u16, chain_id: &str, weight_divisor: Option, batch_size_per_weight: u16, @@ -148,8 +146,8 @@ impl MpcManager { .clone(); let (nodes, threshold) = build_reduced_nodes( &committee, - threshold_basis_points, - weight_reduction_allowed_delta, + committee.mpc_threshold_basis_points(), + committee.mpc_weight_reduction_allowed_delta(), weight_divisor, )?; let total_weight = nodes.total_weight(); @@ -207,8 +205,8 @@ impl MpcManager { Some(prev_committee) => { let (nodes, threshold) = build_reduced_nodes( prev_committee, - threshold_basis_points, - weight_reduction_allowed_delta, + prev_committee.mpc_threshold_basis_points(), + prev_committee.mpc_weight_reduction_allowed_delta(), weight_divisor, )?; (Some(nodes), Some(threshold)) diff --git a/crates/hashi/src/mpc/mpc_except_signing_tests.rs b/crates/hashi/src/mpc/mpc_except_signing_tests.rs index b62a86ce3..fa4f81d51 100644 --- a/crates/hashi/src/mpc/mpc_except_signing_tests.rs +++ b/crates/hashi/src/mpc/mpc_except_signing_tests.rs @@ -176,9 +176,19 @@ impl TestSetup { ) }) .collect(); - let committee = Committee::new(members.clone(), epoch); + let committee = Committee::new( + members.clone(), + epoch, + TEST_THRESHOLD_BASIS_POINTS, + TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, + ); // Also create a previous committee for key rotation tests - let previous_committee = Committee::new(members, epoch - 1); + let previous_committee = Committee::new( + members, + epoch - 1, + TEST_THRESHOLD_BASIS_POINTS, + TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, + ); let mut committees = BTreeMap::new(); committees.insert(epoch - 1, previous_committee); @@ -242,9 +252,19 @@ impl TestSetup { ) }) .collect(); - let committee = Committee::new(members.clone(), epoch); + let committee = Committee::new( + members.clone(), + epoch, + TEST_THRESHOLD_BASIS_POINTS, + TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, + ); // Also create a previous committee for key rotation tests - let previous_committee = Committee::new(members, epoch - 1); + let previous_committee = Committee::new( + members, + epoch - 1, + TEST_THRESHOLD_BASIS_POINTS, + TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, + ); let mut committees = BTreeMap::new(); committees.insert(epoch - 1, previous_committee); @@ -285,8 +305,6 @@ impl TestSetup { self.encryption_keys[validator_index].clone(), self.signing_keys[validator_index].clone(), store, - TEST_THRESHOLD_BASIS_POINTS, - TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, TEST_CHAIN_ID, None, TEST_BATCH_SIZE_PER_WEIGHT, @@ -899,8 +917,6 @@ fn test_mpc_manager_new_from_committee_set() { encryption_key, signing_key, Box::new(MockPublicMessagesStore), - TEST_THRESHOLD_BASIS_POINTS, - TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, TEST_CHAIN_ID, None, TEST_BATCH_SIZE_PER_WEIGHT, @@ -963,8 +979,6 @@ fn test_mpc_manager_new_fails_if_no_committee_for_epoch() { encryption_keys[0].clone(), signing_keys[0].clone(), Box::new(MockPublicMessagesStore), - TEST_THRESHOLD_BASIS_POINTS, - TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, "test", None, TEST_BATCH_SIZE_PER_WEIGHT, @@ -5818,7 +5832,12 @@ async fn test_prepare_previous_output_for_new_member() { 2, ))) .collect(); - let new_current_committee = Committee::new(current_members, epoch); + let new_current_committee = Committee::new( + current_members, + epoch, + TEST_THRESHOLD_BASIS_POINTS, + TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, + ); let previous_committee = rotation_setup .setup .committee_set @@ -5845,8 +5864,6 @@ async fn test_prepare_previous_output_for_new_member() { new_member_encryption_key, new_member_signing_key, Box::new(InMemoryPublicMessagesStore::new()), - TEST_THRESHOLD_BASIS_POINTS, - TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, TEST_CHAIN_ID, None, TEST_BATCH_SIZE_PER_WEIGHT, @@ -6934,8 +6951,18 @@ fn test_reconstruct_from_dkg_certificates_with_shifted_party_ids() { ); let target_epoch = epoch + 1; - let previous_committee = Committee::new(previous_members, epoch); - let target_committee = Committee::new(target_members, target_epoch); + let previous_committee = Committee::new( + previous_members, + epoch, + TEST_THRESHOLD_BASIS_POINTS, + TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, + ); + let target_committee = Committee::new( + target_members, + target_epoch, + TEST_THRESHOLD_BASIS_POINTS, + TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, + ); // Build CommitteeSet simulating a live reconfig: // epoch = 100 (current), pending_epoch_change = 101 (target) @@ -6981,8 +7008,6 @@ fn test_reconstruct_from_dkg_certificates_with_shifted_party_ids() { rotation_setup.setup.encryption_keys[shifted_member_index].clone(), rotation_setup.setup.signing_keys[shifted_member_index].clone(), Box::new(store), - TEST_THRESHOLD_BASIS_POINTS, - TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, TEST_CHAIN_ID, None, TEST_BATCH_SIZE_PER_WEIGHT, @@ -7113,8 +7138,18 @@ fn test_reconstruct_from_dkg_certificates_stops_at_threshold() { // pending_epoch_change=101 is "target". let target_epoch = epoch + 1; let members: Vec<_> = committee.members().to_vec(); - let previous_committee = Committee::new(members.clone(), epoch); - let target_committee = Committee::new(members, target_epoch); + let previous_committee = Committee::new( + members.clone(), + epoch, + TEST_THRESHOLD_BASIS_POINTS, + TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, + ); + let target_committee = Committee::new( + members, + target_epoch, + TEST_THRESHOLD_BASIS_POINTS, + TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, + ); let mut committee_set = CommitteeSet::new(Address::ZERO, Address::ZERO); let mut committees = BTreeMap::new(); @@ -7144,8 +7179,6 @@ fn test_reconstruct_from_dkg_certificates_stops_at_threshold() { setup.encryption_keys[target_index].clone(), setup.signing_keys[target_index].clone(), Box::new(store), - TEST_THRESHOLD_BASIS_POINTS, - TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, TEST_CHAIN_ID, None, TEST_BATCH_SIZE_PER_WEIGHT, @@ -7196,8 +7229,18 @@ fn test_reconstruct_from_rotation_certificates_with_shifted_party_ids() { // committees: {100: 5-member, 101: 5-member (same)}, epoch=100, pending=101 let rotation_epoch = dkg_epoch + 1; let members: Vec<_> = rotation_setup.setup.committee().members().to_vec(); - let committee_at_100 = Committee::new(members.clone(), dkg_epoch); - let committee_at_101 = Committee::new(members.clone(), rotation_epoch); + let committee_at_100 = Committee::new( + members.clone(), + dkg_epoch, + TEST_THRESHOLD_BASIS_POINTS, + TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, + ); + let committee_at_101 = Committee::new( + members.clone(), + rotation_epoch, + TEST_THRESHOLD_BASIS_POINTS, + TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, + ); let mut rotation_committee_set = CommitteeSet::new(Address::ZERO, Address::ZERO); let mut rotation_committees = BTreeMap::new(); @@ -7225,8 +7268,6 @@ fn test_reconstruct_from_rotation_certificates_with_shifted_party_ids() { rotation_setup.setup.encryption_keys[dealer_idx].clone(), rotation_setup.setup.signing_keys[dealer_idx].clone(), Box::new(InMemoryPublicMessagesStore::new()), - TEST_THRESHOLD_BASIS_POINTS, - TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, TEST_CHAIN_ID, None, TEST_BATCH_SIZE_PER_WEIGHT, @@ -7257,8 +7298,6 @@ fn test_reconstruct_from_rotation_certificates_with_shifted_party_ids() { rotation_setup.setup.encryption_keys[other_idx].clone(), rotation_setup.setup.signing_keys[other_idx].clone(), Box::new(InMemoryPublicMessagesStore::new()), - TEST_THRESHOLD_BASIS_POINTS, - TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, TEST_CHAIN_ID, None, TEST_BATCH_SIZE_PER_WEIGHT, @@ -7308,8 +7347,18 @@ fn test_reconstruct_from_rotation_certificates_with_shifted_party_ids() { ); let target_epoch = dkg_epoch + 2; - let previous_committee = Committee::new(members, rotation_epoch); - let target_committee = Committee::new(target_members, target_epoch); + let previous_committee = Committee::new( + members, + rotation_epoch, + TEST_THRESHOLD_BASIS_POINTS, + TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, + ); + let target_committee = Committee::new( + target_members, + target_epoch, + TEST_THRESHOLD_BASIS_POINTS, + TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, + ); let mut committee_set = CommitteeSet::new(Address::ZERO, Address::ZERO); let mut committees = BTreeMap::new(); @@ -7345,8 +7394,6 @@ fn test_reconstruct_from_rotation_certificates_with_shifted_party_ids() { rotation_setup.setup.encryption_keys[shifted_member_index].clone(), rotation_setup.setup.signing_keys[shifted_member_index].clone(), Box::new(store), - TEST_THRESHOLD_BASIS_POINTS, - TEST_WEIGHT_REDUCTION_ALLOWED_DELTA, TEST_CHAIN_ID, None, TEST_BATCH_SIZE_PER_WEIGHT, diff --git a/crates/hashi/src/mpc/signing.rs b/crates/hashi/src/mpc/signing.rs index e838bff8e..09b64520c 100644 --- a/crates/hashi/src/mpc/signing.rs +++ b/crates/hashi/src/mpc/signing.rs @@ -670,7 +670,7 @@ mod tests { ) }) .collect(); - let committee = Committee::new(members, 100); + let committee = Committee::new(members, 100, 3333u16, 0u16); // Fake DKG let sk = S::rand(&mut rng); diff --git a/crates/hashi/src/mpc/types.rs b/crates/hashi/src/mpc/types.rs index f99d32b97..518fc26ed 100644 --- a/crates/hashi/src/mpc/types.rs +++ b/crates/hashi/src/mpc/types.rs @@ -728,7 +728,7 @@ mod tests { ) }) .collect(); - let committee = Committee::new(members, epoch); + let committee = Committee::new(members, epoch, 3333u16, 0u16); // Create a DealerMessagesHash let dealer_address = Address::new([0u8; 32]); diff --git a/crates/hashi/src/onchain/mod.rs b/crates/hashi/src/onchain/mod.rs index 18a4a25b3..787bce982 100644 --- a/crates/hashi/src/onchain/mod.rs +++ b/crates/hashi/src/onchain/mod.rs @@ -929,13 +929,9 @@ async fn scrape_committees( Ok(committee) }) .map_ok(|move_committee| { - let members = move_committee - .members - .into_iter() - .map(convert_move_committee_member) - .collect(); - let committee = Committee::new(members, move_committee.epoch); - (move_committee.epoch, committee) + let epoch = move_committee.epoch; + let committee = convert_move_committee(move_committee); + (epoch, committee) }) .try_collect() .await?; @@ -969,14 +965,7 @@ async fn scrape_committee( .deserialize() .map_err(|e| tonic::Status::from_error(e.into()))?; - let members = field - .value - .members - .into_iter() - .map(convert_move_committee_member) - .collect(); - let committee = Committee::new(members, field.value.epoch); - Ok(committee) + Ok(convert_move_committee(field.value)) } fn convert_move_committee_member( @@ -999,6 +988,24 @@ fn convert_move_committee_member( ) } +fn convert_move_committee(c: move_types::Committee) -> Committee { + let members = c + .members + .into_iter() + .map(convert_move_committee_member) + .collect(); + let threshold_basis_points = u16::try_from(c.mpc_threshold_basis_points) + .expect("mpc_threshold_basis_points exceeds u16::MAX"); + let weight_reduction_allowed_delta = u16::try_from(c.mpc_weight_reduction_allowed_delta) + .expect("mpc_weight_reduction_allowed_delta exceeds u16::MAX"); + Committee::new( + members, + c.epoch, + threshold_basis_points, + weight_reduction_allowed_delta, + ) +} + fn convert_move_uncompressed_g1_pubkey(uncompressed_g1: &[u8]) -> BLS12381PublicKey { use fastcrypto::traits::ToFromBytes; let pubkey = blst::min_pk::PublicKey::deserialize(uncompressed_g1) diff --git a/crates/hashi/src/onchain/types.rs b/crates/hashi/src/onchain/types.rs index 4ce7d1ddd..388e38c23 100644 --- a/crates/hashi/src/onchain/types.rs +++ b/crates/hashi/src/onchain/types.rs @@ -454,9 +454,8 @@ pub struct Config { // This constant mirrors the value in btc_config.move and must be kept in sync. const DUST_RELAY_MIN_VALUE: u64 = 546; -// These mirror the defaults in mpc_config.move and must be kept in sync. -pub const DEFAULT_MPC_THRESHOLD_BASIS_POINTS: u16 = 3333; -pub const DEFAULT_MPC_WEIGHT_REDUCTION_ALLOWED_DELTA: u16 = 800; +pub use hashi_types::committee::DEFAULT_MPC_THRESHOLD_BASIS_POINTS; +pub use hashi_types::committee::DEFAULT_MPC_WEIGHT_REDUCTION_ALLOWED_DELTA; impl Config { /// Minimum deposit amount, mirroring the floor logic in btc_config.move. diff --git a/crates/internal-tools/src/key_recovery.rs b/crates/internal-tools/src/key_recovery.rs index ae869b651..4482918c0 100644 --- a/crates/internal-tools/src/key_recovery.rs +++ b/crates/internal-tools/src/key_recovery.rs @@ -114,8 +114,6 @@ pub async fn run(args: Args, onchain_state: &OnchainState, chain_id: &str) -> an let mut manager = { let state = onchain_state.state(); let hashi = state.hashi(); - let threshold_basis_points = hashi.config.mpc_threshold_basis_points(); - let weight_reduction_allowed_delta = hashi.config.mpc_weight_reduction_allowed_delta(); MpcManager::new( validator_address, &hashi.committees, @@ -123,8 +121,6 @@ pub async fn run(args: Args, onchain_state: &OnchainState, chain_id: &str) -> an encryption_key, dummy_signing_key.clone(), Box::new(store), - threshold_basis_points, - weight_reduction_allowed_delta, chain_id, None, // weight_divisor 0, // batch_size_per_weight (unused for reconstruction) diff --git a/packages/hashi/sources/core/committee/committee.move b/packages/hashi/sources/core/committee/committee.move index 2633f3810..c5ed1959a 100644 --- a/packages/hashi/sources/core/committee/committee.move +++ b/packages/hashi/sources/core/committee/committee.move @@ -36,10 +36,17 @@ public struct Committee has copy, drop, store { members: vector, /// Total voting weight of the committee. total_weight: u64, + mpc_threshold_basis_points: u64, + mpc_weight_reduction_allowed_delta: u64, } /// Constructor for committee. -public(package) fun new_committee(epoch: u64, members: vector): Committee { +public(package) fun new_committee( + epoch: u64, + members: vector, + mpc_threshold_basis_points: u64, + mpc_weight_reduction_allowed_delta: u64, +): Committee { assert!(!members.is_empty()); // Compute the total weight @@ -50,7 +57,13 @@ public(package) fun new_committee(epoch: u64, members: vector): total_weight = total_weight + weight; }); - Committee { members, total_weight, epoch } + Committee { + members, + total_weight, + epoch, + mpc_threshold_basis_points, + mpc_weight_reduction_allowed_delta, + } } /// Constructor for committee member. diff --git a/packages/hashi/sources/core/committee/committee_set.move b/packages/hashi/sources/core/committee/committee_set.move index 886e58b6b..4484fdd3f 100644 --- a/packages/hashi/sources/core/committee/committee_set.move +++ b/packages/hashi/sources/core/committee/committee_set.move @@ -289,6 +289,8 @@ fun verify_proof_of_possession( fun new_committee_from_validator_set( self: &CommitteeSet, sui_system: &sui_system::sui_system::SuiSystemState, + mpc_threshold_basis_points: u64, + mpc_weight_reduction_allowed_delta: u64, ctx: &TxContext, ): Committee { let epoch = ctx.epoch(); @@ -329,7 +331,12 @@ fun new_committee_from_validator_set( // XXX do we sort by address or weight? - committee::new_committee(epoch, committee_members) + committee::new_committee( + epoch, + committee_members, + mpc_threshold_basis_points, + mpc_weight_reduction_allowed_delta, + ) } public(package) fun is_reconfiguring(self: &CommitteeSet): bool { @@ -347,6 +354,8 @@ public(package) fun get_committee(self: &CommitteeSet, epoch: u64): &Committee { public(package) fun start_reconfig( self: &mut CommitteeSet, sui_system: &sui_system::sui_system::SuiSystemState, + mpc_threshold_basis_points: u64, + mpc_weight_reduction_allowed_delta: u64, ctx: &TxContext, ): u64 { // We can't trigger reconfig if we are already reconfiguring @@ -358,7 +367,12 @@ public(package) fun start_reconfig( // our current epoch is not the same as Sui's epoch assert!(self.epoch == 0 || self.epoch != ctx.epoch()); - let committee = self.new_committee_from_validator_set(sui_system, ctx); + let committee = self.new_committee_from_validator_set( + sui_system, + mpc_threshold_basis_points, + mpc_weight_reduction_allowed_delta, + ctx, + ); // Ensure 95% of stake has registered. let mut sui_system_weight = 0; diff --git a/packages/hashi/sources/core/config/config.move b/packages/hashi/sources/core/config/config.move index 01e39eb90..11ba30706 100644 --- a/packages/hashi/sources/core/config/config.move +++ b/packages/hashi/sources/core/config/config.move @@ -35,6 +35,15 @@ public(package) fun get(self: &Config, key: vector): Value { *self.config.get(&key.to_string()) } +public(package) fun try_get(self: &Config, key: vector): Option { + let key = key.to_string(); + if (self.config.contains(&key)) { + option::some(*self.config.get(&key)) + } else { + option::none() + } +} + /// Insert or update a config value. Exposed to other modules in the package /// (e.g. btc_config) so they can define domain-specific setters. public(package) fun upsert(self: &mut Config, key: vector, value: Value) { diff --git a/packages/hashi/sources/core/mpc_config.move b/packages/hashi/sources/core/mpc_config.move index 480e8d0ef..39b39e15c 100644 --- a/packages/hashi/sources/core/mpc_config.move +++ b/packages/hashi/sources/core/mpc_config.move @@ -26,6 +26,20 @@ public(package) fun is_valid_config_entry( } } +public(package) fun threshold_basis_points(config: &Config): u64 { + config + .try_get(b"mpc_threshold_basis_points") + .map!(|v| v.as_u64()) + .destroy_or!(DEFAULT_THRESHOLD_BASIS_POINTS) +} + +public(package) fun allowed_delta(config: &Config): u64 { + config + .try_get(b"mpc_weight_reduction_allowed_delta") + .map!(|v| v.as_u64()) + .destroy_or!(DEFAULT_WEIGHT_REDUCTION_ALLOWED_DELTA) +} + public(package) fun init_defaults(config: &mut Config) { config.upsert( b"mpc_threshold_basis_points", diff --git a/packages/hashi/sources/core/reconfig.move b/packages/hashi/sources/core/reconfig.move index d989095f6..1cbd67e72 100644 --- a/packages/hashi/sources/core/reconfig.move +++ b/packages/hashi/sources/core/reconfig.move @@ -25,14 +25,16 @@ entry fun start_reconfig( self.config().assert_version_enabled(); // Assert that we are not already reconfiguring assert!(!self.committee_set().is_reconfiguring()); - + let mpc_threshold_basis_points = hashi::mpc_config::threshold_basis_points(self.config()); + let mpc_weight_reduction_allowed_delta = hashi::mpc_config::allowed_delta(self.config()); let epoch = self .committee_set_mut() .start_reconfig( sui_system, + mpc_threshold_basis_points, + mpc_weight_reduction_allowed_delta, ctx, ); - sui::event::emit(StartReconfigEvent { epoch }); } diff --git a/packages/hashi/tests/test_utils.move b/packages/hashi/tests/test_utils.move index c2f4eab55..dc76fc314 100644 --- a/packages/hashi/tests/test_utils.move +++ b/packages/hashi/tests/test_utils.move @@ -108,7 +108,7 @@ public fun create_hashi_with_weighted_committee( }; // Create the committee - let committee = committee::new_committee(ctx.epoch(), members); + let committee = committee::new_committee(ctx.epoch(), members, 3333, 800); let sk = bls_sk_for_testing(); let pub_key = bls12381::g1_from_bytes(&bls_min_pk_from_sk(&sk));