ECDSA secp256k1: Bitcoin's Digital Signature Algorithm
Complete guide with Theory, Mathematics, and Code โ understand how you prove ownership of bitcoins
1.1 What is ECDSA?
ECDSA (Elliptic Curve Digital Signature Algorithm) is the cryptographic algorithm Bitcoin uses to prove ownership of funds. When you spend bitcoins, you create a digital signature using your private key. Anyone can verify that signature with your public key โ without knowing your private key.
ECDSA lets you create a unique "cryptographic proof" that only you can make (because only you have your private key). Think of it like a signature on a check โ everyone can see the signature is yours, but only you can write it.
1.2 Why secp256k1?
The "secp256k1" curve was specifically chosen by Satoshi Nakamoto for Bitcoin. The name breaks down as:
- Efficient computation โ Koblitz curves allow faster scalar multiplication
- Avoids backdoor concerns โ Unlike NIST curves (which some suspected of NSA influence)
- 128-bit security level โ Matches Bitcoin's security needs perfectly
- Constant-time operations possible โ Resists side-channel attacks
1.3 The Key Pair: Private and Public Keys
ECDSA uses asymmetric cryptography โ two mathematically linked keys:
| Component | Size | Format Example | Who knows it? |
|---|---|---|---|
| Private Key | 256 bits (32 bytes) | 0x4c3f3f... (64 hex chars) | Only the owner |
| Public Key (uncompressed) | 65 bytes | 04 + x(32B) + y(32B) | Everyone |
| Public Key (compressed) | 33 bytes | 02/03 + x(32B) |
1.4 How ECDSA Signing Works (Non-Mathematical Overview)
To sign a transaction, you need three things: your private key, the message hash (transaction data), and a random nonce k. The signing process creates two numbers: (r, s).
If you reuse the same nonce k for two different messages signed with the same private key, anyone can compute your private key. This has happened in real Bitcoin hacks โ always use a cryptographically secure random number generator!
1.5 How ECDSA Signature Verification Works
Verification requires the message hash (z), the signature (r, s), and the public key (Q). The math proves that only the owner of the corresponding private key could have created this signature.
If the signature verifies, you can be mathematically certain that:
- The message was signed by the owner of the private key
- The message hasn't been altered since signing
- No one else could have forged the signature (assuming ECDSA is secure)
1.6 ECDSA in Bitcoin Transactions
Every Bitcoin transaction input contains a scriptSig that includes an ECDSA signature. The corresponding output's scriptPubKey contains the public key hash.
Transaction Input:
Previous TX: [hash of UTXO]
Index: 0
ScriptSig:
Where:
= DER-encoded ECDSA signature + SIGHASH flag
= 33 or 65-byte public key
Bitcoin adds a SIGHASH flag to the end of each signature (e.g., 01 for SIGHASH_ALL). This tells the verification which parts of the transaction are signed โ allowing advanced transaction types like multisignature and partial signing.
1.7 Common Attacks and Mitigations
| Attack | How it works | Mitigation in Bitcoin |
|---|---|---|
| Nonce Reuse | Same k used for two signatures โ private key revealed | Always use RFC 6979 (deterministic nonce from private key + message) |
| Weak Randomness | Predictable k allows key recovery | Use cryptographic RNG or deterministic ECDSA |
| Side-Channel Attacks | Timing/power analysis reveals k or d | Constant-time implementations in Bitcoin Core |
| Polynomial-Time Attacks | Quantum computers (Shor's algorithm) | Not yet feasible; research into quantum-resistant signatures (e.g., Lamport) |
2.1 The Elliptic Curve Equation
Bitcoin uses the secp256k1 curve defined over a prime field:
Where p is a 256-bit prime number:
p = 0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F
The constant 7 is small and arbitrary โ chosen to be a "nothing up my sleeve" number. The equation avoids special properties that could be backdoors. The prime p is close to 2ยฒโตโถ, making arithmetic efficient on 256-bit computers.
2.2 Curve Parameters (secp256k1)
The order n is the number of points on the curve (the group size). Private keys are integers in the range [1, n-1]. Because h = 1, every point on the curve (except the point at infinity) can be used as a public key.
2.3 Point Addition and Doubling
Elliptic curve points form an abelian group under the "point addition" operation.
Given P(xโ, yโ) and Q(xโ, yโ) with P โ Q:
slope s = (yโ - yโ)/(xโ - xโ) mod p
xโ = sยฒ - xโ - xโ mod p
yโ = s(xโ - xโ) - yโ mod p
Point Doubling: P + P = 2P
slope s = (3xโยฒ + a)/(2yโ) mod p
xโ = sยฒ - 2xโ mod p
yโ = s(xโ - xโ) - yโ mod p
2.4 Scalar Multiplication (The Core Operation)
Scalar multiplication is the heart of ECDSA. It's "multiplying" a scalar integer d by a point G to get a new point Q:
This is done efficiently using the double-and-add algorithm (similar to modular exponentiation):
function scalar_multiply(d, G):
result = point_at_infinity
for bit in binary(d) from most significant to least:
result = double(result)
if bit == 1:
result = add(result, G)
return result
Scalar multiplication is easy to compute (logโ n steps), but reversing it (given Q and G, find d) is the Elliptic Curve Discrete Logarithm Problem (ECDLP) โ believed to be computationally infeasible with classical computers.
2.5 ECDSA Signing (Complete Math)
To sign a message hash z (256 bits) with private key d:
1. Choose random integer k in [1, n-1] (the nonce)
2. Compute the point (xโ, yโ) = k ร G
3. Compute r = xโ mod n. If r = 0, go back to step 1.
4. Compute s = kโปยน ร (z + r ร d) mod n. If s = 0, go back to step 1.
Output: Signature (r, s)
If you reuse k for two different messages, an attacker can compute:
d = (sโ ร k - zโ) ร rโปยน mod n
This is why Bitcoin implementations use RFC 6979 โ deterministic nonce derived from private key and message, eliminating randomness issues.
2.6 ECDSA Verification (Complete Math)
To verify a signature (r, s) for message hash z using public key Q:
1. Verify r and s are in [1, n-1]. If not, reject.
2. Compute w = sโปยน mod n
3. Compute uโ = (z ร w) mod n
4. Compute uโ = (r ร w) mod n
5. Compute point (xโ, yโ) = uโ ร G + uโ ร Q
6. Accept if r โก xโ (mod n), otherwise reject.
If the signature was generated correctly:
Since w = sโปยน, this simplifies to k ร G, whose x-coordinate is r. So verification passes.
2.7 Elliptic Curve Discrete Logarithm Problem
Find: d
The fastest known classical attack is the Pollard's rho algorithm, which takes about โn โ 2ยนยฒโธ steps โ computationally infeasible.
| Key Size | Security Level | Attack Cost (logโ) | Feasibility |
|---|---|---|---|
| 128-bit symmetric | 2ยนยฒโธ | 2ยนยฒโธ | Infeasible |
| 256-bit ECC | 2ยนยฒโธ | 2ยนยฒโธ | Infeasible |
| 3072-bit RSA | 2ยนยฒโธ | 2ยนยฒโธ | Infeasible |
2.8 DER Encoding of Signatures
Bitcoin stores signatures in DER (Distinguished Encoding Rules) format:
3045022100DEADBEEF... (32 bytes r)022100CAFEBABE... (32 bytes s)01
โ โโโ โโโ โ
โ โโโ โโโ โโ SIGHASH flag
โ โโโ โโโโ s value (32 bytes)
โ โโโ โโโ s marker
โ โโโ โโ length of s
โ โโโโ r value (32 bytes)
โ โโโ r marker
โ โโ length of r
โโ SEQUENCE marker
3.1 Installation
For production use, always use audited libraries. For learning, we'll implement from scratch and also show the higher-level API.
pip install ecdsa cryptography
3.2 Simple ECDSA with Python's `ecdsa` Library
from ecdsa import SigningKey, SECP256k1
import hashlib
# Generate a private key
private_key = SigningKey.generate(curve=SECP256k1)
public_key = private_key.get_verifying_key()
# Message to sign
message = b"Send 1 BTC to address 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
message_hash = hashlib.sha256(message).digest()
# Sign
signature = private_key.sign_deterministic(message_hash, hashfunc=hashlib.sha256)
# Verify
verified = public_key.verify(signature, message_hash)
print(f"Signature valid: {verified}")
# Display keys
print(f"Private key (hex): {private_key.to_string().hex()}")
print(f"Public key (hex): {public_key.to_string('compressed').hex()}")
print(f"Signature (hex): {signature.hex()}")
If you see this error, it means the signature does NOT match the message and public key. Here's what could cause it:
| Symptom | Likely Cause | How to fix |
|---|---|---|
Signature verification failsBadSignatureError
|
Wrong public key | You're verifying with a different key than the one that signed the message |
| Message was altered after signing | The message hash changed โ ensure you're verifying the EXACT same bytes | |
| Corrupted signature | The signature was truncated, modified, or from a different algorithm | |
| Wrong signature format | DER encoding mismatch | Bitcoin uses DER-encoded signatures; some libraries use raw (r,s) pairs |
| Missing SIGHASH flag | Bitcoin signatures include a hash type byte at the end |
If verification fails, try these steps:
# 1. Check if the public key matches the private key
assert public_key.to_string() == private_key.get_verifying_key().to_string()
# 2. Verify the message hash hasn't changed
message_hash_v2 = hashlib.sha256(message).digest()
assert message_hash == message_hash_v2
# 3. Try raw (r,s) verification instead of DER
from ecdsa.util import sigencode_der, sigdecode_der
r, s = sigdecode_der(signature, 0)
print(f"r = {r}\ns = {s}")
# 4. Verify with explicit parameters
verified = public_key.verify(signature, message_hash,
hashfunc=hashlib.sha256,
sigdecode=sigdecode_der)
print(f"Explicit verification: {verified}")
Bitcoin transactions use double SHA-256 (hashlib.sha256(hashlib.sha256(data).digest()).digest()). If you only hash once, verification will fail!
tx_hash = hashlib.sha256(hashlib.sha256(tx_data).digest()).digest()
3.3 Complete Python Implementation from Scratch
import hashlib
import random
import hmac
# secp256k1 curve parameters
P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
G_X = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
G_Y = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
def mod_inverse(a, p):
"""Modular inverse using Fermat's little theorem (p prime)"""
return pow(a, p-2, p)
def point_add(p1, p2):
"""Add two points on secp256k1"""
if p1 is None: return p2
if p2 is None: return p1
x1, y1 = p1
x2, y2 = p2
if x1 == x2 and y1 != y2:
return None
if x1 == x2 and y1 == y2:
# Point doubling
slope = (3 * x1 * x1) * mod_inverse(2 * y1, P) % P
else:
# Point addition
slope = (y2 - y1) * mod_inverse(x2 - x1, P) % P
x3 = (slope * slope - x1 - x2) % P
y3 = (slope * (x1 - x3) - y1) % P
return (x3, y3)
def scalar_multiply(k, point):
"""Multiply point by scalar using double-and-add"""
result = None
addend = point
while k:
if k & 1:
result = point_add(result, addend)
addend = point_add(addend, addend)
k >>= 1
return result
def private_key_to_public_key(private_key_hex):
"""Derive public key from private key"""
d = int(private_key_hex, 16)
G = (G_X, G_Y)
public_point = scalar_multiply(d, G)
if public_point is None:
return None
# Compressed public key format
prefix = '02' if public_point[1] % 2 == 0 else '03'
return prefix + format(public_point[0], '064x')
def deterministic_nonce(private_key_hex, msg_hash_hex):
"""RFC 6979 deterministic nonce generation"""
private_key_bytes = bytes.fromhex(private_key_hex)
msg_hash_bytes = bytes.fromhex(msg_hash_hex)
v = b'\x01' * 32
k = b'\x00' * 32
k = hmac.new(k, v + b'\x00' + private_key_bytes + msg_hash_bytes, hashlib.sha256).digest()
v = hmac.new(k, v, hashlib.sha256).digest()
k = hmac.new(k, v + b'\x01' + private_key_bytes + msg_hash_bytes, hashlib.sha256).digest()
v = hmac.new(k, v, hashlib.sha256).digest()
while True:
v = hmac.new(k, v, hashlib.sha256).digest()
candidate = int.from_bytes(v, 'big')
if 1 <= candidate < N:
return candidate
def sign(private_key_hex, message):
"""ECDSA sign a message (deterministic nonce)"""
# Hash message
msg_hash = hashlib.sha256(message).digest()
z = int.from_bytes(msg_hash, 'big')
d = int(private_key_hex, 16)
# Deterministic nonce
k = deterministic_nonce(private_key_hex, msg_hash.hex())
# Compute r
G = (G_X, G_Y)
R = scalar_multiply(k, G)
r = R[0] % N
if r == 0:
raise Exception("r is zero, try again")
# Compute s
k_inv = mod_inverse(k, N)
s = (k_inv * (z + r * d)) % N
if s == 0:
raise Exception("s is zero, try again")
# DER encode signature (simplified)
r_hex = format(r, '064x')
s_hex = format(s, '064x')
return r_hex, s_hex
def verify(public_key_hex, message, r_hex, s_hex):
"""ECDSA verify a signature"""
# Parse public key
prefix = public_key_hex[:2]
x = int(public_key_hex[2:], 16)
# Recover y from x (simplified - assumes correct parity)
y_sq = (pow(x, 3, P) + 7) % P
y = pow(y_sq, (P+1)//4, P)
if (prefix == '03' and y % 2 == 0) or (prefix == '02' and y % 2 == 1):
y = P - y
Q = (x, y)
# Hash message
msg_hash = hashlib.sha256(message).digest()
z = int.from_bytes(msg_hash, 'big')
r = int(r_hex, 16)
s = int(s_hex, 16)
if not (1 <= r < N and 1 <= s < N):
return False
w = mod_inverse(s, N)
u1 = (z * w) % N
u2 = (r * w) % N
G = (G_X, G_Y)
point1 = scalar_multiply(u1, G)
point2 = scalar_multiply(u2, Q)
point_sum = point_add(point1, point2)
if point_sum is None:
return False
return (point_sum[0] % N) == r
# Example usage
if __name__ == '__main__':
# Demo private key (don't use for real funds!)
test_private_key = "4c3f3f5b2b5b2b5b2b5b2b5b2b5b2b5b2b5b2b5b2b5b2b5b2b5b2b5b2b5b"
message = b"Send 1 BTC to address 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
# Generate public key
pub_key = private_key_to_public_key(test_private_key)
print(f"Public key: {pub_key}")
# Sign
r, s = sign(test_private_key, message)
print(f"Signature r: {r}")
print(f"Signature s: {s}")
# Verify
valid = verify(pub_key, message, r, s)
print(f"Signature valid: {valid}")
3.3.1 Error Handling in Production Code
from ecdsa import SigningKey, VerifyingKey, SECP256k1, BadSignatureError
import hashlib
def safe_verify(public_key_hex, message, signature_hex):
"""
Safely verify an ECDSA signature with proper error handling
"""
try:
# Convert hex to bytes
pub_key_bytes = bytes.fromhex(public_key_hex)
sig_bytes = bytes.fromhex(signature_hex)
# Load the verifying key
verifying_key = VerifyingKey.from_string(pub_key_bytes, curve=SECP256k1)
# Hash the message (Bitcoin style: double SHA-256)
msg_hash = hashlib.sha256(hashlib.sha256(message).digest()).digest()
# Attempt verification
verifying_key.verify(sig_bytes, msg_hash, hashfunc=hashlib.sha256)
return True, "Signature is valid"
except BadSignatureError:
return False, "Signature verification failed - the signature does not match"
except ValueError as e:
return False, f"Invalid format: {e}"
except Exception as e:
return False, f"Unexpected error: {e}"
# Example usage
private_key = SigningKey.generate(curve=SECP256k1)
public_key = private_key.get_verifying_key()
message = b"Send 1 BTC"
# Good signature
sig = private_key.sign_deterministic(message, hashfunc=hashlib.sha256)
valid, msg = safe_verify(public_key.to_string().hex(), message, sig.hex())
print(f"Good signature: {msg}")
# Tampered message
tampered_message = b"Send 100 BTC"
valid, msg = safe_verify(public_key.to_string().hex(), tampered_message, sig.hex())
print(f"Tampered message: {msg}")
# Random bytes as signature
valid, msg = safe_verify(public_key.to_string().hex(), message, "deadbeef" * 32)
print(f"Corrupt signature: {msg}")
The exception class is imported directly from ecdsa:
If you use from ecdsa.errors import BadSignatureError โ that will cause an ImportError!
3.4 Test Vectors
| Private Key (hex) | Message | Expected r (first 16 chars) | Expected s (first 16 chars) |
|---|---|---|---|
| 0000000000000000000000000000000000000000000000000000000000000001 | "dev---test" | 71f848f94719b9c | 3b08ea4a44a1618 |
3.5 Running the Code
- Copy the complete code into
ecdsa_secp256k1.py - Run with:
python ecdsa_secp256k1.py - No external dependencies needed (uses only built-in modules)
- โ ๏ธ This implementation is for educational purposes only โ use audited libraries like `ecdsa` or `cryptography` for production!