Skip to content

Commit

Permalink
Add internal curve checking, using BigInt
Browse files Browse the repository at this point in the history
  • Loading branch information
reardencode committed Mar 22, 2022
1 parent a04b9d5 commit 1c44f41
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 100 deletions.
4 changes: 2 additions & 2 deletions src/payments/p2tr.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ function p2tr(a, opts) {
else pubkey = tweakedKey.x;
}
if (pubkey && pubkey.length) {
if (!_ecc().isXOnlyPoint(pubkey))
if (!(0, types_1.isXOnlyPoint)(pubkey))
throw new TypeError('Invalid pubkey for p2tr');
}
if (a.hash && a.scriptTree) {
Expand Down Expand Up @@ -251,7 +251,7 @@ function p2tr(a, opts) {
const internalPubkey = controlBlock.slice(1, 33);
if (a.internalPubkey && !a.internalPubkey.equals(internalPubkey))
throw new TypeError('Internal pubkey mismatch');
if (!_ecc().isXOnlyPoint(internalPubkey))
if (!(0, types_1.isXOnlyPoint)(internalPubkey))
throw new TypeError('Invalid internalPubkey for p2tr witness');
const leafVersion = controlBlock[0] & LEAF_VERSION_MASK;
const script = witness[witness.length - 2];
Expand Down
31 changes: 0 additions & 31 deletions src/payments/verifyecc.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,6 @@ Object.defineProperty(exports, '__esModule', { value: true });
exports.verifyEcc = void 0;
const h = hex => Buffer.from(hex, 'hex');
function verifyEcc(ecc) {
assert(typeof ecc.isXOnlyPoint === 'function');
assert(
ecc.isXOnlyPoint(
h('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'),
),
);
assert(
ecc.isXOnlyPoint(
h('fffffffffffffffffffffffffffffffffffffffffffffffffffffffeeffffc2e'),
),
);
assert(
ecc.isXOnlyPoint(
h('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9'),
),
);
assert(
ecc.isXOnlyPoint(
h('0000000000000000000000000000000000000000000000000000000000000001'),
),
);
assert(
!ecc.isXOnlyPoint(
h('0000000000000000000000000000000000000000000000000000000000000000'),
),
);
assert(
!ecc.isXOnlyPoint(
h('fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'),
),
);
assert(typeof ecc.xOnlyPointAddTweak === 'function');
tweakAddVectors.forEach(t => {
const r = ecc.xOnlyPointAddTweak(h(t.pubkey), h(t.tweak));
Expand Down
2 changes: 1 addition & 1 deletion 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 All @@ -20,7 +21,6 @@ export interface Tapleaf {
}
export declare type Taptree = Array<[Tapleaf, Tapleaf] | Tapleaf>;
export interface TinySecp256k1Interface {
isXOnlyPoint(p: Uint8Array): boolean;
xOnlyPointAddTweak(p: Uint8Array, tweak: Uint8Array): XOnlyPointAddTweakResult | null;
privateAdd(d: Uint8Array, tweak: Uint8Array): Uint8Array | null;
privateNegate(d: Uint8Array): Uint8Array;
Expand Down
69 changes: 54 additions & 15 deletions src/types.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,69 @@
'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 EC_P = BigInt(
`0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f`,
);
const EC_B = BigInt(7);
// Idea from noble-secp256k1, to be nice to bad JS parsers
const _0n = BigInt(0);
const _1n = BigInt(1);
const _2n = BigInt(2);
const _3n = BigInt(3);
const _4n = BigInt(4);
const _5n = BigInt(5);
const _8n = BigInt(8);
function weistrass(x) {
const x2 = (x * x) % EC_P;
const x3 = (x2 * x) % EC_P;
return (x3 /* + a=0 a*x */ + EC_B) % EC_P;
}
// For prime P, the Jacobi symbol is 1 iff a is a quadratic residue mod P
function jacobiSymbol(a) {
let p = EC_P;
let sign = 1;
while (a > _1n) {
if (_0n === a % _2n) {
if (_3n === p % _8n || _5n === p % _8n) sign = -sign;
a >>= _1n;
} else {
if (_3n === p % _4n && _3n === a % _4n) sign = -sign;
[a, p] = [p % a, a];
}
}
return a === _0n ? 0 : 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 === _0n) return false;
if (x >= EC_P) return false;
const y = BigInt(`0x${p.slice(33).toString('hex')}`);
if (y === _0n) return false;
if (y >= EC_P) return false;
const left = (y * y) % EC_P;
const right = weistrass(x);
return (left - right) % EC_P === _0n;
}
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 === _0n) return false;
if (x >= EC_P) return false;
const y2 = weistrass(x);
return jacobiSymbol(y2) === 1;
}
exports.isXOnlyPoint = isXOnlyPoint;
const UINT31_MAX = Math.pow(2, 31) - 1;
function UInt31(value) {
return exports.typeforce.UInt32(value) && value <= UINT31_MAX;
Expand Down
11 changes: 7 additions & 4 deletions ts_src/payments/p2tr.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Buffer as NBuffer } from 'buffer';
import { bitcoin as BITCOIN_NETWORK } from '../networks';
import * as bscript from '../script';
import { typeforce as typef, TinySecp256k1Interface } from '../types';
import {
isXOnlyPoint,
typeforce as typef,
TinySecp256k1Interface,
} from '../types';
import {
toHashTree,
rootHashFromPath,
Expand Down Expand Up @@ -215,8 +219,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment {
}

if (pubkey && pubkey.length) {
if (!_ecc().isXOnlyPoint(pubkey))
throw new TypeError('Invalid pubkey for p2tr');
if (!isXOnlyPoint(pubkey)) throw new TypeError('Invalid pubkey for p2tr');
}

if (a.hash && a.scriptTree) {
Expand Down Expand Up @@ -280,7 +283,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment {
if (a.internalPubkey && !a.internalPubkey.equals(internalPubkey))
throw new TypeError('Internal pubkey mismatch');

if (!_ecc().isXOnlyPoint(internalPubkey))
if (!isXOnlyPoint(internalPubkey))
throw new TypeError('Invalid internalPubkey for p2tr witness');

const leafVersion = controlBlock[0] & LEAF_VERSION_MASK;
Expand Down
32 changes: 0 additions & 32 deletions ts_src/payments/verifyecc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,6 @@ import { TinySecp256k1Interface } from '../types';
const h = (hex: string): Buffer => Buffer.from(hex, 'hex');

export function verifyEcc(ecc: TinySecp256k1Interface): void {
assert(typeof ecc.isXOnlyPoint === 'function');
assert(
ecc.isXOnlyPoint(
h('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'),
),
);
assert(
ecc.isXOnlyPoint(
h('fffffffffffffffffffffffffffffffffffffffffffffffffffffffeeffffc2e'),
),
);
assert(
ecc.isXOnlyPoint(
h('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9'),
),
);
assert(
ecc.isXOnlyPoint(
h('0000000000000000000000000000000000000000000000000000000000000001'),
),
);
assert(
!ecc.isXOnlyPoint(
h('0000000000000000000000000000000000000000000000000000000000000000'),
),
);
assert(
!ecc.isXOnlyPoint(
h('fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'),
),
);

assert(typeof ecc.xOnlyPointAddTweak === 'function');
tweakAddVectors.forEach(t => {
const r = ecc.xOnlyPointAddTweak(h(t.pubkey), h(t.tweak));
Expand Down
73 changes: 58 additions & 15 deletions ts_src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,73 @@ 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 EC_P = BigInt(
`0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f`,
);
const EC_B = BigInt(7);
// Idea from noble-secp256k1, to be nice to bad JS parsers
const _0n = BigInt(0);
const _1n = BigInt(1);
const _2n = BigInt(2);
const _3n = BigInt(3);
const _4n = BigInt(4);
const _5n = BigInt(5);
const _8n = BigInt(8);

function weistrass(x: bigint): bigint {
const x2 = (x * x) % EC_P;
const x3 = (x2 * x) % EC_P;
return (x3 /* + a=0 a*x */ + EC_B) % EC_P;
}

// For prime P, the Jacobi symbol is 1 iff a is a quadratic residue mod P
function jacobiSymbol(a: bigint): -1 | 0 | 1 {
let p = EC_P;
let sign = 1;
while (a > _1n) {
if (_0n === a % _2n) {
if (_3n === p % _8n || _5n === p % _8n) sign = -sign;
a >>= _1n;
} else {
if (_3n === p % _4n && _3n === a % _4n) sign = -sign;
[a, p] = [p % a, a];
}
}
return a === _0n ? 0 : 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 === _0n) return false;
if (x >= EC_P) return false;

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

const left = (y * y) % EC_P;
const right = weistrass(x);
return (left - right) % EC_P === _0n;
}

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 === _0n) return false;
if (x >= EC_P) return false;
const y2 = weistrass(x);
return jacobiSymbol(y2) === 1;
}

const UINT31_MAX: number = Math.pow(2, 31) - 1;
Expand Down Expand Up @@ -80,7 +124,6 @@ export interface Tapleaf {
export type Taptree = Array<[Tapleaf, Tapleaf] | Tapleaf>;

export interface TinySecp256k1Interface {
isXOnlyPoint(p: Uint8Array): boolean;
xOnlyPointAddTweak(
p: Uint8Array,
tweak: Uint8Array,
Expand Down

3 comments on commit 1c44f41

@junderw
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove isXOnlyPoint? Why not make it optional and test it only if it's present.

@brandonblack
Copy link

@brandonblack brandonblack commented on 1c44f41 Mar 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just code simplicity. This is really just a proposal for having an in-library curve equation check, exactly when we use it vs. a provided one if available is less interesting.

BTW, I was able to bring the difference between tiny-secp256k1 native and this algorithm down from 100% slower to < 25% slower by optimizing the algorithm.

@brandonblack
Copy link

@brandonblack brandonblack commented on 1c44f41 Mar 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a more discoverable / reachable place to discuss, and with the updated code that is nearly as fast as tiny-secp native: bitcoinjs#1786

Please sign in to comment.