sigma_proofs/linear_relation/
canonical.rs1use alloc::format;
2use alloc::vec::Vec;
3use core::iter;
4use core::marker::PhantomData;
5use itertools::Itertools;
6
7use ff::Field;
8use group::prime::PrimeGroup;
9use subtle::{Choice, ConstantTimeEq};
10
11use super::{GroupMap, GroupVar, LinearCombination, LinearRelation, ScalarTerm, ScalarVar};
12use crate::errors::{Error, InvalidInstance};
13use crate::group::msm::MultiScalarMul;
14
15#[derive(Clone, Debug, Default)]
25pub struct CanonicalLinearRelation<G: PrimeGroup> {
26 pub image: Vec<GroupVar<G>>,
28 pub linear_combinations: Vec<Vec<(ScalarVar<G>, GroupVar<G>)>>,
31 pub group_elements: GroupMap<G>,
33 pub num_scalars: usize,
35}
36
37type WeightedGroupCache<G> = Vec<Vec<(<G as group::Group>::Scalar, GroupVar<G>)>>;
41
42impl<G: PrimeGroup> CanonicalLinearRelation<G> {
43 fn new() -> Self {
48 Self {
49 image: Vec::new(),
50 linear_combinations: Vec::new(),
51 group_elements: GroupMap::default(),
52 num_scalars: 0,
53 }
54 }
55
56 pub fn evaluate(&self, scalars: &[G::Scalar]) -> Vec<G>
67 where
68 G: MultiScalarMul,
69 {
70 self.linear_combinations
71 .iter()
72 .map(|lc| {
73 let scalars = lc
74 .iter()
75 .map(|(scalar_var, _)| scalars[scalar_var.index()])
76 .collect::<Vec<_>>();
77 let bases = lc
78 .iter()
79 .map(|(_, group_var)| self.group_elements.get(*group_var).unwrap())
80 .collect::<Vec<_>>();
81 G::msm(&scalars, &bases)
82 })
83 .collect()
84 }
85
86 fn get_or_create_weighted_group_var(
88 &mut self,
89 group_var: GroupVar<G>,
90 weight: &G::Scalar,
91 original_group_elements: &GroupMap<G>,
92 weighted_group_cache: &mut WeightedGroupCache<G>,
93 ) -> Result<GroupVar<G>, InvalidInstance> {
94 let index = group_var.index();
96 if weighted_group_cache.len() <= index {
97 weighted_group_cache.resize_with(index + 1, Vec::new);
98 }
99 let entry = &mut weighted_group_cache[index];
100
101 if let Some((_, existing_var)) = entry.iter().find(|(w, _)| w == weight) {
103 return Ok(*existing_var);
104 }
105
106 let original_group_val = original_group_elements.get(group_var)?;
109 let weighted_group = match *weight == G::Scalar::ONE {
110 true => original_group_val,
111 false => original_group_val * weight,
112 };
113
114 let new_var = self.group_elements.push(weighted_group);
116
117 entry.push((*weight, new_var));
119
120 Ok(new_var)
121 }
122
123 fn process_constraint(
125 &mut self,
126 &image_var: &GroupVar<G>,
127 equation: &LinearCombination<G>,
128 original_relation: &LinearRelation<G>,
129 weighted_group_cache: &mut WeightedGroupCache<G>,
130 ) -> Result<(), InvalidInstance> {
131 let mut rhs_terms = Vec::new();
132
133 for weighted_term in equation.terms() {
135 if let ScalarTerm::Var(scalar_var) = weighted_term.term.scalar {
136 let group_var = weighted_term.term.elem;
137 let weight = &weighted_term.weight;
138
139 if weight.is_zero_vartime() {
140 continue; }
142
143 let canonical_group_var = self.get_or_create_weighted_group_var(
144 group_var,
145 weight,
146 &original_relation.linear_map.group_elements,
147 weighted_group_cache,
148 )?;
149
150 rhs_terms.push((scalar_var, canonical_group_var));
151 }
152 }
153
154 let mut canonical_image = original_relation.linear_map.group_elements.get(image_var)?;
156 for weighted_term in equation.terms() {
157 if let ScalarTerm::Unit = weighted_term.term.scalar {
158 let group_val = original_relation
159 .linear_map
160 .group_elements
161 .get(weighted_term.term.elem)?;
162 canonical_image -= group_val * weighted_term.weight;
163 }
164 }
165
166 #[expect(clippy::collapsible_if)]
168 if rhs_terms.is_empty() {
169 if canonical_image.is_identity().into() {
170 return Ok(());
171 }
172 }
181
182 let canonical_image_group_var = self.group_elements.push(canonical_image);
183 self.image.push(canonical_image_group_var);
184 self.linear_combinations.push(rhs_terms);
185
186 Ok(())
187 }
188
189 pub fn label(&self) -> Vec<u8> {
204 let mut out = Vec::new();
205
206 let mut constraint_data = Vec::<(u32, Vec<(u32, u32)>)>::new();
209
210 for (image_var, constraint_terms) in iter::zip(&self.image, &self.linear_combinations) {
211 let mut rhs_terms = Vec::new();
213 for (scalar_var, group_var) in constraint_terms {
214 rhs_terms.push((scalar_var.0 as u32, group_var.0 as u32));
215 }
216
217 constraint_data.push((image_var.0 as u32, rhs_terms));
218 }
219
220 let ne = constraint_data.len();
222 out.extend_from_slice(&(ne as u32).to_le_bytes());
223
224 for (lhs_index, rhs_terms) in constraint_data {
226 out.extend_from_slice(&lhs_index.to_le_bytes());
228
229 out.extend_from_slice(&(rhs_terms.len() as u32).to_le_bytes());
231
232 for (scalar_index, group_index) in rhs_terms {
234 out.extend_from_slice(&scalar_index.to_le_bytes());
235 out.extend_from_slice(&group_index.to_le_bytes());
236 }
237 }
238
239 for (_, elem) in self.group_elements.iter() {
241 out.extend_from_slice(
242 elem.expect("expected group variable to be assigned")
243 .to_bytes()
244 .as_ref(),
245 );
246 }
247
248 out
249 }
250
251 pub fn from_label(data: &[u8]) -> Result<Self, Error> {
267 use crate::errors::InvalidInstance;
268
269 fn read_u32(data: &[u8], offset: &mut usize, field: &str) -> Result<u32, Error> {
270 let end = offset.checked_add(4).ok_or_else(|| {
271 InvalidInstance::new(format!("Invalid label: offset overflow reading {field}"))
272 })?;
273 let bytes = data
274 .get(*offset..end)
275 .ok_or_else(|| InvalidInstance::new(format!("Invalid label: truncated {field}")))?;
276 *offset = end;
277 Ok(u32::from_le_bytes(<[u8; 4]>::try_from(bytes).map_err(
278 |_| InvalidInstance::new(format!("Invalid label: truncated {field}")),
279 )?))
280 }
281
282 let mut offset = 0;
283
284 let num_equations = read_u32(data, &mut offset, "equation count")? as usize;
286
287 let mut constraint_data = Vec::new();
289 let mut max_scalar_index: Option<u32> = None;
290 let mut max_group_index: Option<u32> = None;
291
292 for _ in 0..num_equations {
293 let lhs_index = read_u32(data, &mut offset, "LHS index")?;
295 max_group_index = Some(max_group_index.map_or(lhs_index, |max| max.max(lhs_index)));
296
297 let num_rhs_terms = read_u32(data, &mut offset, "RHS count")? as usize;
299
300 let mut rhs_terms = Vec::new();
302 for _ in 0..num_rhs_terms {
303 let scalar_index = read_u32(data, &mut offset, "scalar index")?;
305 max_scalar_index =
306 Some(max_scalar_index.map_or(scalar_index, |max| max.max(scalar_index)));
307
308 let group_index = read_u32(data, &mut offset, "group index")?;
310 max_group_index =
311 Some(max_group_index.map_or(group_index, |max| max.max(group_index)));
312
313 rhs_terms.push((scalar_index, group_index));
314 }
315
316 constraint_data.push((lhs_index, rhs_terms));
317 }
318
319 let num_group_elements = max_group_index
321 .map(|max| {
322 max.checked_add(1)
323 .ok_or_else(|| InvalidInstance::new("Invalid label: too many group elements"))
324 })
325 .transpose()?
326 .unwrap_or(0) as usize;
327 let group_element_size = G::Repr::default().as_ref().len();
328 let expected_remaining = num_group_elements
329 .checked_mul(group_element_size)
330 .ok_or_else(|| InvalidInstance::new("Invalid label: group element data too large"))?;
331
332 if data.len() - offset != expected_remaining {
333 return Err(InvalidInstance::new(format!(
334 "Invalid label: expected {} bytes for {} group elements, got {}",
335 expected_remaining,
336 num_group_elements,
337 data.len() - offset
338 ))
339 .into());
340 }
341
342 let mut group_elements_ordered = Vec::new();
344 for i in 0..num_group_elements {
345 let start = offset + i * group_element_size;
346 let end = start + group_element_size;
347 let elem_bytes = &data[start..end];
348
349 let mut repr = G::Repr::default();
350 repr.as_mut().copy_from_slice(elem_bytes);
351
352 let elem = Option::<G>::from(G::from_bytes(&repr)).ok_or_else(|| {
353 Error::from(InvalidInstance::new(format!(
354 "Invalid group element at index {i}"
355 )))
356 })?;
357
358 group_elements_ordered.push(elem);
359 }
360
361 let mut canonical = Self::new();
363 canonical.num_scalars = max_scalar_index
364 .map(|max| {
365 max.checked_add(1)
366 .ok_or_else(|| InvalidInstance::new("Invalid label: too many scalars"))
367 })
368 .transpose()?
369 .unwrap_or(0) as usize;
370
371 let mut group_var_map = Vec::new();
373 for elem in &group_elements_ordered {
374 let var = canonical.group_elements.push(*elem);
375 group_var_map.push(var);
376 }
377
378 for (lhs_index, rhs_terms) in constraint_data {
380 let lhs = group_var_map
382 .get(lhs_index as usize)
383 .ok_or_else(|| InvalidInstance::new("Invalid label: LHS index out of bounds"))?;
384 canonical.image.push(*lhs);
385
386 let mut linear_combination = Vec::new();
388 for (scalar_index, group_index) in rhs_terms {
389 let scalar_var = ScalarVar(scalar_index as usize, PhantomData);
390 let group_var = group_var_map.get(group_index as usize).ok_or_else(|| {
391 InvalidInstance::new("Invalid label: group index out of bounds")
392 })?;
393 linear_combination.push((scalar_var, *group_var));
394 }
395 canonical.linear_combinations.push(linear_combination);
396 }
397
398 Ok(canonical)
399 }
400
401 pub(crate) fn image_elements(&self) -> impl Iterator<Item = G> + use<'_, G> {
404 self.image.iter().map(|var| {
405 self.group_elements
406 .get(*var)
407 .expect("expected group variable to be assigned")
408 })
409 }
410}
411
412impl<G: PrimeGroup + MultiScalarMul> TryFrom<LinearRelation<G>> for CanonicalLinearRelation<G> {
413 type Error = InvalidInstance;
414
415 fn try_from(value: LinearRelation<G>) -> Result<Self, Self::Error> {
416 Self::try_from(&value)
417 }
418}
419
420impl<G: PrimeGroup + MultiScalarMul> TryFrom<&LinearRelation<G>> for CanonicalLinearRelation<G> {
421 type Error = InvalidInstance;
422
423 fn try_from(relation: &LinearRelation<G>) -> Result<Self, Self::Error> {
424 if relation.image.len() != relation.linear_map.linear_combinations.len() {
425 return Err(InvalidInstance::new(
426 "Number of equations must be equal to number of image elements.",
427 ));
428 }
429
430 let mut canonical = CanonicalLinearRelation::new();
431 canonical.num_scalars = relation.linear_map.num_scalars;
432
433 let mut weighted_group_cache = Vec::new();
435
436 for (lhs, rhs) in iter::zip(&relation.image, &relation.linear_map.linear_combinations) {
438 let lhs_value = relation.linear_map.group_elements.get(*lhs)?;
440
441 let rhs_constant_terms = rhs
444 .0
445 .iter()
446 .filter(|term| matches!(term.term.scalar, ScalarTerm::Unit))
447 .map(|term| {
448 let elem = relation.linear_map.group_elements.get(term.term.elem)?;
449 let scalar = term.weight;
450 Ok((elem, scalar))
451 })
452 .collect::<Result<(Vec<G>, Vec<G::Scalar>), _>>()?;
453
454 let rhs_constant_term = G::msm(&rhs_constant_terms.1, &rhs_constant_terms.0);
455
456 let is_trivial = rhs.0.iter().all(|term| {
459 matches!(term.term.scalar, ScalarTerm::Unit) || term.weight.is_zero_vartime()
460 });
461
462 let is_homogenous = rhs_constant_term == lhs_value;
464
465 if is_trivial && is_homogenous {
468 continue;
469 }
470
471 if !is_trivial && is_homogenous {
473 return Err(InvalidInstance::new("Trivial kernel in this relation"));
474 }
475
476 canonical.process_constraint(lhs, rhs, relation, &mut weighted_group_cache)?;
477 }
478
479 Ok(canonical)
480 }
481}
482
483impl<G: PrimeGroup + ConstantTimeEq + MultiScalarMul> CanonicalLinearRelation<G> {
484 pub fn is_witness_valid(&self, witness: &[G::Scalar]) -> Choice {
493 let got = self.evaluate(witness);
494 self.image_elements()
495 .zip_eq(got)
496 .fold(Choice::from(1), |acc, (lhs, rhs)| acc & lhs.ct_eq(&rhs))
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::CanonicalLinearRelation;
503 use alloc::vec::Vec;
504 use group::GroupEncoding;
505
506 type G = bls12_381::G1Projective;
507
508 #[test]
509 fn from_label_rejects_max_lhs_group_index() {
510 let mut label = Vec::new();
511 label.extend_from_slice(&1u32.to_le_bytes());
512 label.extend_from_slice(&u32::MAX.to_le_bytes());
513 label.extend_from_slice(&0u32.to_le_bytes());
514
515 assert!(CanonicalLinearRelation::<G>::from_label(&label).is_err());
516 }
517
518 #[test]
519 fn from_label_rejects_max_scalar_index() {
520 let mut label = Vec::new();
521 label.extend_from_slice(&1u32.to_le_bytes());
522 label.extend_from_slice(&0u32.to_le_bytes());
523 label.extend_from_slice(&1u32.to_le_bytes());
524 label.extend_from_slice(&u32::MAX.to_le_bytes());
525 label.extend_from_slice(&0u32.to_le_bytes());
526 label.extend_from_slice(G::identity().to_bytes().as_ref());
527
528 assert!(CanonicalLinearRelation::<G>::from_label(&label).is_err());
529 }
530}