# secp256k1_pure_python.py # Pure Python scalar->public (affine x,y) for secp256k1 # No external libs required. # Curve parameters for secp256k1 P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F A = 0 # a = 0 for secp256k1 B = 7 # Base point G Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 # Order of base point N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 # ---------- Field helpers ---------- def inv_mod(x, p=P): """Multiplicative inverse modulo p (Python built-in pow with -1 not supported)""" return pow(x, p - 2, p) # ---------- Jacobian coordinates helpers ---------- # Represent point as tuple (X, Y, Z) meaning affine (X/Z^2, Y/Z^3). INFINITY = (0, 1, 0) # Z == 0 means point at infinity in these helpers def is_infinity(Pj): return Pj[2] == 0 def to_jacobian(x, y): return (x % P, y % P, 1) def from_jacobian(Pj): X, Y, Z = Pj if Z == 0: return None # infinity Z_inv = inv_mod(Z) Z_inv2 = (Z_inv * Z_inv) % P Z_inv3 = (Z_inv2 * Z_inv) % P x = (X * Z_inv2) % P y = (Y * Z_inv3) % P return (x, y) # Point doubling in Jacobian coords def jacobian_double(Pj): X1, Y1, Z1 = Pj if Z1 == 0 or Y1 == 0: return INFINITY # Formula for a = 0 S = (4 * X1 * pow(Y1, 2, P)) % P M = (3 * pow(X1, 2, P)) % P # since a=0 X3 = (pow(M, 2, P) - 2 * S) % P Y3 = (M * (S - X3) - 8 * pow(Y1, 4, P)) % P Z3 = (2 * Y1 * Z1) % P return (X3, Y3, Z3) # Mixed addition: add Jacobian Pj and affine Q=(x2,y2) def jacobian_add_affine(Pj, Qx, Qy): X1, Y1, Z1 = Pj if Z1 == 0: return to_jacobian(Qx, Qy) if Y1 == 0 and X1 == 0 and Z1 == 0: return to_jacobian(Qx, Qy) Z1z = (Z1 * Z1) % P U2 = (Qx * Z1z) % P S2 = (Qy * Z1z * Z1) % P # Qy * Z1^3 H = (U2 - X1) % P r = (S2 - Y1) % P if H == 0: if r == 0: return jacobian_double(Pj) else: return INFINITY HH = (H * H) % P HHH = (H * HH) % P V = (X1 * HH) % P X3 = (r * r - HHH - 2 * V) % P Y3 = (r * (V - X3) - Y1 * HHH) % P Z3 = (Z1 * HH) % P return (X3, Y3, Z3) # ---------- Scalar multiplication ---------- def scalar_mult(k, Px=Gx, Py=Gy): """ Multiply base point (Px,Py) by scalar k. Returns affine (x,y) or None for infinity. k should be integer >=0. """ if k % N == 0: return None if k < 0: # use -P return scalar_mult(-k, Px, (-Py) % P) # Ensure scalar reduced mod N and >0 k = k % N if k == 0: return None # Initialize result as infinity (Jacobian) R = INFINITY # Take base point as affine (we'll use mixed add) Px = Px % P Py = Py % P # Left-to-right binary method bits = k.bit_length() for i in range(bits - 1, -1, -1): # R = 2*R R = jacobian_double(R) if (k >> i) & 1: # R = R + P R = jacobian_add_affine(R, Px, Py) # Convert R to affine return from_jacobian(R) # ---------- Utility outputs ---------- def pubkey_bytes_compressed(x, y): prefix = b'\x02' if (y % 2 == 0) else b'\x03' xb = x.to_bytes(32, 'big') return prefix + xb def pubkey_bytes_uncompressed(x, y): return b'\x04' + x.to_bytes(32, 'big') + y.to_bytes(32, 'big') # ---------- Public function as user asked ---------- def priv_to_pub(priv_int): """ Input: priv_int (Python int) — private key (assumed 0 < priv < N but function reduces) Output: (x, y) tuple of Python ints (affine coordinates). Returns None if point at infinity (shouldn't for valid keys). """ if not isinstance(priv_int, int): raise TypeError("priv must be int") k = priv_int % N if k == 0: raise ValueError("private key is zero modulo curve order") pt = scalar_mult(k, Gx, Gy) if pt is None: raise RuntimeError("Result is point at infinity (shouldn't happen for valid priv)") return pt # ---------- Example / quick test ---------- if __name__ == "__main__": # Test vector: priv = 1 -> pub = G priv = 1 x, y = priv_to_pub(priv) assert x == Gx and y == Gy print("priv=1 -> pub == G : OK") # Test with a random private (example) import secrets priv = secrets.randbelow(N - 1) + 1 x, y = priv_to_pub(priv) print("priv (hex):", hex(priv)) print("pub x:", hex(x)) print("pub y:", hex(y)) print("compressed pubkey:", pubkey_bytes_compressed(x, y).hex())