Skip to content

Commit e288cb6

Browse files
committed
feat!: support varying t and allowed_delta across epochs
1 parent dfe42fc commit e288cb6

File tree

23 files changed

+399
-81
lines changed

23 files changed

+399
-81
lines changed

crates/e2e-tests/src/e2e_flow.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1342,6 +1342,131 @@ mod tests {
13421342
Ok(())
13431343
}
13441344

1345+
#[tokio::test(flavor = "multi_thread")]
1346+
async fn test_varying_t_and_allowed_delta_across_epochs() -> Result<()> {
1347+
init_test_logging();
1348+
1349+
let mut networks = TestNetworksBuilder::new().with_nodes(4).build().await?;
1350+
1351+
use hashi::onchain::types::DEFAULT_MPC_THRESHOLD_IN_BASIS_POINTS;
1352+
use hashi::onchain::types::DEFAULT_MPC_WEIGHT_REDUCTION_ALLOWED_DELTA;
1353+
1354+
// Wait for DKG (epoch 1 committee created with defaults).
1355+
let nodes = networks.hashi_network.nodes();
1356+
let futs: Vec<_> = nodes
1357+
.iter()
1358+
.map(|n| n.wait_for_mpc_key(Duration::from_secs(120)))
1359+
.collect();
1360+
for (i, r) in futures::future::join_all(futs)
1361+
.await
1362+
.into_iter()
1363+
.enumerate()
1364+
{
1365+
r.unwrap_or_else(|e| panic!("Node {i} DKG failed: {e}"));
1366+
}
1367+
1368+
let initial_epoch = nodes[0].current_epoch().unwrap();
1369+
let pk_before = nodes[0].hashi().mpc_handle().unwrap().public_key().unwrap();
1370+
1371+
// Verify epoch 1 committee has defaults.
1372+
let epoch1_committee = nodes[0]
1373+
.hashi()
1374+
.onchain_state()
1375+
.current_committee()
1376+
.unwrap();
1377+
assert_eq!(
1378+
epoch1_committee.mpc_threshold_in_basis_points(),
1379+
DEFAULT_MPC_THRESHOLD_IN_BASIS_POINTS
1380+
);
1381+
assert_eq!(
1382+
epoch1_committee.mpc_weight_reduction_allowed_delta(),
1383+
DEFAULT_MPC_WEIGHT_REDUCTION_ALLOWED_DELTA
1384+
);
1385+
1386+
// Change config between epochs.
1387+
let new_threshold: u64 = 5000;
1388+
let new_delta: u64 = 1200;
1389+
crate::apply_onchain_config_overrides(
1390+
&mut networks,
1391+
&[
1392+
(
1393+
"mpc_threshold_in_basis_points".into(),
1394+
hashi_types::move_types::ConfigValue::U64(new_threshold),
1395+
),
1396+
(
1397+
"mpc_weight_reduction_allowed_delta".into(),
1398+
hashi_types::move_types::ConfigValue::U64(new_delta),
1399+
),
1400+
],
1401+
)
1402+
.await?;
1403+
1404+
// Force key rotation → epoch 2 committee created with new config.
1405+
let target_epoch = initial_epoch + 1;
1406+
networks.sui_network.force_close_epoch().await?;
1407+
let futs: Vec<_> = networks
1408+
.hashi_network()
1409+
.nodes()
1410+
.iter()
1411+
.map(|n| n.wait_for_epoch(target_epoch, Duration::from_secs(480)))
1412+
.collect();
1413+
for (i, r) in futures::future::join_all(futs)
1414+
.await
1415+
.into_iter()
1416+
.enumerate()
1417+
{
1418+
r.unwrap_or_else(|e| panic!("Node {i} failed to reach epoch {target_epoch}: {e}"));
1419+
}
1420+
1421+
// Verify key rotation succeeded: all nodes agree and key is preserved.
1422+
let nodes = networks.hashi_network().nodes();
1423+
let pk_after = nodes[0].hashi().mpc_handle().unwrap().public_key().unwrap();
1424+
assert_eq!(
1425+
pk_before, pk_after,
1426+
"MPC public key changed during rotation"
1427+
);
1428+
for (i, node) in nodes.iter().enumerate().skip(1) {
1429+
let pk = node.hashi().mpc_handle().unwrap().public_key().unwrap();
1430+
assert_eq!(
1431+
pk, pk_after,
1432+
"Node {i} MPC key differs from node 0 after rotation"
1433+
);
1434+
}
1435+
1436+
// Epoch 1 committee retains original defaults.
1437+
let state = networks.hashi_network.nodes()[0].hashi().onchain_state();
1438+
let committees = {
1439+
let s = state.state();
1440+
s.hashi().committees.committees().clone()
1441+
};
1442+
let epoch1 = committees.get(&initial_epoch).expect("epoch 1 committee");
1443+
assert_eq!(
1444+
epoch1.mpc_threshold_in_basis_points(),
1445+
DEFAULT_MPC_THRESHOLD_IN_BASIS_POINTS,
1446+
"epoch {initial_epoch} committee should retain original threshold_basis_points"
1447+
);
1448+
assert_eq!(
1449+
epoch1.mpc_weight_reduction_allowed_delta(),
1450+
DEFAULT_MPC_WEIGHT_REDUCTION_ALLOWED_DELTA,
1451+
"epoch {initial_epoch} committee should retain original allowed_delta"
1452+
);
1453+
1454+
// Epoch 2 committee has new values.
1455+
let epoch2 = committees.get(&target_epoch).expect("epoch 2 committee");
1456+
assert_eq!(
1457+
epoch2.mpc_threshold_in_basis_points(),
1458+
new_threshold as u16,
1459+
"epoch {target_epoch} committee should have updated threshold_basis_points"
1460+
);
1461+
assert_eq!(
1462+
epoch2.mpc_weight_reduction_allowed_delta(),
1463+
new_delta as u16,
1464+
"epoch {target_epoch} committee should have updated allowed_delta"
1465+
);
1466+
1467+
Ok(())
1468+
}
1469+
13451470
/// Verify that a withdrawal can spend a change output whose producing
13461471
/// transaction is mined on Bitcoin but not yet confirmed on Sui. The
13471472
/// actual Bitcoin confirmation count must be queried from the node

crates/e2e-tests/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ impl TestNetworksBuilder {
265265
/// Waits for DKG to complete first so the committee is ready to vote.
266266
/// All nodes vote on every proposal, ensuring quorum is always reached
267267
/// regardless of the number of nodes or their weight distribution.
268-
async fn apply_onchain_config_overrides(
268+
pub(crate) async fn apply_onchain_config_overrides(
269269
networks: &mut TestNetworks,
270270
overrides: &[(String, hashi_types::move_types::ConfigValue)],
271271
) -> Result<()> {

crates/hashi-types/proto/sui/hashi/v1alpha/signature.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ message Committee {
66
optional uint64 epoch = 1;
77
repeated CommitteeMember members = 2;
88
optional uint64 total_weight = 3;
9+
optional uint64 mpc_threshold_in_basis_points = 4;
10+
optional uint64 mpc_weight_reduction_allowed_delta = 5;
911
}
1012

1113
message CommitteeMember {

crates/hashi-types/src/committee.rs

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ use serde::Serialize;
2121
use sui_crypto::SignatureError;
2222
use sui_sdk_types::Address;
2323

24+
/// Default MPC threshold in basis points. Mirrors `DEFAULT_THRESHOLD_IN_BASIS_POINTS` in
25+
/// `mpc_config.move`.
26+
pub const DEFAULT_MPC_THRESHOLD_IN_BASIS_POINTS: u16 = 3334;
27+
28+
/// Default allowed delta for weight reduction. Mirrors `DEFAULT_WEIGHT_REDUCTION_ALLOWED_DELTA` in
29+
/// `mpc_config.move`.
30+
pub const DEFAULT_MPC_WEIGHT_REDUCTION_ALLOWED_DELTA: u16 = 800;
31+
2432
// TODO: Read threshold from on-chain config once it is made configurable.
2533
const THRESHOLD_NUMERATOR: u64 = 2;
2634
const THRESHOLD_DENOMINATOR: u64 = 3;
@@ -85,6 +93,8 @@ pub struct Committee {
8593
members: Vec<CommitteeMember>,
8694
address_to_index: HashMap<Address, usize>,
8795
total_weight: u64,
96+
mpc_threshold_in_basis_points: u16,
97+
mpc_weight_reduction_allowed_delta: u16,
8898
}
8999

90100
#[derive(Clone, PartialEq)]
@@ -147,7 +157,12 @@ impl MemberSignature {
147157
}
148158

149159
impl Committee {
150-
pub fn new(members: Vec<CommitteeMember>, epoch: u64) -> Self {
160+
pub fn new(
161+
members: Vec<CommitteeMember>,
162+
epoch: u64,
163+
mpc_threshold_in_basis_points: u16,
164+
mpc_weight_reduction_allowed_delta: u16,
165+
) -> Self {
151166
let total_weight = members.iter().map(|member| member.weight).sum();
152167
let address_to_index = members
153168
.iter()
@@ -159,6 +174,8 @@ impl Committee {
159174
members,
160175
address_to_index,
161176
total_weight,
177+
mpc_threshold_in_basis_points,
178+
mpc_weight_reduction_allowed_delta,
162179
}
163180
}
164181

@@ -175,6 +192,14 @@ impl Committee {
175192
self.total_weight
176193
}
177194

195+
pub fn mpc_threshold_in_basis_points(&self) -> u16 {
196+
self.mpc_threshold_in_basis_points
197+
}
198+
199+
pub fn mpc_weight_reduction_allowed_delta(&self) -> u16 {
200+
self.mpc_weight_reduction_allowed_delta
201+
}
202+
178203
fn member(&self, address: &Address) -> Result<&CommitteeMember, SignatureError> {
179204
let index = self
180205
.address_to_index
@@ -662,6 +687,9 @@ mod test {
662687
use fastcrypto::groups::FiatShamirChallenge;
663688
use fastcrypto::groups::bls12381::Scalar;
664689
use fastcrypto::serde_helpers::ToFromByteArray;
690+
691+
const TEST_THRESHOLD_IN_BASIS_POINTS: u16 = 3333;
692+
const TEST_WEIGHT_REDUCTION_ALLOWED_DELTA: u16 = 0;
665693
use test_strategy::proptest;
666694

667695
impl proptest::arbitrary::Arbitrary for Bls12381PrivateKey {
@@ -719,7 +747,12 @@ mod test {
719747
weight: 1,
720748
})
721749
.collect();
722-
let committee = Committee::new(members, epoch);
750+
let committee = Committee::new(
751+
members,
752+
epoch,
753+
TEST_THRESHOLD_IN_BASIS_POINTS,
754+
TEST_WEIGHT_REDUCTION_ALLOWED_DELTA,
755+
);
723756

724757
let mut aggregator = BlsSignatureAggregator::new(&committee, message.clone());
725758

@@ -816,7 +849,12 @@ mod test {
816849
weight: 1,
817850
})
818851
.collect();
819-
let committee = Committee::new(members, epoch);
852+
let committee = Committee::new(
853+
members,
854+
epoch,
855+
TEST_THRESHOLD_IN_BASIS_POINTS,
856+
TEST_WEIGHT_REDUCTION_ALLOWED_DELTA,
857+
);
820858

821859
let mut aggregator = BlsSignatureAggregator::new(&committee, message.clone());
822860

@@ -858,6 +896,8 @@ mod test {
858896
})
859897
.collect(),
860898
999, // Different epoch
899+
TEST_THRESHOLD_IN_BASIS_POINTS,
900+
TEST_WEIGHT_REDUCTION_ALLOWED_DELTA,
861901
);
862902
assert!(
863903
certificate
@@ -887,7 +927,12 @@ mod test {
887927
weight: 2500, // committee weight
888928
})
889929
.collect();
890-
let committee = Committee::new(members, epoch);
930+
let committee = Committee::new(
931+
members,
932+
epoch,
933+
TEST_THRESHOLD_IN_BASIS_POINTS,
934+
TEST_WEIGHT_REDUCTION_ALLOWED_DELTA,
935+
);
891936

892937
// Reduced weights: different from committee weights
893938
let reduced_weights: HashMap<Address, u16> =
@@ -970,7 +1015,12 @@ mod test {
9701015
weight: 1,
9711016
})
9721017
.collect();
973-
let committee = Committee::new(members, epoch);
1018+
let committee = Committee::new(
1019+
members,
1020+
epoch,
1021+
TEST_THRESHOLD_IN_BASIS_POINTS,
1022+
TEST_WEIGHT_REDUCTION_ALLOWED_DELTA,
1023+
);
9741024

9751025
// Create a certificate via aggregator
9761026
let mut aggregator = BlsSignatureAggregator::new(&committee, message.clone());

crates/hashi-types/src/guardian/proto_conversions.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ use hpke::Serializable;
5454
use std::num::NonZeroU16;
5555
use std::str::FromStr;
5656

57+
use crate::committee::DEFAULT_MPC_THRESHOLD_IN_BASIS_POINTS;
58+
use crate::committee::DEFAULT_MPC_WEIGHT_REDUCTION_ALLOWED_DELTA;
59+
5760
// --------------------------------------------
5861
// Proto -> Domain (deserialization)
5962
// --------------------------------------------
@@ -666,7 +669,20 @@ fn pb_to_hashi_committee(c: pb::Committee) -> GuardianResult<HashiCommittee> {
666669

667670
let total_weight = c.total_weight.ok_or_else(|| missing("total_weight"))?;
668671

669-
let committee = HashiCommittee::new(members, epoch);
672+
let mpc_threshold_in_basis_points = c
673+
.mpc_threshold_in_basis_points
674+
.map(|v| v as u16)
675+
.unwrap_or(DEFAULT_MPC_THRESHOLD_IN_BASIS_POINTS);
676+
let mpc_weight_reduction_allowed_delta = c
677+
.mpc_weight_reduction_allowed_delta
678+
.map(|v| v as u16)
679+
.unwrap_or(DEFAULT_MPC_WEIGHT_REDUCTION_ALLOWED_DELTA);
680+
let committee = HashiCommittee::new(
681+
members,
682+
epoch,
683+
mpc_threshold_in_basis_points,
684+
mpc_weight_reduction_allowed_delta,
685+
);
670686

671687
if committee.total_weight() != total_weight {
672688
return Err(InvalidInputs(format!(
@@ -687,6 +703,8 @@ fn hashi_committee_to_pb(c: HashiCommittee) -> pb::Committee {
687703
.map(|m| hashi_committee_member_to_pb(m.clone()))
688704
.collect(),
689705
total_weight: Some(c.total_weight()),
706+
mpc_threshold_in_basis_points: Some(c.mpc_threshold_in_basis_points() as u64),
707+
mpc_weight_reduction_allowed_delta: Some(c.mpc_weight_reduction_allowed_delta() as u64),
690708
}
691709
}
692710

crates/hashi-types/src/guardian/test_utils.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ use hpke::Deserializable;
4646
use std::num::NonZeroU16;
4747
use sui_sdk_types::Address as SuiAddress;
4848
use sui_sdk_types::bcs::FromBcs;
49+
50+
use crate::committee::DEFAULT_MPC_THRESHOLD_IN_BASIS_POINTS;
51+
use crate::committee::DEFAULT_MPC_WEIGHT_REDUCTION_ALLOWED_DELTA;
52+
4953
// -------------------------------
5054
// Shared deterministic test values
5155
// -------------------------------
@@ -185,7 +189,12 @@ fn mock_committee_member() -> HashiCommitteeMember {
185189
}
186190

187191
fn mock_committee_with_one_member(epoch: u64) -> HashiCommittee {
188-
HashiCommittee::new(vec![mock_committee_member()], epoch)
192+
HashiCommittee::new(
193+
vec![mock_committee_member()],
194+
epoch,
195+
DEFAULT_MPC_THRESHOLD_IN_BASIS_POINTS,
196+
DEFAULT_MPC_WEIGHT_REDUCTION_ALLOWED_DELTA,
197+
)
189198
}
190199

191200
impl ProvisionerInitState {

crates/hashi-types/src/move_types/mod.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ pub struct Committee {
159159
pub members: Vec<CommitteeMember>,
160160
/// Total voting weight of the committee.
161161
pub total_weight: u64,
162+
/// MPC threshold in basis points
163+
pub mpc_threshold_in_basis_points: u64,
164+
/// Allowed delta for weight reduction
165+
pub mpc_weight_reduction_allowed_delta: u64,
162166
}
163167

164168
/// Rust version of the Move hashi::config::Config type.
@@ -1141,6 +1145,8 @@ impl From<&crate::committee::Committee> for Committee {
11411145
epoch: c.epoch(),
11421146
members: c.members().iter().map(Into::into).collect(),
11431147
total_weight: c.total_weight(),
1148+
mpc_threshold_in_basis_points: c.mpc_threshold_in_basis_points() as u64,
1149+
mpc_weight_reduction_allowed_delta: c.mpc_weight_reduction_allowed_delta() as u64,
11441150
}
11451151
}
11461152
}
@@ -1154,6 +1160,13 @@ impl TryFrom<Committee> for crate::committee::Committee {
11541160
.into_iter()
11551161
.map(crate::committee::CommitteeMember::try_from)
11561162
.collect::<Result<Vec<_>, _>>()?;
1157-
Ok(crate::committee::Committee::new(members, c.epoch))
1163+
Ok(crate::committee::Committee::new(
1164+
members,
1165+
c.epoch,
1166+
u16::try_from(c.mpc_threshold_in_basis_points)
1167+
.expect("mpc_threshold_in_basis_points exceeds u16::MAX"),
1168+
u16::try_from(c.mpc_weight_reduction_allowed_delta)
1169+
.expect("mpc_weight_reduction_allowed_delta exceeds u16::MAX"),
1170+
))
11581171
}
11591172
}
225 Bytes
Binary file not shown.

crates/hashi-types/src/proto/generated/sui.hashi.v1alpha.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2822,6 +2822,10 @@ pub struct Committee {
28222822
pub members: ::prost::alloc::vec::Vec<CommitteeMember>,
28232823
#[prost(uint64, optional, tag = "3")]
28242824
pub total_weight: ::core::option::Option<u64>,
2825+
#[prost(uint64, optional, tag = "4")]
2826+
pub mpc_threshold_in_basis_points: ::core::option::Option<u64>,
2827+
#[prost(uint64, optional, tag = "5")]
2828+
pub mpc_weight_reduction_allowed_delta: ::core::option::Option<u64>,
28252829
}
28262830
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
28272831
pub struct CommitteeMember {

0 commit comments

Comments
 (0)