Skip to content

Commit fdb8698

Browse files
committed
feat: extract tiny-secp256k1 out of the Psbt module
- the Psbt() constructor options accept an eccLib - `tweakSigner()` is a public helper method - `tiny-secp256k1` is only a dev dependency now
1 parent 3ee59b7 commit fdb8698

File tree

14 files changed

+172
-123
lines changed

14 files changed

+172
-123
lines changed

package-lock.json

Lines changed: 3 additions & 1 deletion
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,7 +53,6 @@
5353
"bip174": "^2.0.1",
5454
"bs58check": "^2.1.2",
5555
"create-hash": "^1.1.0",
56-
"tiny-secp256k1": "^2.2.0",
5756
"typeforce": "^1.11.3",
5857
"varuint-bitcoin": "^1.1.2",
5958
"wif": "^2.0.1"
@@ -84,6 +83,7 @@
8483
"randombytes": "^2.1.0",
8584
"regtest-client": "0.2.0",
8685
"rimraf": "^2.6.3",
86+
"tiny-secp256k1": "^2.2.0",
8787
"ts-node": "^8.3.0",
8888
"tslint": "^6.1.3",
8989
"typescript": "^4.4.4"

src/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as script from './script';
66
export { address, crypto, networks, payments, script };
77
export { Block } from './block';
88
export { TaggedHashPrefix } from './crypto';
9-
export { Psbt, PsbtTxInput, PsbtTxOutput, Signer, SignerAsync, HDSigner, HDSignerAsync, } from './psbt';
9+
export { Psbt, PsbtTxInput, PsbtTxOutput, Signer, SignerAsync, HDSigner, HDSignerAsync, tweakSigner, } from './psbt';
1010
export { OPS as opcodes } from './ops';
1111
export { Transaction } from './transaction';
1212
export { Network } from './networks';

src/index.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22
Object.defineProperty(exports, '__esModule', { value: true });
3-
exports.Transaction = exports.opcodes = exports.Psbt = exports.Block = exports.script = exports.payments = exports.networks = exports.crypto = exports.address = void 0;
3+
exports.Transaction = exports.opcodes = exports.tweakSigner = exports.Psbt = exports.Block = exports.script = exports.payments = exports.networks = exports.crypto = exports.address = void 0;
44
const address = require('./address');
55
exports.address = address;
66
const crypto = require('./crypto');
@@ -25,6 +25,12 @@ Object.defineProperty(exports, 'Psbt', {
2525
return psbt_1.Psbt;
2626
},
2727
});
28+
Object.defineProperty(exports, 'tweakSigner', {
29+
enumerable: true,
30+
get: function() {
31+
return psbt_1.tweakSigner;
32+
},
33+
});
2834
var ops_1 = require('./ops');
2935
Object.defineProperty(exports, 'opcodes', {
3036
enumerable: true,

src/payments/p2tr.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ function p2tr(a, opts) {
1717
!a.address &&
1818
!a.output &&
1919
!a.pubkey &&
20-
!a.output &&
2120
!a.internalPubkey &&
2221
!(a.witness && a.witness.length > 1)
2322
)
@@ -115,6 +114,7 @@ function p2tr(a, opts) {
115114
return witness[witness.length - 1].slice(1, 33);
116115
});
117116
lazy.prop(o, 'signature', () => {
117+
if (a.signature) return a.signature;
118118
if (!a.witness || a.witness.length !== 1) return;
119119
return a.witness[0];
120120
});
@@ -192,9 +192,6 @@ function p2tr(a, opts) {
192192
// key spending
193193
if (a.signature && !a.signature.equals(witness[0]))
194194
throw new TypeError('Signature mismatch');
195-
// todo: recheck
196-
// if (!bscript.isSchnorSignature(a.pubkey, a.witness[0]))
197-
// throw new TypeError('Witness has invalid signature');
198195
} else {
199196
// script path spending
200197
const controlBlock = witness[witness.length - 1];

src/psbt.d.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
/// <reference types="node" />
22
import { Psbt as PsbtBase } from 'bip174';
33
import { KeyValue, PsbtGlobalUpdate, PsbtInput, PsbtInputUpdate, PsbtOutput, PsbtOutputUpdate } from 'bip174/src/lib/interfaces';
4+
import { TinySecp256k1Interface as ECPairTinySecp256k1Interface } from 'ecpair';
45
import { Network } from './networks';
56
import { Transaction } from './transaction';
7+
import { TinySecp256k1Interface } from './types';
68
export interface TransactionInput {
79
hash: string | Buffer;
810
index: number;
@@ -56,17 +58,6 @@ export declare class Psbt {
5658
static fromBase64(data: string, opts?: PsbtOptsOptional): Psbt;
5759
static fromHex(data: string, opts?: PsbtOptsOptional): Psbt;
5860
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;
7061
private __CACHE;
7162
private opts;
7263
constructor(opts?: PsbtOptsOptional, data?: PsbtBase);
@@ -118,9 +109,21 @@ export declare class Psbt {
118109
addUnknownKeyValToOutput(outputIndex: number, keyVal: KeyValue): this;
119110
clearFinalizedInput(inputIndex: number): this;
120111
}
112+
/**
113+
* Helper method for converting a normal Signer into a Taproot Signer.
114+
* Note that this helper method requires the Private Key of the Signer to be present.
115+
* Steps:
116+
* - if the Y coordinate of the Signer Public Key is odd then negate the Private Key
117+
* - tweak the private key with the provided hash (should be empty for key-path spending)
118+
* @param signer - a taproot signer object, the Private Key must be present
119+
* @param opts - tweak options
120+
* @returns a Signer having the Private and Public keys tweaked
121+
*/
122+
export declare function tweakSigner(signer: Signer, opts: TaprootSignerOpts): Signer;
121123
interface PsbtOptsOptional {
122124
network?: Network;
123125
maximumFeeRate?: number;
126+
eccLib?: TinySecp256k1Interface;
124127
}
125128
interface PsbtInputExtended extends PsbtInput, TransactionInput {
126129
}
@@ -181,7 +184,7 @@ export interface Signer {
181184
*/
182185
export interface TaprootSignerOpts {
183186
network?: Network;
184-
eccLib?: any;
187+
eccLib: TinySecp256k1Interface & ECPairTinySecp256k1Interface;
185188
/** The hash used to tweak the Signer */
186189
tweakHash?: Buffer;
187190
}

src/psbt.js

Lines changed: 64 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use strict';
22
Object.defineProperty(exports, '__esModule', { value: true });
3-
exports.Psbt = void 0;
4-
const ecc = require('tiny-secp256k1'); // TODO: extract
3+
exports.tweakSigner = exports.Psbt = void 0;
54
const ecpair_1 = require('ecpair');
65
const bip174_1 = require('bip174');
76
const varuint = require('bip174/src/lib/converter/varint');
@@ -81,6 +80,7 @@ class Psbt {
8180
// We will disable exporting the Psbt when unsafe sign is active.
8281
// because it is not BIP174 compliant.
8382
__UNSAFE_SIGN_NONSEGWIT: false,
83+
__EC_LIB: opts.eccLib,
8484
};
8585
if (this.data.inputs.length === 0) this.setVersion(2);
8686
// Make data hidden when enumerating
@@ -106,39 +106,6 @@ class Psbt {
106106
checkTxForDupeIns(psbt.__CACHE.__TX, psbt.__CACHE);
107107
return psbt;
108108
}
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-
}
142109
get inputCount() {
143110
return this.data.inputs.length;
144111
}
@@ -307,7 +274,7 @@ class Psbt {
307274
range(this.data.inputs.length).forEach(idx => this.finalizeInput(idx));
308275
return this;
309276
}
310-
finalizeInput(inputIndex, finalScriptsFunc = getFinalScripts) {
277+
finalizeInput(inputIndex, finalScriptsFunc) {
311278
const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex);
312279
const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput(
313280
inputIndex,
@@ -316,13 +283,15 @@ class Psbt {
316283
);
317284
if (!script) throw new Error(`No script found for input #${inputIndex}`);
318285
checkPartialSigSighashes(input);
319-
const { finalScriptSig, finalScriptWitness } = finalScriptsFunc(
286+
const fn = finalScriptsFunc || getFinalScripts;
287+
const { finalScriptSig, finalScriptWitness } = fn(
320288
inputIndex,
321289
input,
322290
script,
323291
isSegwit,
324292
isP2SH,
325293
isP2WSH,
294+
this.__CACHE.__EC_LIB,
326295
);
327296
if (finalScriptSig) this.data.updateInput(inputIndex, { finalScriptSig });
328297
if (finalScriptWitness)
@@ -348,7 +317,10 @@ class Psbt {
348317
redeemFromFinalWitnessScript(input.finalScriptWitness),
349318
);
350319
const type = result.type === 'raw' ? '' : result.type + '-';
351-
const mainType = classifyScript(result.meaningfulScript);
320+
const mainType = classifyScript(
321+
result.meaningfulScript,
322+
this.__CACHE.__EC_LIB,
323+
);
352324
return type + mainType;
353325
}
354326
inputHasPubkey(inputIndex, pubkey) {
@@ -676,6 +648,41 @@ class Psbt {
676648
}
677649
}
678650
exports.Psbt = Psbt;
651+
/**
652+
* Helper method for converting a normal Signer into a Taproot Signer.
653+
* Note that this helper method requires the Private Key of the Signer to be present.
654+
* Steps:
655+
* - if the Y coordinate of the Signer Public Key is odd then negate the Private Key
656+
* - tweak the private key with the provided hash (should be empty for key-path spending)
657+
* @param signer - a taproot signer object, the Private Key must be present
658+
* @param opts - tweak options
659+
* @returns a Signer having the Private and Public keys tweaked
660+
*/
661+
function tweakSigner(signer, opts) {
662+
// todo: test ecc??
663+
let privateKey = signer.privateKey;
664+
if (!privateKey) {
665+
throw new Error('Private key is required for tweaking signer!');
666+
}
667+
if (signer.publicKey[0] === 3) {
668+
privateKey = opts.eccLib.privateNegate(privateKey);
669+
}
670+
const tweakedPrivateKey = opts.eccLib.privateAdd(
671+
privateKey,
672+
(0, taprootutils_1.tapTweakHash)(
673+
signer.publicKey.slice(1, 33),
674+
opts.tweakHash,
675+
),
676+
);
677+
if (!tweakedPrivateKey) {
678+
throw new Error('Invalid tweaked private key!');
679+
}
680+
const ECPair = (0, ecpair_1.ECPairFactory)(opts.eccLib);
681+
return ECPair.fromPrivateKey(Buffer.from(tweakedPrivateKey), {
682+
network: opts.network,
683+
});
684+
}
685+
exports.tweakSigner = tweakSigner;
679686
/**
680687
* This function is needed to pass to the bip174 base class's fromBuffer.
681688
* It takes the "transaction buffer" portion of the psbt buffer and returns a
@@ -928,8 +935,16 @@ function getTxCacheValue(key, name, inputs, c) {
928935
if (key === '__FEE_RATE') return c.__FEE_RATE;
929936
else if (key === '__FEE') return c.__FEE;
930937
}
931-
function getFinalScripts(inputIndex, input, script, isSegwit, isP2SH, isP2WSH) {
932-
const scriptType = classifyScript(script);
938+
function getFinalScripts(
939+
inputIndex,
940+
input,
941+
script,
942+
isSegwit,
943+
isP2SH,
944+
isP2WSH,
945+
eccLib,
946+
) {
947+
const scriptType = classifyScript(script, eccLib);
933948
if (!canFinalize(input, script, scriptType))
934949
throw new Error(`Can not finalize input #${inputIndex}`);
935950
return prepareFinalScripts(
@@ -1063,7 +1078,7 @@ function getHashForSig(
10631078
prevout.value,
10641079
sighashType,
10651080
);
1066-
} else if (isP2TR(meaningfulScript, ecc)) {
1081+
} else if (isP2TR(meaningfulScript, cache.__EC_LIB)) {
10671082
const prevOuts = inputs.map((i, index) =>
10681083
getScriptAndAmountFromUtxo(index, i, cache),
10691084
);
@@ -1143,7 +1158,7 @@ function getPayment(script, scriptType, partialSig) {
11431158
output: script,
11441159
signature: partialSig[0].signature,
11451160
},
1146-
{ eccLib: ecc },
1161+
{ validate: false },
11471162
);
11481163
break;
11491164
}
@@ -1190,7 +1205,11 @@ function getScriptFromInput(inputIndex, input, cache) {
11901205
res.script = input.witnessUtxo.script;
11911206
}
11921207
}
1193-
if (input.witnessScript || isP2WPKH(res.script) || isP2TR(res.script, ecc)) {
1208+
if (
1209+
input.witnessScript ||
1210+
isP2WPKH(res.script) ||
1211+
isP2TR(res.script, cache.__EC_LIB)
1212+
) {
11941213
res.isSegwit = true;
11951214
}
11961215
return res;
@@ -1504,12 +1523,12 @@ function pubkeyInScript(pubkey, script) {
15041523
);
15051524
});
15061525
}
1507-
function classifyScript(script) {
1526+
function classifyScript(script, eccLib) {
15081527
if (isP2WPKH(script)) return 'witnesspubkeyhash';
15091528
if (isP2PKH(script)) return 'pubkeyhash';
15101529
if (isP2MS(script)) return 'multisig';
15111530
if (isP2PK(script)) return 'pubkey';
1512-
if (isP2TR(script, ecc)) return 'taproot';
1531+
if (isP2TR(script, eccLib)) return 'taproot';
15131532
return 'nonstandard';
15141533
}
15151534
function range(n) {

src/types.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export interface TaprootLeaf {
2121
export interface TinySecp256k1Interface {
2222
isXOnlyPoint(p: Uint8Array): boolean;
2323
xOnlyPointAddTweak(p: Uint8Array, tweak: Uint8Array): XOnlyPointAddTweakResult | null;
24+
privateAdd(d: Uint8Array, tweak: Uint8Array): Uint8Array | null;
25+
privateNegate(d: Uint8Array): Uint8Array;
2426
}
2527
export declare const Buffer256bit: any;
2628
export declare const Hash160bit: any;

test/fixtures/psbt.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@
281281
{
282282
"description": "Sign PSBT with 3 inputs [P2PKH, P2TR, P2WPKH] and two outputs [P2TR, P2WPKH]",
283283
"psbt": "cHNidP8BAM8CAAAAAwPzd9k+uLSN1rgF01xY1TIA/8N+YytNZ4VP9gKFP4MyAAAAAAD/////ZtAAqL2E1fKcmGo+7xuqS+nSQeKFVKGRYaHfIvLXn4sAAAAAAP////9+h+SlCwIx1MUDT7Bek0NrWXS7xnSPi5LbYbDc9sxYIgAAAAAA/////wIgKRsAAAAAACJRIEb2SXyy8Z1Qw+npgqlQ3MhiFLAfzOQ3pCBhx72xIw0zuAUBAAAAAAAWABTJijE0v48z5ZmmfEAADXdCBcG0FAAAAAAAAQDiAgAAAAABAUfY2D1t0dyMeEH39C1yOdIxigpqm7XJNqHVT3Lc+FkiAAAAAAD+////AhIsGwAAAAAAGXapFJ5+8XZ3ZP80oFldvEwrcNsBftBmiKyYdK6xAAAAABepFLDBn59UffGbX7u/olyFDG0eG1UJhwJHMEQCIDAd3s05C61flXVFqOtov0NoHRGr8KFcOpH6R/81F46EAiBt+j9hHyvT2hYEyf8fdYsM9IgbnybtPV+kRTHDa6Rj0AEhAmmZfwmoHsmCkEOn9AfRTh+863mURelmE8hSqL4MG1EydJwgAAABASu4BQEAAAAAACJRIJQh5zSw+dLEZ+p90ZfGGstEZ83LyfTLDFcfi2OlxAyuAAEBHxAnAAAAAAAAFgAUT6KsoSi2+d7lMJxPcAUeScZf1zIAAAA=",
284+
"isTaproot": true,
284285
"keys": [
285286
{
286287
"inputToSign": 0,

0 commit comments

Comments
 (0)