authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-09-27 17:32:31+02:00
committergravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-10-01 12:06:49+02:00
log8874073a3fddd9409018a88dcc7f85076b8fdc19
treeb12ae6ad543dd3a3fcdf96310661fba2b5625d8c
parentb4ee9c1fee470aa8fb05610803bccc5075256571

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.

1 files changed, 62 insertions(+), 135 deletions(-)

lib/std/crypto/pcurves/secp256k1.zig+62-135
...@@ -36,14 +36,16 @@ pub const Secp256k1 = struct {...@@ -36,14 +36,16 @@ pub const Secp256k1 = struct {
36 pub const B = Fe.fromInt(7) catch unreachable;36 pub const B = Fe.fromInt(7) catch unreachable;
3737
38 pub const Endormorphism = struct {38 pub const Endormorphism = struct {
39 const lambda: u256 = 37718080363155996902926221483475020450927657555482586988616620542887997980018;39 const lambda = scalarFromInt(37718080363155996902926221483475020450927657555482586988616620542887997980018);
40 const beta: u256 = 55594575648329892869085402983802832744385952214688224221778511981742606582254;40 const beta = Fe.fromInt(55594575648329892869085402983802832744385952214688224221778511981742606582254) catch unreachable;
41 const b1_neg = scalarFromInt(303414439467246543595250775667605759171);
42 const b2_neg = scalarFromInt(scalar.field_order - 64502973549206556628585045361533709077);
4143
42 const lambda_s = s: {44 fn scalarFromInt(x: u256) scalar.Scalar {
43 var buf: [32]u8 = undefined;45 var buf: [32]u8 = undefined;
44 mem.writeInt(u256, &buf, Endormorphism.lambda, .little);46 mem.writeInt(u256, &buf, x, .little);
45 break :s buf;47 return scalar.Scalar.fromBytes(buf, .little) catch unreachable;
46 };48 }
4749
48 pub const SplitScalar = struct {50 pub const SplitScalar = struct {
49 r1: [32]u8,51 r1: [32]u8,
...@@ -51,39 +53,21 @@ pub const Secp256k1 = struct {...@@ -51,39 +53,21 @@ pub const Secp256k1 = struct {
51 };53 };
5254
53 /// Compute r1 and r2 so that k = r1 + r2*lambda (mod L).55 /// Compute r1 and r2 so that k = r1 + r2*lambda (mod L).
56 /// Both outputs use the requested byte order.
54 pub fn splitScalar(s: [32]u8, endian: std.lang.Endian) NonCanonicalError!SplitScalar {57 pub fn splitScalar(s: [32]u8, endian: std.lang.Endian) NonCanonicalError!SplitScalar {
55 const b1_neg_s = comptime s: {58 const k = try scalar.Scalar.fromBytes(s, endian);
56 var buf: [32]u8 = undefined;59 const k_int = mem.readInt(u256, &s, endian);
57 mem.writeInt(u256, &buf, 303414439467246543595250775667605759171, .little);60
58 break :s buf;61 const t1 = math.mulWide(u256, k_int, 21949224512762693861512883645436906316123769664773102907882521278123970637873);
59 };62 const t2 = math.mulWide(u256, k_int, 103246583619904461035481197785446227098457807945486720222659797044629401272177);
60 const b2_neg_s = comptime s: {
61 var buf: [32]u8 = undefined;
62 mem.writeInt(u256, &buf, scalar.field_order - 64502973549206556628585045361533709077, .little);
63 break :s buf;
64 };
65 const k = mem.readInt(u256, &s, endian);
66
67 const t1 = math.mulWide(u256, k, 21949224512762693861512883645436906316123769664773102907882521278123970637873);
68 const t2 = math.mulWide(u256, k, 103246583619904461035481197785446227098457807945486720222659797044629401272177);
6963
70 const c1 = @as(u128, @truncate(t1 >> 384)) + @as(u1, @truncate(t1 >> 383));64 const c1 = @as(u128, @truncate(t1 >> 384)) + @as(u1, @truncate(t1 >> 383));
71 const c2 = @as(u128, @truncate(t2 >> 384)) + @as(u1, @truncate(t2 >> 383));65 const c2 = @as(u128, @truncate(t2 >> 384)) + @as(u1, @truncate(t2 >> 383));
7266
73 var buf: [32]u8 = undefined;67 const r2 = scalarFromInt(c1).mul(b1_neg).add(scalarFromInt(c2).mul(b2_neg));
7468 const r1 = k.sub(r2.mul(lambda));
75 mem.writeInt(u256, &buf, c1, .little);
76 const c1x = try scalar.mul(buf, b1_neg_s, .little);
77
78 mem.writeInt(u256, &buf, c2, .little);
79 const c2x = try scalar.mul(buf, b2_neg_s, .little);
8069
81 const r2 = try scalar.add(c1x, c2x, .little);70 return .{ .r1 = r1.toBytes(endian), .r2 = r2.toBytes(endian) };
82
83 var r1 = try scalar.mul(r2, lambda_s, .little);
84 r1 = try scalar.sub(s, r1, .little);
85
86 return SplitScalar{ .r1 = r1, .r2 = r2 };
87 }71 }
88 };72 };
8973
...@@ -367,17 +351,48 @@ pub const Secp256k1 = struct {...@@ -367,17 +351,48 @@ pub const Secp256k1 = struct {
367 return e;351 return e;
368 }352 }
369353
370 fn pcMul(pc: *const [9]Secp256k1, s: [32]u8, comptime vartime: bool) IdentityElementError!Secp256k1 {354 // Compute the sum of s[i]*p[i] *IN VARIABLE TIME*. Scalars can be larger than the group order.
371 std.debug.assert(vartime);355 fn mulMultiPublic(comptime n: usize, p: [n]Secp256k1, s: [n][32]u8, endian: std.lang.Endian) IdentityElementError!Secp256k1 {
372 const e = slide(s);356 var pcs: [2 * n][9]Secp256k1 = undefined;
357 var es: [2 * n][2 * 32 + 1]i8 = undefined;
358 for (p, s, 0..) |point, k, i| {
359 pcs[2 * i] = if (point.is_base) basePointPc[0..9].* else precompute(point, 8);
360 // lambda*P
361 for (&pcs[2 * i + 1], pcs[2 * i]) |*pc_lambda, pc| {
362 pc_lambda.* = .{ .x = pc.x.mul(Endormorphism.beta), .y = pc.y, .z = pc.z };
363 }
364 var s48: [48]u8 = undefined;
365 mem.writeInt(u384, &s48, mem.readInt(u256, &k, endian), .little);
366 const split = Endormorphism.splitScalar(scalar.reduce48(s48, .little), .little) catch unreachable;
367
368 for ([_][32]u8{ split.r1, split.r2 }, es[2 * i ..][0..2]) |r, *e| {
369 const x = mem.readInt(u256, &r, .little);
370 if (x <= scalar.field_order / 2) {
371 e.* = slide(r);
372 } else {
373 var r_neg: [32]u8 = undefined;
374 mem.writeInt(u256, &r_neg, scalar.field_order - x, .little);
375 e.* = slide(r_neg);
376 for (e) |*d| d.* = -d.*;
377 }
378 }
379 }
380
381 var pos: usize = 2 * 32;
382 top: while (pos > 0) : (pos -= 1) {
383 for (&es) |*e| {
384 if (e[pos] != 0) break :top;
385 }
386 }
373 var q = Secp256k1.identityElement;387 var q = Secp256k1.identityElement;
374 var pos = e.len - 1;
375 while (true) : (pos -= 1) {388 while (true) : (pos -= 1) {
376 const slot = e[pos];389 for (&pcs, &es) |*pc, *e| {
377 if (slot > 0) {390 const slot = e[pos];
378 q = q.add(pc[@as(usize, @intCast(slot))]);391 if (slot > 0) {
379 } else if (slot < 0) {392 q = q.add(pc[@as(usize, @intCast(slot))]);
380 q = q.sub(pc[@as(usize, @intCast(-slot))]);393 } else if (slot < 0) {
394 q = q.sub(pc[@as(usize, @intCast(-slot))]);
395 }
381 }396 }
382 if (pos == 0) break;397 if (pos == 0) break;
383 q = q.dbl().dbl().dbl().dbl();398 q = q.dbl().dbl().dbl().dbl();
...@@ -435,105 +450,17 @@ pub const Secp256k1 = struct {...@@ -435,105 +450,17 @@ pub const Secp256k1 = struct {
435450
436 /// Multiply an elliptic curve point by a *PUBLIC* scalar *IN VARIABLE TIME*451 /// Multiply an elliptic curve point by a *PUBLIC* scalar *IN VARIABLE TIME*
437 /// This can be used for signature verification.452 /// This can be used for signature verification.
438 pub fn mulPublic(p: Secp256k1, s_: [32]u8, endian: std.lang.Endian) (IdentityElementError || NonCanonicalError)!Secp256k1 {453 pub fn mulPublic(p: Secp256k1, s: [32]u8, endian: std.lang.Endian) (IdentityElementError || NonCanonicalError)!Secp256k1 {
439 const s = if (endian == .little) s_ else Fe.orderSwap(s_);454 try scalar.rejectNonCanonical(s, endian);
440 const zero = comptime scalar.Scalar.zero.toBytes(.little);455 return mulMultiPublic(1, .{p}, .{s}, endian);
441 if (mem.eql(u8, &zero, &s)) {
442 return error.IdentityElement;
443 }
444 const pc = precompute(p, 8);
445 var lambda_p = try pcMul(&pc, Endormorphism.lambda_s, true);
446 var split_scalar = try Endormorphism.splitScalar(s, .little);
447 var px = p;
448
449 // If a key is negative, flip the sign to keep it half-sized,
450 // and flip the sign of the Y point coordinate to compensate.
451 if (split_scalar.r1[split_scalar.r1.len / 2] != 0) {
452 split_scalar.r1 = scalar.neg(split_scalar.r1, .little) catch zero;
453 px = px.neg();
454 }
455 if (split_scalar.r2[split_scalar.r2.len / 2] != 0) {
456 split_scalar.r2 = scalar.neg(split_scalar.r2, .little) catch zero;
457 lambda_p = lambda_p.neg();
458 }
459 return mulDoubleBasePublicEndo(px, split_scalar.r1, lambda_p, split_scalar.r2);
460 }
461
462 // Half-size double-base public multiplication when using the curve endomorphism.
463 // Scalars must be in little-endian.
464 // The second point is unlikely to be the generator, so don't even try to use the comptime table for it.
465 fn mulDoubleBasePublicEndo(p1: Secp256k1, s1: [32]u8, p2: Secp256k1, s2: [32]u8) IdentityElementError!Secp256k1 {
466 var pc1_array: [9]Secp256k1 = undefined;
467 const pc1 = if (p1.is_base) basePointPc[0..9] else pc: {
468 pc1_array = precompute(p1, 8);
469 break :pc &pc1_array;
470 };
471 const pc2 = precompute(p2, 8);
472 std.debug.assert(s1[s1.len / 2] == 0);
473 std.debug.assert(s2[s2.len / 2] == 0);
474 const e1 = slide(s1);
475 const e2 = slide(s2);
476 var q = Secp256k1.identityElement;
477 var pos: usize = 2 * 32 / 2; // second half is all zero
478 while (true) : (pos -= 1) {
479 const slot1 = e1[pos];
480 if (slot1 > 0) {
481 q = q.add(pc1[@as(usize, @intCast(slot1))]);
482 } else if (slot1 < 0) {
483 q = q.sub(pc1[@as(usize, @intCast(-slot1))]);
484 }
485 const slot2 = e2[pos];
486 if (slot2 > 0) {
487 q = q.add(pc2[@as(usize, @intCast(slot2))]);
488 } else if (slot2 < 0) {
489 q = q.sub(pc2[@as(usize, @intCast(-slot2))]);
490 }
491 if (pos == 0) break;
492 q = q.dbl().dbl().dbl().dbl();
493 }
494 try q.rejectIdentity();
495 return q;
496 }456 }
497457
498 /// Double-base multiplication of public parameters - Compute (p1*s1)+(p2*s2) *IN VARIABLE TIME*458 /// Double-base multiplication of public parameters - Compute (p1*s1)+(p2*s2) *IN VARIABLE TIME*
499 /// This can be used for signature verification.459 /// This can be used for signature verification.
500 pub fn mulDoubleBasePublic(p1: Secp256k1, s1_: [32]u8, p2: Secp256k1, s2_: [32]u8, endian: std.lang.Endian) IdentityElementError!Secp256k1 {460 pub fn mulDoubleBasePublic(p1: Secp256k1, s1: [32]u8, p2: Secp256k1, s2: [32]u8, endian: std.lang.Endian) IdentityElementError!Secp256k1 {
501 const s1 = if (endian == .little) s1_ else Fe.orderSwap(s1_);
502 const s2 = if (endian == .little) s2_ else Fe.orderSwap(s2_);
503 try p1.rejectIdentity();461 try p1.rejectIdentity();
504 var pc1_array: [9]Secp256k1 = undefined;
505 const pc1 = if (p1.is_base) basePointPc[0..9] else pc: {
506 pc1_array = precompute(p1, 8);
507 break :pc &pc1_array;
508 };
509 try p2.rejectIdentity();462 try p2.rejectIdentity();
510 var pc2_array: [9]Secp256k1 = undefined;463 return mulMultiPublic(2, .{ p1, p2 }, .{ s1, s2 }, endian);
511 const pc2 = if (p2.is_base) basePointPc[0..9] else pc: {
512 pc2_array = precompute(p2, 8);
513 break :pc &pc2_array;
514 };
515 const e1 = slide(s1);
516 const e2 = slide(s2);
517 var q = Secp256k1.identityElement;
518 var pos: usize = 2 * 32;
519 while (true) : (pos -= 1) {
520 const slot1 = e1[pos];
521 if (slot1 > 0) {
522 q = q.add(pc1[@as(usize, @intCast(slot1))]);
523 } else if (slot1 < 0) {
524 q = q.sub(pc1[@as(usize, @intCast(-slot1))]);
525 }
526 const slot2 = e2[pos];
527 if (slot2 > 0) {
528 q = q.add(pc2[@as(usize, @intCast(slot2))]);
529 } else if (slot2 < 0) {
530 q = q.sub(pc2[@as(usize, @intCast(-slot2))]);
531 }
532 if (pos == 0) break;
533 q = q.dbl().dbl().dbl().dbl();
534 }
535 try q.rejectIdentity();
536 return q;
537 }464 }
538};465};
539466