Skip to content

Commit c23f19f

Browse files
committed
Add internal curve checking, using BigInt
1 parent bff562d commit c23f19f

File tree

4 files changed

+114
-43
lines changed

4 files changed

+114
-43
lines changed

src/types.d.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
/// <reference types="node" />
22
export declare const typeforce: any;
3-
declare function isFieldElement(c: Buffer | number | undefined | null): boolean;
4-
export declare const isXOnlyPoint: typeof isFieldElement;
53
export declare function isPoint(p: Buffer | number | undefined | null): boolean;
4+
export declare function isXOnlyPoint(p: Buffer | number | undefined | null): boolean;
65
export declare function UInt31(value: number): boolean;
76
export declare function BIP32Path(value: string): boolean;
87
export declare namespace BIP32Path {
@@ -30,7 +29,6 @@ export declare function isTapleaf(o: any): o is Tapleaf;
3029
export declare type Taptree = [Taptree | Tapleaf, Taptree | Tapleaf] | Tapleaf;
3130
export declare function isTaptree(scriptTree: any): scriptTree is Taptree;
3231
export interface TinySecp256k1Interface {
33-
isXOnlyPoint(p: Uint8Array): boolean;
3432
xOnlyPointAddTweak(p: Uint8Array, tweak: Uint8Array): XOnlyPointAddTweakResult | null;
3533
privateAdd(d: Uint8Array, tweak: Uint8Array): Uint8Array | null;
3634
privateNegate(d: Uint8Array): Uint8Array;
@@ -52,4 +50,3 @@ export declare const Function: any;
5250
export declare const BufferN: any;
5351
export declare const Null: any;
5452
export declare const oneOf: any;
55-
export {};

src/types.js

+54-17
Original file line numberDiff line numberDiff line change
@@ -1,32 +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.isTaptree = exports.isTapleaf = exports.TAPLEAF_VERSION_MASK = exports.Network = exports.ECPoint = exports.Satoshi = exports.Signer = exports.BIP32Path = exports.UInt31 = exports.isPoint = exports.isXOnlyPoint = 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.isTaptree = exports.isTapleaf = exports.TAPLEAF_VERSION_MASK = 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
);
11-
function isFieldElement(c) {
12-
if (!buffer_1.Buffer.isBuffer(c)) return false;
13-
if (c.length !== 32) return false;
14-
if (c.compare(ZERO32) === 0) return false;
15-
if (c.compare(EC_P) >= 0) return false;
16-
return true;
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;
1737
}
18-
exports.isXOnlyPoint = isFieldElement;
1938
function isPoint(p) {
2039
if (!buffer_1.Buffer.isBuffer(p)) return false;
2140
if (p.length < 33) return false;
2241
const t = p[0];
23-
if (!isFieldElement(p.slice(1, 33))) return false;
24-
if ((t === 0x02 || t === 0x03) && p.length === 33) return true;
25-
if (!isFieldElement(p.slice(33))) return false;
26-
if (t === 0x04 && p.length === 65) return true;
27-
return false;
42+
if (p.length === 33) {
43+
return (t === 0x02 || t === 0x03) && isXOnlyPoint(p.slice(1));
44+
}
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;
2855
}
2956
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;
3067
const UINT31_MAX = Math.pow(2, 31) - 1;
3168
function UInt31(value) {
3269
return exports.typeforce.UInt32(value) && value <= UINT31_MAX;

test/fixtures/p2tr.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -863,21 +863,21 @@
863863
"description": "Invalid x coordinate for pubkey in pubkey",
864864
"exception": "Invalid pubkey for p2tr",
865865
"arguments": {
866-
"pubkey": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
866+
"pubkey": "f136e956540197c21ff3c075d32a6e3c82f1ee1e646cc0f08f51b0b5edafa762"
867867
}
868868
},
869869
{
870870
"description": "Invalid x coordinate for pubkey in output",
871871
"exception": "Invalid pubkey for p2tr",
872872
"arguments": {
873-
"output": "OP_1 ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
873+
"output": "OP_1 f136e956540197c21ff3c075d32a6e3c82f1ee1e646cc0f08f51b0b5edafa762"
874874
}
875875
},
876876
{
877877
"description": "Invalid x coordinate for pubkey in address",
878878
"exception": "Invalid pubkey for p2tr",
879879
"arguments": {
880-
"address": "bc1plllllllllllllllllllllllllllllllllllllllllllllllllllsr7rg6v"
880+
"address": "bc1p7ymwj4j5qxtuy8lncp6ax2nw8jp0rms7v3kvpuy02xcttmd05a3qmwlnez"
881881
}
882882
},
883883
{
@@ -1007,7 +1007,7 @@
10071007
"9675a9982c6398ea9d441cb7a943bcd6ff033cc3a2e01a0178a7d3be4575be863871c6bf3eef5ecd34721c784259385ca9101c3a313e010ac942c99de05aaaa602",
10081008
"5799cf4b193b730fb99580b186f7477c2cca4d28957326f6f1a5d14116438530e7ec0ce1cd465ad96968ae8a6a09d4d37a060a115919f56fcfebe7b2277cc2df5cc08fb6cda9105ee2512b2e22635aba",
10091009
"7520c7b5db9562078049719228db2ac80cb9643ec96c8055aa3b29c2c03d4d99edb0ac",
1010-
"c1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff68f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c"
1010+
"c14444444444444444453d9e0c9436e8a8a3247fd515095d66ddf6201918b40a3668f9a4ccdffcf778da624dca2dda0b08e763ec52fd4ad403ec7563a3504d0cc168b9a77a410029e01dac89567c9b2e6cd726e840351df3f2f58fefe976200a19244150d04153909f660184d656ee95fa7bf8e1d4ec83da1fca34f64bc279b76d257ec623e08baba2cfa4ea9e99646e88f1eb1668c00c0f15b7443c8ab83481611cc3ae85eb89a7bfc40067eb1d2e6354a32426d0ce710e88bc4cc0718b99c325509c9d02a6a980d675a8969be10ee9bef82cafee2fc913475667ccda37b1bc7f13f64e56c449c532658ba8481631c02ead979754c809584a875951619cec8fb040c33f06468ae0266cd8693d6a64cea5912be32d8de95a6da6300b0c50fdcd6001ea41126e7b7e5280d455054a816560028f5ca53c9a50ee52f10e15c5337315bad1f5277acb109a1418649dc6ead2fe14699742fee7182f2f15e54279c7d932ed2799d01d73c97e68bbc94d6f7f56ee0a80efd7c76e3169e10d1a1ba3b5f1eb02369dc43af687461c7a2a3344d13eb5485dca29a67f16b4cb988923060fd3b65d0f0352bb634bcc44f2fe668836dcd0f604150049835135dc4b4fbf90fb334b3938a1f137eb32f047c65b85e6c1173b890b6d0162b48b186d1f1af8521945924ac8ac8efec321bf34f1d4b3d4a304a10313052c652d53f6ecb8a55586614e8950cde9ab6fe8e22802e93b3b9139112250b80ebc589aba231af535bb20f7eeec2e412f698c17f3fdc0a2e20924a5e38b21a628a9e3b2a61e35958e60c7f5087c"
10111011
]
10121012
}
10131013
},

ts_src/types.ts

+55-18
Original file line numberDiff line numberDiff line change
@@ -2,35 +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
);
10-
11-
function isFieldElement(c: Buffer | number | undefined | null): boolean {
12-
if (!NBuffer.isBuffer(c)) return false;
13-
if (c.length !== 32) return false;
14-
if (c.compare(ZERO32) === 0) return false;
15-
if (c.compare(EC_P) >= 0) return false;
16-
return true;
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;
1722
}
1823

19-
export const isXOnlyPoint = isFieldElement;
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+
}
2039

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

2544
const t = p[0];
45+
if (p.length === 33) {
46+
return (t === 0x02 || t === 0x03) && isXOnlyPoint(p.slice(1));
47+
}
48+
49+
if (t !== 0x04 || p.length !== 65) return false;
2650

27-
if (!isFieldElement(p.slice(1, 33))) return false;
28-
if ((t === 0x02 || t === 0x03) && p.length === 33) return true;
51+
const x = BigInt(`0x${p.slice(1, 33).toString('hex')}`);
52+
if (x === _0n) return false;
53+
if (x >= EC_P) return false;
2954

30-
if (!isFieldElement(p.slice(33))) return false;
31-
if (t === 0x04 && p.length === 65) return true;
55+
const y = BigInt(`0x${p.slice(33).toString('hex')}`);
56+
if (y === _0n) return false;
57+
if (y >= EC_P) return false;
3258

33-
return false;
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;
3472
}
3573

3674
const UINT31_MAX: number = Math.pow(2, 31) - 1;
@@ -106,7 +144,6 @@ export function isTaptree(scriptTree: any): scriptTree is Taptree {
106144
}
107145

108146
export interface TinySecp256k1Interface {
109-
isXOnlyPoint(p: Uint8Array): boolean;
110147
xOnlyPointAddTweak(
111148
p: Uint8Array,
112149
tweak: Uint8Array,

0 commit comments

Comments
 (0)