this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: precomputed base table + jacobian point arithmetic

Separate base-point and public-key multiply paths:
- u1*G via 16x256 comptime byte table (~32 mixed adds, zero doublings)
- u2*Q via 2-way Jacobian Shamir (a=0 dbl 2M+5S, mixed add 7M+4S)

Also set version to 0.0.1 for patch-level releases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

zzstoatzz 473ce55d 05a207aa

+358 -42
+1 -1
build.zig.zon
··· 1 1 .{ 2 2 .name = .k256, 3 - .version = "0.1.0", 3 + .version = "0.0.1", 4 4 .fingerprint = 0x8707c9cf9f636ac3, 5 5 .minimum_zig_version = "0.15.0", 6 6 .paths = .{
+108
src/affine.zig
··· 1 + const std = @import("std"); 2 + const Secp256k1 = std.crypto.ecc.Secp256k1; 3 + const Fe = Secp256k1.Fe; 4 + const AffineCoordinates = @TypeOf(Secp256k1.basePoint.affineCoordinates()); 5 + 6 + /// Batch-convert projective points to affine using Montgomery's trick. 7 + /// Uses 1 field inversion + 3*(n-1) field multiplications. 8 + /// Identity points (z=0) are mapped to AffineCoordinates.identityElement. 9 + pub fn batchToAffine(comptime n: usize, points: [n]Secp256k1) [n]AffineCoordinates { 10 + // Replace zero Z with one to keep products invertible 11 + var zs: [n]Fe = undefined; 12 + for (0..n) |i| { 13 + zs[i] = if (points[i].z.isZero()) Fe.one else points[i].z; 14 + } 15 + 16 + // Forward: accumulate Z products 17 + var products: [n]Fe = undefined; 18 + products[0] = zs[0]; 19 + for (1..n) |i| { 20 + products[i] = products[i - 1].mul(zs[i]); 21 + } 22 + 23 + // Invert total product 24 + var inv = products[n - 1].invert(); 25 + 26 + // Backward: recover individual Z inverses and convert to affine 27 + var result: [n]AffineCoordinates = undefined; 28 + var i: usize = n - 1; 29 + while (true) { 30 + const z_inv = if (i > 0) inv.mul(products[i - 1]) else inv; 31 + if (i > 0) inv = inv.mul(zs[i]); 32 + 33 + result[i] = if (points[i].z.isZero()) 34 + AffineCoordinates.identityElement 35 + else 36 + .{ .x = points[i].x.mul(z_inv), .y = points[i].y.mul(z_inv) }; 37 + 38 + if (i == 0) break; 39 + i -= 1; 40 + } 41 + 42 + return result; 43 + } 44 + 45 + /// Build a byte-indexed precomputed table for scalar multiplication. 46 + /// table[i][j] = j * 256^i * base, stored as affine coordinates. 47 + /// table[i][0] is the identity element (unused in lookups). 48 + pub fn buildByteTable(base: Secp256k1) [16][256]AffineCoordinates { 49 + // Phase 1: compute all 16*256 points in projective 50 + var flat: [16 * 256]Secp256k1 = undefined; 51 + 52 + var cur_base = base; 53 + for (0..16) |sub| { 54 + flat[sub * 256] = Secp256k1.identityElement; 55 + flat[sub * 256 + 1] = cur_base; 56 + for (2..256) |j| { 57 + flat[sub * 256 + j] = flat[sub * 256 + j - 1].add(cur_base); 58 + } 59 + if (sub < 15) { 60 + // next subtable base = 256 * current base (8 doublings) 61 + var next = cur_base; 62 + next = next.dbl().dbl().dbl().dbl().dbl().dbl().dbl().dbl(); 63 + cur_base = next; 64 + } 65 + } 66 + 67 + // Phase 2: batch convert to affine 68 + const affine_flat = batchToAffine(16 * 256, flat); 69 + 70 + // Reshape to [16][256] 71 + var result: [16][256]AffineCoordinates = undefined; 72 + for (0..16) |sub| { 73 + for (0..256) |j| { 74 + result[sub][j] = affine_flat[sub * 256 + j]; 75 + } 76 + } 77 + return result; 78 + } 79 + 80 + test "batchToAffine matches individual conversion" { 81 + const G = Secp256k1.basePoint; 82 + const points = [_]Secp256k1{ 83 + Secp256k1.identityElement, 84 + G, 85 + G.dbl(), 86 + G.dbl().add(G), 87 + }; 88 + 89 + const result = batchToAffine(4, points); 90 + 91 + // Identity 92 + try std.testing.expect(result[0].x.equivalent(AffineCoordinates.identityElement.x)); 93 + 94 + // G 95 + const g_affine = G.affineCoordinates(); 96 + try std.testing.expect(result[1].x.equivalent(g_affine.x)); 97 + try std.testing.expect(result[1].y.equivalent(g_affine.y)); 98 + 99 + // 2G 100 + const g2_affine = G.dbl().affineCoordinates(); 101 + try std.testing.expect(result[2].x.equivalent(g2_affine.x)); 102 + try std.testing.expect(result[2].y.equivalent(g2_affine.y)); 103 + 104 + // 3G 105 + const g3_affine = G.dbl().add(G).affineCoordinates(); 106 + try std.testing.expect(result[3].x.equivalent(g3_affine.x)); 107 + try std.testing.expect(result[3].y.equivalent(g3_affine.y)); 108 + }
+7
src/endo.zig
··· 15 15 return .{ .x = p.x.mul(beta), .y = p.y, .z = p.z }; 16 16 } 17 17 18 + /// Apply the endomorphism to an affine point: (beta * x, y). 19 + pub fn phiAffine(p: AffineCoordinates) AffineCoordinates { 20 + return .{ .x = p.x.mul(beta), .y = p.y }; 21 + } 22 + 23 + const AffineCoordinates = @TypeOf(Secp256k1.basePoint.affineCoordinates()); 24 + 18 25 /// Re-export stdlib's GLV scalar decomposition. 19 26 /// Decomposes k into (r1, r2) such that k = r1 + r2*lambda (mod n), 20 27 /// where |r1|, |r2| < sqrt(n) (approximately 128 bits each).
+145
src/jacobian.zig
··· 1 + const std = @import("std"); 2 + const Secp256k1 = std.crypto.ecc.Secp256k1; 3 + const Fe = Secp256k1.Fe; 4 + const AffineCoordinates = @TypeOf(Secp256k1.basePoint.affineCoordinates()); 5 + 6 + /// Jacobian point on secp256k1: affine (X/Z², Y/Z³). 7 + /// Uses a=0 specialized formulas for fewer field operations than 8 + /// the stdlib's complete projective formulas. 9 + pub const JacobianPoint = struct { 10 + x: Fe, 11 + y: Fe, 12 + z: Fe, 13 + 14 + pub const identity: JacobianPoint = .{ .x = Fe.zero, .y = Fe.one, .z = Fe.zero }; 15 + 16 + /// Convert affine (x, y) to Jacobian (x, y, 1). 17 + pub fn fromAffine(p: AffineCoordinates) JacobianPoint { 18 + return .{ .x = p.x, .y = p.y, .z = Fe.one }; 19 + } 20 + 21 + /// Convert Jacobian to projective (stdlib Secp256k1 point). 22 + /// Jacobian: x = X/Z², y = Y/Z³. Projective: x = X'/Z', y = Y'/Z'. 23 + /// Set X' = X*Z, Y' = Y, Z' = Z³. 24 + pub fn toProjective(self: JacobianPoint) Secp256k1 { 25 + if (self.z.isZero()) return Secp256k1.identityElement; 26 + return .{ 27 + .x = self.x.mul(self.z), 28 + .y = self.y, 29 + .z = self.z.sq().mul(self.z), 30 + }; 31 + } 32 + 33 + /// Doubling for a=0 curves (secp256k1). 2M + 5S. 34 + /// EFD dbl-2009-l with a=0 specialization. 35 + pub fn dbl(self: JacobianPoint) JacobianPoint { 36 + if (self.z.isZero()) return self; 37 + 38 + const a = self.x.sq(); // X1² 39 + const b = self.y.sq(); // Y1² 40 + const c = b.sq(); // Y1⁴ 41 + const d = self.x.add(b).sq().sub(a).sub(c).dbl(); // 4*X1*Y1² 42 + const e = a.dbl().add(a); // 3*X1² 43 + const f = e.sq(); // (3*X1²)² 44 + const x3 = f.sub(d.dbl()); // F - 2*D 45 + const c8 = c.dbl().dbl().dbl(); // 8*Y1⁴ 46 + const y3 = e.mul(d.sub(x3)).sub(c8); // E*(D-X3) - 8*C 47 + const z3 = self.y.mul(self.z).dbl(); // 2*Y1*Z1 48 + 49 + return .{ .x = x3, .y = y3, .z = z3 }; 50 + } 51 + 52 + /// Mixed addition: Jacobian + Affine → Jacobian. 7M + 4S. 53 + /// EFD madd-2007-bl. 54 + pub fn addMixed(self: JacobianPoint, q: AffineCoordinates) JacobianPoint { 55 + if (self.z.isZero()) return fromAffine(q); 56 + 57 + const z1z1 = self.z.sq(); // Z1² 58 + const qx_z2 = q.x.mul(z1z1); // x2*Z1² 59 + const s2 = q.y.mul(self.z.mul(z1z1)); // y2*Z1³ 60 + const h = qx_z2.sub(self.x); // U2 - X1 61 + const hh = h.sq(); // H² 62 + const i = hh.dbl().dbl(); // 4*H² 63 + const j = h.mul(i); // H*4*H² 64 + const r = s2.sub(self.y).dbl(); // 2*(S2 - Y1) 65 + const v = self.x.mul(i); // X1*4*H² 66 + const x3 = r.sq().sub(j).sub(v.dbl()); // r²-J-2*V 67 + const y3 = r.mul(v.sub(x3)).sub(self.y.mul(j).dbl()); // r*(V-X3)-2*Y1*J 68 + const z3 = self.z.add(h).sq().sub(z1z1).sub(hh); // (Z1+H)²-Z1²-H² 69 + 70 + return .{ .x = x3, .y = y3, .z = z3 }; 71 + } 72 + 73 + /// Mixed subtraction: Jacobian - Affine → Jacobian. 74 + pub fn subMixed(self: JacobianPoint, q: AffineCoordinates) JacobianPoint { 75 + return self.addMixed(q.neg()); 76 + } 77 + }; 78 + 79 + test "dbl matches stdlib" { 80 + const G = Secp256k1.basePoint; 81 + const g_affine = G.affineCoordinates(); 82 + 83 + const j_2g = JacobianPoint.fromAffine(g_affine).dbl(); 84 + const j_2g_affine = j_2g.toProjective().affineCoordinates(); 85 + 86 + const stdlib_2g_affine = G.dbl().affineCoordinates(); 87 + 88 + try std.testing.expect(j_2g_affine.x.equivalent(stdlib_2g_affine.x)); 89 + try std.testing.expect(j_2g_affine.y.equivalent(stdlib_2g_affine.y)); 90 + } 91 + 92 + test "addMixed matches stdlib add" { 93 + const G = Secp256k1.basePoint; 94 + const g_affine = G.affineCoordinates(); 95 + 96 + // 2G + G = 3G 97 + const j_3g = JacobianPoint.fromAffine(g_affine).dbl().addMixed(g_affine); 98 + const j_3g_affine = j_3g.toProjective().affineCoordinates(); 99 + 100 + const stdlib_3g_affine = G.dbl().add(G).affineCoordinates(); 101 + 102 + try std.testing.expect(j_3g_affine.x.equivalent(stdlib_3g_affine.x)); 103 + try std.testing.expect(j_3g_affine.y.equivalent(stdlib_3g_affine.y)); 104 + } 105 + 106 + test "identity + affine = affine" { 107 + const G = Secp256k1.basePoint; 108 + const g_affine = G.affineCoordinates(); 109 + 110 + const result = JacobianPoint.identity.addMixed(g_affine); 111 + const result_affine = result.toProjective().affineCoordinates(); 112 + 113 + try std.testing.expect(result_affine.x.equivalent(g_affine.x)); 114 + try std.testing.expect(result_affine.y.equivalent(g_affine.y)); 115 + } 116 + 117 + test "subMixed is inverse of addMixed" { 118 + const G = Secp256k1.basePoint; 119 + const g_affine = G.affineCoordinates(); 120 + 121 + // 3G - G = 2G 122 + const j_3g = JacobianPoint.fromAffine(g_affine).dbl().addMixed(g_affine); 123 + const j_2g = j_3g.subMixed(g_affine); 124 + const j_2g_affine = j_2g.toProjective().affineCoordinates(); 125 + 126 + const stdlib_2g_affine = G.dbl().affineCoordinates(); 127 + 128 + try std.testing.expect(j_2g_affine.x.equivalent(stdlib_2g_affine.x)); 129 + try std.testing.expect(j_2g_affine.y.equivalent(stdlib_2g_affine.y)); 130 + } 131 + 132 + test "repeated doubling" { 133 + const G = Secp256k1.basePoint; 134 + const g_affine = G.affineCoordinates(); 135 + 136 + // 16G via 4 doublings 137 + var j = JacobianPoint.fromAffine(g_affine); 138 + j = j.dbl().dbl().dbl().dbl(); 139 + const j_affine = j.toProjective().affineCoordinates(); 140 + 141 + const stdlib_affine = G.dbl().dbl().dbl().dbl().affineCoordinates(); 142 + 143 + try std.testing.expect(j_affine.x.equivalent(stdlib_affine.x)); 144 + try std.testing.expect(j_affine.y.equivalent(stdlib_affine.y)); 145 + }
+11
src/point.zig
··· 25 25 return result; 26 26 } 27 27 28 + const AffineCoordinates = @TypeOf(Secp256k1.basePoint.affineCoordinates()); 29 + 30 + /// Apply the endomorphism to each entry in an affine precompute table. 31 + pub fn phiTableAffine(comptime n: usize, table: [n]AffineCoordinates) [n]AffineCoordinates { 32 + var result: [n]AffineCoordinates = undefined; 33 + for (0..n) |i| { 34 + result[i] = endo.phiAffine(table[i]); 35 + } 36 + return result; 37 + } 38 + 28 39 /// Encode a scalar into a signed 4-bit windowed representation. 29 40 /// Returns 65 digits in [-8, 8], suitable for use with a 9-entry precompute table. 30 41 /// For half-sized scalars (128-bit), digits 33-64 are zero.
+4
src/root.zig
··· 10 10 pub const endo = @import("endo.zig"); 11 11 pub const point = @import("point.zig"); 12 12 pub const verify = @import("verify.zig"); 13 + pub const jacobian = @import("jacobian.zig"); 14 + pub const affine = @import("affine.zig"); 13 15 14 16 /// Drop-in replacement for std.crypto.sign.ecdsa.EcdsaSecp256k1Sha256 15 17 /// with optimized verification (~3.6x faster). ··· 124 126 _ = @import("endo.zig"); 125 127 _ = @import("point.zig"); 126 128 _ = @import("verify.zig"); 129 + _ = @import("jacobian.zig"); 130 + _ = @import("affine.zig"); 127 131 }
+82 -41
src/verify.zig
··· 2 2 const mem = std.mem; 3 3 const Secp256k1 = std.crypto.ecc.Secp256k1; 4 4 const Fe = Secp256k1.Fe; 5 + const AffineCoordinates = @TypeOf(Secp256k1.basePoint.affineCoordinates()); 5 6 const scalar = Secp256k1.scalar; 6 7 7 8 const endo = @import("endo.zig"); 8 9 const point = @import("point.zig"); 10 + const affine_mod = @import("affine.zig"); 11 + const jacobian_mod = @import("jacobian.zig"); 12 + const JacobianPoint = jacobian_mod.JacobianPoint; 9 13 10 14 pub const VerifyError = std.crypto.errors.IdentityElementError || 11 15 std.crypto.errors.NonCanonicalError || 12 16 error{SignatureVerificationFailed}; 13 17 14 - /// Precomputed tables for the base point and its endomorphism. 15 - const base_pc = pc: { 16 - @setEvalBranchQuota(50000); 17 - break :pc point.precompute(Secp256k1.basePoint, 8); 18 - }; 19 - const base_phi_pc = pc: { 20 - @setEvalBranchQuota(100000); 21 - break :pc point.phiTable(9, base_pc); 18 + /// Precomputed base point table: G_TABLE[i][j] = j * 256^i * G in affine. 19 + /// 16 subtables × 256 entries = 4096 affine points (~256KB). 20 + /// Enables u1*G computation via byte-at-a-time lookup with zero doublings. 21 + const G_TABLE: [16][256]AffineCoordinates = blk: { 22 + @setEvalBranchQuota(50_000_000); 23 + break :blk affine_mod.buildByteTable(Secp256k1.basePoint); 22 24 }; 23 25 24 26 /// Curve order as a field element, for the projective overflow check. ··· 34 36 return scalar.Scalar.fromBytes48(xs, .big); 35 37 } 36 38 37 - /// Verify an ECDSA signature using optimized 4-way multi-scalar multiplication. 39 + /// Verify an ECDSA signature using precomputed tables and Jacobian arithmetic. 38 40 /// 39 - /// Three optimizations over stdlib: 40 - /// 1. Endomorphism via 1 field multiply (not ~65 doublings) 41 - /// 2. Single 4-way Shamir loop (128 doublings, not 256) 42 - /// 3. Projective-space comparison (no field inversion) 41 + /// Optimizations over v0.1: 42 + /// 1. u1*G via precomputed byte table: ~32 mixed adds, zero doublings 43 + /// 2. u2*Q via 2-way Jacobian Shamir: cheaper dbl (2M+5S) and mixed add (7M+4S) 44 + /// 3. Endomorphism + projective comparison (carried from v0.1) 43 45 pub fn verify(sig_r: [32]u8, sig_s: [32]u8, msg_hash: [32]u8, public_key: Secp256k1) VerifyError!void { 44 46 // parse and validate r, s 45 47 const r_sc = scalar.Scalar.fromBytes(sig_r, .big) catch return error.SignatureVerificationFailed; ··· 52 54 const scalar_u1 = z.mul(s_inv).toBytes(.little); 53 55 const scalar_u2 = r_sc.mul(s_inv).toBytes(.little); 54 56 55 - // GLV split: u1 = a1 + a2*lambda, u2 = b1 + b2*lambda 57 + // GLV split: u = r1 + r2*lambda 56 58 var split_u1 = endo.splitScalar(scalar_u1, .little) catch return error.SignatureVerificationFailed; 57 59 var split_u2 = endo.splitScalar(scalar_u2, .little) catch return error.SignatureVerificationFailed; 58 60 59 - // precompute tables for P and phi(P) 60 - const pk_pc = point.precompute(public_key, 8); 61 - const pk_phi_pc = point.phiTable(9, pk_pc); 62 - 63 - // handle negative half-scalars: negate scalar, track sign flip for table lookup 61 + // handle negative half-scalars: negate and track sign 64 62 const zero_s = scalar.Scalar.zero.toBytes(.little); 65 63 66 64 var neg_g = false; ··· 85 83 neg_p_phi = true; 86 84 } 87 85 88 - // encode all 4 half-scalars as signed 4-bit digits 89 - const e1 = point.slide(split_u1.r1); 90 - const e2 = point.slide(split_u1.r2); 91 - const e3 = point.slide(split_u2.r1); 92 - const e4 = point.slide(split_u2.r2); 86 + // 1. u1*G via precomputed byte table (zero doublings) 87 + const r1 = basePointMul(split_u1.r1, neg_g, split_u1.r2, neg_g_phi); 93 88 94 - // 4-way Shamir loop over 128-bit half-scalars (positions 0..32) 95 - var q = Secp256k1.identityElement; 96 - var pos: usize = 2 * 32 / 2; // = 32; upper half is zero 97 - while (true) : (pos -= 1) { 98 - q = addSlot(q, &base_pc, e1[pos], neg_g); 99 - q = addSlot(q, &base_phi_pc, e2[pos], neg_g_phi); 100 - q = addSlot(q, &pk_pc, e3[pos], neg_p); 101 - q = addSlot(q, &pk_phi_pc, e4[pos], neg_p_phi); 102 - if (pos == 0) break; 103 - q = q.dbl().dbl().dbl().dbl(); 104 - } 89 + // 2. u2*Q via 2-way Jacobian Shamir 90 + const pk_pc = point.precompute(public_key, 8); 91 + const pk_affine = affine_mod.batchToAffine(9, pk_pc); 92 + const pk_phi_affine = point.phiTableAffine(9, pk_affine); 93 + const r2 = publicKeyMul(split_u2, neg_p, neg_p_phi, pk_affine, pk_phi_affine); 94 + 95 + // 3. combine results: convert Jacobian → projective, add via stdlib 96 + const p1 = r1.toProjective(); 97 + const p2 = r2.toProjective(); 98 + const q = p1.add(p2); 105 99 106 100 // reject identity (point at infinity has no valid x-coordinate) 107 101 q.rejectIdentity() catch return error.SignatureVerificationFailed; 108 102 109 103 // projective comparison: check x(R) mod n == r 110 - // stdlib uses projective coords: x_affine = X / Z (not X / Z^2) 111 - // so we check r * Z == X (mod p) instead of converting to affine 112 104 if (!projectiveCompare(sig_r, q)) { 113 105 return error.SignatureVerificationFailed; 114 106 } 115 107 } 116 108 117 - /// Add/subtract a table entry based on the signed digit and negation flag. 109 + /// Compute u1*G using the precomputed byte table. 110 + /// Decomposes the two 128-bit half-scalars into bytes and does direct lookups. 111 + /// Cost: ~32 mixed Jacobian-affine additions, zero doublings. 112 + fn basePointMul(a1: [32]u8, neg1: bool, a2: [32]u8, neg2: bool) JacobianPoint { 113 + var acc = JacobianPoint.identity; 114 + 115 + // a1 half: direct table lookup 116 + for (0..16) |i| { 117 + const byte = a1[i]; 118 + if (byte != 0) { 119 + acc = if (neg1) acc.subMixed(G_TABLE[i][byte]) else acc.addMixed(G_TABLE[i][byte]); 120 + } 121 + } 122 + 123 + // a2 half: table lookup + phi on the fly (1 field mul per lookup) 124 + for (0..16) |i| { 125 + const byte = a2[i]; 126 + if (byte != 0) { 127 + const p = endo.phiAffine(G_TABLE[i][byte]); 128 + acc = if (neg2) acc.subMixed(p) else acc.addMixed(p); 129 + } 130 + } 131 + 132 + return acc; 133 + } 134 + 135 + /// Compute u2*Q using 2-way Jacobian Shamir with windowed scalars. 136 + /// Cost: 128 Jacobian doublings + ~44 mixed additions. 137 + fn publicKeyMul( 138 + split: endo.SplitScalar, 139 + neg_p: bool, 140 + neg_p_phi: bool, 141 + pk_affine: [9]AffineCoordinates, 142 + pk_phi_affine: [9]AffineCoordinates, 143 + ) JacobianPoint { 144 + const e1 = point.slide(split.r1); 145 + const e2 = point.slide(split.r2); 146 + 147 + var q = JacobianPoint.identity; 148 + var pos: usize = 32; // 128-bit half-scalars → 32 nybbles + carry 149 + while (true) : (pos -= 1) { 150 + q = addSlot(q, &pk_affine, e1[pos], neg_p); 151 + q = addSlot(q, &pk_phi_affine, e2[pos], neg_p_phi); 152 + if (pos == 0) break; 153 + q = q.dbl().dbl().dbl().dbl(); 154 + } 155 + return q; 156 + } 157 + 158 + /// Add/subtract a table entry based on signed digit and negation flag. 118 159 /// Variable-time: branches on digit value (safe for public verification). 119 - inline fn addSlot(q: Secp256k1, table: *const [9]Secp256k1, slot: i8, negate: bool) Secp256k1 { 160 + inline fn addSlot(q: JacobianPoint, table: *const [9]AffineCoordinates, slot: i8, negate: bool) JacobianPoint { 120 161 var s = slot; 121 162 if (negate) s = -s; 122 163 if (s > 0) { 123 - return q.add(table[@intCast(s)]); 164 + return q.addMixed(table[@intCast(s)]); 124 165 } else if (s < 0) { 125 - return q.sub(table[@intCast(-s)]); 166 + return q.subMixed(table[@intCast(-s)]); 126 167 } 127 168 return q; 128 169 }