@pbnjam/bandersnatch 0.1.0
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 +61 -0
- package/README.md +109 -0
- package/package.json +37 -0
- package/src/__tests__/curve.test.ts +298 -0
- package/src/config.ts +111 -0
- package/src/curve.ts +341 -0
- package/src/index.ts +20 -0
- package/src/math.ts +91 -0
- package/src/types.ts +22 -0
- package/tsconfig.json +31 -0
- package/tsconfig.typedoc.json +11 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name: Deploy Documentation
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
pages: write
|
|
12
|
+
id-token: write
|
|
13
|
+
|
|
14
|
+
concurrency:
|
|
15
|
+
group: "pages"
|
|
16
|
+
cancel-in-progress: false
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
build:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout
|
|
23
|
+
uses: actions/checkout@v4
|
|
24
|
+
with:
|
|
25
|
+
submodules: recursive
|
|
26
|
+
|
|
27
|
+
- name: Setup Bun
|
|
28
|
+
uses: oven-sh/setup-bun@v2
|
|
29
|
+
with:
|
|
30
|
+
bun-version: latest
|
|
31
|
+
|
|
32
|
+
- name: Install dependencies
|
|
33
|
+
run: |
|
|
34
|
+
bun install
|
|
35
|
+
# Ensure TypeDoc can find all dependencies
|
|
36
|
+
bun install --frozen-lockfile || bun install
|
|
37
|
+
|
|
38
|
+
- name: Generate documentation
|
|
39
|
+
run: bun run docs
|
|
40
|
+
env:
|
|
41
|
+
NODE_ENV: production
|
|
42
|
+
|
|
43
|
+
- name: Setup Pages
|
|
44
|
+
uses: actions/configure-pages@v4
|
|
45
|
+
|
|
46
|
+
- name: Upload artifact
|
|
47
|
+
uses: actions/upload-pages-artifact@v3
|
|
48
|
+
with:
|
|
49
|
+
path: docs
|
|
50
|
+
|
|
51
|
+
deploy:
|
|
52
|
+
environment:
|
|
53
|
+
name: github-pages
|
|
54
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
needs: build
|
|
57
|
+
steps:
|
|
58
|
+
- name: Deploy to GitHub Pages
|
|
59
|
+
id: deployment
|
|
60
|
+
uses: actions/deploy-pages@v4
|
|
61
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# `@pbnjam/bandersnatch`
|
|
2
|
+
|
|
3
|
+
Bandersnatch elliptic curve primitives built on top of `@noble/curves`.
|
|
4
|
+
|
|
5
|
+
This package provides:
|
|
6
|
+
|
|
7
|
+
- A **Bandersnatch curve instance** (`Bandersnatch`) configured with the official parameters.
|
|
8
|
+
- A **high-level helper class** (`BandersnatchCurve`) for point operations and arkworks-compatible serialization.
|
|
9
|
+
- Public **curve parameters** (`BANDERSNATCH_PARAMS`).
|
|
10
|
+
|
|
11
|
+
> Note: VRF functionality and Elligator2 hash-to-curve are implemented in `@pbnjam/bandersnatch-vrf`.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
This repository uses Bun workspaces. From the monorepo root, install deps once:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Within another workspace package, add a dependency on `@pbnjam/bandersnatch` via the workspace tooling you use (Bun/npm/pnpm/yarn).
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### Import the public API
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { Bandersnatch, BandersnatchCurve, BANDERSNATCH_PARAMS } from '@pbnjam/bandersnatch'
|
|
29
|
+
import type { CurvePoint } from '@pbnjam/bandersnatch'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Work with points (high-level helpers)
|
|
33
|
+
|
|
34
|
+
`BandersnatchCurve` exposes common operations over **Noble** Bandersnatch points (`EdwardsPoint` from `@noble/curves`), such as:
|
|
35
|
+
|
|
36
|
+
- `GENERATOR`: the canonical generator point
|
|
37
|
+
- `INFINITY`: point at infinity (identity)
|
|
38
|
+
- `add(P, Q)`, `negate(P)`, `scalarMultiply(P, k)`
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { BandersnatchCurve } from '@pbnjam/bandersnatch'
|
|
42
|
+
|
|
43
|
+
const P = BandersnatchCurve.GENERATOR
|
|
44
|
+
const Q = BandersnatchCurve.scalarMultiply(P, 2n)
|
|
45
|
+
const R = BandersnatchCurve.add(P, Q)
|
|
46
|
+
const negR = BandersnatchCurve.negate(R)
|
|
47
|
+
|
|
48
|
+
// Access affine coordinates (BigInt) via Noble helpers
|
|
49
|
+
const { x, y } = R.toAffine()
|
|
50
|
+
void x
|
|
51
|
+
void y
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Arkworks-compatible point serialization
|
|
55
|
+
|
|
56
|
+
The helper methods are intended to be compatible with arkworks’ Twisted Edwards compression format.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { Bandersnatch, BandersnatchCurve } from '@pbnjam/bandersnatch'
|
|
60
|
+
|
|
61
|
+
// Noble point → compressed bytes (arkworks-compatible)
|
|
62
|
+
const noblePoint = Bandersnatch.BASE
|
|
63
|
+
const compressed = BandersnatchCurve.pointToBytes(noblePoint)
|
|
64
|
+
|
|
65
|
+
// Compressed bytes → Noble point (throws on invalid encoding)
|
|
66
|
+
const parsed = BandersnatchCurve.bytesToPoint(compressed)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Public API
|
|
70
|
+
|
|
71
|
+
### Exports
|
|
72
|
+
|
|
73
|
+
From `src/index.ts`:
|
|
74
|
+
|
|
75
|
+
- `BandersnatchCurve`
|
|
76
|
+
- `Bandersnatch`
|
|
77
|
+
- `BANDERSNATCH_PARAMS`
|
|
78
|
+
- `CurvePoint` (type)
|
|
79
|
+
|
|
80
|
+
### `CurvePoint`
|
|
81
|
+
|
|
82
|
+
`CurvePoint` is a simple structural representation used by some algorithms (notably `@pbnjam/bandersnatch-vrf`’s Elligator2 hash-to-curve helpers):
|
|
83
|
+
|
|
84
|
+
- `x: bigint`
|
|
85
|
+
- `y: bigint`
|
|
86
|
+
- `isInfinity: boolean`
|
|
87
|
+
|
|
88
|
+
## Parameters
|
|
89
|
+
|
|
90
|
+
All protocol constants are defined in `src/config.ts` as `BANDERSNATCH_PARAMS` (field modulus, curve order, generator, coefficients, and related configuration used by dependent packages).
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
From `packages/bandersnatch`:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
bun run test
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
bun run build
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Security & correctness notes
|
|
105
|
+
|
|
106
|
+
- `BandersnatchCurve` uses BigInt arithmetic and performs modular operations over the Bandersnatch field.
|
|
107
|
+
- When handling serialized points, treat all external inputs as untrusted and rely on parsing/validation helpers (avoid manual decoding).
|
|
108
|
+
|
|
109
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pbnjam/bandersnatch",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Bandersnatch elliptic curve implementation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/Esscrypt/bandersnatch.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/Esscrypt/bandersnatch",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Esscrypt/bandersnatch/issues"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"test": "bun test",
|
|
17
|
+
"dev": "tsc --watch",
|
|
18
|
+
"docs": "npx typedoc --out docs --entryPoints src/index.ts --entryPointStrategy expand --readme none --name '@pbnjam/bandersnatch' --includeVersion --tsconfig tsconfig.typedoc.json"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@noble/curves": "^2.0.1",
|
|
22
|
+
"@noble/hashes": "^2.0.1"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/bun": "^1.3.1",
|
|
26
|
+
"@types/node": "^22.0.0",
|
|
27
|
+
"bun-types": "^1.3.1",
|
|
28
|
+
"typescript": "5.9.3",
|
|
29
|
+
"typedoc": "0.28.15"
|
|
30
|
+
},
|
|
31
|
+
"exports": {
|
|
32
|
+
".": "./src/index.ts"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { BandersnatchCurve } from '../curve'
|
|
3
|
+
|
|
4
|
+
describe('BandersnatchCurve Operations', () => {
|
|
5
|
+
test('Point addition: P + Q = Q + P (commutativity)', () => {
|
|
6
|
+
const P = BandersnatchCurve.GENERATOR
|
|
7
|
+
const Q = BandersnatchCurve.scalarMultiply(P, 2n)
|
|
8
|
+
|
|
9
|
+
const P_plus_Q = BandersnatchCurve.add(P, Q)
|
|
10
|
+
const Q_plus_P = BandersnatchCurve.add(Q, P)
|
|
11
|
+
|
|
12
|
+
expect(P_plus_Q.x).toBe(Q_plus_P.x)
|
|
13
|
+
expect(P_plus_Q.y).toBe(Q_plus_P.y)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('Point addition: P + O = P (identity)', () => {
|
|
17
|
+
const P = BandersnatchCurve.GENERATOR
|
|
18
|
+
const O = BandersnatchCurve.INFINITY
|
|
19
|
+
|
|
20
|
+
const P_plus_O = BandersnatchCurve.add(P, O)
|
|
21
|
+
|
|
22
|
+
expect(P_plus_O.x).toBe(P.x)
|
|
23
|
+
expect(P_plus_O.y).toBe(P.y)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('Point addition: P + (-P) = O (inverse)', () => {
|
|
27
|
+
const P = BandersnatchCurve.GENERATOR
|
|
28
|
+
const negP = BandersnatchCurve.negate(P)
|
|
29
|
+
|
|
30
|
+
const P_plus_negP = BandersnatchCurve.add(P, negP)
|
|
31
|
+
|
|
32
|
+
expect(P_plus_negP.x).toBe(BandersnatchCurve.INFINITY.x)
|
|
33
|
+
expect(P_plus_negP.y).toBe(BandersnatchCurve.INFINITY.y)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('Scalar multiplication: 1 * P = P', () => {
|
|
37
|
+
const P = BandersnatchCurve.GENERATOR
|
|
38
|
+
const oneP = BandersnatchCurve.scalarMultiply(P, 1n)
|
|
39
|
+
|
|
40
|
+
expect(oneP.x).toBe(P.x)
|
|
41
|
+
expect(oneP.y).toBe(P.y)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('Scalar multiplication: 0 * P = O', () => {
|
|
45
|
+
const P = BandersnatchCurve.GENERATOR
|
|
46
|
+
const zeroP = BandersnatchCurve.scalarMultiply(P, 0n)
|
|
47
|
+
|
|
48
|
+
expect(zeroP.x).toBe(BandersnatchCurve.INFINITY.x)
|
|
49
|
+
expect(zeroP.y).toBe(BandersnatchCurve.INFINITY.y)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('Scalar multiplication: 2 * P = P + P', () => {
|
|
53
|
+
const P = BandersnatchCurve.GENERATOR
|
|
54
|
+
|
|
55
|
+
const twoP = BandersnatchCurve.scalarMultiply(P, 2n)
|
|
56
|
+
const P_plus_P = BandersnatchCurve.add(P, P)
|
|
57
|
+
|
|
58
|
+
expect(twoP.x).toBe(P_plus_P.x)
|
|
59
|
+
expect(twoP.y).toBe(P_plus_P.y)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('Scalar multiplication: 3 * P = P + P + P', () => {
|
|
63
|
+
const P = BandersnatchCurve.GENERATOR
|
|
64
|
+
|
|
65
|
+
const threeP = BandersnatchCurve.scalarMultiply(P, 3n)
|
|
66
|
+
const P_plus_P_plus_P = BandersnatchCurve.add(
|
|
67
|
+
BandersnatchCurve.add(P, P),
|
|
68
|
+
P
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
expect(threeP.x).toBe(P_plus_P_plus_P.x)
|
|
72
|
+
expect(threeP.y).toBe(P_plus_P_plus_P.y)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('Scalar multiplication: (a + b) * P = a * P + b * P', () => {
|
|
76
|
+
const P = BandersnatchCurve.GENERATOR
|
|
77
|
+
const a = 5n
|
|
78
|
+
const b = 7n
|
|
79
|
+
|
|
80
|
+
const a_plus_b_P = BandersnatchCurve.scalarMultiply(P, a + b)
|
|
81
|
+
const aP_plus_bP = BandersnatchCurve.add(
|
|
82
|
+
BandersnatchCurve.scalarMultiply(P, a),
|
|
83
|
+
BandersnatchCurve.scalarMultiply(P, b)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
expect(a_plus_b_P.x).toBe(aP_plus_bP.x)
|
|
87
|
+
expect(a_plus_b_P.y).toBe(aP_plus_bP.y)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('Scalar multiplication: (a * b) * P = a * (b * P)', () => {
|
|
91
|
+
const P = BandersnatchCurve.GENERATOR
|
|
92
|
+
const a = 3n
|
|
93
|
+
const b = 4n
|
|
94
|
+
|
|
95
|
+
const ab_P = BandersnatchCurve.scalarMultiply(P, a * b)
|
|
96
|
+
const a_bP = BandersnatchCurve.scalarMultiply(
|
|
97
|
+
BandersnatchCurve.scalarMultiply(P, b),
|
|
98
|
+
a
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
expect(ab_P.x).toBe(a_bP.x)
|
|
102
|
+
expect(ab_P.y).toBe(a_bP.y)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('Point serialization round-trip', () => {
|
|
106
|
+
const P = BandersnatchCurve.GENERATOR
|
|
107
|
+
|
|
108
|
+
const P_bytes = BandersnatchCurve.pointToBytes(P)
|
|
109
|
+
const P_reconstructed = BandersnatchCurve.bytesToPoint(P_bytes)
|
|
110
|
+
|
|
111
|
+
expect(P_reconstructed.x).toBe(P.x)
|
|
112
|
+
expect(P_reconstructed.y).toBe(P.y)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('Point serialization round-trip with random point', () => {
|
|
116
|
+
const P = BandersnatchCurve.scalarMultiply(BandersnatchCurve.GENERATOR, 12345n)
|
|
117
|
+
|
|
118
|
+
const P_bytes = BandersnatchCurve.pointToBytes(P)
|
|
119
|
+
const P_reconstructed = BandersnatchCurve.bytesToPoint(P_bytes)
|
|
120
|
+
|
|
121
|
+
expect(P_reconstructed.x).toBe(P.x)
|
|
122
|
+
expect(P_reconstructed.y).toBe(P.y)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('Point is on curve validation', () => {
|
|
126
|
+
const P = BandersnatchCurve.GENERATOR
|
|
127
|
+
const Q = BandersnatchCurve.scalarMultiply(P, 2n)
|
|
128
|
+
const R = BandersnatchCurve.add(P, Q)
|
|
129
|
+
|
|
130
|
+
expect(BandersnatchCurve.isOnCurve(P)).toBe(true)
|
|
131
|
+
expect(BandersnatchCurve.isOnCurve(Q)).toBe(true)
|
|
132
|
+
expect(BandersnatchCurve.isOnCurve(R)).toBe(true)
|
|
133
|
+
expect(BandersnatchCurve.isOnCurve(BandersnatchCurve.INFINITY)).toBe(true)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('Associativity: (P + Q) + R = P + (Q + R)', () => {
|
|
137
|
+
const P = BandersnatchCurve.GENERATOR
|
|
138
|
+
const Q = BandersnatchCurve.scalarMultiply(P, 2n)
|
|
139
|
+
const R = BandersnatchCurve.scalarMultiply(P, 3n)
|
|
140
|
+
|
|
141
|
+
const left = BandersnatchCurve.add(BandersnatchCurve.add(P, Q), R)
|
|
142
|
+
const right = BandersnatchCurve.add(P, BandersnatchCurve.add(Q, R))
|
|
143
|
+
|
|
144
|
+
expect(left.x).toBe(right.x)
|
|
145
|
+
expect(left.y).toBe(right.y)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('Distributivity: a * (P + Q) = a * P + a * Q', () => {
|
|
149
|
+
const P = BandersnatchCurve.GENERATOR
|
|
150
|
+
const Q = BandersnatchCurve.scalarMultiply(P, 2n)
|
|
151
|
+
const a = 5n
|
|
152
|
+
|
|
153
|
+
const left = BandersnatchCurve.scalarMultiply(BandersnatchCurve.add(P, Q), a)
|
|
154
|
+
const right = BandersnatchCurve.add(
|
|
155
|
+
BandersnatchCurve.scalarMultiply(P, a),
|
|
156
|
+
BandersnatchCurve.scalarMultiply(Q, a)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
expect(left.x).toBe(right.x)
|
|
160
|
+
expect(left.y).toBe(right.y)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('Large scalar multiplication', () => {
|
|
164
|
+
const P = BandersnatchCurve.GENERATOR
|
|
165
|
+
const largeScalar = BigInt('0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef')
|
|
166
|
+
|
|
167
|
+
const largeP = BandersnatchCurve.scalarMultiply(P, largeScalar)
|
|
168
|
+
|
|
169
|
+
// Verify the point is still on the curve
|
|
170
|
+
expect(BandersnatchCurve.isOnCurve(largeP)).toBe(true)
|
|
171
|
+
|
|
172
|
+
// Verify it's not the identity
|
|
173
|
+
expect(largeP.x).not.toBe(BandersnatchCurve.INFINITY.x)
|
|
174
|
+
expect(largeP.y).not.toBe(BandersnatchCurve.INFINITY.y)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('Modular arithmetic consistency', () => {
|
|
178
|
+
const P = BandersnatchCurve.GENERATOR
|
|
179
|
+
// Use a large scalar to test modular arithmetic
|
|
180
|
+
// Reduce modulo curve order since @noble/curves requires 1 <= scalar < curve.n
|
|
181
|
+
const largeScalar = BigInt('0x10000000000000000000000000000000000000000000000000000000000000000')
|
|
182
|
+
const curveOrder = BandersnatchCurve.CURVE_ORDER
|
|
183
|
+
const reducedScalar = largeScalar % curveOrder
|
|
184
|
+
// Ensure scalar is in valid range [1, curve.n) for @noble/curves
|
|
185
|
+
const validScalar = reducedScalar === 0n ? 1n : reducedScalar
|
|
186
|
+
|
|
187
|
+
const nP = BandersnatchCurve.scalarMultiply(P, validScalar)
|
|
188
|
+
|
|
189
|
+
// The result should be a valid point on the curve
|
|
190
|
+
expect(BandersnatchCurve.isOnCurve(nP)).toBe(true)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('Specific values from debug output', () => {
|
|
194
|
+
// Test with a valid point on the curve instead of hardcoded debug values
|
|
195
|
+
const P = BandersnatchCurve.GENERATOR
|
|
196
|
+
|
|
197
|
+
// Test that P is on the curve
|
|
198
|
+
expect(BandersnatchCurve.isOnCurve(P)).toBe(true)
|
|
199
|
+
|
|
200
|
+
// Test x*P computation
|
|
201
|
+
const x = BigInt('0x101010101010101010101010101010101010101010101010101010101010101')
|
|
202
|
+
const xP = BandersnatchCurve.scalarMultiply(P, x)
|
|
203
|
+
|
|
204
|
+
// Test that xP is on the curve
|
|
205
|
+
expect(BandersnatchCurve.isOnCurve(xP)).toBe(true)
|
|
206
|
+
|
|
207
|
+
// Test round-trip serialization
|
|
208
|
+
const xP_bytes = BandersnatchCurve.pointToBytes(xP)
|
|
209
|
+
const xP_reconstructed = BandersnatchCurve.bytesToPoint(xP_bytes)
|
|
210
|
+
|
|
211
|
+
expect(xP_reconstructed.x).toBe(xP.x)
|
|
212
|
+
expect(xP_reconstructed.y).toBe(xP.y)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
test('Challenge computation consistency', () => {
|
|
216
|
+
// Test with valid points on the curve
|
|
217
|
+
const P = BandersnatchCurve.GENERATOR
|
|
218
|
+
const Q = BandersnatchCurve.scalarMultiply(P, 2n)
|
|
219
|
+
const R = BandersnatchCurve.scalarMultiply(P, 3n)
|
|
220
|
+
|
|
221
|
+
// Test that all points are on the curve
|
|
222
|
+
expect(BandersnatchCurve.isOnCurve(P)).toBe(true)
|
|
223
|
+
expect(BandersnatchCurve.isOnCurve(Q)).toBe(true)
|
|
224
|
+
expect(BandersnatchCurve.isOnCurve(R)).toBe(true)
|
|
225
|
+
|
|
226
|
+
// Test valid mathematical relationships
|
|
227
|
+
const a = 5n
|
|
228
|
+
const b = 7n
|
|
229
|
+
|
|
230
|
+
// Test (a + b) * P = a * P + b * P (distributivity)
|
|
231
|
+
const aP = BandersnatchCurve.scalarMultiply(P, a)
|
|
232
|
+
const bP = BandersnatchCurve.scalarMultiply(P, b)
|
|
233
|
+
const abP = BandersnatchCurve.scalarMultiply(P, a + b)
|
|
234
|
+
const aP_plus_bP = BandersnatchCurve.add(aP, bP)
|
|
235
|
+
|
|
236
|
+
expect(abP.x).toBe(aP_plus_bP.x)
|
|
237
|
+
expect(abP.y).toBe(aP_plus_bP.y)
|
|
238
|
+
|
|
239
|
+
// Test (a * b) * P = a * (b * P) (associativity)
|
|
240
|
+
const ab_times_P = BandersnatchCurve.scalarMultiply(P, a * b)
|
|
241
|
+
const a_times_bP = BandersnatchCurve.scalarMultiply(bP, a)
|
|
242
|
+
|
|
243
|
+
expect(ab_times_P.x).toBe(a_times_bP.x)
|
|
244
|
+
expect(ab_times_P.y).toBe(a_times_bP.y)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('Comparison with BandersnatchCurve implementation', () => {
|
|
248
|
+
// Test that both implementations produce the same results
|
|
249
|
+
const P = BandersnatchCurve.GENERATOR
|
|
250
|
+
const scalar = 12345n
|
|
251
|
+
|
|
252
|
+
const nobleResult = BandersnatchCurve.scalarMultiply(P, scalar)
|
|
253
|
+
|
|
254
|
+
// Test that the result is on the curve
|
|
255
|
+
expect(BandersnatchCurve.isOnCurve(nobleResult)).toBe(true)
|
|
256
|
+
|
|
257
|
+
// Test serialization round-trip
|
|
258
|
+
const bytes = BandersnatchCurve.pointToBytes(nobleResult)
|
|
259
|
+
const reconstructed = BandersnatchCurve.bytesToPoint(bytes)
|
|
260
|
+
|
|
261
|
+
expect(reconstructed.x).toBe(nobleResult.x)
|
|
262
|
+
expect(reconstructed.y).toBe(nobleResult.y)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
test('Edge cases with zero and one', () => {
|
|
266
|
+
const P = BandersnatchCurve.GENERATOR
|
|
267
|
+
|
|
268
|
+
// Test 0 * P = O
|
|
269
|
+
const zeroP = BandersnatchCurve.scalarMultiply(P, 0n)
|
|
270
|
+
expect(zeroP.x).toBe(BandersnatchCurve.INFINITY.x)
|
|
271
|
+
expect(zeroP.y).toBe(BandersnatchCurve.INFINITY.y)
|
|
272
|
+
|
|
273
|
+
// Test 1 * P = P
|
|
274
|
+
const oneP = BandersnatchCurve.scalarMultiply(P, 1n)
|
|
275
|
+
expect(oneP.x).toBe(P.x)
|
|
276
|
+
expect(oneP.y).toBe(P.y)
|
|
277
|
+
|
|
278
|
+
// Test P + O = P
|
|
279
|
+
const P_plus_O = BandersnatchCurve.add(P, BandersnatchCurve.INFINITY)
|
|
280
|
+
expect(P_plus_O.x).toBe(P.x)
|
|
281
|
+
expect(P_plus_O.y).toBe(P.y)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test('Negative scalar multiplication', () => {
|
|
285
|
+
const P = BandersnatchCurve.GENERATOR
|
|
286
|
+
const scalar = 5n
|
|
287
|
+
|
|
288
|
+
// Test positive scalar
|
|
289
|
+
const positiveP = BandersnatchCurve.scalarMultiply(P, scalar)
|
|
290
|
+
|
|
291
|
+
// Test negative scalar (should be equivalent to negating the point)
|
|
292
|
+
const negativeP = BandersnatchCurve.scalarMultiply(P, -scalar)
|
|
293
|
+
const negatedP = BandersnatchCurve.negate(positiveP)
|
|
294
|
+
|
|
295
|
+
expect(negativeP.x).toBe(negatedP.x)
|
|
296
|
+
expect(negativeP.y).toBe(negatedP.y)
|
|
297
|
+
})
|
|
298
|
+
})
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bandersnatch curve parameters
|
|
3
|
+
* Reference: MSZ21 - https://eprint.iacr.org/2021/1152
|
|
4
|
+
* Bandersnatch is defined over the BLS12-381 scalar field
|
|
5
|
+
*/
|
|
6
|
+
export const BANDERSNATCH_PARAMS = {
|
|
7
|
+
/** Field modulus for Bandersnatch curve (from official specification) */
|
|
8
|
+
FIELD_MODULUS: BigInt(
|
|
9
|
+
'0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001',
|
|
10
|
+
),
|
|
11
|
+
|
|
12
|
+
/** Curve order for Bandersnatch curve (from official specification) */
|
|
13
|
+
CURVE_ORDER: BigInt(
|
|
14
|
+
'0x1cfb69d4ca675f520cce760202687600ff8f87007419047174fd06b52876e7e1',
|
|
15
|
+
),
|
|
16
|
+
|
|
17
|
+
/** Generator point coordinates (from official specification) */
|
|
18
|
+
GENERATOR: {
|
|
19
|
+
x: BigInt(
|
|
20
|
+
'18886178867200960497001835917649091219057080094937609519140440539760939937304',
|
|
21
|
+
),
|
|
22
|
+
y: BigInt(
|
|
23
|
+
'19188667384257783945677642223292697773471335439753913231509108946878080696678',
|
|
24
|
+
),
|
|
25
|
+
isInfinity: false,
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
/** Curve coefficients for Twisted Edwards form */
|
|
29
|
+
CURVE_COEFFICIENTS: {
|
|
30
|
+
/** Coefficient a = -5 */
|
|
31
|
+
a: BigInt(-5),
|
|
32
|
+
/** Coefficient d */
|
|
33
|
+
d: BigInt(
|
|
34
|
+
'0x6389c12633c267cbc66e3bf86be3b6d8cb66677177e54f92b369f2f5188d58e7',
|
|
35
|
+
),
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
/** Cofactor */
|
|
39
|
+
COFACTOR: BigInt(4),
|
|
40
|
+
|
|
41
|
+
/** Blinding base point for Pedersen VRF (from official specification) */
|
|
42
|
+
BLINDING_BASE: {
|
|
43
|
+
x: BigInt(
|
|
44
|
+
'6150229251051246713677296363717454238956877613358614224171740096471278798312',
|
|
45
|
+
),
|
|
46
|
+
y: BigInt(
|
|
47
|
+
'28442734166467795856797249030329035618871580593056783094884474814923353898473',
|
|
48
|
+
),
|
|
49
|
+
isInfinity: false,
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
/** Accumulator base point for Ring VRF (from ark-vrf specification) */
|
|
53
|
+
ACCUMULATOR_BASE: {
|
|
54
|
+
x: BigInt(
|
|
55
|
+
'3955725774225903122339172568337849452553276548604445833196164961773358506589',
|
|
56
|
+
),
|
|
57
|
+
y: BigInt(
|
|
58
|
+
'29870564530691725960104983716673293929719207405660860235233811770612192692323',
|
|
59
|
+
),
|
|
60
|
+
isInfinity: false,
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/** Padding point for Ring VRF (from ark-vrf specification) */
|
|
64
|
+
PADDING_POINT: {
|
|
65
|
+
x: BigInt(
|
|
66
|
+
'23942223917106120326220291257397678561637131227432899006603244452561725937075',
|
|
67
|
+
),
|
|
68
|
+
y: BigInt(
|
|
69
|
+
'1605027200774560580022502723165578671697794116420567297367317898913080293877',
|
|
70
|
+
),
|
|
71
|
+
isInfinity: false,
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
/** Curve characteristics */
|
|
75
|
+
CHARACTERISTICS: {
|
|
76
|
+
j_invariant: BigInt(0x1f40),
|
|
77
|
+
discriminant: BigInt(-8),
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/** Elligator2 hash-to-curve configuration (from arkworks) */
|
|
81
|
+
ELLIGATOR2_CONFIG: {
|
|
82
|
+
/** Non-square element Z = 5 */
|
|
83
|
+
Z: BigInt(5),
|
|
84
|
+
/** Precomputed 1/(COEFF_B)^2 */
|
|
85
|
+
ONE_OVER_COEFF_B_SQUARE: BigInt(
|
|
86
|
+
'35484827650731063748396669747216844996598387089274032563585525486049249153249',
|
|
87
|
+
),
|
|
88
|
+
/** Precomputed COEFF_A/COEFF_B */
|
|
89
|
+
COEFF_A_OVER_COEFF_B: BigInt(
|
|
90
|
+
'22511181562295907836254750456843438087744031914659733450388350895537307167857',
|
|
91
|
+
),
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
/** KZG Polynomial Commitment Scheme parameters (from bandersnatch-vrf-spec) */
|
|
95
|
+
KZG_CONFIG: {
|
|
96
|
+
/** Polynomial domain generator ω */
|
|
97
|
+
DOMAIN_GENERATOR: BigInt(
|
|
98
|
+
'49307615728544765012166121802278658070711169839041683575071795236746050763237',
|
|
99
|
+
),
|
|
100
|
+
/** Domain size |𝔻| = 2048 (2^11) */
|
|
101
|
+
DOMAIN_SIZE: 2048,
|
|
102
|
+
/** Maximum ring size (domain_size / 2 - 1) */
|
|
103
|
+
MAX_RING_SIZE: 1023,
|
|
104
|
+
/** SRS source identifier */
|
|
105
|
+
SRS_SOURCE: 'zcash-powers-of-tau-ceremony',
|
|
106
|
+
/** Required SRS degree (for domain size 2048) */
|
|
107
|
+
SRS_DEGREE: 11, // 2^11 = 2048
|
|
108
|
+
/** BLS12-381 curve identifier for c-kzg compatibility */
|
|
109
|
+
CURVE_ID: 'BLS12-381',
|
|
110
|
+
},
|
|
111
|
+
} as const
|
package/src/curve.ts
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bandersnatch Curve Implementation using @noble/curves
|
|
3
|
+
*
|
|
4
|
+
* This uses the @noble/curves Twisted Edwards implementation as a base
|
|
5
|
+
* and customizes it for Bandersnatch parameters
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type EdwardsPoint, edwards } from '@noble/curves/abstract/edwards.js'
|
|
9
|
+
import { Field } from '@noble/curves/abstract/modular.js'
|
|
10
|
+
import { BANDERSNATCH_PARAMS } from './config'
|
|
11
|
+
import { mod, modInverse, modSqrt } from './math'
|
|
12
|
+
|
|
13
|
+
// Elligator2 hash-to-curve moved to bandersnatch-vrf package
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Bandersnatch curve parameters for @noble/curves
|
|
17
|
+
*/
|
|
18
|
+
const BANDERSNATCH_CURVE = {
|
|
19
|
+
// Field modulus (BLS12-381 scalar field)
|
|
20
|
+
p: BANDERSNATCH_PARAMS.FIELD_MODULUS,
|
|
21
|
+
|
|
22
|
+
// Curve order
|
|
23
|
+
n: BANDERSNATCH_PARAMS.CURVE_ORDER,
|
|
24
|
+
|
|
25
|
+
// Cofactor
|
|
26
|
+
h: BANDERSNATCH_PARAMS.COFACTOR,
|
|
27
|
+
|
|
28
|
+
// Twisted Edwards coefficients
|
|
29
|
+
a:
|
|
30
|
+
BANDERSNATCH_PARAMS.CURVE_COEFFICIENTS.a +
|
|
31
|
+
BANDERSNATCH_PARAMS.FIELD_MODULUS, // Convert -5 to positive
|
|
32
|
+
d: BANDERSNATCH_PARAMS.CURVE_COEFFICIENTS.d,
|
|
33
|
+
|
|
34
|
+
// Generator point
|
|
35
|
+
Gx: BANDERSNATCH_PARAMS.GENERATOR.x,
|
|
36
|
+
Gy: BANDERSNATCH_PARAMS.GENERATOR.y,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create Bandersnatch curve using @noble/curves
|
|
41
|
+
*/
|
|
42
|
+
export const Bandersnatch = edwards(BANDERSNATCH_CURVE)
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Bandersnatch curve operations using @noble/curves.
|
|
46
|
+
*
|
|
47
|
+
* This class provides a high-level interface for working with points on the
|
|
48
|
+
* Bandersnatch elliptic curve. It implements all standard elliptic curve operations
|
|
49
|
+
* including point addition, scalar multiplication, and point compression/decompression
|
|
50
|
+
* compatible with arkworks serialization format.
|
|
51
|
+
*
|
|
52
|
+
* The Bandersnatch curve is a Twisted Edwards curve defined over the BLS12-381 scalar field,
|
|
53
|
+
* designed for efficient cryptographic operations in the JAM protocol.
|
|
54
|
+
*/
|
|
55
|
+
export class BandersnatchCurve {
|
|
56
|
+
static Fp = Field(BANDERSNATCH_PARAMS.FIELD_MODULUS)
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Convert Noble EdwardsPoint to arkworks-compatible compressed bytes
|
|
60
|
+
*
|
|
61
|
+
* This implements the exact arkworks Twisted Edwards point compression algorithm
|
|
62
|
+
* (Compress::Yes mode):
|
|
63
|
+
* 1. Extract affine coordinates (x, y) from the point
|
|
64
|
+
* 2. Determine x-coordinate sign using TEFlags::from_x_coordinate logic
|
|
65
|
+
* 3. Serialize y-coordinate in little-endian format (32 bytes)
|
|
66
|
+
* 4. Encode x-coordinate sign in the MSB (bit 7) of the last byte
|
|
67
|
+
*
|
|
68
|
+
* This always uses compressed form (32 bytes) as required by the Bandersnatch VRF spec
|
|
69
|
+
* section 2.1 for `point_to_string` function. This matches arkworks' `Compress::Yes` mode.
|
|
70
|
+
*
|
|
71
|
+
* Reference: arkworks-algebra/ec/src/models/twisted_edwards/serialization_flags.rs
|
|
72
|
+
* Reference: arkworks-algebra/ec/src/models/twisted_edwards/mod.rs (serialize_with_mode)
|
|
73
|
+
*
|
|
74
|
+
* @param noblePoint - Noble EdwardsPoint to compress
|
|
75
|
+
* @returns Compressed point bytes (32 bytes, arkworks-compatible, compressed format)
|
|
76
|
+
*/
|
|
77
|
+
static pointToBytes(noblePoint: EdwardsPoint): Uint8Array {
|
|
78
|
+
const { x, y } = noblePoint.toAffine()
|
|
79
|
+
// Fp.toBytes() allows non-canonical encoding of y (>= p).
|
|
80
|
+
const bytes = Bandersnatch.Fp.toBytes(y)
|
|
81
|
+
// Each y has 2 valid points: (x, y), (x,-y).
|
|
82
|
+
// When compressing, it's enough to store y and use the last byte to encode sign of x
|
|
83
|
+
// Use arkworks TEFlags logic: x > -x determines sign bit
|
|
84
|
+
const negX = mod(
|
|
85
|
+
BANDERSNATCH_PARAMS.FIELD_MODULUS - x,
|
|
86
|
+
BANDERSNATCH_PARAMS.FIELD_MODULUS,
|
|
87
|
+
)
|
|
88
|
+
const xIsNegative = x > negX // TEFlags::XIsNegative if x > -x
|
|
89
|
+
bytes[bytes.length - 1] |= xIsNegative ? 0x80 : 0
|
|
90
|
+
return bytes
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Decompresses arkworks-compatible point bytes to a Noble EdwardsPoint.
|
|
95
|
+
*
|
|
96
|
+
* This is the inverse operation of `pointToBytes`. It handles arkworks sign bit logic
|
|
97
|
+
* to reconstruct the full point from compressed bytes. The method:
|
|
98
|
+
* 1. Extracts the y-coordinate from the first 31 bytes (little-endian)
|
|
99
|
+
* 2. Extracts the x-coordinate sign from bit 7 of the last byte
|
|
100
|
+
* 3. Computes the x-coordinate from y using the curve equation
|
|
101
|
+
* 4. Validates the point is in the prime subgroup G
|
|
102
|
+
*
|
|
103
|
+
* @param bytes - Compressed point bytes in arkworks format (32 bytes)
|
|
104
|
+
* @returns Decompressed Noble EdwardsPoint
|
|
105
|
+
* @throws {Error} If the byte array length is not 32
|
|
106
|
+
* @throws {Error} If the y-coordinate exceeds the field modulus
|
|
107
|
+
* @throws {Error} If the point is not on the curve (no square root exists)
|
|
108
|
+
* @throws {Error} If the point is not in the prime subgroup G
|
|
109
|
+
*/
|
|
110
|
+
static bytesToPoint(bytes: Uint8Array): EdwardsPoint {
|
|
111
|
+
if (bytes.length !== 32) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Invalid compressed point length: ${bytes.length}, expected 32`,
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Extract sign bit (bit 7 of last byte) - arkworks TEFlags format
|
|
118
|
+
const lastByte = bytes[31]
|
|
119
|
+
const signBit = (lastByte & 0x80) !== 0
|
|
120
|
+
|
|
121
|
+
// Clear sign bit to get pure y-coordinate
|
|
122
|
+
const yBytes = new Uint8Array(bytes)
|
|
123
|
+
yBytes[31] = lastByte & 0x7f
|
|
124
|
+
|
|
125
|
+
// Convert little-endian y-coordinate to bigint
|
|
126
|
+
let y = 0n
|
|
127
|
+
for (let i = 0; i < 32; i++) {
|
|
128
|
+
y += BigInt(yBytes[i]) << (8n * BigInt(i))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Validate y is in field
|
|
132
|
+
if (y >= BANDERSNATCH_PARAMS.FIELD_MODULUS) {
|
|
133
|
+
throw new Error('Invalid y-coordinate: exceeds field modulus')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Calculate x from y using curve equation: a*x^2 + y^2 = 1 + d*x^2*y^2
|
|
137
|
+
// Rearranged: x^2 = (y^2 - 1) / (d*y^2 - a)
|
|
138
|
+
const { a, d } = BANDERSNATCH_PARAMS.CURVE_COEFFICIENTS
|
|
139
|
+
const p = BANDERSNATCH_PARAMS.FIELD_MODULUS
|
|
140
|
+
|
|
141
|
+
const y2 = mod(y * y, p)
|
|
142
|
+
const numerator = mod(y2 - 1n, p)
|
|
143
|
+
const denominator = mod(d * y2 - a, p)
|
|
144
|
+
|
|
145
|
+
// Calculate modular inverse of denominator
|
|
146
|
+
const denominatorInv = modInverse(denominator, p)
|
|
147
|
+
const x2 = mod(numerator * denominatorInv, p)
|
|
148
|
+
|
|
149
|
+
// Calculate square root
|
|
150
|
+
const x = modSqrt(x2, p, Bandersnatch.Fp)
|
|
151
|
+
if (x === null) {
|
|
152
|
+
throw new Error('Point is not on curve: no square root exists')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Apply arkworks sign bit logic
|
|
156
|
+
// Rust: if flags.is_negative() { (neg_x, y) } else { (x, y) }
|
|
157
|
+
// The flag is set (signBit = true) when the original x satisfied x > -x (XIsNegative)
|
|
158
|
+
// We computed x from y, but we need to determine which of the two possible x values
|
|
159
|
+
// matches the flag. The flag tells us which x was originally used.
|
|
160
|
+
const negX = mod(p - x, p)
|
|
161
|
+
const xIsNegative = x > negX // Check if computed x satisfies x > -x
|
|
162
|
+
|
|
163
|
+
// Choose correct x based on sign bit
|
|
164
|
+
// If signBit matches xIsNegative, use x; otherwise use negX
|
|
165
|
+
const finalX = signBit === xIsNegative ? x : negX
|
|
166
|
+
|
|
167
|
+
// Create Noble point from affine coordinates
|
|
168
|
+
const point = Bandersnatch.fromAffine({ x: finalX, y })
|
|
169
|
+
|
|
170
|
+
// Validate point is in prime subgroup as required by bandersnatch-vrf-spec section 2.1:
|
|
171
|
+
// "This function MUST outputs 'INVALID' if the octet-string does not decode
|
|
172
|
+
// to a point on the prime subgroup G"
|
|
173
|
+
// A point is in the prime subgroup if and only if multiplying by the curve order
|
|
174
|
+
// gives the identity point (infinity)
|
|
175
|
+
// Since @noble/curves requires 1 <= scalar < curve.n, we use CURVE_ORDER - 1
|
|
176
|
+
// and then add the point once more: point * CURVE_ORDER = point * (CURVE_ORDER - 1) + point
|
|
177
|
+
const curveOrderMinusOne = BANDERSNATCH_PARAMS.CURVE_ORDER - 1n
|
|
178
|
+
const pointTimesOrderMinusOne = this.scalarMultiply(
|
|
179
|
+
point,
|
|
180
|
+
curveOrderMinusOne,
|
|
181
|
+
)
|
|
182
|
+
const pointTimesOrder = this.add(pointTimesOrderMinusOne, point)
|
|
183
|
+
const isInPrimeSubgroup = pointTimesOrder.equals(Bandersnatch.ZERO)
|
|
184
|
+
|
|
185
|
+
if (!isInPrimeSubgroup) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
'Point is not in prime subgroup: decoded point is not in G',
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return point
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Performs scalar multiplication on a curve point.
|
|
196
|
+
*
|
|
197
|
+
* Computes `scalar * point` on the Bandersnatch curve. Handles edge cases including:
|
|
198
|
+
* - Scalar 0: returns the identity point (infinity)
|
|
199
|
+
* - Negative scalars: negates the point and uses positive scalar
|
|
200
|
+
* - Scalars >= curve order: reduces modulo curve order
|
|
201
|
+
*
|
|
202
|
+
* @param point - The curve point to multiply
|
|
203
|
+
* @param scalar - The scalar multiplier (can be negative or >= curve order)
|
|
204
|
+
* @returns The result of scalar multiplication: `scalar * point`
|
|
205
|
+
* @throws {Error} If the point is invalid or not on the curve
|
|
206
|
+
*/
|
|
207
|
+
static scalarMultiply(point: EdwardsPoint, scalar: bigint): EdwardsPoint {
|
|
208
|
+
// Handle scalar 0: return identity point
|
|
209
|
+
if (scalar === 0n) {
|
|
210
|
+
return Bandersnatch.ZERO
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Handle negative scalars: negate point and use positive scalar
|
|
214
|
+
if (scalar < 0n) {
|
|
215
|
+
const negPoint = point.negate()
|
|
216
|
+
const positiveScalar = -scalar
|
|
217
|
+
// Reduce modulo curve order
|
|
218
|
+
const reducedScalar = positiveScalar % BANDERSNATCH_PARAMS.CURVE_ORDER
|
|
219
|
+
if (reducedScalar === 0n) {
|
|
220
|
+
return Bandersnatch.ZERO
|
|
221
|
+
}
|
|
222
|
+
return negPoint.multiply(reducedScalar)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Reduce scalar modulo curve order if it's >= curve order
|
|
226
|
+
// @noble/curves requires 1 <= scalar < curve.n
|
|
227
|
+
const reducedScalar = scalar % BANDERSNATCH_PARAMS.CURVE_ORDER
|
|
228
|
+
if (reducedScalar === 0n) {
|
|
229
|
+
return Bandersnatch.ZERO
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return point.multiply(reducedScalar)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Adds two curve points together.
|
|
237
|
+
*
|
|
238
|
+
* Performs point addition on the Bandersnatch curve: `P + Q`.
|
|
239
|
+
* This operation is commutative: `add(P, Q) === add(Q, P)`.
|
|
240
|
+
*
|
|
241
|
+
* @param p1 - First curve point
|
|
242
|
+
* @param p2 - Second curve point
|
|
243
|
+
* @returns The sum of the two points: `p1 + p2`
|
|
244
|
+
* @throws {Error} If either point is invalid or not on the curve
|
|
245
|
+
*/
|
|
246
|
+
static add(p1: EdwardsPoint, p2: EdwardsPoint): EdwardsPoint {
|
|
247
|
+
return p1.add(p2)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Doubles a curve point.
|
|
252
|
+
*
|
|
253
|
+
* Computes `2 * point` on the Bandersnatch curve. This is equivalent to
|
|
254
|
+
* `add(point, point)` but is typically more efficient.
|
|
255
|
+
*
|
|
256
|
+
* @param point - The curve point to double
|
|
257
|
+
* @returns The doubled point: `2 * point`
|
|
258
|
+
* @throws {Error} If the point is invalid or not on the curve
|
|
259
|
+
*/
|
|
260
|
+
static double(point: EdwardsPoint): EdwardsPoint {
|
|
261
|
+
return point.double()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Negates a curve point.
|
|
266
|
+
*
|
|
267
|
+
* Computes the additive inverse of a point on the Bandersnatch curve.
|
|
268
|
+
* The result satisfies: `add(point, negate(point)) === INFINITY`.
|
|
269
|
+
*
|
|
270
|
+
* @param point - The curve point to negate
|
|
271
|
+
* @returns The negated point: `-point`
|
|
272
|
+
* @throws {Error} If the point is invalid or not on the curve
|
|
273
|
+
*/
|
|
274
|
+
static negate(point: EdwardsPoint): EdwardsPoint {
|
|
275
|
+
return point.negate()
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Checks if a point lies on the Bandersnatch curve.
|
|
280
|
+
*
|
|
281
|
+
* Validates that the point satisfies the Twisted Edwards curve equation:
|
|
282
|
+
* `a*x^2 + y^2 = 1 + d*x^2*y^2` where `a = -5` and `d` is the curve parameter.
|
|
283
|
+
*
|
|
284
|
+
* @param point - The curve point to validate
|
|
285
|
+
* @returns `true` if the point is on the curve, `false` otherwise
|
|
286
|
+
*/
|
|
287
|
+
static isOnCurve(point: EdwardsPoint): boolean {
|
|
288
|
+
return point.isTorsionFree()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Gets the generator point (base point) of the Bandersnatch curve.
|
|
293
|
+
*
|
|
294
|
+
* The generator is a point on the curve that generates the prime subgroup G.
|
|
295
|
+
* All points in the prime subgroup can be expressed as scalar multiples of the generator.
|
|
296
|
+
*
|
|
297
|
+
* @returns The generator point G
|
|
298
|
+
*/
|
|
299
|
+
static get GENERATOR() {
|
|
300
|
+
return Bandersnatch.BASE
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Gets the identity point (point at infinity) of the Bandersnatch curve.
|
|
305
|
+
*
|
|
306
|
+
* The identity point is the neutral element for point addition:
|
|
307
|
+
* `add(point, INFINITY) === point` for any point on the curve.
|
|
308
|
+
*
|
|
309
|
+
* @returns The identity point (point at infinity)
|
|
310
|
+
*/
|
|
311
|
+
static get INFINITY() {
|
|
312
|
+
return Bandersnatch.ZERO
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Converts a curve point to its byte representation.
|
|
317
|
+
*
|
|
318
|
+
* Serializes the point to bytes, typically used for challenge generation
|
|
319
|
+
* in cryptographic protocols. The output format matches the point compression
|
|
320
|
+
* format used by the curve implementation.
|
|
321
|
+
*
|
|
322
|
+
* @param point - The curve point to hash
|
|
323
|
+
* @returns Byte representation of the point
|
|
324
|
+
* @throws {Error} If the point is invalid
|
|
325
|
+
*/
|
|
326
|
+
static hashPoint(point: EdwardsPoint): Uint8Array {
|
|
327
|
+
return point.toBytes()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Gets the order (cardinality) of the prime subgroup of the Bandersnatch curve.
|
|
332
|
+
*
|
|
333
|
+
* The curve order is the number of points in the prime subgroup G.
|
|
334
|
+
* For any point P in G, `scalarMultiply(P, CURVE_ORDER) === INFINITY`.
|
|
335
|
+
*
|
|
336
|
+
* @returns The curve order as a bigint
|
|
337
|
+
*/
|
|
338
|
+
static get CURVE_ORDER() {
|
|
339
|
+
return BANDERSNATCH_PARAMS.CURVE_ORDER
|
|
340
|
+
}
|
|
341
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bandersnatch Curve Package
|
|
3
|
+
*
|
|
4
|
+
* This package provides the core Bandersnatch elliptic curve implementation
|
|
5
|
+
* with all necessary operations for cryptographic applications.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Export types
|
|
9
|
+
export type { CurvePoint } from './types'
|
|
10
|
+
// Export curve parameters
|
|
11
|
+
export { BANDERSNATCH_PARAMS } from './config'
|
|
12
|
+
// Elligator2 hash-to-curve is now available in bandersnatch-vrf package
|
|
13
|
+
// Export curve implementations
|
|
14
|
+
// Temporary alias for legacy compatibility
|
|
15
|
+
export {
|
|
16
|
+
BandersnatchCurve,
|
|
17
|
+
Bandersnatch,
|
|
18
|
+
} from './curve'
|
|
19
|
+
// VRF functionality moved to bandersnatch-vrf package
|
|
20
|
+
export * from './math'
|
package/src/math.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modular arithmetic utilities for Bandersnatch curve operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
FpIsSquare,
|
|
7
|
+
FpSqrt,
|
|
8
|
+
type IField,
|
|
9
|
+
} from '@noble/curves/abstract/modular.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Computes the non-negative modular reduction.
|
|
13
|
+
*
|
|
14
|
+
* Returns `a mod m` where the result is always non-negative (0 <= result < m).
|
|
15
|
+
* This handles negative inputs correctly by adding the modulus to negative results.
|
|
16
|
+
*
|
|
17
|
+
* @param a - The value to reduce
|
|
18
|
+
* @param m - The modulus
|
|
19
|
+
* @returns Non-negative result of `a mod m`
|
|
20
|
+
*/
|
|
21
|
+
export function mod(a: bigint, m: bigint): bigint {
|
|
22
|
+
const result = a % m
|
|
23
|
+
return result < 0n ? result + m : result
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Computes the modular inverse using the extended Euclidean algorithm.
|
|
28
|
+
*
|
|
29
|
+
* Finds the value `x` such that `(a * x) mod m = 1`. The modular inverse exists
|
|
30
|
+
* if and only if `gcd(a, m) = 1`.
|
|
31
|
+
*
|
|
32
|
+
* @param a - The value to find the inverse of
|
|
33
|
+
* @param m - The modulus
|
|
34
|
+
* @returns The modular inverse of `a` modulo `m`
|
|
35
|
+
* @throws {Error} If the modular inverse does not exist (gcd(a, m) !== 1)
|
|
36
|
+
*/
|
|
37
|
+
export function modInverse(a: bigint, m: bigint): bigint {
|
|
38
|
+
let [oldR, r] = [a, m]
|
|
39
|
+
let [oldS, s] = [1n, 0n]
|
|
40
|
+
|
|
41
|
+
while (r !== 0n) {
|
|
42
|
+
const quotient = oldR / r
|
|
43
|
+
;[oldR, r] = [r, oldR - quotient * r]
|
|
44
|
+
;[oldS, s] = [s, oldS - quotient * s]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (oldR > 1n) {
|
|
48
|
+
throw new Error('Modular inverse does not exist')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return oldS < 0n ? oldS + m : oldS
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Computes the modular square root using the noble package FpSqrt.
|
|
56
|
+
*
|
|
57
|
+
* Finds a value `x` such that `x^2 mod p = value`. The square root exists
|
|
58
|
+
* if and only if `value` is a quadratic residue modulo `p`.
|
|
59
|
+
*
|
|
60
|
+
* @param value - The value to find the square root of
|
|
61
|
+
* @param p - The prime modulus
|
|
62
|
+
* @param field - The field implementation from @noble/curves
|
|
63
|
+
* @returns The modular square root if it exists, or `null` if the value is not a quadratic residue
|
|
64
|
+
*/
|
|
65
|
+
export function modSqrt(
|
|
66
|
+
value: bigint,
|
|
67
|
+
p: bigint,
|
|
68
|
+
field: IField<bigint>,
|
|
69
|
+
): bigint | null {
|
|
70
|
+
if (value === 0n) return 0n
|
|
71
|
+
if (value === 1n) return 1n
|
|
72
|
+
|
|
73
|
+
// Check if value is a quadratic residue using noble package
|
|
74
|
+
if (!FpIsSquare(field, value)) {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Use noble package FpSqrt for modular square root
|
|
79
|
+
const sqrtFn = FpSqrt(p)
|
|
80
|
+
return sqrtFn(field, value)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function numberToBytesLittleEndian(value: bigint): Uint8Array {
|
|
84
|
+
const bytes = new Uint8Array(32)
|
|
85
|
+
const hex = value.toString(16).padStart(64, '0')
|
|
86
|
+
for (let i = 0; i < 32; i++) {
|
|
87
|
+
bytes[i] = Number.parseInt(hex.slice(62 - i * 2, 64 - i * 2), 16)
|
|
88
|
+
}
|
|
89
|
+
return bytes
|
|
90
|
+
}
|
|
91
|
+
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for Bandersnatch curve operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Represents a point on an elliptic curve.
|
|
7
|
+
*
|
|
8
|
+
* This interface defines the structure for curve points used in Bandersnatch
|
|
9
|
+
* curve operations. Points can be either finite points with (x, y) coordinates
|
|
10
|
+
* or the point at infinity.
|
|
11
|
+
*
|
|
12
|
+
* @interface CurvePoint
|
|
13
|
+
*/
|
|
14
|
+
export interface CurvePoint {
|
|
15
|
+
/** X-coordinate of the point (undefined for point at infinity) */
|
|
16
|
+
x: bigint
|
|
17
|
+
/** Y-coordinate of the point (undefined for point at infinity) */
|
|
18
|
+
y: bigint
|
|
19
|
+
/** Whether this point is the point at infinity (identity element) */
|
|
20
|
+
isInfinity: boolean
|
|
21
|
+
}
|
|
22
|
+
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"display": "Default",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"lib": ["ESNext"],
|
|
6
|
+
"target": "ESNext",
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"moduleDetection": "force",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
|
|
16
|
+
"strict": true,
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"noFallthroughCasesInSwitch": true,
|
|
19
|
+
|
|
20
|
+
"noUnusedLocals": true,
|
|
21
|
+
"noUnusedParameters": true,
|
|
22
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
23
|
+
"types": ["bun-types", "node"]
|
|
24
|
+
},
|
|
25
|
+
"exclude": [
|
|
26
|
+
"**/__tests__/**",
|
|
27
|
+
"**/scripts/**",
|
|
28
|
+
"**/*.test.ts",
|
|
29
|
+
"**/*.spec.ts"
|
|
30
|
+
]
|
|
31
|
+
}
|