@scure/btc-signer 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/p2p.ts CHANGED
@@ -22,19 +22,31 @@ import { FpIsSquare } from '@noble/curves/abstract/modular.js';
22
22
  import { concatBytes, abytes } from '@noble/curves/utils.js';
23
23
  import { schnorr, secp256k1 } from '@noble/curves/secp256k1.js';
24
24
  import { randomBytes } from '@noble/hashes/utils.js';
25
- import { tagSchnorr, type Bytes } from './utils.ts';
25
+ import { tagSchnorr, type Bytes, type TArg, type TRet } from './utils.ts';
26
26
 
27
+ // BIP324's EllSwift formulas use full secp256k1 points for priv*G and x-only ECDH
28
+ // before exporting x coordinates or x-only bytes.
27
29
  const Point = secp256k1.Point;
30
+ // BIP324 defines XSwiftEC over integers modulo secp256k1's field prime p, so the
31
+ // EllSwift u/t/x arithmetic and 32-byte field encodings in this file all go through Point.Fp.
28
32
  const Fp = Point.Fp;
33
+ // EllSwift private scalars use secp256k1's subgroup order n, not the field prime p used
34
+ // by Point.Fp; Point.Fn is the generic scalar field object, not a secret-key validator.
29
35
  const Fn = Point.Fn;
30
36
  const _1n = BigInt(1);
31
37
  const _2n = BigInt(2);
32
38
 
39
+ // BIP324's XSwiftEC uses c = sqrt(-3) mod p and chooses the square root that is
40
+ // itself a square, which is what Fp.sqrt(Fp.create(-3)) returns here.
33
41
  const MINUS_3_SQRT = Fp.sqrt(Fp.create(BigInt(-3)));
34
42
  const _3n = BigInt(3);
35
43
  const _4n = BigInt(4);
36
44
  const _7n = BigInt(7);
45
+ // This is the "lift_x(x) succeeds" predicate for field-normalized x values.
46
+ // Raw x >= p would need the full BIP340 range check before reducing modulo p.
37
47
  const isValidX = (x: bigint) => FpIsSquare(Fp, Fp.add(Fp.mul(Fp.mul(x, x), x), _7n));
48
+ // BIP324's "return None if the square root does not exist" branches are modeled with
49
+ // undefined here; current callers only pass field-normalized values from Fp arithmetic.
38
50
  const trySqrt = (x: bigint): bigint | void => {
39
51
  try {
40
52
  return Fp.sqrt(x);
@@ -45,9 +57,20 @@ const trySqrt = (x: bigint): bigint | void => {
45
57
  * Experimental ElligatorSwift implementation:
46
58
  * Schnorr-like x-only ECDH with public keys indistinguishable from uniformly random bytes.
47
59
  * Documented in BIP324.
60
+ * @example
61
+ * Encode an x-only secp256k1 public key into the 64-byte BIP324 pseudorandom form.
62
+ * ```ts
63
+ * import { bytesToNumberBE } from '@noble/curves/utils.js';
64
+ * import { schnorr } from '@noble/curves/secp256k1.js';
65
+ * import { elligatorSwift } from '@scure/btc-signer/p2p.js';
66
+ * const secret = schnorr.utils.randomSecretKey();
67
+ * const encoded = elligatorSwift.encode(bytesToNumberBE(schnorr.getPublicKey(secret)));
68
+ * elligatorSwift.decode(encoded);
69
+ * ```
48
70
  */
49
- export const elligatorSwift = {
71
+ export const elligatorSwift = /* @__PURE__ */ Object.freeze({
50
72
  // (internal stuff, exported for tests only): decode(u, _inv(x, u)) = x
73
+ // Returns the case-selected BIP324 XSwiftECInv representative, or undefined for None.
51
74
  _inv: (x: bigint, u: bigint, ellCase: number): bigint | void => {
52
75
  if (!Number.isSafeInteger(ellCase) || ellCase < 0 || ellCase > 7)
53
76
  throw new Error(`elligatorSwift._inv: wrong case=${ellCase}`);
@@ -79,7 +102,13 @@ export const elligatorSwift = {
79
102
  return Fp.mul(w0, Fp.add(Fp.div(Fp.mul(u, t0), _2n), v));
80
103
  },
81
104
  // Encode public key (point or x coordinate bigint) into 64-byte pseudorandom encoding
82
- encode: (x: bigint): Uint8Array => {
105
+ // BIP324 samples encodings for x(P), so callers must pass a curve X coordinate in 0..p-1;
106
+ // without an explicit guard, the field helpers below interpret out-of-range x modulo p.
107
+ encode: (x: bigint): TRet<Uint8Array> => {
108
+ // BIP324 XSwiftEC uses field elements in `0..p-1`, and ellswift_create passes `XElligatorSwift(x(P))`,
109
+ // so encode() must reject out-of-range x instead of silently reducing a different bigint modulo p.
110
+ if (!Fp.isValid(x))
111
+ throw new RangeError('elligatorSwift.encode: expected x coordinate in range 0..p-1');
83
112
  // 200k test cycles per keygen: avg=4 max=48
84
113
  // seems too much, but same as for reference implementation
85
114
  while (true) {
@@ -88,12 +117,14 @@ export const elligatorSwift = {
88
117
  const ellCase = randomBytes(1)[0] & 7; // [0..8)
89
118
  const t = elligatorSwift._inv(x, u, ellCase);
90
119
  if (!t) continue;
91
- return concatBytes(Fp.toBytes(u), Fp.toBytes(t));
120
+ return concatBytes(Fp.toBytes(u), Fp.toBytes(t)) as TRet<Uint8Array>;
92
121
  }
93
122
  },
94
123
  // Decode elligatorSwift point to xonly
95
- decode: (data: Uint8Array): Uint8Array => {
124
+ decode: (data: TArg<Uint8Array>): TRet<Uint8Array> => {
96
125
  const _data = abytes(data, 64, 'data');
126
+ // BIP324 interprets both 32-byte halves as integers modulo p before the
127
+ // XSwiftEC remaps below, so arbitrary 64-byte inputs are valid here.
97
128
  let u = Fp.create(Fp.fromBytes(_data.subarray(0, 32), true));
98
129
  let t = Fp.create(Fp.fromBytes(_data.subarray(32, 64), true));
99
130
  if (Fp.is0(u)) u = Fp.create(_1n);
@@ -108,39 +139,44 @@ export const elligatorSwift = {
108
139
  const y = Fp.div(Fp.add(x, t), Fp.mul(MINUS_3_SQRT, u));
109
140
  // try different cases
110
141
  let res = Fp.add(u, Fp.mul(Fp.mul(y, y), _4n)); // u + 4 * Y ** 2,
111
- if (isValidX(res)) return Fp.toBytes(res);
142
+ if (isValidX(res)) return Fp.toBytes(res) as TRet<Uint8Array>;
112
143
  res = Fp.div(Fp.sub(Fp.div(Fp.neg(x), y), u), _2n); // (-X / Y - u) / 2
113
- if (isValidX(res)) return Fp.toBytes(res);
144
+ if (isValidX(res)) return Fp.toBytes(res) as TRet<Uint8Array>;
114
145
  res = Fp.div(Fp.sub(Fp.div(x, y), u), _2n); // (X / Y - u) / 2
115
- if (isValidX(res)) return Fp.toBytes(res);
146
+ if (isValidX(res)) return Fp.toBytes(res) as TRet<Uint8Array>;
116
147
  throw new Error('elligatorSwift: cannot decode public key');
117
148
  },
118
149
  // Generate pair (public key, secret key)
119
150
  keygen: () => {
151
+ // Use a subgroup-valid secp256k1 secret key, then ElligatorSwift-encode x(priv*G).
120
152
  const privateKey: Bytes = secp256k1.utils.randomSecretKey();
121
153
  const p = Point.BASE.multiply(Point.Fn.fromBytes(privateKey));
122
154
  const publicKey: Bytes = elligatorSwift.encode(p.x);
123
155
  return { privateKey, publicKey };
124
156
  },
125
157
  // Generates shared secret between a pub key and a priv key
126
- getSharedSecret: (privateKeyA: Uint8Array, publicKeyB: Uint8Array): Bytes => {
158
+ getSharedSecret: (privateKeyA: TArg<Uint8Array>, publicKeyB: TArg<Uint8Array>): TRet<Bytes> => {
159
+ // decode() accepts arbitrary 64-byte ElligatorSwift encodings, but the private scalar
160
+ // here still follows the usual secp256k1 subgroup-secret domain (1..n-1).
127
161
  const pub = elligatorSwift.decode(publicKeyB);
128
162
  const priv = abytes(privateKeyA, 32, 'privKey');
129
163
  const point = schnorr.utils.lift_x(Fp.fromBytes(pub));
130
164
  const d = Fn.fromBytes(priv);
131
- return Fp.toBytes(point.multiply(d).x);
165
+ return Fp.toBytes(point.multiply(d).x) as TRet<Bytes>;
132
166
  },
133
167
  // BIP324 shared secret
134
168
  getSharedSecretBip324: (
135
- privateKeyOurs: Uint8Array,
136
- publicKeyTheirs: Uint8Array,
137
- publicKeyOurs: Uint8Array,
169
+ privateKeyOurs: TArg<Uint8Array>,
170
+ publicKeyTheirs: TArg<Uint8Array>,
171
+ publicKeyOurs: TArg<Uint8Array>,
138
172
  initiating: boolean
139
- ): Uint8Array => {
140
- const ours = abytes(publicKeyOurs, undefined, 'publicKeyOurs');
141
- const theirs = abytes(publicKeyTheirs, undefined, 'publicKeyTheirs');
173
+ ): TRet<Uint8Array> => {
174
+ // BIP324 Shared secret computation hashes "the exactly 64-byte public keys'
175
+ // encodings sent over the wire", so both ElligatorSwift inputs must be 64 bytes here.
176
+ const ours = abytes(publicKeyOurs, 64, 'publicKeyOurs');
177
+ const theirs = abytes(publicKeyTheirs, 64, 'publicKeyTheirs');
142
178
  const ecdhPoint = elligatorSwift.getSharedSecret(privateKeyOurs, theirs);
143
179
  const pubs = initiating ? [ours, theirs] : [theirs, ours];
144
- return tagSchnorr('bip324_ellswift_xonly_ecdh', ...pubs, ecdhPoint);
180
+ return tagSchnorr('bip324_ellswift_xonly_ecdh', ...pubs, ecdhPoint) as TRet<Uint8Array>;
145
181
  },
146
- };
182
+ });