本章范围:16.4 交易、余额模型与签名验证 → 16.5 简易 P2P 网络:节点发现与广播 → 16.6 构建 HTTP API 与区块链浏览器。
目标:在前三节(16.1–16.3 数据结构、哈希链、PoW 挖矿)的基础上,叠加交易经济层、去中心化通信层与用户交互层,三节合一形成一个带交易能力、可多节点组网的迷你区块链系统。
前三节我们已经有了一个能独立挖矿的单节点区块链。但真实的区块链之所以是"价值网络",是因为它允许参与者之间转移资产,并且多个节点共同维护一条账本。本节将分三步完成这个跃迁:
- 16.4 — 引入余额模型与椭圆曲线签名,让交易具备经济含义和身份认证;
- 16.5 — 用 HTTP 端点模拟 P2P 通信,让多个节点发现彼此、广播交易和区块;
- 16.6 — 构建 REST API 与简易前端,提供浏览器访问和交互能力。
16.4 交易、余额模型与签名验证
16.4.1 为什么需要余额模型?
第 15 章讨论的 UTXO(未花费交易输出)模型 是比特币的选择——每笔交易消耗旧的 UTXO、创建新的 UTXO,像一个"硬币拆零"的过程。UTXO 模型隐私性更好、并行度高,但状态查询比较复杂:要知道某个地址的余额,必须扫描全链上所有未花费的输出。
我们的迷你链选择 余额模型(Account Model),和以太坊一致——用一个全局字典 {address: balance} 记录每个地址的余额,查询 O(1) 完成。代价是必须保证交易按序执行,且需要 nonce 计数器防止重放攻击。
state = {
"Alice": 100.0,
"Bob": 0.0
}
nonces = {
"Alice": 0,
"Bob": 0
}余额模型的状态转移约束可以写为:
16.4.2 交易的数据结构与哈希
每一笔交易需要携带发送方、接收方、金额、nonce 和签名,其中签名由发送方的私钥生成。我们在 16.1 节 Transaction 的基础上增加 nonce、signature 两个字段:
import hashlib, json
from dataclasses import dataclass, asdict, field
from typing import Optional
@dataclass
class Transaction:
sender: str
recipient: str
amount: float
nonce: int = 0
signature: str = ""
def to_dict(self) -> dict:
# 用有序 dict 保证序列化一致性,避免哈希波动
return {
"sender": self.sender,
"recipient": self.recipient,
"amount": self.amount,
"nonce": self.nonce,
}
def hash(self) -> str:
raw = json.dumps(self.to_dict(), sort_keys=True, separators=(",", ":"))
return hashlib.sha256(raw.encode()).hexdigest()交易哈希的计算方式为:
其中 JSON_canonical 指的是通过 sort_keys=True 保证字段顺序一致、separators=(",", ":") 去除多余空白,确保同一笔交易在任何环境中计算出的哈希值完全相同。
16.4.3 使用 ecdsa 库签名和验证
Python 的 ecdsa 库提供了对椭圆曲线数字签名算法(ECDSA)的开箱支持。我们使用 SECP256k1 曲线——也就是比特币和以太坊所用的标准曲线。
from ecdsa import SigningKey, VerifyingKey, SECP256k1
from ecdsa.util import sigencode_der, sigdecode_der
def generate_keypair():
sk = SigningKey.generate(curve=SECP256k1)
vk = sk.verifying_key
return sk, vk
def sign_tx(sk: SigningKey, tx_hash: str) -> str:
return sk.sign(tx_hash.encode(), sigencode=sigencode_der).hex()
def verify_tx(vk: VerifyingKey, tx_hash: str, signature_hex: str) -> bool:
try:
return vk.verify(
bytes.fromhex(signature_hex),
tx_hash.encode(),
sigdecode=sigdecode_der,
)
except Exception:
return False签名验证的数学本质(仅作展示,不要求读者实现底层运算):
验证者使用发送方的公钥 PK,对交易哈希 H(tx) 和签名 σ 执行 ECDSA 验签算法,输出布尔值。若验证通过,说明签名者确实持有与 PK 对应的私钥,且交易内容未被篡改。
16.4.4 交易的验证规则
一个交易在进入内存池(mempool)之前必须通过全部验证规则。我们定义统一的验证函数:
def validate_transaction(tx: Transaction, state: dict, nonces: dict) -> bool:
# 1. 余额足够
if state.get(tx.sender, 0) < tx.amount:
return False
# 2. nonce 严格递增(不允许跳号)
if nonces.get(tx.sender, 0) != tx.nonce:
return False
# 3. 签名验证(要求 sender 是十六进制公钥地址)
try:
vk = VerifyingKey.from_string(bytes.fromhex(tx.sender), curve=SECP256k1)
tx_hash = tx.hash()
if not verify_tx(vk, tx_hash, tx.signature):
return False
except Exception:
return False
return Trueflowchart LR
A["用户创建交易<br/>sender, recipient, amount, nonce"] --> B["发送方用私钥<br/>对交易哈希签名"]
B --> C["交易广播到节点"]
C --> D{"验证:<br/>余额 ≥ 金额?<br/>nonce 正确?<br/>签名有效?"}
D -- 全部通过 --> E["加入 mempool<br/>等待打包"]
D -- 任意失败 --> F["交易被丢弃"]
E --> G["矿工从 mempool<br/>取出交易打包进区块"]
G --> H["更新全局状态<br/>sender -= amount<br/>recipient += amount<br/>nonce += 1"]
H --> I["新区块广播到<br/>其他节点"]
16.4.5 状态更新与内存池
验证通过后,执行原子状态更新——要么全部生效,要么全部回滚:
def apply_transaction(tx: Transaction, state: dict, nonces: dict):
if not validate_transaction(tx, state, nonces):
return False
state[tx.sender] = state.get(tx.sender, 0) - tx.amount
state[tx.recipient] = state.get(tx.recipient, 0) + tx.amount
nonces[tx.sender] = tx.nonce + 1
return True内存池(mempool)就是一个暂存未打包交易的列表。挖矿时一次性取出所有有效交易打包进区块。注意创世块和挖矿奖励交易(coinbase) 不需要签名验证——coinbase 交易没有发送方,纯由协议产出:
def create_coinbase_tx(miner_address: str, reward: float = 50.0) -> Transaction:
return Transaction(
sender="COINBASE",
recipient=miner_address,
amount=reward,
nonce=0,
signature="",
)✅ 16.4 要点总结
- 余额模型用
{address: balance}字典维护状态,查询 O(1),但交易必须严格串行并按 nonce 递增。 - 交易哈希对规范化的 JSON 字符串做 SHA-256,确保跨节点哈希一致。
- ECDSA(SECP256k1)签名保证交易身份认证与完整性;验证通过
VerifyingKey.verify()完成。 - 交易进入 mempool 前需通过余额、nonce、签名三项检查;coinbase 交易免验证。
16.5 简易 P2P 网络:节点发现与广播
16.5.1 网络架构思路
真正的 P2P 网络(如比特币的 addr 消息传播、Kademlia DHT)需要实现底层 TCP 长连接和复杂的节点路由表。我们的迷你链选择用 HTTP 请求模拟 P2P 通信——每个节点运行一个 Flask(或 FastAPI)HTTP 服务器,同时通过 requests 库主动调用其他节点的接口。这种设计在教学上非常直观:读者只需要理解"节点 A 向节点 B 发了一个 POST 请求"即可。
每个节点维护:
peers = set() # 已知邻居的 URL,如 {"http://localhost:5001", ...}
blockchain = [] # 本地区块链副本
state = {} # 全局余额状态
nonces = {} # 地址 nonce 计数器
mempool = [] # 待打包交易列表
node_id = str(uuid.uuid4())[:8]16.5.2 Bootstrap 节点与邻居发现
新节点启动时至少需要知道一个种子(bootstrap)节点的地址。注册过程非常简单:
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/nodes/register", methods=["POST"])
def register_node():
data = request.get_json()
node_url = data.get("url")
if node_url and node_url not in peers:
peers.add(node_url)
return jsonify({"message": "registered", "peers": list(peers)})
@app.route("/nodes", methods=["GET"])
def get_peers():
return jsonify({"peers": list(peers)})新节点首次启动时,向 bootstrap 发送 POST /nodes/register,bootstrap 返回当前已知的邻居列表。之后新节点主动向每个邻居再发一次注册,整个网络就建立了连接。
16.5.3 消息类型与协议格式
我们定义四种核心消息类型,统一封装为 JSON:
| 消息类型 | 方向 | 说明 |
|---|---|---|
NEW_BLOCK | 广播 | 挖到新区块后向所有 peer 推送 |
NEW_TRANSACTION | 广播 | 收到新交易后向所有 peer 转发 |
REQUEST_CHAIN | 点对点 | 请求对方全链 |
RESPONSE_CHAIN | 点对点 | 返回完整的区块链 |
广播函数遍历 peers 集合,用 requests.post 并发推送:
import requests
from concurrent.futures import ThreadPoolExecutor
def broadcast(msg_type: str, data: dict):
payload = {"type": msg_type, "data": data, "timestamp": time.time()}
with ThreadPoolExecutor(max_workers=10) as pool:
for peer in peers:
pool.submit(requests.post, f"{peer}/p2p", json=payload, timeout=2)
@app.route("/p2p", methods=["POST"])
def handle_p2p_message():
msg = request.get_json()
msg_type = msg["type"]
data = msg["data"]
if msg_type == "NEW_TRANSACTION":
tx = Transaction(**data)
if validate_transaction(tx, state, nonces):
mempool.append(tx)
broadcast("NEW_TRANSACTION", data) # 继续转发
elif msg_type == "NEW_BLOCK":
handle_incoming_block(data)
elif msg_type == "REQUEST_CHAIN":
return jsonify({"chain": [b.__dict__ for b in blockchain], "length": len(blockchain)})
elif msg_type == "RESPONSE_CHAIN":
remote_chain = data["chain"]
resolve_conflicts(remote_chain)
return jsonify({"status": "ok"})sequenceDiagram
participant A as 节点 A (矿工)
participant B as 节点 B
participant C as 节点 C
A->>A: 挖矿成功,得到新区块
A->>B: POST /p2p (NEW_BLOCK)
A->>C: POST /p2p (NEW_BLOCK)
B->>B: 验证区块 (PoW + 前序哈希 + 交易)
C->>C: 验证区块
B-->>B: 验证通过,追加到本地链
C-->>C: 验证通过,追加到本地链
Note over B,C: 双方链长一致,网络达到共识
16.5.4 最长链规则与冲突解决
区块链最根本的共识规则是最长链规则(Longest Chain Rule):当节点收到一个与本地链冲突的区块时,如果传来的链更长且有效,就用它替换本地链。
其数学形式为:
收到新区块时有三种分叉情况:
flowchart TD
RX["收到 NEW_BLOCK"] --> CHECK{"block.index<br/>与本地链关系?"}
CHECK -->|"index == len(local_chain)"| APPEND["验证通过则直接追加"]
CHECK -->|"index > len(local_chain)"| PULL["发送 REQUEST_CHAIN<br/>拉取完整远程链"]
CHECK -->|"index < len(local_chain)"| IGNORE["丢弃:已更新"]
APPEND --> DONE["更新状态"]
PULL --> COMPARE{"远程链更长<br/>且有效?"}
COMPARE -->|是| REPLACE["替换本地链<br/>回滚并重放交易"]
COMPARE -->|否| STAY["保留本地链"]
REPLACE --> DONE
def resolve_conflicts(remote_chain: list) -> bool:
# 只接受严格更长的有效链
if len(remote_chain) <= len(blockchain):
return False
# 验证远程链的 PoW 与前序哈希
for i in range(1, len(remote_chain)):
prev = remote_chain[i - 1]
cur = remote_chain[i]
cur_hash = calculate_block_hash(cur)
if cur["prev_hash"] != calculate_block_hash(prev):
return False
if not cur_hash.startswith("0" * difficulty):
return False
# 替换本地链,重新计算状态
blockchain.clear()
blockchain.extend(remote_chain)
rebuild_state()
return True
def rebuild_state():
"""从创世块开始逐块重放交易,重建 state 和 nonces"""
state.clear()
nonces.clear()
for block in blockchain:
for tx_data in block["transactions"]:
tx = Transaction(**tx_data)
apply_transaction(tx, state, nonces)16.5.5 并发与一致性问题
HTTP 模拟 P2P 天然存在竞态:两个节点可能同时出块,产生同高度分叉;网络延迟可能导致 NEW_BLOCK 和 REQUEST_CHAIN 交叉到达。我们的应对策略:
- 对
blockchain、state、mempool等全局可变数据使用threading.Lock()保护; - 同高度分叉采用"先到先用"原则——先收到的区块保留,后收到的丢弃,等待下一次出块后通过最长链规则自然收敛;
- 暂不考虑女巫攻击(Sybil Attack)和 51% 攻击,这些内容将在下一章深入讨论。
✅ 16.5 要点总结
- 迷你链用 HTTP 端点(Flask)模拟 P2P 通信,每个节点既是服务器也是客户端。
- Bootstrap 节点提供初始邻居发现;
peers集合维护已知节点列表。 - 四种核心消息(NEW_BLOCK / NEW_TRANSACTION / REQUEST_CHAIN / RESPONSE_CHAIN)构成完整的点对点协议。
- 最长链规则 + 全链验证实现冲突解决;同高度分叉"先到先用",等待下一轮出块自然收敛。
- 全局数据需加锁,防止 HTTP 并发请求导致状态不一致。
16.6 构建 HTTP API 与区块链浏览器
16.6.1 RESTful API 端点设计
在 16.5 的 Flask 节点之上,我们暴露一组 REST 端点,供钱包客户端、命令行工具和前端浏览器调用:
flowchart TB
CLIENT["浏览器 / curl"] --> API["Flask REST API<br/>localhost:5000"]
API -->|GET /blocks| BC["返回整条区块链"]
API -->|GET /blocks/<index>| BLOCK["返回单个区块详情"]
API -->|POST /transactions/new| TX["验证后加入 mempool<br/>并广播"]
API -->|GET /mine| MINE["执行 PoW 挖矿<br/>打包交易 + 广播区块"]
API -->|GET /balance/<addr>| BAL["从 state 查询余额"]
API -->|GET /chain/validate| VAL["遍历验证全链"]
API -->|POST /nodes/register| PEERS["注册邻居节点"]
API -->|GET /peers| LIST["列出所有邻居"]
TX --> BROADCAST["广播 NEW_TRANSACTION"]
MINE --> BROADCAST2["广播 NEW_BLOCK"]
16.6.2 核心接口实现
以下是与区块和交易相关的三个核心路由:
@app.route("/blocks", methods=["GET"])
def get_blocks():
return jsonify([b.__dict__ for b in blockchain])
@app.route("/blocks/<int:index>", methods=["GET"])
def get_block(index):
if 0 <= index < len(blockchain):
return jsonify(blockchain[index].__dict__)
return jsonify({"error": "block not found"}), 404
@app.route("/transactions/new", methods=["POST"])
def new_transaction():
data = request.get_json()
tx = Transaction(
sender=data["sender"],
recipient=data["recipient"],
amount=data["amount"],
nonce=data.get("nonce", 0),
signature=data.get("signature", ""),
)
if validate_transaction(tx, state, nonces):
mempool.append(tx)
broadcast("NEW_TRANSACTION", tx.to_dict())
return jsonify({"message": "transaction accepted", "index": len(blockchain)})
return jsonify({"error": "invalid transaction"}), 400
@app.route("/mine", methods=["GET", "POST"])
def mine():
if not mempool:
# 没有用户交易时,至少包含 coinbase 奖励
reward_tx = create_coinbase_tx(node_id)
mempool.append(reward_tx)
new_block = mine_block(list(mempool), blockchain, difficulty)
if new_block:
mempool.clear()
broadcast("NEW_BLOCK", new_block.__dict__)
return jsonify(new_block.__dict__)
return jsonify({"error": "mining failed"}), 500
@app.route("/balance/<address>", methods=["GET"])
def get_balance(address):
return jsonify({"address": address, "balance": state.get(address, 0)})挖矿难度与目标值的数学关系延续 16.3 的定义:
其中 D 是难度(前导零个数)。每增加 1 个前导零,目标值缩小为原来的 。
16.6.3 最小前端区块链浏览器
一个纯 HTML + JavaScript 的单页应用即可提供完整的浏览体验。我们将其放在 static/index.html,Flask 自动提供静态文件服务。
<!DOCTYPE html>
<html>
<head><title>Mini Blockchain Explorer</title></head>
<body>
<h1>🔗 Mini Blockchain Explorer</h1>
<div id="stats">
<span id="height">Height: --</span>
<span id="txcount">Txs: --</span>
<span id="peers">Peers: --</span>
</div>
<h2>发起交易</h2>
<form id="txForm">
<input name="sender" placeholder="发送方公钥(hex)" size="50" />
<input name="recipient" placeholder="接收方公钥(hex)" size="50" />
<input name="amount" type="number" step="0.01" placeholder="金额" />
<button type="submit">发送交易</button>
</form>
<h2>区块链</h2>
<table border="1" id="chainTable">
<thead>
<tr>
<th>#</th><th>Time</th><th>Miner</th><th>Txs</th><th>Hash (前 16 位)</th>
</tr>
</thead>
<tbody id="chainBody"></tbody>
</table>
<script>
const API = "/blocks";
function renderBlocks(blocks) {
const body = document.getElementById("chainBody");
body.innerHTML = blocks.map(b => `
<tr>
<td>${b.index}</td>
<td>${new Date(b.timestamp * 1000).toLocaleTimeString()}</td>
<td>${(b.miner || "?").slice(0, 10)}...</td>
<td>${b.transactions?.length || 0}</td>
<td>${b.hash.slice(0, 16)}</td>
</tr>
`).join("");
document.getElementById("height").textContent = `Height: ${blocks.length - 1}`;
}
function fetchChain() {
fetch(API).then(r => r.json()).then(renderBlocks).catch(console.error);
}
document.getElementById("txForm").addEventListener("submit", e => {
e.preventDefault();
const fd = new FormData(e.target);
fetch("/transactions/new", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(Object.fromEntries(fd)),
}).then(r => r.json()).then(console.log);
});
setInterval(fetchChain, 3000);
fetchChain();
</script>
</body>
</html>16.6.4 多节点本地测试网演示
在一个机器上启动 3 个不同端口的节点,使用同一个 bootstrap 地址注册:
# 终端 1:Bootstrap 节点
python node.py --port 5000 --bootstrap http://localhost:5000
# 终端 2:节点 B
python node.py --port 5001 --bootstrap http://localhost:5000
# 终端 3:节点 C
python node.py --port 5002 --bootstrap http://localhost:5000演示流程:
- 节点 A(5000)调用
/transactions/new从 Alice 向 Bob 转账 10 个币; - 节点 A 广播
NEW_TRANSACTION到节点 B、C,三方的 mempool 同步更新; - 节点 C 调用
/mine打包区块,成功后广播NEW_BLOCK; - 节点 A、B 验证并追加,三条链保持一致。
# network_sim.py —— 多进程一键启动示例
import subprocess, time
nodes = [
("Alice", 5000),
("Bob", 5001),
("Carol", 5002),
]
procs = []
for name, port in nodes:
p = subprocess.Popen(["python", "node.py",
"--port", str(port),
"--bootstrap", "http://localhost:5000",
"--name", name,
])
procs.append(p)
time.sleep(0.5)
print("网络启动完毕。按 Enter 关闭所有节点。")
input()
for p in procs:
p.kill()16.6.5 终态验证:整条链路跑通
用 curl 模拟一次完整交易流程:
# 1. 查询余额
curl http://localhost:5000/balance/Alice
# 2. 生成密钥对并签名(Python 伪代码,实际用编程方式调用)
# sk, vk = generate_keypair()
# tx = Transaction(sender=vk.hex(), recipient="Bob", amount=5, nonce=0)
# sig = sign_tx(sk, tx.hash())
# tx.signature = sig
# 3. 发送签名交易
curl -X POST http://localhost:5000/transactions/new \
-H "Content-Type: application/json" \
-d '{"sender":"<公钥hex>","recipient":"Bob","amount":5,"nonce":0,"signature":"<签名hex>"}'
# 4. 触发挖矿
curl http://localhost:5000/mine
# 5. 检查区块和余额
curl http://localhost:5000/blocks/1
curl http://localhost:5000/balance/Bob✅ 16.6 要点总结
- REST API 提供区块链、交易、挖矿、余额查询等标准端点,7 个路由覆盖全部核心功能。
- 前端区块链浏览器用纯 HTML/JS 实现,
fetch()+setInterval每 3 秒自动刷新。 - 多节点测试网通过
--port和--bootstrap参数一键启动,支持本地三节点联调演示。 - 终态验证范式:签名交易 → 广播 → 打包 → 同步,完整模拟了真实区块链的交易生命周期。
本节核心认知
- 余额模型 + ECDSA 签名 = 数字资产所有权证明。余额模型让状态查询变得简单,而椭圆曲线签名确保只有私钥持有者才能转移其资产。这两者共同构成了区块链经济层的基础——没有签名的交易只是数据,加上签名才有了"资产转移"的法律效力。
- HTTP 模拟 P2P 是教学正确但生产不可用的折中方案。迷你链用 Flask 端点模拟广播和节点发现,让读者在单机上就能感受到去中心化网络的分叉和收敛过程。但真实的 P2P 需要处理 NAT 穿透、节点表路由、gossip 协议、加密传输等复杂问题——这是从"演示原型"到"生产系统"的必经鸿沟。
- REST API + 前端浏览器让看不见的区块链变得可见。命令行和日志只能展示区块链的数据结构,而浏览器 UI 能将哈希、难度、交易流程以图形化方式呈现,极大地降低理解门槛。同时,API 层也为后续章节实现"钱包 SDK"和"简单智能合约"提供了通用的交互接口。
参考与附录
- Python
ecdsa库文档:https://github.com/tlsfuzn/python-ecdsa - Flask 官方文档:https://flask.palletsprojects.com/
- Nakamoto, S. (2008). Bitcoin: A Peer-to-Peer Electronic Cash System. Sections 4–5.
- Wood, G. (2014). Ethereum: A Secure Decentralised Generalised Transaction Ledger. Yellow Paper.
- Antonopoulos, A. M. (2017). Mastering Bitcoin (2nd ed.), Chapter 6: The Transaction Lifecycle.
代码文件组织(与大纲对应)
| 文件 | 说明 |
|------|------|
|
transaction.py|Transaction类 + ecdsa 签名/验签 + 生成密钥对 ||
state.py| 全局状态字典 +validate_transaction()/apply_transaction()||
p2p.py| Flask 节点骨架 + 广播函数 + 邻居管理 + 链冲突解决 ||
api.py| 全部 REST 端点路由 + 前端静态文件服务 ||
frontend/index.html| 最小 HTML/JS 区块链浏览器 ||
network_sim.py| 多节点本地测试网一键启动脚本 |
评论
0评论加载中…