Skip to content

Commit 7f6dbfb

Browse files
Reshare keys to the updated reconstruction threshold
Changing a domain's reconstruction threshold via a resharing proposal had no cryptographic effect. The resharing key event was built from the old domain registry, so the node reshared at the old threshold, and per_domain_thresholds was only folded into the registry on completion. Lowering t left an unusable (higher-degree) key; raising t left the advertised threshold unenforced. Build the resharing key events from the effective (threshold-updated) domains via a new DomainRegistry::effective_domain_by_index, so the reshare targets the new degree. The old-side threshold still comes from the previous registry, so reshare(old_t, .., new_t) receives the correct pair.
1 parent fdeefd5 commit 7f6dbfb

3 files changed

Lines changed: 80 additions & 11 deletions

File tree

crates/contract/src/primitives/domain.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,26 @@ impl DomainRegistry {
147147
self.domains.get(index)
148148
}
149149

150+
/// Like [`Self::get_domain_by_index`], but with `reconstruction_threshold` overridden
151+
/// by `threshold_updates` when present, so a resharing key event reshares to the new
152+
/// degree. Matches the update [`Self::with_threshold_updates`] folds in on completion.
153+
pub fn effective_domain_by_index(
154+
&self,
155+
index: usize,
156+
threshold_updates: &BTreeMap<DomainId, ReconstructionThreshold>,
157+
) -> Option<DomainConfig> {
158+
self.get_domain_by_index(index).map(|domain| {
159+
let reconstruction_threshold = threshold_updates
160+
.get(&domain.id)
161+
.copied()
162+
.unwrap_or(domain.reconstruction_threshold);
163+
DomainConfig {
164+
reconstruction_threshold,
165+
..domain.clone()
166+
}
167+
})
168+
}
169+
150170
/// Returns the given domain by the DomainId.
151171
pub fn get_domain_by_domain_id(&self, id: DomainId) -> Option<&DomainConfig> {
152172
self.domains.iter().find(|domain| domain.id == id)
@@ -771,4 +791,33 @@ pub mod tests {
771791
ReconstructionThreshold::new(2)
772792
);
773793
}
794+
795+
#[test]
796+
fn effective_domain_by_index__should_override_only_the_targeted_domains_threshold() {
797+
// Given a registry with two domains and an update targeting the second.
798+
let registry = registry_of(vec![
799+
DomainConfig {
800+
id: DomainId(0),
801+
protocol: Protocol::CaitSith,
802+
reconstruction_threshold: ReconstructionThreshold::new(2),
803+
purpose: DomainPurpose::Sign,
804+
},
805+
DomainConfig {
806+
id: DomainId(1),
807+
protocol: Protocol::CaitSith,
808+
reconstruction_threshold: ReconstructionThreshold::new(2),
809+
purpose: DomainPurpose::Sign,
810+
},
811+
]);
812+
let updates = BTreeMap::from([(DomainId(1), ReconstructionThreshold::new(4))]);
813+
814+
// When reading each domain's effective config.
815+
let d0 = registry.effective_domain_by_index(0, &updates).unwrap();
816+
let d1 = registry.effective_domain_by_index(1, &updates).unwrap();
817+
818+
// Then only the targeted domain reports the new threshold.
819+
assert_eq!(d0.reconstruction_threshold, ReconstructionThreshold::new(2));
820+
assert_eq!(d1.reconstruction_threshold, ReconstructionThreshold::new(4));
821+
assert!(registry.effective_domain_by_index(2, &updates).is_none());
822+
}
774823
}

crates/contract/src/state/resharing.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,8 @@ impl ResharingContractState {
8282
self.prospective_epoch_id().next(),
8383
self.previous_running_state
8484
.domains
85-
.get_domain_by_index(0)
86-
.unwrap()
87-
.clone(),
85+
.effective_domain_by_index(0, proposal.per_domain_thresholds())
86+
.unwrap(),
8887
proposal.parameters().clone(),
8988
),
9089
cancellation_requests: HashSet::new(),
@@ -136,11 +135,11 @@ impl ResharingContractState {
136135
if let Some(next_domain) = self
137136
.previous_running_state
138137
.domains
139-
.get_domain_by_index(self.reshared_keys.len())
138+
.effective_domain_by_index(self.reshared_keys.len(), &self.per_domain_thresholds)
140139
{
141140
self.resharing_key = KeyEvent::new(
142141
self.prospective_epoch_id(),
143-
next_domain.clone(),
142+
next_domain,
144143
self.resharing_key.proposed_parameters().clone(),
145144
);
146145
} else {

crates/contract/src/state/running.rs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,10 @@ impl RunningContractState {
6767
&mut self,
6868
proposal: &ProposedThresholdParameters,
6969
) -> Option<ResharingContractState> {
70-
if let Some(first_domain) = self.domains.get_domain_by_index(0) {
70+
if let Some(first_domain) = self
71+
.domains
72+
.effective_domain_by_index(0, proposal.per_domain_thresholds())
73+
{
7174
let epoch_id = self.prospective_epoch_id();
7275

7376
Some(ResharingContractState {
@@ -78,11 +81,7 @@ impl RunningContractState {
7881
self.add_domains_votes.clone(),
7982
),
8083
reshared_keys: Vec::new(),
81-
resharing_key: KeyEvent::new(
82-
epoch_id,
83-
first_domain.clone(),
84-
proposal.parameters().clone(),
85-
),
84+
resharing_key: KeyEvent::new(epoch_id, first_domain, proposal.parameters().clone()),
8685
cancellation_requests: HashSet::new(),
8786
per_domain_thresholds: proposal.per_domain_thresholds().clone(),
8887
})
@@ -641,6 +640,28 @@ pub mod running_tests {
641640
);
642641
}
643642

643+
#[test]
644+
fn transition_to_resharing__should_carry_threshold_update_into_the_resharing_key_event() {
645+
// Given a running state with one domain and a proposal changing its threshold.
646+
let mut state = gen_running_state_with_params(1, 5, 5);
647+
let domain_id = state.domains.get_domain_by_index(0).unwrap().id;
648+
let proposal = gen_valid_params_proposal(&state.parameters).with_per_domain_thresholds(
649+
BTreeMap::from([(domain_id, ReconstructionThreshold::new(3))]),
650+
);
651+
652+
// When transitioning into resharing.
653+
let resharing = state
654+
.transition_to_resharing_no_checks(&proposal)
655+
.expect("state has a domain, so it transitions into resharing");
656+
657+
// Then the resharing key event carries the updated threshold, so the node
658+
// reshares to the new degree rather than the stale one.
659+
assert_eq!(
660+
resharing.resharing_key.domain().reconstruction_threshold,
661+
ReconstructionThreshold::new(3),
662+
);
663+
}
664+
644665
/// Builds a `DomainConfig` for the next domain id with the given protocol,
645666
/// purpose, and reconstruction threshold.
646667
fn single_domain_proposal(

0 commit comments

Comments
 (0)