@ledgerhq/hw-app-btc 6.10.0 → 6.12.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 +384 -60
- package/lib/Btc.d.ts +9 -6
- package/lib/Btc.d.ts.map +1 -1
- package/lib/Btc.js +73 -8
- package/lib/Btc.js.map +1 -1
- package/lib/BtcNew.d.ts +79 -32
- package/lib/BtcNew.d.ts.map +1 -1
- package/lib/BtcNew.js +168 -207
- package/lib/BtcNew.js.map +1 -1
- package/lib/newops/accounttype.d.ts +110 -0
- package/lib/newops/accounttype.d.ts.map +1 -0
- package/lib/newops/accounttype.js +233 -0
- package/lib/newops/accounttype.js.map +1 -0
- package/lib/newops/appClient.d.ts +6 -2
- package/lib/newops/appClient.d.ts.map +1 -1
- package/lib/newops/appClient.js +8 -4
- package/lib/newops/appClient.js.map +1 -1
- package/lib/newops/clientCommands.d.ts +18 -2
- package/lib/newops/clientCommands.d.ts.map +1 -1
- package/lib/newops/clientCommands.js +34 -12
- package/lib/newops/clientCommands.js.map +1 -1
- package/lib/newops/merkelizedPsbt.d.ts +11 -0
- package/lib/newops/merkelizedPsbt.d.ts.map +1 -1
- package/lib/newops/merkelizedPsbt.js +11 -0
- package/lib/newops/merkelizedPsbt.js.map +1 -1
- package/lib/newops/merkle.d.ts +5 -0
- package/lib/newops/merkle.d.ts.map +1 -1
- package/lib/newops/merkle.js +5 -0
- package/lib/newops/merkle.js.map +1 -1
- package/lib/newops/merkleMap.d.ts +10 -0
- package/lib/newops/merkleMap.d.ts.map +1 -1
- package/lib/newops/merkleMap.js +10 -0
- package/lib/newops/merkleMap.js.map +1 -1
- package/lib/newops/policy.d.ts +8 -0
- package/lib/newops/policy.d.ts.map +1 -1
- package/lib/newops/policy.js +9 -1
- package/lib/newops/policy.js.map +1 -1
- package/lib/newops/psbtExtractor.d.ts +6 -0
- package/lib/newops/psbtExtractor.d.ts.map +1 -1
- package/lib/newops/psbtExtractor.js +6 -0
- package/lib/newops/psbtExtractor.js.map +1 -1
- package/lib/newops/psbtFinalizer.d.ts +11 -1
- package/lib/newops/psbtFinalizer.d.ts.map +1 -1
- package/lib/newops/psbtFinalizer.js +26 -1
- package/lib/newops/psbtFinalizer.js.map +1 -1
- package/lib/newops/psbtv2.d.ts +22 -2
- package/lib/newops/psbtv2.d.ts.map +1 -1
- package/lib/newops/psbtv2.js +33 -8
- package/lib/newops/psbtv2.js.map +1 -1
- package/lib-es/Btc.d.ts +9 -6
- package/lib-es/Btc.d.ts.map +1 -1
- package/lib-es/Btc.js +73 -8
- package/lib-es/Btc.js.map +1 -1
- package/lib-es/BtcNew.d.ts +79 -32
- package/lib-es/BtcNew.d.ts.map +1 -1
- package/lib-es/BtcNew.js +170 -209
- package/lib-es/BtcNew.js.map +1 -1
- package/lib-es/newops/accounttype.d.ts +110 -0
- package/lib-es/newops/accounttype.d.ts.map +1 -0
- package/lib-es/newops/accounttype.js +230 -0
- package/lib-es/newops/accounttype.js.map +1 -0
- package/lib-es/newops/appClient.d.ts +6 -2
- package/lib-es/newops/appClient.d.ts.map +1 -1
- package/lib-es/newops/appClient.js +8 -4
- package/lib-es/newops/appClient.js.map +1 -1
- package/lib-es/newops/clientCommands.d.ts +18 -2
- package/lib-es/newops/clientCommands.d.ts.map +1 -1
- package/lib-es/newops/clientCommands.js +34 -12
- package/lib-es/newops/clientCommands.js.map +1 -1
- package/lib-es/newops/merkelizedPsbt.d.ts +11 -0
- package/lib-es/newops/merkelizedPsbt.d.ts.map +1 -1
- package/lib-es/newops/merkelizedPsbt.js +11 -0
- package/lib-es/newops/merkelizedPsbt.js.map +1 -1
- package/lib-es/newops/merkle.d.ts +5 -0
- package/lib-es/newops/merkle.d.ts.map +1 -1
- package/lib-es/newops/merkle.js +5 -0
- package/lib-es/newops/merkle.js.map +1 -1
- package/lib-es/newops/merkleMap.d.ts +10 -0
- package/lib-es/newops/merkleMap.d.ts.map +1 -1
- package/lib-es/newops/merkleMap.js +10 -0
- package/lib-es/newops/merkleMap.js.map +1 -1
- package/lib-es/newops/policy.d.ts +8 -0
- package/lib-es/newops/policy.d.ts.map +1 -1
- package/lib-es/newops/policy.js +10 -2
- package/lib-es/newops/policy.js.map +1 -1
- package/lib-es/newops/psbtExtractor.d.ts +6 -0
- package/lib-es/newops/psbtExtractor.d.ts.map +1 -1
- package/lib-es/newops/psbtExtractor.js +6 -0
- package/lib-es/newops/psbtExtractor.js.map +1 -1
- package/lib-es/newops/psbtFinalizer.d.ts +11 -1
- package/lib-es/newops/psbtFinalizer.d.ts.map +1 -1
- package/lib-es/newops/psbtFinalizer.js +26 -1
- package/lib-es/newops/psbtFinalizer.js.map +1 -1
- package/lib-es/newops/psbtv2.d.ts +22 -2
- package/lib-es/newops/psbtv2.d.ts.map +1 -1
- package/lib-es/newops/psbtv2.js +33 -8
- package/lib-es/newops/psbtv2.js.map +1 -1
- package/package.json +3 -3
- package/src/Btc.ts +111 -9
- package/src/BtcNew.ts +204 -209
- package/src/newops/accounttype.ts +370 -0
- package/src/newops/appClient.ts +12 -4
- package/src/newops/clientCommands.ts +34 -12
- package/src/newops/merkelizedPsbt.ts +11 -0
- package/src/newops/merkle.ts +5 -0
- package/src/newops/merkleMap.ts +10 -0
- package/src/newops/policy.ts +10 -2
- package/src/newops/psbtExtractor.ts +6 -0
- package/src/newops/psbtFinalizer.ts +26 -1
- package/src/newops/psbtv2.ts +34 -14
- package/tests/Btc.test.ts +89 -0
- package/tests/newops/BtcNew.test.ts +48 -21
- package/tests/newops/integrationtools.ts +47 -36
- package/tests/newops/testtx.ts +0 -55
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { crypto } from "bitcoinjs-lib";
|
|
2
|
+
import { pointAddScalar } from "tiny-secp256k1";
|
|
3
|
+
import { BufferWriter } from "../buffertools";
|
|
4
|
+
import {
|
|
5
|
+
HASH_SIZE,
|
|
6
|
+
OP_CHECKSIG,
|
|
7
|
+
OP_DUP,
|
|
8
|
+
OP_EQUAL,
|
|
9
|
+
OP_EQUALVERIFY,
|
|
10
|
+
OP_HASH160,
|
|
11
|
+
} from "../constants";
|
|
12
|
+
import { hashPublicKey } from "../hashPublicKey";
|
|
13
|
+
import { DefaultDescriptorTemplate } from "./policy";
|
|
14
|
+
import { PsbtV2 } from "./psbtv2";
|
|
15
|
+
|
|
16
|
+
export type SpendingCondition = {
|
|
17
|
+
scriptPubKey: Buffer;
|
|
18
|
+
redeemScript?: Buffer;
|
|
19
|
+
// Possible future extension:
|
|
20
|
+
// witnessScript?: Buffer; // For p2wsh witnessScript
|
|
21
|
+
// tapScript?: {tapPath: Buffer[], script: Buffer} // For taproot
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type SpentOutput = { cond: SpendingCondition; amount: Buffer };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Encapsulates differences between account types, for example p2wpkh,
|
|
28
|
+
* p2wpkhWrapped, p2tr.
|
|
29
|
+
*/
|
|
30
|
+
export interface AccountType {
|
|
31
|
+
/**
|
|
32
|
+
* Generates a scriptPubKey (output script) from a list of public keys. If a
|
|
33
|
+
* p2sh redeemScript or a p2wsh witnessScript is needed it will also be set on
|
|
34
|
+
* the returned SpendingCondition.
|
|
35
|
+
*
|
|
36
|
+
* The pubkeys are expected to be 33 byte ecdsa compressed pubkeys.
|
|
37
|
+
*/
|
|
38
|
+
spendingCondition(pubkeys: Buffer[]): SpendingCondition;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Populates the psbt with account type-specific data for an input.
|
|
42
|
+
* @param i The index of the input map to populate
|
|
43
|
+
* @param inputTx The full transaction containing the spent output. This may
|
|
44
|
+
* be omitted for taproot.
|
|
45
|
+
* @param spentOutput The amount and spending condition of the spent output
|
|
46
|
+
* @param pubkeys The 33 byte ecdsa compressed public keys involved in the input
|
|
47
|
+
* @param pathElems The paths corresponding to the pubkeys, in same order.
|
|
48
|
+
*/
|
|
49
|
+
setInput(
|
|
50
|
+
i: number,
|
|
51
|
+
inputTx: Buffer | undefined,
|
|
52
|
+
spentOutput: SpentOutput,
|
|
53
|
+
pubkeys: Buffer[],
|
|
54
|
+
pathElems: number[][]
|
|
55
|
+
): void;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Populates the psbt with account type-specific data for an output. This is typically
|
|
59
|
+
* done for change outputs and other outputs that goes to the same account as
|
|
60
|
+
* being spent from.
|
|
61
|
+
* @param i The index of the output map to populate
|
|
62
|
+
* @param cond The spending condition for this output
|
|
63
|
+
* @param pubkeys The 33 byte ecdsa compressed public keys involved in this output
|
|
64
|
+
* @param paths The paths corresponding to the pubkeys, in same order.
|
|
65
|
+
*/
|
|
66
|
+
setOwnOutput(
|
|
67
|
+
i: number,
|
|
68
|
+
cond: SpendingCondition,
|
|
69
|
+
pubkeys: Buffer[],
|
|
70
|
+
paths: number[][]
|
|
71
|
+
): void;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Returns the descriptor template for this account type. Currently only
|
|
75
|
+
* DefaultDescriptorTemplates are allowed, but that might be changed in the
|
|
76
|
+
* future. See class WalletPolicy for more information on descriptor
|
|
77
|
+
* templates.
|
|
78
|
+
*/
|
|
79
|
+
getDescriptorTemplate(): DefaultDescriptorTemplate;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
83
|
+
interface BaseAccount extends AccountType {}
|
|
84
|
+
|
|
85
|
+
abstract class BaseAccount implements AccountType {
|
|
86
|
+
constructor(protected psbt: PsbtV2, protected masterFp: Buffer) {}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Superclass for single signature accounts. This will make sure that the pubkey
|
|
91
|
+
* arrays and path arrays in the method arguments contains exactly one element
|
|
92
|
+
* and calls an abstract method to do the actual work.
|
|
93
|
+
*/
|
|
94
|
+
abstract class SingleKeyAccount extends BaseAccount {
|
|
95
|
+
spendingCondition(pubkeys: Buffer[]): SpendingCondition {
|
|
96
|
+
if (pubkeys.length != 1) {
|
|
97
|
+
throw new Error("Expected single key, got " + pubkeys.length);
|
|
98
|
+
}
|
|
99
|
+
return this.singleKeyCondition(pubkeys[0]);
|
|
100
|
+
}
|
|
101
|
+
protected abstract singleKeyCondition(pubkey: Buffer): SpendingCondition;
|
|
102
|
+
|
|
103
|
+
setInput(
|
|
104
|
+
i: number,
|
|
105
|
+
inputTx: Buffer | undefined,
|
|
106
|
+
spentOutput: SpentOutput,
|
|
107
|
+
pubkeys: Buffer[],
|
|
108
|
+
pathElems: number[][]
|
|
109
|
+
) {
|
|
110
|
+
if (pubkeys.length != 1) {
|
|
111
|
+
throw new Error("Expected single key, got " + pubkeys.length);
|
|
112
|
+
}
|
|
113
|
+
if (pathElems.length != 1) {
|
|
114
|
+
throw new Error("Expected single path, got " + pathElems.length);
|
|
115
|
+
}
|
|
116
|
+
this.setSingleKeyInput(i, inputTx, spentOutput, pubkeys[0], pathElems[0]);
|
|
117
|
+
}
|
|
118
|
+
protected abstract setSingleKeyInput(
|
|
119
|
+
i: number,
|
|
120
|
+
inputTx: Buffer | undefined,
|
|
121
|
+
spentOutput: SpentOutput,
|
|
122
|
+
pubkey: Buffer,
|
|
123
|
+
path: number[]
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
setOwnOutput(
|
|
127
|
+
i: number,
|
|
128
|
+
cond: SpendingCondition,
|
|
129
|
+
pubkeys: Buffer[],
|
|
130
|
+
paths: number[][]
|
|
131
|
+
) {
|
|
132
|
+
if (pubkeys.length != 1) {
|
|
133
|
+
throw new Error("Expected single key, got " + pubkeys.length);
|
|
134
|
+
}
|
|
135
|
+
if (paths.length != 1) {
|
|
136
|
+
throw new Error("Expected single path, got " + paths.length);
|
|
137
|
+
}
|
|
138
|
+
this.setSingleKeyOutput(i, cond, pubkeys[0], paths[0]);
|
|
139
|
+
}
|
|
140
|
+
protected abstract setSingleKeyOutput(
|
|
141
|
+
i: number,
|
|
142
|
+
cond: SpendingCondition,
|
|
143
|
+
pubkey: Buffer,
|
|
144
|
+
path: number[]
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export class p2pkh extends SingleKeyAccount {
|
|
149
|
+
singleKeyCondition(pubkey: Buffer): SpendingCondition {
|
|
150
|
+
const buf = new BufferWriter();
|
|
151
|
+
const pubkeyHash = hashPublicKey(pubkey);
|
|
152
|
+
buf.writeSlice(Buffer.of(OP_DUP, OP_HASH160, HASH_SIZE));
|
|
153
|
+
buf.writeSlice(pubkeyHash);
|
|
154
|
+
buf.writeSlice(Buffer.of(OP_EQUALVERIFY, OP_CHECKSIG));
|
|
155
|
+
return { scriptPubKey: buf.buffer() };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
setSingleKeyInput(
|
|
159
|
+
i: number,
|
|
160
|
+
inputTx: Buffer | undefined,
|
|
161
|
+
_spentOutput: SpentOutput,
|
|
162
|
+
pubkey: Buffer,
|
|
163
|
+
path: number[]
|
|
164
|
+
) {
|
|
165
|
+
if (!inputTx) {
|
|
166
|
+
throw new Error("Full input base transaction required");
|
|
167
|
+
}
|
|
168
|
+
this.psbt.setInputNonWitnessUtxo(i, inputTx);
|
|
169
|
+
this.psbt.setInputBip32Derivation(i, pubkey, this.masterFp, path);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
setSingleKeyOutput(
|
|
173
|
+
i: number,
|
|
174
|
+
cond: SpendingCondition,
|
|
175
|
+
pubkey: Buffer,
|
|
176
|
+
path: number[]
|
|
177
|
+
) {
|
|
178
|
+
this.psbt.setOutputBip32Derivation(i, pubkey, this.masterFp, path);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
getDescriptorTemplate(): DefaultDescriptorTemplate {
|
|
182
|
+
return "pkh(@0)";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export class p2tr extends SingleKeyAccount {
|
|
187
|
+
singleKeyCondition(pubkey: Buffer): SpendingCondition {
|
|
188
|
+
const xonlyPubkey = pubkey.slice(1); // x-only pubkey
|
|
189
|
+
const buf = new BufferWriter();
|
|
190
|
+
const outputKey = this.getTaprootOutputKey(xonlyPubkey);
|
|
191
|
+
buf.writeSlice(Buffer.of(0x51, 32)); // push1, pubkeylen
|
|
192
|
+
buf.writeSlice(outputKey);
|
|
193
|
+
return { scriptPubKey: buf.buffer() };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
setSingleKeyInput(
|
|
197
|
+
i: number,
|
|
198
|
+
_inputTx: Buffer | undefined,
|
|
199
|
+
spentOutput: SpentOutput,
|
|
200
|
+
pubkey: Buffer,
|
|
201
|
+
path: number[]
|
|
202
|
+
) {
|
|
203
|
+
const xonly = pubkey.slice(1);
|
|
204
|
+
this.psbt.setInputTapBip32Derivation(i, xonly, [], this.masterFp, path);
|
|
205
|
+
this.psbt.setInputWitnessUtxo(
|
|
206
|
+
i,
|
|
207
|
+
spentOutput.amount,
|
|
208
|
+
spentOutput.cond.scriptPubKey
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
setSingleKeyOutput(
|
|
213
|
+
i: number,
|
|
214
|
+
cond: SpendingCondition,
|
|
215
|
+
pubkey: Buffer,
|
|
216
|
+
path: number[]
|
|
217
|
+
) {
|
|
218
|
+
const xonly = pubkey.slice(1);
|
|
219
|
+
this.psbt.setOutputTapBip32Derivation(i, xonly, [], this.masterFp, path);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
getDescriptorTemplate(): DefaultDescriptorTemplate {
|
|
223
|
+
return "tr(@0)";
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/*
|
|
227
|
+
The following two functions are copied from wallet-btc and adapted.
|
|
228
|
+
They should be moved to a library to avoid code reuse.
|
|
229
|
+
*/
|
|
230
|
+
private hashTapTweak(x: Buffer): Buffer {
|
|
231
|
+
// hash_tag(x) = SHA256(SHA256(tag) || SHA256(tag) || x), see BIP340
|
|
232
|
+
// See https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#specification
|
|
233
|
+
const h = crypto.sha256(Buffer.from("TapTweak", "utf-8"));
|
|
234
|
+
return crypto.sha256(Buffer.concat([h, h, x]));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Calculates a taproot output key from an internal key. This output key will be
|
|
239
|
+
* used as witness program in a taproot output. The internal key is tweaked
|
|
240
|
+
* according to recommendation in BIP341:
|
|
241
|
+
* https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_ref-22-0
|
|
242
|
+
*
|
|
243
|
+
* @param internalPubkey A 32 byte x-only taproot internal key
|
|
244
|
+
* @returns The output key
|
|
245
|
+
*/
|
|
246
|
+
getTaprootOutputKey(internalPubkey: Buffer): Buffer {
|
|
247
|
+
if (internalPubkey.length != 32) {
|
|
248
|
+
throw new Error("Expected 32 byte pubkey. Got " + internalPubkey.length);
|
|
249
|
+
}
|
|
250
|
+
// A BIP32 derived key can be converted to a schnorr pubkey by dropping
|
|
251
|
+
// the first byte, which represent the oddness/evenness. In schnorr all
|
|
252
|
+
// pubkeys are even.
|
|
253
|
+
// https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#public-key-conversion
|
|
254
|
+
const evenEcdsaPubkey = Buffer.concat([Buffer.of(0x02), internalPubkey]);
|
|
255
|
+
const tweak = this.hashTapTweak(internalPubkey);
|
|
256
|
+
|
|
257
|
+
// Q = P + int(hash_TapTweak(bytes(P)))G
|
|
258
|
+
const outputEcdsaKey = Buffer.from(pointAddScalar(evenEcdsaPubkey, tweak));
|
|
259
|
+
// Convert to schnorr.
|
|
260
|
+
const outputSchnorrKey = outputEcdsaKey.slice(1);
|
|
261
|
+
// Create address
|
|
262
|
+
return outputSchnorrKey;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export class p2wpkhWrapped extends SingleKeyAccount {
|
|
267
|
+
singleKeyCondition(pubkey: Buffer): SpendingCondition {
|
|
268
|
+
const buf = new BufferWriter();
|
|
269
|
+
const redeemScript = this.createRedeemScript(pubkey);
|
|
270
|
+
const scriptHash = hashPublicKey(redeemScript);
|
|
271
|
+
buf.writeSlice(Buffer.of(OP_HASH160, HASH_SIZE));
|
|
272
|
+
buf.writeSlice(scriptHash);
|
|
273
|
+
buf.writeUInt8(OP_EQUAL);
|
|
274
|
+
return { scriptPubKey: buf.buffer(), redeemScript: redeemScript };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
setSingleKeyInput(
|
|
278
|
+
i: number,
|
|
279
|
+
inputTx: Buffer | undefined,
|
|
280
|
+
spentOutput: SpentOutput,
|
|
281
|
+
pubkey: Buffer,
|
|
282
|
+
path: number[]
|
|
283
|
+
) {
|
|
284
|
+
if (!inputTx) {
|
|
285
|
+
throw new Error("Full input base transaction required");
|
|
286
|
+
}
|
|
287
|
+
this.psbt.setInputNonWitnessUtxo(i, inputTx);
|
|
288
|
+
this.psbt.setInputBip32Derivation(i, pubkey, this.masterFp, path);
|
|
289
|
+
|
|
290
|
+
const userSuppliedRedeemScript = spentOutput.cond.redeemScript;
|
|
291
|
+
const expectedRedeemScript = this.createRedeemScript(pubkey);
|
|
292
|
+
if (
|
|
293
|
+
userSuppliedRedeemScript &&
|
|
294
|
+
!expectedRedeemScript.equals(userSuppliedRedeemScript)
|
|
295
|
+
) {
|
|
296
|
+
// At what point might a user set the redeemScript on its own?
|
|
297
|
+
throw new Error(`User-supplied redeemScript ${userSuppliedRedeemScript.toString(
|
|
298
|
+
"hex"
|
|
299
|
+
)} doesn't
|
|
300
|
+
match expected ${expectedRedeemScript.toString("hex")} for input ${i}`);
|
|
301
|
+
}
|
|
302
|
+
this.psbt.setInputRedeemScript(i, expectedRedeemScript);
|
|
303
|
+
this.psbt.setInputWitnessUtxo(
|
|
304
|
+
i,
|
|
305
|
+
spentOutput.amount,
|
|
306
|
+
spentOutput.cond.scriptPubKey
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
setSingleKeyOutput(
|
|
311
|
+
i: number,
|
|
312
|
+
cond: SpendingCondition,
|
|
313
|
+
pubkey: Buffer,
|
|
314
|
+
path: number[]
|
|
315
|
+
) {
|
|
316
|
+
this.psbt.setOutputRedeemScript(i, cond.redeemScript!);
|
|
317
|
+
this.psbt.setOutputBip32Derivation(i, pubkey, this.masterFp, path);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
getDescriptorTemplate(): DefaultDescriptorTemplate {
|
|
321
|
+
return "sh(wpkh(@0))";
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private createRedeemScript(pubkey: Buffer): Buffer {
|
|
325
|
+
const pubkeyHash = hashPublicKey(pubkey);
|
|
326
|
+
return Buffer.concat([Buffer.from("0014", "hex"), pubkeyHash]);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export class p2wpkh extends SingleKeyAccount {
|
|
331
|
+
singleKeyCondition(pubkey: Buffer): SpendingCondition {
|
|
332
|
+
const buf = new BufferWriter();
|
|
333
|
+
const pubkeyHash = hashPublicKey(pubkey);
|
|
334
|
+
buf.writeSlice(Buffer.of(0, HASH_SIZE));
|
|
335
|
+
buf.writeSlice(pubkeyHash);
|
|
336
|
+
return { scriptPubKey: buf.buffer() };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
setSingleKeyInput(
|
|
340
|
+
i: number,
|
|
341
|
+
inputTx: Buffer | undefined,
|
|
342
|
+
spentOutput: SpentOutput,
|
|
343
|
+
pubkey: Buffer,
|
|
344
|
+
path: number[]
|
|
345
|
+
) {
|
|
346
|
+
if (!inputTx) {
|
|
347
|
+
throw new Error("Full input base transaction required");
|
|
348
|
+
}
|
|
349
|
+
this.psbt.setInputNonWitnessUtxo(i, inputTx);
|
|
350
|
+
this.psbt.setInputBip32Derivation(i, pubkey, this.masterFp, path);
|
|
351
|
+
this.psbt.setInputWitnessUtxo(
|
|
352
|
+
i,
|
|
353
|
+
spentOutput.amount,
|
|
354
|
+
spentOutput.cond.scriptPubKey
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
setSingleKeyOutput(
|
|
359
|
+
i: number,
|
|
360
|
+
cond: SpendingCondition,
|
|
361
|
+
pubkey: Buffer,
|
|
362
|
+
path: number[]
|
|
363
|
+
) {
|
|
364
|
+
this.psbt.setOutputBip32Derivation(i, pubkey, this.masterFp, path);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
getDescriptorTemplate(): DefaultDescriptorTemplate {
|
|
368
|
+
return "wpkh(@0)";
|
|
369
|
+
}
|
|
370
|
+
}
|
package/src/newops/appClient.ts
CHANGED
|
@@ -23,6 +23,10 @@ enum FrameworkIns {
|
|
|
23
23
|
CONTINUE_INTERRUPTED = 0x01,
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* This class encapsulates the APDU protocol documented at
|
|
28
|
+
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md
|
|
29
|
+
*/
|
|
26
30
|
export class AppClient {
|
|
27
31
|
transport: Transport;
|
|
28
32
|
|
|
@@ -59,7 +63,10 @@ export class AppClient {
|
|
|
59
63
|
return response.slice(0, -2); // drop the status word (can only be 0x9000 at this point)
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
async
|
|
66
|
+
async getExtendedPubkey(
|
|
67
|
+
display: boolean,
|
|
68
|
+
pathElements: number[]
|
|
69
|
+
): Promise<string> {
|
|
63
70
|
if (pathElements.length > 6) {
|
|
64
71
|
throw new Error("Path too long. At most 6 levels allowed.");
|
|
65
72
|
}
|
|
@@ -89,7 +96,7 @@ export class AppClient {
|
|
|
89
96
|
throw new Error("Invalid HMAC length");
|
|
90
97
|
}
|
|
91
98
|
|
|
92
|
-
const clientInterpreter = new ClientCommandInterpreter();
|
|
99
|
+
const clientInterpreter = new ClientCommandInterpreter(() => {});
|
|
93
100
|
clientInterpreter.addKnownList(
|
|
94
101
|
walletPolicy.keys.map((k) => Buffer.from(k, "ascii"))
|
|
95
102
|
);
|
|
@@ -116,7 +123,8 @@ export class AppClient {
|
|
|
116
123
|
async signPsbt(
|
|
117
124
|
psbt: PsbtV2,
|
|
118
125
|
walletPolicy: WalletPolicy,
|
|
119
|
-
walletHMAC: Buffer | null
|
|
126
|
+
walletHMAC: Buffer | null,
|
|
127
|
+
progressCallback: () => void
|
|
120
128
|
): Promise<Map<number, Buffer>> {
|
|
121
129
|
const merkelizedPsbt = new MerkelizedPsbt(psbt);
|
|
122
130
|
|
|
@@ -124,7 +132,7 @@ export class AppClient {
|
|
|
124
132
|
throw new Error("Invalid HMAC length");
|
|
125
133
|
}
|
|
126
134
|
|
|
127
|
-
const clientInterpreter = new ClientCommandInterpreter();
|
|
135
|
+
const clientInterpreter = new ClientCommandInterpreter(progressCallback);
|
|
128
136
|
|
|
129
137
|
// prepare ClientCommandInterpreter
|
|
130
138
|
clientInterpreter.addKnownList(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { crypto } from "bitcoinjs-lib";
|
|
2
|
+
import { BufferReader } from "../buffertools";
|
|
2
3
|
import { createVarint } from "../varint";
|
|
3
4
|
import { hashLeaf, Merkle } from "./merkle";
|
|
4
5
|
import { MerkleMap } from "./merkleMap";
|
|
@@ -21,13 +22,14 @@ export class YieldCommand extends ClientCommand {
|
|
|
21
22
|
|
|
22
23
|
code = ClientCommandCode.YIELD;
|
|
23
24
|
|
|
24
|
-
constructor(results: Buffer[]) {
|
|
25
|
+
constructor(results: Buffer[], private progressCallback: () => void) {
|
|
25
26
|
super();
|
|
26
27
|
this.results = results;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
execute(request: Buffer): Buffer {
|
|
30
31
|
this.results.push(Buffer.from(request.subarray(1)));
|
|
32
|
+
this.progressCallback();
|
|
31
33
|
return Buffer.from("");
|
|
32
34
|
}
|
|
33
35
|
}
|
|
@@ -105,19 +107,24 @@ export class GetMerkleLeafProofCommand extends ClientCommand {
|
|
|
105
107
|
execute(request: Buffer): Buffer {
|
|
106
108
|
const req = request.subarray(1);
|
|
107
109
|
|
|
108
|
-
if (req.length
|
|
109
|
-
throw new Error("Invalid request,
|
|
110
|
+
if (req.length < 32 + 1 + 1) {
|
|
111
|
+
throw new Error("Invalid request, expected at least 34 bytes");
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
|
|
113
|
-
const hash =
|
|
114
|
-
for (let i = 0; i < 32; i++) {
|
|
115
|
-
hash[i] = req.readUInt8(i);
|
|
116
|
-
}
|
|
114
|
+
const reqBuf = new BufferReader(req);
|
|
115
|
+
const hash = reqBuf.readSlice(32);
|
|
117
116
|
const hash_hex = hash.toString("hex");
|
|
118
117
|
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
let tree_size;
|
|
119
|
+
let leaf_index;
|
|
120
|
+
try {
|
|
121
|
+
tree_size = reqBuf.readVarInt();
|
|
122
|
+
leaf_index = reqBuf.readVarInt();
|
|
123
|
+
} catch (e: any) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
"Invalid request, couldn't parse tree_size or leaf_index"
|
|
126
|
+
);
|
|
127
|
+
}
|
|
121
128
|
|
|
122
129
|
const mt = this.known_trees.get(hash_hex);
|
|
123
130
|
if (!mt) {
|
|
@@ -247,6 +254,21 @@ export class GetMoreElementsCommand extends ClientCommand {
|
|
|
247
254
|
}
|
|
248
255
|
}
|
|
249
256
|
|
|
257
|
+
/**
|
|
258
|
+
* This class will dispatch a client command coming from the hardware device to
|
|
259
|
+
* the appropriate client command implementation. Those client commands
|
|
260
|
+
* typically requests data from a merkle tree or merkelized maps.
|
|
261
|
+
*
|
|
262
|
+
* A ClientCommandInterpreter is prepared by adding the merkle trees and
|
|
263
|
+
* merkelized maps it should be able to serve to the hardware device. This class
|
|
264
|
+
* doesn't know anything about the semantics of the data it holds, it just
|
|
265
|
+
* serves merkle data. It doesn't even know in what context it is being
|
|
266
|
+
* executed, ie SignPsbt, getWalletAddress, etc.
|
|
267
|
+
*
|
|
268
|
+
* If the command yelds results to the client, as signPsbt does, the yielded
|
|
269
|
+
* data will be accessible after the command completed by calling getYielded(),
|
|
270
|
+
* which will return the yields in the same order as they came in.
|
|
271
|
+
*/
|
|
250
272
|
export class ClientCommandInterpreter {
|
|
251
273
|
private roots: Map<string, Merkle> = new Map();
|
|
252
274
|
private preimages: Map<string, Buffer> = new Map();
|
|
@@ -257,9 +279,9 @@ export class ClientCommandInterpreter {
|
|
|
257
279
|
|
|
258
280
|
private commands: Map<ClientCommandCode, ClientCommand> = new Map();
|
|
259
281
|
|
|
260
|
-
constructor() {
|
|
282
|
+
constructor(progressCallback: () => void) {
|
|
261
283
|
const commands = [
|
|
262
|
-
new YieldCommand(this.yielded),
|
|
284
|
+
new YieldCommand(this.yielded, progressCallback),
|
|
263
285
|
new GetPreimageCommand(this.preimages, this.queue),
|
|
264
286
|
new GetMerkleLeafIndexCommand(this.roots),
|
|
265
287
|
new GetMerkleLeafProofCommand(this.roots, this.queue),
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { MerkleMap } from "./merkleMap";
|
|
2
2
|
import { PsbtV2 } from "./psbtv2";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* This class merkelizes a PSBTv2, by merkelizing the different
|
|
6
|
+
* maps of the psbt. This is used during the transaction signing process,
|
|
7
|
+
* where the hardware app can request specific parts of the psbt from the
|
|
8
|
+
* client code and be sure that the response data actually belong to the psbt.
|
|
9
|
+
* The reason for this is the limited amount of memory available to the app,
|
|
10
|
+
* so it can't always store the full psbt in memory.
|
|
11
|
+
*
|
|
12
|
+
* The signing process is documented at
|
|
13
|
+
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md#sign_psbt
|
|
14
|
+
*/
|
|
4
15
|
export class MerkelizedPsbt extends PsbtV2 {
|
|
5
16
|
public globalMerkleMap: MerkleMap;
|
|
6
17
|
public inputMerkleMaps: MerkleMap[] = [];
|
package/src/newops/merkle.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { crypto } from "bitcoinjs-lib";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* This class implements the merkle tree used by Ledger Bitcoin app v2+,
|
|
5
|
+
* which is documented at
|
|
6
|
+
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/merkle.md
|
|
7
|
+
*/
|
|
3
8
|
export class Merkle {
|
|
4
9
|
private leaves: Buffer[];
|
|
5
10
|
private rootNode: Node;
|
package/src/newops/merkleMap.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { createVarint } from "../varint";
|
|
2
2
|
import { hashLeaf, Merkle } from "./merkle";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* This implements "Merkelized Maps", documented at
|
|
6
|
+
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/merkle.md#merkleized-maps
|
|
7
|
+
*
|
|
8
|
+
* A merkelized map consist of two merkle trees, one for the keys of
|
|
9
|
+
* a map and one for the values of the same map, thus the two merkle
|
|
10
|
+
* trees have the same shape. The commitment is the number elements
|
|
11
|
+
* in the map followed by the keys' merkle root followed by the
|
|
12
|
+
* values' merkle root.
|
|
13
|
+
*/
|
|
4
14
|
export class MerkleMap {
|
|
5
15
|
keys: Buffer[];
|
|
6
16
|
keysTree: Merkle;
|
package/src/newops/policy.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { crypto } from "bitcoinjs-lib";
|
|
1
2
|
import { pathArrayToString } from "../bip32";
|
|
2
3
|
import { BufferWriter } from "../buffertools";
|
|
3
|
-
import {
|
|
4
|
-
import { Merkle, hashLeaf } from "./merkle";
|
|
4
|
+
import { hashLeaf, Merkle } from "./merkle";
|
|
5
5
|
|
|
6
6
|
export type DefaultDescriptorTemplate =
|
|
7
7
|
| "pkh(@0)"
|
|
@@ -9,6 +9,14 @@ export type DefaultDescriptorTemplate =
|
|
|
9
9
|
| "wpkh(@0)"
|
|
10
10
|
| "tr(@0)";
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* The Bitcon hardware app uses a descriptors-like thing to describe
|
|
14
|
+
* how to construct output scripts from keys. A "Wallet Policy" consists
|
|
15
|
+
* of a "Descriptor Template" and a list of "keys". A key is basically
|
|
16
|
+
* a serialized BIP32 extended public key with some added derivation path
|
|
17
|
+
* information. This is documented at
|
|
18
|
+
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/wallet.md
|
|
19
|
+
*/
|
|
12
20
|
export class WalletPolicy {
|
|
13
21
|
descriptorTemplate: string;
|
|
14
22
|
keys: string[];
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { BufferWriter } from "../buffertools";
|
|
2
2
|
import { PsbtV2 } from "./psbtv2";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* This implements the "Transaction Extractor" role of BIP370 (PSBTv2
|
|
6
|
+
* https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#transaction-extractor). However
|
|
7
|
+
* the role is partially documented in BIP174 (PSBTv0
|
|
8
|
+
* https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#transaction-extractor).
|
|
9
|
+
*/
|
|
4
10
|
export function extract(psbt: PsbtV2): Buffer {
|
|
5
11
|
const tx = new BufferWriter();
|
|
6
12
|
tx.writeUInt32(psbt.getGlobalTxVersion());
|
|
@@ -2,8 +2,18 @@ import { BufferWriter } from "../buffertools";
|
|
|
2
2
|
import { psbtIn, PsbtV2 } from "./psbtv2";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
+
* This roughly implements the "input finalizer" role of BIP370 (PSBTv2
|
|
6
|
+
* https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki). However
|
|
7
|
+
* the role is documented in BIP174 (PSBTv0
|
|
8
|
+
* https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki).
|
|
5
9
|
*
|
|
6
|
-
*
|
|
10
|
+
* Verify that all inputs have a signature, and set inputFinalScriptwitness
|
|
11
|
+
* and/or inputFinalScriptSig depending on the type of the spent outputs. Clean
|
|
12
|
+
* fields that aren't useful anymore, partial signatures, redeem script and
|
|
13
|
+
* derivation paths.
|
|
14
|
+
*
|
|
15
|
+
* @param psbt The psbt with all signatures added as partial sigs, either
|
|
16
|
+
* through PSBT_IN_PARTIAL_SIG or PSBT_IN_TAP_KEY_SIG
|
|
7
17
|
*/
|
|
8
18
|
export function finalize(psbt: PsbtV2): void {
|
|
9
19
|
// First check that each input has a signature
|
|
@@ -75,6 +85,13 @@ export function finalize(psbt: PsbtV2): void {
|
|
|
75
85
|
}
|
|
76
86
|
}
|
|
77
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Deletes fields that are no longer neccesary from the psbt.
|
|
90
|
+
*
|
|
91
|
+
* Note, the spec doesn't say anything about removing ouput fields
|
|
92
|
+
* like PSBT_OUT_BIP32_DERIVATION_PATH and others, so we keep them
|
|
93
|
+
* without actually knowing why. I think we should remove them too.
|
|
94
|
+
*/
|
|
78
95
|
function clearFinalizedInput(psbt: PsbtV2, inputIndex: number) {
|
|
79
96
|
const keyTypes = [
|
|
80
97
|
psbtIn.BIP32_DERIVATION,
|
|
@@ -93,6 +110,14 @@ function clearFinalizedInput(psbt: PsbtV2, inputIndex: number) {
|
|
|
93
110
|
psbt.deleteInputEntries(inputIndex, keyTypes);
|
|
94
111
|
}
|
|
95
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Writes a script push operation to buf, which looks different
|
|
115
|
+
* depending on the size of the data. See
|
|
116
|
+
* https://en.bitcoin.it/wiki/Script#Constants
|
|
117
|
+
*
|
|
118
|
+
* @param buf the BufferWriter to write to
|
|
119
|
+
* @param data the Buffer to be pushed.
|
|
120
|
+
*/
|
|
96
121
|
function writePush(buf: BufferWriter, data: Buffer) {
|
|
97
122
|
if (data.length <= 75) {
|
|
98
123
|
buf.writeUInt8(data.length);
|