P05: Proof of Work Miner

P05: Proof of Work Miner

Project Overview

Attribute Value
Main Language C
Alternative Languages Rust, Go, C++
Difficulty Advanced
Coolness Level Level 4: Hardcore Tech Flex
Business Potential Resume Gold (Systems Programming/Blockchain)
Knowledge Area Consensus / Mining
Main Book “Mastering Bitcoin” by Andreas M. Antonopoulos

Learning Objectives

By completing this project, you will:

  1. Understand Proof of Work at a mathematical level - grasp why finding valid hashes is computationally difficult but verification is instant
  2. Master multi-threaded programming in C - implement parallel nonce search with proper synchronization and cache-friendly memory access
  3. Implement difficulty adjustment algorithms - understand how Bitcoin maintains 10-minute blocks despite changing hash power
  4. Build a Stratum protocol client - communicate with real mining pools and understand pool mining economics
  5. Connect cryptography to economics - appreciate why PoW creates an unforgeable, trustless timestamping system

Deep Theoretical Foundation

The Fundamental Problem: How Do You Prove Computational Work?

In a decentralized network with no central authority, how do you:

  1. Determine whose version of the ledger is correct?
  2. Prevent attackers from rewriting history?
  3. Rate-limit block production without a trusted clock?

Satoshi Nakamoto’s insight was to leverage computational work as proof of time and commitment. If producing a block requires solving a puzzle that takes 10 minutes on average, then a chain with 100 blocks represents roughly 1000 minutes of cumulative work. An attacker would need to redo all that work to replace the chain.

The PoW Puzzle: Finding a Needle in a Haystack

The Proof of Work puzzle is elegantly simple:

Find a nonce such that:
    SHA256(SHA256(block_header || nonce)) < target

Where:

  • block_header: 80 bytes containing version, previous block hash, Merkle root, timestamp, difficulty bits, and nonce
  • nonce: 4 bytes (32-bit integer) that miners vary
  • target: A 256-bit number; smaller target = harder puzzle

The hash function produces uniformly distributed outputs, so finding a hash below target is pure trial and error. If target is 2^248, you need approximately 2^8 = 256 attempts on average. If target is 2^240, you need approximately 2^16 = 65,536 attempts.

Why Double SHA-256?

Bitcoin uses SHA256(SHA256(x)) rather than single SHA256. The reasons include:

  1. Length extension attack prevention: SHA-256 (like all Merkle-Damgard constructions) is vulnerable to length extension. Double hashing prevents this.
  2. Defense in depth: If a weakness is found in SHA-256, the double construction may still be secure.
  3. Historical precedent: This was a common cryptographic practice when Bitcoin was designed.

The Difficulty Target System

Bitcoin’s difficulty adjustment is a negative feedback loop that maintains 10-minute average block times:

new_difficulty = old_difficulty * (actual_time / expected_time)

Where:
- expected_time = 2016 blocks * 10 minutes = 2,016,000 seconds
- actual_time = time taken to mine last 2016 blocks

The adjustment is clamped to prevent extreme changes:

  • Maximum increase: 4x (400%)
  • Maximum decrease: 0.25x (25%)

This means even if all miners suddenly left the network, it would take no more than 8 weeks to readjust to the new hash rate.

Difficulty Representation: nBits Encoding

Bitcoin stores difficulty in a compact 4-byte “nBits” format:

nBits = 0x1d00ffff (genesis block)

Target = coefficient * 2^(8 * (exponent - 3))

Where:
- exponent = first byte (0x1d = 29)
- coefficient = remaining 3 bytes (0x00ffff = 65535)

Target = 65535 * 2^(8 * (29 - 3))
       = 65535 * 2^208
       = 0x00000000FFFF0000...0000 (256-bit number with 4 leading zero bytes)

Higher difficulty = smaller target = more leading zeros required in valid hashes.

The Economics of Mining

Energy-to-Security Conversion

PoW converts electricity into cryptographic security. The cost of attacking the network equals the cost of generating equivalent hash power:

Cost to attack = (hash_rate * time * electricity_cost) + hardware_cost

As of 2024, reversing even a single Bitcoin block would require:

  • ~500 EH/s (exahashes per second) of hash power
  • Billions of dollars in specialized ASIC hardware
  • Enormous electricity costs
  • And you’d still need to outpace the honest network

The Honest Majority Assumption

PoW security requires that >50% of hash power is controlled by honest participants. If an attacker controls >50%, they can:

  1. Double spend: Reverse transactions by mining a longer alternative chain
  2. Censor: Exclude specific transactions from blocks
  3. Halt: Stop mining entirely (though they can’t steal funds)

Note: The attacker cannot:

  • Steal coins from arbitrary addresses (requires private keys)
  • Create coins beyond the block reward
  • Change consensus rules unilaterally

Selfish Mining and Block Withholding

Advanced attacks exist even with <50% hash power:

  • Selfish mining: Withhold discovered blocks to orphan competitors’ blocks
  • Block withholding: In a pool, submit partial work but discard valid blocks
  • Fee sniping: Attempt to re-mine recent blocks to steal transaction fees

These attacks are theoretical concerns but demonstrate PoW’s game-theoretic complexity.

Sybil Resistance Through Proof of Work

In a permissionless network, anyone can create unlimited identities (Sybil attack). How do you prevent an attacker from creating millions of fake “voters”?

PoW solves this by making each “vote” (block proposal) expensive:

  • Each identity must prove computational work
  • Creating fake identities provides no advantage
  • What matters is cumulative hash power, not number of participants

This is fundamentally different from Proof of Stake, which uses economic stake instead of computational work.

Energy Consumption: The Intentional Feature

Critics often cite Bitcoin’s energy consumption as a flaw. From a cryptographic perspective, it’s the core feature:

Security = Energy Consumed

Less energy consumption would mean:
- Cheaper attacks
- Less security
- Easier chain reorganization

The question isn’t whether PoW uses energy, but whether that energy provides commensurate value. The network currently pays miners ~$10 billion annually in block rewards and fees.

The Mining Hardware Evolution

Mining hardware has evolved through distinct eras:

Era Hardware Hash Rate Energy Efficiency
2009-2010 CPU ~10 MH/s ~1000 J/MH
2010-2013 GPU ~500 MH/s ~50 J/MH
2013-2015 FPGA ~1 GH/s ~10 J/MH
2015-present ASIC ~100 TH/s ~0.00003 J/MH

Modern ASICs are 30 million times more efficient than CPUs for SHA-256 hashing.

The Nonce Search Space

The 32-bit nonce provides only 2^32 = 4.3 billion attempts. At 100 TH/s, this is exhausted in:

4.3 billion / 100 trillion = 0.000043 seconds = 43 microseconds

Modern miners must also vary:

  1. Timestamp: Within acceptable range
  2. Coinbase transaction: Changes Merkle root
  3. Extra nonce: Additional nonce in coinbase

This creates effectively unlimited search space.

Pool Mining and Share Submission

Solo mining is essentially lottery-playing with terrible odds. Mining pools aggregate hash power and distribute rewards proportionally.

Share concept: A share is a proof of work that meets a lower difficulty than the actual target. Shares prove miners are working honestly without requiring them to find actual blocks.

Example:

  • Network difficulty: Target with 76 leading zero bits
  • Share difficulty: Target with 40 leading zero bits
  • Miner finds many shares (proving work) but few blocks

The Stratum protocol standardizes pool-miner communication:

  1. Pool sends block template (prevhash, transactions, difficulty)
  2. Miner searches for valid hashes
  3. Miner submits shares (and occasionally blocks)
  4. Pool distributes rewards based on shares

Complete Project Specification

Functional Requirements

  1. Core Mining Engine
    • Implement double SHA-256 hashing
    • Nonce iteration with exhaustion detection
    • Extra nonce support for extended search space
    • Multi-threaded parallel search
  2. Difficulty System
    • Parse nBits difficulty encoding
    • Calculate target from difficulty
    • Implement difficulty adjustment algorithm
    • Support variable share difficulty
  3. Block Construction
    • Build 80-byte block headers
    • Construct coinbase transactions
    • Calculate Merkle roots
    • Update timestamp within valid range
  4. Pool Communication (Stratum)
    • Connect to mining pool via TCP
    • Implement Stratum protocol messages
    • Submit shares and handle responses
    • Reconnect on connection loss
  5. Statistics and Monitoring
    • Real-time hash rate calculation
    • Shares submitted/accepted tracking
    • Estimated time to block
    • Hardware utilization metrics

Non-Functional Requirements

  • Performance: Achieve maximum hash rate for the hardware
  • Efficiency: Minimize cache misses and memory bandwidth
  • Reliability: Run continuously without memory leaks
  • Portability: Compile on Linux, macOS, Windows

Command-Line Interface

# Solo mining simulation
$ powminer --solo --difficulty 1000000 --threads 8
[INFO] Starting PoW miner with 8 threads
[INFO] Target difficulty: 1000000 (requires ~20 leading zero bits)
[MINING] Thread 0: Searching nonces 0x00000000 - 0x1FFFFFFF
[MINING] Thread 1: Searching nonces 0x20000000 - 0x3FFFFFFF
...
[HASH] Current rate: 45.2 MH/s
[FOUND] Block found! Nonce: 0x1a2b3c4d
[FOUND] Hash: 00000000000abc123...
[FOUND] Time: 3.2 seconds, Attempts: 145,000,000

# Pool mining mode
$ powminer --pool stratum+tcp://pool.example.com:3333 \
           --user worker.1 --threads 8
[STRATUM] Connected to pool.example.com:3333
[STRATUM] Subscribed, extranonce1: 0xaabbccdd, extranonce2_size: 4
[STRATUM] New job received: job_id=1234
[SHARE] Submitted share #1
[SHARE] Share #1 accepted (difficulty 65536)
...

# Difficulty adjustment simulation
$ powminer --simulate-adjustment --blocks 2016 --initial-diff 1000000
[SIM] Block 0: difficulty=1000000, time=10.2s
[SIM] Block 1: difficulty=1000000, time=9.8s
...
[SIM] Block 2015: difficulty=1000000, time=11.1s
[ADJ] Adjustment epoch complete
[ADJ] Expected time: 20160s, Actual time: 20412s
[ADJ] New difficulty: 1012500 (+1.25%)

Solution Architecture

Module Structure

src/
├── main.c              # Entry point and CLI parsing
├── miner.c/.h          # Mining orchestration
├── sha256.c/.h         # Double SHA-256 implementation
├── block.c/.h          # Block header construction
├── difficulty.c/.h     # Difficulty calculation and adjustment
├── merkle.c/.h         # Merkle tree construction
├── stratum.c/.h        # Stratum protocol client
├── threadpool.c/.h     # Multi-threaded nonce search
├── stats.c/.h          # Statistics and monitoring
├── endian.c/.h         # Byte order utilities
└── tests/
    ├── test_sha256.c       # SHA-256 test vectors
    ├── test_difficulty.c   # Difficulty encoding tests
    ├── test_mining.c       # Mining validation tests
    └── test_stratum.c      # Protocol message tests

Core Data Structures

/* 256-bit hash value */
typedef struct {
    uint8_t bytes[32];
} hash256_t;

/* 80-byte block header */
typedef struct __attribute__((packed)) {
    int32_t  version;        /* Block version (4 bytes) */
    uint8_t  prev_hash[32];  /* Previous block hash */
    uint8_t  merkle_root[32];/* Merkle root of transactions */
    uint32_t timestamp;      /* Unix timestamp */
    uint32_t bits;           /* Difficulty target (nBits) */
    uint32_t nonce;          /* Nonce being searched */
} block_header_t;

/* Mining job from pool */
typedef struct {
    char     job_id[64];
    uint8_t  prev_hash[32];
    char     coinbase1[1024];
    char     coinbase2[1024];
    char     merkle_branches[32][64];
    int      merkle_branch_count;
    uint32_t version;
    uint32_t bits;
    uint32_t timestamp;
    bool     clean_jobs;
} stratum_job_t;

/* Mining thread context */
typedef struct {
    int           thread_id;
    uint32_t      nonce_start;
    uint32_t      nonce_end;
    block_header_t header;
    hash256_t     target;
    atomic_bool   *found;
    atomic_uint64_t *hash_count;
    uint32_t      *result_nonce;
} thread_context_t;

/* Global miner state */
typedef struct {
    int             num_threads;
    pthread_t       *threads;
    thread_context_t *contexts;
    atomic_bool     running;
    atomic_uint64_t total_hashes;
    time_t          start_time;

    /* Pool connection */
    int             socket_fd;
    char            extranonce1[16];
    int             extranonce2_size;
    stratum_job_t   current_job;
    pthread_mutex_t job_mutex;
} miner_state_t;

/* Difficulty configuration */
typedef struct {
    hash256_t target;        /* 256-bit target threshold */
    uint64_t  difficulty;    /* Numeric difficulty value */
    uint32_t  bits;          /* Compact nBits encoding */
    int       leading_zeros; /* Number of leading zero bits required */
} difficulty_t;

Key Algorithms

Double SHA-256 Hash

function double_sha256(data):
    first_hash = sha256(data)
    second_hash = sha256(first_hash)
    return second_hash

Nonce Search (Single Thread)

function search_nonce(header, target, nonce_start, nonce_end):
    for nonce = nonce_start to nonce_end:
        header.nonce = nonce
        hash = double_sha256(header)

        if hash < target:
            return (true, nonce, hash)

        if should_stop():
            break

    return (false, 0, null)

Difficulty Adjustment

function adjust_difficulty(prev_difficulty, actual_time, expected_time):
    # Calculate adjustment ratio
    ratio = actual_time / expected_time

    # Clamp to prevent extreme changes
    if ratio > 4.0:
        ratio = 4.0
    if ratio < 0.25:
        ratio = 0.25

    # Apply adjustment
    new_difficulty = prev_difficulty * ratio

    return new_difficulty

Target Calculation from nBits

function bits_to_target(bits):
    exponent = bits >> 24
    coefficient = bits & 0x007FFFFF

    if exponent <= 3:
        target = coefficient >> (8 * (3 - exponent))
    else:
        target = coefficient << (8 * (exponent - 3))

    # Handle sign bit (negative targets are invalid)
    if coefficient & 0x800000:
        return INVALID

    return target

Phased Implementation Guide

Phase 1: SHA-256 Implementation

Goal: Implement a correct and efficient SHA-256 function.

Tasks:

  1. Define the 64 round constants K[0..63]
  2. Define the 8 initial hash values H[0..7]
  3. Implement message padding (with length encoding)
  4. Implement the message schedule expansion (W array)
  5. Implement the compression function (64 rounds)
  6. Wrap as double_sha256() function

Validation:

/* Test vector: empty string */
uint8_t hash[32];
double_sha256("", 0, hash);
assert(memcmp(hash, expected_empty_hash, 32) == 0);

/* Test vector: "abc" */
double_sha256("abc", 3, hash);
assert(memcmp(hash, expected_abc_hash, 32) == 0);

Hints if stuck:

  • Use NIST FIPS 180-4 as the authoritative reference
  • Bitcoin uses little-endian byte order in some places
  • The second SHA-256 hashes a 32-byte input (already padded in first pass)

Phase 2: Block Header and Difficulty

Goal: Construct valid block headers and parse difficulty.

Tasks:

  1. Define the 80-byte block header structure
  2. Implement nBits to 256-bit target conversion
  3. Implement target to nBits conversion
  4. Implement hash comparison against target
  5. Create test block headers

Validation:

/* Genesis block nBits */
difficulty_t diff;
bits_to_target(0x1d00ffff, &diff);
assert(diff.leading_zeros == 32);

/* Hash comparison */
hash256_t hash = { .bytes = {0x00, 0x00, 0x00, 0x01, ...} };
hash256_t target = { .bytes = {0x00, 0x00, 0x00, 0x02, ...} };
assert(hash_below_target(&hash, &target) == true);

Hints if stuck:

  • The target is stored big-endian, but comparisons treat it as a number
  • Genesis block target has leading zeros: 0x00000000FFFF…
  • Block headers are little-endian except for previous hash and Merkle root

Phase 3: Single-Threaded Mining

Goal: Find valid blocks with a single thread.

Tasks:

  1. Initialize block header with test data
  2. Set difficulty to easily solvable level
  3. Iterate through nonce values
  4. Check each hash against target
  5. Report found blocks with statistics

Validation:

$ ./powminer --solo --difficulty 1000 --threads 1
[FOUND] Block found in 0.01 seconds
[FOUND] Hash: 000abc123...

Hints if stuck:

  • Start with very low difficulty (high target) for quick testing
  • Print hash rate every second to verify progress
  • The nonce field is the last 4 bytes of the header

Phase 4: Multi-Threaded Mining

Goal: Parallelize nonce search across CPU cores.

Tasks:

  1. Create thread pool with configurable size
  2. Divide nonce space equally among threads
  3. Implement atomic found flag for early termination
  4. Add atomic hash counter for statistics
  5. Ensure proper thread synchronization

Validation:

$ ./powminer --solo --difficulty 1000000 --threads 1
Hash rate: 5 MH/s

$ ./powminer --solo --difficulty 1000000 --threads 8
Hash rate: 38 MH/s  # Near-linear scaling

Hints if stuck:

  • Each thread works on a disjoint nonce range
  • Use memory_order_relaxed for non-critical atomics
  • Avoid false sharing by padding thread contexts to cache line size

Phase 5: Merkle Tree Construction

Goal: Build Merkle roots from transaction hashes.

Tasks:

  1. Implement Merkle tree construction
  2. Handle odd numbers of transactions (duplicate last)
  3. Create coinbase transaction structure
  4. Update Merkle root when coinbase changes

Validation:

/* Single transaction (just coinbase) */
hash256_t tx_hash = sha256(coinbase_tx);
hash256_t merkle_root = compute_merkle_root(&tx_hash, 1);
assert(memcmp(&merkle_root, &tx_hash, 32) == 0);

/* Two transactions */
merkle_root = compute_merkle_root(tx_hashes, 2);
expected = sha256(tx_hashes[0] || tx_hashes[1]);
assert(memcmp(&merkle_root, &expected, 32) == 0);

Hints if stuck:

  • Merkle tree uses double SHA-256 at each level
  • Leaf nodes are transaction hashes (already double-SHA-256)
  • If odd count, duplicate the last element before hashing

Phase 6: Difficulty Adjustment

Goal: Implement Bitcoin’s difficulty adjustment algorithm.

Tasks:

  1. Track block timestamps for adjustment period
  2. Calculate actual vs expected time for 2016 blocks
  3. Apply adjustment with 4x/0.25x clamping
  4. Convert between difficulty, target, and nBits

Validation:

/* No adjustment needed (exactly 2 weeks) */
new_diff = adjust_difficulty(1000000, 2016 * 600, 2016 * 600);
assert(new_diff == 1000000);

/* Blocks too fast (1 week instead of 2) */
new_diff = adjust_difficulty(1000000, 2016 * 300, 2016 * 600);
assert(new_diff == 2000000);  /* Doubled */

/* Blocks too slow (8 weeks instead of 2) - clamped */
new_diff = adjust_difficulty(1000000, 2016 * 2400, 2016 * 600);
assert(new_diff == 250000);  /* Clamped at 4x reduction */

Hints if stuck:

  • Bitcoin clamps before applying, not after
  • Difficulty is inverse of target (higher difficulty = lower target)
  • Real adjustment uses integer math to avoid floating-point issues

Phase 7: Stratum Protocol Client

Goal: Connect to a mining pool and submit shares.

Tasks:

  1. Implement TCP socket connection
  2. Parse JSON-RPC Stratum messages
  3. Handle subscribe, authorize, and notify
  4. Build work from pool job data
  5. Submit shares when found

Validation:

$ ./powminer --pool stratum+tcp://testnet.pool:3333 --user test.worker
[STRATUM] Connected
[STRATUM] Subscribed successfully
[SHARE] Submitted share, waiting response...
[SHARE] Share accepted!

Hints if stuck:

  • Stratum uses newline-delimited JSON-RPC
  • Messages are identified by “id” field for request/response matching
  • Pool sends work via “mining.notify” method

Testing Strategy

Unit Tests

/* SHA-256 NIST test vectors */
void test_sha256_empty(void) {
    uint8_t hash[32];
    sha256("", 0, hash);

    uint8_t expected[] = {
        0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14,
        0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
        0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c,
        0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55
    };

    assert(memcmp(hash, expected, 32) == 0);
}

/* Double SHA-256 (Bitcoin style) */
void test_double_sha256(void) {
    uint8_t hash[32];
    double_sha256("hello", 5, hash);

    /* SHA256(SHA256("hello")) */
    uint8_t expected[] = {
        0x95, 0x95, 0xc9, 0xdf, 0x90, 0x07, 0x51, 0x48,
        /* ... remaining bytes ... */
    };

    assert(memcmp(hash, expected, 32) == 0);
}

/* Difficulty nBits encoding */
void test_bits_encoding(void) {
    hash256_t target;

    /* Genesis block difficulty */
    bits_to_target(0x1d00ffff, &target);

    /* First 4 bytes should be zero */
    assert(target.bytes[0] == 0x00);
    assert(target.bytes[1] == 0x00);
    assert(target.bytes[2] == 0x00);
    assert(target.bytes[3] == 0x00);
}

Integration Tests

/* Full mining cycle */
void test_mining_finds_block(void) {
    miner_state_t miner;
    init_miner(&miner, 4);  /* 4 threads */

    /* Very low difficulty - should find quickly */
    set_difficulty(&miner, 100);

    block_header_t header;
    init_test_header(&header);

    uint32_t nonce;
    hash256_t hash;
    bool found = mine_block(&miner, &header, &nonce, &hash);

    assert(found == true);
    assert(hash_below_target(&hash, &miner.target));
}

/* Stratum job construction */
void test_stratum_job_to_header(void) {
    stratum_job_t job;
    parse_stratum_notify(&job, sample_notify_json);

    block_header_t header;
    build_header_from_job(&job, 0x12345678, &header);

    assert(header.version == job.version);
    assert(memcmp(header.prev_hash, job.prev_hash, 32) == 0);
}

Performance Tests

/* Hash rate benchmark */
void benchmark_hash_rate(void) {
    uint8_t data[80];
    uint8_t hash[32];

    struct timespec start, end;
    clock_gettime(CLOCK_MONOTONIC, &start);

    for (int i = 0; i < 1000000; i++) {
        data[76] = i & 0xFF;  /* Vary nonce */
        double_sha256(data, 80, hash);
    }

    clock_gettime(CLOCK_MONOTONIC, &end);

    double seconds = (end.tv_sec - start.tv_sec) +
                     (end.tv_nsec - start.tv_nsec) / 1e9;
    double rate = 1000000.0 / seconds;

    printf("Hash rate: %.2f MH/s\n", rate / 1000000.0);

    /* Should achieve at least 1 MH/s per core */
    assert(rate > 1000000);
}

/* Thread scaling */
void benchmark_thread_scaling(void) {
    for (int threads = 1; threads <= 8; threads++) {
        double rate = measure_hash_rate(threads);
        printf("%d threads: %.2f MH/s (%.2fx scaling)\n",
               threads, rate, rate / baseline_rate);
    }
}

Stratum Protocol Tests

/* Parse subscribe response */
void test_stratum_subscribe(void) {
    const char *response =
        "{\"id\":1,\"result\":[[\"mining.notify\",\"ae6812eb4cd7735a\"],"
        "\"08000002\",4],\"error\":null}";

    stratum_subscribe_result_t result;
    bool ok = parse_subscribe_response(response, &result);

    assert(ok == true);
    assert(strcmp(result.extranonce1, "08000002") == 0);
    assert(result.extranonce2_size == 4);
}

/* Parse mining.notify */
void test_stratum_notify(void) {
    const char *notify =
        "{\"id\":null,\"method\":\"mining.notify\",\"params\":["
        "\"bf\",\"4d16b6f8...\",\"01000000...\",\"...\","
        "[\"...\"],\"00000002\",\"1c2ac4af\",\"504e86b9\",false]}";

    stratum_job_t job;
    bool ok = parse_notify(notify, &job);

    assert(ok == true);
    assert(strcmp(job.job_id, "bf") == 0);
    assert(job.clean_jobs == false);
}

Common Pitfalls and Debugging

Pitfall 1: Endianness Confusion

Problem: Bitcoin uses mixed endianness - some fields little-endian, others big-endian.

Symptom: Valid-looking blocks are rejected; hashes don’t match expected values.

Solution:

/* Block header endianness */
typedef struct __attribute__((packed)) {
    int32_t  version;        /* Little-endian */
    uint8_t  prev_hash[32];  /* Internal byte order (display reversed) */
    uint8_t  merkle_root[32];/* Internal byte order */
    uint32_t timestamp;      /* Little-endian */
    uint32_t bits;           /* Little-endian */
    uint32_t nonce;          /* Little-endian */
} block_header_t;

/* When displaying hash, reverse bytes for human-readable format */
void hash_to_hex(const hash256_t *hash, char *out) {
    for (int i = 31; i >= 0; i--) {
        sprintf(out + (31-i)*2, "%02x", hash->bytes[i]);
    }
}

Pitfall 2: Nonce Exhaustion

Problem: 32-bit nonce space exhausted before finding valid block.

Symptom: Mining loops forever at high difficulty without finding blocks.

Solution:

/* Implement extra nonce via coinbase modification */
typedef struct {
    uint32_t nonce;          /* Standard header nonce */
    uint64_t extra_nonce;    /* In coinbase transaction */
} extended_nonce_t;

bool mine_with_extranonce(miner_state_t *miner) {
    for (uint64_t extra = 0; ; extra++) {
        update_coinbase_extranonce(&miner->coinbase, extra);
        update_merkle_root(&miner->header);

        for (uint32_t nonce = 0; nonce <= UINT32_MAX; nonce++) {
            miner->header.nonce = nonce;
            hash256_t hash;
            double_sha256(&miner->header, 80, &hash);

            if (hash_below_target(&hash, &miner->target)) {
                return true;
            }
        }
    }
}

Pitfall 3: Target Comparison

Problem: Comparing 256-bit numbers incorrectly.

Symptom: Wrong blocks accepted, valid blocks rejected.

Solution:

/* Compare 256-bit values as big-endian numbers */
int hash_compare(const hash256_t *a, const hash256_t *b) {
    /* Compare from most significant byte (index 0) */
    for (int i = 0; i < 32; i++) {
        if (a->bytes[i] < b->bytes[i]) return -1;
        if (a->bytes[i] > b->bytes[i]) return 1;
    }
    return 0;
}

bool hash_below_target(const hash256_t *hash, const hash256_t *target) {
    return hash_compare(hash, target) < 0;
}

Pitfall 4: Thread Synchronization Issues

Problem: Race conditions when multiple threads find valid blocks simultaneously.

Symptom: Crashes, duplicate submissions, or missed blocks.

Solution:

/* Use atomic compare-and-swap for found flag */
bool try_submit_block(miner_state_t *miner, uint32_t nonce, hash256_t *hash) {
    bool expected = false;

    if (atomic_compare_exchange_strong(&miner->found, &expected, true)) {
        /* We won the race - submit this block */
        miner->result_nonce = nonce;
        memcpy(&miner->result_hash, hash, 32);
        return true;
    }

    /* Another thread already found a block */
    return false;
}

Pitfall 5: Stratum JSON Parsing

Problem: Malformed or unexpected JSON crashes the miner.

Symptom: Random crashes during pool communication.

Solution:

/* Always validate JSON structure before accessing fields */
bool parse_notify(const char *json, stratum_job_t *job) {
    cJSON *root = cJSON_Parse(json);
    if (!root) {
        log_error("Failed to parse JSON: %s", json);
        return false;
    }

    cJSON *params = cJSON_GetObjectItem(root, "params");
    if (!params || !cJSON_IsArray(params)) {
        log_error("Missing or invalid 'params' array");
        cJSON_Delete(root);
        return false;
    }

    int param_count = cJSON_GetArraySize(params);
    if (param_count < 9) {
        log_error("Expected 9 params, got %d", param_count);
        cJSON_Delete(root);
        return false;
    }

    /* Safe to access array elements now */
    /* ... */

    cJSON_Delete(root);
    return true;
}

Pitfall 6: Memory Leaks in Long-Running Miner

Problem: Memory usage grows over time.

Symptom: Miner slows down or crashes after hours of operation.

Solution:

/* Use memory pools for frequently allocated objects */
typedef struct {
    stratum_job_t jobs[16];  /* Fixed-size job ring buffer */
    int write_idx;
    int read_idx;
} job_pool_t;

stratum_job_t *get_job_slot(job_pool_t *pool) {
    stratum_job_t *job = &pool->jobs[pool->write_idx];
    pool->write_idx = (pool->write_idx + 1) % 16;
    return job;
}

/* Use valgrind to detect leaks during development */
/* valgrind --leak-check=full ./powminer --solo --difficulty 1000 */

Extensions and Challenges

Challenge 1: ASIC-Resistant Algorithm

Implement an ASIC-resistant algorithm like Scrypt or Ethash that uses large memory requirements to level the playing field between CPUs and ASICs.

/* Scrypt memory-hard function */
void scrypt(const uint8_t *password, size_t password_len,
            const uint8_t *salt, size_t salt_len,
            uint64_t N, uint32_t r, uint32_t p,
            uint8_t *output, size_t output_len);

Challenge 2: GPU Mining with OpenCL

Port the mining kernel to GPU using OpenCL for significant performance improvement:

/* OpenCL kernel */
__kernel void sha256_search(
    __global const uint8_t *header_base,
    __global const uint8_t *target,
    __global uint32_t *result_nonce,
    __global uint8_t *result_hash,
    uint32_t nonce_offset
) {
    uint32_t gid = get_global_id(0);
    uint32_t nonce = nonce_offset + gid;

    /* Compute double SHA-256 */
    uint8_t hash[32];
    /* ... GPU-optimized SHA-256 ... */

    if (hash_below_target(hash, target)) {
        *result_nonce = nonce;
        /* Copy hash to result */
    }
}

Challenge 3: Mining Pool Simulator

Build a complete mining pool that:

  • Accepts connections from multiple miners
  • Distributes work fairly
  • Calculates and distributes rewards (PPS, PPLNS, PROP)
  • Handles share validation

Challenge 4: Selfish Mining Simulator

Simulate selfish mining attack to understand its profitability threshold:

typedef struct {
    double honest_hashrate;
    double selfish_hashrate;
    int honest_blocks;
    int selfish_blocks;
    int orphaned_blocks;
    double selfish_revenue_share;
} selfish_mining_sim_t;

void simulate_selfish_mining(selfish_mining_sim_t *sim, int num_rounds);

Challenge 5: Real-Time Difficulty Visualization

Create an ncurses or web-based UI showing:

  • Current hash rate per thread
  • Shares submitted vs accepted
  • Estimated time to block
  • Difficulty over time (chart)
  • Pool latency and statistics

Real-World Connections

Bitcoin Mining

Your implementation performs the same core operation as billion-dollar mining farms. The only differences are:

  • They use ASIC hardware (10 million times more efficient)
  • They optimize for power efficiency (operations per watt)
  • They negotiate block templates with full nodes

Mining Pool Infrastructure

Major pools (Antpool, F2Pool, FoundryUSA) process millions of share submissions per second. Understanding Stratum helps you appreciate:

  • How pools aggregate hash power from globally distributed miners
  • Why pool latency matters (orphaned shares waste energy)
  • The game theory of pool hopping and loyalty

Blockchain Security Analysis

PoW underlies security assumptions for:

  • Bitcoin (and forks like BCH, BSV)
  • Pre-merge Ethereum
  • Litecoin (Scrypt)
  • Monero (RandomX)
  • Dogecoin

Understanding PoW helps analyze:

  • 51% attack economics
  • Difficulty bomb mechanics
  • Hash rate security requirements

Green Mining Initiatives

The industry is evolving:

  • Mining at stranded energy sources (flared gas, curtailed renewables)
  • Using mining for grid balancing
  • Heat recapture for industrial/residential use

Your understanding of PoW energy economics is directly applicable to these emerging fields.


Resources

Primary References

  1. “Mastering Bitcoin” Chapter 10 by Andreas Antonopoulos - Mining and Consensus
  2. Bitcoin Developer Documentation: Mining
  3. Stratum Protocol Specification: GitHub

Technical Deep Dives

  1. NIST FIPS 180-4: SHA-256 Specification
  2. Bitcoin Improvement Proposals: BIP-34, BIP-66 (consensus rules affecting mining)
  3. “Majority is not Enough”: Selfish Mining Paper by Eyal and Sirer

Code References

  1. Bitcoin Core Mining Code: GitHub
  2. cgminer: Open-source GPU/ASIC miner GitHub
  3. cpuminer: Simple CPU miner reference GitHub

Supplementary Reading

  1. “Bitcoin and Cryptocurrency Technologies” by Narayanan et al. - Chapter 5
  2. Hashcash Paper by Adam Back - Original PoW concept
  3. Bitcoin Mining Energy: Research on energy consumption and environmental impact

Self-Assessment Checklist

Before moving to the next project, verify:

  • I can explain why finding a hash below target is hard but verification is instant
  • I understand how difficulty adjustment maintains 10-minute block times
  • My implementation correctly produces double SHA-256 hashes
  • I can convert between nBits, target, and difficulty values
  • My multi-threaded miner achieves near-linear scaling
  • I understand the Stratum protocol and can parse its messages
  • I can explain why PoW provides Sybil resistance
  • I understand the energy-to-security tradeoff in PoW systems
  • I can calculate the cost of a 51% attack given network hash rate
  • I understand why ASIC resistance is difficult to achieve

What’s Next?

With PoW mining mastered, you now understand how decentralized consensus is achieved through computational work. In Project 6: HD Wallet (BIP-32/BIP-39), you’ll learn how cryptocurrency wallets derive infinite addresses from a single seed phrase, enabling secure backup and recovery of blockchain assets.