Skip to content

Commit

Permalink
Do real secp256k1 point->curve checking
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
reardencode committed Mar 24, 2022
1 parent 24e4d6f commit bf1a94a
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 30 deletions.
1 change: 1 addition & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// <reference types="node" />
export declare const typeforce: any;
export declare function isPoint(p: Buffer | number | undefined | null): boolean;
export declare function isXOnlyPoint(p: Buffer | number | undefined | null): boolean;
export declare function UInt31(value: number): boolean;
export declare function BIP32Path(value: string): boolean;
export declare namespace BIP32Path {
Expand Down
79 changes: 64 additions & 15 deletions src/types.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,79 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
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;
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;
const buffer_1 = require('buffer');
exports.typeforce = require('typeforce');
const ZERO32 = buffer_1.Buffer.alloc(32, 0);
const EC_P = buffer_1.Buffer.from(
'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f',
'hex',
const BN_ZERO = BigInt(0);
// Bitcoin uses the secp256k1 curve, whose parameters can be found on
// page 13, section 2.4.1, of https://www.secg.org/sec2-v2.pdf
const EC_P = BigInt(
`0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f`,
);
// The short Weierstrass form curve equation simplifes to y^2 = x^3 + 7.
function secp256k1Right(x) {
const EC_B = BigInt(7);
const x2 = (x * x) % EC_P;
const x3 = (x2 * x) % EC_P;
return (x3 + EC_B) % EC_P;
}
// For prime P, the Jacobi Symbol of 'a' is 1 if and only if 'a' is a quadratic
// residue mod P, ie. there exists a value 'x' for whom x^2 = a.
function jacobiSymbol(a) {
// Idea from noble-secp256k1, to be nice to bad JS parsers
const _1n = BigInt(1);
const _2n = BigInt(2);
const _3n = BigInt(3);
const _5n = BigInt(5);
const _7n = BigInt(7);
if (a === BN_ZERO) return 0;
let p = EC_P;
let sign = 1;
// This algorithm is fairly heavily optimized, so don't simplify it w/o benchmarking
for (;;) {
let and3;
// Handle runs of zeros efficiently w/o flipping sign each time
for (and3 = a & _3n; and3 === BN_ZERO; a >>= _2n, and3 = a & _3n);
// If there's one more zero, shift it off and flip the sign
if (and3 === _2n) {
a >>= _1n;
const pand7 = p & _7n;
if (pand7 === _3n || pand7 === _5n) sign = -sign;
}
if (a === _1n) break;
if ((_3n & a) === _3n && (_3n & p) === _3n) sign = -sign;
[a, p] = [p % a, a];
}
return sign > 0 ? 1 : -1;
}
function isPoint(p) {
if (!buffer_1.Buffer.isBuffer(p)) return false;
if (p.length < 33) return false;
const t = p[0];
const x = p.slice(1, 33);
if (x.compare(ZERO32) === 0) return false;
if (x.compare(EC_P) >= 0) return false;
if ((t === 0x02 || t === 0x03) && p.length === 33) {
return true;
if (p.length === 33) {
return (t === 0x02 || t === 0x03) && isXOnlyPoint(p.slice(1));
}
const y = p.slice(33);
if (y.compare(ZERO32) === 0) return false;
if (y.compare(EC_P) >= 0) return false;
if (t === 0x04 && p.length === 65) return true;
return false;
if (t !== 0x04 || p.length !== 65) return false;
const x = BigInt(`0x${p.slice(1, 33).toString('hex')}`);
if (x === BN_ZERO) return false;
if (x >= EC_P) return false;
const y = BigInt(`0x${p.slice(33).toString('hex')}`);
if (y === BN_ZERO) return false;
if (y >= EC_P) return false;
const left = (y * y) % EC_P;
const right = secp256k1Right(x);
return left === right;
}
exports.isPoint = isPoint;
function isXOnlyPoint(p) {
if (!buffer_1.Buffer.isBuffer(p)) return false;
if (p.length !== 32) return false;
const x = BigInt(`0x${p.toString('hex')}`);
if (x === BN_ZERO) return false;
if (x >= EC_P) return false;
const y2 = secp256k1Right(x);
return jacobiSymbol(y2) === 1; // If sqrt(y^2) exists, x is on the curve.
}
exports.isXOnlyPoint = isXOnlyPoint;
const UINT31_MAX = Math.pow(2, 31) - 1;
function UInt31(value) {
return exports.typeforce.UInt32(value) && value <= UINT31_MAX;
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/crypto.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@
"result": "71ae15bad52efcecf4c9f672bfbded68a4adb8258f1b95f0d06aefdb5ebd14e9"
}
]
}
}
58 changes: 58 additions & 0 deletions test/fixtures/types.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"isPoint": [
{
"hex": "0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"expected": false
},
{
"hex": "04ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"expected": false
},
{
"hex": "044289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34cec320a0565fb7caf11b1ca2f445f9b7b012dda5718b3cface369ee3a034ded6",
"expected": true
},
{
"hex": "044289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34cec320a0565fb7caf11b1ca2f445f9b7b012dda5718b3cface369ee3a034ded0",
"expected": false
},
{
"hex": "04ff",
"expected": false
}
],
"isXOnlyPoint": [
{
"hex": "ff",
"expected": false
},
{
"hex": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800",
"expected": false
},
{
"hex": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
"expected": true
},
{
"hex": "fffffffffffffffffffffffffffffffffffffffffffffffffffffffeeffffc2e",
"expected": true
},
{
"hex": "f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9",
"expected": true
},
{
"hex": "0000000000000000000000000000000000000000000000000000000000000001",
"expected": true
},
{
"hex": "0000000000000000000000000000000000000000000000000000000000000000",
"expected": false
},
{
"hex": "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f",
"expected": false
}
]
}
29 changes: 29 additions & 0 deletions test/types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as assert from 'assert';
import { describe, it } from 'mocha';
import * as types from '../src/types';
const typeforce = require('typeforce');
import * as fixtures from './fixtures/types.json';

describe('types', () => {
describe('Buffer Hash160/Hash256', () => {
Expand Down Expand Up @@ -91,4 +92,32 @@ describe('types', () => {
assert.equal(toJsonValue, '"BIP32 derivation path"');
});
});

describe('isPoint (uncompressed)', () => {
fixtures.isPoint.forEach(f => {
it(`returns ${f.expected} for isPoint(${f.hex})`, () => {
const bytes = Buffer.from(f.hex, 'hex');
assert.strictEqual(types.isPoint(bytes), f.expected);
});
});
});

describe('isPoint (compressed) + isXOnlyPoint', () => {
fixtures.isXOnlyPoint.forEach(f => {
it(`returns ${f.expected} for isPoint(02${f.hex})`, () => {
const bytes = Buffer.from(`02${f.hex}`, 'hex');
assert.strictEqual(types.isPoint(bytes), f.expected);
});

it(`returns ${f.expected} for isPoint(03${f.hex})`, () => {
const bytes = Buffer.from(`03${f.hex}`, 'hex');
assert.strictEqual(types.isPoint(bytes), f.expected);
});

it(`returns ${f.expected} for isXOnlyPoint(${f.hex})`, () => {
const bytes = Buffer.from(f.hex, 'hex');
assert.strictEqual(types.isXOnlyPoint(bytes), f.expected);
});
});
});
});
85 changes: 71 additions & 14 deletions ts_src/types.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,85 @@
import { Buffer as NBuffer } from 'buffer';
export const typeforce = require('typeforce');

const ZERO32 = NBuffer.alloc(32, 0);
const EC_P = NBuffer.from(
'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f',
'hex',
const BN_ZERO = BigInt(0);
// Bitcoin uses the secp256k1 curve, whose parameters can be found on
// page 13, section 2.4.1, of https://www.secg.org/sec2-v2.pdf
const EC_P = BigInt(
`0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f`,
);

// The short Weierstrass form curve equation simplifes to y^2 = x^3 + 7.
function secp256k1Right(x: bigint): bigint {
const EC_B = BigInt(7);
const x2 = (x * x) % EC_P;
const x3 = (x2 * x) % EC_P;
return (x3 + EC_B) % EC_P;
}

// For prime P, the Jacobi Symbol of 'a' is 1 if and only if 'a' is a quadratic
// residue mod P, ie. there exists a value 'x' for whom x^2 = a.
function jacobiSymbol(a: bigint): -1 | 0 | 1 {
// Idea from noble-secp256k1, to be nice to bad JS parsers
const _1n = BigInt(1);
const _2n = BigInt(2);
const _3n = BigInt(3);
const _5n = BigInt(5);
const _7n = BigInt(7);

if (a === BN_ZERO) return 0;

let p = EC_P;
let sign = 1;
// This algorithm is fairly heavily optimized, so don't simplify it w/o benchmarking
for (;;) {
let and3;
// Handle runs of zeros efficiently w/o flipping sign each time
for (and3 = a & _3n; and3 === BN_ZERO; a >>= _2n, and3 = a & _3n);
// If there's one more zero, shift it off and flip the sign
if (and3 === _2n) {
a >>= _1n;
const pand7 = p & _7n;
if (pand7 === _3n || pand7 === _5n) sign = -sign;
}
if (a === _1n) break;
if ((_3n & a) === _3n && (_3n & p) === _3n) sign = -sign;
[a, p] = [p % a, a];
}
return sign > 0 ? 1 : -1;
}

export function isPoint(p: Buffer | number | undefined | null): boolean {
if (!NBuffer.isBuffer(p)) return false;
if (p.length < 33) return false;

const t = p[0];
const x = p.slice(1, 33);
if (x.compare(ZERO32) === 0) return false;
if (x.compare(EC_P) >= 0) return false;
if ((t === 0x02 || t === 0x03) && p.length === 33) {
return true;
if (p.length === 33) {
return (t === 0x02 || t === 0x03) && isXOnlyPoint(p.slice(1));
}

const y = p.slice(33);
if (y.compare(ZERO32) === 0) return false;
if (y.compare(EC_P) >= 0) return false;
if (t === 0x04 && p.length === 65) return true;
return false;
if (t !== 0x04 || p.length !== 65) return false;

const x = BigInt(`0x${p.slice(1, 33).toString('hex')}`);
if (x === BN_ZERO) return false;
if (x >= EC_P) return false;

const y = BigInt(`0x${p.slice(33).toString('hex')}`);
if (y === BN_ZERO) return false;
if (y >= EC_P) return false;

const left = (y * y) % EC_P;
const right = secp256k1Right(x);
return left === right;
}

export function isXOnlyPoint(p: Buffer | number | undefined | null): boolean {
if (!NBuffer.isBuffer(p)) return false;
if (p.length !== 32) return false;
const x = BigInt(`0x${p.toString('hex')}`);
if (x === BN_ZERO) return false;
if (x >= EC_P) return false;
const y2 = secp256k1Right(x);
return jacobiSymbol(y2) === 1; // If sqrt(y^2) exists, x is on the curve.
}

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

0 comments on commit bf1a94a

Please sign in to comment.