Skip to content

Commit af639f9

Browse files
committed
feat: validate witness data (partial)
1 parent fb7df4a commit af639f9

File tree

9 files changed

+396
-44
lines changed

9 files changed

+396
-44
lines changed

src/merkle.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const buffer_1 = require('buffer');
55
const bcrypto = require('./crypto');
66
// todo: use varuint-bitcoin??
77
const varuint = require('bip174/src/lib/converter/varint');
8+
// todo: find better place for these consts
89
const TAP_LEAF_TAG = buffer_1.Buffer.from('TapLeaf', 'utf8');
910
const TAP_BRANCH_TAG = buffer_1.Buffer.from('TapBranch', 'utf8');
1011
const LEAF_VERSION_TAPSCRIPT = 0xc0;

src/payments/p2tr.js

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,19 @@ const lazy = require('./lazy');
99
const bech32_1 = require('bech32');
1010
const OPS = bscript.OPS;
1111
const TAPROOT_VERSION = 0x01;
12+
const ANNEX_PREFIX = 0x50;
1213
// witness: {signature}
1314
// input: <>
1415
// output: OP_1 {pubKey}
1516
function p2tr(a, opts) {
16-
if (!a.address && !a.output && !a.pubkey && !a.output && !a.internalPubkey)
17+
if (
18+
!a.address &&
19+
!a.output &&
20+
!a.pubkey &&
21+
!a.output &&
22+
!a.internalPubkey &&
23+
!(a.witness && a.witness.length > 1)
24+
)
1725
throw new TypeError('Not enough data');
1826
opts = Object.assign({ validate: true }, opts || {});
1927
(0, types_1.typeforce)(
@@ -43,7 +51,17 @@ function p2tr(a, opts) {
4351
data: Buffer.from(data),
4452
};
4553
});
46-
// todo: clean-up withness (annex), etc
54+
const _witness = lazy.value(() => {
55+
if (!a.witness || !a.witness.length) return;
56+
if (
57+
a.witness.length >= 2 &&
58+
a.witness[a.witness.length - 1][0] === ANNEX_PREFIX
59+
) {
60+
// remove annex, ignored by taproot
61+
return a.witness.slice(0, -1);
62+
}
63+
return a.witness.slice();
64+
});
4765
const network = a.network || networks_1.bitcoin;
4866
const o = { name: 'p2tr', network };
4967
lazy.prop(o, 'address', () => {
@@ -55,6 +73,7 @@ function p2tr(a, opts) {
5573
lazy.prop(o, 'hash', () => {
5674
if (a.hash) return a.hash;
5775
if (a.scriptsTree) return (0, merkle_1.computeMastRoot)(a.scriptsTree);
76+
// todo: compute from witness
5877
return null;
5978
});
6079
lazy.prop(o, 'output', () => {
@@ -65,11 +84,17 @@ function p2tr(a, opts) {
6584
if (a.pubkey) return a.pubkey;
6685
if (a.output) return a.output.slice(2);
6786
if (a.address) return _address().data;
68-
if (a.internalPubkey) {
69-
const tweakedKey = (0, types_1.tweakPublicKey)(a.internalPubkey, o.hash);
87+
if (o.internalPubkey) {
88+
const tweakedKey = (0, types_1.tweakPublicKey)(o.internalPubkey, o.hash);
7089
if (tweakedKey) return tweakedKey.x;
7190
}
7291
});
92+
lazy.prop(o, 'internalPubkey', () => {
93+
if (a.internalPubkey) return a.internalPubkey;
94+
const witness = _witness();
95+
if (witness && witness.length > 1)
96+
return witness[witness.length - 1].slice(1, 33);
97+
});
7398
lazy.prop(o, 'signature', () => {
7499
if (a.witness?.length !== 1) return;
75100
return a.witness[0];
@@ -78,6 +103,7 @@ function p2tr(a, opts) {
78103
// todo: not sure
79104
});
80105
lazy.prop(o, 'witness', () => {
106+
if (a.witness) return a.witness;
81107
if (!a.signature) return;
82108
return [a.signature];
83109
});
@@ -109,7 +135,6 @@ function p2tr(a, opts) {
109135
throw new TypeError('Pubkey mismatch');
110136
else pubkey = a.output.slice(2);
111137
}
112-
// todo: optimze o.hash?
113138
if (a.internalPubkey) {
114139
const tweakedKey = (0, types_1.tweakPublicKey)(a.internalPubkey, o.hash);
115140
if (tweakedKey === null)
@@ -126,13 +151,59 @@ function p2tr(a, opts) {
126151
const hash = (0, merkle_1.computeMastRoot)(a.scriptsTree);
127152
if (!a.hash.equals(hash)) throw new TypeError('Hash mismatch');
128153
}
129-
if (a.witness) {
130-
if (a.witness.length !== 1) throw new TypeError('Witness is invalid');
131-
// todo: recheck
132-
// if (!bscript.isCanonicalScriptSignature(a.witness[0]))
133-
// throw new TypeError('Witness has invalid signature');
134-
if (a.signature && !a.signature.equals(a.witness[0]))
135-
throw new TypeError('Signature mismatch');
154+
// todo: review cache
155+
const witness = _witness();
156+
if (witness && witness.length) {
157+
if (witness.length === 1) {
158+
// key spending
159+
if (a.signature && !a.signature.equals(witness[0]))
160+
throw new TypeError('Signature mismatch');
161+
// todo: recheck
162+
// if (!bscript.isSchnorSignature(a.pubkey, a.witness[0]))
163+
// throw new TypeError('Witness has invalid signature');
164+
} else {
165+
// script path spending
166+
const controlBlock = witness[witness.length - 1];
167+
if (controlBlock.length < 33)
168+
throw new TypeError(
169+
`The control-block length is too small. Got ${
170+
controlBlock.length
171+
}, expected min 33.`,
172+
);
173+
if ((controlBlock.length - 33) % 32 !== 0)
174+
throw new TypeError(
175+
`The control-block length of ${controlBlock.length} is incorrect!`,
176+
);
177+
const m = (controlBlock.length - 33) / 32;
178+
if (m > 128)
179+
throw new TypeError(
180+
`The script path is too long. Got ${m}, expected max 128.`,
181+
);
182+
const internalPubkey = controlBlock.slice(1, 33);
183+
if (a.internalPubkey && !a.internalPubkey.equals(internalPubkey))
184+
throw new TypeError('Internal pubkey mismatch');
185+
const internalPubkeyPoint = (0, types_1.liftX)(internalPubkey);
186+
if (!internalPubkeyPoint)
187+
throw new TypeError('Invalid internalPubkey for p2tr witness');
188+
const leafVersion = controlBlock[0] & 0b11111110;
189+
const script = witness[witness.length - 2];
190+
const tweak = (0, types_1.computeTweakFromScriptPath)(
191+
controlBlock,
192+
script,
193+
internalPubkey,
194+
m,
195+
leafVersion,
196+
);
197+
const outputKey = (0, types_1.tweakPublicKey)(internalPubkey, tweak);
198+
if (!outputKey)
199+
// todo: needs test data
200+
throw new TypeError('Invalid outputKey for p2tr witness');
201+
if (pubkey.length && !pubkey.equals(outputKey.x))
202+
throw new TypeError('Pubkey mismatch for p2tr witness');
203+
const controlBlockOddParity = (controlBlock[0] & 1) === 1;
204+
if (outputKey.isOdd !== controlBlockOddParity)
205+
throw new Error('Incorrect parity');
206+
}
136207
}
137208
}
138209
return Object.assign(o, a);

src/types.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
/// <reference types="node" />
2+
import { Buffer as NBuffer } from 'buffer';
23
export declare const typeforce: any;
34
export declare function isPoint(p: Buffer | number | undefined | null): boolean;
45
export declare function liftX(buffer: Buffer): Buffer | null;
56
export declare function tweakPublicKey(pubKey: Buffer, h: Buffer | undefined): TweakedPublicKey | null;
7+
export declare function computeTweakFromScriptPath(controlBlock: Buffer, script: Buffer, internalPubkey: Buffer, m: number, v: number): NBuffer;
68
export declare function UInt31(value: number): boolean;
79
export declare function BIP32Path(value: string): boolean;
810
export declare namespace BIP32Path {

src/types.js

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
'use strict';
22
Object.defineProperty(exports, '__esModule', { value: true });
3-
exports.oneOf = exports.Null = exports.BufferN = exports.Function = exports.UInt32 = exports.UInt8 = exports.tuple = exports.maybe = exports.Hex = exports.Buffer = exports.String = exports.Boolean = exports.Array = exports.Number = exports.Hash256bit = exports.Hash160bit = exports.Buffer256bit = exports.TaprootNode = exports.TaprootLeaf = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.tweakPublicKey = exports.liftX = exports.isPoint = exports.typeforce = void 0;
3+
exports.oneOf = exports.Null = exports.BufferN = exports.Function = exports.UInt32 = exports.UInt8 = exports.tuple = exports.maybe = exports.Hex = exports.Buffer = exports.String = exports.Boolean = exports.Array = exports.Number = exports.Hash256bit = exports.Hash160bit = exports.Buffer256bit = exports.TaprootNode = exports.TaprootLeaf = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.computeTweakFromScriptPath = exports.tweakPublicKey = exports.liftX = exports.isPoint = exports.typeforce = void 0;
44
const buffer_1 = require('buffer');
55
const bcrypto = require('./crypto');
6+
const varuint = require('bip174/src/lib/converter/varint');
67
// Temp, to be replaced
78
// Only works because bip32 has it as dependecy. Linting will fail.
89
const ecc = require('tiny-secp256k1');
@@ -62,12 +63,12 @@ function liftX(buffer) {
6263
}
6364
exports.liftX = liftX;
6465
const TAP_TWEAK_TAG = buffer_1.Buffer.from('TapTweak', 'utf8');
65-
const GROUP_ORDER = new BN(
66-
buffer_1.Buffer.from(
67-
'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141',
68-
'hex',
69-
),
66+
const GROUP_ORDER = buffer_1.Buffer.from(
67+
'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141',
68+
'hex',
7069
);
70+
// todo: compare buffers dirrectly
71+
const GROUP_ORDER_BN = new BN(GROUP_ORDER);
7172
function tweakPublicKey(pubKey, h) {
7273
if (!buffer_1.Buffer.isBuffer(pubKey)) return null;
7374
if (pubKey.length !== 32) return null;
@@ -77,7 +78,8 @@ function tweakPublicKey(pubKey, h) {
7778
buffer_1.Buffer.concat(h ? [pubKey, h] : [pubKey]),
7879
);
7980
const t = new BN(tweakHash);
80-
if (t.gte(GROUP_ORDER)) {
81+
if (t.gte(GROUP_ORDER_BN)) {
82+
// todo: add test for this case
8183
throw new Error('Tweak value over the SECP256K1 Order');
8284
}
8385
const P = liftX(pubKey);
@@ -89,6 +91,53 @@ function tweakPublicKey(pubKey, h) {
8991
};
9092
}
9193
exports.tweakPublicKey = tweakPublicKey;
94+
const TAP_LEAF_TAG = buffer_1.Buffer.from('TapLeaf', 'utf8');
95+
const TAP_BRANCH_TAG = buffer_1.Buffer.from('TapBranch', 'utf8');
96+
function computeTweakFromScriptPath(
97+
controlBlock,
98+
script,
99+
internalPubkey,
100+
m,
101+
v,
102+
) {
103+
const k = [];
104+
const e = [];
105+
const tapLeafMsg = buffer_1.Buffer.concat([
106+
buffer_1.Buffer.from([v]),
107+
serializeScript(script),
108+
]);
109+
k[0] = bcrypto.taggedHash(TAP_LEAF_TAG, tapLeafMsg);
110+
for (let j = 0; j < m; j++) {
111+
e[j] = controlBlock.slice(33 + 32 * j, 65 + 32 * j);
112+
if (k[j].compare(e[j]) < 0) {
113+
k[j + 1] = bcrypto.taggedHash(
114+
TAP_BRANCH_TAG,
115+
buffer_1.Buffer.concat([k[j], e[j]]),
116+
);
117+
} else {
118+
k[j + 1] = bcrypto.taggedHash(
119+
TAP_BRANCH_TAG,
120+
buffer_1.Buffer.concat([e[j], k[j]]),
121+
);
122+
}
123+
}
124+
const t = bcrypto.taggedHash(
125+
TAP_TWEAK_TAG,
126+
buffer_1.Buffer.concat([internalPubkey, k[m]]),
127+
);
128+
if (t.compare(GROUP_ORDER) >= 0) {
129+
throw new Error('Over the order of secp256k1');
130+
}
131+
return t;
132+
}
133+
exports.computeTweakFromScriptPath = computeTweakFromScriptPath;
134+
// todo: move out
135+
function serializeScript(s) {
136+
const varintLen = varuint.encodingLength(s.length);
137+
const buffer = buffer_1.Buffer.allocUnsafe(varintLen); // better
138+
varuint.encode(s.length, buffer);
139+
return buffer_1.Buffer.concat([buffer, s]);
140+
}
92141
// todo: do not use ecc
93142
function pointAddScalar(P, h) {
94143
return ecc.pointAddScalar(P, h);

0 commit comments

Comments
 (0)