Building Bitcoin's Proof of Work: A TypeScript Implementation Guide

Building Bitcoin's Proof of Work: A TypeScript Implementation Guide

January 22, 2025

6 min read

Building Bitcoin's Proof of Work: A TypeScript Implementation Guide

Understanding Bitcoin's Proof of Work: A TypeScript Implementation

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.

Testing the mining process
Testing the mining process

Fetching Real Bitcoin Block Data

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()
}

Block Header Structure and Hash Calculation

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:

  • Version: Tracks protocol upgrades
  • Previous Block Hash: Links blocks into a chain
  • Merkle Root: Provides a compact representation of all transactions in the block (explained in detail in our Merkle Tree article)
  • Timestamp: Ensures chronological order
  • Bits: Encodes the current mining difficulty target
  • Nonce: The variable that miners modify to find a valid hash
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")

Implementing the Mining Process

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.

Difficulty Validation

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
}

Conclusion

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.