Skip to main content

sigma_proofs/linear_relation/
canonical.rs

1use 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/// A [`LinearRelation`] in canonical form, compatible with the IETF spec.
16///
17/// This relation is type-safe:
18/// it can be instantiated only if all group vars are assigned,
19/// size match, and the relation is not trivially false.
20///
21/// This struct represents a normalized form of a linear relation where each
22/// constraint is of the form: image_i = Σ (scalar_j * group_element_k)
23/// without weights or extra scalars.
24#[derive(Clone, Debug, Default)]
25pub struct CanonicalLinearRelation<G: PrimeGroup> {
26    /// The image group elements (left-hand side of equations)
27    pub image: Vec<GroupVar<G>>,
28    /// The constraints, where each constraint is a vector of (scalar_var, group_var) pairs
29    /// representing the right-hand side of the equation
30    pub linear_combinations: Vec<Vec<(ScalarVar<G>, GroupVar<G>)>>,
31    /// The group elements map
32    pub group_elements: GroupMap<G>,
33    /// Number of scalar variables
34    pub num_scalars: usize,
35}
36
37/// Private type alias used to simplify function signatures below.
38///
39/// The cache maps each `GroupVar` index to a list of `(weight, canonical_group_var)` pairs.
40type WeightedGroupCache<G> = Vec<Vec<(<G as group::Group>::Scalar, GroupVar<G>)>>;
41
42impl<G: PrimeGroup> CanonicalLinearRelation<G> {
43    /// Create a new empty canonical linear relation.
44    ///
45    /// This function is not meant to be publicly exposed. It is internally used to build a type-safe linear relation,
46    /// so that all instances guaranteed to be "good" relations over which the prover will want to make a proof.
47    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    /// Evaluate the canonical linear relation with the provided scalars
57    ///
58    /// This returns a list of image points produced by evaluating each linear combination in the
59    /// relation. The order of the returned list matches the order of [`Self::linear_combinations`].
60    ///
61    /// # Panic
62    ///
63    /// Panics if the number of scalars given is less than the number of scalar variables in this
64    /// linear relation.
65    /// If the vector of scalars if longer than the number of terms in each linear combinations, the extra terms are ignored.
66    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    /// Get or create a GroupVar for a weighted group element, with deduplication
87    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        // Check if we already have this (weight, group_var) combination.
95        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        // Find if we already have this weight for this group_var
102        if let Some((_, existing_var)) = entry.iter().find(|(w, _)| w == weight) {
103            return Ok(*existing_var);
104        }
105
106        // Create new weighted group element
107        // Use a special case for one, as this is the most common weight.
108        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        // Add to our group elements with new index (length)
115        let new_var = self.group_elements.push(weighted_group);
116
117        // Cache the mapping for this group_var and weight
118        entry.push((*weight, new_var));
119
120        Ok(new_var)
121    }
122
123    /// Process a single constraint equation and add it to the canonical relation.
124    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        // Collect RHS terms that have scalar variables and apply weights
134        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; // Skip zero weights
141                }
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        // Compute the canonical image by subtracting constant terms from the original image
155        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        // Only include constraints that are non-trivial (not zero constraints).
167        #[expect(clippy::collapsible_if)]
168        if rhs_terms.is_empty() {
169            if canonical_image.is_identity().into() {
170                return Ok(());
171            }
172            // In this location, we have determined that the constraint is trivially false.
173            // If the constraint is added to the relation, proving will always fail for this
174            // constraint. A composed relation containing a trivially false constraint in an OR
175            // branch may still be provable.
176            //
177            // TODO: In this case, we can optimize and improve error reporting by having this
178            // library special-case trvially false statements.
179            // One approach would be to return an error here and handle it in the OR composition.
180        }
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    /// Serialize the linear relation to bytes.
190    ///
191    /// The output format is:
192    ///
193    /// - `[Ne: u32]` number of equations
194    /// - `Ne × equations`:
195    ///   - `[lhs_index: u32]` output group element index
196    ///   - `[Nt: u32]` number of terms
197    ///   - `Nt × [scalar_index: u32, group_index: u32]` term entries
198    /// - All group elements in serialized form.
199    ///
200    /// Only scalar variables that appear in at least one encoded term are part of this label.
201    /// Unused scalar variables are not statement components and are not preserved by
202    /// [`from_label`](Self::from_label).
203    pub fn label(&self) -> Vec<u8> {
204        let mut out = Vec::new();
205
206        // Build constraint data in the same order as original, as a nested list of group and
207        // scalar indices. Note that the group indices are into group_elements_ordered.
208        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            // Build the RHS terms
212            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        // 1. Number of equations
221        let ne = constraint_data.len();
222        out.extend_from_slice(&(ne as u32).to_le_bytes());
223
224        // 2. Encode each equation
225        for (lhs_index, rhs_terms) in constraint_data {
226            // a. Output point index (LHS)
227            out.extend_from_slice(&lhs_index.to_le_bytes());
228
229            // b. Number of terms in the RHS linear combination
230            out.extend_from_slice(&(rhs_terms.len() as u32).to_le_bytes());
231
232            // c. Each term: scalar index and point index
233            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        // Dump the group elements.
240        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    /// Parse a canonical linear relation from its label representation.
252    ///
253    /// Returns an [`InvalidInstance`] error if the label is malformed.
254    ///
255    /// # Examples
256    ///
257    /// ```
258    /// use hex_literal::hex;
259    /// use sigma_proofs::linear_relation::CanonicalLinearRelation;
260    /// type G = bls12_381::G1Projective;
261    ///
262    /// let dlog_instance_label = hex!("01000000000000000100000000000000010000009823a3def60a6e07fb25feb35f211ee2cbc9c130c1959514f5df6b5021a2b21a4c973630ec2090c733c1fe791834ce1197f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb");
263    /// let instance = CanonicalLinearRelation::<G>::from_label(&dlog_instance_label).unwrap();
264    /// assert_eq!(&dlog_instance_label[..], &instance.label()[..]);
265    /// ```
266    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        // Read number of equations (4 bytes, little endian)
285        let num_equations = read_u32(data, &mut offset, "equation count")? as usize;
286
287        // Parse constraints and collect unique group element indices
288        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            // Read LHS index (4 bytes)
294            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            // Read number of RHS terms (4 bytes)
298            let num_rhs_terms = read_u32(data, &mut offset, "RHS count")? as usize;
299
300            // Read RHS terms
301            let mut rhs_terms = Vec::new();
302            for _ in 0..num_rhs_terms {
303                // Read scalar index (4 bytes)
304                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                // Read group index (4 bytes)
309                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        // Calculate expected number of group elements
320        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        // Parse group elements
343        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        // Build the canonical relation
362        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        // Add all group elements to the map
372        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        // Build constraints
379        for (lhs_index, rhs_terms) in constraint_data {
380            // Add image element
381            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            // Build linear combination
387            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    /// Access the group elements associated with the image (i.e. left-hand side), panicking if any
402    /// of the image variables are unassigned in the group mkap.
403    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        // Cache for deduplicating weighted group elements.
434        let mut weighted_group_cache = Vec::new();
435
436        // Process each constraint using the modular helper method
437        for (lhs, rhs) in iter::zip(&relation.image, &relation.linear_map.linear_combinations) {
438            // If any group element in the image is not assigned, return `InvalidInstance`.
439            let lhs_value = relation.linear_map.group_elements.get(*lhs)?;
440
441            // Compute the constant terms on the right-hand side of the equation.
442            // If any group element in the linear constraints is not assigned, return `InvalidInstance`.
443            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            // We say that an equation is trivial if it contains no scalar variables.
457            // To "contain no scalar variables" means that each term in the right-hand side is a unit or its weight is zero.
458            let is_trivial = rhs.0.iter().all(|term| {
459                matches!(term.term.scalar, ScalarTerm::Unit) || term.weight.is_zero_vartime()
460            });
461
462            // We say that an equation is homogenous if the constant term is zero.
463            let is_homogenous = rhs_constant_term == lhs_value;
464
465            // Skip processing trivial equations that are always true.
466            // There's nothing to prove here.
467            if is_trivial && is_homogenous {
468                continue;
469            }
470
471            // Disallow non-trivial equations with trivial solutions.
472            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    /// Tests is the witness is valid.
485    ///
486    /// Returns a [`Choice`] indicating if the witness is valid for the instance constructed.
487    ///
488    /// # Panic
489    ///
490    /// Panics if the number of scalars given is less than the number of scalar variables.
491    /// If the number of scalars is more than the number of scalar variables, the extra elements are ignored.
492    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}