Understanding Bitcoin's Proof of Work (PoW) consensus mechanism is crucial for anyone diving into blockchain technology. In this article, we'll explore a TypeScript implementation that demystifies the core components of Bitcoin's mining process. We'll walk through code that handles block header construction, hash calculations, and the mining process itself - all compatible with the actual Bitcoin protocol. By breaking down these concepts into readable TypeScript code, we'll gain practical insights into how Bitcoin's PoW actually works under the hood. You can find the full code on GitHub.
To make our exploration more practical and grounded in reality, we'll work with actual Bitcoin blockchain data. We'll use the Blockstream API to fetch a real Bitcoin block, allowing us to verify our implementation against genuine blockchain data and demonstrate how mining works in practice.
import { queryUrl } from "@lib/utils/query/queryUrl"
import { getBlockHash } from "./core/getBlockHash"
import { mineBlock } from "./core/mineBlock"
import { BlockData } from "./core/BlockData"
import { omit } from "@lib/utils/record/omit"
const blockHash =
"0000000000000000000266819fe415c51f9c025e1059b4c1332c1033236166f0"
interface BlockDataResponse {
id: string
height: number
version: number
timestamp: number
tx_count: number
size: number
weight: number
merkle_root: string
previousblockhash: string
mediantime: number
nonce: number
bits: number
difficulty: number
}
const test = async () => {
const blockDataResponse = await queryUrl<BlockDataResponse>(
`https://blockstream.info/api/block/${blockHash}`,
)
const blockData: BlockData = {
version: blockDataResponse.version,
previousBlockHash: blockDataResponse.previousblockhash,
merkleRoot: blockDataResponse.merkle_root,
timestamp: blockDataResponse.timestamp,
bits: blockDataResponse.bits,
nonce: blockDataResponse.nonce,
}
const computedHash = getBlockHash(blockData)
console.log("\nVerifying actual block:")
console.log("Computed block hash:", computedHash)
console.log("Actual block hash: ", blockHash)
console.log(
"Hash verification:",
computedHash === blockHash ? "Valid ✅" : "Invalid ❌",
)
console.log("\nDemonstrating mining process:")
const miningResult = mineBlock({
blockData: omit(blockData, "nonce"),
startNonce: blockData.nonce - 20, // Start from 20 nonces before the solution
})
console.log("Mining completed!")
console.log("Found valid hash:", miningResult.hash)
console.log("Nonce used:", miningResult.nonce)
}
test()
We'll make HTTP requests using a helper function called queryUrl
that handles the API calls and response parsing.
import { assertFetchResponse } from "../fetch/assertFetchResponse"
export const queryUrl = async <T>(url: string): Promise<T> => {
const response = await fetch(url)
await assertFetchResponse(response)
return response.json()
}
Before we do the mining, let's verify that we can properly compute the block hash from the block data. A block hash in Bitcoin is calculated by hashing the block header, which is an 80-byte structure containing six essential fields that each serve a specific purpose:
export type BlockData = {
version: number
previousBlockHash: string
merkleRoot: string
timestamp: number
bits: number
nonce: number
}
Our getBlockHash
function handles the process of packing these fields into the header structure, ensuring proper byte ordering (Bitcoin uses little-endian format) and field sizes. By comparing our computed hash with the actual block hash from the blockchain, we can verify that our implementation correctly follows Bitcoin's protocol specifications. Together, these fields ensure the integrity, ordering, and immutability of the blockchain while enabling the proof-of-work consensus mechanism.
import { bitcoinHash } from "@lib/chain/bitcoin/bitcoinHash"
import { BlockData } from "./BlockData"
export const getBlockHash = ({
version,
previousBlockHash,
merkleRoot,
timestamp,
bits,
nonce,
}: BlockData): string => {
const header = Buffer.alloc(80)
header.writeInt32LE(version, 0)
Buffer.from(previousBlockHash, "hex").reverse().copy(header, 4)
Buffer.from(merkleRoot, "hex").reverse().copy(header, 36)
header.writeUInt32LE(timestamp, 68)
header.writeUInt32LE(bits, 72)
header.writeUInt32LE(nonce, 76)
return bitcoinHash(header)
}
Bitcoin uses a specific hashing algorithm that applies SHA256 twice to the input data and returns the result in reversed byte order.
import { sha256 } from "../crypto/sha256"
/**
* Performs Bitcoin's standard double SHA256 hashing and returns the result in reversed hex format
*/
export const bitcoinHash = (data: Buffer): string =>
sha256(sha256(data)).reverse().toString("hex")
Now, let's look at the core mining function that implements the proof-of-work algorithm. This function takes a block template (without a nonce) and a starting nonce value, then incrementally tries different nonces until it finds one that produces a hash meeting the required difficulty target. This process is computationally intensive and is the heart of Bitcoin's security model - miners must expend real computational work to find a valid solution, but verifying that solution is trivial for other nodes in the network.
import { getBlockHash } from "./getBlockHash"
import { checkDifficulty } from "./checkDifficulty"
import { BlockData } from "./BlockData"
type MineBlockInput = {
blockData: Omit<BlockData, "nonce">
startNonce: number
}
type MineBlockResult = {
nonce: number
hash: string
}
export const mineBlock = ({
blockData,
startNonce,
}: MineBlockInput): MineBlockResult => {
let nonce = startNonce
while (true) {
const hash = getBlockHash({
...blockData,
nonce,
})
if (checkDifficulty(hash, blockData.bits)) {
return {
nonce,
hash,
}
}
nonce++
}
}
In our demonstration, we start mining from startNonce = blockData.nonce - 20
to quickly find a solution, since the actual nonce for this block is 1094210853. In reality, miners have to test billions of nonces before finding a valid one. What's fascinating is that despite the immense computational effort required to find a valid nonce, verifying the solution is lightning-fast - just two SHA256 operations! This asymmetry between the difficulty of finding a solution and the ease of verifying it is what makes Bitcoin's proof-of-work both secure and practical.
The final piece of our mining implementation is the difficulty check. Bitcoin uses a compact format called "bits" to represent the difficulty target - a 256-bit number that a block's hash must be less than or equal to for the block to be valid. The bits field uses a floating-point-like format with an exponent and coefficient to encode this target threshold efficiently. Our checkDifficulty
function decodes this format and performs the comparison, implementing the same validation that Bitcoin nodes use to verify blocks.
/**
* Checks if a hash meets the difficulty target specified by bits.
* The bits field is Bitcoin's compact format for target threshold.
*/
export const checkDifficulty = (hash: string, bits: number): boolean => {
// Convert bits to target threshold
const exponent = bits >> 24
const coefficient = bits & 0x007fffff
// Calculate target based on Bitcoin's difficulty formula
// target = coefficient * 2^(8 * (exponent - 3))
const targetBytes = Buffer.alloc(32).fill(0)
// Convert coefficient to bytes
const coefficientHex = coefficient.toString(16).padStart(6, "0")
Buffer.from(coefficientHex, "hex").copy(
targetBytes,
32 - Math.floor(((exponent + 1) * 8) / 8),
)
// Get target as hex string
const targetHex = targetBytes.toString("hex")
// Compare hash with target (hash must be less than or equal to target)
return hash <= targetHex
}
Through this implementation, we've broken down Bitcoin's proof-of-work into its essential components: block header construction, double SHA256 hashing, difficulty adjustment, and the mining process itself. While our TypeScript code is simplified compared to Bitcoin Core's implementation, it demonstrates the core principles that secure billions of dollars worth of transactions on the Bitcoin network.