本文提供了以太坊共识层Phase 0的网络规范,详细阐述了网络基础知识、不同网络交互领域的协议(如gossipsub、discv5和Req/Resp)以及设计决策的合理性。文档内容丰富,包括网络传输、加密协议、协议协商、消息格式、节点发现和数据压缩编码等多个方面,旨在为开发人员提供一致且高效的协议框架。
<!-- mdformat-toc start --slug=github --no-anchors --maxlevel=6 --minlevel=2 -->
StrictNoSign
签名策略?message-id
?MAXIMUM_GOSSIP_CLOCK_DISPARITY
?ATTESTATION_SUBNET_COUNT
个证明子网?SLOTS_PER_EPOCH
个时段内在八卦频道上广播?AggregateAndProof
而不只是作为 Attestation
?BeaconBlocksByRange
让服务器选择从哪个分支发送块?BlocksByRange
请求只要求为最新的 MIN_EPOCHS_FOR_BLOCK_REQUESTS
纪元提供服务?<!-- mdformat-toc end -->
本文档包含了阶段 0 的网络规范。
它由四个主要部分组成:
本节概述了以太坊共识层客户端的网络堆栈规范。
尽管 libp2p 是一个多传输堆栈(设计用于透明地监听多个同时的传输和端点), 我们在此定义基本互操作性的配置文件。
所有实现必须支持 TCP libp2p 传输,可以选择支持 QUIC(UDP)libp2p 传输,并且必须启用拨号和监听(即出站和入站连接)。 libp2p TCP 和 QUIC(UDP)传输支持在 IPv4 和 IPv6 地址上监听(并可以同时监听多个地址)。
客户端必须支持至少在 IPv4 或 IPv6 上监听。 不支持 IPv4 监听的客户端应意识到在互联网全局可路由性/支持方面的潜在劣势。客户端可以选择仅在 IPv6 上监听,但必须能够拨号 IPv4 和 IPv6 地址。
所有监听端点必须是公开可拨号的,因此不得依赖 libp2p 电路中继、AutoNAT 或 AutoRelay 功能。 (将特别重新审查对电路中继、AutoNAT 或 AutoRelay 的使用。)
在 NAT 后面的节点,或其他默认不可拨号的节点(例如容器运行时、防火墙等), 必须配置其基础设施以启用在公布的公共监听端点上的入站流量。
将使用带有 secp256k1
身份的 Libp2p-noise 安全通道握手进行加密。
正如 libp2p 规范中所规定,客户端必须支持 XX
握手模式。
客户端必须在协商使用的协议版本时使用精确匹配,并且可以选择使用版本来优先考虑更高的版本号。
客户端必须支持 multistream-select 1.0 并且在规范稳定时可以选择支持 multiselect 2.0。 一旦所有客户端都有对 multiselect 2.0 的实现,就可以逐步淘汰 multistream-select 1.0。
在连接引导期间,libp2p 动态协商一个共同支持的多路复用方法进行并行交互。 这适用于本身不支持多路复用的传输(例如 TCP、WebSockets、WebRTC),而省略了支持多路复用的传输(例如 QUIC)。
在 libp2p 实现中两种多路复用器是常见的:
mplex 和 yamux。
它们的协议 ID 分别是:/mplex/6.7.0
和 /yamux/1.0.0
。
客户端必须支持 mplex 并可以选择支持 yamux。 如果客户端同时支持两个,则在协商期间必须优先选择 yamux。 有关权衡,请参见下文的 理由 部分。
我们为类型提示和可读性定义以下 Python 自定义类型:
名称 | SSZ 等价物 | 描述 |
---|---|---|
NodeID |
uint256 |
节点标识符 |
SubnetID |
uint64 |
子网标识符 |
名称 | 值 | 单位 |
---|---|---|
NODE_ID_BITS |
256 |
uint256 的位长度是 256 |
本节概述了在本规范中使用的配置。
名称 | 值 | 描述 |
---|---|---|
MAX_PAYLOAD_SIZE |
10 * 2**20 (= 10485760, 10 MiB) |
gossipsub 消息和 RPC 块中未压缩有效负载的最大允许大小 |
MAX_REQUEST_BLOCKS |
2**10 (= 1024) |
单个请求中的最大区块数 |
EPOCHS_PER_SUBNET_SUBSCRIPTION |
2**8 (= 256) |
子网订阅上的纪元数(~27 小时) |
MIN_EPOCHS_FOR_BLOCK_REQUESTS |
MIN_VALIDATOR_WITHDRAWABILITY_DELAY + CHURN_LIMIT_QUOTIENT // 2 (= 33024, ~5 months) |
节点必须服务块的最小纪元范围 |
ATTESTATION_PROPAGATION_SLOT_RANGE |
32 |
可以传播证明的最大时段数 |
MAXIMUM_GOSSIP_CLOCK_DISPARITY |
500 |
在诚实节点之间假设的最大 毫秒 时钟差 |
MESSAGE_DOMAIN_INVALID_SNAPPY |
DomainType('0x00000000') |
用于八卦消息 ID 隔离 无效 snappy 消息的 4 字节域 |
MESSAGE_DOMAIN_VALID_SNAPPY |
DomainType('0x01000000') |
用于八卦消息 ID 隔离 有效 snappy 消息的 4 字节域 |
SUBNETS_PER_NODE |
2 |
beacon 节点应该订阅的长期存在的子网数量 |
ATTESTATION_SUBNET_COUNT |
2**6 (= 64) |
gossipsub 协议使用的证明子网数量。 |
ATTESTATION_SUBNET_EXTRA_BITS |
0 |
映射到所订阅的子网时要使用的 NodeId 的额外位数 |
ATTESTATION_SUBNET_PREFIX_BITS |
int(ceillog2(ATTESTATION_SUBNET_COUNT) + ATTESTATION_SUBNET_EXTRA_BITS) |
|
MAX_CONCURRENT_REQUESTS |
2 |
客户端可以对每个协议 ID 发起的最大并发请求数 |
客户端必须本地存储以下 MetaData
:
(
seq_number: uint64
attnets: Bitvector[ATTESTATION_SUBNET_COUNT]
)
其中
seq_number
是一个从 0
开始的 uint64
,用于版本控制节点的元数据。
如果本地 MetaData
中的任何其他字段发生变化,节点必须将 seq_number
增加 1。attnets
是一个 Bitvector
,表示节点的持久证明子网订阅。注意: MetaData.seq_number
用于节点元数据的版本控制,
与 ENR 序列号完全独立,
并且在大多数情况下将与 ENR 序列号不同步。
最大消息大小来自网络能够传输的最大有效负载大小,按照以下函数推导:
max_compressed_len
def max_compressed_len(n: uint64) -> uint64:
# 给定有效负载大小 n 时使用 snappy 的最坏情况压缩长度:
# https://github.com/google/snappy/blob/32ded457c0b1fe78ceb8397632c416568d6714a0/snappy.cc#L218C1-L218C47
return uint64(32 + n + n / 6)
max_message_size
def max_message_size() -> uint64:
# 允许 1024 字节用于框架和编码开销,但至少为 1MiB 以防 MAX_PAYLOAD_SIZE 较小。
return max(max_compressed_len(MAX_PAYLOAD_SIZE) + 1024, 1024 * 1024)
客户端必须支持 gossipsub v1 libp2p 协议 包括 gossipsub v1.1 扩展。
协议 ID: /meshsub/1.1.0
Gossipsub 参数
以下 gossipsub 参数 将被使用:
D
(主题稳定网格目标计数):8D_low
(主题稳定网格低水位):6D_high
(主题稳定网格高水位):12D_lazy
(八卦目标):6heartbeat_interval
(心跳频率,秒):0.7fanout_ttl
(未订阅但已发布主题的 ttl 地图,秒):60mcache_len
(在缓存中保留完整消息的窗口数量,以响应 IWANT
):6mcache_gossip
(进行八卦的窗口数):3seen_ttl
(看到的消息 ID 缓存的过期时间,以秒为单位):SECONDS_PER_SLOT SLOTS_PER_EPOCH 2注意: Gossipsub v1.1 引入了一些 额外参数 用于对等体评分和其他攻击缓解。 这些当前正在调查中,并将在准备好后进行规范并发布到主网。
主题是普通的 UTF-8 字符串,并根据 protobuf 确定在网络上的编码方式(gossipsub 消息被封装在 protobuf 消息中)。
主题字符串的形式为:/eth2/ForkDigestValue/Name/Encoding
。
这定义了在主题上发送的数据类型和消息的数据字段的编码方式。
ForkDigestValue
- 小写十六进制编码(无 "0x" 前缀)的字节,来自 compute_fork_digest(current_fork_version, genesis_validators_root)
,其中
current_fork_version
是要在主题上发送的消息的纪元的分叉版本genesis_validators_root
是在 state.genesis_validators_root
中找到的静态 Root
Name
- 见下表Encoding
- 编码策略描述将在网络上传输的字节的特定表示方式。
请参见 编码 部分以获得进一步的详细信息。客户端必须拒绝不认识的主题的消息。
注意: ForkDigestValue
由在创世块/状态可用之前不知晓的值组成。
因此,客户端在这些创世值已知之前不应订阅 gossipsub 主题。
可选的 from
(1)、seqno
(3)、signature
(5) 和 key
(6) protobuf 字段被从消息中省略,
因为消息是按内容识别的、匿名的,并在必要时在应用层签名。
从 Gossipsub v1.1 开始,客户端必须通过应用 StrictNoSign
签名策略 来强制执行这一点。
gossipsub 消息的 message-id
必须是以下从消息数据计算的 20 字节值:
message.data
具有有效的 snappy 解压缩,则将 message-id
设置为
用 MESSAGE_DOMAIN_VALID_SNAPPY
与 snappy 解压缩的消息数据拼接的 SHA256
哈希的前 20 个字节,
即 SHA256(MESSAGE_DOMAIN_VALID_SNAPPY + snappy_decompress(message.data))[:20]
。message-id
为用 MESSAGE_DOMAIN_INVALID_SNAPPY
与原始消息数据拼接的 SHA256
哈希的前 20 个字节,
即 SHA256(MESSAGE_DOMAIN_INVALID_SNAPPY + message.data)[:20]
。在相关情况下,客户端必须拒绝 message-id
大小不是 20 字节的消息。
注意: 上述逻辑处理了两个特殊情况:
(1) 多个 snappy data
可以解压成相同的值,
以及 (2) 某些消息 data
完全无法 snappy 解压。
有效负载在 gossipsub 消息的 data
字段中携带,具体取决于主题:
名称 | 消息类型 |
---|---|
beacon_block |
SignedBeaconBlock |
beacon_aggregate_and_proof |
SignedAggregateAndProof |
beacon_attestation_{subnet_id} |
Attestation |
voluntary_exit |
SignedVoluntaryExit |
proposer_slashing |
ProposerSlashing |
attester_slashing |
AttesterSlashing |
客户端必须拒绝(失败验证)包含错误类型或无效有效负载的消息。
处理入站八卦时,客户端可以对违反这些约束的对等体进行降分或断开连接。
对于任何可选排队,客户端应维持最大队列大小以避免 DoS 向量。
Gossipsub v1.1 引入了 扩展验证器
以帮助应用 gossipsub 对等体评分方案。
我们利用 ACCEPT
、REJECT
和 IGNORE
。对于每个 gossipsub 主题,有特定的应用验证。
如果所有验证通过,返回 ACCEPT
。
如果在按顺序处理项目时一个或多个验证失败,返回在特定条件前缀中指定的 REJECT
或 IGNORE
。
有两个主要的全球主题用于将区块 (beacon_block
) 和聚合证明 (beacon_aggregate_and_proof
) 传播到网络上的所有节点。
还有三个额外的全球主题用于传播低频率的验证者消息
(voluntary_exit
、proposer_slashing
和 attester_slashing
)。
####### beacon_block
beacon_block
主题仅用于将新的签名区块传播到网络上的所有节点。
签名区块会完整发送。
以下验证必须在将 signed_beacon_block
转发到网络之前通过。
MAXIMUM_GOSSIP_CLOCK_DISPARITY
) --
也就是说,验证 signed_beacon_block.message.slot <= current_slot
(客户端可以将未来的区块排队,以便在适当的时段进行处理)。signed_beacon_block.message.slot > compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)
(客户端可以选择验证并存储此类区块以供其他目的 -- 例如检测惩罚、归档节点等)。signed_beacon_block.message.slot
接收到的第一块具有有效签名的区块。signed_beacon_block.signature
在 proposer_index
公钥下有效。block.parent_root
定义)已被看到
(通过八卦或非八卦来源)
(客户端可以在检索到父区块后排队区块进行处理)。block.parent_root
定义)通过验证。finalized_checkpoint
是 block
的祖先 -- 即
get_checkpoint_block(store, block.parent_root, store.finalized_checkpoint.epoch) == store.finalized_checkpoint.root
proposer_index
提议的,以此时的分区背景(由 parent_root
/ slot
定义)。
如果无法立即验证 proposer_index
对应期望的分区,
则该区块可以排队以在计算区块分支的提议者后再进行处理 --
在这种情况下不要 REJECT
,而要 IGNORE
此消息。####### beacon_aggregate_and_proof
beacon_aggregate_and_proof
主题用于将聚合证明(作为 SignedAggregateAndProof
)传播到订阅节点(通常是验证者),以便纳入未来的区块。
为了方便,我们定义以下变量:
aggregate_and_proof = signed_aggregate_and_proof.message
aggregate = aggregate_and_proof.aggregate
index = aggregate.data.index
aggregation_bits = attestation.aggregation_bits
以下验证必须在将 signed_aggregate_and_proof
转发到网络之前通过。
index < get_committee_count_per_slot(state, aggregate.data.target.epoch)
。aggregate.data.slot
在最后 ATTESTATION_PROPAGATION_SLOT_RANGE
个时段内(允许 MAXIMUM_GOSSIP_CLOCK_DISPARITY
) --
即 aggregate.data.slot + ATTESTATION_PROPAGATION_SLOT_RANGE >= current_slot >= aggregate.data.slot
(客户端可以将未来的聚合排队,以便在适当的时段进行处理)。aggregate.data.target.epoch == compute_epoch_at_slot(aggregate.data.slot)
。len(aggregation_bits) == len(get_beacon_committee(state, aggregate.data.slot, index))
。len(get_attesting_indices(state, aggregate)) >= 1
。hash_tree_root(aggregate.data)
定义;如果其 aggregation_bits
是非严格超集,则 未 被看到。
(通过聚合八卦、在验证区块中,或通过在本地创建等效聚合)。aggregate
是为 epoch aggregate.data.target.epoch
的聚合器有索引 aggregate_and_proof.aggregator_index
接收到的第一个有效聚合。len(get_attesting_indices(state, aggregate)) >= 1
。aggregate_and_proof.selection_proof
选择验证者作为该时段的聚合器 --
即 is_aggregator(state, aggregate.data.slot, index, aggregate_and_proof.selection_proof)
返回 True
。aggregate_and_proof.aggregator_index in get_beacon_committee(state, aggregate.data.slot, index)
。aggregate_and_proof.selection_proof
是有效的签名
针对聚合器索引 aggregate_and_proof.aggregator_index
的 aggregate.data.slot
的;signed_aggregate_and_proof.signature
有效。aggregate
的签名有效。aggregate.data.beacon_block_root
)的块投票的块已被看到
(通过八卦或非八卦来源)
(客户端可能在检索块后排队聚合进行处理)。aggregate.data.beacon_block_root
)的块投票的块通过验证。get_checkpoint_block(store, aggregate.data.beacon_block_root, aggregate.data.target.epoch) == aggregate.data.target.root
。finalized_checkpoint
是定义为 aggregate.data.beacon_block_root
的 block
的祖先 -- 即
get_checkpoint_block(store, aggregate.data.beacon_block_root, finalized_checkpoint.epoch) == store.finalized_checkpoint.root
。####### voluntary_exit
voluntary_exit
主题仅用于将签名的自愿验证者退出消息传播到网络上的提议者。
签名的自愿退出消息会完整发送。
以下验证必须在将 signed_voluntary_exit
转发到网络之前通过。
signed_voluntary_exit.message.validator_index
的验证者。process_voluntary_exit
内部的所有条件通过验证。####### proposer_slashing
proposer_slashing
主题仅用于将提议者惩罚传播到网络上的提议者。
提议者惩罚消息会完整发送。
以下验证必须在将 proposer_slashing
转发到网络之前通过。
proposer_slashing.signed_header_1.message.proposer_index
的提议者。process_proposer_slashing
内部的所有条件通过验证。####### attester_slashing
attester_slashing
主题仅用于将验证者惩罚传播到网络上的提议者。
验证者惩罚消息会完整发送。
接收到此主题上的验证者惩罚的客户端必须在转发到网络之前验证 process_attester_slashing
内部的条件。
attester_slashing
中看到
(即 attester_slashed_indices = set(attestation_1.attesting_indices).intersection(attestation_2.attesting_indices)
,验证任何(attester_slashed_indices.difference(prior_seen_attester_slashed_indices))
)。process_attester_slashing
内部的所有条件通过验证。证明子网用于在网络的子部分中传播未聚合的证明。
beacon_attestation_{subnet_id}
beacon_attestation_{subnet_id}
主题用于将未聚合的证明传播到 subnet_id
子网(通常是 beacon 和持久委员会),
以在被八卦到 beacon_aggregate_and_proof
之前进行聚合。
为了方便,我们定义以下变量:
index = attestation.data.index
aggregation_bits = attestation.aggregation_bits
以下验证必须在将 attestation
转发到子网之前通过。- [拒绝] 委员会索引在预期范围内 -- 即 index < get_committee_count_per_slot(state, attestation.data.target.epoch)
。
compute_subnet_for_attestation(committees_per_slot, attestation.data.slot, index) == subnet_id
,
其中 committees_per_slot = get_committee_count_per_slot(state, attestation.data.target.epoch)
,
可以与签名检查的委员会信息一起预先计算。attestation.data.slot
在过去的 ATTESTATION_PROPAGATION_SLOT_RANGE
个槽内
(在 MAXIMUM_GOSSIP_CLOCK_DISPARITY
允许范围内) -- 即 attestation.data.slot + ATTESTATION_PROPAGATION_SLOT_RANGE >= current_slot >= attestation.data.slot
(客户端可以在适当的槽内排队未来的证明进行处理)。attestation.data.target.epoch == compute_epoch_at_slot(attestation.data.slot)
len([bit for bit in aggregation_bits if bit]) == 1
,即恰好 1 位已设置)。len(aggregation_bits) == len(get_beacon_committee(state, attestation.data.slot, index))
。attestation.data.target.epoch
和参与验证者索引。attestation
的签名是有效的。attestation.data.beacon_block_root
)已经被看到
(通过 gossip 或非 gossip 来源)
(客户端可以在区块被检索后排队处理证明)。attestation.data.beacon_block_root
)通过了验证。get_checkpoint_block(store, attestation.data.beacon_block_root, attestation.data.target.epoch) == attestation.data.target.root
finalized_checkpoint
是由 attestation.data.beacon_block_root
定义的区块的祖先 -- 即
get_checkpoint_block(store, attestation.data.beacon_block_root, store.finalized_checkpoint.epoch) == store.finalized_checkpoint.root
证明广播被分组为由主题定义的子网。
子网的数量通过 ATTESTATION_SUBNET_COUNT
定义。
可以通过 compute_subnet_for_attestation
计算证明的正确子网。
beacon_attestation_{subnet_id}
主题,在整个纪元中以类似于在委员会中轮换分片的方式进行轮换(未来的信标链升级)。
子网以 committees_per_slot = get_committee_count_per_slot(state, attestation.data.target.epoch)
每个槽轮换。
未聚合的证明作为 Attestation
发送到子网主题,
beacon_attestation_{compute_subnet_for_attestation(committees_per_slot, attestation.data.slot, attestation.data.index)}
作为 Attestation
。
聚合的证明作为 AggregateAndProof
发送到 beacon_aggregate_and_proof
主题。
主题后缀带有编码。编码定义了 gossip消息的有效负载是如何编码的。
ssz_snappy
- 所有对象都被 SSZ 编码,然后使用 Snappy 块压缩。
例子:信标聚合证明主题字符串是 /eth2/446a7232/beacon_aggregate_and_proof/ssz_snappy
,
分叉摘要是 446a7232
,并且 gossip 消息的数据字段是已被 SSZ 编码并使用 Snappy 压缩的 AggregateAndProof
。Snappy 有两种格式:“块”和“帧”(流式)。 Gossip 消息保持相对较小(100 字节到 100 千字节), 因此使用 基本 snappy 块压缩 以避免与 snappy 帧相关的额外开销。
实现必须为 gossip 使用单一编码。 更改编码将需要参与实现之间的协调。
规模限制施加在 RPCMsg
框架以及每个 Message
中的编码有效负载上。
客户端必须拒绝,并且100%不会产生或传播超出以下限制的消息:
RPCMsg
的大小(包括控制消息、帧、主题等)不得超过 max_message_size()
。Message.data
字段中压缩有效负载的大小不得超过 max_compressed_len(MAX_PAYLOAD_SIZE)
。MAX_PAYLOAD_SIZE
或 特定类型的 SSZ 边界,以较小者为准。每种消息类型被分隔到其自己的 libp2p 协议 ID,这是一种区分大小写的 UTF-8 字符串,格式为:
/ProtocolPrefix/MessageName/SchemaVersion/Encoding
包括:
ProtocolPrefix
- 消息按共享的 libp2p 协议名称前缀分组到家族。
在这种情况下,我们使用 /eth2/beacon_chain/req
。MessageName
- 每个请求由一个名称识别,该名称由英文字母、数字和下划线(_
)组成。SchemaVersion
- 一个序数版本号(例如 1, 2, 3…)。
每个模式都被版本化,以便在可能的情况下促进向后和向前兼容。Encoding
- 虽然模式以更抽象的术语定义数据类型,
编码策略描述将通过网络传输的特定字节表示。
有关更多详细信息,请参见 编码 部分。这种协议分隔允许 libp2p 的 multistream-select 1.0
/ multiselect 2.0
在建立基础流之前处理请求类型、版本和编码协商。
我们在每个请求/响应交互中仅使用一个流。 当互动完成时,无论是成功还是出现错误,流都会关闭。
请求/响应消息必须遵循协议名称中指定的编码,并遵循以下结构(放松的 BNF 语法):
request ::= <encoding-dependent-header> | <encoded-payload>
response ::= <response_chunk>*
response_chunk ::= <result> | <encoding-dependent-header> | <encoded-payload>
result ::= “0” | “1” | “2” | [“128” ... ”255”]
依赖于编码的标题可能携带元数据或断言,例如编码有效负载的长度,出于完整性和防攻击目的。 由于请求/响应流是单次使用的,并且流关闭隐式限制了边界,因此不严格要求长度前缀有效负载; 然而,某些编码,如 SSZ,是如此,以保证增加安全性。
response
是由零个或多个 response_chunk
形成。
由单个 SSZ 列表组成的响应(例如 BlocksByRange
和 BlocksByRoot
)将每个列表项作为 response_chunk
发送。
所有其他响应类型(非列表)发送单个 response_chunk
。
对于请求和响应,encoding-dependent-header
必须有效,
并且 encoded-payload
必须在 encoding-dependent-header
的约束内有效。
这包括某些编码策略的有效负载大小的特定类型边界。
不论这些特定于类型的边界,所有方法响应片段都必须适用全局最大未压缩字节大小 MAX_PAYLOAD_SIZE
。
客户端必须确保长度在这些边界内;如果不符合,客户端应立即重置流。 跟踪对等体声誉的客户端,在这种情况下应降低不当行为的对等体的分数。
一旦协商了针对请求类型的协议 ID 新流,应立即发送完整的请求消息。 请求必须根据编码策略进行编码。
请求方必须在撰写请求消息后关闭流的写入端。 此时,流将半关闭。
请求方必须不使用相同协议 ID 进行超过 MAX_CONCURRENT_REQUESTS
的并发请求。
如果发生超时或响应不再相关,请求者应重置流。
请求方应从流中读取,直到以下任一情况:
response_chunk
部分未通过验证。对于由单个有效 response_chunk
组成的请求,
请求者应在关闭流之前完全读取该片段,如 encoding-dependent-header
所定义。
一旦协商了针对请求类型的协议 ID 新流, 响应方应处理传入请求,并且在处理之前必须验证它。 请求处理和验证必须按照编码策略进行,直到达到 EOF(表示请求者关闭流一半)。
响应方必须:
N
,它应当从流中精确读取 N
字节,此时应出现 EOF(不应再读取更多字节)。
如果情况不是如此,应视为失败。response_chunk
(结果、可选头部、有效负载)组成的响应。如果步骤(1)、(2)或(3)由于数据无效、格式错误或不一致而失败,响应方必须以错误响应。 跟踪对等体声誉的客户端可能会记录此类失败以及意外事件,例如异常流重置。
响应方可以通过在有可用的容量时保留每个片段来限制流速。响应方在限速时必须不响应错误或关闭流。
当限速时,响应方必须迅速完整发送每个 response_chunk
但可能在每个片段之间引入延迟。
片段以 单字节 响应代码开头,该代码决定 response_chunk
的内容(BNF 语法中的 result
元素)。
对于多个片段,仅最后一个片段允许有非零错误代码(即,当发生错误时终止片段流)。
响应代码可以具有以下值之一,作为单个无符号字节编码:
ErrorMessage
架构(如下所述)。ErrorMessage
架构(如下所述)。ErrorMessage
架构(如下所述)。
注意: 此响应代码仅在指定的响应的上下文中有效。客户端可以使用大于 128
的响应代码来指示替代的、错误的请求特定响应。
范围 [4, 127]
为未来用途保留,并且如果未明确识别,应作为错误处理。
ErrorMessage
架构为:
(
error_message: List[byte, 256]
)
注意: 根据约定,error_message
是可以解释为 UTF-8 字符串(用于调试目的)的字节序列。
客户端必须将任何字节序列视为有效。
响应方可以惩罚同时为相同请求类型打开超过 MAX_CONCURRENT_REQUESTS
流的对等体,关于本规范中定义的协议 ID。
协商的协议 ID 的Token指定用于 req/resp 交互的编码类型。 此时仅可能有一个值:
ssz_snappy
: 内容首先通过 SSZ 编码
然后用 Snappy 帧压缩。
对于仅包含单个字段的对象,仅编码字段而不是带有单个字段的容器。
例如,BeaconBlocksByRoot
请求是 SSZ 编码的 Root
列表。
此编码类型必须由所有客户端支持。简单序列化 (SSZ) 规范 说明了对象是如何被 SSZ 编码的。
为了实现对 SSZ 的 snappy 编码,我们在编码时将对象的序列化形式传递给 Snappy 压缩器。 解码时则反之。
Snappy 有两种格式:“块”和“帧”(流式)。 为了支持大型请求和响应块,使用 snappy-framing。
由于 snappy 帧内容 具有最大大小为 65536
字节
而帧头仅是 标识符 (1) + 校验和 (4)
字节,因此单个帧的预期缓冲是可以接受的。
编码依赖的头部: 使用 ssz_snappy
编码策略的请求/响应协议必须编码原始 SSZ 字节的长度,
以无符号 protobuf varint 格式。
写入: 首先通过计算和写入 SSZ 字节长度,SSZ 编码器可以直接将块内容写入流。 当施加 Snappy 后,可以通过缓冲 Snappy 写入器逐帧压缩。
读取: 在阅读预期的 SSZ 字节长度之后,SSZ 解码器可以直接从流中读取内容。 当施加 snappy 时,可以通过缓冲 Snappy 读取器逐帧解压。
在读取有效负载之前,必须验证头部:
uint64
值。长度前缀必须解码为支持 uint64
所有取值范围的类型。MAX_PAYLOAD_SIZE
中较小者。在读取有效的头部之后,可以读取有效负载,同时保持来自头部的大小约束。
读取器不得在从头部分辨出的 SSZ 长度前缀 n
之后读取超过 max_compressed_len(n)
字节。
读取器必须将以下情况视为无效输入:
n
个 SSZ 字节后,任何剩余的字节。如果读取的字节数超过所需字节,则应预期 EOF。在无效输入(头部或有效负载)情况下,读取器必须:
InvalidRequest
。请求本身将被忽略。仅包含单个字段的消息必须直接编码为该字段的类型,而不得编码为 SSZ 容器。
以 SSZ 列表(例如 List[SignedBeaconBlock, ...]
)的响应将它们的组成部分各自作为 response_chunk
发送。例如,List[SignedBeaconBlock, ...]
响应类型发送零个或多个 response_chunk
。
每个成功的 response_chunk
包含一个 SignedBeaconBlock
有效负载。
协议 ID: /eth2/beacon_chain/req/status/1/
请求、响应内容:
(
fork_digest: ForkDigest
finalized_root: Root
finalized_epoch: Epoch
head_root: Root
head_slot: Slot
)
字段如下,客户端在发送消息时观察到:
fork_digest
: 节点的 ForkDigest
(compute_fork_digest(current_fork_version, genesis_validators_root)
) ,其中
current_fork_version
是节点当前纪元的分叉版本,按墙钟时间定义
(不一定是节点所同步的纪元)genesis_validators_root
是在 state.genesis_validators_root
中找到的静态 Root
finalized_root
: 根据 分叉选择 的 store.finalized_checkpoint.root
。
(注意,初始化最终检查点的默认值为 Root(b'\x00' * 32)
)。finalized_epoch
: 根据 分叉选择 的 store.finalized_checkpoint.epoch
。head_root
: 当前头区块的 hash_tree_root
(BeaconBlock
)。head_slot
: 对应于 head_root
的渠道槽。拨号客户端必须在连接时发送 Status
请求。
请求/响应必须编码为 SSZ 容器。
响应必须由单个 response_chunk
组成。
在以下条件下,客户端应立即与另一个客户端断开连接:
fork_digest
不与节点的本地 fork_digest
匹配,因为客户端的链在另一个分叉上。finalized_root
,finalized_epoch
)在预期的纪元内不在客户端的链上。
例如,如果对等方 1 发送(根,纪元)为(A, 5),并且对等方 2 发送(B, 3),但对等方 1 在纪元 3 时有根 C,
那么对等方 1 将断开连接,因为它知道它们的链是不可修复地分离的。一旦握手完成,具有较低 finalized_epoch
或 head_slot
的客户端(如果客户端具有相等的 finalized_epoch
)应通过 BeaconBlocksByRange
请求向其对方请求信标块。
注意: 在异常网络条件或经过若干轮 BeaconBlocksByRange
请求后,客户端可能需要再发送 Status
请求,以了解对等方是否有更高的头。
实现者可以自行实现这种行为。
协议 ID: /eth2/beacon_chain/req/goodbye/1/
请求、响应内容:
(
uint64
)
客户端可以在断开连接时发送再见消息。原因字段可能为以下值之一:
客户端可以使用大于 128
的原因代码来指示替代的、错误的请求特定响应。
范围 [4, 127]
为未来用途保留。
请求/响应必须编码为单个 SSZ 字段。
响应必须由单个 response_chunk
组成。
协议 ID: /eth2/beacon_chain/req/beacon_blocks_by_range/1/
请求内容:
(
start_slot: Slot
count: uint64
step: uint64 # 已废弃,必须设置为 1
)
响应内容:
(
List[SignedBeaconBlock, MAX_REQUEST_BLOCKS]
)
请求在区块槽范围内 [start_slot, start_slot + count)
,最终到达当前头区块(根据分叉选择)选取。
例如,请求从 start_slot=2
开始,计数为 4,将返回槽 [2, 3, 4, 5]
上的区块。
在某个给定槽号的槽是空的情况下,不返回任何区块。
例如,如果在前一个例子中槽 4 是空的,返回的数组将包含 [2, 3, 5]
。
step
已废弃并且必须设置为1。在过渡期间,客户端可以在返回较大步长时响应单个区块。
/eth2/beacon_chain/req/beacon_blocks_by_range/1/
已废弃。客户端可以在过渡期间响应一个空列表。
BeaconBlocksByRange
主要用于同步历史区块。
请求必须编码为 SSZ 容器。
响应必须由零个或多个 response_chunk
组成。
每个成功的 response_chunk
必须包含单个 SignedBeaconBlock
有效负载。
客户端必须记录在纪元范围 [max(GENESIS_EPOCH, current_epoch - MIN_EPOCHS_FOR_BLOCK_REQUESTS), current_epoch]
内看到的签名块,
其中 current_epoch
定义为当前墙钟时间,并且客户端必须支持在此范围内解除块请求。
在 MIN_EPOCHS_FOR_BLOCK_REQUESTS
纪元范围内无法回复块请求的节点应响应错误代码 3: ResourceUnavailable
。
在无法成功回复此请求范围的节点,可能随时被断开连接或降级。
注意: 以上要求意味着,从最近的弱主观检查点启动的节点必须至少为局部区块数据库填补直到纪元 current_epoch - MIN_EPOCHS_FOR_BLOCK_REQUESTS
,以完全符合 BlocksByRange
请求。为了安全地对这一块进行填补到最新状态,节点必须验证两个条件(1)提议者签名和(2)这些块形成一个有效的链,直到在弱主观状态中引用的最新块。
注意: 尽管从弱主观检查点引导的客户端可以立即参与网络,但其他对等体可能会断开连接并/或临时禁止这样的未同步或半同步的客户端。
客户端必须至少对范围内存在的第一个区块作出回应(如果它们拥有),并且最多必须为 MAX_REQUEST_BLOCKS
个块。
必须按顺序发送以下块。
客户端可以限制响应中的块数量。
响应必须包含不超过 count
的块。
客户端必须回应代表它们对当前分叉选择的视图的块——即,对由当前头定义的单个链的块。
值得注意的是,来自最终化之前的槽的块必须通向 Status
握手中报告的最终化块。
客户端必须响应在请求上下文中从单链的一致性下的块。
这适用于任何 step
值。
特别是对于 step == 1
,每个 parent_root
必须与前面块的 hash_tree_root
匹配。
在第一个块之后,客户端可以在处理响应的过程中停止, 如果它们的分叉选择在请求的上下文中更改了链的视图。
协议 ID: /eth2/beacon_chain/req/beacon_blocks_by_root/1/
请求内容:
(
List[Root, MAX_REQUEST_BLOCKS]
)
响应内容:
(
List[SignedBeaconBlock, MAX_REQUEST_BLOCKS]
)
根据区块根(=hash_tree_root(SignedBeaconBlock.message)
)请求区块。
响应是一个关于 SignedBeaconBlock
的列表,长度小于或等于请求的块数。
在响应对等方缺少块的情况下,可能会更少。
一次请求不得超过 MAX_REQUEST_BLOCKS
。
BeaconBlocksByRoot
主要用于恢复最近的块(例如,当收到的块或证明的父节点未知时)。
请求必须编码为 SSZ 字段。
响应必须由零个或多个 response_chunk
组成。
每个成功的 response_chunk
必须包含单个 SignedBeaconBlock
有效负载。
客户端必须支持请求自最近最终化的纪元以来的区块。
如果客户端拥有区块,则必须至少回应一个块。 客户端可以限制响应中块的数量。
客户端可以在通过 gossip 验证规则后,只要块通过验证就应立即包含在响应中。 客户端在实施信标链状态转换失败的情况下,则应不作出响应。
/eth2/beacon_chain/req/beacon_blocks_by_root/1/
已废弃。客户端可以在过渡期间回应一个空列表。
协议 ID: /eth2/beacon_chain/req/ping/1/
请求内容:
(
uint64
)
响应内容:
(
uint64
)
间歇性发送,Ping
协议检查已连接对等体的生存状况。
对等体请求并响应其本地元数据序列号(MetaData.seq_number
)。
如果对等体未响应 Ping
请求,客户端可以断开与对等体的连接。
客户端随后可以确定其本地跟踪的对等体 MetaData
是否是最新的,如果没有,可以通过 MetaData
RPC 方法请求更新版本。
请求必须编码为 SSZ 字段。
响应必须由单个 response_chunk
组成。
协议 ID: /eth2/beacon_chain/req/metadata/1/
无请求内容。
响应内容:
(
MetaData
)
请求对等体的 MetaData。 请求在打开和协商流时不发送任何请求内容。 建立后,接收对等体响应其本地最新的 MetaData。
响应必须编码为 SSZ 容器。
响应必须由单个 response_chunk
组成。
发现版本 5 (discv5) (协议版本 v5.1)用于对等体发现。
discv5
是一个独立的协议,在专用端口上运行 UDP,仅用于对等体发现。
discv5
支持自认证、灵活的对等记录(ENR)和基于主题的广告,这些都是在此上下文中(或将成为)要求。
discv5
应通过实现适配器来集成到客户端的 libp2p 栈中,
使其符合服务发现
和 对等路由 抽象和接口(提供 go-libp2p 链接)。
操作的输入包括对等 ID (在定位特定对等合适时)或能力(在搜索具有特定能力的对等体时),
输出将是从 discv5
后端返回的 ENR 记录转换而成的多地址。
此集成使 libp2p 栈能够随后与发现的对等体形成连接和流。
以太坊共识客户端的以太坊节点记录(ENR)必须包含以下条目 (免去序列号和签名,这必须在 ENR 中显示):
secp256k1
字段)。ENR 可以包含以下条目:
ip
字段)和/或 IPv6 地址(ip6
字段)。tcp
字段)和/或相应的 IPv6 端口(tcp6
字段)。quic
字段)和/或相应的 IPv6 端口(quic6
字段)。udp
字段)和/或相应的 IPv6 端口(udp6
字段)。这些参数的规范可以在 ENR 规范中找到。
ENR attnets
条目表示具有以下形式的证明子网位域,
使发现参与特定证明 gossip 子网的对等体更加简单。
键 | 值 |
---|---|
attnets |
SSZ Bitvector[ATTESTATION_SUBNET_COUNT] |
如果节点的 MetaData.attnets
具有任何非零位,ENR 必须包含与 MetaData.attnets
的相同值的 attnets
条目。
如果节点的 MetaData.attnets
全部为零,则 ENR 可以选择性地包含 attnets
条目或完全省略。
eth2
字段ENR 必须具有一个通用的 eth2
键,其 16 字节的值为节点的当前分叉摘要、下一个分叉版本和下一个分叉纪元,以确保与希望连接的以太坊网络的对等体建立连接。
键 | 值 |
---|---|
eth2 |
SSZ ENRForkID |
具体来说,eth2
键的值必须是以下 SSZ 编码对象(ENRForkID
)
(
fork_digest: ForkDigest
next_fork_version: Version
next_fork_epoch: Epoch
)
其中 ENRForkID
的字段定义为:
fork_digest
为 compute_fork_digest(current_fork_version, genesis_validators_root)
,其中
current_fork_version
是节点当前纪元的分叉版本,按墙钟时间定义
(不一定是节点所同步的纪元)genesis_validators_root
是在 state.genesis_validators_root
中找到的静态 Root
next_fork_version
是与未来某个纪元的下一个计划硬分叉对应的分叉版本。
如果没有计划未来的分叉,请将 next_fork_version = current_fork_version
来表明这一事实。next_fork_epoch
是计划下一个分叉和更新的 current_fork_version
的纪元。
如果没有计划未来的分叉,请将 next_fork_epoch = FAR_FUTURE_EPOCH
来表明这一事实。注意: fork_digest
由在生成区块/状态可用之前未知的值组成。
因此,客户端不应在生成值已知之前形成 ENR 和开始对等体发现。
该规则的一个显着例外是生成之前的引导节点 ENR 的分发。
在这种情况下,应该最初将引导节点 ENR 分发,eth2
字段设置为
ENRForkID(fork_digest=compute_fork_digest(GENESIS_FORK_VERSION, b'\x00'*32), next_fork_version=GENESIS_FORK_VERSION, next_fork_epoch=FAR_FUTURE_EPOCH)
。
生成值被知晓后,引导节点应更新 ENR 以参与正常发现过程。
客户端应连接到与本地值匹配的 fork_digest
、next_fork_version
和 next_fork_epoch
的对等体。
客户端可能连接到具有相同 fork_digest
但不同 next_fork_version
/next_fork_epoch
的对等体。
除非在早期的 next_fork_epoch
之前手动将 ENRForkID
更新为匹配,否则这些连接的客户端将无法从之前的 next_fork_epoch
成功交互。
由于阶段 0 没有分片,因此没有分片委员会,因此对证明子网(beacon_attestation_{subnet_id}
)没有稳定的支撑。为了提供这种稳定性,每个信标节点应:
SUBNETS_PER_NODE
,持续 EPOCHS_PER_SUBNET_SUBSCRIPTION
个纪元。attnets
条目中维持所选子网的广告,通过将选定的 subnet_id
位设置为 True
(例如 ENR["attnets"][subnet_id] = True
)以涵盖所有持久的证明子网。compute_subscribed_subnets(node_id, epoch)
函数,基于其节点 ID 选择这些子网。def compute_subscribed_subnet(node_id: NodeID, epoch: Epoch, index: int) -> SubnetID:
node_id_prefix = node_id >> (NODE_ID_BITS - ATTESTATION_SUBNET_PREFIX_BITS)
node_offset = node_id % EPOCHS_PER_SUBNET_SUBSCRIPTION
permutation_seed = hash(uint_to_bytes(uint64((epoch + node_offset) // EPOCHS_PER_SUBNET_SUBSCRIPTION)))
permutated_prefix = compute_shuffled_index(
node_id_prefix,
1 << ATTESTATION_SUBNET_PREFIX_BITS,
permutation_seed,
)
return SubnetID((permutated_prefix + index) % ATTESTATION_SUBNET_COUNT)
def compute_subscribed_subnets(node_id: NodeID, epoch: Epoch) -> Sequence[SubnetID]:
return [compute_subscribed_subnet(node_id, epoch, index) for index in range(SUBNETS_PER_NODE)]
注意: 在准备硬分叉时,节点必须在计划的分叉发生前至少提前 EPOCHS_PER_SUBNET_SUBSCRIPTION
个纪元选择并订阅未来的分叉版本的子网。这些新子网存在于当前分叉的子网之上,直到分叉发生。分叉发生后,让来源于以前分叉的子网到达其生命周期结束,不进行替换。
libp2p 对等体可以同时监听多个传输,并且这些传输可以随时间变化。 Multiaddrs 不仅编码地址,也编码用于拨号的传输。
关于这种动态性,在纸面上就协议定义特定的传输(如 TCP、QUIC 或 WebSockets)变得无关紧要。
然而,为了互操作性,定义最低基准是有用的。
客户端可以支持其他传输,如 libp2p QUIC、WebSockets 和 WebRTC 传输,前提是其所在语言支持。 虽然缺乏这种支持不会对互操作性造成损害,但其优点是可取的:
libp2p QUIC 传输固有地依赖于 TLS 1.3,符合 QUIC 协议规范 和相关 QUIC-TLS 文档 第 7 节的要求。
使用一种握手程序或另一种握手程序将对应用层透明, 一旦 libp2p 主机/节点对象获得适当配置。
TCP 是一种可靠、按照顺序、全双工、流量控制的网络协议,支持我们今天所知的许多互联网。 HTTP/1.1 和 HTTP/2 建立在 TCP 之上。
QUIC 是一种新的协议,正在由 IETF QUIC WG 进行最终规范化。 它起源于谷歌的 SPDY 实验。QUIC 传输无疑是有前途的。 它基于 UDP,但又可靠、有序、复用、本地安全(TLS 1.3)、相比 TCP 减少了延迟, 并且提供流级和连接级流量控制(从而消除了先到先得的问题),支持 0-RTT 连接建立和端点迁移等特性。 与 TCP 相比,UDP 还具有更好的 NAT 穿透性能——这是我们在对等网络中急切追求的目标。QUIC 正在被接受为 HTTP/3 的底层协议。 这使我们有可能免费获得反审查能力,能够抵御深度数据包检测。 只要我们使用与 HTTP/3 相同的端口号和加密机制,我们的流量可能就与标准网络流量无法区分, 我们可能只会受到标准基于 IP 的防火墙过滤——这是我们可以通过其他机制反制的。
WebSockets 和/或 WebRTC 传输是与浏览器进行交互所必需的, 随着我们将基于浏览器的轻客户端纳入以太坊网络,它们的重要性将愈加凸显。
网络在发展。 硬编码设计决策会导致网络的僵化,阻碍网络与技术的演变。 在一个僵化的协议上引入变化是非常昂贵的,有时甚至是不切实际的,可能导致不可控的破坏。
从一开始就规划可升级性和动态传输选择,为未来的技术栈奠定了基础。
客户端可以在不破坏旧传输的情况下采用新传输,支持多种传输的能力使得受限和沙箱环境 (例如浏览器、嵌入式设备)能够通过适合的本地传输(例如 WSS),以第一类公民的身份与网络进行交互, 而无需代理或信任委托给服务器。
QUIC 标准仍未最终确定(在撰写本文时为工作草案 22), 并且并非所有主流运行时/语言都具备成熟、标准和/或完全互操作的 QUIC 支持。 一个显著的例子是 node.js,其中 QUIC 的实现正在 初期开发中。
注意:TLS 1.3 是 QUIC 传输的前提, 尽管存在一个集成 Noise 作为 QUIC 加密层的实验: nQUIC。
另一方面,TLS 1.3 是 TLS 的最新简化版本。 旧的、不安全的过时密码和算法已被移除,采用 Ed25519 作为唯一的 ECDH 密钥协商功能。 握手速度更快,支持 1-RTT 数据,且会话恢复已经成为现实,除此之外还有其他功能。
Yamux 是 Hashicorp 发明的一个多路复用器,支持流级别的拥塞控制。 在有限的一组语言中存在实现,这并不是一个简单的开发任务。
对此,libp2p 社区构思了 mplex 作为与 libp2p 一起使用的简单、最小化的多路复用器。 它不支持流级别的拥塞控制,并且容易发生头阻塞。
由于 QUIC 提供原生多路复用,因此在使用它时不需要覆盖多路复用器, 但它们需要在缺乏这种支持的 TCP、WebSockets 和其他传输之上叠加。
multiselect 2.0 正在构思中。 讨论始于 这个问题, 但它过于复杂——在与系统的核心和核心部分相关的大型概念 OSS 讨论中通常会发生这种情况。
我们预计在 2020 年的某个时候会有一个更新的倡议,首先定义需求、约束、假设和特性, 以便事先锁定基本共识,然后基于此共识提交实施规范进行构建。
我们计划最终迁移到 multiselect 2.0,因为它将:
所有 libp2p 连接必须经过身份验证、加密和多路复用。 使用不支持原生身份验证/加密和多路复用的网络传输(例如 TCP)进行的连接,需经过协议协商,以达成相互支持的:
在本规范中,我们称这两者为 连接级协商。 支持这些功能的传输(例如 QUIC)则省略这些协商。
在成功选择多路复用器后,所有后续 I/O 会在 流 上发生。 在打开流时,对等方固定一个协议到该流,通过进行 流级协议协商。
目前,multistream-select 1.0 被用于这两种类型的协商, 但 multiselect 2.0 将使用专门的机制进行连接引导过程和流协议协商。
SecIO 多年来一直是 libp2p 的默认加密层。 它在 IPFS 和 Filecoin 中使用。虽然它很快就会被替代,但其已被证明能在大规模下工作。
尽管 SecIO 具有广泛的语言支持,但在主网中我们不会使用它,原因之一是, 它需要进行多次来回往返才能行之有效,并且不支持早期数据(0-RTT 数据), 这是 multiselect 2.0 将利用减少连接引导期间往返次数的机制。
在本规范中,SecIO 不被认为是安全的。
摘自 Noise 协议框架的 网站:
Noise 是一个用于构建加密协议的框架。 Noise 协议支持互相授权和可选授权、身份隐藏、前向保密、零往返加密及其他高级功能。
Noise 本身并未指定单一的握手过程, 而是提供了一个基于 Diffie-Hellman 密钥协商的安全握手构建框架,同样拥有各种权衡和保证。
Noise 握手轻量且易于理解, 并被应用于像 WireGuard、I2P 和 Lightning 等主要以加密为中心的项目中。 各种 研究 对多个 Noise 握手的安全目标进行了正面评估。
传输层加密保护消息交换并提供有益于隐私、安全和反审查的特性。 这些特性源自应用于两个对等体之间整个通信的以下安全保证:
请注意,传输层加密并不排斥应用层加密或加密学。 传输层加密保护通信本身, 而应用层加密学对于应用程序的用例(例如签名、随机数等)是必要的。
Pubsub 是一种快速广播/传播数据的方法。 这些数据被打包为火并遗忘消息,无需每个接收者都给予响应。 订阅某个主题的对等方参与该主题中消息的传播。
替代方案是维护一个完全连接的网状网络(所有对等方一对一连接),这在扩展性上表现较差 (O(n^2))。
为了将来扩展性,而在当前几乎可忽略不计的开销(除了主题名称中的额外字节)。
更改 gossipsub/广播需要一个协调的升级,所有客户端需同时开始发布到新的主题,期间可进行硬分叉。
当节点在 gossipsub 主题上为即将到来的任务(例如,验证者职责提前预告)进行准备时, 节点应该加入未来纪元的主题,以完成该任务,同时侦听当前纪元的主题。
支持多个主题/编码需要存在中继者以在编码和主题之间进行转换, 以避免网络碎片化的情况,即参与者对 gossip 状态持不同看法, 使协议变得更加复杂和脆弱。
Gossip 协议通常会记住它们在有限时间内看到过的消息,基于消息身份 ——如果你在此时间已过后再次发布相同的消息, 它将被重新广播——添加中继延迟也会使这种情况更为常见。
可以想象,在复杂的升级场景中,我们可能会有对等体在两个主题/编码上发布相同的消息, 但在开销方面这里的代价很高——无论是计算开销还是网络开销——因此我们更希望避免这种情况。
允许客户端在替代主题上发布数据,只要他们也在网络范围的强制主题上进行发布。
主题名称有一个层次结构。 未来,gossipsub 可能通过前缀匹配支持通配符订阅 (例如,订阅所有子主题以 root 前缀),通过这种方法进行匹配。 而使用哈希作为主题名称将使我们无法利用未来的特性。
选择明文主题名称并不会导致任何安全或隐私保证的丧失, 因为该域是有限的,且计算一个摘要的预图样并不复杂。
此外,主题名称比其哈希等效物更短(假设使用 SHA-256 哈希), 因此哈希主题将会不必要地增大消息。
StrictNoSign
签名策略?该策略省略了 from
(1)、seqno
(3)、signature
(5) 和 key
(6) 字段。这些字段会:
from
)、发送者类型(根据 seqno
)data
而非 from
和 seqno
。signature
。message-id
?对于我们目前的需求,实际上不需要根据源对等体地址做消息识别,或跟踪消息 seqno
。
通过覆盖默认的 message-id
使用内容寻址,我们可以在进入应用层之前过滤掉不必要的重复消息。
一些消息重发可能的场景示例:
D
、D_low
、D_high
、D_lazy
:推荐默认值。heartbeat_interval
:0.7秒,推荐用于 beacon 链,详见 Protocol Labs 的 GossipSub 评估报告。fanout_ttl
:60秒,推荐默认值。
Fanout 主要用于委员会向子网发布验证。
这在每个验证者每个纪元发生一次,并且每个纪元都会更改子网,
因此 fanout_ttl
的推荐默认不需要增加也没什么好处。mcache_len
:6,增加1以确保 mcache 在 IWANT
可响应 IHAVE
时存在足够长的时间,
在较短的 heartbeat_interval
背景下。如果 mcache_gossip
增加,此参数应增加到至少比 mcache_gossip
超过 3
(~2秒)。mcache_gossip
:3,推荐默认。如果 gossip 时间超出预期且当前窗口无法在恶劣条件下提供足够响应,此值可增加到5或6(~4秒)。seen_ttl
:SLOTS_PER_EPOCH * SECONDS_PER_SLOT / heartbeat_interval = 约 550
。
验证的 gossip 有效性受限于一个纪元,因此这是安全的最大边界。MAXIMUM_GOSSIP_CLOCK_DISPARITY
以验证 gossip 子网中消息的插槽范围?对于某些 gossip 通道(例如,它们用于验证和 BeaconBlocks), 设定了特定的插槽范围,在这些范围内可以发送特定消息, 限制了 gossip 的消息,这些消息应能够合理用于当前时间/插槽的共识。 这是为了减少 DoS 攻击的可选性。
MAXIMUM_GOSSIP_CLOCK_DISPARITY
为验证插槽范围提供了一定的宽容度,以防止 gossip 网络
在时钟偏差方面变得过于脆弱。
对于最小和最大允许的插槽广播时间,
MAXIMUM_GOSSIP_CLOCK_DISPARITY
必须相应地被减去或添加,轻微扩展有效范围。
尽管消息有时会急切地传播到网络,
但节点的选择分叉会阻止这些消息实际融入共识,直到指定插槽的 实际本地开始。
ATTESTATION_SUBNET_COUNT
的验证子网?根据验证者的数量,将分片子网分组可能更有效,并且可能为 gossipsub 通道提供更好的稳定性。
具体的分组将取决于更复杂的网络测试。
这个常数允许在建立验证集合聚合(因为聚合应该在每个子网进行)时提供更大的灵活性。
目前值设置为 MAX_COMMITTEES_PER_SLOT
的值,直到网络测试显示其他情况。
SLOTS_PER_EPOCH
插槽内在 gossip 通道中广播?验证只能在一个纪元的插槽中包含在链上,因此这是自然的截止点。 将验证广播至比一个纪元更早的时间对于链是没有用处的, 而且由于验证者有机会在每个纪元进行新的验证, 提交旧验证给选择分叉就没有多大好处,因为每个验证者几乎很快就可以创建一个新的验证。
除此之外,中继验证需要在创建时的 state
上验证该确认。
因此,任意旧确认的验证将给节点增加额外要求,导致需要及时提供的状态。
这会造成更高的资源负担,并可能形成 DoS 向量。
AggregateAndProof
,而不单单作为 Attestation
?单个验证者的主导策略是始终将包含自身验证的聚合广播到全局通道, 以确保提议者看到他们的验证以便纳入。 使用私有选择标准,并提供该选择的证据以及 gossip 聚合,确保该策略不会淹没全局通道。
此外,攻击者可以制作任意数量的看似诚实的聚合并将其广播到全局 pubsub 通道。 因此,若没有某种选择证据作为聚合者,全局通道将很容易被垃圾消息淹没。
发送整个对象可确保最佳的传播速度。 如果只发送哈希,则区块和验证的传播依赖于每个对等体的递归请求。 在仅哈希的场景下,对等体可能会收到哈希但不知道实际内容来自何处。 发送整个对象确保它们能够在整个网络传播。
禁止大量未经验证的块传播扩展到无法验证签名的节点, 以确保不可能发生此类(放大型的)DoS 攻击。
在阶段 0 中,发布的验证子网对等体将利用 ENR 中的 attnets
条目进行查找。
尽管这种方法在初始提升信标链时有效,但我们计划将来使用更合适的 discv5 主题以实现此和其他类似任务。 ENR 终究不应被用作此目的。 它们更适合存储标识、位置和能力信息,而不是更为波动的广告信息。
分叉版本将在每次硬分叉时手动更新(可能通过递增)。 这是为提供签名的原生域分离,以及帮助识别对等体(通过 ENR)和版本化网络协议(例如,使用分叉版本自然版本化 gossipsub 主题)。
BeaconState.genesis_validators_root
被混合进签名和 ENR 分叉域 (ForkDigest
) ,以便简化双链之间的安全性。
这允许分叉版本在跨链使用期间得到安全复用,除了在内容争议分叉的情况下。
在上述情况下,应额外注意隔离分叉版本(例如,可以在未来版本的其中一个链中翻转高位比特)。
节点在本地存储所有过去和未来计划的分叉版本,每个分叉纪元。 这允许处理从过去分叉/纪元开始的同步和处理消息。
请求按照协议 ID 将其分隔是为了:
/protocol/43-{a,b,c,d,...}
)。注意:当前版本 libp2p 中的协议协商组件称为 multistream-select 1.0。 它在每个请求的协商流时引入了一些过头,尽管可以采用特定于实现的优化来节省这个成本。 Multiselect 2.0 将最终通过记忆式选择之前所选择的协议并模型共享协议表,来消除这个过头。 幸运的是,这个请求/响应协议并不是协议中的预期网络瓶颈, 因此额外的过头不会在很大程度上阻碍这一领域的布局。
我们使用一次性流,每个流在消息结束时关闭。 因此,libp2p 透明地处理底层流中的消息定界。 libp2p 流是全双工的,每一方有责任关闭它们的写入端(类似于 TCP)。 因此,我们可以利用流关闭来独立标记请求和响应的结束。
然而,在 ssz_snappy
的情况下,消息仍然以底层数据的长度进行长度前缀:
Status
消息实际上并不那么耗需要 MAX_PAYLOAD_SIZE
字节。Protobuf varint 是一种有效编码可变(此处无符号)长度整数的技术。 与其保留一个固定大小字段以传达可能的最大值,该字段是弹性的以换取每字节 1 位的开销。
使用 semver 来版本控制网络协议令人困惑。 即使字段的更改向后兼容,也常常并不清楚这些更改实际上意味着什么。 网络协议的协议应显式。想象两个对等体:
这两方绝不应彼此交谈,因为结果不可预测。 这个简化了情形的例子:想象同样的问题与10个可能版本的集合。 因此,结果可能性就成为了 10^2(100)。在此过程中,对等体需要进行建模,造成了无法承担的复杂性。
因此,我们依赖显式、逐字的协议交涉。 在上述情况下,对等体 B 可以通过共存和宣传两个协议的 v1.1.1 与 v1.1.2 来提供向后兼容性。
因此,semver 被降格为在人层面上传达预期,并且在此方面的确表现不佳,因为不明确 “向后兼容”和“破坏性变更” 是仅适用于实例电线约定的层面,还是适用于行为等其他方面。
因此,我们去除与替代 semver 的序列号,该序列号需明示协议,为更改提供不会强制拨入特定政策的权利。
请求/响应的用法是为了避免与 JSON-RPC 和类似的用户-客户端交互机制混淆。
响应者通常希望流量限制请求,以防止垃圾邮件并管理资源消耗,而请求者则希望根据其自身的资源配置策略最大化性能。对于网络而言,优化可用资源的使用是有益的。
大体上,请求者并不知晓每个服务的能力/限制,但可以根据响应率推导出来以选择下一个请求的同伴。
因为服务器在容量可用之前会保存响应,客户端可以乐观地发送请求,而不会冒着进入负评分场景或次优板块轮询的风险。
请求者的典型策略是实现一个与请求性质及连通性参数通常相关的请求超时——例如,在请求区块时,如果第一个对等体在合理时间内未响应,一对等体将选择向第二个请求发送请求;如果第二个对等体响应更快,则重置到第一个对等体。客户端可能会利用过去响应性能来奖励快速的对等体,从而实现对等评价。
响应者的典型策略是实现一二级Token/漏桶策略,包含每个对等体的限制和一个全局限制。流量限制的粒度可能基于整个请求或单个块,后者更为优选。一个Token费用可被赋予请求本身,同时单独为响应中的每个块。因此保护请求,使其不受大块和频繁请求的损害。
对于请求者,流量限制并不显著区分其他导致响应缓慢的条件(缓慢对等体、拥堵等),而后者的条件是必须处理的,因此在此策略中包括流量限制会使实现变得简单。
在按范围或根请求块时,可能会发生所选范围内没有块,或者响应节点没有请求的块。
因此,可能需要传递一个空列表——传递这种状态有多种方式:
success
响应中添加 null
选项,例如通过引入一个额外字节从语义上而言,在时间插槽中缺失某个区块并不会误导槽请求,这让选项 2 显得不自然。
选项 1 允许响应者显示“没有块”,但这信息可能不正确——例如在出于恶意的节点情况。
在选择选项 0 时,客户端无法区分没有块的插槽和不完全响应,但既然它已包含处理恶意对等体的不确定性逻辑,因此选择了选项 0。 客户端应在检测到缺块槽间时标记为未知,直到后续区块验证其不包含块。
假设选择选项 0,没有特殊的 null
编码,考虑请求的插槽 2, 3, 4
——如果在插槽 4 处没有各块,那么响应会是 2, 3, EOF
。
现在考虑相同的情况,但只请求 4
——关闭流并仅显示 EOF
(没有任何 response_chunk
)是一致的。
当节点“应该”拥有的区块未提供是理由让同伴的信任降低——比如,在特定同伴 gossip 一个块时,确保其可以访问其父块。 如果请求父块失败,表明它承载信誉较低,因为节点应在 gossip 之前验证区块。
BeaconBlocksByRange
让服务器选择发送哪个分支的块?连接时,Status
消息会显示特定对等体的同步状态,但这一状态会随时间变化。
在后续处理请求 BeaconBlockByRange
时,信息可能已过期,
并且响应者可能在新的最终化点上移动,修剪之前的头块和已最终化块旁边的块。
为了避免这一竞争条件,我们允许响应者选择向请求者发送哪个分支。 请求者随后验证块并将其合并到自己的数据库——因为他们遵循相同规则,他们此刻应能够到达相同的标准链。
BlocksByRange
请求仅要求服务于最新的 MIN_EPOCHS_FOR_BLOCK_REQUESTS
纪元?由于经济最终性和强弱主观性要求,结点必须提供最近的检查点以安全地加入网络
,这个检查点可以以 root
和 epoch
的形式提出,也可以是整条信标状态,然后简单地从建立的状态同步到顶部。我们期待后者成为主流的 UX 策略。
在最坏的情况(即非常大的验证者集合和最大合法安全衰减)中,
此其他检点必须来自最近 MIN_EPOCHS_FOR_BLOCK_REQUESTS
纪元,因此用户必须能够从该起点阻止同步。
因此,这就定义了位于此之外节点可能修剪块的周期范围,
并且在检查点同步过程中,新节点必须反向插入的时间间隔范围。
MIN_EPOCHS_FOR_BLOCK_REQUESTS
是通过 compute_weak_subjectivity_period
的算法计算得出的,详见
弱主观性指南。为了找到这个最大纪元范围,我们使用的最坏情况事件是拥有非常大的验证者规模
(>= MIN_PER_EPOCH_CHURN_LIMIT * CHURN_LIMIT_QUOTIENT
)。
<!-- eth2spec: skip -->
MIN_EPOCHS_FOR_BLOCK_REQUESTS = (
MIN_VALIDATOR_WITHDRAWABILITY_DELAY
+ MAX_SAFETY_DECAY * CHURN_LIMIT_QUOTIENT // (2 * 100)
)
其中 MAX_SAFETY_DECAY = 100
因此 MIN_EPOCHS_FOR_BLOCK_REQUESTS = 33024
(约 5 个月)。
在从一个已知安全块/状态(例如,从一个弱主观状态开始)中反向插填块到数据库时,
节点不仅必须确保 BeaconBlock
形成一个能够向已知安全块衔接的链,
还必须在 SignedBeaconBlock
抽象中检查提议者签名是否有效。
这是因为签名并未成为 BeaconBlock
哈希链的一部分,
从而可能会被攻击者通过有效 BeaconBlock
但不合法签名进行反复篡改。
虽然在这种特定情况中不构成安全降低(由于从弱主观检查点出发的假设), 但这样的情况可能代表历史数据的无效性,并可能毫无意地传发送给额外节点。
在同步过程中仅通过检查接下来的块并分析父根形成的图形, 我们才能判断插槽是否在特定的分支上被跳过。 因为服务器端在响应中可能出于任何原因选择省略某个块,客户端必须验证该图形,并准备填补相应的空隙。
例如,如果对等体在请求 [2, 3, 4]
时响应为块 [2, 3]
,客户端不能假设块 4 不存在——
这只是意味着响应的对等体没有发送它(他们可能还没有它,或者可能是恶意企图)
而必须以后续块来判断此特定分支上是否仍存在块 4。
discv5 是一项独立的协议,基于UDP在专用端口上运行,仅用于对等体和服务的发现。 discv5 支持自证的灵活对等体记录(ENR)和基于主题的广告,两者都是在这种情况下的要求。
另一方面,libp2p Kademlia DHT 是一种完整的 DHT 协议/实现, 具有内容路由和存储能力,而在此上下文下这些都不相关。
以太坊执行层节点将进化以支持 discv5。 通过使以太坊共识层和执行层客户之间共享发现网络模块, 我们能从网络规模逐步增加的效益中受益,从而提高抵抗某些攻击的韧性,防止小规模网络更易遭受攻击。 这还有助于两条网络的轻客户端找节点获取特定能力。
discv5 正在接受审计。
以太坊节点记录是自证的节点记录。 节点为自身撰写并传播 ENR,通过加密签名证明其作者身份。 ENR 是按自然序列索引,便于解决冲突。
ENR 是具有字符串索引的 ASCII 键值记录。 它们可以存储任意信息,但 EIP-778 指定了预定义的字典,包括 IPv4 和 IPv6 地址、secp256k1 公钥等。
比较 ENR 和 multiaddr 有如比较苹果与橙子。 ENR 是节点身份、地址和元数据的自证形式。 multiaddr 是地址字符串,显现出自我描述、可组合和未来可预测的特性。 一个 ENR 可以包含多条 multiaddr,而 multiaddr 也可以安全地从一个经过认证的 ENR 的字段中导出。
discv5 使用 ENR,我们假设需要:
multiaddr
添加至字典,以便节点可以在 ENR 下的保留命名空间中以 multiaddr
宣传自身的 multiaddr。– 和/或 –尽管客户端软件在信标链的创世状态与块确定之前可能在本地运行,
但是在此之前客户端无法形成有效的 ENR。
ENR 中包含 fork_digest
,利用 genesis_validators_root
来实现链之间的更干净的分离
,因此在不知道创世数据之前,无法通过 fork_digest
更干净地找到目标链上的对等体。
一旦创世数据被知悉,我们便可形成 ENR 并安全地发现对等体。
使用工作证明存款合约进行存款时,fork_digest
将在 genesis_time
前 GENESIS_DELAY
(主网配置 7 天) 已知,
从而为找到对等体、形成初始连接和 gossip 子网提供了充足时间。
SSZ 是共识层所使用的,并且所有实施都应支持 SSZ 编码/解码, 不需要将其他依赖项添加到客户端实现中。 这是跨网络发送对象序列化的自然选择。 在大多数协议中,实际数据将进一步压缩以提高效率。
SSZ 有为共识对象(通常发送到网络以外)定义的明确定义模式,从而减少了需发送的序列化模式数据。 它还定义了本网络规范所要求的所有必要的类型。
我们在网络传输过程中压缩数据以实现每条消息更小的负载,这综合结果提高了整体效率,更好地利用了可用带宽,并降低了网络总交通成本。
目前,libp2p 没有现成的压缩功能,这种压缩能够进行动态协商,并整合在连接和流的表层之上,但 正在考虑中。
此功能并不简单,因为需要考虑网络 IO 循环、内核缓冲、分块及数据包分包等行为。 libp2p 流是无限制的流,而压缩算法则在有先前知识的有界字节流中表现得最好。
压缩通常并非一刀切的问题。 许多变量需要仔细评估,而通用方法/选择会导致大小削减不良, 并且在计算和内存之间的交换中可能会适得其反。
鉴于这些原因,生成性协商压缩算法可视为 libp2p 社区的一项研发任务, 在中期内我们乐于解决这一难题。
在此阶段,明智的选择是视 libp2p 为字节的信使, 并让应用层参与这些字节的压缩。 具体取决于交互层的情况:
Snappy 在以太坊 1.0 中得以使用。它由谷歌进行维护,有良好的基准测试, 并可以在不将其内存膨胀的情况下计算出未压缩对象的大小。 这种算法可以阻止在发送较大未压缩数据时造成的 DoS 向量。
可以,你可以在 libp2p 协议处理程序中添加日志以记录进出消息。 建议使用编程设计模式,将日志逻辑干净地封装起来。
如果你的 libp2p 库依赖于 Netty(JVM)或 Node.js(JavaScript)等框架/运行时, 你可以在这些框架/运行时的日志记录设施中使用来启用消息跟踪。对于特定的临时测试场景,你可以使用 plaintext/2.0.0 secure channel (本质上是无操作加密或消息认证),结合 tcpdump 或 Wireshark 进行线上的检查。
每种类型的 SSZ 编码输出都有大小范围:每个动态类型,如列表,有一个“限制”,可用于计算有效输出的最大大小。 请注意,对于一些更复杂的动态长度对象,可能需要包含元素偏移量(每个4字节)。 其他类型是静态的,它们有固定的大小:不涉及动态长度内容,最小和最大范围是相同的。
作为参考,可以预先计算出类型边界, 如本例所示。 建议从使用的 SSZ 类型定义中推导这些长度,以确保版本更改不会导致类型边界不同步。
在通过 gossipsub 和/或 req/resp 域传输消息时,我们希望确保无论底层传输如何,支持相同的有效负载大小,从而将共识层与 libp2p 引起的开销和特定传输策略解耦。
为了从所需的应用大小推导“编码大小限制”,我们考虑了 snappy 压缩和框架开销。
在 gossipsub 的情况下,协议支持在每个 gossipsub 帧中发送多个应用有效负载,以及将应用数据与控制消息混合。限制设置为至少允许一个最大大小的应用级消息加上一小部分(1 KiB)的 gossipsub 开销。实现可以自由地将多个较小的应用消息打包到单个 gossipsub 帧中,和/或根据需要与控制消息结合。
该限制是针对无压缩有效负载大小设定的,特别是为了防止解压缩炸弹。
消息大小限制保护了多种形式的 DoS 和基于网络的放大攻击,并根据协议需求为客户端资源(网络、内存)使用提供上限,以解码、缓冲、缓存、存储和重新传输消息,这又转化为性能和保护的权衡,确保在从网络不稳定中恢复期间能处理最坏情况的能力。
特别是,块——当前唯一没有实用的 SSZ 派生尺寸上限的消息类型——无法作为 gossipsub 验证性检查的一部分同步完全验证。这意味着某些情况下,由验证者签名的无效消息可能会被网络放大。
本节将很快包含一个矩阵,显示本规范所需的 libp2p 特性的成熟度/状态,覆盖正在开发客户端的语言。
- 原文链接: github.com/ethereum/cons...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!