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 most efficient implementation
111        // c00 = cs0co0 + xi(cs1co2 + cs2co1 + cs4co4 + cs5co3)
112        // 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 c^2 * l_{2Q}
154    fn pre_loop(
155        Q_acc: Vec<AffinePoint<Self::Fp2>>,
156        _Q: &[AffinePoint<Self::Fp2>],
157        c: Option<Self::Fp12>,
158        xy_fracs: &[(Self::Fp, Self::Fp)],
159    ) -> (Self::Fp12, Vec<AffinePoint<Self::Fp2>>) {
160        let mut f = if let Some(mut c) = c {
161            // for the miller loop with embedded exponent, f will be set to c at the beginning of the function, and we
162            // will square c due to the last two values of the pseudo-binary encoding (BN254_PSEUDO_BINARY_ENCODING) being 0 and 1.
163            // Therefore, the final value of f at the end of this block is c^2.
164            c.square_assign();
165            c
166        } else {
167            Self::Fp12::ONE
168        };
169
170        let mut Q_acc = Q_acc;
171        let mut initial_lines = Vec::<EvaluatedLine<Self::Fp2>>::new();
172
173        // We don't need to special case the first iteration for Bn254, but since we are using the same Miller loop implementation
174        // for both Bn254 and Bls12_381, we need to do the first iteration separately here.
175        let (Q_out_double, lines_2S) = Q_acc
176            .into_iter()
177            .map(|Q| Self::miller_double_step(&Q))
178            .unzip::<_, _, Vec<_>, Vec<_>>();
179        Q_acc = Q_out_double;
180
181        let lines_iter = izip!(lines_2S.iter(), xy_fracs.iter());
182        for (line_2S, xy_frac) in lines_iter {
183            let line = line_2S.evaluate(xy_frac);
184            initial_lines.push(line);
185        }
186
187        f = Self::evaluate_lines_vec(f, initial_lines);
188
189        (f, Q_acc)
190    }
191
192    /// Compute f_{Miller,Q}(P) from f_{6x+2,Q}(P)
193    fn post_loop(
194        f: &Self::Fp12,
195        Q_acc: Vec<AffinePoint<Self::Fp2>>, // at this point, Q_acc = (6x+2)Q
196        Q: &[AffinePoint<Self::Fp2>],
197        _c: Option<Self::Fp12>,
198        xy_fracs: &[(Self::Fp, Self::Fp)],
199    ) -> (Self::Fp12, Vec<AffinePoint<Self::Fp2>>) {
200        let mut Q_acc = Q_acc;
201        let mut lines = Vec::<EvaluatedLine<Self::Fp2>>::new();
202
203        let x_to_q_minus_1_over_3 = &Self::FROBENIUS_COEFF_FQ6_C1[1];
204        let x_to_q_sq_minus_1_over_3 = &Self::FROBENIUS_COEFF_FQ6_C1[2];
205
206        // For each q, compute q1 such that `frob_p(twist(q)) = twist(q1)`
207        let q1_vec = Q
208            .iter()
209            .map(|Q| {
210                let x = Q.x.frobenius_map(1);
211                let x = x * x_to_q_minus_1_over_3;
212                let y = Q.y.frobenius_map(1);
213                let y = y * &Self::XI_TO_Q_MINUS_1_OVER_2;
214                AffinePoint { x, y }
215            })
216            .collect::<Vec<_>>();
217
218        // compute l_{(6x+2)\Psi(Q), \phi_p(\Psi(Q))} where \phi_p is the Frobenius map
219        let (Q_out_add, lines_S_plus_Q) = Q_acc
220            .iter()
221            .zip(q1_vec.iter())
222            .map(|(Q_acc, q1)| Self::miller_add_step(Q_acc, q1))
223            .unzip::<_, _, Vec<_>, Vec<_>>();
224        Q_acc = Q_out_add;
225
226        let lines_iter = izip!(lines_S_plus_Q.iter(), xy_fracs.iter());
227        for (lines_S_plus_Q, xy_frac) in lines_iter {
228            let line = lines_S_plus_Q.evaluate(xy_frac);
229            lines.push(line);
230        }
231
232        // For each q, compute q2 such that `-frob_p^2(twist(q)) = twist(q2)`
233        let q2_vec = Q
234            .iter()
235            .map(|Q| {
236                // There is a frobenius mapping π²(Q) that we skip here since it is equivalent to the identity mapping
237                let x = &Q.x * x_to_q_sq_minus_1_over_3;
238                AffinePoint { x, y: Q.y.clone() }
239            })
240            .collect::<Vec<_>>();
241
242        // compute l_{(6x+2)\Psi(Q) + \phi_p(\Psi(Q)), -(\phi_p)^2(\Psi(Q))} where \phi_p is the Frobenius map
243        let (Q_out_add, lines_S_plus_Q) = Q_acc
244            .iter()
245            .zip(q2_vec.iter())
246            .map(|(Q_acc, q2)| Self::miller_add_step(Q_acc, q2))
247            .unzip::<_, _, Vec<_>, Vec<_>>();
248        Q_acc = Q_out_add;
249
250        let lines_iter = izip!(lines_S_plus_Q.iter(), xy_fracs.iter());
251        for (lines_S_plus_Q, xy_frac) in lines_iter {
252            let line = lines_S_plus_Q.evaluate(xy_frac);
253            lines.push(line);
254        }
255
256        let mut f = f.clone();
257        f = Self::evaluate_lines_vec(f, lines);
258
259        (f, Q_acc)
260    }
261}
262
263#[allow(non_snake_case)]
264impl PairingCheck for Bn254 {
265    type Fp = Fp;
266    type Fp2 = Fp2;
267    type Fp12 = Fp12;
268
269    #[allow(unused_variables)]
270    fn pairing_check_hint(
271        P: &[AffinePoint<Self::Fp>],
272        Q: &[AffinePoint<Self::Fp2>],
273    ) -> (Self::Fp12, Self::Fp12) {
274        #[cfg(not(target_os = "zkvm"))]
275        {
276            #[cfg(not(feature = "halo2curves"))]
277            panic!("`halo2curves` feature must be enabled to use pairing check hint on host");
278
279            #[cfg(feature = "halo2curves")]
280            {
281                let p_halo2 = P
282                    .iter()
283                    .map(|p| {
284                        AffinePoint::new(
285                            convert_bn254_fp_to_halo2_fq(p.x.clone()),
286                            convert_bn254_fp_to_halo2_fq(p.y.clone()),
287                        )
288                    })
289                    .collect::<Vec<_>>();
290                let q_halo2 = Q
291                    .iter()
292                    .map(|q| {
293                        AffinePoint::new(
294                            convert_bn254_fp2_to_halo2_fq2(q.x.clone()),
295                            convert_bn254_fp2_to_halo2_fq2(q.y.clone()),
296                        )
297                    })
298                    .collect::<Vec<_>>();
299                let fq12 = Halo2CurvesBn254::multi_miller_loop(&p_halo2, &q_halo2);
300                let (c_fq12, s_fq12) = Halo2CurvesBn254::final_exp_hint(&fq12);
301                let c = convert_bn254_halo2_fq12_to_fp12(c_fq12);
302                let s = convert_bn254_halo2_fq12_to_fp12(s_fq12);
303                (c, s)
304            }
305        }
306        #[cfg(target_os = "zkvm")]
307        {
308            let hint = MaybeUninit::<(Fp12, Fp12)>::uninit();
309            // We do not rely on the slice P's memory layout since rust does not guarantee it across compiler versions.
310            let p_fat_ptr = (P.as_ptr() as u32, P.len() as u32);
311            let q_fat_ptr = (Q.as_ptr() as u32, Q.len() as u32);
312            unsafe {
313                custom_insn_r!(
314                    opcode = OPCODE,
315                    funct3 = PAIRING_FUNCT3,
316                    funct7 = ((Bn254::PAIRING_IDX as u8) * PairingBaseFunct7::PAIRING_MAX_KINDS + PairingBaseFunct7::HintFinalExp as u8),
317                    rd = Const "x0",
318                    rs1 = In &p_fat_ptr,
319                    rs2 = In &q_fat_ptr
320                );
321                let ptr = hint.as_ptr() as *const u8;
322                hint_buffer_u32!(ptr, (32 * 12 * 2) / 4);
323                hint.assume_init()
324            }
325        }
326    }
327
328    fn pairing_check(
329        P: &[AffinePoint<Self::Fp>],
330        Q: &[AffinePoint<Self::Fp2>],
331    ) -> Result<(), PairingCheckError> {
332        Self::try_honest_pairing_check(P, Q).unwrap_or_else(|| {
333            let f = Self::multi_miller_loop(P, Q);
334            exp_check_fallback(&f, &Self::FINAL_EXPONENT)
335        })
336    }
337}
338
339#[allow(non_snake_case)]
340impl Bn254 {
341    fn try_honest_pairing_check(
342        P: &[AffinePoint<<Self as PairingCheck>::Fp>],
343        Q: &[AffinePoint<<Self as PairingCheck>::Fp2>],
344    ) -> Option<Result<(), PairingCheckError>> {
345        let (c, u) = Self::pairing_check_hint(P, Q);
346        if c == Fp12::ZERO {
347            return None;
348        }
349        let c_inv = Fp12::ONE.div_unsafe(&c);
350
351        // We follow Theorem 3 of https://eprint.iacr.org/2024/640.pdf to check that the pairing equals 1
352        // By the theorem, it suffices to provide c and u such that f * u == c^λ.
353        // Since λ = 6x + 2 + q^3 - q^2 + q, we will check the equivalent condition:
354        // f * c^-{6x + 2} * u * c^-{q^3 - q^2 + q} == 1
355        // This is because we can compute f * c^-{6x+2} by embedding the c^-{6x+2} computation in the miller loop.
356
357        // c_mul = c^-{q^3 - q^2 + q}
358        let c_q3_inv = FieldExtension::frobenius_map(&c_inv, 3);
359        let c_q2 = FieldExtension::frobenius_map(&c, 2);
360        let c_q_inv = FieldExtension::frobenius_map(&c_inv, 1);
361        let c_mul = c_q3_inv * c_q2 * c_q_inv;
362
363        // Pass c inverse into the miller loop so that we compute fc == f * c^-{6x + 2}
364        let fc = Self::multi_miller_loop_embedded_exp(P, Q, Some(c_inv));
365
366        if fc * c_mul * u == Fp12::ONE {
367            Some(Ok(()))
368        } else {
369            None
370        }
371    }
372}