diff --git a/secp256kfun/src/poly.rs b/secp256kfun/src/poly.rs index 8fb5a46a..b47cfb84 100644 --- a/secp256kfun/src/poly.rs +++ b/secp256kfun/src/poly.rs @@ -46,7 +46,70 @@ pub mod scalar { (0..threshold).map(|_| Scalar::random(rng)).collect() } - /// Evalulate the polynomial that passes through the points in `x_and_y` at `0`. + /// Generate a [`Scalar`] polynomial for sharing a particular secret scalar. + /// + /// ## Panics + /// + /// Panics if `threshold` == 0 + pub fn generate_shamir_sharing_poly( + secret: Scalar, + threshold: usize, + rng: &mut impl RngCore, + ) -> Vec> { + if threshold == 0 { + panic!("threshold cannot be 0"); + } + core::iter::once(secret) + .chain((1..threshold).map(|_| Scalar::random(rng).mark_zero_choice())) + .collect() + } + + /// Splits up `secret` into `n_shares` shamir secret shares. + /// + /// ## Panics + /// + /// - if `n_shares` < `threshold` + /// - if `threshold == 0` + /// + /// ## Examples + /// + /// ``` + /// use secp256kfun::{ prelude::*, poly }; + /// use rand::seq::SliceRandom; + /// let my_secret = s!(42); + /// + /// let shares: Vec<_> = poly::scalar::trusted_dealer_shamir_sharing(my_secret, 3, 5, &mut rand::thread_rng()).collect(); + /// + /// // Sample 3 random shares (threshold amount) to reconstruct the secret + /// let mut rng = rand::thread_rng(); + /// let random_shares: Vec<_> = shares.choose_multiple(&mut rng, 3).cloned().collect(); + /// + /// let recovered_secret = poly::scalar::interpolate_and_eval_poly_at_0(&random_shares); + /// assert_eq!(recovered_secret, my_secret); + /// ``` + pub fn trusted_dealer_shamir_sharing( + secret: Scalar, + threshold: usize, + n_shares: usize, + rng: &mut impl RngCore, + ) -> impl Iterator, Scalar)> { + if n_shares < threshold { + panic!("n_shares must be >= threshold"); + } + let poly = generate_shamir_sharing_poly(secret, threshold, rng); + (1..=n_shares).map(move |i| { + let i = Scalar::::from(i).non_zero().expect("> 0"); + ( + i, + eval(&poly, i) + .non_zero() + .expect("computationally unreachable if rng is really random"), + ) + }) + } + + /// Evalulate the polynomial that passes through the points in `x_and_y` at `0` i.e. reconstruct + /// the shamir shared secret. /// /// This is useful for recovering a secret from a set of Sharmir Secret Shares. Each shamir /// secret share is associated with a participant index (index, share). diff --git a/secp256kfun/tests/poly.rs b/secp256kfun/tests/poly.rs index f0d341ca..f76839be 100644 --- a/secp256kfun/tests/poly.rs +++ b/secp256kfun/tests/poly.rs @@ -1,6 +1,9 @@ #![cfg(feature = "alloc")] use secp256kfun::{poly, prelude::*}; +#[cfg(feature = "proptest")] +use proptest::prelude::*; + #[test] fn test_lagrange_lambda() { 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() { assert_eq!(scalar_poly[0], reconstructed_secret); } +#[test] +fn test_trusted_dealer_shamir_sharing() { + let secret = s!(42); + let threshold = 3; + let n_shares = 5; + + let shares: Vec<_> = poly::scalar::trusted_dealer_shamir_sharing( + secret, + threshold, + n_shares, + &mut rand::thread_rng(), + ) + .collect(); + + // Verify we got the expected number of shares + assert_eq!(shares.len(), n_shares); + + // Take threshold shares and reconstruct + let selected_shares = &shares[0..threshold]; + let reconstructed = poly::scalar::interpolate_and_eval_poly_at_0(selected_shares); + assert_eq!(reconstructed, secret); + + // Test with different subset of shares + let selected_shares = &shares[2..5]; + let reconstructed = poly::scalar::interpolate_and_eval_poly_at_0(selected_shares); + assert_eq!(reconstructed, secret); +} + #[test] fn test_mul_scalar_poly() { let poly1 = [s!(1), s!(2), s!(3)]; @@ -136,3 +167,59 @@ fn test_mul_scalar_poly() { assert_eq!(res, vec![s!(4), s!(13), s!(22), s!(15)]); } + +#[cfg(feature = "proptest")] +mod proptest_tests { + use super::*; + use rand::seq::SliceRandom; + + proptest! { + #[test] + fn trusted_dealer_shamir_sharing_reconstruction( + secret_bytes in any::<[u8; 32]>(), + threshold in 2usize..10usize, + extra_shares in 0usize..5usize, + ) { + let secret = Scalar::::from_bytes_mod_order(secret_bytes); + let n_shares = threshold + extra_shares; + + let shares: Vec<_> = poly::scalar::trusted_dealer_shamir_sharing( + secret, + threshold, + n_shares, + &mut rand::thread_rng() + ).collect(); + + // Verify we got the expected number of shares + prop_assert_eq!(shares.len(), n_shares); + + // Verify all share indices are unique and sequential + for (i, (share_index, _)) in shares.iter().enumerate() { + let expected_index = Scalar::::from(i + 1).non_zero().expect("> 0"); + prop_assert_eq!(*share_index, expected_index); + } + + // Test reconstruction with exactly threshold shares + let mut rng = rand::thread_rng(); + let selected_shares: Vec<_> = shares + .choose_multiple(&mut rng, threshold) + .cloned() + .collect(); + + let reconstructed = poly::scalar::interpolate_and_eval_poly_at_0(&selected_shares); + prop_assert_eq!(reconstructed, secret); + + // Test reconstruction with more than threshold shares (overdetermined) + if extra_shares > 0 { + let overdetermined_shares: Vec<_> = shares + .choose_multiple(&mut rng, threshold + 1) + .cloned() + .collect(); + + let reconstructed_overdetermined = + poly::scalar::interpolate_and_eval_poly_at_0(&overdetermined_shares); + prop_assert_eq!(reconstructed_overdetermined, secret); + } + } + } +}