Skip to content

Commit ac411e2

Browse files
authored
Merge pull request bitcoinjs#1747 from bitcoinjs/tests/taproot
Add taproot test with new CJS compatible tiny-secp256k1
2 parents 424abf2 + 4674433 commit ac411e2

File tree

7 files changed

+167
-20
lines changed

7 files changed

+167
-20
lines changed

.github/workflows/main_ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
runs-on: ubuntu-latest
4242
services:
4343
regtest:
44-
image: junderw/bitcoinjs-regtest-server@sha256:a46ec1a651ca5b1a5408f2b2526ea5f435421dd2bc2f28fae3bc33e1fd614552
44+
image: junderw/bitcoinjs-regtest-server@sha256:5b69cf95d9edf6d5b3a00504665d6b3c382a6aa3728fe8ce897974c519061463
4545
ports:
4646
- 8080:8080
4747
steps:

package-lock.json

Lines changed: 31 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
"@types/proxyquire": "^1.3.28",
6767
"@types/randombytes": "^2.0.0",
6868
"@types/wif": "^2.0.2",
69-
"bip32": "^2.0.6",
69+
"bip32": "^3.0.1",
7070
"bip39": "^3.0.2",
7171
"bip65": "^1.0.1",
7272
"bip68": "^1.0.3",
@@ -84,6 +84,7 @@
8484
"randombytes": "^2.1.0",
8585
"regtest-client": "0.2.0",
8686
"rimraf": "^2.6.3",
87+
"tiny-secp256k1": "^2.1.1",
8788
"ts-node": "^8.3.0",
8889
"tslint": "^6.1.3",
8990
"typescript": "^4.4.4"

test/integration/bip32.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import * as assert from 'assert';
2-
import * as bip32 from 'bip32';
2+
import BIP32Factory from 'bip32';
3+
import * as ecc from 'tiny-secp256k1';
34
import * as bip39 from 'bip39';
45
import { describe, it } from 'mocha';
56
import * as bitcoin from '../..';
67

8+
const bip32 = BIP32Factory(ecc);
9+
710
function getAddress(node: any, network?: any): string {
811
return bitcoin.payments.p2pkh({ pubkey: node.publicKey, network }).address!;
912
}

test/integration/taproot.spec.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import BIP32Factory from 'bip32';
2+
import * as ecc from 'tiny-secp256k1';
3+
import { describe, it } from 'mocha';
4+
import * as bitcoin from '../..';
5+
import { regtestUtils } from './_regtest';
6+
const rng = require('randombytes');
7+
const regtest = regtestUtils.network;
8+
const bip32 = BIP32Factory(ecc);
9+
10+
describe('bitcoinjs-lib (transaction with taproot)', () => {
11+
it('can create (and broadcast via 3PBP) a taproot keyspend Transaction', async () => {
12+
const myKey = bip32.fromSeed(rng(64), regtest);
13+
14+
const output = createKeySpendOutput(myKey.publicKey);
15+
const address = bitcoin.address.fromOutputScript(output, regtest);
16+
// amount from faucet
17+
const amount = 42e4;
18+
// amount to send
19+
const sendAmount = amount - 1e4;
20+
// get faucet
21+
const unspent = await regtestUtils.faucetComplex(output, amount);
22+
23+
const tx = createSigned(
24+
myKey,
25+
unspent.txId,
26+
unspent.vout,
27+
sendAmount,
28+
[output],
29+
[amount],
30+
);
31+
32+
const hex = tx.toHex();
33+
// console.log('Valid tx sent from:');
34+
// console.log(address);
35+
// console.log('tx hex:');
36+
// console.log(hex);
37+
await regtestUtils.broadcast(hex);
38+
await regtestUtils.verify({
39+
txId: tx.getId(),
40+
address,
41+
vout: 0,
42+
value: sendAmount,
43+
});
44+
});
45+
});
46+
47+
// Order of the curve (N) - 1
48+
const N_LESS_1 = Buffer.from(
49+
'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140',
50+
'hex',
51+
);
52+
// 1 represented as 32 bytes BE
53+
const ONE = Buffer.from(
54+
'0000000000000000000000000000000000000000000000000000000000000001',
55+
'hex',
56+
);
57+
58+
// Function for creating a tweaked p2tr key-spend only address
59+
// (This is recommended by BIP341)
60+
function createKeySpendOutput(publicKey: Buffer): Buffer {
61+
// x-only pubkey (remove 1 byte y parity)
62+
const myXOnlyPubkey = publicKey.slice(1, 33);
63+
const commitHash = bitcoin.crypto.taggedHash('TapTweak', myXOnlyPubkey);
64+
const tweakResult = ecc.xOnlyPointAddTweak(myXOnlyPubkey, commitHash);
65+
if (tweakResult === null) throw new Error('Invalid Tweak');
66+
const { xOnlyPubkey: tweaked } = tweakResult;
67+
// scriptPubkey
68+
return Buffer.concat([
69+
// witness v1, PUSH_DATA 32 bytes
70+
Buffer.from([0x51, 0x20]),
71+
// x-only tweaked pubkey
72+
tweaked,
73+
]);
74+
}
75+
76+
// Function for signing for a tweaked p2tr key-spend only address
77+
// (Required for the above address)
78+
interface KeyPair {
79+
publicKey: Buffer;
80+
privateKey?: Buffer;
81+
}
82+
function signTweaked(messageHash: Buffer, key: KeyPair): Uint8Array {
83+
const privateKey =
84+
key.publicKey[0] === 2
85+
? key.privateKey
86+
: ecc.privateAdd(ecc.privateSub(N_LESS_1, key.privateKey!)!, ONE)!;
87+
const tweakHash = bitcoin.crypto.taggedHash(
88+
'TapTweak',
89+
key.publicKey.slice(1, 33),
90+
);
91+
const newPrivateKey = ecc.privateAdd(privateKey!, tweakHash);
92+
if (newPrivateKey === null) throw new Error('Invalid Tweak');
93+
return ecc.signSchnorr(messageHash, newPrivateKey, Buffer.alloc(32));
94+
}
95+
96+
// Function for creating signed tx
97+
function createSigned(
98+
key: KeyPair,
99+
txid: string,
100+
vout: number,
101+
amountToSend: number,
102+
scriptPubkeys: Buffer[],
103+
values: number[],
104+
): bitcoin.Transaction {
105+
const tx = new bitcoin.Transaction();
106+
tx.version = 2;
107+
// Add input
108+
tx.addInput(Buffer.from(txid, 'hex').reverse(), vout);
109+
// Add output
110+
tx.addOutput(scriptPubkeys[0], amountToSend);
111+
const sighash = tx.hashForWitnessV1(
112+
0, // which input
113+
scriptPubkeys, // All previous outputs of all inputs
114+
values, // All previous values of all inputs
115+
bitcoin.Transaction.SIGHASH_DEFAULT, // sighash flag, DEFAULT is schnorr-only (DEFAULT == ALL)
116+
);
117+
const signature = Buffer.from(signTweaked(sighash, key));
118+
// witness stack for keypath spend is just the signature.
119+
// If sighash is not SIGHASH_DEFAULT (ALL) then you must add 1 byte with sighash value
120+
tx.ins[0].witness = [signature];
121+
return tx;
122+
}

test/integration/transactions.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import * as assert from 'assert';
2-
import * as bip32 from 'bip32';
2+
import BIP32Factory from 'bip32';
3+
import * as ecc from 'tiny-secp256k1';
34
import { ECPair } from 'ecpair';
45
import { describe, it } from 'mocha';
56
import * as bitcoin from '../..';
67
import { regtestUtils } from './_regtest';
78
const rng = require('randombytes');
89
const regtest = regtestUtils.network;
10+
const bip32 = BIP32Factory(ecc);
911

1012
const validator = (
1113
pubkey: Buffer,

test/psbt.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import * as assert from 'assert';
2-
import * as bip32 from 'bip32';
2+
import BIP32Factory from 'bip32';
3+
import * as ecc from 'tiny-secp256k1';
34
import * as crypto from 'crypto';
45
import { ECPair } from 'ecpair';
56
import { describe, it } from 'mocha';
67

8+
const bip32 = BIP32Factory(ecc);
9+
710
import { networks as NETWORKS, payments, Psbt, Signer, SignerAsync } from '..';
811

912
import * as preFixtures from './fixtures/psbt.json';

0 commit comments

Comments
 (0)