Short answer. secp256k1 is the elliptic curve y² = x³ + 7 over the prime field p = 2²⁵⁶ − 2³² − 977, defined in SEC 2 in 2000. Its group has a prime number of points (cofactor 1) and offers about 128-bit security. Its coefficients were chosen for speed, not generated from a seed: a = 0 simplifies the formulas, and the curve has an efficiently computable endomorphism that makes scalar multiplication faster. Bitcoin used it from its first commit in 2009, Ethereum followed, and Nostr and most EVM chains use it today. It is not in WebCrypto, TLS 1.3 or most secure hardware; outside blockchain protocols, prefer P-256 or Ed25519. On top of plain ECDSA, its ecosystems add rules of their own: only low-S signatures in Bitcoin relay policy and Ethereum transactions, public key recovery instead of transmitting keys, and Keccak-256 or double SHA-256 as the hash.
The parameters
SEC 2 v2.0 §2.4.1 defines the curve by six numbers (SEC 2):
| Parameter | Value | Meaning |
|---|---|---|
| p | 2²⁵⁶ − 2³² − 977 (0xffff…fffefffffc2f) | Field prime; its shape makes reduction modulo p cheap |
| a, b | 0, 7 | The curve y² = x³ + 7 |
| G | x = 0x79be667e…16f81798, y = 0x483ada77…fb10d4b8 | Base point |
| n | 0xffff…fffebaaedce6af48a03bbfd25e8cd0364141 | Order of G, prime |
| h | 1 | Cofactor: every point except infinity generates the whole group |
Nothing here has to be taken on trust, and nothing came from an unexplained seed. The script below checks the constants with plain Python integers:
G = (Gx, Gy)
print("p == 2^256 - 2^32 - 977:", hex(p) == "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f")
print("G is on y^2 = x^3 + 7: ", (Gy * Gy - Gx**3 - b) % p == 0)
print("n is a probable prime: ", probable_prime(n))
print("n * G is infinity: ", mul(n, G) is None)
t = p + 1 - n
print("trace t = p + 1 - n: ", t)
print("Hasse bound |t| <= 2*sqrt(p):", t * t <= 4 * p)
print("n < p, p - n has", (p - n).bit_length(), "bits: the group has n points, cofactor 1")
p == 2^256 - 2^32 - 977: True
G is on y^2 = x^3 + 7: True
n is a probable prime: True
n * G is infinity: True
trace t = p + 1 - n: 432420386565659656852420866390673177327
Hasse bound |t| <= 2*sqrt(p): True
n < p, p - n has 129 bits: the group has n points, cofactor 1
add and mul are textbook affine point addition and double-and-add (fine for checking constants, not for handling secrets: they are not constant-time). The trace matches the value published by SafeCurves. Because n is prime and equals the number of points, there are no small subgroups: the cofactor problems that complicate Curve25519 protocols do not exist here.
Why the shape makes it fast
Three properties of these numbers are deliberate:
- The prime is 2²⁵⁶ minus a small number, so reducing a 512-bit product modulo p takes a few multiplications by 2³² + 977 instead of a general division.
- a = 0 removes a multiplication from every point doubling.
- The endomorphism. Because p ≡ 1 (mod 3), there is a cube root of unity β modulo p, and the map (x, y) → (βx, y) sends every point to another point on the curve. That map is the same as multiplying by a cube root of unity λ modulo n. A scalar k can then be split into two halves of about 128 bits, k = k₁ + k₂λ, and kP computed as k₁P + k₂(λP), where λP costs one field multiplication. This is the GLV method (Gallant, Lambert, Vanstone, 2001).
The same script finds β and λ and checks the identity:
print("p % 3 == 1 (cube roots of unity exist mod p):", p % 3 == 1)
beta = pow(2, (p - 1) // 3, p)
lam = pow(3, (n - 1) // 3, n)
# Pick the lambda that matches this beta (there are two non-trivial cube roots of unity mod n).
if mul(lam, G) != (beta * Gx % p, Gy):
lam = lam * lam % n
print("beta^3 == 1 mod p, lambda^3 == 1 mod n:", pow(beta, 3, p) == 1, pow(lam, 3, n) == 1)
print("lambda * G == (beta * Gx, Gy): ", mul(lam, G) == (beta * Gx % p, Gy))
print("beta =", hex(beta))
print("lambda =", hex(lam))
p % 3 == 1 (cube roots of unity exist mod p): True
beta^3 == 1 mod p, lambda^3 == 1 mod n: True True
lambda * G == (beta * Gx, Gy): True
beta = 0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee
lambda = 0x5363ad4cc05c30e0a5261c028812645a122e22ea20816678df02967c1b23bd72
These are the constants libsecp256k1 uses. Hal Finney described the trick for Bitcoin in a February 2011 bitcointalk thread and expected verification to get 20 to 25% faster; his code was the starting point of Pieter Wuille's libsecp256k1. Bitcoin Core switched the GLV optimization on by default with the libsecp256k1 update in PR #20147 (Bitcoin Core 0.21, 2021), measured there at about 16% faster initial sync with signature checks enabled. How much any of this matters depends on the library: OpenSSL has no special code for secp256k1 and is much faster on P-256 (the numbers are in P-256 vs secp256k1).
Why Bitcoin and Ethereum use it
Bitcoin's first commit (August 2009) creates keys with OpenSSL's EC_KEY_new_by_curve_name(NID_secp256k1). Satoshi Nakamoto never explained the choice. The reasons usually given are properties of the curve rather than statements by its user: the Bitcoin wiki notes that it was "constructed in a special non-random way which allows for especially efficient computation" and that "unlike the popular NIST curves, secp256k1's constants were selected in a predictable way" (Bitcoin wiki). Ethereum kept Bitcoin's curve and replaced the hash: transactions and messages are hashed with Keccak-256, and the address is derived from the key with it.
Standards bodies caught up later. RFC 8812 (2020) registered ES256K for JOSE and COSE, and NIST SP 800-186 (2023) §H.2 says the curve "is allowed to be used for blockchain-related applications" (it is not one of the curves recommended for general use).
How strong it is
The best known attack on the discrete logarithm problem in this group is Pollard's rho, at about 0.886·√n ≈ 2¹²⁷·⁸ additions according to SafeCurves, the same figure as for P-256 and the usual "128-bit security". (The order-3 endomorphism can be used to speed up rho a little further on this curve, by a factor of about √3, which does not change the picture.) SafeCurves lists secp256k1 as not "safe", but for reasons that concern implementations, not the discrete logarithm:
| SafeCurves criterion | secp256k1 | What it means for you |
|---|---|---|
| rho, transfer, rigidity | pass | The discrete logarithm is hard and the constants are explained |
| CM discriminant | fail (D = −3) | The small discriminant is what creates the endomorphism; no attack follows from it |
| twist security | pass, 2¹⁰⁹·⁵ | Lower than the curve itself; matters only to code that skips point validation |
| ladder, complete formulas | fail | Constant-time, exception-free code takes more care than on Curve25519 |
| indistinguishability | fail | Points do not look like random strings, which matters for censorship-resistant protocols, not signatures |
The practical lesson is the same as for P-256: use a library written for the curve (libsecp256k1, @noble/curves, k256 in Rust, go-ethereum's binding), validate public keys on input, and never write your own scalar multiplication for secrets. A key that arrives as a point must be checked to lie on y² = x³ + 7 before use; skipping that is the classic invalid-curve attack, which is where the twist figure comes in. And as with every ECDSA curve, a repeated or biased nonce reveals the key (ECDSA nonce reuse).
Keys and addresses
A private key is a number d in [1, n − 1]; the public key is the point Q = dG. The same Q is written in several ways, and mixing them up is a common source of "wrong address" bugs. With d = 1, whose public key is G itself (a key everyone knows, so never use it):
compressed (33 bytes): 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
uncompressed (65 bytes): 0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
x-only, BIP-340 (32 bytes): 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
Ethereum address, keccak256(x || y)[12:]: 0x7e5f4552091a69125d5dfcb7b8c2659029395bdf
Bitcoin P2PKH from the compressed key: 1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH
Bitcoin P2PKH from the uncompressed key: 1EHNa6Q4Jz2uvNExL497mE43ikXhwF6kZm
- Compressed (SEC 1 §2.3.3):
02or03for even or odd y, then x. Bitcoin has used compressed keys by default for over a decade. Decompressing needs a square root modulo p, which for this p (p ≡ 3 mod 4) is a single exponentiation, y = (x³ + 7)^((p+1)/4). - Uncompressed:
04 ‖ x ‖ y. The same key gives a different Bitcoin address in this form, which is why old wallets that used uncompressed keys show other addresses for the same secret. - x-only (32 bytes): Taproot and Nostr use BIP-340 Schnorr signatures, whose keys drop the y-coordinate and imply even y.
- Ethereum address: the last 20 bytes of Keccak-256 over the 64-byte
x ‖ y, without the04prefix. Keccak-256 is not SHA3-256: the padding differs, and usinghashlib.sha3_256gives wrong addresses.
Low-S: one signature, two valid forms
An ECDSA signature (r, s) has a twin (r, n − s) that verifies just as well, because negating the nonce point does not change r. For P-256 in TLS nobody cares. For Bitcoin it mattered: the twin produced a different transaction ID for the same transaction, and services that tracked payments by ID could be fooled. Bitcoin Core 0.11.1 (2015) made the node "require the canonical 'low-s' encoding for ECDSA signatures when relaying or mining" (release notes), a relay policy rather than a consensus rule. Ethereum went further with EIP-2 (Homestead, 2016): "All transaction signatures whose s-value is greater than secp256k1n/2 are now considered invalid", while "the ECDSA recover precompiled contract remains unchanged and will keep accepting high s-values".
Generic libraries do not follow these rules, and blockchain libraries do. The lab signed 1,000 random messages with Node.js's OpenSSL, then verified one high-S signature and its low-S twin with three libraries:
const { msg, sig, s } = sample;
const low = Buffer.concat([sig.subarray(0, 32), Buffer.from((n - s).toString(16).padStart(64, "0"), "hex")]);
const opensslOk = (x) => verify("sha256", msg, { key: publicKey, dsaEncoding: "ieee-p1363" }, x);
console.log(`node:crypto verify high-S: ${opensslOk(sig)} low-S twin: ${opensslOk(low)}`);
console.log(`noble default high-S: ${secp256k1.verify(sig, msg, pub)} low-S twin: ${secp256k1.verify(low, msg, pub)}`);
console.log(`noble lowS: false high-S: ${secp256k1.verify(sig, msg, pub, { lowS: false })}`);
OpenSSL 3.5.8: 494 of 1000 signatures have s > n/2
node:crypto verify high-S: true low-S twin: true
noble default high-S: false low-S twin: true
noble lowS: false high-S: true
ethers 6.17.0 v=27 high-S: non-canonical s; use ._s
About half of OpenSSL's signatures are high-S, OpenSSL accepts both forms, @noble/curves rejects high-S on secp256k1 unless told otherwise, and ethers refuses to even construct a signature object from one. A system that signs with a general-purpose library or an HSM and submits to a blockchain must normalize: if s > n/2, replace it with n − s and flip the recovery bit. Google Cloud KMS documents that its secp256k1 keys return only low-S signatures; AWS KMS's ECC_SECG_P256K1 documentation makes no such promise, so normalize its output yourself. The DER and raw encodings these signatures travel in are covered in ECDSA signature formats.
Public key recovery
Given a signature, the message hash and one or two extra bits, the signer's public key can be computed, so Ethereum transactions carry no public key at all and Bitcoin signed messages verify against an address. The extra bits are the recovery id (v, yParity, or the header byte of a Bitcoin message signature). How recovery works, what can go wrong with it and how ecrecover behaves are explained in ECDSA public key recovery.
Where it is and is not supported
| Platform | secp256k1 |
|---|---|
WebCrypto (browsers, Deno, Node's crypto.subtle) | No; use @noble/curves or a WASM build of libsecp256k1 |
OpenSSL and Node.js crypto | Yes, with generic (slower) code |
| BoringSSL | No: it builds in only P-224, P-256, P-384 and P-521 |
| Java (SunEC) | Removed in JDK 16: Curve not supported: secp256k1; use Bouncy Castle |
| Apple CryptoKit, Secure Enclave, Android Keystore, TPM 2.0, PIV cards | No |
| Cloud KMS | AWS KMS ECC_SECG_P256K1; Google Cloud KMS EC_SIGN_SECP256K1_SHA256 (HSM protection only); Azure Key Vault P-256K with ES256K |
| TLS 1.3, X.509 in the Web PKI, WebAuthn | No |
The gap in phone and laptop secure hardware is why mobile wallets hold secp256k1 keys in software (protected by a key that does live in hardware) or rely on a separate hardware wallet.
Common mistakes
- Hashing with SHA3-256 instead of Keccak-256 for Ethereum, or single SHA-256 instead of the double SHA-256 Bitcoin uses in messages and transactions. The signature verifies against a different message, and recovery yields a different, valid-looking address.
- Submitting high-S signatures from OpenSSL, an HSM or a cloud KMS to Ethereum or the Bitcoin network. Normalize first.
- Mixing compressed and uncompressed keys when deriving Bitcoin addresses.
- Raw byte lengths. A 64-byte
r ‖ sand a 65-byter ‖ s ‖ vare different formats; libraries say so loudly, as in ethers's invalid raw signature length. - Assuming platform support. Code that worked on Java 11 fails on Java 17; WebCrypto never had the curve.
- Using secp256k1 where nothing requires it. For tokens, TLS, device identity or passkeys it brings no benefit and loses hardware support.
Sources
- SEC 2 v2.0: Recommended Elliptic Curve Domain Parameters, §2.4.1; SEC 1 v2.0, §2.3.3 (point encoding)
- NIST SP 800-186, Appendix H.2
- SafeCurves: rho, discriminant and twist tables
- Gallant, Lambert, Vanstone, "Faster Point Multiplication on Elliptic Curves with Efficient Endomorphisms", CRYPTO 2001; Hal Finney, Speeding up signature verification (2011); Bitcoin Core PR #20147
- Bitcoin Core 0.11.1 release notes (low-S relay policy); EIP-2
- RFC 8812 (
ES256K) - Key services: AWS KMS key specs, Google Cloud KMS algorithms, Azure Key Vault key types