经过前几节的工作,我们已经构建了一个包含 PoW 挖矿、交易签名验证、P2P 网络与 HTTP API 的迷你区块链。本节将把各模块串联起来,通过完整的运行示例和自动化测试验证系统可靠性。
一键启动三节点网络
为了方便演示,我们提供一个自动化启动脚本 scripts/run_3_nodes.sh,它会在本地启动三个独立的节点进程,分别绑定 5001、5002、5003 端口:
#!/bin/bash
# scripts/run_3_nodes.sh
python node.py --port 5001 --data-dir ./data/node1 &
python node.py --port 5002 --data-dir ./data/node2 --bootstrap http://127.0.0.1:5001 &
python node.py --port 5003 --data-dir ./data/node3 --bootstrap http://127.0.0.1:5001 &
waitflowchart LR
subgraph Scenario1 [场景一:正常出块广播]
A[节点1<br/>创建交易→签名] --> B[节点1<br/>挖矿出块]
B --> C[节点2<br/>同步新区块]
B --> D[节点3<br/>同步新区块]
end
subgraph Scenario2 [场景二:分叉与最长链]
E[节点2 挖矿<br/>分叉A] --> F{等N个区块}
G[节点3 挖矿<br/>分叉B] --> F
F --> H[较长分叉胜出<br/>孤立区块回滚]
end
Scenario1 --> Tests[Pytest 套件]
Scenario2 --> Tests
节点启动后,每个节点会自动加载本地存储的区块链数据,通过 bootstrap 节点发现对等节点,并开始监听来自其他节点的区块和交易广播。
模拟场景一:正常出块与广播
该场景模拟一条理想的交易链,交互流程如下:
sequenceDiagram
participant N1 as Node1 (:5001)
participant N2 as Node2 (:5002)
participant N3 as Node3 (:5003)
Note over N1,N3: 启动后建立P2P连接
N1->>N1: 创建交易并签名
N1->>N1: 运行PoW挖出新区块
N1-->>N2: 广播 Block + Tx
N1-->>N3: 广播 Block + Tx
N2->>N2: 验证区块 & 交易签名
N3->>N3: 验证区块 & 交易签名
Note over N2,N3: 链长度+1,交易池清空该交易
- 节点1 创建交易:Alice 向 Bob 转账 10 个代币,使用 Alice 的私钥对交易哈希签名。
- 节点1 挖矿打包:运行 PoW,调整 nonce 直到区块哈希满足当前难度目标。
- 广播新区块:节点1 将新区块(含交易)通过 HTTP POST 发送给所有已知 peer。
- 节点2/3 同步:收到新区块后验证哈希链连续性、交易签名有效性,确认通过后追加到本地链。
关键验证点:同步后三个节点的链高度一致,节点2 和 3 的交易池中不再包含已被打包的交易。
模拟场景二:分叉与最长链规则
真实的区块链网络中,分叉是网络延迟导致的自然现象。我们通过控制挖矿时机来人为制造分叉:
- 节点2 和 节点3 在同一高度(例如高度 5)同时开始挖矿。
- 节点2 先找到合法 nonce,生成区块 A(高度 5);节点3 随后也找到合法 nonce,生成区块 B(高度 5)。
- 节点2 广播区块 A,节点3 广播区块 B——此时网络中出现了两个合法分叉。
- 继续挖矿:假设节点3 率先挖出下一个区块 C(高度 6),将其链接到区块 B 之后。
- 最长链规则生效:节点2 收到区块 B + C 后,发现该链长度(7)大于本地链(6),自动切换分叉,回滚高度 5 的区块 A,将区块 B 和 C 追加到链尾。
最长链选择的形式化描述为:
当两条链长度相同时,选择累积工作量(即所有区块的难度值之和)更大的分支。
配套 pytest 测试套件
为了确保每次修改后系统功能不受影响,我们编写了三组核心测试用例:
# tests/test_integration.py
import pytest
from blockchain import Blockchain, Transaction
from crypto import sign_tx, verify_tx
def test_chain_validity(chain_with_blocks):
"""验证链上每个区块的哈希与前块Hash字段连续性"""
for i in range(1, len(chain_with_blocks.chain)):
block = chain_with_blocks.chain[i]
prev_block = chain_with_blocks.chain[i - 1]
assert block.prev_hash == prev_block.hash
assert block.hash == block.calculate_hash()
def test_tx_signature_verification(key_pair):
"""构造合法/篡改交易,断言验证通过/拒绝"""
priv_key, pub_key = key_pair
tx = Transaction(from_addr=pub_key, to="Bob", amount=5, nonce=1)
tx.signature = sign_tx(tx.hash(), priv_key)
assert verify_tx(tx, pub_key) == True
# 篡改交易内容
tx.amount = 100
assert verify_tx(tx, pub_key) == False
def test_difficulty_adjustment(mock_time):
"""构造跨难度周期的时间戳,验证目标值重新计算"""
bc = Blockchain(difficulty=4)
# 模拟出块时间远快于预期(10秒出了20个区块)
for i in range(20):
bc.mine_block([], timestamp=bc.last_block.timestamp + 0.5)
# 验证难度已增加(目标值变小)
assert bc.target < (1 << (256 - 4))测试夹具(fixtures)封装在 conftest.py 中,提供临时目录、模拟时钟和预生成的密钥对:
# tests/conftest.py
import pytest
from ecdsa import SigningKey, SECP256k1
@pytest.fixture
def key_pair():
sk = SigningKey.generate(curve=SECP256k1)
vk = sk.verifying_key
return sk, vk
@pytest.fixture
def mock_time():
import time
original = time.time
time.time = lambda: 1234567890
yield
time.time = original难度调整公式
当新区块的出块时间偏离预期时,难度目标值按以下公式调整:
其中 expected_time 是预设的期望出块间隔(如 10 秒),actual_time 是上一个难度周期中所有区块的实际平均出块时间。
要点总结:
- 三节点启动脚本实现了 P2P 网络的快速部署与自动发现
- 正常场景验证了交易创建→签名→挖矿→广播→同步的完整链路
- 分叉场景演示了最长链规则的自动切换与孤立区块回滚
- pytest 套件从链结构、签名验证、难度调整三个维度保障代码质量
16.8 扩展挑战:账户模型与 UTXO 对比实现
前 16.1–16.7 的迷你区块链采用余额模型(Account-based Model,类似以太坊),即每个地址对应一个余额数值,交易直接修改地址余额。本节作为选做实验,引导你将余额模型改写为 UTXO 模型(Unspent Transaction Output Model,类似比特币),并讨论两种设计的本质差异。
UTXO 模型的核心数据结构
UTXO 模型中没有"账户余额"的概念,取而代之的是交易输出(TxOutput)——每一笔输出都是"一定数量的代币,被锁定给某个公钥哈希"。当你要花费这些代币时,需要提供交易输入(TxInput),引用前序交易的输出并附上签名证明所有权。
classDiagram
class Transaction {
+txid: str
+inputs: list[TxInput]
+outputs: list[TxOutput]
+is_coinbase: bool
+hash() -> str
}
class TxInput {
+prev_txid: str
+output_index: int
+signature: bytes
+unlock_script: str
}
class TxOutput {
+amount: int
+pubkey_hash: str
+lock_script: str
}
class UTXOSet {
-utxos: dict[str, TxOutput]
+apply_tx(tx: Transaction) -> bool
+revert_tx(tx: Transaction)
+validate_inputs(inputs: list[TxInput]) -> bool
}
Transaction "1" *-- "many" TxInput
Transaction "1" *-- "many" TxOutput
UTXOSet --> TxOutput : manages
TxInput --> TxOutput : references
Coinbase 交易
每个区块的第一个交易是 Coinbase 交易(类似于余额模型中的区块奖励发放),它没有输入(或输入为特殊占位符),输出金额为区块奖励与手续费之和:
UTXO 守恒约束
每笔交易的输入总额必须 ≥ 输出总额,差额即为矿工收取的手续费。这是 UTXO 模型的核心不变量:
当输入总额 > 输出总额时,矿工地址自动获得一个找零输出,金额为差值,打包在 coinbase 交易中。
UTXO 集管理与链重组
UTXO 集是内存中的 {outpoint: TxOutput} 字典,用于快速查詢某个输出是否未被花费:
class UTXOSet:
def __init__(self):
self.utxos: dict[OutPoint, TxOutput] = {}
def apply_tx(self, tx: Transaction) -> bool:
"""验证并应用一笔交易:删除已花费输出,添加新输出"""
if not self.validate_inputs(tx.inputs):
return False
for inp in tx.inputs:
outpoint = OutPoint(inp.prev_txid, inp.output_index)
del self.utxos[outpoint] # 删除已花费输出
for idx, outp in enumerate(tx.outputs):
outpoint = OutPoint(tx.txid, idx)
self.utxos[outpoint] = outp # 添加新输出
return True
def revert_tx(self, tx: Transaction):
"""链重组时回滚:恢复旧输出,删除新输出"""
for idx, outp in enumerate(tx.outputs):
outpoint = OutPoint(tx.txid, idx)
del self.utxos[outpoint]
for inp in tx.inputs:
outpoint = OutPoint(inp.prev_txid, inp.output_index)
self.utxos[outpoint] = ... # 从存档中恢复链重组的挑战:当最长链切换时,原分叉上的所有交易需要回滚——被花费的 UTXO 需要恢复,新区块中的 UTXO 需要删除。这要求 UTXOSet 支持快照或操作日志,以便在重组时回退到之前的状态。
两种模型的多维度对比
flowchart TD
A[余额模型<br/>Account-based] --> B[UTXO模型<br/>改写方向]
B --> C1[Coinbase交易<br/>作为区块首个交易]
B --> C2[TxInput 结构<br/>prev_txid + output_index + sig]
B --> C3[TxOutput 结构<br/>amount + pubkey_hash]
B --> C4[UTXO 集管理<br/>字典增删 + 双花检查]
B --> C5[链重组回滚<br/>恢复旧UTXO / 删除新UTXO]
C1 --> D[对比讨论]
C2 --> D
C3 --> D
C4 --> D
C5 --> D
D --> E1[代码复杂度<br/>~200行 vs ~450行]
D --> E2[并行性<br/>无锁并发 vs 账户锁]
D --> E3[隐私性<br/>关联性暴露 vs 单地址追踪]
D --> E4[真实感<br/>更贴近比特币协议]
| 维度 | 余额模型(以太坊风格) | UTXO 模型(比特币风格) |
|---|---|---|
| 代码复杂度 | 核心逻辑约 200 行,状态维护简单 | 引入输入/输出结构、UTXO 集、重组回滚,约 450 行 |
| 并行性 | 同一账户并发交易需加锁(nonce 递增) | 不同 UTXO 可同时被花费,天然无冲突,更易并行 |
| 隐私性 | 单一地址的余额变动可被公开追踪 | 地址可复用度低,但多输入交易可能暴露输入关联性 |
| 真实感 | 适合智能合约场景,状态管理直观 | 更贴近比特币底层协议,理解比特币工作原理的必修课 |
实现提示
由于本例定位为"选做实验",以下文件仅提供结构框架,核心部分使用 # TODO 占位标记,鼓励读者自行完成迁移:
models/utxo_transaction.py:TxInput、TxOutput数据类,Transaction增加is_coinbase属性models/utxo_set.py:UTXOSet类及apply_tx、revert_tx、validate_inputs方法consensus/utxo_validator.py:交易校验规则(输入存在、签名匹配、输出总和不大于输入总和)
迁移建议步骤:
- 先从余额模型中提取现有的
Transaction类,将其改为 UTXO 风格的输入/输出列表。 - 实现
UTXOSet,替换掉余额模型中的state: dict[str, int]。 - 修改区块验证逻辑,使其在矿工时调用
UTXOSet.apply_tx()而非直接操作余额字典。 - 在链重组逻辑中增加
UTXOSet.revert_tx()调用。 - 运行 16.7 的测试套件,验证 UTXO 版本通过所有测试。
要点总结:
- UTXO 模型通过交易输入/输出链式引用替代了全局余额状态
- Coinbase 交易是 UTXO 系统中产生新代币的唯一途径
- UTXO 集管理(增/删/回滚)是实现链重组正确性的关键
- 与余额模型相比,UTXO 模型代码更复杂但在并行性和真实性上更优
- 推荐作为独立练习完成,以深入理解两种经典账本模型的本质差异
评论
0评论加载中…