confium_crypto_vss/
vss.rs1use p256::FieldBytes;
15use p256::elliptic_curve::PrimeField;
16use p256::elliptic_curve::sec1::{FromSec1Point, ToSec1Point};
17use p256::{AffinePoint, ProjectivePoint, Scalar};
18use sha2::{Digest as _, Sha256};
19
20#[derive(Debug, Clone)]
25pub struct VssCommitment {
26 pub commitments: Vec<AffinePoint>,
28}
29
30impl VssCommitment {
31 pub fn new(commitments: Vec<AffinePoint>) -> Self {
33 Self { commitments }
34 }
35
36 pub fn threshold(&self) -> usize {
38 self.commitments.len()
39 }
40
41 pub fn joint_public_key(&self) -> AffinePoint {
43 self.commitments[0]
44 }
45
46 pub fn verify_share(&self, party_idx_1based: u64, share: Scalar) -> bool {
51 let x = u64_to_scalar(party_idx_1based);
52 let lhs = ProjectivePoint::GENERATOR * share;
53
54 let mut rhs = ProjectivePoint::IDENTITY;
55 let mut x_pow = Scalar::ONE;
56 for c in &self.commitments {
57 let c_proj = ProjectivePoint::from(*c);
58 rhs += c_proj * x_pow;
59 x_pow *= x;
60 }
61 lhs == rhs
62 }
63
64 pub fn encode(&self) -> Vec<u8> {
66 let mut out = Vec::with_capacity(self.commitments.len() * 33);
67 for c in &self.commitments {
68 let encoded = c.to_sec1_point(true);
69 out.extend_from_slice(encoded.as_bytes());
70 }
71 out
72 }
73
74 pub fn decode(bytes: &[u8]) -> Option<Self> {
77 if bytes.is_empty() || bytes.len() % 33 != 0 {
78 return None;
79 }
80 let mut commitments = Vec::with_capacity(bytes.len() / 33);
81 for chunk in bytes.chunks_exact(33) {
82 let point =
83 p256::elliptic_curve::sec1::Sec1Point::<p256::NistP256>::from_bytes(chunk).ok()?;
84 let affine = Option::<AffinePoint>::from(AffinePoint::from_sec1_point(&point))?;
85 commitments.push(affine);
86 }
87 Some(Self { commitments })
88 }
89}
90
91fn u64_to_scalar(v: u64) -> Scalar {
92 let mut arr = [0u8; 32];
93 arr[24..32].copy_from_slice(&v.to_be_bytes());
94 loop {
95 if let Some(s) = Option::<Scalar>::from(Scalar::from_repr(FieldBytes::from(arr))) {
96 return s;
97 }
98 arr = {
99 let mut h = Sha256::new();
100 h.update(b"confium-scalar-reduce-v1");
101 h.update(arr);
102 h.finalize().into()
103 };
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 fn random_scalar() -> Scalar {
112 use p256::elliptic_curve::Field;
113 use p256::elliptic_curve::rand_core::UnwrapErr;
114 Scalar::random(&mut UnwrapErr(getrandom::SysRng))
115 }
116
117 fn make_commitment_for_polynomial(coeffs: &[Scalar]) -> VssCommitment {
118 let commitments: Vec<AffinePoint> = coeffs
119 .iter()
120 .map(|c| (ProjectivePoint::GENERATOR * c).to_affine())
121 .collect();
122 VssCommitment::new(commitments)
123 }
124
125 fn evaluate_polynomial(coeffs: &[Scalar], x: u64) -> Scalar {
126 let x_scalar = u64_to_scalar(x);
127 let mut result = Scalar::ZERO;
128 let mut x_pow = Scalar::ONE;
129 for c in coeffs {
130 result += c * &x_pow;
131 x_pow *= x_scalar;
132 }
133 result
134 }
135
136 #[test]
137 fn valid_share_verifies() {
138 let coeffs: Vec<Scalar> = (0..3).map(|_| random_scalar()).collect();
139 let commitment = make_commitment_for_polynomial(&coeffs);
140 let share = evaluate_polynomial(&coeffs, 1);
141 assert!(commitment.verify_share(1, share));
142 }
143
144 #[test]
145 fn invalid_share_rejected() {
146 let coeffs: Vec<Scalar> = (0..3).map(|_| random_scalar()).collect();
147 let commitment = make_commitment_for_polynomial(&coeffs);
148 let bad_share = random_scalar();
149 assert!(!commitment.verify_share(1, bad_share));
150 }
151
152 #[test]
153 fn multiple_party_indices_verify() {
154 let coeffs: Vec<Scalar> = (0..4).map(|_| random_scalar()).collect();
155 let commitment = make_commitment_for_polynomial(&coeffs);
156 for party_idx in 1..=5u64 {
157 let share = evaluate_polynomial(&coeffs, party_idx);
158 assert!(
159 commitment.verify_share(party_idx, share),
160 "party {party_idx} should verify"
161 );
162 }
163 }
164
165 #[test]
166 fn joint_public_key_is_first_commitment() {
167 let coeffs: Vec<Scalar> = (0..3).map(|_| random_scalar()).collect();
168 let commitment = make_commitment_for_polynomial(&coeffs);
169 let expected_pk = (ProjectivePoint::GENERATOR * coeffs[0]).to_affine();
170 assert_eq!(commitment.joint_public_key(), expected_pk);
171 }
172
173 #[test]
174 fn threshold_is_commitment_count() {
175 let commitment = make_commitment_for_polynomial(&[random_scalar(); 5]);
176 assert_eq!(commitment.threshold(), 5);
177 }
178
179 #[test]
180 fn encode_decode_round_trips() {
181 let coeffs: Vec<Scalar> = (0..3).map(|_| random_scalar()).collect();
182 let commitment = make_commitment_for_polynomial(&coeffs);
183 let encoded = commitment.encode();
184 assert_eq!(encoded.len(), 3 * 33);
185
186 let decoded = VssCommitment::decode(&encoded).unwrap();
187 assert_eq!(decoded.commitments.len(), commitment.commitments.len());
188 for (a, b) in commitment
189 .commitments
190 .iter()
191 .zip(decoded.commitments.iter())
192 {
193 assert_eq!(a, b);
194 }
195 }
196
197 #[test]
198 fn decode_rejects_wrong_length() {
199 assert!(VssCommitment::decode(&[0; 10]).is_none());
200 assert!(VssCommitment::decode(&[0; 34]).is_none());
201 }
202
203 #[test]
204 fn decode_rejects_invalid_point() {
205 assert!(VssCommitment::decode(&[0; 33]).is_none());
206 }
207}