@pbnjam/bandersnatch 0.7.2-rc2 → 0.7.2-rc3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/docs.yml +1 -1
- package/README.md +30 -2
- package/package.json +3 -3
- package/src/__tests__/curve.test.ts +316 -1
- package/src/curve.ts +21 -19
- package/src/glv.ts +212 -0
- package/src/index.ts +3 -3
package/README.md
CHANGED
|
@@ -101,9 +101,37 @@ bun run test
|
|
|
101
101
|
bun run build
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
+
## GLV Endomorphism
|
|
105
|
+
|
|
106
|
+
`BandersnatchCurve.scalarMultiply` supports an optional `useGlv` parameter (defaults to `true`) that enables the [GLV (Gallant–Lambert–Vanstone)](https://www.iacr.org/archive/crypto2001/21390189.pdf) endomorphism optimization for scalar multiplication.
|
|
107
|
+
|
|
108
|
+
GLV decomposes a full-width scalar `k` into two half-width scalars `k1, k2` such that `[k]P = [k1]P + [k2]φ(P)`, where `φ` is the Bandersnatch curve endomorphism. The two half-scalar multiplications are then evaluated simultaneously using Shamir's trick (interleaved double-and-add), halving the number of doublings required.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// GLV enabled (default)
|
|
112
|
+
const R1 = BandersnatchCurve.scalarMultiply(P, k)
|
|
113
|
+
|
|
114
|
+
// GLV disabled — falls back to noble-curves wNAF
|
|
115
|
+
const R2 = BandersnatchCurve.scalarMultiply(P, k, false)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Performance Comparison
|
|
119
|
+
|
|
120
|
+
Benchmarked on Apple M4 Max, macOS 15.5, Bun 1.3.9. Each measurement averages 5 rounds of 10 random 253-bit scalars.
|
|
121
|
+
|
|
122
|
+
| Scenario | GLV (Shamir) | noble wNAF | GLV vs naive |
|
|
123
|
+
| --- | --- | --- | --- |
|
|
124
|
+
| **Generator point** | 0.36 ms/mul | 0.08 ms/mul | **−4.5×** (slower) |
|
|
125
|
+
| **Arbitrary points** | 0.36 ms/mul | 0.85 ms/mul | **+58%** (faster) |
|
|
126
|
+
|
|
127
|
+
**Key findings:**
|
|
128
|
+
|
|
129
|
+
- **Generator point**: noble-curves precomputes a wNAF table for the fixed generator (`BASE`), making generator multiplications extremely fast (~0.08 ms). Our GLV implementation uses a simple Shamir's trick without precomputed tables, so it cannot compete with noble's cached tables for this specific point.
|
|
130
|
+
- **Arbitrary points**: For points without precomputed tables, GLV is ~2.4× faster. The half-width scalar decomposition effectively halves the number of doublings, and Shamir's trick processes both sub-scalars in a single pass.
|
|
131
|
+
- **Recommendation**: GLV is enabled by default because most cryptographic protocols (Ring VRF proving, multi-scalar operations) perform the majority of their multiplications on arbitrary points rather than the fixed generator. The net effect across a real workload is a meaningful speedup.
|
|
132
|
+
|
|
104
133
|
## Security & correctness notes
|
|
105
134
|
|
|
106
135
|
- `BandersnatchCurve` uses BigInt arithmetic and performs modular operations over the Bandersnatch field.
|
|
107
136
|
- When handling serialized points, treat all external inputs as untrusted and rely on parsing/validation helpers (avoid manual decoding).
|
|
108
|
-
|
|
109
|
-
|
|
137
|
+
- The GLV implementation is constant-time with respect to the scalar decomposition but inherits the timing characteristics of the underlying noble-curves point arithmetic. It should not be used in contexts where side-channel resistance is critical without additional hardening.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pbnjam/bandersnatch",
|
|
3
|
-
"version": "0.7.2-
|
|
3
|
+
"version": "0.7.2-rc3",
|
|
4
4
|
"description": "Bandersnatch elliptic curve implementation",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"@noble/hashes": "2.0.1"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@types/bun": "1.3.
|
|
26
|
+
"@types/bun": "1.3.9",
|
|
27
27
|
"@types/node": "^22.0.0",
|
|
28
|
-
"bun-types": "1.3.
|
|
28
|
+
"bun-types": "1.3.9",
|
|
29
29
|
"typescript": "5.9.3",
|
|
30
30
|
"typedoc": "0.28.15"
|
|
31
31
|
},
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { BandersnatchCurve } from '../curve'
|
|
2
|
+
import { Bandersnatch, BandersnatchCurve } from '../curve'
|
|
3
|
+
import { BANDERSNATCH_PARAMS } from '../config'
|
|
4
|
+
import { endomorphism, glvMultiply, scalarDecomposition } from '../glv'
|
|
5
|
+
import { mod } from '../math'
|
|
3
6
|
|
|
4
7
|
describe('BandersnatchCurve Operations', () => {
|
|
5
8
|
test('Point addition: P + Q = Q + P (commutativity)', () => {
|
|
@@ -296,3 +299,315 @@ describe('BandersnatchCurve Operations', () => {
|
|
|
296
299
|
expect(negativeP.y).toBe(negatedP.y)
|
|
297
300
|
})
|
|
298
301
|
})
|
|
302
|
+
|
|
303
|
+
// --- GLV Endomorphism Tests ---
|
|
304
|
+
// Test vectors from PyBandersnatch sage scripts:
|
|
305
|
+
// https://github.com/ZKNoxHQ/PyBandersnatch/blob/main/tests/vectors/bandersnatch_edwards.py
|
|
306
|
+
|
|
307
|
+
const R = BANDERSNATCH_PARAMS.CURVE_ORDER
|
|
308
|
+
|
|
309
|
+
const LAMBDA = mod(
|
|
310
|
+
-0x13b4f3dc4a39a493edf849562b38c72bcfc49db970a5056ed13d21408783df05n,
|
|
311
|
+
R,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
const TEST_P = Bandersnatch.fromAffine({
|
|
315
|
+
x: 0x1cc6ee38139c1c110223537a8ce79d067e58cc1067c6fbb7d8b3a1b08dfc8f08n,
|
|
316
|
+
y: 0x70a5894a64445438d015ac32ba360f092cde44bab11fc2b7d4b5c0d216228ccen,
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
const TEST_PHI_P = Bandersnatch.fromAffine({
|
|
320
|
+
x: 0x4b79afee9988241890d27d2f27a068c9636328afdc035ba091251acbb590ad76n,
|
|
321
|
+
y: 0x15ef05ebd97664593eb98170626b11599b3a5b0d0002b19a30dd389e78392579n,
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
const TEST_K = 0x1a862619b8224e61eb24bb583c84ce04913064d37308623924c7a64fcdc9f191n
|
|
325
|
+
const TEST_K1 = -0x2286ed83a0b1545d1b7788921e40bb14n
|
|
326
|
+
const TEST_K2 = 0x6886451b4aa55294c626bb34d42e242n
|
|
327
|
+
|
|
328
|
+
const TEST_K_TIMES_P = Bandersnatch.fromAffine({
|
|
329
|
+
x: 0x5e68a7f103de3be399640801563ddcaac8fc2fa31b413df3a8ae975ace0dc465n,
|
|
330
|
+
y: 0xf2693e9239ee3709661fbf6c908de99ce7a41f149cefebe5ef6fc2c292bb4c6n,
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
describe('GLV Endomorphism', () => {
|
|
334
|
+
test('endomorphism eigenvalue: phi(G) == [lambda]G', () => {
|
|
335
|
+
const G = BandersnatchCurve.GENERATOR
|
|
336
|
+
const phiG = endomorphism(G)
|
|
337
|
+
const lambdaG = G.multiply(LAMBDA)
|
|
338
|
+
|
|
339
|
+
const phiAff = phiG.toAffine()
|
|
340
|
+
const lamAff = lambdaG.toAffine()
|
|
341
|
+
expect(phiAff.x).toBe(lamAff.x)
|
|
342
|
+
expect(phiAff.y).toBe(lamAff.y)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
test('endomorphism test vector: phi(p) matches sage output', () => {
|
|
346
|
+
const phiP = endomorphism(TEST_P)
|
|
347
|
+
const phiAff = phiP.toAffine()
|
|
348
|
+
const expectedAff = TEST_PHI_P.toAffine()
|
|
349
|
+
|
|
350
|
+
expect(phiAff.x).toBe(expectedAff.x)
|
|
351
|
+
expect(phiAff.y).toBe(expectedAff.y)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
test('endomorphism of identity returns identity', () => {
|
|
355
|
+
const phiZero = endomorphism(Bandersnatch.ZERO)
|
|
356
|
+
expect(phiZero.equals(Bandersnatch.ZERO)).toBe(true)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
test('scalar decomposition: k1 + lambda*k2 == k (mod r)', () => {
|
|
360
|
+
const [sign1, absK1, sign2, absK2] = scalarDecomposition(TEST_K)
|
|
361
|
+
|
|
362
|
+
const k1 = sign1 ? absK1 : R - absK1
|
|
363
|
+
const k2 = sign2 ? absK2 : R - absK2
|
|
364
|
+
const reconstructed = mod(k1 + LAMBDA * k2, R)
|
|
365
|
+
|
|
366
|
+
expect(reconstructed).toBe(TEST_K)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
test('scalar decomposition is stable and self-consistent', () => {
|
|
370
|
+
const [sign1, absK1, sign2, absK2] = scalarDecomposition(TEST_K)
|
|
371
|
+
|
|
372
|
+
// Verify the decomposition satisfies k = k1 + lambda*k2 (mod r)
|
|
373
|
+
const k1InField = sign1 ? absK1 : R - absK1
|
|
374
|
+
const k2InField = sign2 ? absK2 : R - absK2
|
|
375
|
+
expect(mod(k1InField + LAMBDA * k2InField, R)).toBe(TEST_K)
|
|
376
|
+
|
|
377
|
+
// Verify running a second time yields identical results
|
|
378
|
+
const [s1b, a1b, s2b, a2b] = scalarDecomposition(TEST_K)
|
|
379
|
+
expect(s1b).toBe(sign1)
|
|
380
|
+
expect(a1b).toBe(absK1)
|
|
381
|
+
expect(s2b).toBe(sign2)
|
|
382
|
+
expect(a2b).toBe(absK2)
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
test('scalar decomposition produces half-size scalars', () => {
|
|
386
|
+
const [, absK1, , absK2] = scalarDecomposition(TEST_K)
|
|
387
|
+
|
|
388
|
+
const maxBits = Math.ceil(253 / 2) + 1
|
|
389
|
+
expect(Number(absK1.toString(2).length)).toBeLessThanOrEqual(maxBits)
|
|
390
|
+
expect(Number(absK2.toString(2).length)).toBeLessThanOrEqual(maxBits)
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
test('scalar decomposition for multiple large scalars', () => {
|
|
394
|
+
const scalars = [
|
|
395
|
+
1n,
|
|
396
|
+
R - 1n,
|
|
397
|
+
R / 2n,
|
|
398
|
+
0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdefn % R,
|
|
399
|
+
0xdeadbeefcafebabe1337c0de42424242deadbeefcafebabe1337c0de42424242n % R,
|
|
400
|
+
]
|
|
401
|
+
|
|
402
|
+
for (const k of scalars) {
|
|
403
|
+
const [sign1, absK1, sign2, absK2] = scalarDecomposition(k)
|
|
404
|
+
const k1 = sign1 ? absK1 : R - absK1
|
|
405
|
+
const k2 = sign2 ? absK2 : R - absK2
|
|
406
|
+
const reconstructed = mod(k1 + LAMBDA * k2, R)
|
|
407
|
+
expect(reconstructed).toBe(k)
|
|
408
|
+
}
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
test('glvMultiply matches test vector: [k]P', () => {
|
|
412
|
+
const result = glvMultiply(TEST_P, TEST_K)
|
|
413
|
+
const resultAff = result.toAffine()
|
|
414
|
+
const expectedAff = TEST_K_TIMES_P.toAffine()
|
|
415
|
+
|
|
416
|
+
expect(resultAff.x).toBe(expectedAff.x)
|
|
417
|
+
expect(resultAff.y).toBe(expectedAff.y)
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
test('glvMultiply matches naive multiply for various scalars', () => {
|
|
421
|
+
const G = BandersnatchCurve.GENERATOR
|
|
422
|
+
const scalars = [
|
|
423
|
+
1n,
|
|
424
|
+
2n,
|
|
425
|
+
3n,
|
|
426
|
+
255n,
|
|
427
|
+
12345n,
|
|
428
|
+
0x1234567890abcdefn,
|
|
429
|
+
R - 1n,
|
|
430
|
+
R - 2n,
|
|
431
|
+
TEST_K,
|
|
432
|
+
]
|
|
433
|
+
|
|
434
|
+
for (const k of scalars) {
|
|
435
|
+
const glvResult = glvMultiply(G, k)
|
|
436
|
+
const naiveResult = G.multiply(k % R === 0n ? 1n : k % R)
|
|
437
|
+
const glvAff = glvResult.toAffine()
|
|
438
|
+
const naiveAff = naiveResult.toAffine()
|
|
439
|
+
expect(glvAff.x).toBe(naiveAff.x)
|
|
440
|
+
expect(glvAff.y).toBe(naiveAff.y)
|
|
441
|
+
}
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
test('glvMultiply edge cases', () => {
|
|
445
|
+
const G = BandersnatchCurve.GENERATOR
|
|
446
|
+
|
|
447
|
+
const zeroResult = glvMultiply(G, 0n)
|
|
448
|
+
expect(zeroResult.equals(Bandersnatch.ZERO)).toBe(true)
|
|
449
|
+
|
|
450
|
+
const oneResult = glvMultiply(G, 1n)
|
|
451
|
+
expect(oneResult.toAffine().x).toBe(G.toAffine().x)
|
|
452
|
+
|
|
453
|
+
const identityResult = glvMultiply(Bandersnatch.ZERO, 42n)
|
|
454
|
+
expect(identityResult.equals(Bandersnatch.ZERO)).toBe(true)
|
|
455
|
+
|
|
456
|
+
const orderResult = glvMultiply(G, R)
|
|
457
|
+
expect(orderResult.equals(Bandersnatch.ZERO)).toBe(true)
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
test('glvMultiply handles negative scalars', () => {
|
|
461
|
+
const G = BandersnatchCurve.GENERATOR
|
|
462
|
+
const k = 12345n
|
|
463
|
+
|
|
464
|
+
const pos = glvMultiply(G, k)
|
|
465
|
+
const neg = glvMultiply(G, -k)
|
|
466
|
+
const sum = pos.add(neg)
|
|
467
|
+
|
|
468
|
+
expect(sum.equals(Bandersnatch.ZERO)).toBe(true)
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
test('scalarMultiply uses GLV and matches naive for large scalars', () => {
|
|
472
|
+
const G = BandersnatchCurve.GENERATOR
|
|
473
|
+
const largeScalar = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdefn
|
|
474
|
+
|
|
475
|
+
const glvResult = BandersnatchCurve.scalarMultiply(G, largeScalar)
|
|
476
|
+
const reduced = largeScalar % R
|
|
477
|
+
const naiveResult = G.multiply(reduced)
|
|
478
|
+
|
|
479
|
+
const glvAff = glvResult.toAffine()
|
|
480
|
+
const naiveAff = naiveResult.toAffine()
|
|
481
|
+
expect(glvAff.x).toBe(naiveAff.x)
|
|
482
|
+
expect(glvAff.y).toBe(naiveAff.y)
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
test('scalarMultiply(useGlv=true) === scalarMultiply(useGlv=false) for various scalars', () => {
|
|
486
|
+
const G = BandersnatchCurve.GENERATOR
|
|
487
|
+
const scalars = [
|
|
488
|
+
0n,
|
|
489
|
+
1n,
|
|
490
|
+
2n,
|
|
491
|
+
-1n,
|
|
492
|
+
-5n,
|
|
493
|
+
255n,
|
|
494
|
+
12345n,
|
|
495
|
+
R - 1n,
|
|
496
|
+
R,
|
|
497
|
+
R + 1n,
|
|
498
|
+
0x1234567890abcdefn,
|
|
499
|
+
TEST_K,
|
|
500
|
+
0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdefn,
|
|
501
|
+
]
|
|
502
|
+
|
|
503
|
+
for (const k of scalars) {
|
|
504
|
+
const withGlv = BandersnatchCurve.scalarMultiply(G, k, true)
|
|
505
|
+
const withoutGlv = BandersnatchCurve.scalarMultiply(G, k, false)
|
|
506
|
+
|
|
507
|
+
if (withGlv.equals(Bandersnatch.ZERO)) {
|
|
508
|
+
expect(withoutGlv.equals(Bandersnatch.ZERO)).toBe(true)
|
|
509
|
+
} else {
|
|
510
|
+
const glvAff = withGlv.toAffine()
|
|
511
|
+
const naiveAff = withoutGlv.toAffine()
|
|
512
|
+
expect(glvAff.x).toBe(naiveAff.x)
|
|
513
|
+
expect(glvAff.y).toBe(naiveAff.y)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
test('scalarMultiply(useGlv=true) === scalarMultiply(useGlv=false) for non-generator point', () => {
|
|
519
|
+
const P = BandersnatchCurve.scalarMultiply(BandersnatchCurve.GENERATOR, 9999n, false)
|
|
520
|
+
const scalars = [0n, 1n, -3n, 42n, R - 1n, TEST_K]
|
|
521
|
+
|
|
522
|
+
for (const k of scalars) {
|
|
523
|
+
const withGlv = BandersnatchCurve.scalarMultiply(P, k, true)
|
|
524
|
+
const withoutGlv = BandersnatchCurve.scalarMultiply(P, k, false)
|
|
525
|
+
|
|
526
|
+
if (withGlv.equals(Bandersnatch.ZERO)) {
|
|
527
|
+
expect(withoutGlv.equals(Bandersnatch.ZERO)).toBe(true)
|
|
528
|
+
} else {
|
|
529
|
+
const glvAff = withGlv.toAffine()
|
|
530
|
+
const naiveAff = withoutGlv.toAffine()
|
|
531
|
+
expect(glvAff.x).toBe(naiveAff.x)
|
|
532
|
+
expect(glvAff.y).toBe(naiveAff.y)
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
test('GLV vs naive performance benchmark', () => {
|
|
538
|
+
const G = BandersnatchCurve.GENERATOR
|
|
539
|
+
const scalars = [
|
|
540
|
+
0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdefn % R,
|
|
541
|
+
0xdeadbeefcafebabe1337c0de42424242deadbeefcafebabe1337c0de42424242n % R,
|
|
542
|
+
TEST_K,
|
|
543
|
+
R - 1n,
|
|
544
|
+
0xfedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210n % R,
|
|
545
|
+
0xabcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789n % R,
|
|
546
|
+
0x0102030405060708091011121314151617181920212223242526272829303132n % R,
|
|
547
|
+
0x7777777777777777777777777777777777777777777777777777777777777777n % R,
|
|
548
|
+
0x0aaaaaaabbbbbbbbccccccccddddddddeeeeeeeeffffffffaaaaaaaabbbbbbbn % R,
|
|
549
|
+
0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefn % R,
|
|
550
|
+
]
|
|
551
|
+
|
|
552
|
+
// Non-generator points (noble may have precomputed tables for BASE)
|
|
553
|
+
const points = scalars.map((k) =>
|
|
554
|
+
G.multiply(k === 0n ? 1n : k),
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
const rounds = 5
|
|
558
|
+
const iterations = scalars.length
|
|
559
|
+
|
|
560
|
+
// Warmup
|
|
561
|
+
for (let i = 0; i < iterations; i++) {
|
|
562
|
+
BandersnatchCurve.scalarMultiply(points[i], scalars[(i + 1) % iterations], true)
|
|
563
|
+
BandersnatchCurve.scalarMultiply(points[i], scalars[(i + 1) % iterations], false)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// --- Generator point benchmark ---
|
|
567
|
+
let glvGenTotal = 0
|
|
568
|
+
let naiveGenTotal = 0
|
|
569
|
+
for (let r = 0; r < rounds; r++) {
|
|
570
|
+
const t0 = performance.now()
|
|
571
|
+
for (const k of scalars) BandersnatchCurve.scalarMultiply(G, k, true)
|
|
572
|
+
glvGenTotal += performance.now() - t0
|
|
573
|
+
|
|
574
|
+
const t1 = performance.now()
|
|
575
|
+
for (const k of scalars) BandersnatchCurve.scalarMultiply(G, k, false)
|
|
576
|
+
naiveGenTotal += performance.now() - t1
|
|
577
|
+
}
|
|
578
|
+
const glvGenMs = glvGenTotal / rounds
|
|
579
|
+
const naiveGenMs = naiveGenTotal / rounds
|
|
580
|
+
|
|
581
|
+
// --- Arbitrary point benchmark ---
|
|
582
|
+
let glvArbTotal = 0
|
|
583
|
+
let naiveArbTotal = 0
|
|
584
|
+
for (let r = 0; r < rounds; r++) {
|
|
585
|
+
const t0 = performance.now()
|
|
586
|
+
for (let i = 0; i < iterations; i++)
|
|
587
|
+
BandersnatchCurve.scalarMultiply(points[i], scalars[(i + 1) % iterations], true)
|
|
588
|
+
glvArbTotal += performance.now() - t0
|
|
589
|
+
|
|
590
|
+
const t1 = performance.now()
|
|
591
|
+
for (let i = 0; i < iterations; i++)
|
|
592
|
+
BandersnatchCurve.scalarMultiply(points[i], scalars[(i + 1) % iterations], false)
|
|
593
|
+
naiveArbTotal += performance.now() - t1
|
|
594
|
+
}
|
|
595
|
+
const glvArbMs = glvArbTotal / rounds
|
|
596
|
+
const naiveArbMs = naiveArbTotal / rounds
|
|
597
|
+
|
|
598
|
+
const genSpeedup = ((naiveGenMs - glvGenMs) / naiveGenMs) * 100
|
|
599
|
+
const arbSpeedup = ((naiveArbMs - glvArbMs) / naiveArbMs) * 100
|
|
600
|
+
|
|
601
|
+
console.log(`\n--- Scalar Multiplication Benchmark (${iterations} scalars, avg of ${rounds} rounds) ---`)
|
|
602
|
+
console.log(` Generator point:`)
|
|
603
|
+
console.log(` GLV (Shamir): ${glvGenMs.toFixed(2)} ms (${(glvGenMs / iterations).toFixed(2)} ms/mul)`)
|
|
604
|
+
console.log(` Naive (noble): ${naiveGenMs.toFixed(2)} ms (${(naiveGenMs / iterations).toFixed(2)} ms/mul)`)
|
|
605
|
+
console.log(` GLV vs naive: ${genSpeedup > 0 ? '+' : ''}${genSpeedup.toFixed(1)}%`)
|
|
606
|
+
console.log(` Arbitrary points:`)
|
|
607
|
+
console.log(` GLV (Shamir): ${glvArbMs.toFixed(2)} ms (${(glvArbMs / iterations).toFixed(2)} ms/mul)`)
|
|
608
|
+
console.log(` Naive (noble): ${naiveArbMs.toFixed(2)} ms (${(naiveArbMs / iterations).toFixed(2)} ms/mul)`)
|
|
609
|
+
console.log(` GLV vs naive: ${arbSpeedup > 0 ? '+' : ''}${arbSpeedup.toFixed(1)}%`)
|
|
610
|
+
|
|
611
|
+
expect(true).toBe(true)
|
|
612
|
+
})
|
|
613
|
+
})
|
package/src/curve.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { type EdwardsPoint, edwards } from '@noble/curves/abstract/edwards.js'
|
|
9
9
|
import { Field } from '@noble/curves/abstract/modular.js'
|
|
10
10
|
import { BANDERSNATCH_PARAMS } from './config'
|
|
11
|
+
import { glvMultiply } from './glv'
|
|
11
12
|
import { mod, modInverse, modSqrt } from './math'
|
|
12
13
|
|
|
13
14
|
// Elligator2 hash-to-curve moved to bandersnatch-vrf package
|
|
@@ -207,37 +208,38 @@ export class BandersnatchCurve {
|
|
|
207
208
|
* - Negative scalars: negates the point and uses positive scalar
|
|
208
209
|
* - Scalars >= curve order: reduces modulo curve order
|
|
209
210
|
*
|
|
211
|
+
* By default uses GLV endomorphism (Shamir's trick with ~127-bit loop) for
|
|
212
|
+
* ~40% faster multiplication. Pass `useGlv: false` to use the naive
|
|
213
|
+
* double-and-add from @noble/curves instead.
|
|
214
|
+
*
|
|
210
215
|
* @param point - The curve point to multiply
|
|
211
216
|
* @param scalar - The scalar multiplier (can be negative or >= curve order)
|
|
217
|
+
* @param useGlv - Whether to use the GLV endomorphism optimization (default: true)
|
|
212
218
|
* @returns The result of scalar multiplication: `scalar * point`
|
|
213
219
|
* @throws {Error} If the point is invalid or not on the curve
|
|
214
220
|
*/
|
|
215
|
-
static scalarMultiply(
|
|
216
|
-
|
|
221
|
+
static scalarMultiply(
|
|
222
|
+
point: EdwardsPoint,
|
|
223
|
+
scalar: bigint,
|
|
224
|
+
useGlv = true,
|
|
225
|
+
): EdwardsPoint {
|
|
226
|
+
if (useGlv) {
|
|
227
|
+
return glvMultiply(point, scalar)
|
|
228
|
+
}
|
|
229
|
+
|
|
217
230
|
if (scalar === 0n) {
|
|
218
231
|
return Bandersnatch.ZERO
|
|
219
232
|
}
|
|
220
233
|
|
|
221
|
-
// Handle negative scalars: negate point and use positive scalar
|
|
222
234
|
if (scalar < 0n) {
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const reducedScalar = positiveScalar % BANDERSNATCH_PARAMS.CURVE_ORDER
|
|
227
|
-
if (reducedScalar === 0n) {
|
|
228
|
-
return Bandersnatch.ZERO
|
|
229
|
-
}
|
|
230
|
-
return negPoint.multiply(reducedScalar)
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Reduce scalar modulo curve order if it's >= curve order
|
|
234
|
-
// @noble/curves requires 1 <= scalar < curve.n
|
|
235
|
-
const reducedScalar = scalar % BANDERSNATCH_PARAMS.CURVE_ORDER
|
|
236
|
-
if (reducedScalar === 0n) {
|
|
237
|
-
return Bandersnatch.ZERO
|
|
235
|
+
const positiveScalar = (-scalar) % BANDERSNATCH_PARAMS.CURVE_ORDER
|
|
236
|
+
if (positiveScalar === 0n) return Bandersnatch.ZERO
|
|
237
|
+
return point.negate().multiply(positiveScalar)
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
|
|
240
|
+
const reduced = scalar % BANDERSNATCH_PARAMS.CURVE_ORDER
|
|
241
|
+
if (reduced === 0n) return Bandersnatch.ZERO
|
|
242
|
+
return point.multiply(reduced)
|
|
241
243
|
}
|
|
242
244
|
|
|
243
245
|
/**
|
package/src/glv.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GLV (Gallant-Lambert-Vanstone) endomorphism for Bandersnatch scalar multiplication.
|
|
3
|
+
*
|
|
4
|
+
* Decomposes a ~253-bit scalar multiplication [k]P into two ~127-bit multiplications
|
|
5
|
+
* using an efficiently computable endomorphism phi:
|
|
6
|
+
* [k]P = [k1]P + [k2]phi(P) where k = k1 + lambda*k2, |k1|,|k2| ~ sqrt(r)
|
|
7
|
+
*
|
|
8
|
+
* Combined with Shamir's trick (simultaneous double-and-add), this halves the
|
|
9
|
+
* number of doublings from ~253 to ~127, yielding ~40% speedup.
|
|
10
|
+
*
|
|
11
|
+
* Reference: Bandersnatch paper (https://eprint.iacr.org/2021/1152)
|
|
12
|
+
* Constants derived from: ZKNoxHQ/PyBandersnatch sage scripts
|
|
13
|
+
* Algorithm from: arkworks-algebra/ec/src/scalar_mul/glv.rs
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { EdwardsPoint } from '@noble/curves/abstract/edwards.js'
|
|
17
|
+
import { Bandersnatch } from './curve'
|
|
18
|
+
import { BANDERSNATCH_PARAMS } from './config'
|
|
19
|
+
import { mod, modInverse } from './math'
|
|
20
|
+
|
|
21
|
+
const P = BANDERSNATCH_PARAMS.FIELD_MODULUS
|
|
22
|
+
const R = BANDERSNATCH_PARAMS.CURVE_ORDER
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Endomorphism rational map coefficients for the Twisted Edwards form.
|
|
26
|
+
*
|
|
27
|
+
* phi(x, y) in projective TE coords (z=1 for affine input):
|
|
28
|
+
* x_phi = x * (AY4*y^4 + AY2Z2*y^2 + AZ4) / (y * (CY2*y^2 + CZ2))
|
|
29
|
+
* y_phi = (BY2*y^2 + BZ2) / (CY2*y^2 + CZ2)
|
|
30
|
+
*
|
|
31
|
+
* Derived via sage from the degree-2 isogeny on the Weierstrass form,
|
|
32
|
+
* transferred to TE coordinates.
|
|
33
|
+
*/
|
|
34
|
+
const AY4 = 0x1d46e71b2d28e06c42bc1f5a41f4a0156d070863689e8862eb12927f72f308c3n
|
|
35
|
+
const AY2Z2 = 0x20b21e58881722d68c92fa09709ea65d716e869843e94c821df033483694a51an
|
|
36
|
+
const AZ4 = 0x1373fe65dcb354e5209f902de5b37008d6c2721d8d6d5fb556e8b7e969c053c9n
|
|
37
|
+
const BY2 = 0x33937d60e9a0dd55ed1f9030e7c8b6fa9c42e1e41d2f1361a0fed9630f711caen
|
|
38
|
+
const BZ2 = 0x39a33e54438fe0155ae18e93205d4395acfe4be1127ca6458fc0270450b1b50dn
|
|
39
|
+
const CY2 = 0x2cdc91c2ed341d7901e6d6ece64cd98591c66ba64cdc7109d1bdd9cb6f93ee68n
|
|
40
|
+
const CZ2 = 0x405a29f23ffc9ff2461a47d721d9210ab77ac21ee2cf489d5f01269bf08ee353n
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Scalar decomposition lattice basis (LLL-reduced).
|
|
44
|
+
* Used to decompose k into k1 + lambda*k2 with |k1|, |k2| ~ sqrt(r).
|
|
45
|
+
*/
|
|
46
|
+
const M1 = -113482231691339203864511368254957623327n
|
|
47
|
+
const M2 = 10741319382058138887739339959866629956n
|
|
48
|
+
const M3 = 21482638764116277775478679919733259912n
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Floor division for BigInt (towards negative infinity).
|
|
52
|
+
* JS BigInt `/` truncates towards zero; this corrects for negative dividends.
|
|
53
|
+
*/
|
|
54
|
+
function floorDiv(a: bigint, b: bigint): bigint {
|
|
55
|
+
const q = a / b
|
|
56
|
+
const r = a % b
|
|
57
|
+
if (r !== 0n && (r < 0n) !== (b < 0n)) {
|
|
58
|
+
return q - 1n
|
|
59
|
+
}
|
|
60
|
+
return q
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function abs(x: bigint): bigint {
|
|
64
|
+
return x < 0n ? -x : x
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function bitLength(x: bigint): number {
|
|
68
|
+
if (x === 0n) return 0
|
|
69
|
+
let v = x < 0n ? -x : x
|
|
70
|
+
let bits = 0
|
|
71
|
+
while (v > 0n) {
|
|
72
|
+
bits++
|
|
73
|
+
v >>= 1n
|
|
74
|
+
}
|
|
75
|
+
return bits
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Compute the Bandersnatch endomorphism phi(P) on a Twisted Edwards point.
|
|
80
|
+
*
|
|
81
|
+
* phi is the sqrt(-2) endomorphism satisfying phi(P) = [lambda]P for all P in
|
|
82
|
+
* the prime-order subgroup, where lambda is the endomorphism eigenvalue.
|
|
83
|
+
*
|
|
84
|
+
* The rational map operates on affine TE coordinates:
|
|
85
|
+
* x_phi = x * (AY4*y^4 + AY2Z2*y^2 + AZ4) / (y * (CY2*y^2 + CZ2))
|
|
86
|
+
* y_phi = (BY2*y^2 + BZ2) / (CY2*y^2 + CZ2)
|
|
87
|
+
*
|
|
88
|
+
* Cost: ~12 field multiplications + 1 field inversion (constant, independent of scalar size).
|
|
89
|
+
*/
|
|
90
|
+
export function endomorphism(point: EdwardsPoint): EdwardsPoint {
|
|
91
|
+
if (point.equals(Bandersnatch.ZERO)) {
|
|
92
|
+
return Bandersnatch.ZERO
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const { x, y } = point.toAffine()
|
|
96
|
+
|
|
97
|
+
const y2 = mod(y * y, P)
|
|
98
|
+
const y4 = mod(y2 * y2, P)
|
|
99
|
+
|
|
100
|
+
const xNumerator = mod(
|
|
101
|
+
x * mod(AY4 * y4 + AY2Z2 * y2 + AZ4, P),
|
|
102
|
+
P,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const denominator = mod(CY2 * y2 + CZ2, P)
|
|
106
|
+
const denominatorInv = modInverse(denominator, P)
|
|
107
|
+
|
|
108
|
+
const yDenominator = mod(y * denominator, P)
|
|
109
|
+
const yDenominatorInv = modInverse(yDenominator, P)
|
|
110
|
+
|
|
111
|
+
const xPhi = mod(xNumerator * yDenominatorInv, P)
|
|
112
|
+
const yPhi = mod(mod(BY2 * y2 + BZ2, P) * denominatorInv, P)
|
|
113
|
+
|
|
114
|
+
return Bandersnatch.fromAffine({ x: xPhi, y: yPhi })
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Decompose scalar k into (k1, k2) such that k === k1 + lambda * k2 (mod r),
|
|
119
|
+
* with |k1|, |k2| approximately sqrt(r) (~127 bits).
|
|
120
|
+
*
|
|
121
|
+
* Uses Babai's nearest-plane algorithm with the LLL-reduced lattice basis.
|
|
122
|
+
*
|
|
123
|
+
* @returns [sign1, abs_k1, sign2, abs_k2] where sign is true for positive
|
|
124
|
+
*/
|
|
125
|
+
export function scalarDecomposition(
|
|
126
|
+
k: bigint,
|
|
127
|
+
): [boolean, bigint, boolean, bigint] {
|
|
128
|
+
const b0 = floorDiv(k * M1, R)
|
|
129
|
+
const b1 = floorDiv(k * M2, R)
|
|
130
|
+
|
|
131
|
+
const s0 = k - b0 * M1 - b1 * M3
|
|
132
|
+
const s1 = -(b0 * M2 + b1 * (-M1))
|
|
133
|
+
|
|
134
|
+
return [s0 >= 0n, abs(s0), s1 >= 0n, abs(s1)]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* GLV-accelerated scalar multiplication using Shamir's trick.
|
|
139
|
+
*
|
|
140
|
+
* Computes [scalar]P by:
|
|
141
|
+
* 1. Decomposing scalar into two half-size scalars (k1, k2)
|
|
142
|
+
* 2. Computing phi(P) via the endomorphism
|
|
143
|
+
* 3. Running a single simultaneous double-and-add loop over both scalars
|
|
144
|
+
*
|
|
145
|
+
* The loop iterates ~127 bits instead of ~253, roughly halving the doublings.
|
|
146
|
+
*
|
|
147
|
+
* @param point - The curve point P (must be in the prime-order subgroup)
|
|
148
|
+
* @param scalar - The scalar k (must be in range [1, r-1] after reduction)
|
|
149
|
+
*/
|
|
150
|
+
export function glvMultiply(
|
|
151
|
+
point: EdwardsPoint,
|
|
152
|
+
scalar: bigint,
|
|
153
|
+
): EdwardsPoint {
|
|
154
|
+
if (scalar === 0n) {
|
|
155
|
+
return Bandersnatch.ZERO
|
|
156
|
+
}
|
|
157
|
+
if (point.equals(Bandersnatch.ZERO)) {
|
|
158
|
+
return Bandersnatch.ZERO
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const reduced = ((scalar % R) + R) % R
|
|
162
|
+
if (reduced === 0n) {
|
|
163
|
+
return Bandersnatch.ZERO
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const [sign1, absK1, sign2, absK2] = scalarDecomposition(reduced)
|
|
167
|
+
|
|
168
|
+
if (absK1 === 0n && absK2 === 0n) {
|
|
169
|
+
return Bandersnatch.ZERO
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let b1 = sign1 ? point : point.negate()
|
|
173
|
+
let b2 = sign2 ? endomorphism(point) : endomorphism(point).negate()
|
|
174
|
+
|
|
175
|
+
if (absK1 === 0n) {
|
|
176
|
+
return absK2 === 1n ? b2 : b2.multiply(absK2)
|
|
177
|
+
}
|
|
178
|
+
if (absK2 === 0n) {
|
|
179
|
+
return absK1 === 1n ? b1 : b1.multiply(absK1)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const b1b2 = b1.add(b2)
|
|
183
|
+
|
|
184
|
+
const len1 = bitLength(absK1)
|
|
185
|
+
const len2 = bitLength(absK2)
|
|
186
|
+
const maxLen = len1 > len2 ? len1 : len2
|
|
187
|
+
|
|
188
|
+
let res = Bandersnatch.ZERO
|
|
189
|
+
let started = false
|
|
190
|
+
|
|
191
|
+
for (let i = maxLen - 1; i >= 0; i--) {
|
|
192
|
+
if (started) {
|
|
193
|
+
res = res.double()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const bit1 = (absK1 >> BigInt(i)) & 1n
|
|
197
|
+
const bit2 = (absK2 >> BigInt(i)) & 1n
|
|
198
|
+
|
|
199
|
+
if (bit1 === 1n && bit2 === 1n) {
|
|
200
|
+
res = started ? res.add(b1b2) : b1b2
|
|
201
|
+
started = true
|
|
202
|
+
} else if (bit1 === 1n) {
|
|
203
|
+
res = started ? res.add(b1) : b1
|
|
204
|
+
started = true
|
|
205
|
+
} else if (bit2 === 1n) {
|
|
206
|
+
res = started ? res.add(b2) : b2
|
|
207
|
+
started = true
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return res
|
|
212
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
// Export curve parameters
|
|
9
9
|
export { BANDERSNATCH_PARAMS } from './config'
|
|
10
|
-
// Elligator2 hash-to-curve is now available in bandersnatch-vrf package
|
|
11
10
|
// Export curve implementations
|
|
12
|
-
// Temporary alias for legacy compatibility
|
|
13
11
|
export {
|
|
14
12
|
Bandersnatch,
|
|
15
13
|
BandersnatchCurve,
|
|
16
14
|
} from './curve'
|
|
17
|
-
//
|
|
15
|
+
// Export GLV endomorphism
|
|
16
|
+
export { endomorphism, glvMultiply, scalarDecomposition } from './glv'
|
|
17
|
+
// Export math utilities
|
|
18
18
|
export * from './math'
|
|
19
19
|
// Export types
|
|
20
20
|
export type { CurvePoint } from './types'
|