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:
- Understand Proof of Work at a mathematical level - grasp why finding valid hashes is computationally difficult but verification is instant
- Master multi-threaded programming in C - implement parallel nonce search with proper synchronization and cache-friendly memory access
- Implement difficulty adjustment algorithms - understand how Bitcoin maintains 10-minute blocks despite changing hash power
- Build a Stratum protocol client - communicate with real mining pools and understand pool mining economics
- 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:
- Determine whose version of the ledger is correct?
- Prevent attackers from rewriting history?
- 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:
- Length extension attack prevention: SHA-256 (like all Merkle-Damgard constructions) is vulnerable to length extension. Double hashing prevents this.
- Defense in depth: If a weakness is found in SHA-256, the double construction may still be secure.
- 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:
- Double spend: Reverse transactions by mining a longer alternative chain
- Censor: Exclude specific transactions from blocks
- 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:
- Timestamp: Within acceptable range
- Coinbase transaction: Changes Merkle root
- 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:
- Pool sends block template (prevhash, transactions, difficulty)
- Miner searches for valid hashes
- Miner submits shares (and occasionally blocks)
- Pool distributes rewards based on shares
Complete Project Specification
Functional Requirements
- Core Mining Engine
- Implement double SHA-256 hashing
- Nonce iteration with exhaustion detection
- Extra nonce support for extended search space
- Multi-threaded parallel search
- Difficulty System
- Parse nBits difficulty encoding
- Calculate target from difficulty
- Implement difficulty adjustment algorithm
- Support variable share difficulty
- Block Construction
- Build 80-byte block headers
- Construct coinbase transactions
- Calculate Merkle roots
- Update timestamp within valid range
- Pool Communication (Stratum)
- Connect to mining pool via TCP
- Implement Stratum protocol messages
- Submit shares and handle responses
- Reconnect on connection loss
- 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:
- Define the 64 round constants K[0..63]
- Define the 8 initial hash values H[0..7]
- Implement message padding (with length encoding)
- Implement the message schedule expansion (W array)
- Implement the compression function (64 rounds)
- 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:
- Define the 80-byte block header structure
- Implement nBits to 256-bit target conversion
- Implement target to nBits conversion
- Implement hash comparison against target
- 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:
- Initialize block header with test data
- Set difficulty to easily solvable level
- Iterate through nonce values
- Check each hash against target
- 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:
- Create thread pool with configurable size
- Divide nonce space equally among threads
- Implement atomic found flag for early termination
- Add atomic hash counter for statistics
- 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:
- Implement Merkle tree construction
- Handle odd numbers of transactions (duplicate last)
- Create coinbase transaction structure
- 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:
- Track block timestamps for adjustment period
- Calculate actual vs expected time for 2016 blocks
- Apply adjustment with 4x/0.25x clamping
- 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:
- Implement TCP socket connection
- Parse JSON-RPC Stratum messages
- Handle subscribe, authorize, and notify
- Build work from pool job data
- 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
- “Mastering Bitcoin” Chapter 10 by Andreas Antonopoulos - Mining and Consensus
- Bitcoin Developer Documentation: Mining
- Stratum Protocol Specification: GitHub
Technical Deep Dives
- NIST FIPS 180-4: SHA-256 Specification
- Bitcoin Improvement Proposals: BIP-34, BIP-66 (consensus rules affecting mining)
- “Majority is not Enough”: Selfish Mining Paper by Eyal and Sirer
Code References
- Bitcoin Core Mining Code: GitHub
- cgminer: Open-source GPU/ASIC miner GitHub
- cpuminer: Simple CPU miner reference GitHub
Supplementary Reading
- “Bitcoin and Cryptocurrency Technologies” by Narayanan et al. - Chapter 5
- Hashcash Paper by Adam Back - Original PoW concept
- 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.