区块是账本的批量铸造单元:区块头携带父哈希、Merkle 根、难度目标与时间戳等控制字段,区块体装交易列表;交易模型定义"钱如何被描述与花费",主流分 UTXO(花的是未花费输出)与账户模型(改的是全局余额)两条路线。本节解剖区块结构、复算区块头校验,并对比两种交易模型的工程取舍。
收到新区块时,先看哪里?直觉答案是里面的交易,工程答案恰恰相反:先验区块头,再决定要不要为区块体花时间。区块头是这块铸件的合格证——百来字节的体量,浓缩了本块与全局历史的全部约束:父哈希声明血统,Merkle 根打包全部交易,难度目标与随机数记录熔炉竞赛成绩,时间戳给出铸造时刻。头部验不过,内容连字节都不必看。轻节点更是常年只收头不收体,靠第 2 章的包含证明按需抽查。
区块体则是交易列表。这里的"交易"含义比日常转账宽:转账、合约调用、证书签发,凡是需要全网见证并排序的状态变更,都以交易的形式进入区块。区块存在的意义也在此——把海量交易批量焊进历史:逐笔焊缝太贵,逐块焊缝让验证、传播、存储都获得批处理红利,难度调整也以块为节拍器。

节点收到新块后的头部校验是纯粹的规则执行,写成代码后没有神秘感可言:
import hashlib, time def check_header(new_block, best_chain, params): hdr = new_block["header"] # 血统:父块必须已在本地最长链上 if hdr["prev_hash"] != best_chain.tip_hash: return "拒绝:父块不在主链(可能是分叉块,转分叉处理)" # 封条:Merkle 根必须与区块体一致 if merkle_root(new_block["txs"]) != hdr["merkle_root"]: return "拒绝:交易封条不匹配" # 竞赛:哈希必须低于难度目标 if int(hash_header(hdr), 16) >= hdr["difficulty_target"]: return "拒绝:竞赛成绩不达标" # 节拍:时间戳不能落后中位数时间,也不能超前太多 if hdr["timestamp"] <= best_chain.median_time_past: return "拒绝:时间戳倒挂" if hdr["timestamp"] > time.time() + 2 * 3600: return "拒绝:时间戳超前过多" # 尺寸:区块重量不越限 if new_block["weight"] > params.MAX_BLOCK_WEIGHT: return "拒绝:超重" return "通过:接入主链,递交给交易验证层" def hash_header(hdr) -> str: """区块哈希只对头部计算——体已被 Merkle 根代言""" return hashlib.sha256(repr(sorted(hdr.items())).encode()).hexdigest()
注意最后一行的注释:区块身份只由头部决定。这就是矿工拼命改 nonce 而不动区块体的原因——改 nonce 只是重算一次头部哈希,货柜纹丝不动。
交易如何描述"钱的移动"?两条路线给出了几乎对称的答案。UTXO 路线:钱以"未花费输出"的形式存在,像一张张不同面额的支票;花费即销毁旧支票、签发新支票,输入总额必须等于输出总额加手续费。账户路线:全局维护一张余额表,交易直接写"从某账户扣若干、向某账户加若干"。用两段最小模型对照:
# UTXO 模型:钱是支票,花费即销毁与签发 utxo_set = {("T0", 0): 500} # 交易 T0 的第 0 号输出:500 def spend_utxo(tx): for ref in tx["inputs"]: # 每个输入必须存在且未花 if ref not in utxo_set: return f"拒绝:{ref} 不存在或已花费(双花!)" total_in = sum(utxo_set.pop(r) for r in tx["inputs"]) total_out = sum(o["amount"] for o in tx["outputs"]) if total_in != total_out + tx["fee"]: return "拒绝:收支不平" for i, o in enumerate(tx["outputs"]): utxo_set[(tx["txid"], i)] = o["amount"] return "接受:旧票销毁,新票诞生" print(spend_utxo({"txid": "T1", "inputs": [("T0", 0)], "outputs": [{"amount": 300}, {"amount": 195}], "fee": 5}))
# 账户模型:全局余额表,交易即加减 accounts = {"alice": 500, "bob": 0, "carol": 0} def transfer_accounts(sender, actions, fee=0): total = sum(a["amount"] for a in actions) + fee if accounts.get(sender, 0) < total: return "拒绝:余额不足" accounts[sender] -= total for a in actions: accounts[a["to"]] = accounts.get(a["to"], 0) + a["amount"] return f"接受:状态直接更新 {accounts}" print(transfer_accounts("alice", [ {"to": "bob", "amount": 300}, {"to": "carol", "amount": 195}], fee=5))
两条路线的工程后果对照成表:
| 维度 | UTXO 模型 | 账户模型 |
|---|---|---|
| 并行验证 | 天然并行(支票互不重叠) | 需按账户串行或加锁 |
| 余额查询 | 需汇总全部未花支票 | 直查余额表 |
| 智能合约表达 | 别扭(要绕道支票脚本) | 自然(状态即变量) |
| 隐私潜力 | 较好(支票可混入他人输出) | 较弱(账户即画像) |
| 空间开销 | 每笔带全套输入输出引用 | 紧凑 |
| 代表系统 | 比特币及其近亲 | 以太坊及其兼容链 |
⚠️ UTXO 模型里"找零"不是可选项:输入花掉的面额若大于付款额,必须显式创建找零输出指向自己,忘了找零的部分会成为矿工的额外手续费——历史上真实发生过的"天价手续费"乌龙多源于此。
交易进块不是先来后到,而是手续费竞拍:区块空间稀缺,记账者按"每单位重量手续费"从高到低打包。这解释了拥堵时的手续费暴涨,也解释了某些加速服务的原理——同一笔交易提高手续费重新广播(UTXO 模型下通过"花费自己的输出"实现替换)。时间戳也值得一句提醒:它由出块者填报,协议只做粗粒度合理性检查,拿区块时间戳做精确计时的系统都会被现实教育。
💡 折叠记忆法:UTXO 是支票本,账户模型是存折。支票本适合支付与清点(并行、隐私),存折适合记账与编程(状态、合约)——没有代差,只有场景。
链式焊缝挡住改历史,区块结构自身的攻击面在别处:分叉攻击利用"同一高度多个候选块"制造短暂混乱(胜负交给第 4 章);交易池阻塞用海量低费交易囤积毛坯架,抬升正常用户成本;时间戳微操试图影响难度调整节拍。结构的防守哲学一贯是"把复杂问题推迟到共识层裁决",本章交付的数据单元因此保持极度克制——这种克制正是它历经多年攻击仍稳如磐石的原因。
数据单元定型,下一节让它们流动起来:节点分类、网络拓扑与 P2P 传播——看一笔交易如何在几秒内跑遍全球陌生机器。