@scure/btc-signer
Version:
Audited & minimal library for Bitcoin. Handle transactions, Schnorr, Taproot, UTXO & PSBT
152 lines • 6.51 kB
JavaScript
/**
* BTC P2P layer from BIP324.
*
* Experimental ElligatorSwift implementation:
* Schnorr-like x-only ECDH with public keys indistinguishable from uniformly random bytes.
*
* Documented in
* [BIP324](https://github.com/bitcoin/bips/blob/master/bip-0324.mediawiki),
* [libsecp](https://github.com/bitcoin/bitcoin/blob/master/src/secp256k1/doc/ellswift.md).
*
* SwiftEC: Shallue-van de Woestijne Indifferentiable Function to Elliptic Curves.
* Documented in https://eprint.iacr.org/2022/759.pdf.
*
* Curve25519 & P-521 are incompatible with SwiftEC. Differences from SwiftEC:
* * undefined inputs are remapped
* * y-parity is encoded in u/t values
*
* @module
*/
import { FpIsSquare } from '@noble/curves/abstract/modular.js';
import { concatBytes, abytes } from '@noble/curves/utils.js';
import { schnorr, secp256k1 } from '@noble/curves/secp256k1.js';
import { randomBytes } from '@noble/hashes/utils.js';
import { tagSchnorr } from "./utils.js";
const Point = secp256k1.Point;
const Fp = Point.Fp;
const Fn = Point.Fn;
const _1n = BigInt(1);
const _2n = BigInt(2);
const MINUS_3_SQRT = Fp.sqrt(Fp.create(BigInt(-3)));
const _3n = BigInt(3);
const _4n = BigInt(4);
const _7n = BigInt(7);
const isValidX = (x) => FpIsSquare(Fp, Fp.add(Fp.mul(Fp.mul(x, x), x), _7n));
const trySqrt = (x) => {
try {
return Fp.sqrt(x);
}
catch (_e) { }
};
/**
* Experimental ElligatorSwift implementation:
* Schnorr-like x-only ECDH with public keys indistinguishable from uniformly random bytes.
* Documented in BIP324.
*/
export const elligatorSwift = {
// (internal stuff, exported for tests only): decode(u, _inv(x, u)) = x
_inv: (x, u, ellCase) => {
if (!Number.isSafeInteger(ellCase) || ellCase < 0 || ellCase > 7)
throw new Error(`elligatorSwift._inv: wrong case=${ellCase}`);
let v, s;
// Most rejections happens in 3 condition (in comments, ~33% each)
const u2 = Fp.mul(u, u); // u**2
const u3 = Fp.mul(u2, u); // u**3
if ((ellCase & 2) === 0) {
if (isValidX(Fp.sub(Fp.neg(x), u)))
return; // [1 condition]
v = x;
s = Fp.div(Fp.neg(Fp.add(u3, _7n)), Fp.add(Fp.add(u2, Fp.mul(u, v)), Fp.mul(v, v))); // = -(u**3 + 7) / (u**2 + u*v + v**2)
}
else {
s = Fp.sub(x, u); // x - u
if (Fp.is0(s))
return;
const t0 = Fp.add(u3, _7n); // (u**3 + 7)
const t1 = Fp.mul(Fp.mul(_3n, s), u2); // 3 * s * u**2
// r = (-s * (4 * (u**3 + 7) + 3 * s * u**2)).sqrt()
const r = trySqrt(Fp.mul(Fp.neg(s), Fp.add(Fp.mul(_4n, t0), t1)));
if (r === undefined)
return; // [2 condition]
if (ellCase & 1 && Fp.is0(r))
return;
v = Fp.div(Fp.add(Fp.neg(u), Fp.div(r, s)), _2n); // v = (-u + r / s) / 2
}
const w = trySqrt(s);
if (w === undefined)
return; // [3 condition]
const last = ellCase & 5; // ellCase = 0..8, last = 0,1,4,5
const t0 = last & 1 ? Fp.add(_1n, MINUS_3_SQRT) : Fp.sub(_1n, MINUS_3_SQRT);
const w0 = last === 0 || last === 5 ? Fp.neg(w) : w; // -w | w
// w0 * (u * t0 / 2 + v)
return Fp.mul(w0, Fp.add(Fp.div(Fp.mul(u, t0), _2n), v));
},
// Encode public key (point or x coordinate bigint) into 64-byte pseudorandom encoding
encode: (x) => {
// 200k test cycles per keygen: avg=4 max=48
// seems too much, but same as for reference implementation
while (true) {
// random scalar 1..Fp.ORDER
const u = Fp.create(Fp.fromBytes(secp256k1.utils.randomSecretKey()));
const ellCase = randomBytes(1)[0] & 7; // [0..8)
const t = elligatorSwift._inv(x, u, ellCase);
if (!t)
continue;
return concatBytes(Fp.toBytes(u), Fp.toBytes(t));
}
},
// Decode elligatorSwift point to xonly
decode: (data) => {
const _data = abytes(data, 64, 'data');
let u = Fp.create(Fp.fromBytes(_data.subarray(0, 32), true));
let t = Fp.create(Fp.fromBytes(_data.subarray(32, 64), true));
if (Fp.is0(u))
u = Fp.create(_1n);
if (Fp.is0(t))
t = Fp.create(_1n);
const u3 = Fp.mul(Fp.mul(u, u), u); // u**3
const u3plus7 = Fp.add(u3, _7n);
// u**3 + t**2 + 7 == 0 -> t = 2 * t
if (Fp.is0(Fp.add(u3plus7, Fp.mul(t, t))))
t = Fp.add(t, t);
// X = (u**3 + 7 - t**2) / (2 * t)
const x = Fp.div(Fp.sub(u3plus7, Fp.mul(t, t)), Fp.add(t, t));
// Y = (X + t) / (MINUS_3_SQRT * u);
const y = Fp.div(Fp.add(x, t), Fp.mul(MINUS_3_SQRT, u));
// try different cases
let res = Fp.add(u, Fp.mul(Fp.mul(y, y), _4n)); // u + 4 * Y ** 2,
if (isValidX(res))
return Fp.toBytes(res);
res = Fp.div(Fp.sub(Fp.div(Fp.neg(x), y), u), _2n); // (-X / Y - u) / 2
if (isValidX(res))
return Fp.toBytes(res);
res = Fp.div(Fp.sub(Fp.div(x, y), u), _2n); // (X / Y - u) / 2
if (isValidX(res))
return Fp.toBytes(res);
throw new Error('elligatorSwift: cannot decode public key');
},
// Generate pair (public key, secret key)
keygen: () => {
const privateKey = secp256k1.utils.randomSecretKey();
const p = Point.BASE.multiply(Point.Fn.fromBytes(privateKey));
const publicKey = elligatorSwift.encode(p.x);
return { privateKey, publicKey };
},
// Generates shared secret between a pub key and a priv key
getSharedSecret: (privateKeyA, publicKeyB) => {
const pub = elligatorSwift.decode(publicKeyB);
const priv = abytes(privateKeyA, 32, 'privKey');
const point = schnorr.utils.lift_x(Fp.fromBytes(pub));
const d = Fn.fromBytes(priv);
return Fp.toBytes(point.multiply(d).x);
},
// BIP324 shared secret
getSharedSecretBip324: (privateKeyOurs, publicKeyTheirs, publicKeyOurs, initiating) => {
const ours = abytes(publicKeyOurs, undefined, 'publicKeyOurs');
const theirs = abytes(publicKeyTheirs, undefined, 'publicKeyTheirs');
const ecdhPoint = elligatorSwift.getSharedSecret(privateKeyOurs, theirs);
const pubs = initiating ? [ours, theirs] : [theirs, ours];
return tagSchnorr('bip324_ellswift_xonly_ecdh', ...pubs, ecdhPoint);
},
};
//# sourceMappingURL=p2p.js.map