Skip to main content

midnight_circuits/verifier/
accumulator.rs

1// This file is part of MIDNIGHT-ZK.
2// Copyright (C) Midnight Foundation
3// SPDX-License-Identifier: Apache-2.0
4// Licensed under the Apache License, Version 2.0 (the "License");
5// You may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7// http://www.apache.org/licenses/LICENSE-2.0
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14//! Module for in-circuit accumulators (and their off-circuit counterpart).
15//!
16//! An accumulator is a pair of points (lhs, rhs), represented with
17//! respective MSMs that is supposed to satisfy:
18//!
19//!   e(lhs, \[τ\]₂) = e(rhs, \[1\]₂)
20//!
21//! where τ is the corresponding SRS toxic waste.
22//!
23//! This property is preserved by the `accumulate` function, which combines two
24//! accumulators into one; the resulting accumulator satisfies the property iff
25//! both inputs do. We thus call this property the accumulator "invariant".
26//!
27//! Note that implication <= holds unconditionally, whereas implication => holds
28//! "computationally".
29
30use std::collections::BTreeMap;
31
32use ff::Field;
33use group::Group;
34use midnight_proofs::{
35    circuit::{Layouter, Value},
36    plonk::Error,
37    poly::{
38        kzg::{
39            msm::{DualMSM, MSMKZG},
40            params::ParamsVerifierKZG,
41        },
42        CommitmentLabel,
43    },
44};
45use num_bigint::BigUint;
46use num_traits::One;
47
48#[cfg(not(feature = "truncated-challenges"))]
49use crate::verifier::utils::powers;
50#[cfg(feature = "truncated-challenges")]
51use crate::verifier::utils::{truncate_off_circuit, truncated_powers};
52use crate::{
53    instructions::{hash::HashCPU, HashInstructions, PublicInputInstructions},
54    types::{AssignedBit, InnerValue, Instantiable},
55    verifier::{
56        fixed_commitment_name,
57        msm::{AssignedMsm, Msm},
58        perm_commitment_name,
59        utils::AssignedBoundedScalar,
60        SelfEmulation,
61    },
62};
63
64/// Type for off-circuit accumulators.
65///
66/// Note that the points are represented with MSMs which may have
67/// a fixed-base scalars part. In order to evaluate the accumulator, one may
68/// thus need to provide the corresponding fixed bases.
69#[derive(Clone, Debug)]
70pub struct Accumulator<S: SelfEmulation> {
71    lhs: Msm<S>,
72    rhs: Msm<S>,
73}
74
75/// Type for in-circuit accumulators (in-circuit analog of `Accumulator`).
76#[derive(Clone, Debug)]
77pub struct AssignedAccumulator<C: SelfEmulation> {
78    pub(crate) lhs: AssignedMsm<C>,
79    pub(crate) rhs: AssignedMsm<C>,
80}
81
82impl<S: SelfEmulation> Accumulator<S> {
83    /// Converts the off-circuit dual MSM into an `Accumulator<S>` by separating
84    /// the fixed-base scalars aside in a BTreeMap indexed by the base name with
85    /// a custom prefix.
86    ///
87    /// This function also takes a map of fixed bases indexed by their name,
88    /// which is used to perform a sanity check on the fixed-base scalars of the
89    /// dual MSM.
90    pub fn from_dual_msm(
91        dual_msm: DualMSM<S::Engine>,
92        prefix: &str,
93        fixed_bases: &BTreeMap<String, S::C>,
94    ) -> Self {
95        let (lhs, rhs) = dual_msm.split();
96
97        let process_msm = |msm: Vec<(&CommitmentLabel, &S::F, &S::C)>| {
98            let mut bases = Vec::with_capacity(msm.len());
99            let mut scalars = Vec::with_capacity(msm.len());
100            let mut fixed_base_scalars = BTreeMap::new();
101            for (label, scalar, base) in msm {
102                match label {
103                    CommitmentLabel::Fixed(i) => {
104                        let name = fixed_commitment_name(prefix, *i);
105                        assert_eq!(fixed_bases.get(&name), Some(base));
106                        fixed_base_scalars.insert(name, *scalar);
107                    }
108                    CommitmentLabel::Permutation(i) => {
109                        let name = perm_commitment_name(prefix, *i);
110                        assert_eq!(fixed_bases.get(&name), Some(base));
111                        fixed_base_scalars.insert(name, *scalar);
112                    }
113                    CommitmentLabel::Custom(s) if s == "-G" => {
114                        assert_eq!(fixed_bases.get(s), Some(base));
115                        fixed_base_scalars.insert("-G".into(), *scalar);
116                    }
117                    _ => {
118                        bases.push(*base);
119                        scalars.push(*scalar);
120                    }
121                }
122            }
123            (bases, scalars, fixed_base_scalars)
124        };
125
126        let (lhs_bases, lhs_scalars, lhs_fixed_base_scalars) = process_msm(lhs);
127        let (rhs_bases, rhs_scalars, rhs_fixed_base_scalars) = process_msm(rhs);
128
129        Accumulator {
130            lhs: Msm::new(&lhs_bases, &lhs_scalars, &lhs_fixed_base_scalars),
131            rhs: Msm::new(&rhs_bases, &rhs_scalars, &rhs_fixed_base_scalars),
132        }
133    }
134
135    /// Checks whether the accumulator, when evaluated with the provided
136    /// fixed-bases, satisfies the pairing invariant w.r.t. the SRS verifier
137    /// parameters.
138    pub fn check(
139        &self,
140        params: &ParamsVerifierKZG<S::Engine>,
141        fixed_bases: &BTreeMap<String, S::C>,
142    ) -> bool {
143        let lhs = MSMKZG::<S::Engine>::from_base(&self.lhs.eval(fixed_bases));
144        let rhs = MSMKZG::<S::Engine>::from_base(&self.rhs.eval(fixed_bases));
145        DualMSM::new(lhs, rhs).check(params)
146    }
147
148    /// Returns a trivial accumulator that satisfies the pairing invariant.
149    ///
150    /// The variable-base scalar is 1 (matching the invariant of collapsed
151    /// accumulators, where the variable part has been collapsed to a single
152    /// base with scalar 1). The base is the identity point and all
153    /// fixed-base scalars are zero, so both sides evaluate to the identity
154    /// regardless.
155    pub fn trivial(fixed_base_names: &[String]) -> Self {
156        let zero_fixed = fixed_base_names.iter().map(|n| (n.clone(), S::F::ZERO)).collect();
157        Accumulator {
158            lhs: Msm::new(&[S::C::identity()], &[S::F::ONE], &BTreeMap::new()),
159            rhs: Msm::new(&[S::C::identity()], &[S::F::ONE], &zero_fixed),
160        }
161    }
162
163    /// An accumulator a given lhs and rhs terms respectively.
164    pub fn new(lhs: Msm<S>, rhs: Msm<S>) -> Self {
165        Accumulator { lhs, rhs }
166    }
167
168    /// The left-hand side of this accumulator.
169    pub fn lhs(&self) -> Msm<S> {
170        self.lhs.clone()
171    }
172
173    /// The right-hand side of this accumulator.
174    pub fn rhs(&self) -> Msm<S> {
175        self.rhs.clone()
176    }
177
178    /// Given the actual fixed bases, resolves the fixed-base part of the
179    /// internal MSMs by pairing each named scalar with its base and moving
180    /// them to regular variable-base entries.
181    ///
182    /// After this call, `fixed_base_scalars` of each internal MSM becomes
183    /// empty.
184    ///
185    /// # Panics
186    ///
187    /// If some of the keys in `fixed_base_scalars` from the internal MSMs do
188    /// not appear in the provided `fixed_bases` map.
189    pub fn resolve_fixed_bases(&mut self, fixed_bases: &BTreeMap<String, S::C>) {
190        self.lhs.resolve_fixed_bases(fixed_bases);
191        self.rhs.resolve_fixed_bases(fixed_bases);
192    }
193
194    /// Evaluates the variable part of the Accumulator collapsing each
195    /// side to a single point (and a scalar of 1), leaving the fixed-base part
196    /// of both sides intact.
197    ///
198    /// This function mutates self.
199    pub fn collapse(&mut self) {
200        self.lhs.collapse();
201        self.rhs.collapse();
202    }
203
204    /// Accumulates several accumulators together. The resulting acc will
205    /// satisfy the invariant iff all the accumulators individually do.
206    pub fn accumulate(accs: &[Self]) -> Self {
207        let hash_input =
208            accs.iter().flat_map(AssignedAccumulator::as_public_input).collect::<Vec<_>>();
209
210        let r = <S::SpongeChip as HashCPU<S::F, S::F>>::hash(&hash_input);
211        let rs = (0..accs.len()).map(|i| r.pow([i as u64]));
212        #[cfg(feature = "truncated-challenges")]
213        let rs = rs.map(truncate_off_circuit).collect::<Vec<_>>();
214
215        let mut acc = accs[0].clone();
216        for (other, ri) in accs.iter().zip(rs).skip(1) {
217            acc.lhs = acc.lhs.accumulate_with_r(&other.lhs, ri);
218            acc.rhs = acc.rhs.accumulate_with_r(&other.rhs, ri);
219        }
220
221        acc
222    }
223}
224
225impl<S: SelfEmulation> InnerValue for AssignedAccumulator<S> {
226    type Element = Accumulator<S>;
227
228    fn value(&self) -> Value<Accumulator<S>> {
229        (self.lhs.value())
230            .zip(self.rhs.value())
231            .map(|(lhs, rhs)| Accumulator { lhs, rhs })
232    }
233}
234
235impl<S: SelfEmulation> Instantiable<S::F> for AssignedAccumulator<S> {
236    fn as_public_input(acc: &Accumulator<S>) -> Vec<S::F> {
237        [
238            AssignedMsm::as_public_input(&acc.lhs),
239            AssignedMsm::as_public_input(&acc.rhs),
240        ]
241        .into_iter()
242        .flatten()
243        .collect()
244    }
245
246    fn from_public_input(_fields: &[S::F]) -> Option<Accumulator<S>> {
247        unimplemented!("Size of inner MSMs cannot be known from public input format.")
248    }
249}
250
251impl<S: SelfEmulation> AssignedAccumulator<S> {
252    /// Converts the off-circuit accumulator into two vectors of scalars. The
253    /// first will be used as a normal instance, whereas the second will be
254    /// plugged-in in as a committed instance.
255    ///
256    /// The committed instance part corresponds to the MSM (fixed and non-fixed)
257    /// scalars of the accumulator RHS.
258    pub fn as_public_input_with_committed_scalars(acc: &Accumulator<S>) -> (Vec<S::F>, Vec<S::F>) {
259        let (rhs_scalars, rhs_committed_scalars) =
260            AssignedMsm::as_public_input_with_committed_scalars(&acc.rhs);
261
262        let normal_instance = [AssignedMsm::as_public_input(&acc.lhs), rhs_scalars]
263            .into_iter()
264            .flatten()
265            .collect();
266
267        (normal_instance, rhs_committed_scalars)
268    }
269}
270
271impl<S: SelfEmulation> AssignedAccumulator<S> {
272    /// Witnesses an accumulator of `lhs_len` bases/scalars and a `BTreeMap` of
273    /// fixed_base_scalars indexed by the given `lhs_fixed_base_names`.
274    ///
275    /// Similar arguments determine the size and shape of the accumulator
276    /// right-hand side.
277    #[allow(clippy::too_many_arguments)]
278    pub fn assign(
279        layouter: &mut impl Layouter<S::F>,
280        curve_chip: &S::CurveChip,
281        scalar_chip: &S::ScalarChip,
282        lhs_len: usize,
283        rhs_len: usize,
284        lhs_fixed_base_names: &[String],
285        rhs_fixed_base_names: &[String],
286        acc_val: Value<Accumulator<S>>,
287    ) -> Result<Self, Error> {
288        let (acc_lhs_val, acc_rhs_val) = acc_val.map(|acc| (acc.lhs, acc.rhs)).unzip();
289        Ok(AssignedAccumulator::new(
290            AssignedMsm::<S>::assign(
291                layouter,
292                curve_chip,
293                scalar_chip,
294                lhs_len,
295                lhs_fixed_base_names,
296                acc_lhs_val,
297            )?,
298            AssignedMsm::<S>::assign(
299                layouter,
300                curve_chip,
301                scalar_chip,
302                rhs_len,
303                rhs_fixed_base_names,
304                acc_rhs_val,
305            )?,
306        ))
307    }
308
309    /// An `AssignedAccumulator` a given lhs and rhs terms respectively.
310    pub fn new(lhs: AssignedMsm<S>, rhs: AssignedMsm<S>) -> Self {
311        Self { lhs, rhs }
312    }
313
314    /// Scales the given acc by the given assigned bit.
315    ///
316    /// This function mutates self.
317    pub fn scale_by_bit(
318        layouter: &mut impl Layouter<S::F>,
319        scalar_chip: &S::ScalarChip,
320        cond: &AssignedBit<S::F>,
321        acc: &mut Self,
322    ) -> Result<(), Error> {
323        let cond_as_bounded = AssignedBoundedScalar {
324            scalar: cond.clone().into(),
325            bound: BigUint::one(),
326        };
327        acc.lhs.scale(layouter, scalar_chip, &cond_as_bounded)?;
328        acc.rhs.scale(layouter, scalar_chip, &cond_as_bounded)
329    }
330
331    /// Evaluates the variable part of the AssignedAccumulator collapsing each
332    /// side to a single point (and a scalar of 1), leaving the fixed-base part
333    /// of both sides intact.
334    ///
335    /// Calls to this function will probably be the bottleneck of any recursive
336    /// circuit, but it allows one to condense a carrying computation into a
337    /// single point, enabling powerful predicates such as
338    /// incrementally-verifiable computation (IVC).
339    ///
340    /// Alternatively, one may choose not to collapse an accumulator, fully
341    /// restrict it with public inputs and evaluate it off-circuit.
342    ///
343    /// This function mutates self.
344    pub fn collapse(
345        &mut self,
346        layouter: &mut impl Layouter<S::F>,
347        curve_chip: &S::CurveChip,
348        scalar_chip: &S::ScalarChip,
349    ) -> Result<(), Error> {
350        self.lhs.collapse(layouter, curve_chip, scalar_chip)?;
351        self.rhs.collapse(layouter, curve_chip, scalar_chip)
352    }
353
354    /// Given the actual fixed bases, resolves the fixed-base part of the
355    /// internal MSMs by pairing each named scalar with its base and moving
356    /// them to regular variable-base entries.
357    ///
358    /// After this call, `fixed_base_scalars` of each internal MSM becomes
359    /// empty.
360    ///
361    /// # Panics
362    ///
363    /// If some of the keys in `fixed_base_scalars` from the internal MSMs do
364    /// not appear in the provided `fixed_bases` map.
365    pub fn resolve_fixed_bases(&mut self, fixed_bases: &BTreeMap<String, S::AssignedPoint>) {
366        self.lhs.resolve_fixed_bases(fixed_bases);
367        self.rhs.resolve_fixed_bases(fixed_bases);
368    }
369
370    /// Accumulates several accumulators together. The resulting acc will
371    /// satisfy the invariant iff all the accumulators individually do.
372    pub fn accumulate(
373        layouter: &mut impl Layouter<S::F>,
374        acc_pi_chip: &impl PublicInputInstructions<S::F, AssignedAccumulator<S>>,
375        scalar_chip: &S::ScalarChip,
376        sponge_chip: &S::SpongeChip,
377        accs: &[Self],
378    ) -> Result<Self, Error> {
379        let hash_input = accs
380            .iter()
381            .map(|acc| acc_pi_chip.as_public_input(layouter, acc))
382            .collect::<Result<Vec<_>, Error>>()?
383            .into_iter()
384            .flatten()
385            .collect::<Vec<_>>();
386
387        let r = sponge_chip.hash(layouter, &hash_input)?;
388        #[cfg(feature = "truncated-challenges")]
389        let rs = truncated_powers::<S::F>(layouter, scalar_chip, &r, accs.len())?;
390        #[cfg(not(feature = "truncated-challenges"))]
391        let rs = powers::<S::F>(layouter, scalar_chip, &r, accs.len())?
392            .iter()
393            .map(|ri| AssignedBoundedScalar::new(ri, None))
394            .collect::<Vec<_>>();
395
396        let mut acc = accs[0].clone();
397        for (other, ri) in accs.iter().zip(rs).skip(1) {
398            acc.lhs = acc.lhs.accumulate_with_r(layouter, scalar_chip, &other.lhs, &ri)?;
399            acc.rhs = acc.rhs.accumulate_with_r(layouter, scalar_chip, &other.rhs, &ri)?;
400        }
401
402        Ok(acc)
403    }
404}