// Package secp256k1 implements the BIP-340 Schnorr signature scheme over the // secp256k1 curve, which is what Nostr uses to sign events. // // The arithmetic uses math/big with Jacobian projective coordinates: no // inversions are performed during scalar multiplication, only once when the // final point is converted back to affine form. This keeps the code obviously // correct and portable across TinyGo's supported targets. Signature // verification is the hot path for a relay; the representation can be replaced // with fixed-width limb arithmetic later without changing this API. package secp256k1 import "math/big" var ( // P is the field prime 2^256 - 2^32 - 977. P = mustBig("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f") // N is the group order. N = mustBig("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141") gx = mustBig("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798") gy = mustBig("483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8") seven = big.NewInt(7) // sqrtExp is (P+1)/4, the exponent that yields a square root for the // p ≡ 3 (mod 4) field used by secp256k1. sqrtExp = new(big.Int).Rsh(new(big.Int).Add(P, big.NewInt(1)), 2) ) // G is the generator in affine coordinates. var G = affine{gx, gy} type affine struct { x, y *big.Int } // point is a Jacobian projective point. Z == 0 encodes the point at infinity. type point struct { x, y, z *big.Int } func mustBig(s string) *big.Int { v, ok := new(big.Int).SetString(s, 16) if !ok { panic("secp256k1: bad constant") } return v } func infinity() *point { return &point{new(big.Int), big.NewInt(1), new(big.Int)} } func (p *point) isInfinity() bool { return p.z.Sign() == 0 } func fromAffine(a affine) *point { return &point{new(big.Int).Set(a.x), new(big.Int).Set(a.y), big.NewInt(1)} } // --- field helpers --- func fAdd(a, b *big.Int) *big.Int { z := new(big.Int).Add(a, b) if z.Cmp(P) >= 0 { z.Sub(z, P) } return z } func fSub(a, b *big.Int) *big.Int { z := new(big.Int).Sub(a, b) if z.Sign() < 0 { z.Add(z, P) } return z } func fMul(a, b *big.Int) *big.Int { return new(big.Int).Mod(new(big.Int).Mul(a, b), P) } func fSqr(a *big.Int) *big.Int { return new(big.Int).Mod(new(big.Int).Mul(a, a), P) } func fMulSmall(a *big.Int, k int64) *big.Int { z := new(big.Int).Mul(a, big.NewInt(k)) return z.Mod(z, P) } func fNeg(a *big.Int) *big.Int { if a.Sign() == 0 { return new(big.Int) } return new(big.Int).Sub(P, a) } // --- group operations (Jacobian, a = 0) --- // double returns 2*a. func double(a *point) *point { if a.isInfinity() || a.y.Sign() == 0 { return infinity() } A := fSqr(a.x) B := fSqr(a.y) C := fSqr(B) xb := fAdd(a.x, B) D := fSub(fSub(fSqr(xb), A), C) D = fMulSmall(D, 2) E := fMulSmall(A, 3) F := fSqr(E) x3 := fSub(F, fMulSmall(D, 2)) y3 := fSub(fMul(E, fSub(D, x3)), fMulSmall(C, 8)) z3 := fMulSmall(fMul(a.y, a.z), 2) return &point{x3, y3, z3} } // add returns a+b. func add(a, b *point) *point { if a.isInfinity() { return &point{new(big.Int).Set(b.x), new(big.Int).Set(b.y), new(big.Int).Set(b.z)} } if b.isInfinity() { return &point{new(big.Int).Set(a.x), new(big.Int).Set(a.y), new(big.Int).Set(a.z)} } z1z1 := fSqr(a.z) z2z2 := fSqr(b.z) u1 := fMul(a.x, z2z2) u2 := fMul(b.x, z1z1) s1 := fMul(fMul(a.y, b.z), z2z2) s2 := fMul(fMul(b.y, a.z), z1z1) h := fSub(u2, u1) // r carries the factor 2 that the complete addition formula requires // (EFD add-2007-bl); using the raw S2-S1 makes the result order-dependent. r := fMulSmall(fSub(s2, s1), 2) if h.Sign() == 0 { if r.Sign() == 0 { return double(a) } return infinity() } i := fSqr(fMulSmall(h, 2)) j := fMul(h, i) v := fMul(u1, i) x3 := fSub(fSub(fSqr(r), j), fMulSmall(v, 2)) y3 := fSub(fMul(r, fSub(v, x3)), fMulSmall(fMul(s1, j), 2)) zsum := fAdd(a.z, b.z) z3 := fMul(fSub(fSub(fSqr(zsum), z1z1), z2z2), h) return &point{x3, y3, z3} } // toAffine converts a projective point. The caller must not pass infinity. func toAffine(p *point) affine { zinv := new(big.Int).ModInverse(p.z, P) z2 := fSqr(zinv) z3 := fMul(z2, zinv) return affine{fMul(p.x, z2), fMul(p.y, z3)} } // scalarMult returns k*p using a 4-bit fixed window. The scalar is reduced // modulo N first; a zero scalar yields infinity. func scalarMult(k *big.Int, p *point) *point { kk := new(big.Int).Mod(k, N) if kk.Sign() == 0 || p.isInfinity() { return infinity() } // Precompute 0*P .. 15*P. var table [16]*point table[0] = infinity() table[1] = p for i := 2; i < 16; i++ { table[i] = add(table[i-1], p) } result := infinity() // Process the scalar nibble by nibble from the most significant one. byteLen := (kk.BitLen() + 7) / 8 buf := kk.Bytes() _ = byteLen for i := 0; i < len(buf); i++ { hi := buf[i] >> 4 lo := buf[i] & 0x0f result = double(double(double(double(result)))) if hi != 0 { result = add(result, table[hi]) } result = double(double(double(double(result)))) if lo != 0 { result = add(result, table[lo]) } } return result } // liftX returns the affine point with the given x coordinate and even y, as // BIP-340 defines public keys. It returns nil when x is not on the curve. func liftX(x *big.Int) *affine { if x.Sign() < 0 || x.Cmp(P) >= 0 { return nil } c := fAdd(fMul(fSqr(x), x), seven) y := new(big.Int).Exp(c, sqrtExp, P) if fSqr(y).Cmp(c) != 0 { return nil } if y.Bit(0) == 1 { y = fNeg(y) } return &affine{new(big.Int).Set(x), y} }