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.

Prereq: basic number theory, modular arithmetic Time to read: ~35 min Interactive figures: 2

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:

THE CORE INSIGHT

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:

$$H : \{0,1\}^* \;\to\; \{0,1\}^{256}$$

Any input, no matter how long, is compressed to exactly 256 bits. The function must satisfy five properties to be cryptographically useful:

PropertyInformal meaningSecurity level
DeterministicSame input always gives same outputRequired by definition
EfficientComputing $H(x)$ takes nanoseconds
Preimage resistanceGiven $h$, finding any $x$ with $H(x)=h$ is infeasible$2^{256}$ operations
Second preimage resistanceGiven $x$, finding $x' \neq x$ with $H(x')=H(x)$ is infeasible$2^{256}$ operations
Collision resistanceFinding any pair $x \neq x'$ with $H(x)=H(x')$ is infeasible$2^{128}$ operations (birthday bound)
Avalanche effectFlipping 1 bit of input flips ~50% of output bitsEssential 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:

IV (256 b) M₀ (512 b) f() compression H₁ M₁ (512 b) f() compression Hash (256 b) initial value block 0 block 1
Merkle-Damgård construction. The initial value (IV) is a fixed constant. Each 512-bit message block is mixed with the current 256-bit state by the compression function 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:

$$\text{HASH256}(x) = \text{SHA256}(\text{SHA256}(x))$$

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:

InputSHA-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}$:

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 Root H(H01 ‖ H23) H01 H(L0 ‖ L1) H23 H(L2 ‖ L3) L0 H(tx₀) L1 H(tx₁) L2 ★ H(tx₂) L3 H(tx₃) tx₀ tx₁ tx₂ (proving) tx₃ transaction being proved proof path (sibling hashes)
A 4-leaf Merkle tree. To prove tx₂ is in the tree, the prover supplies L3 and H01 (2 hashes). The verifier computes H(L2 ‖ L3) = H23, then H(H01 ‖ H23) and checks it equals the known Merkle root.

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:

  1. Computes $L_i = H(\text{tx}_i)$
  2. At each level, hashes the current value with its sibling (from the proof)
  3. 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.

SECURITY NOTE

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$:

$$y^2 \equiv x^3 + 7 \pmod{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:

$$n \approx 1.158 \times 10^{77} \approx 2^{256}$$

Key generation

$d$
Private key — a random integer in $[1, n-1]$, generated from a cryptographically secure random number generator. 256 bits = 32 bytes.
$Q$
Public key — the curve point $Q = d \cdot G$. 64 bytes (uncompressed) or 33 bytes (compressed). Shared publicly.
ONE-WAY FUNCTION: ECDLP

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.

Private Key k 256 random bits e.g. 0x3a7f... ×G Public Key K K = k·G (point) 64 bytes (x,y) Keccak256 Hash h Keccak256(K) 32 bytes last 20B Ethereum Address 0x + last 20 bytes 0x71C7656EC7… ← one-way: cannot reverse any step →

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$:

  1. Pick a cryptographically random nonce $k \in [1, n-1]$
  2. 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$.
  3. Compute $s = k^{-1}(z + r \cdot d) \bmod n$. If $s = 0$, pick a new $k$.
  4. The signature is the pair $(r,\, s)$
$$s = k^{-1}(z + r \cdot d) \pmod{n}$$
CRITICAL: NONCE REUSE IS CATASTROPHIC

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$:

  1. Compute $u_1 = s^{-1} z \bmod n$ and $u_2 = s^{-1} r \bmod n$
  2. Compute the curve point $P = u_1 G + u_2 Q$
  3. The signature is valid if and only if $P_x \equiv r \pmod{n}$
$$P = u_1 G + u_2 Q = s^{-1}z \cdot G + s^{-1}r \cdot Q$$

Substituting $Q = d \cdot G$ and $s = k^{-1}(z + rd)$:

$$\begin{aligned} P &= s^{-1}(z + rd) \cdot G \\ &= k \cdot G \quad\text{(by definition of }s\text{)} \\ &= R \end{aligned}$$

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 (Alice) message m hash z = H(m) random nonce k R = k·G → r = Rₓ s = k⁻¹(z + r·d) signature: (r, s) send message m + signature (r, s) public channel VERIFICATION (Bob) has: m, (r,s), Q z = H(m) u₁=s⁻¹z, u₂=s⁻¹r P = u₁G + u₂Q ✓ valid if Pₓ ≡ r (mod n)

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:

  1. Generate private key $d$: 32 random bytes from a CSPRNG
  2. 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)
  3. Hash the public key: $h = \text{Keccak256}(Q)$ — a 32-byte digest
  4. Take the last 20 bytes of $h$, prefix with 0x
$$\text{address} = \texttt{0x} \| \text{last20bytes}(\text{Keccak256}(Q))$$

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

  1. Generate private key $d$ (random 32 bytes)
  2. Compute compressed public key: $(x,\, \text{sign}(y))$ — 33 bytes
  3. Compute $h_1 = \text{SHA256}(Q_\text{compressed})$
  4. Compute $h_2 = \text{RIPEMD160}(h_1)$ — 20-byte "public key hash"
  5. 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:

$$\text{CKD}(k_\text{parent},\, c_\text{parent},\, i) = \text{HMAC-SHA512}(c_\text{parent},\; k_\text{parent} \| i)$$

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:

For the most common output type, P2PKH (Pay-to-Public-Key-Hash), validation proceeds as follows:

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

INPUTS (UTXOs being spent) UTXO #1 — 0.5 BTC scriptSig: <sig><pubKey> ← unlocks prev output UTXO #2 — 0.3 BTC scriptSig: <sig><pubKey> ← unlocks prev output TX version, locktime fee: 0.01 BTC OUTPUTS (new UTXOs) Output #1 — 0.7 BTC scriptPubKey: OP_DUP OP_HASH160 <pubKeyHash> (Bob) Output #2 — 0.09 BTC scriptPubKey: OP_DUP OP_HASH160 <pubKeyHash> (Alice, change) Σ inputs = Σ outputs + fee 0.5 + 0.3 = 0.7 + 0.09 + 0.01 ✓ UTXOs are fully consumed — no partial spend allowed

Ethereum account model

Ethereum uses an account-based model instead of UTXOs. An Externally Owned Account (EOA) transaction contains:

nonce
Transaction counter for this account — prevents replay attacks
gasPrice
Wei per gas unit the sender is willing to pay (legacy) or maxFeePerGas/maxPriorityFeePerGas (EIP-1559)
gasLimit
Maximum gas units this transaction may consume
to
Recipient address (20 bytes); empty for contract creation
value
Wei to transfer
data
ABI-encoded function call for contract interactions; empty for simple transfers
v, r, s
ECDSA signature components. v encodes the recovery ID (allows recovering the public key from the signature alone)

The signing hash for a legacy Ethereum transaction is:

$$z = \text{Keccak256}(\text{RLP}(\text{nonce},\, \text{gasPrice},\, \text{gasLimit},\, \text{to},\, \text{value},\, \text{data},\, \text{chainId},\, 0,\, 0))$$

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:

FieldSizeDescription
Version4 bytesBlock version number
Previous block hash32 bytesSHA256d of the previous block header — the chain link
Merkle root32 bytesCommits to all transactions in this block
Timestamp4 bytesUnix timestamp of block creation
Bits4 bytesCompact encoding of the current difficulty target
Nonce4 bytesValue miners iterate over to find a valid proof-of-work

Proof-of-work requires that the block header hash satisfy:

$$\text{SHA256}(\text{SHA256}(\text{header})) < \text{target}$$

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.

Block N-1 prev_hash: 0x0000…ab3f merkle_root: 0xd4e8… timestamp / nonce hash: 0x0000…c7f2 hash ptr Block N prev_hash: 0x0000…c7f2 merkle_root: 0xa91c… timestamp / nonce hash: 0x0000…8a11 hash ptr Block N+1 prev_hash: 0x0000…8a11 merkle_root: 0x3bc0… timestamp / nonce hash: 0x0000…1d4a
Three consecutive blocks. Each block's 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:

  1. Modify the transaction, which changes that block's Merkle root
  2. Re-mine block $N$ to find a new nonce satisfying proof-of-work (expected cost: $\sim 10^{20}$ hashes at current difficulty)
  3. Block $N+1$ references the old hash of block $N$, so it's now orphaned — re-mine block $N+1$ too
  4. 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

ORIGINAL INPUT
HASH (128-bit hex)
MODIFIED INPUT (1 bit flipped)
HASH (128-bit hex)
Click "Flip a bit" to see the avalanche effect

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_hash field 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.