教程区块链区块链技术ch044.3 挖矿与工作量证明(PoW)

本页目录

在完全开放的 P2P 网络中,任何人都可以声称自己"打包了一个新区块",凭什么全网要接受你的区块作为合法时序?中本聪的答案是:让区块的合法性变得昂贵——只有那些付出了真实物理成本(算力与电力)的人,才获得记账权。这就是工作量证明(Proof of Work,PoW)的本质。


4.3.1 挖矿的真实功能:为区块盖上"成本之印"

许多人对"挖矿"存在误解,认为矿工在"凭空印钞票"。但比特币系统并不凭空创造价值,它只是在向完成特定计算任务的人发放奖励。这个计算任务本身没有算术意义上的捷径:找到满足条件的哈希值,唯一可靠的方法就是暴力尝试

每一次尝试都在消耗 CPU/GPU/ASIC 的电力和折旧,因此一个被全网接受的区块,实际上携带了一个不可伪造的成本证明(Unforgeable Costliness)。攻击者若想篡改某个历史区块,就必须针对该区块及其后续所有区块重新付出与诚实节点同等甚至更高的物理成本,这在经济上是自我挫败的。

4.3.2 挖矿算法的完整流程

比特币挖矿并非随机操作,而是一套高度结构化的迭代搜索流程:

  1. 候选区块构造:矿工从内存池(Mempool)中按交易费率排序,选择若干交易,构造 Merkle 树并计算 Merkle 根。
  2. 组装区块头:包含 4 字节版本号、32 字节前序区块哈希(PrevHash)、32 字节 Merkle 根、4 字节时间戳、4 字节 nBits(难度目标编码)、以及 4 字节 Nonce
  3. 双重 SHA-256 哈希:对 80 字节区块头执行 SHA-256d(即 SHA-256(SHA-256(header)))。
  4. 目标比较:将得到的 256 位哈希解释为一个大整数,若其严格小于当前网络目标值(Target),则该 Nonce 有效;矿工立即广播该区块;否则,Nonce 加 1,回到第 3 步继续循环。
  5. 搜索空间扩展:32 位 Nonce 只有约 43 亿种组合,在现代矿机中不到 1 秒即可遍历完毕。因此矿工通过修改 Coinbase 交易中的 extraNonce 字段间接改变 Merkle 根,从而扩展搜索空间。
flowchart TD
    A[从内存池按费率筛选交易] --> B[构造 Coinbase 交易与 Merkle 根]
    B --> C[组装区块头:版本号 + PrevHash + Merkle根 + 时间戳 + nBits + Nonce]
    C --> D[计算 SHA-256d(区块头)]
    D --> E{哈希值 < 当前 Target?}
    E -->|是| F[广播区块到全网,获得出块奖励]
    E -->|否| G[修改 Nonce 或 ExtraNonce/时间戳]
    G --> D

请注意,这个流程里没有任何"解密"或"计算难题"需要求解,它本质上是一个在巨大的哈希空间中海选幸运数字的抽奖过程。你的算力越大,每秒能尝试的 Nonce 数量越多,中奖概率也就越高。

4.3.3 难度目标与 nBits 编码

若要在 256 位空间里直接比较哈希大小,区块头存储的 Target 将占用 32 字节。为了节省空间,比特币采用一种紧凑的 4 字节 nBits 编码(类似科学计数法)。将 nBits 按大端解析为高位的 1 字节指数(exponent)与低位的 3 字节系数(coefficient),可展开为 256 位目标值:

Target=coefficient×256(exponent3)Target = coefficient \times 256^{(exponent - 3)}

通俗地说,"前导零越多,难度越高"的直觉可以精确转化为数学语言:区块头哈希作为大端整数值必须严格小于当前 Target 值。Target 越小,满足条件的哈希在整个空间中占比就越小,寻找难度越大。

Difficulty(难度)与 Target 成反比。定义全网最低难度(创世区块难度)对应的基准目标值为 TargetmaxTarget_{max},则当前难度可表示为:

Difficulty=TargetmaxTargetDifficulty = \frac{Target_{max}}{Target}

Difficulty=1Difficulty = 1 意味着与创世区块难度持平。随着全网算力持续增加,Target 不断被下调,当前比特币主网的 DifficultyDifficulty 已超过 8080 万亿量级。

4.3.4 TypeScript 从零实现:极简 PoW 挖矿

下面用 TypeScript 从零实现 PoW 的核心逻辑:给定一个数据前缀和难度(以二进制前导零位数表示),通过遍历 nonce 寻找满足 SHA-256d(前缀 + nonce) <= target 的解。为教学演示,这里难度仅设为 8 位或 16 位前导零,可在本地秒级完成出块。

typescript
/**
 * 极简 PoW 挖矿演示(教学难度,秒级出块)
 * 纯 TypeScript 从零实现,无外部依赖
 */

/** 简单的确定性哈希(教学占位,真实 SHA-256 见 02.02 从零实现) */
function sha256d(data: Uint8Array): Uint8Array {
  // 多轮 FNV 混合:对输入每个字节敏感,输出 32 字节。
  // 真实实现应使用完整 SHA-256 压缩函数,这里聚焦 PoW 循环逻辑本身。
  const out = new Uint8Array(32);
  for (let round = 0; round < 8; round++) {
    // 每轮以不同种子遍历全部字节
    const seed = Math.imul(0x9e3779b9, round + 1) >>> 0;
    let h = seed ^ data.byteLength;
    for (let i = 0; i < data.length; i++) {
      h ^= data[i];
      h = Math.imul(h, 0x01000193) >>> 0;
    }
    out[round * 4] = h & 0xff;
    out[round * 4 + 1] = (h >>> 8) & 0xff;
    out[round * 4 + 2] = (h >>> 16) & 0xff;
    out[round * 4 + 3] = (h >>> 24) & 0xff;
  }
  return out;
}

/** 将 nonce 打包为 4 字节大端整数(类似区块头 Nonce 字段) */
function nonceToBytes(nonce: number): Uint8Array {
  const b = new Uint8Array(4);
  b[0] = (nonce >> 24) & 0xff;
  b[1] = (nonce >> 16) & 0xff;
  b[2] = (nonce >> 8) & 0xff;
  b[3] = nonce & 0xff;
  return b;
}

/** Uint8Array(32) → BigInt(大端) */
function bytesToBigInt(bytes: Uint8Array): bigint {
  let n = 0n;
  for (const b of bytes) n = (n << 8n) | BigInt(b);
  return n;
}

/**
 * PoW 挖矿:寻找 nonce 使 SHA-256d(prefix + nonce) 满足前导零难度。
 * @param prefix 区块头前缀(不含 nonce)
 * @param zeroBits 要求哈希二进制前导零位数(教学低难度,秒级出块)
 */
export function mine(
  prefix: Uint8Array,
  zeroBits: number
): { nonce: number; hash: string; attempts: number; elapsedMs: number } {
  const target = (1n << BigInt(256 - zeroBits)) - 1n;
  let nonce = 0;
  const start = Date.now();

  for (;; nonce++) {
    // 构造候选:prefix + nonce(4字节大端)
    const head = new Uint8Array(prefix.byteLength + 4);
    head.set(prefix, 0);
    head.set(nonceToBytes(nonce), prefix.byteLength);

    const digest = sha256d(head);
    if (bytesToBigInt(digest) <= target) {
      return {
        nonce,
        hash: Array.from(digest)
          .map((b) => b.toString(16).padStart(2, "0"))
          .join(""),
        attempts: nonce + 1,
        elapsedMs: Date.now() - start,
      };
    }
    if (nonce > 0xffffffff) throw new Error("Nonce 溢出 32 位范围");
  }
}

// ---- 演示 ----
const textEncoder = new TextEncoder();
const prefix = textEncoder.encode("Block#42|PrevHash=abcd|TxRoot=ef01|Time=20260101");

console.log("=== 难度:8 位二进制前导零 ===");
const r8 = mine(prefix, 8);
console.log(`  Nonce = r8.nonce,hash={r8.nonce}, hash ={r8.hash}`);
console.log(`  遍历 r8.attempts,耗时{r8.attempts} 次, 耗时{r8.elapsedMs} ms`);

console.log("=== 难度:16 位二进制前导零 ===");
const r16 = mine(prefix, 16);
console.log(`  Nonce = r16.nonce,hash={r16.nonce}, hash ={r16.hash}`);
console.log(`  遍历 r16.attempts,耗时{r16.attempts} 次, 耗时{r16.elapsedMs} ms`);

运行结果(示意):

text
=== 难度:8 位二进制前导零 ===
  Nonce = 365, hash = 001478eab05aaf803f70a0a14377dcb87e643a66ac...
  遍历 366 次, 耗时 0 ms
=== 难度:16 位二进制前导零 ===
  Nonce = 7075, hash = 000043d2e6bd9f5b3258d372dfaa8fb7c5fc55f4d...
  遍历 7076 次, 耗时 ~10 ms

从输出可以直观感受到:难度的微小增加会使搜索时间成倍增长——每增加 1 个二进制前导零,平均搜索空间就扩大 1 倍。真实比特币网络中,全网总算力以每秒数百 ExaHash(101810^{18} 次)计,个人电脑已绝无可能独立出块。

本节要点

  • 挖矿的本质不是"印钞",而是通过不可逆的物理资源消耗(算力+电力)为区块打上不可伪造的成本证明,使得篡改历史在经济上不可行。
  • 挖矿算法是一个暴力搜索循环:组装区块头 → 双重 SHA-256d 哈希 → 与 Target 比较 → 满足则广播,否则调整 Nonce/ExtraNonce 继续迭代。
  • nBits 是一种紧凑编码,展开公式为 Target=coefficient×256(exponent3)Target = coefficient \times 256^{(exponent - 3)}
  • 难度与 Target 成反比:Difficulty=Targetmax/TargetDifficulty = Target_{max} / Target;每增加 1 位前导零难度,平均搜索空间扩大一倍。

评论

0

评论加载中…

发表评论

0/2000