Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions crates/e2e-tests/src/e2e_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1317,6 +1317,34 @@ mod tests {
Ok(())
}

#[tokio::test]
async fn test_mpc_config_defaults_match_rust() -> Result<()> {
init_test_logging();

let networks = TestNetworksBuilder::new().with_nodes(1).build().await?;
networks.hashi_network.nodes()[0]
.wait_for_mpc_key(Duration::from_secs(60))
.await?;

use hashi::onchain::types::DEFAULT_MPC_THRESHOLD_BASIS_POINTS;
use hashi::onchain::types::DEFAULT_MPC_WEIGHT_REDUCTION_ALLOWED_DELTA;

let hashi = networks.hashi_network.nodes()[0].hashi();
let threshold_bps = hashi.onchain_state().mpc_threshold_basis_points();
let weight_reduction_allowed_delta =
hashi.onchain_state().mpc_weight_reduction_allowed_delta();

assert_eq!(
threshold_bps, DEFAULT_MPC_THRESHOLD_BASIS_POINTS,
"on-chain mpc_threshold_basis_points ({threshold_bps}) != Rust default ({DEFAULT_MPC_THRESHOLD_BASIS_POINTS})"
);
assert_eq!(
weight_reduction_allowed_delta, DEFAULT_MPC_WEIGHT_REDUCTION_ALLOWED_DELTA,
"on-chain mpc_weight_reduction_allowed_delta ({weight_reduction_allowed_delta}) != Rust default ({DEFAULT_MPC_WEIGHT_REDUCTION_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
Expand Down
11 changes: 6 additions & 5 deletions crates/hashi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@ pub mod tls;
pub mod utxo_pool;
pub mod withdrawals;

/// The allowed delta for weight reduction in basis points (800 means 8%).
/// This matches Sui's `random_beacon_reduction_allowed_delta` configuration.
const WEIGHT_REDUCTION_ALLOWED_DELTA: u16 = 800;
// TODO: Tune based on production workload.
const BATCH_SIZE_PER_WEIGHT: u16 = 10;

Expand Down Expand Up @@ -220,7 +217,10 @@ impl Hashi {
protocol_type: mpc::types::ProtocolType,
) -> anyhow::Result<mpc::MpcManager> {
let state = self.onchain_state().state();
let committee_set = &state.hashi().committees;
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
Expand Down Expand Up @@ -261,7 +261,8 @@ impl Hashi {
encryption_key,
signing_key,
store,
WEIGHT_REDUCTION_ALLOWED_DELTA,
threshold_basis_points,
weight_reduction_allowed_delta,
chain_id,
self.config.test_weight_divisor,
batch_size_per_weight,
Expand Down
37 changes: 20 additions & 17 deletions crates/hashi/src/mpc/mpc_except_signing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const ERR_PUBLISH_CERT_FAILED: &str = "Failed to publish certificate";
const EXPECT_THRESHOLD_VALIDATED: &str = "Threshold already validated";
const EXPECT_THRESHOLD_MET: &str = "Already checked earlier that threshold is met";
const EXPECT_SERIALIZATION_SUCCESS: &str = "Serialization should always succeed";
const MAX_BASIS_POINTS: u32 = 10000;

// DKG protocol
// 1) A dealer sends out a message to all parties containing the encrypted shares and the public keys of the nonces.
Expand Down Expand Up @@ -123,7 +124,8 @@ impl MpcManager {
encryption_key: PrivateKey<EncryptionGroupElement>,
signing_key: Bls12381PrivateKey,
public_message_store: Box<dyn PublicMessagesStore>,
allowed_delta: u16,
threshold_basis_points: u16,
weight_reduction_allowed_delta: u16,
chain_id: &str,
weight_divisor: Option<u16>,
batch_size_per_weight: u16,
Expand All @@ -144,8 +146,12 @@ impl MpcManager {
.get(&epoch)
.ok_or_else(|| MpcError::InvalidConfig(format!("no committee for epoch {epoch}")))?
.clone();
// TODO: Pass t and f as arguments instead of computing them
let (nodes, threshold) = build_reduced_nodes(&committee, allowed_delta, weight_divisor)?;
let (nodes, threshold) = build_reduced_nodes(
&committee,
threshold_basis_points,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name confused me a bit. You call it basis points here because you're defining the threshold as some ratio with a fixed denominator, MAX_BASIS_POINTS, and then use this to compute the threshold with the actual given weights, right? if so, I'd call this something else, like TOTAL_BASIS_WEIGHT, or just document the relationship with THRESHOLD_BASIS_POINTS.

weight_reduction_allowed_delta,
weight_divisor,
)?;
let total_weight = nodes.total_weight();
let max_faulty = ((total_weight - threshold) / 2).min(threshold - 1);
let dkg_config = MpcConfig::new(epoch, nodes, threshold, max_faulty)?;
Expand Down Expand Up @@ -199,8 +205,12 @@ impl MpcManager {
};
let (previous_nodes, previous_threshold) = match previous_committee.as_ref() {
Some(prev_committee) => {
let (nodes, threshold) =
build_reduced_nodes(prev_committee, allowed_delta, weight_divisor)?;
let (nodes, threshold) = build_reduced_nodes(
prev_committee,
threshold_basis_points,
weight_reduction_allowed_delta,
weight_divisor,
)?;
(Some(nodes), Some(threshold))
}
None => (None, None),
Expand Down Expand Up @@ -3505,18 +3515,10 @@ fn compute_messages_hash(messages: &Messages) -> MessageHash {
MessageHash::from(Blake2b256::digest(&bytes).digest)
}

fn compute_bft_threshold(total_weight: u16) -> MpcResult<u16> {
if total_weight == 0 {
return Err(MpcError::InvalidConfig(
"committee has zero total weight".into(),
));
}
Ok((total_weight - 1) / 3 + 1)
}

fn build_reduced_nodes(
committee: &Committee,
allowed_delta: u16,
threshold_basis_points: u16,
weight_reduction_allowed_delta: u16,
test_weight_divisor: u16,
) -> MpcResult<(Nodes<EncryptionGroupElement>, u16)> {
let nodes_vec: Vec<Node<EncryptionGroupElement>> = committee
Expand All @@ -3530,8 +3532,9 @@ fn build_reduced_nodes(
})
.collect();
let total_weight: u16 = nodes_vec.iter().map(|n| n.weight).sum();
let threshold = compute_bft_threshold(total_weight)?;
Nodes::new_reduced(nodes_vec, threshold, allowed_delta, 1)
let threshold =
(total_weight as u32 * threshold_basis_points as u32).div_ceil(MAX_BASIS_POINTS) as u16;
Nodes::new_reduced(nodes_vec, threshold, weight_reduction_allowed_delta, 1)
.map_err(|e| MpcError::CryptoError(e.to_string()))
}

Expand Down
59 changes: 40 additions & 19 deletions crates/hashi/src/mpc/mpc_except_signing_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;

/// Use 0 for allowed_delta in tests to disable weight reduction.
const TEST_ALLOWED_DELTA: u16 = 0;
const TEST_THRESHOLD_BASIS_POINTS: u16 = 3333;
/// Use 0 for weight_reduction_allowed_delta in tests to disable weight reduction.
const TEST_WEIGHT_REDUCTION_ALLOWED_DELTA: u16 = 0;
/// Use 1 for test_weight_divisor in unit tests (they already use small weights).
const TEST_WEIGHT_DIVISOR: u16 = 1;
const TEST_CHAIN_ID: &str = "testchain";
Expand Down Expand Up @@ -284,7 +285,8 @@ impl TestSetup {
self.encryption_keys[validator_index].clone(),
self.signing_keys[validator_index].clone(),
store,
TEST_ALLOWED_DELTA,
TEST_THRESHOLD_BASIS_POINTS,
TEST_WEIGHT_REDUCTION_ALLOWED_DELTA,
TEST_CHAIN_ID,
None,
TEST_BATCH_SIZE_PER_WEIGHT,
Expand Down Expand Up @@ -897,7 +899,8 @@ fn test_mpc_manager_new_from_committee_set() {
encryption_key,
signing_key,
Box::new(MockPublicMessagesStore),
TEST_ALLOWED_DELTA,
TEST_THRESHOLD_BASIS_POINTS,
TEST_WEIGHT_REDUCTION_ALLOWED_DELTA,
TEST_CHAIN_ID,
None,
TEST_BATCH_SIZE_PER_WEIGHT,
Expand Down Expand Up @@ -960,7 +963,8 @@ fn test_mpc_manager_new_fails_if_no_committee_for_epoch() {
encryption_keys[0].clone(),
signing_keys[0].clone(),
Box::new(MockPublicMessagesStore),
TEST_ALLOWED_DELTA,
TEST_THRESHOLD_BASIS_POINTS,
TEST_WEIGHT_REDUCTION_ALLOWED_DELTA,
"test",
None,
TEST_BATCH_SIZE_PER_WEIGHT,
Expand All @@ -983,7 +987,8 @@ fn test_mpc_manager_new_with_weighted_committee() {

let manager = setup.create_manager(0);

// With total_weight=15: max_faulty = (15-1)/3 = 4, threshold = 5
// With total_weight=15, threshold=ceil(15*3333/10000)=ceil(4.9995)=5
// max_faulty = min((15-5)/2, 5-1) = min(5, 4) = 4
assert_eq!(manager.mpc_config.threshold, 5);
assert_eq!(manager.mpc_config.max_faulty, 4);
}
Expand Down Expand Up @@ -1262,7 +1267,7 @@ fn test_complete_dkg_success() {
let mut rng = rand::thread_rng();

// Use different weights: [3, 2, 4, 1, 2] (total = 12)
// threshold = (12 - 1) / 3 + 1 = 4
// threshold = ceil(12 * 3333 / 10000) = 4
let weights = [3, 2, 4, 1, 2];
let setup = TestSetup::with_weights(&weights);

Expand Down Expand Up @@ -4756,8 +4761,8 @@ struct RotationTestSetup {

impl RotationTestSetup {
/// Creates a rotation test setup with weighted validators and completed DKG.
/// Uses weights [3, 2, 4, 1, 2] (total = 12, threshold = 4).
/// Dealers are validators 0, 1, 4 (total weight = 7 >= threshold).
/// Uses weights [3, 2, 4, 1, 2] (total = 12, threshold = ceil(12*3333/10000) = 4).
/// Dealers are validators 0, 1, 4 (total weight = 7 >= threshold + max_faulty = 7).
fn new() -> Self {
let mut rng = rand::thread_rng();
let weights = [3, 2, 4, 1, 2];
Expand Down Expand Up @@ -4816,8 +4821,13 @@ impl RotationTestSetup {
/// This matches `run_as_party` behavior during live DKG.
fn threshold_dealer_addresses(&self) -> Vec<Address> {
let committee = self.setup.committee();
let (nodes, threshold) =
build_reduced_nodes(committee, TEST_ALLOWED_DELTA, TEST_WEIGHT_DIVISOR).unwrap();
let (nodes, threshold) = build_reduced_nodes(
committee,
TEST_THRESHOLD_BASIS_POINTS,
TEST_WEIGHT_REDUCTION_ALLOWED_DELTA,
TEST_WEIGHT_DIVISOR,
)
.unwrap();
let mut result = Vec::new();
let mut weight_sum = 0u16;
for addr in self.certificates.keys() {
Expand All @@ -4837,8 +4847,13 @@ impl RotationTestSetup {
fn prepare_for_rotation(&self, manager: &mut MpcManager) {
let previous_committee = self.setup.committee_set.previous_committee().cloned();
if let Some(ref prev) = previous_committee {
let (nodes, threshold) =
build_reduced_nodes(prev, TEST_ALLOWED_DELTA, TEST_WEIGHT_DIVISOR).unwrap();
let (nodes, threshold) = build_reduced_nodes(
prev,
TEST_THRESHOLD_BASIS_POINTS,
TEST_WEIGHT_REDUCTION_ALLOWED_DELTA,
TEST_WEIGHT_DIVISOR,
)
.unwrap();
manager.previous_nodes = Some(nodes);
manager.previous_threshold = Some(threshold);
}
Expand Down Expand Up @@ -5830,7 +5845,8 @@ async fn test_prepare_previous_output_for_new_member() {
new_member_encryption_key,
new_member_signing_key,
Box::new(InMemoryPublicMessagesStore::new()),
TEST_ALLOWED_DELTA,
TEST_THRESHOLD_BASIS_POINTS,
TEST_WEIGHT_REDUCTION_ALLOWED_DELTA,
TEST_CHAIN_ID,
None,
TEST_BATCH_SIZE_PER_WEIGHT,
Expand Down Expand Up @@ -6965,7 +6981,8 @@ 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_ALLOWED_DELTA,
TEST_THRESHOLD_BASIS_POINTS,
TEST_WEIGHT_REDUCTION_ALLOWED_DELTA,
TEST_CHAIN_ID,
None,
TEST_BATCH_SIZE_PER_WEIGHT,
Expand Down Expand Up @@ -7127,7 +7144,8 @@ 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_ALLOWED_DELTA,
TEST_THRESHOLD_BASIS_POINTS,
TEST_WEIGHT_REDUCTION_ALLOWED_DELTA,
TEST_CHAIN_ID,
None,
TEST_BATCH_SIZE_PER_WEIGHT,
Expand Down Expand Up @@ -7207,7 +7225,8 @@ 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_ALLOWED_DELTA,
TEST_THRESHOLD_BASIS_POINTS,
TEST_WEIGHT_REDUCTION_ALLOWED_DELTA,
TEST_CHAIN_ID,
None,
TEST_BATCH_SIZE_PER_WEIGHT,
Expand Down Expand Up @@ -7238,7 +7257,8 @@ 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_ALLOWED_DELTA,
TEST_THRESHOLD_BASIS_POINTS,
TEST_WEIGHT_REDUCTION_ALLOWED_DELTA,
TEST_CHAIN_ID,
None,
TEST_BATCH_SIZE_PER_WEIGHT,
Expand Down Expand Up @@ -7325,7 +7345,8 @@ 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_ALLOWED_DELTA,
TEST_THRESHOLD_BASIS_POINTS,
TEST_WEIGHT_REDUCTION_ALLOWED_DELTA,
TEST_CHAIN_ID,
None,
TEST_BATCH_SIZE_PER_WEIGHT,
Expand Down
11 changes: 11 additions & 0 deletions crates/hashi/src/onchain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,17 @@ impl OnchainState {
self.state().hashi().config.bitcoin_confirmation_threshold()
}

pub fn mpc_threshold_basis_points(&self) -> u16 {
self.state().hashi().config.mpc_threshold_basis_points()
}

pub fn mpc_weight_reduction_allowed_delta(&self) -> u16 {
self.state()
.hashi()
.config
.mpc_weight_reduction_allowed_delta()
}

pub fn bridge_service_client(
&self,
validator: &Address,
Expand Down
22 changes: 22 additions & 0 deletions crates/hashi/src/onchain/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,10 @@ 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;

impl Config {
/// Minimum deposit amount, mirroring the floor logic in btc_config.move.
pub fn bitcoin_deposit_minimum(&self) -> u64 {
Expand Down Expand Up @@ -495,6 +499,24 @@ impl Config {
_ => 6,
}
}

pub fn mpc_threshold_basis_points(&self) -> u16 {
match self.config.get("mpc_threshold_basis_points") {
Some(ConfigValue::U64(v)) => {
u16::try_from(*v).expect("mpc_threshold_basis_points exceeds u16::MAX")
}
_ => DEFAULT_MPC_THRESHOLD_BASIS_POINTS,
}
}

pub fn mpc_weight_reduction_allowed_delta(&self) -> u16 {
match self.config.get("mpc_weight_reduction_allowed_delta") {
Some(ConfigValue::U64(v)) => {
u16::try_from(*v).expect("mpc_weight_reduction_allowed_delta exceeds u16::MAX")
}
_ => DEFAULT_MPC_WEIGHT_REDUCTION_ALLOWED_DELTA,
}
}
}

#[derive(Debug)]
Expand Down
8 changes: 6 additions & 2 deletions crates/internal-tools/src/key_recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,18 @@ pub async fn run(args: Args, onchain_state: &OnchainState, chain_id: &str) -> an
let session_id = SessionId::new(chain_id, reconstruction_epoch, &ProtocolType::KeyRotation);
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,
&state.hashi().committees,
&hashi.committees,
session_id,
encryption_key,
dummy_signing_key.clone(),
Box::new(store),
800, // allowed_delta (same as devnet)
threshold_basis_points,
weight_reduction_allowed_delta,
chain_id,
None, // weight_divisor
0, // batch_size_per_weight (unused for reconstruction)
Expand Down
Loading
Loading