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