Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e768f5e
test: add PSBT example for taproot
motorina0 Mar 2, 2022
f125ee4
refactor: variable renaming
motorina0 Mar 2, 2022
2f33ce1
refactor: use constant for tapscript leaf version
motorina0 Mar 2, 2022
23daa11
feat: reuse redeem field for taproot spend
motorina0 Mar 2, 2022
0c4c747
test: make sure it defaults to tapscript version 192
motorina0 Mar 4, 2022
15d5ab2
chore: remove `scriptLeaf` logic
motorina0 Mar 4, 2022
c59f322
feat: add tapscript sign() a finalize() logic
motorina0 Mar 4, 2022
be4b6a9
test: spend taproot script-path
motorina0 Mar 4, 2022
a408697
chore: add forgotten file
motorina0 Mar 4, 2022
1ab0a59
chore: format code
motorina0 Mar 4, 2022
4cbb7a8
fix: typescript declaration
motorina0 Mar 4, 2022
fa8dd83
chore: format
motorina0 Mar 4, 2022
55342cd
choe: chore, chore, chore
motorina0 Mar 4, 2022
9772030
test: add tapscript for OP_CHECKSEQUENCEVERIFY
motorina0 Mar 4, 2022
c4e5bd1
feat: add multisig integration test
motorina0 Mar 8, 2022
aa5b0dd
refactor: rename scriptsTree to scriptTree (as per BP341)
motorina0 Mar 8, 2022
a58bdbf
feat: check that the scriptTree is a binary tree
motorina0 Mar 8, 2022
e4a83f3
feat: compute the redeem from witness; add unit tests
motorina0 Mar 9, 2022
cbf6a62
refactor: move TaprootLeaf back to types
motorina0 Mar 9, 2022
452961c
test: add test for invalid redeem script
motorina0 Mar 9, 2022
1d52cb8
feat: check the redeemVersion on the input data first
motorina0 Mar 9, 2022
cbde5fe
test: add tests for taproot script-path sign, and key-path finalize
motorina0 Mar 9, 2022
aa2bda8
fix: small fixes and unit-tests
motorina0 Mar 9, 2022
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
1 change: 1 addition & 0 deletions src/ops.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ const OPS = {
OP_NOP8: 183,
OP_NOP9: 184,
OP_NOP10: 185,
OP_CHECKSIGADD: 186,
OP_PUBKEYHASH: 253,
OP_PUBKEY: 254,
OP_INVALIDOPCODE: 255,
Expand Down
6 changes: 3 additions & 3 deletions src/payments/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="node" />
import { Network } from '../networks';
import { TaprootLeaf, TinySecp256k1Interface } from '../types';
import { TinySecp256k1Interface, TaprootLeaf } from '../types';
import { p2data as embed } from './embed';
import { p2ms } from './p2ms';
import { p2pk } from './p2pk';
Expand All @@ -25,8 +25,8 @@ export interface Payment {
address?: string;
hash?: Buffer;
redeem?: Payment;
scriptsTree?: any;
scriptLeaf?: TaprootLeaf;
redeemVersion?: number;
scriptTree?: TaprootLeaf[];
witness?: Buffer[];
}
export declare type PaymentCreator = (a: Payment, opts?: PaymentOpts) => Payment;
Expand Down
94 changes: 69 additions & 25 deletions src/payments/p2tr.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ const lazy = require('./lazy');
const bech32_1 = require('bech32');
const testecc_1 = require('./testecc');
const OPS = bscript.OPS;
const TAPROOT_VERSION = 0x01;
const TAPROOT_WITNESS_VERSION = 0x01;
const ANNEX_PREFIX = 0x50;
const LEAF_VERSION_MASK = 0b11111110;
function p2tr(a, opts) {
if (
!a.address &&
Expand Down Expand Up @@ -40,11 +41,15 @@ function p2tr(a, opts) {
witness: types_1.typeforce.maybe(
types_1.typeforce.arrayOf(types_1.typeforce.Buffer),
),
// scriptsTree: typef.maybe(typef.TaprootNode), // use merkel.isMast ?
scriptLeaf: types_1.typeforce.maybe({
version: types_1.typeforce.maybe(types_1.typeforce.Number),
scriptTree: types_1.typeforce.maybe(taprootutils_1.isTapTree),
redeem: types_1.typeforce.maybe({
output: types_1.typeforce.maybe(types_1.typeforce.Buffer),
redeemVersion: types_1.typeforce.maybe(types_1.typeforce.Number),
witness: types_1.typeforce.maybe(
types_1.typeforce.arrayOf(types_1.typeforce.Buffer),
),
}),
redeemVersion: types_1.typeforce.maybe(types_1.typeforce.Number),
},
a,
);
Expand All @@ -58,13 +63,13 @@ function p2tr(a, opts) {
data: buffer_1.Buffer.from(data),
};
});
// remove annex if present, ignored by taproot
const _witness = lazy.value(() => {
if (!a.witness || !a.witness.length) return;
if (
a.witness.length >= 2 &&
a.witness[a.witness.length - 1][0] === ANNEX_PREFIX
) {
// remove annex, ignored by taproot
return a.witness.slice(0, -1);
}
return a.witness.slice();
Expand All @@ -74,17 +79,16 @@ function p2tr(a, opts) {
lazy.prop(o, 'address', () => {
if (!o.pubkey) return;
const words = bech32_1.bech32m.toWords(o.pubkey);
words.unshift(TAPROOT_VERSION);
words.unshift(TAPROOT_WITNESS_VERSION);
return bech32_1.bech32m.encode(network.bech32, words);
});
lazy.prop(o, 'hash', () => {
if (a.hash) return a.hash;
if (a.scriptsTree)
return (0, taprootutils_1.toHashTree)(a.scriptsTree).hash;
if (a.scriptTree) return (0, taprootutils_1.toHashTree)(a.scriptTree).hash;
const w = _witness();
if (w && w.length > 1) {
const controlBlock = w[w.length - 1];
const leafVersion = controlBlock[0] & 0b11111110;
const leafVersion = controlBlock[0] & LEAF_VERSION_MASK;
const script = w[w.length - 2];
const leafHash = (0, taprootutils_1.tapLeafHash)(script, leafVersion);
return (0, taprootutils_1.rootHashFromPath)(controlBlock, leafHash);
Expand All @@ -95,8 +99,25 @@ function p2tr(a, opts) {
if (!o.pubkey) return;
return bscript.compile([OPS.OP_1, o.pubkey]);
});
lazy.prop(o, 'scriptLeaf', () => {
if (a.scriptLeaf) return a.scriptLeaf;
lazy.prop(o, 'redeemVersion', () => {
if (a.redeemVersion) return a.redeemVersion;
if (
a.redeem &&
a.redeem.redeemVersion !== undefined &&
a.redeem.redeemVersion !== null
) {
return a.redeem.redeemVersion;
}
return taprootutils_1.LEAF_VERSION_TAPSCRIPT;
});
lazy.prop(o, 'redeem', () => {
const witness = _witness(); // witness without annex
if (!witness || witness.length < 2) return;
return {
output: witness[witness.length - 2],
witness: witness.slice(0, -2),
redeemVersion: witness[witness.length - 1][0] & LEAF_VERSION_MASK,
};
});
lazy.prop(o, 'pubkey', () => {
if (a.pubkey) return a.pubkey;
Expand All @@ -118,29 +139,25 @@ function p2tr(a, opts) {
if (!a.witness || a.witness.length !== 1) return;
return a.witness[0];
});
lazy.prop(o, 'input', () => {
// todo
});
lazy.prop(o, 'witness', () => {
if (a.witness) return a.witness;
if (a.scriptsTree && a.scriptLeaf && a.internalPubkey) {
if (a.scriptTree && a.redeem && a.redeem.output && a.internalPubkey) {
// todo: optimize/cache
const hashTree = (0, taprootutils_1.toHashTree)(a.scriptsTree);
const hashTree = (0, taprootutils_1.toHashTree)(a.scriptTree);
const leafHash = (0, taprootutils_1.tapLeafHash)(
a.scriptLeaf.output,
a.scriptLeaf.version,
a.redeem.output,
o.redeemVersion,
);
const path = (0, taprootutils_1.findScriptPath)(hashTree, leafHash);
const outputKey = tweakKey(a.internalPubkey, hashTree.hash, _ecc());
if (!outputKey) return;
const version = a.scriptLeaf.version || 0xc0;
const controlBock = buffer_1.Buffer.concat(
[
buffer_1.Buffer.from([version | outputKey.parity]),
buffer_1.Buffer.from([o.redeemVersion | outputKey.parity]),
a.internalPubkey,
].concat(path.reverse()),
);
return [a.scriptLeaf.output, controlBock];
return [a.redeem.output, controlBock];
}
if (a.signature) return [a.signature];
});
Expand All @@ -150,7 +167,7 @@ function p2tr(a, opts) {
if (a.address) {
if (network && network.bech32 !== _address().prefix)
throw new TypeError('Invalid prefix or Network mismatch');
if (_address().version !== TAPROOT_VERSION)
if (_address().version !== TAPROOT_WITNESS_VERSION)
throw new TypeError('Invalid address version');
if (_address().data.length !== 32)
throw new TypeError('Invalid address data');
Expand Down Expand Up @@ -182,11 +199,32 @@ function p2tr(a, opts) {
if (!_ecc().isXOnlyPoint(pubkey))
throw new TypeError('Invalid pubkey for p2tr');
}
if (a.hash && a.scriptsTree) {
const hash = (0, taprootutils_1.toHashTree)(a.scriptsTree).hash;
if (a.hash && a.scriptTree) {
const hash = (0, taprootutils_1.toHashTree)(a.scriptTree).hash;
if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch');
}
const witness = _witness();
// compare the provided redeem data with the one computed from witness
if (a.redeem && o.redeem) {
if (a.redeem.redeemVersion) {
if (a.redeem.redeemVersion !== o.redeem.redeemVersion)
throw new TypeError('Redeem.redeemVersion and witness mismatch');
}
if (a.redeem.output) {
if (bscript.decompile(a.redeem.output).length === 0)
throw new TypeError('Redeem.output is invalid');
// output redeem is constructed from the witness
if (o.redeem.output && !a.redeem.output.equals(o.redeem.output))
throw new TypeError('Redeem.output and witness mismatch');
}
if (a.redeem.witness) {
if (
o.redeem.witness &&
!stacksEqual(a.redeem.witness, o.redeem.witness)
)
throw new TypeError('Redeem.witness and witness mismatch');
}
}
if (witness && witness.length) {
if (witness.length === 1) {
// key spending
Expand Down Expand Up @@ -215,7 +253,7 @@ function p2tr(a, opts) {
throw new TypeError('Internal pubkey mismatch');
if (!_ecc().isXOnlyPoint(internalPubkey))
throw new TypeError('Invalid internalPubkey for p2tr witness');
const leafVersion = controlBlock[0] & 0b11111110;
const leafVersion = controlBlock[0] & LEAF_VERSION_MASK;
const script = witness[witness.length - 2];
const leafHash = (0, taprootutils_1.tapLeafHash)(script, leafVersion);
const hash = (0, taprootutils_1.rootHashFromPath)(
Expand Down Expand Up @@ -248,3 +286,9 @@ function tweakKey(pubKey, h, eccLib) {
x: buffer_1.Buffer.from(res.xOnlyPubkey),
};
}
function stacksEqual(a, b) {
if (a.length !== b.length) return false;
return a.every((x, i) => {
return x.equals(b[i]);
});
}
9 changes: 7 additions & 2 deletions src/payments/taprootutils.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="node" />
import { TaprootLeaf } from '../types';
export declare const LEAF_VERSION_TAPSCRIPT = 192;
export declare function rootHashFromPath(controlBlock: Buffer, tapLeafMsg: Buffer): Buffer;
export interface HashTree {
hash: Buffer;
Expand All @@ -9,12 +10,16 @@ export interface HashTree {
/**
* Build the hash tree from the scripts binary tree.
* The binary tree can be balanced or not.
* @param scriptsTree - is a list representing a binary tree where an element can be:
* @param scriptTree - is a list representing a binary tree where an element can be:
* - a taproot leaf [(output, version)], or
* - a pair of two taproot leafs [(output, version), (output, version)], or
* - one taproot leaf and a list of elements
*/
export declare function toHashTree(scriptsTree: TaprootLeaf[]): HashTree;
export declare function toHashTree(scriptTree: TaprootLeaf[]): HashTree;
/**
* Check if the tree is a binary tree with leafs of type TaprootLeaf
*/
export declare function isTapTree(scriptTree: TaprootLeaf[]): boolean;
/**
* Given a MAST tree, it finds the path of a particular hash.
* @param node - the root of the tree
Expand Down
40 changes: 30 additions & 10 deletions src/payments/taprootutils.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
exports.tapTweakHash = exports.tapLeafHash = exports.findScriptPath = exports.toHashTree = exports.rootHashFromPath = void 0;
exports.tapTweakHash = exports.tapLeafHash = exports.findScriptPath = exports.isTapTree = exports.toHashTree = exports.rootHashFromPath = exports.LEAF_VERSION_TAPSCRIPT = void 0;
const buffer_1 = require('buffer');
const bcrypto = require('../crypto');
const bufferutils_1 = require('../bufferutils');
const LEAF_VERSION_TAPSCRIPT = 0xc0;
const TAP_LEAF_TAG = 'TapLeaf';
const TAP_BRANCH_TAG = 'TapBranch';
const TAP_TWEAK_TAG = 'TapTweak';
exports.LEAF_VERSION_TAPSCRIPT = 0xc0;
function rootHashFromPath(controlBlock, tapLeafMsg) {
const k = [tapLeafMsg];
const e = [];
Expand All @@ -26,26 +26,26 @@ exports.rootHashFromPath = rootHashFromPath;
/**
* Build the hash tree from the scripts binary tree.
* The binary tree can be balanced or not.
* @param scriptsTree - is a list representing a binary tree where an element can be:
* @param scriptTree - is a list representing a binary tree where an element can be:
* - a taproot leaf [(output, version)], or
* - a pair of two taproot leafs [(output, version), (output, version)], or
* - one taproot leaf and a list of elements
*/
function toHashTree(scriptsTree) {
if (scriptsTree.length === 1) {
const script = scriptsTree[0];
function toHashTree(scriptTree) {
if (scriptTree.length === 1) {
const script = scriptTree[0];
if (Array.isArray(script)) {
return toHashTree(script);
}
script.version = script.version || LEAF_VERSION_TAPSCRIPT;
script.version = script.version || exports.LEAF_VERSION_TAPSCRIPT;
if ((script.version & 1) !== 0)
throw new TypeError('Invalid script version');
return {
hash: tapLeafHash(script.output, script.version),
};
}
const left = toHashTree([scriptsTree[0]]);
const right = toHashTree([scriptsTree[1]]);
const left = toHashTree([scriptTree[0]]);
const right = toHashTree([scriptTree[1]]);
let leftHash = left.hash;
let rightHash = right.hash;
if (leftHash.compare(rightHash) === 1)
Expand All @@ -57,6 +57,26 @@ function toHashTree(scriptsTree) {
};
}
exports.toHashTree = toHashTree;
/**
* Check if the tree is a binary tree with leafs of type TaprootLeaf
*/
function isTapTree(scriptTree) {
if (scriptTree.length > 2) return false;
if (scriptTree.length === 1) {
const script = scriptTree[0];
if (Array.isArray(script)) {
return isTapTree(script);
}
if (!script.output) return false;
script.version = script.version || exports.LEAF_VERSION_TAPSCRIPT;
if ((script.version & 1) !== 0) return false;
return true;
}
if (!isTapTree([scriptTree[0]])) return false;
if (!isTapTree([scriptTree[1]])) return false;
return true;
}
exports.isTapTree = isTapTree;
/**
* Given a MAST tree, it finds the path of a particular hash.
* @param node - the root of the tree
Expand All @@ -80,7 +100,7 @@ function findScriptPath(node, hash) {
}
exports.findScriptPath = findScriptPath;
function tapLeafHash(script, version) {
version = version || LEAF_VERSION_TAPSCRIPT;
version = version || exports.LEAF_VERSION_TAPSCRIPT;
return bcrypto.taggedHash(
TAP_LEAF_TAG,
buffer_1.Buffer.concat([
Expand Down
7 changes: 4 additions & 3 deletions src/psbt.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,10 @@ input: PsbtInput, // The PSBT input contents
script: Buffer, // The "meaningful" locking script Buffer (redeemScript for P2SH etc.)
isSegwit: boolean, // Is it segwit?
isP2SH: boolean, // Is it P2SH?
isP2WSH: boolean) => {
isP2WSH: boolean, // Is it P2WSH?
eccLib?: TinySecp256k1Interface) => {
finalScriptSig: Buffer | undefined;
finalScriptWitness: Buffer | undefined;
finalScriptWitness: Buffer | Buffer[] | undefined;
};
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';
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' | 'p2tr-pubkey' | 'p2tr-nonstandard';
export {};
Loading