@scure/btc-signer 2.2.0 → 2.3.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
@@ -42,6 +42,9 @@ const MINUS_3_SQRT = Fp.sqrt(Fp.create(BigInt(-3)));
42
42
  const _3n = BigInt(3);
43
43
  const _4n = BigInt(4);
44
44
  const _7n = BigInt(7);
45
+ // Precomputed 1/2 mod p: turns the frequent "divide by 2" steps of XSwiftEC/XSwiftECInv
46
+ // into single multiplications instead of one modular inversion per call.
47
+ const INV_2 = Fp.inv(Fp.create(_2n));
45
48
  // This is the "lift_x(x) succeeds" predicate for field-normalized x values.
46
49
  // Raw x >= p would need the full BIP340 range check before reducing modulo p.
47
50
  const isValidX = (x: bigint) => FpIsSquare(Fp, Fp.add(Fp.mul(Fp.mul(x, x), x), _7n));
@@ -91,7 +94,7 @@ export const elligatorSwift = /* @__PURE__ */ Object.freeze({
91
94
  const r = trySqrt(Fp.mul(Fp.neg(s), Fp.add(Fp.mul(_4n, t0), t1)));
92
95
  if (r === undefined) return; // [2 condition]
93
96
  if (ellCase & 1 && Fp.is0(r)) return;
94
- v = Fp.div(Fp.add(Fp.neg(u), Fp.div(r, s)), _2n); // v = (-u + r / s) / 2
97
+ v = Fp.mul(Fp.add(Fp.neg(u), Fp.div(r, s)), INV_2); // v = (-u + r / s) / 2
95
98
  }
96
99
  const w = trySqrt(s);
97
100
  if (w === undefined) return; // [3 condition]
@@ -99,7 +102,7 @@ export const elligatorSwift = /* @__PURE__ */ Object.freeze({
99
102
  const t0 = last & 1 ? Fp.add(_1n, MINUS_3_SQRT) : Fp.sub(_1n, MINUS_3_SQRT);
100
103
  const w0 = last === 0 || last === 5 ? Fp.neg(w) : w; // -w | w
101
104
  // w0 * (u * t0 / 2 + v)
102
- return Fp.mul(w0, Fp.add(Fp.div(Fp.mul(u, t0), _2n), v));
105
+ return Fp.mul(w0, Fp.add(Fp.mul(Fp.mul(u, t0), INV_2), v));
103
106
  },
104
107
  // Encode public key (point or x coordinate bigint) into 64-byte pseudorandom encoding
105
108
  // BIP324 samples encodings for x(P), so callers must pass a curve X coordinate in 0..p-1;
@@ -109,11 +112,18 @@ export const elligatorSwift = /* @__PURE__ */ Object.freeze({
109
112
  // so encode() must reject out-of-range x instead of silently reducing a different bigint modulo p.
110
113
  if (!Fp.isValid(x))
111
114
  throw new RangeError('elligatorSwift.encode: expected x coordinate in range 0..p-1');
115
+ // Off-curve x cannot round-trip: decode() only returns lift_x-able candidates, so
116
+ // the loop below would silently emit an encoding of a *different* public key.
117
+ if (!isValidX(x))
118
+ throw new RangeError('elligatorSwift.encode: expected x coordinate of a curve point');
112
119
  // 200k test cycles per keygen: avg=4 max=48
113
120
  // seems too much, but same as for reference implementation
114
121
  while (true) {
115
- // random scalar 1..Fp.ORDER
116
- const u = Fp.create(Fp.fromBytes(secp256k1.utils.randomSecretKey()));
122
+ // Random field element 1..p-1: BIP324 samples u over the whole field (the previous
123
+ // secret-key sampler silently restricted u to 1..n-1); decode() remaps u = 0, so
124
+ // zero cannot round-trip and is skipped.
125
+ const u = Fp.create(Fp.fromBytes(randomBytes(32), true));
126
+ if (Fp.is0(u)) continue;
117
127
  const ellCase = randomBytes(1)[0] & 7; // [0..8)
118
128
  const t = elligatorSwift._inv(x, u, ellCase);
119
129
  if (!t) continue;
@@ -140,9 +150,11 @@ export const elligatorSwift = /* @__PURE__ */ Object.freeze({
140
150
  // try different cases
141
151
  let res = Fp.add(u, Fp.mul(Fp.mul(y, y), _4n)); // u + 4 * Y ** 2,
142
152
  if (isValidX(res)) return Fp.toBytes(res) as TRet<Uint8Array>;
143
- res = Fp.div(Fp.sub(Fp.div(Fp.neg(x), y), u), _2n); // (-X / Y - u) / 2
153
+ // X / Y is shared by the remaining candidates; computing it once saves an inversion.
154
+ const xDivY = Fp.div(x, y);
155
+ res = Fp.mul(Fp.sub(Fp.neg(xDivY), u), INV_2); // (-X / Y - u) / 2
144
156
  if (isValidX(res)) return Fp.toBytes(res) as TRet<Uint8Array>;
145
- res = Fp.div(Fp.sub(Fp.div(x, y), u), _2n); // (X / Y - u) / 2
157
+ res = Fp.mul(Fp.sub(xDivY, u), INV_2); // (X / Y - u) / 2
146
158
  if (isValidX(res)) return Fp.toBytes(res) as TRet<Uint8Array>;
147
159
  throw new Error('elligatorSwift: cannot decode public key');
148
160
  },
@@ -173,6 +185,10 @@ export const elligatorSwift = /* @__PURE__ */ Object.freeze({
173
185
  ): TRet<Uint8Array> => {
174
186
  // BIP324 Shared secret computation hashes "the exactly 64-byte public keys'
175
187
  // encodings sent over the wire", so both ElligatorSwift inputs must be 64 bytes here.
188
+ // Initiator/responder ordering decides the hash-input order, so require a real boolean
189
+ // instead of letting arbitrary truthy values pick a side.
190
+ if (typeof initiating !== 'boolean')
191
+ throw new TypeError('"initiating" expected boolean, got type=' + typeof initiating);
176
192
  const ours = abytes(publicKeyOurs, 64, 'publicKeyOurs');
177
193
  const theirs = abytes(publicKeyTheirs, 64, 'publicKeyTheirs');
178
194
  const ecdhPoint = elligatorSwift.getSharedSecret(privateKeyOurs, theirs);
package/src/payment.ts CHANGED
@@ -29,11 +29,18 @@ export type P2Ret = {
29
29
  };
30
30
 
31
31
  // Pay to Anchor (P2A)
32
+ // BIP433 Pay-to-Anchor witness program bytes; the scriptPubKey is `OP_1 <0x4e73>`.
33
+ const P2A_PROGRAM = /* @__PURE__ */ Uint8Array.from([0x4e, 0x73]);
32
34
  type OutP2AType = { type: 'p2a'; script: Bytes };
33
35
  const OutP2A: Coder<OptScript, OutP2AType | undefined> = {
34
36
  encode(from: TArg<ScriptType>): TRet<OutP2AType | undefined> {
35
37
  // BIP433 defines P2A as the exact OP_1 <0x4e73> scriptPubKey.
36
- if (from.length !== 2 || from[0] !== 1 || !u.isBytes(from[1]) || hex.encode(from[1]) !== '4e73')
38
+ if (
39
+ from.length !== 2 ||
40
+ from[0] !== 1 ||
41
+ !u.isBytes(from[1]) ||
42
+ !u.equalBytes(from[1], P2A_PROGRAM)
43
+ )
37
44
  return;
38
45
  return { type: 'p2a', script: Script.encode(from) } as TRet<OutP2AType | undefined>;
39
46
  },
@@ -41,7 +48,7 @@ const OutP2A: Coder<OptScript, OutP2AType | undefined> = {
41
48
  if (to.type !== 'p2a') return;
42
49
  // The decoded object keeps `script` for caller convenience, but the `p2a`
43
50
  // tag always canonicalizes back to the fixed BIP433 script.
44
- return [1, hex.decode('4e73')] as TRet<OptScript>;
51
+ return [1, Uint8Array.from(P2A_PROGRAM)] as TRet<OptScript>;
45
52
  },
46
53
  };
47
54
 
@@ -87,8 +94,9 @@ const OutPKH: Coder<OptScript, OutPKHType | undefined> = {
87
94
  encode(from: TArg<ScriptType>): TRet<OutPKHType | undefined> {
88
95
  if (from.length !== 5 || from[0] !== 'DUP' || from[1] !== 'HASH160' || !u.isBytes(from[2]))
89
96
  return;
90
- // OutScript validates that the pushed HASH160 is exactly 20 bytes.
91
- // This child matcher only recognizes the canonical P2PKH opcode skeleton.
97
+ // Require the exact 20-byte HASH160 here so near-miss scripts fall through
98
+ // to OutUnknown instead of throwing in the OutScript validator on decode.
99
+ if (from[2].length !== 20) return;
92
100
  if (from[3] !== 'EQUALVERIFY' || from[4] !== 'CHECKSIG') return;
93
101
  return { type: 'pkh', hash: from[2] } as TRet<OutPKHType | undefined>;
94
102
  },
@@ -105,8 +113,9 @@ const OutSH: Coder<OptScript, OutSHType | undefined> = {
105
113
  encode(from: TArg<ScriptType>): TRet<OutSHType | undefined> {
106
114
  if (from.length !== 3 || from[0] !== 'HASH160' || !u.isBytes(from[1]) || from[2] !== 'EQUAL')
107
115
  return;
108
- // OutScript validates that the pushed HASH160 is exactly 20 bytes.
109
- // This child matcher only recognizes the canonical P2SH opcode skeleton.
116
+ // Require the exact 20-byte HASH160 here so near-miss scripts fall through
117
+ // to OutUnknown instead of throwing in the OutScript validator on decode.
118
+ if (from[1].length !== 20) return;
110
119
  return { type: 'sh', hash: from[1] } as TRet<OutSHType | undefined>;
111
120
  },
112
121
  // OutScript validates `sh.hash` before this branch emits the canonical
@@ -159,9 +168,12 @@ const OutMS: Coder<OptScript, OutMSType | undefined> = {
159
168
  if (typeof m !== 'number' || typeof n !== 'number') return;
160
169
  const pubkeys = from.slice(1, -2);
161
170
  if (n !== pubkeys.length) return;
162
- for (const pub of pubkeys) if (!u.isBytes(pub)) return;
163
- // OutScript validates pubkey encodings and `0 < m <= n <= 16`.
164
- // This child matcher only recognizes the canonical CHECKMULTISIG skeleton.
171
+ // Require valid ECDSA pubkeys and `0 < m <= n` here so near-miss
172
+ // CHECKMULTISIG scripts (garbage keys, degenerate 0-of-0) fall through to
173
+ // OutUnknown instead of throwing in the OutScript validator on decode.
174
+ // Script.decode only yields 0..16 for opcode numbers, so n <= 16 holds.
175
+ for (const pub of pubkeys) if (!u.isBytes(pub) || !isValidPubkey(pub, u.PubT.ecdsa)) return;
176
+ if (!Number.isSafeInteger(m) || m < 1 || m > n) return;
165
177
  // We don't need n here because it is the same as pubkeys.length.
166
178
  return { type: 'ms', m, pubkeys: pubkeys as Bytes[] } as TRet<OutMSType | undefined>;
167
179
  },
@@ -182,6 +194,10 @@ const OutTR: Coder<OptScript, OutTRType | undefined> = {
182
194
  // BIP341 assigns native taproot meaning only to version 1 with a 32-byte x-only program;
183
195
  // other OP_1 program lengths remain reserved future witness programs and should fall through.
184
196
  if (from.length !== 2 || from[0] !== 1 || !u.isBytes(from[1]) || from[1].length !== 32) return;
197
+ // A 32-byte v1 program with an off-curve x coordinate is a fundable but
198
+ // taproot-unspendable output; classify it as unknown instead of throwing
199
+ // in the OutScript validator on decode.
200
+ if (!isValidPubkey(from[1], u.PubT.schnorr)) return;
185
201
  return { type: 'tr', pubkey: from[1] } as TRet<OutTRType | undefined>;
186
202
  },
187
203
  // OutScript validates `tr.pubkey` before this branch emits the canonical
@@ -243,9 +259,13 @@ const OutTRMS: Coder<OptScript, OutTRMSType | undefined> = {
243
259
  if (elm !== (i === 1 ? 'CHECKSIG' : 'CHECKSIGADD')) return;
244
260
  continue;
245
261
  }
246
- if (!u.isBytes(elm)) return;
262
+ // Require actual Schnorr pubkeys here (same as tr_ns) so near-miss
263
+ // CHECKSIGADD scripts fall through to OutUnknown instead of throwing
264
+ // in the OutScript validator on decode.
265
+ if (!u.isBytes(elm) || !isValidPubkey(elm, u.PubT.schnorr)) return;
247
266
  pubkeys.push(elm);
248
267
  }
268
+ if (!Number.isSafeInteger(m) || m < 1 || m > pubkeys.length || pubkeys.length > 999) return;
249
269
  return { type: 'tr_ms', pubkeys, m } as TRet<OutTRMSType | undefined>;
250
270
  },
251
271
  decode: (to: TArg<OutTRMSType>): TRet<OptScript> => {
@@ -408,7 +428,8 @@ export const OutScript: TRet<
408
428
  export type OutScriptType = typeof OutScript;
409
429
  // TRet-wrapping OutScript changes decode() to the normalized descriptor surface, but the local
410
430
  // checkScript/Address caches still need an explicit alias that can carry the decode-side `undefined`.
411
- type OutScriptValue = ReturnType<OutScriptType['decode']> | undefined;
431
+ type AddressValue = NonNullable<ReturnType<OutScriptType['decode']>>;
432
+ type OutScriptValue = AddressValue | undefined;
412
433
 
413
434
  // Basic sanity check for scripts
414
435
  function checkWSH(s: TArg<OutWSHType>, witnessScript: TArg<Bytes>) {
@@ -606,6 +627,7 @@ export const p2sh = <T extends P2Ret>(
606
627
  child: TArg<T>,
607
628
  network: BTC_NETWORK = NETWORK
608
629
  ): TRet<Extends<P2SHReturn<T>, P2Ret>> => {
630
+ u.validateObject(child as Record<string, any>, {}, {}, 'child');
609
631
  // It is already tested inside noble-hashes and checkScript
610
632
  // BIP16 redeemScripts are pushed by scriptSig, so anything over the 520-byte pushed-element
611
633
  // limit would be fundable by HASH160 but unspendable once wrapped in P2SH.
@@ -672,6 +694,7 @@ export const p2wsh = (
672
694
  child: TArg<P2Ret>,
673
695
  network: BTC_NETWORK = NETWORK
674
696
  ): TRet<Extends<P2WSH, P2Ret>> => {
697
+ u.validateObject(child as Record<string, any>, {}, {}, 'child');
675
698
  const cs = child.script;
676
699
  if (!u.isBytes(cs)) throw new Error(`Wrong script: ${typeof cs}, expected Uint8Array`);
677
700
  // BIP141 P2WSH says the witness "must consist of ... a serialized script (witnessScript)"
@@ -782,9 +805,16 @@ function checkTaprootScript(
782
805
  // disable custom. All custom scripts for taproot should have prefix 'tr_'
783
806
  if (customScripts) {
784
807
  const cs = P.apply(Script, P.coders.match(customScripts));
785
- const c = cs.decode(script);
808
+ let c;
809
+ // match() throws when no custom coder matches; treat that as "not a custom
810
+ // script" so the allowUnknownOutputs escape below stays reachable.
811
+ try {
812
+ c = cs.decode(script);
813
+ } catch (e) {
814
+ c = undefined;
815
+ }
786
816
  if (c !== undefined) {
787
- if (typeof c.type !== 'string' || !c.type.startsWith('tr_'))
817
+ if (!u.astring(c.type, 'c.type').startsWith('tr_'))
788
818
  throw new Error(`P2TR: invalid custom type=${c.type}`);
789
819
  return;
790
820
  }
@@ -867,6 +897,13 @@ type _TaprootTreeInternal = {
867
897
  * ```
868
898
  */
869
899
  export function taprootListToTree(taprootList: TArg<TaprootScriptList>): TRet<TaprootScriptTree> {
900
+ u.aarray<TaprootScriptList[number]>(taprootList, 'taprootList', (leaf, title) => {
901
+ // p2tr reduces non-binary trees through this helper, so nested branch arrays are valid here.
902
+ if (Array.isArray(leaf)) return;
903
+ u.validateObject(leaf as Record<string, any>, {}, {}, title);
904
+ // This helper only arranges weighted tree nodes; p2tr validates leaf scripts while hashing.
905
+ if (leaf.weight !== undefined) anumber(leaf.weight, title + '.weight');
906
+ });
870
907
  // Empty flat lists cannot represent a taproot script tree; omit the tree entirely for
871
908
  // key-path-only outputs instead of passing [] here, otherwise this helper would return
872
909
  // undefined and downstream taproot tree walkers would fail much later on a non-tree value.
@@ -947,17 +984,22 @@ function taprootHashTree(
947
984
  allowUnknownOutputs = false,
948
985
  customScripts?: TArg<CustomScript[]>
949
986
  ): TRet<HashedTree> {
950
- if (!tree) throw new Error('taprootHashTree: empty tree');
987
+ if (tree === undefined) throw new Error('taprootHashTree: empty tree');
988
+ if (!Array.isArray(tree) && !P.utils.isPlainObject(tree))
989
+ throw new TypeError('"tree" expected object or array, got type=' + typeof tree);
951
990
  if (Array.isArray(tree) && tree.length === 1) tree = tree[0];
952
991
  // Terminal node (leaf)
953
992
  if (!Array.isArray(tree)) {
993
+ u.validateObject(tree as Record<string, any>, {}, {}, 'tree');
954
994
  const version = tree.leafVersion;
955
995
  const { script: leafScript } = tree;
956
996
  // Earliest tree walk where we can validate tapScripts
957
997
  if (tree.tapLeafScript || (tree.tapMerkleRoot && !u.equalBytes(tree.tapMerkleRoot, P.EMPTY)))
958
998
  throw new Error('P2TR: tapRoot leafScript cannot have tree');
959
- const script = typeof leafScript === 'string' ? hex.decode(leafScript) : leafScript;
960
- if (!u.isBytes(script)) throw new Error(`checkScript: wrong script type=${script}`);
999
+ const script =
1000
+ typeof leafScript === 'string'
1001
+ ? hex.decode(leafScript)
1002
+ : abytes(leafScript, undefined, 'tree.script');
961
1003
  checkTaprootScript(script, internalPubKey, allowUnknownOutputs, customScripts);
962
1004
  return {
963
1005
  type: 'leaf',
@@ -1071,18 +1113,20 @@ export function p2tr(
1071
1113
  );
1072
1114
  const tapMerkleRoot = hashedTree.hash;
1073
1115
  const [tweakedPubkey, parity] = u.taprootTweakPubkey(pubKey, tapMerkleRoot);
1116
+ const tapLeafScript: NonNullable<TransactionInput['tapLeafScript']> = [];
1074
1117
  const leaves = taprootWalkTree(hashedTree).map((l) => {
1075
1118
  const version = tapLeafVersion(l.version);
1076
- return {
1077
- ...l,
1078
- // Leaf versions are stored as the base even byte; only the control block adds the
1079
- // output-key parity bit required by BIP 341 script-path spending.
1080
- controlBlock: TaprootControlBlock.encode({
1081
- version: version + parity,
1082
- internalKey: pubKey,
1083
- merklePath: l.path,
1084
- }),
1119
+ // Leaf versions are stored as the base even byte; only the control block adds the
1120
+ // output-key parity bit required by BIP 341 script-path spending.
1121
+ const controlBlock = {
1122
+ version: version + parity,
1123
+ internalKey: pubKey,
1124
+ merklePath: l.path,
1085
1125
  };
1126
+ // Skip an encode/decode copy for performance; callers must treat returned metadata as
1127
+ // immutable.
1128
+ tapLeafScript.push([controlBlock, u.concatBytes(l.script, new Uint8Array([version]))]);
1129
+ return { ...l, controlBlock: TaprootControlBlock.encode(controlBlock) };
1086
1130
  });
1087
1131
  return {
1088
1132
  type: 'tr',
@@ -1093,10 +1137,7 @@ export function p2tr(
1093
1137
  // PSBT stuff
1094
1138
  tapInternalKey: pubKey,
1095
1139
  leaves,
1096
- tapLeafScript: leaves.map((l) => [
1097
- TaprootControlBlock.decode(l.controlBlock),
1098
- u.concatBytes(l.script, new Uint8Array([tapLeafVersion(l.version)])),
1099
- ]),
1140
+ tapLeafScript,
1100
1141
  tapMerkleRoot,
1101
1142
  } as const as TRet<Extends<P2TR_TREE, P2Ret>>;
1102
1143
  } else {
@@ -1278,6 +1319,7 @@ export function getAddress(
1278
1319
  privKey: TArg<Bytes>,
1279
1320
  network: BTC_NETWORK = NETWORK
1280
1321
  ): string {
1322
+ u.astring(type, 'type');
1281
1323
  if (type === 'tr') {
1282
1324
  return p2tr(u.pubSchnorr(privKey), undefined, network).address;
1283
1325
  }
@@ -1429,18 +1471,24 @@ export function WIF(network: BTC_NETWORK = NETWORK): TRet<Coder<Bytes, string>>
1429
1471
  * coder.encode(p2wpkh(pubECDSA(randomPrivateKeyBytes())));
1430
1472
  * ```
1431
1473
  */
1432
- export function Address(network: BTC_NETWORK = NETWORK) {
1474
+ export function Address(network: BTC_NETWORK = NETWORK): TRet<P.Coder<AddressValue, string>> {
1475
+ u.validateObject(network as Record<string, any>, {}, {}, 'network');
1433
1476
  return {
1434
- encode(from: Exclude<OutScriptValue, undefined>): string {
1477
+ encode(from: TArg<AddressValue>): string {
1478
+ u.validateObject(from as Record<string, any>, {}, {}, 'from');
1435
1479
  const { type } = from;
1480
+ u.astring(type, 'from.type');
1436
1481
  if (type === 'wpkh') return programToWitness(0, from.hash, network);
1437
1482
  else if (type === 'wsh') return programToWitness(0, from.hash, network);
1438
1483
  else if (type === 'tr') return programToWitness(1, from.pubkey, network);
1484
+ // BIP433 P2A is the fixed v1 witness program 0x4e73 ('bc1pfeessrawgf').
1485
+ else if (type === 'p2a') return programToWitness(1, P2A_PROGRAM, network);
1439
1486
  else if (type === 'pkh') return formatKey(from.hash, [network.pubKeyHash]);
1440
1487
  else if (type === 'sh') return formatKey(from.hash, [network.scriptHash]);
1441
1488
  throw new Error(`Unknown address type=${type}`);
1442
1489
  },
1443
- decode(address: string): OutScriptValue {
1490
+ decode(address: string): TRet<AddressValue> {
1491
+ u.astring(address, 'address');
1444
1492
  if (address.length < 14 || address.length > 74) throw new Error('Invalid address length');
1445
1493
  // Bech32
1446
1494
  if (network.bech32 && address.toLowerCase().startsWith(`${network.bech32}1`)) {
@@ -1458,25 +1506,27 @@ export function Address(network: BTC_NETWORK = NETWORK) {
1458
1506
  const data = bech32.fromWords(program);
1459
1507
  validateWitness(version, data);
1460
1508
  if (version === 0 && data.length === 32)
1461
- return { type: 'wsh', hash: data } as OutScriptValue;
1509
+ return { type: 'wsh', hash: data } as TRet<AddressValue>;
1462
1510
  else if (version === 0 && data.length === 20)
1463
- return { type: 'wpkh', hash: data } as OutScriptValue;
1511
+ return { type: 'wpkh', hash: data } as TRet<AddressValue>;
1464
1512
  else if (version === 1 && data.length === 32)
1465
- return { type: 'tr', pubkey: data } as OutScriptValue;
1513
+ return { type: 'tr', pubkey: data } as TRet<AddressValue>;
1514
+ else if (version === 1 && u.equalBytes(data, P2A_PROGRAM))
1515
+ return { type: 'p2a', script: Script.encode([1, data]) } as TRet<AddressValue>;
1466
1516
  // Future witness versions can still be valid addresses, but this helper
1467
- // only returns typed descriptors for recognized v0 and taproot templates.
1517
+ // only returns typed descriptors for recognized v0, taproot and P2A templates.
1468
1518
  else throw new Error('Unknown witness program');
1469
1519
  }
1470
1520
  const data = base58check.decode(address);
1471
1521
  if (data.length !== 21) throw new Error('Invalid base58 address');
1472
1522
  // Pay To Public Key Hash
1473
1523
  if (data[0] === network.pubKeyHash) {
1474
- return { type: 'pkh', hash: data.slice(1) } as OutScriptValue;
1524
+ return { type: 'pkh', hash: data.slice(1) } as TRet<AddressValue>;
1475
1525
  } else if (data[0] === network.scriptHash) {
1476
1526
  return {
1477
1527
  type: 'sh',
1478
1528
  hash: data.slice(1),
1479
- } as OutScriptValue;
1529
+ } as TRet<AddressValue>;
1480
1530
  }
1481
1531
  throw new Error(`Invalid address prefix=${data[0]}`);
1482
1532
  },
package/src/psbt.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { hex } from '@scure/base';
2
+ import { anumber } from '@noble/hashes/utils.js';
2
3
  import * as P from 'micro-packed';
3
4
  import {
4
5
  CompactSize,
@@ -10,17 +11,23 @@ import {
10
11
  VarBytes,
11
12
  } from './script.ts';
12
13
  import {
14
+ aarray,
13
15
  type Bytes,
14
16
  compareBytes,
15
17
  equalBytes,
16
18
  PubT,
17
19
  type TArg,
18
20
  type TRet,
21
+ validateObject,
19
22
  validatePubkey,
20
23
  } from './utils.ts';
21
24
 
22
25
  // PSBT BIP174, BIP370, BIP371
23
26
 
27
+ // Be friendly to bad ECMAScript parsers by not using bigint literals.
28
+ // prettier-ignore
29
+ const _0n = /* @__PURE__ */ BigInt(0), _1n = /* @__PURE__ */ BigInt(1);
30
+
24
31
  // BIP174 keydata only says "public key", so legacy PSBT ECDSA fields still accept both
25
32
  // compressed (33-byte) and uncompressed (65-byte) SEC1 encodings, but not x-only keys.
26
33
  const PubKeyECDSA: P.CoderType<Bytes> = /* @__PURE__ */ (() =>
@@ -157,9 +164,9 @@ const tapTree = /* @__PURE__ */ (() =>
157
164
  while (next.length < depth) next.push(0);
158
165
  path = next;
159
166
  }
160
- let leaves = 0n;
161
- for (let i = 0; i < tree.length; i++) leaves += 1n << BigInt(maxDepth - tree[i].depth);
162
- if (leaves !== 1n << BigInt(maxDepth))
167
+ let leaves = _0n;
168
+ for (let i = 0; i < tree.length; i++) leaves += _1n << BigInt(maxDepth - tree[i].depth);
169
+ if (leaves !== _1n << BigInt(maxDepth))
163
170
  throw new Error('tapTree: tuples must describe a complete binary tree');
164
171
  return tree;
165
172
  }
@@ -593,7 +600,7 @@ export const PSBTOutputCoder = /* @__PURE__ */ (() =>
593
600
  // in the field coder itself because BIP371 constrains the tuple value, not just the row shape.
594
601
  // BIP174/BIP370 define PSBT_OUT_AMOUNT as a signed int64 transport field, but it still
595
602
  // represents the transaction output amount in satoshis, so negative output values are invalid.
596
- if (o.amount !== undefined && o.amount < 0n)
603
+ if (o.amount !== undefined && o.amount < _0n)
597
604
  throw new Error(`validateOutput: wrong amount=${o.amount}`);
598
605
  if (o.bip32Derivation) for (const [k] of o.bip32Derivation) validatePubkey(k, PubT.ecdsa);
599
606
  return o;
@@ -726,6 +733,9 @@ export function cleanPSBTFields<T extends PSBTKeyMap>(
726
733
  info: T,
727
734
  lst: TArg<PSBTKeyMapKeys<T>>
728
735
  ): TRet<PSBTKeyMapKeys<T>> {
736
+ anumber(version, 'version');
737
+ validateObject(info as Record<string, any>, {}, {}, 'info');
738
+ validateObject(lst as Record<string, any>, {}, {}, 'lst');
729
739
  const _lst = lst as PSBTKeyMapKeys<T>;
730
740
  const out: PSBTKeyMapKeys<T> = {};
731
741
  for (const _k in _lst) {
@@ -747,7 +757,7 @@ export function cleanPSBTFields<T extends PSBTKeyMap>(
747
757
  return out as TRet<PSBTKeyMapKeys<T>>;
748
758
  }
749
759
 
750
- function validatePSBT(tx: P.UnwrapCoder<PSBTRaw>) {
760
+ function validatePSBT(tx: P.UnwrapCoder<PSBTRaw>): P.UnwrapCoder<PSBTRaw> {
751
761
  const version = (tx && tx.global && tx.global.version) || 0;
752
762
  validatePSBTFields(version, PSBTGlobal, tx.global);
753
763
  for (const i of tx.inputs) validatePSBTFields(version, PSBTInput, i);
@@ -803,6 +813,10 @@ export function mergeKeyMap<T extends PSBTKeyMap>(
803
813
  allowedFields?: TArg<readonly (keyof PSBTKeyMapKeys<T>)[]>,
804
814
  allowUnknown?: boolean
805
815
  ): TRet<PSBTKeyMapKeys<T>> {
816
+ validateObject(psbtEnum as Record<string, any>, {}, {}, 'psbtEnum');
817
+ validateObject(val as Record<string, any>, {}, {}, 'val');
818
+ if (cur !== undefined) validateObject(cur as Record<string, any>, {}, {}, 'cur');
819
+ if (allowedFields !== undefined) aarray(allowedFields, 'allowedFields');
806
820
  const _val = val as PSBTKeyMapKeys<T>;
807
821
  const _cur = cur as PSBTKeyMapKeys<T> | undefined;
808
822
  const _allowedFields = allowedFields as readonly (keyof PSBTKeyMapKeys<T>)[] | undefined;
package/src/script.ts CHANGED
@@ -1,5 +1,14 @@
1
1
  import * as P from 'micro-packed';
2
- import { isBytes, reverseObject, type ValueOf, type Bytes, type TArg, type TRet } from './utils.ts';
2
+ import {
3
+ aarray,
4
+ abytes,
5
+ isBytes,
6
+ reverseObject,
7
+ type Bytes,
8
+ type TArg,
9
+ type TRet,
10
+ type ValueOf,
11
+ } from './utils.ts';
3
12
 
4
13
  /**
5
14
  * Maximum byte size allowed for a single pushed script element.
@@ -8,6 +17,12 @@ import { isBytes, reverseObject, type ValueOf, type Bytes, type TArg, type TRet
8
17
  */
9
18
  export const MAX_SCRIPT_BYTE_LENGTH = 520;
10
19
 
20
+ // Be friendly to bad ECMAScript parsers by not using bigint literals.
21
+ // prettier-ignore
22
+ const _0n = /* @__PURE__ */ BigInt(0), _1n = /* @__PURE__ */ BigInt(1), _2n = /* @__PURE__ */ BigInt(2), _8n = /* @__PURE__ */ BigInt(8);
23
+ const U8_MAX = /* @__PURE__ */ BigInt(0xff);
24
+ const COMPACT_DIRECT_MAX = /* @__PURE__ */ BigInt(0xfc);
25
+
11
26
  // prettier-ignore
12
27
  /**
13
28
  * Bitcoin Script opcode table.
@@ -87,11 +102,11 @@ export type ScriptType = ScriptOP[];
87
102
  export function ScriptNum(bytesLimit = 6, forceMinimal = false): P.CoderType<bigint> {
88
103
  return P.wrap({
89
104
  encodeStream: (w: P.Writer, value: bigint) => {
90
- if (value === 0n) return;
105
+ if (value === _0n) return;
91
106
  const neg = value < 0;
92
107
  const val = BigInt(value);
93
108
  const nums = [];
94
- for (let abs = neg ? -val : val; abs; abs >>= 8n) nums.push(Number(abs & 0xffn));
109
+ for (let abs = neg ? -val : val; abs; abs >>= _8n) nums.push(Number(abs & U8_MAX));
95
110
  if (nums[nums.length - 1] >= 0x80) nums.push(neg ? 0x80 : 0);
96
111
  else if (neg) nums[nums.length - 1] |= 0x80;
97
112
  w.bytes(new Uint8Array(nums));
@@ -100,24 +115,22 @@ export function ScriptNum(bytesLimit = 6, forceMinimal = false): P.CoderType<big
100
115
  const len = r.leftBytes;
101
116
  if (len > bytesLimit)
102
117
  throw new Error(`ScriptNum: number (${len}) bigger than limit=${bytesLimit}`);
103
- if (len === 0) return 0n;
118
+ if (len === 0) return _0n;
119
+ // Read the payload once instead of peeking for the minimality check and
120
+ // then re-reading it byte-by-byte through the Reader.
121
+ const data = r.bytes(len);
104
122
  if (forceMinimal) {
105
- const data = r.bytes(len, true);
106
123
  // MSB is zero (without sign bit) -> not minimally encoded
107
- if ((data[data.length - 1] & 0x7f) === 0) {
124
+ if ((data[len - 1] & 0x7f) === 0) {
108
125
  // exception
109
- if (len <= 1 || (data[data.length - 2] & 0x80) === 0)
126
+ if (len <= 1 || (data[len - 2] & 0x80) === 0)
110
127
  throw new Error('Non-minimally encoded ScriptNum');
111
128
  }
112
129
  }
113
- let last = 0;
114
- let res = 0n;
115
- for (let i = 0; i < len; ++i) {
116
- last = r.byte();
117
- res |= BigInt(last) << (8n * BigInt(i));
118
- }
119
- if (last >= 0x80) {
120
- res &= (2n ** BigInt(len * 8) - 1n) >> 1n;
130
+ let res = _0n;
131
+ for (let i = 0; i < len; ++i) res |= BigInt(data[i]) << (_8n * BigInt(i));
132
+ if (data[len - 1] >= 0x80) {
133
+ res &= (_2n ** BigInt(len * 8) - _1n) >> _1n;
121
134
  res = -res;
122
135
  }
123
136
  return res;
@@ -148,7 +161,9 @@ export function OpToNum(
148
161
  if (isBytes(op)) {
149
162
  try {
150
163
  const val = ScriptNum(bytesLimit, forceMinimal).decode(op);
151
- if (val > Number.MAX_SAFE_INTEGER) return;
164
+ // Symmetric safe-integer bound: large negative values would otherwise
165
+ // coerce through Number() with silent precision loss.
166
+ if (val > Number.MAX_SAFE_INTEGER || val < -Number.MAX_SAFE_INTEGER) return;
152
167
  return Number(val);
153
168
  } catch (e) {
154
169
  return;
@@ -203,10 +218,14 @@ export const Script: TRet<P.CoderType<ScriptType>> = /* @__PURE__ */ (() =>
203
218
  Object.freeze(
204
219
  P.wrap({
205
220
  encodeStream: (w: P.Writer, value: TArg<ScriptType>) => {
221
+ aarray(value, 'value');
206
222
  for (let o of value) {
207
223
  if (typeof o === 'string') {
208
- if (OP[o] === undefined) throw new Error(`Unknown opcode=${o}`);
209
- w.byte(OP[o]);
224
+ const op = OP[o];
225
+ // OP is a plain object, so inherited Object.prototype keys ('toString',
226
+ // 'constructor', ...) are not opcodes and must be rejected here too.
227
+ if (typeof op !== 'number') throw new Error(`Unknown opcode=${o}`);
228
+ w.byte(op);
210
229
  continue;
211
230
  } else if (typeof o === 'number') {
212
231
  if (o === 0x00) {
@@ -224,7 +243,7 @@ export const Script: TRet<P.CoderType<ScriptType>> = /* @__PURE__ */ (() =>
224
243
  }
225
244
  // Encode big numbers
226
245
  if (typeof o === 'number') o = ScriptNum().encode(BigInt(o));
227
- if (!isBytes(o)) throw new Error(`Wrong Script OP=${o} (${typeof o})`);
246
+ abytes(o, undefined, 'value');
228
247
  // Bytes
229
248
  const len = o.length;
230
249
  if (len < OP.PUSHDATA1) w.byte(len);
@@ -267,13 +286,6 @@ export const Script: TRet<P.CoderType<ScriptType>> = /* @__PURE__ */ (() =>
267
286
  })
268
287
  ))() as TRet<P.CoderType<ScriptType>>;
269
288
 
270
- // BTC specific variable length integer encoding
271
- // https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer
272
- const CSLimits: Record<number, [number, number, bigint, bigint]> = {
273
- 0xfd: [0xfd, 2, 253n, 65535n],
274
- 0xfe: [0xfe, 4, 65536n, 4294967295n],
275
- 0xff: [0xff, 8, 4294967296n, 18446744073709551615n],
276
- };
277
289
  /**
278
290
  * Bitcoin CompactSize integer coder.
279
291
  * @example
@@ -282,16 +294,25 @@ const CSLimits: Record<number, [number, number, bigint, bigint]> = {
282
294
  * CompactSize.encode(1n);
283
295
  * ```
284
296
  */
285
- export const CompactSize: P.CoderType<bigint> = /* @__PURE__ */ (() =>
286
- Object.freeze(
297
+ export const CompactSize: P.CoderType<bigint> = /* @__PURE__ */ (() => {
298
+ // BTC specific variable length integer encoding
299
+ // https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer
300
+ const limits: Record<number, [number, number, bigint, bigint]> = {
301
+ 0xfd: [0xfd, 2, BigInt(0xfd), BigInt(0xffff)],
302
+ 0xfe: [0xfe, 4, BigInt(0x10000), BigInt(0xffffffff)],
303
+ 0xff: [0xff, 8, BigInt(0x100000000), BigInt('0xffffffffffffffff')],
304
+ };
305
+ // Hoisted: Object.values() would otherwise allocate a fresh array on every encode.
306
+ const limitsList = Object.values(limits);
307
+ return Object.freeze(
287
308
  P.wrap({
288
309
  encodeStream: (w: P.Writer, value: bigint) => {
289
310
  if (typeof value === 'number') value = BigInt(value);
290
- if (0n <= value && value <= 252n) return w.byte(Number(value));
291
- for (const [flag, bytes, start, stop] of Object.values(CSLimits)) {
311
+ if (_0n <= value && value <= COMPACT_DIRECT_MAX) return w.byte(Number(value));
312
+ for (const [flag, bytes, start, stop] of limitsList) {
292
313
  if (start > value || value > stop) continue;
293
314
  w.byte(flag);
294
- for (let i = 0; i < bytes; i++) w.byte(Number((value >> (8n * BigInt(i))) & 0xffn));
315
+ for (let i = 0; i < bytes; i++) w.byte(Number((value >> (_8n * BigInt(i))) & U8_MAX));
295
316
  return;
296
317
  }
297
318
  throw w.err(`VarInt too big: ${value}`);
@@ -299,16 +320,17 @@ export const CompactSize: P.CoderType<bigint> = /* @__PURE__ */ (() =>
299
320
  decodeStream: (r: P.Reader): bigint => {
300
321
  const b0 = r.byte();
301
322
  if (b0 <= 0xfc) return BigInt(b0);
302
- const [_, bytes, start] = CSLimits[b0];
303
- let num = 0n;
304
- for (let i = 0; i < bytes; i++) num |= BigInt(r.byte()) << (8n * BigInt(i));
323
+ const [_, bytes, start] = limits[b0];
324
+ let num = _0n;
325
+ for (let i = 0; i < bytes; i++) num |= BigInt(r.byte()) << (_8n * BigInt(i));
305
326
  // BIP 152 / BIP 174: CompactSize fields must use the shortest encoding,
306
327
  // so wider prefixes for smaller values are rejected here.
307
328
  if (num < start) throw r.err(`Wrong CompactSize(${8 * bytes})`);
308
329
  return num;
309
330
  },
310
331
  })
311
- ))();
332
+ );
333
+ })();
312
334
 
313
335
  // Same thing, but in number instead of bigint. Checks for safe integer inside
314
336
  /**