“六个确认就安全了”——大概听过这句话。这个实验让你把它跑出来:六个确认到底对应多小的概率,而攻击者算力占比变化时它怎么动。

打个比方

这像称一称"差不多"到底有多重

PoW 的安全性是概率性的,日常说法里它被压缩成了"等几个块就行"。跑一遍之后你会看到那条曲线的形状——它在某个算力占比之前掉得飞快,之后就再也掉不下去了。

⚠️ 一个容易让人得出错误结论的坑:模拟次数太少时,曲线会看起来"很安全"。

你要测的是小概率事件,而小概率事件在几百次模拟里可能一次都不出现——于是你会得到一条漂亮但错误的曲线。跑不够次数时,得到的不是"噪声大一点的答案",是"系统性偏乐观的答案"。 这本身就是这个实验想让你记住的一件事。

对应第 15–19 讲

这个实验分两部分,它们的目的正好相反:第一部分让你看到 PoW 的概率性质,第二部分让你看到 BFT 的确定性边界

第一部分:工作量证明

任务 1.1:挖矿

// Mine 穷举 nonce 直到哈希小于 target。
func Mine(header []byte, target *big.Int, maxNonce uint32) (uint32, bool)

注意比特币把区块哈希按小端序解释为整数——比较前要反转字节。

任务 1.2:验证出块间隔是指数分布

用模拟而不是数学,验证第 15 讲第四节的结论:

// SimulateBlockTimes 模拟 n 个区块的出块间隔。
// ⭐ 每次尝试成功的概率 p = target / 2²⁵⁶,
//    直接用几何分布采样即可,不必真的算哈希。
func SimulateBlockTimes(p float64, hashrate float64, n int) []float64

跑 10 万个区块,填这张表:

                        理论值      你的模拟值
均值                     600 秒       ?
P(间隔 > 600 秒)        36.8%        ?
P(间隔 > 1800 秒)        5.0%        ?
P(间隔 > 3600 秒)        0.25%       ?
P(间隔 <  60 秒)         9.5%        ?
最长的一次间隔            —           ?

然后回答:

① 你观察到的最长间隔是多少?它出现的频率符合 e^(−t/600) 吗?
② ⭐ 已经等了 20 分钟后,再等 10 分钟内出块的概率是多少?
   用模拟数据验证它等于"刚出完块时"的值——这就是无记忆性。
③ 有人说"最近半小时没出块,网络一定出问题了"。用你的数据回应他。

任务 1.3:难度调整

// NextTarget 实现比特币的难度调整(第 15 讲第五节)。
// ⚠️ 记得 [1/4, 4] 限幅。
func NextTarget(oldTarget *big.Int, actualSpan time.Duration) *big.Int

模拟两个场景:

场景 A:算力在第 5 个周期突然翻倍
       ⟹ 那个周期出块加快,下一次调整后恢复

场景 B:⚠️ 算力在第 5 个周期骤降 90%
       ⟹ 观察限幅如何让恢复变得极其缓慢——
          算出这个周期实际要花多久才能走完 2016 个块

场景 B 是重点:它解释了为什么算力大幅撤离时,一条链可能"卡住"数周。

任务 1.4:自私挖矿模拟

实现第 16 讲第五节的策略:

// SelfishMining 返回攻击者获得的【收益份额】。
// alpha: 攻击者算力占比
// gamma: 平局时跟随攻击者区块的诚实算力比例
func SelfishMining(alpha, gamma float64, blocks int) float64

状态机(state = 私链领先的区块数):

攻击者挖到块(概率 α):  state++

诚实方挖到块(概率 1−α):
    state == 0 ⟹ 诚实方的块直接成为主链
    state == 1 ⟹ ⭐ 攻击者【立即发布】,形成竞争
                   以概率 γ 攻击者赢,1−γ 诚实方赢
    state == 2 ⟹ 攻击者发布两个块,诚实方那个作废
    state >  2 ⟹ 攻击者发布一个块盖过去,state−−

扫描 α 从 0.10 到 0.45,找出"收益份额 > 算力份额"的临界点:

                γ=0      γ=0.5     γ=1
理论阈值        33.3%     25%       →0
你的模拟结果      ?         ?         ?

亲眼看到 α = 0.26、γ = 0.5 时收益份额确实超过 0.26,比读十遍公式有说服力得多。

第二部分:简化 PBFT

⭐ 第一部分称的是"差不多有多重"(概率)。这一部分换成另一种东西:它不给概率,它给边界——超过 f 个坏节点就一定不安全,不超过就一定安全,中间没有灰带。

任务 2.1:三阶段协议

type Node struct {
    ID        int
    N, F      int   // 总数与最大容错数
    View      int
    Byzantine bool  // ⭐ 用于测试:是否作恶
    // ...
}

func (n *Node) OnPrePrepare(m *PrePrepare) []Message
func (n *Node) OnPrepare(m *Prepare) []Message
func (n *Node) OnCommit(m *Commit) []Message

法定人数固定为 2f+1,且每一步都必须收齐才能推进。

任务 2.2:证明安全边界

这是本实验最重要的部分。

func TestSafetyWithFByzantine(t *testing.T) {
    // n = 4, f = 1
    // ⭐ 让 1 个节点作恶:对不同节点发送不同的值
    // 断言:所有【诚实】节点最终提交【同一个】值
}

func TestSafetyBreaksWithFPlusOne(t *testing.T) {
    // n = 4,但让 2 个节点作恶(超出 f = 1)
    // 构造场景,让两个诚实节点提交【不同】的值
    // 这个测试要断言"安全性确实被违反了"——
    //    看到它真的发生,才算理解 n ≥ 3f+1 不是随便定的
}

第二个测试是这个实验的核心产出。 提示:让两个拜占庭节点分别去配合两个不同的诚实节点,各自凑够 2f+1

任务 2.3:去掉 COMMIT 阶段会怎样

func TestNoCommitPhaseIsUnsafe(t *testing.T) {
    // ⭐ 实现一个只有 PRE-PREPARE 和 PREPARE 的版本
    // 构造场景:某节点 prepared 并执行后触发视图切换,
    // 新主节点提议了另一个值
    // 断言:出现了不一致
}

这个测试把第 19 讲第二节的论证变成了可运行的代码。

任务 2.4:法定人数交集

func TestQuorumIntersection(t *testing.T) {
    for n := 4; n <= 31; n += 3 {
        f := (n - 1) / 3
        q := 2*n/3 + 1
        // ⭐ 断言 2q − n > f —— 即交集必含诚实节点
    }
}

n 较大时不要真的枚举全部子集——用最坏情况的计数论证 |A∩B| ≥ 2q − n 即可。

任务 2.5(选做):Tendermint 的锁定规则

// 实现锁定规则,然后证明:
// ⭐ 若 v 在第 r 轮被提交,则第 r+1 轮不可能提交任何 v' ≠ v。
func TestLockingPreventsConflict(t *testing.T)

常见错误

① 用 math/rand 的固定种子 ⟹ 每次结果相同,看不出分布
② ⭐ 自私挖矿状态机漏掉 state = 2 的特殊处理("一次发布两个块")
③ PBFT 里统计"收到 2f+1 条消息"而不是"来自 2f+1 个【不同节点】"
   ⟹ 一个作恶节点重复发送就能凑数
④ 忘记按 (view, sequence, digest) 三元组区分消息 ⟹ 跨轮次串味

相关第 15 讲第 16 讲第 19 讲