Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,66 @@ When utilizing Taproot features with bitcoinjs-lib, you may need to include an a
Another alternative library for ECC functionality. This requires access to the global `BigInt` primitive.
For advantages and detailed comparison of these libraries, visit: [tiny-secp256k1 GitHub page](https://github.com/bitcoinjs/tiny-secp256k1).

#### Signing a tapscript path without the script

`signInput` derives the BIP341 tapleaf hash from the leaf script on the input.
`generateTaprootScriptHashSignature` takes that hash directly instead, returning
the same signature without the script ever being present. Unlike the `sign*`
methods it stores nothing - it hands the signature back to you:

```javascript
const { generateTaprootScriptHashSignature } = require('bitcoinjs-lib');

const signature = generateTaprootScriptHashSignature(psbt, 0, keyPair, leafScriptHash);
```

The whole `psbt` is required rather than just the input being signed: the BIP341
sighash commits to every prevout, and depending on the sighash type to the
outputs too, so the PSBT must already hold its inputs and outputs. The same thing
is available as `psbt.generateTaprootScriptHashSignature(0, keyPair, leafScriptHash)`.

`leafScriptHash` is the tapleaf hash - exactly what `tapleafHash({ output:
script, version: leafVersion })` from `bitcoinjs-lib/src/payments/bip341`
returns - not a plain hash of the script bytes. The leaf version is already
committed to inside it, and the control block plays no part in the signature, so
neither is passed here.

The PSBT is **not** modified. To attach the signature, put it in a
`tapScriptSig` - a standard field carrying only the pubkey, the signature and the
leaf hash, so it needs no script and serializes normally:

```javascript
psbt.updateInput(0, {
tapScriptSig: [{ pubkey: toXOnly(keyPair.publicKey), signature, leafHash: leafScriptHash }],
});
```

Sighash type comes from the input, defaulting to `SIGHASH_DEFAULT`. Anything else
must be set on the input *and* whitelisted in the call:

```javascript
psbt.updateInput(0, { sighashType: Transaction.SIGHASH_ALL });
const signature = generateTaprootScriptHashSignature(psbt, 0, keyPair, leafScriptHash, [
Transaction.SIGHASH_ALL,
]);
```

**WARNING**: this is a blind signature. Nothing can check that your pubkey appears
in the leaf, that the leaf belongs to the taptree being spent, or what the script
authorises - every guard the normal `signInput` path relies on comes from reading
the script. Only use this when the leaf is authenticated by some other means.

Two consequences follow from the script being absent. `validateSignaturesOfInput`
cannot check the signature, and `finalizeInput` cannot build the witness. Both
work again once the script is supplied:

```javascript
psbt.updateInput(0, {
tapLeafScript: [{ leafVersion: LEAF_VERSION_TAPSCRIPT, script, controlBlock }],
});
psbt.finalizeInput(0);
```

**NOTE**: We use Node Maintenance LTS features, if you need strict ES5, use [`--transform babelify`](https://github.com/babel/babelify) in conjunction with your `browserify` step (using an [`es2015`](https://babeljs.io/docs/plugins/preset-es2015/) preset).

**WARNING**: iOS devices have [problems](https://github.com/feross/buffer/issues/136), use at least [buffer@5.0.5](https://github.com/feross/buffer/pull/155) or greater, and enforce the test suites (for `Buffer`, and any other dependency) pass before use.
Expand Down
7 changes: 7 additions & 0 deletions src/cjs/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
exports.initEccLib =
exports.Transaction =
exports.opcodes =
exports.generateTaprootScriptHashSignature =
exports.toXOnly =
exports.Psbt =
exports.Block =
Expand Down Expand Up @@ -86,6 +87,12 @@ Object.defineProperty(exports, 'toXOnly', {
return psbt_js_1.toXOnly;
},
});
Object.defineProperty(exports, 'generateTaprootScriptHashSignature', {
enumerable: true,
get: function () {
return psbt_js_1.generateTaprootScriptHashSignature;
},
});
/** @hidden */
var ops_js_1 = require('./ops.cjs');
Object.defineProperty(exports, 'opcodes', {
Expand Down
2 changes: 1 addition & 1 deletion src/cjs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export { address, crypto, networks, payments, script };
export { Block } from './block.js';
/** @hidden */
export { TaggedHashPrefix } from './crypto.js';
export { Psbt, PsbtTxInput, PsbtTxOutput, Signer, SignerAsync, HDSigner, HDSignerAsync, toXOnly, } from './psbt.js';
export { Psbt, PsbtTxInput, PsbtTxOutput, Signer, SignerAsync, HDSigner, HDSignerAsync, toXOnly, generateTaprootScriptHashSignature, } from './psbt.js';
/** @hidden */
export { OPS as opcodes } from './ops.js';
export { Transaction } from './transaction.js';
Expand Down
121 changes: 121 additions & 0 deletions src/cjs/psbt.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ var __importStar =
};
Object.defineProperty(exports, '__esModule', { value: true });
exports.Psbt = exports.toXOnly = void 0;
exports.generateTaprootScriptHashSignature = generateTaprootScriptHashSignature;
const bip174_1 = require('bip174');
const varuint = __importStar(require('varuint-bitcoin'));
const bip174_2 = require('bip174');
Expand Down Expand Up @@ -720,6 +721,84 @@ class Psbt {
);
throw new Error(`Input #${inputIndex} is not of type Taproot.`);
}
/**
* Generates a taproot script-path signature for a leaf whose script is not
* known, given only that leaf's BIP341 tapleaf hash. The signature is
* returned; nothing is written to this PSBT.
*
* `scriptHash` is the tapleaf hash, i.e.
* `taggedHash('TapLeaf', leafVersion || compactSize(script) || script)` - the
* exact value `tapleafHash({ output: script, version: leafVersion })` returns,
* and the value the BIP341 script-path sighash commits to. It is NOT a plain
* hash of the script bytes. The leaf version is already committed to inside
* it, and the control block plays no part in the signature, so neither is
* needed here.
*
* The signature is returned rather than written to the PSBT. To attach it,
* put it in a `tapScriptSig` - a field that carries only the pubkey, the
* signature and the leaf hash, so no script is needed:
*
* ```
* psbt.updateInput(inputIndex, {
* tapScriptSig: [{
* pubkey: toXOnly(keyPair.publicKey),
* signature,
* leafHash: scriptHash,
* }],
* });
* ```
*
* WARNING: this is a blind signature. Nothing here can check that your pubkey
* appears in the leaf, that the leaf belongs to the taptree being spent, or
* what the script authorises. Every guard the normal `signInput` path relies
* on comes from reading the script. Only use this when the leaf is
* authenticated by some other means.
*
* @param inputIndex the position of the PSBT input.
* @param keyPair the Schnorr signer.
* @param scriptHash the BIP341 tapleaf hash of the leaf being spent.
* @param sighashTypes whitelist of allowed sighash types.
* @returns the taproot signature, with the sighash byte appended unless the
* sighash type is SIGHASH_DEFAULT.
*/
generateTaprootScriptHashSignature(
inputIndex,
keyPair,
scriptHash,
sighashTypes = [transaction_js_1.Transaction.SIGHASH_DEFAULT],
) {
if (!keyPair || !keyPair.publicKey)
throw new Error('Need Signer to sign input');
if (typeof keyPair.signSchnorr !== 'function')
throw new Error(
`Need Schnorr Signer to sign taproot input #${inputIndex}.`,
);
if (!(scriptHash instanceof Uint8Array) || scriptHash.length !== 32)
throw new Error(
`Need a 32 byte tapleaf hash to sign input #${inputIndex}.`,
);
const input = (0, bip174_2.checkForInput)(this.data.inputs, inputIndex);
const sighashType =
input.sighashType || transaction_js_1.Transaction.SIGHASH_DEFAULT;
checkSighashTypeAllowed(sighashType, sighashTypes);
// BIP341 commits to every prevout, not just this one.
const prevOuts = this.data.inputs.map((i, index) =>
getScriptAndAmountFromUtxo(index, i, this.__CACHE),
);
if (!(0, psbtutils_js_1.isP2TR)(prevOuts[inputIndex].script))
throw new Error(`Input #${inputIndex} is not of type Taproot.`);
const hash = this.__CACHE.__TX.hashForWitnessV1(
inputIndex,
prevOuts.map(o => o.script),
prevOuts.map(o => o.value),
sighashType,
scriptHash,
);
return (0, bip371_js_1.serializeTaprootSignature)(
keyPair.signSchnorr(hash),
input.sighashType,
);
}
_signInput(
inputIndex,
keyPair,
Expand Down Expand Up @@ -976,6 +1055,48 @@ class Psbt {
}
}
exports.Psbt = Psbt;
/**
* Generates a taproot script-path signature for a leaf whose script is not
* known, given only that leaf's BIP341 tapleaf hash. The signature is returned;
* the PSBT is not modified.
*
* The whole `psbt` is required, not just the input being signed: the BIP341
* sighash commits to every prevout, and - depending on the sighash type - to the
* outputs as well. So the PSBT must already hold its inputs and outputs.
*
* `scriptHash` is the tapleaf hash, i.e.
* `taggedHash('TapLeaf', leafVersion || compactSize(script) || script)`. The
* leaf version is already committed to inside it, and the control block plays no
* part in the signature, so neither is passed here.
*
* WARNING: this is a blind signature. Nothing here can check that your pubkey
* appears in the leaf, that the leaf belongs to the taptree being spent, or what
* the script authorises. Every guard the normal signing path relies on comes
* from reading the script. Only use this when the leaf is authenticated by some
* other means.
*
* @param psbt the PSBT, with its inputs and outputs already added.
* @param inputIndex the position of the input to sign.
* @param keyPair the Schnorr signer.
* @param scriptHash the BIP341 tapleaf hash of the leaf being spent.
* @param sighashTypes whitelist of allowed sighash types.
* @returns the taproot signature. The PSBT is not modified; attach it yourself
* with `updateInput(inputIndex, { tapScriptSig: [...] })` if you want it there.
*/
function generateTaprootScriptHashSignature(
psbt,
inputIndex,
keyPair,
scriptHash,
sighashTypes,
) {
return psbt.generateTaprootScriptHashSignature(
inputIndex,
keyPair,
scriptHash,
sighashTypes,
);
}
/**
* This function is needed to pass to the bip174 base class's fromBuffer.
* It takes the "transaction buffer" portion of the psbt buffer and returns a
Expand Down
70 changes: 70 additions & 0 deletions src/cjs/psbt.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,47 @@ export declare class Psbt {
signAllInputsAsync(keyPair: Signer | SignerAsync, sighashTypes?: number[]): Promise<void>;
signInput(inputIndex: number, keyPair: Signer, sighashTypes?: number[]): this;
signTaprootInput(inputIndex: number, keyPair: Signer, tapLeafHashToSign?: Uint8Array, sighashTypes?: number[]): this;
/**
* Generates a taproot script-path signature for a leaf whose script is not
* known, given only that leaf's BIP341 tapleaf hash. The signature is
* returned; nothing is written to this PSBT.
*
* `scriptHash` is the tapleaf hash, i.e.
* `taggedHash('TapLeaf', leafVersion || compactSize(script) || script)` - the
* exact value `tapleafHash({ output: script, version: leafVersion })` returns,
* and the value the BIP341 script-path sighash commits to. It is NOT a plain
* hash of the script bytes. The leaf version is already committed to inside
* it, and the control block plays no part in the signature, so neither is
* needed here.
*
* The signature is returned rather than written to the PSBT. To attach it,
* put it in a `tapScriptSig` - a field that carries only the pubkey, the
* signature and the leaf hash, so no script is needed:
*
* ```
* psbt.updateInput(inputIndex, {
* tapScriptSig: [{
* pubkey: toXOnly(keyPair.publicKey),
* signature,
* leafHash: scriptHash,
* }],
* });
* ```
*
* WARNING: this is a blind signature. Nothing here can check that your pubkey
* appears in the leaf, that the leaf belongs to the taptree being spent, or
* what the script authorises. Every guard the normal `signInput` path relies
* on comes from reading the script. Only use this when the leaf is
* authenticated by some other means.
*
* @param inputIndex the position of the PSBT input.
* @param keyPair the Schnorr signer.
* @param scriptHash the BIP341 tapleaf hash of the leaf being spent.
* @param sighashTypes whitelist of allowed sighash types.
* @returns the taproot signature, with the sighash byte appended unless the
* sighash type is SIGHASH_DEFAULT.
*/
generateTaprootScriptHashSignature(inputIndex: number, keyPair: Signer, scriptHash: Uint8Array, sighashTypes?: number[]): Uint8Array;
private _signInput;
private _signTaprootInput;
signInputAsync(inputIndex: number, keyPair: Signer | SignerAsync, sighashTypes?: number[]): Promise<void>;
Expand All @@ -125,6 +166,35 @@ export declare class Psbt {
addUnknownKeyValToOutput(outputIndex: number, keyVal: KeyValue): this;
clearFinalizedInput(inputIndex: number): this;
}
/**
* Generates a taproot script-path signature for a leaf whose script is not
* known, given only that leaf's BIP341 tapleaf hash. The signature is returned;
* the PSBT is not modified.
*
* The whole `psbt` is required, not just the input being signed: the BIP341
* sighash commits to every prevout, and - depending on the sighash type - to the
* outputs as well. So the PSBT must already hold its inputs and outputs.
*
* `scriptHash` is the tapleaf hash, i.e.
* `taggedHash('TapLeaf', leafVersion || compactSize(script) || script)`. The
* leaf version is already committed to inside it, and the control block plays no
* part in the signature, so neither is passed here.
*
* WARNING: this is a blind signature. Nothing here can check that your pubkey
* appears in the leaf, that the leaf belongs to the taptree being spent, or what
* the script authorises. Every guard the normal signing path relies on comes
* from reading the script. Only use this when the leaf is authenticated by some
* other means.
*
* @param psbt the PSBT, with its inputs and outputs already added.
* @param inputIndex the position of the input to sign.
* @param keyPair the Schnorr signer.
* @param scriptHash the BIP341 tapleaf hash of the leaf being spent.
* @param sighashTypes whitelist of allowed sighash types.
* @returns the taproot signature. The PSBT is not modified; attach it yourself
* with `updateInput(inputIndex, { tapScriptSig: [...] })` if you want it there.
*/
export declare function generateTaprootScriptHashSignature(psbt: Psbt, inputIndex: number, keyPair: Signer, scriptHash: Uint8Array, sighashTypes?: number[]): Uint8Array;
interface PsbtOptsOptional {
network?: Network;
maximumFeeRate?: number;
Expand Down
2 changes: 1 addition & 1 deletion src/esm/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as payments from './payments/index.js';
import * as script from './script.js';
export { address, crypto, networks, payments, script };
export { Block } from './block.js';
export { Psbt, toXOnly } from './psbt.js';
export { Psbt, toXOnly, generateTaprootScriptHashSignature } from './psbt.js';
/** @hidden */
export { OPS as opcodes } from './ops.js';
export { Transaction } from './transaction.js';
Expand Down
Loading