@sequence0/sdk 1.1.0 → 1.1.2
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/dist/chains/bitcoin-taproot.d.ts +77 -14
- package/dist/chains/bitcoin-taproot.d.ts.map +1 -1
- package/dist/chains/bitcoin-taproot.js +324 -65
- package/dist/chains/bitcoin-taproot.js.map +1 -1
- package/dist/chains/bitcoin.d.ts +12 -7
- package/dist/chains/bitcoin.d.ts.map +1 -1
- package/dist/chains/bitcoin.js +14 -9
- package/dist/chains/bitcoin.js.map +1 -1
- package/dist/core/client.d.ts +4 -5
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +54 -29
- package/dist/core/client.js.map +1 -1
- package/dist/erc4337/types.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/discovery.d.ts.map +1 -1
- package/dist/utils/discovery.js +56 -1
- package/dist/utils/discovery.js.map +1 -1
- package/dist/utils/eip712.d.ts +36 -0
- package/dist/utils/eip712.d.ts.map +1 -0
- package/dist/utils/eip712.js +80 -0
- package/dist/utils/eip712.js.map +1 -0
- package/dist/utils/fee.d.ts +2 -2
- package/dist/utils/fee.js +2 -2
- package/dist/utils/validation.d.ts +8 -0
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +18 -0
- package/dist/utils/validation.js.map +1 -1
- package/dist/utils/websocket.js +1 -1
- package/dist/utils/websocket.js.map +1 -1
- package/dist/wallet/wallet.d.ts +16 -2
- package/dist/wallet/wallet.d.ts.map +1 -1
- package/dist/wallet/wallet.js +73 -28
- package/dist/wallet/wallet.js.map +1 -1
- package/package.json +1 -1
|
@@ -8,10 +8,15 @@
|
|
|
8
8
|
* FROST-secp256k1 produces BIP-340 compatible Schnorr signatures that are
|
|
9
9
|
* NATIVE to Taproot key-path spends -- no signature format conversion needed.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
14
|
-
* -
|
|
11
|
+
* Key features:
|
|
12
|
+
* - **Real secp256k1 EC point arithmetic** for BIP-341 key tweaking
|
|
13
|
+
* (lift_x, point_add, scalar_mul -- no external crypto dependencies)
|
|
14
|
+
* - **Per-input sighash computation** for multi-input transactions
|
|
15
|
+
* (each input gets its own BIP-341 sighash for independent FROST signing)
|
|
16
|
+
* - **Full transaction serialization** with proper segwit witness structure
|
|
17
|
+
* - **Broadcast via Mempool.space API** (mainnet, testnet, signet, regtest)
|
|
18
|
+
*
|
|
19
|
+
* No external dependencies beyond Node.js crypto (SHA-256).
|
|
15
20
|
*
|
|
16
21
|
* @example
|
|
17
22
|
* ```typescript
|
|
@@ -101,9 +106,33 @@ class BitcoinTaprootAdapter {
|
|
|
101
106
|
xOnlyPubkey: bytesToHex(xOnly),
|
|
102
107
|
outputKey: bytesToHex(outputKey),
|
|
103
108
|
scriptPubkey,
|
|
109
|
+
tapTweak: bytesToHex(tweak),
|
|
104
110
|
network: this.network,
|
|
105
111
|
};
|
|
106
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Compute the Taproot tweak for a FROST group public key.
|
|
115
|
+
*
|
|
116
|
+
* The FROST signing protocol must apply this tweak to the group private key
|
|
117
|
+
* before signing. This ensures the Schnorr signature verifies against the
|
|
118
|
+
* tweaked output key (which is what the scriptPubKey commits to).
|
|
119
|
+
*
|
|
120
|
+
* The tweak scalar t = hash_TapTweak(internal_key) is returned as hex.
|
|
121
|
+
* During FROST signing, the group's secret share is tweaked:
|
|
122
|
+
* tweaked_share = share + t (mod n)
|
|
123
|
+
*
|
|
124
|
+
* @param groupPubkeyHex - Hex-encoded FROST group verifying key (33 or 32 bytes)
|
|
125
|
+
* @returns Hex-encoded 32-byte tweak scalar
|
|
126
|
+
*/
|
|
127
|
+
getTapTweak(groupPubkeyHex) {
|
|
128
|
+
const pubkeyClean = groupPubkeyHex.startsWith('0x')
|
|
129
|
+
? groupPubkeyHex.slice(2)
|
|
130
|
+
: groupPubkeyHex;
|
|
131
|
+
const pubkeyBytes = hexToBytes(pubkeyClean);
|
|
132
|
+
const xOnly = extractXOnlyPubkey(pubkeyBytes);
|
|
133
|
+
const tweak = computeTapTweak(xOnly);
|
|
134
|
+
return bytesToHex(tweak);
|
|
135
|
+
}
|
|
107
136
|
// ────────────────────────────────────────────────
|
|
108
137
|
// UTXO Management
|
|
109
138
|
// ────────────────────────────────────────────────
|
|
@@ -283,9 +312,11 @@ class BitcoinTaprootAdapter {
|
|
|
283
312
|
value: u.value,
|
|
284
313
|
scriptPubkey: u.scriptPubkey,
|
|
285
314
|
}));
|
|
286
|
-
// Build the unsigned transaction
|
|
315
|
+
// Build the unsigned transaction with per-input sighashes
|
|
316
|
+
const sighashes = this.computeAllSighashes(inputs, outputs);
|
|
287
317
|
const unsignedTx = {
|
|
288
|
-
sighash:
|
|
318
|
+
sighash: sighashes[0],
|
|
319
|
+
sighashes,
|
|
289
320
|
rawUnsigned: this.serializeUnsignedTx(inputs, outputs),
|
|
290
321
|
inputs,
|
|
291
322
|
outputs,
|
|
@@ -328,8 +359,10 @@ class BitcoinTaprootAdapter {
|
|
|
328
359
|
throw new errors_1.ChainError(`Insufficient funds: ${totalInput} sat available, ` +
|
|
329
360
|
`${totalOutput + fee} sat needed (${totalOutput} output + ${fee} fee)`, 'bitcoin');
|
|
330
361
|
}
|
|
362
|
+
const sighashes = this.computeAllSighashes(inputs, outputs);
|
|
331
363
|
return {
|
|
332
|
-
sighash:
|
|
364
|
+
sighash: sighashes[0],
|
|
365
|
+
sighashes,
|
|
333
366
|
rawUnsigned: this.serializeUnsignedTx(inputs, outputs),
|
|
334
367
|
inputs,
|
|
335
368
|
outputs,
|
|
@@ -338,25 +371,29 @@ class BitcoinTaprootAdapter {
|
|
|
338
371
|
};
|
|
339
372
|
}
|
|
340
373
|
/**
|
|
341
|
-
* Attach
|
|
374
|
+
* Attach FROST Schnorr signature(s) to an unsigned Taproot transaction.
|
|
375
|
+
*
|
|
376
|
+
* The FROST signing protocol produces 64-byte BIP-340 Schnorr signatures
|
|
377
|
+
* (R_x || s) that are directly used as Taproot witness for key-path spends.
|
|
342
378
|
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
379
|
+
* For single-input transactions: pass a single 128-char hex signature.
|
|
380
|
+
* For multi-input transactions: pass signatures separated by commas, or
|
|
381
|
+
* a single signature that will be applied to all inputs (if all inputs
|
|
382
|
+
* share the same signing key and the caller signs each sighash separately).
|
|
345
383
|
*
|
|
346
384
|
* @param unsignedTxHex - Hex-encoded unsigned transaction (from buildTransaction)
|
|
347
|
-
* @param signatureHex - 64-byte FROST Schnorr signature
|
|
385
|
+
* @param signatureHex - 64-byte FROST Schnorr signature(s). For multi-input
|
|
386
|
+
* transactions, separate per-input signatures with commas.
|
|
348
387
|
* @returns Hex-encoded signed transaction ready for broadcast
|
|
349
388
|
*/
|
|
350
389
|
async attachSignature(unsignedTxHex, signatureHex) {
|
|
351
390
|
try {
|
|
352
|
-
const sig = signatureHex.startsWith('0x') ? signatureHex.slice(2) : signatureHex;
|
|
353
|
-
if (sig.length !== 128) {
|
|
354
|
-
throw new Error(`Schnorr signature must be 64 bytes (128 hex chars), got ${sig.length} chars`);
|
|
355
|
-
}
|
|
356
391
|
// Parse the unsigned transaction
|
|
357
392
|
const unsignedTx = JSON.parse(Buffer.from(unsignedTxHex, 'hex').toString());
|
|
393
|
+
// Parse signature(s)
|
|
394
|
+
const signatures = this.parseSignatures(signatureHex, unsignedTx.inputs.length);
|
|
358
395
|
// Build the signed transaction with Taproot witness
|
|
359
|
-
const signedTx = this.serializeSignedTx(unsignedTx,
|
|
396
|
+
const signedTx = this.serializeSignedTx(unsignedTx, signatures);
|
|
360
397
|
return Buffer.from(JSON.stringify(signedTx)).toString('hex');
|
|
361
398
|
}
|
|
362
399
|
catch (e) {
|
|
@@ -364,18 +401,58 @@ class BitcoinTaprootAdapter {
|
|
|
364
401
|
}
|
|
365
402
|
}
|
|
366
403
|
/**
|
|
367
|
-
* Attach
|
|
404
|
+
* Attach FROST Schnorr signature(s) with full output (returns structured data).
|
|
368
405
|
*
|
|
369
406
|
* @param unsignedTx - The UnsignedTaprootTx from buildUnsignedTx
|
|
370
|
-
* @param signatureHex - 64-byte FROST Schnorr signature (hex
|
|
407
|
+
* @param signatureHex - 64-byte FROST Schnorr signature(s) (hex). For multi-input
|
|
408
|
+
* transactions, pass an array of per-input signatures or a comma-separated string.
|
|
371
409
|
* @returns SignedTaprootTx with raw_signed, txid, and vsize
|
|
372
410
|
*/
|
|
373
411
|
attachSignatureToTx(unsignedTx, signatureHex) {
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
412
|
+
const sigs = Array.isArray(signatureHex)
|
|
413
|
+
? signatureHex.map(s => {
|
|
414
|
+
const clean = s.startsWith('0x') ? s.slice(2) : s;
|
|
415
|
+
if (clean.length !== 128) {
|
|
416
|
+
throw new errors_1.ChainError(`Schnorr signature must be 64 bytes (128 hex chars), got ${clean.length}`, 'bitcoin');
|
|
417
|
+
}
|
|
418
|
+
return clean;
|
|
419
|
+
})
|
|
420
|
+
: this.parseSignatures(signatureHex, unsignedTx.inputs.length);
|
|
421
|
+
return this.serializeSignedTx(unsignedTx, sigs);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Parse signature hex into per-input signatures.
|
|
425
|
+
* Supports: single sig (applied to all inputs), comma-separated, or concatenated.
|
|
426
|
+
*/
|
|
427
|
+
parseSignatures(signatureHex, inputCount) {
|
|
428
|
+
const raw = signatureHex.startsWith('0x') ? signatureHex.slice(2) : signatureHex;
|
|
429
|
+
// Check for comma-separated signatures
|
|
430
|
+
if (raw.includes(',')) {
|
|
431
|
+
const parts = raw.split(',').map(s => s.trim());
|
|
432
|
+
if (parts.length !== inputCount) {
|
|
433
|
+
throw new Error(`Expected ${inputCount} signatures for ${inputCount} inputs, got ${parts.length}`);
|
|
434
|
+
}
|
|
435
|
+
for (const s of parts) {
|
|
436
|
+
if (s.length !== 128) {
|
|
437
|
+
throw new Error(`Each Schnorr signature must be 128 hex chars, got ${s.length}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return parts;
|
|
377
441
|
}
|
|
378
|
-
|
|
442
|
+
// Single signature (128 hex chars) — replicate for all inputs
|
|
443
|
+
if (raw.length === 128) {
|
|
444
|
+
return new Array(inputCount).fill(raw);
|
|
445
|
+
}
|
|
446
|
+
// Concatenated signatures (128 * inputCount hex chars)
|
|
447
|
+
if (raw.length === 128 * inputCount) {
|
|
448
|
+
const sigs = [];
|
|
449
|
+
for (let i = 0; i < inputCount; i++) {
|
|
450
|
+
sigs.push(raw.slice(i * 128, (i + 1) * 128));
|
|
451
|
+
}
|
|
452
|
+
return sigs;
|
|
453
|
+
}
|
|
454
|
+
throw new Error(`Invalid signature format: expected 128 hex chars (single), ` +
|
|
455
|
+
`${128 * inputCount} chars (concatenated), or comma-separated. Got ${raw.length} chars.`);
|
|
379
456
|
}
|
|
380
457
|
// ────────────────────────────────────────────────
|
|
381
458
|
// Broadcast
|
|
@@ -562,10 +639,16 @@ class BitcoinTaprootAdapter {
|
|
|
562
639
|
return bytesToHex(new Uint8Array(buf));
|
|
563
640
|
}
|
|
564
641
|
/**
|
|
565
|
-
* Serialize a signed Taproot transaction.
|
|
642
|
+
* Serialize a signed Taproot transaction with per-input signatures.
|
|
643
|
+
*
|
|
644
|
+
* @param unsignedTx - The unsigned transaction data
|
|
645
|
+
* @param signatures - Array of per-input signature hex strings (128 chars each)
|
|
566
646
|
*/
|
|
567
|
-
serializeSignedTx(unsignedTx,
|
|
568
|
-
|
|
647
|
+
serializeSignedTx(unsignedTx, signatures) {
|
|
648
|
+
if (signatures.length !== unsignedTx.inputs.length) {
|
|
649
|
+
throw new Error(`Signature count (${signatures.length}) does not match ` +
|
|
650
|
+
`input count (${unsignedTx.inputs.length})`);
|
|
651
|
+
}
|
|
569
652
|
const buf = [];
|
|
570
653
|
// Version 2
|
|
571
654
|
pushLE32(buf, 2);
|
|
@@ -591,8 +674,9 @@ class BitcoinTaprootAdapter {
|
|
|
591
674
|
pushVarint(buf, script.length);
|
|
592
675
|
buf.push(...script);
|
|
593
676
|
}
|
|
594
|
-
// Witness:
|
|
677
|
+
// Witness: per-input Schnorr signatures for key-path spend
|
|
595
678
|
for (let i = 0; i < unsignedTx.inputs.length; i++) {
|
|
679
|
+
const sigBytes = hexToBytes(signatures[i]);
|
|
596
680
|
buf.push(0x01); // 1 witness item
|
|
597
681
|
buf.push(0x40); // 64 bytes (Schnorr signature, no sighash type suffix)
|
|
598
682
|
buf.push(...sigBytes);
|
|
@@ -630,20 +714,18 @@ class BitcoinTaprootAdapter {
|
|
|
630
714
|
* - Spend type (0x00 for key-path, no annex)
|
|
631
715
|
* - Input index (4 bytes LE)
|
|
632
716
|
*/
|
|
633
|
-
|
|
634
|
-
|
|
717
|
+
/**
|
|
718
|
+
* Compute all per-input BIP-341 sighashes for the transaction.
|
|
719
|
+
*
|
|
720
|
+
* Each input has its own sighash because the input_index field differs.
|
|
721
|
+
* The common transaction-level hashes (prevouts, amounts, scripts, sequences,
|
|
722
|
+
* outputs) are precomputed once and reused across all inputs.
|
|
723
|
+
*
|
|
724
|
+
* @returns Array of hex-encoded sighashes, one per input
|
|
725
|
+
*/
|
|
726
|
+
computeAllSighashes(inputs, outputs) {
|
|
727
|
+
// Precompute the transaction-level hashes (shared across all inputs)
|
|
635
728
|
const tagHash = sha256(new TextEncoder().encode('TapSighash'));
|
|
636
|
-
const parts = [];
|
|
637
|
-
// Tag hash prefix (used twice per BIP-340 tagged hash convention)
|
|
638
|
-
parts.push(...tagHash, ...tagHash);
|
|
639
|
-
// Epoch (1 byte)
|
|
640
|
-
parts.push(0x00);
|
|
641
|
-
// Sighash type: SIGHASH_DEFAULT (1 byte)
|
|
642
|
-
parts.push(0x00);
|
|
643
|
-
// Transaction version (4 bytes LE)
|
|
644
|
-
pushLE32(parts, 2);
|
|
645
|
-
// Locktime (4 bytes LE)
|
|
646
|
-
pushLE32(parts, 0);
|
|
647
729
|
// SHA-256 of prevouts
|
|
648
730
|
const prevoutsBuf = [];
|
|
649
731
|
for (const input of inputs) {
|
|
@@ -652,13 +734,13 @@ class BitcoinTaprootAdapter {
|
|
|
652
734
|
prevoutsBuf.push(...txidBytes);
|
|
653
735
|
pushLE32(prevoutsBuf, input.vout);
|
|
654
736
|
}
|
|
655
|
-
|
|
737
|
+
const hashPrevouts = sha256(new Uint8Array(prevoutsBuf));
|
|
656
738
|
// SHA-256 of amounts
|
|
657
739
|
const amountsBuf = [];
|
|
658
740
|
for (const input of inputs) {
|
|
659
741
|
pushLE64(amountsBuf, input.value);
|
|
660
742
|
}
|
|
661
|
-
|
|
743
|
+
const hashAmounts = sha256(new Uint8Array(amountsBuf));
|
|
662
744
|
// SHA-256 of scriptPubKeys (with compact size prefix)
|
|
663
745
|
const scriptsBuf = [];
|
|
664
746
|
for (const input of inputs) {
|
|
@@ -666,13 +748,13 @@ class BitcoinTaprootAdapter {
|
|
|
666
748
|
pushVarint(scriptsBuf, script.length);
|
|
667
749
|
scriptsBuf.push(...script);
|
|
668
750
|
}
|
|
669
|
-
|
|
751
|
+
const hashScripts = sha256(new Uint8Array(scriptsBuf));
|
|
670
752
|
// SHA-256 of sequences
|
|
671
753
|
const seqsBuf = [];
|
|
672
754
|
for (let i = 0; i < inputs.length; i++) {
|
|
673
755
|
pushLE32(seqsBuf, 0xfffffffd);
|
|
674
756
|
}
|
|
675
|
-
|
|
757
|
+
const hashSequences = sha256(new Uint8Array(seqsBuf));
|
|
676
758
|
// SHA-256 of outputs
|
|
677
759
|
const outputsBuf = [];
|
|
678
760
|
for (const output of outputs) {
|
|
@@ -681,18 +763,185 @@ class BitcoinTaprootAdapter {
|
|
|
681
763
|
pushVarint(outputsBuf, script.length);
|
|
682
764
|
outputsBuf.push(...script);
|
|
683
765
|
}
|
|
684
|
-
|
|
685
|
-
//
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
766
|
+
const hashOutputs = sha256(new Uint8Array(outputsBuf));
|
|
767
|
+
// Compute per-input sighash
|
|
768
|
+
const sighashes = [];
|
|
769
|
+
for (let inputIdx = 0; inputIdx < inputs.length; inputIdx++) {
|
|
770
|
+
const parts = [];
|
|
771
|
+
// Tag hash prefix (used twice per BIP-340 tagged hash convention)
|
|
772
|
+
parts.push(...tagHash, ...tagHash);
|
|
773
|
+
// Epoch (1 byte)
|
|
774
|
+
parts.push(0x00);
|
|
775
|
+
// Sighash type: SIGHASH_DEFAULT (1 byte)
|
|
776
|
+
parts.push(0x00);
|
|
777
|
+
// Transaction version (4 bytes LE)
|
|
778
|
+
pushLE32(parts, 2);
|
|
779
|
+
// Locktime (4 bytes LE)
|
|
780
|
+
pushLE32(parts, 0);
|
|
781
|
+
// Precomputed hashes
|
|
782
|
+
parts.push(...hashPrevouts);
|
|
783
|
+
parts.push(...hashAmounts);
|
|
784
|
+
parts.push(...hashScripts);
|
|
785
|
+
parts.push(...hashSequences);
|
|
786
|
+
parts.push(...hashOutputs);
|
|
787
|
+
// Spend type: 0x00 (key-path, no annex)
|
|
788
|
+
parts.push(0x00);
|
|
789
|
+
// Input index (4 bytes LE) — THIS is what differs per input
|
|
790
|
+
pushLE32(parts, inputIdx);
|
|
791
|
+
// Final sighash = SHA-256 of the tagged hash message
|
|
792
|
+
const sighash = sha256(new Uint8Array(parts));
|
|
793
|
+
sighashes.push(bytesToHex(sighash));
|
|
794
|
+
}
|
|
795
|
+
return sighashes;
|
|
693
796
|
}
|
|
694
797
|
}
|
|
695
798
|
exports.BitcoinTaprootAdapter = BitcoinTaprootAdapter;
|
|
799
|
+
const Secp256k1 = {
|
|
800
|
+
/** Field prime */
|
|
801
|
+
P: 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn,
|
|
802
|
+
/** Curve order */
|
|
803
|
+
N: 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n,
|
|
804
|
+
/** Generator x */
|
|
805
|
+
Gx: 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n,
|
|
806
|
+
/** Generator y */
|
|
807
|
+
Gy: 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n,
|
|
808
|
+
/** Get generator point */
|
|
809
|
+
G() {
|
|
810
|
+
return { x: this.Gx, y: this.Gy };
|
|
811
|
+
},
|
|
812
|
+
/**
|
|
813
|
+
* Modular exponentiation: base^exp mod m
|
|
814
|
+
* Uses square-and-multiply for efficiency.
|
|
815
|
+
*/
|
|
816
|
+
modPow(base, exp, m) {
|
|
817
|
+
let result = 1n;
|
|
818
|
+
base = ((base % m) + m) % m;
|
|
819
|
+
while (exp > 0n) {
|
|
820
|
+
if (exp & 1n) {
|
|
821
|
+
result = (result * base) % m;
|
|
822
|
+
}
|
|
823
|
+
exp >>= 1n;
|
|
824
|
+
base = (base * base) % m;
|
|
825
|
+
}
|
|
826
|
+
return result;
|
|
827
|
+
},
|
|
828
|
+
/** Modular inverse using Fermat's little theorem: a^(p-2) mod p */
|
|
829
|
+
modInverse(a, m) {
|
|
830
|
+
return this.modPow(((a % m) + m) % m, m - 2n, m);
|
|
831
|
+
},
|
|
832
|
+
/** Modular square root: returns r such that r^2 = a (mod p), or throws.
|
|
833
|
+
* Uses the Tonelli-Shanks shortcut for p = 3 mod 4: r = a^((p+1)/4) mod p */
|
|
834
|
+
modSqrt(a) {
|
|
835
|
+
const p = this.P;
|
|
836
|
+
// secp256k1 p % 4 == 3, so we can use the simple formula
|
|
837
|
+
const r = this.modPow(((a % p) + p) % p, (p + 1n) / 4n, p);
|
|
838
|
+
if ((r * r) % p !== ((a % p) + p) % p) {
|
|
839
|
+
throw new Error('No square root exists for this value');
|
|
840
|
+
}
|
|
841
|
+
return r;
|
|
842
|
+
},
|
|
843
|
+
/**
|
|
844
|
+
* Lift an x-only public key to a full curve point with even y-coordinate.
|
|
845
|
+
* Per BIP-340: given x, compute y = sqrt(x^3 + 7), pick the even root.
|
|
846
|
+
*/
|
|
847
|
+
liftX(xBytes) {
|
|
848
|
+
const x = bytesToBigInt(xBytes);
|
|
849
|
+
const p = this.P;
|
|
850
|
+
if (x >= p) {
|
|
851
|
+
throw new Error('x-coordinate exceeds field prime');
|
|
852
|
+
}
|
|
853
|
+
// y^2 = x^3 + 7 mod p
|
|
854
|
+
const y2 = (this.modPow(x, 3n, p) + 7n) % p;
|
|
855
|
+
let y = this.modSqrt(y2);
|
|
856
|
+
// BIP-340: pick the even y (y % 2 == 0)
|
|
857
|
+
if (y & 1n) {
|
|
858
|
+
y = p - y;
|
|
859
|
+
}
|
|
860
|
+
return { x, y };
|
|
861
|
+
},
|
|
862
|
+
/**
|
|
863
|
+
* Point addition: P + Q on secp256k1.
|
|
864
|
+
* Handles identity, doubling, and general addition.
|
|
865
|
+
*/
|
|
866
|
+
pointAdd(p1, p2) {
|
|
867
|
+
if (p1 === null)
|
|
868
|
+
return p2;
|
|
869
|
+
if (p2 === null)
|
|
870
|
+
return p1;
|
|
871
|
+
const p = this.P;
|
|
872
|
+
if (p1.x === p2.x) {
|
|
873
|
+
if (p1.y === p2.y) {
|
|
874
|
+
// Point doubling
|
|
875
|
+
return this.pointDouble(p1);
|
|
876
|
+
}
|
|
877
|
+
// P + (-P) = O (point at infinity)
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
// General addition: lambda = (y2 - y1) / (x2 - x1)
|
|
881
|
+
const dy = ((p2.y - p1.y) % p + p) % p;
|
|
882
|
+
const dx = ((p2.x - p1.x) % p + p) % p;
|
|
883
|
+
const lambda = (dy * this.modInverse(dx, p)) % p;
|
|
884
|
+
const x3 = ((lambda * lambda - p1.x - p2.x) % p + p) % p;
|
|
885
|
+
const y3 = ((lambda * (p1.x - x3) - p1.y) % p + p) % p;
|
|
886
|
+
return { x: x3, y: y3 };
|
|
887
|
+
},
|
|
888
|
+
/** Point doubling: 2P on secp256k1 */
|
|
889
|
+
pointDouble(pt) {
|
|
890
|
+
const p = this.P;
|
|
891
|
+
if (pt.y === 0n)
|
|
892
|
+
return null;
|
|
893
|
+
// lambda = (3 * x^2 + a) / (2 * y) where a = 0 for secp256k1
|
|
894
|
+
const num = (3n * pt.x * pt.x) % p;
|
|
895
|
+
const den = (2n * pt.y) % p;
|
|
896
|
+
const lambda = (num * this.modInverse(den, p)) % p;
|
|
897
|
+
const x3 = ((lambda * lambda - 2n * pt.x) % p + p) % p;
|
|
898
|
+
const y3 = ((lambda * (pt.x - x3) - pt.y) % p + p) % p;
|
|
899
|
+
return { x: x3, y: y3 };
|
|
900
|
+
},
|
|
901
|
+
/**
|
|
902
|
+
* Scalar multiplication: n * G using double-and-add.
|
|
903
|
+
* Only used for tweaking, so performance is not critical.
|
|
904
|
+
*/
|
|
905
|
+
mulG(n) {
|
|
906
|
+
const order = this.N;
|
|
907
|
+
n = ((n % order) + order) % order;
|
|
908
|
+
if (n === 0n) {
|
|
909
|
+
throw new Error('Zero scalar in mulG');
|
|
910
|
+
}
|
|
911
|
+
let result = null;
|
|
912
|
+
let base = this.G();
|
|
913
|
+
let k = n;
|
|
914
|
+
while (k > 0n) {
|
|
915
|
+
if (k & 1n) {
|
|
916
|
+
result = this.pointAdd(result, base);
|
|
917
|
+
}
|
|
918
|
+
base = this.pointAdd(base, base);
|
|
919
|
+
k >>= 1n;
|
|
920
|
+
}
|
|
921
|
+
if (result === null) {
|
|
922
|
+
throw new Error('mulG produced point at infinity');
|
|
923
|
+
}
|
|
924
|
+
return result;
|
|
925
|
+
},
|
|
926
|
+
};
|
|
927
|
+
/** Convert a Uint8Array to a bigint (big-endian) */
|
|
928
|
+
function bytesToBigInt(bytes) {
|
|
929
|
+
let result = 0n;
|
|
930
|
+
for (const b of bytes) {
|
|
931
|
+
result = (result << 8n) | BigInt(b);
|
|
932
|
+
}
|
|
933
|
+
return result;
|
|
934
|
+
}
|
|
935
|
+
/** Convert a bigint to a 32-byte Uint8Array (big-endian, zero-padded) */
|
|
936
|
+
function bigIntToBytes32(n) {
|
|
937
|
+
const bytes = new Uint8Array(32);
|
|
938
|
+
let val = n;
|
|
939
|
+
for (let i = 31; i >= 0; i--) {
|
|
940
|
+
bytes[i] = Number(val & 0xffn);
|
|
941
|
+
val >>= 8n;
|
|
942
|
+
}
|
|
943
|
+
return bytes;
|
|
944
|
+
}
|
|
696
945
|
// ────────────────────────────────────────────────
|
|
697
946
|
// Pure Helper Functions
|
|
698
947
|
// ────────────────────────────────────────────────
|
|
@@ -727,23 +976,33 @@ function computeTapTweak(internalKey) {
|
|
|
727
976
|
return sha256(msg);
|
|
728
977
|
}
|
|
729
978
|
/**
|
|
730
|
-
* Tweak a public key
|
|
979
|
+
* Tweak a public key per BIP-341: Q = lift_x(P) + int(tweak) * G
|
|
731
980
|
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
981
|
+
* Uses real secp256k1 EC point arithmetic. The output key Q is the
|
|
982
|
+
* x-only coordinate of the resulting point. If the resulting point
|
|
983
|
+
* has an odd y-coordinate, the tweak is negated (BIP-341 spec).
|
|
734
984
|
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
985
|
+
* @param internalKey - 32-byte x-only internal public key
|
|
986
|
+
* @param tweak - 32-byte tweak scalar (hash_TapTweak output)
|
|
987
|
+
* @returns 32-byte x-only output key
|
|
738
988
|
*/
|
|
739
989
|
function tweakPublicKey(internalKey, tweak) {
|
|
740
|
-
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
990
|
+
// Lift the x-only key to a full point with even y
|
|
991
|
+
const P = Secp256k1.liftX(internalKey);
|
|
992
|
+
// Convert tweak bytes to a bigint scalar
|
|
993
|
+
const t = bytesToBigInt(tweak);
|
|
994
|
+
if (t >= Secp256k1.N) {
|
|
995
|
+
throw new Error('Tweak scalar exceeds curve order');
|
|
996
|
+
}
|
|
997
|
+
// Compute t * G
|
|
998
|
+
const tG = Secp256k1.mulG(t);
|
|
999
|
+
// Compute Q = P + t * G
|
|
1000
|
+
const Q = Secp256k1.pointAdd(P, tG);
|
|
1001
|
+
if (Q === null) {
|
|
1002
|
+
throw new Error('Tweaked key is point at infinity');
|
|
1003
|
+
}
|
|
1004
|
+
// Return x-coordinate of Q as 32 bytes
|
|
1005
|
+
return bigIntToBytes32(Q.x);
|
|
747
1006
|
}
|
|
748
1007
|
/** Get the Bech32 HRP for a Bitcoin network */
|
|
749
1008
|
function networkToHrp(network) {
|