]> git.codecow.com Git - libnemo.git/commitdiff
Start forking noble secp256k1 for worker embedding.
authorChris Duncan <chris@zoso.dev>
Mon, 24 Nov 2025 08:52:57 +0000 (00:52 -0800)
committerChris Duncan <chris@zoso.dev>
Mon, 24 Nov 2025 08:52:57 +0000 (00:52 -0800)
12 files changed:
src/lib/crypto/bip44.ts
src/lib/crypto/index.ts
src/lib/crypto/secp256k1.ts [new file with mode: 0644]
src/lib/crypto/wallet-aes-gcm.ts
src/lib/vault/index.ts
src/lib/vault/vault-worker.ts
src/lib/wallet/backup.ts
src/lib/wallet/get.ts
src/lib/wallet/verify.ts
test/VECTORS.mjs
test/main.test.mjs
test/test.import-wallet.mjs

index 1007f171911eb7a7e3868241581cc001ec7fe901..3e2562648a6e066363ac45471d2a5f4269940fa2 100644 (file)
@@ -1,7 +1,7 @@
 //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
-import { getPublicKey } from '@noble/secp256k1'
+import * as secp256k1 from '.'
 
 type ExtendedKey = {
        privateKey: ArrayBuffer
@@ -71,9 +71,9 @@ export class Bip44 {
        }
 
        static CKDpriv (curve: Curve, { privateKey, chainCode }: ExtendedKey, index?: number): Promise<ExtendedKey> {
-               console.log(index)
-               console.log([...(new Uint8Array(privateKey))].map(v => v.toString(16).padStart(2, '0')).join(''))
-               console.log([...(new Uint8Array(chainCode))].map(v => v.toString(16).padStart(2, '0')).join(''))
+               // console.log(index)
+               // console.log([...(new Uint8Array(privateKey))].map(v => v.toString(16).padStart(2, '0')).join(''))
+               // console.log([...(new Uint8Array(chainCode))].map(v => v.toString(16).padStart(2, '0')).join(''))
                if (index === undefined) {
                        return Promise.resolve({ privateKey, chainCode })
                }
@@ -87,7 +87,7 @@ export class Bip44 {
                } else if (curve === 'ed25519 seed') {
                        throw new RangeError('Only hardened child keys are supported for ed25519')
                } else {
-                       data.set(getPublicKey(pk))
+                       data.set(secp256k1.getPublicKey(pk))
                        data.set(this.ser32(index), 33)
                }
                return this.hmac(key, data)
index 6c61c9fe145c235ce8030047cf8fd006dd423d1e..c0f3d8c73b8863b2ae7c056cc01c601de7d641ca 100644 (file)
@@ -5,6 +5,7 @@ import { Bip39 } from './bip39'
 import { Bip44 } from './bip44'
 import { Blake2b } from './blake2b'
 import { NanoNaCl } from './nano-nacl'
+import { Secp256k1 } from './secp256k1'
 import { WalletAesGcm } from './wallet-aes-gcm'
 
-export { Bip39, Bip44, Blake2b, NanoNaCl, WalletAesGcm }
+export { Bip39, Bip44, Blake2b, NanoNaCl, Secp256k1, WalletAesGcm }
diff --git a/src/lib/crypto/secp256k1.ts b/src/lib/crypto/secp256k1.ts
new file mode 100644 (file)
index 0000000..8e615f9
--- /dev/null
@@ -0,0 +1,1185 @@
+/*! noble-secp256k1 - MIT License (c) 2019 Paul Miller (paulmillr.com) */
+
+import { Point, Signature, hashes, signAsync, verifyAsync, recoverPublicKey, recoverPublicKeyAsync, getSharedSecret, getPublicKey, etc, utils } from "@noble/secp256k1"
+import { randomBytes, sign, verify } from "crypto"
+import { format } from "path"
+import { _verify } from "../wallet/verify"
+
+/**
+ * 5KB JS implementation of secp256k1 ECDSA / Schnorr signatures & ECDH.
+ * Compliant with RFC6979 & BIP340.
+ * @module
+ */
+
+/**
+ * Fork rereleased under GPLv3.
+ * Functionality wrapped in a class for embedding into a Web Worker.
+ */
+
+/** Alias to Uint8Array. */
+export type Bytes = Uint8Array
+/** Signature instance, which allows recovering pubkey from it. */
+export type RecoveredSignature = Secp256k1['Signature'] & { recovery: number }
+/** Weierstrass elliptic curve options. */
+export type WeierstrassOpts<T> = Readonly<{
+       p: bigint
+       n: bigint
+       h: bigint
+       a: T
+       b: T
+       Gx: T
+       Gy: T
+}>
+declare const globalThis: Record<string, any> | undefined // Typescript symbol present in browsers
+/** Point in 2d xy affine coordinates. */
+export type AffinePoint = {
+       x: bigint
+       y: bigint
+}
+export type ECDSAExtraEntropy = boolean | Bytes
+/**
+ * - `compact` is the default format
+ * - `recovered` is the same as compact, but with an extra byte indicating recovery byte
+ * - `der` is not supported; and provided for consistency.
+ *   Switch to noble-curves if you need der.
+ */
+export type ECDSASignatureFormat = 'compact' | 'recovered' | 'der'
+/**
+ * - `prehash`: (default: true) indicates whether to do sha256(message).
+ *   When a custom hash is used, it must be set to `false`.
+ */
+export type ECDSARecoverOpts = {
+       prehash?: boolean
+}
+/**
+ * - `prehash`: (default: true) indicates whether to do sha256(message).
+ *   When a custom hash is used, it must be set to `false`.
+ * - `lowS`: (default: true) prohibits signatures which have (sig.s >= CURVE.n/2n).
+ *   Compatible with BTC/ETH. Setting `lowS: false` allows to create malleable signatures,
+ *   which is default openssl behavior.
+ *   Non-malleable signatures can still be successfully verified in openssl.
+ * - `format`: (default: 'compact') 'compact' or 'recovered' with recovery byte
+ */
+export type ECDSAVerifyOpts = {
+       prehash?: boolean
+       lowS?: boolean
+       format?: ECDSASignatureFormat
+}
+/**
+ * - `prehash`: (default: true) indicates whether to do sha256(message).
+ *   When a custom hash is used, it must be set to `false`.
+ * - `lowS`: (default: true) prohibits signatures which have (sig.s >= CURVE.n/2n).
+ *   Compatible with BTC/ETH. Setting `lowS: false` allows to create malleable signatures,
+ *   which is default openssl behavior.
+ *   Non-malleable signatures can still be successfully verified in openssl.
+ * - `format`: (default: 'compact') 'compact' or 'recovered' with recovery byte
+ * - `extraEntropy`: (default: false) creates sigs with increased security, see {@link ECDSAExtraEntropy}
+ */
+export type ECDSASignOpts = {
+       prehash?: boolean
+       lowS?: boolean
+       format?: ECDSASignatureFormat
+       extraEntropy?: ECDSAExtraEntropy
+}
+type Pred<T> = (v: Bytes) => T | undefined
+
+export class Secp256k1 {
+       /**
+        * Curve params. secp256k1 is short weierstrass / koblitz curve. Equation is y² == x³ + ax + b.
+        * * P = `2n**256n-2n**32n-2n**977n` // field over which calculations are done
+        * * N = `2n**256n - 0x14551231950b75fc4402da1732fc9bebfn` // group order, amount of curve points
+        * * h = `1n` // cofactor
+        * * a = `0n` // equation param
+        * * b = `7n` // equation param
+        * * Gx, Gy are coordinates of Generator / base point
+        */
+       static P = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn
+       static N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n
+       static h = 1n
+       static a = 0n
+       static _b = 7n
+       static Gx = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n
+       static Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n
+
+       static secp256k1_CURVE: WeierstrassOpts<bigint> = {
+               p: this.P,
+               n: this.N,
+               h: 1n,
+               a: 0n,
+               b: this._b,
+               Gx: this.Gx,
+               Gy: this.Gy,
+       }
+
+       static L = 32 // field / group byte length
+       static L2 = 64
+       static lengths = {
+               publicKey: this.L + 1,
+               publicKeyUncompressed: this.L2 + 1,
+               signature: this.L2,
+               seed: this.L + this.L / 2,
+       }
+
+       // Helpers and Precomputes sections are reused between libraries
+
+       // ## Helpers
+       // ----------
+       static captureTrace = (...args: Parameters<typeof Error.captureStackTrace>): void => {
+               if ('captureStackTrace' in Error && typeof Error.captureStackTrace === 'function') {
+                       Error.captureStackTrace(...args)
+               }
+       }
+       static err = (message = ''): never => {
+               const e = new Error(message)
+               this.captureTrace(e, this.err)
+               throw e
+       }
+       static isBig = (n: unknown): n is bigint => typeof n === 'bigint' // is big integer
+       static isStr = (s: unknown): s is string => typeof s === 'string' // is string
+       static isBytes = (a: unknown): a is Uint8Array =>
+               a instanceof Uint8Array || (ArrayBuffer.isView(a) && a.constructor.name === 'Uint8Array')
+       /** Asserts something is Uint8Array. */
+       static abytes = (value: Bytes, length?: number, title: string = ''): Bytes => {
+               const bytes = this.isBytes(value)
+               const len = value?.length
+               const needsLen = length !== undefined
+               if (!bytes || (needsLen && len !== length)) {
+                       const prefix = title && `"${title}" `
+                       const ofLen = needsLen ? ` of length ${length}` : ''
+                       const got = bytes ? `length=${len}` : `type=${typeof value}`
+                       this.err(prefix + 'expected Uint8Array' + ofLen + ', got ' + got)
+               }
+               return value
+       }
+       /** create Uint8Array */
+       static u8n = (len: number): Bytes => new Uint8Array(len)
+       static padh = (n: number | bigint, pad: number) => n.toString(16).padStart(pad, '0')
+       static bytesToHex = (b: Bytes): string =>
+               Array.from(this.abytes(b))
+                       .map((e) => this.padh(e, 2))
+                       .join('')
+       static C = { _0: 48, _9: 57, A: 65, F: 70, a: 97, f: 102 } as const // ASCII characters
+       static _ch = (ch: number): number | undefined => {
+               if (ch >= this.C._0 && ch <= this.C._9) return ch - this.C._0 // '2' => 50-48
+               if (ch >= this.C.A && ch <= this.C.F) return ch - (this.C.A - 10) // 'B' => 66-(65-10)
+               if (ch >= this.C.a && ch <= this.C.f) return ch - (this.C.a - 10) // 'b' => 98-(97-10)
+               return
+       }
+       static hexToBytes = (hex: string): Bytes => {
+               const e = 'hex invalid'
+               if (!this.isStr(hex)) return this.err(e)
+               const hl = hex.length
+               const al = hl / 2
+               if (hl % 2) return this.err(e)
+               const array = this.u8n(al)
+               for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) {
+                       // treat each char as ASCII
+                       const n1 = this._ch(hex.charCodeAt(hi)) // parse first char, multiply it by 16
+                       const n2 = this._ch(hex.charCodeAt(hi + 1)) // parse second char
+                       if (n1 === undefined || n2 === undefined) return this.err(e)
+                       array[ai] = n1 * 16 + n2 // example: 'A9' => 10*16 + 9
+               }
+               return array
+       }
+       static cr = () => globalThis?.crypto // WebCrypto is available in all modern environments
+       static subtle = () => this.cr()?.subtle ?? this.err('crypto.subtle must be defined, consider polyfill')
+       // prettier-ignore
+       static concatBytes = (...arrs: Bytes[]): Bytes => {
+               const r = this.u8n(arrs.reduce((sum, a) => sum + this.abytes(a).length, 0)) // create u8a of summed length
+               let pad = 0 // walk through each array,
+               arrs.forEach(a => { r.set(a, pad); pad += a.length }) // ensure they have proper type
+               return r
+       }
+       /** WebCrypto OS-level CSPRNG (random number generator). Will throw when not available. */
+       static randomBytes = (len: number = this.L): Bytes => {
+               const c = this.cr()
+               return c.getRandomValues(this.u8n(len))
+       }
+       static big = BigInt
+       static arange = (n: bigint, min: bigint, max: bigint, msg = 'bad number: out of range'): bigint =>
+               this.isBig(n) && min <= n && n < max ? n : this.err(msg)
+       /** modular division */
+       static M = (a: bigint, b: bigint = this.P) => {
+               const r = a % b
+               return r >= 0n ? r : b + r
+       }
+       static modN = (a: bigint) => this.M(a, this.N)
+       /** Modular inversion using eucledian GCD (non-CT). No negative exponent for now. */
+       // prettier-ignore
+       static invert = (num: bigint, md: bigint): bigint => {
+               if (num === 0n || md <= 0n) this.err('no inverse n=' + num + ' mod=' + md)
+               let a = this.M(num, md), b = md, x = 0n, y = 1n, u = 1n, v = 0n
+               while (a !== 0n) {
+                       const q = b / a, r = b % a
+                       const m = x - u * q, n = y - v * q
+                       b = a, a = r, x = u, y = v, u = m, v = n
+               }
+               return b === 1n ? this.M(x, md) : this.err('no inverse') // b is gcd at this point
+       }
+       static callHash = (name: string) => {
+               // @ts-ignore
+               const fn = hashes[name]
+               if (typeof fn !== 'function') this.err('hashes.' + name + ' not set')
+               return fn
+       }
+       static hash = (msg: Bytes): Bytes => this.callHash('sha256')(msg)
+       static apoint = (p: unknown) => (p instanceof this.Point ? p : this.err('Point expected'))
+       // ## End of Helpers
+       // -----------------
+
+       /** secp256k1 formula. Koblitz curves are subclass of weierstrass curves with a=0, making it x³+b */
+       static koblitz = (x: bigint) => this.M(this.M(x * x) * x + this._b)
+       /** assert is element of field mod P (incl. 0) */
+       static FpIsValid = (n: bigint) => this.arange(n, 0n, this.P)
+       /** assert is element of field mod P (excl. 0) */
+       static FpIsValidNot0 = (n: bigint) => this.arange(n, 1n, this.P)
+       /** assert is element of field mod N (excl. 0) */
+       static FnIsValidNot0 = (n: bigint) => this.arange(n, 1n, this.N)
+       static isEven = (y: bigint) => (y & 1n) === 0n
+       /** create Uint8Array of byte n */
+       static u8of = (n: number): Bytes => Uint8Array.of(n)
+       static getPrefix = (y: bigint) => this.u8of(this.isEven(y) ? 0x02 : 0x03)
+       /** lift_x from BIP340 calculates square root. Validates x, then validates root*root. */
+       static lift_x = (x: bigint) => {
+               // Let c = x³ + 7 mod p. Fail if x ≥ p. (also fail if x < 1)
+               const c = this.koblitz(this.FpIsValidNot0(x))
+               // c = √y
+               // y = c^((p+1)/4) mod p
+               // This formula works for fields p = 3 mod 4 -- a special, fast case.
+               // Paper: "Square Roots from 1;24,51,10 to Dan Shanks".
+               let r = 1n
+               for (let num = c, e = (this.P + 1n) / 4n; e > 0n; e >>= 1n) {
+                       // powMod: modular exponentiation.
+                       if (e & 1n) r = (r * num) % this.P // Uses exponentiation by squaring.
+                       num = (num * num) % this.P // Not constant-time.
+               }
+               return this.M(r * r) === c ? r : this.err('sqrt invalid') // check if result is valid
+       }
+
+       /** Point in 3d xyz projective coordinates. 3d takes less inversions than 2d. */
+       static Point = class {
+               static BASE: Secp256k1['Point']
+               static ZERO: Secp256k1['Point']
+               readonly X: bigint
+               readonly Y: bigint
+               readonly Z: bigint
+               constructor (X: bigint, Y: bigint, Z: bigint) {
+                       this.X = Secp256k1.FpIsValid(X)
+                       this.Y = Secp256k1.FpIsValidNot0(Y) // Y can't be 0 in Projective
+                       this.Z = Secp256k1.FpIsValid(Z)
+                       Object.freeze(this)
+               }
+               static CURVE (): WeierstrassOpts<bigint> {
+                       return Secp256k1.secp256k1_CURVE
+               }
+               /** Create 3d xyz point from 2d xy. (0, 0) => (0, 1, 0), not (0, 0, 1) */
+               static fromAffine (ap: AffinePoint): Point {
+                       const { x, y } = ap
+                       return x === 0n && y === 0n ? Secp256k1.I : new Secp256k1.Point(x, y, 1n)
+               }
+               /** Convert Uint8Array or hex string to Point. */
+               static fromBytes (bytes: Bytes): Point {
+                       Secp256k1.abytes(bytes)
+                       const { publicKey: comp, publicKeyUncompressed: uncomp } = Secp256k1.lengths // e.g. for 32-byte: 33, 65
+                       let p: Point | undefined = undefined
+                       const length = bytes.length
+                       const head = bytes[0]
+                       const tail = bytes.subarray(1)
+                       const x = Secp256k1.sliceBytesNumBE(tail, 0, Secp256k1.L)
+                       // No actual validation is done here: use .assertValidity()
+                       if (length === comp && (head === 0x02 || head === 0x03)) {
+                               // Equation is y² == x³ + ax + b. We calculate y from x.
+                               // y = √y²; there are two solutions: y, -y. Determine proper solution based on prefix
+                               let y = Secp256k1.lift_x(x)
+                               const evenY = Secp256k1.isEven(y)
+                               const evenH = Secp256k1.isEven(Secp256k1.big(head))
+                               if (evenH !== evenY) y = Secp256k1.M(-y)
+                               p = new Point(x, y, 1n)
+                       }
+                       // Uncompressed 65-byte point, 0x04 prefix
+                       if (length === uncomp && head === 0x04) p = new Point(x, Secp256k1.sliceBytesNumBE(tail, Secp256k1.L, Secp256k1.L2), 1n)
+                       // Validate point
+                       return p ? p.assertValidity() : Secp256k1.err('bad point: not on curve')
+               }
+               static fromHex (hex: string): Point {
+                       return Secp256k1.Point.fromBytes(Secp256k1.hexToBytes(hex))
+               }
+               get x (): bigint {
+                       return this.toAffine().x
+               }
+               get y (): bigint {
+                       return this.toAffine().y
+               }
+               /** Equality check: compare points P&Q. */
+               equals (other: Point): boolean {
+                       const { X: X1, Y: Y1, Z: Z1 } = this
+                       const { X: X2, Y: Y2, Z: Z2 } = Secp256k1.apoint(other) // checks class equality
+                       const X1Z2 = Secp256k1.M(X1 * Z2)
+                       const X2Z1 = Secp256k1.M(X2 * Z1)
+                       const Y1Z2 = Secp256k1.M(Y1 * Z2)
+                       const Y2Z1 = Secp256k1.M(Y2 * Z1)
+                       return X1Z2 === X2Z1 && Y1Z2 === Y2Z1
+               }
+               is0 (): boolean {
+                       return this.equals(I)
+               }
+               /** Flip point over y coordinate. */
+               negate (): Point {
+                       return new Point(this.X, Secp256k1.M(-this.Y), this.Z)
+               }
+               /** Point doubling: P+P, complete formula. */
+               double (): Point {
+                       return this.add(this)
+               }
+               /**
+                * Point addition: P+Q, complete, exception-free formula
+                * (Renes-Costello-Batina, algo 1 of [2015/1060](https://eprint.iacr.org/2015/1060)).
+                * Cost: `12M + 0S + 3*a + 3*b3 + 23add`.
+                */
+               // prettier-ignore
+               add (other: Point): Point {
+                       const { X: X1, Y: Y1, Z: Z1 } = this
+                       const { X: X2, Y: Y2, Z: Z2 } = Secp256k1.apoint(other)
+                       const a = 0n
+                       const b = Secp256k1._b
+                       let X3 = 0n, Y3 = 0n, Z3 = 0n
+                       const b3 = Secp256k1.M(b * 3n)
+                       let t0 = Secp256k1.M(X1 * X2), t1 = Secp256k1.M(Y1 * Y2), t2 = Secp256k1.M(Z1 * Z2), t3 = Secp256k1.M(X1 + Y1) // step 1
+                       let t4 = Secp256k1.M(X2 + Y2) // step 5
+                       t3 = Secp256k1.M(t3 * t4); t4 = Secp256k1.M(t0 + t1); t3 = Secp256k1.M(t3 - t4); t4 = Secp256k1.M(X1 + Z1)
+                       let t5 = Secp256k1.M(X2 + Z2) // step 10
+                       t4 = Secp256k1.M(t4 * t5); t5 = Secp256k1.M(t0 + t2); t4 = Secp256k1.M(t4 - t5); t5 = Secp256k1.M(Y1 + Z1)
+                       X3 = Secp256k1.M(Y2 + Z2) // step 15
+                       t5 = Secp256k1.M(t5 * X3); X3 = Secp256k1.M(t1 + t2); t5 = Secp256k1.M(t5 - X3); Z3 = Secp256k1.M(a * t4)
+                       X3 = Secp256k1.M(b3 * t2) // step 20
+                       Z3 = Secp256k1.M(X3 + Z3); X3 = Secp256k1.M(t1 - Z3); Z3 = Secp256k1.M(t1 + Z3); Y3 = Secp256k1.M(X3 * Z3)
+                       t1 = Secp256k1.M(t0 + t0) // step 25
+                       t1 = Secp256k1.M(t1 + t0); t2 = Secp256k1.M(a * t2); t4 = Secp256k1.M(b3 * t4); t1 = Secp256k1.M(t1 + t2)
+                       t2 = Secp256k1.M(t0 - t2) // step 30
+                       t2 = Secp256k1.M(a * t2); t4 = Secp256k1.M(t4 + t2); t0 = Secp256k1.M(t1 * t4); Y3 = Secp256k1.M(Y3 + t0)
+                       t0 = Secp256k1.M(t5 * t4) // step 35
+                       X3 = Secp256k1.M(t3 * X3); X3 = Secp256k1.M(X3 - t0); t0 = Secp256k1.M(t3 * t1); Z3 = Secp256k1.M(t5 * Z3)
+                       Z3 = Secp256k1.M(Z3 + t0) // step 40
+                       return new Point(X3, Y3, Z3)
+               }
+               subtract (other: Point): Point {
+                       return this.add(Secp256k1.apoint(other).negate())
+               }
+               /**
+                * Point-by-scalar multiplication. Scalar must be in range 1 <= n < CURVE.n.
+                * Uses {@link wNAF} for base point.
+                * Uses fake point to mitigate side-channel leakage.
+                * @param n scalar by which point is multiplied
+                * @param safe safe mode guards against timing attacks; unsafe mode is faster
+                */
+               multiply (n: bigint, safe = true): Point {
+                       if (!safe && n === 0n) return Secp256k1.I
+                       Secp256k1.FnIsValidNot0(n)
+                       if (n === 1n) return this
+                       if (this.equals(Secp256k1.G)) return wNAF(n).p
+                       // init result point & fake point
+                       let p = Secp256k1.I
+                       let f = Secp256k1.G
+                       for (let d: Point = this; n > 0n; d = d.double(), n >>= 1n) {
+                               // if bit is present, add to point
+                               // if not present, add to fake, for timing safety
+                               if (n & 1n) p = p.add(d)
+                               else if (safe) f = f.add(d)
+                       }
+                       return p
+               }
+               multiplyUnsafe (scalar: bigint): Point {
+                       return this.multiply(scalar, false)
+               }
+               /** Convert point to 2d xy affine point. (X, Y, Z) ∋ (x=X/Z, y=Y/Z) */
+               toAffine (): AffinePoint {
+                       const { X: x, Y: y, Z: z } = this
+                       // fast-paths for ZERO point OR Z=1
+                       if (this.equals(I)) return { x: 0n, y: 0n }
+                       if (z === 1n) return { x, y }
+                       const iz = Secp256k1.invert(z, Secp256k1.P)
+                       // (Z * Z^-1) must be 1, otherwise bad math
+                       if (Secp256k1.M(z * iz) !== 1n) Secp256k1.err('inverse invalid')
+                       // x = X*Z^-1; y = Y*Z^-1
+                       return { x: Secp256k1.M(x * iz), y: Secp256k1.M(y * iz) }
+               }
+               /** Checks if the point is valid and on-curve. */
+               assertValidity (): Point {
+                       const { x, y } = this.toAffine() // convert to 2d xy affine point.
+                       Secp256k1.FpIsValidNot0(x) // must be in range 1 <= x,y < P
+                       Secp256k1.FpIsValidNot0(y)
+                       // y² == x³ + ax + b, equation sides must be equal
+                       return Secp256k1.M(y * y) === Secp256k1.koblitz(x) ? this : Secp256k1.err('bad point: not on curve')
+               }
+               /** Converts point to 33/65-byte Uint8Array. */
+               toBytes (isCompressed = true): Bytes {
+                       const { x, y } = this.assertValidity().toAffine()
+                       const x32b = Secp256k1.numTo32b(x)
+                       if (isCompressed) return Secp256k1.concatBytes(Secp256k1.getPrefix(y), x32b)
+                       return Secp256k1.concatBytes(Secp256k1.u8of(0x04), x32b, Secp256k1.numTo32b(y))
+               }
+
+               toHex (isCompressed?: boolean): string {
+                       return Secp256k1.bytesToHex(this.toBytes(isCompressed))
+               }
+       }
+       /** Generator / base point */
+       static G: Point = new Point(this.Gx, this.Gy, 1n)
+       /** Identity / zero point */
+       static I: Point = new Point(0n, 1n, 0n)
+       // Static aliases
+       static {
+               this.Point.BASE = this.G
+               this.Point.ZERO = this.I
+       }
+       /** `Q = u1⋅G + u2⋅R`. Verifies Q is not ZERO. Unsafe: non-CT. */
+       static doubleScalarMulUns = (R: Point, u1: bigint, u2: bigint): Point => {
+               return this.G.multiply(u1, false).add(R.multiply(u2, false)).assertValidity()
+       }
+       static bytesToNumBE = (b: Bytes): bigint => this.big('0x' + (this.bytesToHex(b) || '0'))
+       static sliceBytesNumBE = (b: Bytes, from: number, to: number) => this.bytesToNumBE(b.subarray(from, to))
+       static B256 = 2n ** 256n // secp256k1 is weierstrass curve. Equation is x³ + ax + b.
+       /** Number to 32b. Must be 0 <= num < B256. validate, pad, to bytes. */
+       static numTo32b = (num: bigint): Bytes => this.hexToBytes(this.padh(this.arange(num, 0n, this.B256), this.L2))
+       /** Normalize private key to scalar (bigint). Verifies scalar is in range 1<s<N */
+       static secretKeyToScalar = (secretKey: Bytes): bigint => {
+               const num = this.bytesToNumBE(this.abytes(secretKey, this.L, 'secret key'))
+               return this.arange(num, 1n, this.N, 'invalid secret key: outside of range')
+       }
+       /** For Signature malleability, validates sig.s is bigger than N/2. */
+       static highS = (n: bigint): boolean => n > this.N >> 1n
+       /** Creates 33/65-byte public key from 32-byte private key. */
+       static getPublicKey = (privKey: Bytes, isCompressed = true): Bytes => {
+               return this.G.multiply(this.secretKeyToScalar(privKey)).toBytes(isCompressed)
+       }
+
+       static isValidSecretKey = (secretKey: Bytes): boolean => {
+               try {
+                       return !!this.secretKeyToScalar(secretKey)
+               } catch (error) {
+                       return false
+               }
+       }
+       static isValidPublicKey = (publicKey: Bytes, isCompressed?: boolean): boolean => {
+               const { publicKey: comp, publicKeyUncompressed } = this.lengths
+               try {
+                       const l = publicKey.length
+                       if (isCompressed === true && l !== comp) return false
+                       if (isCompressed === false && l !== publicKeyUncompressed) return false
+                       return !!this.Point.fromBytes(publicKey)
+               } catch (error) {
+                       return false
+               }
+       }
+
+       static assertRecoveryBit = (recovery?: number) => {
+               if (![0, 1, 2, 3].includes(recovery!)) this.err('recovery id must be valid and present')
+       }
+       static assertSigFormat = (format?: ECDSASignatureFormat) => {
+               if (format != null && !ALL_SIG.includes(format))
+                       err(`Signature format must be one of: ${ALL_SIG.join(', ')}`)
+               if (format === SIG_DER) this.err('Signature format "der" is not supported: switch to noble-curves')
+       }
+       static assertSigLength = (sig: Bytes, format: ECDSASignatureFormat = SIG_COMPACT) => {
+               this.assertSigFormat(format)
+               const SL = this.lengths.signature
+               const RL = SL + 1
+               let msg = `Signature format "${format}" expects Uint8Array with length `
+               if (format === SIG_COMPACT && sig.length !== SL) this.err(msg + SL)
+               if (format === SIG_RECOVERED && sig.length !== RL) this.err(msg + RL)
+       }
+       /** ECDSA Signature class. Supports only compact 64-byte representation, not DER. */
+       static Signature = class {
+               readonly r: bigint
+               readonly s: bigint
+               readonly recovery?: number
+               constructor (r: bigint, s: bigint, recovery?: number) {
+                       this.r = Secp256k1.FnIsValidNot0(r) // 1 <= r < N
+                       this.s = Secp256k1.FnIsValidNot0(s) // 1 <= s < N
+                       if (recovery != null) this.recovery = recovery
+                       Object.freeze(this)
+               }
+               static fromBytes (b: Bytes, format: ECDSASignatureFormat = SIG_COMPACT): Secp256k1['Signature'] {
+                       Secp256k1.assertSigLength(b, format)
+                       let rec: number | undefined
+                       if (format === SIG_RECOVERED) {
+                               rec = b[0]
+                               b = b.subarray(1)
+                       }
+                       const r = Secp256k1.sliceBytesNumBE(b, 0, Secp256k1.L)
+                       const s = Secp256k1.sliceBytesNumBE(b, Secp256k1.L, Secp256k1.L2)
+                       return new Signature(r, s, rec)
+               }
+               addRecoveryBit (bit: number): RecoveredSignature {
+                       return new Signature(this.r, this.s, bit) as RecoveredSignature
+               }
+               hasHighS (): boolean {
+                       return Secp256k1.highS(this.s)
+               }
+               toBytes (format: ECDSASignatureFormat = SIG_COMPACT): Bytes {
+                       const { r, s, recovery } = this
+                       const res = Secp256k1.concatBytes(Secp256k1.numTo32b(r), Secp256k1.numTo32b(s))
+                       if (format === SIG_RECOVERED) {
+                               Secp256k1.assertRecoveryBit(recovery)
+                               return Secp256k1.concatBytes(Uint8Array.of(recovery!), res)
+                       }
+                       return res
+               }
+       }
+
+       /**
+        * RFC6979: ensure ECDSA msg is X bytes, convert to BigInt.
+        * RFC suggests optional truncating via bits2octets.
+        * FIPS 186-4 4.6 suggests the leftmost min(nBitLen, outLen) bits,
+        * which matches bits2int. bits2int can produce res>N.
+        */
+       static bits2int = (bytes: Bytes): bigint => {
+               const delta = bytes.length * 8 - 256
+               if (delta > 1024) this.err('msg invalid') // our CUSTOM check, "just-in-case": prohibit long inputs
+               const num = this.bytesToNumBE(bytes)
+               return delta > 0 ? num >> this.big(delta) : num
+       }
+       /** int2octets can't be used; pads small msgs with 0: BAD for truncation as per RFC vectors */
+       static bits2int_modN = (bytes: Bytes): bigint => this.modN(this.bits2int(this.abytes(bytes)))
+       /**
+        * Option to enable hedged signatures with improved security.
+        *
+        * * Randomly generated k is bad, because broken CSPRNG would leak private keys.
+        * * Deterministic k (RFC6979) is better; but is suspectible to fault attacks.
+        *
+        * We allow using technique described in RFC6979 3.6: additional k', a.k.a. adding randomness
+        * to deterministic sig. If CSPRNG is broken & randomness is weak, it would STILL be as secure
+        * as ordinary sig without ExtraEntropy.
+        *
+        * * `true` means "fetch data, from CSPRNG, incorporate it into k generation"
+        * * `false` means "disable extra entropy, use purely deterministic k"
+        * * `Uint8Array` passed means "incorporate following data into k generation"
+        *
+        * https://paulmillr.com/posts/deterministic-signatures/
+        */
+       // todo: better name
+       static SIG_COMPACT: ECDSASignatureFormat = 'compact'
+       static SIG_RECOVERED = 'recovered'
+       static SIG_DER = 'der'
+       static ALL_SIG = [this.SIG_COMPACT, this.SIG_RECOVERED, this.SIG_DER] as const
+
+       static defaultSignOpts: ECDSASignOpts = {
+               lowS: true,
+               prehash: true,
+               format: this.SIG_COMPACT,
+               extraEntropy: false,
+       }
+
+       static _sha = 'SHA-256'
+       static hashes = {
+               hmacSha256Async: async (key: Bytes, message: Bytes): Promise<Bytes> => {
+                       const s = this.subtle()
+                       const name = 'HMAC'
+                       const k = await s.importKey('raw', key, { name, hash: { name: this._sha } }, false, ['sign'])
+                       return this.u8n(await s.sign(name, k, message))
+               },
+               hmacSha256: undefined as undefined | ((key: Bytes, message: Bytes) => Bytes),
+               sha256Async: async (msg: Bytes): Promise<Bytes> => this.u8n(await sthis.ubtle().digest(this._sha, msg)),
+               sha256: undefined as undefined | ((message: Bytes) => Bytes),
+       }
+
+       static prepMsg = (msg: Bytes, opts: ECDSARecoverOpts, async_: boolean): Bytes | Promise<Bytes> => {
+               this.abytes(msg, undefined, 'message')
+               if (!opts.prehash) return msg
+               return async_ ? this.hashes.sha256Async(msg) : this.callHash('sha256')(msg)
+       }
+
+       static NULL = this.u8n(0)
+       static byte0 = this.u8of(0x00)
+       static byte1 = this.u8of(0x01)
+       static _maxDrbgIters = 1000
+       static _drbgErr = 'drbg: tried max amount of iterations'
+       // HMAC-DRBG from NIST 800-90. Minimal, non-full-spec - used for RFC6979 signatures.
+       static hmacDrbg = (seed: Bytes, pred: Pred<Bytes>): Bytes => {
+               let v = u8n(L) // Steps B, C of RFC6979 3.2: set hashLen
+               let k = u8n(L) // In our case, it's always equal to L
+               let i = 0 // Iterations counter, will throw when over max
+       static reset = () => {
+               v.fill(1)
+               k.fill(0)
+       }
+       // h = hmac(K || V || ...)
+       static h = (...b: Bytes[]) => callHash('hmacSha256')(k, concatBytes(v, ...b))
+       static reseed = (seed = NULL) => {
+               // HMAC-DRBG reseed() function. Steps D-G
+               k = h(byte0, seed) // k = hmac(k || v || 0x00 || seed)
+               v = h() // v = hmac(k || v)
+               if (seed.length === 0) return
+               k = h(byte1, seed) // k = hmac(k || v || 0x01 || seed)
+               v = h() // v = hmac(k || v)
+       }
+       // HMAC-DRBG generate() function
+       static gen = () => {
+               if (i++ >= _maxDrbgIters) err(_drbgErr)
+               v = h() // v = hmac(k || v)
+               return v // this diverges from noble-curves: we don't allow arbitrary output len!
+       }
+       reset ()
+       reseed (seed) // Steps D-G
+       let res: Bytes | undefined = undefined // Step H: grind until k is in [1..n-1]
+while (!(res = pred(gen()))) reseed() // test predicate until it returns ok
+reset()
+return res!
+}
+
+// Identical to hmacDrbg, but async: uses built-in WebCrypto
+static hmacDrbgAsync = async (seed: Bytes, pred: Pred<Bytes>): Promise<Bytes> => {
+       let v = u8n(L) // Steps B, C of RFC6979 3.2: set hashLen
+       let k = u8n(L) // In our case, it's always equal to L
+       let i = 0 // Iterations counter, will throw when over max
+       static reset = () => {
+               v.fill(1)
+               k.fill(0)
+       }
+       // h = hmac(K || V || ...)
+       static h = (...b: Bytes[]) => hashes.hmacSha256Async(k, concatBytes(v, ...b))
+       static reseed = async (seed = NULL) => {
+               // HMAC-DRBG reseed() function. Steps D-G
+               k = await h(byte0, seed) // k = hmac(K || V || 0x00 || seed)
+               v = await h() // v = hmac(K || V)
+               if (seed.length === 0) return
+               k = await h(byte1, seed) // k = hmac(K || V || 0x01 || seed)
+               v = await h() // v = hmac(K || V)
+       }
+       // HMAC-DRBG generate() function
+       static gen = async () => {
+               if (i++ >= _maxDrbgIters) err(_drbgErr)
+               v = await h() // v = hmac(K || V)
+               return v // this diverges from noble-curves: we don't allow arbitrary output len!
+       }
+       reset()
+       await reseed(seed) // Steps D-G
+       let res: Bytes | undefined = undefined // Step H: grind until k is in [1..n-1]
+       while (!(res = pred(await gen()))) await reseed() // test predicate until it returns ok
+       reset()
+       return res!
+}
+
+// RFC6979 signature generation, preparation step.
+// Follows [SEC1](https://secg.org/sec1-v2.pdf) 4.1.2 & RFC6979.
+static _sign = <T> (
+       messageHash: Bytes,
+       secretKey: Bytes,
+       opts: ECDSASignOpts,
+       hmacDrbg: (seed: Bytes, pred: Pred<Bytes>) => T
+): T => {
+       let { lowS, extraEntropy } = opts // generates low-s sigs by default
+       // RFC6979 3.2: we skip step A
+       static int2octets = numTo32b // int to octets
+       static h1i = bits2int_modN(messageHash) // msg bigint
+       static h1o = int2octets(h1i) // msg octets
+       static d = secretKeyToScalar(secretKey) // validate private key, convert to bigint
+       static seedArgs = [int2octets(d), h1o] // Step D of RFC6979 3.2
+       /** RFC6979 3.6: additional k' (optional). See {@link ECDSAExtraEntropy}. */
+       if (extraEntropy != null && extraEntropy !== false) {
+               // K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1) || k')
+               // gen random bytes OR pass as-is
+               const e = extraEntropy === true ? randomBytes(L) : extraEntropy
+               seedArgs.push(abytes(e, undefined, 'extraEntropy')) // check for being bytes
+       }
+       static seed = concatBytes(...seedArgs)
+       static m = h1i // convert msg to bigint
+       // Converts signature params into point w r/s, checks result for validity.
+       // To transform k => Signature:
+       // q = k⋅G
+       // r = q.x mod n
+       // s = k^-1(m + rd) mod n
+       // Can use scalar blinding b^-1(bm + bdr) where b ∈ [1,q−1] according to
+       // https://tches.iacr.org/index.php/TCHES/article/view/7337/6509. We've decided against it:
+       // a) dependency on CSPRNG b) 15% slowdown c) doesn't really help since bigints are not CT
+       static k2sig = (kBytes: Bytes): Bytes | undefined => {
+               // RFC 6979 Section 3.2, step 3: k = bits2int(T)
+               // Important: all mod() calls here must be done over N
+               const k = bits2int(kBytes)
+               if (!(1n <= k && k < N)) return // Valid scalars (including k) must be in 1..N-1
+               const ik = invert(k, N) // k^-1 mod n
+               const q = G.multiply(k).toAffine() // q = k⋅G
+               const r = modN(q.x) // r = q.x mod n
+               if (r === 0n) return
+               const s = modN(ik * modN(m + r * d)) // s = k^-1(m + rd) mod n
+               if (s === 0n) return
+               let recovery = (q.x === r ? 0 : 2) | Number(q.y & 1n) // recovery bit (2 or 3, when q.x > n)
+               let normS = s // normalized S
+               if (lowS && highS(s)) {
+                       // if lowS was passed, ensure s is always
+                       normS = modN(-s) // in the bottom half of CURVE.n
+                       recovery ^= 1
+               }
+               const sig = new Signature(r, normS, recovery) as RecoveredSignature // use normS, not s
+               return sig.toBytes(opts.format)
+       }
+       return hmacDrbg(seed, k2sig)
+}
+
+// Follows [SEC1](https://secg.org/sec1-v2.pdf) 4.1.4.
+static _verify = (sig: Bytes, messageHash: Bytes, publicKey: Bytes, opts: ECDSAVerifyOpts = {}) => {
+       static { lowS, format } = opts
+       if (sig instanceof Signature) err('Signature must be in Uint8Array, use .toBytes()')
+       assertSigLength(sig, format)
+       abytes(publicKey, undefined, 'publicKey')
+       try {
+               const { r, s } = Signature.fromBytes(sig, format)
+               const h = bits2int_modN(messageHash) // Truncate hash
+               const P = Point.fromBytes(publicKey) // Validate public key
+               if (lowS && highS(s)) return false // lowS bans sig.s >= CURVE.n/2
+               const is = invert(s, N) // s^-1
+               const u1 = modN(h * is) // u1 = hs^-1 mod n
+               const u2 = modN(r * is) // u2 = rs^-1 mod n
+               const R = doubleScalarMulUns(P, u1, u2).toAffine() // R = u1⋅G + u2⋅P
+               // Stop if R is identity / zero point. Check is done inside `doubleScalarMulUns`
+               const v = modN(R.x) // R.x must be in N's field, not P's
+               return v === r // mod(R.x, n) == r
+       } catch (error) {
+               return false
+       }
+}
+
+static setDefaults = (opts: ECDSASignOpts): Required<ECDSASignOpts> => {
+       static res: ECDSASignOpts = {}
+       Object.keys(defaultSignOpts).forEach((k: string) => {
+               // @ts-ignore
+               res[k] = opts[k] ?? defaultSignOpts[k]
+       })
+       return res as Required<ECDSASignOpts>
+}
+
+/**
+ * Sign a message using secp256k1. Sync: uses `hashes.sha256` and `hashes.hmacSha256`.
+ * Prehashes message with sha256, disable using `prehash: false`.
+ * @param opts - see {@link ECDSASignOpts} for details. Enabling {@link ECDSAExtraEntropy} will improve security.
+ * @example
+ * ```js
+ * const msg = new TextEncoder().encode('hello noble');
+ * sign(msg, secretKey);
+ * sign(keccak256(msg), secretKey, { prehash: false });
+ * sign(msg, secretKey, { extraEntropy: true });
+ * sign(msg, secretKey, { format: 'recovered' });
+ * ```
+ */
+static sign = (message: Bytes, secretKey: Bytes, opts: ECDSASignOpts = {}): Bytes => {
+       opts = setDefaults(opts)
+       message = prepMsg(message, opts, false) as Bytes
+       return _sign(message, secretKey, opts, hmacDrbg)
+}
+
+/**
+ * Sign a message using secp256k1. Async: uses built-in WebCrypto hashes.
+ * Prehashes message with sha256, disable using `prehash: false`.
+ * @param opts - see {@link ECDSASignOpts} for details. Enabling {@link ECDSAExtraEntropy} will improve security.
+ * @example
+ * ```js
+ * const msg = new TextEncoder().encode('hello noble');
+ * await signAsync(msg, secretKey);
+ * await signAsync(keccak256(msg), secretKey, { prehash: false });
+ * await signAsync(msg, secretKey, { extraEntropy: true });
+ * await signAsync(msg, secretKey, { format: 'recovered' });
+ * ```
+ */
+static signAsync = async (
+       message: Bytes,
+       secretKey: Bytes,
+       opts: ECDSASignOpts = {}
+): Promise<Bytes> => {
+       opts = setDefaults(opts)
+       message = await prepMsg(message, opts, true)
+       return _sign(message, secretKey, opts, hmacDrbgAsync)
+}
+
+/**
+ * Verify a signature using secp256k1. Sync: uses `hashes.sha256` and `hashes.hmacSha256`.
+ * @param signature - default is 64-byte 'compact' format, also see {@link ECDSASignatureFormat}
+ * @param message - message which was signed. Keep in mind `prehash` from opts.
+ * @param publicKey - public key which
+ * @param opts - see {@link ECDSAVerifyOpts} for details.
+ * @example
+ * ```js
+ * const msg = new TextEncoder().encode('hello noble');
+ * verify(sig, msg, publicKey);
+ * verify(sig, keccak256(msg), publicKey, { prehash: false });
+ * verify(sig, msg, publicKey, { lowS: false });
+ * verify(sigr, msg, publicKey, { format: 'recovered' });
+ * ```
+ */
+static verify = (
+       signature: Bytes,
+       message: Bytes,
+       publicKey: Bytes,
+       opts: ECDSAVerifyOpts = {}
+): boolean => {
+       opts = setDefaults(opts)
+       message = prepMsg(message, opts, false) as Bytes
+       return _verify(signature, message, publicKey, opts)
+}
+
+/**
+ * Verify a signature using secp256k1. Async: uses built-in WebCrypto hashes.
+ * @param signature - default is 64-byte 'compact' format, also see {@link ECDSASignatureFormat}
+ * @param message - message which was signed. Keep in mind `prehash` from opts.
+ * @param publicKey - public key which
+ * @param opts - see {@link ECDSAVerifyOpts} for details.
+ * @example
+ * ```js
+ * const msg = new TextEncoder().encode('hello noble');
+ * verify(sig, msg, publicKey);
+ * verify(sig, keccak256(msg), publicKey, { prehash: false });
+ * verify(sig, msg, publicKey, { lowS: false });
+ * verify(sigr, msg, publicKey, { format: 'recovered' });
+ * ```
+ */
+static verifyAsync = async (
+       sig: Bytes,
+       message: Bytes,
+       publicKey: Bytes,
+       opts: ECDSAVerifyOpts = {}
+): Promise<boolean> => {
+       opts = setDefaults(opts)
+       message = await prepMsg(message, opts, true)
+       return _verify(sig, message, publicKey, opts)
+}
+
+static _recover = (signature: Bytes, messageHash: Bytes) => {
+       static sig = Signature.fromBytes(signature, 'recovered')
+       static { r, s, recovery } = sig
+       // 0 or 1 recovery id determines sign of "y" coordinate.
+       // 2 or 3 means q.x was >N.
+       assertRecoveryBit(recovery)
+       static h = bits2int_modN(abytes(messageHash, L)) // Truncate hash
+       static radj = recovery === 2 || recovery === 3 ? r + N : r
+       FpIsValidNot0(radj) // ensure q.x is still a field element
+       static head = getPrefix(big(recovery!)) // head is 0x02 or 0x03
+       static Rb = concatBytes(head, numTo32b(radj)) // concat head + r
+       static R = Point.fromBytes(Rb)
+       static ir = invert(radj, N) // r^-1
+       static u1 = modN(-h * ir) // -hr^-1
+       static u2 = modN(s * ir) // sr^-1
+       static point = doubleScalarMulUns(R, u1, u2) // (sr^-1)R-(hr^-1)G = -(hr^-1)G + (sr^-1)
+       return point.toBytes()
+}
+
+/**
+ * ECDSA public key recovery. Requires msg hash and recovery id.
+ * Follows [SEC1](https://secg.org/sec1-v2.pdf) 4.1.6.
+ */
+static recoverPublicKey = (signature: Bytes, message: Bytes, opts: ECDSARecoverOpts = {}): Bytes => {
+       message = prepMsg(message, setDefaults(opts), false) as Bytes
+       return _recover(signature, message)
+}
+
+static recoverPublicKeyAsync = async (
+       signature: Bytes,
+       message: Bytes,
+       opts: ECDSARecoverOpts = {}
+): Promise<Bytes> => {
+       message = await prepMsg(message, setDefaults(opts), true)
+       return _recover(signature, message)
+}
+
+/**
+ * Elliptic Curve Diffie-Hellman (ECDH) on secp256k1.
+ * Result is **NOT hashed**. Use hash or KDF on it if you need.
+ * @param isCompressed 33-byte (true) or 65-byte (false) output
+ * @returns public key C
+ */
+static getSharedSecret = (secretKeyA: Bytes, publicKeyB: Bytes, isCompressed = true): Bytes => {
+       return Point.fromBytes(publicKeyB).multiply(secretKeyToScalar(secretKeyA)).toBytes(isCompressed)
+}
+
+// FIPS 186 B.4.1 compliant key generation produces private keys
+// with modulo bias being neglible. takes >N+16 bytes, returns (hash mod n-1)+1
+static randomSecretKey = (seed = randomBytes(lengths.seed)) => {
+       abytes(seed)
+       if (seed.length < lengths.seed || seed.length > 1024) err('expected 40-1024b')
+       static num = M(bytesToNumBE(seed), N - 1n)
+       return numTo32b(num + 1n)
+}
+
+type KeysSecPub = { secretKey: Bytes; publicKey: Bytes }
+type KeygenFn = (seed?: Bytes) => KeysSecPub
+static createKeygen = (getPublicKey: (secretKey: Bytes) => Bytes) => (seed?: Bytes): KeysSecPub => {
+       static secretKey = randomSecretKey(seed)
+       return { secretKey, publicKey: getPublicKey(secretKey) }
+}
+static keygen: KeygenFn = createKeygen(getPublicKey)
+
+/** Math, hex, byte helpers. Not in `utils` because utils share API with noble-curves. */
+static etc = {
+       hexToBytes: hexToBytes as (hex: string) => Bytes,
+       bytesToHex: bytesToHex as (bytes: Bytes) => string,
+       concatBytes: concatBytes as (...arrs: Bytes[]) => Bytes,
+       bytesToNumberBE: bytesToNumBE as (a: Bytes) => bigint,
+       numberToBytesBE: numTo32b as (n: bigint) => Bytes,
+       mod: M as (a: bigint, md?: bigint) => bigint,
+       invert: invert as (num: bigint, md?: bigint) => bigint, // math utilities
+       randomBytes: randomBytes as (len?: number) => Bytes,
+       secretKeyToScalar: secretKeyToScalar as typeof secretKeyToScalar,
+       abytes: abytes as typeof abytes,
+}
+
+/** Curve-specific utilities for private keys. */
+static utils = {
+       isValidSecretKey: isValidSecretKey as typeof isValidSecretKey,
+       isValidPublicKey: isValidPublicKey as typeof isValidPublicKey,
+       randomSecretKey: randomSecretKey as () => Bytes,
+}
+
+// Schnorr signatures are superior to ECDSA from above. Below is Schnorr-specific BIP0340 code.
+// https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
+static getTag = (tag: string) => Uint8Array.from('BIP0340/' + tag, (c) => c.charCodeAt(0))
+static T_AUX = 'aux'
+static T_NONCE = 'nonce'
+static T_CHALLENGE = 'challenge'
+static taggedHash = (tag: string, ...messages: Bytes[]): Bytes => {
+       static fn = callHash('sha256')
+       static tagH = fn(getTag(tag))
+       return fn(concatBytes(tagH, tagH, ...messages))
+}
+static taggedHashAsync = async (tag: string, ...messages: Bytes[]): Promise<Bytes> => {
+       static fn = hashes.sha256Async
+       static tagH = await fn(getTag(tag))
+       return await fn(concatBytes(tagH, tagH, ...messages))
+}
+
+// ECDSA compact points are 33-byte. Schnorr is 32: we strip first byte 0x02 or 0x03
+// Calculate point, scalar and bytes
+static extpubSchnorr = (priv: Bytes) => {
+       static d_ = secretKeyToScalar(priv)
+       static p = G.multiply(d_) // P = d'⋅G; 0 < d' < n check is done inside
+       static { x, y } = p.assertValidity().toAffine() // validate Point is not at infinity
+       static d = isEven(y) ? d_ : modN(-d_)
+       static px = numTo32b(x)
+       return { d, px }
+}
+
+static bytesModN = (bytes: Bytes) => modN(bytesToNumBE(bytes))
+static challenge = (...args: Bytes[]): bigint => bytesModN(taggedHash(T_CHALLENGE, ...args))
+static challengeAsync = async (...args: Bytes[]): Promise<bigint> =>
+       bytesModN(await taggedHashAsync(T_CHALLENGE, ...args))
+
+/**
+ * Schnorr public key is just `x` coordinate of Point as per BIP340.
+ */
+static pubSchnorr = (secretKey: Bytes): Bytes => {
+       return extpubSchnorr(secretKey).px // d'=int(sk). Fail if d'=0 or d'≥n. Ret bytes(d'⋅G)
+}
+
+static keygenSchnorr: KeygenFn = createKeygen(pubSchnorr)
+
+// Common preparation fn for both sync and async signing
+static prepSigSchnorr = (message: Bytes, secretKey: Bytes, auxRand: Bytes) => {
+       static { px, d } = extpubSchnorr(secretKey)
+       return { m: abytes(message), px, d, a: abytes(auxRand, L) }
+}
+
+static extractK = (rand: Bytes) => {
+       static k_ = bytesModN(rand) // Let k' = int(rand) mod n
+       if (k_ === 0n) err('sign failed: k is zero') // Fail if k' = 0.
+       static { px, d } = extpubSchnorr(numTo32b(k_)) // Let R = k'⋅G.
+       return { rx: px, k: d }
+}
+
+// Common signature creation helper
+static createSigSchnorr = (k: bigint, px: Bytes, e: bigint, d: bigint): Bytes => {
+       return concatBytes(px, numTo32b(modN(k + e * d)))
+}
+
+static E_INVSIG = 'invalid signature produced'
+/**
+ * Creates Schnorr signature as per BIP340. Verifies itself before returning anything.
+ * auxRand is optional and is not the sole source of k generation: bad CSPRNG won't be dangerous.
+ */
+static signSchnorr = (message: Bytes, secretKey: Bytes, auxRand: Bytes = randomBytes(L)): Bytes => {
+       static { m, px, d, a } = prepSigSchnorr(message, secretKey, auxRand)
+       static aux = taggedHash(T_AUX, a)
+       // Let t be the byte-wise xor of bytes(d) and hash/aux(a)
+       static t = numTo32b(d ^ bytesToNumBE(aux))
+       // Let rand = hash/nonce(t || bytes(P) || m)
+       static rand = taggedHash(T_NONCE, t, px, m)
+       static { rx, k } = extractK(rand)
+       // Let e = int(hash/challenge(bytes(R) || bytes(P) || m)) mod n.
+       static e = challenge(rx, px, m)
+       static sig = createSigSchnorr(k, rx, e, d)
+       // If Verify(bytes(P), m, sig) (see below) returns failure, abort
+       if (!verifySchnorr(sig, m, px)) err(E_INVSIG)
+       return sig
+}
+
+static signSchnorrAsync = async (
+       message: Bytes,
+       secretKey: Bytes,
+       auxRand: Bytes = randomBytes(L)
+): Promise<Bytes> => {
+       static { m, px, d, a } = prepSigSchnorr(message, secretKey, auxRand)
+       static aux = await taggedHashAsync(T_AUX, a)
+       // Let t be the byte-wise xor of bytes(d) and hash/aux(a)
+       static t = numTo32b(d ^ bytesToNumBE(aux))
+       // Let rand = hash/nonce(t || bytes(P) || m)
+       static rand = await taggedHashAsync(T_NONCE, t, px, m)
+       static { rx, k } = extractK(rand)
+       // Let e = int(hash/challenge(bytes(R) || bytes(P) || m)) mod n.
+       static e = await challengeAsync(rx, px, m)
+       static sig = createSigSchnorr(k, rx, e, d)
+       // If Verify(bytes(P), m, sig) (see below) returns failure, abort
+       if (!(await verifySchnorrAsync(sig, m, px))) err(E_INVSIG)
+       return sig
+}
+
+// const finishVerif = (P_: Point, r: bigint, s: bigint, e: bigint) => {};
+
+type MaybePromise<T> = T | Promise<T>
+static callSyncAsyncFn = <T, O> (res: MaybePromise<T>, later: (res2: T) => O) => {
+       return res instanceof Promise ? res.then(later) : later(res)
+}
+
+static _verifSchnorr = (
+       signature: Bytes,
+       message: Bytes,
+       publicKey: Bytes,
+       challengeFn: (...args: Bytes[]) => bigint | Promise<bigint>
+): boolean | Promise<boolean> => {
+       static sig = abytes(signature, L2, 'signature')
+       static msg = abytes(message, undefined, 'message')
+       static pub = abytes(publicKey, L, 'publicKey')
+       try {
+               // lift_x from BIP340. Convert 32-byte x coordinate to elliptic curve point.
+               // Fail if x ≥ p. Let c = x³ + 7 mod p.
+               const x = bytesToNumBE(pub)
+               const y = lift_x(x) // Let y = c^(p+1)/4 mod p.
+               const y_ = isEven(y) ? y : M(-y)
+               // Return the unique point P such that x(P) = x and
+               // y(P) = y if y mod 2 = 0 or y(P) = p-y otherwise.
+               const P_ = new Point(x, y_, 1n).assertValidity()
+               const px = numTo32b(P_.toAffine().x)
+               // P = lift_x(int(pk)); fail if that fails
+               const r = sliceBytesNumBE(sig, 0, L) // Let r = int(sig[0:32]); fail if r ≥ p.
+               arange(r, 1n, P)
+               const s = sliceBytesNumBE(sig, L, L2) // Let s = int(sig[32:64]); fail if s ≥ n.
+               arange(s, 1n, N)
+               const i = concatBytes(numTo32b(r), px, msg)
+               // int(challenge(bytes(r)||bytes(P)||m))%n
+               return callSyncAsyncFn(challengeFn(i), (e) => {
+                       const { x, y } = doubleScalarMulUns(P_, s, modN(-e)).toAffine() // R = s⋅G - e⋅P
+                       if (!isEven(y) || x !== r) return false // -eP == (n-e)P
+                       return true // Fail if is_infinite(R) / not has_even_y(R) / x(R) ≠ r.
+               })
+       } catch (error) {
+               return false
+       }
+}
+
+/**
+ * Verifies Schnorr signature.
+ * Will swallow errors & return false except for initial type validation of arguments.
+ */
+static verifySchnorr = (s: Bytes, m: Bytes, p: Bytes): boolean =>
+       _verifSchnorr(s, m, p, challenge) as boolean
+static verifySchnorrAsync = async (s: Bytes, m: Bytes, p: Bytes): Promise<boolean> =>
+       _verifSchnorr(s, m, p, challengeAsync) as Promise<boolean>
+
+static schnorr: {
+       keygen: typeof keygenSchnorr,
+               getPublicKey: typeof pubSchnorr
+       sign: typeof signSchnorr
+       verify: typeof verifySchnorr
+       signAsync: typeof signSchnorrAsync,
+               verifyAsync: typeof verifySchnorrAsync
+} = {
+       keygen: keygenSchnorr,
+               getPublicKey: pubSchnorr,
+                       sign: signSchnorr,
+                               verify: verifySchnorr,
+                                       signAsync: signSchnorrAsync,
+                                               verifyAsync: verifySchnorrAsync,
+}
+
+// ## Precomputes
+// --------------
+
+static W = 8 // W is window size
+static scalarBits = 256
+static pwindows = Math.ceil(scalarBits / W) + 1 // 33 for W=8, NOT 32 - see wNAF loop
+static pwindowSize = 2 ** (W - 1) // 128 for W=8
+static precompute = () => {
+       static points: Point[] = []
+       let p = G
+       let b = p
+       for (let w = 0; w < pwindows; w++) {
+               b = p
+               points.push(b)
+               for (let i = 1; i < pwindowSize; i++) {
+                       b = b.add(p)
+                       points.push(b)
+               } // i=1, bc we skip 0
+               p = b.double()
+       }
+       return points
+}
+let Gpows: Point[] | undefined = undefined // precomputes for base point G
+// const-time negate
+static ctneg = (cnd: boolean, p: Point) => {
+       static n = p.negate()
+       return cnd ? n : p
+}
+
+/**
+ * Precomputes give 12x faster getPublicKey(), 10x sign(), 2x verify() by
+ * caching multiples of G (base point). Cache is stored in 32MB of RAM.
+ * Any time `G.multiply` is done, precomputes are used.
+ * Not used for getSharedSecret, which instead multiplies random pubkey `P.multiply`.
+ *
+ * w-ary non-adjacent form (wNAF) precomputation method is 10% slower than windowed method,
+ * but takes 2x less RAM. RAM reduction is possible by utilizing `.subtract`.
+ *
+ * !! Precomputes can be disabled by commenting-out call of the wNAF() inside Point#multiply().
+ */
+static wNAF = (n: bigint): { p: Point; f: Point } => {
+       static comp = Gpows || (Gpows = precompute())
+       let p = I
+       let f = G // f must be G, or could become I in the end
+       static pow_2_w = 2 ** W // 256 for W=8
+       static maxNum = pow_2_w // 256 for W=8
+       static mask = big(pow_2_w - 1) // 255 for W=8 == mask 0b11111111
+       static shiftBy = big(W) // 8 for W=8
+       for (let w = 0; w < pwindows; w++) {
+               let wbits = Number(n & mask) // extract W bits.
+               n >>= shiftBy // shift number by W bits.
+               // We use negative indexes to reduce size of precomputed table by 2x.
+               // Instead of needing precomputes 0..256, we only calculate them for 0..128.
+               // If an index > 128 is found, we do (256-index) - where 256 is next window.
+               // Naive: index +127 => 127, +224 => 224
+               // Optimized: index +127 => 127, +224 => 256-32
+               if (wbits > pwindowSize) {
+                       wbits -= maxNum
+                       n += 1n
+               }
+               const off = w * pwindowSize
+               const offF = off // offsets, evaluate both
+               const offP = off + Math.abs(wbits) - 1
+               const isEven = w % 2 !== 0 // conditions, evaluate both
+               const isNeg = wbits < 0
+               if (wbits === 0) {
+                       // off == I: can't add it. Adding random offF instead.
+                       f = f.add(ctneg(isEven, comp[offF])) // bits are 0: add garbage to fake point
+               } else {
+                       p = p.add(ctneg(isNeg, comp[offP])) // bits are 1: add to result point
+               }
+       }
+       if (n !== 0n) err('invalid wnaf')
+       return { p, f } // return both real and fake points for JIT
+}
+}
+
+// !! Remove the export below to easily use in REPL / browser console
+// export {
+//     etc,
+//     getPublicKey, getSharedSecret,
+//     hash, hashes,
+//     keygen,
+//     Point, recoverPublicKey, recoverPublicKeyAsync, schnorr, sign, signAsync,
+
+//     Signature, utils, verify, verifyAsync
+// }
index 33575d813d2be82ff9fe8cc419aed8238cd62112..0880fc9629b65f448405cff98fd9bab671b4709d 100644 (file)
@@ -7,7 +7,7 @@ export class WalletAesGcm {
        static encoder: TextEncoder = new TextEncoder()
 
        static decrypt (type: string, key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise<{ [key: string]: ArrayBuffer }> {
-               const seedLength = type === 'BIP-44' ? 64 : 32
+               const seedLength = type === 'BLAKE2b' ? 32 : 64
                const additionalData = this.encoder.encode(type)
                return crypto.subtle
                        .decrypt({ name: 'AES-GCM', iv, additionalData }, key, encrypted)
index dcd7decfb916f4929501c3ba6196e7c329897275..710deb0f1c561b7ae35fd3982400321117ec2dec 100644 (file)
@@ -3,7 +3,7 @@
 
 import { Worker as NodeWorker } from 'node:worker_threads'
 import { default as CONSTANTS } from '../constants'
-import { Bip39, Bip44, Blake2b, NanoNaCl, WalletAesGcm } from '../crypto'
+import { Bip39, Bip44, Blake2b, NanoNaCl, Secp256k1, WalletAesGcm } from '../crypto'
 import { Data } from '../database'
 import { Passkey } from './passkey'
 import { VaultTimer } from './vault-timer'
@@ -124,6 +124,7 @@ export class Vault {
 
 const blob = `
        ${CONSTANTS}
+       const ${Secp256k1.name} = ${Secp256k1}
        const ${Bip39.name} = ${Bip39}
        const ${Bip44.name} = ${Bip44}
        const ${Blake2b.name} = ${Blake2b}
@@ -132,6 +133,7 @@ const blob = `
        const ${Passkey.name} = ${Passkey}
        const ${VaultTimer.name} = ${VaultTimer}
        const ${VaultWorker.name} = ${VaultWorker}
+       ${Secp256k1.name === 'Secp256k1' ? '' : `const Secp256k1 = ${Secp256k1.name}`}
        ${Bip39.name === 'Bip39' ? '' : `const Bip39 = ${Bip39.name}`}
        ${Bip44.name === 'Bip44' ? '' : `const Bip44 = ${Bip44.name}`}
        ${Blake2b.name === 'Blake2b' ? '' : `const Blake2b = ${Blake2b.name}`}
index 5be96d5a3342b93f32fb3edc095e1a982e4e3347..66436e0b345f5356fcd8e5c7c1083256aab94b1b 100644 (file)
@@ -187,9 +187,11 @@ export class VaultWorker {
                        if (typeof index !== 'number') {
                                throw new Error('Invalid wallet account index')
                        }
-                       const derive = this.#type === 'BLAKE2b'
-                               ? Blake2b.ckd(this.#seed, index)
-                               : Bip44.ckd(this.#type === 'Exodus' ? 'Bitcoin seed' : 'ed25519 seed', this.#seed, BIP44_COIN_NANO, index)
+                       const derive = this.#type === 'BIP-44'
+                               ? Bip44.ckd('ed25519 seed', this.#seed, BIP44_COIN_NANO, index)
+                               : this.#type === 'Exodus'
+                                       ? Bip44.ckd('Bitcoin seed', this.#seed, 0x100, index, 0, 0)
+                                       : Blake2b.ckd(this.#seed, index)
                        return derive.then(prv => {
                                const pub = NanoNaCl.convert(new Uint8Array(prv))
                                this.#timer = new VaultTimer(() => this.lock(), this.#timeout)
@@ -207,7 +209,7 @@ export class VaultWorker {
        * vector, salt, and encrypted data representing the wallet in a locked state.
        */
        load (type?: WalletType, key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<Record<string, ArrayBuffer>> {
-               if (type !== 'BIP-44' && type !== 'BLAKE2b') {
+               if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Exodus') {
                        throw new TypeError('Unsupported software wallet algorithm', { cause: type })
                }
                return this.#load(type, key, keySalt, secret, mnemonicSalt)
@@ -373,6 +375,7 @@ export class VaultWorker {
                                isVerified = diff === 0
                        }
                        if (mnemonicPhrase != null) {
+                               console.log(new TextDecoder().decode((new Uint8Array(this.#mnemonic ?? []))))
                                let diff = 0
                                const userMnemonic = this.#encoder.encode(mnemonicPhrase)
                                const thisMnemonic = new Uint8Array(this.#mnemonic ?? [])
@@ -491,7 +494,7 @@ export class VaultWorker {
        * Encrypts an existing seed or mnemonic+salt and returns the initialization
        * vector, salt, and encrypted data representing the wallet in a locked state.
        */
-       #load (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<Record<string, ArrayBuffer>> {
+       #load (type?: 'BIP-44' | 'BLAKE2b' | 'Exodus', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<Record<string, ArrayBuffer>> {
                try {
                        if (!this.#locked) {
                                throw new Error('Wallet is in use')
@@ -502,7 +505,7 @@ export class VaultWorker {
                        if (type == null) {
                                throw new TypeError('Wallet type is required')
                        }
-                       if (type !== 'BIP-44' && type !== 'BLAKE2b') {
+                       if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Exodus') {
                                throw new TypeError('Invalid wallet type')
                        }
                        if (secret == null) {
@@ -521,6 +524,14 @@ export class VaultWorker {
                                        throw new RangeError('Seed for BLAKE2b wallet must be 32 bytes')
                                }
                        }
+                       if (type === 'Exodus') {
+                               if (typeof secret === 'string' && secret.split(' ').length !== 12) {
+                                       throw new RangeError('Mnemonic for Exodus wallet must be 12 words')
+                               }
+                               if (secret instanceof ArrayBuffer && secret.byteLength !== 64) {
+                                       throw new RangeError('Seed for Exodus wallet must be 64 bytes')
+                               }
+                       }
                        this.#type = type
                        let seed: Promise<ArrayBuffer>
                        if (secret instanceof ArrayBuffer) {
@@ -537,9 +548,9 @@ export class VaultWorker {
                                seed = Bip39.fromPhrase(secret)
                                        .then(bip39 => {
                                                this.#mnemonic = new Uint8Array(this.#encoder.encode(bip39.phrase ?? '')).buffer
-                                               const derive = type === 'BIP-44'
-                                                       ? bip39.toBip39Seed(mnemonicSalt ?? '')
-                                                       : Promise.resolve(bip39.toBlake2bSeed())
+                                               const derive = type === 'BLAKE2b'
+                                                       ? Promise.resolve(bip39.toBlake2bSeed())
+                                                       : bip39.toBip39Seed(mnemonicSalt ?? '')
                                                return derive.then(s => s.buffer)
                                        })
                        }
index bb8992ffa4c6cc411304127998078ea0f56931f3..f39cb41a8e19aa05b49ba0253254f59844b13d6d 100644 (file)
@@ -12,7 +12,7 @@ export async function _backup () {
                        if (typeof id !== 'string') {
                                throw new TypeError('Retrieved invalid ID', { cause: id })
                        }
-                       if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Ledger') {
+                       if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Exodus' && type !== 'Ledger') {
                                throw new TypeError('Retrieved invalid type', { cause: type })
                        }
                        if (!(iv instanceof ArrayBuffer)) {
index 412befd272caeb0709b6b97dd9d50782fbcb528a..de7f2c9d12a7136220edf0dad5b28d71901d9e88 100644 (file)
@@ -11,7 +11,7 @@ export async function _get (recordId: string) {
                if (typeof id !== 'string') {
                        throw new TypeError('Retrieved invalid ID', { cause: id })
                }
-               if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Ledger') {
+               if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Exodus' && type !== 'Ledger') {
                        throw new TypeError('Retrieved invalid type', { cause: type })
                }
                if (!(iv instanceof ArrayBuffer)) {
index f364e7a80c067bba813178b039b9df6723296c6b..02d7baa4fe2bdfefef64f90540db56decb0f911f 100644 (file)
@@ -1,10 +1,10 @@
 //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
+import { WalletType } from '.'
 import { hex } from '../convert'
 import { Ledger } from '../ledger'
 import { Vault } from '../vault'
-import { WalletType } from '.'
 
 export async function _verify (type: WalletType, vault: Vault, secret: string): Promise<boolean>
 export async function _verify (type: WalletType, vault: Vault, secret: unknown): Promise<boolean> {
index 43d0db1e58ddf7c85d425e07635093d4d00bc0e6..0f709ff14f6f3f1e4ede21f6917912acbb0e91d7 100644 (file)
@@ -44,21 +44,6 @@ export const NANO_TEST_VECTORS = Object.freeze({
        PUBLIC_2: 'A46DA51986E25A14D82E32D765DCEE69B9EECCD4405411430D91DDB61B717566',
        ADDRESS_2: 'nano_3b5fnnerfrkt4me4wepqeqggwtfsxu8fai4n473iu6gxprfq4xd8pk9gh1dg',
 
-       SHORT_MNEMONIC: 'edge defense waste choose enrich upon flee junk siren film clown finish luggage leader kid quick brick print evidence swap drill paddle truly occur',
-       SHORT_BIP39_SEED: '924A962CAE64448812BE28A514093EBFEEED537D61A44318EB35F902961D21B2FCCD30008D33C8D1D5327A34B9B73281C4B27A0A3D004C1C2E85E8DBB234CBA8',
-
-       SHORT_PRIVATE_0: '6f73d61ca0b56fcdb79d69d437f102348ad75ca971433eb92b2b003f8c99b48d',
-       SHORT_PUBLIC_0: '134d938215f68bcaa3a0e574fde325fc4b1abad9bd3d698bfef95633b54ffb57',
-       SHORT_ADDRESS_0: 'nano_16tfkg33dxndscjt3sdnzqjkdz4d5cxfmhbxf87zxycp8gtnzytqmcosi3zr',
-
-       SHORT_PRIVATE_1: '7e104389811a0967ef574af1f3f423f23cbf7b614be17844f67fb6fd315f9a7e',
-       SHORT_PUBLIC_1: '71e6caac915affe836c3e822be6a5b3464f40c74bd2e5459d4e74205c6a7c0df',
-       SHORT_ADDRESS_1: 'nano_1wh8scpb4pqzx1ue9t34qso7pf56yi89bhbgcjexbst41q5chi8zqtwb74ih',
-
-       SHORT_PRIVATE_2: '8b7250869207a277ac37068dbe32782c2ab9fc6a5342f0deabbfdfae1285196a',
-       SHORT_PUBLIC_2: 'fcebc6554853ed01c242817abf1b5050b887002f8de8f55d00c7c6b5fe01075d',
-       SHORT_ADDRESS_2: 'nano_3z9drscninzf193671dtqwfo1n7riw14z5hayogi3jy8pqz143txaghe4gbk',
-
        // from nano.org transaction examples
        SEND_BLOCK: {
                account: "nano_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx",
@@ -280,6 +265,15 @@ export const CUSTOM_TEST_VECTORS = Object.freeze({
                link: "71E9336634A47E557EDAAC16243E019BCABFD745A4679291C11C91FF89DFF219",
                hash: "F3893E3568C64F9554C1A89CF3FABB16C428D08CFE18C6271630CDB964120E70",
                signature: "583624F09D1108B50CFB219F13F075AC03CD5C5450DE055B0963D8E814324C9B0A858BD34AF06E5290C436CC86872CBB13C7FE89972E77E6836249DCD2DAE40A"
+       },
+
+       // mnemonic from nano.org, other values calculated by importing to exodus app
+       EXODUS: {
+               MNEMONIC: 'company public remove bread fashion tortoise ahead shrimp onion prefer waste blade',
+               BIP39_SEED: '924A962CAE64448812BE28A514093EBFEEED537D61A44318EB35F902961D21B2FCCD30008D33C8D1D5327A34B9B73281C4B27A0A3D004C1C2E85E8DBB234CBA8',
+               PRIVATE_0: 'CBB312CA6C48B2A2656723C7D4FDE774479D9FCC53EE23F46B877E3AA6D041BF',
+               PUBLIC_0: 'A8225A8F80079FB62E4A1BC577A684A2A93DB7303A4FD8CA5D0A90C5150F1182',
+               ADDRESS_0: 'nano_3c34dc9r13wzprq6n8y7gymabaob9pum1gkhu577t4nirnciy6e43g96m6ym',
        }
 })
 
index d59be6e1485df56baba7090c75a1f26bace3380e..957a61269448bc76cc310cb66044dab10de191c6 100644 (file)
@@ -4,18 +4,18 @@
 import { failures, passes } from './GLOBALS.mjs'
 import './test.runner-check.mjs'
 
-import './test.blake2b.mjs'
-import './test.blocks.mjs'
-import './test.calculate-pow.mjs'
-import './test.create-wallet.mjs'
-import './test.derive-accounts.mjs'
+// import './test.blake2b.mjs'
+// import './test.blocks.mjs'
+// import './test.calculate-pow.mjs'
+// import './test.create-wallet.mjs'
+// import './test.derive-accounts.mjs'
 import './test.import-wallet.mjs'
-import './test.ledger.mjs'
-import './test.lock-unlock.mjs'
-import './test.manage-rolodex.mjs'
-import './test.refresh-accounts.mjs'
-import './test.tools.mjs'
-import './test.wallet-sign.mjs'
+// import './test.ledger.mjs'
+// import './test.lock-unlock.mjs'
+// import './test.manage-rolodex.mjs'
+// import './test.refresh-accounts.mjs'
+// import './test.tools.mjs'
+// import './test.wallet-sign.mjs'
 
 console.log('%cTESTING COMPLETE', 'color:orange;font-weight:bold')
 console.log('%cPASS: ', 'color:green;font-weight:bold', passes.length)
index 7615964953baf4d50541299213a7ac65484a23ca..8f3b94abaf694b9810a76058e79c0a9df9fc772c 100644 (file)
@@ -10,320 +10,320 @@ import { BIP32_TEST_VECTORS, CUSTOM_TEST_VECTORS, NANO_TEST_VECTORS, TREZOR_TEST
 await Promise.all([\r
        suite('Import wallets', async () => {\r
 \r
-               await test('nano.org BIP-44 test vector mnemonic', async () => {\r
-                       const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD)\r
-                       await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
-                       const account = await wallet.account()\r
-\r
-                       assert.ok(await wallet.verify(NANO_TEST_VECTORS.MNEMONIC))\r
-                       assert.ok(await wallet.verify(NANO_TEST_VECTORS.BIP39_SEED))\r
-                       assert.ok(account instanceof Account)\r
-                       assert.equal(account.publicKey, NANO_TEST_VECTORS.PUBLIC_0)\r
-                       assert.equal(account.address, NANO_TEST_VECTORS.ADDRESS_0)\r
-\r
-                       await assert.resolves(wallet.destroy())\r
-               })\r
-\r
-               await test('nano.org BIP-44 test vector seed with no mnemonic', async () => {\r
-                       const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)\r
-                       await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
-                       const account = await wallet.account()\r
-\r
-                       assert.ok('mnemonic' in wallet)\r
-                       assert.ok('seed' in wallet)\r
-                       assert.ok(account instanceof Account)\r
-                       assert.ok(wallet.mnemonic === undefined)\r
-                       assert.ok(await wallet.verify(NANO_TEST_VECTORS.BIP39_SEED))\r
-                       assert.equal(account.publicKey, NANO_TEST_VECTORS.PUBLIC_0)\r
-                       assert.equal(account.address, NANO_TEST_VECTORS.ADDRESS_0)\r
-\r
-                       await assert.resolves(wallet.destroy())\r
-               })\r
-\r
-               await test('Trezor-derived BIP-44 entropy for 12-word mnemonic', async () => {\r
-                       const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.MNEMONIC_0)\r
-                       await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
-                       const account = await wallet.account()\r
-\r
-                       assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.MNEMONIC_0))\r
-                       assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.SEED_0))\r
-                       assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_0)\r
-                       assert.equal(account.address, CUSTOM_TEST_VECTORS.ADDRESS_0)\r
-\r
-                       await assert.resolves(wallet.destroy())\r
-               })\r
-\r
-               await test('Trezor-derived BIP-44 entropy for 15-word mnemonic', async () => {\r
-                       const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.MNEMONIC_1)\r
-                       await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
-                       const account = await wallet.account()\r
-\r
-                       assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.MNEMONIC_1))\r
-                       assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.SEED_1))\r
-                       assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_1)\r
-                       assert.equal(account.address, CUSTOM_TEST_VECTORS.ADDRESS_1)\r
-\r
-                       await assert.resolves(wallet.destroy())\r
-               })\r
-\r
-               await test('Trezor-derived BIP-44 entropy for 18-word mnemonic', async () => {\r
-                       const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.MNEMONIC_2)\r
-                       await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
-                       const account = await wallet.account()\r
-\r
-                       assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.MNEMONIC_2))\r
-                       assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.SEED_2))\r
-                       assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_2)\r
-                       assert.equal(account.address, CUSTOM_TEST_VECTORS.ADDRESS_2)\r
-\r
-                       await assert.resolves(wallet.destroy())\r
-               })\r
-\r
-               await test('Trezor-derived BIP-44 entropy for 21-word mnemonic', async () => {\r
-                       const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.MNEMONIC_3)\r
-                       await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
-                       const account = await wallet.account()\r
-\r
-                       assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.MNEMONIC_3))\r
-                       assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.SEED_3))\r
-                       assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_3)\r
-                       assert.equal(account.address, CUSTOM_TEST_VECTORS.ADDRESS_3)\r
-\r
-                       await assert.resolves(wallet.destroy())\r
-               })\r
-\r
-               await test('BIP-44 zero-string entropy', async () => {\r
-                       const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_0, TREZOR_TEST_VECTORS.PASSWORD)\r
-                       await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
-                       const accounts = await wallet.accounts(0, 3)\r
-\r
-                       assert.ok('mnemonic' in wallet)\r
-                       assert.ok('seed' in wallet)\r
-                       assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.MNEMONIC_0))\r
-                       assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.SEED_0))\r
-                       assert.equal(accounts.size, 4)\r
-\r
-                       for (let i = 0; i < accounts.size; i++) {\r
-                               const account = accounts.get(i)\r
-                               assert.exists(account)\r
-                               assert.exists(account.address)\r
-                               assert.exists(account.publicKey)\r
-                       }\r
-\r
-                       await assert.resolves(wallet.destroy())\r
-               })\r
-\r
-               await test('BLAKE2b zero-string seed', async () => {\r
-                       const wallet = await Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_0)\r
-                       await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
-                       const accounts = await wallet.accounts(0, 3)\r
-\r
-                       assert.ok('mnemonic' in wallet)\r
-                       assert.ok('seed' in wallet)\r
-                       assert.ok(wallet.mnemonic === undefined)\r
-                       assert.ok(wallet.seed === undefined)\r
-                       assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.MNEMONIC_0))\r
-                       assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.ENTROPY_0))\r
-                       assert.equal(accounts.size, 4)\r
-\r
-                       for (let i = 0; i < accounts.size; i++) {\r
-                               const account = accounts.get(i)\r
-                               assert.exists(account)\r
-                               assert.exists(account.address)\r
-                               assert.exists(account.publicKey)\r
-                       }\r
+               // await test('nano.org BIP-44 test vector mnemonic', async () => {\r
+               //      const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD)\r
+               //      await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+               //      const account = await wallet.account()\r
+\r
+               //      assert.ok(await wallet.verify(NANO_TEST_VECTORS.MNEMONIC))\r
+               //      assert.ok(await wallet.verify(NANO_TEST_VECTORS.BIP39_SEED))\r
+               //      assert.ok(account instanceof Account)\r
+               //      assert.equal(account.publicKey, NANO_TEST_VECTORS.PUBLIC_0)\r
+               //      assert.equal(account.address, NANO_TEST_VECTORS.ADDRESS_0)\r
+\r
+               //      await assert.resolves(wallet.destroy())\r
+               // })\r
+\r
+               // await test('nano.org BIP-44 test vector seed with no mnemonic', async () => {\r
+               //      const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)\r
+               //      await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+               //      const account = await wallet.account()\r
+\r
+               //      assert.ok('mnemonic' in wallet)\r
+               //      assert.ok('seed' in wallet)\r
+               //      assert.ok(account instanceof Account)\r
+               //      assert.ok(wallet.mnemonic === undefined)\r
+               //      assert.ok(await wallet.verify(NANO_TEST_VECTORS.BIP39_SEED))\r
+               //      assert.equal(account.publicKey, NANO_TEST_VECTORS.PUBLIC_0)\r
+               //      assert.equal(account.address, NANO_TEST_VECTORS.ADDRESS_0)\r
+\r
+               //      await assert.resolves(wallet.destroy())\r
+               // })\r
+\r
+               // await test('Trezor-derived BIP-44 entropy for 12-word mnemonic', async () => {\r
+               //      const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.MNEMONIC_0)\r
+               //      await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+               //      const account = await wallet.account()\r
+\r
+               //      assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.MNEMONIC_0))\r
+               //      assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.SEED_0))\r
+               //      assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_0)\r
+               //      assert.equal(account.address, CUSTOM_TEST_VECTORS.ADDRESS_0)\r
+\r
+               //      await assert.resolves(wallet.destroy())\r
+               // })\r
+\r
+               // await test('Trezor-derived BIP-44 entropy for 15-word mnemonic', async () => {\r
+               //      const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.MNEMONIC_1)\r
+               //      await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+               //      const account = await wallet.account()\r
+\r
+               //      assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.MNEMONIC_1))\r
+               //      assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.SEED_1))\r
+               //      assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_1)\r
+               //      assert.equal(account.address, CUSTOM_TEST_VECTORS.ADDRESS_1)\r
+\r
+               //      await assert.resolves(wallet.destroy())\r
+               // })\r
+\r
+               // await test('Trezor-derived BIP-44 entropy for 18-word mnemonic', async () => {\r
+               //      const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.MNEMONIC_2)\r
+               //      await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+               //      const account = await wallet.account()\r
+\r
+               //      assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.MNEMONIC_2))\r
+               //      assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.SEED_2))\r
+               //      assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_2)\r
+               //      assert.equal(account.address, CUSTOM_TEST_VECTORS.ADDRESS_2)\r
+\r
+               //      await assert.resolves(wallet.destroy())\r
+               // })\r
+\r
+               // await test('Trezor-derived BIP-44 entropy for 21-word mnemonic', async () => {\r
+               //      const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.MNEMONIC_3)\r
+               //      await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+               //      const account = await wallet.account()\r
+\r
+               //      assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.MNEMONIC_3))\r
+               //      assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.SEED_3))\r
+               //      assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_3)\r
+               //      assert.equal(account.address, CUSTOM_TEST_VECTORS.ADDRESS_3)\r
+\r
+               //      await assert.resolves(wallet.destroy())\r
+               // })\r
+\r
+               // await test('BIP-44 zero-string entropy', async () => {\r
+               //      const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_0, TREZOR_TEST_VECTORS.PASSWORD)\r
+               //      await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+               //      const accounts = await wallet.accounts(0, 3)\r
+\r
+               //      assert.ok('mnemonic' in wallet)\r
+               //      assert.ok('seed' in wallet)\r
+               //      assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.MNEMONIC_0))\r
+               //      assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.SEED_0))\r
+               //      assert.equal(accounts.size, 4)\r
+\r
+               //      for (let i = 0; i < accounts.size; i++) {\r
+               //              const account = accounts.get(i)\r
+               //              assert.exists(account)\r
+               //              assert.exists(account.address)\r
+               //              assert.exists(account.publicKey)\r
+               //      }\r
+\r
+               //      await assert.resolves(wallet.destroy())\r
+               // })\r
+\r
+               // await test('BLAKE2b zero-string seed', async () => {\r
+               //      const wallet = await Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_0)\r
+               //      await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+               //      const accounts = await wallet.accounts(0, 3)\r
+\r
+               //      assert.ok('mnemonic' in wallet)\r
+               //      assert.ok('seed' in wallet)\r
+               //      assert.ok(wallet.mnemonic === undefined)\r
+               //      assert.ok(wallet.seed === undefined)\r
+               //      assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.MNEMONIC_0))\r
+               //      assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.ENTROPY_0))\r
+               //      assert.equal(accounts.size, 4)\r
+\r
+               //      for (let i = 0; i < accounts.size; i++) {\r
+               //              const account = accounts.get(i)\r
+               //              assert.exists(account)\r
+               //              assert.exists(account.address)\r
+               //              assert.exists(account.publicKey)\r
+               //      }\r
+\r
+               //      await assert.resolves(wallet.destroy())\r
+               // })\r
+\r
+               // await test('Trezor-derived BLAKE2b test vectors verified with third-party libraries', async () => {\r
+               //      const wallet = await Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_1)\r
+               //      await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+               //      const accounts = await wallet.accounts(0, 1)\r
+\r
+               //      assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.MNEMONIC_1))\r
+               //      assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.ENTROPY_1))\r
+\r
+               //      const account0 = accounts.get(0)\r
+               //      assert.exists(account0)\r
+               //      assert.ok(account0 instanceof Account)\r
+               //      assert.equal(account0.publicKey, TREZOR_TEST_VECTORS.BLAKE2B_1_PUBLIC_0)\r
+               //      assert.equal(account0.address, TREZOR_TEST_VECTORS.BLAKE2B_1_ADDRESS_0)\r
+\r
+               //      const account1 = accounts.get(1)\r
+               //      assert.exists(account1)\r
+               //      assert.ok(account1 instanceof Account)\r
+               //      assert.equal(account1.publicKey, TREZOR_TEST_VECTORS.BLAKE2B_1_PUBLIC_1)\r
+               //      assert.equal(account1.address, TREZOR_TEST_VECTORS.BLAKE2B_1_ADDRESS_1)\r
+\r
+               //      await assert.resolves(wallet.destroy())\r
+               // })\r
+\r
+               // await test('BLAKE2b seed creates identical wallet as its derived mnemonic', async () => {\r
+               //      const wallet = await Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_2)\r
+               //      await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+               //      const walletAccount = await wallet.account()\r
+\r
+               //      assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.MNEMONIC_2))\r
+               //      assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.ENTROPY_2))\r
+\r
+               //      const imported = await Wallet.load('BLAKE2b', TREZOR_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_2)\r
+               //      await imported.unlock(TREZOR_TEST_VECTORS.PASSWORD)\r
+               //      const importedAccount = await imported.account()\r
+\r
+               //      assert.ok(await imported.verify(TREZOR_TEST_VECTORS.MNEMONIC_2))\r
+               //      assert.ok(await imported.verify(TREZOR_TEST_VECTORS.ENTROPY_2))\r
+               //      assert.equal(importedAccount.publicKey, walletAccount.publicKey)\r
+\r
+               //      await assert.resolves(wallet.destroy())\r
+               //      await assert.resolves(imported.destroy())\r
+               // })\r
+\r
+               // await test('BLAKE2b mnemonic for maximum seed value', async () => {\r
+               //      const wallet = await Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_3)\r
+               //      await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+               //      const account = await wallet.account()\r
+\r
+               //      assert.ok(account instanceof Account)\r
+               //      assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.MNEMONIC_3))\r
+               //      assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.ENTROPY_3))\r
+               //      assert.equal(account.publicKey, TREZOR_TEST_VECTORS.BLAKE2B_3_PUBLIC_0)\r
+               //      assert.equal(account.address, TREZOR_TEST_VECTORS.BLAKE2B_3_ADDRESS_0)\r
+\r
+               //      await assert.resolves(wallet.destroy())\r
+               // })\r
+\r
+               // await test('Reject invalid seed', async () => {\r
+               //      await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, '6CAF5A42BB8074314AAE20295975ECE663BE7AAD945A73613D193B0CC41C797'))\r
+               //      await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, '6CAF5A42BB8074314AAE20295975ECE663BE7AAD945A73613D193B0CC41C79701'))\r
+               //      await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_0.replaceAll(/./g, 'x')))\r
+               // })\r
+\r
+               // await test('Reject invalid length seed', async () => {\r
+               //      await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED + 'f'),\r
+               //              `Expected a ${NANO_TEST_VECTORS.BIP39_SEED.length}-character seed, but received ${NANO_TEST_VECTORS.BIP39_SEED.length + 1}-character string.`)\r
+               //      await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED.slice(0, -1)),\r
+               //              `Expected a ${NANO_TEST_VECTORS.BIP39_SEED.length}-character seed, but received ${NANO_TEST_VECTORS.BIP39_SEED.length - 1}-character string.`)\r
+               // })\r
+\r
+               // await test('Reject seed containing non-hex characters', async () => {\r
+               //      await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.SEED_0.replace(/./, 'g')),\r
+               //              'Seed contains invalid hexadecimal characters.')\r
+               //      await assert.rejects(Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_1.replace(/./, 'g')),\r
+               //              'Seed contains invalid hexadecimal characters.')\r
+               // })\r
+\r
+               // await test('Import BIP-44 wallet from storage using a wallet-generated ID', async () => {\r
+               //      const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD)\r
+               //      const restored = await Wallet.restore(wallet.id)\r
+\r
+               //      assert.ok('mnemonic' in restored)\r
+               //      assert.ok('seed' in restored)\r
+               //      assert.ok(restored.mnemonic === undefined)\r
+               //      assert.ok(restored.seed === undefined)\r
+               //      await assert.rejects(restored.verify(NANO_TEST_VECTORS.MNEMONIC))\r
+               //      await assert.rejects(restored.verify(NANO_TEST_VECTORS.BIP39_SEED))\r
+\r
+               //      await restored.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+\r
+               //      assert.ok(await restored.verify(NANO_TEST_VECTORS.MNEMONIC))\r
+               //      assert.ok(await restored.verify(NANO_TEST_VECTORS.BIP39_SEED))\r
+\r
+               //      await assert.resolves(restored.destroy())\r
+               //      await assert.resolves(wallet.destroy())\r
+               // })\r
 \r
-                       await assert.resolves(wallet.destroy())\r
-               })\r
-\r
-               await test('Trezor-derived BLAKE2b test vectors verified with third-party libraries', async () => {\r
-                       const wallet = await Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_1)\r
-                       await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
-                       const accounts = await wallet.accounts(0, 1)\r
-\r
-                       assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.MNEMONIC_1))\r
-                       assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.ENTROPY_1))\r
-\r
-                       const account0 = accounts.get(0)\r
-                       assert.exists(account0)\r
-                       assert.ok(account0 instanceof Account)\r
-                       assert.equal(account0.publicKey, TREZOR_TEST_VECTORS.BLAKE2B_1_PUBLIC_0)\r
-                       assert.equal(account0.address, TREZOR_TEST_VECTORS.BLAKE2B_1_ADDRESS_0)\r
-\r
-                       const account1 = accounts.get(1)\r
-                       assert.exists(account1)\r
-                       assert.ok(account1 instanceof Account)\r
-                       assert.equal(account1.publicKey, TREZOR_TEST_VECTORS.BLAKE2B_1_PUBLIC_1)\r
-                       assert.equal(account1.address, TREZOR_TEST_VECTORS.BLAKE2B_1_ADDRESS_1)\r
-\r
-                       await assert.resolves(wallet.destroy())\r
-               })\r
-\r
-               await test('BLAKE2b seed creates identical wallet as its derived mnemonic', async () => {\r
-                       const wallet = await Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_2)\r
-                       await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
-                       const walletAccount = await wallet.account()\r
-\r
-                       assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.MNEMONIC_2))\r
-                       assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.ENTROPY_2))\r
-\r
-                       const imported = await Wallet.load('BLAKE2b', TREZOR_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_2)\r
-                       await imported.unlock(TREZOR_TEST_VECTORS.PASSWORD)\r
-                       const importedAccount = await imported.account()\r
-\r
-                       assert.ok(await imported.verify(TREZOR_TEST_VECTORS.MNEMONIC_2))\r
-                       assert.ok(await imported.verify(TREZOR_TEST_VECTORS.ENTROPY_2))\r
-                       assert.equal(importedAccount.publicKey, walletAccount.publicKey)\r
-\r
-                       await assert.resolves(wallet.destroy())\r
-                       await assert.resolves(imported.destroy())\r
-               })\r
-\r
-               await test('BLAKE2b mnemonic for maximum seed value', async () => {\r
-                       const wallet = await Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_3)\r
-                       await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
-                       const account = await wallet.account()\r
-\r
-                       assert.ok(account instanceof Account)\r
-                       assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.MNEMONIC_3))\r
-                       assert.ok(await wallet.verify(TREZOR_TEST_VECTORS.ENTROPY_3))\r
-                       assert.equal(account.publicKey, TREZOR_TEST_VECTORS.BLAKE2B_3_PUBLIC_0)\r
-                       assert.equal(account.address, TREZOR_TEST_VECTORS.BLAKE2B_3_ADDRESS_0)\r
-\r
-                       await assert.resolves(wallet.destroy())\r
-               })\r
+               // await test('Import BLAKE2B wallet from storage using a wallet-generated ID', async () => {\r
+               //      const wallet = await Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_0)\r
+               //      const restored = await Wallet.restore(wallet.id)\r
+\r
+               //      assert.ok('mnemonic' in restored)\r
+               //      assert.ok('seed' in restored)\r
+               //      assert.ok(restored.mnemonic === undefined)\r
+               //      assert.ok(restored.seed === undefined)\r
+               //      await assert.rejects(restored.verify(TREZOR_TEST_VECTORS.MNEMONIC_0))\r
+               //      await assert.rejects(restored.verify(TREZOR_TEST_VECTORS.ENTROPY_0))\r
 \r
-               await test('Reject invalid seed', async () => {\r
-                       await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, '6CAF5A42BB8074314AAE20295975ECE663BE7AAD945A73613D193B0CC41C797'))\r
-                       await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, '6CAF5A42BB8074314AAE20295975ECE663BE7AAD945A73613D193B0CC41C79701'))\r
-                       await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_0.replaceAll(/./g, 'x')))\r
-               })\r
-\r
-               await test('Reject invalid length seed', async () => {\r
-                       await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED + 'f'),\r
-                               `Expected a ${NANO_TEST_VECTORS.BIP39_SEED.length}-character seed, but received ${NANO_TEST_VECTORS.BIP39_SEED.length + 1}-character string.`)\r
-                       await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED.slice(0, -1)),\r
-                               `Expected a ${NANO_TEST_VECTORS.BIP39_SEED.length}-character seed, but received ${NANO_TEST_VECTORS.BIP39_SEED.length - 1}-character string.`)\r
-               })\r
-\r
-               await test('Reject seed containing non-hex characters', async () => {\r
-                       await assert.rejects(Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.SEED_0.replace(/./, 'g')),\r
-                               'Seed contains invalid hexadecimal characters.')\r
-                       await assert.rejects(Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_1.replace(/./, 'g')),\r
-                               'Seed contains invalid hexadecimal characters.')\r
-               })\r
-\r
-               await test('Import BIP-44 wallet from storage using a wallet-generated ID', async () => {\r
-                       const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD)\r
-                       const restored = await Wallet.restore(wallet.id)\r
-\r
-                       assert.ok('mnemonic' in restored)\r
-                       assert.ok('seed' in restored)\r
-                       assert.ok(restored.mnemonic === undefined)\r
-                       assert.ok(restored.seed === undefined)\r
-                       await assert.rejects(restored.verify(NANO_TEST_VECTORS.MNEMONIC))\r
-                       await assert.rejects(restored.verify(NANO_TEST_VECTORS.BIP39_SEED))\r
-\r
-                       await restored.unlock(NANO_TEST_VECTORS.PASSWORD)\r
-\r
-                       assert.ok(await restored.verify(NANO_TEST_VECTORS.MNEMONIC))\r
-                       assert.ok(await restored.verify(NANO_TEST_VECTORS.BIP39_SEED))\r
-\r
-                       await assert.resolves(restored.destroy())\r
-                       await assert.resolves(wallet.destroy())\r
-               })\r
-\r
-               await test('Import BLAKE2B wallet from storage using a wallet-generated ID', async () => {\r
-                       const wallet = await Wallet.load('BLAKE2b', NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_0)\r
-                       const restored = await Wallet.restore(wallet.id)\r
+               //      await restored.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+\r
+               //      assert.ok(await restored.verify(TREZOR_TEST_VECTORS.MNEMONIC_0))\r
+               //      assert.ok(await restored.verify(TREZOR_TEST_VECTORS.ENTROPY_0))\r
 \r
-                       assert.ok('mnemonic' in restored)\r
-                       assert.ok('seed' in restored)\r
-                       assert.ok(restored.mnemonic === undefined)\r
-                       assert.ok(restored.seed === undefined)\r
-                       await assert.rejects(restored.verify(TREZOR_TEST_VECTORS.MNEMONIC_0))\r
-                       await assert.rejects(restored.verify(TREZOR_TEST_VECTORS.ENTROPY_0))\r
+               //      await assert.resolves(restored.destroy())\r
+               //      await assert.resolves(wallet.destroy())\r
+               // })\r
 \r
-                       await restored.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+               // await test('export wallet IDs from storage and reimport them', async () => {\r
+               //      const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD)\r
+               //      const backups = await Wallet.backup()\r
 \r
-                       assert.ok(await restored.verify(TREZOR_TEST_VECTORS.MNEMONIC_0))\r
-                       assert.ok(await restored.verify(TREZOR_TEST_VECTORS.ENTROPY_0))\r
+               //      assert.ok(backups.some(record => record.id === wallet.id))\r
 \r
-                       await assert.resolves(restored.destroy())\r
-                       await assert.resolves(wallet.destroy())\r
-               })\r
+               //      const restored = await Wallet.restore(wallet.id)\r
 \r
-               await test('export wallet IDs from storage and reimport them', async () => {\r
-                       const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD)\r
-                       const backups = await Wallet.backup()\r
+               //      assert.ok('mnemonic' in restored)\r
+               //      assert.ok('seed' in restored)\r
+               //      assert.ok(restored.mnemonic === undefined)\r
+               //      assert.ok(restored.seed === undefined)\r
 \r
-                       assert.ok(backups.some(record => record.id === wallet.id))\r
+               //      await restored.unlock(NANO_TEST_VECTORS.PASSWORD)\r
 \r
-                       const restored = await Wallet.restore(wallet.id)\r
+               //      assert.ok(await restored.verify(NANO_TEST_VECTORS.MNEMONIC))\r
+               //      assert.ok(await restored.verify(NANO_TEST_VECTORS.BIP39_SEED))\r
 \r
-                       assert.ok('mnemonic' in restored)\r
-                       assert.ok('seed' in restored)\r
-                       assert.ok(restored.mnemonic === undefined)\r
-                       assert.ok(restored.seed === undefined)\r
+               //      await assert.resolves(wallet.destroy())\r
+               //      await assert.resolves(restored.destroy())\r
+               // })\r
 \r
-                       await restored.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+               // await test('restore wallets without referencing IDs', async () => {\r
+               //      const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD)\r
+               //      const backups = await Wallet.backup()\r
 \r
-                       assert.ok(await restored.verify(NANO_TEST_VECTORS.MNEMONIC))\r
-                       assert.ok(await restored.verify(NANO_TEST_VECTORS.BIP39_SEED))\r
+               //      assert.ok(backups.some(record => record.id === wallet.id))\r
 \r
-                       await assert.resolves(wallet.destroy())\r
-                       await assert.resolves(restored.destroy())\r
-               })\r
+               //      const restored = await Wallet.restore()\r
+               //      const restoredWallet = restored.find(restoredWallet => restoredWallet.id === wallet.id)\r
 \r
-               await test('restore wallets without referencing IDs', async () => {\r
-                       const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD)\r
-                       const backups = await Wallet.backup()\r
+               //      assert.exists(restoredWallet)\r
+               //      assert.ok('mnemonic' in restoredWallet)\r
+               //      assert.ok('seed' in restoredWallet)\r
+               //      assert.ok(restoredWallet.mnemonic === undefined)\r
+               //      assert.ok(restoredWallet.seed === undefined)\r
 \r
-                       assert.ok(backups.some(record => record.id === wallet.id))\r
+               //      await restoredWallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
 \r
-                       const restored = await Wallet.restore()\r
-                       const restoredWallet = restored.find(restoredWallet => restoredWallet.id === wallet.id)\r
+               //      assert.ok(await restoredWallet.verify(NANO_TEST_VECTORS.MNEMONIC))\r
+               //      assert.ok(await restoredWallet.verify(NANO_TEST_VECTORS.BIP39_SEED))\r
 \r
-                       assert.exists(restoredWallet)\r
-                       assert.ok('mnemonic' in restoredWallet)\r
-                       assert.ok('seed' in restoredWallet)\r
-                       assert.ok(restoredWallet.mnemonic === undefined)\r
-                       assert.ok(restoredWallet.seed === undefined)\r
+               //      for (const restoredWallet of restored) {\r
+               //              await assert.resolves(restoredWallet.destroy())\r
+               //      }\r
+               //      await assert.resolves(wallet.destroy())\r
+               // })\r
 \r
-                       await restoredWallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+               // await test('load account from legacy address', async () => {\r
+               //      const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD)\r
+               //      await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
+               //      const account = await wallet.account()\r
+               //      const legacy = Account.load(NANO_TEST_VECTORS.ADDRESS_0.replace('nano_', 'xrb_'))\r
 \r
-                       assert.ok(await restoredWallet.verify(NANO_TEST_VECTORS.MNEMONIC))\r
-                       assert.ok(await restoredWallet.verify(NANO_TEST_VECTORS.BIP39_SEED))\r
-\r
-                       for (const restoredWallet of restored) {\r
-                               await assert.resolves(restoredWallet.destroy())\r
-                       }\r
-                       await assert.resolves(wallet.destroy())\r
-               })\r
+               //      assert.equal(account.address, legacy.address)\r
+               //      assert.equal(NANO_TEST_VECTORS.ADDRESS_0, legacy.address)\r
+               //      assert.equal(NANO_TEST_VECTORS.PUBLIC_0, legacy.publicKey)\r
 \r
-               await test('load account from legacy address', async () => {\r
-                       const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD)\r
-                       await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
-                       const account = await wallet.account()\r
-                       const legacy = Account.load(NANO_TEST_VECTORS.ADDRESS_0.replace('nano_', 'xrb_'))\r
-\r
-                       assert.equal(account.address, legacy.address)\r
-                       assert.equal(NANO_TEST_VECTORS.ADDRESS_0, legacy.address)\r
-                       assert.equal(NANO_TEST_VECTORS.PUBLIC_0, legacy.publicKey)\r
-\r
-                       await assert.resolves(wallet.destroy())\r
-               })\r
+               //      await assert.resolves(wallet.destroy())\r
+               // })\r
 \r
                await test('nano.org Exodus test vector mnemonic', async () => {\r
-                       const wallet = await Wallet.load('Exodus', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.SHORT_MNEMONIC)\r
+                       const wallet = await Wallet.load('Exodus', NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.EXODUS.MNEMONIC)\r
                        await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
                        const account = await wallet.account()\r
 \r
-                       assert.ok(await wallet.verify(NANO_TEST_VECTORS.SHORT_MNEMONIC))\r
-                       assert.ok(await wallet.verify(NANO_TEST_VECTORS.SHORT_BIP39_SEED))\r
+                       assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.EXODUS.MNEMONIC))\r
+                       assert.ok(await wallet.verify(CUSTOM_TEST_VECTORS.EXODUS.BIP39_SEED))\r
                        assert.ok(account instanceof Account)\r
-                       assert.equal(account.publicKey, NANO_TEST_VECTORS.SHORT_PUBLIC_0)\r
-                       assert.equal(account.address, NANO_TEST_VECTORS.SHORT_ADDRESS_0)\r
+                       assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.EXODUS.PUBLIC_0)\r
+                       assert.equal(account.address, CUSTOM_TEST_VECTORS.EXODUS.ADDRESS_0)\r
 \r
                        await assert.resolves(wallet.destroy())\r
                })\r