BN254MillerLoop.jl
1 # BN254 Miller loop implementation for the optimal ate pairing. 2 # 3 # Implements the Miller loop core of the BN254 pairing. The ate loop is shorter 4 # than the Tate loop, improving performance. 5 # 6 # References: 7 # - Aranha et al., "The Realm of the Pairings", 2013. 8 # - LambdaClass, "How we implemented the BN254 Ate pairing in lambdaworks", 9 # 2024. 10 # - Arkworks `ark-bn254` for sparse multiplication helpers. 11 12 # using GrothAlgebra 13 # using StaticArrays 14 15 # Import what we need 16 import GrothCurves: conjugate, BN254_PRIME 17 18 # BN254 parameter u = 4965661367192848881 19 # For optimal ate pairing with Frobenius corrections, we iterate over u only 20 # The factor of 6 and addition of 2 are handled by the two Frobenius correction lines 21 const BN254_U = BigInt(4965661367192848881) 22 const BN254_U_IS_NEGATIVE = false # u > 0 for BN254 23 # NAF representation of u for the ate loop (from arkworks) 24 # This is the signed binary representation where digits are in {0, 1, -1} 25 const ATE_LOOP_COUNT_NAF = Int8[ 26 0, 0, 0, 1, 0, 1, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, -1, 0, -1, 0, 0, 0, 1, 0, -1, 0, 0, 0, 27 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, -1, 0, 0, -1, 0, 1, 0, -1, 0, 0, 0, -1, 0, 28 -1, 0, 0, 0, 1, 0, 1, 1, 29 ] 30 31 # ============================================================================ 32 # p-power Endomorphism Constants for G2 Pairing 33 # ============================================================================ 34 35 # These are the correct p-power endomorphism coefficients from arkworks 36 # PSI_X = (u+9)^((p-1)/3) for x-coordinate multiplication after Frobenius 37 const P_POWER_ENDOMORPHISM_COEFF_0 = Fp2Element( 38 bn254_field(parse(BigInt, "21575463638280843010398324269430826099269044274347216827212613867836435027261")), 39 bn254_field(parse(BigInt, "10307601595873709700152284273816112264069230130616436755625194854815875713954")) 40 ) 41 42 # PSI_Y = (u+9)^((p-1)/2) for y-coordinate multiplication after Frobenius 43 const P_POWER_ENDOMORPHISM_COEFF_1 = Fp2Element( 44 bn254_field(parse(BigInt, "2821565182194536844548159561693502659359617185244120367078079554186484126554")), 45 bn254_field(parse(BigInt, "3505843767911556378687030309984248845540243509899259641013678093033130930403")) 46 ) 47 48 # Note: For π²(Q), we compose the endomorphism twice rather than using squared coefficients 49 50 """ 51 LineCoeffs 52 53 Raw coefficients for a line function in the Miller loop, before evaluation at P. 54 For D-twist, the line has form: a*y + b*x + c where x,y are indeterminates. 55 Note the order difference from M-twist! 56 """ 57 struct LineCoeffs 58 a::Fp2Element # y coefficient (for D-twist) 59 b::Fp2Element # x coefficient (for D-twist) 60 c::Fp2Element # constant term 61 end 62 63 """ 64 doubling_step(T::G2Point) -> (G2Point, LineCoeffs) 65 66 Compute the doubling step in the Miller loop. 67 Returns 2T and the raw line coefficients for the tangent line at T. 68 69 Uses D-twist formulas for BN254 with Jacobian coordinates. 70 """ 71 function doubling_step(T::G2Point) 72 # Handle point at infinity 73 if iszero(T) 74 return (T, LineCoeffs(zero(Fp2Element), zero(Fp2Element), one(Fp2Element))) 75 end 76 77 # Get coordinates of T in Jacobian form 78 X, Y, Z = x_coord(T), y_coord(T), z_coord(T) 79 80 # Precomputations 81 X2 = X^2 82 X3 = X2 * X 83 Y2 = Y^2 84 Y4 = Y2^2 85 Z2 = Z^2 86 Z3 = Z2 * Z 87 Z6 = Z2^3 # More efficient than Z3 * Z3 88 89 # Step 1: Compute line coefficients for D-twist 90 # For point T = (X:Y:Z) where x = X/Z², y = Y/Z³ 91 # D-twist coefficients (note the different order from M-twist): 92 a = -Fp2Element(2) * Y * Z3 # y coefficient (D-twist) 93 b = Fp2Element(3) * X2 * Z2 # x coefficient (D-twist) 94 c = -X3 + Fp2Element(2) * G2_B_TWIST * Z6 # constant term 95 96 # Step 2: NOW compute 2T 97 S = Fp2Element(4) * X * Y2 98 M = Fp2Element(3) * X2 99 T_val = M^2 - Fp2Element(2) * S 100 101 # New coordinates for 2T 102 X_2T = T_val 103 Y_2T = M * (S - T_val) - Fp2Element(8) * Y4 104 Z_2T = Fp2Element(2) * Y * Z 105 106 result_point = G2Point(X_2T, Y_2T, Z_2T) 107 coeffs = LineCoeffs(a, b, c) 108 109 return (result_point, coeffs) 110 end 111 112 """ 113 addition_step(T::G2Point, Q::G2Point) -> (G2Point, LineCoeffs) 114 115 Compute the addition step in the Miller loop. 116 Returns T+Q and the raw line coefficients for the line through T and Q. 117 118 This implements the first equation from page 14 of "The Realm of the Pairings". 119 Uses mixed addition with Q in affine coordinates. 120 """ 121 function addition_step(T::G2Point, Q::G2Point) 122 # Handle special cases 123 if iszero(T) 124 return (Q, LineCoeffs(zero(Fp2Element), zero(Fp2Element), one(Fp2Element))) 125 end 126 if iszero(Q) 127 return (T, LineCoeffs(zero(Fp2Element), zero(Fp2Element), one(Fp2Element))) 128 end 129 130 # If T == Q, use doubling instead 131 if T == Q 132 return doubling_step(T) 133 end 134 135 # Get coordinates 136 X, Y, Z = x_coord(T), y_coord(T), z_coord(T) 137 xQ, yQ = to_affine(Q) # Q in affine (common for pairing) 138 139 # Mixed addition formulas (T projective, Q affine) 140 Z2 = Z^2 141 Z3 = Z2 * Z 142 143 # Bring Q to same projective denominator as T 144 U2 = xQ * Z2 145 S2 = yQ * Z3 146 147 # Differences 148 H = U2 - X # x-coordinate difference 149 R = S2 - Y # y-coordinate difference 150 151 # Check if points are equal (shouldn't happen if we checked T == Q above) 152 if iszero(H) && iszero(R) 153 return doubling_step(T) 154 end 155 156 # Step 1: Compute line coefficients for D-twist BEFORE mutating T 157 # For Jacobian coordinates where T = (X:Y:Z) with x = X/Z², y = Y/Z³ 158 # and Q = (xQ, yQ) in affine 159 # D-twist coefficients (note the different order from M-twist): 160 a = -Z * H # coefficient for y (D-twist) 161 b = R # coefficient for x (D-twist) 162 c = Z * (H * yQ) - R * xQ # constant term 163 164 # Step 2: NOW compute T + Q (mutate the point) 165 H2 = H^2 166 H3 = H2 * H 167 168 X3 = R^2 - H3 - Fp2Element(2) * X * H2 169 Y3 = R * (X * H2 - X3) - Y * H3 170 Z3 = Z * H 171 172 result_point = G2Point(X3, Y3, Z3) 173 coeffs = LineCoeffs(a, b, c) 174 175 return (result_point, coeffs) 176 end 177 178 """ 179 evaluate_line(coeffs::LineCoeffs, P::G1Point) -> Fp12Element 180 181 Evaluate the line function at point P and embed into Fp12. 182 Uses sparse 0-1-4 embedding matching arkworks' mul_by_014. 183 184 The line a*x + b*y + c is evaluated at P = (x_P, y_P) to give 185 a*x_P + b*y_P + c, which is then embedded into Fp12. 186 """ 187 function evaluate_line(coeffs::LineCoeffs, P::G1Point) 188 # Get affine coordinates of P 189 P_x, P_y = if iszero(P) 190 (zero(BN254Field), zero(BN254Field)) 191 else 192 to_affine(P) 193 end 194 195 # Convert to Fp2 for computation 196 xP = Fp2Element(P_x, zero(BN254Field)) 197 yP = Fp2Element(P_y, zero(BN254Field)) 198 199 # D-twist embedding - different from M-twist! 200 # For D-twist, the line evaluation gives a sparse Fp12 element 201 # with non-zero entries at positions (0, 3, 4) using mul_by_034 format 202 # Line ℓ(P) = a*yP + b*xP*w + c*w² 203 # This maps to Fp12 coordinates as: 204 # c0 = (a*yP, 0, 0) 205 # c1 = (b*xP, c, 0) 206 207 # Build sparse Fp12 element with correct D-twist mapping 208 c0_fp6 = Fp6Element(coeffs.a * yP, zero(Fp2Element), zero(Fp2Element)) 209 c1_fp6 = Fp6Element(coeffs.b * xP, coeffs.c, zero(Fp2Element)) 210 211 return Fp12Element(c0_fp6, c1_fp6) 212 end 213 214 """ 215 p_power_endomorphism(Q::G2Point) -> G2Point 216 217 Apply the p-power endomorphism to a G2 point. 218 This is the untwist-Frobenius-twist endomorphism: ψ ∘ π_p ∘ ψ^(-1) 219 Maps (x,y) → (x^p * PSI_X, y^p * PSI_Y) 220 """ 221 function p_power_endomorphism(Q::G2Point) 222 if iszero(Q) 223 return Q 224 end 225 226 # Get affine coordinates 227 x, y = to_affine(Q) 228 229 # Apply Frobenius (conjugation in Fp2) 230 x_frob = conjugate(x) 231 y_frob = conjugate(y) 232 233 # Multiply by the p-power endomorphism coefficients 234 x_final = x_frob * P_POWER_ENDOMORPHISM_COEFF_0 235 y_final = y_frob * P_POWER_ENDOMORPHISM_COEFF_1 236 237 return G2Point(x_final, y_final) 238 end 239 240 """ 241 frobenius_g2(Q::G2Point, power::Int) -> G2Point 242 243 Apply powers of the p-power endomorphism to a G2 point. 244 For power=1: applies p_power_endomorphism once 245 For power=2: applies it twice (π²) 246 """ 247 function frobenius_g2(Q::G2Point, power::Int) 248 if iszero(Q) 249 return Q 250 end 251 252 if power == 1 253 # π(Q) = p-power endomorphism 254 return p_power_endomorphism(Q) 255 256 elseif power == 2 257 # π²(Q) = apply p-power endomorphism twice 258 # We need to compose: π²(Q) = π(π(Q)) 259 intermediate = p_power_endomorphism(Q) 260 return p_power_endomorphism(intermediate) 261 262 else 263 # For other powers, compose 264 result = Q 265 for _ in 1:power 266 result = p_power_endomorphism(result) 267 end 268 return result 269 end 270 end 271 272 """ 273 miller_loop(::BN254Engine, P::G1Point, Q::G2Point) -> Fp12Element 274 275 Compute the Miller loop for the optimal ate pairing using the BN254 backend. 276 This is the main computational component of the pairing. 277 278 The loop iterates over u (not 6u+2), with two Frobenius correction lines 279 at the end to achieve the full optimal ate pairing. 280 """ 281 function miller_loop(::BN254Engine, P::G1Point, Q::G2Point) 282 # Handle special cases 283 if iszero(P) || iszero(Q) 284 return one(Fp12Element) 285 end 286 287 # Initialize 288 T = Q 289 f = one(Fp12Element) 290 291 # Process NAF representation from MSB to LSB 292 # The loop matches arkworks: iterate from len-1 down to 1, indexing bits at i-1 293 for i in (length(ATE_LOOP_COUNT_NAF)):-1:2 294 # Square f (except on the first iteration) 295 if i != length(ATE_LOOP_COUNT_NAF) 296 f = f^2 297 end 298 299 # Always perform doubling step 300 T_new, line_coeffs = doubling_step(T) 301 T = T_new 302 f = f * evaluate_line(line_coeffs, P) 303 304 # Addition/subtraction step based on NAF digit at index i-1 305 bit = ATE_LOOP_COUNT_NAF[i-1] 306 if bit == 1 307 T_new, line_coeffs = addition_step(T, Q) 308 T = T_new 309 f = f * evaluate_line(line_coeffs, P) 310 elseif bit == -1 311 T_new, line_coeffs = addition_step(T, -Q) 312 T = T_new 313 f = f * evaluate_line(line_coeffs, P) 314 end 315 # If bit == 0, we do nothing extra 316 end 317 318 # For BN254 ate pairing, we need two Frobenius-based correction steps 319 # These are essential for bilinearity to work correctly 320 321 # Compute Frobenius images of Q 322 # π(Q) applies Frobenius to the x,y coordinates 323 Q_pi = frobenius_g2(Q, 1) # π(Q) 324 Q_pi2 = frobenius_g2(Q, 2) # π²(Q) 325 326 # Correction step 1: Line through T and π(Q) 327 T1, line1 = addition_step(T, Q_pi) 328 f = f * evaluate_line(line1, P) 329 330 # Correction step 2: Line through T1 and -π²(Q) 331 neg_Q_pi2 = -Q_pi2 # Use proper group negation 332 T2, line2 = addition_step(T1, neg_Q_pi2) 333 f = f * evaluate_line(line2, P) 334 335 return f 336 end 337 338 """ 339 miller_loop(P::G1Point, Q::G2Point) 340 341 Convenience overload that reuses the shared BN254 engine. 342 """ 343 miller_loop(P::G1Point, Q::G2Point) = miller_loop(BN254_ENGINE, P, Q) 344 345 # Export functions 346 export LineCoeffs, doubling_step, addition_step, evaluate_line, miller_loop 347 export frobenius_g2, p_power_endomorphism 348 export ATE_LOOP_COUNT_NAF