From 8874073a3fddd9409018a88dcc7f85076b8fdc19 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Sun, 27 Sep 2026 17:32:31 +0200 Subject: [PATCH 1/5] crypto.secp256k1: use the endomorphism in public multiplications mulPublic() computed lambda*P with a full scalar multiplication before the half-size double-base loop. But lambda(x,y) is just (beta*x, y), so the lambda table can be derived from the regular table with one multiplication / entry. And make mulDoubleBasePublic() use the endomorphism too. After this, mulPublic() is about 2.5x faster, mulDoubleBasePublic() is about 1.5x faster, and ECDSA verification over secp256k1 obviously gets about 2x faster as well. --- lib/std/crypto/pcurves/secp256k1.zig | 195 +++++++++------------------ 1 file changed, 61 insertions(+), 134 deletions(-) diff --git a/lib/std/crypto/pcurves/secp256k1.zig b/lib/std/crypto/pcurves/secp256k1.zig index 3c36b9b2ed49d45dc1a13c0d9aab2a69d6dd3398..b9644673072d6ebdee4f0a69b1b3f28c277f7231 100644 --- a/lib/std/crypto/pcurves/secp256k1.zig +++ b/lib/std/crypto/pcurves/secp256k1.zig @@ -36,14 +36,16 @@ pub const Secp256k1 = struct { pub const B = Fe.fromInt(7) catch unreachable; pub const Endormorphism = struct { - const lambda: u256 = 37718080363155996902926221483475020450927657555482586988616620542887997980018; - const beta: u256 = 55594575648329892869085402983802832744385952214688224221778511981742606582254; + const lambda = scalarFromInt(37718080363155996902926221483475020450927657555482586988616620542887997980018); + const beta = Fe.fromInt(55594575648329892869085402983802832744385952214688224221778511981742606582254) catch unreachable; + const b1_neg = scalarFromInt(303414439467246543595250775667605759171); + const b2_neg = scalarFromInt(scalar.field_order - 64502973549206556628585045361533709077); - const lambda_s = s: { + fn scalarFromInt(x: u256) scalar.Scalar { var buf: [32]u8 = undefined; - mem.writeInt(u256, &buf, Endormorphism.lambda, .little); - break :s buf; - }; + mem.writeInt(u256, &buf, x, .little); + return scalar.Scalar.fromBytes(buf, .little) catch unreachable; + } pub const SplitScalar = struct { r1: [32]u8, @@ -51,39 +53,21 @@ pub const Secp256k1 = struct { }; /// Compute r1 and r2 so that k = r1 + r2*lambda (mod L). + /// Both outputs use the requested byte order. pub fn splitScalar(s: [32]u8, endian: std.lang.Endian) NonCanonicalError!SplitScalar { - const b1_neg_s = comptime s: { - var buf: [32]u8 = undefined; - mem.writeInt(u256, &buf, 303414439467246543595250775667605759171, .little); - break :s buf; - }; - const b2_neg_s = comptime s: { - var buf: [32]u8 = undefined; - mem.writeInt(u256, &buf, scalar.field_order - 64502973549206556628585045361533709077, .little); - break :s buf; - }; - const k = mem.readInt(u256, &s, endian); + const k = try scalar.Scalar.fromBytes(s, endian); + const k_int = mem.readInt(u256, &s, endian); - const t1 = math.mulWide(u256, k, 21949224512762693861512883645436906316123769664773102907882521278123970637873); - const t2 = math.mulWide(u256, k, 103246583619904461035481197785446227098457807945486720222659797044629401272177); + const t1 = math.mulWide(u256, k_int, 21949224512762693861512883645436906316123769664773102907882521278123970637873); + const t2 = math.mulWide(u256, k_int, 103246583619904461035481197785446227098457807945486720222659797044629401272177); const c1 = @as(u128, @truncate(t1 >> 384)) + @as(u1, @truncate(t1 >> 383)); const c2 = @as(u128, @truncate(t2 >> 384)) + @as(u1, @truncate(t2 >> 383)); - var buf: [32]u8 = undefined; + const r2 = scalarFromInt(c1).mul(b1_neg).add(scalarFromInt(c2).mul(b2_neg)); + const r1 = k.sub(r2.mul(lambda)); - mem.writeInt(u256, &buf, c1, .little); - const c1x = try scalar.mul(buf, b1_neg_s, .little); - - mem.writeInt(u256, &buf, c2, .little); - const c2x = try scalar.mul(buf, b2_neg_s, .little); - - const r2 = try scalar.add(c1x, c2x, .little); - - var r1 = try scalar.mul(r2, lambda_s, .little); - r1 = try scalar.sub(s, r1, .little); - - return SplitScalar{ .r1 = r1, .r2 = r2 }; + return .{ .r1 = r1.toBytes(endian), .r2 = r2.toBytes(endian) }; } }; @@ -367,17 +351,48 @@ pub const Secp256k1 = struct { return e; } - fn pcMul(pc: *const [9]Secp256k1, s: [32]u8, comptime vartime: bool) IdentityElementError!Secp256k1 { - std.debug.assert(vartime); - const e = slide(s); + // Compute the sum of s[i]*p[i] *IN VARIABLE TIME*. Scalars can be larger than the group order. + fn mulMultiPublic(comptime n: usize, p: [n]Secp256k1, s: [n][32]u8, endian: std.lang.Endian) IdentityElementError!Secp256k1 { + var pcs: [2 * n][9]Secp256k1 = undefined; + var es: [2 * n][2 * 32 + 1]i8 = undefined; + for (p, s, 0..) |point, k, i| { + pcs[2 * i] = if (point.is_base) basePointPc[0..9].* else precompute(point, 8); + // lambda*P + for (&pcs[2 * i + 1], pcs[2 * i]) |*pc_lambda, pc| { + pc_lambda.* = .{ .x = pc.x.mul(Endormorphism.beta), .y = pc.y, .z = pc.z }; + } + var s48: [48]u8 = undefined; + mem.writeInt(u384, &s48, mem.readInt(u256, &k, endian), .little); + const split = Endormorphism.splitScalar(scalar.reduce48(s48, .little), .little) catch unreachable; + + for ([_][32]u8{ split.r1, split.r2 }, es[2 * i ..][0..2]) |r, *e| { + const x = mem.readInt(u256, &r, .little); + if (x <= scalar.field_order / 2) { + e.* = slide(r); + } else { + var r_neg: [32]u8 = undefined; + mem.writeInt(u256, &r_neg, scalar.field_order - x, .little); + e.* = slide(r_neg); + for (e) |*d| d.* = -d.*; + } + } + } + + var pos: usize = 2 * 32; + top: while (pos > 0) : (pos -= 1) { + for (&es) |*e| { + if (e[pos] != 0) break :top; + } + } var q = Secp256k1.identityElement; - var pos = e.len - 1; while (true) : (pos -= 1) { - const slot = e[pos]; - if (slot > 0) { - q = q.add(pc[@as(usize, @intCast(slot))]); - } else if (slot < 0) { - q = q.sub(pc[@as(usize, @intCast(-slot))]); + for (&pcs, &es) |*pc, *e| { + const slot = e[pos]; + if (slot > 0) { + q = q.add(pc[@as(usize, @intCast(slot))]); + } else if (slot < 0) { + q = q.sub(pc[@as(usize, @intCast(-slot))]); + } } if (pos == 0) break; q = q.dbl().dbl().dbl().dbl(); @@ -435,105 +450,17 @@ pub const Secp256k1 = struct { /// Multiply an elliptic curve point by a *PUBLIC* scalar *IN VARIABLE TIME* /// This can be used for signature verification. - pub fn mulPublic(p: Secp256k1, s_: [32]u8, endian: std.lang.Endian) (IdentityElementError || NonCanonicalError)!Secp256k1 { - const s = if (endian == .little) s_ else Fe.orderSwap(s_); - const zero = comptime scalar.Scalar.zero.toBytes(.little); - if (mem.eql(u8, &zero, &s)) { - return error.IdentityElement; - } - const pc = precompute(p, 8); - var lambda_p = try pcMul(&pc, Endormorphism.lambda_s, true); - var split_scalar = try Endormorphism.splitScalar(s, .little); - var px = p; - - // If a key is negative, flip the sign to keep it half-sized, - // and flip the sign of the Y point coordinate to compensate. - if (split_scalar.r1[split_scalar.r1.len / 2] != 0) { - split_scalar.r1 = scalar.neg(split_scalar.r1, .little) catch zero; - px = px.neg(); - } - if (split_scalar.r2[split_scalar.r2.len / 2] != 0) { - split_scalar.r2 = scalar.neg(split_scalar.r2, .little) catch zero; - lambda_p = lambda_p.neg(); - } - return mulDoubleBasePublicEndo(px, split_scalar.r1, lambda_p, split_scalar.r2); - } - - // Half-size double-base public multiplication when using the curve endomorphism. - // Scalars must be in little-endian. - // The second point is unlikely to be the generator, so don't even try to use the comptime table for it. - fn mulDoubleBasePublicEndo(p1: Secp256k1, s1: [32]u8, p2: Secp256k1, s2: [32]u8) IdentityElementError!Secp256k1 { - var pc1_array: [9]Secp256k1 = undefined; - const pc1 = if (p1.is_base) basePointPc[0..9] else pc: { - pc1_array = precompute(p1, 8); - break :pc &pc1_array; - }; - const pc2 = precompute(p2, 8); - std.debug.assert(s1[s1.len / 2] == 0); - std.debug.assert(s2[s2.len / 2] == 0); - const e1 = slide(s1); - const e2 = slide(s2); - var q = Secp256k1.identityElement; - var pos: usize = 2 * 32 / 2; // second half is all zero - while (true) : (pos -= 1) { - const slot1 = e1[pos]; - if (slot1 > 0) { - q = q.add(pc1[@as(usize, @intCast(slot1))]); - } else if (slot1 < 0) { - q = q.sub(pc1[@as(usize, @intCast(-slot1))]); - } - const slot2 = e2[pos]; - if (slot2 > 0) { - q = q.add(pc2[@as(usize, @intCast(slot2))]); - } else if (slot2 < 0) { - q = q.sub(pc2[@as(usize, @intCast(-slot2))]); - } - if (pos == 0) break; - q = q.dbl().dbl().dbl().dbl(); - } - try q.rejectIdentity(); - return q; + pub fn mulPublic(p: Secp256k1, s: [32]u8, endian: std.lang.Endian) (IdentityElementError || NonCanonicalError)!Secp256k1 { + try scalar.rejectNonCanonical(s, endian); + return mulMultiPublic(1, .{p}, .{s}, endian); } /// Double-base multiplication of public parameters - Compute (p1*s1)+(p2*s2) *IN VARIABLE TIME* /// This can be used for signature verification. - pub fn mulDoubleBasePublic(p1: Secp256k1, s1_: [32]u8, p2: Secp256k1, s2_: [32]u8, endian: std.lang.Endian) IdentityElementError!Secp256k1 { - const s1 = if (endian == .little) s1_ else Fe.orderSwap(s1_); - const s2 = if (endian == .little) s2_ else Fe.orderSwap(s2_); + pub fn mulDoubleBasePublic(p1: Secp256k1, s1: [32]u8, p2: Secp256k1, s2: [32]u8, endian: std.lang.Endian) IdentityElementError!Secp256k1 { try p1.rejectIdentity(); - var pc1_array: [9]Secp256k1 = undefined; - const pc1 = if (p1.is_base) basePointPc[0..9] else pc: { - pc1_array = precompute(p1, 8); - break :pc &pc1_array; - }; try p2.rejectIdentity(); - var pc2_array: [9]Secp256k1 = undefined; - const pc2 = if (p2.is_base) basePointPc[0..9] else pc: { - pc2_array = precompute(p2, 8); - break :pc &pc2_array; - }; - const e1 = slide(s1); - const e2 = slide(s2); - var q = Secp256k1.identityElement; - var pos: usize = 2 * 32; - while (true) : (pos -= 1) { - const slot1 = e1[pos]; - if (slot1 > 0) { - q = q.add(pc1[@as(usize, @intCast(slot1))]); - } else if (slot1 < 0) { - q = q.sub(pc1[@as(usize, @intCast(-slot1))]); - } - const slot2 = e2[pos]; - if (slot2 > 0) { - q = q.add(pc2[@as(usize, @intCast(slot2))]); - } else if (slot2 < 0) { - q = q.sub(pc2[@as(usize, @intCast(-slot2))]); - } - if (pos == 0) break; - q = q.dbl().dbl().dbl().dbl(); - } - try q.rejectIdentity(); - return q; + return mulMultiPublic(2, .{ p1, p2 }, .{ s1, s2 }, endian); } }; -- 2.54.0 From b30ccf20d34a94627aaa458c94ca191d5119b473 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Sun, 27 Sep 2026 17:38:01 +0200 Subject: [PATCH 2/5] Add tests for public scalar multiplication and scalar split --- lib/std/crypto/pcurves/tests/secp256k1.zig | 39 ++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/lib/std/crypto/pcurves/tests/secp256k1.zig b/lib/std/crypto/pcurves/tests/secp256k1.zig index 94fb65ccec59ab493d6f03be44bc363f9302a2f4..05ddb4d950035c61c08a5419e718b7f7694de3a4 100644 --- a/lib/std/crypto/pcurves/tests/secp256k1.zig +++ b/lib/std/crypto/pcurves/tests/secp256k1.zig @@ -4,6 +4,8 @@ const testing = std.testing; const Secp256k1 = @import("../secp256k1.zig").Secp256k1; +const lambda: u256 = 0x5363ad4cc05c30e0a5261c028812645a122e22ea20816678df02967c1b23bd72; + test "secp256k1 ECDH key exchange" { const io = testing.io; const dha = Secp256k1.scalar.random(io, .little); @@ -138,6 +140,43 @@ test "secp256k1 double base multiplication" { try testing.expect(pr1.equivalent(pr2)); } +test "secp256k1 public multiplication" { + const io = testing.io; + const n = Secp256k1.scalar.field_order; + const p = Secp256k1.random(io); + for ([_]u256{ 1, 2, n - 1, lambda, n - lambda, (1 << 128) - 1 }) |x| { + var s: [32]u8 = undefined; + std.mem.writeInt(u256, &s, x, .big); + for ([_]Secp256k1{ p, Secp256k1.basePoint }) |q| { + try testing.expect((try q.mulPublic(s, .big)).equivalent(try q.mul(s, .big))); + } + } + + var s_n: [32]u8 = undefined; + std.mem.writeInt(u256, &s_n, n, .little); + try testing.expectError(error.NonCanonical, p.mulPublic(s_n, .little)); +} + +test "secp256k1 scalar split" { + const io = testing.io; + const Scalar = Secp256k1.scalar.Scalar; + const n = Secp256k1.scalar.field_order; + var lambda_s: [32]u8 = undefined; + std.mem.writeInt(u256, &lambda_s, lambda, .little); + const lambda_scalar = try Scalar.fromBytes(lambda_s, .little); + inline for (.{ .little, .big }) |endian| { + const k = Secp256k1.scalar.random(io, endian); + const split = try Secp256k1.Endormorphism.splitScalar(k, endian); + const r1 = try Scalar.fromBytes(split.r1, endian); + const r2 = try Scalar.fromBytes(split.r2, endian); + try testing.expect(r1.add(r2.mul(lambda_scalar)).equivalent(try Scalar.fromBytes(k, endian))); + for ([_][32]u8{ split.r1, split.r2 }) |r_s| { + const r = std.mem.readInt(u256, &r_s, endian); + try testing.expect(@min(r, n - r) < 1 << 128); + } + } +} + test "secp256k1 scalar inverse" { const expected = "08d0684a0fe8ea978b68a29e4b4ffdbd19eeb59db25301cf23ecbe568e1f9822"; var out: [32]u8 = undefined; -- 2.54.0 From 56eba31be2ecf85c11403db7ef76afa60cc4ebe6 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Sun, 27 Sep 2026 17:51:33 +0200 Subject: [PATCH 3/5] Shorter --- lib/std/crypto/pcurves/secp256k1.zig | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/lib/std/crypto/pcurves/secp256k1.zig b/lib/std/crypto/pcurves/secp256k1.zig index b9644673072d6ebdee4f0a69b1b3f28c277f7231..78a657076ba0a5a98f5b062a74799f3a1205e15d 100644 --- a/lib/std/crypto/pcurves/secp256k1.zig +++ b/lib/std/crypto/pcurves/secp256k1.zig @@ -367,23 +367,18 @@ pub const Secp256k1 = struct { for ([_][32]u8{ split.r1, split.r2 }, es[2 * i ..][0..2]) |r, *e| { const x = mem.readInt(u256, &r, .little); - if (x <= scalar.field_order / 2) { - e.* = slide(r); - } else { - var r_neg: [32]u8 = undefined; - mem.writeInt(u256, &r_neg, scalar.field_order - x, .little); - e.* = slide(r_neg); + const negate = x > scalar.field_order / 2; + var r_abs: [32]u8 = undefined; + mem.writeInt(u256, &r_abs, if (negate) scalar.field_order - x else x, .little); + e.* = slide(r_abs); + if (negate) { for (e) |*d| d.* = -d.*; } } } - var pos: usize = 2 * 32; - top: while (pos > 0) : (pos -= 1) { - for (&es) |*e| { - if (e[pos] != 0) break :top; - } - } + var pos: usize = 0; + for (&es) |*e| pos = @max(pos, mem.findLastNone(i8, e, &.{0}) orelse 0); var q = Secp256k1.identityElement; while (true) : (pos -= 1) { for (&pcs, &es) |*pc, *e| { -- 2.54.0 From 4e38cc560315a8f5813fbd976a930601fc7f463b Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Sun, 27 Sep 2026 18:38:51 +0200 Subject: [PATCH 4/5] Simplify the test --- lib/std/crypto/pcurves/tests/secp256k1.zig | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/std/crypto/pcurves/tests/secp256k1.zig b/lib/std/crypto/pcurves/tests/secp256k1.zig index 05ddb4d950035c61c08a5419e718b7f7694de3a4..2de0143a145e667a0e65d11d45ad9d1bde18e30c 100644 --- a/lib/std/crypto/pcurves/tests/secp256k1.zig +++ b/lib/std/crypto/pcurves/tests/secp256k1.zig @@ -159,17 +159,13 @@ test "secp256k1 public multiplication" { test "secp256k1 scalar split" { const io = testing.io; - const Scalar = Secp256k1.scalar.Scalar; const n = Secp256k1.scalar.field_order; - var lambda_s: [32]u8 = undefined; - std.mem.writeInt(u256, &lambda_s, lambda, .little); - const lambda_scalar = try Scalar.fromBytes(lambda_s, .little); inline for (.{ .little, .big }) |endian| { + var lambda_s: [32]u8 = undefined; + std.mem.writeInt(u256, &lambda_s, lambda, endian); const k = Secp256k1.scalar.random(io, endian); const split = try Secp256k1.Endormorphism.splitScalar(k, endian); - const r1 = try Scalar.fromBytes(split.r1, endian); - const r2 = try Scalar.fromBytes(split.r2, endian); - try testing.expect(r1.add(r2.mul(lambda_scalar)).equivalent(try Scalar.fromBytes(k, endian))); + try testing.expectEqual(k, try Secp256k1.scalar.mulAdd(split.r2, lambda_s, split.r1, endian)); for ([_][32]u8{ split.r1, split.r2 }) |r_s| { const r = std.mem.readInt(u256, &r_s, endian); try testing.expect(@min(r, n - r) < 1 << 128); -- 2.54.0 From a14e2cda6388e4b3eb2098d01208636b0a1165da Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Mon, 28 Sep 2026 08:01:16 +0200 Subject: [PATCH 5/5] Fix typo by the way: Endormorphism -> Endomorphism --- lib/std/crypto/pcurves/secp256k1.zig | 6 +++--- lib/std/crypto/pcurves/tests/secp256k1.zig | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/std/crypto/pcurves/secp256k1.zig b/lib/std/crypto/pcurves/secp256k1.zig index 78a657076ba0a5a98f5b062a74799f3a1205e15d..37bb8288688adb1d1d0a1a788e1ef15daba3f0c5 100644 --- a/lib/std/crypto/pcurves/secp256k1.zig +++ b/lib/std/crypto/pcurves/secp256k1.zig @@ -35,7 +35,7 @@ pub const Secp256k1 = struct { pub const B = Fe.fromInt(7) catch unreachable; - pub const Endormorphism = struct { + pub const Endomorphism = struct { const lambda = scalarFromInt(37718080363155996902926221483475020450927657555482586988616620542887997980018); const beta = Fe.fromInt(55594575648329892869085402983802832744385952214688224221778511981742606582254) catch unreachable; const b1_neg = scalarFromInt(303414439467246543595250775667605759171); @@ -359,11 +359,11 @@ pub const Secp256k1 = struct { pcs[2 * i] = if (point.is_base) basePointPc[0..9].* else precompute(point, 8); // lambda*P for (&pcs[2 * i + 1], pcs[2 * i]) |*pc_lambda, pc| { - pc_lambda.* = .{ .x = pc.x.mul(Endormorphism.beta), .y = pc.y, .z = pc.z }; + pc_lambda.* = .{ .x = pc.x.mul(Endomorphism.beta), .y = pc.y, .z = pc.z }; } var s48: [48]u8 = undefined; mem.writeInt(u384, &s48, mem.readInt(u256, &k, endian), .little); - const split = Endormorphism.splitScalar(scalar.reduce48(s48, .little), .little) catch unreachable; + const split = Endomorphism.splitScalar(scalar.reduce48(s48, .little), .little) catch unreachable; for ([_][32]u8{ split.r1, split.r2 }, es[2 * i ..][0..2]) |r, *e| { const x = mem.readInt(u256, &r, .little); diff --git a/lib/std/crypto/pcurves/tests/secp256k1.zig b/lib/std/crypto/pcurves/tests/secp256k1.zig index 2de0143a145e667a0e65d11d45ad9d1bde18e30c..b92085f8a81ec411c16ce2c8457309e9e8011182 100644 --- a/lib/std/crypto/pcurves/tests/secp256k1.zig +++ b/lib/std/crypto/pcurves/tests/secp256k1.zig @@ -164,7 +164,7 @@ test "secp256k1 scalar split" { var lambda_s: [32]u8 = undefined; std.mem.writeInt(u256, &lambda_s, lambda, endian); const k = Secp256k1.scalar.random(io, endian); - const split = try Secp256k1.Endormorphism.splitScalar(k, endian); + const split = try Secp256k1.Endomorphism.splitScalar(k, endian); try testing.expectEqual(k, try Secp256k1.scalar.mulAdd(split.r2, lambda_s, split.r1, endian)); for ([_][32]u8{ split.r1, split.r2 }) |r_s| { const r = std.mem.readInt(u256, &r_s, endian); -- 2.54.0