Bitcoin Keys and Signatures
Cryptographic foundations of Bitcoin for agents. Private keys, public keys, ECDSA, Schnorr signatures, and key derivation.
Bitcoin Keys and Signatures
Bitcoin uses elliptic curve cryptography for key generation and transaction signing. Understanding these primitives is essential for agents that need to generate addresses, sign transactions, or verify ownership.
Key Hierarchy
Entropy (256 bits)
↓
Private Key (256-bit scalar)
↓ (elliptic curve multiplication)
Public Key (point on secp256k1)
↓ (hash functions)
Address (Base58/Bech32 encoded)
Private Keys
A private key is a 256-bit (32-byte) random number within the secp256k1 curve’s valid range.
Valid Range
1 to n-1, where n = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140
Formats
| Format | Example |
|---|---|
| Raw hex | e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35 |
| WIF | 5Kb8kLf9zgWQnogidDA76MzPL6TsZZY36hWXMssSzNydYXYB9KF |
| WIF-compressed | L53fCHmQhbNp1B4JipfBtfeHZH7cAibzG9oK19XfiFzxHgAkz6JK |
WIF Structure
Version (1 byte) + Key (32 bytes) + [Compression flag (1 byte)] + Checksum (4 bytes)
Generation
import secrets
# Generate cryptographically secure random bytes
private_key = secrets.token_bytes(32)
# Verify within valid range
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140
key_int = int.from_bytes(private_key, 'big')
assert 0 < key_int < n
Critical: Use cryptographically secure random number generators. Never use Math.random() or predictable sources.
Public Keys
A public key is a point on the secp256k1 elliptic curve, derived by multiplying the generator point G by the private key.
Public Key = Private Key × G
Formats
| Format | Size | Prefix |
|---|---|---|
| Uncompressed | 65 bytes | 04 |
| Compressed | 33 bytes | 02 or 03 |
Compressed Public Keys
Uncompressed: 04 + x (32 bytes) + y (32 bytes) = 65 bytes
Compressed: 02/03 + x (32 bytes) = 33 bytes
(02 if y is even, 03 if y is odd)
Best Practice: Always use compressed public keys (33 bytes).
Derivation Example
const secp256k1 = require('secp256k1');
const privateKey = Buffer.from('e8f32e...', 'hex');
const publicKey = secp256k1.publicKeyCreate(privateKey, true); // compressed
// Result: 033ea6d...
secp256k1 Curve Parameters
p = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
(field prime)
a = 0, b = 7
(curve: y² = x³ + 7)
G = (79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8)
(generator point)
n = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140
(curve order)
ECDSA Signatures
Elliptic Curve Digital Signature Algorithm—used for legacy and SegWit v0 transactions.
Signing Process
- Hash the message:
z = SHA256(SHA256(message)) - Generate random
k(nonce) - Calculate point
R = k × G - Calculate
r = R.x mod n - Calculate
s = k⁻¹ × (z + r × private_key) mod n - Signature is
(r, s)
DER Encoding
ECDSA signatures in Bitcoin use DER format:
30 <total-length>
02 <r-length> <r>
02 <s-length> <s>
Example:
30450221 00abc123... 0220 def456...
Low-S Requirement
Bitcoin enforces “low-s” signatures to prevent malleability:
if s > n/2:
s = n - s
Signature Hash Types
| Type | Value | Behavior |
|---|---|---|
| SIGHASH_ALL | 0x01 | Sign all inputs and outputs |
| SIGHASH_NONE | 0x02 | Sign inputs only |
| SIGHASH_SINGLE | 0x03 | Sign one output per input |
| SIGHASH_ANYONECANPAY | 0x80 | Combinable flag |
Schnorr Signatures (BIP-340)
Taproot (SegWit v1) uses Schnorr signatures with several advantages:
Benefits
| Feature | ECDSA | Schnorr |
|---|---|---|
| Size | 71-73 bytes | 64 bytes |
| Batch verification | No | Yes |
| Key aggregation | Complex | Native |
| Provable security | Assumed | Proven |
Signature Format
Schnorr signature = R (32 bytes) + s (32 bytes) = 64 bytes
Signing Process
- Generate nonce
kdeterministically (BIP-340 nonce function) - Calculate
R = k × G - Calculate
e = tagged_hash("BIP0340/challenge", R || P || m) - Calculate
s = k + e × private_key - Signature is
(R.x, s)
Tagged Hashes
BIP-340 uses domain-separated hashes:
tagged_hash(tag, msg) = SHA256(SHA256(tag) || SHA256(tag) || msg)
Key Derivation (HD Wallets)
Hierarchical Deterministic wallets derive multiple keys from a single seed.
BIP-32 Path Structure
m / purpose' / coin_type' / account' / change / address_index
Example: m/84'/0'/0'/0/0
│ │ │ │ └── First receiving address
│ │ │ └───── External (receiving) chain
│ │ └───────── First account
│ └────────────── Bitcoin mainnet
└────────────────── Native SegWit (BIP-84)
Common Derivation Paths
| Path | Standard | Address Type |
|---|---|---|
m/44'/0'/0' | BIP-44 | P2PKH (Legacy) |
m/49'/0'/0' | BIP-49 | P2SH-P2WPKH (Wrapped SegWit) |
m/84'/0'/0' | BIP-84 | P2WPKH (Native SegWit) |
m/86'/0'/0' | BIP-86 | P2TR (Taproot) |
Hardened vs. Non-Hardened
| Notation | Type | Leaks Parent Key? |
|---|---|---|
0' or 0h | Hardened | No |
0 | Non-hardened | Yes (with child private key) |
Rule: Always use hardened derivation for purpose, coin_type, and account levels.
Address Derivation
Private Key
↓ (EC multiply)
Public Key (compressed, 33 bytes)
↓ (SHA256)
SHA256 hash (32 bytes)
↓ (RIPEMD160)
Public Key Hash (20 bytes)
↓ (encoding)
Address
Code Example
const bitcoin = require('bitcoinjs-lib');
const { BIP32Factory } = require('bip32');
const ecc = require('tiny-secp256k1');
const bip32 = BIP32Factory(ecc);
// From seed
const seed = Buffer.from('...', 'hex');
const root = bip32.fromSeed(seed);
// Derive BIP-84 address
const child = root.derivePath("m/84'/0'/0'/0/0");
const { address } = bitcoin.payments.p2wpkh({
pubkey: child.publicKey,
network: bitcoin.networks.bitcoin
});
console.log(address); // bc1q...
Security Considerations
For Agents
- Never log private keys
- Use deterministic nonces (RFC 6979 or BIP-340)
- Validate all signatures before trusting
- Derive keys fresh for each operation
- Clear memory after key use
Common Vulnerabilities
| Vulnerability | Cause | Prevention |
|---|---|---|
| Weak entropy | Bad RNG | Use system CSPRNG |
| Nonce reuse | k collision | Deterministic k (RFC 6979) |
| Key leakage | Logging/exposure | Memory protection |
| Path reuse | Address reuse | Fresh derivation per tx |
Machine-Readable Summary
{
"topic": "bitcoin-keys-signatures",
"curve": "secp256k1",
"signature_schemes": ["ecdsa", "schnorr"],
"key_formats": {
"private": ["raw_hex", "wif", "wif_compressed"],
"public": ["uncompressed_65", "compressed_33"]
},
"derivation_paths": {
"bip44_legacy": "m/44'/0'/0'",
"bip84_segwit": "m/84'/0'/0'",
"bip86_taproot": "m/86'/0'/0'"
},
"libraries": ["secp256k1", "bitcoinjs-lib", "rust-bitcoin"]
}