你要记一本账,一笔笔往下写,而且要防住一件事:有人回头偷偷改上一页。
打个比方
现实里的做法你见过——装订成册、页码连号、盖骑缝章。改一页不是撕掉重写就完事的,因为骑缝章跨着相邻两页,你改了这一页,缝上的印就对不上了。
于是"改一页"的代价,被抬高成了"重做整本"。
这一讲讲的就是数字版的装订:为什么要打包成"块",以及那条把块串起来的哈希链,怎么把"改一页"的代价变成"重做后面每一页"——而且是在一个所有人都在盯着的情况下重做。
一、为什么是"块"
第 1 讲说共识的任务是给交易定序。那为什么不一笔一笔地定序,非要打包成块?
三个原因:
① 摊薄共识开销
每达成一次共识都要一轮网络通信。
一次定 3000 笔的顺序,比定 3000 次便宜三个数量级。
② ⭐ 让"确认"这件事有粒度
有了块,就有了"第几个确认"这个可度量的概念(第 16 讲)。
③ 摊薄证明成本
一个 Merkle 根承诺整块交易(第 5 讲),
轻客户端下载 80 字节的头就能验证任意一笔。
⚠️ 代价是延迟:一笔交易必须等到下一个块才能被确认。比特币平均 10 分钟,以太坊 12 秒。这是一个刻意的取舍,第 16 讲会讲为什么比特币选了这么长的间隔。
二、哈希链
块与块之间用哈希连接——这就是那道骑缝章:每个块的头里存着上一个块的哈希,印跨在两页之间。
区块 100 区块 101 区块 102
┌────────────┐ ┌────────────┐ ┌────────────┐
│ prev: H₉₉ │◄──┤ prev: H₁₀₀ │◄──┤ prev: H₁₀₁ │
│ merkle: … │ │ merkle: … │ │ merkle: … │
│ ... │ │ ... │ │ ... │
└────────────┘ └────────────┘ └────────────┘
H₁₀₀ = H(整个区块 100 的头)
⭐ 这个结构的力量在于连锁反应:
改动区块 100 里的任何一笔交易
⟹ Merkle 根变(第 5 讲)
⟹ 区块 100 的头变 ⟹ H₁₀₀ 变
⟹ 区块 101 的 prev 字段对不上 ⟹ 区块 101 失效
⟹ 区块 102 失效 ⟹ ... 一直到链尾
要篡改一笔历史交易,必须重做它之后所有区块的工作。
⚠️ 这里必须说清楚一件常被误解的事:
“不可篡改"不是密码学保证的,是经济成本保证的。
密码学只保证"篡改会被发现”。真正阻止篡改的是"重做后续全部工作的成本高到不划算"——这是一个经济命题(第 1 讲、第 17 讲)。
三、比特币区块头:80 字节,六个字段
⭐ 这 80 个字节里,PrevHash 那一项就是骑缝章;MerkleRoot 那一项是本页内容的封条。这两样加起来,才让"改一页要重做后面每一页"成立。
┌──────────────────────────────────────────────┐
│ version 4 字节 协议版本 / 软分叉信号 │
│ prev_block_hash 32 字节 前一个区块的哈希 │
│ merkle_root 32 字节 本块所有交易的承诺 │
│ timestamp 4 字节 Unix 时间戳 │
│ bits 4 字节 ⭐ 当前难度目标(压缩) │
│ nonce 4 字节 PoW 要找的那个数 │
└──────────────────────────────────────────────┘
合计 80 字节
为什么 80 字节这件事很重要
⭐ 轻客户端只需要下载区块头,就能验证任意一笔交易的包含性(第 5 讲 + 第 13 讲):
比特币目前约 96 万个区块
96 万 × 80 字节 ≈ 77 MB
整条链的全部区块头,77 MB,能装进任何一部手机。 而完整区块数据是数百 GB。
⚠️ 这个设计不是巧合。中本聪在白皮书第 8 节专门写了"简化支付验证",80 字节的固定头是为它服务的。
逐字段
| 字段 | 说明 |
|---|---|
version |
后来被用作软分叉信号:矿工通过设置特定比特来表态是否支持某个升级(BIP-9) |
prev_block_hash |
链的连接。创世块此处全 0 |
merkle_root |
本块交易的承诺。交易本身不在头里,所以头的大小与交易数无关 |
timestamp |
见第五节,不可靠 |
bits |
难度目标的紧凑编码(尾数 + 指数)。第 15 讲展开 |
nonce |
只有 4 字节 = 40 亿种可能。现代矿机几秒就穷举完,所以还需要改动 coinbase 交易里的 extraNonce 来扩大搜索空间 |
四、以太坊区块头:从"账本"到"状态机"
以太坊的头大得多(约 500+ 字节),因为它承诺的东西更多:
parentHash 前块哈希
ommersHash 叔块列表哈希(PoS 后固定为空列表哈希)
beneficiary 出块者地址
⭐ stateRoot 全部账户与合约存储的根(第 12 讲)
transactionsRoot 本块交易的根
receiptsRoot 本块执行结果与日志的根
logsBloom 日志的布隆过滤器,用于快速筛选事件
number 区块高度
gasLimit / gasUsed 本块的 Gas 上限与实际消耗(第 22 讲)
timestamp
extraData 出块者可写的任意 32 字节
prevRandao PoS 后取代 difficulty,携带 RANDAO 随机值(第 8 讲)
baseFeePerGas EIP-1559 基础费(第 22 讲)
withdrawalsRoot 质押提款的根
blobGasUsed / excessBlobGas EIP-4844 的 blob 计量(第 29 讲)
⭐ 三个根是理解以太坊的关键:
transactionsRoot ── 承诺"发生了什么"(输入)
stateRoot ── 承诺"现在是什么样"(结果)
receiptsRoot ── 承诺"执行产生了什么"(日志、Gas 消耗、成功与否)
比特币的头里没有状态根——因为比特币的"状态"就是 UTXO 集合,而它不被承诺在区块里(第 10 讲会讲这个差别的后果)。
⭐ 有了 stateRoot,任何人都能用一个 Merkle 证明向轻客户端证明"某地址在某高度的余额是多少",而这在比特币上做不到。
五、时间戳:链上没有精确时间
区块链没有时钟。时间戳是出块者自己填的,所以必须有规则约束:
比特币的两条规则:
① 必须大于前 11 个区块时间戳的中位数 ← 防止把时间往回调
② 必须小于网络调整时间 + 2 小时 ← 防止把时间往前调太多
⭐ 后果:链上时间戳的实际精度只有小时级,而且允许非单调。
⚠️ 相邻两个区块的时间戳可以是倒序的(后一个比前一个早)!
这完全合法,只要满足上面两条规则。
对以太坊,规则更紧(必须严格大于父块),且出块间隔固定 12 秒,所以精度好得多——但仍然可以被出块者在几秒范围内操纵。
对智能合约的影响
// ⚠️ 危险:出块者能在秒级操纵 block.timestamp
if (block.timestamp % 2 == 0) { winner = msg.sender; }
// 🟡 可接受:容忍分钟级误差的场景
require(block.timestamp > deadline); // deadline 是天级的
⭐ 判断标准很简单:如果操纵几秒钟能带来收益,就不能用 block.timestamp。 第 8 讲的 VRF 才是随机数的正确来源。
六、创世块与链的身份
第一个区块没有前驱,它的 prev_block_hash 是全零。创世块是硬编码在客户端代码里的——这也意味着:
⭐ 创世块的哈希就是这条链的身份。 两条链是不是同一条链,看的就是创世块。
比特币的创世块 coinbase 里嵌了一句当天的《泰晤士报》标题(The Times 03/Jan/2009 Chancellor on brink of second bailout for banks),它同时起到两个作用:证明创世块不早于那一天(因此不存在预挖),以及一句注解。
七、分叉:三个不同的概念
“分叉"这个词被用在三件完全不同的事上:
| 类型 | 是什么 | 例子 |
|---|---|---|
| 临时分叉 | 两个矿工几乎同时出块,网络暂时看到两条链 | 每天都在发生,几个块内自动解决(第 16 讲) |
| 软分叉 | 规则收紧:新规则下合法的块,老节点也认 | SegWit、P2SH |
| 硬分叉 | 规则放宽或改变:新块老节点不认 | 会永久分裂成两条链,除非全网升级 |
⭐ 软硬分叉的判据是"老节点认不认新块”,而不是"改动大不大"。
软分叉:新规则 ⊂ 老规则
⟹ 老节点看到新块:"符合我的规则" ✅ 继续跟随
⟹ 只需多数算力升级即可生效
硬分叉:新规则 ⊄ 老规则
⟹ 老节点看到新块:"违反规则" ❌ 拒绝
⟹ ⚠️ 必须全网升级,否则永久分裂(ETH/ETC、BTC/BCH)
八、区块大小与中心化压力
一个容易被忽略但很关键的机制:
区块越大 ⟹ 传播越慢 ⟹ 别人收到你的块之前挖出的块越多
⟹ ⭐ 你的块成为孤块的概率越高
⟹ 你白白损失了区块奖励
⚠️ 而这个惩罚对大矿池更轻:大矿池内部不需要网络传播,它自己的下一个块可以立刻接上。所以:
⭐ 大区块 ⟹ 孤块率上升 ⟹ 小矿工受损更重 ⟹ 算力向大矿池集中。
这是"区块大小之争"背后真正的技术论据,也是第 3 讲那个约束的另一种表现形式。压缩区块传播(Compact Blocks、FIBRE、以太坊的 blob 传播)都是为了缓解这一点。
九、Go 实现
package chain
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"errors"
"math/big"
)
// Header 是比特币风格的 80 字节区块头。
type Header struct {
Version uint32
PrevHash [32]byte
MerkleRoot [32]byte
Timestamp uint32
Bits uint32
Nonce uint32
}
// Serialize 按小端序输出恰好 80 字节。
// ⚠️ 字段顺序和字节序必须与共识规则完全一致,否则算出的哈希全网对不上。
func (h *Header) Serialize() []byte {
buf := make([]byte, 0, 80)
buf = binary.LittleEndian.AppendUint32(buf, h.Version)
buf = append(buf, h.PrevHash[:]...)
buf = append(buf, h.MerkleRoot[:]...)
buf = binary.LittleEndian.AppendUint32(buf, h.Timestamp)
buf = binary.LittleEndian.AppendUint32(buf, h.Bits)
buf = binary.LittleEndian.AppendUint32(buf, h.Nonce)
return buf
}
// Hash 使用双重 SHA-256(第 4 讲第四节)。
func (h *Header) Hash() [32]byte {
first := sha256.Sum256(h.Serialize())
return sha256.Sum256(first[:])
}
// Target 把紧凑格式的 bits 展开成 256 位目标值。
// 格式:最高字节是指数 e,低三字节是尾数 m,目标 = m × 256^(e−3)。
func (h *Header) Target() *big.Int {
exp := h.Bits >> 24
mant := big.NewInt(int64(h.Bits & 0x00ffffff))
if exp <= 3 {
return mant.Rsh(mant, 8*(3-uint(exp)))
}
return mant.Lsh(mant, 8*(uint(exp)-3))
}
// VerifyChain 检查一串区块头的链接关系、PoW 与时间戳规则。
// 注意它只需要区块头——这正是轻客户端能工作的原因。
func VerifyChain(headers []*Header) error {
for i := 1; i < len(headers); i++ {
cur, prev := headers[i], headers[i-1]
// ① 链接:本块的 PrevHash 必须等于前块的哈希
if got := prev.Hash(); !bytes.Equal(cur.PrevHash[:], got[:]) {
return errors.New("chain: 第 " + itoa(i) + " 块的 PrevHash 对不上")
}
// ② 工作量:区块哈希必须小于目标值(第 15 讲)
hash := cur.Hash()
if new(big.Int).SetBytes(reverse(hash[:])).Cmp(cur.Target()) >= 0 {
return errors.New("chain: 第 " + itoa(i) + " 块的 PoW 不达标")
}
// ③ 时间戳:必须大于前 11 块的中位数
if i >= 11 && cur.Timestamp <= medianTimePast(headers[i-11:i]) {
return errors.New("chain: 第 " + itoa(i) + " 块的时间戳过早")
}
}
return nil
}
// medianTimePast 返回给定区块头时间戳的中位数。
func medianTimePast(hs []*Header) uint32 {
ts := make([]uint32, len(hs))
for i, h := range hs {
ts[i] = h.Timestamp
}
// 11 个元素,简单插入排序足够
for i := 1; i < len(ts); i++ {
for j := i; j > 0 && ts[j] < ts[j-1]; j-- {
ts[j], ts[j-1] = ts[j-1], ts[j]
}
}
return ts[len(ts)/2]
}
// reverse 反转字节序:比特币的区块哈希按小端序解释为整数。
func reverse(b []byte) []byte {
out := make([]byte, len(b))
for i := range b {
out[i] = b[len(b)-1-i]
}
return out
}
func itoa(n int) string {
if n == 0 {
return "0"
}
var d []byte
for n > 0 {
d = append([]byte{byte('0' + n%10)}, d...)
n /= 10
}
return string(d)
}
十、本讲小结
- 打包成块的三个理由:摊薄共识开销、让"确认数"有粒度、摊薄证明成本。代价是延迟。
- 哈希链的力量在连锁失效:改一笔交易 ⟹ Merkle 根变 ⟹ 本块哈希变 ⟹ 后面所有块失效。
- ⭐ “不可篡改"不是密码学保证的,是经济成本保证的。 密码学只保证篡改会被发现。
- ⭐ 比特币区块头恰好 80 字节,且与交易数无关。全链 96 万个头合计约 77 MB——能装进手机,这是轻客户端存在的前提。
nonce只有 4 字节,现代矿机几秒穷举完,所以实际还要改 coinbase 里的 extraNonce 扩大搜索空间。- 以太坊头的三个根:交易根(发生了什么)、状态根(现在什么样)、收据根(产生了什么)。比特币没有状态根,所以无法用证明说明"某地址余额是多少”。
- 链上时间戳只有小时级精度,且相邻区块可以倒序——比特币只要求大于前 11 块的中位数、小于网络时间 +2 小时。
- 判断合约能否用
block.timestamp的标准:操纵几秒钟是否能带来收益。 - 创世块的哈希就是链的身份。 比特币创世块的报纸标题同时证明了不存在预挖。
- 软硬分叉的判据是"老节点认不认新块":软分叉收紧规则(新规则 ⊂ 老规则),硬分叉改变规则,后者必须全网升级否则永久分裂。
- 大区块导致孤块率上升,而这对小矿工的伤害更重 ⟹ 算力向大矿池集中。这是区块大小之争真正的技术论据。
思考题
- 如果比特币逐笔交易达成共识(不打包),网络通信量会增加多少倍?确认粒度会变成什么样?
- 为什么区块头的大小与交易数量无关?这个性质对轻客户端意味着什么?
- 比特币没有状态根。请说明为什么"证明某地址在第 90 万块时余额是 3 BTC"在比特币上做不到,在以太坊上怎么做。
- 相邻两个比特币区块的时间戳可以倒序。构造一个具体的例子说明这为什么不违反规则。
- 一个赌博合约用
block.timestamp % 100决定输赢。矿工能做什么?如果改成% 86400(一天的秒数)呢? nonce只有 4 字节。假设矿机每秒 100 TH,穷举完 4 字节 nonce 需要多久?这说明了什么?- 判断下面哪些是软分叉、哪些是硬分叉,并说明理由:① 把区块大小上限从 1MB 降到 0.5MB;② 从 1MB 提到 8MB;③ 新增一个操作码;④ 缩短出块间隔。
- 大区块为什么对小矿工伤害更重?请从"传播时间"和"矿池内部无需传播"两点展开。