Skip to content

Commit 96d1df2

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 96d1df2

File tree

8 files changed

+242
-40
lines changed

8 files changed

+242
-40
lines changed

Diff for: src/crypto.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
/// <reference types="node" />
2+
export declare function isPoint(p: Buffer): boolean;
3+
export declare function isXOnlyPoint(p: Buffer): boolean;
24
export declare function ripemd160(buffer: Buffer): Buffer;
35
export declare function sha1(buffer: Buffer): Buffer;
46
export declare function sha256(buffer: Buffer): Buffer;

Diff for: src/crypto.js

+65-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,71 @@
11
'use strict';
22
Object.defineProperty(exports, '__esModule', { value: true });
3-
exports.taggedHash = exports.hash256 = exports.hash160 = exports.sha256 = exports.sha1 = exports.ripemd160 = void 0;
3+
exports.taggedHash = exports.hash256 = exports.hash160 = exports.sha256 = exports.sha1 = exports.ripemd160 = exports.isXOnlyPoint = exports.isPoint = void 0;
44
const createHash = require('create-hash');
5+
const EC_P = BigInt(
6+
`0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f`,
7+
);
8+
const EC_B = BigInt(7);
9+
const BN_ZERO = BigInt(0);
10+
function weierstrass(x) {
11+
const x2 = (x * x) % EC_P;
12+
const x3 = (x2 * x) % EC_P;
13+
return (x3 /* + a=0 a*x */ + EC_B) % EC_P;
14+
}
15+
// For prime P, the Jacobi symbol is 1 iff a is a quadratic residue mod P
16+
function jacobiSymbol(a) {
17+
// Idea from noble-secp256k1, to be nice to bad JS parsers
18+
const _1n = BigInt(1);
19+
const _2n = BigInt(2);
20+
const _3n = BigInt(3);
21+
const _5n = BigInt(5);
22+
const _7n = BigInt(7);
23+
if (a === BN_ZERO) return 0;
24+
let p = EC_P;
25+
let sign = 1;
26+
for (;;) {
27+
let and3;
28+
// Handle runs of zeros efficiently w/o flipping sign each time
29+
for (and3 = a & _3n; and3 === BN_ZERO; a >>= _2n, and3 = a & _3n);
30+
// If there's one more zero, shift it off and flip the sign
31+
if (and3 === _2n) {
32+
a >>= _1n;
33+
const pand7 = p & _7n;
34+
if (pand7 === _3n || pand7 === _5n) sign = -sign;
35+
}
36+
if (a === _1n) break;
37+
if ((_3n & a) === _3n && (_3n & p) === _3n) sign = -sign;
38+
[a, p] = [p % a, a];
39+
}
40+
return sign > 0 ? 1 : -1;
41+
}
42+
function isPoint(p) {
43+
if (p.length < 33) return false;
44+
const t = p[0];
45+
if (p.length === 33) {
46+
return (t === 0x02 || t === 0x03) && isXOnlyPoint(p.slice(1));
47+
}
48+
if (t !== 0x04 || p.length !== 65) return false;
49+
const x = BigInt(`0x${p.slice(1, 33).toString('hex')}`);
50+
if (x === BN_ZERO) return false;
51+
if (x >= EC_P) return false;
52+
const y = BigInt(`0x${p.slice(33).toString('hex')}`);
53+
if (y === BN_ZERO) return false;
54+
if (y >= EC_P) return false;
55+
const left = (y * y) % EC_P;
56+
const right = weierstrass(x);
57+
return (left - right) % EC_P === BN_ZERO;
58+
}
59+
exports.isPoint = isPoint;
60+
function isXOnlyPoint(p) {
61+
if (p.length !== 32) return false;
62+
const x = BigInt(`0x${p.toString('hex')}`);
63+
if (x === BN_ZERO) return false;
64+
if (x >= EC_P) return false;
65+
const y2 = weierstrass(x);
66+
return jacobiSymbol(y2) === 1;
67+
}
68+
exports.isXOnlyPoint = isXOnlyPoint;
569
function ripemd160(buffer) {
670
try {
771
return createHash('rmd160')

Diff for: 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 {

Diff for: src/types.js

+8-19
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,19 @@
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');
5+
const bcrypto = require('./crypto');
56
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',
10-
);
117
function isPoint(p) {
128
if (!buffer_1.Buffer.isBuffer(p)) return false;
13-
if (p.length < 33) return false;
14-
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;
20-
}
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;
9+
return bcrypto.isPoint(p);
2610
}
2711
exports.isPoint = isPoint;
12+
function isXOnlyPoint(p) {
13+
if (!buffer_1.Buffer.isBuffer(p)) return false;
14+
return bcrypto.isXOnlyPoint(p);
15+
}
16+
exports.isXOnlyPoint = isXOnlyPoint;
2817
const UINT31_MAX = Math.pow(2, 31) - 1;
2918
function UInt31(value) {
3019
return exports.typeforce.UInt32(value) && value <= UINT31_MAX;

Diff for: test/crypto.spec.ts

+28
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,32 @@ describe('crypto', () => {
3030
});
3131
});
3232
});
33+
34+
describe('isPoint (uncompressed)', () => {
35+
fixtures.isPoint.forEach(f => {
36+
it(`returns ${f.expected} for isPoint(${f.hex})`, () => {
37+
const bytes = Buffer.from(f.hex, 'hex');
38+
assert.strictEqual(bcrypto.isPoint(bytes), f.expected);
39+
});
40+
});
41+
});
42+
43+
describe('isPoint (compressed) + isXOnlyPoint', () => {
44+
fixtures.isXOnlyPoint.forEach(f => {
45+
it(`returns ${f.expected} for isPoint(02${f.hex})`, () => {
46+
const bytes = Buffer.from(`02${f.hex}`, 'hex');
47+
assert.strictEqual(bcrypto.isPoint(bytes), f.expected);
48+
});
49+
50+
it(`returns ${f.expected} for isPoint(03${f.hex})`, () => {
51+
const bytes = Buffer.from(`03${f.hex}`, 'hex');
52+
assert.strictEqual(bcrypto.isPoint(bytes), f.expected);
53+
});
54+
55+
it(`returns ${f.expected} for isXOnlyPoint(${f.hex})`, () => {
56+
const bytes = Buffer.from(f.hex, 'hex');
57+
assert.strictEqual(bcrypto.isXOnlyPoint(bytes), f.expected);
58+
});
59+
});
60+
});
3361
});

Diff for: test/fixtures/crypto.json

+57-1
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,61 @@
3939
"hex": "0101010101010101",
4040
"result": "71ae15bad52efcecf4c9f672bfbded68a4adb8258f1b95f0d06aefdb5ebd14e9"
4141
}
42+
],
43+
"isPoint": [
44+
{
45+
"hex": "0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
46+
"expected": false
47+
},
48+
{
49+
"hex": "04ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
50+
"expected": false
51+
},
52+
{
53+
"hex": "044289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34cec320a0565fb7caf11b1ca2f445f9b7b012dda5718b3cface369ee3a034ded6",
54+
"expected": true
55+
},
56+
{
57+
"hex": "044289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34cec320a0565fb7caf11b1ca2f445f9b7b012dda5718b3cface369ee3a034ded0",
58+
"expected": false
59+
},
60+
{
61+
"hex": "04ff",
62+
"expected": false
63+
}
64+
],
65+
"isXOnlyPoint": [
66+
{
67+
"hex": "ff",
68+
"expected": false
69+
},
70+
{
71+
"hex": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800",
72+
"expected": false
73+
},
74+
{
75+
"hex": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
76+
"expected": true
77+
},
78+
{
79+
"hex": "fffffffffffffffffffffffffffffffffffffffffffffffffffffffeeffffc2e",
80+
"expected": true
81+
},
82+
{
83+
"hex": "f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9",
84+
"expected": true
85+
},
86+
{
87+
"hex": "0000000000000000000000000000000000000000000000000000000000000001",
88+
"expected": true
89+
},
90+
{
91+
"hex": "0000000000000000000000000000000000000000000000000000000000000000",
92+
"expected": false
93+
},
94+
{
95+
"hex": "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f",
96+
"expected": false
97+
}
4298
]
43-
}
99+
}

Diff for: ts_src/crypto.ts

+75
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,80 @@
11
import * as createHash from 'create-hash';
22

3+
const EC_P = BigInt(
4+
`0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f`,
5+
);
6+
const EC_B = BigInt(7);
7+
const BN_ZERO = BigInt(0);
8+
9+
function weierstrass(x: bigint): bigint {
10+
const x2 = (x * x) % EC_P;
11+
const x3 = (x2 * x) % EC_P;
12+
return (x3 /* + a=0 a*x */ + EC_B) % EC_P;
13+
}
14+
15+
// For prime P, the Jacobi symbol is 1 iff a is a quadratic residue mod P
16+
// This algorithm is fairly heavily optimized, so don't simplify it w/o benchmarking
17+
function jacobiSymbol(a: bigint): -1 | 0 | 1 {
18+
// Idea from noble-secp256k1, to be nice to bad JS parsers
19+
const _1n = BigInt(1);
20+
const _2n = BigInt(2);
21+
const _3n = BigInt(3);
22+
const _5n = BigInt(5);
23+
const _7n = BigInt(7);
24+
25+
if (a === BN_ZERO) return 0;
26+
27+
let p = EC_P;
28+
let sign = 1;
29+
for (;;) {
30+
let and3;
31+
// Handle runs of zeros efficiently w/o flipping sign each time
32+
for (and3 = a & _3n; and3 === BN_ZERO; a >>= _2n, and3 = a & _3n);
33+
// If there's one more zero, shift it off and flip the sign
34+
if (and3 === _2n) {
35+
a >>= _1n;
36+
const pand7 = p & _7n;
37+
if (pand7 === _3n || pand7 === _5n) sign = -sign;
38+
}
39+
if (a === _1n) break;
40+
if ((_3n & a) === _3n && (_3n & p) === _3n) sign = -sign;
41+
[a, p] = [p % a, a];
42+
}
43+
return sign > 0 ? 1 : -1;
44+
}
45+
46+
export function isPoint(p: Buffer): boolean {
47+
if (p.length < 33) return false;
48+
49+
const t = p[0];
50+
if (p.length === 33) {
51+
return (t === 0x02 || t === 0x03) && isXOnlyPoint(p.slice(1));
52+
}
53+
54+
if (t !== 0x04 || p.length !== 65) return false;
55+
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+
60+
const y = BigInt(`0x${p.slice(33).toString('hex')}`);
61+
if (y === BN_ZERO) return false;
62+
if (y >= EC_P) return false;
63+
64+
const left = (y * y) % EC_P;
65+
const right = weierstrass(x);
66+
return (left - right) % EC_P === BN_ZERO;
67+
}
68+
69+
export function isXOnlyPoint(p: Buffer): boolean {
70+
if (p.length !== 32) return false;
71+
const x = BigInt(`0x${p.toString('hex')}`);
72+
if (x === BN_ZERO) return false;
73+
if (x >= EC_P) return false;
74+
const y2 = weierstrass(x);
75+
return jacobiSymbol(y2) === 1;
76+
}
77+
378
export function ripemd160(buffer: Buffer): Buffer {
479
try {
580
return createHash('rmd160')

Diff for: ts_src/types.ts

+6-19
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,15 @@
11
import { Buffer as NBuffer } from 'buffer';
2+
import * as bcrypto from './crypto';
23
export const typeforce = require('typeforce');
34

4-
const ZERO32 = NBuffer.alloc(32, 0);
5-
const EC_P = NBuffer.from(
6-
'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f',
7-
'hex',
8-
);
95
export function isPoint(p: Buffer | number | undefined | null): boolean {
106
if (!NBuffer.isBuffer(p)) return false;
11-
if (p.length < 33) return false;
12-
13-
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;
19-
}
7+
return bcrypto.isPoint(p);
8+
}
209

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;
10+
export function isXOnlyPoint(p: Buffer | number | undefined | null): boolean {
11+
if (!NBuffer.isBuffer(p)) return false;
12+
return bcrypto.isXOnlyPoint(p);
2613
}
2714

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

0 commit comments

Comments
 (0)