1use crate::algebra::{Additive, Object, Ring, Space};
35#[cfg(not(feature = "std"))]
36use alloc::{collections::BTreeMap, vec::Vec};
37use commonware_parallel::{Sequential, Strategy};
38use core::{
39 cmp::Ordering,
40 ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign},
41};
42#[cfg(feature = "std")]
43use std::collections::BTreeMap;
44
45#[derive(Clone, Debug)]
50pub struct Synthetic<F, G> {
51 free: BTreeMap<u32, F>,
52 concrete: Vec<(F, G)>,
53}
54
55impl<F, G> Default for Synthetic<F, G> {
56 fn default() -> Self {
57 Self {
58 free: Default::default(),
59 concrete: Default::default(),
60 }
61 }
62}
63
64impl<F, G> Synthetic<F, G> {
65 pub fn concrete(weighted_points: impl IntoIterator<Item = (F, G)>) -> Self {
67 Self {
68 concrete: weighted_points.into_iter().collect(),
69 ..Default::default()
70 }
71 }
72
73 pub fn max_free_index(&self) -> Option<u32> {
75 self.free.keys().next_back().copied()
76 }
77
78 fn for_each_weight(&mut self, mut f: impl FnMut(&mut F)) {
80 self.free.values_mut().for_each(&mut f);
81 self.concrete.iter_mut().for_each(|(w, _)| f(w));
82 }
83
84 pub fn generators() -> impl Iterator<Item = Self>
86 where
87 F: Ring,
88 {
89 (0u32..).map(|i| {
90 let mut out = Self::default();
91 out.free.insert(i, F::one());
92 out
93 })
94 }
95
96 pub fn generators_array<const N: usize>() -> [Self; N]
98 where
99 F: Ring,
100 {
101 let mut iter = Self::generators();
102 core::array::from_fn(|_| iter.next().expect("generators is infinite"))
103 }
104}
105
106impl<F: Additive, G: Space<F>> Synthetic<F, G> {
107 pub fn eval(self, generators: &[G], strategy: &impl Strategy) -> G {
115 let total = self.free.len() + self.concrete.len();
116 let mut points = Vec::with_capacity(total);
117 let mut weights = Vec::with_capacity(total);
118 for (idx, weight) in self.free {
119 points.push(generators[idx as usize].clone());
120 weights.push(weight);
121 }
122 for (weight, point) in self.concrete {
123 points.push(point);
124 weights.push(weight);
125 }
126 G::msm(&points, &weights, strategy)
127 }
128}
129
130impl<F: Additive, G: Space<F>> PartialEq for Synthetic<F, G> {
131 fn eq(&self, other: &Self) -> bool {
132 let zero = F::zero();
133 let mut lhs = self.free.iter().peekable();
134 let mut rhs = other.free.iter().peekable();
135 let free_equal = core::iter::from_fn(|| {
136 let ordering = match (lhs.peek().copied(), rhs.peek().copied()) {
137 (Some((li, _)), Some((ri, _))) => li.cmp(ri),
138 (Some(_), None) => Ordering::Less,
139 (None, Some(_)) => Ordering::Greater,
140 (None, None) => return None,
141 };
142 Some(match ordering {
143 Ordering::Equal => (lhs.next().map(|(_, w)| w), rhs.next().map(|(_, w)| w)),
144 Ordering::Less => (lhs.next().map(|(_, w)| w), None),
145 Ordering::Greater => (None, rhs.next().map(|(_, w)| w)),
146 })
147 })
148 .all(|(lw, rw)| lw.unwrap_or(&zero) == rw.unwrap_or(&zero));
149 if !free_equal {
150 return false;
151 }
152
153 let size = self.concrete.len() + other.concrete.len();
154 let mut points = Vec::with_capacity(size);
155 let mut weights = Vec::with_capacity(size);
156 for (weight, point) in &self.concrete {
157 points.push(point.clone());
158 weights.push(weight.clone());
159 }
160 for (weight, point) in &other.concrete {
161 points.push(point.clone());
162 weights.push(-weight.clone());
163 }
164 G::msm(&points, &weights, &Sequential) == G::zero()
165 }
166}
167
168impl<F: Additive, G: Space<F>> Eq for Synthetic<F, G> {}
169
170impl<F: Additive, G: Space<F>> Object for Synthetic<F, G> {}
171
172impl<'a, F: Additive, G: Space<F>> AddAssign<&'a Self> for Synthetic<F, G> {
173 fn add_assign(&mut self, rhs: &'a Self) {
174 for (idx, weight) in &rhs.free {
175 self.free
176 .entry(*idx)
177 .and_modify(|existing| *existing += weight)
178 .or_insert_with(|| weight.clone());
179 }
180 self.concrete.extend(rhs.concrete.iter().cloned());
181 }
182}
183
184impl<'a, F: Additive, G: Space<F>> Add<&'a Self> for Synthetic<F, G> {
185 type Output = Self;
186
187 fn add(mut self, rhs: &'a Self) -> Self::Output {
188 self += rhs;
189 self
190 }
191}
192
193impl<'a, F: Additive, G: Space<F>> SubAssign<&'a Self> for Synthetic<F, G> {
194 fn sub_assign(&mut self, rhs: &'a Self) {
195 for (idx, weight) in &rhs.free {
196 self.free
197 .entry(*idx)
198 .and_modify(|existing| *existing -= weight)
199 .or_insert_with(|| -weight.clone());
200 }
201 self.concrete.extend(
202 rhs.concrete
203 .iter()
204 .cloned()
205 .map(|(weight, point)| (-weight, point)),
206 );
207 }
208}
209
210impl<'a, F: Additive, G: Space<F>> Sub<&'a Self> for Synthetic<F, G> {
211 type Output = Self;
212
213 fn sub(mut self, rhs: &'a Self) -> Self::Output {
214 self -= rhs;
215 self
216 }
217}
218
219impl<F: Additive, G: Space<F>> Neg for Synthetic<F, G> {
220 type Output = Self;
221
222 fn neg(mut self) -> Self::Output {
223 self.for_each_weight(|w| *w = -core::mem::replace(w, F::zero()));
224 self
225 }
226}
227
228impl<F: Additive, G: Space<F>> Additive for Synthetic<F, G> {
229 fn zero() -> Self {
230 Self::default()
231 }
232}
233
234impl<'a, F: Space<F>, G: Space<F>> MulAssign<&'a F> for Synthetic<F, G> {
235 fn mul_assign(&mut self, rhs: &'a F) {
236 self.for_each_weight(|w| *w *= rhs);
237 }
238}
239
240impl<'a, F: Space<F>, G: Space<F>> Mul<&'a F> for Synthetic<F, G> {
241 type Output = Self;
242
243 fn mul(mut self, rhs: &'a F) -> Self::Output {
244 self *= rhs;
245 self
246 }
247}
248
249impl<F: Space<F>, G: Space<F>> Space<F> for Synthetic<F, G> {}
250
251#[cfg(any(test, feature = "arbitrary"))]
252impl<'a, F: arbitrary::Arbitrary<'a>, G: arbitrary::Arbitrary<'a>> arbitrary::Arbitrary<'a>
253 for Synthetic<F, G>
254{
255 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
256 let len: usize = u.int_in_range(0..=8)?;
257 let free: BTreeMap<u32, F> = (0..len)
258 .map(|_| Ok((u.int_in_range(0..=32u32)?, u.arbitrary()?)))
259 .collect::<arbitrary::Result<_>>()?;
260 Ok(Self {
261 free,
262 concrete: u.arbitrary()?,
263 })
264 }
265}
266
267#[commonware_macros::stability(ALPHA)]
268#[cfg(any(test, feature = "fuzz"))]
269pub mod fuzz {
270 use super::*;
271 use crate::{
272 algebra::test_suites,
273 test::{F, G},
274 };
275 use arbitrary::{Arbitrary, Unstructured};
276 use commonware_parallel::Sequential;
277
278 #[derive(Debug, Arbitrary)]
279 pub enum Plan {
280 EvalMatchesMsm(Vec<F>, Vec<(F, G)>),
281 EvalIsLinear(Synthetic<F, G>, Synthetic<F, G>, Vec<G>),
282 FuzzAdditive,
283 FuzzSpaceRing,
284 }
285
286 fn cover_generators(
287 u: &mut Unstructured<'_>,
288 virtuals: &[&Synthetic<F, G>],
289 mut gens: Vec<G>,
290 ) -> arbitrary::Result<Vec<G>> {
291 let needed = virtuals
292 .iter()
293 .filter_map(|v| v.max_free_index())
294 .max()
295 .map_or(0, |m| m as usize + 1);
296 while gens.len() < needed {
297 gens.push(u.arbitrary()?);
298 }
299 Ok(gens)
300 }
301
302 impl Plan {
303 pub fn run(self, u: &mut Unstructured<'_>) -> arbitrary::Result<()> {
304 match self {
305 Self::EvalMatchesMsm(free_weights, concrete) => {
306 let mut expr = Synthetic::<F, G>::zero();
307 let mut gen_iter = Synthetic::<F, G>::generators();
308 for w in &free_weights {
309 expr += &(gen_iter.next().unwrap() * w);
310 }
311 expr += &Synthetic::concrete(concrete.iter().cloned());
312
313 let gens: Vec<G> = (0..free_weights.len())
314 .map(|_| u.arbitrary())
315 .collect::<arbitrary::Result<_>>()?;
316
317 let mut points = Vec::with_capacity(free_weights.len() + concrete.len());
318 let mut weights = Vec::with_capacity(free_weights.len() + concrete.len());
319 for (i, w) in free_weights.into_iter().enumerate() {
320 points.push(gens[i]);
321 weights.push(w);
322 }
323 for (w, p) in &concrete {
324 points.push(*p);
325 weights.push(*w);
326 }
327
328 assert_eq!(
329 expr.eval(&gens, &Sequential),
330 G::msm(&points, &weights, &Sequential)
331 );
332 }
333 Self::EvalIsLinear(lhs, rhs, generators) => {
334 let gens = cover_generators(u, &[&lhs, &rhs], generators)?;
335 let lhs_eval = lhs.clone().eval(&gens, &Sequential);
336 let rhs_eval = rhs.clone().eval(&gens, &Sequential);
337 assert_eq!((lhs + &rhs).eval(&gens, &Sequential), lhs_eval + &rhs_eval);
338 }
339 Self::FuzzAdditive => {
340 test_suites::fuzz_additive::<Synthetic<F, G>>(u)?;
341 }
342 Self::FuzzSpaceRing => {
343 test_suites::fuzz_space_ring::<F, Synthetic<F, G>>(u)?;
344 }
345 }
346 Ok(())
347 }
348 }
349
350 #[test]
351 fn test_fuzz() {
352 commonware_invariants::minifuzz::test(|u| u.arbitrary::<Plan>()?.run(u));
353 }
354}
355
356#[cfg(test)]
357mod test {
358 use super::*;
359 use crate::{
360 algebra::CryptoGroup,
361 test::{F, G},
362 };
363
364 #[test]
365 fn generators_array_tracks_indices() {
366 let empty = Synthetic::<F, G>::zero();
367 assert_eq!(empty.max_free_index(), None);
368
369 let [g0, g1, g2] = Synthetic::<F, G>::generators_array();
370 assert_eq!(g0.max_free_index(), Some(0));
371 assert_eq!(g1.max_free_index(), Some(1));
372 assert_eq!(g2.max_free_index(), Some(2));
373 }
374
375 #[test]
376 fn equality_handles_zero_free_weights_and_concrete_terms() {
377 let [g0] = Synthetic::<F, G>::generators_array();
378 let zero_weight = g0 * &F::zero();
379 assert_eq!(Synthetic::<F, G>::zero(), zero_weight);
380
381 let point = G::generator();
382 let concrete = Synthetic::concrete([(F::from(3u64), point)]);
383 assert_eq!(concrete.clone(), concrete);
384 assert_ne!(concrete, Synthetic::zero());
385 }
386}