Skip to content

Commit 7989b6a

Browse files
authored
Merge pull request #2 from bitcoincoretech/p2tr-v1-addr-check
feat: add stricter validation for taproot addresses
2 parents 6f1e7ea + d3e9aa6 commit 7989b6a

File tree

6 files changed

+90
-26
lines changed

6 files changed

+90
-26
lines changed

src/address.d.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/// <reference types="node" />
22
import { Network } from './networks';
3+
import { TinySecp256k1Interface } from './types';
34
export interface Base58CheckResult {
45
hash: Buffer;
56
version: number;
@@ -13,5 +14,5 @@ export declare function fromBase58Check(address: string): Base58CheckResult;
1314
export declare function fromBech32(address: string): Bech32Result;
1415
export declare function toBase58Check(hash: Buffer, version: number): string;
1516
export declare function toBech32(data: Buffer, version: number, prefix: string): string;
16-
export declare function fromOutputScript(output: Buffer, network?: Network): string;
17-
export declare function toOutputScript(address: string, network?: Network): Buffer;
17+
export declare function fromOutputScript(output: Buffer, network?: Network, eccLib?: TinySecp256k1Interface): string;
18+
export declare function toOutputScript(address: string, network?: Network, eccLib?: TinySecp256k1Interface): Buffer;

src/address.js

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@ exports.toOutputScript = exports.fromOutputScript = exports.toBech32 = exports.t
44
const networks = require('./networks');
55
const payments = require('./payments');
66
const bscript = require('./script');
7-
const types = require('./types');
7+
const types_1 = require('./types');
88
const bech32_1 = require('bech32');
99
const bs58check = require('bs58check');
10-
const { typeforce } = types;
1110
const FUTURE_SEGWIT_MAX_SIZE = 40;
1211
const FUTURE_SEGWIT_MIN_SIZE = 2;
1312
const FUTURE_SEGWIT_MAX_VERSION = 16;
14-
const FUTURE_SEGWIT_MIN_VERSION = 1;
13+
const FUTURE_SEGWIT_MIN_VERSION = 2;
1514
const FUTURE_SEGWIT_VERSION_DIFF = 0x50;
1615
const FUTURE_SEGWIT_VERSION_WARNING =
1716
'WARNING: Sending to a future segwit version address can lead to loss of funds. ' +
@@ -69,7 +68,10 @@ function fromBech32(address) {
6968
}
7069
exports.fromBech32 = fromBech32;
7170
function toBase58Check(hash, version) {
72-
typeforce(types.tuple(types.Hash160bit, types.UInt8), arguments);
71+
(0, types_1.typeforce)(
72+
(0, types_1.tuple)(types_1.Hash160bit, types_1.UInt8),
73+
arguments,
74+
);
7375
const payload = Buffer.allocUnsafe(21);
7476
payload.writeUInt8(version, 0);
7577
hash.copy(payload, 1);
@@ -84,7 +86,7 @@ function toBech32(data, version, prefix) {
8486
: bech32_1.bech32m.encode(prefix, words);
8587
}
8688
exports.toBech32 = toBech32;
87-
function fromOutputScript(output, network) {
89+
function fromOutputScript(output, network, eccLib) {
8890
// TODO: Network
8991
network = network || networks.bitcoin;
9092
try {
@@ -99,13 +101,16 @@ function fromOutputScript(output, network) {
99101
try {
100102
return payments.p2wsh({ output, network }).address;
101103
} catch (e) {}
104+
try {
105+
if (eccLib) return payments.p2tr({ output, network }, { eccLib }).address;
106+
} catch (e) {}
102107
try {
103108
return _toFutureSegwitAddress(output, network);
104109
} catch (e) {}
105110
throw new Error(bscript.toASM(output) + ' has no matching Address');
106111
}
107112
exports.fromOutputScript = fromOutputScript;
108-
function toOutputScript(address, network) {
113+
function toOutputScript(address, network, eccLib) {
109114
network = network || networks.bitcoin;
110115
let decodeBase58;
111116
let decodeBech32;
@@ -129,6 +134,10 @@ function toOutputScript(address, network) {
129134
return payments.p2wpkh({ hash: decodeBech32.data }).output;
130135
if (decodeBech32.data.length === 32)
131136
return payments.p2wsh({ hash: decodeBech32.data }).output;
137+
} else if (decodeBech32.version === 1) {
138+
if (decodeBech32.data.length === 32 && eccLib)
139+
return payments.p2tr({ pubkey: decodeBech32.data }, { eccLib })
140+
.output;
132141
} else if (
133142
decodeBech32.version >= FUTURE_SEGWIT_MIN_VERSION &&
134143
decodeBech32.version <= FUTURE_SEGWIT_MAX_VERSION &&

test/address.spec.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as assert from 'assert';
22
import { describe, it } from 'mocha';
3+
import * as ecc from 'tiny-secp256k1';
34
import * as baddress from '../src/address';
45
import * as bscript from '../src/script';
56
import * as fixtures from './fixtures/address.json';
@@ -68,7 +69,11 @@ describe('address', () => {
6869
fixtures.standard.forEach(f => {
6970
it('encodes ' + f.script.slice(0, 30) + '... (' + f.network + ')', () => {
7071
const script = bscript.fromASM(f.script);
71-
const address = baddress.fromOutputScript(script, NETWORKS[f.network]);
72+
const address = baddress.fromOutputScript(
73+
script,
74+
NETWORKS[f.network],
75+
ecc,
76+
);
7277

7378
assert.strictEqual(address, f.base58check || f.bech32!.toLowerCase());
7479
});
@@ -79,7 +84,7 @@ describe('address', () => {
7984
const script = bscript.fromASM(f.script);
8085

8186
assert.throws(() => {
82-
baddress.fromOutputScript(script);
87+
baddress.fromOutputScript(script, undefined, ecc);
8388
}, new RegExp(f.exception));
8489
});
8590
});
@@ -131,17 +136,19 @@ describe('address', () => {
131136
const script = baddress.toOutputScript(
132137
(f.base58check || f.bech32)!,
133138
NETWORKS[f.network],
139+
ecc,
134140
);
135141

136142
assert.strictEqual(bscript.toASM(script), f.script);
137143
});
138144
});
139145

140146
fixtures.invalid.toOutputScript.forEach(f => {
141-
it('throws when ' + f.exception, () => {
147+
it('throws when ' + (f.exception || f.paymentException), () => {
148+
const exception = f.paymentException || `${f.address} ${f.exception}`;
142149
assert.throws(() => {
143-
baddress.toOutputScript(f.address, f.network as any);
144-
}, new RegExp(f.address + ' ' + f.exception));
150+
baddress.toOutputScript(f.address, f.network as any, ecc);
151+
}, new RegExp(exception));
145152
});
146153
});
147154
});

test/fixtures/address.json

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,10 @@
8080
},
8181
{
8282
"network": "bitcoin",
83-
"bech32": "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y",
83+
"bech32": "bc1p3efq8ujsj0qr5xvms7mv89p8cz0crqdtuxe9ms6grqgxc9sgsntslthf6w",
8484
"version": 1,
85-
"data": "751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6",
86-
"script": "OP_1 751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6"
85+
"data": "8e5203f25093c03a199b87b6c39427c09f8181abe1b25dc34818106c160884d7",
86+
"script": "OP_1 8e5203f25093c03a199b87b6c39427c09f8181abe1b25dc34818106c160884d7"
8787
},
8888
{
8989
"network": "bitcoin",
@@ -116,10 +116,16 @@
116116
],
117117
"bech32": [
118118
{
119-
"address": "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y",
119+
"address": "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy",
120+
"version": 0,
121+
"prefix": "tb",
122+
"data": "000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"
123+
},
124+
{
125+
"address": "bc1p3efq8ujsj0qr5xvms7mv89p8cz0crqdtuxe9ms6grqgxc9sgsntslthf6w",
120126
"version": 1,
121127
"prefix": "bc",
122-
"data": "751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6"
128+
"data": "8e5203f25093c03a199b87b6c39427c09f8181abe1b25dc34818106c160884d7"
123129
},
124130
{
125131
"address": "bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs",
@@ -195,6 +201,19 @@
195201
{
196202
"exception": "has no matching Address",
197203
"script": "OP_0 751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd675"
204+
},
205+
{
206+
"exception": "has no matching Address",
207+
"script": "OP_1 75"
208+
},
209+
{
210+
"exception": "has no matching Address",
211+
"script": "OP_1 751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd675"
212+
},
213+
{
214+
"description": "pubkey is not valid x coordinate",
215+
"exception": "has no matching Address",
216+
"script": "OP_1 fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"
198217
}
199218
],
200219
"toOutputScript": [
@@ -297,6 +316,14 @@
297316
{
298317
"address": "bc1gmk9yu",
299318
"exception": "has no matching Script"
319+
},
320+
{
321+
"address": "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw55h884v",
322+
"exception": "has no matching Script"
323+
},
324+
{
325+
"address": "bc1pllllllllllllllllllllllllllllllllllllllllllllallllshsdfvw2y",
326+
"paymentException": "TypeError: Invalid pubkey for p2tr"
300327
}
301328
]
302329
}

test/integration/taproot.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe('bitcoinjs-lib (transaction with taproot)', () => {
1212
const myKey = bip32.fromSeed(rng(64), regtest);
1313

1414
const output = createKeySpendOutput(myKey.publicKey);
15-
const address = bitcoin.address.fromOutputScript(output, regtest);
15+
const address = bitcoin.address.fromOutputScript(output, regtest, ecc);
1616
// amount from faucet
1717
const amount = 42e4;
1818
// amount to send

ts_src/address.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ import { Network } from './networks';
22
import * as networks from './networks';
33
import * as payments from './payments';
44
import * as bscript from './script';
5-
import * as types from './types';
5+
import {
6+
typeforce,
7+
tuple,
8+
Hash160bit,
9+
UInt8,
10+
TinySecp256k1Interface,
11+
} from './types';
612
import { bech32, bech32m } from 'bech32';
713
import * as bs58check from 'bs58check';
8-
const { typeforce } = types;
9-
1014
export interface Base58CheckResult {
1115
hash: Buffer;
1216
version: number;
@@ -21,7 +25,7 @@ export interface Bech32Result {
2125
const FUTURE_SEGWIT_MAX_SIZE: number = 40;
2226
const FUTURE_SEGWIT_MIN_SIZE: number = 2;
2327
const FUTURE_SEGWIT_MAX_VERSION: number = 16;
24-
const FUTURE_SEGWIT_MIN_VERSION: number = 1;
28+
const FUTURE_SEGWIT_MIN_VERSION: number = 2;
2529
const FUTURE_SEGWIT_VERSION_DIFF: number = 0x50;
2630
const FUTURE_SEGWIT_VERSION_WARNING: string =
2731
'WARNING: Sending to a future segwit version address can lead to loss of funds. ' +
@@ -93,7 +97,7 @@ export function fromBech32(address: string): Bech32Result {
9397
}
9498

9599
export function toBase58Check(hash: Buffer, version: number): string {
96-
typeforce(types.tuple(types.Hash160bit, types.UInt8), arguments);
100+
typeforce(tuple(Hash160bit, UInt8), arguments);
97101

98102
const payload = Buffer.allocUnsafe(21);
99103
payload.writeUInt8(version, 0);
@@ -115,7 +119,11 @@ export function toBech32(
115119
: bech32m.encode(prefix, words);
116120
}
117121

118-
export function fromOutputScript(output: Buffer, network?: Network): string {
122+
export function fromOutputScript(
123+
output: Buffer,
124+
network?: Network,
125+
eccLib?: TinySecp256k1Interface,
126+
): string {
119127
// TODO: Network
120128
network = network || networks.bitcoin;
121129

@@ -131,14 +139,22 @@ export function fromOutputScript(output: Buffer, network?: Network): string {
131139
try {
132140
return payments.p2wsh({ output, network }).address as string;
133141
} catch (e) {}
142+
try {
143+
if (eccLib)
144+
return payments.p2tr({ output, network }, { eccLib }).address as string;
145+
} catch (e) {}
134146
try {
135147
return _toFutureSegwitAddress(output, network);
136148
} catch (e) {}
137149

138150
throw new Error(bscript.toASM(output) + ' has no matching Address');
139151
}
140152

141-
export function toOutputScript(address: string, network?: Network): Buffer {
153+
export function toOutputScript(
154+
address: string,
155+
network?: Network,
156+
eccLib?: TinySecp256k1Interface,
157+
): Buffer {
142158
network = network || networks.bitcoin;
143159

144160
let decodeBase58: Base58CheckResult | undefined;
@@ -165,6 +181,10 @@ export function toOutputScript(address: string, network?: Network): Buffer {
165181
return payments.p2wpkh({ hash: decodeBech32.data }).output as Buffer;
166182
if (decodeBech32.data.length === 32)
167183
return payments.p2wsh({ hash: decodeBech32.data }).output as Buffer;
184+
} else if (decodeBech32.version === 1) {
185+
if (decodeBech32.data.length === 32 && eccLib)
186+
return payments.p2tr({ pubkey: decodeBech32.data }, { eccLib })
187+
.output as Buffer;
168188
} else if (
169189
decodeBech32.version >= FUTURE_SEGWIT_MIN_VERSION &&
170190
decodeBech32.version <= FUTURE_SEGWIT_MAX_VERSION &&

0 commit comments

Comments
 (0)