以下摘录和翻译自 go-ethereum#1889,直接访问原文获取原始说明,go-ethereum 中真实实现的代码中一些常量与 issue 中讨论的不一致,以代码中的为准。
快速同步算法的目标是用带宽换计算,可以理解为批处理操作,不执行历史事务,而是直接将 receipt 导入到数据库,然后拉取整个最近的 StateDB 数据库。这允许快速同步的节点仍然保持其包含用于用户查询的所有历史数据的存档节点的状态(并且因此一般不会影响网络的健康状况),对于最新的区块状态更改,会使用全量的区块处理方式。
快速同步算法的概要:
- 与原有的同步类似,下载组成区块链的 Header 和 Body
- 类似于原有的同步,验证区块头的一致性(POW,TD 等)
- 下载由区块头定义的 transaction receipt,而不是处理区块而后生成 receipt
- 存储下载的 block chain 和 receipt chain,允许这些区块上的历史查询
- 当区块达到最近的状态(head - 1024 个块)时,暂停状态同步(state sync):
- 获取由 pivot point 定义的区块的完整的 State Merkel Patricia Trie 树
- 对于 Merkel Patricia Trie 里面的每个账户,获取他的合约代码和内部存储的 Trie
- 当 Merkel Patricia Trie 下载成功后,将 pivot point(head - 1024 blocks) 定义的区块作为当前的区块头
- 通过原有的全量同步(full sync)方式,导入所剩余的 1024 个块
通过下载和验证整个 HeaderChain,我们可以保证传统同步的所有安全性,头部中包含的哈希(receipt trie, state trie 等)是有效的。 基于这些哈希,我们可以自信地下载 receipt 和 state trie。 另外,通过将 快速同步切换到常规同步的位置放置在当前区块头的下方一点(1024 块),这样可以确保甚至可以处理更大的区块链 reorg,而不需要新的同步(因为我们有区块对应的所有状态)。
基于历史块处理的同步机制具有两个(近似相似成本)瓶颈:交易处理和 PoW 验证。 快速同步算法成功地绕开了事务处理,跳过了系统曾经处于的对每一个 state object 进行迭代处理的需要。但是,验证与每个头相关联的工作证明仍然是 CPU 密集型操作(也就是说, fast sync 模式下也是需要验证 Header 和 Chain 的正确性)。
但是,我们可以在 Header 验证期间注意到一个有趣的现象:由于错误概率可以忽略不计,保证链的有效性前提下,为了加快区块的验证速度,我们可以只需要验证连续区块中的第 K 个 Header,而不是每个 Header。 下面来计算一下出错的概率,假设在 K 块中我们有
我们将可忽略概率 {N,K}
组合进行计算是非常直接的,一个简单和宽松的解决方案是 http://play.golang.org/p/B-8sX_6Dq0
package main import ( "fmt" "math" ) func main() { var ( collision = math.Pow(2, 128) target = 1 / collision ) for n := 1024; n <= 4096; n += 128 { best := 1 for k := 2; ; k++ { // Check if (1/k)^(n/k) <= Pn if math.Pow(1/float64(k), float64(n)/float64(k)) > target { break } best = k } fmt.Printf("N: %d => max(K): %d\n", n, best) } }
N | K | N | K | N | K | N | K |
---|---|---|---|---|---|---|---|
1024 | 43 | 1792 | 91 | 2560 | 143 | 3328 | 198 |
1152 | 51 | 1920 | 99 | 2688 | 152 | 3456 | 207 |
1280 | 58 | 2048 | 108 | 2816 | 161 | 3584 | 217 |
1408 | 66 | 2176 | 116 | 2944 | 170 | 3712 | 226 |
1536 | 74 | 2304 | 128 | 3072 | 179 | 3840 | 236 |
1664 | 82 | 2432 | 134 | 3200 | 189 | 3968 | 246 |
上面的表格应该这样解释:在 N 个区块头之间,如果我们每隔 K 个验证一次区块头,伪造的概率小于攻击者产生 SHA3 冲突的概率时 K 的取值,也就是说在 N 个连续区块之间间隔 <= K 个区块验证的时候是安全的。 这也意味着,如果确实发现了伪造,那么最后的 N 个头部应该被丢弃,因为不够安全。可以从上表中选择任何 {N,K}
组合,为了选择一个看起来好看点的数字,我们选择 N = 2048,K = 100。 后续可能会根据网络带宽/延迟影响以及可能在一些 CPU 性能比较受限的设备上运行的情况来进行调整。
然而,使用这个特性意味着,只有导入 N 个区块之后再导入 pivot 节点才被认为是安全的。为了更快地证明 pivot 的安全性,我们在距离 pivot 节点 X 距离的地方停止隔块验证的行为,对随后出现的每一个块进行验证直到 pivot。 鉴于上述 N 和 K 数字,我们选择
通过计算这些注意事项,快速同步需要修改为
常见的区块链(比如比特币,以太坊以及其他)是比较容易受女巫攻击(Sybil attack)的影响,攻击者试图把被攻击者从主网络上完全隔离开,让被攻击者接收一个虚假的状态,这就允许攻击者在真实的网络和这个虚假的网络上花费同一笔资金。然而这个需要攻击者提供真实的自己锻造的区块,而且需要成功的影响真实的网络,就需要在区块高度和难度上接近真实的网络。简单来说,为了成功的实施女巫攻击,攻击者需要有接近主网络的 hash rate,而这是一个非常昂贵的攻击。
与传统的女巫攻击相比,快速同步为攻击者提供了一种额外的能力,即为节点提供一个不仅与真实网络不同的网络视图,而且还可能绕过 EVM 的机制。以太坊协议只通过处理所有事务与以前的 state root 来验证 state root 哈希。但是跳过事务处理的话,我们无法证明快速同步 pivot point 中包含的 state root 是否有效,所以只要攻击者能够保持与真实网络相同的假区块链,就可以创造一个无效的网络状态视图。
为了避免将节点开放给这个额外的攻击者能力,快速同步(特别指定)将只在初始同步期间运行(节点的本地区块链是空的)。在一个节点成功与网络同步后,快速同步永远被禁用。 这样任何人都可以快速地追赶上主网络,但是在节点追上之后,额外的攻击矢量就被插入了。这个特性允许用户安全地使用快速同步标志(--fast),而不用担心潜在的状态 在未来发生的根攻击。作为附加的安全功能,如果快速同步在随机 pivot point 附近或之后失败,则作为安全预防措施禁用快速同步,并且节点恢复到基于块处理的完全同步。
为了对新算法的性能进行基准测试,运行了四个单独的测试:使用经典同步以及新的同步机制,从 Frontier 和 Olympic 上的 scrath 完全同步。在所有情况下,在一台机器上运行两个节点:具有完全同步的数据库的种子节点,以及只有起始块拉动数据的水蛭节点。在所有测试场景中,种子节点都有一个快速同步的数据库(更小,更少的磁盘占用),两个节点都有 1GB 的数据库缓存(--cache = 1024)。
运行测试的机器是 Zenbook Pro, Core i7 4720HQ, 12GB RAM, 256GB m.2 SSD, Ubuntu 15.04。
Dataset (blocks, states) | Normal sync (time, db) | Fast sync (time, db) |
---|---|---|
Frontier, 357677 blocks, 42.4K states | 12:21 mins, 1.6 GB | 2:49 mins, 235.2 MB |
Olympic, 837869 blocks, 10.2M states | 4:07:55 hours, 21 GB | 31:32 mins, 3.8 GB |
结果数据库中包含:
- 整个区块链(所有的 block, uncle, transaction),
- 每个交易的 receipt 和生成的 log,
- 以及最近 1024 个块的整个 state root。
这使得一个快速的同步节点可以充当所有意图和目的的完整归档节点。
快速同步算法需要由 eth63 定义的功能,正因为如此,现在主网中的测试至少需要少数几个可发现的对等节点将其节点更新到 eth63。同样的说明,验证这个实施是否真正正确还需要等待 eth63 的更广泛部署。