confium_tc_cmp20/
recovery.rs1use p256::Scalar;
20
21use crate::share::Cmp20Share;
22
23pub fn recover_share_scalar(
31 surviving_shares: &[Cmp20Share],
32 lost_party_idx: u32,
33) -> Result<Scalar, RecoverError> {
34 if surviving_shares.is_empty() {
35 return Err(RecoverError::NoShares);
36 }
37 let mut seen = std::collections::HashSet::new();
39 for s in surviving_shares {
40 if !seen.insert(s.party_idx) {
41 return Err(RecoverError::DuplicateParty(s.party_idx));
42 }
43 }
44
45 let x_target = Scalar::from(lost_party_idx as u64);
48 let mut result = Scalar::ZERO;
49 for s_i in surviving_shares {
50 let x_i = Scalar::from(s_i.party_idx as u64);
51 let mut numerator = Scalar::ONE;
52 let mut denominator = Scalar::ONE;
53 for s_j in surviving_shares {
54 if s_j.party_idx == s_i.party_idx {
55 continue;
56 }
57 let x_j = Scalar::from(s_j.party_idx as u64);
58 numerator *= x_target - x_j;
60 denominator *= x_i - x_j;
62 }
63 let denom_inv = invert_scalar(&denominator);
64 let lagrange = numerator * denom_inv;
65 let term = s_i.scalar() * lagrange;
66 result += term;
67 }
68 Ok(result)
69}
70
71pub fn recover_share(
75 surviving_shares: &[Cmp20Share],
76 lost_party_idx: u32,
77) -> Result<Cmp20Share, RecoverError> {
78 if surviving_shares.is_empty() {
79 return Err(RecoverError::NoShares);
80 }
81 let scalar = recover_share_scalar(surviving_shares, lost_party_idx)?;
82 let pk = surviving_shares[0].public_key;
83 let x_i = p256::NonZeroScalar::new(scalar)
88 .unwrap_or_else(|| p256::NonZeroScalar::new(Scalar::ONE).unwrap());
89 Ok(Cmp20Share::from_parts(x_i, pk, lost_party_idx))
90}
91
92fn invert_scalar(s: &Scalar) -> Scalar {
93 let ct: p256::elliptic_curve::subtle::CtOption<Scalar> = s.invert();
97 Option::<Scalar>::from(ct).unwrap_or(Scalar::ZERO)
98}
99
100#[derive(Debug)]
102pub enum RecoverError {
103 NoShares,
105 DuplicateParty(u32),
107}
108
109impl std::fmt::Display for RecoverError {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 match self {
112 RecoverError::NoShares => write!(f, "no surviving shares"),
113 RecoverError::DuplicateParty(idx) => write!(f, "duplicate party index: {idx}"),
114 }
115 }
116}
117
118impl std::error::Error for RecoverError {}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::inprocess;
124 use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier};
125
126 #[test]
127 fn recovered_share_produces_valid_signatures() {
128 let kg = inprocess::keygen(3, 5).expect("dkg");
130 let original_shares: Vec<Cmp20Share> = kg
131 .shares
132 .iter()
133 .map(|b| Cmp20Share::from_bytes(b).expect("parse"))
134 .collect();
135 let original_pk = original_shares[0].public_key;
136
137 let surviving: Vec<&Cmp20Share> = original_shares.iter().take(4).collect();
139
140 let surviving_cloned: Vec<Cmp20Share> =
142 surviving.iter().map(|s| (*s).clone()).take(3).collect();
143 let recovered = recover_share(&surviving_cloned, 5).expect("recover");
144
145 assert_eq!(recovered.party_idx, 5);
147 assert_eq!(recovered.public_key, original_pk);
148
149 let mut signing_shares = [
152 original_shares[0].clone(),
153 original_shares[1].clone(),
154 recovered,
155 ];
156 signing_shares.sort_by_key(|s| s.party_idx);
157 let share_blobs: Vec<Vec<u8>> = signing_shares.iter().map(|s| s.to_bytes()).collect();
158 let sig = inprocess::sign(&share_blobs, 3, b"recovery test").expect("sign");
159 assert_eq!(sig.len(), 64);
160
161 let pk = inprocess::decode_public_key(&kg.public_key).expect("pk");
163 let vk = VerifyingKey::from_affine(pk).expect("vk");
164 let s = Signature::from_slice(&sig).expect("sig");
165 vk.verify(b"recovery test", &s).expect("verify");
166 }
167
168 #[test]
169 fn recovered_scalar_matches_original() {
170 let kg = inprocess::keygen(2, 3).expect("dkg");
171 let shares: Vec<Cmp20Share> = kg
172 .shares
173 .iter()
174 .map(|b| Cmp20Share::from_bytes(b).expect("parse"))
175 .collect();
176
177 let recovered_scalar = recover_share_scalar(&shares[..2], 3).expect("recover");
179 let original_scalar = shares[2].scalar();
180 assert_eq!(recovered_scalar, original_scalar);
181 }
182
183 #[test]
184 fn empty_shares_errors() {
185 assert!(matches!(recover_share(&[], 1), Err(RecoverError::NoShares)));
186 }
187}