Cryptographic Foundations of Blockchain
Hash functions, digital signatures, and Merkle trees — the three primitives that let strangers agree on truth without trusting each other. Built from first principles with real math, concrete examples, and interactive visualizations.
1. Why cryptography is the foundation
A distributed ledger sounds deceptively simple: a list of transactions that everyone can read and no one can alter. But without a trusted central authority, how do you stop participants from lying? If Alice claims she sent 1 BTC to Bob, why should any stranger believe her? And how can 10,000 nodes around the world agree on the same history of events without ever talking to a central server?
The answer is cryptography. Three primitives do all the heavy lifting:
- Hash functions — take any data (a transaction, a block, a public key) and produce a fixed-size fingerprint. Any modification to the input changes the fingerprint completely. This makes tampering detectable without sharing the original data.
- Digital signatures — let Alice prove she authorized a transaction without ever revealing her secret key. Only Alice could have produced the signature, but anyone can verify it with her public key.
- Merkle trees — let you verify that a single transaction is included in a block containing thousands of transactions by checking only $O(\log n)$ hashes, not the whole block.
Bitcoin and Ethereum do not eliminate trust — they encode it in mathematics. Every node independently verifies every claim using the same deterministic rules. Disagreement is impossible because math has no politics. There is no committee deciding whether a transaction is valid; there is only computation.
Strip these three primitives out and what remains is just a replicated database that any participant can corrupt. The cryptography is not a feature added on top — it is the entire reason a decentralized ledger can exist.
2. Hash functions (SHA-256 and Keccak-256)
A cryptographic hash function maps an arbitrary-length bit string to a fixed-length digest:
Any input, no matter how long, is compressed to exactly 256 bits. The function must satisfy five properties to be cryptographically useful:
| Property | Informal meaning | Security level |
|---|---|---|
| Deterministic | Same input always gives same output | Required by definition |
| Efficient | Computing $H(x)$ takes nanoseconds | — |
| Preimage resistance | Given $h$, finding any $x$ with $H(x)=h$ is infeasible | $2^{256}$ operations |
| Second preimage resistance | Given $x$, finding $x' \neq x$ with $H(x')=H(x)$ is infeasible | $2^{256}$ operations |
| Collision resistance | Finding any pair $x \neq x'$ with $H(x)=H(x')$ is infeasible | $2^{128}$ operations (birthday bound) |
| Avalanche effect | Flipping 1 bit of input flips ~50% of output bits | Essential for security |
Note the asymmetry between preimage resistance ($2^{256}$) and collision resistance ($2^{128}$). The birthday paradox guarantees that collisions exist with $2^{128}$ trials even for a perfect 256-bit hash — but $2^{128}$ is still absurdly large. At $10^{18}$ hashes per second (an impossibly fast hypothetical computer), finding a collision would take $\sim 10^{20}$ years.
SHA-256: the Merkle-Damgård construction
SHA-256 processes input in 512-bit blocks using the Merkle-Damgård construction. The message is padded to a multiple of 512 bits (the padding always starts with a 1 bit followed by zeros and ends with the 64-bit message length). Each block is fed into a compression function $f$ together with the current state:
f. The final state is the hash output.Inside the compression function, SHA-256 runs 64 rounds of bit mixing using rotations, XORs, bitwise AND/OR, and modular addition — specifically designed to maximize diffusion so every output bit depends on every input bit.
Bitcoin vs. Ethereum: SHA-256 vs. Keccak-256
Bitcoin uses double-SHA-256 everywhere — the hash of a hash:
The double application eliminates a theoretical length-extension attack against Merkle-Damgård hashes.
Ethereum uses Keccak-256 — the original SHA-3 submission by the Keccak team. Note carefully: Keccak-256 is not NIST SHA-3. When NIST standardized SHA-3 in 2015 they changed the padding. Ethereum uses the pre-NIST variant. This trips up many developers using standard SHA-3 libraries.
Avalanche effect: a concrete example
The table below shows SHA-256 outputs for three nearly identical inputs. Notice that changing a single character or capitalizing the first letter produces a completely different digest — on average ~128 out of 256 bits flip:
| Input | SHA-256 hex (first 32 chars shown) | Bits different from "hello" |
|---|---|---|
"hello" |
2cf24dba5fb0a30e26e83b2ac5b9e29e… |
— (reference) |
"hello!" |
334d016f755cd6dc58c53a86e183882f… |
~130 bits (~51%) |
"Hello" |
185f8db32921bd46d35c0984c81b7e6b… |
~127 bits (~50%) |
This is the avalanche effect in action. The compression function is specifically designed so that any single-bit input change propagates through all 64 rounds and flips roughly half the output bits.
3. Merkle trees
A Merkle tree is a binary hash tree that lets you prove the inclusion of a single item in a large set by sharing only $O(\log n)$ hashes. Every block header in Bitcoin and Ethereum stores a single 32-byte Merkle root that commits to all transactions in that block.
Construction
Given $n$ transactions $\text{tx}_0, \ldots, \text{tx}_{n-1}$:
- Leaf nodes: $L_i = H(\text{tx}_i)$
- Internal nodes: $N = H(N_\text{left} \| N_\text{right})$ where $\|$ denotes concatenation
- Root: the single hash at the apex — this is what goes in the block header
If $n$ is odd, the last leaf is duplicated to form a pair. The tree has depth $\lceil \log_2 n \rceil$, so for a block with 2000 transactions the depth is 11.
Merkle proofs (SPV proofs)
A Merkle proof for transaction $\text{tx}_i$ in a tree of $n$ transactions consists of $\lceil \log_2 n \rceil$ sibling hashes — one per level of the tree. The verifier:
- Computes $L_i = H(\text{tx}_i)$
- At each level, hashes the current value with its sibling (from the proof)
- Checks that the computed root matches the block header's Merkle root
For Bitcoin's largest blocks (~4000 transactions), a Merkle proof is only 12 hashes = 384 bytes. A lightweight wallet (SPV client) can verify any transaction without downloading the whole block.
The Merkle root is cryptographically bound to all transactions in the block. If an attacker modifies even one byte of any transaction, every hash on the path from that leaf to the root changes — and the root no longer matches the block header that miners have committed to via proof-of-work.
Click any transaction leaf to see its Merkle proof path.
4. Public-key cryptography (ECDSA)
Bitcoin and Ethereum use the Elliptic Curve Digital Signature Algorithm (ECDSA) over the secp256k1 curve. Understanding it requires a brief look at elliptic curve arithmetic.
The secp256k1 curve
The curve is defined over a large prime field $\mathbb{F}_p$:
where $p = 2^{256} - 2^{32} - 977$, a 256-bit prime. "Points" on the curve are pairs $(x, y)$ satisfying this equation, plus a special "point at infinity" $\mathcal{O}$ that acts as the identity element.
Elliptic curves form a group under a geometrically defined addition law: adding two points $P$ and $Q$ gives a third point $R = P + Q$. Scalar multiplication $k \cdot P$ means adding $P$ to itself $k$ times. The secp256k1 generator point $G$ is a fixed public constant with prime order:
Key generation
Computing $Q = d \cdot G$ is fast (double-and-add algorithm, $O(\log d)$ point additions). But recovering $d$ from $Q$ — the Elliptic Curve Discrete Logarithm Problem (ECDLP) — has no known polynomial-time algorithm on classical computers. The best known attack (Pollard's rho) requires $O(\sqrt{n}) \approx 2^{128}$ operations. This asymmetry is the entire basis of elliptic curve cryptography.
Key derivation is a one-way pipeline: multiply private key by the curve generator G to get the public key, hash the public key, take the last 20 bytes as the address.
ECDSA signing
Given a message hash $z$ (a 256-bit integer) and private key $d$:
- Pick a cryptographically random nonce $k \in [1, n-1]$
- Compute the curve point $R = k \cdot G$; let $r = R_x \bmod n$ (the x-coordinate mod $n$). If $r = 0$, pick a new $k$.
- Compute $s = k^{-1}(z + r \cdot d) \bmod n$. If $s = 0$, pick a new $k$.
- The signature is the pair $(r,\, s)$
If the same nonce $k$ is used for two different messages $z_1$ and $z_2$, an attacker can recover the private key $d$. This is how Sony's PS3 signing key was extracted in 2010 — they used a constant "random" nonce. Always use a CSPRNG or RFC 6979 deterministic nonce derivation.
ECDSA verification
Given message hash $z$, signature $(r, s)$, and public key $Q$:
- Compute $u_1 = s^{-1} z \bmod n$ and $u_2 = s^{-1} r \bmod n$
- Compute the curve point $P = u_1 G + u_2 Q$
- The signature is valid if and only if $P_x \equiv r \pmod{n}$
Substituting $Q = d \cdot G$ and $s = k^{-1}(z + rd)$:
So $P_x = R_x = r$, which is exactly the check. The verification is a proof that whoever produced the signature knew $d$ (without revealing it), because only knowing $d$ allows computing an $s$ that causes $P_x = r$.
Signing uses the private key d; verification uses only the public key Q. The nonce k must be unique per signature — reusing k reveals d.
5. Wallets and addresses
Ethereum address derivation
An Ethereum address is derived deterministically from a private key in four steps:
- Generate private key $d$: 32 random bytes from a CSPRNG
- Compute public key $Q = d \cdot G$: the uncompressed secp256k1 point, represented as two 256-bit integers $(x, y)$ concatenated — 64 bytes total (the 0x04 prefix byte is dropped)
- Hash the public key: $h = \text{Keccak256}(Q)$ — a 32-byte digest
- Take the last 20 bytes of $h$, prefix with
0x
Addresses are case-insensitive on-chain, but EIP-55 defines a checksum encoding using mixed case that encodes 4 bits of error-detection per character: the $i$-th hex character is uppercased if the $i$-th nibble of the Keccak hash of the lowercase address is $\geq 8$.
Bitcoin address derivation
Bitcoin uses a slightly different pipeline. For the most common format (P2PKH):
- Generate private key $d$ (random 32 bytes)
- Compute compressed public key: $(x,\, \text{sign}(y))$ — 33 bytes
- Compute $h_1 = \text{SHA256}(Q_\text{compressed})$
- Compute $h_2 = \text{RIPEMD160}(h_1)$ — 20-byte "public key hash"
- Add version byte (0x00 for mainnet), compute double-SHA256 checksum (4 bytes), encode as Base58Check
BIP-32: Hierarchical Deterministic (HD) wallets
A seed phrase (12 or 24 BIP-39 mnemonic words) encodes 128 or 256 bits of entropy. From the seed, BIP-32 derives a tree of key pairs using HMAC-SHA512:
The 64-byte output is split: the left 32 bytes are added to the parent key (mod $n$) to derive the child private key; the right 32 bytes become the child chain code. The path notation m/44'/60'/0'/0/0 describes the BIP-44 derivation path for the first Ethereum address. A single seed phrase controls the entire hierarchy — back it up and you recover every derived account.
6. Digital signatures in transactions
Bitcoin UTXO model
Bitcoin uses an Unspent Transaction Output (UTXO) model. Every transaction:
- Consumes one or more UTXOs (inputs) — each input references a previous output by its transaction ID and output index, and provides an unlocking script (
scriptSig) - Creates one or more new UTXOs (outputs) — each output specifies an amount and a locking script (
scriptPubKey)
For the most common output type, P2PKH (Pay-to-Public-Key-Hash), validation proceeds as follows:
| Script | Contents | Purpose |
|---|---|---|
scriptPubKey |
OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG |
Locks funds to a public key hash |
scriptSig |
<sig> <pubKey> |
Provides signature and public key |
| Combined execution | Stack machine evaluates both together | Succeeds iff pubKey hashes to pubKeyHash AND sig verifies |
The Bitcoin Script stack machine concatenates scriptSig and scriptPubKey and executes them. OP_CHECKSIG pops a signature and public key, hashes the current transaction (with the output being spent), and verifies the ECDSA signature. If it returns 1, the UTXO is validly spent.
The transaction hash that is signed (SIGHASH_ALL mode) covers: version, inputs (prev txid + index + sequence), outputs (value + script), and locktime. Crucially it does not include the scriptSig being constructed — otherwise the signature would need to sign itself.
Ethereum account model
Ethereum uses an account-based model instead of UTXOs. An Externally Owned Account (EOA) transaction contains:
noncegasPricegasLimittovaluedatav, r, sv encodes the recovery ID (allows recovering the public key from the signature alone)The signing hash for a legacy Ethereum transaction is:
The chainId was introduced by EIP-155 to prevent replay attacks across networks (e.g., the Ethereum / Ethereum Classic split). Without it, a transaction valid on mainnet is also valid on any test network using the same address.
7. Hash pointers and the chain structure
The "chain" in blockchain comes from a specific data structure: each block header contains the hash of the previous block header. This creates a chain of hash pointers stretching back to the genesis block.
Block header structure
A Bitcoin block header is exactly 80 bytes:
| Field | Size | Description |
|---|---|---|
| Version | 4 bytes | Block version number |
| Previous block hash | 32 bytes | SHA256d of the previous block header — the chain link |
| Merkle root | 32 bytes | Commits to all transactions in this block |
| Timestamp | 4 bytes | Unix timestamp of block creation |
| Bits | 4 bytes | Compact encoding of the current difficulty target |
| Nonce | 4 bytes | Value miners iterate over to find a valid proof-of-work |
Proof-of-work requires that the block header hash satisfy:
where the target is a 256-bit number encoding the required number of leading zero bits. Miners increment the nonce (and occasionally the timestamp and coinbase extra nonce) until they find a header that hashes below the target.
prev_hash field (pink) contains the exact SHA256d hash of the previous block header. The Merkle root (cyan) commits to all transactions. The block hash (yellow) must satisfy the proof-of-work difficulty target.Why the chain is immutable
Suppose an attacker wants to change a transaction in block $N$. They must:
- Modify the transaction, which changes that block's Merkle root
- Re-mine block $N$ to find a new nonce satisfying proof-of-work (expected cost: $\sim 10^{20}$ hashes at current difficulty)
- Block $N+1$ references the old hash of block $N$, so it's now orphaned — re-mine block $N+1$ too
- Repeat for every subsequent block
Meanwhile, honest miners continue extending the real chain. The attacker must outpace the entire honest hash rate — and keep doing so indefinitely. This is the 51% attack threshold. The hash chain converts computational work into historical immutability.
8. Interactive: avalanche effect visualizer
The widget below demonstrates the avalanche property — a small change to the input produces a completely different hash output. Because we cannot run SHA-256 natively in the browser without a library, this uses a simulated hash (djb2 applied four times to produce a 128-bit output). The behavior is qualitatively identical to SHA-256's avalanche effect.
Simulated hash (djb2×4) — for illustration only, not SHA-256
9. Source code
Source code — Blockchain Cryptography (Pseudocode · Python · JavaScript · C · C++ · Java · Go)
========== SHA-256 STRUCTURE ==========
function SHA256(message):
# 1. Pre-processing: padding
padded = message ++ bit(1) ++ zeros ++ length_64bit
# padded length is a multiple of 512 bits
# 2. Initialize hash values (first 32 bits of sqrt of first 8 primes)
H = [H0, H1, H2, H3, H4, H5, H6, H7]
# 3. Process each 512-bit (64-byte) block
for each 512-bit block B in padded:
W = message_schedule(B) # 64 × 32-bit words
a, b, c, d, e, f, g, h = H
for i in 0..63:
S1 = ROTR(e,6) XOR ROTR(e,11) XOR ROTR(e,25)
ch = (e AND f) XOR ((NOT e) AND g)
T1 = h + S1 + ch + K[i] + W[i] (mod 2^32)
S0 = ROTR(a,2) XOR ROTR(a,13) XOR ROTR(a,22)
maj = (a AND b) XOR (a AND c) XOR (b AND c)
T2 = S0 + maj (mod 2^32)
h=g; g=f; f=e; e=d+T1
d=c; c=b; b=a; a=T1+T2
H = H + [a,b,c,d,e,f,g,h] (component-wise mod 2^32)
return concat(H)
========== MERKLE TREE ==========
function build_merkle_tree(transactions):
leaves = [SHA256d(tx) for tx in transactions]
if len(leaves) is odd: leaves.append(leaves[-1]) # duplicate last
tree = [leaves]
current = leaves
while len(current) > 1:
next_level = []
for i in 0, 2, 4, ..., len(current)-2:
next_level.append(SHA256d(current[i] ++ current[i+1]))
if len(next_level) is odd: next_level.append(next_level[-1])
tree.append(next_level)
current = next_level
return tree[-1][0] # Merkle root
function merkle_proof(tree, leaf_index):
proof = []
idx = leaf_index
for level in tree[:-1]: # skip root level
sibling = idx XOR 1 # 0->1, 1->0, 2->3, 3->2, …
proof.append((level[sibling], position: LEFT if sibling"""
Blockchain Cryptography — Python examples
Requires: pip install ecdsa web3 eth-account
"""
import hashlib, hmac, struct
from ecdsa import SigningKey, SECP256k1, util
from eth_account import Account
from web3 import Web3
# ── SHA-256 ─────────────────────────────────────────────────────────
def sha256(data: bytes) -> bytes:
return hashlib.sha256(data).digest()
def sha256d(data: bytes) -> bytes:
"""Bitcoin-style double SHA-256"""
return hashlib.sha256(hashlib.sha256(data).digest()).digest()
def keccak256(data: bytes) -> bytes:
"""Ethereum Keccak-256 (NOT the same as NIST SHA-3!)"""
from Crypto.Hash import keccak
k = keccak.new(digest_bits=256)
k.update(data)
return k.digest()
# Avalanche example
for msg in [b"hello", b"hello!", b"Hello"]:
print(f"{msg!r:12s} -> {sha256(msg).hex()}")
# ── Merkle Tree ──────────────────────────────────────────────────────
class MerkleTree:
def __init__(self, transactions: list[bytes]):
self.leaves = [sha256d(tx) for tx in transactions]
if len(self.leaves) % 2 == 1:
self.leaves.append(self.leaves[-1])
self.tree = self._build(self.leaves)
def _build(self, level):
if len(level) == 1:
return [level]
next_level = []
for i in range(0, len(level), 2):
next_level.append(sha256d(level[i] + level[i+1]))
if len(next_level) % 2 == 1:
next_level.append(next_level[-1])
return [level] + self._build(next_level)
@property
def root(self) -> bytes:
return self.tree[-1][0]
def proof(self, leaf_index: int) -> list[tuple[bytes, str]]:
"""Returns list of (sibling_hash, 'left'|'right')"""
proof = []
idx = leaf_index
for level in self.tree[:-1]:
sib = idx ^ 1
if sib < len(level):
side = 'left' if sib < idx else 'right'
proof.append((level[sib], side))
idx //= 2
return proof
def verify(self, tx: bytes, proof: list, root: bytes) -> bool:
h = sha256d(tx)
for sibling, side in proof:
if side == 'left':
h = sha256d(sibling + h)
else:
h = sha256d(h + sibling)
return h == root
txs = [b"tx0: Alice->Bob 1 BTC", b"tx1: Bob->Carol 0.5 BTC",
b"tx2: Carol->Dave 0.3 BTC", b"tx3: Dave->Eve 0.1 BTC"]
mt = MerkleTree(txs)
print("Merkle root:", mt.root.hex())
proof = mt.proof(2)
assert mt.verify(txs[2], proof, mt.root), "proof failed!"
print("Proof for tx2 verified in", len(proof), "hashes")
# ── ECDSA with secp256k1 ─────────────────────────────────────────────
sk = SigningKey.generate(curve=SECP256k1)
vk = sk.get_verifying_key()
message = b"Transfer 1 BTC to Bob"
msg_hash = sha256d(message)
# Sign
sig = sk.sign_digest(msg_hash, sigencode=util.sigencode_der)
print("DER signature:", sig.hex())
# Verify
assert vk.verify_digest(sig, msg_hash, sigdecode=util.sigdecode_der)
print("Signature verified!")
# ── Ethereum address derivation ──────────────────────────────────────
# Using eth-account (which uses Keccak-256 correctly)
acct = Account.create()
print("Private key:", acct.key.hex())
print("Address: ", acct.address)
# Manual derivation for illustration
import os
from eth_keys import keys
private_key_bytes = os.urandom(32)
pk = keys.PrivateKey(private_key_bytes)
pub = pk.public_key
pub_bytes = pub.to_bytes() # 64 bytes, no 0x04 prefix
keccak_hash = Web3.keccak(pub_bytes)
address = "0x" + keccak_hash[-20:].hex()
print("Derived address:", address)/**
* Blockchain Cryptography — JavaScript examples
* Uses the Web Crypto API (browser / Node >=15) and ethers.js
*/
// ── SHA-256 via Web Crypto ───────────────────────────────────────────
async function sha256(data) {
// data: string or Uint8Array
const bytes = typeof data === 'string'
? new TextEncoder().encode(data)
: data;
const buf = await crypto.subtle.digest('SHA-256', bytes);
return new Uint8Array(buf);
}
async function sha256hex(str) {
const hash = await sha256(str);
return Array.from(hash).map(b => b.toString(16).padStart(2,'0')).join('');
}
// Avalanche demo
(async () => {
for (const msg of ['hello', 'hello!', 'Hello']) {
console.log(msg.padEnd(8), '->', await sha256hex(msg));
}
})();
// ── Merkle Tree ──────────────────────────────────────────────────────
class MerkleTree {
constructor(txHashes) {
// txHashes: array of Uint8Array (already hashed leaves)
let level = [...txHashes];
if (level.length % 2 === 1) level.push(level[level.length - 1]);
this.levels = [level];
this._buildSync = false; // async build required
}
static async fromTransactions(txStrings) {
const leaves = await Promise.all(
txStrings.map(tx => sha256(tx))
);
return MerkleTree._build(leaves);
}
static async _build(leaves) {
const tree = new MerkleTree(leaves);
let current = tree.levels[0];
while (current.length > 1) {
const next = [];
for (let i = 0; i < current.length; i += 2) {
const pair = new Uint8Array([...current[i], ...current[i+1]]);
next.push(await sha256(pair));
}
if (next.length % 2 === 1) next.push(next[next.length - 1]);
tree.levels.push(next);
current = next;
}
return tree;
}
get root() {
return this.levels[this.levels.length - 1][0];
}
proof(leafIndex) {
const proof = [];
let idx = leafIndex;
for (const level of this.levels.slice(0, -1)) {
const sib = idx ^ 1;
if (sib < level.length) {
proof.push({ hash: level[sib], side: sib < idx ? 'left' : 'right' });
}
idx = Math.floor(idx / 2);
}
return proof;
}
static async verify(txBytes, proof, root) {
let h = await sha256(txBytes);
for (const { hash, side } of proof) {
const pair = side === 'left'
? new Uint8Array([...hash, ...h])
: new Uint8Array([...h, ...hash]);
h = await sha256(pair);
}
return h.every((b, i) => b === root[i]);
}
}
// ── Ethers.js wallet ─────────────────────────────────────────────────
// npm install ethers
import { ethers } from 'ethers';
// Create a random wallet
const wallet = ethers.Wallet.createRandom();
console.log('Mnemonic: ', wallet.mnemonic.phrase);
console.log('Private key: ', wallet.privateKey);
console.log('Address: ', wallet.address);
// Sign a message
const sig = await wallet.signMessage('Hello, blockchain!');
console.log('Signature:', sig);
// Recover address from signature
const recovered = ethers.verifyMessage('Hello, blockchain!', sig);
console.log('Recovered:', recovered); // should equal wallet.address
// HD wallet derivation (BIP-44 Ethereum)
const hdNode = ethers.HDNodeWallet.fromMnemonic(wallet.mnemonic);
const child = hdNode.derivePath("m/44'/60'/0'/0/0");
console.log('Child address:', child.address);/*
* Blockchain Cryptography — C examples
* Compile: gcc -o blockchain_crypto blockchain_crypto.c -lssl -lcrypto
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/sha.h>
#include <openssl/evp.h>
#include <openssl/ec.h>
#include <openssl/ecdsa.h>
#include <openssl/obj_mac.h>
/* ── SHA-256 ──────────────────────────────────────────────────────── */
void sha256(const unsigned char *data, size_t len, unsigned char *out) {
SHA256(data, len, out);
}
void sha256d(const unsigned char *data, size_t len, unsigned char *out) {
unsigned char tmp[32];
SHA256(data, len, tmp);
SHA256(tmp, 32, out);
}
void print_hex(const unsigned char *data, size_t len) {
for (size_t i = 0; i < len; i++) printf("%02x", data[i]);
printf("\n");
}
/* ── Merkle Tree (4-leaf example) ────────────────────────────────── */
#define HASH_LEN 32
typedef struct {
unsigned char hash[HASH_LEN];
} HashNode;
void merkle_parent(const HashNode *left, const HashNode *right, HashNode *out) {
unsigned char combined[HASH_LEN * 2];
memcpy(combined, left->hash, HASH_LEN);
memcpy(combined + HASH_LEN, right->hash, HASH_LEN);
sha256d(combined, HASH_LEN * 2, out->hash);
}
void compute_merkle_root(HashNode *leaves, int n, unsigned char *root_out) {
if (n == 0) return;
/* Copy to working array; duplicate last if odd */
int sz = n + (n % 2);
HashNode *level = calloc(sz, sizeof(HashNode));
memcpy(level, leaves, n * sizeof(HashNode));
if (n % 2) memcpy(&level[n], &level[n-1], sizeof(HashNode));
while (sz > 1) {
int next_sz = (sz + 1) / 2;
HashNode *next = calloc(next_sz, sizeof(HashNode));
for (int i = 0; i < sz; i += 2)
merkle_parent(&level[i], &level[i+1], &next[i/2]);
free(level);
level = next;
sz = next_sz;
}
memcpy(root_out, level[0].hash, HASH_LEN);
free(level);
}
/* ── ECDSA (secp256k1) ───────────────────────────────────────────── */
void ecdsa_example(void) {
/* Generate key pair on secp256k1 */
EC_KEY *key = EC_KEY_new_by_curve_name(NID_secp256k1);
if (!EC_KEY_generate_key(key)) { perror("keygen"); return; }
const char *msg = "Transfer 1 BTC";
unsigned char digest[32];
sha256d((unsigned char*)msg, strlen(msg), digest);
/* Sign */
ECDSA_SIG *sig = ECDSA_do_sign(digest, 32, key);
if (!sig) { perror("sign"); goto cleanup; }
/* Verify */
int ok = ECDSA_do_verify(digest, 32, sig, key);
printf("ECDSA verify: %s\n", ok == 1 ? "VALID" : "INVALID");
/* Print r and s */
const BIGNUM *r, *s;
ECDSA_SIG_get0(sig, &r, &s);
printf("r = %s\n", BN_bn2hex(r));
printf("s = %s\n", BN_bn2hex(s));
ECDSA_SIG_free(sig);
cleanup:
EC_KEY_free(key);
}
int main(void) {
/* SHA-256 avalanche */
const char *msgs[] = {"hello", "hello!", "Hello"};
for (int i = 0; i < 3; i++) {
unsigned char h[32];
sha256((unsigned char*)msgs[i], strlen(msgs[i]), h);
printf("%-8s -> ", msgs[i]);
print_hex(h, 32);
}
/* Merkle root of 4 transactions */
const char *txs[] = {"tx0", "tx1", "tx2", "tx3"};
HashNode leaves[4];
for (int i = 0; i < 4; i++)
sha256d((unsigned char*)txs[i], strlen(txs[i]), leaves[i].hash);
unsigned char root[32];
compute_merkle_root(leaves, 4, root);
printf("Merkle root -> "); print_hex(root, 32);
ecdsa_example();
return 0;
}/*
* Blockchain Cryptography — C++ examples
* Compile: g++ -std=c++17 -o blockchain_crypto blockchain_crypto.cpp -lssl -lcrypto -lsecp256k1
*/
#include <iostream>
#include <vector>
#include <string>
#include <array>
#include <iomanip>
#include <sstream>
#include <openssl/sha.h>
#include <openssl/evp.h>
#include <secp256k1.h>
using Hash = std::array<uint8_t, 32>;
// ── SHA-256 ──────────────────────────────────────────────────────────
Hash sha256(const std::vector<uint8_t>& data) {
Hash out;
SHA256(data.data(), data.size(), out.data());
return out;
}
Hash sha256d(const std::vector<uint8_t>& data) {
auto h1 = sha256(data);
std::vector<uint8_t> h1v(h1.begin(), h1.end());
return sha256(h1v);
}
std::string toHex(const Hash& h) {
std::ostringstream ss;
for (auto b : h) ss << std::hex << std::setw(2) << std::setfill('0') << (int)b;
return ss.str();
}
// ── Merkle Tree ──────────────────────────────────────────────────────
class MerkleTree {
public:
explicit MerkleTree(const std::vector<std::string>& transactions) {
std::vector<Hash> level;
for (auto& tx : transactions) {
std::vector<uint8_t> bytes(tx.begin(), tx.end());
level.push_back(sha256d(bytes));
}
if (level.size() % 2 == 1) level.push_back(level.back());
tree_.push_back(level);
while (level.size() > 1) {
std::vector<Hash> next;
for (size_t i = 0; i < level.size(); i += 2) {
std::vector<uint8_t> combined;
combined.insert(combined.end(), level[i].begin(), level[i].end());
combined.insert(combined.end(), level[i+1].begin(), level[i+1].end());
next.push_back(sha256d(combined));
}
if (next.size() % 2 == 1) next.push_back(next.back());
tree_.push_back(next);
level = next;
}
}
Hash root() const { return tree_.back()[0]; }
// proof: vector of (sibling_hash, is_right_sibling)
std::vector<std::pair<Hash, bool>> proof(size_t idx) const {
std::vector<std::pair<Hash, bool>> pf;
for (size_t lvl = 0; lvl + 1 < tree_.size(); ++lvl) {
size_t sib = idx ^ 1;
if (sib < tree_[lvl].size())
pf.push_back({tree_[lvl][sib], sib > idx});
idx /= 2;
}
return pf;
}
private:
std::vector<std::vector<Hash>> tree_;
};
// ── secp256k1 ECDSA ──────────────────────────────────────────────────
void ecdsaExample() {
secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY);
// Generate private key (in production: use /dev/urandom)
uint8_t privkey[32] = {0};
FILE* urandom = fopen("/dev/urandom", "rb");
fread(privkey, 1, 32, urandom); fclose(urandom);
// Derive public key
secp256k1_pubkey pubkey;
secp256k1_ec_pubkey_create(ctx, &pubkey, privkey);
// Message hash
std::string msg = "Transfer 1 BTC to Bob";
std::vector<uint8_t> msgBytes(msg.begin(), msg.end());
Hash msgHash = sha256d(msgBytes);
// Sign
secp256k1_ecdsa_signature sig;
secp256k1_ecdsa_sign(ctx, &sig, msgHash.data(), privkey, nullptr, nullptr);
// Verify
int ok = secp256k1_ecdsa_verify(ctx, &sig, msgHash.data(), &pubkey);
std::cout << "ECDSA verify: " << (ok ? "VALID" : "INVALID") << "\n";
secp256k1_context_destroy(ctx);
}
int main() {
// Avalanche
for (auto msg : {"hello", "hello!", "Hello"}) {
std::vector<uint8_t> b(msg, msg + strlen(msg));
std::cout << std::setw(8) << std::left << msg << " -> " << toHex(sha256(b)) << "\n";
}
// Merkle tree
MerkleTree mt({"tx0", "tx1", "tx2", "tx3"});
std::cout << "Merkle root: " << toHex(mt.root()) << "\n";
// ECDSA
ecdsaExample();
return 0;
}/**
* Blockchain Cryptography — Java examples
* Dependencies: Bouncy Castle (bcprov-jdk18on)
* Build: javac -cp bcprov-jdk18on-1.77.jar BlockchainCrypto.java
*/
import java.security.*;
import java.security.spec.*;
import java.util.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.crypto.params.*;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.signers.ECDSASigner;
import org.bouncycastle.math.ec.ECPoint;
import java.math.BigInteger;
public class BlockchainCrypto {
static { Security.addProvider(new BouncyCastleProvider()); }
// ── SHA-256 ──────────────────────────────────────────────────────
public static byte[] sha256(byte[] data) throws Exception {
return MessageDigest.getInstance("SHA-256").digest(data);
}
public static byte[] sha256d(byte[] data) throws Exception {
return sha256(sha256(data));
}
public static String toHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) sb.append(String.format("%02x", b));
return sb.toString();
}
// ── Merkle Tree ──────────────────────────────────────────────────
static class MerkleTree {
private final List<List<byte[]>> levels = new ArrayList<>();
public MerkleTree(List<byte[]> transactions) throws Exception {
List<byte[]> leaves = new ArrayList<>();
for (byte[] tx : transactions) leaves.add(sha256d(tx));
if (leaves.size() % 2 == 1) leaves.add(leaves.get(leaves.size()-1));
levels.add(leaves);
List<byte[]> current = leaves;
while (current.size() > 1) {
List<byte[]> next = new ArrayList<>();
for (int i = 0; i < current.size(); i += 2) {
byte[] combined = new byte[64];
System.arraycopy(current.get(i), 0, combined, 0, 32);
System.arraycopy(current.get(i+1), 0, combined, 32, 32);
next.add(sha256d(combined));
}
if (next.size() % 2 == 1) next.add(next.get(next.size()-1));
levels.add(next);
current = next;
}
}
public byte[] root() { return levels.get(levels.size()-1).get(0); }
public List<byte[]> proof(int leafIndex) {
List<byte[]> pf = new ArrayList<>();
int idx = leafIndex;
for (int lvl = 0; lvl < levels.size()-1; lvl++) {
int sib = idx ^ 1;
List<byte[]> level = levels.get(lvl);
if (sib < level.size()) pf.add(level.get(sib));
idx /= 2;
}
return pf;
}
}
// ── ECDSA with BouncyCastle ──────────────────────────────────────
public static void ecdsaDemo() throws Exception {
ECNamedCurveParameterSpec params = ECNamedCurveTable.getParameterSpec("secp256k1");
ECDomainParameters domain = new ECDomainParameters(
params.getCurve(), params.getG(), params.getN(), params.getH());
// Generate key pair
ECKeyPairGenerator gen = new ECKeyPairGenerator();
gen.init(new ECKeyGenerationParameters(domain, new SecureRandom()));
org.bouncycastle.crypto.AsymmetricCipherKeyPair kp = gen.generateKeyPair();
ECPrivateKeyParameters privKey = (ECPrivateKeyParameters) kp.getPrivate();
ECPublicKeyParameters pubKey = (ECPublicKeyParameters) kp.getPublic();
// Hash message
byte[] msgHash = sha256d("Transfer 1 BTC to Bob".getBytes());
// Sign
ECDSASigner signer = new ECDSASigner();
signer.init(true, privKey);
BigInteger[] sig = signer.generateSignature(msgHash);
System.out.println("r = " + sig[0].toString(16));
System.out.println("s = " + sig[1].toString(16));
// Verify
signer.init(false, pubKey);
boolean ok = signer.verifySignature(msgHash, sig[0], sig[1]);
System.out.println("Verified: " + ok);
}
public static void main(String[] args) throws Exception {
// Avalanche
for (String msg : new String[]{"hello", "hello!", "Hello"}) {
System.out.printf("%-8s -> %s%n", msg, toHex(sha256(msg.getBytes())));
}
// Merkle tree
List<byte[]> txs = Arrays.asList(
"tx0".getBytes(), "tx1".getBytes(), "tx2".getBytes(), "tx3".getBytes());
MerkleTree mt = new MerkleTree(txs);
System.out.println("Merkle root: " + toHex(mt.root()));
System.out.println("Proof length for tx2: " + mt.proof(2).size() + " hashes");
// ECDSA
ecdsaDemo();
}
}// Blockchain Cryptography — Go examples
// go get github.com/ethereum/go-ethereum
package main
import (
"crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"math/big"
gocrypto "github.com/ethereum/go-ethereum/crypto"
)
// ── SHA-256 ───────────────────────────────────────────────────────────
func Sha256(data []byte) []byte {
h := sha256.Sum256(data)
return h[:]
}
func Sha256d(data []byte) []byte {
h := sha256.Sum256(data)
h2 := sha256.Sum256(h[:])
return h2[:]
}
// ── Merkle Tree ───────────────────────────────────────────────────────
type MerkleTree struct {
Levels [][][]byte
}
func NewMerkleTree(txs [][]byte) *MerkleTree {
leaves := make([][]byte, len(txs))
for i, tx := range txs {
leaves[i] = Sha256d(tx)
}
if len(leaves)%2 == 1 {
leaves = append(leaves, leaves[len(leaves)-1])
}
tree := &MerkleTree{Levels: [][][]byte{leaves}}
current := leaves
for len(current) > 1 {
var next [][]byte
for i := 0; i+1 < len(current); i += 2 {
combined := append(current[i], current[i+1]...)
next = append(next, Sha256d(combined))
}
if len(next)%2 == 1 {
next = append(next, next[len(next)-1])
}
tree.Levels = append(tree.Levels, next)
current = next
}
return tree
}
func (m *MerkleTree) Root() []byte {
return m.Levels[len(m.Levels)-1][0]
}
type ProofStep struct {
Hash []byte
Right bool // true if sibling is to the right
}
func (m *MerkleTree) Proof(leafIdx int) []ProofStep {
var proof []ProofStep
idx := leafIdx
for lvl := 0; lvl+1 < len(m.Levels); lvl++ {
sib := idx ^ 1
level := m.Levels[lvl]
if sib < len(level) {
proof = append(proof, ProofStep{Hash: level[sib], Right: sib > idx})
}
idx /= 2
}
return proof
}
func VerifyProof(tx []byte, proof []ProofStep, root []byte) bool {
h := Sha256d(tx)
for _, step := range proof {
var combined []byte
if step.Right {
combined = append(h, step.Hash...)
} else {
combined = append(step.Hash, h...)
}
h = Sha256d(combined)
}
for i := range h {
if h[i] != root[i] {
return false
}
}
return true
}
// ── ECDSA (go-ethereum) ───────────────────────────────────────────────
func ecdsaDemo() {
// Generate private key on secp256k1
privKey, err := gocrypto.GenerateKey()
if err != nil {
panic(err)
}
fmt.Printf("Private key: %x\n", privKey.D.Bytes())
// Ethereum address = last 20 bytes of Keccak256(pubKey)
addr := gocrypto.PubkeyToAddress(privKey.PublicKey)
fmt.Printf("Address: %s\n", addr.Hex())
// Sign a message hash (must be 32 bytes)
msgHash := gocrypto.Keccak256([]byte("Transfer 1 ETH to Bob"))
sig, err := gocrypto.Sign(msgHash, privKey)
if err != nil {
panic(err)
}
fmt.Printf("Signature: %x\n", sig)
// Recover public key from signature (Ethereum-style)
recoveredPub, err := gocrypto.SigToPub(msgHash, sig)
if err != nil {
panic(err)
}
recoveredAddr := gocrypto.PubkeyToAddress(*recoveredPub)
fmt.Printf("Recovered: %s\n", recoveredAddr.Hex())
fmt.Printf("Match: %v\n", addr == recoveredAddr)
}
// ── secp256k1 ECDSA (stdlib, for illustration) ────────────────────────
func stdlibECDSA() {
// Note: stdlib uses P-256 (not secp256k1); for secp256k1 use go-ethereum
curve := gocrypto.S256() // secp256k1 via go-ethereum
privKey, _ := ecdsa.GenerateKey(curve, rand.Reader)
digest := Sha256d([]byte("hello blockchain"))
r, s, _ := ecdsa.Sign(rand.Reader, privKey, digest)
ok := ecdsa.Verify(&privKey.PublicKey, digest, r, s)
fmt.Printf("stdlib ECDSA (secp256k1) verify: %v r=%s\n",
ok, hex.EncodeToString(r.Bytes()[:8]))
_ = s
}
func main() {
// Avalanche demo
for _, msg := range []string{"hello", "hello!", "Hello"} {
h := Sha256([]byte(msg))
fmt.Printf("%-8s -> %s\n", msg, hex.EncodeToString(h))
}
// Merkle tree
txs := [][]byte{[]byte("tx0"), []byte("tx1"), []byte("tx2"), []byte("tx3")}
mt := NewMerkleTree(txs)
fmt.Printf("Merkle root: %s\n", hex.EncodeToString(mt.Root()))
proof := mt.Proof(2)
ok := VerifyProof(txs[2], proof, mt.Root())
fmt.Printf("Proof for tx2 verified: %v (len=%d)\n", ok, len(proof))
// ECDSA
ecdsaDemo()
stdlibECDSA()
}10. Summary
- Hash functions map arbitrary data to a fixed 256-bit digest. SHA-256 (Bitcoin) and Keccak-256 (Ethereum) provide preimage resistance ($2^{256}$), collision resistance ($2^{128}$), and the avalanche effect — flipping one input bit flips ~50% of output bits.
- The Merkle-Damgård construction processes messages in 512-bit blocks through a compression function, chaining outputs. Bitcoin uses double-SHA-256; Ethereum uses the pre-NIST Keccak-256 variant.
- Merkle trees commit to all transactions in a block via a single 32-byte root hash stored in the block header. A Merkle proof verifies a single transaction's inclusion in $O(\log n)$ hashes — enabling lightweight SPV clients.
- secp256k1 ECDSA uses a 256-bit elliptic curve over a prime field. Key generation: pick random $d$, compute $Q = d \cdot G$ (one-way due to ECDLP hardness). Signing: $(r, s) = (R_x, k^{-1}(z + rd))$. Verification: recompute $R$ from the signature and check $R_x = r$.
- Ethereum addresses are the last 20 bytes of the Keccak-256 hash of the 64-byte uncompressed public key. Bitcoin uses RIPEMD160(SHA256(compressed pubkey)) encoded as Base58Check.
- BIP-32 HD wallets derive an entire hierarchy of key pairs from a single seed using HMAC-SHA512. One mnemonic phrase controls all accounts.
- Transaction authorization: Bitcoin uses a stack-based script engine (scriptSig + scriptPubKey) where OP_CHECKSIG verifies an ECDSA signature over the transaction hash. Ethereum signs the RLP-encoded transaction fields (including chainId per EIP-155) and includes the recovery parameter $v$ so verifiers can recover the sender's public key without storing it.
- Hash pointers chain blocks together: each block header's
prev_hashfield is the SHA256d of the previous header. Modifying any historical block invalidates all subsequent hashes and requires re-mining the entire suffix — computationally infeasible against an honest majority. - Nonce secrecy in ECDSA is critical: reusing the same nonce $k$ for two signatures immediately leaks the private key. Use RFC 6979 deterministic nonce derivation in production.