本文档详细阐述了ERC4337的捆绑器软件网络规范,涵盖了网络基础和三种网络交互领域,包括gossip、req/resp和发现域。文中深入探讨了传输、加密、协议协商及多路复用等技术细节,提供了丰富的技术背景与实现细节。
注意: 本文档为研究人员和实施者的进行中工作。
本文档包含有关ERC4337的捆绑软件网络规范。
它主要由两个部分组成:
<!-- TOC --> <!-- START doctoc generated TOC please keep comment here to allow auto update --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
<!-- END doctoc generated TOC please keep comment here to allow auto update --> <!-- /TOC -->
本节概述了ERC4337捆绑器网络栈的规范。
所有实现必须支持TCP libp2p传输,并且这必须同时在拨号和监听时启用(即出站和入站连接)。libp2p TCP传输支持在IPv4和IPv6地址上监听(并可以同时在多个地址上监听)。
捆绑器必须支持至少在IPv4或IPv6上监听。 不支持在IPv4上监听的捆绑器应该意识到在 互联网范围内路由性/支持方面可能会存在的潜在劣势。捆绑器可以选择仅在IPv6上监听,但必须能够同时拨号IPv4和IPv6地址。
所有监听端点必须是公开可拨号的,因此不能依赖于libp2p电路中继、AutoNAT或AutoRelay设施。 (电路中继、AutoNAT或AutoRelay的使用将很快具体重审。)
在NAT后面操作的节点,或者默认情况下无法拨号的节点(例如容器运行时、防火墙等), 必须配置其基础设施以在宣布的公共监听端点上启用入站流量。
将使用Libp2p-noise安全频道握手与secp256k1
身份进行加密。
根据libp2p规范,捆绑器必须支持XX
握手模式。
捆绑器在协商要使用的协议版本时必须使用精确等值,并且可以使用版本来优先考虑更高的版本号。
捆绑器必须支持multistream-select 1.0 并且在规范明确时可以支持multiselect 2.0。 一旦所有捆绑器都实现了multiselect 2.0,multistream-select 1.0可能会被淘汰。
像以太坊共识客户端的p2p规范一样,捆绑器应该支持mplex和yamux这两种复用器实现。它们的协议ID分别是:/mplex/6.7.0和/yamux/1.0.0。
客户端必须支持mplex,并且可以支持yamux。如果客户端同时支持这两者,则在协商期间yamux必须优先。
本节概述了在此规范中使用的常量。
名称 | 值 | 描述 | |
---|---|---|---|
GOSSIP_MAX_SIZE |
2**20 (= 1048576, 1 MiB) |
未压缩的gossip消息的最大允许大小。 | |
MAX_OPS_PER_REQUEST |
4096 |
单个请求中用户操作的最大数量。 | |
RESP_TIMEOUT |
10s |
完整响应传输的最大时间。 | |
TTFB_TIMEOUT |
5s |
等待请求响应第一个字节的最大时间(首次字节时间)。 | |
POOLED_HASHES_CONTEXT_TIMEOUT |
10s |
保持池哈希请求上下文的时间。 | |
MAX_SUPPORTED_MEMPOOLS |
1024 |
支持的内存池的最大数量。 | |
MESSAGE_DOMAIN_INVALID_SNAPPY |
DomainType('0x00000000') |
用于*gossip消息-ID隔离无效snappy消息的4字节域 | |
MESSAGE_DOMAIN_VALID_SNAPPY |
DomainType('0x01000000') |
用于*gossip消息-ID隔离有效snappy消息的4字节域 | (feat(p2p): 重定义gossip消息ID和内容) |
MAX_IPFS_CID_LENGTH |
256 |
IPFS CID字符串的最大长度。 |
本节概述了在此规范中使用的类型定义。
名称 | 描述 |
---|---|
bytes32 |
长度为32的固定长度字节数组 |
捆绑器必须本地存储以下MetaData
:
(
seq_number: uint64
supported_mempools: List[MempoolID, MAX_SUPPORTED_MEMPOOLS]
)
其中
seq_number
是一个 uint64
,从0开始,用于版本化节点的元数据。如果本地MetaData
中的其他字段发生更改,节点必须将seq_number
增加1。supported_mempools
是一个mempool-id
的列表。捆绑器必须支持gossipsub v1 libp2p协议 包括gossipsub v1.1扩展。
主题是普通的UTF-8字符串,并且根据protobuf的规定在网络上进行编码(gossipsub消息被封装在protobuf消息中)。
主题字符串的形式为:/account_abstraction/mempool_id/Name/Encoding
。
这定义了在主题上发送的数据类型以及消息的数据字段的编码方式。
mempool_id
- 包含内存池元数据描述的文件的IPFS CID字符串。有关更多详细信息,请参见mempool-id部分。Name
- 请见下表Encoding
- 编码策略描述在网络上传输的字节的特定表示。有关更多详细信息,请参见编码部分。每个gossipsub 消息的最大大小为GOSSIP_MAX_SIZE
。
捆绑器必须拒绝(失败验证)超出此大小限制的消息。
同样,捆绑器必须不发送或传播超过此限制的消息。与ETH2一样,客户端必须在消息上强制实施StrictNoSign
签名协议。
一个gossipsub消息的message-id
必须是以下从消息数据计算的20字节值:
如果message.data
有有效的snappy解压缩,将message-id
设置为以下数据连接的SHA256
哈希的前20个字节: MESSAGE_DOMAIN_VALID_SNAPPY
,主题字节字符串的长度(以小端uint64
编码),主题字节字符串,以及snappy解压缩的消息数据
SHA256(MESSAGE_DOMAIN_VALID_SNAPPY + uint_to_bytes(uint64(len(message.topic))) + message.topic + snappy_decompress(message.data))[:20]
否则,设定message-id
为以下数据连接的SHA256
哈希的前20个字节:MESSAGE_DOMAIN_INVALID_SNAPPY
,主题字节字符串的长度(以小端uint64
编码),主题字节字符串,以及原始消息数据:
SHA256(MESSAGE_DOMAIN_INVALID_SNAPPY + uint_to_bytes(uint64(len(message.topic))) + message.topic + message.data)[:20]
注意: 上述逻辑处理两个特殊情况:
(1)多个Snappy data
可以解压到相同的值,
和 (2)某些消息 data
可能完全无法Snappy解压缩。
有效负载在gossipsub消息的data
字段中被传递,具体取决于主题:
名称 | 消息类型 |
---|---|
user_operation |
VerifiedUserOperation |
捆绑器必须拒绝(失败验证)包含不正确类型或无效负载的消息。
在处理传入gossip时,捆绑器可以降低或者断开未遵循这些约束的对等体的评分。
对于任何可选排队,捆绑器应保持最大队列大小以避免拒绝服务攻击向量。
用于将用户操作传播到共享相同内存池的对等体的主要内存池主题是user_operation
。
user_operation
user_operation
主题用于在用户操作有效的内存池上gossip用户操作。
在将user_operation
转发到网络之前,必须通过以下验证:
verified_at_block_hash
过于陈旧。在从eth_sendUserOperation
RPC调用接收用户操作后,捆绑器必须:
主题后缀有编码。编码定义了gossipsub消息负载的编码方式。
ssz_snappy - 所有对象都经过SSZ编码,然后使用Snappy块压缩。示例:规范内存池的user_operation
主题字符串为/account_abstraction/<mempool_id>/user_operation/ssz_snappy,其中<mempool_id>为TBD
(内存池yaml/JSON文件的IPFS CID字符串),gossipsub消息的data字段是一个已经SSZ编码并且用Snappy压缩的VerifiedUserOperation。
Snappy有两种格式:“块”和“帧”(流)。gossip消息保持相对较小(100字节到100千字节),因此使用基本的Snappy块压缩以避免与Snappy帧相关的附加开销。
实现必须使用单一编码进行gossip。更改编码将需要参与实现之间的协作。
捆绑器所支持的每个内存池相关的元数据在IPFS中进行了记录和存储(建议将其副本提交到eth-infinitism
Github库)。
该IPFS CID
字符串称为mempool-id
,并用于捆绑器中的订阅主题。
MempoolID
的模式为:
(
List[byte, MAX_IPFS_CID_LENGTH]
)
注意: 这是IPFS CID字符串的UTF-8编码。客户端必须将此字节序列解释为UTF-8字符串,并且必须拒绝任何无效的字节序列。
内存池元数据的拟议结构如下:
chainId: '1'
entryPointContract: '0x0576a174d229e3cfa37253523e645a78a0c91b57'
description: >-
这是默认/规范内存池,绝大多数捆绑器将在以太坊主网上使用。
minimumStake: '0.0'
规范内存池的mempool-id
为TBD
(yaml/JSON文件的IPFS CID字符串)。
将有一个由捆绑器社区维护的规范内存池已发布列表。此列表代表支持完整ERC-7562验证规则以及某些内存池配置参数和特定入口点合约的内存池。所有捆绑器应支持这些内存池。不需要访问备用内存池的用户操作将由至少一个这些规范内存池支持。
这些内存池将按优先顺序发布。用户操作仅应在最前面的有效内存池主题中发送。
每个消息类型被划分到其自己的libp2p协议ID中,这是一个大小写敏感的UTF-8字符串,格式为:
/ProtocolPrefix/MessageName/SchemaVersion/Encoding
其中:
ProtocolPrefix
- 消息按共享libp2p协议名称前缀分组为家族。
在此情况下,我们使用/account_abstraction/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”]
与编码相关的头部可以携带元数据或断言,例如编码的有效负载长度,以确保完整性和抗攻击目的。 由于req/resp流是一次性使用,并且流关闭隐式地限定了边界,因此不严格需要长度前缀有效负载; 但是,如SSZ等某些编码确实需要,以增加安全性。
一个response
是由零个或多个response_chunk
形成的。
对于请求和响应,编码相关的头部必须是有效的,而编码负载必须在编码相关头部的约束内有效。
这包括某些编码策略中对于有效负载大小的类型特定限制。
无论这些类型特定的限制如何,所有方法响应块都必须应用最大未压缩字节大小MAX_CHUNK_SIZE
的全局最大限制。
客户端必须确保长度在这些限制范围内;如果没有,他们应立即重置流。 跟踪对等体声誉的客户端可以在这种情况下降低不良行为对等体的评分。
一旦新流的请求类型协议ID协商完成,完整的请求消息应立即发送。 请求必须根据编码策略进行编码。
请求者在完成请求消息的写入后,必须关闭流的写入端。 此时,流将处于半关闭状态。
请求者必须等待最多TTFB_TIMEOUT
以接收第一个响应字节(首次字节时间 - TTFB 超时)。
当发生时,请求者允许在接收到的每个后续response_chunk
中有进一步的RESP_TIMEOUT
。
如果任何这些超时触发,请求者应重置流,并将req/resp操作视为失败。
请求者应从流中读取,直到出现以下情况之一:
response_chunk
的任何部分未通过验证。对于由单个有效的response_chunk
组成的请求,请求者应在关闭流之前完全读取该块,如编码相关头部所定义的。
一旦新流的请求类型协议ID完成协商, 响应方应处理传入请求,并必须在处理之前验证它。 请求的处理和验证必须根据编码策略进行,直到EOF(由请求者表示的流半关闭)。
响应者必须:
N
有任何长度断言,则必须恰好从流中读取N
字节,此时应出现EOF(没有更多字节)。
如果不是这样,应视为失败。response_chunk
(结果、可选头部、有效负载)组成。如果步骤(1)、(2)或(3)因无效、格式错误或不一致的数据而失败,响应者必须以错误方式响应。 跟踪对等体声誉的客户端可以记录此类失败以及意外事件,例如早期的流重置。
整个请求应在不超过RESP_TIMEOUT
内读取。
在超时的情况下,响应方应重置流。
响应方应及时发送response_chunk
。
块以单字节响应代码开始,确定response_chunk
的内容(BNF语法中的result
部分)。
对于多个块,仅最后一个块可以具有非零错误代码(即在发生错误后终止块流)。
响应代码可以是以下值之一,以单个无符号字节编码:
ErrorMessage
架构(如下所述)。ErrorMessage
架构(如下所述)。ErrorMessage
架构(如下所述)。
注: 此响应代码仅在指定位置的响应中有效。客户端可以使用响应代码大于128
来指示其他替代的、错误的请求特定响应。
范围 [4, 127]
保留用于未来使用,且在没有明确识别的情况下应视为错误。
ErrorMessage
架构为:
(
error_message: List[byte, 256]
)
注: 按惯例,error_message
是可以解释为UTF-8字符串的字节序列(用于调试目的)。
客户端必须将任何字节序列视为有效。
协商协议ID的标记指定用于req/resp交互的编码类型。 目前只能有一个值:
SimpleSerialize (SSZ)规范阐明了对象如何进行SSZ编码。
为实现SSZ上的Snappy编码,我们在编码时将对象的序列化形式传递给Snappy压缩机。 解码时则反之。
Snappy有两种格式:“块”和“帧”(流)。 为了支持大型请求和响应块,使用Snappy帧。
因为Snappy帧内容最大大小为65536
字节
且帧头不过是identifier (1) + checksum (4)
字节,因此单个帧的预期缓存是可接受的。
编码相关头部:使用ssz_snappy
编码策略的Req/resp协议必须编码原始SSZ字节的长度,
以无符号protobuf变长整型进行编码。
写入: 首先计算并写入SSZ字节长度,随后SSZ编码器可以直接将块内容写入流。 应用Snappy时,可以将其通过缓冲的Snappy写入器逐帧压缩。
读取: 在读取预期的SSZ字节长度后,SSZ解码器可以直接从流中读取内容。 应用Snappy时,可以将其通过缓冲的Snappy读取器逐帧解压缩。
在读取负载之前,必须验证头部:
uint64
均足够。在读取有效头部后,可能会读取负载,同时保持来自头部的大小约束。
读取器在读取头部的SSZ长度前缀n
后,不应读取超过max_encoded_len(n)
字节。
ssz_snappy
,这是:32 + n + n // 6
。
这被认为是Snappy的最坏情况压缩结果。读取器应将以下情况视为无效输入:
n
个SSZ字节后,仍有剩余字节。如果读取的字节超过要求,应该期待EOF。在出现无效输入(头部或有效负载)情况下,读取器必须:
InvalidRequest
。请求本身被忽略。所有仅包含单个字段的消息必须直接作为该字段的类型进行编码,而不得作为SSZ容器编码。
协议ID: /account_abstraction/req/status/1/
请求、响应内容:
(
chain_id: uint64
block_hash: Bytes32
block_number: uint64
)
字段如下,当客户端发送消息时所见:
拨号客户端在连接后必须发送Status
请求。
请求/响应必须作为单个SSZ字段进行编码。
响应必须由单个response_chunk
组成。
客户在以下条件下必须立即断开连接:
chain_id
与节点的本地chain_id
不匹配,因为对等体在另一个网络上。这是配置错误。客户端可以在以下条件下断开与对等体的连接:
block_number
远远落后于当前本地区块号。这是为了检查对等体的活跃性和重组状态。实施者可以自由定义此限制,并且可能会依赖于网络。建议提供一些宽限期,因为节点以不同的速度发现新块。协议ID: /account_abstraction/req/goodbye/1/
请求、响应内容:
(
uint64
)
客户端可以在断开连接时发送再见消息。理由字段可以是以下值之一:
客户端可以使用超过128
的原因代码来指示其他替代的、错误的请求特定响应。
范围 [4, 127]
保留用于未来使用。
请求/响应必须作为单个SSZ字段编码。
响应必须由单个response_chunk
组成。
协议ID: /account_abstraction/req/ping/1/
请求内容:
(
uint64
)
响应内容:
(
uint64
)
间歇性发送,Ping
协议检查连接对等体的存活性。
对等体请求并响应其本地元数据序列号(MetaData.seq_number
)。
如果对等体未对Ping
请求做出响应,则客户端可以断开与该对等体的连接。
客户端可以确定其本地对等体的MetaData是否是最新的,
并且如果不是,则可以通过MetaData
RPC方法请求更新版本。
请求/响应必须作为单个SSZ字段编码。
响应必须由单个response_chunk
组成。
协议ID: /account_abstraction/req/metadata/1/
没有请求内容。
响应内容:
(
MetaData
)
请求对等体的元数据。 请求在打开和协商流时不发送任何请求内容。 建立后,接收对等体应回复其本地最新的MetaData。
响应必须编码为SSZ容器。
响应必须由单个response_chunk
组成。
协议ID: /account_abstraction/req/pooled_user_op_hashes/1/
请求内容:
(
// 游标
Bytes32
)
响应内容:
(
hashes: List[Bytes32, MAX_OPS_PER_REQUEST]
next_cursor: Bytes32
)
pooled_user_ops_by_hash
请求用于同步连接对等体的内存池内容。客户端可能在捆绑器节点启动时发送此请求给每个已连接的对等体以同步其内存池。
在初始请求中,请求者应发送零值游标,指示接收方开始新的请求上下文。
作为初始握手过程的一部分,此请求的接收者必须知道请求者和接收者共有的内存池ID。接收到带有空游标的请求后,接收者必须从这些共同的内存池构建一个用户操作哈希列表。接收者用此哈希列表响应。如果哈希数量超过MAX_OPS_PER_REQUEST
,接收者可以响应一个非零的next_cursor
值并保存请求上下文。
next_cursor
是一个由接收者选择的透明值,用于在请求上下文中启用分页。
如果请求者收到非零next_cursor
值,他们可以发送带有此游标值的请求。接收者解释此游标并用另一组不重叠的哈希回应。此过程可以持续,直到接收者没有有效哈希并返回零next_cursor
值。在此请求上下文中,接收者必须移除已被矿工合并的哈希,以只响应有效用户操作。接收者应当在初始请求后不更改上下文,新添加到池中的用户操作不受此请求上下文限制,且可通过开始新的请求接收。
在初始请求后的POOLED_HASHES_CONTEXT_TIMEOUT
秒后,接收者应丢弃请求上下文,如果收到的游标已超时,则接收者必须返回错误。
请求方应:
POOLED_HASHES_CONTEXT_TIMEOUT
内完成其请求请求必须作为单个SSZ字段进行编码。
响应必须作为SSZ容器进行编码,并且必须由单个response_chunk
组成。
协议ID: /account_abstraction/req/pooled_user_ops_by_hash/1/
请求内容:
(
List[bytes32, MAX_OPS_PER_REQUEST]
)
响应内容:
(
List[VerifiedUserOperation, MAX_OPS_PER_REQUEST]
)
pooled_user_ops_by_hash
请求用户操作的接收者内存池中的给定用户操作哈希列表。对PooledUserOpsByHash
请求的推荐软限制是MAX_OPS_PER_REQUEST
哈希数量。接收者可以施加任意限制(大小或服务时间),这不得视为协议违反。
请求必须作为单个SSZ字段进行编码。
响应必须由零个或多个response_chunk
组成。每个成功的response_chunk
中必须包含一个单独的UserOp
有效负载。
发现版本5(discv5)(协议版本v5.1)用于对等体发现。
discv5
是一个独立的协议,在专用端口上运行UDP,仅用于对等体发现。
discv5
支持自我认证的灵活对等记录(ENRs)和基于主题的广告,这些都是在此上下文中(或将在此上下文中)需要的。
discv5
应通过实现适配器集成到捆绑器的libp2p堆栈中,以使其符合服务发现
和对等体路由的抽象和接口(go-libp2p链接提供)。
操作的输入包括对等体ID(在查找特定对等体时)或能力(在搜索具有特定能力的对等体时),输出将是从discv5后端返回的ENR记录转换的multiaddrs。
这种集成使libp2p堆栈能够随后与发现的对等体形成连接和流。
捆绑器将使用与以太坊共识客户端相同类型的以太坊节点记录(ENR)。基本上,它们必须包含以下条目 (排除序列号和签名,这在ENR中必须存在):
secp256k1
字段)。ENR可以包含以下条目:
ip
字段)和/或IPv6地址(ip6
字段)。tcp
字段)。udp
字段)。这些参数的规范可以在ENR规范中找到。
ENR必须携带一个chain_id
键,包含捆绑器连接的网络的ID。这是为了确保建立的连接与意图以太坊网络中的对等体。
键 | 值 |
---|---|
chain_id |
SSZ uint64 |
客户端必须连接到具有其本地值匹配的chain_id
的对等体。
以下类型为SimpleSerialize (SSZ)容器。
UserOp
class UserOp(Container):
sender: Address
nonce: uint256
init_code: bytes
call_data: bytes
call_gas_limit: uint256
verification_gas_limit: uint256
pre_verification_gas: uint256
max_fee_per_gas: uint256
max_priority_fee_per_gas: uint256
paymaster_and_data: bytes
signature: bytes
VerifiedUserOperation
class VerifiedUserOperation(Container):
user_operation: UserOp
entry_point: Address
verified_at_block_hash: uint256
- 原文链接: github.com/eth-infinitis...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!