1use elliptic_curve::PrimeField;
25use elliptic_curve::rand_core::UnwrapErr;
26use getrandom::SysRng;
27use p256::{AffinePoint, ProjectivePoint, Scalar};
28
29use confium_tc::Result;
30use confium_tc::message::Message;
31use confium_tc::registry::{RoundResult, SessionImpl};
32use confium_tc::session::SessionParams;
33
34use crate::error::{Cmp20ErrorCode, scheme_error};
35use crate::share::{Cmp20Share, SHARE_BYTES};
36use crate::vss::FeldmanVss;
37
38pub struct Cmp20DkgP256;
40
41impl Cmp20DkgP256 {
42 pub fn build_session(params: &SessionParams) -> Result<Box<dyn SessionImpl>> {
43 let party_id = params.parties.get(params.this_party_idx)?.id.clone();
44 let n = params.parties.len();
45 let t = params.threshold as usize;
46 let party_idx_1based = (params.this_party_idx + 1) as u32;
47 let party_ids: Vec<String> = params
48 .parties
49 .parties()
50 .iter()
51 .map(|p| p.id.clone())
52 .collect();
53
54 let vss = FeldmanVss::deal(&mut UnwrapErr(SysRng), n, t);
55
56 Ok(Box::new(Cmp20DkgSession {
57 party_id,
58 party_idx_1based,
59 party_ids,
60 n,
61 t,
62 our_vss: vss,
63 received_shares: Vec::new(),
64 joint_public_key: None,
65 our_combined_share: None,
66 round_done: 0,
67 }))
68 }
69}
70
71pub struct Cmp20DkgSession {
72 party_id: String,
73 party_idx_1based: u32,
74 party_ids: Vec<String>,
75 n: usize,
76 t: usize,
77 our_vss: FeldmanVss,
78 received_shares: Vec<(u64, Scalar)>,
79 joint_public_key: Option<AffinePoint>,
80 our_combined_share: Option<Scalar>,
81 round_done: u8,
82}
83
84const TAG_COMMITMENTS: u8 = 0xCC;
85const TAG_SHARE: u8 = 0xCE;
86
87impl Cmp20DkgSession {
88 fn round1_deal_and_assemble(&mut self, incoming: &[Message]) -> Result<RoundResult> {
93 let mut outgoing = Vec::with_capacity(1 + self.n);
94
95 let commitments_bytes = FeldmanVss::encode_commitments(&self.our_vss.commitments);
97 let mut bc_payload = Vec::with_capacity(3 + commitments_bytes.len());
98 bc_payload.push(TAG_COMMITMENTS);
99 bc_payload.push(self.party_idx_1based as u8);
100 bc_payload.push(self.our_vss.commitments.len() as u8);
101 bc_payload.extend_from_slice(&commitments_bytes);
102 outgoing.push(Message::broadcast(&self.party_id, 1, bc_payload));
103
104 for (peer_pos, peer_id) in self.party_ids.iter().enumerate() {
108 if peer_id == &self.party_id {
109 continue;
110 }
111 let eval = self.our_vss.shares[peer_pos];
112 let mut payload = Vec::with_capacity(2 + 32);
113 payload.push(TAG_SHARE);
114 payload.push(self.party_idx_1based as u8);
115 payload.extend_from_slice(&eval.to_bytes());
116 outgoing.push(Message::directed(&self.party_id, peer_id, 1, payload));
117 }
118
119 if incoming.is_empty() {
125 return Ok(RoundResult::new(outgoing, false));
126 }
127
128 let mut commitments_by_dealer: Vec<(u64, Vec<AffinePoint>)> = Vec::new();
129 let mut own_evaluations: Vec<(u64, Scalar)> = Vec::new();
130
131 for msg in incoming {
132 if msg.round != 1 || msg.payload.is_empty() {
133 continue;
134 }
135 let tag = msg.payload[0];
136 match tag {
137 TAG_COMMITMENTS => {
138 if msg.payload.len() < 3 {
139 return Err(scheme_error(Cmp20ErrorCode::BAD_ROUND_MESSAGE));
140 }
141 let dealer_idx = msg.payload[1] as u64;
142 let num_c = msg.payload[2] as usize;
143 let expected = 3 + num_c * 33;
144 if msg.payload.len() != expected {
145 return Err(scheme_error(Cmp20ErrorCode::BAD_ROUND_MESSAGE));
146 }
147 let cs = FeldmanVss::decode_commitments(&msg.payload[3..expected])
148 .ok_or_else(|| scheme_error(Cmp20ErrorCode::BAD_ROUND_MESSAGE))?;
149 if cs.len() != num_c || cs.len() < self.t {
150 return Err(scheme_error(Cmp20ErrorCode::VSS_VERIFY_FAILED));
151 }
152 commitments_by_dealer.push((dealer_idx, cs));
153 }
154 TAG_SHARE => {
155 if msg.payload.len() != 2 + 32 {
156 return Err(scheme_error(Cmp20ErrorCode::BAD_ROUND_MESSAGE));
157 }
158 if !msg.is_for(&self.party_id) {
159 continue;
160 }
161 let dealer_idx = msg.payload[1] as u64;
162 let mut eval_bytes = [0u8; 32];
163 eval_bytes.copy_from_slice(&msg.payload[2..34]);
164 let fb: p256::FieldBytes = eval_bytes.into();
165 let eval: Scalar = Option::from(Scalar::from_repr(fb))
166 .ok_or_else(|| scheme_error(Cmp20ErrorCode::BAD_ROUND_MESSAGE))?;
167 own_evaluations.push((dealer_idx, eval));
168 }
169 _ => continue,
170 }
171 }
172
173 own_evaluations.push((
176 self.party_idx_1based as u64,
177 self.our_vss.shares[self.party_idx_1based as usize - 1],
178 ));
179 let self_idx = self.party_idx_1based as u64;
180 if !commitments_by_dealer.iter().any(|(d, _)| *d == self_idx) {
181 commitments_by_dealer.push((self_idx, self.our_vss.commitments.clone()));
182 }
183
184 let mut verified_shares: Vec<(u64, Scalar)> = Vec::new();
185 for (dealer_idx, eval) in &own_evaluations {
186 let commitments = commitments_by_dealer
187 .iter()
188 .find(|(d, _)| d == dealer_idx)
189 .map(|(_, c)| c.as_slice())
190 .ok_or_else(|| scheme_error(Cmp20ErrorCode::VSS_VERIFY_FAILED))?;
191 if !FeldmanVss::verify_share(commitments, self.party_idx_1based as u64, *eval) {
192 return Err(scheme_error(Cmp20ErrorCode::VSS_VERIFY_FAILED));
193 }
194 verified_shares.push((*dealer_idx, *eval));
195 }
196
197 let distinct_dealers: std::collections::HashSet<u64> =
198 verified_shares.iter().map(|(d, _)| *d).collect();
199 if distinct_dealers.len() < self.t {
200 return Err(scheme_error(Cmp20ErrorCode::BELOW_THRESHOLD));
201 }
202
203 let combined: Scalar = verified_shares
204 .iter()
205 .fold(Scalar::ZERO, |acc, &(_, ev)| acc + ev);
206 self.received_shares = verified_shares;
207 self.our_combined_share = Some(combined);
208
209 let mut joint = ProjectivePoint::IDENTITY;
211 for (_, cs) in &commitments_by_dealer {
212 joint += ProjectivePoint::from(cs[0]);
213 }
214 self.joint_public_key = Some(joint.to_affine());
215
216 Ok(RoundResult::done())
217 }
218}
219
220impl SessionImpl for Cmp20DkgSession {
221 fn round(&mut self, incoming: &[Message]) -> Result<RoundResult> {
222 self.round_done = self.round_done.checked_add(1).ok_or_else(|| {
223 confium_tc::error::RoundOverflowSnafu {
224 round: self.round_done,
225 }
226 .build()
227 })?;
228 match self.round_done {
229 1 | 2 => self.round1_deal_and_assemble(incoming),
235 other => Err(confium_tc::error::RoundOverflowSnafu { round: other }.build()),
236 }
237 }
238
239 fn result(&self) -> Result<Vec<u8>> {
240 if self.round_done < 1 || self.our_combined_share.is_none() {
241 return Err(confium_tc::error::SessionNotCompleteSnafu {}.build());
242 }
243 let combined = self
244 .our_combined_share
245 .ok_or_else(|| scheme_error(Cmp20ErrorCode::INTERNAL))?;
246 let pk = self
247 .joint_public_key
248 .ok_or_else(|| scheme_error(Cmp20ErrorCode::INTERNAL))?;
249 let x_i: p256::NonZeroScalar = Option::from(p256::NonZeroScalar::new(combined))
250 .ok_or_else(|| scheme_error(Cmp20ErrorCode::INTERNAL))?;
251 let share = Cmp20Share::from_parts(x_i, pk, self.party_idx_1based);
252 Ok(share.to_bytes())
253 }
254
255 fn destroy(&mut self) {
256 if let Some(s) = self.our_combined_share.take() {
257 let _ = s;
258 }
259 for (_, s) in self.received_shares.drain(..) {
260 let _ = s;
261 }
262 self.our_vss.shares.fill(Scalar::ZERO);
263 }
264}
265
266pub fn parse_share(bytes: &[u8]) -> Result<Cmp20Share> {
268 if bytes.len() != SHARE_BYTES {
269 return Err(scheme_error(Cmp20ErrorCode::BAD_SHARE));
270 }
271 Cmp20Share::from_bytes(bytes)
272}
273
274#[cfg(test)]
275pub(crate) fn reconstruct_secret_for_test(shares: &[Cmp20Share]) -> Scalar {
276 use crate::lagrange;
277 let pairs: Vec<(Scalar, Scalar)> = shares
278 .iter()
279 .map(|s| (Scalar::from(s.party_idx), s.scalar()))
280 .collect();
281 lagrange::lagrange_weighted_sum(&pairs)
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use confium_tc::party::{Party, PartyList};
288 use confium_tc::share::Share;
289 use elliptic_curve::sec1::ToSec1Point;
290
291 fn params(n: usize, t: u32, idx: usize) -> SessionParams {
292 let roster: Vec<Party> = (0..n).map(|i| Party::inproc(format!("p{}", i))).collect();
293 SessionParams {
294 scheme: crate::DKG_SCHEME_NAME.to_string(),
295 parties: PartyList::from_parties(roster),
296 threshold: t,
297 this_party_idx: idx,
298 local_share: None,
299 message: None,
300 }
301 }
302
303 fn run_dkg(n: usize, t: u32) -> Vec<Cmp20Share> {
309 let party_ids: Vec<String> = (0..n).map(|i| format!("p{}", i)).collect();
310 let mut sessions: Vec<Box<dyn SessionImpl>> = (0..n)
311 .map(|i| {
312 let p = params(n, t, i);
313 Cmp20DkgP256::build_session(&p).expect("session")
314 })
315 .collect();
316
317 let mut outgoing_r1: Vec<Vec<Message>> = Vec::new();
319 for sess in sessions.iter_mut() {
320 let r = sess.round(&[]).expect("round 1 deal");
321 assert!(!r.complete, "round 1 must not complete without peer input");
322 outgoing_r1.push(r.outgoing);
323 }
324
325 let mut incoming: Vec<Vec<Message>> = vec![Vec::new(); n];
327 for (sender_pos, outs) in outgoing_r1.iter().enumerate() {
328 for m in outs {
329 for (recv_pos, pid) in party_ids.iter().enumerate() {
330 if recv_pos == sender_pos {
331 continue;
332 }
333 if m.is_for(pid) {
334 incoming[recv_pos].push(m.clone());
335 }
336 }
337 }
338 }
339
340 for (i, sess) in sessions.iter_mut().enumerate() {
343 let r = sess.round(&incoming[i]).expect("round 1 assemble");
344 assert!(
345 r.complete,
346 "DKG must complete after the single broadcast round"
347 );
348 }
349
350 sessions
351 .iter()
352 .map(|s| {
353 let bytes = s.result().expect("result");
354 Cmp20Share::from_bytes(&bytes).expect("share decodes")
355 })
356 .collect()
357 }
358
359 #[test]
360 fn dkg_two_of_three_produces_consistent_shares() {
361 let shares = run_dkg(3, 2);
362 assert_eq!(shares.len(), 3);
363 let pk0 = shares[0].public_key;
364 for s in &shares[1..] {
365 let a = pk0.to_sec1_point(true);
366 let b = s.public_key.to_sec1_point(true);
367 assert_eq!(a.as_bytes(), b.as_bytes(), "joint public key must match");
368 }
369 let secret_01 = reconstruct_secret_for_test(&shares[0..2]);
370 let secret_02 = reconstruct_secret_for_test(&[shares[0].clone(), shares[2].clone()]);
371 let secret_12 = reconstruct_secret_for_test(&shares[1..3]);
372 assert_eq!(secret_01, secret_02);
373 assert_eq!(secret_02, secret_12);
374 let g = ProjectivePoint::GENERATOR;
375 let expected_pk = (g * secret_01).to_affine();
376 let got_pk = shares[0].public_key.to_sec1_point(true);
377 let want_pk = expected_pk.to_sec1_point(true);
378 assert_eq!(got_pk.as_bytes(), want_pk.as_bytes());
379 }
380
381 #[test]
382 fn dkg_three_of_three_produces_consistent_shares() {
383 let shares = run_dkg(3, 3);
384 let secret = reconstruct_secret_for_test(&shares);
385 let g = ProjectivePoint::GENERATOR;
386 let pk = (g * secret).to_affine().to_sec1_point(true);
387 assert_eq!(
388 pk.as_bytes(),
389 shares[0].public_key.to_sec1_point(true).as_bytes()
390 );
391 }
392
393 #[test]
394 fn dkg_share_is_loadable_as_framework_share() {
395 let shares = run_dkg(3, 2);
396 let bytes = shares[0].to_bytes();
397 let fw = Share::new(crate::DKG_SCHEME_NAME, bytes);
398 assert_eq!(fw.scheme(), crate::DKG_SCHEME_NAME);
399 let rt = Share::from_bytes(&fw.to_bytes()).expect("framework decode");
400 assert_eq!(rt.scheme(), crate::DKG_SCHEME_NAME);
401 let inner = Cmp20Share::from_bytes(rt.bytes()).expect("inner decode");
402 assert_eq!(inner.party_idx, shares[0].party_idx);
403 }
404}