From a34d4fb303f7f5d6bd0f3174b6e03aa42e8f25ae Mon Sep 17 00:00:00 2001 From: Rob Woodgate Date: Thu, 21 May 2026 23:53:50 +0200 Subject: [PATCH] refactor(crypto): split curve primitives --- migration-5.0.0.md | 21 ++++ src/crypto/NUT01.ts | 27 +---- src/crypto/NUT12.ts | 3 +- src/crypto/NUT13.ts | 4 +- src/crypto/NUT28.ts | 2 +- src/crypto/core.ts | 162 +--------------------------- src/crypto/{bls.ts => curve_bls.ts} | 15 +++ src/crypto/curve_secp.ts | 112 +++++++++++++++++++ src/crypto/curves.ts | 65 +++++++++++ src/crypto/index.ts | 4 +- src/model/BlindedMessage.ts | 2 +- test/crypto/bls.test.ts | 2 +- test/model/OutputData.bls.test.ts | 2 +- test/utils/core.test.ts | 4 +- 14 files changed, 232 insertions(+), 193 deletions(-) rename src/crypto/{bls.ts => curve_bls.ts} (95%) create mode 100644 src/crypto/curve_secp.ts create mode 100644 src/crypto/curves.ts diff --git a/migration-5.0.0.md b/migration-5.0.0.md index 1a8bc9015..43def70b5 100644 --- a/migration-5.0.0.md +++ b/migration-5.0.0.md @@ -12,6 +12,27 @@ Existing v0 (legacy base64), v1 (`00…`), and v2 (`01…`) keysets continue to --- +## Crypto deep imports were reorganized + +The internal crypto module layout changed to separate curve-specific primitives from shared +coordination code. The new curve files are implementation modules, not stable import targets. If +you were relying on existing file-level imports such as `crypto/core`, move those imports to the +public package entry point. + +Use the public package entry point instead: + +```ts +// Before +import { blindMessage, hashToCurve } from '@cashu/cashu-ts/crypto/core'; + +// After +import { blindMessage, hashToCurve } from '@cashu/cashu-ts'; +``` + +If you were relying on a symbol that is not exported by the package entry point in v5, treat it as internal implementation detail and open an issue before depending on it. + +--- + ## `checkProofsStates` now requires `id` on every proof `Wallet.checkProofsStates` previously accepted `Array>` — only `secret` was required. v5 requires both `id` and `secret`: `Array>`. diff --git a/src/crypto/NUT01.ts b/src/crypto/NUT01.ts index e2726686f..b47f9be1b 100644 --- a/src/crypto/NUT01.ts +++ b/src/crypto/NUT01.ts @@ -8,8 +8,10 @@ import { HDKey } from '@scure/bip32'; import { CTSError } from '../model/Errors'; import { deriveKeysetId } from '../utils'; -import { BLS_G2_GENERATOR, hashToCurveBls } from './bls'; -import { type UnblindedSignature, createRandomSecretKey, hashToCurve, isBlsKeyset } from './core'; +import { type UnblindedSignature } from './core'; +import { getG2PubKeyFromPrivKey, hashToCurveBls } from './curve_bls'; +import { createRandomSecretKey, getPubKeyFromPrivKey, hashToCurve } from './curve_secp'; +import { isBlsKeyset } from './curves'; const DERIVATION_PATH = "m/0'/0'/0'"; @@ -47,25 +49,6 @@ export function deserializeMintKeys(serializedMintKeys: SerializedMintKeys): Raw return mintKeys; } -export function getPubKeyFromPrivKey(privKey: Uint8Array): Uint8Array { - return secp256k1.getPublicKey(privKey, true); -} - -/** - * V3 (BLS) mint pubkey: K2 = a · G2_gen, compressed to 96 bytes. - * - * The 32-byte private key is interpreted as a big-endian scalar and reduced mod the BLS Fr order - * (same convention as the mint-side blind signer for v3). - */ -export function getG2PubKeyFromPrivKey(privKey: Uint8Array): Uint8Array { - const a = bls12_381.fields.Fr.fromBytes(privKey); - /* c8 ignore next 3 — defensive guard; a==0 requires all-zero privKey bytes (impossible in practice). */ - if (a === 0n) { - throw new CTSError('Mint scalar must be non-zero'); - } - return BLS_G2_GENERATOR.multiply(a).toBytes(true); -} - /** * Creates new mint keys. * @@ -137,7 +120,7 @@ export function createNewMintKeys( * * @remarks * Dispatches by keyset version. v0/v1/v2 keysets use secp256k1; v3 keysets use BLS12-381 G1. The - * wallet-side pairing equivalent for v3 is {@link verifyUnblindedSignatureBls} in `./bls`. + * wallet-side pairing equivalent for v3 is {@link verifyUnblindedSignatureBls} in `./curve_bls`. */ export function verifyUnblindedSignature(proof: UnblindedSignature, privKey: Uint8Array): boolean { if (isBlsKeyset(proof.id)) { diff --git a/src/crypto/NUT12.ts b/src/crypto/NUT12.ts index 1312488e4..ca3d5c40a 100644 --- a/src/crypto/NUT12.ts +++ b/src/crypto/NUT12.ts @@ -8,7 +8,8 @@ import { concatBytes, utf8ToBytes } from '@noble/hashes/utils.js'; import { CTSError } from '../model/Errors'; import { Bytes } from '../utils'; -import { type DLEQ, hash_e, hashToCurve } from './core'; +import { type DLEQ } from './core'; +import { hash_e, hashToCurve } from './curve_secp'; const DST_R = utf8ToBytes('Cashu_DLEQ_R_v1'); const SECP256K1_N = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141'); diff --git a/src/crypto/NUT13.ts b/src/crypto/NUT13.ts index 4042b0671..b4e9644c1 100644 --- a/src/crypto/NUT13.ts +++ b/src/crypto/NUT13.ts @@ -6,8 +6,8 @@ import { HDKey } from '@scure/bip32'; import { CTSError } from '../model/Errors'; import { Bytes, isBase64String } from '../utils'; -import { BLS_FR_ORDER } from './bls'; -import { getKeysetIdInt, isBlsKeyset } from './core'; +import { BLS_FR_ORDER } from './curve_bls'; +import { getKeysetIdInt, isBlsKeyset } from './curves'; const STANDARD_DERIVATION_PATH = `m/129372'/0'`; diff --git a/src/crypto/NUT28.ts b/src/crypto/NUT28.ts index 3bbe3bf13..0838a9b6e 100644 --- a/src/crypto/NUT28.ts +++ b/src/crypto/NUT28.ts @@ -6,7 +6,7 @@ import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/hashes/utils.js'; import { CTSError } from '../model/Errors'; import { Bytes, hexToNumber, numberToHexPadded64 } from '../utils'; -import { pointFromHex } from './core'; +import { pointFromHex } from './curve_secp'; /** * BIP340-style domain separation tag (DST) for P2BK. diff --git a/src/crypto/core.ts b/src/crypto/core.ts index 8b138dd27..61e20ccab 100644 --- a/src/crypto/core.ts +++ b/src/crypto/core.ts @@ -1,13 +1,9 @@ import { type WeierstrassPoint } from '@noble/curves/abstract/weierstrass.js'; import { schnorr, secp256k1 } from '@noble/curves/secp256k1.js'; -import { randomBytes, bytesToHex, hexToBytes } from '@noble/curves/utils.js'; +import { bytesToHex, hexToBytes } from '@noble/curves/utils.js'; import { sha256 } from '@noble/hashes/sha2.js'; -import { utf8ToBytes } from '@noble/hashes/utils.js'; import { CTSError } from '../model/Errors'; -import { Bytes, hexToNumber, encodeBase64toUint8, isValidHex } from '../utils'; - -import { type G1Point, pointFromHexG1 } from './bls'; /** * Private key type - can be hex string or Uint8Array. @@ -37,162 +33,6 @@ export type UnblindedSignature = { id: string; }; -const DOMAIN_SEPARATOR = utf8ToBytes('Secp256k1_HashToCurve_Cashu_'); - -export function hashToCurve(secret: Uint8Array): WeierstrassPoint { - const msgToHash = sha256(Bytes.concat(DOMAIN_SEPARATOR, secret)); - const counter = new Uint32Array(1); - const maxIterations = 2 ** 16; - for (let i = 0; i < maxIterations; i++) { - const counterBytes = new Uint8Array(counter.buffer); - const hash = sha256(Bytes.concat(msgToHash, counterBytes)); - try { - return pointFromHex(bytesToHex(Bytes.concat(new Uint8Array([0x02]), hash))); - } catch { - counter[0]++; - } - } - throw new CTSError('No valid point found'); -} - -export function hash_e(pubkeys: Array>): Uint8Array { - const hexStrings = pubkeys.map((p) => p.toHex(false)); - const e_ = hexStrings.join(''); - return sha256(new TextEncoder().encode(e_)); -} - -export function pointFromBytes(bytes: Uint8Array) { - return secp256k1.Point.fromHex(bytesToHex(bytes)); -} - -export function pointFromHex(hex: string) { - return secp256k1.Point.fromHex(hex); -} - -/** - * Tagged-union point covering both keyset curves on the wallet output / proof path. - * - * - `secp`: secp256k1 compressed point (33 bytes, 66 hex) — v0/v1/v2 keysets. - * - `blsG1`: BLS12-381 G1 compressed point (48 bytes, 96 hex) — v3 keysets. - */ -export type CurvePoint = - | { kind: 'secp'; pt: WeierstrassPoint } - | { kind: 'blsG1'; pt: G1Point }; - -export function asSecpPoint(pt: WeierstrassPoint): CurvePoint { - return { kind: 'secp', pt }; -} - -export function asBlsG1Point(pt: G1Point): CurvePoint { - return { kind: 'blsG1', pt }; -} - -/** - * Decode a compressed point hex string to a {@link CurvePoint}, picking the curve by length: 66 hex - * chars → secp256k1, 96 hex chars → BLS12-381 G1. - * - * Lengths are disjoint across the supported curves (secp uncompressed is 130; G2 compressed is - * 192), so there is no ambiguity. - */ -export function pointFromHexAuto(hex: string): CurvePoint { - if (hex.length === 66) return { kind: 'secp', pt: secp256k1.Point.fromHex(hex) }; - if (hex.length === 96) return { kind: 'blsG1', pt: pointFromHexG1(hex) }; - throw new CTSError(`Cannot decode point: unexpected hex length ${hex.length}`); -} - -export function pointToHex(p: CurvePoint): string { - return p.pt.toHex(true); -} - -/** - * True if `keysetId` is a v3 BLS12-381 keyset id (modern hex, version byte 0x02). - * - * @remarks - * Strict version gate: does not assume future keyset versions are BLS. - */ -export function isBlsKeyset(keysetId: string): boolean { - if (keysetId.length !== 16 && keysetId.length !== 66) return false; - if (!isValidHex(keysetId)) return false; - return keysetId.startsWith('02'); -} - -export const getKeysetIdInt = (keysetId: string): bigint => { - let keysetIdInt: bigint; - if (/^[a-fA-F0-9]+$/.test(keysetId)) { - keysetIdInt = hexToNumber(keysetId) % BigInt(2 ** 31 - 1); - } else { - //legacy keyset compatibility - keysetIdInt = Bytes.toBigInt(encodeBase64toUint8(keysetId)) % BigInt(2 ** 31 - 1); - } - return keysetIdInt; -}; - -export function createRandomSecretKey(): Uint8Array { - return secp256k1.utils.randomSecretKey(); -} - -export function createBlindSignature( - B_: WeierstrassPoint, - privateKey: Uint8Array, - id: string, -): BlindSignature { - const a = secp256k1.Point.Fn.fromBytes(privateKey); - const C_: WeierstrassPoint = B_.multiply(a); - return { C_, id }; -} - -/** - * Creates a random blinded message. - * - * @remarks - * The secret is a UTF-8 encoded 64-character lowercase hex string, generated from 32 random bytes - * as recommended by NUT-00. - * @returns A RawBlindedMessage: {B_, r, secret} - */ -export function createRandomRawBlindedMessage(): RawBlindedMessage { - const secretStr = bytesToHex(randomBytes(32)); // 64 char ASCII hex string - const secretBytes = new TextEncoder().encode(secretStr); // UTF-8 of the hex - return blindMessage(secretBytes); -} - -/** - * Blind a secret message. - * - * @param secret A UTF-8 byte encoded string. - * @param r Optional. Deterministic blinding scalar to use (eg: for testing / seeded) - * @returns A RawBlindedMessage: {B_, r, secret} - */ -export function blindMessage(secret: Uint8Array, r?: bigint): RawBlindedMessage { - const Y = hashToCurve(secret); - if (r === undefined) { - r = secp256k1.Point.Fn.fromBytes(createRandomSecretKey()); - } else if (r === 0n) { - throw new CTSError('Blinding factor r must be non-zero'); - } - const rG = secp256k1.Point.BASE.multiply(r); - const B_ = Y.add(rG); - return { B_, r, secret }; -} - -export function unblindSignature( - C_: WeierstrassPoint, - r: bigint, - A: WeierstrassPoint, -): WeierstrassPoint { - const C = C_.subtract(A.multiply(r)); - return C; -} - -export function constructUnblindedSignature( - blindSig: BlindSignature, - r: bigint, - secret: Uint8Array, - key: WeierstrassPoint, -): UnblindedSignature { - const C = unblindSignature(blindSig.C_, r, key); - return { id: blindSig.id, secret, C }; -} - // ------------------------------ // Schnorr Signing / Verification // ------------------------------ diff --git a/src/crypto/bls.ts b/src/crypto/curve_bls.ts similarity index 95% rename from src/crypto/bls.ts rename to src/crypto/curve_bls.ts index 343735273..1056024c9 100644 --- a/src/crypto/bls.ts +++ b/src/crypto/curve_bls.ts @@ -115,6 +115,21 @@ export function pointFromHexG2(hex: string): G2Point { return p; } +/** + * V3 (BLS) mint pubkey: K2 = a · G2_gen, compressed to 96 bytes. + * + * The 32-byte private key is interpreted as a big-endian scalar and reduced mod the BLS Fr order + * (same convention as the mint-side blind signer for v3). + */ +export function getG2PubKeyFromPrivKey(privKey: Uint8Array): Uint8Array { + const a = Fr.fromBytes(privKey); + /* c8 ignore next 3 — defensive guard; a==0 requires all-zero privKey bytes (impossible in practice). */ + if (a === 0n) { + throw new CTSError('Mint scalar must be non-zero'); + } + return BLS_G2_GENERATOR.multiply(a).toBytes(true); +} + function randomScalar(): bigint { // bls12_381's Fr.fromBytes accepts 32 bytes BE and reduces mod ORDER. return Fr.fromBytes(randomBytes(32)); diff --git a/src/crypto/curve_secp.ts b/src/crypto/curve_secp.ts new file mode 100644 index 000000000..14f9f128d --- /dev/null +++ b/src/crypto/curve_secp.ts @@ -0,0 +1,112 @@ +import { type WeierstrassPoint } from '@noble/curves/abstract/weierstrass.js'; +import { secp256k1 } from '@noble/curves/secp256k1.js'; +import { randomBytes, bytesToHex } from '@noble/curves/utils.js'; +import { sha256 } from '@noble/hashes/sha2.js'; +import { utf8ToBytes } from '@noble/hashes/utils.js'; + +import { CTSError } from '../model/Errors'; +import { Bytes } from '../utils'; + +import type { BlindSignature, RawBlindedMessage, UnblindedSignature } from './core'; + +const DOMAIN_SEPARATOR = utf8ToBytes('Secp256k1_HashToCurve_Cashu_'); + +export function hashToCurve(secret: Uint8Array): WeierstrassPoint { + const msgToHash = sha256(Bytes.concat(DOMAIN_SEPARATOR, secret)); + const counter = new Uint32Array(1); + const maxIterations = 2 ** 16; + for (let i = 0; i < maxIterations; i++) { + const counterBytes = new Uint8Array(counter.buffer); + const hash = sha256(Bytes.concat(msgToHash, counterBytes)); + try { + return pointFromHex(bytesToHex(Bytes.concat(new Uint8Array([0x02]), hash))); + } catch { + counter[0]++; + } + } + throw new CTSError('No valid point found'); +} + +export function hash_e(pubkeys: Array>): Uint8Array { + const hexStrings = pubkeys.map((p) => p.toHex(false)); + const e_ = hexStrings.join(''); + return sha256(new TextEncoder().encode(e_)); +} + +export function pointFromBytes(bytes: Uint8Array) { + return secp256k1.Point.fromHex(bytesToHex(bytes)); +} + +export function pointFromHex(hex: string) { + return secp256k1.Point.fromHex(hex); +} + +export function getPubKeyFromPrivKey(privKey: Uint8Array): Uint8Array { + return secp256k1.getPublicKey(privKey, true); +} + +export function createRandomSecretKey(): Uint8Array { + return secp256k1.utils.randomSecretKey(); +} + +export function createBlindSignature( + B_: WeierstrassPoint, + privateKey: Uint8Array, + id: string, +): BlindSignature { + const a = secp256k1.Point.Fn.fromBytes(privateKey); + const C_: WeierstrassPoint = B_.multiply(a); + return { C_, id }; +} + +/** + * Creates a random blinded message. + * + * @remarks + * The secret is a UTF-8 encoded 64-character lowercase hex string, generated from 32 random bytes + * as recommended by NUT-00. + * @returns A RawBlindedMessage: {B_, r, secret} + */ +export function createRandomRawBlindedMessage(): RawBlindedMessage { + const secretStr = bytesToHex(randomBytes(32)); // 64 char ASCII hex string + const secretBytes = new TextEncoder().encode(secretStr); // UTF-8 of the hex + return blindMessage(secretBytes); +} + +/** + * Blind a secret message. + * + * @param secret A UTF-8 byte encoded string. + * @param r Optional. Deterministic blinding scalar to use (eg: for testing / seeded) + * @returns A RawBlindedMessage: {B_, r, secret} + */ +export function blindMessage(secret: Uint8Array, r?: bigint): RawBlindedMessage { + const Y = hashToCurve(secret); + if (r === undefined) { + r = secp256k1.Point.Fn.fromBytes(createRandomSecretKey()); + } else if (r === 0n) { + throw new CTSError('Blinding factor r must be non-zero'); + } + const rG = secp256k1.Point.BASE.multiply(r); + const B_ = Y.add(rG); + return { B_, r, secret }; +} + +export function unblindSignature( + C_: WeierstrassPoint, + r: bigint, + A: WeierstrassPoint, +): WeierstrassPoint { + const C = C_.subtract(A.multiply(r)); + return C; +} + +export function constructUnblindedSignature( + blindSig: BlindSignature, + r: bigint, + secret: Uint8Array, + key: WeierstrassPoint, +): UnblindedSignature { + const C = unblindSignature(blindSig.C_, r, key); + return { id: blindSig.id, secret, C }; +} diff --git a/src/crypto/curves.ts b/src/crypto/curves.ts new file mode 100644 index 000000000..65e51e991 --- /dev/null +++ b/src/crypto/curves.ts @@ -0,0 +1,65 @@ +import { type WeierstrassPoint } from '@noble/curves/abstract/weierstrass.js'; +import { secp256k1 } from '@noble/curves/secp256k1.js'; + +import { CTSError } from '../model/Errors'; +import { Bytes, encodeBase64toUint8, hexToNumber, isValidHex } from '../utils'; + +import { type G1Point, pointFromHexG1 } from './curve_bls'; + +/** + * Tagged-union point covering both keyset curves on the wallet output / proof path. + * + * - `secp`: secp256k1 compressed point (33 bytes, 66 hex) — v0/v1/v2 keysets. + * - `blsG1`: BLS12-381 G1 compressed point (48 bytes, 96 hex) — v3 keysets. + */ +export type CurvePoint = + | { kind: 'secp'; pt: WeierstrassPoint } + | { kind: 'blsG1'; pt: G1Point }; + +export function asSecpPoint(pt: WeierstrassPoint): CurvePoint { + return { kind: 'secp', pt }; +} + +export function asBlsG1Point(pt: G1Point): CurvePoint { + return { kind: 'blsG1', pt }; +} + +/** + * Decode a compressed point hex string to a {@link CurvePoint}, picking the curve by length: 66 hex + * chars → secp256k1, 96 hex chars → BLS12-381 G1. + * + * Lengths are disjoint across the supported curves (secp uncompressed is 130; G2 compressed is + * 192), so there is no ambiguity. + */ +export function pointFromHexAuto(hex: string): CurvePoint { + if (hex.length === 66) return { kind: 'secp', pt: secp256k1.Point.fromHex(hex) }; + if (hex.length === 96) return { kind: 'blsG1', pt: pointFromHexG1(hex) }; + throw new CTSError(`Cannot decode point: unexpected hex length ${hex.length}`); +} + +export function pointToHex(p: CurvePoint): string { + return p.pt.toHex(true); +} + +/** + * True if `keysetId` is a v3 BLS12-381 keyset id (modern hex, version byte 0x02). + * + * @remarks + * Strict version gate: does not assume future keyset versions are BLS. + */ +export function isBlsKeyset(keysetId: string): boolean { + if (keysetId.length !== 16 && keysetId.length !== 66) return false; + if (!isValidHex(keysetId)) return false; + return keysetId.startsWith('02'); +} + +export const getKeysetIdInt = (keysetId: string): bigint => { + let keysetIdInt: bigint; + if (/^[a-fA-F0-9]+$/.test(keysetId)) { + keysetIdInt = hexToNumber(keysetId) % BigInt(2 ** 31 - 1); + } else { + //legacy keyset compatibility + keysetIdInt = Bytes.toBigInt(encodeBase64toUint8(keysetId)) % BigInt(2 ** 31 - 1); + } + return keysetIdInt; +}; diff --git a/src/crypto/index.ts b/src/crypto/index.ts index a5a2f2112..544877468 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -1,5 +1,7 @@ export * from './core'; -export * from './bls'; +export * from './curve_bls'; +export * from './curve_secp'; +export * from './curves'; export * from './NUT01'; export * from './NUT10'; export * from './NUT11'; diff --git a/src/model/BlindedMessage.ts b/src/model/BlindedMessage.ts index fcf0b5260..2367a491a 100644 --- a/src/model/BlindedMessage.ts +++ b/src/model/BlindedMessage.ts @@ -1,4 +1,4 @@ -import { type CurvePoint, pointToHex } from '../crypto/core'; +import { type CurvePoint, pointToHex } from '../crypto/curves'; import { Amount, type AmountLike } from './Amount'; import { type SerializedBlindedMessage } from './types/index'; diff --git a/test/crypto/bls.test.ts b/test/crypto/bls.test.ts index 46266b76c..1b7474b89 100644 --- a/test/crypto/bls.test.ts +++ b/test/crypto/bls.test.ts @@ -15,7 +15,7 @@ import { deriveBatchWeights, pointFromHexG1, pointFromHexG2, -} from '../../src/crypto/bls'; +} from '../../src/crypto'; import { CTSError } from '../../src/model/Errors'; // NUT-00 v3 round-trip vector (nuts/tests/00-tests.md "BLS12-381 (v3) round-trip"). The same hex diff --git a/test/model/OutputData.bls.test.ts b/test/model/OutputData.bls.test.ts index 946dcc769..e39bf0a6a 100644 --- a/test/model/OutputData.bls.test.ts +++ b/test/model/OutputData.bls.test.ts @@ -7,7 +7,7 @@ import { createBlindSignatureBls, pointFromHexG1, verifyUnblindedSignatureBls, -} from '../../src/crypto/bls'; +} from '../../src/crypto'; import { verifyUnblindedSignature } from '../../src/crypto/NUT01'; import { Amount } from '../../src/model/Amount'; import { OutputData } from '../../src/model/OutputData'; diff --git a/test/utils/core.test.ts b/test/utils/core.test.ts index 32d6c346b..ad993d5f5 100644 --- a/test/utils/core.test.ts +++ b/test/utils/core.test.ts @@ -937,7 +937,7 @@ describe('test zero-knowledge utilities', () => { }); test('mixed-denomination v3 batch verifies in one pairing', async () => { - const bls = await import('../../src/crypto/bls'); + const bls = await import('../../src/crypto'); // Same mint key (a=2), different secrets + amounts → realistic mixed-denomination receive. const aBytes = hexToBytes('0'.repeat(63) + '2'); const K2hex = bytesToHex(getG2PubKeyFromPrivKey(aBytes)); @@ -969,7 +969,7 @@ describe('test zero-knowledge utilities', () => { }); test('tampered C in a 5-proof v3 batch is rejected and offender named', async () => { - const bls = await import('../../src/crypto/bls'); + const bls = await import('../../src/crypto'); const aBytes = hexToBytes('0'.repeat(63) + '2'); const K2hex = bytesToHex(getG2PubKeyFromPrivKey(aBytes)); const makeProof = (amount: bigint, secret: string, r: bigint): Proof => {