Skip to content

Commit bf1a94a

Browse files
committed
Do real secp256k1 point->curve checking
* This is a breaking change, as it requires the JS environment to have BigInt (all supported versions of JavaScript engines appear to). * This check may prevent loss of funds by eliminating a category of unspendable addresses from being created. * Performance is almost as fast as tiny-secp256k1 39-42us vs 33-35us. * Added `isXOnlyPoint` to types, expecting it to be used for Taproot.
1 parent 24e4d6f commit bf1a94a

File tree

6 files changed

+224
-30
lines changed

6 files changed

+224
-30
lines changed

src/types.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/// <reference types="node" />
22
export declare const typeforce: any;
33
export declare function isPoint(p: Buffer | number | undefined | null): boolean;
4+
export declare function isXOnlyPoint(p: Buffer | number | undefined | null): boolean;
45
export declare function UInt31(value: number): boolean;
56
export declare function BIP32Path(value: string): boolean;
67
export declare namespace BIP32Path {

src/types.js

+64-15
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,79 @@
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.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = 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.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.isXOnlyPoint = exports.isPoint = exports.typeforce = void 0;
44
const buffer_1 = require('buffer');
55
exports.typeforce = require('typeforce');
6-
const ZERO32 = buffer_1.Buffer.alloc(32, 0);
7-
const EC_P = buffer_1.Buffer.from(
8-
'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f',
9-
'hex',
6+
const BN_ZERO = BigInt(0);
7+
// Bitcoin uses the secp256k1 curve, whose parameters can be found on
8+
// page 13, section 2.4.1, of https://www.secg.org/sec2-v2.pdf
9+
const EC_P = BigInt(
10+
`0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f`,
1011
);
12+
// The short Weierstrass form curve equation simplifes to y^2 = x^3 + 7.
13+
function secp256k1Right(x) {
14+
const EC_B = BigInt(7);
15+
const x2 = (x * x) % EC_P;
16+
const x3 = (x2 * x) % EC_P;
17+
return (x3 + EC_B) % EC_P;
18+
}
19+
// For prime P, the Jacobi Symbol of 'a' is 1 if and only if 'a' is a quadratic
20+
// residue mod P, ie. there exists a value 'x' for whom x^2 = a.
21+
function jacobiSymbol(a) {
22+
// Idea from noble-secp256k1, to be nice to bad JS parsers
23+
const _1n = BigInt(1);
24+
const _2n = BigInt(2);
25+
const _3n = BigInt(3);
26+
const _5n = BigInt(5);
27+
const _7n = BigInt(7);
28+
if (a === BN_ZERO) return 0;
29+
let p = EC_P;
30+
let sign = 1;
31+
// This algorithm is fairly heavily optimized, so don't simplify it w/o benchmarking
32+
for (;;) {
33+
let and3;
34+
// Handle runs of zeros efficiently w/o flipping sign each time
35+
for (and3 = a & _3n; and3 === BN_ZERO; a >>= _2n, and3 = a & _3n);
36+
// If there's one more zero, shift it off and flip the sign
37+
if (and3 === _2n) {
38+
a >>= _1n;
39+
const pand7 = p & _7n;
40+
if (pand7 === _3n || pand7 === _5n) sign = -sign;
41+
}
42+
if (a === _1n) break;
43+
if ((_3n & a) === _3n && (_3n & p) === _3n) sign = -sign;
44+
[a, p] = [p % a, a];
45+
}
46+
return sign > 0 ? 1 : -1;
47+
}
1148
function isPoint(p) {
1249
if (!buffer_1.Buffer.isBuffer(p)) return false;
1350
if (p.length < 33) return false;
1451
const t = p[0];
15-
const x = p.slice(1, 33);
16-
if (x.compare(ZERO32) === 0) return false;
17-
if (x.compare(EC_P) >= 0) return false;
18-
if ((t === 0x02 || t === 0x03) && p.length === 33) {
19-
return true;
52+
if (p.length === 33) {
53+
return (t === 0x02 || t === 0x03) && isXOnlyPoint(p.slice(1));
2054
}
21-
const y = p.slice(33);
22-
if (y.compare(ZERO32) === 0) return false;
23-
if (y.compare(EC_P) >= 0) return false;
24-
if (t === 0x04 && p.length === 65) return true;
25-
return false;
55+
if (t !== 0x04 || p.length !== 65) return false;
56+
const x = BigInt(`0x${p.slice(1, 33).toString('hex')}`);
57+
if (x === BN_ZERO) return false;
58+
if (x >= EC_P) return false;
59+
const y = BigInt(`0x${p.slice(33).toString('hex')}`);
60+
if (y === BN_ZERO) return false;
61+
if (y >= EC_P) return false;
62+
const left = (y * y) % EC_P;
63+
const right = secp256k1Right(x);
64+
return left === right;
2665
}
2766
exports.isPoint = isPoint;
67+
function isXOnlyPoint(p) {
68+
if (!buffer_1.Buffer.isBuffer(p)) return false;
69+
if (p.length !== 32) return false;
70+
const x = BigInt(`0x${p.toString('hex')}`);
71+
if (x === BN_ZERO) return false;
72+
if (x >= EC_P) return false;
73+
const y2 = secp256k1Right(x);
74+
return jacobiSymbol(y2) === 1; // If sqrt(y^2) exists, x is on the curve.
75+
}
76+
exports.isXOnlyPoint = isXOnlyPoint;
2877
const UINT31_MAX = Math.pow(2, 31) - 1;
2978
function UInt31(value) {
3079
return exports.typeforce.UInt32(value) && value <= UINT31_MAX;

test/fixtures/crypto.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,4 @@
4040
"result": "71ae15bad52efcecf4c9f672bfbded68a4adb8258f1b95f0d06aefdb5ebd14e9"
4141
}
4242
]
43-
}
43+
}

test/fixtures/types.json

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"isPoint": [
3+
{
4+
"hex": "0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
5+
"expected": false
6+
},
7+
{
8+
"hex": "04ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
9+
"expected": false
10+
},
11+
{
12+
"hex": "044289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34cec320a0565fb7caf11b1ca2f445f9b7b012dda5718b3cface369ee3a034ded6",
13+
"expected": true
14+
},
15+
{
16+
"hex": "044289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34cec320a0565fb7caf11b1ca2f445f9b7b012dda5718b3cface369ee3a034ded0",
17+
"expected": false
18+
},
19+
{
20+
"hex": "04ff",
21+
"expected": false
22+
}
23+
],
24+
"isXOnlyPoint": [
25+
{
26+
"hex": "ff",
27+
"expected": false
28+
},
29+
{
30+
"hex": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800",
31+
"expected": false
32+
},
33+
{
34+
"hex": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
35+
"expected": true
36+
},
37+
{
38+
"hex": "fffffffffffffffffffffffffffffffffffffffffffffffffffffffeeffffc2e",
39+
"expected": true
40+
},
41+
{
42+
"hex": "f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9",
43+
"expected": true
44+
},
45+
{
46+
"hex": "0000000000000000000000000000000000000000000000000000000000000001",
47+
"expected": true
48+
},
49+
{
50+
"hex": "0000000000000000000000000000000000000000000000000000000000000000",
51+
"expected": false
52+
},
53+
{
54+
"hex": "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f",
55+
"expected": false
56+
}
57+
]
58+
}

test/types.spec.ts

+29
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as assert from 'assert';
22
import { describe, it } from 'mocha';
33
import * as types from '../src/types';
44
const typeforce = require('typeforce');
5+
import * as fixtures from './fixtures/types.json';
56

67
describe('types', () => {
78
describe('Buffer Hash160/Hash256', () => {
@@ -91,4 +92,32 @@ describe('types', () => {
9192
assert.equal(toJsonValue, '"BIP32 derivation path"');
9293
});
9394
});
95+
96+
describe('isPoint (uncompressed)', () => {
97+
fixtures.isPoint.forEach(f => {
98+
it(`returns ${f.expected} for isPoint(${f.hex})`, () => {
99+
const bytes = Buffer.from(f.hex, 'hex');
100+
assert.strictEqual(types.isPoint(bytes), f.expected);
101+
});
102+
});
103+
});
104+
105+
describe('isPoint (compressed) + isXOnlyPoint', () => {
106+
fixtures.isXOnlyPoint.forEach(f => {
107+
it(`returns ${f.expected} for isPoint(02${f.hex})`, () => {
108+
const bytes = Buffer.from(`02${f.hex}`, 'hex');
109+
assert.strictEqual(types.isPoint(bytes), f.expected);
110+
});
111+
112+
it(`returns ${f.expected} for isPoint(03${f.hex})`, () => {
113+
const bytes = Buffer.from(`03${f.hex}`, 'hex');
114+
assert.strictEqual(types.isPoint(bytes), f.expected);
115+
});
116+
117+
it(`returns ${f.expected} for isXOnlyPoint(${f.hex})`, () => {
118+
const bytes = Buffer.from(f.hex, 'hex');
119+
assert.strictEqual(types.isXOnlyPoint(bytes), f.expected);
120+
});
121+
});
122+
});
94123
});

ts_src/types.ts

+71-14
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,85 @@
11
import { Buffer as NBuffer } from 'buffer';
22
export const typeforce = require('typeforce');
33

4-
const ZERO32 = NBuffer.alloc(32, 0);
5-
const EC_P = NBuffer.from(
6-
'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f',
7-
'hex',
4+
const BN_ZERO = BigInt(0);
5+
// Bitcoin uses the secp256k1 curve, whose parameters can be found on
6+
// page 13, section 2.4.1, of https://www.secg.org/sec2-v2.pdf
7+
const EC_P = BigInt(
8+
`0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f`,
89
);
10+
11+
// The short Weierstrass form curve equation simplifes to y^2 = x^3 + 7.
12+
function secp256k1Right(x: bigint): bigint {
13+
const EC_B = BigInt(7);
14+
const x2 = (x * x) % EC_P;
15+
const x3 = (x2 * x) % EC_P;
16+
return (x3 + EC_B) % EC_P;
17+
}
18+
19+
// For prime P, the Jacobi Symbol of 'a' is 1 if and only if 'a' is a quadratic
20+
// residue mod P, ie. there exists a value 'x' for whom x^2 = a.
21+
function jacobiSymbol(a: bigint): -1 | 0 | 1 {
22+
// Idea from noble-secp256k1, to be nice to bad JS parsers
23+
const _1n = BigInt(1);
24+
const _2n = BigInt(2);
25+
const _3n = BigInt(3);
26+
const _5n = BigInt(5);
27+
const _7n = BigInt(7);
28+
29+
if (a === BN_ZERO) return 0;
30+
31+
let p = EC_P;
32+
let sign = 1;
33+
// This algorithm is fairly heavily optimized, so don't simplify it w/o benchmarking
34+
for (;;) {
35+
let and3;
36+
// Handle runs of zeros efficiently w/o flipping sign each time
37+
for (and3 = a & _3n; and3 === BN_ZERO; a >>= _2n, and3 = a & _3n);
38+
// If there's one more zero, shift it off and flip the sign
39+
if (and3 === _2n) {
40+
a >>= _1n;
41+
const pand7 = p & _7n;
42+
if (pand7 === _3n || pand7 === _5n) sign = -sign;
43+
}
44+
if (a === _1n) break;
45+
if ((_3n & a) === _3n && (_3n & p) === _3n) sign = -sign;
46+
[a, p] = [p % a, a];
47+
}
48+
return sign > 0 ? 1 : -1;
49+
}
50+
951
export function isPoint(p: Buffer | number | undefined | null): boolean {
1052
if (!NBuffer.isBuffer(p)) return false;
1153
if (p.length < 33) return false;
1254

1355
const t = p[0];
14-
const x = p.slice(1, 33);
15-
if (x.compare(ZERO32) === 0) return false;
16-
if (x.compare(EC_P) >= 0) return false;
17-
if ((t === 0x02 || t === 0x03) && p.length === 33) {
18-
return true;
56+
if (p.length === 33) {
57+
return (t === 0x02 || t === 0x03) && isXOnlyPoint(p.slice(1));
1958
}
2059

21-
const y = p.slice(33);
22-
if (y.compare(ZERO32) === 0) return false;
23-
if (y.compare(EC_P) >= 0) return false;
24-
if (t === 0x04 && p.length === 65) return true;
25-
return false;
60+
if (t !== 0x04 || p.length !== 65) return false;
61+
62+
const x = BigInt(`0x${p.slice(1, 33).toString('hex')}`);
63+
if (x === BN_ZERO) return false;
64+
if (x >= EC_P) return false;
65+
66+
const y = BigInt(`0x${p.slice(33).toString('hex')}`);
67+
if (y === BN_ZERO) return false;
68+
if (y >= EC_P) return false;
69+
70+
const left = (y * y) % EC_P;
71+
const right = secp256k1Right(x);
72+
return left === right;
73+
}
74+
75+
export function isXOnlyPoint(p: Buffer | number | undefined | null): boolean {
76+
if (!NBuffer.isBuffer(p)) return false;
77+
if (p.length !== 32) return false;
78+
const x = BigInt(`0x${p.toString('hex')}`);
79+
if (x === BN_ZERO) return false;
80+
if (x >= EC_P) return false;
81+
const y2 = secp256k1Right(x);
82+
return jacobiSymbol(y2) === 1; // If sqrt(y^2) exists, x is on the curve.
2683
}
2784

2885
const UINT31_MAX: number = Math.pow(2, 31) - 1;

0 commit comments

Comments
 (0)