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.

๐Ÿ’ก In Simple Terms

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:

sec โ€” Standards for Efficient Cryptography
p โ€” Prime field (the curve is over a prime field)
256 โ€” 256-bit prime (field size)
k โ€” Koblitz curve (special optimization for efficiency)
1 โ€” First variant of this type
โœจ Why Bitcoin Chose secp256k1
  • 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:

Private Key (d)
โ†’
Public Key (Q = d ร— G)
The public key is derived from the private key using elliptic curve multiplication โ€” easy to compute in one direction, impossible to reverse.
ComponentSizeFormat ExampleWho 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)
๐Ÿ’ก Key generation is easy: Pick a random 256-bit number (your private key). Multiply it by the generator point G (a fixed point on the curve). The result is your public key. You can't go backwards โ€” the math is one-way.

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).

Message (Transaction)
โ–ผ
Hash (SHA-256)
โ–ผ
z (message hash, 256-bit)
+
Private Key (d)
Random Nonce (k)
โ–ผ
ECDSA Signing Algorithm
โ–ผ
Signature: (r, s)
โš ๏ธ CRITICAL: The Nonce Must Be Random!

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.

Signature (r, s)
Message hash (z)
Public Key (Q)
โ–ผ
ECDSA Verify Algorithm
โ–ผ
โœ… Valid
or
โŒ Invalid
๐Ÿ” Security Guarantee

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.

Bitcoin transaction signature structure
Transaction Input:
  Previous TX: [hash of UTXO]
  Index: 0
  ScriptSig:  
  
Where:
   = DER-encoded ECDSA signature + SIGHASH flag
   = 33 or 65-byte public key
๐Ÿ”„ The SIGHASH Flag

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

AttackHow it worksMitigation 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:

yยฒ โ‰ก xยณ + 7 (mod p)

Where p is a 256-bit prime number:

p = 2ยฒโตโถ - 2ยณยฒ - 977
p = 0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F
๐Ÿงฎ Why This Specific Equation?

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)

p โ€” Prime field modulus: 0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F
a โ€” Curve coefficient: 0x00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
b โ€” Curve constant: 0x00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000007
Gโ‚“ โ€” Generator point x: 0x79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798
Gแตง โ€” Generator point y: 0x483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8
n โ€” Order of G (number of points): 0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141
h โ€” Cofactor: 1
๐ŸŽฏ Why the Order n Matters

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.

Point Addition: P + Q = R
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
๐Ÿ’ก Geometric intuition: Adding two points P and Q means drawing a line through them. It hits the curve at a third point -R. Reflect that point across the x-axis to get R = P+Q.

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:

Q = d ร— G = G + G + G + ... + G (d times)

This is done efficiently using the double-and-add algorithm (similar to modular exponentiation):

Double-and-Add Algorithm (Pseudo-code)
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
๐Ÿ”’ One-Way Function

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:

Input: Private key d, message hash z, curve parameters (G, n)

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)
โš ๏ธ The Nonce k Must Be Secret and Random!

If you reuse k for two different messages, an attacker can compute:

k = (zโ‚ - zโ‚‚) ร— (sโ‚ - sโ‚‚)โปยน mod n
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:

Input: Public key Q, message hash z, signature (r, s), curve parameters (G, n)

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.
๐Ÿ” Why Verification Works

If the signature was generated correctly:

uโ‚ร—G + uโ‚‚ร—Q = (zร—w)ร—G + (rร—w)ร—(dร—G) = w ร— (z + rร—d) ร— G = w ร— (sร—k) ร— G = k ร— w ร— s ร— G

Since w = sโปยน, this simplifies to k ร— G, whose x-coordinate is r. So verification passes.

2.7 Elliptic Curve Discrete Logarithm Problem

Given: Q = d ร— G
Find: d

The fastest known classical attack is the Pollard's rho algorithm, which takes about โˆšn โ‰ˆ 2ยนยฒโธ steps โ€” computationally infeasible.

Key SizeSecurity LevelAttack Cost (logโ‚‚)Feasibility
128-bit symmetric2ยนยฒโธ2ยนยฒโธInfeasible
256-bit ECC2ยนยฒโธ2ยนยฒโธInfeasible
3072-bit RSA2ยนยฒโธ2ยนยฒโธInfeasible
๐Ÿ’ก 2ยนยฒโธ is astronomically large: If you had a billion computers each trying a billion keys per second, it would still take longer than the age of the universe to find the key.

2.8 DER Encoding of Signatures

Bitcoin stores signatures in DER (Distinguished Encoding Rules) format:

0x30 || [total length] || 0x02 || [r length] || r || 0x02 || [s length] || s
Example DER signature
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.

Install required library
pip install ecdsa cryptography

3.2 Simple ECDSA with Python's `ecdsa` Library

Basic ECDSA signing and verification
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()}")
โš ๏ธ What does `BadSignatureError: Signature verification failed` mean?

If you see this error, it means the signature does NOT match the message and public key. Here's what could cause it:

SymptomLikely CauseHow to fix
Signature verification fails
BadSignatureError
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
๐Ÿ” How to Debug Signature Failures

If verification fails, try these steps:

Debugging checklist
# 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}")
โœ… Common Mistake โ€” Using the Wrong Hash Function

Bitcoin transactions use double SHA-256 (hashlib.sha256(hashlib.sha256(data).digest()).digest()). If you only hash once, verification will fail!

# Bitcoin's actual transaction hash
tx_hash = hashlib.sha256(hashlib.sha256(tx_data).digest()).digest()

3.3 Complete Python Implementation from Scratch

ecdsa_secp256k1.py โ€” Educational implementation
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

Proper error handling for signature operations
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}")
๐Ÿ“ฆ Import Statement

The exception class is imported directly from ecdsa:

from ecdsa import BadSignatureError

If you use from ecdsa.errors import BadSignatureError โ€” that will cause an ImportError!

3.4 Test Vectors

Private Key (hex)MessageExpected r (first 16 chars)Expected s (first 16 chars)
0000000000000000000000000000000000000000000000000000000000000001 "dev---test" 71f848f94719b9c 3b08ea4a44a1618

3.5 Running the Code

๐Ÿ’ป How to Run
  • 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!