用于链兼容性校验的分叉标识符

本文档提出了一种新的以太坊网络节点发现机制,通过引入一个简洁的“分叉标识符”(fork identifier)来精确总结链的配置和已应用的分叉,以避免节点连接不兼容的对等节点时浪费时间和资源。该标识符由创世哈希和分叉块号的CRC32校验和以及下一个即将到来的分叉块号组成,旨在提高P2P网络的效率和鲁棒性。

简单摘要

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

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

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

摘要

存在许多公共和私有以太坊网络,但发现协议并未区分它们。检查对等节点是好是坏(是否在同一条链上)的唯一方法是建立 TCP/IP 连接,用 RLPx 加密对其进行封装,然后执行 eth 握手。如果发现远程对等节点位于不同的网络上,并且其精确度不足以区分 Ethereum 和 Ethereum Classic,那么承担此成本是极其昂贵的。对于小型网络而言,这种成本会进一步放大,因为需要更多的试错才能找到好的节点。

即使对等节点 同一条链上,在非争议性共识升级期间,也不是每个人都能及时更新他们的节点(开发者节点、遗留节点等)。这些过时的节点给点对点网络带来了毫无意义的负担,因为它们只是依附于好的节点,却不接受已升级的区块。这会导致宝贵的对等节点槽位和带宽在过时节点最终更新之前被浪费。这对于测试网络来说是一个严重的问题,其中遗留节点可能会存在数月之久。

本 EIP 提出了一种新的身份方案,旨在精确而简洁地总结链的当前状态(创世块和所有已应用的分叉)。简洁性对于使身份在数据报协议中也发挥作用尤为重要。本 EIP 解决了以下一些问题:

  • 如果两个节点位于不同的网络上,它们根本不应考虑连接。
  • 如果发生硬分叉,已升级的节点应拒绝未升级的节点,但 不能 提前拒绝。
  • 如果两条链共享相同的创世块,但分叉不同(ETH / ETC),它们应相互拒绝。

本 EIP 不试图解决三向分叉的清晰分离问题!如果未来在同一区块高度,网络分裂成三部分(非分叉、分叉 A 和分叉 B),则将分叉方彼此分离将需要逐案特殊处理。不处理此问题可使提案保持实用、简单,并避免使其过于容易从主网分叉。

为限制范围,本 EIP 仅定义了身份方案和验证规则。相同的方案和算法可以嵌入到各种网络协议中,从而使 eth/6x 握手更加精确(Ethereum 与 Ethereum Classic);并使发现功能更加有用(无需连接即可拒绝确定是无关的对等节点)。

动机

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

通过积极切断不兼容节点之间的连接,我们可以从公共节点中获取更多价值,使整个 P2P 网络更加健壮和可靠。在发现层支持这种网络分区可以进一步提高性能,因为我们避免了在建立流连接时产生的昂贵的加密以及延迟/带宽开销。

规范

每个节点维护以下值:

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

例如,主网的 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 经过交叉验证(而非 天真地比较)以评估远程链的兼容性。无论分叉状态如何,双方都必须达成相同的结论,以避免一方无限期的重连尝试。

验证规则

  • 1) 如果本地和远程 FORK_HASH 匹配,则将本地头与 FORK_NEXT 进行比较。

    • 这两个节点当前处于相同的分叉状态。它们可能知道不同的未来分叉,但这在分叉触发之前不相关(可能会推迟,节点可能会更新以匹配)。
    • 1a) 如果远程宣布但远程尚未通过的区块已在本地通过,则断开连接,因为链不兼容。
    • 1b) 没有远程宣布的分叉;或本地尚未通过,则连接。
  • 2) 如果远程 FORK_HASH 是本地过去分叉的子集,并且远程 FORK_NEXT 与本地接下来的分叉区块号匹配,则连接。

    • 远程节点当前正在同步。它最终可能会与我们分歧,但目前我们没有足够的信息。
  • 3) 如果远程 FORK_HASH 是本地过去分叉的超集,并且可以通过本地已知的未来分叉完成,则连接。

    • 本地节点当前正在同步。它最终可能会与远程节点分歧,但目前我们没有足够的信息。
  • 4) 在所有其他情况下拒绝。

过时软件示例

以下示例试图穷尽当节点未运行匹配软件版本但遵循同一条链(主网节点、测试网节点等)时可能出现的分叉组合情况。

过去分叉 未来分叉 头区块 远程 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 仅定义了一种身份方案,并未定义功能性变更。

测试用例

以下是针对主网、Ropsten、Rinkeby 和 Görli 在 Petersburg 分叉上限(撰写本文时)下可能公布的所有分叉 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}},       // Unsynced (未同步)
            {1149999, ID{Hash: 0xfc64ec04, Next: 1150000}}, // Last Frontier block (最后一个 Frontier 区块)
            {1150000, ID{Hash: 0x97c2c34c, Next: 1920000}}, // First Homestead block (第一个 Homestead 区块)
            {1919999, ID{Hash: 0x97c2c34c, Next: 1920000}}, // Last Homestead block (最后一个 Homestead 区块)
            {1920000, ID{Hash: 0x91d1f948, Next: 2463000}}, // First DAO block (第一个 DAO 区块)
            {2462999, ID{Hash: 0x91d1f948, Next: 2463000}}, // Last DAO block (最后一个 DAO 区块)
            {2463000, ID{Hash: 0x7a64da13, Next: 2675000}}, // First Tangerine block (第一个 Tangerine 区块)
            {2674999, ID{Hash: 0x7a64da13, Next: 2675000}}, // Last Tangerine block (最后一个 Tangerine 区块)
            {2675000, ID{Hash: 0x3edd5b10, Next: 4370000}}, // First Spurious block (第一个 Spurious 区块)
            {4369999, ID{Hash: 0x3edd5b10, Next: 4370000}}, // Last Spurious block (最后一个 Spurious 区块)
            {4370000, ID{Hash: 0xa00bc324, Next: 7280000}}, // First Byzantium block (第一个 Byzantium 区块)
            {7279999, ID{Hash: 0xa00bc324, Next: 7280000}}, // Last Byzantium block (最后一个 Byzantium 区块)
            {7280000, ID{Hash: 0x668db0af, Next: 0}},       // First and last Constantinople, first Petersburg block (第一个和最后一个 Constantinople,第一个 Petersburg 区块)
            {7987396, ID{Hash: 0x668db0af, Next: 0}},       // Today Petersburg block (今日 Petersburg 区块)
        },
    },
    // Ropsten test cases (Ropsten 测试用例)
    {
        params.TestnetChainConfig,
        params.TestnetGenesisHash,
        []testcase{
            {0, ID{Hash: 0x30c7ddbc, Next: 10}},            // Unsynced, last Frontier, Homestead and first Tangerine block (未同步,最后一个 Frontier, Homestead 和第一个 Tangerine 区块)
            {9, ID{Hash: 0x30c7ddbc, Next: 10}},            // Last Tangerine block (最后一个 Tangerine 区块)
            {10, ID{Hash: 0x63760190, Next: 1700000}},      // First Spurious block (第一个 Spurious 区块)
            {1699999, ID{Hash: 0x63760190, Next: 1700000}}, // Last Spurious block (最后一个 Spurious 区块)
            {1700000, ID{Hash: 0x3ea159c7, Next: 4230000}}, // First Byzantium block (第一个 Byzantium 区块)
            {4229999, ID{Hash: 0x3ea159c7, Next: 4230000}}, // Last Byzantium block (最后一个 Byzantium 区块)
            {4230000, ID{Hash: 0x97b544f3, Next: 4939394}}, // First Constantinople block (第一个 Constantinople 区块)
            {4939393, ID{Hash: 0x97b544f3, Next: 4939394}}, // Last Constantinople block (最后一个 Constantinople 区块)
            {4939394, ID{Hash: 0xd6e2149b, Next: 6485846}}, // First Petersburg block (第一个 Petersburg 区块)
            {6485845, ID{Hash: 0xd6e2149b, Next: 6485846}}, // Last Petersburg block (最后一个 Petersburg 区块)
            {6485846, ID{Hash: 0x4bc66396, Next: 0}},       // First Istanbul block (第一个 Istanbul 区块)
            {7500000, ID{Hash: 0x4bc66396, Next: 0}},       // Future Istanbul block (未来的 Istanbul 区块)
        },
    },
    // Rinkeby test cases (Rinkeby 测试用例)
    {
        params.RinkebyChainConfig,
        params.RinkebyGenesisHash,
        []testcase{
            {0, ID{Hash: 0x3b8e0691, Next: 1}},             // Unsynced, last Frontier block (未同步,最后一个 Frontier 区块)
            {1, ID{Hash: 0x60949295, Next: 2}},             // First and last Homestead block (第一个和最后一个 Homestead 区块)
            {2, ID{Hash: 0x8bde40dd, Next: 3}},             // First and last Tangerine block (第一个和最后一个 Tangerine 区块)
            {3, ID{Hash: 0xcb3a64bb, Next: 1035301}},       // First Spurious block (第一个 Spurious 区块)
            {1035300, ID{Hash: 0xcb3a64bb, Next: 1035301}}, // Last Spurious block (最后一个 Spurious 区块)
            {1035301, ID{Hash: 0x8d748b57, Next: 3660663}}, // First Byzantium block (第一个 Byzantium 区块)
            {3660662, ID{Hash: 0x8d748b57, Next: 3660663}}, // Last Byzantium block (最后一个 Byzantium 区块)
            {3660663, ID{Hash: 0xe49cab14, Next: 4321234}}, // First Constantinople block (第一个 Constantinople 区块)
            {4321233, ID{Hash: 0xe49cab14, Next: 4321234}}, // Last Constantinople block (最后一个 Constantinople 区块)
            {4321234, ID{Hash: 0xafec6b27, Next: 5435345}}, // First Petersburg block (第一个 Petersburg 区块)
            {5435344, ID{Hash: 0xafec6b27, Next: 5435345}}, // Last Petersburg block (最后一个 Petersburg 区块)
            {5435345, ID{Hash: 0xcbdb8838, Next: 0}},       // First Istanbul block (第一个 Istanbul 区块)
            {6000000, ID{Hash: 0xcbdb8838, Next: 0}},       // Future Istanbul block (未来的 Istanbul 区块)
        },
    },
    // Goerli test cases (Goerli 测试用例)
    {
        params.GoerliChainConfig,
        params.GoerliGenesisHash,
        []testcase{
            {0, ID{Hash: 0xa3f5ab08, Next: 1561651}},       // Unsynced, last Frontier, Homestead, Tangerine, Spurious, Byzantium, Constantinople and first Petersburg block (未同步,最后一个 Frontier, Homestead, Tangerine, Spurious, Byzantium, Constantinople 和第一个 Petersburg 区块)
            {1561650, ID{Hash: 0xa3f5ab08, Next: 1561651}}, // Last Petersburg block (最后一个 Petersburg 区块)
            {1561651, ID{Hash: 0xc25efa5c, Next: 0}},       // First Istanbul block (第一个 Istanbul 区块)
            {2000000, ID{Hash: 0xc25efa5c, Next: 0}},       // Future Istanbul block (未来的 Istanbul 区块)
        },
    },
}

以下是主网节点可能处于的不同状态以及它可能需要验证和决定接受或拒绝的不同远程分叉标识符的测试套件:

tests := []struct {
    head uint64
    id   ID
    err  error
}{
    // Local is mainnet Petersburg, remote announces the same. No future fork is announced.
    // 本地是主网 Petersburg,远程宣布相同。没有宣布未来的分叉。
    {7987396, ID{Hash: 0x668db0af, Next: 0}, nil},

    // Local is mainnet Petersburg, remote announces the same. Remote also announces a next fork
    // at block 0xffffffff, but that is uncertain.
    // 本地是主网 Petersburg,远程宣布相同。远程还宣布下一个分叉
    // 在区块 0xffffffff,但这不确定。
    {7987396, ID{Hash: 0x668db0af, Next: math.MaxUint64}, nil},

    // Local is mainnet currently in Byzantium only (so it's aware of Petersburg), remote announces
    // also Byzantium, but it's not yet aware of Petersburg (e.g. non updated node before the fork).
    // In this case we don't know if Petersburg passed yet or not.
    // 本地是主网目前仅在 Byzantium(因此它知道 Petersburg),远程也宣布 Byzantium,
    // 但它尚未知道 Petersburg(例如分叉前未更新的节点)。
    // 在这种情况下,我们不知道 Petersburg 是否已经通过。
    {7279999, ID{Hash: 0xa00bc324, Next: 0}, nil},

    // Local is mainnet currently in Byzantium only (so it's aware of Petersburg), remote announces
    // also Byzantium, and it's also aware of Petersburg (e.g. updated node before the fork). We
    // don't know if Petersburg passed yet (will pass) or not.
    // 本地是主网目前仅在 Byzantium(因此它知道 Petersburg),远程也宣布 Byzantium,
    // 并且它也知道 Petersburg(例如分叉前已更新的节点)。我们
    // 不知道 Petersburg 是否已经通过(或将要通过)。
    {7279999, ID{Hash: 0xa00bc324, Next: 7280000}, nil},

    // Local is mainnet currently in Byzantium only (so it's aware of Petersburg), remote announces
    // also Byzantium, and it's also aware of some random fork (e.g. misconfigured Petersburg). As
    // neither forks passed at neither nodes, they may mismatch, but we still connect for now.
    // 本地是主网目前仅在 Byzantium(因此它知道 Petersburg),远程也宣布 Byzantium,
    // 并且它也知道一些随机分叉(例如配置错误的 Petersburg)。由于
    // 两个分叉都没有在任何节点通过,它们可能不匹配,但我们目前仍然连接。
    {7279999, ID{Hash: 0xa00bc324, Next: math.MaxUint64}, nil},

    // Local is mainnet Petersburg, remote announces Byzantium + knowledge about Petersburg. Remote
    // is simply out of sync, accept.
    // 本地是主网 Petersburg,远程宣布 Byzantium + 关于 Petersburg 的知识。远程
    // 只是未同步,接受。
    {7987396, ID{Hash: 0xa00bc324, Next: 7280000}, nil},

    // Local is mainnet Petersburg, remote announces Spurious + knowledge about Byzantium. Remote
    // is definitely out of sync. It may or may not need the Petersburg update, we don't know yet.
    // 本地是主网 Petersburg,远程宣布 Spurious + 关于 Byzantium 的知识。远程
    // 肯定未同步。它可能需要或不需要 Petersburg 更新,我们尚不清楚。
    {7987396, ID{Hash: 0x3edd5b10, Next: 4370000}, nil},

    // Local is mainnet Byzantium, remote announces Petersburg. Local is out of sync, accept.
    // 本地是主网 Byzantium,远程宣布 Petersburg。本地未同步,接受。
    {7279999, ID{Hash: 0x668db0af, Next: 0}, nil},

    // Local is mainnet Spurious, remote announces Byzantium, but is not aware of Petersburg. Local
    // out of sync. Local also knows about a future fork, but that is uncertain yet.
    // 本地是主网 Spurious,远程宣布 Byzantium,但不知道 Petersburg。本地
    // 未同步。本地也知道未来的分叉,但这尚不确定。
    {4369999, ID{Hash: 0xa00bc324, Next: 0}, nil},

    // Local is mainnet Petersburg. remote announces Byzantium but is not aware of further forks.
    // Remote needs software update.
    // 本地是主网 Petersburg。远程宣布 Byzantium 但不知道更多分叉。
    // 远程需要软件更新。
    {7987396, ID{Hash: 0xa00bc324, Next: 0}, ErrRemoteStale},

    // Local is mainnet Petersburg, and isn't aware of more forks. Remote announces Petersburg +
    // 0xffffffff. Local needs software update, reject.
    // 本地是主网 Petersburg,且不知道更多分叉。远程宣布 Petersburg +
    // 0xffffffff。本地需要软件更新,拒绝。
    {7987396, ID{Hash: 0x5cddc0e1, Next: 0}, ErrLocalIncompatibleOrStale},

    // Local is mainnet Byzantium, and is aware of Petersburg. Remote announces Petersburg +
    // 0xffffffff. Local needs software update, reject.
    // 本地是主网 Byzantium,且知道 Petersburg。远程宣布 Petersburg +
    // 0xffffffff。本地需要软件更新,拒绝。
    {7279999, ID{Hash: 0x5cddc0e1, Next: 0}, ErrLocalIncompatibleOrStale},

    // Local is mainnet Petersburg, remote is Rinkeby Petersburg.
    // 本地是主网 Petersburg,远程是 Rinkeby Petersburg。
    {7987396, ID{Hash: 0xafec6b27, Next: 0}, ErrLocalIncompatibleOrStale},

    // Local is mainnet Petersburg, far in the future. Remote announces Gopherium (non existing fork)
    // at some future block 88888888, for itself, but past block for local. Local is incompatible.
    //
    // This case detects non-upgraded nodes with majority hash power (typical Ropsten mess).
    // 本地是主网 Petersburg,远在未来。远程宣布 Gopherium(不存在的分叉)
    // 在未来某个区块 88888888,对其自身而言,但对本地而言是过去的区块。本地不兼容。
    //
    // 此案例检测具有多数哈希算力的未升级节点(典型的 Ropsten 混乱情况)。
    {88888888, ID{Hash: 0x668db0af, Next: 88888888}, ErrLocalIncompatibleOrStale},

    // Local is mainnet Byzantium. Remote is also in Byzantium, but announces Gopherium (non existing
    // fork) at block 7279999, before Petersburg. Local is incompatible.
    // 本地是主网 Byzantium。远程也在 Byzantium,但宣布 Gopherium(不存在的
    // 分叉)在区块 7279999,在 Petersburg 之前。本地不兼容。
    {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 放弃版权和相关权利。

  • 原文链接: github.com/nerolation/EI...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Nerolation
Nerolation
江湖只有他的大名,没有他的介绍。