Skip to content

Commit 5730669

Browse files
LLFournclaude
andcommitted
Add Shamir secret sharing helpers for scalar polynomials
This PR makes creating and restoring secret backups easier to work on by adding convenient Shamir secret sharing functionality. Changes: - Add `generate_shamir_sharing_poly` to create polynomial with secret at x=0 - Add `trusted_dealer_shamir_sharing` that returns an iterator of shares - Update `interpolate_and_eval_poly_at_0` docs to clarify it reconstructs secrets - Add comprehensive tests including proptest for random testing The trusted_dealer_shamir_sharing function splits a secret into n shares with a threshold, returning (share_index, share_value) pairs as an iterator for memory efficiency. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e6d7274 commit 5730669

File tree

2 files changed

+151
-1
lines changed

2 files changed

+151
-1
lines changed

secp256kfun/src/poly.rs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,70 @@ pub mod scalar {
4646
(0..threshold).map(|_| Scalar::random(rng)).collect()
4747
}
4848

49-
/// Evalulate the polynomial that passes through the points in `x_and_y` at `0`.
49+
/// Generate a [`Scalar`] polynomial for sharing a particular secret scalar.
50+
///
51+
/// ## Panics
52+
///
53+
/// Panics if `threshold` == 0
54+
pub fn generate_shamir_sharing_poly<Z: ZeroChoice>(
55+
secret: Scalar<Secret, Z>,
56+
threshold: usize,
57+
rng: &mut impl RngCore,
58+
) -> Vec<Scalar<Secret, Z>> {
59+
if threshold == 0 {
60+
panic!("threshold cannot be 0");
61+
}
62+
core::iter::once(secret)
63+
.chain((1..threshold).map(|_| Scalar::random(rng).mark_zero_choice()))
64+
.collect()
65+
}
66+
67+
/// Splits up `secret` into `n_shares` shamir secret shares.
68+
///
69+
/// ## Panics
70+
///
71+
/// - if `n_shares` < `threshold`
72+
/// - if `threshold == 0`
73+
///
74+
/// ## Examples
75+
///
76+
/// ```
77+
/// use secp256kfun::{ prelude::*, poly };
78+
/// use rand::seq::SliceRandom;
79+
/// let my_secret = s!(42);
80+
///
81+
/// let shares: Vec<_> = poly::scalar::trusted_dealer_shamir_sharing(my_secret, 3, 5, &mut rand::thread_rng()).collect();
82+
///
83+
/// // Sample 3 random shares (threshold amount) to reconstruct the secret
84+
/// let mut rng = rand::thread_rng();
85+
/// let random_shares: Vec<_> = shares.choose_multiple(&mut rng, 3).cloned().collect();
86+
///
87+
/// let recovered_secret = poly::scalar::interpolate_and_eval_poly_at_0(&random_shares);
88+
/// assert_eq!(recovered_secret, my_secret);
89+
/// ```
90+
pub fn trusted_dealer_shamir_sharing(
91+
secret: Scalar<Secret, impl ZeroChoice>,
92+
threshold: usize,
93+
n_shares: usize,
94+
rng: &mut impl RngCore,
95+
) -> impl Iterator<Item = (Scalar<Public>, Scalar<Secret, NonZero>)> {
96+
if n_shares < threshold {
97+
panic!("n_shares must be >= threshold");
98+
}
99+
let poly = generate_shamir_sharing_poly(secret, threshold, rng);
100+
(1..=n_shares).map(move |i| {
101+
let i = Scalar::<Public, Zero>::from(i).non_zero().expect("> 0");
102+
(
103+
i,
104+
eval(&poly, i)
105+
.non_zero()
106+
.expect("computationally unreachable if rng is really random"),
107+
)
108+
})
109+
}
110+
111+
/// Evalulate the polynomial that passes through the points in `x_and_y` at `0` i.e. reconstruct
112+
/// the shamir shared secret.
50113
///
51114
/// This is useful for recovering a secret from a set of Sharmir Secret Shares. Each shamir
52115
/// secret share is associated with a participant index (index, share).

secp256kfun/tests/poly.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
#![cfg(feature = "alloc")]
22
use secp256kfun::{poly, prelude::*};
33

4+
#[cfg(feature = "proptest")]
5+
use proptest::prelude::*;
6+
47
#[test]
58
fn test_lagrange_lambda() {
69
let res = s!((1 * 4 * 5) / { s!((1 - 2) * (4 - 2) * (5 - 2)).non_zero().unwrap() });
@@ -127,6 +130,34 @@ fn test_reconstruct_shared_secret() {
127130
assert_eq!(scalar_poly[0], reconstructed_secret);
128131
}
129132

133+
#[test]
134+
fn test_trusted_dealer_shamir_sharing() {
135+
let secret = s!(42);
136+
let threshold = 3;
137+
let n_shares = 5;
138+
139+
let shares: Vec<_> = poly::scalar::trusted_dealer_shamir_sharing(
140+
secret,
141+
threshold,
142+
n_shares,
143+
&mut rand::thread_rng(),
144+
)
145+
.collect();
146+
147+
// Verify we got the expected number of shares
148+
assert_eq!(shares.len(), n_shares);
149+
150+
// Take threshold shares and reconstruct
151+
let selected_shares = &shares[0..threshold];
152+
let reconstructed = poly::scalar::interpolate_and_eval_poly_at_0(selected_shares);
153+
assert_eq!(reconstructed, secret);
154+
155+
// Test with different subset of shares
156+
let selected_shares = &shares[2..5];
157+
let reconstructed = poly::scalar::interpolate_and_eval_poly_at_0(selected_shares);
158+
assert_eq!(reconstructed, secret);
159+
}
160+
130161
#[test]
131162
fn test_mul_scalar_poly() {
132163
let poly1 = [s!(1), s!(2), s!(3)];
@@ -136,3 +167,59 @@ fn test_mul_scalar_poly() {
136167

137168
assert_eq!(res, vec![s!(4), s!(13), s!(22), s!(15)]);
138169
}
170+
171+
#[cfg(feature = "proptest")]
172+
mod proptest_tests {
173+
use super::*;
174+
use rand::seq::SliceRandom;
175+
176+
proptest! {
177+
#[test]
178+
fn trusted_dealer_shamir_sharing_reconstruction(
179+
secret_bytes in any::<[u8; 32]>(),
180+
threshold in 2usize..10usize,
181+
extra_shares in 0usize..5usize,
182+
) {
183+
let secret = Scalar::<Secret, Zero>::from_bytes_mod_order(secret_bytes);
184+
let n_shares = threshold + extra_shares;
185+
186+
let shares: Vec<_> = poly::scalar::trusted_dealer_shamir_sharing(
187+
secret,
188+
threshold,
189+
n_shares,
190+
&mut rand::thread_rng()
191+
).collect();
192+
193+
// Verify we got the expected number of shares
194+
prop_assert_eq!(shares.len(), n_shares);
195+
196+
// Verify all share indices are unique and sequential
197+
for (i, (share_index, _)) in shares.iter().enumerate() {
198+
let expected_index = Scalar::<Public, Zero>::from(i + 1).non_zero().expect("> 0");
199+
prop_assert_eq!(*share_index, expected_index);
200+
}
201+
202+
// Test reconstruction with exactly threshold shares
203+
let mut rng = rand::thread_rng();
204+
let selected_shares: Vec<_> = shares
205+
.choose_multiple(&mut rng, threshold)
206+
.cloned()
207+
.collect();
208+
209+
let reconstructed = poly::scalar::interpolate_and_eval_poly_at_0(&selected_shares);
210+
prop_assert_eq!(reconstructed, secret);
211+
212+
// Test reconstruction with more than threshold shares (overdetermined)
213+
if extra_shares > 0 {
214+
let overdetermined_shares: Vec<_> = shares
215+
.choose_multiple(&mut rng, threshold + 1)
216+
.cloned()
217+
.collect();
218+
219+
let reconstructed_overdetermined =
220+
poly::scalar::interpolate_and_eval_poly_at_0(&overdetermined_shares);
221+
prop_assert_eq!(reconstructed_overdetermined, secret);
222+
}
223+
}
224+
}
225+
}

0 commit comments

Comments
 (0)