Skip to content

Commit 68361a1

Browse files
committed
WIP: p2tr_ns
1 parent bbf5cda commit 68361a1

20 files changed

+699
-165
lines changed

src/payments/index.d.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { p2sh } from './p2sh';
99
import { p2wpkh } from './p2wpkh';
1010
import { p2wsh } from './p2wsh';
1111
import { p2tr } from './p2tr';
12+
import { p2tr_ns } from './p2tr_ns';
1213
export interface Payment {
1314
name?: string;
1415
network?: Network;
@@ -38,4 +39,4 @@ export interface PaymentOpts {
3839
export declare type StackElement = Buffer | number;
3940
export declare type Stack = StackElement[];
4041
export declare type StackFunction = () => Stack;
41-
export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh, p2tr };
42+
export { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh, p2tr, p2tr_ns };

src/payments/index.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22
Object.defineProperty(exports, '__esModule', { value: true });
3-
exports.p2tr = exports.p2wsh = exports.p2wpkh = exports.p2sh = exports.p2pkh = exports.p2pk = exports.p2ms = exports.embed = void 0;
3+
exports.p2tr_ns = exports.p2tr = exports.p2wsh = exports.p2wpkh = exports.p2sh = exports.p2pkh = exports.p2pk = exports.p2ms = exports.embed = void 0;
44
const embed_1 = require('./embed');
55
Object.defineProperty(exports, 'embed', {
66
enumerable: true,
@@ -57,5 +57,12 @@ Object.defineProperty(exports, 'p2tr', {
5757
return p2tr_1.p2tr;
5858
},
5959
});
60+
const p2tr_ns_1 = require('./p2tr_ns');
61+
Object.defineProperty(exports, 'p2tr_ns', {
62+
enumerable: true,
63+
get: function() {
64+
return p2tr_ns_1.p2tr_ns;
65+
},
66+
});
6067
// TODO
6168
// witness commitment

src/payments/p2tr.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ function p2tr(a, opts) {
9191
const script = w[w.length - 2];
9292
const leafHash = (0, taprootutils_1.tapleafHash)({
9393
output: script,
94-
version: leafVersion,
94+
redeemVersion: leafVersion,
9595
});
9696
return (0, taprootutils_1.rootHashFromPath)(controlBlock, leafHash);
9797
}
@@ -148,7 +148,7 @@ function p2tr(a, opts) {
148148
if (hashTree && a.redeem && a.redeem.output && a.internalPubkey) {
149149
const leafHash = (0, taprootutils_1.tapleafHash)({
150150
output: a.redeem.output,
151-
version: o.redeemVersion,
151+
redeemVersion: o.redeemVersion,
152152
});
153153
const path = (0, taprootutils_1.findScriptPath)(hashTree, leafHash);
154154
if (!path) return;
@@ -209,7 +209,7 @@ function p2tr(a, opts) {
209209
if (a.redeem && a.redeem.output && hashTree) {
210210
const leafHash = (0, taprootutils_1.tapleafHash)({
211211
output: a.redeem.output,
212-
version: o.redeemVersion,
212+
redeemVersion: o.redeemVersion,
213213
});
214214
if (!(0, taprootutils_1.findScriptPath)(hashTree, leafHash))
215215
throw new TypeError('Redeem script not in tree');
@@ -268,7 +268,7 @@ function p2tr(a, opts) {
268268
const script = witness[witness.length - 2];
269269
const leafHash = (0, taprootutils_1.tapleafHash)({
270270
output: script,
271-
version: leafVersion,
271+
redeemVersion: leafVersion,
272272
});
273273
const hash = (0, taprootutils_1.rootHashFromPath)(
274274
controlBlock,

src/payments/p2tr_ns.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { Payment, PaymentOpts } from './index';
2+
export declare function p2tr_ns(a: Payment, opts?: PaymentOpts): Payment;

src/payments/p2tr_ns.js

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
'use strict';
2+
Object.defineProperty(exports, '__esModule', { value: true });
3+
exports.p2tr_ns = void 0;
4+
const ecc_lib_1 = require('../ecc_lib');
5+
const networks_1 = require('../networks');
6+
const bscript = require('../script');
7+
const lazy = require('./lazy');
8+
const OPS = bscript.OPS;
9+
const typef = require('typeforce');
10+
function stacksEqual(a, b) {
11+
if (a.length !== b.length) return false;
12+
return a.every((x, i) => {
13+
return x.equals(b[i]);
14+
});
15+
}
16+
// input: [signatures ...]
17+
// output: [pubKeys[0:n-1] OP_CHECKSIGVERIFY] pubKeys[n-1] OP_CHECKSIG
18+
function p2tr_ns(a, opts) {
19+
if (
20+
!a.input &&
21+
!a.output &&
22+
!(a.pubkeys && a.pubkeys.length) &&
23+
!a.signatures
24+
)
25+
throw new TypeError('Not enough data');
26+
opts = Object.assign({ validate: true }, opts || {});
27+
function isAcceptableSignature(x) {
28+
if (Buffer.isBuffer(x))
29+
return (
30+
// empty signatures may be represented as empty buffers
31+
(opts && opts.allowIncomplete && x.length === 0) ||
32+
bscript.isCanonicalSchnorrSignature(x)
33+
);
34+
return !!(opts && opts.allowIncomplete && x === OPS.OP_0);
35+
}
36+
typef(
37+
{
38+
network: typef.maybe(typef.Object),
39+
output: typef.maybe(typef.Buffer),
40+
pubkeys: typef.maybe(
41+
typef.arrayOf((0, ecc_lib_1.getEccLib)().isXOnlyPoint),
42+
),
43+
signatures: typef.maybe(typef.arrayOf(isAcceptableSignature)),
44+
input: typef.maybe(typef.Buffer),
45+
},
46+
a,
47+
);
48+
const network = a.network || networks_1.bitcoin;
49+
const o = { network };
50+
const _chunks = lazy.value(() => {
51+
if (!a.output) return;
52+
return bscript.decompile(a.output);
53+
});
54+
lazy.prop(o, 'output', () => {
55+
if (!a.pubkeys) return;
56+
return bscript.compile(
57+
[].concat(
58+
...a.pubkeys.map((pk, i, pks) => [
59+
pk,
60+
i === pks.length - 1 ? OPS.OP_CHECKSIG : OPS.OP_CHECKSIGVERIFY,
61+
]),
62+
),
63+
);
64+
});
65+
lazy.prop(o, 'n', () => {
66+
if (!o.pubkeys) return;
67+
return o.pubkeys.length;
68+
});
69+
lazy.prop(o, 'pubkeys', () => {
70+
const chunks = _chunks();
71+
if (!chunks) return;
72+
return chunks.filter((_, index) => index % 2 === 0);
73+
});
74+
lazy.prop(o, 'signatures', () => {
75+
if (!a.input) return;
76+
return bscript.decompile(a.input).reverse();
77+
});
78+
lazy.prop(o, 'input', () => {
79+
if (!a.signatures) return;
80+
return bscript.compile([...a.signatures].reverse());
81+
});
82+
lazy.prop(o, 'witness', () => {
83+
if (!o.input) return;
84+
return [];
85+
});
86+
lazy.prop(o, 'name', () => {
87+
if (!o.n) return;
88+
return `p2tr_ns(${o.n})`;
89+
});
90+
// extended validation
91+
if (opts.validate) {
92+
const chunks = _chunks();
93+
if (chunks) {
94+
if (chunks[chunks.length - 1] !== OPS.OP_CHECKSIG)
95+
throw new TypeError('Output ends with unexpected opcode');
96+
if (
97+
chunks
98+
.filter((_, index) => index % 2 === 1)
99+
.slice(0, -1)
100+
.some(op => op !== OPS.OP_CHECKSIGVERIFY)
101+
)
102+
throw new TypeError('Output contains unexpected opcode');
103+
if (o.n > 16 || o.n !== chunks.length / 2)
104+
throw new TypeError('Output contains too many pubkeys');
105+
if (o.pubkeys.some(x => !(0, ecc_lib_1.getEccLib)().isXOnlyPoint(x)))
106+
throw new TypeError('Output contains invalid pubkey(s)');
107+
if (a.pubkeys && !stacksEqual(a.pubkeys, o.pubkeys))
108+
throw new TypeError('Pubkeys mismatch');
109+
}
110+
if (a.pubkeys && a.pubkeys.length) {
111+
o.n = a.pubkeys.length;
112+
}
113+
if (a.signatures) {
114+
if (a.signatures.length < o.n)
115+
throw new TypeError('Not enough signatures provided');
116+
if (a.signatures.length > o.n)
117+
throw new TypeError('Too many signatures provided');
118+
}
119+
if (a.input) {
120+
if (!o.signatures.every(isAcceptableSignature))
121+
throw new TypeError('Input has invalid signature(s)');
122+
if (a.signatures && !stacksEqual(a.signatures, o.signatures))
123+
throw new TypeError('Signature mismatch');
124+
if (o.n !== o.signatures.length)
125+
throw new TypeError(
126+
`Signature count mismatch (n: ${o.n}, signatures.length: ${
127+
o.signatures.length
128+
}`,
129+
);
130+
}
131+
}
132+
return Object.assign(o, a);
133+
}
134+
exports.p2tr_ns = p2tr_ns;

src/payments/taprootutils.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ function findScriptPath(node, hash) {
5959
}
6060
exports.findScriptPath = findScriptPath;
6161
function tapleafHash(leaf) {
62-
const version = leaf.version || exports.LEAF_VERSION_TAPSCRIPT;
62+
const version = leaf.redeemVersion || exports.LEAF_VERSION_TAPSCRIPT;
6363
return bcrypto.taggedHash(
6464
'TapLeaf',
6565
buffer_1.Buffer.concat([

src/script.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ export declare function toStack(chunks: Buffer | Array<number | Buffer>): Buffer
1313
export declare function isCanonicalPubKey(buffer: Buffer): boolean;
1414
export declare function isDefinedHashType(hashType: number): boolean;
1515
export declare function isCanonicalScriptSignature(buffer: Buffer): boolean;
16+
export declare function isCanonicalSchnorrSignature(buffer: Buffer): boolean;
1617
export declare const number: typeof scriptNumber;
1718
export declare const signature: typeof scriptSignature;

src/script.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22
Object.defineProperty(exports, '__esModule', { value: true });
3-
exports.signature = exports.number = exports.isCanonicalScriptSignature = exports.isDefinedHashType = exports.isCanonicalPubKey = exports.toStack = exports.fromASM = exports.toASM = exports.decompile = exports.compile = exports.isPushOnly = exports.OPS = void 0;
3+
exports.signature = exports.number = exports.isCanonicalSchnorrSignature = exports.isCanonicalScriptSignature = exports.isDefinedHashType = exports.isCanonicalPubKey = exports.toStack = exports.fromASM = exports.toASM = exports.decompile = exports.compile = exports.isPushOnly = exports.OPS = void 0;
44
const bip66 = require('./bip66');
55
const ops_1 = require('./ops');
66
Object.defineProperty(exports, 'OPS', {
@@ -177,6 +177,13 @@ function isCanonicalScriptSignature(buffer) {
177177
return bip66.check(buffer.slice(0, -1));
178178
}
179179
exports.isCanonicalScriptSignature = isCanonicalScriptSignature;
180+
function isCanonicalSchnorrSignature(buffer) {
181+
if (!Buffer.isBuffer(buffer)) return false;
182+
if (buffer.length === 64) return true; // implied SIGHASH_DEFAULT
183+
if (buffer.length === 65 && isDefinedHashType(buffer[64])) return true; // explicit SIGHASH trailing byte
184+
return false;
185+
}
186+
exports.isCanonicalSchnorrSignature = isCanonicalSchnorrSignature;
180187
// tslint:disable-next-line variable-name
181188
exports.number = scriptNumber;
182189
exports.signature = scriptSignature;

src/types.d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export interface XOnlyPointAddTweakResult {
1616
}
1717
export interface Tapleaf {
1818
output: Buffer;
19-
version?: number;
19+
redeemVersion?: number;
2020
}
2121
export declare const TAPLEAF_VERSION_MASK = 254;
2222
export declare function isTapleaf(o: any): o is Tapleaf;
@@ -25,7 +25,7 @@ export declare function isTapleaf(o: any): o is Tapleaf;
2525
* Each node is either a single Tapleaf, or a pair of Tapleaf | Taptree.
2626
* The tree has no balancing requirements.
2727
*/
28-
export declare type Taptree = [Taptree | Tapleaf, Taptree | Tapleaf] | Tapleaf;
28+
export declare type Taptree = [Taptree, Taptree] | Tapleaf;
2929
export declare function isTaptree(scriptTree: any): scriptTree is Taptree;
3030
export interface TinySecp256k1Interface {
3131
isXOnlyPoint(p: Uint8Array): boolean;

src/types.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ exports.TAPLEAF_VERSION_MASK = 0xfe;
7272
function isTapleaf(o) {
7373
if (!('output' in o)) return false;
7474
if (!buffer_1.Buffer.isBuffer(o.output)) return false;
75-
if (o.version !== undefined)
76-
return (o.version & exports.TAPLEAF_VERSION_MASK) === o.version;
75+
if (o.redeemVersion !== undefined)
76+
return (o.redeemVersion & exports.TAPLEAF_VERSION_MASK) === o.redeemVersion;
7777
return true;
7878
}
7979
exports.isTapleaf = isTapleaf;

0 commit comments

Comments
 (0)