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
125 changes: 125 additions & 0 deletions crates/e2e-tests/src/e2e_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/e2e-tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand Down
2 changes: 2 additions & 0 deletions crates/hashi-types/proto/sui/hashi/v1alpha/signature.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
60 changes: 55 additions & 5 deletions crates/hashi-types/src/committee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -85,6 +93,8 @@ pub struct Committee {
members: Vec<CommitteeMember>,
address_to_index: HashMap<Address, usize>,
total_weight: u64,
mpc_threshold_basis_points: u16,
mpc_weight_reduction_allowed_delta: u16,
}

#[derive(Clone, PartialEq)]
Expand Down Expand Up @@ -147,7 +157,12 @@ impl MemberSignature {
}

impl Committee {
pub fn new(members: Vec<CommitteeMember>, epoch: u64) -> Self {
pub fn new(
members: Vec<CommitteeMember>,
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()
Expand All @@ -159,6 +174,8 @@ impl Committee {
members,
address_to_index,
total_weight,
mpc_threshold_basis_points,
mpc_weight_reduction_allowed_delta,
}
}

Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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());

Expand Down Expand Up @@ -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());

Expand Down Expand Up @@ -858,6 +896,8 @@ mod test {
})
.collect(),
999, // Different epoch
TEST_THRESHOLD_BASIS_POINTS,
TEST_ALLOWED_DELTA,
);
assert!(
certificate
Expand Down Expand Up @@ -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<Address, u16> =
Expand Down Expand Up @@ -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());
Expand Down
20 changes: 19 additions & 1 deletion crates/hashi-types/src/guardian/proto_conversions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
// --------------------------------------------
Expand Down Expand Up @@ -666,7 +669,20 @@ fn pb_to_hashi_committee(c: pb::Committee) -> GuardianResult<HashiCommittee> {

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!(
Expand All @@ -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),
}
}

Expand Down
11 changes: 10 additions & 1 deletion crates/hashi-types/src/guardian/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
// -------------------------------
Expand Down Expand Up @@ -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 {
Expand Down
13 changes: 12 additions & 1 deletion crates/hashi-types/src/move_types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ pub struct Committee {
pub members: Vec<CommitteeMember>,
/// 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.
Expand Down Expand Up @@ -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,
}
}
}
Expand All @@ -1114,6 +1118,13 @@ impl TryFrom<Committee> for crate::committee::Committee {
.into_iter()
.map(crate::committee::CommitteeMember::try_from)
.collect::<Result<Vec<_>, _>>()?;
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"),
))
}
}
Binary file modified crates/hashi-types/src/proto/generated/sui.hashi.v1alpha.fds.bin
Binary file not shown.
4 changes: 4 additions & 0 deletions crates/hashi-types/src/proto/generated/sui.hashi.v1alpha.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2822,6 +2822,10 @@ pub struct Committee {
pub members: ::prost::alloc::vec::Vec<CommitteeMember>,
#[prost(uint64, optional, tag = "3")]
pub total_weight: ::core::option::Option<u64>,
#[prost(uint64, optional, tag = "4")]
pub mpc_threshold_basis_points: ::core::option::Option<u64>,
#[prost(uint64, optional, tag = "5")]
pub mpc_weight_reduction_allowed_delta: ::core::option::Option<u64>,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct CommitteeMember {
Expand Down
Loading
Loading