Skip to main content

commonware_cryptography/bls12381/primitives/
sharing.rs

1use crate::bls12381::primitives::{Error, group::Scalar, variant::Variant};
2#[cfg(not(feature = "std"))]
3use alloc::sync::Arc;
4#[cfg(not(feature = "std"))]
5use alloc::vec::Vec;
6use cfg_if::cfg_if;
7use commonware_codec::{
8    EncodeSize, FixedSize, Mode as CodecMode, RangeCfg, Read, ReadExt, Write, mode,
9};
10use commonware_macros::stability;
11#[stability(ALPHA)]
12use commonware_math::algebra::{FieldNTT, Ring};
13use commonware_math::poly::{Interpolator, Poly};
14use commonware_parallel::Sequential;
15use commonware_utils::{NZU32, Participant, ordered::Set};
16#[stability(ALPHA)]
17use commonware_utils::{TryFromIterator, ordered::BiMap};
18#[cfg(feature = "std")]
19use core::iter;
20use core::num::NonZeroU32;
21#[cfg(feature = "std")]
22use std::sync::{Arc, OnceLock};
23#[cfg(feature = "std")]
24use std::vec::Vec;
25
26/// Configures how participants are assigned shares of a secret.
27///
28/// More specifically, this configures how evaluation points of a polynomial
29/// are assigned to participant identities.
30#[derive(Copy, Clone, PartialEq, Eq, Debug)]
31#[repr(u8)]
32pub enum Mode {
33    NonZeroCounter = 0,
34
35    /// Assigns participants to powers of a root of unity.
36    ///
37    /// This mode enables sub-quadratic interpolation using NTT-based algorithms.
38    #[cfg(not(any(
39        commonware_stability_BETA,
40        commonware_stability_GAMMA,
41        commonware_stability_DELTA,
42        commonware_stability_EPSILON,
43        commonware_stability_RESERVED
44    )))]
45    RootsOfUnity = 1,
46}
47
48impl From<Mode> for CodecMode {
49    fn from(mode: Mode) -> Self {
50        match mode {
51            Mode::NonZeroCounter => mode!(0),
52            #[cfg(not(any(
53                commonware_stability_BETA,
54                commonware_stability_GAMMA,
55                commonware_stability_DELTA,
56                commonware_stability_EPSILON,
57                commonware_stability_RESERVED
58            )))]
59            Mode::RootsOfUnity => mode!(1),
60        }
61    }
62}
63
64impl Mode {
65    /// Compute the scalar for one participant.
66    ///
67    /// This will return `None` only if `i >= total`.
68    pub(crate) fn scalar(self, total: NonZeroU32, i: Participant) -> Option<Scalar> {
69        if i.get() >= total.get() {
70            return None;
71        }
72        match self {
73            Self::NonZeroCounter => {
74                // Adding 1 is critical, because f(0) will contain the secret.
75                Some(Scalar::from_u64(i.get() as u64 + 1))
76            }
77            #[cfg(not(any(
78                commonware_stability_BETA,
79                commonware_stability_GAMMA,
80                commonware_stability_DELTA,
81                commonware_stability_EPSILON,
82                commonware_stability_RESERVED
83            )))]
84            Self::RootsOfUnity => {
85                // Participant i gets w^i. Since w^i != 0 for any i, this never
86                // collides with the secret at f(0).
87                let size = (total.get() as u64).next_power_of_two();
88                let lg_size = size.ilog2() as u8;
89                let w = Scalar::root_of_unity(lg_size).expect("domain too large for NTT");
90                Some(w.exp(&[i.get() as u64]))
91            }
92        }
93    }
94
95    /// Compute the scalars for all participants.
96    #[cfg(feature = "std")]
97    pub(crate) fn all_scalars(self, total: NonZeroU32) -> Vec<Scalar> {
98        match self {
99            Self::NonZeroCounter => (0..total.get())
100                .map(|i| Scalar::from_u64(i as u64 + 1))
101                .collect(),
102            #[cfg(not(any(
103                commonware_stability_BETA,
104                commonware_stability_GAMMA,
105                commonware_stability_DELTA,
106                commonware_stability_EPSILON,
107                commonware_stability_RESERVED
108            )))]
109            Self::RootsOfUnity => {
110                let size = (total.get() as u64).next_power_of_two();
111                let lg_size = size.ilog2() as u8;
112                let w = Scalar::root_of_unity(lg_size).expect("domain too large for NTT");
113                (0..total.get())
114                    .scan(Scalar::one(), |state, _| {
115                        let val = state.clone();
116                        *state *= &w;
117                        Some(val)
118                    })
119                    .collect()
120            }
121        }
122    }
123
124    /// Create an interpolator for this mode, given a set of indices.
125    ///
126    /// This will return `None` if:
127    /// - any `to_index` call on the provided `indices` returns `None`,
128    /// - any index returned by `to_index` is >= `total`.
129    ///
130    /// To be generic over different use cases, we need:
131    /// - the total number of participants,
132    /// - a set of indices (of any type),
133    /// - a means to convert indices to Participant values.
134    fn interpolator<I: Clone + Ord>(
135        self,
136        total: NonZeroU32,
137        indices: &Set<I>,
138        to_index: impl Fn(&I) -> Option<Participant>,
139    ) -> Option<Interpolator<I, Scalar>> {
140        match self {
141            Self::NonZeroCounter => {
142                let mut count = 0;
143                let iter = indices
144                    .iter()
145                    .filter_map(|i| {
146                        let scalar = self.scalar(total, to_index(i)?)?;
147                        Some((i.clone(), scalar))
148                    })
149                    .inspect(|_| {
150                        count += 1;
151                    });
152                let out = Interpolator::new(iter);
153                // If any indices fail to produce a scalar, reject.
154                if count != indices.len() {
155                    return None;
156                }
157                Some(out)
158            }
159            #[cfg(not(any(
160                commonware_stability_BETA,
161                commonware_stability_GAMMA,
162                commonware_stability_DELTA,
163                commonware_stability_EPSILON,
164                commonware_stability_RESERVED
165            )))]
166            Self::RootsOfUnity => {
167                // For roots of unity mode, we use the fast O(n log n) interpolation.
168                // Participant i maps to exponent i, so the evaluation point is w^i.
169                let size = (total.get() as u64).next_power_of_two();
170                let ntt_total = NonZeroU32::new(u32::try_from(size).ok()?)?;
171
172                let mut count = 0;
173                let points: Vec<(I, u32)> = indices
174                    .iter()
175                    .filter_map(|i| {
176                        let participant = to_index(i)?;
177                        if participant.get() >= total.get() {
178                            return None;
179                        }
180                        count += 1;
181                        Some((i.clone(), participant.get()))
182                    })
183                    .collect();
184
185                // If any indices fail to produce a scalar, reject.
186                if count != indices.len() {
187                    return None;
188                }
189
190                let points = BiMap::try_from_iter(points).ok()?;
191                Some(Interpolator::roots_of_unity(ntt_total, points))
192            }
193        }
194    }
195
196    /// Create an interpolator for this mode, given a set, and a subset.
197    ///
198    /// The set determines the total number of participants to use for interpolation,
199    /// and the indices that will get assigned to the subset.
200    ///
201    /// This function will return `None` only if `subset` contains elements
202    /// not in `set`.
203    #[cfg(feature = "std")]
204    pub(crate) fn subset_interpolator<I: Clone + Ord>(
205        self,
206        set: &Set<I>,
207        subset: &Set<I>,
208    ) -> Option<Interpolator<I, Scalar>> {
209        let Ok(total) = NonZeroU32::try_from(set.len() as u32) else {
210            return Some(Interpolator::new(iter::empty()));
211        };
212        self.interpolator(total, subset, |i| {
213            set.position(i).map(Participant::from_usize)
214        })
215    }
216}
217
218impl FixedSize for Mode {
219    const SIZE: usize = 1;
220}
221
222impl Write for Mode {
223    fn write(&self, buf: &mut impl bytes::BufMut) {
224        buf.put_u8(CodecMode::from(*self).into());
225    }
226}
227
228/// Determines which modes can be parsed.
229///
230/// As modes have been added over time, this versioning mechanism helps with
231/// supporting compatibility.
232///
233/// This allows upgrading to a new version of the library, including more modes,
234/// while using this version to determine which modes are supported at runtime.
235#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
236pub struct ModeVersion(u8);
237
238impl ModeVersion {
239    /// Version 0, supporting:
240    ///
241    /// - [`Mode::NonZeroCounter`]
242    pub const fn v0() -> Self {
243        Self(0)
244    }
245
246    /// Version 1, supporting v0, and:
247    ///
248    /// - [`Mode::RootsOfUnity`]
249    #[stability(ALPHA)]
250    pub const fn v1() -> Self {
251        Self(1)
252    }
253
254    /// Returns whether this version supports `mode`.
255    pub const fn supports(&self, mode: &Mode) -> bool {
256        match mode {
257            Mode::NonZeroCounter => true,
258            #[cfg(not(any(
259                commonware_stability_BETA,
260                commonware_stability_GAMMA,
261                commonware_stability_DELTA,
262                commonware_stability_EPSILON,
263                commonware_stability_RESERVED
264            )))]
265            Mode::RootsOfUnity => self.0 >= 1,
266        }
267    }
268}
269
270impl Read for Mode {
271    type Cfg = ModeVersion;
272
273    fn read_cfg(
274        buf: &mut impl bytes::Buf,
275        version: &Self::Cfg,
276    ) -> Result<Self, commonware_codec::Error> {
277        let tag: u8 = ReadExt::read(buf)?;
278        let mode = match tag {
279            0 => Self::NonZeroCounter,
280            #[cfg(not(any(
281                commonware_stability_BETA,
282                commonware_stability_GAMMA,
283                commonware_stability_DELTA,
284                commonware_stability_EPSILON,
285                commonware_stability_RESERVED
286            )))]
287            1 => Self::RootsOfUnity,
288            o => return Err(commonware_codec::Error::InvalidEnum(o)),
289        };
290        if !version.supports(&mode) {
291            return Err(commonware_codec::Error::Invalid(
292                "Mode",
293                "unsupported mode for version",
294            ));
295        }
296        Ok(mode)
297    }
298}
299
300/// Represents the public output of a polynomial secret sharing.
301///
302/// This does not contain any secret information.
303#[derive(Clone, Debug)]
304pub struct Sharing<V: Variant> {
305    mode: Mode,
306    total: NonZeroU32,
307    poly: Arc<Poly<V::Public>>,
308    #[cfg(feature = "std")]
309    evals: Arc<Vec<OnceLock<V::Public>>>,
310}
311
312impl<V: Variant> PartialEq for Sharing<V> {
313    fn eq(&self, other: &Self) -> bool {
314        self.mode == other.mode && self.total == other.total && self.poly == other.poly
315    }
316}
317
318impl<V: Variant> Eq for Sharing<V> {}
319
320impl<V: Variant> Sharing<V> {
321    pub(crate) fn new(mode: Mode, total: NonZeroU32, poly: Poly<V::Public>) -> Self {
322        Self {
323            mode,
324            total,
325            poly: Arc::new(poly),
326            #[cfg(feature = "std")]
327            evals: Arc::new(vec![OnceLock::new(); total.get() as usize]),
328        }
329    }
330
331    /// Get the mode used for this sharing.
332    #[cfg(feature = "std")]
333    pub(crate) const fn mode(&self) -> Mode {
334        self.mode
335    }
336
337    pub(crate) fn scalar(&self, i: Participant) -> Option<Scalar> {
338        self.mode.scalar(self.total, i)
339    }
340
341    #[cfg(feature = "std")]
342    fn all_scalars(&self) -> Vec<Scalar> {
343        self.mode.all_scalars(self.total)
344    }
345
346    /// Return the number of participants required to recover the secret.
347    ///
348    /// This is one more than the polynomial's [`Poly::degree_exact`].
349    pub fn required(&self) -> u32 {
350        // A polynomial has at most u32::MAX coefficients, so its exact degree
351        // is at most u32::MAX - 1.
352        self.poly.degree_exact() + 1
353    }
354
355    /// Return the total number of participants in this sharing.
356    pub const fn total(&self) -> NonZeroU32 {
357        self.total
358    }
359
360    /// Create an interpolator over some indices.
361    ///
362    /// This will return an error if any of the indices are >= [`Self::total`].
363    pub(crate) fn interpolator(
364        &self,
365        indices: &Set<Participant>,
366    ) -> Result<Interpolator<Participant, Scalar>, Error> {
367        self.mode
368            .interpolator(self.total, indices, |&x| Some(x))
369            .ok_or(Error::InvalidIndex)
370    }
371
372    /// Call this to pre-compute the results of [`Self::partial_public`].
373    ///
374    /// This should be used if you expect to access many of the partial public
375    /// keys, e.g. if verifying several public signatures.
376    ///
377    /// The first time this method is called can be expensive, but subsequent
378    /// calls are idempotent, and cheap.
379    #[cfg(feature = "std")]
380    pub fn precompute_partial_publics(&self) {
381        // NOTE: once we add more interpolation methods, this can be smarter.
382        self.evals
383            .iter()
384            .zip(self.all_scalars())
385            .for_each(|(e, s)| {
386                e.get_or_init(|| self.poly.eval_msm(&s, &Sequential));
387            })
388    }
389
390    /// Get the partial public key associated with a given participant.
391    ///
392    /// Returns [`Error::InvalidIndex`] if the index is greater than or equal to
393    /// [`Self::total`].
394    pub fn partial_public(&self, i: Participant) -> Result<V::Public, Error> {
395        cfg_if! {
396            if #[cfg(feature = "std")] {
397                self.evals
398                    .get(usize::from(i))
399                    .map(|e| {
400                        *e.get_or_init(|| {
401                            self.poly
402                                .eval_msm(&self.scalar(i).expect("i < total"), &Sequential)
403                        })
404                    })
405                    .ok_or(Error::InvalidIndex)
406            } else {
407                Ok(self
408                    .poly
409                    .eval_msm(&self.scalar(i).ok_or(Error::InvalidIndex)?, &Sequential))
410            }
411        }
412    }
413
414    /// Get the group public key of this sharing.
415    ///
416    /// In other words, the public key associated with the shared secret.
417    pub fn public(&self) -> &V::Public {
418        self.poly.constant()
419    }
420}
421
422impl<V: Variant> EncodeSize for Sharing<V> {
423    fn encode_size(&self) -> usize {
424        self.mode.encode_size() + self.total.get().encode_size() + self.poly.encode_size()
425    }
426}
427
428impl<V: Variant> Write for Sharing<V> {
429    fn write(&self, buf: &mut impl bytes::BufMut) {
430        self.mode.write(buf);
431        self.total.get().write(buf);
432        self.poly.write(buf);
433    }
434}
435
436impl<V: Variant> Read for Sharing<V> {
437    type Cfg = (NonZeroU32, ModeVersion);
438
439    fn read_cfg(
440        buf: &mut impl bytes::Buf,
441        (max_participants, max_supported_mode): &Self::Cfg,
442    ) -> Result<Self, commonware_codec::Error> {
443        let mode = Read::read_cfg(buf, max_supported_mode)?;
444        // We bound total to the config, in order to prevent doing arbitrary
445        // computation if we precompute public keys.
446        let total = {
447            let out: u32 = ReadExt::read(buf)?;
448            if out == 0 || out > max_participants.get() {
449                return Err(commonware_codec::Error::Invalid(
450                    "Sharing",
451                    "total not in range",
452                ));
453            }
454            // This will not panic, because we checked != 0 above.
455            NZU32!(out)
456        };
457        let poly = Read::read_cfg(buf, &(RangeCfg::from(NZU32!(1)..=*max_participants), ()))?;
458        Ok(Self::new(mode, total, poly))
459    }
460}
461
462#[cfg(all(test, feature = "std"))]
463mod tests {
464    use super::*;
465    use crate::bls12381::primitives::variant::MinSig;
466    use commonware_invariants::minifuzz;
467    use commonware_utils::{TestRng, ordered::Map};
468
469    #[test]
470    fn test_roots_of_unity_interpolator_large_total_returns_none() {
471        let total = NonZeroU32::new(u32::MAX).expect("u32::MAX is non-zero");
472        let indices = Set::from_iter_dedup([Participant::new(0)]);
473        let interpolator =
474            Mode::RootsOfUnity.interpolator(total, &indices, |participant| Some(*participant));
475        assert!(
476            interpolator.is_none(),
477            "domain > u32::MAX should be rejected instead of panicking"
478        );
479    }
480
481    #[test]
482    fn test_mode_read_rejects_mode_above_max_supported_mode() {
483        let encoded = [1];
484        Mode::read_cfg(&mut &encoded[..], &ModeVersion::v0())
485            .expect_err("roots mode must be rejected when max mode is counter");
486    }
487
488    #[test]
489    fn test_all_scalars_matches_scalar() {
490        minifuzz::test(|u| {
491            let mode = match u.int_in_range(0u8..=1)? {
492                0 => Mode::NonZeroCounter,
493                1 => Mode::RootsOfUnity,
494                _ => unreachable!("range is 0..=1"),
495            };
496            let total = NonZeroU32::new(u.int_in_range(1u32..=512u32)?).expect("range is non-zero");
497            let index = u.int_in_range(0u32..=total.get() - 1)?;
498            let participant = Participant::new(index);
499
500            let scalars = mode.all_scalars(total);
501            assert_eq!(
502                scalars[usize::from(participant)].clone(),
503                mode.scalar(total, participant).expect("index is in range")
504            );
505            Ok(())
506        });
507    }
508
509    #[test]
510    fn test_required_uses_exact_degree() {
511        // Subtraction preserves the three coefficient slots while setting every
512        // coefficient to zero.
513        let polynomial = Poly::<Scalar>::new(TestRng::new(0), 2);
514        let padded_zero = polynomial.clone() - &polynomial;
515        assert_eq!(padded_zero.required().get(), 3);
516
517        // The padded zero polynomial has exact degree zero, so recovering its
518        // secret requires only one share.
519        let sharing =
520            Sharing::<MinSig>::new(Mode::NonZeroCounter, NZU32!(4), Poly::commit(padded_zero));
521        assert_eq!(sharing.required(), 1);
522    }
523
524    #[test]
525    fn test_subset_interpolation_recovers_constant() {
526        minifuzz::test(|u| {
527            let mode = match u.int_in_range(0u8..=1)? {
528                0 => Mode::NonZeroCounter,
529                1 => Mode::RootsOfUnity,
530                _ => unreachable!("range is 0..=1"),
531            };
532            let total = NonZeroU32::new(u.int_in_range(1u32..=64u32)?).expect("range is non-zero");
533
534            let mut subset_vec = Vec::new();
535            for i in 0..total.get() {
536                if u.arbitrary::<bool>()? {
537                    subset_vec.push(Participant::new(i));
538                }
539            }
540            if subset_vec.is_empty() {
541                let i = u.int_in_range(0u32..=total.get() - 1)?;
542                subset_vec.push(Participant::new(i));
543            }
544            let subset = Set::from_iter_dedup(subset_vec);
545
546            let max_degree = u32::try_from(subset.len() - 1).expect("subset len fits in u32");
547            let degree = u.int_in_range(0u32..=max_degree)?;
548            let seed: u64 = u.arbitrary()?;
549            let poly: Poly<Scalar> = Poly::new(TestRng::new(seed), degree);
550
551            let all_shares = Map::from_iter_dedup((0..total.get()).map(|i| {
552                let participant = Participant::new(i);
553                let scalar = mode.scalar(total, participant).expect("in range");
554                let share = poly.eval(&scalar);
555                (participant, share)
556            }));
557
558            let subset_evals = Map::from_iter_dedup(subset.iter().map(|participant| {
559                (
560                    *participant,
561                    all_shares
562                        .get_value(participant)
563                        .expect("participant exists")
564                        .clone(),
565                )
566            }));
567
568            let interpolator = mode
569                .interpolator(total, &subset, |participant| Some(*participant))
570                .expect("subset indices are valid");
571            let recovered = interpolator
572                .interpolate(&subset_evals, &Sequential)
573                .expect("subset should match interpolator domain");
574
575            assert_eq!(recovered, poly.constant().clone());
576            Ok(())
577        });
578    }
579}
580
581#[cfg(feature = "arbitrary")]
582mod fuzz {
583    use super::*;
584    use arbitrary::Arbitrary;
585    use commonware_utils::{Faults, N3f1, NZU32, TestRng};
586
587    impl<'a> Arbitrary<'a> for Mode {
588        fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
589            match u.int_in_range(0u8..=1)? {
590                0 => Ok(Self::NonZeroCounter),
591                1 => Ok(Self::RootsOfUnity),
592                _ => Err(arbitrary::Error::IncorrectFormat),
593            }
594        }
595    }
596
597    impl<'a, V: Variant> Arbitrary<'a> for Sharing<V> {
598        fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
599            let total: u32 = u.int_in_range(1..=100)?;
600            let mode: Mode = u.arbitrary()?;
601            let seed: u64 = u.arbitrary()?;
602            let poly = Poly::new(TestRng::new(seed), N3f1::quorum(total) - 1);
603            Ok(Self::new(
604                mode,
605                NZU32!(total),
606                Poly::<V::Public>::commit(poly),
607            ))
608        }
609    }
610}