Alert Source Discuss
Standards Track: Networking

EIP-2124: 用于链兼容性检查的 Fork 标识符

Authors Péter Szilágyi <peterke@gmail.com>, Felix Lange <fjl@ethereum.org>
Created 2019-05-03

简单总结

目前,以太坊网络中的节点尝试通过建立与“看起来像”以太坊节点的远程机器(公共网络、私有网络、测试网络等)的随机连接来找到彼此,希望它们找到了一个有用的对等节点(相同的创世区块,相同的分叉)。这浪费了时间和资源,对于较小的网络尤其如此。

为了避免这种开销,以太坊需要一种机制,可以尽早准确地识别节点是否有用。这种机制需要一种总结链配置的方法,以及一种在网络中传播所述摘要的方法。

本提案仅关注所述摘要的定义 - 一个普遍有用的 fork identifier - 及其验证规则,允许将其嵌入到任意网络协议中(例如 discovery ENRseth/6x 握手)。

摘要

存在许多公共和私有的以太坊网络,但发现协议无法区分它们。检查对等节点是好是坏(同一条链与否)的唯一方法是建立一个 TCP/IP 连接,用 RLPx 加密包装它,然后执行一个 eth 握手。如果结果证明远程对等节点位于不同的网络上,这需要付出极高的代价,并且它甚至不够精确,无法区分以太坊和以太坊经典。对于小型网络来说,这种成本会成倍增加,在小型网络中,需要更多的尝试和错误才能找到好的节点。

即使对等节点确实位于同一条链上,在非争议性的共识升级期间,并非每个人都会及时更新他们的节点(开发者节点、残留节点等)。这些过时的节点给点对点网络带来了毫无意义的负担,因为它们只是依附于好的节点,但不接受升级后的区块。这导致宝贵的对等节点槽和带宽的损失,直到过时的节点最终更新。对于测试网络来说,这是一个严重的问题,在测试网络中,残留节点可能会持续数月。

本 EIP 提出了一个新的身份方案,以精确且简洁地总结链的当前状态(创世区块和所有应用的分叉)。简洁性对于使身份在数据报协议中也有用尤其重要。该 EIP 解决了许多问题:

  • 如果两个节点位于不同的网络上,它们甚至不应该考虑连接。
  • 如果硬分叉通过,升级后的节点应该拒绝未升级的节点,但不是在此之前。
  • 如果两条链共享相同的创世区块,但不共享分叉(ETH / ETC),它们应该拒绝彼此。

本 EIP 不试图解决三向分叉的干净分离!如果在未来的同一区块号,网络分成三个(非分叉、A 分叉和 B 分叉),将分叉器彼此分离将需要具体情况具体处理。不处理这个问题可以使提案务实、简单,并且还可以避免使其太容易从主网上分叉。

为了保持范围有限,本 EIP 仅定义身份方案和验证规则。相同的方案和算法可以嵌入到各种网络协议中,从而使 eth/6x 握手更加精确(以太坊 vs. 以太坊经典);以及使发现更有用(在不连接的情况下拒绝确定节点)。

动机

由于防火墙和网络地址转换 (NAT),点对点网络连接混乱且困难。通常,只有一小部分节点具有公共路由的地址,并且 P2P 网络主要依靠这些节点来为其他所有人转发数据。最大化公共节点效用的最佳方法是确保它们的资源不会浪费在对网络毫无价值的任务上。

通过积极地将不兼容的节点彼此隔离,我们可以从公共节点中提取更多的价值,从而使整个 P2P 网络更加健壮和可靠。在发现层支持这种网络分区可以进一步提高性能,因为我们可以避免与首先建立流连接相关的昂贵的加密、延迟和带宽冲击。

规范

每个节点维护以下值:

  • FORK_HASH: 已通过的创世哈希和分叉区块号的 IEEE CRC32 校验和 ([4]byte)。
    • 分叉区块号按升序输入到 CRC32 校验和中。
    • 如果在同一区块应用了多个分叉,则该区块号仅校验一次。
    • 区块号被视为 uint64 整数,在校验时以大端格式编码。
    • 如果链被配置为从其创世区块开始就使用非 Frontier 规则集,则不将其视为分叉。
  • FORK_NEXT: 下一个即将到来的分叉的区块号 (uint64),如果不知道下一个分叉,则为 0

例如,mainnet 的 FORK_HASH 将是:

  • forkhash₀ = 0xfc64ec04 (Genesis) = CRC32(<genesis-hash>)
  • forkhash₁ = 0x97c2c34c (Homestead) = CRC32(<genesis-hash> || uint64(1150000))
  • forkhash₂ = 0x91d1f948 (DAO fork) = CRC32(<genesis-hash> || uint64(1150000) || uint64(1920000))

fork identifier 定义为 RLP([FORK_HASH, FORK_NEXT])。此 forkid 经过交叉验证(NOT 简单比较)以评估远程链的兼容性。无论分叉状态如何,双方都必须得出相同的结论,以避免一方无限期地尝试重新连接。

验证规则

  • 1) 如果本地和远程 FORK_HASH 匹配,则将本地头部与 FORK_NEXT 进行比较。
    • 这两个节点当前处于相同的分叉状态。他们可能知道不同的未来分叉,但这在分叉触发之前无关紧要(可能会被推迟,节点可能会被更新以匹配)。
      • 1a) 远程宣布但远程未通过的区块已经在本地通过,断开连接,因为链不兼容。
      • 1b) 没有远程宣布的分叉;或者尚未在本地通过,连接。
  • 2) 如果远程 FORK_HASH 是本地过去分叉的子集,并且远程 FORK_NEXT 与本地后续分叉区块号匹配,则连接。
    • 远程节点当前正在同步。它最终可能会与我们分道扬镳,但在目前的这个时间点,我们没有足够的信息。
  • 3) 如果远程 FORK_HASH 是本地过去分叉的超集,并且可以用本地已知的未来分叉来完成,则连接。
    • 本地节点当前正在同步。它最终可能会与远程节点分道扬镳,但在目前的这个时间点,我们没有足够的信息。
  • 4) 在所有其他情况下拒绝。

过时软件示例

下面的示例试图详尽列出当节点没有运行匹配的软件版本,但以其他方式遵循同一条链(mainnet 节点、testnet 节点等)时出现的分叉组合可能性。

过去分叉 未来分叉 头部 远程 FORK_HASH 远程 FORK_NEXT 连接 原因
A     A   是 (1b) 相同分叉,相同同步状态。
A   < B A B 是 (1b) 远程正在宣传一个未来的分叉,但这是不确定的。
A   >= B A B 否 (1a) 远程正在宣传一个已经在本地通过的未来分叉。
A B   A   是 (1b) 本地知道一个未来的分叉,但这是不确定的。
A B   A B 是 (1b) 两者都知道一个未来的分叉,但这是不确定的。
A B1 < B2 A B2 是 (1b) 两者都知道不同的未来分叉,但这些是不确定的。
A B1 >= B2 A B2 否 (1a) 两者都知道不同的未来分叉,但远程的未来分叉已经在本地通过。
[A,B]     A B 是 (2) 远程未同步。
[A,B,C]     A B 是¹ (2) 远程未同步。远程将需要软件更新,但我们还不知道。
A B   A ⊕ B   是 (3) 本地未同步。
A B,C   A ⊕ B   是 (3) 本地未同步。本地也知道一个未来的分叉,但这是不确定的。
A     A ⊕ B   否 (4) 本地需要软件更新。
A B   A ⊕ B ⊕ C   否² (4) 本地需要软件更新。
[A,B]     A   否 (4) 远程需要软件更新。

注意,表格中存在一种不对称性,标有 ¹ 和 ²。由于我们无法访问远程节点的未来分叉列表(只有下一个),我们无法检测到它的软件是否过时,直到它同步为止。这是可以接受的,因为 1) 远程节点无论如何都会与我们断开连接,并且 2) 这是同步期间的临时侥幸,而不是遗留节点的永久性侥幸。

原理

为什么要将 FORK_HASH 扁平化为 4 个字节?为什么不共享整个创世区块和分叉列表?

虽然 eth devp2p 协议允许传输任意多的数据,但发现协议对所有 ENR 条目的总空间分配为 300 字节。

FORK_HASH 减少为 4 字节校验和可确保我们在 ENR 中为未来的扩展留下充足的空间;从(实际的)冲突角度来看,4 个字节对于任意数量的以太坊网络来说已经足够了。

为什么使用 IEEE CRC32 作为校验和而不是 Keccak256?

我们需要一种可以将任意数据扁平化为 4 个字节的机制,而不会忽略任何输入。任何其他校验和或哈希算法都可以工作,但由于节点可以随时撒谎,因此加密哈希函数没有任何价值。

与其只取 Keccak256 哈希的前 4 个字节(似乎很奇怪)或对所有 4 字节组进行异或运算(混乱),不如选择 CRC32,因为这正是它的设计目的。IEEE CRC32 也被以太网、gzip、zip、png 等使用,因此每种编程语言的支持应该都不是问题。

我们对 FORK_NEXT 的使用不多,我们不能以某种方式摆脱它吗?

我们需要能够区分远程节点是不同步还是其软件已过时。仅共享过去的分叉无法告诉我们节点是合法落后还是卡住。

为什么只宣传一个下一个分叉,而不是像 FORK_HASH 那样“哈希”所有已知的未来分叉?

与已经通过的过去分叉(对于我们本地而言)并且可以被认为是不可变的相反,我们对未来分叉一无所知。也许我们不同步,或者也许分叉尚未通过。如果它尚未通过,则可能会被推迟,因此强制执行它会使网络分裂。也可能发生我们还不了解所有未来分叉(有一段时间没有更新我们的软件)。

向后兼容性

此 EIP 仅定义一个身份方案,它不定义功能更改。

测试用例

这是一个完整的测试套件,用于测试在 Petersburg 分叉上限(撰写本文时)之后,Mainnet、Ropsten、Rinkeby 和 Görli 可能宣传的所有可能的 fork ID。

type testcase struct {
	head uint64
	want ID
}
tests := []struct {
	config  *params.ChainConfig
	genesis common.Hash
	cases   []testcase
}{
	// Mainnet test cases
	{
		params.MainnetChainConfig,
		params.MainnetGenesisHash,
		[]testcase{
			{0, ID{Hash: 0xfc64ec04, Next: 1150000}},       // 未同步
			{1149999, ID{Hash: 0xfc64ec04, Next: 1150000}}, // 最后一个 Frontier 区块
			{1150000, ID{Hash: 0x97c2c34c, Next: 1920000}}, // 第一个 Homestead 区块
			{1919999, ID{Hash: 0x97c2c34c, Next: 1920000}}, // 最后一个 Homestead 区块
			{1920000, ID{Hash: 0x91d1f948, Next: 2463000}}, // 第一个 DAO 区块
			{2462999, ID{Hash: 0x91d1f948, Next: 2463000}}, // 最后一个 DAO 区块
			{2463000, ID{Hash: 0x7a64da13, Next: 2675000}}, // 第一个 Tangerine 区块
			{2674999, ID{Hash: 0x7a64da13, Next: 2675000}}, // 最后一个 Tangerine 区块
			{2675000, ID{Hash: 0x3edd5b10, Next: 4370000}}, // 第一个 Spurious 区块
			{4369999, ID{Hash: 0x3edd5b10, Next: 4370000}}, // 最后一个 Spurious 区块
			{4370000, ID{Hash: 0xa00bc324, Next: 7280000}}, // 第一个 Byzantium 区块
			{7279999, ID{Hash: 0xa00bc324, Next: 7280000}}, // 最后一个 Byzantium 区块
			{7280000, ID{Hash: 0x668db0af, Next: 0}},       // 第一个也是最后一个 Constantinople,第一个 Petersburg 区块
			{7987396, ID{Hash: 0x668db0af, Next: 0}},       // 今天的 Petersburg 区块
		},
	},
	// Ropsten test cases
	{
		params.TestnetChainConfig,
		params.TestnetGenesisHash,
		[]testcase{
			{0, ID{Hash: 0x30c7ddbc, Next: 10}},            // 未同步,最后一个 Frontier,Homestead 和第一个 Tangerine 区块
			{9, ID{Hash: 0x30c7ddbc, Next: 10}},            // 最后一个 Tangerine 区块
			{10, ID{Hash: 0x63760190, Next: 1700000}},      // 第一个 Spurious 区块
			{1699999, ID{Hash: 0x63760190, Next: 1700000}}, // 最后一个 Spurious 区块
			{1700000, ID{Hash: 0x3ea159c7, Next: 4230000}}, // 第一个 Byzantium 区块
			{4229999, ID{Hash: 0x3ea159c7, Next: 4230000}}, // 最后一个 Byzantium 区块
			{4230000, ID{Hash: 0x97b544f3, Next: 4939394}}, // 第一个 Constantinople 区块
			{4939393, ID{Hash: 0x97b544f3, Next: 4939394}}, // 最后一个 Constantinople 区块
			{4939394, ID{Hash: 0xd6e2149b, Next: 6485846}}, // 第一个 Petersburg 区块
			{6485845, ID{Hash: 0xd6e2149b, Next: 6485846}}, // 最后一个 Petersburg 区块
			{6485846, ID{Hash: 0x4bc66396, Next: 0}},       // 第一个 Istanbul 区块
			{7500000, ID{Hash: 0x4bc66396, Next: 0}},       // 未来的 Istanbul 区块
		},
	},
	// Rinkeby test cases
	{
		params.RinkebyChainConfig,
		params.RinkebyGenesisHash,
		[]testcase{
			{0, ID{Hash: 0x3b8e0691, Next: 1}},             // 未同步,最后一个 Frontier 区块
			{1, ID{Hash: 0x60949295, Next: 2}},             // 第一个也是最后一个 Homestead 区块
			{2, ID{Hash: 0x8bde40dd, Next: 3}},             // 第一个也是最后一个 Tangerine 区块
			{3, ID{Hash: 0xcb3a64bb, Next: 1035301}},       // 第一个 Spurious 区块
			{1035300, ID{Hash: 0xcb3a64bb, Next: 1035301}}, // 最后一个 Spurious 区块
			{1035301, ID{Hash: 0x8d748b57, Next: 3660663}}, // 第一个 Byzantium 区块
			{3660662, ID{Hash: 0x8d748b57, Next: 3660663}}, // 最后一个 Byzantium 区块
			{3660663, ID{Hash: 0xe49cab14, Next: 4321234}}, // 第一个 Constantinople 区块
			{4321233, ID{Hash: 0xe49cab14, Next: 4321234}}, // 最后一个 Constantinople 区块
			{4321234, ID{Hash: 0xafec6b27, Next: 5435345}}, // 第一个 Petersburg 区块
			{5435344, ID{Hash: 0xafec6b27, Next: 5435345}}, // 最后一个 Petersburg 区块
			{5435345, ID{Hash: 0xcbdb8838, Next: 0}},       // 第一个 Istanbul 区块
			{6000000, ID{Hash: 0xcbdb8838, Next: 0}},       // 未来的 Istanbul 区块
		},
	},
	// Goerli test cases
	{
		params.GoerliChainConfig,
		params.GoerliGenesisHash,
		[]testcase{
			{0, ID{Hash: 0xa3f5ab08, Next: 1561651}},       // 未同步,最后一个 Frontier,Homestead,Tangerine,Spurious,Byzantium,Constantinople 和第一个 Petersburg 区块
			{1561650, ID{Hash: 0xa3f5ab08, Next: 1561651}}, // 最后一个 Petersburg 区块
			{1561651, ID{Hash: 0xc25efa5c, Next: 0}},       // 第一个 Istanbul 区块
			{2000000, ID{Hash: 0xc25efa5c, Next: 0}},       // 未来的 Istanbul 区块
		},
	},
}

这是针对 Mainnet 节点可能处于的不同状态以及可能需要验证的不同远程 fork identifier 并决定接受或拒绝它的一组测试:

tests := []struct {
	head uint64
	id   ID
	err  error
}{
	// Local 是 mainnet Petersburg,remote 宣布相同的内容。未宣布未来的分叉。
	{7987396, ID{Hash: 0x668db0af, Next: 0}, nil},

	// Local 是 mainnet Petersburg,remote 宣布相同的内容。Remote 还将在区块 0xffffffff 处宣布下一个分叉,但这不确定。
	{7987396, ID{Hash: 0x668db0af, Next: math.MaxUint64}, nil},

	// Local 当前仅在 Byzantium 中的 mainnet 中(因此它知道 Petersburg),remote 也宣布 Byzantium,但它还不知道 Petersburg(例如,在分叉之前未更新的节点)。
	// 在这种情况下,我们不知道 Petersburg 是否已经通过。
	{7279999, ID{Hash: 0xa00bc324, Next: 0}, nil},

	// Local 当前仅在 Byzantium 中的 mainnet 中(因此它知道 Petersburg),remote 也宣布 Byzantium,并且它也知道 Petersburg(例如,在分叉之前已更新的节点)。我们
	// 不知道 Petersburg 是否已经通过(将通过)。
	{7279999, ID{Hash: 0xa00bc324, Next: 7280000}, nil},

	// Local 当前仅在 Byzantium 中的 mainnet 中(因此它知道 Petersburg),remote 也宣布 Byzantium,并且它也知道一些随机分叉(例如,错误配置的 Petersburg)。因为
	// 两个节点都没有通过任何分叉,它们可能不匹配,但我们现在仍然连接。
	{7279999, ID{Hash: 0xa00bc324, Next: math.MaxUint64}, nil},

	// Local 是 mainnet Petersburg,remote 宣布 Byzantium + 了解 Petersburg。Remote
	// 只是不同步,接受。
	{7987396, ID{Hash: 0xa00bc324, Next: 7280000}, nil},

	// Local 是 mainnet Petersburg,remote 宣布 Spurious + 了解 Byzantium。Remote
	// 肯定不同步。它可能需要也可能不需要 Petersburg 更新,我们还不知道。
	{7987396, ID{Hash: 0x3edd5b10, Next: 4370000}, nil},

	// Local 是 mainnet Byzantium,remote 宣布 Petersburg。Local 不同步,接受。
	{7279999, ID{Hash: 0x668db0af, Next: 0}, nil},

	// Local 是 mainnet Spurious,remote 宣布 Byzantium,但不知道 Petersburg。Local
	// 不同步。Local 也知道一个未来的分叉,但这尚不确定。
	{4369999, ID{Hash: 0xa00bc324, Next: 0}, nil},

	// Local 是 mainnet Petersburg。remote 宣布 Byzantium 但不知道进一步的分叉。
	// Remote 需要软件更新。
	{7987396, ID{Hash: 0xa00bc324, Next: 0}, ErrRemoteStale},

	// Local 是 mainnet Petersburg,并且不知道更多的分叉。Remote 宣布 Petersburg +
	// 0xffffffff。本地需要软件更新,拒绝。
	{7987396, ID{Hash: 0x5cddc0e1, Next: 0}, ErrLocalIncompatibleOrStale},

	// Local 是 mainnet Byzantium,并且知道 Petersburg。Remote 宣布 Petersburg +
	// 0xffffffff。本地需要软件更新,拒绝。
	{7279999, ID{Hash: 0x5cddc0e1, Next: 0}, ErrLocalIncompatibleOrStale},

	// Local 是 mainnet Petersburg,remote 是 Rinkeby Petersburg。
	{7987396, ID{Hash: 0xafec6b27, Next: 0}, ErrLocalIncompatibleOrStale},

	// Local 是 mainnet Petersburg,遥遥领先。Remote 宣布了 Gopherium(不存在的分叉)
	// 在未来的某个区块 88888888,对于它自己,但对于本地来说是过去的区块。Local 不兼容。
	//
	// 这种情况检测具有多数哈希算力的未升级节点(典型的 Ropsten 混乱)。
	{88888888, ID{Hash: 0x668db0af, Next: 88888888}, ErrLocalIncompatibleOrStale},

	// Local 是 mainnet Byzantium。Remote 也是在 Byzantium 中,但宣布 Gopherium(不存在的
	// 分叉)在区块 7279999,在 Petersburg 之前。Local 不兼容。
	{7279999, ID{Hash: 0xa00bc324, Next: 7279999}, ErrLocalIncompatibleOrStale},
}

以下是一些测试,用于验证正确的 RLP 编码(因为 FORK_HASH 是 4 字节二进制文件,但 FORK_NEXT 是 8 字节数量):

tests := []struct {
  id   ID
  want []byte
}{
  {
    ID{Hash: 0, Next: 0},
    common.Hex2Bytes("c6840000000080"),
  },
  {
    ID{Hash: 0xdeadbeef, Next: 0xBADDCAFE},
    common.Hex2Bytes("ca84deadbeef84baddcafe"),
  },
  {
    ID{Hash: math.MaxUint32, Next: math.MaxUint64},
    common.Hex2Bytes("ce84ffffffff88ffffffffffffffff"),
  },
}

实现

Geth: https://github.com/ethereum/go-ethereum/tree/master/core/forkid

版权

版权和相关权利通过 CC0 放弃。

Citation

Please cite this document as:

Péter Szilágyi <peterke@gmail.com>, Felix Lange <fjl@ethereum.org>, "EIP-2124: 用于链兼容性检查的 Fork 标识符," Ethereum Improvement Proposals, no. 2124, May 2019. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-2124.