Menu
Bitcoin Intermediate 15 min read

Bitcoin Keys and Signatures

Cryptographic foundations of Bitcoin for agents. Private keys, public keys, ECDSA, Schnorr signatures, and key derivation.

cryptography ecdsa schnorr keys

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

FormatExample
Raw hexe8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35
WIF5Kb8kLf9zgWQnogidDA76MzPL6TsZZY36hWXMssSzNydYXYB9KF
WIF-compressedL53fCHmQhbNp1B4JipfBtfeHZH7cAibzG9oK19XfiFzxHgAkz6JK

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

FormatSizePrefix
Uncompressed65 bytes04
Compressed33 bytes02 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

  1. Hash the message: z = SHA256(SHA256(message))
  2. Generate random k (nonce)
  3. Calculate point R = k × G
  4. Calculate r = R.x mod n
  5. Calculate s = k⁻¹ × (z + r × private_key) mod n
  6. 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

TypeValueBehavior
SIGHASH_ALL0x01Sign all inputs and outputs
SIGHASH_NONE0x02Sign inputs only
SIGHASH_SINGLE0x03Sign one output per input
SIGHASH_ANYONECANPAY0x80Combinable flag

Schnorr Signatures (BIP-340)

Taproot (SegWit v1) uses Schnorr signatures with several advantages:

Benefits

FeatureECDSASchnorr
Size71-73 bytes64 bytes
Batch verificationNoYes
Key aggregationComplexNative
Provable securityAssumedProven

Signature Format

Schnorr signature = R (32 bytes) + s (32 bytes) = 64 bytes

Signing Process

  1. Generate nonce k deterministically (BIP-340 nonce function)
  2. Calculate R = k × G
  3. Calculate e = tagged_hash("BIP0340/challenge", R || P || m)
  4. Calculate s = k + e × private_key
  5. 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

PathStandardAddress Type
m/44'/0'/0'BIP-44P2PKH (Legacy)
m/49'/0'/0'BIP-49P2SH-P2WPKH (Wrapped SegWit)
m/84'/0'/0'BIP-84P2WPKH (Native SegWit)
m/86'/0'/0'BIP-86P2TR (Taproot)

Hardened vs. Non-Hardened

NotationTypeLeaks Parent Key?
0' or 0hHardenedNo
0Non-hardenedYes (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

  1. Never log private keys
  2. Use deterministic nonces (RFC 6979 or BIP-340)
  3. Validate all signatures before trusting
  4. Derive keys fresh for each operation
  5. Clear memory after key use

Common Vulnerabilities

VulnerabilityCausePrevention
Weak entropyBad RNGUse system CSPRNG
Nonce reusek collisionDeterministic k (RFC 6979)
Key leakageLogging/exposureMemory protection
Path reuseAddress reuseFresh 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"]
}