this repo has no description
at main 297 lines 11 kB view raw
1const std = @import("std"); 2const Secp256k1 = std.crypto.ecc.Secp256k1; 3const Fe = @import("field.zig").Fe; 4const StdFe = Secp256k1.Fe; 5const StdAffineCoordinates = @TypeOf(Secp256k1.basePoint.affineCoordinates()); 6 7/// Affine point in Fe representation. 8pub const AffinePoint = struct { 9 x: Fe, 10 y: Fe, 11 12 pub const identity: AffinePoint = .{ .x = Fe.zero, .y = Fe.zero }; 13 14 pub fn neg(self: AffinePoint) AffinePoint { 15 return .{ .x = self.x, .y = self.y.neg(1) }; 16 } 17 18 pub fn fromStdlib(ac: StdAffineCoordinates) AffinePoint { 19 return .{ 20 .x = Fe.fromBytes(ac.x.toBytes(.big), .big), 21 .y = Fe.fromBytes(ac.y.toBytes(.big), .big), 22 }; 23 } 24 25 pub fn fromStdlibIdentity(ac: StdAffineCoordinates) AffinePoint { 26 // check if this is the identity element (x=0, y=0 in stdlib) 27 if (ac.x.isZero() and ac.y.isZero()) return identity; 28 return fromStdlib(ac); 29 } 30}; 31 32/// Jacobian point on secp256k1 using Fe: affine (X/Z², Y/Z³). 33/// Uses a=0 specialized formulas for fewer field operations than 34/// the stdlib's complete projective formulas. 35pub const JacobianPoint = struct { 36 x: Fe, 37 y: Fe, 38 z: Fe, 39 40 pub const identity: JacobianPoint = .{ .x = Fe.zero, .y = Fe.one, .z = Fe.zero }; 41 42 /// Convert Fe affine (x, y) to Jacobian (x, y, 1). 43 pub fn fromAffine(p: AffinePoint) JacobianPoint { 44 return .{ .x = p.x, .y = p.y, .z = Fe.one }; 45 } 46 47 /// Convert stdlib affine to Jacobian via Fe. 48 pub fn fromStdlibAffine(ac: StdAffineCoordinates) JacobianPoint { 49 return fromAffine(AffinePoint.fromStdlib(ac)); 50 } 51 52 /// Doubling for a=0 curves (secp256k1). 2M + 5S + 3 normalize. 53 /// EFD dbl-2009-l with a=0 specialization. 54 pub fn dbl(self: JacobianPoint) JacobianPoint { 55 if (self.z.isZero()) return self; 56 57 const a = self.x.sq(); // mag 1 58 const b = self.y.sq(); // mag 1 59 const c = b.sq(); // mag 1 60 const xb = self.x.add(b); // mag 2 61 const d = xb.sq().subMag(a, 1).subMag(c, 1).dbl().normalize(); 62 const e = a.dbl().add(a); // 3a = mag 3 63 const f = e.sq(); 64 const x3 = f.subMag(d.dbl(), 2).normalize(); 65 const c8 = c.dbl().dbl().dbl(); // mag 8 66 const y3 = e.mul(d.subMag(x3, 1)).subMag(c8, 8).normalize(); 67 const z3 = self.y.mul(self.z).dbl(); // mag 2 68 69 return .{ .x = x3, .y = y3, .z = z3 }; 70 } 71 72 /// Mixed addition: Jacobian + Affine → Jacobian. 7M + 4S + 4 normalize. 73 /// EFD madd-2007-bl. Falls back to dbl() when both points are equal. 74 pub fn addMixed(self: JacobianPoint, q: AffinePoint) JacobianPoint { 75 if (self.z.isZero()) return fromAffine(q); 76 77 const z1z1 = self.z.sq(); // mag 1 78 const qx_z2 = q.x.mul(z1z1); // mag 1 79 const s2 = q.y.mul(self.z.mul(z1z1)); // mag 1 80 const h = qx_z2.subMag(self.x, 1).normalize(); 81 82 // Degenerate case: same x-coordinate 83 if (h.isZero()) { 84 if (s2.subMag(self.y, 1).normalize().isZero()) return self.dbl(); 85 return identity; 86 } 87 88 const hh = h.sq(); 89 const i = hh.dbl().dbl(); // mag 4 90 const j = h.mul(i); 91 const r = s2.subMag(self.y, 1).dbl().normalize(); 92 const v = self.x.mul(i); 93 const x3 = r.sq().subMag(j, 1).subMag(v.dbl(), 2).normalize(); 94 const y3 = r.mul(v.subMag(x3, 1)).subMag(self.y.mul(j).dbl(), 2).normalize(); 95 const z3 = self.z.add(h).sq().subMag(z1z1, 1).subMag(hh, 1); 96 97 return .{ .x = x3, .y = y3, .z = z3 }; 98 } 99 100 /// Mixed subtraction: Jacobian - Affine → Jacobian. 101 pub fn subMixed(self: JacobianPoint, q: AffinePoint) JacobianPoint { 102 return self.addMixed(q.neg()); 103 } 104 105 /// Negate the Y coordinate: returns (X, -Y, Z). 106 pub fn negY(self: JacobianPoint) JacobianPoint { 107 return .{ .x = self.x, .y = self.y.normalize().neg(1), .z = self.z }; 108 } 109 110 /// Full Jacobian addition: J + J → J. ~12M + 4S. 111 /// Needed for combining r1 + r2 at end of verify. 112 pub fn add(self: JacobianPoint, other: JacobianPoint) JacobianPoint { 113 if (self.z.isZero()) return other; 114 if (other.z.isZero()) return self; 115 116 const z1z1 = self.z.sq(); 117 const z2z2 = other.z.sq(); 118 const p1 = self.x.mul(z2z2); // X1*Z2² 119 const p2 = other.x.mul(z1z1); // X2*Z1² 120 const s1 = self.y.mul(other.z.mul(z2z2)); // Y1*Z2³ 121 const s2 = other.y.mul(self.z.mul(z1z1)); // Y2*Z1³ 122 123 // h = p2 - p1 ; both mag 1 124 const h = p2.subMag(p1, 1).normalize(); 125 const r = s2.subMag(s1, 1).normalize(); 126 127 // check for doubling case (h == 0 && r == 0) 128 if (h.isZero()) { 129 if (r.isZero()) return self.dbl(); 130 return identity; 131 } 132 133 const hh = h.sq(); // mag 1 134 const hhh = h.mul(hh); // mag 1 135 const v = p1.mul(hh); // mag 1 136 const r2 = r.sq(); // mag 1 137 // x3 = r² - hhh - 2v ; sub(1)=3, sub(2v=2, 2)=6 138 const x3 = r2.subMag(hhh, 1).subMag(v.dbl(), 2).normalize(); 139 // y3 = r*(v-x3) - s1*hhh ; v.sub(x3,1)→3, mul=1; s1.mul(hhh)=1; sub(1)=3 140 const y3 = r.mul(v.subMag(x3, 1)).subMag(s1.mul(hhh), 1).normalize(); 141 const z3 = self.z.mul(other.z).mul(h); // mag 1 142 143 return .{ .x = x3, .y = y3, .z = z3 }; 144 } 145 146 /// Compare signature r against R's x-coordinate in Jacobian space. 147 /// Checks r * Z² == X (mod p), avoiding field inversion. 148 /// Returns true if the x-coordinates match. 149 pub fn jacobianCompare(self: JacobianPoint, r_fe: Fe) bool { 150 // Jacobian: x_affine = X / Z² 151 // So check: r * Z² == X 152 const z2 = self.z.sq(); 153 const rz2 = r_fe.mul(z2); 154 if (rz2.equivalent(self.x)) return true; 155 156 // rare overflow case: x_affine could be in [n, p) 157 // check (r + n) * Z² == X 158 const n_fe26 = comptime Fe.fromInt(Secp256k1.scalar.field_order); 159 const p_minus_n: u256 = StdFe.field_order - Secp256k1.scalar.field_order; 160 const r_norm = r_fe.normalize(); 161 const r_bytes = r_norm.toBytes(.big); 162 const r_int = std.mem.readInt(u256, &r_bytes, .big); 163 if (r_int < p_minus_n) { 164 const rn_z2 = r_fe.add(n_fe26).mul(z2); 165 if (rn_z2.equivalent(self.x)) return true; 166 } 167 168 return false; 169 } 170 171 /// Convert to stdlib projective for backward compatibility. 172 /// Jacobian: x = X/Z², y = Y/Z³. Projective: x = X'/Z', y = Y'/Z'. 173 /// Set X' = X*Z, Y' = Y, Z' = Z³. 174 pub fn toProjective(self: JacobianPoint) Secp256k1 { 175 if (self.z.isZero()) return Secp256k1.identityElement; 176 return .{ 177 .x = self.x.mul(self.z).toStdlib(), 178 .y = self.y.toStdlib(), 179 .z = self.z.sq().mul(self.z).toStdlib(), 180 }; 181 } 182}; 183 184// ============================================================ 185// tests — verify Fe Jacobian matches stdlib results 186// ============================================================ 187 188fn toStdlibAffine(j: JacobianPoint) StdAffineCoordinates { 189 return j.toProjective().affineCoordinates(); 190} 191 192test "dbl matches stdlib" { 193 const G = Secp256k1.basePoint; 194 const g_affine = G.affineCoordinates(); 195 196 const j_2g = JacobianPoint.fromStdlibAffine(g_affine).dbl(); 197 const j_2g_affine = toStdlibAffine(j_2g); 198 199 const stdlib_2g_affine = G.dbl().affineCoordinates(); 200 201 try std.testing.expect(j_2g_affine.x.equivalent(stdlib_2g_affine.x)); 202 try std.testing.expect(j_2g_affine.y.equivalent(stdlib_2g_affine.y)); 203} 204 205test "addMixed matches stdlib add" { 206 const G = Secp256k1.basePoint; 207 const g_affine = G.affineCoordinates(); 208 const g26 = AffinePoint.fromStdlib(g_affine); 209 210 // 2G + G = 3G 211 const j_3g = JacobianPoint.fromAffine(g26).dbl().addMixed(g26); 212 const j_3g_affine = toStdlibAffine(j_3g); 213 214 const stdlib_3g_affine = G.dbl().add(G).affineCoordinates(); 215 216 try std.testing.expect(j_3g_affine.x.equivalent(stdlib_3g_affine.x)); 217 try std.testing.expect(j_3g_affine.y.equivalent(stdlib_3g_affine.y)); 218} 219 220test "identity + affine = affine" { 221 const G = Secp256k1.basePoint; 222 const g_affine = G.affineCoordinates(); 223 const g26 = AffinePoint.fromStdlib(g_affine); 224 225 const result = JacobianPoint.identity.addMixed(g26); 226 const result_affine = toStdlibAffine(result); 227 228 try std.testing.expect(result_affine.x.equivalent(g_affine.x)); 229 try std.testing.expect(result_affine.y.equivalent(g_affine.y)); 230} 231 232test "subMixed is inverse of addMixed" { 233 const G = Secp256k1.basePoint; 234 const g_affine = G.affineCoordinates(); 235 const g26 = AffinePoint.fromStdlib(g_affine); 236 237 // 3G - G = 2G 238 const j_3g = JacobianPoint.fromAffine(g26).dbl().addMixed(g26); 239 const j_2g = j_3g.subMixed(g26); 240 const j_2g_affine = toStdlibAffine(j_2g); 241 242 const stdlib_2g_affine = G.dbl().affineCoordinates(); 243 244 try std.testing.expect(j_2g_affine.x.equivalent(stdlib_2g_affine.x)); 245 try std.testing.expect(j_2g_affine.y.equivalent(stdlib_2g_affine.y)); 246} 247 248test "repeated doubling" { 249 const G = Secp256k1.basePoint; 250 const g_affine = G.affineCoordinates(); 251 252 // 16G via 4 doublings 253 const j = JacobianPoint.fromStdlibAffine(g_affine).dbl().dbl().dbl().dbl(); 254 const j_affine = toStdlibAffine(j); 255 256 const stdlib_affine = G.dbl().dbl().dbl().dbl().affineCoordinates(); 257 258 try std.testing.expect(j_affine.x.equivalent(stdlib_affine.x)); 259 try std.testing.expect(j_affine.y.equivalent(stdlib_affine.y)); 260} 261 262test "full Jacobian add matches stdlib" { 263 const G = Secp256k1.basePoint; 264 const g_affine = G.affineCoordinates(); 265 const g26 = AffinePoint.fromStdlib(g_affine); 266 267 // 2G + 3G = 5G 268 const j_2g = JacobianPoint.fromAffine(g26).dbl(); 269 const j_3g = j_2g.addMixed(g26); 270 const j_5g = j_2g.add(j_3g); 271 const j_5g_affine = toStdlibAffine(j_5g); 272 273 const stdlib_5g = G.dbl().add(G.dbl().add(G)); 274 const stdlib_5g_affine = stdlib_5g.affineCoordinates(); 275 276 try std.testing.expect(j_5g_affine.x.equivalent(stdlib_5g_affine.x)); 277 try std.testing.expect(j_5g_affine.y.equivalent(stdlib_5g_affine.y)); 278} 279 280test "add with identity" { 281 const G = Secp256k1.basePoint; 282 const g_affine = G.affineCoordinates(); 283 const g26 = AffinePoint.fromStdlib(g_affine); 284 285 const j_g = JacobianPoint.fromAffine(g26); 286 const result = j_g.add(JacobianPoint.identity); 287 const result_affine = toStdlibAffine(result); 288 289 try std.testing.expect(result_affine.x.equivalent(g_affine.x)); 290 try std.testing.expect(result_affine.y.equivalent(g_affine.y)); 291 292 const result2 = JacobianPoint.identity.add(j_g); 293 const result2_affine = toStdlibAffine(result2); 294 295 try std.testing.expect(result2_affine.x.equivalent(g_affine.x)); 296 try std.testing.expect(result2_affine.y.equivalent(g_affine.y)); 297}