Skip to content

Commit 5aa9345

Browse files
committed
feat: add Key Spend support for taproot to PSBT (more in commit description)
- `signInput()` creates and serialises the Schnorr signature for the taproot inptuts - only `SIGHASH_DEFAULT` supported at the moment - `validateSignaturesOfInput()` validates taproot input Key Spend signatures - add `tweakSigner()` as static method - the `Signer` interface has an optional `privateKey` field (used for tweaking) - direct dependency to `tiny-secp256k1` introduced - it is awkward to pass an `ecc` lib from outside <- must be revisited - added 'tiny-secp256k1' to package.json deps <- must be revisited
1 parent 0eb9961 commit 5aa9345

File tree

7 files changed

+406
-45
lines changed

7 files changed

+406
-45
lines changed

package-lock.json

Lines changed: 4 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"bip174": "^2.0.1",
5454
"bs58check": "^2.1.2",
5555
"create-hash": "^1.1.0",
56+
"tiny-secp256k1": "^2.2.0",
5657
"typeforce": "^1.11.3",
5758
"varuint-bitcoin": "^1.1.2",
5859
"wif": "^2.0.1"
@@ -83,7 +84,6 @@
8384
"randombytes": "^2.1.0",
8485
"regtest-client": "0.2.0",
8586
"rimraf": "^2.6.3",
86-
"tiny-secp256k1": "^2.1.2",
8787
"ts-node": "^8.3.0",
8888
"tslint": "^6.1.3",
8989
"typescript": "^4.4.4"

src/psbt.d.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,17 @@ export declare class Psbt {
5656
static fromBase64(data: string, opts?: PsbtOptsOptional): Psbt;
5757
static fromHex(data: string, opts?: PsbtOptsOptional): Psbt;
5858
static fromBuffer(buffer: Buffer, opts?: PsbtOptsOptional): Psbt;
59+
/**
60+
* Helper method for converting a normal Signer into a Taproot Signer.
61+
* Note that this helper method requires the Private Key of the Signer to be present.
62+
* Steps:
63+
* - if the Y coordinate of the Signer Public Key is odd then negate the Private Key
64+
* - tweak the private key with the provided hash (should be empty for key-path spending)
65+
* @param signer - a taproot signer object, the Private Key must be present
66+
* @param opts - tweak options
67+
* @returns a Signer having the Private and Public keys tweaked
68+
*/
69+
static tweakSigner(signer: Signer, opts?: TaprootSignerOpts): Signer;
5970
private __CACHE;
6071
private opts;
6172
constructor(opts?: PsbtOptsOptional, data?: PsbtBase);
@@ -143,24 +154,42 @@ export interface HDSigner extends HDSignerBase {
143154
* Return a 64 byte signature (32 byte r and 32 byte s in that order)
144155
*/
145156
sign(hash: Buffer): Buffer;
157+
signSchnorr?(hash: Buffer): Buffer;
146158
}
147159
/**
148160
* Same as above but with async sign method
149161
*/
150162
export interface HDSignerAsync extends HDSignerBase {
151163
derivePath(path: string): HDSignerAsync;
152164
sign(hash: Buffer): Promise<Buffer>;
165+
signSchnorr?(hash: Buffer): Promise<Buffer>;
153166
}
154167
export interface Signer {
155168
publicKey: Buffer;
169+
/**
170+
* Private Key is optional, it is required only if the signer must be tweaked.
171+
* See the `tweakSigner()` method.
172+
*/
173+
privateKey?: Buffer;
156174
network?: any;
157175
sign(hash: Buffer, lowR?: boolean): Buffer;
176+
signSchnorr?(hash: Buffer): Buffer;
158177
getPublicKey?(): Buffer;
159178
}
179+
/**
180+
* Options for tweaking a Signer into a valid Taproot Signer
181+
*/
182+
export interface TaprootSignerOpts {
183+
network?: Network;
184+
eccLib?: any;
185+
/** The hash used to tweak the Signer */
186+
tweakHash?: Buffer;
187+
}
160188
export interface SignerAsync {
161189
publicKey: Buffer;
162190
network?: any;
163191
sign(hash: Buffer, lowR?: boolean): Promise<Buffer>;
192+
signSchnorr?(hash: Buffer): Promise<Buffer>;
164193
getPublicKey?(): Buffer;
165194
}
166195
/**
@@ -178,5 +207,5 @@ isP2WSH: boolean) => {
178207
finalScriptSig: Buffer | undefined;
179208
finalScriptWitness: Buffer | undefined;
180209
};
181-
declare type AllScriptType = 'witnesspubkeyhash' | 'pubkeyhash' | 'multisig' | 'pubkey' | 'nonstandard' | 'p2sh-witnesspubkeyhash' | 'p2sh-pubkeyhash' | 'p2sh-multisig' | 'p2sh-pubkey' | 'p2sh-nonstandard' | 'p2wsh-pubkeyhash' | 'p2wsh-multisig' | 'p2wsh-pubkey' | 'p2wsh-nonstandard' | 'p2sh-p2wsh-pubkeyhash' | 'p2sh-p2wsh-multisig' | 'p2sh-p2wsh-pubkey' | 'p2sh-p2wsh-nonstandard';
210+
declare type AllScriptType = 'witnesspubkeyhash' | 'pubkeyhash' | 'multisig' | 'pubkey' | 'taproot' | 'nonstandard' | 'p2sh-witnesspubkeyhash' | 'p2sh-pubkeyhash' | 'p2sh-multisig' | 'p2sh-pubkey' | 'p2sh-nonstandard' | 'p2wsh-pubkeyhash' | 'p2wsh-multisig' | 'p2wsh-pubkey' | 'p2wsh-nonstandard' | 'p2sh-p2wsh-pubkeyhash' | 'p2sh-p2wsh-multisig' | 'p2sh-p2wsh-pubkey' | 'p2sh-p2wsh-nonstandard';
182211
export {};

src/psbt.js

Lines changed: 124 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
'use strict';
22
Object.defineProperty(exports, '__esModule', { value: true });
33
exports.Psbt = void 0;
4+
const ecc = require('tiny-secp256k1'); // TODO: extract
5+
const ecpair_1 = require('ecpair');
46
const bip174_1 = require('bip174');
57
const varuint = require('bip174/src/lib/converter/varint');
68
const utils_1 = require('bip174/src/lib/utils');
@@ -11,6 +13,7 @@ const networks_1 = require('./networks');
1113
const payments = require('./payments');
1214
const bscript = require('./script');
1315
const transaction_1 = require('./transaction');
16+
const taprootutils_1 = require('./payments/taprootutils');
1417
/**
1518
* These are the default arguments for a Psbt instance.
1619
*/
@@ -103,6 +106,39 @@ class Psbt {
103106
checkTxForDupeIns(psbt.__CACHE.__TX, psbt.__CACHE);
104107
return psbt;
105108
}
109+
/**
110+
* Helper method for converting a normal Signer into a Taproot Signer.
111+
* Note that this helper method requires the Private Key of the Signer to be present.
112+
* Steps:
113+
* - if the Y coordinate of the Signer Public Key is odd then negate the Private Key
114+
* - tweak the private key with the provided hash (should be empty for key-path spending)
115+
* @param signer - a taproot signer object, the Private Key must be present
116+
* @param opts - tweak options
117+
* @returns a Signer having the Private and Public keys tweaked
118+
*/
119+
static tweakSigner(signer, opts = {}) {
120+
let privateKey = signer.privateKey;
121+
if (!privateKey) {
122+
throw new Error('Private key is required for tweaking signer!');
123+
}
124+
if (signer.publicKey[0] === 3) {
125+
privateKey = ecc.privateNegate(privateKey);
126+
}
127+
const tweakedPrivateKey = ecc.privateAdd(
128+
privateKey,
129+
(0, taprootutils_1.tapTweakHash)(
130+
signer.publicKey.slice(1, 33),
131+
opts.tweakHash,
132+
),
133+
);
134+
if (!tweakedPrivateKey) {
135+
throw new Error('Invalid tweaked private key!');
136+
}
137+
const ECPair = (0, ecpair_1.ECPairFactory)(ecc);
138+
return ECPair.fromPrivateKey(Buffer.from(tweakedPrivateKey), {
139+
network: opts.network,
140+
});
141+
}
106142
get inputCount() {
107143
return this.data.inputs.length;
108144
}
@@ -298,7 +334,11 @@ class Psbt {
298334
}
299335
getInputType(inputIndex) {
300336
const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex);
301-
const script = getScriptFromUtxo(inputIndex, input, this.__CACHE);
337+
const { script } = getScriptAndAmountFromUtxo(
338+
inputIndex,
339+
input,
340+
this.__CACHE,
341+
);
302342
const result = getMeaningfulScript(
303343
script,
304344
inputIndex,
@@ -355,13 +395,21 @@ class Psbt {
355395
let hashCache;
356396
let scriptCache;
357397
let sighashCache;
398+
const scriptType = this.getInputType(inputIndex);
358399
for (const pSig of mySigs) {
359-
const sig = bscript.signature.decode(pSig.signature);
400+
const sig =
401+
scriptType === 'taproot'
402+
? {
403+
signature: pSig.signature,
404+
hashType: transaction_1.Transaction.SIGHASH_DEFAULT,
405+
}
406+
: bscript.signature.decode(pSig.signature);
360407
const { hash, script } =
361408
sighashCache !== sig.hashType
362409
? getHashForSig(
363410
inputIndex,
364411
Object.assign({}, input, { sighashType: sig.hashType }),
412+
this.data.inputs,
365413
this.__CACHE,
366414
true,
367415
)
@@ -526,13 +574,30 @@ class Psbt {
526574
this.__CACHE,
527575
sighashTypes,
528576
);
529-
const partialSig = [
530-
{
531-
pubkey: keyPair.publicKey,
532-
signature: bscript.signature.encode(keyPair.sign(hash), sighashType),
533-
},
534-
];
535-
this.data.updateInput(inputIndex, { partialSig });
577+
const scriptType = this.getInputType(inputIndex);
578+
if (scriptType === 'taproot') {
579+
if (!keyPair.signSchnorr) {
580+
throw new Error(
581+
`Need Schnorr Signer to sign taproot input #${inputIndex}.`,
582+
);
583+
}
584+
const partialSig = [
585+
{
586+
pubkey: keyPair.publicKey,
587+
signature: keyPair.signSchnorr(hash),
588+
},
589+
];
590+
// must be changed to use the `updateInput()` public API
591+
this.data.inputs[inputIndex].partialSig = partialSig;
592+
} else {
593+
const partialSig = [
594+
{
595+
pubkey: keyPair.publicKey,
596+
signature: bscript.signature.encode(keyPair.sign(hash), sighashType),
597+
},
598+
];
599+
this.data.updateInput(inputIndex, { partialSig });
600+
}
536601
return this;
537602
}
538603
signInputAsync(
@@ -671,6 +736,7 @@ function canFinalize(input, script, scriptType) {
671736
case 'pubkey':
672737
case 'pubkeyhash':
673738
case 'witnesspubkeyhash':
739+
case 'taproot':
674740
return hasSigs(1, input.partialSig);
675741
case 'multisig':
676742
const p2ms = payments.p2ms({ output: script });
@@ -704,9 +770,9 @@ function isFinalized(input) {
704770
return !!input.finalScriptSig || !!input.finalScriptWitness;
705771
}
706772
function isPaymentFactory(payment) {
707-
return script => {
773+
return (script, eccLib) => {
708774
try {
709-
payment({ output: script });
775+
payment({ output: script }, { eccLib });
710776
return true;
711777
} catch (err) {
712778
return false;
@@ -719,6 +785,7 @@ const isP2PKH = isPaymentFactory(payments.p2pkh);
719785
const isP2WPKH = isPaymentFactory(payments.p2wpkh);
720786
const isP2WSHScript = isPaymentFactory(payments.p2wsh);
721787
const isP2SHScript = isPaymentFactory(payments.p2sh);
788+
const isP2TR = isPaymentFactory(payments.p2tr);
722789
function bip32DerivationIsMine(root) {
723790
return d => {
724791
if (!d.masterFingerprint.equals(root.fingerprint)) return false;
@@ -920,6 +987,7 @@ function getHashAndSighashType(
920987
const { hash, sighashType, script } = getHashForSig(
921988
inputIndex,
922989
input,
990+
inputs,
923991
cache,
924992
false,
925993
sighashTypes,
@@ -930,7 +998,14 @@ function getHashAndSighashType(
930998
sighashType,
931999
};
9321000
}
933-
function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) {
1001+
function getHashForSig(
1002+
inputIndex,
1003+
input,
1004+
inputs,
1005+
cache,
1006+
forValidate,
1007+
sighashTypes,
1008+
) {
9341009
const unsignedTx = cache.__TX;
9351010
const sighashType =
9361011
input.sighashType || transaction_1.Transaction.SIGHASH_ALL;
@@ -988,6 +1063,18 @@ function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) {
9881063
prevout.value,
9891064
sighashType,
9901065
);
1066+
} else if (isP2TR(meaningfulScript, ecc)) {
1067+
const prevOuts = inputs.map((i, index) =>
1068+
getScriptAndAmountFromUtxo(index, i, cache),
1069+
);
1070+
const signingScripts = prevOuts.map(o => o.script);
1071+
const values = prevOuts.map(o => o.value);
1072+
hash = unsignedTx.hashForWitnessV1(
1073+
inputIndex,
1074+
signingScripts,
1075+
values,
1076+
transaction_1.Transaction.SIGHASH_DEFAULT,
1077+
);
9911078
} else {
9921079
// non-segwit
9931080
if (
@@ -1050,6 +1137,15 @@ function getPayment(script, scriptType, partialSig) {
10501137
signature: partialSig[0].signature,
10511138
});
10521139
break;
1140+
case 'taproot':
1141+
payment = payments.p2tr(
1142+
{
1143+
output: script,
1144+
signature: partialSig[0].signature,
1145+
},
1146+
{ eccLib: ecc },
1147+
);
1148+
break;
10531149
}
10541150
return payment;
10551151
}
@@ -1094,7 +1190,7 @@ function getScriptFromInput(inputIndex, input, cache) {
10941190
res.script = input.witnessUtxo.script;
10951191
}
10961192
}
1097-
if (input.witnessScript || isP2WPKH(res.script)) {
1193+
if (input.witnessScript || isP2WPKH(res.script) || isP2TR(res.script, ecc)) {
10981194
res.isSegwit = true;
10991195
}
11001196
return res;
@@ -1267,22 +1363,26 @@ function nonWitnessUtxoTxFromCache(cache, input, inputIndex) {
12671363
}
12681364
return c[inputIndex];
12691365
}
1270-
function getScriptFromUtxo(inputIndex, input, cache) {
1366+
function getScriptAndAmountFromUtxo(inputIndex, input, cache) {
12711367
if (input.witnessUtxo !== undefined) {
1272-
return input.witnessUtxo.script;
1368+
return {
1369+
script: input.witnessUtxo.script,
1370+
value: input.witnessUtxo.value,
1371+
};
12731372
} else if (input.nonWitnessUtxo !== undefined) {
12741373
const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache(
12751374
cache,
12761375
input,
12771376
inputIndex,
12781377
);
1279-
return nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index].script;
1378+
const o = nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index];
1379+
return { script: o.script, value: o.value };
12801380
} else {
12811381
throw new Error("Can't find pubkey in input without Utxo data");
12821382
}
12831383
}
12841384
function pubkeyInInput(pubkey, input, inputIndex, cache) {
1285-
const script = getScriptFromUtxo(inputIndex, input, cache);
1385+
const { script } = getScriptAndAmountFromUtxo(inputIndex, input, cache);
12861386
const { meaningfulScript } = getMeaningfulScript(
12871387
script,
12881388
inputIndex,
@@ -1392,18 +1492,24 @@ function checkInvalidP2WSH(script) {
13921492
}
13931493
function pubkeyInScript(pubkey, script) {
13941494
const pubkeyHash = (0, crypto_1.hash160)(pubkey);
1495+
const pubkeyXOnly = pubkey.slice(1, 33);
13951496
const decompiled = bscript.decompile(script);
13961497
if (decompiled === null) throw new Error('Unknown script error');
13971498
return decompiled.some(element => {
13981499
if (typeof element === 'number') return false;
1399-
return element.equals(pubkey) || element.equals(pubkeyHash);
1500+
return (
1501+
element.equals(pubkey) ||
1502+
element.equals(pubkeyHash) ||
1503+
element.equals(pubkeyXOnly)
1504+
);
14001505
});
14011506
}
14021507
function classifyScript(script) {
14031508
if (isP2WPKH(script)) return 'witnesspubkeyhash';
14041509
if (isP2PKH(script)) return 'pubkeyhash';
14051510
if (isP2MS(script)) return 'multisig';
14061511
if (isP2PK(script)) return 'pubkey';
1512+
if (isP2TR(script, ecc)) return 'taproot';
14071513
return 'nonstandard';
14081514
}
14091515
function range(n) {

0 commit comments

Comments
 (0)