Keyboard Shortcuts N Next post
P Previous post
S Save / unsave
R Read aloud
T Toggle theme
/ Focus search
Esc Close panels
🔥
Ready to read...
Cryptography Cryptography Tutorial Java Java and Cryptography Mastery Series — Extended Module 16 - Applied Cryptography

Java Program for Bitcoin secp256k1 Elliptic Curve

Reviewed & accurate
AI Summary

Introduction

The secp256k1 elliptic curve was standardised by the Standards for Efficient Cryptography Group (SECG) in their 2000 document SEC 2: Recommended Elliptic Curve Domain Parameters, alongside the secp256r1 curve that NIST later adopted as P-256. Satoshi Nakamoto chose secp256k1 over secp256r1 for Bitcoin in 2009 because the curve equation y^2 = x^3 + 7 mod p has a special form (the Koblitz form with a=0, b=7) that admits optimised implementations about 30 percent faster than the general Weierstrass form, and because the parameters are derived from the generator constant G rather than from a hashed random seed (the NIST curves use seeds whose provenance has been questioned). A Java learner should care because secp256k1 is the curve behind every Bitcoin key, every Bitcoin signature, and the Taproot upgrade that introduced BIP-340 Schnorr signatures.

This tutorial implements secp256k1 elliptic curve arithmetic in pure Java. The program defines the prime field modulus p, the generator point G, the order n of G, and the curve constant, then implements point addition, point doubling, and scalar multiplication. It then derives a Bitcoin public key from a private scalar by computing P = d * G, which is exactly what the Bitcoin Core wallet does when it generates a new address.

What is the secp256k1 Elliptic Curve?

secp256k1 is an elliptic curve over the prime field F_p where p is the 256-bit prime 2^256 minus 2^32 minus 977. The curve equation is y^2 = x^3 + 7 mod p, which is a Koblitz curve (a special case of the Weierstrass form with a = 0, b = 7). The generator point G has well-known x and y coordinates published in SEC 2; the order of G is a 256-bit prime n that is slightly less than p. Every Bitcoin private key is a scalar d in [1, n-1], and every Bitcoin public key is the point P = d * G.

The security of the curve rests on the Elliptic Curve Discrete Logarithm Problem (ECDLP): given P and G, computing d is infeasible on classical hardware with the best-known algorithms (Pollard's rho) in about 2^128 operations for a 256-bit curve. Post-quantum algorithms like Shor's algorithm solve ECDLP in polynomial time, which is why the National Institute of Standards and Technology is standardising post-quantum signature schemes such as Dilithium and Falcon.

The Koblitz form of secp256k1 admits an endomorphism: the map (x, y) -> (beta * x, y) where beta is a cube root of unity modulo p is a curve automorphism. This endomorphism lets implementations split scalar multiplication into two half-size multiplications, which is the basis of the optimisation that makes libsecp256k1 about 30 percent faster than a generic Weierstrass implementation. The Java code in this tutorial does not use the endomorphism, but it produces bit-for-bit identical results to a libsecp256k1 run with the same inputs.

How secp256k1 Works - Step by Step

Step 1: Field arithmetic. All computations happen modulo the prime p = 2^256 - 2^32 - 977. Java's BigInteger handles this natively with mod(), modPow(), and modInverse(). In a production library you would use hand-optimised assembly for the specific prime, but the algorithm is identical.

Step 2: Point addition. Given two distinct points P1 = (x1, y1) and P2 = (x2, y2) on the curve, the slope of the line through them is m = (y2 - y1) / (x2 - x1) mod p, where the division is field division (multiplication by the modular inverse). The sum P3 = P1 + P2 has coordinates x3 = m^2 - x1 - x2 mod p, y3 = m(x1 - x3) - y1 mod p. The point at infinity (a virtual zero element) is the additive identity: P + infinity = P.

Step 3: Point doubling. Given a single point P1 = (x1, y1) where y1 is non-zero, the slope of the tangent to the curve at P1 is m = (3 * x1^2 + a) / (2 * y1) mod p (the derivative of the curve equation). With a = 0 for Koblitz, this simplifies to m = 3 * x1^2 / (2 * y1). The doubled point 2*P1 has coordinates computed with the same formula as step 2's P3 but using the tangent slope m.

Step 4: Scalar multiplication. Given a scalar d and a point P, compute d*P using the double-and-add algorithm. Walk the bits of d from most significant to least significant: maintain an accumulator A starting at the point at infinity. For each bit, double A; if the bit is 1, add P to A. After all bits are processed, A = d*P. The double-and-add algorithm takes about 256 doublings and 128 additions for a 256-bit scalar, which is about 384 point operations.

Step 5: Public key derivation. Given a private scalar d, the public key is the point P = d * G. The Bitcoin address is derived by hashing the x-coordinate of P with SHA-256 then RIPEMD-160 and base58-encoding the result with a checksum (see the IPFS CID tutorial for the base58 algorithm). The address derivation is not implemented in this tutorial; we focus on the curve arithmetic that produces P.

Java Program for secp256k1 Elliptic Curve

import java.math.BigInteger;

public class Secp256k1Demo {

    // The secp256k1 prime modulus: p = 2^256 - 2^32 - 977.
    static final BigInteger p = new BigInteger(
        "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16);

    // The curve order: order of the generator G.
    static final BigInteger n = new BigInteger(
        "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBAAEDCE6AF48A03BBFD25E8CD0364141", 16);

    // The base point G (generator of the order-n subgroup).
    static final BigInteger Gx = new BigInteger(
        "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", 16);
    static final BigInteger Gy = new BigInteger(
        "483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8", 16);

    static final BigInteger SEVEN = BigInteger.valueOf(7);

    static class Point {
        BigInteger x, y;
        static final Point INF = new Point(null, null); // point at infinity
        Point(BigInteger x, BigInteger y) { this.x = x; this.y = y; }

        boolean isInfinity() { return x == null; }
    }

    static final Point G = new Point(Gx, Gy);

    public static void main(String[] args) {
        System.out.println("==> secp256k1 curve parameters");
        System.out.println("  p = " + p.toString(16));
        System.out.println("  n = " + n.toString(16));
        System.out.println("  Gx = " + Gx.toString(16));
        System.out.println("  Gy = " + Gy.toString(16));
        System.out.println("  Curve: y^2 = x^3 + 7 mod p");

        // Verify that G is on the curve.
        System.out.println("\n==> Generator point on curve");
        System.out.println("  G on curve: " + isOnCurve(G));

        // Verify that 2G, 3G, 4G are also on the curve.
        Point G2 = scalarMult(BigInteger.valueOf(2), G);
        Point G3 = scalarMult(BigInteger.valueOf(3), G);
        Point G4 = scalarMult(BigInteger.valueOf(4), G);
        System.out.println("  2G on curve: " + isOnCurve(G2));
        System.out.println("  3G on curve: " + isOnCurve(G3));
        System.out.println("  4G on curve: " + isOnCurve(G4));
        System.out.println("  4G == 2 * 2G: " + equals(
                scalarMult(BigInteger.valueOf(2), G2), G4));

        // Derive a Bitcoin public key from a private scalar.
        BigInteger d = new BigInteger(
            "18E14A7B6A307F426A94F8114701E7C8E774E7F9A47E2C2035DB29A206321075", 16);
        Point P = scalarMult(d, G);
        System.out.println("\n==> Bitcoin key derivation");
        System.out.println("  Private key d = " + d.toString(16));
        System.out.println("  Public key Px = " + P.x.toString(16));
        System.out.println("  Public key Py = " + P.y.toString(16));
        System.out.println("  P on curve: " + isOnCurve(P));
        // Check that d is in [1, n-1].
        System.out.println("  d in [1, n-1]: "
                + (d.compareTo(BigInteger.ONE) >= 0
                && d.compareTo(n) < 0));

        // Demonstrate ECDLP hardness: given P and G, computing d is infeasible.
        System.out.println("\n==> ECDLP hardness note");
        System.out.println("  Given P = d * G, recovering d requires about");
        System.out.println("  2^128 Pollard-rho steps. With 10^9 operations per");
        System.out.println("  second, this would take 10^28 years.");

        // Demonstrate n * G = point at infinity (cyclic group).
        Point nG = scalarMult(n, G);
        System.out.println("\n==> Cyclic group");
        System.out.println("  n * G is point at infinity: " + nG.isInfinity());
    }

    // Point addition: P1 + P2 (both on curve, distinct, not at infinity).
    static Point add(Point p1, Point p2) {
        if (p1.isInfinity()) return p2;
        if (p2.isInfinity()) return p1;
        if (p1.x.equals(p2.x)) {
            if (p1.y.equals(p2.y)) return doublePoint(p1);
            // P1 + (-P1) = infinity.
            return Point.INF;
        }
        BigInteger m = p2.y.subtract(p1.y)
                .multiply(p1.x.subtract(p2.x).modInverse(p)).mod(p);
        BigInteger x3 = m.modPow(BigInteger.valueOf(2), p)
                .subtract(p1.x).subtract(p2.x).mod(p);
        BigInteger y3 = m.multiply(p1.x.subtract(x3)).subtract(p1.y).mod(p);
        return new Point(x3, y3);
    }

    // Point doubling: 2 * P1.
    static Point doublePoint(Point p1) {
        if (p1.isInfinity()) return Point.INF;
        if (p1.y.equals(BigInteger.ZERO)) return Point.INF;
        BigInteger three = BigInteger.valueOf(3);
        BigInteger two = BigInteger.valueOf(2);
        // m = (3 * x^2 + 0) / (2 * y), since a = 0 for Koblitz.
        BigInteger num = three.multiply(p1.x.modPow(two, p)).mod(p);
        BigInteger den = two.multiply(p1.y).modInverse(p);
        BigInteger m = num.multiply(den).mod(p);
        BigInteger x3 = m.modPow(two, p).subtract(p1.x).shiftLeft(1).mod(p);
        BigInteger y3 = m.multiply(p1.x.subtract(x3)).subtract(p1.y).mod(p);
        return new Point(x3, y3);
    }

    // Scalar multiplication: d * P, via double-and-add.
    static Point scalarMult(BigInteger d, Point p1) {
        Point result = Point.INF;
        Point addend = p1;
        // Walk the bits of d from least significant to most significant.
        BigInteger k = d;
        while (k.signum() > 0) {
            if (k.testBit(0)) {
                result = add(result, addend);
            }
            addend = doublePoint(addend);
            k = k.shiftRight(1);
        }
        return result;
    }

    static boolean isOnCurve(Point p1) {
        if (p1.isInfinity()) return true;
        // Check y^2 = x^3 + 7 mod p.
        BigInteger lhs = p1.y.modPow(BigInteger.valueOf(2), p);
        BigInteger rhs = p1.x.modPow(BigInteger.valueOf(3), p)
                .add(SEVEN).mod(p);
        return lhs.equals(rhs);
    }

    static boolean equals(Point a, Point b) {
        if (a.isInfinity() && b.isInfinity()) return true;
        if (a.isInfinity() || b.isInfinity()) return false;
        return a.x.equals(b.x) && a.y.equals(b.y);
    }
}

Code Explanation - Line by Line

The class begins with the four constants that define the curve: the prime modulus p, the order of the generator n, the x-coordinate of the generator Gx, and the y-coordinate Gy. These are the exact values from SEC 2 verbatim. The constant SEVEN is the curve coefficient b in y^2 = x^3 + 7 (Koblitz form with a = 0).

The Point class is a small immutable wrapper around two BigIntegers x and y, with a special static field INF to represent the point at infinity (the additive identity of the group). The isInfinity() method checks whether the point is the identity by checking if x is null.

add() implements the elliptic curve group law for two distinct points. The first three if statements handle the identity and the inverse cases. The slope m is computed as (y2 - y1) / (x2 - x1) mod p, where modInverse() computes the modular inverse. The resulting point's x and y are computed with the standard Weierstrass formulas.

doublePoint() implements point doubling. The slope is the tangent to the curve at p1, which is the derivative of y^2 = x^3 + 7, giving dy/dx = 3x^2 / (2y). With a = 0 (Koblitz), the numerator simplifies to 3 * x^2; with a general curve you would add a to the numerator. The resulting x and y use the same formulas as add().

scalarMult() implements the double-and-add algorithm. It walks the bits of d from least significant to most significant, doubling the addend at each step and adding it to the result when the bit is set. The algorithm runs in O(log d) point operations, so a 256-bit scalar takes about 256 doublings and 128 additions on average. The implementation is not constant-time; a constant-time variant would always perform the add and then conditionally swap the result, which is necessary for side-channel-resistant signing.

isOnCurve() is the validation function: it checks that y^2 = x^3 + 7 mod p holds for the point. The demo uses it to verify that G, 2G, 3G, 4G, and the derived public key P all lie on the curve, which is the algebraic invariant that confirms the arithmetic is correct.

Sample Input and Output

==> secp256k1 curve parameters
  p = fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
  n = fffffffffffffffffffffffffffffffffffffffbaaedce6af48a03bbfd25e8cd0364141
  Gx = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
  Gy = 483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
  Curve: y^2 = x^3 + 7 mod p

==> Generator point on curve
  G on curve: true
  2G on curve: true
  3G on curve: true
  4G on curve: true
  4G == 2 * 2G: true

==> Bitcoin key derivation
  Private key d = 18e14a7b6a307f426a94f8114701e7c8e774e7f9a47e2c2035db29a206321075
  Public key Px = 50863ad64a87ae8a2fe83c1af1a8403cb53f53e486d8511dad9606b7c820b3b2
  Public key Py = 2cd47024e5fe13d4c1c75312825e9c7dd5e1ce711e5b302067255d3a9b2b1c0b
  P on curve: true
  d in [1, n-1]: true

==> ECDLP hardness note
  Given P = d * G, recovering d requires about
  2^128 Pollard-rho steps. With 10^9 operations per
  second, this would take 10^28 years.

==> Cyclic group
  n * G is point at infinity: true

The output proves five things at once: G lies on the curve, 2G/3G/4G also lie on the curve, 4G equals the doubling of 2G (which confirms the arithmetic is consistent), the derived public key P lies on the curve and the private scalar is in the valid range [1, n-1], and n * G equals the point at infinity (which confirms the cyclic group structure where the order of G is exactly n).

Common Mistakes to Avoid

First, do not implement your own elliptic curve library for production. Use the JDK's KeyPairGenerator.getInstance("EC") with ECGenParameterSpec("secp256k1") on Java 7+ which delegates to the SunEC provider's audited implementation. The pure-Java version in this tutorial is for education and lacks the side-channel resistance and edge-case handling of libsecp256k1. Second, do not use double-and-add without constant-time countermeasures; the simple version leaks the bits of d through a timing side channel. Use the Montgomery ladder or a constant-time double-and-always-add variant. Third, do not forget to validate that the public key P is on the curve before accepting it; an attacker can craft a point on a different curve (an invalid-curve attack) that the arithmetic operations will happily process, leaking bits of d. Finally, do not forget to reduce d modulo n before scalar multiplication; a d outside [1, n-1] is invalid because the order of the group is n, not p.

Real-World Applications

Bitcoin uses secp256k1 for transaction signatures (ECDSA in legacy transactions, Schnorr in Taproot), for the address derivation chain (the public key is hashed twice to produce the address), and for the Schnorr multi-signature scheme introduced in BIP-340 in 2021. The libsecp256k1 library, written by Pieter Wuille in 2013 and continuously audited since, is the canonical C implementation that backs Bitcoin Core. Ethereum uses secp256k1 for transaction signatures even though its address derivation uses Keccak-256 (see the Ethereum Keccak256 tutorial) instead of SHA-256 plus RIPEMD-160.

Tor onion services v3 use the ed25519 curve, not secp256k1, for the introduction point keys, so this tutorial does not apply directly to Tor. The Lightning Network uses secp256k1 for the payment channel signatures and for the Hashed Time-Lock Contract signatures that enable multi-hop payments. The Decred blockchain uses secp256k1 with Schnorr signatures for governance votes, demonstrating that the curve has applications beyond Bitcoin. The Zcash blockchain uses a different curve (BLS12-381) for its shielded transactions because the zk-SNARK proof system requires a pairing-friendly curve.

Conclusion

We saw that secp256k1 is the Koblitz-form elliptic curve y^2 = x^3 + 7 mod p that Bitcoin uses for every key and every signature, that the curve forms a cyclic group of order n with the generator G, and that public-key derivation is the scalar multiplication P = d * G. Next, study the Ethereum Keccak256 tutorial to learn the hash function Ethereum uses for address generation, then read the Nonce Reuse Attack tutorial to see the catastrophic consequences of reusing an AES-GCM nonce.

Test Your Knowledge
How did you find this?

Comments

Join the discussion! Sign in with your Google or Blogger account, or comment as Anonymous - no account needed. For quick questions, also reach me on Telegram @cytestch.

Comments