use crate::algebra::{Additive, Object, Ring, Space};
#[cfg(not(feature = "std"))]
use alloc::{collections::BTreeMap, vec::Vec};
use commonware_parallel::{Sequential, Strategy};
use core::{
cmp::Ordering,
ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign},
};
#[cfg(feature = "std")]
use std::collections::BTreeMap;
#[derive(Clone, Debug)]
pub struct Synthetic<F, G> {
free: BTreeMap<u32, F>,
concrete: Vec<(F, G)>,
}
impl<F, G> Default for Synthetic<F, G> {
fn default() -> Self {
Self {
free: Default::default(),
concrete: Default::default(),
}
}
}
impl<F, G> Synthetic<F, G> {
pub fn concrete(weighted_points: impl IntoIterator<Item = (F, G)>) -> Self {
Self {
concrete: weighted_points.into_iter().collect(),
..Default::default()
}
}
pub fn max_free_index(&self) -> Option<u32> {
self.free.keys().next_back().copied()
}
fn for_each_weight(&mut self, mut f: impl FnMut(&mut F)) {
self.free.values_mut().for_each(&mut f);
self.concrete.iter_mut().for_each(|(w, _)| f(w));
}
pub fn generators() -> impl Iterator<Item = Self>
where
F: Ring,
{
(0u32..).map(|i| {
let mut out = Self::default();
out.free.insert(i, F::one());
out
})
}
pub fn generators_array<const N: usize>() -> [Self; N]
where
F: Ring,
{
let mut iter = Self::generators();
core::array::from_fn(|_| iter.next().expect("generators is infinite"))
}
}
impl<F: Additive, G: Space<F>> Synthetic<F, G> {
pub fn eval(self, generators: &[G], strategy: &impl Strategy) -> G {
let total = self.free.len() + self.concrete.len();
let mut points = Vec::with_capacity(total);
let mut weights = Vec::with_capacity(total);
for (idx, weight) in self.free {
points.push(generators[idx as usize].clone());
weights.push(weight);
}
for (weight, point) in self.concrete {
points.push(point);
weights.push(weight);
}
G::msm(&points, &weights, strategy)
}
}
impl<F: Additive, G: Space<F>> PartialEq for Synthetic<F, G> {
fn eq(&self, other: &Self) -> bool {
let zero = F::zero();
let mut lhs = self.free.iter().peekable();
let mut rhs = other.free.iter().peekable();
let free_equal = core::iter::from_fn(|| {
let ordering = match (lhs.peek().copied(), rhs.peek().copied()) {
(Some((li, _)), Some((ri, _))) => li.cmp(ri),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => return None,
};
Some(match ordering {
Ordering::Equal => (lhs.next().map(|(_, w)| w), rhs.next().map(|(_, w)| w)),
Ordering::Less => (lhs.next().map(|(_, w)| w), None),
Ordering::Greater => (None, rhs.next().map(|(_, w)| w)),
})
})
.all(|(lw, rw)| lw.unwrap_or(&zero) == rw.unwrap_or(&zero));
if !free_equal {
return false;
}
let size = self.concrete.len() + other.concrete.len();
let mut points = Vec::with_capacity(size);
let mut weights = Vec::with_capacity(size);
for (weight, point) in &self.concrete {
points.push(point.clone());
weights.push(weight.clone());
}
for (weight, point) in &other.concrete {
points.push(point.clone());
weights.push(-weight.clone());
}
G::msm(&points, &weights, &Sequential) == G::zero()
}
}
impl<F: Additive, G: Space<F>> Eq for Synthetic<F, G> {}
impl<F: Additive, G: Space<F>> Object for Synthetic<F, G> {}
impl<'a, F: Additive, G: Space<F>> AddAssign<&'a Self> for Synthetic<F, G> {
fn add_assign(&mut self, rhs: &'a Self) {
for (idx, weight) in &rhs.free {
self.free
.entry(*idx)
.and_modify(|existing| *existing += weight)
.or_insert_with(|| weight.clone());
}
self.concrete.extend(rhs.concrete.iter().cloned());
}
}
impl<'a, F: Additive, G: Space<F>> Add<&'a Self> for Synthetic<F, G> {
type Output = Self;
fn add(mut self, rhs: &'a Self) -> Self::Output {
self += rhs;
self
}
}
impl<'a, F: Additive, G: Space<F>> SubAssign<&'a Self> for Synthetic<F, G> {
fn sub_assign(&mut self, rhs: &'a Self) {
for (idx, weight) in &rhs.free {
self.free
.entry(*idx)
.and_modify(|existing| *existing -= weight)
.or_insert_with(|| -weight.clone());
}
self.concrete.extend(
rhs.concrete
.iter()
.cloned()
.map(|(weight, point)| (-weight, point)),
);
}
}
impl<'a, F: Additive, G: Space<F>> Sub<&'a Self> for Synthetic<F, G> {
type Output = Self;
fn sub(mut self, rhs: &'a Self) -> Self::Output {
self -= rhs;
self
}
}
impl<F: Additive, G: Space<F>> Neg for Synthetic<F, G> {
type Output = Self;
fn neg(mut self) -> Self::Output {
self.for_each_weight(|w| *w = -core::mem::replace(w, F::zero()));
self
}
}
impl<F: Additive, G: Space<F>> Additive for Synthetic<F, G> {
fn zero() -> Self {
Self::default()
}
}
impl<'a, F: Space<F>, G: Space<F>> MulAssign<&'a F> for Synthetic<F, G> {
fn mul_assign(&mut self, rhs: &'a F) {
self.for_each_weight(|w| *w *= rhs);
}
}
impl<'a, F: Space<F>, G: Space<F>> Mul<&'a F> for Synthetic<F, G> {
type Output = Self;
fn mul(mut self, rhs: &'a F) -> Self::Output {
self *= rhs;
self
}
}
impl<F: Space<F>, G: Space<F>> Space<F> for Synthetic<F, G> {}
#[cfg(any(test, feature = "arbitrary"))]
impl<'a, F: arbitrary::Arbitrary<'a>, G: arbitrary::Arbitrary<'a>> arbitrary::Arbitrary<'a>
for Synthetic<F, G>
{
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let len: usize = u.int_in_range(0..=8)?;
let free: BTreeMap<u32, F> = (0..len)
.map(|_| Ok((u.int_in_range(0..=32u32)?, u.arbitrary()?)))
.collect::<arbitrary::Result<_>>()?;
Ok(Self {
free,
concrete: u.arbitrary()?,
})
}
}
#[commonware_macros::stability(ALPHA)]
#[cfg(any(test, feature = "fuzz"))]
pub mod fuzz {
use super::*;
use crate::{
algebra::test_suites,
test::{F, G},
};
use arbitrary::{Arbitrary, Unstructured};
use commonware_parallel::Sequential;
#[derive(Debug, Arbitrary)]
pub enum Plan {
EvalMatchesMsm(Vec<F>, Vec<(F, G)>),
EvalIsLinear(Synthetic<F, G>, Synthetic<F, G>, Vec<G>),
FuzzAdditive,
FuzzSpaceRing,
}
fn cover_generators(
u: &mut Unstructured<'_>,
virtuals: &[&Synthetic<F, G>],
mut gens: Vec<G>,
) -> arbitrary::Result<Vec<G>> {
let needed = virtuals
.iter()
.filter_map(|v| v.max_free_index())
.max()
.map_or(0, |m| m as usize + 1);
while gens.len() < needed {
gens.push(u.arbitrary()?);
}
Ok(gens)
}
impl Plan {
pub fn run(self, u: &mut Unstructured<'_>) -> arbitrary::Result<()> {
match self {
Self::EvalMatchesMsm(free_weights, concrete) => {
let mut expr = Synthetic::<F, G>::zero();
let mut gen_iter = Synthetic::<F, G>::generators();
for w in &free_weights {
expr += &(gen_iter.next().unwrap() * w);
}
expr += &Synthetic::concrete(concrete.iter().cloned());
let gens: Vec<G> = (0..free_weights.len())
.map(|_| u.arbitrary())
.collect::<arbitrary::Result<_>>()?;
let mut points = Vec::with_capacity(free_weights.len() + concrete.len());
let mut weights = Vec::with_capacity(free_weights.len() + concrete.len());
for (i, w) in free_weights.into_iter().enumerate() {
points.push(gens[i]);
weights.push(w);
}
for (w, p) in &concrete {
points.push(*p);
weights.push(*w);
}
assert_eq!(
expr.eval(&gens, &Sequential),
G::msm(&points, &weights, &Sequential)
);
}
Self::EvalIsLinear(lhs, rhs, generators) => {
let gens = cover_generators(u, &[&lhs, &rhs], generators)?;
let lhs_eval = lhs.clone().eval(&gens, &Sequential);
let rhs_eval = rhs.clone().eval(&gens, &Sequential);
assert_eq!((lhs + &rhs).eval(&gens, &Sequential), lhs_eval + &rhs_eval);
}
Self::FuzzAdditive => {
test_suites::fuzz_additive::<Synthetic<F, G>>(u)?;
}
Self::FuzzSpaceRing => {
test_suites::fuzz_space_ring::<F, Synthetic<F, G>>(u)?;
}
}
Ok(())
}
}
#[test]
fn test_fuzz() {
commonware_invariants::minifuzz::test(|u| u.arbitrary::<Plan>()?.run(u));
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::{
algebra::CryptoGroup,
test::{F, G},
};
#[test]
fn generators_array_tracks_indices() {
let empty = Synthetic::<F, G>::zero();
assert_eq!(empty.max_free_index(), None);
let [g0, g1, g2] = Synthetic::<F, G>::generators_array();
assert_eq!(g0.max_free_index(), Some(0));
assert_eq!(g1.max_free_index(), Some(1));
assert_eq!(g2.max_free_index(), Some(2));
}
#[test]
fn equality_handles_zero_free_weights_and_concrete_terms() {
let [g0] = Synthetic::<F, G>::generators_array();
let zero_weight = g0 * &F::zero();
assert_eq!(Synthetic::<F, G>::zero(), zero_weight);
let point = G::generator();
let concrete = Synthetic::concrete([(F::from(3u64), point)]);
assert_eq!(concrete.clone(), concrete);
assert_ne!(concrete, Synthetic::zero());
}
}