教程区块链区块链技术第4章 比特币详解

本页目录

从 2008 年白皮书出发,精读比特币这个点对点电子现金系统:UTXO、脚本、工作量证明、难度调整与隔离见证——一个真正去中心化的电子现金如何运转。

本章目录:

  • 4.1 比特币白皮书精读:一个点对点的电子现金系统
  • 4.2 交易、UTXO 模型与脚本系统
  • 4.3 挖矿与工作量证明(PoW)
  • 4.4 难度调整与出块时间稳定
  • 4.5 比特币改进协议(BIP)与隔离见证
  • 4.6 比特币核心认知回顾

4.1 比特币白皮书精读:一个点对点的电子现金系统

在掌握哈希函数、数字签名与椭圆曲线密码学等"工具"之后,我们终于迎来了它们的第一次大规模工程实践——比特币。中本聪用仅 9 页的白皮书,构建了一个不依赖任何中心化中介即可运转的电子现金网络。本节精读白皮书的核心思想。


4.1.1 设计愿景与核心挑战

传统的电子支付建立在"基于信任的模型(trust-based model)"之上:当你用信用卡向朋友转账 100 元,银行作为可信中介验证你账户有足够余额,并防止你将同一笔钱同时转给两个人。

这种双重支付(Double-Spending)问题的本质是数字信息的可复制性——一串电子比特可以被无限制复制,而现金的物理唯一性天然避免了这一点。

中本聪在白皮书中提出的命题极其简洁:如何在无需银行的情况下,让电子支付像现金一样不可双花? 他的答案不是更先进的加密算法,而是一种全新的系统架构——用公开广播、时间戳、经济激励和密码学共同编织出一条不可篡改的链式账本。

flowchart LR

    A["传统银行"] -->|信任中介| B["防止双花验证"]

    C["比特币网络"] -->|公开广播+PoW| D["全网共识确认"]

    style A fill:#fca5a5

    style C fill:#86efac

核心矛盾拆解:

  • 信息互联网(复制成本低)→ 无法直接承载价值
  • 价值互联网(不可双花、确权转移)→ 需要新的信任机制
  • 关键命题:如何在互不信任的各方之间,不通过中心化中介,实现价值的安全转移与一致记账

4.1.2 核心解决思路:时间戳服务器 + 工作量证明 + 最长链

比特币白皮书提出的三大支柱:

  1. 时间戳服务器(Timestamp Server):将所有已发生的交易数字指纹(Hash)打包进一个区块,并对区块头盖上时间戳。每个区块的时间戳都包含前一个区块的哈希,形成一条链条。
  2. 工作量证明(Proof of Work):让制造新区块变得昂贵——只有付出真实计算成本的人才能获得记账权。这使攻击者"重写历史"的成本随时间线性增长。
  3. 最长链原则(Longest Chain Rule):当网络出现分叉时,节点始终选择工作量证明累计最多的那条链。这确保全网最终收敛到同一个账本。
flowchart TD

    B0["区块 0<br/>创世区块"] -->|PrevHash: 0| B1["区块 1"]

    B1 -->|PrevHash: H1| B2["区块 2"]

    B2 -->|PrevHash: H2| B3["区块 3"]

    B3 -->|PrevHash: H3| B4["区块 4"]

    B1 -.->|分叉| C2["区块 2'"] 

    C2 -.->|算力较少被抛弃| C3["区块 3'"]

时间戳链的核心价值:

设第 nn 个区块的哈希为 HnH_n,则链式约束为:

Hn=SHA256d(Headern)其中Headern包含 Hn1H_n = \text{SHA256d}(\text{Header}_n) \quad \text{其中} \quad \text{Header}_n \text{包含 } H_{n-1}

要篡改区块 kk,攻击者必须依次重算 k,k+1,,nk, k+1, \ldots, n 的所有工作量证明——这就是"篡改成本随时间线性增长"的数学本质。

4.1.3 最长链的安全分析:追赶概率

中本聪在论文第 11 节给出了一个著名结论:如果诚实节点控制多数算力,攻击者追上最长链的概率随确认数指数衰减

qq 为攻击者算力占比,p=1qp = 1-q。某诚实区块已领先 zz 个区块时,攻击者追上(aggression)的概率为:

P(z)=1k=0zλkeλk!(1(qp)zk)P(z) = 1 - \sum_{k=0}^{z} \frac{\lambda^k e^{-\lambda}}{k!} \cdot \left(1 - \left(\frac{q}{p}\right)^{z-k}\right)

q>pq > p(攻击者算力过半)时,上述概率趋于 1——这正是"51% 攻击"的原理。

xychart-beta

    title "攻击者追赶概率 vs 确认数 (q=0.3)"

    x-axis [0, 1, 2, 3, 4, 5, 6]

    y-axis "追赶概率" 0 --> 1

    line [1, 0.422, 0.175, 0.054, 0.031, 0.011, 0.004]

4.1.4 隐私模型:伪匿名而非绝对匿名

比特币并不提供绝对匿名。每个地址(由公钥哈希生成)是一个伪匿名标识(Pseudonym)

  • 观察者可把同一地址产生的所有交易关联起来
  • 通过交易图分析、IP 关联、中心化交易所 KYC 数据,可实现去匿名化(De-anonymization)
  • 真正的隐私需要额外工具(混币、隐私币种、zk 技术,见第 10 章)
sequenceDiagram

    participant Alice

    participant Network

    participant Bob

    participant Analyst

    Alice->>Network: 交易: addr_A → addr_B (金额 X)

    Network->>Bob: 广播并确认

    Analyst->>Network: 分析交易图

    Analyst->>Analyst: 聚类 addr_A 的所有交易

    Note over Analyst: 通过地址聚合 + 链外信息 → 身份推测

4.1.5 激励设计与货币政策

矿工挖出区块后获得的奖励由两部分构成:协议发行的区块奖励(Block Reward)和交易者自愿支付的交易手续费(Transaction Fee)。白皮书设计了一个总量收敛的发行曲线:初始奖励为 50 BTC,每产出 21 万个区块(约 4 年)减半一次。

R(n)=50×2n210000 (BTC per block)R(n) = 50 \times 2^{-\left\lfloor \frac{n}{210000} \right\rfloor} \text{ (BTC per block)}

总供应量收敛于:

i=050×2i×210000=2100×104 BTC\sum_{i=0}^{\infty} 50 \times 2^{-i} \times 210000 = 2100 \times 10^4 \text{ BTC}

随着区块奖励趋于零,手续费将成为矿工收入的主要来源,白皮书以此收尾:"一旦预定数量的硬币进入流通,激励机制就可以完全由交易费来支撑,从而完全免受通货膨胀的影响。"

flowchart LR

    subgraph 区块奖励

        R1[50 BTC<br/>2009-2012]

        R2[25 BTC<br/>2012-2016]

        R3[12.5 BTC<br/>2016-2020]

        R4[6.25 BTC<br/>2020-2024]

        R5[3.125 BTC<br/>2024-2028]

    end

    R1 --> R2 --> R3 --> R4 --> R5 --> R6["...趋近于0"]

4.1.6 TypeScript 从零实现:PoW 难度追赶概率模拟

下面用 TypeScript 从零实现中本聪追赶概率的原型,纯语言内置(BigInt 处理 256 位整数),无任何外部依赖:

typescript

/**

 * 中本聪追赶概率模拟(白皮书第11节)

 * 仅使用语言内置,纯从零实现

 */



/** 计算 x 的 n 次幂 */

function powInt(x: number, n: number): number {

  let r = 1;

  for (let i = 0; i < n; i++) r *= x;

  return r;

}



/** 阶乘 */

function factorial(n: number): number {

  let r = 1;

  for (let i = 2; i <= n; i++) r *= i;

  return r;

}



/** 泊松分布概率质量函数 P(X=k) = λ^k e^{-λ} / k! */

function poisson(k: number, lambda: number): number {

  return (Math.pow(lambda, k) * Math.exp(-lambda)) / factorial(k);

}



/**

 * 攻击者追上 z 个区块的概率(Nakamoto 白皮书第 11 节)

 * P = 1 - Σ_{k=0}^{z} [ e^{-λ} λ^k / k! · (1 - (q/p)^{z-k}) ]

 */

function attackerProbability(q: number, z: number): number {

  const p = 1 - q;

  if (q >= 0.5) return 1.0; // 攻击者算力过半,追及概率趋近 1

  const lambda = z * (q / p);



  let sum = 1.0;

  for (let k = 0; k <= z; k++) {

    const prob = poisson(k, lambda);

    const geo = Math.pow(q / p, z - k);

    sum -= prob * (1 - geo);

  }

  return sum;

}



// 演示:q=0.3 时不同确认数的追及概率

const q = 0.3;

console.log("攻击者算力 q =", q);

for (let z = 0; z <= 6; z++) {

  const prob = attackerProbability(q, z);

  console.log(`  领先 z区块:追及概率={z} 区块: 追及概率 ={(prob * 100).toFixed(3)}%`);

}

运行结果:

text

攻击者算力 q = 0.3

  领先 0 区块: 追及概率 = 100.000%

  领先 1 区块: 追及概率 = 62.775%

  领先 2 区块: 追及概率 = 44.572%

  领先 3 区块: 追及概率 = 32.458%

  领先 4 区块: 追及概率 = 23.913%

  领先 5 区块: 追及概率 = 17.735%

  领先 6 区块: 追及概率 = 13.211%

可以看到,随着确认数 z 增加,攻击者追及概率指数级下降。这就是为什么交易所通常要求 6 个确认(约 1 小时)才视为交易最终有效。

本节要点

  • 双重支付问题是数字货币必须解决的首要问题;比特币用去中心化的全网共识替代了银行这一单一信任中介。
  • 三大支柱——时间戳服务器、工作量证明、最长链原则——共同构成"去信任化的一致记账"。
  • 追及概率随确认数指数衰减,是比特币安全性的数学基石。
  • 2100 万总量上限是代码中写死的"社会契约",不可通过软分叉修改。
  • 伪匿名不是绝对匿名,链上分析可部分去匿名化。

4.2 交易、UTXO 模型与脚本系统

比特币账本上不存在"账户余额"这样的概念。本节深入 UTXO 这一基本状态单元,拆解交易结构、脚本系统的栈式执行,以及从 P2PK 到 Taproot 的脚本演进史。


4.2.1 账户余额并不存在——UTXO 才是基本单元

很多初学者会问:"我的比特币钱包里到底存了什么?"答案是:你的钱包并没有存储一个叫做"余额"的数字。你看到的"余额",实际上只是你的钱包软件扫描整条区块链后,把所有"属于你公钥但尚未被花费的输出"加总的结果。

比特币的基本状态单元叫做 UTXO(Unspent Transaction Output,未花费交易输出)。它有三个明确的生命阶段:

  1. 创建(Created):一笔交易在某次出块时将其作为输出(Output)产出;
  2. 可花费(Unspent):属于你的密钥对锁定、尚未被引用;
  3. 销毁(Spent):在新的交易输入(Input)中被引用并签名验证后,该 UTXO 从全局 UTXO 集合中移除,同时生成新的 UTXO。
stateDiagram-v2

    [*] --> Created: 某笔交易 Output 产生

    Created --> Unspent: 确认上链

    Unspent --> Spent: 被新交易 Input 引用并签名消耗

    Spent --> [*]: 从 UtxoSet 中移除

这种模型与日常生活中的现金更为相似:你钱包中有 1 张 100 元和 2 张 20 元——它们不是用一个数字"140 元"来代表,而是由三张具体面值的钞票(UTXO)所组成。当你需要支付 130 元时,你必须花费 100+20 这两张"钞票",并生成一张 10 元的"找零"UTXO 回到你的地址。

4.2.2 一笔标准交易的输入输出结构

一笔标准的 P2PKH(Pay-to-PubKeyHash,付给公钥哈希)交易由以下部分构成:

字段说明
------------
版本号(4字节)当前为 0x02000000(版本 2)
输入计数(VarInt)包含几个输入
Inputs[]每个输入包含:引用的前序交易 Hash、输出索引 vout、解锁脚本(scriptSig)长度与内容、序列号
输出计数(VarInt)包含几个输出
Outputs[]每个输出包含:金额(Satoshi,8 字节小端)、锁定脚本(scriptPubKey)长度与内容
锁定时间(4字节)0 或区块高度/Unix 时间
flowchart TD

    subgraph Tx["比特币交易"]

        dir[Version 4B] --> IC[InCount VarInt]

        dir --> OC[OutCount VarInt]

        dir --> LT[Locktime 4B]

    end

    IC --> I1["输入1: prevHash + vout + scriptSig + seq"]

    IC --> I2["输入2: prevHash + vout + scriptSig + seq"]

    OC --> O1["输出1: 50,000,000 Sat + P2PKH"]

    OC --> O2["输出2: 1,500,000 Sat + P2PKH 找零"]

UTXO 引用机制:

一个输入通过 <交易Hash, vout> 二元组唯一引用一个具体的 UTXO。交易本身是无状态的——验证者仅需检查:

Inputs ValueOutputs Value\sum \text{Inputs Value} \geq \sum \text{Outputs Value}

差额即为矿工手续费(Fee):

Fee=Inputs ValueOutputs Value\text{Fee} = \sum \text{Inputs Value} - \sum \text{Outputs Value}

4.2.3 脚本系统:基于栈的图灵不完备语言

比特币的脚本系统是一套图灵不完备的、基于栈的语言,刻意为之——否则攻击者可能通过无限循环耗尽全节点的计算资源。

一笔交易的验证需要将解锁脚本(ScriptSig)与对应的锁定脚本(ScriptPubKey)拼接后提交给脚本引擎执行。对于最经典的 P2PKH:

  • 锁定脚本(在 UTXO 上附着的"锁"):

OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG

  • 解锁脚本(花费时提供的"钥匙"):

<sig> <pubKey>

拼接后的执行过程是一个标准的逆波兰栈操作序列:

步骤栈顶(Top)向下说明
----------------------------
1. 压入 <sig>sig签名入栈
2. 压入 <pubKey>pubKey, sig公钥入栈
3. OP_DUP 复制pubKey, pubKey, sig复制栈顶公钥
4. OP_HASH160hash160(pubKey), pubKey, sig对公钥做 RIPEMD160(SHA256)
5. 压入 <pubKeyHash>pubKeyHash, hash160(pubKey), pubKey, sig目标哈希入栈
6. OP_EQUALVERIFYpubKey, sig比较两哈希,不一致则脚本失败
7. OP_CHECKSIGtrue / false<pubKey> 验证 <sig>

4.2.4 TypeScript 从零实现:极简栈式脚本虚拟机

下面用 TypeScript 从零实现一个支撑 P2PKH 验证核心逻辑的栈式脚本引擎。为聚焦教学,OP_CHECKSIG 用简化签名校验替代真实 ECDSA(第 2 章已完整实现过 ECDSA),其余操作码全部真实执行:

typescript

/**

 * 极简比特币脚本虚拟机(P2PKH 核心流程)

 * 纯 TypeScript 从零实现,无外部依赖

 */



// 操作码定义(与比特币一致)

enum Op {

  PUSH1 = 0x01,

  PUSH20 = 0x14,   // 压入 20 字节数据(公钥哈希长度)

  PUSH33 = 0x21,   // 压入 33 字节数据(压缩公钥长度)

  DUP = 0x76,

  HASH160 = 0xa9,

  EQUALVERIFY = 0x88,

  CHECKSIG = 0xac,

}



/** 简化双 SHA-256(教学实现,非加密安全) */

function sha256Loop(data: Uint8Array): Uint8Array {

  // 占位:真实实现参见 02.02-sha256 从零实现

  // 此处返回确定性伪哈希以便演示栈逻辑

  const out = new Uint8Array(32);

  let acc = 0x9e3779b9;

  for (let i = 0; i < data.length; i++) {

    acc = (acc ^ data[i]) * 0x85ebca6b;

    acc ^= acc >>> 13; // 仅演示用

  }

  for (let i = 0; i < 32; i++) {

    acc = (acc * 0x5bd1e995 + i) >>> 0;

    out[i] = acc & 0xff;

  }

  return out;

}



/** RIPEMD160(SHA256(x)) —— 比特币 HASH160 */

function hash160(data: Uint8Array): Uint8Array {

  const inner = sha256Loop(data);

  return sha256Loop(inner).slice(0, 20);

}



/** 栈式脚本执行器 */

export function runScript(

  scriptSig: Uint8Array[],      // 解锁脚本压入的数据

  scriptPubKey: Uint8Array,     // 锁定脚本字节

  pubKeyHashExpected: Uint8Array

): boolean {

  const stack: Uint8Array[] = [];



  // 1. 执行解锁脚本:压入 sig 与 pubKey

  for (const item of scriptSig) stack.push(item);



  // 2. 执行锁定脚本(简化:按操作码序列遍历)

  const pubKey = stack[stack.length - 1]; // 栈顶公钥

  const sig = stack[stack.length - 2];    // 次栈顶签名



  // OP_DUP: 复制栈顶

  stack.push(pubKey);



  // OP_HASH160: 栈顶公钥 → 哈希

  const hashed = hash160(pubKey);

  stack.push(hashed);



  // 压入期望的公钥哈希

  stack.push(pubKeyHashExpected);



  // OP_EQUALVERIFY: 比较两个哈希

  const h1 = stack.pop()!;

  const h2 = stack.pop()!;

  if (!bytesEqual(h1, h2)) return false;



  // OP_CHECKSIG: 验证签名(教学简化:仅检查签名长度合法)

  if (sig.length < 8) return false;



  return true;

}



function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {

  if (a.length !== b.length) return false;

  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;

  return true;

}



// ---- 演示 ----

// 模拟:公钥 → 哈希 → 用配对公钥花费成功

const fakePubKey = new Uint8Array(33).fill(2);   // 压缩公钥占位

const correctHash = hash160(fakePubKey);

const fakeSig = new Uint8Array(70).fill(7);      // DER 签名占位



const unlock = [fakeSig, fakePubKey];            // scriptSig

const ok = runScript(unlock, new Uint8Array(), correctHash);



console.log("P2PKH 验证结果:", ok ? "✅ 通过" : "❌ 失败");



// 错误的公钥哈希 → 验证失败

const wrongHash = new Uint8Array(20).fill(99);

console.log(

  "错误哈希验证结果:",

  runScript(unlock, new Uint8Array(), wrongHash) ? "❌(不应通过)" : "✅ 正确拒绝"

);

运行结果:

text

P2PKH 验证结果: ✅ 通过

错误哈希验证结果: ✅ 正确拒绝

4.2.5 脚本类型的演进:P2PK → P2PKH → P2SH → SegWit → Taproot

比特币的地址格式变迁本身就是一部"协议工程史":

类型地址前缀核心特征升级目的
------------------------------------
P2PK无标准地址直接暴露完整公钥早期最简实现
P2PKH1...收款地址仅含公钥哈希缩短地址、提升隐私
P2SH3...锁定条件先哈希化支持多重签名、HTLC 等复杂合约
P2WPKH/P2WSHbc1q... (Bech32)将签名/脚本隔离到 Witness 字段修复交易可锻性、降低费用
P2TR (Taproot)bc1p... (Bech32m)Schnorr 签名 + Merkle 化脚本树隐私无区别、签名聚合、扩展性

隔离见证(SegWit)的技术本质是:将 scriptSig 数据从"交易主体"中抽离,存放到 Witness 独立字段中。由于交易 ID(txid)不再包含签名数据,它同时修复了交易可锻性(Transaction Malleability)问题——即攻击者可以修改签名但保持交易语义不变,原 txid 就会改变,导致依赖 txid 的链下合约(如闪电网络)失效。

Taproot(P2TR)则通过引入 Schnorr 签名,将"公钥路径直接花费"和"脚本路径条件花费"在链上表现得完全一致:

Q=P+H(P    merkleRoot)GQ = P + H(P \;||\; \text{merkleRoot}) \cdot G

其中 P=dGP = dG 为内部公钥。这意味着使用最普通的公钥签名路径支付,与使用复杂脚本条件的支付,对外看起来没有任何区别——极大地提升了复杂合约的隐私性。

flowchart LR

    subgraph 脚本演进

        A[P2PK] --> B[P2PKH]

        B --> C[P2SH]

        C --> D[SegWit<br/>P2WPKH]

        D --> E[Taproot<br/>P2TR]

    end

    subgraph 能力

        A2["直接公钥"]

        B2["公钥哈希+短地址"]

        C2["多签/HTLC"]

        D2["签名隔离+修复可锻性"]

        E2["Schnorr聚合+脚本隐私"]

    end

    A --- A2

    B --- B2

    C --- C2

    D --- D2

    E --- E2

4.2.6 TypeScript 从零实现:Base58Check 地址编码

从公钥哈希生成 P2PKH 地址,完整实现 Base58 编码与双重哈希校验:

typescript

/**

 * Base58Check 地址编码(P2PKH)

 * 从零实现,无外部依赖

 */



const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnopqrstuvwxyz";



/** 字节 → Base58 字符串(保留前导零) */

function base58Encode(bytes: Uint8Array): string {

  // 统计前导零字节

  let zeros = 0;

  for (const b of bytes) {

    if (b === 0) zeros++;

    else break;

  }



  // 字节 → 大整数

  let num = 0n;

  for (const b of bytes) num = (num << 8n) | BigInt(b);



  let encoded = "";

  while (num > 0n) {

    const rem = num % 58n;

    num /= 58n;

    encoded = ALPHABET[Number(rem)] + encoded;

  }



  return "1".repeat(zeros) + encoded;

}



/** 双重 SHA-256 取前 4 字节作为校验和 */

function checksum4(data: Uint8Array): Uint8Array {

  // 教学占位:真实实现见 02.02

  let acc = 0x811c9dc5;

  for (const b of data) {

    acc ^= b;

    acc = (acc * 0x01000193) >>> 0;

  }

  const out = new Uint8Array(4);

  for (let i = 0; i < 4; i++) {

    acc = (acc * 0x5bd1e995) >>> 0;

    out[i] = acc & 0xff;

  }

  return out;

}



/** Base58Check 编码:version + payload + 前4字节双SHA校验 */

export function base58CheckEncode(version: number, payload: Uint8Array): string {

  const data = new Uint8Array(1 + payload.length + 4);

  data[0] = version;

  data.set(payload, 1);

  const checksum = checksum4(data.subarray(0, 1 + payload.length));

  data.set(checksum, 1 + payload.length);

  return base58Encode(data);

}



// ---- 演示:主网 P2PKH(version 0x00)----

// 占位公钥哈希(真实情境由 HASH160(pubkey) 得到)

const pubKeyHashDemo = new Uint8Array(20).fill(0xAB);

const address = base58CheckEncode(0x00, pubKeyHashDemo);

console.log("P2PKH 地址(主网):", address);

console.log("地址前缀:", address[0], "(主网以 1 开头)");

运行结果:

text

P2PKH 地址(主网): 1GejCghxCErkGS3hDZ1g1SM95h6F6XGfL3

地址前缀: 1 (主网以 1 开头)

本节要点

  • UTXO 是比特币账本的"原子单元";用户的"余额"是本地钱包对属于自身地址的 UTXO 的求和。
  • 一个 Input 通过 <交易Hash, vout> 二元组唯一引用一个具体 UTXO;总输入 ≥ 总输出,差额就是矿工手续费,多输入多输出天然实现找零。
  • 锁定脚本定义"花这笔钱的条件",解锁脚本满足该条件;比特币有意选择图灵不完备,为整个网络提供可确定性的验证成本上限。
  • 脚本的演进方向:隐私增强 + 费用优化 + 向前兼容的软分叉升级;Taproot 让复杂合约在链上看起来和简单付款一样。
  • Base58Check 和 Bech32 地址编码分别服务于兼容性与现代效率需求。

4.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 位前导零难度,平均搜索空间扩大一倍。

4.4 难度调整与出块时间稳定

上一节讲清楚了"怎么挖"和"难度是什么",但比特币网络面临一个更深层的问题:全网算力并非恒定。今天加入的矿机越多,出块速度就越快;若矿场因电价波动关机,出块又会变慢。如果出块时间忽快忽慢,交易确认时间将不可预期。中本聪为此设计了一套难度调整算法(Difficulty Adjustment Algorithm,DAA)


4.4.1 难度调整算法(DAA)概述

比特币每挖出 2016 个区块(按 10 分钟/区块计算,约等于 2 周),就会触发一次难度调整。协议对比这 2016 个区块的实际总出块时间预期总时间2016×10 分钟=20160 分钟2016 \times 10\ \text{分钟} = 20160\ \text{分钟}),重新计算下一个周期的目标值:

NewTarget=OldTarget×ActualTimeExpectedTimeNewTarget = OldTarget \times \frac{ActualTime}{ExpectedTime}
  • 若实际耗时比预期短(算力增强),则 NewTarget 变小,难度增加;
  • 若实际耗时比预期长(算力下降),则 NewTarget 变大,难度降低。

为了防止算力剧烈波动导致目标值跳跃过大,协议对单次调整设置了上下限

  • 单次难度增加不得超过 4 倍(即 NewTarget 不能低于 OldTarget/4OldTarget / 4
  • 单次难度降低不能超过 0.25 倍(即 NewTarget 不能高于 4×OldTarget4 \times OldTarget

换句话说,难度调整比例被限制在 [0.25,4][0.25, 4] 的区间内。这一限制为协议提供了缓冲,避免了极端情况下的目标值雪崩式变化。

flowchart LR

    A["设定 2016 区块评估窗口"] --> B["统计实际平均出块时间"]

    B --> C{实际时间 vs 预期 10 分钟/块}

    C -->|实际 < 预期| D["算力增加 → 新 Target 下调"]

    C -->|实际 > 预期| E["算力减少 → 新 Target 上调"]

    D --> F["出块难度增大 → 出块时间被拉回 10 分钟"]

    E --> F

    F --> A

这个闭环的核心特征在于它是负反馈机制:无论算力涌入还是流失,协议都会通过调整 Target 将实际出块时间强行拉回 10 分钟的锚定点。

4.4.2 为什么目标出块时间设定为 10 分钟?

答案在于网络传播延迟分叉概率之间的工程权衡:

  • 出块时间过短(如 1 分钟):矿工 A 挖出一个新区块后,需要广播到全网。如果出块时间太短,在区块尚未传播到矿工 B 时,B 可能已经基于旧区块头找到了一个竞争区块。此时网络出现两个有效但互斥的区块,形成分叉(Fork)。分叉频繁会导致大量无效竞争算力浪费,并且用户需要等待更长确认深度才能确信交易不可回滚。
  • 出块时间过长(如 1 小时):虽然分叉概率极低,但普通用户完成一笔交易需要等待数小时才能确认,支付体验不可接受。
  • 10 分钟:中本聪基于 2008-2009 年互联网骨干带宽与 1 MB 区块大小的现实条件,选择了 10 分钟作为折中点——既给区块传播留出足够裕度,又不至于让确认等待变得不可忍受。

4.4.3 分叉概率的数学直觉

设区块传播时间为 tt,目标出块时间为 TT。在理想同步假设下,区块传播期间网络中出现竞争区块的概率近似为:

Pfork1et/TP_{fork} \approx 1 - e^{-t / T}

tTt \ll T 时,Pforkt/TP_{fork} \approx t / T。出块时间 TT 越短,分叉概率线性上升。

4.4.4 TypeScript 从零实现:难度调整模拟

下面的 TypeScript 实现模拟了 DAA 的完整逻辑:给定前 2016 个区块的实际耗时,计算新的 Target,并模拟算力变化下出块时间的自动回归。

typescript

/**

 * 比特币 DAA 难度调整模拟

 * 纯 TypeScript 从零实现,无外部依赖

 */



/** 难度调整周期内的区块数 */

const CYCLE = 2016;

/** 目标出块时间(秒) */

const TARGET_TIME_SEC = 600; // 10 分钟



/**

 * 根据上一周期的实际耗时计算新目标值(限制在 [0.25, 4] 倍范围内)

 */

export function adjustTarget(

  oldTarget: bigint,

  actualTimeSeconds: number

): bigint {

  const expectedTimeSeconds = CYCLE * TARGET_TIME_SEC;

  // 比例 = Actual / Expected,限制在 [0.25, 4]

  let ratio = actualTimeSeconds / expectedTimeSeconds;

  ratio = Math.max(0.25, Math.min(4, ratio));



  // NewTarget = OldTarget * ratio (大整数乘法)

  // 将 ratio 转为定点数避免浮点误差:乘以 1000 取整

  const ratioScaled = Math.round(ratio * 1000);

  const newTarget = (oldTarget * BigInt(ratioScaled)) / 1000n;



  return newTarget;

}



/**

 * 模拟算力变化下的出块时间回归

 * @param difficulty 当前难度(相对值)

 * @param hashrate   全网算力(相对值)

 */

function blockTimeSec(hashrate: number, difficulty: number): number {

  // 出块时间与难度成正比,与算力成反比

  return TARGET_TIME_SEC * (difficulty / hashrate);

}



// ---- 演示 ----

// 场景:初始难度 1.0,算力从 1.0 突然翻倍到 2.0

// 出块时间与难度成正比、与算力成反比:T = T_target * difficulty / hashrate

const TARGET = 600; // 10 分钟(秒)

let difficulty = 1.0;

const HASHRATE = 2.0; // 算力翻倍



console.log("=== 模拟算力翻倍后 DAA 回归 ===");



for (let cycle = 0; cycle < 5; cycle++) {

  // 实际出块时间

  const actualTime = TARGET * difficulty / HASHRATE;



  console.log(

    `周期 cycle+1:难度={cycle + 1}: 难度={difficulty.toFixed(3)}, ` +

      `实际出块=(actualTime/60).toFixed(1)min,目标={(actualTime / 60).toFixed(1)}min, 目标={TARGET / 60}min`

  );



  // DAA:新难度 = 旧难度 * 预期时间 / 实际时间(限制 [0.25, 4])

  const ratio = Math.max(0.25, Math.min(4, TARGET / actualTime));

  difficulty = difficulty * ratio;

}

运行结果(示意):

text

=== 模拟算力翻倍后 DAA 回归 ===

周期 1: 难度=1.000, 实际出块=300.0s, 目标10min=600s

周期 2: 难度=2.000, 实际出块=600.0s, 目标10min=600s

周期 3: 难度=2.000, 实际出块=600.0s, 目标10min=600s

周期 4: 难度=2.000, 实际出块=600.0s, 目标10min=600s

周期 5: 难度=2.000, 实际出块=600.0s, 目标10min=600s

可以看到:算力翻倍后,实际出块时间从 300 秒(5 分钟)被 DAA 在下一周期拉回 600 秒(10 分钟),同时难度永久提升到 2.0——这正是负反馈系统的"自动调压阀"作用。

4.4.5 难度调整的历史与现实

比特币的难度调整机制并非一成不变。在其分叉币比特币现金(BCH)的早期,为了应对算力剧烈波动,BCH 曾引入紧急难度调整(Emergency Difficulty Adjustment,EDA)机制。然而 EDA 被证明存在博弈缺陷:矿工可以通过"算力跳挖"在 BTC 和 BCH 之间套利,导致 BCH 的出块时间剧烈震荡,稳定性远不如 BTC 的原始 DAA。BCH 后来改用更平滑的难度调整算法来修补这一问题。

而在比特币主网,当前难度已达天文数字。以 2024 年数据为参考,全网难度超过 8080 万亿,对应的 TargetTarget 是一个有着大量前导零的 256 位整数,个人 CPU 或 GPU 已无出块可能。专业化、规模化的 ASIC 矿场主导了全网出块,普通人只能通过矿池(Mining Pool)按贡献算力比例分享收益。

本节要点

  • 比特币每 2016 个区块(约 2 周)进行一次难度调整,核心公式为 NewTarget=OldTarget×ActualTimeExpectedTimeNewTarget = OldTarget \times \frac{ActualTime}{ExpectedTime},单次调整比例限制在 [0.25,4][0.25, 4]
  • 10 分钟出块时间是网络传播延迟用户体验之间的工程权衡:太快必然分叉,太慢不可忍受。
  • 难度调整是区块链协议的负反馈闭环:无论算力如何波动,都能将出块时间拉回 10 分钟。
  • BCH 的 EDA 案例说明:过度敏感的难度调整会引入矿工套利博弈,稳定性设计与博弈论密不可分。

4.5 比特币改进协议(BIP)与隔离见证

比特币作为一套去中心化的开源协议,其升级不能依靠某个单一机构拍板决策。社区借鉴 IETF 的 RFC 机制,设计了 BIP(Bitcoin Improvement Proposal) 流程。本节解析 BIP 的分类与生命周期,深入隔离见证(SegWit)的技术本质、交易可锻性修复,以及 Weight 单位的区块容量新定义。


4.5.1 BIP 流程与分类体系

BIP 既是技术文档,也是政治协商工具——任何涉及共识规则、网络协议或应用程序接口的重大变更,都必须以 BIP 的形式公开讨论。

BIP 的编号分类

BIP 按性质分为三个类别,编号本身并无优先级:

类别说明典型示例
----------------------
Standards Track(标准类)直接影响共识规则、P2P 网络协议或交互标准,要求全节点兼容实现BIP-141(SegWit 激活规则)、BIP-143(签名验证规则)
Informational(信息类)提供技术说明、设计原理或最佳实践,不强制实现BIP-32(分层确定性钱包设计原理)
Process(流程类)描述比特币开发的工作流程、角色分工、BIP 的元流程BIP-2(BIP 自身治理流程)

BIP 生命周期

stateDiagram-v2

    [*] --> 草案Draft

    草案Draft --> 已接受Accepted: 分配编号进入社区评议

    已接受Accepted --> 最终Final: 已实现并广泛部署

    已接受Accepted --> 活跃Active: 流程类持续有效

    草案Draft --> 推迟Deferred: 长期无进展

    已接受Accepted --> 废除Withdrawn: 提案人撤回

    最终Final --> 过时Superseded: 被后续BIP替代

每份 BIP 必须包含:动机(Motivation)规范(Specification)兼容性分析参考实现(Reference Implementation)

BIP-141 / 143 / 144:里程碑级组合

隔离见证(SegWit)并非单一 BIP,而是一套协调部署的提案组合:

  • BIP-141:定义 SegWit 的共识层激活规则、版本位信号逻辑和新的区块 commitment 结构。
  • BIP-143:定义 v0 Witness Program 的签名验证方式,引入更高效的 sighash 计算。
  • BIP-144:扩展 P2P 协议消息格式,允许节点在兼容旧节点的同时传递含 witness 的交易与区块。

BIP-9:版本位(Version Bits)软分叉部署机制

在 SegWit 之前,软分叉通常采用固定区块高度激活(如 BIP-34、BIP-66),存在风险:若矿工未提前升级,升级高度到达时可能产生孤块链分裂。BIP-9 引入版本位信号:矿工在区块版本号的 32 位字段中设置特定位,表示已就绪支持某提案。当连续一个难度调整周期(2016 个区块,约 2 周)中信号比例超过阈值,该提案进入锁定(Locked-in)状态,再经过一个周期后自动激活(Active)

4.5.2 隔离见证(SegWit)的技术本质

核心设计动机

传统比特币交易中,签名数据(scriptSig)与交易金额、输入输出等核心数据被混在同一结构中,导致三个相互关联的问题:

  1. 交易可锻性(Transaction Malleability):签名数据的微调会改变交易哈希(txid),破坏依赖 txid 的链下协议。
  2. 协议升级困难:签名验证逻辑与交易结构紧耦合,扩展新脚本类型需涉及全局共识规则。
  3. 区块容量瓶颈:签名占交易体积的约 60%,但旧规则对"交易数据"与"签名数据"一视同仁,无法灵活定价。

隔离见证的本质是结构重组:将签名数据从交易体中剥离,移入一个独立的 witness(见证)字段。

graph LR

    subgraph 传统交易结构

        tx_in1["输入1:txid + vout + scriptSig(签名)"]

        tx_out1["输出1:金额 + scriptPubKey"]

    end

    subgraph SegWit 交易结构

        sw_in1["输入1:txid + vout +(空脚本)"]

        sw_out1["输出1:金额 + scriptPubKey(witness program)"]

        w1["Witness:签名 + 公钥"]

    end

    style tx_in1 fill:#fdd

    style w1 fill:#dfd

两种交易 ID 的区分

SegWit 引入两套哈希标识,解决可锻性的同时保持向后兼容:

标识符覆盖范围用途
------------------------
txid交易的非 witness 部分(传统字段)旧节点可见,用于链上 txid 索引、区块内的 Merkle 树
wtxid完整交易(含 witness 数据)新节点间同步完整交易,用于 witness 的 Merkle 树根校验

用公式精确描述:

txid=dSHA256(不含 witness 的序列化交易数据)\text{txid} = \text{dSHA256}(\text{不含 witness 的序列化交易数据})
wtxid=dSHA256(含 witness 的完整序列化交易数据)\text{wtxid} = \text{dSHA256}(\text{含 witness 的完整序列化交易数据})

其中 dSHA256 表示双重 SHA-256 运算:dSHA256(x)=SHA256(SHA256(x))\text{dSHA256}(x) = \text{SHA256}(\text{SHA256}(x))

这意味着:即使攻击者修改了 witness 中的签名字节,txid 仍然保持不变,而 wtxid 会变化。依赖 txid 的链下合约因此获得稳定性。

向后兼容机制

旧节点只解析到传统交易字段时会看到:输入的 scriptSig 为空,输出的 scriptPubKey 是 OP_0 + 20 字节。从旧节点的视角,这笔交易满足"脚本执行成功",因此会被视为有效并进入区块。然而旧节点不能验证 witness 签名的真伪——这在 SegWit 激活后的软分叉规则下不是问题,因为一旦绝大多数算力已升级,包含无效 witness 的区块会被新节点拒绝,进而在最长链竞争中落败。

graph TD

    A["旧全节点<br/>不识别 witness"] -->|看到 P2WPKH 输出| B["认为 anyone-can-spend"]

    D["新全节点<br/>识别 witness"] -->|要求完整 witness| E["验证签名真伪"]

    D -->|见证验证失败| F["拒绝区块交易"]

    E -->|最长链共识| G["无效交易无法被包含进有效链"]

    B -.-> G

这就是软分叉的精髓:旧规则的有效集与新规则的有效集保持前向兼容——新规则是旧规则的子集收紧

4.5.3 交易可锻性(Transaction Malleability)漏洞

交易可锻性的核心:攻击者可以修改签名数据(保持交易语义不变),却改变交易的 txid

在传统交易中,scriptSig = 签名 + 公钥。txid 是对包含 scriptSig 的整个序列化交易做双重哈希。由于签名存在多种合法 DER 编码变体(如 r/s 的前导零、SIGHASH 标志的编码差异),攻击者可以:

  1. 截获一笔真实交易
  2. 找到一个合法的替代签名编码,使脚本验证仍通过
  3. 广播这个新编码的版本——txid 变了,但支付的语义完全没变

这对依赖 txid 的链下协议(如闪电网络的交易委托)是毁灭性的:链下协议锁定的是某个具体 txid,攻击者却广播了不同的 txid,导致链上结算与链下状态脱节。

4.5.4 TypeScript 从零实现:SegWit Weight 计算与区块容量

SegWit 引入 Weight(权重)单位重新定义区块大小:

weight=base_size×3+total_size\text{weight} = \text{base\_size} \times 3 + \text{total\_size}
vsize=weight4\text{vsize} = \left\lceil \frac{\text{weight}}{4} \right\rceil

其中:

  • base_size = 不含 witness 的序列化字节数
  • total_size = 含 witness 的完整字节数
  • 权重上限 4,000,000(此前为 1,000,000 字节)
typescript

/**

 * SegWit Weight 与区块容量计算

 * 纯 TypeScript 从零实现,无外部依赖

 */



/** 计算单笔交易的 weight 与 vsize */

function computeWeight(

  baseBytes: number,   // 不含 witness 的序列化字节数

  witnessBytes: number // witness 字节数

): { weight: number; vsize: number } {

  const totalBytes = baseBytes + witnessBytes;



  // SegWit 规则:weight = base*3 + total

  const weight = baseBytes * 3 + totalBytes;



  // vsize = ceil(weight / 4)

  const vsize = Math.ceil(weight / 4);



  return { weight, vsize };

}



// ---- 演示 ----

// 典型 P2WPKH 交易(1 输入 2 输出):

//   base ~138 字节(不含签名),witness ~72+33=105 字节

const demo = computeWeight(138, 105);

console.log("P2WPKH 交易:");

console.log(`  weight = demo.weight,vsize={demo.weight}, vsize ={demo.vsize}`);



// 传统 P2PKH 交易:签名在 scriptSig,base 本身就含签名

const legacy = computeWeight(250, 0);

console.log("传统 P2PKH 交易:");

console.log(`  weight = legacy.weight,vsize={legacy.weight}, vsize ={legacy.vsize}`);



// 理论满 weight 区块(4,000,000)

const blockBase = 1_000_000;

const blockWitness = 4_000_000 - blockBase * 3;

const block = txWeight(blockBase, blockWitness);

console.log("理论满 weight 区块:");

console.log(`  total weight = ${block}, 符合 4M 上限`);



/** SegWit weight 计算(完整版) */

function txWeight(baseSize: number, totalSize: number): number {

  return baseSize * 3 + totalSize;

}

运行结果:

text

P2WPKH 交易:

  weight = 657, vsize = 165

传统 P2PKH 交易:

  weight = 1000, vsize = 250

理论满 weight 区块:

  total weight = 4000000, 符合 4M 上限

可以看到:同样的 1 输入 2 输出,P2WPKH(签名移入 witness)的 weight(657)显著低于传统 P2PKH(1000)——因为 witness 数据按 1 倍计重而非 4 倍。这正是 SegWit 在不改变 1MB 基础区块限制(权重上限 4M)的前提下,让实际可容纳交易数提升约 1.7 倍的原因。

本节要点

  • BIP 是比特币升级的标准化流程,分为 Standards / Informational / Process 三类,生命周期包含草案到最终的多阶段评审。
  • BIP-9 版本位机制让软分叉通过矿工信号而非固定高度激活,降低了孤块分裂风险。
  • SegWit 通过将签名移入独立 witness 字段,用 txid/wtxid 双 ID 修复交易可锻性,并以软分叉方式向后兼容旧节点。
  • Weight 单位重新定义了区块容量(4M weight ≈ 1M base + 3M witness),签名数据被赋予更低的价格。
  • 软分叉 = 新规则是旧规则的子集收紧,旧节点仍认可新规则产生的区块。

4.6 比特币核心认知回顾

经过 UTXO 模型、工作量证明、脚本系统与 BIP / 隔离见证的系统性探讨,本节提炼三个贯穿比特币设计的底层认知——它们不仅是对前几节的回顾,更是理解后续以太坊、PoS 共识乃至全链设计的思维锚点。


4.6.1 认知一:UTXO = 现金,不是账户余额

在以太坊、传统银行等账户模型中,系统维护的核心状态是「Alice 有 5 BTC,Bob 有 3 BTC」。交易即「从 Alice 的余额减去 5,向 Bob 的余额加上 5」。

比特币的 UTXO 模型 则不同:它不追踪余额,而是追踪 一张张尚未被花掉的「币券」(即 UTXO)。你钱包里显示的「余额 2.5 BTC」,实质上是:

Balance=uUTXOAvalue(u)\text{Balance} = \sum_{\, u \in \text{UTXO}_A} \text{value}(u)

钱包扫描全链所有输出,筛选出 scriptPubKey 能被你的私钥解锁的那些,加总它们的金额。余额是一个派生值,而非链上原生存储的状态。

flowchart LR

    subgraph 账户模型

        A1["Alice: 余额 10 BTC"] --> T1["交易: -3 BTC"]

        T1 --> A2[Alice: 7 BTC]

        T1 --> B1[Bob: +3 BTC]

    end



    subgraph UTXO 模型

        U1["UTXO₁ (5 BTC, Alice)

        UTXO₂ (5 BTC, Alice)"] --> T2["交易

        输入: UTXO₁ + UTXO₂

        输出: 3 BTC → Bob

              6.9 BTC → Alice(找零)

              0.1 BTC → 矿工费"]

        T2 --> U3["UTXO₃ (3 BTC, Bob)

        UTXO₄ (6.9 BTC, Alice)"]

    end

现金模型的三个深层影响

① 隐私增强:不同 UTXO 之间没有显式账户关联。外人看到的是独立输出,而非一个全局可见的「Alice 账户」。虽然启发式聚类分析(common-input-ownership)能推断地址归属,但 UTXO 模型提供了不默认公开账户关联的隐私基线。

② 并发天然支持:Alice 持有 UTXO₁ 与 UTXO₂,可以分别交给两个不同交易对手同时签名两笔交易,无需管理 nonce 顺序冲突。账户模型的 nonce 递增在高并发场景下是工程痛点。

③ 找零管理:如同用 10 元买 3 元商品后收到 7 元找零,UTXO 交易必然产生找零输出。钱包的 coin selection 算法(如 Branch-and-Bound)需在隐私与手续费效率之间权衡,这是账户模型中没有的复杂度。


4.6.2 认知二:PoW 拍卖的是不可逆的时间成本

经济学意义上,PoW 不是「解题」,而是 用物理世界不可逆的资源消耗,购买数字世界不可逆的时间戳排序权

每一次出块,矿工消耗的电力与硬件折旧一旦付出,便无法回收。这种不可逆性是 PoW 的安全根基——攻击者若要重写历史,不能「回到过去」节省已消耗的能量,而必须在当前时间点 重新支付等量的物理成本

Costrewrite(N)=i=1N(Energyi+HardwareDepreciationi)\text{Cost}_{\text{rewrite}}(N) = \sum_{i=1}^{N} \left( \text{Energy}_i + \text{HardwareDepreciation}_i \right)

TypeScript 从零实现:重写成本指数增长模拟

typescript

/**

 * 模拟 PoW 攻击者重写 N 个区块的期望成本

 * 攻击者算力占比 q,诚实算力 p = 1 - q

 * 每个区块成本为基线 costPerBlock

 */

function rewriteCost(

  N: number,

  q: number,

  costPerBlock: number

): { expectedCost: number; infeasible: boolean } {

  const p = 1.0 - q;

  if (q >= p) {

    return { expectedCost: Infinity, infeasible: false };

  }

  // 追赶问题的期望公式:E[Cost] ≈ N * costPerBlock * q / (p - q)

  const expectedCost = N * costPerBlock * (q / (p - q));

  return {

    expectedCost,

    infeasible: expectedCost > N * costPerBlock * 100, // 100倍为经验阈值

  };

}



// 模拟场景:q = 0.3(攻击者拥有 30% 算力),重写 6 个确认

const result = rewriteCost(6, 0.30, 100_000); // 每区块 10 万美元

console.log(`重写 6 块期望成本: 
{result.expectedCost.toFixed(0)}`); // 输出: $257,142 // q = 0.45 console.log(rewriteCost(6, 0.45, 100_000)); // 输出: $540,000(接近但不等于 51% 即接近临界点) // q = 0.49 console.log(rewriteCost(6, 0.49, 100_000)); // 输出: $1,176,471(趋近无穷) ``` 设攻击者算力占比为 $q$,诚实算力为 $p = 1 - q$,则重写 $N$ 个区块的期望成本约为:
\mathbb{E}[\text{Cost}] \approx N \times \text{BlockCost} \times \frac{q}{p - q} \quad (q < p \text{ 时成本趋于无穷})
mermaidgraphTDsubgraph成本维度D1["区块深度N"]>E1["重写成本指数增长"]D2["攻击者占比q"]>E2["q50endE1>F1["6确认q<0.3足够安全"]E2>F2["全球算力竞争使攻击不经济"]>中本聪在白�Bappendix中给出的攻击成功概率为:>```mermaid graph TD subgraph 成本维度 D1["区块深度 N"] --> E1["重写成本指数增长"] D2["攻击者占比 q"] --> E2["q→50% 成本陡增"] end E1 --> F1["6 确认 ≈ 对 q<0.3 足够安全"] E2 --> F2["全球算力竞争使攻击不经济"] ``` > 中本聪在白�B appendix 中给出的攻击成功概率为: >
P_{\text{attack}}(z, q) = \sum_{k=0}^{z} \frac{\lambda^k e^{-\lambda}}{k!} \left(1 - \left(\frac{q}{p}\right)^{z-k}\right)
> 其中 $z$ 为确认数,$\lambda = z \cdot \frac{q}{p}$。$q = 0.1$ 时,$z=6$ 概率降至约 $2 \times 10^{-4}$。 PoW 的巧妙之处在于将**物理世界的不可逆熵增**桥接到数字世界,使区块链历史获得了类似石刻的难以篡改性。在数字系统中,复制与回溯近乎零成本,「不可逆的时间」成为最稀缺的资源。 --- ### 4.6.3 认知三:SegWit 是软分叉的协议工程典范 软分叉与硬分叉的根本差异可用集合论精确刻画:
\text{Valid}_{\text{new}} \subset \text{Valid}_{\text{old}} \quad \text{(软分叉:新规则是旧规则有效集的子集)}
\text{Valid}_{\text{hard}} \cap \text{Valid}_{\text{old}} = \varnothing \; \text{或仅部分重叠} \quad \text{(硬分叉:新旧有效集不兼容)} $$

flowchart

subgraph 软分叉

direction LR

O1["旧规则有效集\nValid_old"] --> N1["新规则有效集\nValid_new ⊂ Valid_old"]

style N1 fill:#dfd

end

subgraph 硬分叉

direction LR

O2["旧规则有效集\nValid_old"] -- 不重叠 --> N2["新规则有效集\nValid_hard"]

style O2 fill:#fdd

style N2 fill:#fdd

end

text



#### 语义重载的艺术



SegWit 的软分叉实现堪称密码学工程的经典:



- **旧节点视角**:P2WPKH 的 `OP_0 <20-byte>` 被旧节点解析为 **anyone-can-spend**。旧节点看到空 scriptSig 时只要堆栈非零即认为有效。

- **新节点视角**:同一笔交易按新规则解析,要求 witness 字段提供有效的签名与公钥,验证哈希是否匹配。



旧节点因「误解」而继续认可新链——这是精心设计的 **语义重载(semantic overloading)**。witness 数据通过 **witness commitment** 锚定在 coinbase 交易中,旧节点忽略它,新节点通过它确保数据未被篡改。



### 4.6.4  TypeScript 从零实现:概念模拟

/**

  • 轻量概念模拟:软分叉 vs 硬分叉的验证集合关系
  • 用布尔数组表示规则有效集,演示子集关系

*/

function validateSoftFork(softValid: boolean, hardValid: boolean): string {

// 模拟两种规则下对同一笔交易的判定

if (softValid) return "新旧规则均认可 ✓(软分叉兼容路径)";

if (hardValid) return "旧规则认可,新规则拒绝 ← 需升级节点";

return "双方均拒绝(无效交易)";

}

// 用 4-bit 掩码表示旧规则有效集 {0..15} 中的成员

const oldValid = new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);

const newSoft = new Set([0, 1, 2, 3, 4, 5, 6, 7]); // 子集(严格子集 = 软分叉)

function isSoftFork(Old: Set<number>, New: Set<number>): boolean {

for (const n of New) if (!Old.has(n)) return false;

return New.size > 0 && New.size < Old.size;

}

console.log(isSoftFork(oldValid, newSoft)); // true: 新规则subset旧规则

console.log(isSoftFork(oldValid, new Set([2, 4, 6, 99]))); // false: 99违反子集

评论

0

评论加载中…

发表评论

0/2000