Files
brk/website_next/wallets/derive/secp256k1.js
T
2026-06-17 11:25:42 +02:00

256 lines
5.6 KiB
JavaScript

import { bigIntToBytes, bytesToBigInt, createBytes } from "./bytes.js";
const FIELD_PRIME =
0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn;
const GROUP_ORDER =
0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
const GENERATOR = /** @type {const} */ ({
x: 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n,
y: 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n,
});
/**
* @typedef {Object} Secp256k1Point
* @property {bigint} x
* @property {bigint} y
*/
/**
* @param {bigint} value
* @param {bigint} modulo
*/
function mod(value, modulo) {
const result = value % modulo;
return result >= 0n ? result : result + modulo;
}
/**
* @param {bigint} value
* @param {bigint} exponent
* @param {bigint} modulo
*/
function modPow(value, exponent, modulo) {
let result = 1n;
let base = mod(value, modulo);
let power = exponent;
while (power > 0n) {
if (power & 1n) result = mod(result * base, modulo);
base = mod(base * base, modulo);
power >>= 1n;
}
return result;
}
/**
* @param {bigint} value
*/
function invertField(value) {
let low = mod(value, FIELD_PRIME);
let high = FIELD_PRIME;
let lowCoefficient = 1n;
let highCoefficient = 0n;
while (low > 1n) {
const ratio = high / low;
[low, high] = [high - low * ratio, low];
[lowCoefficient, highCoefficient] = [
highCoefficient - lowCoefficient * ratio,
lowCoefficient,
];
}
return mod(lowCoefficient, FIELD_PRIME);
}
/**
* @param {Secp256k1Point} point
*/
function isOnCurve(point) {
const left = mod(point.y * point.y, FIELD_PRIME);
const right = mod(point.x * point.x * point.x + 7n, FIELD_PRIME);
return left === right;
}
/**
* @param {Secp256k1Point} point
*/
function doublePoint(point) {
if (point.y === 0n) return null;
const slope = mod(
3n * point.x * point.x * invertField(2n * point.y),
FIELD_PRIME,
);
const x = mod(slope * slope - 2n * point.x, FIELD_PRIME);
const y = mod(slope * (point.x - x) - point.y, FIELD_PRIME);
return { x, y };
}
/**
* @param {Secp256k1Point} point
*/
function negatePoint(point) {
return { x: point.x, y: mod(-point.y, FIELD_PRIME) };
}
/**
* @param {Secp256k1Point} point
*/
function forceEvenY(point) {
return point.y & 1n ? negatePoint(point) : point;
}
/**
* @param {Secp256k1Point | null} left
* @param {Secp256k1Point | null} right
* @returns {Secp256k1Point | null}
*/
function addPoints(left, right) {
if (!left) return right;
if (!right) return left;
if (left.x === right.x) {
if (mod(left.y + right.y, FIELD_PRIME) === 0n) return null;
return doublePoint(left);
}
const slope = mod(
(right.y - left.y) * invertField(right.x - left.x),
FIELD_PRIME,
);
const x = mod(slope * slope - left.x - right.x, FIELD_PRIME);
const y = mod(slope * (left.x - x) - left.y, FIELD_PRIME);
return { x, y };
}
/**
* @param {bigint} scalar
* @param {Secp256k1Point} point
* @returns {Secp256k1Point | null}
*/
function multiplyPoint(scalar, point) {
let result = /** @type {Secp256k1Point | null} */ (null);
let addend = /** @type {Secp256k1Point | null} */ (point);
let remaining = scalar;
while (remaining > 0n) {
if (remaining & 1n) result = addPoints(result, addend);
addend = addPoints(addend, addend);
remaining >>= 1n;
}
return result;
}
/**
* @param {bigint} x
* @param {boolean} odd
*/
function liftX(x, odd) {
if (x >= FIELD_PRIME) {
throw new Error("Invalid secp256k1 x coordinate");
}
let y = modPow(x * x * x + 7n, (FIELD_PRIME + 1n) / 4n, FIELD_PRIME);
if (Boolean(y & 1n) !== odd) {
y = FIELD_PRIME - y;
}
const point = { x, y };
if (!isOnCurve(point)) {
throw new Error("Invalid secp256k1 point");
}
return point;
}
/**
* @param {Uint8Array} publicKey
*/
function parseCompressedPublicKey(publicKey) {
if (
publicKey.length !== 33 ||
(publicKey[0] !== 0x02 && publicKey[0] !== 0x03)
) {
throw new Error("Expected a compressed public key");
}
return liftX(bytesToBigInt(publicKey.slice(1)), publicKey[0] === 0x03);
}
/**
* @param {Secp256k1Point} point
*/
function compressPublicKey(point) {
const publicKey = createBytes(33);
publicKey[0] = point.y & 1n ? 0x03 : 0x02;
publicKey.set(bigIntToBytes(point.x, 32), 1);
return publicKey;
}
/**
* @param {Uint8Array} publicKey
*/
export function getXOnlyPublicKey(publicKey) {
const point = forceEvenY(parseCompressedPublicKey(publicKey));
return bigIntToBytes(point.x, 32);
}
/**
* @param {Uint8Array} publicKey
* @param {Uint8Array} tweak
*/
export function addPublicKeyTweak(publicKey, tweak) {
const scalar = bytesToBigInt(tweak);
if (scalar === 0n || scalar >= GROUP_ORDER) {
throw new Error("Invalid secp256k1 public key tweak");
}
const tweakPoint = multiplyPoint(scalar, GENERATOR);
const childPoint = addPoints(parseCompressedPublicKey(publicKey), tweakPoint);
if (!childPoint) {
throw new Error("Invalid secp256k1 child public key");
}
return compressPublicKey(childPoint);
}
/**
* @param {Uint8Array} publicKey
* @param {Uint8Array} tweak
*/
export function addXOnlyPublicKeyTweak(publicKey, tweak) {
const scalar = bytesToBigInt(tweak);
if (scalar >= GROUP_ORDER) {
throw new Error("Invalid secp256k1 x-only public key tweak");
}
const internalPoint = forceEvenY(parseCompressedPublicKey(publicKey));
const tweakPoint = scalar === 0n ? null : multiplyPoint(scalar, GENERATOR);
const outputPoint = addPoints(internalPoint, tweakPoint);
if (!outputPoint) {
throw new Error("Invalid secp256k1 x-only child public key");
}
return bigIntToBytes(outputPoint.x, 32);
}