openvm_pairing_guest/bn254/
pairing.rs

1use alloc::vec::Vec;
2
3use itertools::izip;
4use openvm_algebra_guest::{field::FieldExtension, DivUnsafe, Field};
5use openvm_ecc_guest::AffinePoint;
6#[cfg(target_os = "zkvm")]
7use {
8    crate::{PairingBaseFunct7, OPCODE, PAIRING_FUNCT3},
9    core::mem::MaybeUninit,
10    openvm_platform::custom_insn_r,
11    openvm_rv32im_guest::hint_buffer_u32,
12};
13
14use super::{Bn254, Fp, Fp12, Fp2, BN254_PSEUDO_BINARY_ENCODING, BN254_SEED};
15use crate::pairing::{
16    exp_check_fallback, Evaluatable, EvaluatedLine, FromLineDType, LineMulDType, MillerStep,
17    MultiMillerLoop, PairingCheck, PairingCheckError, PairingIntrinsics, UnevaluatedLine,
18};
19#[cfg(all(feature = "halo2curves", not(target_os = "zkvm")))]
20use crate::{
21    bn254::utils::{
22        convert_bn254_fp2_to_halo2_fq2, convert_bn254_fp_to_halo2_fq,
23        convert_bn254_halo2_fq12_to_fp12,
24    },
25    halo2curves_shims::bn254::Bn254 as Halo2CurvesBn254,
26    pairing::FinalExp,
27};
28
29impl Evaluatable<Fp, Fp2> for UnevaluatedLine<Fp2> {
30    fn evaluate(&self, xy_frac: &(Fp, Fp)) -> EvaluatedLine<Fp2> {
31        let (x_over_y, y_inv) = xy_frac;
32        // Represents the line L(x,y) = 1 + b (x/y) w^1 + c (1/y) w^3
33        EvaluatedLine {
34            b: self.b.mul_base(x_over_y),
35            c: self.c.mul_base(y_inv),
36        }
37    }
38}
39
40impl FromLineDType<Fp2> for Fp12 {
41    fn from_evaluated_line_d_type(line: EvaluatedLine<Fp2>) -> Fp12 {
42        FieldExtension::<Fp2>::from_coeffs([
43            Fp2::ONE,
44            line.b,
45            Fp2::ZERO,
46            line.c,
47            Fp2::ZERO,
48            Fp2::ZERO,
49        ])
50    }
51}
52
53// TODO[jpw]: make this into a macro depending on P::PAIRING_IDX when we have more curves
54impl LineMulDType<Fp2, Fp12> for Bn254 {
55    /// Multiplies two lines in 013-form to get an element in 01234-form
56    fn mul_013_by_013(l0: &EvaluatedLine<Fp2>, l1: &EvaluatedLine<Fp2>) -> [Fp2; 5] {
57        let b0 = &l0.b;
58        let c0 = &l0.c;
59        let b1 = &l1.b;
60        let c1 = &l1.c;
61
62        // where w⁶ = xi
63        // l0 * l1 = 1 + (b0 + b1)w + (b0b1)w² + (c0 + c1)w³ + (b0c1 + b1c0)w⁴ + (c0c1)w⁶
64        //         = (1 + c0c1 * xi) + (b0 + b1)w + (b0b1)w² + (c0 + c1)w³ + (b0c1 + b1c0)w⁴
65        let x0 = Fp2::ONE + c0 * c1 * &Bn254::XI;
66        let x1 = b0 + b1;
67        let x2 = b0 * b1;
68        let x3 = c0 + c1;
69        let x4 = b0 * c1 + b1 * c0;
70
71        [x0, x1, x2, x3, x4]
72    }
73
74    /// Multiplies a line in 013-form with a Fp12 element to get an Fp12 element
75    fn mul_by_013(f: &Fp12, l: &EvaluatedLine<Fp2>) -> Fp12 {
76        Fp12::from_evaluated_line_d_type(l.clone()) * f
77    }
78
79    /// Multiplies a line in 01234-form with a Fp12 element to get an Fp12 element
80    fn mul_by_01234(f: &Fp12, x: &[Fp2; 5]) -> Fp12 {
81        // we update the order of the coefficients to match the Fp12 coefficient ordering:
82        // Fp12 {
83        //   c0: Fp6 {
84        //     c0: x0,
85        //     c1: x2,
86        //     c2: x4,
87        //   },
88        //   c1: Fp6 {
89        //     c0: x1,
90        //     c1: x3,
91        //     c2: x5,
92        //   },
93        // }
94        let o0 = &x[0];
95        let o1 = &x[2];
96        let o2 = &x[4];
97        let o3 = &x[1];
98        let o4 = &x[3];
99
100        let xi = &Bn254::XI;
101
102        let self_coeffs = &f.c;
103        let s0 = &self_coeffs[0];
104        let s1 = &self_coeffs[2];
105        let s2 = &self_coeffs[4];
106        let s3 = &self_coeffs[1];
107        let s4 = &self_coeffs[3];
108        let s5 = &self_coeffs[5];
109
110        // NOTE[yj]: Hand-calculated multiplication for Fp12 * 01234 ∈ Fp2; this is likely not the
111        // most efficient implementation c00 = cs0co0 + xi(cs1co2 + cs2co1 + cs4co4 +
112        // cs5co3) c01 = cs0co1 + cs1co0 + cs3co3 + xi(cs2co2 + cs5co4)
113        // c02 = cs0co2 + cs1co1 + cs2co0 + cs3co4 + cs4co3
114        // c10 = cs0co3 + cs3co0 + xi(cs2co4 + cs4co2 + cs5co1)
115        // c11 = cs0co4 + cs1co3 + cs3co1 + cs4co0 + xi(cs5co2)
116        // c12 = cs1co4 + cs2co3 + cs3co2 + cs4co1 + cs5co0
117        let c00 = s0 * o0 + xi * &(s1 * o2 + s2 * o1 + s4 * o4 + s5 * o3);
118        let c01 = s0 * o1 + s1 * o0 + s3 * o3 + xi * &(s2 * o2 + s5 * o4);
119        let c02 = s0 * o2 + s1 * o1 + s2 * o0 + s3 * o4 + s4 * o3;
120        let c10 = s0 * o3 + s3 * o0 + xi * &(s2 * o4 + s4 * o2 + s5 * o1);
121        let c11 = s0 * o4 + s1 * o3 + s3 * o1 + s4 * o0 + xi * &(s5 * o2);
122        let c12 = s1 * o4 + s2 * o3 + s3 * o2 + s4 * o1 + s5 * o0;
123
124        Fp12::from_coeffs([c00, c10, c01, c11, c02, c12])
125    }
126}
127
128#[allow(non_snake_case)]
129impl MultiMillerLoop for Bn254 {
130    type Fp = Fp;
131    type Fp12 = Fp12;
132
133    const SEED_ABS: u64 = BN254_SEED;
134    const PSEUDO_BINARY_ENCODING: &[i8] = &BN254_PSEUDO_BINARY_ENCODING;
135
136    fn evaluate_lines_vec(f: Self::Fp12, lines: Vec<EvaluatedLine<Self::Fp2>>) -> Self::Fp12 {
137        let mut f = f;
138        let mut lines = lines;
139        if lines.len() % 2 == 1 {
140            f = Self::mul_by_013(&f, &lines.pop().unwrap());
141        }
142        for chunk in lines.chunks(2) {
143            if let [line0, line1] = chunk {
144                let prod = Self::mul_013_by_013(line0, line1);
145                f = Self::mul_by_01234(&f, &prod);
146            } else {
147                panic!("lines.len() % 2 should be 0 at this point");
148            }
149        }
150        f
151    }
152
153    /// The expected output of this function when running the Miller loop with embedded exponent is
154    /// c^2 * l_{2Q}
155    fn pre_loop(
156        Q_acc: Vec<AffinePoint<Self::Fp2>>,
157        _Q: &[AffinePoint<Self::Fp2>],
158        c: Option<Self::Fp12>,
159        xy_fracs: &[(Self::Fp, Self::Fp)],
160    ) -> (Self::Fp12, Vec<AffinePoint<Self::Fp2>>) {
161        let mut f = if let Some(mut c) = c {
162            // for the miller loop with embedded exponent, f will be set to c at the beginning of
163            // the function, and we will square c due to the last two values of the
164            // pseudo-binary encoding (BN254_PSEUDO_BINARY_ENCODING) being 0 and 1.
165            // Therefore, the final value of f at the end of this block is c^2.
166            c.square_assign();
167            c
168        } else {
169            Self::Fp12::ONE
170        };
171
172        let mut Q_acc = Q_acc;
173        let mut initial_lines = Vec::<EvaluatedLine<Self::Fp2>>::new();
174
175        // We don't need to special case the first iteration for Bn254, but since we are using the
176        // same Miller loop implementation for both Bn254 and Bls12_381, we need to do the
177        // first iteration separately here.
178        let (Q_out_double, lines_2S) = Q_acc
179            .into_iter()
180            .map(|Q| Self::miller_double_step(&Q))
181            .unzip::<_, _, Vec<_>, Vec<_>>();
182        Q_acc = Q_out_double;
183
184        let lines_iter = izip!(lines_2S.iter(), xy_fracs.iter());
185        for (line_2S, xy_frac) in lines_iter {
186            let line = line_2S.evaluate(xy_frac);
187            initial_lines.push(line);
188        }
189
190        f = Self::evaluate_lines_vec(f, initial_lines);
191
192        (f, Q_acc)
193    }
194
195    /// Compute f_{Miller,Q}(P) from f_{6x+2,Q}(P)
196    fn post_loop(
197        f: &Self::Fp12,
198        Q_acc: Vec<AffinePoint<Self::Fp2>>, // at this point, Q_acc = (6x+2)Q
199        Q: &[AffinePoint<Self::Fp2>],
200        _c: Option<Self::Fp12>,
201        xy_fracs: &[(Self::Fp, Self::Fp)],
202    ) -> (Self::Fp12, Vec<AffinePoint<Self::Fp2>>) {
203        let mut Q_acc = Q_acc;
204        let mut lines = Vec::<EvaluatedLine<Self::Fp2>>::new();
205
206        let x_to_q_minus_1_over_3 = &Self::FROBENIUS_COEFF_FQ6_C1[1];
207        let x_to_q_sq_minus_1_over_3 = &Self::FROBENIUS_COEFF_FQ6_C1[2];
208
209        // For each q, compute q1 such that `frob_p(twist(q)) = twist(q1)`
210        let q1_vec = Q
211            .iter()
212            .map(|Q| {
213                let x = Q.x.frobenius_map(1);
214                let x = x * x_to_q_minus_1_over_3;
215                let y = Q.y.frobenius_map(1);
216                let y = y * &Self::XI_TO_Q_MINUS_1_OVER_2;
217                AffinePoint { x, y }
218            })
219            .collect::<Vec<_>>();
220
221        // compute l_{(6x+2)\Psi(Q), \phi_p(\Psi(Q))} where \phi_p is the Frobenius map
222        let (Q_out_add, lines_S_plus_Q) = Q_acc
223            .iter()
224            .zip(q1_vec.iter())
225            .map(|(Q_acc, q1)| Self::miller_add_step(Q_acc, q1))
226            .unzip::<_, _, Vec<_>, Vec<_>>();
227        Q_acc = Q_out_add;
228
229        let lines_iter = izip!(lines_S_plus_Q.iter(), xy_fracs.iter());
230        for (lines_S_plus_Q, xy_frac) in lines_iter {
231            let line = lines_S_plus_Q.evaluate(xy_frac);
232            lines.push(line);
233        }
234
235        // For each q, compute q2 such that `-frob_p^2(twist(q)) = twist(q2)`
236        let q2_vec = Q
237            .iter()
238            .map(|Q| {
239                // There is a frobenius mapping π²(Q) that we skip here since it is equivalent to
240                // the identity mapping
241                let x = &Q.x * x_to_q_sq_minus_1_over_3;
242                AffinePoint { x, y: Q.y.clone() }
243            })
244            .collect::<Vec<_>>();
245
246        // compute l_{(6x+2)\Psi(Q) + \phi_p(\Psi(Q)), -(\phi_p)^2(\Psi(Q))} where \phi_p is the
247        // Frobenius map
248        let (Q_out_add, lines_S_plus_Q) = Q_acc
249            .iter()
250            .zip(q2_vec.iter())
251            .map(|(Q_acc, q2)| Self::miller_add_step(Q_acc, q2))
252            .unzip::<_, _, Vec<_>, Vec<_>>();
253        Q_acc = Q_out_add;
254
255        let lines_iter = izip!(lines_S_plus_Q.iter(), xy_fracs.iter());
256        for (lines_S_plus_Q, xy_frac) in lines_iter {
257            let line = lines_S_plus_Q.evaluate(xy_frac);
258            lines.push(line);
259        }
260
261        let mut f = f.clone();
262        f = Self::evaluate_lines_vec(f, lines);
263
264        (f, Q_acc)
265    }
266}
267
268#[allow(non_snake_case)]
269impl PairingCheck for Bn254 {
270    type Fp = Fp;
271    type Fp2 = Fp2;
272    type Fp12 = Fp12;
273
274    #[allow(unused_variables)]
275    fn pairing_check_hint(
276        P: &[AffinePoint<Self::Fp>],
277        Q: &[AffinePoint<Self::Fp2>],
278    ) -> (Self::Fp12, Self::Fp12) {
279        #[cfg(not(target_os = "zkvm"))]
280        {
281            #[cfg(not(feature = "halo2curves"))]
282            panic!("`halo2curves` feature must be enabled to use pairing check hint on host");
283
284            #[cfg(feature = "halo2curves")]
285            {
286                let p_halo2 = P
287                    .iter()
288                    .map(|p| {
289                        AffinePoint::new(
290                            convert_bn254_fp_to_halo2_fq(p.x.clone()),
291                            convert_bn254_fp_to_halo2_fq(p.y.clone()),
292                        )
293                    })
294                    .collect::<Vec<_>>();
295                let q_halo2 = Q
296                    .iter()
297                    .map(|q| {
298                        AffinePoint::new(
299                            convert_bn254_fp2_to_halo2_fq2(q.x.clone()),
300                            convert_bn254_fp2_to_halo2_fq2(q.y.clone()),
301                        )
302                    })
303                    .collect::<Vec<_>>();
304                let fq12 = Halo2CurvesBn254::multi_miller_loop(&p_halo2, &q_halo2);
305                let (c_fq12, s_fq12) = Halo2CurvesBn254::final_exp_hint(&fq12);
306                let c = convert_bn254_halo2_fq12_to_fp12(c_fq12);
307                let s = convert_bn254_halo2_fq12_to_fp12(s_fq12);
308                (c, s)
309            }
310        }
311        #[cfg(target_os = "zkvm")]
312        {
313            let hint = MaybeUninit::<(Fp12, Fp12)>::uninit();
314            // We do not rely on the slice P's memory layout since rust does not guarantee it across
315            // compiler versions.
316            let p_fat_ptr = (P.as_ptr() as u32, P.len() as u32);
317            let q_fat_ptr = (Q.as_ptr() as u32, Q.len() as u32);
318            unsafe {
319                custom_insn_r!(
320                    opcode = OPCODE,
321                    funct3 = PAIRING_FUNCT3,
322                    funct7 = ((Bn254::PAIRING_IDX as u8) * PairingBaseFunct7::PAIRING_MAX_KINDS + PairingBaseFunct7::HintFinalExp as u8),
323                    rd = Const "x0",
324                    rs1 = In &p_fat_ptr,
325                    rs2 = In &q_fat_ptr
326                );
327                let ptr = hint.as_ptr() as *const u8;
328                hint_buffer_u32!(ptr, (32 * 12 * 2) / 4);
329                hint.assume_init()
330            }
331        }
332    }
333
334    fn pairing_check(
335        P: &[AffinePoint<Self::Fp>],
336        Q: &[AffinePoint<Self::Fp2>],
337    ) -> Result<(), PairingCheckError> {
338        Self::try_honest_pairing_check(P, Q).unwrap_or_else(|| {
339            let f = Self::multi_miller_loop(P, Q);
340            exp_check_fallback(&f, &Self::FINAL_EXPONENT)
341        })
342    }
343}
344
345#[allow(non_snake_case)]
346impl Bn254 {
347    fn try_honest_pairing_check(
348        P: &[AffinePoint<<Self as PairingCheck>::Fp>],
349        Q: &[AffinePoint<<Self as PairingCheck>::Fp2>],
350    ) -> Option<Result<(), PairingCheckError>> {
351        let (c, u) = Self::pairing_check_hint(P, Q);
352        if c == Fp12::ZERO {
353            return None;
354        }
355        let c_inv = Fp12::ONE.div_unsafe(&c);
356
357        // We follow Theorem 3 of https://eprint.iacr.org/2024/640.pdf to check that the pairing equals 1
358        // By the theorem, it suffices to provide c and u such that f * u == c^λ.
359        // Since λ = 6x + 2 + q^3 - q^2 + q, we will check the equivalent condition:
360        // f * c^-{6x + 2} * u * c^-{q^3 - q^2 + q} == 1
361        // This is because we can compute f * c^-{6x+2} by embedding the c^-{6x+2} computation in
362        // the miller loop.
363
364        // c_mul = c^-{q^3 - q^2 + q}
365        let c_q3_inv = FieldExtension::frobenius_map(&c_inv, 3);
366        let c_q2 = FieldExtension::frobenius_map(&c, 2);
367        let c_q_inv = FieldExtension::frobenius_map(&c_inv, 1);
368        let c_mul = c_q3_inv * c_q2 * c_q_inv;
369
370        // Pass c inverse into the miller loop so that we compute fc == f * c^-{6x + 2}
371        let fc = Self::multi_miller_loop_embedded_exp(P, Q, Some(c_inv));
372
373        if fc * c_mul * u == Fp12::ONE {
374            Some(Ok(()))
375        } else {
376            None
377        }
378    }
379}