@scure/btc-signer 1.6.0 → 1.7.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/README.md +142 -24
- package/esm/musig2.d.ts +148 -0
- package/esm/musig2.d.ts.map +1 -0
- package/esm/musig2.js +405 -0
- package/esm/musig2.js.map +1 -0
- package/esm/payment.d.ts.map +1 -1
- package/esm/psbt.d.ts +462 -462
- package/esm/psbt.d.ts.map +1 -1
- package/esm/psbt.js +8 -4
- package/esm/psbt.js.map +1 -1
- package/esm/script.d.ts +16 -16
- package/esm/script.d.ts.map +1 -1
- package/esm/transaction.d.ts +3 -2
- package/esm/transaction.d.ts.map +1 -1
- package/esm/transaction.js +3 -3
- package/esm/transaction.js.map +1 -1
- package/esm/utils.d.ts.map +1 -1
- package/esm/utxo.d.ts +73 -73
- package/esm/utxo.d.ts.map +1 -1
- package/esm/utxo.js +14 -10
- package/esm/utxo.js.map +1 -1
- package/musig2.d.ts +148 -0
- package/musig2.d.ts.map +1 -0
- package/musig2.js +417 -0
- package/musig2.js.map +1 -0
- package/package.json +8 -4
- package/payment.d.ts.map +1 -1
- package/psbt.d.ts +462 -462
- package/psbt.d.ts.map +1 -1
- package/psbt.js +8 -4
- package/psbt.js.map +1 -1
- package/script.d.ts +16 -16
- package/script.d.ts.map +1 -1
- package/src/musig2.ts +517 -0
- package/src/psbt.ts +9 -3
- package/src/transaction.ts +14 -5
- package/src/utxo.ts +23 -14
- package/transaction.d.ts +3 -2
- package/transaction.d.ts.map +1 -1
- package/transaction.js +3 -3
- package/transaction.js.map +1 -1
- package/utils.d.ts.map +1 -1
- package/utxo.d.ts +73 -73
- package/utxo.d.ts.map +1 -1
- package/utxo.js +14 -10
- package/utxo.js.map +1 -1
package/src/musig2.ts
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import { mod } from '@noble/curves/abstract/modular';
|
|
2
|
+
import {
|
|
3
|
+
aInRange,
|
|
4
|
+
bytesToNumberBE,
|
|
5
|
+
concatBytes,
|
|
6
|
+
equalBytes,
|
|
7
|
+
numberToBytesBE,
|
|
8
|
+
} from '@noble/curves/abstract/utils';
|
|
9
|
+
import { schnorr, secp256k1 } from '@noble/curves/secp256k1';
|
|
10
|
+
import { abytes, anumber } from '@noble/hashes/_assert';
|
|
11
|
+
import { randomBytes } from '@noble/hashes/utils';
|
|
12
|
+
import * as P from 'micro-packed';
|
|
13
|
+
import { compareBytes } from './utils.js';
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
MuSig2. This is not the full protocol: only an implementation of primitives from BIP-327.
|
|
17
|
+
The implementation can be used to create own protocol,
|
|
18
|
+
but you need to implement nonce/partial signatures exchange yourself.
|
|
19
|
+
Someday BIP-373 will be more "implementable" and we can use this from PSBT.
|
|
20
|
+
|
|
21
|
+
Links:
|
|
22
|
+
- https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki#user-content-Test_Vectors_and_Reference_Code
|
|
23
|
+
- https://github.com/bitcoin/bips/blob/master/bip-0373.mediawiki (PSBT MUSIG2): very raw, no vectors, not implemented for now.
|
|
24
|
+
- https://github.com/bitcoin/bips/blob/master/bip-0327/reference.py
|
|
25
|
+
*/
|
|
26
|
+
// Types
|
|
27
|
+
/**
|
|
28
|
+
* Represents a pair of public and secret nonces used in MuSig2 signing.
|
|
29
|
+
*/
|
|
30
|
+
export type Nonces = { public: Uint8Array; secret: Uint8Array };
|
|
31
|
+
/**
|
|
32
|
+
* Represents a deterministic nonce, including its public part and the resulting partial signature.
|
|
33
|
+
*/
|
|
34
|
+
export type DetNonce = { publicNonce: Uint8Array; partialSig: Uint8Array };
|
|
35
|
+
/**
|
|
36
|
+
* Represents an error indicating an invalid contribution from a signer.
|
|
37
|
+
* This allows pointing out which participant is malicious and what specifically is wrong.
|
|
38
|
+
*/
|
|
39
|
+
export class InvalidContributionErr extends Error {
|
|
40
|
+
readonly idx: number; // Indice of participant
|
|
41
|
+
constructor(idx: number, m: string) {
|
|
42
|
+
super(m);
|
|
43
|
+
this.idx = idx;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Utils
|
|
48
|
+
const { taggedHash, pointToBytes } = schnorr.utils;
|
|
49
|
+
const Point = secp256k1.ProjectivePoint;
|
|
50
|
+
type Point = typeof Point.BASE;
|
|
51
|
+
const PUBKEY_LEN = 33;
|
|
52
|
+
const ZERO = new Uint8Array(PUBKEY_LEN); // Compressed zero point
|
|
53
|
+
const SECP_N = secp256k1.CURVE.n;
|
|
54
|
+
|
|
55
|
+
// Encoding
|
|
56
|
+
// TODO: re-use in PSBT?
|
|
57
|
+
const compressed = P.apply(P.bytes(33), {
|
|
58
|
+
decode: (p: Point) => (isZero(p) ? ZERO : p.toRawBytes(true)),
|
|
59
|
+
encode: (b: Uint8Array) => (equalBytes(b, ZERO) ? Point.ZERO : Point.fromHex(b)),
|
|
60
|
+
});
|
|
61
|
+
const scalar = P.validate(P.U256BE, (n) => {
|
|
62
|
+
aInRange('n', n, 1n, SECP_N);
|
|
63
|
+
return n;
|
|
64
|
+
});
|
|
65
|
+
const PubNonce = P.struct({ R1: compressed, R2: compressed });
|
|
66
|
+
const SecretNonce = P.struct({ k1: scalar, k2: scalar, publicKey: P.bytes(PUBKEY_LEN) });
|
|
67
|
+
|
|
68
|
+
function abytesOptional(b: Uint8Array | undefined, ...lengths: number[]) {
|
|
69
|
+
if (b !== undefined) abytes(b, ...lengths);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function abytesArray(lst: Uint8Array[], ...lengths: number[]) {
|
|
73
|
+
if (!Array.isArray(lst)) throw new Error('expected array');
|
|
74
|
+
lst.forEach((i) => abytes(i, ...lengths));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function aXonly(lst: boolean[]) {
|
|
78
|
+
if (!Array.isArray(lst)) throw new Error('expected array');
|
|
79
|
+
lst.forEach((i, j) => {
|
|
80
|
+
if (typeof i !== 'boolean')
|
|
81
|
+
throw new Error('expected boolean in xOnly array, got' + i + '(' + j + ')');
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const modN = (x: bigint) => mod(x, SECP_N);
|
|
86
|
+
const taggedInt = (tag: string, ...messages: Uint8Array[]) =>
|
|
87
|
+
modN(bytesToNumberBE(taggedHash(tag, ...messages)));
|
|
88
|
+
const evenScalar = (p: Point, n: bigint) => (p.hasEvenY() ? n : modN(-n));
|
|
89
|
+
|
|
90
|
+
// Short utility for compat with reference implementation
|
|
91
|
+
export function IndividualPubkey(seckey: Uint8Array): Uint8Array {
|
|
92
|
+
return secp256k1.getPublicKey(seckey, true);
|
|
93
|
+
}
|
|
94
|
+
// Same, but returns Point
|
|
95
|
+
function mulBase(n: bigint): Point {
|
|
96
|
+
return Point.BASE.multiply(n);
|
|
97
|
+
}
|
|
98
|
+
function isZero(point: Point): boolean {
|
|
99
|
+
return point.equals(Point.ZERO);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Lexicographically sorts an array of public keys.
|
|
104
|
+
* @param publicKeys An array of public keys (Uint8Array).
|
|
105
|
+
* @returns A new array containing the sorted public keys.
|
|
106
|
+
* @throws {Error} If the input is not an array or if any element is not a Uint8Array of the correct length.
|
|
107
|
+
*/
|
|
108
|
+
export function sortKeys(publicKeys: Uint8Array[]): Uint8Array[] {
|
|
109
|
+
abytesArray(publicKeys, PUBKEY_LEN);
|
|
110
|
+
return publicKeys.sort(compareBytes);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Finds second distinct key (to make coefficient 1)
|
|
114
|
+
function getSecondKey(publicKeys: Uint8Array[]): Uint8Array {
|
|
115
|
+
abytesArray(publicKeys, PUBKEY_LEN);
|
|
116
|
+
for (let j = 1; j < publicKeys.length; j++)
|
|
117
|
+
if (!equalBytes(publicKeys[j], publicKeys[0])) return publicKeys[j];
|
|
118
|
+
return ZERO;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function keyAggL(publicKeys: Uint8Array[]) {
|
|
122
|
+
abytesArray(publicKeys, PUBKEY_LEN);
|
|
123
|
+
return taggedHash('KeyAgg list', ...publicKeys);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function keyAggCoeffInternal(
|
|
127
|
+
publicKey1: Uint8Array,
|
|
128
|
+
publicKey2: Uint8Array,
|
|
129
|
+
L: Uint8Array
|
|
130
|
+
): bigint {
|
|
131
|
+
abytes(publicKey1, PUBKEY_LEN);
|
|
132
|
+
abytes(publicKey2, PUBKEY_LEN);
|
|
133
|
+
if (equalBytes(publicKey1, publicKey2)) return 1n;
|
|
134
|
+
return taggedInt('KeyAgg coefficient', L, publicKey1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Aggregates multiple public keys using the MuSig2 key aggregation algorithm.
|
|
139
|
+
* @param publicKeys An array of individual public keys (Uint8Array).
|
|
140
|
+
* @param tweaks An optional array of tweaks (Uint8Array) to apply to the aggregate public key.
|
|
141
|
+
* @param isXonly An optional array of booleans indicating whether each tweak is an X-only tweak.
|
|
142
|
+
* @returns An object containing the aggregate public key, accumulated sign, and accumulated tweak.
|
|
143
|
+
* @throws {Error} If the input is invalid, such as non array publicKeys, tweaks and isXonly array length not matching.
|
|
144
|
+
* @throws {InvalidContributionErr} If any of the public keys are invalid and cannot be processed.
|
|
145
|
+
*/
|
|
146
|
+
export function keyAggregate(
|
|
147
|
+
publicKeys: Uint8Array[],
|
|
148
|
+
tweaks: Uint8Array[] = [],
|
|
149
|
+
isXonly: boolean[] = []
|
|
150
|
+
) {
|
|
151
|
+
abytesArray(publicKeys, PUBKEY_LEN);
|
|
152
|
+
abytesArray(tweaks, 32);
|
|
153
|
+
if (tweaks.length !== isXonly.length)
|
|
154
|
+
throw new Error('The tweaks and isXonly arrays must have the same length');
|
|
155
|
+
// Aggregate
|
|
156
|
+
const pk2 = getSecondKey(publicKeys);
|
|
157
|
+
const L = keyAggL(publicKeys);
|
|
158
|
+
let aggPublicKey = Point.ZERO;
|
|
159
|
+
for (let i = 0; i < publicKeys.length; i++) {
|
|
160
|
+
let Pi;
|
|
161
|
+
try {
|
|
162
|
+
Pi = Point.fromHex(publicKeys[i]);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
throw new InvalidContributionErr(i, 'pubkey');
|
|
165
|
+
}
|
|
166
|
+
aggPublicKey = aggPublicKey.add(Pi.multiply(keyAggCoeffInternal(publicKeys[i], pk2, L)));
|
|
167
|
+
}
|
|
168
|
+
let gAcc = 1n;
|
|
169
|
+
let tweakAcc = 0n;
|
|
170
|
+
// Apply tweaks
|
|
171
|
+
for (let i = 0; i < tweaks.length; i++) {
|
|
172
|
+
const g = isXonly[i] && !aggPublicKey.hasEvenY() ? modN(-1n) : 1n;
|
|
173
|
+
const t = bytesToNumberBE(tweaks[i]);
|
|
174
|
+
aInRange('tweak', t, 0n, SECP_N);
|
|
175
|
+
aggPublicKey = aggPublicKey.multiply(g).add(mulBase(t));
|
|
176
|
+
if (isZero(aggPublicKey)) throw new Error('The result of tweaking cannot be infinity');
|
|
177
|
+
gAcc = modN(g * gAcc);
|
|
178
|
+
tweakAcc = modN(t + g * tweakAcc);
|
|
179
|
+
}
|
|
180
|
+
return { aggPublicKey, gAcc, tweakAcc };
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Exports the aggregate public key to a byte array.
|
|
184
|
+
* @param ctx The result of the keyAggregate function.
|
|
185
|
+
* @returns The aggregate public key as a byte array.
|
|
186
|
+
*/
|
|
187
|
+
export function keyAggExport(ctx: ReturnType<typeof keyAggregate>) {
|
|
188
|
+
return pointToBytes(ctx.aggPublicKey);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function aux(secret: Uint8Array, rand: Uint8Array): Uint8Array {
|
|
192
|
+
const rand2 = taggedHash('MuSig/aux', rand);
|
|
193
|
+
if (secret.length !== rand2.length) throw new Error('Cannot XOR arrays of different lengths');
|
|
194
|
+
const res = new Uint8Array(secret.length);
|
|
195
|
+
for (let i = 0; i < secret.length; i++) res[i] = secret[i] ^ rand2[i];
|
|
196
|
+
return res;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const nonceHash = (
|
|
200
|
+
rand: Uint8Array,
|
|
201
|
+
publicKey: Uint8Array,
|
|
202
|
+
aggPublicKey: Uint8Array,
|
|
203
|
+
i: number,
|
|
204
|
+
msgPrefixed: Uint8Array,
|
|
205
|
+
extraIn: Uint8Array
|
|
206
|
+
): bigint =>
|
|
207
|
+
taggedInt(
|
|
208
|
+
'MuSig/nonce',
|
|
209
|
+
rand,
|
|
210
|
+
new Uint8Array([publicKey.length]),
|
|
211
|
+
publicKey,
|
|
212
|
+
new Uint8Array([aggPublicKey.length]),
|
|
213
|
+
aggPublicKey,
|
|
214
|
+
msgPrefixed,
|
|
215
|
+
numberToBytesBE(extraIn.length, 4),
|
|
216
|
+
extraIn,
|
|
217
|
+
new Uint8Array([i])
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Generates a nonce pair (public and secret) for MuSig2 signing.
|
|
222
|
+
* @param publicKey The individual public key of the signer (Uint8Array).
|
|
223
|
+
* @param secretKey The secret key of the signer (Uint8Array). Optional, included to xor randomness
|
|
224
|
+
* @param aggPublicKey The aggregate public key of all signers (Uint8Array).
|
|
225
|
+
* @param msg The message to be signed (Uint8Array).
|
|
226
|
+
* @param extraIn Extra input for nonce generation (Uint8Array).
|
|
227
|
+
* @param rand Random 32-bytes for generating the nonces (Uint8Array).
|
|
228
|
+
* @returns An object containing the public and secret nonces.
|
|
229
|
+
* @throws {Error} If the input is invalid, such as non array publicKey, secretKey, aggPublicKey.
|
|
230
|
+
*/
|
|
231
|
+
export function nonceGen(
|
|
232
|
+
publicKey: Uint8Array,
|
|
233
|
+
secretKey?: Uint8Array,
|
|
234
|
+
aggPublicKey: Uint8Array = new Uint8Array(0),
|
|
235
|
+
msg?: Uint8Array,
|
|
236
|
+
extraIn: Uint8Array = new Uint8Array(0),
|
|
237
|
+
rand: Uint8Array = randomBytes(32)
|
|
238
|
+
): Nonces {
|
|
239
|
+
abytes(publicKey, PUBKEY_LEN);
|
|
240
|
+
abytesOptional(secretKey, 32);
|
|
241
|
+
abytes(aggPublicKey, 0, 32);
|
|
242
|
+
abytesOptional(msg);
|
|
243
|
+
abytes(extraIn);
|
|
244
|
+
abytes(rand, 32);
|
|
245
|
+
|
|
246
|
+
if (secretKey !== undefined) rand = aux(secretKey, rand);
|
|
247
|
+
const msgPrefixed =
|
|
248
|
+
msg !== undefined
|
|
249
|
+
? concatBytes(new Uint8Array([1]), numberToBytesBE(msg.length, 8), msg)
|
|
250
|
+
: new Uint8Array([0]);
|
|
251
|
+
const k1 = nonceHash(rand, publicKey, aggPublicKey, 0, msgPrefixed, extraIn);
|
|
252
|
+
const k2 = nonceHash(rand, publicKey, aggPublicKey, 1, msgPrefixed, extraIn);
|
|
253
|
+
return {
|
|
254
|
+
secret: SecretNonce.encode({ k1, k2, publicKey }),
|
|
255
|
+
public: PubNonce.encode({ R1: mulBase(k1), R2: mulBase(k2) }),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Aggregates public nonces from multiple signers into a single aggregate nonce.
|
|
261
|
+
* @param pubNonces An array of public nonces from each signer (Uint8Array). Each pubnonce is assumed to be 66 bytes (two 33‐byte parts).
|
|
262
|
+
* @returns The aggregate nonce (Uint8Array).
|
|
263
|
+
* @throws {Error} If the input is not an array or if any element is not a Uint8Array of the correct length.
|
|
264
|
+
* @throws {InvalidContributionErr} If any of the public nonces are invalid and cannot be processed.
|
|
265
|
+
*/
|
|
266
|
+
export function nonceAggregate(pubNonces: Uint8Array[]): Uint8Array {
|
|
267
|
+
abytesArray(pubNonces, 66);
|
|
268
|
+
let R1 = Point.ZERO;
|
|
269
|
+
let R2 = Point.ZERO;
|
|
270
|
+
for (let i = 0; i < pubNonces.length; i++) {
|
|
271
|
+
const pn = pubNonces[i];
|
|
272
|
+
try {
|
|
273
|
+
const { R1: R1n, R2: R2n } = PubNonce.decode(pn);
|
|
274
|
+
if (isZero(R1n) || isZero(R2n)) throw new Error('infinity point');
|
|
275
|
+
R1 = R1.add(R1n);
|
|
276
|
+
R2 = R2.add(R2n);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
throw new InvalidContributionErr(i, 'pubnonce');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return PubNonce.encode({ R1, R2 });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Class allows us re-use pre-computed stuff
|
|
285
|
+
// NOTE: it would be nice to aggregate nonce in construdctor, but there is test that passes already aggregated nonce here.
|
|
286
|
+
export class Session {
|
|
287
|
+
private publicKeys: Uint8Array[];
|
|
288
|
+
private Q: Point;
|
|
289
|
+
private gAcc: bigint;
|
|
290
|
+
private tweakAcc: bigint;
|
|
291
|
+
private b: bigint;
|
|
292
|
+
private R: Point;
|
|
293
|
+
private e: bigint;
|
|
294
|
+
private tweaks: Uint8Array[];
|
|
295
|
+
private isXonly: boolean[];
|
|
296
|
+
private L: Uint8Array;
|
|
297
|
+
private secondKey: Uint8Array;
|
|
298
|
+
/**
|
|
299
|
+
* Constructor for the Session class.
|
|
300
|
+
* It precomputes and stores values derived from the aggregate nonce, public keys,
|
|
301
|
+
* message, and optional tweaks, optimizing the signing process.
|
|
302
|
+
* @param aggNonce The aggregate nonce (Uint8Array) from all participants combined, must be 66 bytes.
|
|
303
|
+
* @param publicKeys An array of public keys (Uint8Array) from each participant, must be 33 bytes.
|
|
304
|
+
* @param msg The message (Uint8Array) to be signed.
|
|
305
|
+
* @param tweaks Optional array of tweaks (Uint8Array) to be applied to the aggregate public key, each must be 32 bytes. Defaults to [].
|
|
306
|
+
* @param isXonly Optional array of booleans indicating whether each tweak is an X-only tweak. Defaults to [].
|
|
307
|
+
* @throws {Error} If the input is invalid, such as wrong array sizes or lengths.
|
|
308
|
+
*/
|
|
309
|
+
constructor(
|
|
310
|
+
aggNonce: Uint8Array,
|
|
311
|
+
publicKeys: Uint8Array[],
|
|
312
|
+
msg: Uint8Array,
|
|
313
|
+
tweaks: Uint8Array[] = [],
|
|
314
|
+
isXonly: boolean[] = []
|
|
315
|
+
) {
|
|
316
|
+
abytesArray(publicKeys, 33);
|
|
317
|
+
abytesArray(tweaks, 32);
|
|
318
|
+
aXonly(isXonly);
|
|
319
|
+
abytes(msg);
|
|
320
|
+
if (tweaks.length !== isXonly.length)
|
|
321
|
+
throw new Error('The tweaks and isXonly arrays must have the same length');
|
|
322
|
+
const { aggPublicKey, gAcc, tweakAcc } = keyAggregate(publicKeys, tweaks, isXonly);
|
|
323
|
+
const { R1, R2 } = PubNonce.decode(aggNonce);
|
|
324
|
+
this.publicKeys = publicKeys;
|
|
325
|
+
this.Q = aggPublicKey;
|
|
326
|
+
this.gAcc = gAcc;
|
|
327
|
+
this.tweakAcc = tweakAcc;
|
|
328
|
+
this.b = taggedInt('MuSig/noncecoef', aggNonce, pointToBytes(aggPublicKey), msg);
|
|
329
|
+
const R = R1.add(R2.multiply(this.b));
|
|
330
|
+
this.R = isZero(R) ? Point.BASE : R;
|
|
331
|
+
this.e = taggedInt('BIP0340/challenge', pointToBytes(this.R), pointToBytes(aggPublicKey), msg);
|
|
332
|
+
this.tweaks = tweaks;
|
|
333
|
+
this.isXonly = isXonly;
|
|
334
|
+
this.L = keyAggL(publicKeys);
|
|
335
|
+
this.secondKey = getSecondKey(publicKeys);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Calculates the key aggregation coefficient for a given point.
|
|
339
|
+
* @private
|
|
340
|
+
* @param P The point to calculate the coefficient for.
|
|
341
|
+
* @returns The key aggregation coefficient as a bigint.
|
|
342
|
+
* @throws {Error} If the provided public key is not included in the list of pubkeys.
|
|
343
|
+
*/
|
|
344
|
+
private getSessionKeyAggCoeff(P: Point): bigint {
|
|
345
|
+
const { publicKeys } = this;
|
|
346
|
+
const pk = P.toRawBytes(true);
|
|
347
|
+
const found = publicKeys.some((p) => equalBytes(p, pk));
|
|
348
|
+
if (!found) throw new Error("The signer's pubkey must be included in the list of pubkeys");
|
|
349
|
+
return keyAggCoeffInternal(pk, this.secondKey, this.L);
|
|
350
|
+
}
|
|
351
|
+
private partialSigVerifyInternal(
|
|
352
|
+
partialSig: Uint8Array,
|
|
353
|
+
publicNonce: Uint8Array,
|
|
354
|
+
publicKey: Uint8Array
|
|
355
|
+
): boolean {
|
|
356
|
+
const { Q, gAcc, b, R, e } = this;
|
|
357
|
+
const s = bytesToNumberBE(partialSig);
|
|
358
|
+
if (s >= SECP_N) return false;
|
|
359
|
+
const { R1, R2 } = PubNonce.decode(publicNonce);
|
|
360
|
+
const Re_s_ = R1.add(R2.multiply(b));
|
|
361
|
+
const Re_s = R.hasEvenY() ? Re_s_ : Re_s_.negate();
|
|
362
|
+
const P = Point.fromHex(publicKey);
|
|
363
|
+
const a = this.getSessionKeyAggCoeff(P);
|
|
364
|
+
const g = modN(evenScalar(Q, 1n) * gAcc);
|
|
365
|
+
const left = mulBase(s);
|
|
366
|
+
const right = Re_s.add(P.multiply(modN(e * a * g)));
|
|
367
|
+
return left.equals(right);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Generates a partial signature for a given message, secret nonce, secret key, and session context.
|
|
372
|
+
* @param secretNonce The secret nonce for this signing session (Uint8Array). MUST be securely erased after use.
|
|
373
|
+
* @param secret The secret key of the signer (Uint8Array).
|
|
374
|
+
* @param sessionCtx The session context containing all necessary information for signing.
|
|
375
|
+
* @param fastSign if set to true, the signature is created without checking validity.
|
|
376
|
+
* @returns The partial signature (Uint8Array).
|
|
377
|
+
* @throws {Error} If the input is invalid, such as wrong array sizes, invalid nonce or secret key.
|
|
378
|
+
*/
|
|
379
|
+
sign(secretNonce: Uint8Array, secret: Uint8Array, fastSign = false): Uint8Array {
|
|
380
|
+
abytes(secret, 32);
|
|
381
|
+
if (typeof fastSign !== 'boolean') throw new Error('expected boolean');
|
|
382
|
+
const { Q, gAcc, b, R, e } = this;
|
|
383
|
+
const { k1: k1_, k2: k2_, publicKey: originalPk } = SecretNonce.decode(secretNonce);
|
|
384
|
+
// zero-out the first 64 bytes of secretNonce so it cannot be reused
|
|
385
|
+
// TODO: this was in reference implementation, but feels very broken. Modifying input arguments is pretty bad.
|
|
386
|
+
secretNonce.fill(0, 0, 64);
|
|
387
|
+
aInRange('k1', k1_, 0n, SECP_N);
|
|
388
|
+
aInRange('k2', k2_, 0n, SECP_N);
|
|
389
|
+
const k1 = evenScalar(R, k1_);
|
|
390
|
+
const k2 = evenScalar(R, k2_);
|
|
391
|
+
const d_ = bytesToNumberBE(secret);
|
|
392
|
+
aInRange('d_', d_, 1n, SECP_N);
|
|
393
|
+
const P = mulBase(d_);
|
|
394
|
+
const pk = P.toRawBytes(true);
|
|
395
|
+
if (!equalBytes(pk, originalPk)) throw new Error('Public key does not match nonceGen argument');
|
|
396
|
+
const a = this.getSessionKeyAggCoeff(P);
|
|
397
|
+
const g = evenScalar(Q, 1n);
|
|
398
|
+
const d = modN(g * gAcc * d_);
|
|
399
|
+
const s = modN(k1 + b * k2 + e * a * d);
|
|
400
|
+
const partialSig = numberToBytesBE(s, 32);
|
|
401
|
+
// Skip validation in fast-sign mode
|
|
402
|
+
if (!fastSign) {
|
|
403
|
+
const publicNonce = PubNonce.encode({
|
|
404
|
+
R1: mulBase(k1_),
|
|
405
|
+
R2: mulBase(k2_),
|
|
406
|
+
});
|
|
407
|
+
if (!this.partialSigVerifyInternal(partialSig, publicNonce, pk))
|
|
408
|
+
throw new Error('Partial signature verification failed');
|
|
409
|
+
}
|
|
410
|
+
return partialSig;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Verifies a partial signature against the aggregate public key and other session parameters.
|
|
414
|
+
* @param partialSig The partial signature to verify (Uint8Array).
|
|
415
|
+
* @param pubNonces An array of public nonces from each signer (Uint8Array).
|
|
416
|
+
* @param pubKeys An array of public keys from each signer (Uint8Array).
|
|
417
|
+
* @param tweaks An array of tweaks applied to the aggregate public key.
|
|
418
|
+
* @param isXonly An array of booleans indicating whether each tweak is an X-only tweak.
|
|
419
|
+
* @param msg The message that was signed (Uint8Array).
|
|
420
|
+
* @param i The index of the signer whose partial signature is being verified.
|
|
421
|
+
* @returns True if the partial signature is valid, false otherwise.
|
|
422
|
+
* @throws {Error} If the input is invalid, such as non array partialSig, pubNonces, pubKeys, tweaks.
|
|
423
|
+
*/
|
|
424
|
+
partialSigVerify(partialSig: Uint8Array, pubNonces: Uint8Array[], i: number): boolean {
|
|
425
|
+
const { publicKeys, tweaks, isXonly } = this;
|
|
426
|
+
abytes(partialSig, 32);
|
|
427
|
+
abytesArray(pubNonces, 66);
|
|
428
|
+
abytesArray(publicKeys, PUBKEY_LEN);
|
|
429
|
+
abytesArray(tweaks, 32);
|
|
430
|
+
aXonly(isXonly);
|
|
431
|
+
anumber(i);
|
|
432
|
+
if (pubNonces.length !== publicKeys.length)
|
|
433
|
+
throw new Error('The pubNonces and publicKeys arrays must have the same length');
|
|
434
|
+
if (tweaks.length !== isXonly.length)
|
|
435
|
+
throw new Error('The tweaks and isXonly arrays must have the same length');
|
|
436
|
+
if (i >= pubNonces.length) throw new Error('index outside of pubKeys/pubNonces');
|
|
437
|
+
return this.partialSigVerifyInternal(partialSig, pubNonces[i], publicKeys[i]);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Aggregates partial signatures from multiple signers into a single final signature.
|
|
441
|
+
* @param partialSigs An array of partial signatures from each signer (Uint8Array).
|
|
442
|
+
* @param sessionCtx The session context containing all necessary information for signing.
|
|
443
|
+
* @returns The final aggregate signature (Uint8Array).
|
|
444
|
+
* @throws {Error} If the input is invalid, such as wrong array sizes, invalid signature.
|
|
445
|
+
*/
|
|
446
|
+
partialSigAgg(partialSigs: Uint8Array[]): Uint8Array {
|
|
447
|
+
abytesArray(partialSigs, 32);
|
|
448
|
+
const { Q, tweakAcc, R, e } = this;
|
|
449
|
+
let s = 0n;
|
|
450
|
+
for (let i = 0; i < partialSigs.length; i++) {
|
|
451
|
+
const si = bytesToNumberBE(partialSigs[i]);
|
|
452
|
+
if (si >= SECP_N) throw new InvalidContributionErr(i, 'psig');
|
|
453
|
+
s = modN(s + si);
|
|
454
|
+
}
|
|
455
|
+
const g = evenScalar(Q, 1n);
|
|
456
|
+
s = modN(s + e * g * tweakAcc);
|
|
457
|
+
return concatBytes(pointToBytes(R), numberToBytesBE(s, 32));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const deterministicNonceHash = (
|
|
462
|
+
secret: Uint8Array,
|
|
463
|
+
aggOtherNonce: Uint8Array,
|
|
464
|
+
aggPublicKey: Uint8Array,
|
|
465
|
+
msg: Uint8Array,
|
|
466
|
+
i: number
|
|
467
|
+
): bigint =>
|
|
468
|
+
taggedInt(
|
|
469
|
+
'MuSig/deterministic/nonce',
|
|
470
|
+
secret,
|
|
471
|
+
aggOtherNonce,
|
|
472
|
+
aggPublicKey,
|
|
473
|
+
numberToBytesBE(msg.length, 8),
|
|
474
|
+
msg,
|
|
475
|
+
new Uint8Array([i])
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Generates a nonce pair and partial signature deterministically for a single signer.
|
|
480
|
+
* @param secret The secret key of the signer (Uint8Array).
|
|
481
|
+
* @param aggOtherNonce The aggregate public nonce of all other signers (Uint8Array).
|
|
482
|
+
* @param publicKeys An array of all signers' public keys (Uint8Array).
|
|
483
|
+
* @param tweaks An array of tweaks to apply to the aggregate public key.
|
|
484
|
+
* @param isXonly An array of booleans indicating whether each tweak is an X-only tweak.
|
|
485
|
+
* @param msg The message to be signed (Uint8Array).
|
|
486
|
+
* @param rand Optional extra randomness (Uint8Array).
|
|
487
|
+
* @returns An object containing the public nonce and partial signature.
|
|
488
|
+
*/
|
|
489
|
+
export function deterministicSign(
|
|
490
|
+
secret: Uint8Array,
|
|
491
|
+
aggOtherNonce: Uint8Array,
|
|
492
|
+
publicKeys: Uint8Array[],
|
|
493
|
+
msg: Uint8Array,
|
|
494
|
+
tweaks: Uint8Array[] = [],
|
|
495
|
+
isXonly: boolean[] = [],
|
|
496
|
+
rand?: Uint8Array,
|
|
497
|
+
fastSign = false
|
|
498
|
+
): DetNonce {
|
|
499
|
+
abytes(secret, 32);
|
|
500
|
+
abytes(aggOtherNonce, 66);
|
|
501
|
+
abytesArray(publicKeys, PUBKEY_LEN);
|
|
502
|
+
abytesArray(tweaks, 32);
|
|
503
|
+
abytes(msg);
|
|
504
|
+
abytesOptional(rand);
|
|
505
|
+
const sk = rand !== undefined ? aux(secret, rand) : secret;
|
|
506
|
+
const aggPublicKey = keyAggExport(keyAggregate(publicKeys, tweaks, isXonly));
|
|
507
|
+
const k1 = deterministicNonceHash(sk, aggOtherNonce, aggPublicKey, msg, 0);
|
|
508
|
+
const k2 = deterministicNonceHash(sk, aggOtherNonce, aggPublicKey, msg, 1);
|
|
509
|
+
const R1 = mulBase(k1);
|
|
510
|
+
const R2 = mulBase(k2);
|
|
511
|
+
const publicNonce = PubNonce.encode({ R1, R2 });
|
|
512
|
+
const secretNonce = SecretNonce.encode({ k1, k2, publicKey: IndividualPubkey(secret) });
|
|
513
|
+
const aggNonce = nonceAggregate([publicNonce, aggOtherNonce]);
|
|
514
|
+
const session = new Session(aggNonce, publicKeys, msg, tweaks, isXonly);
|
|
515
|
+
const partialSig = session.sign(secretNonce, secret, fastSign);
|
|
516
|
+
return { publicNonce, partialSig };
|
|
517
|
+
}
|
package/src/psbt.ts
CHANGED
|
@@ -458,7 +458,8 @@ export function mergeKeyMap<T extends PSBTKeyMap>(
|
|
|
458
458
|
psbtEnum: T,
|
|
459
459
|
val: PSBTKeyMapKeys<T>,
|
|
460
460
|
cur?: PSBTKeyMapKeys<T>,
|
|
461
|
-
allowedFields?: (keyof PSBTKeyMapKeys<T>)[]
|
|
461
|
+
allowedFields?: (keyof PSBTKeyMapKeys<T>)[],
|
|
462
|
+
allowUnknown?: boolean
|
|
462
463
|
): PSBTKeyMapKeys<T> {
|
|
463
464
|
const res: PSBTKeyMapKeys<T> = { ...cur, ...val };
|
|
464
465
|
// All arguments can be provided as hex
|
|
@@ -517,8 +518,13 @@ export function mergeKeyMap<T extends PSBTKeyMap>(
|
|
|
517
518
|
throw new Error(`Cannot change signed field=${k}`);
|
|
518
519
|
}
|
|
519
520
|
}
|
|
520
|
-
// Remove unknown keys
|
|
521
|
-
for (const k in res)
|
|
521
|
+
// Remove unknown keys except the "unknown" array if allowUnknown is true
|
|
522
|
+
for (const k in res) {
|
|
523
|
+
if (!psbtEnum[k]) {
|
|
524
|
+
if (allowUnknown && k === 'unknown') continue;
|
|
525
|
+
delete res[k];
|
|
526
|
+
}
|
|
527
|
+
}
|
|
522
528
|
return res;
|
|
523
529
|
}
|
|
524
530
|
|
package/src/transaction.ts
CHANGED
|
@@ -61,7 +61,7 @@ export function cloneDeep<T>(obj: T): T {
|
|
|
61
61
|
|
|
62
62
|
// Mostly security features, hardened defaults;
|
|
63
63
|
// but you still can parse other people tx with unspendable outputs and stuff if you want
|
|
64
|
-
export
|
|
64
|
+
export interface TxOpts {
|
|
65
65
|
version?: number;
|
|
66
66
|
lockTime?: number;
|
|
67
67
|
PSBTVersion?: number;
|
|
@@ -84,7 +84,9 @@ export type TxOpts = {
|
|
|
84
84
|
allowLegacyWitnessUtxo?: boolean;
|
|
85
85
|
lowR?: boolean; // Use lowR signatures
|
|
86
86
|
customScripts?: CustomScript[]; // UNSAFE: Custom payment scripts
|
|
87
|
-
|
|
87
|
+
// Allow to add additional unknown keys/values to the "unknown" array member
|
|
88
|
+
allowUnknown?: boolean;
|
|
89
|
+
}
|
|
88
90
|
|
|
89
91
|
/**
|
|
90
92
|
* Internal, exported only for backwards-compat. Use `SigHash` instead.
|
|
@@ -520,7 +522,8 @@ export class Transaction {
|
|
|
520
522
|
input,
|
|
521
523
|
this.inputs[idx],
|
|
522
524
|
allowedFields,
|
|
523
|
-
this.opts.disableScriptCheck
|
|
525
|
+
this.opts.disableScriptCheck,
|
|
526
|
+
this.opts.allowUnknown
|
|
524
527
|
);
|
|
525
528
|
}
|
|
526
529
|
// Output stuff
|
|
@@ -556,7 +559,7 @@ export class Transaction {
|
|
|
556
559
|
if (script === undefined) script = cur?.script;
|
|
557
560
|
let res: psbt.PSBTKeyMapKeys<typeof psbt.PSBTOutput> = { ...cur, ...o, amount, script };
|
|
558
561
|
if (res.amount === undefined) delete res.amount;
|
|
559
|
-
res = psbt.mergeKeyMap(psbt.PSBTOutput, res, cur, allowedFields);
|
|
562
|
+
res = psbt.mergeKeyMap(psbt.PSBTOutput, res, cur, allowedFields, this.opts.allowUnknown);
|
|
560
563
|
psbt.PSBTOutputCoder.encode(res);
|
|
561
564
|
if (
|
|
562
565
|
res.script &&
|
|
@@ -1070,7 +1073,13 @@ export class Transaction {
|
|
|
1070
1073
|
: P.EMPTY;
|
|
1071
1074
|
if (!equalBytes(thisUnsigned, otherUnsigned))
|
|
1072
1075
|
throw new Error(`Transaction/combine: different unsigned tx`);
|
|
1073
|
-
this.global = psbt.mergeKeyMap(
|
|
1076
|
+
this.global = psbt.mergeKeyMap(
|
|
1077
|
+
psbt.PSBTGlobal,
|
|
1078
|
+
this.global,
|
|
1079
|
+
other.global,
|
|
1080
|
+
undefined,
|
|
1081
|
+
this.opts.allowUnknown
|
|
1082
|
+
);
|
|
1074
1083
|
for (let i = 0; i < this.inputs.length; i++) this.updateInput(i, other.inputs[i], true);
|
|
1075
1084
|
for (let i = 0; i < this.outputs.length; i++) this.updateOutput(i, other.outputs[i], true);
|
|
1076
1085
|
return this;
|
package/src/utxo.ts
CHANGED
|
@@ -37,7 +37,8 @@ export function normalizeInput(
|
|
|
37
37
|
i: psbt.TransactionInputUpdate,
|
|
38
38
|
cur?: psbt.TransactionInput,
|
|
39
39
|
allowedFields?: (keyof psbt.TransactionInput)[],
|
|
40
|
-
disableScriptCheck = false
|
|
40
|
+
disableScriptCheck = false,
|
|
41
|
+
allowUnknown = false
|
|
41
42
|
): psbt.TransactionInput {
|
|
42
43
|
let { nonWitnessUtxo, txid } = i;
|
|
43
44
|
// String support for common fields. We usually prefer Uint8Array to avoid errors
|
|
@@ -55,7 +56,7 @@ export function normalizeInput(
|
|
|
55
56
|
if (!('nonWitnessUtxo' in i) && res.nonWitnessUtxo === undefined) delete res.nonWitnessUtxo;
|
|
56
57
|
if (res.sequence === undefined) res.sequence = DEFAULT_SEQUENCE;
|
|
57
58
|
if (res.tapMerkleRoot === null) delete res.tapMerkleRoot;
|
|
58
|
-
res = psbt.mergeKeyMap(psbt.PSBTInput, res, cur, allowedFields);
|
|
59
|
+
res = psbt.mergeKeyMap(psbt.PSBTInput, res, cur, allowedFields, allowUnknown);
|
|
59
60
|
psbt.PSBTInputCoder.encode(res); // Validates that everything is correct at this point
|
|
60
61
|
|
|
61
62
|
let prevOut;
|
|
@@ -341,11 +342,11 @@ export class _Estimator {
|
|
|
341
342
|
// - change address: can be smaller for segwit
|
|
342
343
|
// - accumExact: ???
|
|
343
344
|
private dust: bigint; // total dust limit (3||opts.dustRelayFeeRate * 182||opts.dust). Default: 546
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
345
|
+
private outputs: Output[];
|
|
346
|
+
private opts: EstimatorOpts;
|
|
347
|
+
constructor(inputs: psbt.TransactionInputUpdate[], outputs: Output[], opts: EstimatorOpts) {
|
|
348
|
+
this.outputs = outputs;
|
|
349
|
+
this.opts = opts;
|
|
349
350
|
if (typeof opts.feePerByte !== 'bigint')
|
|
350
351
|
throw new Error(
|
|
351
352
|
`Estimator: wrong feePerByte=${
|
|
@@ -410,7 +411,13 @@ export class _Estimator {
|
|
|
410
411
|
}
|
|
411
412
|
const inputKeys = new Set();
|
|
412
413
|
this.normalizedInputs = allInputs.map((i) => {
|
|
413
|
-
const normalized = normalizeInput(
|
|
414
|
+
const normalized = normalizeInput(
|
|
415
|
+
i,
|
|
416
|
+
undefined,
|
|
417
|
+
undefined,
|
|
418
|
+
opts.disableScriptCheck,
|
|
419
|
+
opts.allowUnknown
|
|
420
|
+
);
|
|
414
421
|
inputBeforeSign(normalized); // check fields
|
|
415
422
|
const key = `${hex.encode(normalized.txid!)}:${normalized.index}`;
|
|
416
423
|
if (!opts.allowSameUtxo && inputKeys.has(key))
|
|
@@ -487,10 +494,11 @@ export class _Estimator {
|
|
|
487
494
|
let num = 0;
|
|
488
495
|
let inputsAmount = 0n;
|
|
489
496
|
const targetAmount = this.amount;
|
|
490
|
-
const res =
|
|
497
|
+
const res: Set<number> = new Set();
|
|
491
498
|
let fee;
|
|
492
499
|
for (const idx of this.requiredIndices) {
|
|
493
500
|
this.checkInputIdx(idx);
|
|
501
|
+
if (res.has(idx)) throw new Error('required input encountered multiple times'); // should not happen
|
|
494
502
|
const { estimate, amount } = this.normalizedInputs[idx];
|
|
495
503
|
let newWeight = weight + estimate.weight;
|
|
496
504
|
if (!hasWitnesses && estimate.hasWitnesses) newWeight += 2; // enable witness if needed
|
|
@@ -500,13 +508,14 @@ export class _Estimator {
|
|
|
500
508
|
if (estimate.hasWitnesses) hasWitnesses = true;
|
|
501
509
|
num++;
|
|
502
510
|
inputsAmount += amount;
|
|
503
|
-
res.
|
|
511
|
+
res.add(idx);
|
|
504
512
|
// inputsAmount is enough to cover cost of tx
|
|
505
513
|
if (!all && targetAmount + fee <= inputsAmount)
|
|
506
|
-
return { indices: res, fee, weight: totalWeight, total: inputsAmount };
|
|
514
|
+
return { indices: Array.from(res), fee, weight: totalWeight, total: inputsAmount };
|
|
507
515
|
}
|
|
508
516
|
for (const idx of indices) {
|
|
509
517
|
this.checkInputIdx(idx);
|
|
518
|
+
if (res.has(idx)) continue; // skip required inputs
|
|
510
519
|
const { estimate, amount, value } = this.normalizedInputs[idx];
|
|
511
520
|
let newWeight = weight + estimate.weight;
|
|
512
521
|
if (!hasWitnesses && estimate.hasWitnesses) newWeight += 2; // enable witness if needed
|
|
@@ -522,14 +531,14 @@ export class _Estimator {
|
|
|
522
531
|
if (estimate.hasWitnesses) hasWitnesses = true;
|
|
523
532
|
num++;
|
|
524
533
|
inputsAmount += amount;
|
|
525
|
-
res.
|
|
534
|
+
res.add(idx);
|
|
526
535
|
// inputsAmount is enough to cover cost of tx
|
|
527
536
|
if (!all && targetAmount + fee <= inputsAmount)
|
|
528
|
-
return { indices: res, fee, weight: totalWeight, total: inputsAmount };
|
|
537
|
+
return { indices: Array.from(res), fee, weight: totalWeight, total: inputsAmount };
|
|
529
538
|
}
|
|
530
539
|
if (all) {
|
|
531
540
|
const newWeight = weight + 4 * CompactSizeLen.encode(num).length;
|
|
532
|
-
return { indices: res, fee, weight: newWeight, total: inputsAmount };
|
|
541
|
+
return { indices: Array.from(res), fee, weight: newWeight, total: inputsAmount };
|
|
533
542
|
}
|
|
534
543
|
return undefined;
|
|
535
544
|
}
|