@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.
@@ -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
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "./tsconfig.json",
4
+ "compilerOptions": {
5
+ "types": [],
6
+ "skipLibCheck": true,
7
+ "noEmit": false,
8
+ "moduleResolution": "node"
9
+ }
10
+ }
11
+