confium_coordinator/
dkg_coordinator.rs1use getrandom::SysRng;
7use p256::elliptic_curve::rand_core::UnwrapErr;
8use p256::elliptic_curve::{Field, PrimeField};
9use p256::{AffinePoint, FieldBytes, ProjectivePoint, Scalar};
10use serde::{Deserialize, Serialize};
11use sha2::{Digest as _, Sha256};
12use std::collections::HashMap;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct DkgContribution {
17 pub party_idx: u32,
18 pub shares: HashMap<u32, String>,
20 pub commitment_hex: String,
22}
23
24#[derive(Debug)]
26pub struct DkgSession {
27 pub threshold: u32,
28 pub party_count: u32,
29 pub contributions: HashMap<u32, DkgContribution>,
30}
31
32impl DkgSession {
33 pub fn new(threshold: u32, party_count: u32) -> Self {
34 Self {
35 threshold,
36 party_count,
37 contributions: HashMap::new(),
38 }
39 }
40
41 pub fn submit(&mut self, contrib: DkgContribution) -> Result<(), String> {
42 if contrib.party_idx == 0 || contrib.party_idx > self.party_count {
43 return Err("invalid party index".into());
44 }
45 if self.contributions.contains_key(&contrib.party_idx) {
46 return Err("duplicate contribution".into());
47 }
48 self.contributions.insert(contrib.party_idx, contrib);
49 Ok(())
50 }
51
52 pub fn is_complete(&self) -> bool {
53 self.contributions.len() == self.party_count as usize
54 }
55
56 pub fn contribution_count(&self) -> usize {
57 self.contributions.len()
58 }
59
60 pub fn missing_parties(&self) -> Vec<u32> {
61 (1..=self.party_count)
62 .filter(|i| !self.contributions.contains_key(i))
63 .collect()
64 }
65}
66
67pub fn generate_contribution(
69 party_idx: u32,
70 threshold: u32,
71 party_count: u32,
72) -> (DkgContribution, Scalar) {
73 let coeffs: Vec<Scalar> = (0..threshold)
75 .map(|_| Scalar::random(&mut UnwrapErr(SysRng)))
76 .collect();
77
78 let secret = coeffs[0];
79
80 let mut shares = HashMap::new();
82 for i in 1..=party_count {
83 let eval = eval_polynomial(&coeffs, i);
84 let bytes: [u8; 32] = eval.to_repr().into();
85 shares.insert(i, hex::encode(bytes));
86 }
87
88 let commitment = (ProjectivePoint::GENERATOR * secret).to_affine();
90 use p256::elliptic_curve::sec1::ToSec1Point;
91 let commitment_hex = hex::encode(commitment.to_sec1_point(true).as_bytes());
92
93 (
94 DkgContribution {
95 party_idx,
96 shares,
97 commitment_hex,
98 },
99 secret,
100 )
101}
102
103pub fn compute_aggregate_share(session: &DkgSession, party_idx: u32) -> Result<Scalar, String> {
105 if !session.is_complete() {
106 return Err("DKG not complete".into());
107 }
108 let mut aggregate = Scalar::ZERO;
109 for contrib in session.contributions.values() {
110 let share_hex = contrib
111 .shares
112 .get(&party_idx)
113 .ok_or_else(|| format!("missing share for party {party_idx}"))?;
114 let bytes = hex::decode(share_hex).map_err(|e| e.to_string())?;
115 if bytes.len() != 32 {
116 return Err("invalid share length".into());
117 }
118 let arr: [u8; 32] = bytes.as_slice().try_into().unwrap();
119 let fb = FieldBytes::from(arr);
120 let share = Option::<Scalar>::from(Scalar::from_repr(fb))
121 .ok_or_else(|| "invalid scalar".to_string())?;
122 aggregate += share;
123 }
124 Ok(aggregate)
125}
126
127pub fn compute_joint_public_key(session: &DkgSession) -> Result<AffinePoint, String> {
129 if !session.is_complete() {
130 return Err("DKG not complete".into());
131 }
132 use p256::elliptic_curve::sec1::FromSec1Point;
133 let mut sum = ProjectivePoint::IDENTITY;
134 for contrib in session.contributions.values() {
135 let bytes = hex::decode(&contrib.commitment_hex).map_err(|e| e.to_string())?;
136 let encoded = p256::elliptic_curve::sec1::Sec1Point::<p256::NistP256>::from_bytes(&bytes)
137 .map_err(|e| e.to_string())?;
138 let point = Option::<AffinePoint>::from(AffinePoint::from_sec1_point(&encoded))
139 .ok_or_else(|| "invalid commitment point".to_string())?;
140 sum += ProjectivePoint::from(point);
141 }
142 Ok(sum.to_affine())
143}
144
145fn eval_polynomial(coeffs: &[Scalar], x: u32) -> Scalar {
146 let x_scalar = u32_to_scalar(x);
147 let mut result = Scalar::ZERO;
148 let mut x_pow = Scalar::ONE;
149 for c in coeffs {
150 result += c * &x_pow;
151 x_pow *= x_scalar;
152 }
153 result
154}
155
156fn reduce_to_scalar(mut bytes: [u8; 32]) -> Scalar {
159 loop {
160 if let Some(s) = Option::<Scalar>::from(Scalar::from_repr(FieldBytes::from(bytes))) {
161 return s;
162 }
163 let mut h = Sha256::new();
164 h.update(b"confium-scalar-reduce-v1");
165 h.update(bytes);
166 bytes = h.finalize().into();
167 }
168}
169
170fn u32_to_scalar(v: u32) -> Scalar {
171 let mut arr = [0u8; 32];
174 arr[28..32].copy_from_slice(&v.to_be_bytes());
175 let fb = FieldBytes::from(arr);
176 Option::<Scalar>::from(Scalar::from_repr(fb)).unwrap_or(Scalar::ZERO)
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn dkg_session_starts_empty() {
185 let session = DkgSession::new(2, 3);
186 assert_eq!(session.contribution_count(), 0);
187 assert!(!session.is_complete());
188 }
189
190 #[test]
191 fn submit_contribution() {
192 let mut session = DkgSession::new(2, 3);
193 let (contrib, _) = generate_contribution(1, 2, 3);
194 session.submit(contrib).unwrap();
195 assert_eq!(session.contribution_count(), 1);
196 }
197
198 #[test]
199 fn complete_when_all_contributed() {
200 let mut session = DkgSession::new(2, 3);
201 for i in 1..=3 {
202 let (contrib, _) = generate_contribution(i, 2, 3);
203 session.submit(contrib).unwrap();
204 }
205 assert!(session.is_complete());
206 }
207
208 #[test]
209 fn duplicate_contribution_rejected() {
210 let mut session = DkgSession::new(2, 3);
211 let (c1, _) = generate_contribution(1, 2, 3);
212 session.submit(c1).unwrap();
213 let (c2, _) = generate_contribution(1, 2, 3);
214 assert!(session.submit(c2).is_err());
215 }
216
217 #[test]
218 fn aggregate_share_computed() {
219 let mut session = DkgSession::new(2, 3);
220 for i in 1..=3 {
221 let (contrib, _) = generate_contribution(i, 2, 3);
222 session.submit(contrib).unwrap();
223 }
224 let share = compute_aggregate_share(&session, 1).unwrap();
225 assert!(share != Scalar::ZERO);
226 }
227
228 #[test]
229 fn joint_public_key_computed() {
230 let mut session = DkgSession::new(2, 3);
231 for i in 1..=3 {
232 let (contrib, _) = generate_contribution(i, 2, 3);
233 session.submit(contrib).unwrap();
234 }
235 let pk = compute_joint_public_key(&session).unwrap();
236 assert!(pk != AffinePoint::IDENTITY);
238 }
239
240 #[test]
241 fn missing_parties_listed() {
242 let mut session = DkgSession::new(2, 5);
243 let (c1, _) = generate_contribution(1, 2, 5);
244 session.submit(c1).unwrap();
245 let (c3, _) = generate_contribution(3, 2, 5);
246 session.submit(c3).unwrap();
247 assert_eq!(session.missing_parties(), vec![2, 4, 5]);
248 }
249
250 #[test]
251 fn contribution_has_shares_for_all_parties() {
252 let (contrib, _) = generate_contribution(1, 2, 3);
253 assert_eq!(contrib.shares.len(), 3);
254 assert!(contrib.shares.contains_key(&1));
255 assert!(contrib.shares.contains_key(&2));
256 assert!(contrib.shares.contains_key(&3));
257 }
258}