sigma_rs/linear_relation/
mod.rs

1//! # Linear Maps and Relations Handling.
2//!
3//! This module provides utilities for describing and manipulating **linear group linear maps**,
4//! supporting sigma protocols over group-based statements (e.g., discrete logarithms, DLEQ proofs). See Maurer09.
5//!
6//! It includes:
7//! - [`LinearCombination`]: a sparse representation of scalar multiplication relations.
8//! - [`LinearMap`]: a collection of linear combinations acting on group elements.
9//! - [`LinearRelation`]: a higher-level structure managing linear maps and their associated images.
10
11use std::collections::HashMap;
12use std::hash::Hash;
13use std::iter;
14use std::marker::PhantomData;
15
16use ff::Field;
17use group::{Group, GroupEncoding};
18
19use crate::codec::ShakeCodec;
20use crate::errors::Error;
21use crate::schnorr_protocol::SchnorrProof;
22use crate::NISigmaProtocol;
23
24/// Implementations of conversion operations such as From and FromIterator for var and term types.
25mod convert;
26/// Implementations of core ops for the linear combination types.
27mod ops;
28
29/// A wrapper representing an index for a scalar variable.
30///
31/// Used to reference scalars in sparse linear combinations.
32#[derive(Copy, Clone, Debug, PartialEq, Eq)]
33pub struct ScalarVar<G>(usize, PhantomData<G>);
34
35impl<G> ScalarVar<G> {
36    pub fn index(&self) -> usize {
37        self.0
38    }
39}
40
41impl<G> Hash for ScalarVar<G> {
42    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
43        self.0.hash(state)
44    }
45}
46
47/// A wrapper representing an index for a group element (point).
48///
49/// Used to reference group elements in sparse linear combinations.
50#[derive(Copy, Clone, Debug, PartialEq, Eq)]
51pub struct GroupVar<G>(usize, PhantomData<G>);
52
53impl<G> GroupVar<G> {
54    pub fn index(&self) -> usize {
55        self.0
56    }
57}
58
59impl<G> Hash for GroupVar<G> {
60    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
61        self.0.hash(state)
62    }
63}
64
65#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
66pub enum ScalarTerm<G> {
67    Var(ScalarVar<G>),
68    Unit,
69}
70
71impl<G: Group> ScalarTerm<G> {
72    // NOTE: This function is private intentionally as it would be replaced if a ScalarMap struct
73    // were to be added.
74    fn value(self, scalars: &[G::Scalar]) -> G::Scalar {
75        match self {
76            Self::Var(var) => scalars[var.0],
77            Self::Unit => G::Scalar::ONE,
78        }
79    }
80}
81
82/// A term in a linear combination, representing `scalar * elem`.
83#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
84pub struct Term<G> {
85    scalar: ScalarTerm<G>,
86    elem: GroupVar<G>,
87}
88
89#[derive(Copy, Clone, Debug)]
90pub struct Weighted<T, F> {
91    pub term: T,
92    pub weight: F,
93}
94
95#[derive(Clone, Debug)]
96pub struct Sum<T>(Vec<T>);
97
98impl<T> Sum<T> {
99    /// Access the terms of the sum as slice reference.
100    pub fn terms(&self) -> &[T] {
101        &self.0
102    }
103}
104
105/// Represents a sparse linear combination of scalars and group elements.
106///
107/// For example, it can represent an equation like:
108/// `w_1 * (s_1 * P_1) + w_2 * (s_2 * P_2) + ... + w_n * (s_n * P_n)`
109///
110/// where:
111/// - `(s_i * P_i)` are the terms, with `s_i` scalars (referenced by `scalar_vars`) and `P_i` group elements (referenced by `element_vars`).
112/// - `w_i` are the constant weight scalars
113///
114/// The indices refer to external lists managed by the containing LinearMap.
115pub type LinearCombination<G> = Sum<Weighted<Term<G>, <G as Group>::Scalar>>;
116
117/// Ordered mapping of [GroupVar] to group elements assignments.
118#[derive(Clone, Debug)]
119pub struct GroupMap<G>(Vec<Option<G>>);
120
121impl<G: Group> GroupMap<G> {
122    /// Assign a group element value to a point variable.
123    ///
124    /// # Parameters
125    ///
126    /// - `var`: The variable to assign.
127    /// - `element`: The value to assign to the variable.
128    ///
129    /// # Panics
130    ///
131    /// Panics if the given assignment conflicts with the existing assignment.
132    pub fn assign_element(&mut self, var: GroupVar<G>, element: G) {
133        if self.0.len() <= var.0 {
134            self.0.resize(var.0 + 1, None);
135        } else if let Some(assignment) = self.0[var.0] {
136            assert_eq!(
137                assignment, element,
138                "conflicting assignments for var {var:?}"
139            )
140        }
141        self.0[var.0] = Some(element);
142    }
143
144    /// Assigns specific group elements to point variables (indices).
145    ///
146    /// # Parameters
147    ///
148    /// - `assignments`: A collection of `(GroupVar, GroupElement)` pairs that can be iterated over.
149    ///
150    /// # Panics
151    ///
152    /// Panics if the collection contains two conflicting assignments for the same variable.
153    pub fn assign_elements(&mut self, assignments: impl IntoIterator<Item = (GroupVar<G>, G)>) {
154        for (var, elem) in assignments.into_iter() {
155            self.assign_element(var, elem);
156        }
157    }
158
159    /// Get the element value assigned to the given point var.
160    ///
161    /// Returns [`Error::UnassignedGroupVar`] if a value is not assigned.
162    pub fn get(&self, var: GroupVar<G>) -> Result<G, Error> {
163        self.0[var.0].ok_or(Error::UnassignedGroupVar {
164            var_debug: format!("{var:?}"),
165        })
166    }
167
168    /// Iterate over the assigned variable and group element pairs in this mapping.
169    // NOTE: Not implemented as `IntoIterator` for now because doing so requires explicitly
170    // defining an iterator type, See https://github.com/rust-lang/rust/issues/63063
171    #[allow(clippy::should_implement_trait)]
172    pub fn into_iter(self) -> impl Iterator<Item = (GroupVar<G>, Option<G>)> {
173        self.0
174            .into_iter()
175            .enumerate()
176            .map(|(i, x)| (GroupVar(i, PhantomData), x))
177    }
178
179    pub fn iter(&self) -> impl Iterator<Item = (GroupVar<G>, Option<&G>)> {
180        self.0
181            .iter()
182            .enumerate()
183            .map(|(i, opt)| (GroupVar(i, PhantomData), opt.as_ref()))
184    }
185}
186
187impl<G> Default for GroupMap<G> {
188    fn default() -> Self {
189        Self(Vec::default())
190    }
191}
192
193impl<G: Group> FromIterator<(GroupVar<G>, G)> for GroupMap<G> {
194    fn from_iter<T: IntoIterator<Item = (GroupVar<G>, G)>>(iter: T) -> Self {
195        iter.into_iter()
196            .fold(Self::default(), |mut instance, (var, val)| {
197                instance.assign_element(var, val);
198                instance
199            })
200    }
201}
202
203/// A LinearMap represents a list of linear combinations over group elements.
204///
205/// It supports dynamic allocation of scalars and elements,
206/// and evaluates by performing multi-scalar multiplications.
207#[derive(Clone, Default, Debug)]
208pub struct LinearMap<G: Group> {
209    /// The set of linear combination constraints (equations).
210    pub constraints: Vec<LinearCombination<G>>,
211    /// The list of group elements referenced in the linear map.
212    ///
213    /// Uninitialized group elements are presented with `None`.
214    pub group_elements: GroupMap<G>,
215    /// The total number of scalar variables allocated.
216    pub num_scalars: usize,
217    /// The total number of group element variables allocated.
218    pub num_elements: usize,
219}
220
221/// Perform a simple multi-scalar multiplication (MSM) over scalars and points.
222///
223/// Given slices of scalars and corresponding group elements (bases),
224/// returns the sum of each base multiplied by its scalar coefficient.
225///
226/// # Parameters
227/// - `scalars`: slice of scalar multipliers.
228/// - `bases`: slice of group elements to be multiplied by the scalars.
229///
230/// # Returns
231/// The group element result of the MSM.
232pub fn msm_pr<G: Group>(scalars: &[G::Scalar], bases: &[G]) -> G {
233    let mut acc = G::identity();
234    for (s, p) in scalars.iter().zip(bases.iter()) {
235        acc += *p * s;
236    }
237    acc
238}
239
240impl<G: Group> LinearMap<G> {
241    /// Creates a new empty [`LinearMap`].
242    ///
243    /// # Returns
244    ///
245    /// A [`LinearMap`] instance with empty linear combinations and group elements,
246    /// and zero allocated scalars and elements.
247    pub fn new() -> Self {
248        Self {
249            constraints: Vec::new(),
250            group_elements: GroupMap::default(),
251            num_scalars: 0,
252            num_elements: 0,
253        }
254    }
255
256    /// Returns the number of constraints (equations) in this linear map.
257    pub fn num_constraints(&self) -> usize {
258        self.constraints.len()
259    }
260
261    /// Adds a new linear combination constraint to the linear map.
262    ///
263    /// # Parameters
264    /// - `lc`: The [`LinearCombination`] to add.
265    pub fn append(&mut self, lc: LinearCombination<G>) {
266        self.constraints.push(lc);
267    }
268
269    /// Evaluates all linear combinations in the linear map with the provided scalars.
270    ///
271    /// # Parameters
272    /// - `scalars`: A slice of scalar values corresponding to the scalar variables.
273    ///
274    /// # Returns
275    ///
276    /// A vector of group elements, each being the result of evaluating one linear combination with the scalars.
277    pub fn evaluate(&self, scalars: &[<G as Group>::Scalar]) -> Result<Vec<G>, Error> {
278        self.constraints
279            .iter()
280            .map(|lc| {
281                // TODO: The multiplication by the (public) weight is potentially wasteful in the
282                // weight is most commonly 1, but multiplication is constant time.
283                let weighted_coefficients =
284                    lc.0.iter()
285                        .map(|weighted| weighted.term.scalar.value(scalars) * weighted.weight)
286                        .collect::<Vec<_>>();
287                let elements =
288                    lc.0.iter()
289                        .map(|weighted| self.group_elements.get(weighted.term.elem))
290                        .collect::<Result<Vec<_>, Error>>()?;
291                Ok(msm_pr(&weighted_coefficients, &elements))
292            })
293            .collect()
294    }
295}
296
297/// A wrapper struct coupling a [`LinearMap`] with the corresponding expected output (image) elements.
298///
299/// This structure represents the *preimage problem* for a group linear map: given a set of scalar inputs,
300/// determine whether their image under the linear map matches a target set of group elements.
301///
302/// Internally, the constraint system is defined through:
303/// - A list of group elements and linear equations (held in the [`LinearMap`] field),
304/// - A list of [`GroupVar`] indices (`image`) that specify the expected output for each constraint.
305#[derive(Clone, Default, Debug)]
306pub struct LinearRelation<G>
307where
308    G: Group + GroupEncoding,
309{
310    /// The underlying linear map describing the structure of the statement.
311    pub linear_map: LinearMap<G>,
312    /// Indices pointing to elements representing the "target" images for each constraint.
313    pub image: Vec<GroupVar<G>>,
314}
315
316/// A normalized form of the [LinearRelation], which is used for serialization into the transcript.
317// NOTE: This is not intended to be exposed beyond this module.
318#[derive(Clone)]
319struct LinearRelationRepr<G: GroupEncoding> {
320    constraints: Vec<(u32, Vec<(u32, u32)>)>,
321    group_elements: Vec<G::Repr>,
322}
323
324impl<G: GroupEncoding> Default for LinearRelationRepr<G> {
325    fn default() -> Self {
326        Self {
327            constraints: Default::default(),
328            group_elements: Default::default(),
329        }
330    }
331}
332
333// A utility struct used to build the LinearRelationRepr.
334#[derive(Clone)]
335struct LinearRelationReprBuilder<G: Group + GroupEncoding> {
336    repr: LinearRelationRepr<G>,
337    /// Mapping from the serialized group representation to its index in the repr.
338    /// Acts as a reverse index into the group_elements.
339    group_repr_mapping: HashMap<Box<[u8]>, u32>,
340    /// A mapping from GroupVar index and weight to repr index, to avoid recomputing the scalar mul
341    /// of the group element multiple times.
342    weighted_group_cache: HashMap<GroupVar<G>, Vec<(G::Scalar, u32)>>,
343}
344
345impl<G: Group + GroupEncoding> Default for LinearRelationReprBuilder<G> {
346    fn default() -> Self {
347        Self {
348            repr: Default::default(),
349            group_repr_mapping: Default::default(),
350            weighted_group_cache: Default::default(),
351        }
352    }
353}
354
355impl<G: Group + GroupEncoding> LinearRelationReprBuilder<G> {
356    fn repr_index(&mut self, elem: &G::Repr) -> u32 {
357        if let Some(index) = self.group_repr_mapping.get(elem.as_ref()) {
358            return *index;
359        }
360
361        let new_index = self.repr.group_elements.len() as u32;
362        self.repr.group_elements.push(*elem);
363        self.group_repr_mapping
364            .insert(elem.as_ref().into(), new_index);
365        new_index
366    }
367
368    fn weighted_group_var_index(&mut self, var: GroupVar<G>, weight: &G::Scalar, elem: &G) -> u32 {
369        let entry = self.weighted_group_cache.entry(var).or_default();
370
371        // If the (weight, group_var) pair is already in the cache, use it.
372        if let Some(index) = entry
373            .iter()
374            .find_map(|(entry_weight, index)| (weight == entry_weight).then_some(index))
375        {
376            return *index;
377        }
378
379        // Compute the scalar mul of the element and the weight, then the representation.
380        let weighted_elem_repr = (*elem * weight).to_bytes();
381        // Lookup or assign the index to the representation.
382        let index = self.repr_index(&weighted_elem_repr);
383
384        // Add the index to the cache.
385        // NOTE: entry is dropped earlier to satisfy borrow-check rules.
386        self.weighted_group_cache
387            .get_mut(&var)
388            .unwrap()
389            .push((*weight, index));
390
391        index
392    }
393
394    fn finalize(self) -> LinearRelationRepr<G> {
395        self.repr
396    }
397}
398
399impl<G> LinearRelation<G>
400where
401    G: Group + GroupEncoding,
402{
403    /// Create a new empty [`LinearRelation`].
404    pub fn new() -> Self {
405        Self {
406            linear_map: LinearMap::new(),
407            image: Vec::new(),
408        }
409    }
410
411    /// Adds a new equation to the statement of the form:
412    /// `lhs = Σ weight_i * (scalar_i * point_i)`.
413    ///
414    /// # Parameters
415    /// - `lhs`: The image group element variable (left-hand side of the equation).
416    /// - `rhs`: An instance of [`LinearCombination`] representing the linear combination on the right-hand side.
417    pub fn append_equation(&mut self, lhs: GroupVar<G>, rhs: impl Into<LinearCombination<G>>) {
418        self.linear_map.append(rhs.into());
419        self.image.push(lhs);
420    }
421
422    /// Adds a new equation to the statement of the form:
423    /// `lhs = Σ weight_i * (scalar_i * point_i)` without allocating `lhs`.
424    ///
425    /// # Parameters
426    /// - `rhs`: An instance of [`LinearCombination`] representing the linear combination on the right-hand side.
427    pub fn allocate_eq(&mut self, rhs: impl Into<LinearCombination<G>>) -> GroupVar<G> {
428        let var = self.allocate_element();
429        self.append_equation(var, rhs);
430        var
431    }
432
433    /// Allocates a scalar variable for use in the linear map.
434    pub fn allocate_scalar(&mut self) -> ScalarVar<G> {
435        self.linear_map.num_scalars += 1;
436        ScalarVar(self.linear_map.num_scalars - 1, PhantomData)
437    }
438
439    /// Allocates space for `N` new scalar variables.
440    ///
441    /// # Returns
442    /// An array of [`ScalarVar`] representing the newly allocated scalar indices.
443    ///
444    /// # Example
445    /// ```
446    /// # use sigma_rs::LinearRelation;
447    /// use curve25519_dalek::RistrettoPoint as G;
448    ///
449    /// let mut relation = LinearRelation::<G>::new();
450    /// let [var_x, var_y] = relation.allocate_scalars();
451    /// let vars = relation.allocate_scalars::<10>();
452    /// ```
453    pub fn allocate_scalars<const N: usize>(&mut self) -> [ScalarVar<G>; N] {
454        let mut vars = [ScalarVar(usize::MAX, PhantomData); N];
455        for var in vars.iter_mut() {
456            *var = self.allocate_scalar();
457        }
458        vars
459    }
460
461    /// Allocates a point variable (group element) for use in the linear map.
462    pub fn allocate_element(&mut self) -> GroupVar<G> {
463        self.linear_map.num_elements += 1;
464        GroupVar(self.linear_map.num_elements - 1, PhantomData)
465    }
466
467    /// Allocates `N` point variables (group elements) for use in the linear map.
468    ///
469    /// # Returns
470    /// An array of [`GroupVar`] representing the newly allocated group element indices.
471    ///
472    /// # Example
473    /// ```
474    /// # use sigma_rs::LinearRelation;
475    /// use curve25519_dalek::RistrettoPoint as G;
476    ///
477    /// let mut relation = LinearRelation::<G>::new();
478    /// let [var_g, var_h] = relation.allocate_elements();
479    /// let vars = relation.allocate_elements::<10>();
480    /// ```
481    pub fn allocate_elements<const N: usize>(&mut self) -> [GroupVar<G>; N] {
482        let mut vars = [GroupVar(usize::MAX, PhantomData); N];
483        for var in vars.iter_mut() {
484            *var = self.allocate_element();
485        }
486        vars
487    }
488
489    /// Assign a group element value to a point variable.
490    ///
491    /// # Parameters
492    ///
493    /// - `var`: The variable to assign.
494    /// - `element`: The value to assign to the variable.
495    ///
496    /// # Panics
497    ///
498    /// Panics if the given assignment conflicts with the existing assignment.
499    pub fn set_element(&mut self, var: GroupVar<G>, element: G) {
500        self.linear_map.group_elements.assign_element(var, element)
501    }
502
503    /// Assigns specific group elements to point variables (indices).
504    ///
505    /// # Parameters
506    ///
507    /// - `assignments`: A collection of `(GroupVar, GroupElement)` pairs that can be iterated over.
508    ///
509    /// # Panics
510    ///
511    /// Panics if the collection contains two conflicting assignments for the same variable.
512    pub fn set_elements(&mut self, assignments: impl IntoIterator<Item = (GroupVar<G>, G)>) {
513        self.linear_map.group_elements.assign_elements(assignments)
514    }
515
516    /// Evaluates all linear combinations in the linear map with the provided scalars, computing the
517    /// left-hand side of this constraints (i.e. the image).
518    ///
519    /// After calling this function, all point variables will be assigned.
520    ///
521    /// # Parameters
522    ///
523    /// - `scalars`: A slice of scalar values corresponding to the scalar variables.
524    ///
525    /// # Returns
526    ///
527    /// Return `Ok` on success, and an error if unassigned elements prevent the image from being
528    /// computed. Modifies the group elements assigned in the [LinearRelation].
529    pub fn compute_image(&mut self, scalars: &[<G as Group>::Scalar]) -> Result<(), Error> {
530        if self.linear_map.num_constraints() != self.image.len() {
531            // NOTE: This is a panic, rather than a returned error, because this can only happen if
532            // this implementation has a bug.
533            panic!("invalid LinearRelation: different number of constraints and image variables");
534        }
535
536        for (lc, lhs) in iter::zip(
537            self.linear_map.constraints.as_slice(),
538            self.image.as_slice(),
539        ) {
540            // TODO: The multiplication by the (public) weight is potentially wasteful in the
541            // weight is most commonly 1, but multiplication is constant time.
542            let weighted_coefficients =
543                lc.0.iter()
544                    .map(|weighted| weighted.term.scalar.value(scalars) * weighted.weight)
545                    .collect::<Vec<_>>();
546            let elements =
547                lc.0.iter()
548                    .map(|weighted| self.linear_map.group_elements.get(weighted.term.elem))
549                    .collect::<Result<Vec<_>, Error>>()?;
550            self.linear_map
551                .group_elements
552                .assign_element(*lhs, msm_pr(&weighted_coefficients, &elements))
553        }
554        Ok(())
555    }
556
557    /// Returns the current group elements corresponding to the image variables.
558    ///
559    /// # Returns
560    ///
561    /// A vector of group elements (`Vec<G>`) representing the linear map's image.
562    // TODO: Should this return GroupMap?
563    pub fn image(&self) -> Result<Vec<G>, Error> {
564        self.image
565            .iter()
566            .map(|&var| self.linear_map.group_elements.get(var))
567            .collect()
568    }
569
570    /// Returns a binary label describing the linear map.
571    ///
572    /// The format is:
573    /// - [Ne: u32] number of equations
574    /// - For each equation:
575    ///   - [output_point_index: u32]
576    ///   - [Nt: u32] number of terms
577    ///   - Nt × [scalar_index: u32, point_index: u32] term entries
578    pub fn label(&self) -> Vec<u8> {
579        let mut out = Vec::new();
580        // XXX. We should return an error if the group elements are not assigned, instead of panicking.
581        let repr = self.standard_repr().unwrap();
582
583        // 1. Number of equations
584        let ne = repr.constraints.len();
585        out.extend_from_slice(&(ne as u32).to_le_bytes());
586
587        // 2. Encode each equation
588        for (output_index, constraint) in repr.constraints {
589            // a. Output point index (LHS)
590            out.extend_from_slice(&output_index.to_le_bytes());
591
592            // b. Number of terms in the RHS linear combination
593            out.extend_from_slice(&(constraint.len() as u32).to_le_bytes());
594
595            // c. Each term: scalar index and point index
596            for (scalar_index, group_index) in constraint {
597                out.extend_from_slice(&scalar_index.to_le_bytes());
598                out.extend_from_slice(&group_index.to_le_bytes());
599            }
600        }
601
602        // Dump the group elements.
603        // TODO batch serialization of group elements should not require allocation of a new vector in this case and should be part of a Group trait.
604        for elem in repr.group_elements {
605            out.extend_from_slice(elem.as_ref());
606        }
607
608        out
609    }
610
611    /// Construct an equivalent linear relation in the standardized form, without weights and with
612    /// a single group var on the left-hand side.
613    fn standard_repr(&self) -> Result<LinearRelationRepr<G>, Error> {
614        assert_eq!(
615            self.image.len(),
616            self.linear_map.constraints.len(),
617            "Number of equations and image variables must match"
618        );
619
620        let mut repr_builder = LinearRelationReprBuilder::default();
621
622        // Iterate through the constraints, applying to remapping to weighed group variables and
623        // casting scalar vars to u32.
624        for (image_var, equation) in iter::zip(&self.image, &self.linear_map.constraints) {
625            // Construct the right-hand side, omitting any terms that no not include a scalar, as
626            // they will be moved to the left-hand side.
627            let rhs: Vec<(u32, u32)> = equation
628                .terms()
629                .iter()
630                .filter_map(|weighted_term| match weighted_term.term.scalar {
631                    ScalarTerm::Var(var) => {
632                        Some((var, weighted_term.term.elem, weighted_term.weight))
633                    }
634                    ScalarTerm::Unit => None,
635                })
636                .map(|(scalar_var, group_var, weight)| {
637                    let group_val = self.linear_map.group_elements.get(group_var)?;
638                    let group_index =
639                        repr_builder.weighted_group_var_index(group_var, &weight, &group_val);
640                    Ok((scalar_var.0 as u32, group_index))
641                })
642                .collect::<Result<_, _>>()?;
643
644            // Construct the left-hand side, subtracting all the terms on the right that don't have
645            // a variable scalar term.
646            let image_val = self.linear_map.group_elements.get(*image_var)?;
647            let lhs_val = equation
648                .terms()
649                .iter()
650                .filter_map(|weighted_term| match weighted_term.term.scalar {
651                    ScalarTerm::Unit => Some((weighted_term.term.elem, weighted_term.weight)),
652                    ScalarTerm::Var(_) => None,
653                })
654                .try_fold(image_val, |sum, (group_var, weight)| {
655                    let group_val = self.linear_map.group_elements.get(group_var)?;
656                    Ok(sum - group_val * weight)
657                })?;
658            let lhs_index = repr_builder.repr_index(&lhs_val.to_bytes());
659
660            repr_builder.repr.constraints.push((lhs_index, rhs));
661        }
662
663        Ok(repr_builder.finalize())
664    }
665
666    /// Convert this LinearRelation into a non-interactive zero-knowledge protocol
667    /// using the ShakeCodec and a specified context/domain separator.
668    ///
669    /// # Parameters
670    /// - `context`: Domain separator bytes for the Fiat-Shamir transform
671    ///
672    /// # Returns
673    /// A `NISigmaProtocol` instance ready for proving and verification
674    ///
675    /// # Example
676    /// ```
677    /// # use sigma_rs::{LinearRelation, NISigmaProtocol};
678    /// # use curve25519_dalek::RistrettoPoint as G;
679    /// # use curve25519_dalek::scalar::Scalar;
680    /// # use rand::rngs::OsRng;
681    /// # use group::Group;
682    ///
683    /// let mut relation = LinearRelation::<G>::new();
684    /// let x_var = relation.allocate_scalar();
685    /// let g_var = relation.allocate_element();
686    /// let p_var = relation.allocate_eq(x_var * g_var);
687    ///
688    /// relation.set_element(g_var, G::generator());
689    /// let x = Scalar::random(&mut OsRng);
690    /// relation.compute_image(&[x]).unwrap();
691    ///
692    /// // Convert to NIZK with custom context
693    /// let nizk = relation.into_nizk(b"my-protocol-v1");
694    /// let proof = nizk.prove_batchable(&vec![x], &mut OsRng).unwrap();
695    /// assert!(nizk.verify_batchable(&proof).is_ok());
696    /// ```
697    pub fn into_nizk(
698        self,
699        session_identifier: &[u8],
700    ) -> NISigmaProtocol<SchnorrProof<G>, ShakeCodec<G>>
701    where
702        G: group::GroupEncoding,
703    {
704        let schnorr = SchnorrProof::from(self);
705        NISigmaProtocol::new(session_identifier, schnorr)
706    }
707}