Math

secp256k1

Points of secp256k1 as SEC1 hex and scalars mod n. Add and subtract and multiply and lift an x coordinate

Points in, points out

ts
import { addPoints, liftX, multiplyGenerator, multiplyPoint } from "@agntn/curves/secp256k1";

const G = multiplyGenerator("1"); // "0279be667ef9dcbb…1798"
multiplyGenerator(3n); // "02f9308a019258c3…36f9"
addPoints(G, multiplyGenerator(2n)); // "02f9308a019258c3…36f9", 3G again

Points are SEC1 hex without 0x, 33 bytes compressed or 65 uncompressed. Output is compressed unless you pass { compressed: false }. A scalar is hex up to 64 digits or a bigint, from 1 to n - 1.

FunctionDoes
addPoints(a, b)a + b, as in a split key vanity address
subtractPoints(a, b)a - b, the offset between two known keys
negatePoint(p)same x, the other y
multiplyPoint(p, k)k times p
multiplyGenerator(k)k times G, the public key of k
liftX(x)both points above x, even first
isOnCurve(p)true or false
convertPoint(p)between 33 and 65 bytes

Half of G

What's G times the inverse of 2?

ts
import { invertScalar, multiplyPoint } from "@agntn/curves/secp256k1";

invertScalar(2n); // "7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a1"
multiplyPoint(G, invertScalar(2n)); // "0200000000000000000000003b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63"

Eleven zero bytes at the start of x. Out of 2^256 possible values. Make of that what you will.

Scalars mod n

addScalars, subtractScalars, multiplyScalars and invertScalar work mod the group order and give 64 hex digits back:

ts
addScalars("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140", "2"); // "000…001"

That first number is n - 1, so adding 2 wraps to 1.

What it refuses

text
subtractPoints(G, G)        // The result is the point at infinity, which is no public key, as with P minus P
multiplyPoint(G, "0")       // Scalar must be hex without 0x or a bigint, from 1 to the curve order minus 1
negatePoint("02000…0005")   // Invalid SEC1 secp256k1 public key

No point of secp256k1 has x = 5, so the compressed form above is no point at all. isOnCurve says false for it instead of throwing. A coordinate at or above p is refused too, as SEC 1 asks.

Written from the specs

The domain parameters come from SEC 2, section 2.4.1. The encodings follow SEC 1, sections 2.3.3 and 2.3.4. Points multiply in Jacobian coordinates, one inversion per result. No @noble/curves underneath, though the tests hold every result to it.

Is it as fast? No. About 0.9 ms for a multiply of G, against 0.36 ms in noble, which has precomputed tables. Plenty for a puzzle. And it doesn't run in constant time, so keep real keys away from it.

Over MCP

curves_secp256k1_compute does the point side: add, subtract, negate, multiply, lift and check. Scalar math stays in the library.

text
{"operation":"lift","even":"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","odd":"0379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"}