Skip to content

Commit 1c44f41

Browse files
committed
Add internal curve checking, using BigInt
1 parent a04b9d5 commit 1c44f41

File tree

7 files changed

+122
-100
lines changed

7 files changed

+122
-100
lines changed

src/payments/p2tr.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ function p2tr(a, opts) {
196196
else pubkey = tweakedKey.x;
197197
}
198198
if (pubkey && pubkey.length) {
199-
if (!_ecc().isXOnlyPoint(pubkey))
199+
if (!(0, types_1.isXOnlyPoint)(pubkey))
200200
throw new TypeError('Invalid pubkey for p2tr');
201201
}
202202
if (a.hash && a.scriptTree) {
@@ -251,7 +251,7 @@ function p2tr(a, opts) {
251251
const internalPubkey = controlBlock.slice(1, 33);
252252
if (a.internalPubkey && !a.internalPubkey.equals(internalPubkey))
253253
throw new TypeError('Internal pubkey mismatch');
254-
if (!_ecc().isXOnlyPoint(internalPubkey))
254+
if (!(0, types_1.isXOnlyPoint)(internalPubkey))
255255
throw new TypeError('Invalid internalPubkey for p2tr witness');
256256
const leafVersion = controlBlock[0] & LEAF_VERSION_MASK;
257257
const script = witness[witness.length - 2];

src/payments/verifyecc.js

-31
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,6 @@ Object.defineProperty(exports, '__esModule', { value: true });
33
exports.verifyEcc = void 0;
44
const h = hex => Buffer.from(hex, 'hex');
55
function verifyEcc(ecc) {
6-
assert(typeof ecc.isXOnlyPoint === 'function');
7-
assert(
8-
ecc.isXOnlyPoint(
9-
h('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'),
10-
),
11-
);
12-
assert(
13-
ecc.isXOnlyPoint(
14-
h('fffffffffffffffffffffffffffffffffffffffffffffffffffffffeeffffc2e'),
15-
),
16-
);
17-
assert(
18-
ecc.isXOnlyPoint(
19-
h('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9'),
20-
),
21-
);
22-
assert(
23-
ecc.isXOnlyPoint(
24-
h('0000000000000000000000000000000000000000000000000000000000000001'),
25-
),
26-
);
27-
assert(
28-
!ecc.isXOnlyPoint(
29-
h('0000000000000000000000000000000000000000000000000000000000000000'),
30-
),
31-
);
32-
assert(
33-
!ecc.isXOnlyPoint(
34-
h('fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'),
35-
),
36-
);
376
assert(typeof ecc.xOnlyPointAddTweak === 'function');
387
tweakAddVectors.forEach(t => {
398
const r = ecc.xOnlyPointAddTweak(h(t.pubkey), h(t.tweak));

src/types.d.ts

+1-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 {
@@ -20,7 +21,6 @@ export interface Tapleaf {
2021
}
2122
export declare type Taptree = Array<[Tapleaf, Tapleaf] | Tapleaf>;
2223
export interface TinySecp256k1Interface {
23-
isXOnlyPoint(p: Uint8Array): boolean;
2424
xOnlyPointAddTweak(p: Uint8Array, tweak: Uint8Array): XOnlyPointAddTweakResult | null;
2525
privateAdd(d: Uint8Array, tweak: Uint8Array): Uint8Array | null;
2626
privateNegate(d: Uint8Array): Uint8Array;

src/types.js

+54-15
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,69 @@
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 EC_P = BigInt(
7+
`0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f`,
108
);
9+
const EC_B = BigInt(7);
10+
// Idea from noble-secp256k1, to be nice to bad JS parsers
11+
const _0n = BigInt(0);
12+
const _1n = BigInt(1);
13+
const _2n = BigInt(2);
14+
const _3n = BigInt(3);
15+
const _4n = BigInt(4);
16+
const _5n = BigInt(5);
17+
const _8n = BigInt(8);
18+
function weistrass(x) {
19+
const x2 = (x * x) % EC_P;
20+
const x3 = (x2 * x) % EC_P;
21+
return (x3 /* + a=0 a*x */ + EC_B) % EC_P;
22+
}
23+
// For prime P, the Jacobi symbol is 1 iff a is a quadratic residue mod P
24+
function jacobiSymbol(a) {
25+
let p = EC_P;
26+
let sign = 1;
27+
while (a > _1n) {
28+
if (_0n === a % _2n) {
29+
if (_3n === p % _8n || _5n === p % _8n) sign = -sign;
30+
a >>= _1n;
31+
} else {
32+
if (_3n === p % _4n && _3n === a % _4n) sign = -sign;
33+
[a, p] = [p % a, a];
34+
}
35+
}
36+
return a === _0n ? 0 : sign > 0 ? 1 : -1;
37+
}
1138
function isPoint(p) {
1239
if (!buffer_1.Buffer.isBuffer(p)) return false;
1340
if (p.length < 33) return false;
1441
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;
42+
if (p.length === 33) {
43+
return (t === 0x02 || t === 0x03) && isXOnlyPoint(p.slice(1));
2044
}
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;
45+
if (t !== 0x04 || p.length !== 65) return false;
46+
const x = BigInt(`0x${p.slice(1, 33).toString('hex')}`);
47+
if (x === _0n) return false;
48+
if (x >= EC_P) return false;
49+
const y = BigInt(`0x${p.slice(33).toString('hex')}`);
50+
if (y === _0n) return false;
51+
if (y >= EC_P) return false;
52+
const left = (y * y) % EC_P;
53+
const right = weistrass(x);
54+
return (left - right) % EC_P === _0n;
2655
}
2756
exports.isPoint = isPoint;
57+
function isXOnlyPoint(p) {
58+
if (!buffer_1.Buffer.isBuffer(p)) return false;
59+
if (p.length !== 32) return false;
60+
const x = BigInt(`0x${p.toString('hex')}`);
61+
if (x === _0n) return false;
62+
if (x >= EC_P) return false;
63+
const y2 = weistrass(x);
64+
return jacobiSymbol(y2) === 1;
65+
}
66+
exports.isXOnlyPoint = isXOnlyPoint;
2867
const UINT31_MAX = Math.pow(2, 31) - 1;
2968
function UInt31(value) {
3069
return exports.typeforce.UInt32(value) && value <= UINT31_MAX;

ts_src/payments/p2tr.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { Buffer as NBuffer } from 'buffer';
22
import { bitcoin as BITCOIN_NETWORK } from '../networks';
33
import * as bscript from '../script';
4-
import { typeforce as typef, TinySecp256k1Interface } from '../types';
4+
import {
5+
isXOnlyPoint,
6+
typeforce as typef,
7+
TinySecp256k1Interface,
8+
} from '../types';
59
import {
610
toHashTree,
711
rootHashFromPath,
@@ -215,8 +219,7 @@ export function p2tr(a: Payment, opts?: PaymentOpts): Payment {
215219
}
216220

217221
if (pubkey && pubkey.length) {
218-
if (!_ecc().isXOnlyPoint(pubkey))
219-
throw new TypeError('Invalid pubkey for p2tr');
222+
if (!isXOnlyPoint(pubkey)) throw new TypeError('Invalid pubkey for p2tr');
220223
}
221224

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

283-
if (!_ecc().isXOnlyPoint(internalPubkey))
286+
if (!isXOnlyPoint(internalPubkey))
284287
throw new TypeError('Invalid internalPubkey for p2tr witness');
285288

286289
const leafVersion = controlBlock[0] & LEAF_VERSION_MASK;

ts_src/payments/verifyecc.ts

-32
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,6 @@ import { TinySecp256k1Interface } from '../types';
33
const h = (hex: string): Buffer => Buffer.from(hex, 'hex');
44

55
export function verifyEcc(ecc: TinySecp256k1Interface): void {
6-
assert(typeof ecc.isXOnlyPoint === 'function');
7-
assert(
8-
ecc.isXOnlyPoint(
9-
h('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'),
10-
),
11-
);
12-
assert(
13-
ecc.isXOnlyPoint(
14-
h('fffffffffffffffffffffffffffffffffffffffffffffffffffffffeeffffc2e'),
15-
),
16-
);
17-
assert(
18-
ecc.isXOnlyPoint(
19-
h('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9'),
20-
),
21-
);
22-
assert(
23-
ecc.isXOnlyPoint(
24-
h('0000000000000000000000000000000000000000000000000000000000000001'),
25-
),
26-
);
27-
assert(
28-
!ecc.isXOnlyPoint(
29-
h('0000000000000000000000000000000000000000000000000000000000000000'),
30-
),
31-
);
32-
assert(
33-
!ecc.isXOnlyPoint(
34-
h('fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'),
35-
),
36-
);
37-
386
assert(typeof ecc.xOnlyPointAddTweak === 'function');
397
tweakAddVectors.forEach(t => {
408
const r = ecc.xOnlyPointAddTweak(h(t.pubkey), h(t.tweak));

ts_src/types.ts

+58-15
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,73 @@ import { Buffer as NBuffer } from 'buffer';
22

33
export const typeforce = require('typeforce');
44

5-
const ZERO32 = NBuffer.alloc(32, 0);
6-
const EC_P = NBuffer.from(
7-
'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f',
8-
'hex',
5+
const EC_P = BigInt(
6+
`0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f`,
97
);
8+
const EC_B = BigInt(7);
9+
// Idea from noble-secp256k1, to be nice to bad JS parsers
10+
const _0n = BigInt(0);
11+
const _1n = BigInt(1);
12+
const _2n = BigInt(2);
13+
const _3n = BigInt(3);
14+
const _4n = BigInt(4);
15+
const _5n = BigInt(5);
16+
const _8n = BigInt(8);
17+
18+
function weistrass(x: bigint): bigint {
19+
const x2 = (x * x) % EC_P;
20+
const x3 = (x2 * x) % EC_P;
21+
return (x3 /* + a=0 a*x */ + EC_B) % EC_P;
22+
}
23+
24+
// For prime P, the Jacobi symbol is 1 iff a is a quadratic residue mod P
25+
function jacobiSymbol(a: bigint): -1 | 0 | 1 {
26+
let p = EC_P;
27+
let sign = 1;
28+
while (a > _1n) {
29+
if (_0n === a % _2n) {
30+
if (_3n === p % _8n || _5n === p % _8n) sign = -sign;
31+
a >>= _1n;
32+
} else {
33+
if (_3n === p % _4n && _3n === a % _4n) sign = -sign;
34+
[a, p] = [p % a, a];
35+
}
36+
}
37+
return a === _0n ? 0 : sign > 0 ? 1 : -1;
38+
}
1039

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

1544
const t = p[0];
16-
const x = p.slice(1, 33);
17-
if (x.compare(ZERO32) === 0) return false;
18-
if (x.compare(EC_P) >= 0) return false;
19-
if ((t === 0x02 || t === 0x03) && p.length === 33) {
20-
return true;
45+
if (p.length === 33) {
46+
return (t === 0x02 || t === 0x03) && isXOnlyPoint(p.slice(1));
2147
}
2248

23-
const y = p.slice(33);
24-
if (y.compare(ZERO32) === 0) return false;
25-
if (y.compare(EC_P) >= 0) return false;
26-
if (t === 0x04 && p.length === 65) return true;
27-
return false;
49+
if (t !== 0x04 || p.length !== 65) return false;
50+
51+
const x = BigInt(`0x${p.slice(1, 33).toString('hex')}`);
52+
if (x === _0n) return false;
53+
if (x >= EC_P) return false;
54+
55+
const y = BigInt(`0x${p.slice(33).toString('hex')}`);
56+
if (y === _0n) return false;
57+
if (y >= EC_P) return false;
58+
59+
const left = (y * y) % EC_P;
60+
const right = weistrass(x);
61+
return (left - right) % EC_P === _0n;
62+
}
63+
64+
export function isXOnlyPoint(p: Buffer | number | undefined | null): boolean {
65+
if (!NBuffer.isBuffer(p)) return false;
66+
if (p.length !== 32) return false;
67+
const x = BigInt(`0x${p.toString('hex')}`);
68+
if (x === _0n) return false;
69+
if (x >= EC_P) return false;
70+
const y2 = weistrass(x);
71+
return jacobiSymbol(y2) === 1;
2872
}
2973

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

82126
export interface TinySecp256k1Interface {
83-
isXOnlyPoint(p: Uint8Array): boolean;
84127
xOnlyPointAddTweak(
85128
p: Uint8Array,
86129
tweak: Uint8Array,

0 commit comments

Comments
 (0)