Two strangers. No shared secret.
Communicate privately anyway.
This sounds impossible. For most of human history it was. In 1976 two mathematicians solved it. That solution is the reason your bank details travel safely, the reason Bitcoin has no central authority, and the reason you can prove you own a coin without revealing your private key. This page is how it works.
Imagine you want to send a secret message to someone you have never met.
You have never spoken. You share no secret. You have no secure channel. Every message you send can be read by everyone.
How do you communicate privately?
For thousands of years the answer was: you cannot. You need to meet first. Exchange a secret key. Then use that key to encrypt your messages.
But meeting is not always possible. And every meeting is a risk. Every key exchange is a vulnerability.
In 1976 Whitfield Diffie and Martin Hellman published a paper that changed everything. They proved that two strangers could establish a shared secret over a completely public channel.
Without ever meeting. Without ever exchanging a secret. With every eavesdropper watching. And still end up with a secret that only they knew.
This seems impossible. It is not. It is mathematics.
And once you understand it you understand the foundation of every secure system on Earth. Including Bitcoin.
A Bitcoin private key is a secret. A Bitcoin public key is derived from it. Proving you own Bitcoin requires a signature, and a signature proves you know the secret without revealing it.
Every Bitcoin transaction ever broadcast used what you are about to learn. This page is the beginning of that story.
The problem and the two solutions
Cryptography solves one problem. How do two parties communicate privately even when others can see their messages. Everything else is a consequence of that question.
There are two fundamental approaches. One is thousands of years old. One is fifty years old. Both are essential.
The simplest place to start is symmetric encryption, and the simplest symmetric cipher is a single XOR. It is the same gate from page four, applied byte by byte.
// XOR: the simplest cipher.
// XOR encryption: same operation for encrypt and decrypt.
// key XOR plaintext = ciphertext
// key XOR ciphertext = plaintext
// This is the foundation all stream ciphers build on.
fn xor_encrypt(plaintext: &[u8], key: &[u8]) -> Vec<u8> {
plaintext.iter()
.zip(key.iter().cycle()) // repeat key if shorter
.map(|(&p, &k)| p ^ k) // XOR each byte
.collect()
// XOR is the logic gate from page 4.
// AES is XOR applied with much more structure.
}
fn xor_decrypt(ciphertext: &[u8], key: &[u8]) -> Vec<u8> {
xor_encrypt(ciphertext, key) // identical: XOR is self-inverse
// XOR twice with the same key returns the original.
// This symmetry is what makes XOR useful for encryption.
}
fn main() {
let message = b"Send 1 BTC to Bob";
let key = b"secret_key_12345!";
let encrypted = xor_encrypt(message, key);
let decrypted = xor_decrypt(&encrypted, key);
assert_eq!(decrypted, message);
println!("Encrypted: {:02x?}", &encrypted[..8]);
// completely unrecognisable without the key
// XOR cipher weakness: if the key repeats it is breakable.
// AES solves this with proper key scheduling.
// Never use raw XOR for real encryption.
}#include <stdint.h>
#include <stdio.h>
#include <string.h>
/* XOR cipher: foundation of stream encryption.
* Symmetric: same key and operation for both directions.
* Weakness: trivially broken if the key is short or repeating.
* AES is what you use in production. */
void xor_crypt(const uint8_t *in, uint8_t *out,
size_t len,
const uint8_t *key, size_t key_len) {
for (size_t i = 0; i < len; i++)
out[i] = in[i] ^ key[i % key_len];
/* XOR: the same gate from page 4.
* Bit by bit. Every byte. Every message.
* AES: XOR with key scheduling, S-boxes, MixColumns.
* But the core operation is still this. */
}
int main(void) {
const char *msg = "Send 1 BTC to Bob";
const char *key = "secret_key_12345!";
size_t msg_len = strlen(msg);
uint8_t encrypted[64] = {0};
uint8_t decrypted[64] = {0};
xor_crypt((const uint8_t*)msg, encrypted,
msg_len, (const uint8_t*)key, strlen(key));
xor_crypt(encrypted, decrypted,
msg_len, (const uint8_t*)key, strlen(key));
printf("Decrypted: %s\n", (char*)decrypted);
/* Should match the original.
* XOR twice with the same key returns the original.
* This self-inverse property is fundamental. */
return 0;
}How public key cryptography works
Public key cryptography rests on one mathematical concept: a trapdoor function. Easy to compute in one direction. Computationally impossible to reverse. Unless you hold the trapdoor, which is the private key.
Three trapdoors are used in practice.
Diffie-Hellman is worth seeing with small numbers. Both sides agree on public parameters g = 5 and p = 23. Alice picks a private a, Bob picks a private b. Each sends g raised to their private number, mod p. Then each raises the other's value to their own private number. Both land on the same secret, and it never crossed the wire. Change a and b below and watch it hold.
Bitcoin key derivation
A Bitcoin address is the end of a one-way chain that starts at the private key.
The private key is a random 256-bit number, 32 bytes, the secret. The public key is that private key multiplied by the secp256k1 generator point G, giving a point on the curve, 33 bytes compressed. That multiplication is the trapdoor: you cannot run it backwards. The address is the public key hashed by SHA-256 then RIPEMD-160 to 20 bytes, then Base58Check encoded.
Every arrow goes one direction only. You cannot get from the address back to the public key, and you cannot get from the public key back to the private key. The intermediate code shows the hashing half of that chain, plus the commitment scheme it powers.
// Add to Cargo.toml:
// sha2 = "0.10"
// ripemd = "0.1"
use sha2::{Sha256, Digest};
use ripemd::Ripemd160;
// Bitcoin address derivation from a public key.
// Real Bitcoin uses secp256k1 ECDSA to make the key.
// This shows the hashing steps only.
fn hash160(public_key: &[u8]) -> [u8; 20] {
// Step 1: SHA-256 of the public key
let sha256_hash = Sha256::digest(public_key);
// Step 2: RIPEMD-160 of the SHA-256 hash
let ripemd_hash = Ripemd160::digest(sha256_hash);
ripemd_hash.into()
// 20 bytes: the core of a Bitcoin address.
// This is what gets Base58Check encoded.
}
// Commitment scheme: prove you knew a value
// before revealing it.
fn commit(secret: &[u8]) -> [u8; 32] {
Sha256::digest(secret).into()
// publish this. reveal the secret later.
// anyone verifies: SHA256(secret) == commitment.
// this is how pay-to-public-key-hash works:
// publish the hash of the public key,
// reveal the public key when spending.
}
fn verify_commitment(secret: &[u8],
commitment: &[u8; 32]) -> bool {
let hash: [u8; 32] = Sha256::digest(secret).into();
hash == *commitment
}
// XOR as the building block of stream ciphers.
// Rust: explicit types, bounds-checked iteration.
fn xor_stream(data: &[u8], keystream: &[u8]) -> Vec<u8> {
assert_eq!(data.len(), keystream.len(),
"keystream must be as long as data");
data.iter().zip(keystream).map(|(d, k)| d ^ k).collect()
// AES-CTR generates the keystream from key + nonce,
// then XORs with plaintext. This is the final step.
}#include <stdint.h>
#include <string.h>
#include <openssl/sha.h>
#include <openssl/ripemd.h>
/* compile: cc dh.c -lcrypto */
/* Bitcoin HASH160: SHA256 then RIPEMD160.
* The core of every P2PKH address. */
void hash160(const uint8_t *pubkey, size_t len,
uint8_t out[20]) {
uint8_t sha_out[32];
SHA256(pubkey, len, sha_out);
RIPEMD160(sha_out, 32, out);
/* 20-byte output. Base58Check encode it
* to produce a Bitcoin address like
* 1A1zP1eP5QGefi2DMPTfTL5SLmv7Divf. */
}
/* Commitment: publish the hash, reveal later.
* Used in P2PKH, sealed-bid auctions, and
* zero-knowledge protocols. */
int verify_commitment(const uint8_t *secret, size_t len,
const uint8_t commitment[32]) {
uint8_t h[32];
SHA256(secret, len, h);
return memcmp(h, commitment, 32) == 0;
/* returns 1 when the secret matches the commitment */
}
/* Simplified Diffie-Hellman with small numbers.
* NOT cryptographically secure. Concept only.
* Real DH uses 2048-bit or larger primes. */
uint64_t mod_pow(uint64_t base, uint64_t exp,
uint64_t mod) {
uint64_t result = 1;
base %= mod;
while (exp > 0) {
if (exp & 1) result = result * base % mod;
exp >>= 1;
base = base * base % mod;
}
return result;
}
void diffie_hellman_demo(void) {
uint64_t g = 5, p = 23; /* public parameters */
uint64_t a = 6; /* Alice private */
uint64_t b = 15; /* Bob private */
uint64_t A = mod_pow(g, a, p); /* Alice sends 8 */
uint64_t B = mod_pow(g, b, p); /* Bob sends 19 */
uint64_t alice_secret = mod_pow(B, a, p); /* = 2 */
uint64_t bob_secret = mod_pow(A, b, p); /* = 2 */
(void)alice_secret; (void)bob_secret;
/* both equal 2: shared secret established.
* eavesdropper saw g=5, p=23, A=8, B=19,
* and cannot compute 2 without a or b. */
}Digital signatures and Bitcoin transactions
A digital signature proves three things at once, without revealing your private key. Authentication: this message came from you. Integrity: the message was not modified. Non-repudiation: you cannot deny signing it.
The mechanism: hash the message, sign the hash with the private key, then publish the message, the signature and the public key. Anyone recomputes the hash and verifies it against the public key. If the message was altered, its hash no longer matches, verification fails, and the transaction is rejected.
Bitcoin uses ECDSA on the secp256k1 curve with 256-bit keys. Signatures are 71 to 72 bytes DER encoded. Every transaction input carries a scriptSig holding the DER signature and the compressed public key, and every node verifies the transaction hash against that signature and public key, then checks that the public key hashes to the address being spent. Any failure rejects the transaction.
secp256k1
Bitcoin chose one specific elliptic curve, secp256k1, for its efficient arithmetic and absence of known weaknesses. The curve is y squared = x cubed + 7, modulo a 256-bit prime.
Points on the curve form a group with a generator point G, and every public key is private_key times G under elliptic curve point multiplication. Recovering the private key from the public key and G is the elliptic curve discrete logarithm problem: no known efficient algorithm, O(2^128) with the best known attacks.
The Big O page called O(2^256) impossible. O(2^128) is also impossible at any computational scale. That is why a 256-bit key is enough.
The code below is the real thing. Rust uses the secp256k1 crate, C uses OpenSSL. Both generate a key pair, sign the double-SHA256 of a transaction, and verify it exactly as a Bitcoin node would.
// Add to Cargo.toml:
// secp256k1 = { version = "0.27", features = ["rand"] }
// sha2 = "0.10"
use secp256k1::{Secp256k1, Message};
use secp256k1::rand::rngs::OsRng;
use sha2::{Sha256, Digest};
fn bitcoin_sign_verify() {
let secp = Secp256k1::new();
// Generate a key pair. In real Bitcoin the private
// key is cryptographically secure random bytes.
let (private_key, public_key) =
secp.generate_keypair(&mut OsRng);
let tx_data = b"Send 1 BTC to Alice at address 1Alice...";
// Bitcoin signs the double-SHA256 of the transaction.
let hash1 = Sha256::digest(tx_data);
let hash2 = Sha256::digest(hash1);
let msg = Message::from_digest_slice(&hash2).unwrap();
// Sign with the private key. 71-72 bytes DER encoded.
// This goes into the scriptSig of the input.
let signature = secp.sign_ecdsa(&msg, &private_key);
// Verify with the public key.
// This is what every Bitcoin node does.
let valid = secp.verify_ecdsa(&msg, &signature, &public_key);
assert!(valid.is_ok());
println!("Signature valid: {}", valid.is_ok());
// 33-byte compressed public key. hash160 of this,
// then Base58Check, gives the Bitcoin address.
let _pubkey_bytes = public_key.serialize();
}#include <openssl/ec.h>
#include <openssl/ecdsa.h>
#include <openssl/obj_mac.h>
#include <openssl/sha.h>
#include <stdint.h>
#include <string.h>
#include <stdio.h>
/* compile: cc sign.c -lssl -lcrypto */
/* Bitcoin-style ECDSA signing using OpenSSL.
* Curve: NID_secp256k1, Bitcoin's curve. */
void bitcoin_sign_verify(void) {
EC_KEY *key = EC_KEY_new_by_curve_name(NID_secp256k1);
EC_KEY_generate_key(key);
const char *tx = "Send 1 BTC to Alice at 1Alice...";
/* Double SHA-256, Bitcoin's standard */
uint8_t hash1[32], hash2[32];
SHA256((const uint8_t*)tx, strlen(tx), hash1);
SHA256(hash1, 32, hash2);
/* Sign: produces a DER-encoded ECDSA signature */
uint8_t sig_buf[72];
unsigned sig_len = sizeof sig_buf;
ECDSA_sign(0, hash2, 32, sig_buf, &sig_len, key);
printf("Signature length: %u bytes\n", sig_len);
/* 71-72 bytes typically.
* This exact format goes into the scriptSig. */
/* Verify: what every Bitcoin node does */
int valid = ECDSA_verify(0, hash2, 32,
sig_buf, sig_len, key);
printf("Valid: %d\n", valid); /* 1 = valid */
/* Compressed public key, 33 bytes */
uint8_t pubkey[33];
const EC_POINT *pub = EC_KEY_get0_public_key(key);
EC_POINT_point2oct(EC_KEY_get0_group(key), pub,
POINT_CONVERSION_COMPRESSED,
pubkey, 33, NULL);
/* hash160(pubkey) is the Bitcoin address core */
EC_KEY_free(key);
}From private key to transaction: the full picture
Everything on this page runs every time you send Bitcoin. Your wallet holds a private key, a random 256-bit number. From it, secp256k1 derives your public key, and SHA-256 then RIPEMD-160 derive your address. The address is all you share; nobody can recover the public key from it, and nobody can recover the private key from the public key.
Where cryptography appears in ScrapyBytes
Cryptography is not a new subject so much as the previous 27 pages aimed at one goal: secrecy and proof. Here is where it reaches back.
XOR is the most important gate in cryptography: AES uses it every round, stream ciphers XOR keystream with plaintext, and SHA-256 is 64 rounds of XOR, AND, NOT and shifts. Cryptography is logic gates applied with purpose.
scrapybytes.vercel.app/logic-gates →HashingSHA-256 is in every Bitcoin operation: double SHA-256 for transaction IDs, SHA-256 for Merkle roots, SHA-256 then RIPEMD-160 for addresses. The hashing page gave the mechanism; this page is why one-way and avalanche make it secure.
scrapybytes.vercel.app/hashing →Big O NotationPrivate key to public key is effectively O(1); the reverse is O(2^128) with the best known attacks. That gap is the security. The Big O page proved O(2^256) impossible, and O(2^128) is impossible too. Cryptography is weaponised Big O.
scrapybytes.vercel.app/big-o →BinaryEvery private key is 256 random bits, every signature and ciphertext is bits. XOR is a gate on bits and AES is XOR with structure applied ten times. The binary page is the raw material.
scrapybytes.vercel.app/binary →BlockchainEvery transaction uses ECDSA, every block header SHA-256, every address SHA-256 and RIPEMD-160, every Merkle root SHA-256. The blockchain page described the system; this page is the primitives that secure it.
scrapybytes.vercel.app/blockchain →MemoryPrivate keys are 32 bytes, public keys 33, signatures 71, living on the stack or heap. Secure key handling means zeroing that memory after use: Rust drops and zeroes automatically, C needs an explicit memset.
scrapybytes.vercel.app/memory →GraphsThe secp256k1 curve is a group, and the discrete logarithm problem is finding a path through it. Lightning aggregates keys across the payment channel graph. Cryptography and graph theory meet in the most important financial network built.
scrapybytes.vercel.app/graphs →NetworkingEvery HTTPS connection uses Diffie-Hellman to exchange a key, AES to encrypt, and ECDSA for certificates. The networking page sent the packets; cryptography is what makes them private and tamper-evident.
scrapybytes.vercel.app/networking →Distributed SystemsBitcoin's answer to the Byzantine Generals Problem leans on cryptography: signatures stop impersonation, hashes stop tampering, proof of work uses hash difficulty. The problem was the previous arc; this is a large part of the solution.
scrapybytes.vercel.app/distributed-systems →PointersA private key is, in a sense, a pointer to your coins: whoever holds it controls them, and losing it loses them. A hardware wallet holds that pointer without ever letting it touch an internet-connected machine.
scrapybytes.vercel.app/pointers →RecursionThe Merkle commitments under every block are recursive hashing, and the modular arithmetic of signing recurses through the group structure. The cryptographic structures in Bitcoin are recursive at their core.
scrapybytes.vercel.app/recursion →Sorting AlgorithmsCryptographic systems still run on ordinary data structures: certificate transparency logs are Merkle trees, key servers are sorted databases. Security does not replace engineering. It extends it.
scrapybytes.vercel.app/sorting →