ERC4337 -- 网络通信

本文档详细阐述了ERC4337的捆绑器软件网络规范,涵盖了网络基础和三种网络交互领域,包括gossip、req/resp和发现域。文中深入探讨了传输、加密、协议协商及多路复用等技术细节,提供了丰富的技术背景与实现细节。

ERC4337 -- 网络

注意: 本文档为研究人员和实施者的进行中工作。

本文档包含有关ERC4337的捆绑软件网络规范。

它主要由两个部分组成:

  1. 网络基础的规范。
  2. 三个捆绑器网络交互(domains)的规范:(a) gossip域,(b) req/resp域,以及 (c) 发现域。

目录

<!-- 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的列表。

gossip域:gossipsub

捆绑器必须支持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过于陈旧。
  • [拒绝] 如果任何在EIP中指定的合理性检查失败。
  • [拒绝] 如果用户操作的模拟验证未通过接收的内存池的验证规则。

在从eth_sendUserOperation RPC调用接收用户操作后,捆绑器必须:

  1. 模拟用户操作。
  2. 检查发布顺序的规范内存池列表。查找用户操作有效的第一个内存池。如果存在匹配,捆绑器必须仅在该相关内存池主题上gossip用户操作。
  3. 否则,检查支持的备用内存池列表。在用户操作有效的所有主题上gossip用户操作。

编码

主题后缀有编码。编码定义了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。更改编码将需要参与实现之间的协作。

内存池ID

捆绑器所支持的每个内存池相关的元数据在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-idTBD(yaml/JSON文件的IPFS CID字符串)。

规范内存池

将有一个由捆绑器社区维护的规范内存池已发布列表。此列表代表支持完整ERC-7562验证规则以及某些内存池配置参数和特定入口点合约的内存池。所有捆绑器应支持这些内存池。不需要访问备用内存池的用户操作将由至少一个这些规范内存池支持。

这些内存池将按优先顺序发布。用户操作仅应在最前面的有效内存池主题中发送。

Req/Resp域

协议识别

每个消息类型被划分到其自己的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 在建立底层流之前处理请求类型、版本和编码协商。

Req/Resp交互

我们使用每个请求/响应交互的一条流。 无论成功还是出错,流在交互结束时关闭。

请求/响应消息必须遵循协议名称中指定的编码,并遵循此结构(放宽的BNF语法):

request   ::= &lt;encoding-dependent-header> | &lt;encoded-payload>
response  ::= &lt;response_chunk>*
response_chunk  ::= &lt;result> | &lt;encoding-dependent-header> | &lt;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操作视为失败。

请求者应从流中读取,直到出现以下情况之一:

  1. 在其中一个块中收到错误结果(可以在停止之前读取错误负载)。
  2. 响应方关闭流。
  3. response_chunk的任何部分未通过验证。
  4. 已读取请求块的最大数量。

对于由单个有效的response_chunk组成的请求,请求者应在关闭流之前完全读取该块,如编码相关头部所定义的。

响应方

一旦新流的请求类型协议ID完成协商, 响应方应处理传入请求,并必须在处理之前验证它。 请求的处理和验证必须根据编码策略进行,直到EOF(由请求者表示的流半关闭)。

响应者必须:

  1. 使用编码策略读取可选头部。
  2. 如果对长度N有任何长度断言,则必须恰好从流中读取N字节,此时应出现EOF(没有更多字节)。 如果不是这样,应视为失败。
  3. 反序列化预期的类型并处理请求。
  4. 写入响应,该响应可以由零个或多个response_chunk(结果、可选头部、有效负载)组成。
  5. 关闭他们的写入流。在此时,流将完全关闭。

如果步骤(1)、(2)或(3)因无效、格式错误或不一致的数据而失败,响应者必须以错误方式响应。 跟踪对等体声誉的客户端可以记录此类失败以及意外事件,例如早期的流重置。

整个请求应在不超过RESP_TIMEOUT内读取。 在超时的情况下,响应方应重置流。

响应方应及时发送response_chunk。 块以单字节响应代码开始,确定response_chunk的内容(BNF语法中的result部分)。 对于多个块,仅最后一个块可以具有非零错误代码(即在发生错误后终止块流)。

响应代码可以是以下值之一,以单个无符号字节编码:

  • 0: 成功 -- 随后是正常响应,其内容与请求中指定的预期消息架构和编码相匹配。
  • 1: 无效请求 -- 请求的内容在语义上无效,或者有效负载格式错误,或者无法理解。 响应负载符合ErrorMessage架构(如下所述)。
  • 2: 服务器错误 -- 响应者在处理请求时遇到错误。 响应负载符合ErrorMessage架构(如下所述)。
  • 3: 资源不可用 -- 响应者没有请求的资源。 响应负载符合ErrorMessage架构(如下所述)。 : 此响应代码仅在指定位置的响应中有效。

客户端可以使用响应代码大于128来指示其他替代的、错误的请求特定响应。

范围 [4, 127] 保留用于未来使用,且在没有明确识别的情况下应视为错误。

ErrorMessage架构为:

(
  error_message: List[byte, 256]
)

: 按惯例,error_message 是可以解释为UTF-8字符串的字节序列(用于调试目的)。 客户端必须将任何字节序列视为有效。

编码策略

协商协议ID的标记指定用于req/resp交互的编码类型。 目前只能有一个值:

  • ssz_snappy: 内容首先SSZ编码

    然后使用Snappy帧压缩。 对于包含单个字段的对象,仅对该字段进行SSZ编码,而不是对包含单字段的容器进行编码。 所有客户端必须支持这种编码类型。

SSZ-snappy编码策略

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读取器逐帧解压缩。

在读取负载之前,必须验证头部:

  • 用于长度前缀的无符号protobuf变长整型不得长于10字节,这对任意uint64均足够。
  • 长度前缀在源自有效负载SSZ类型的预期大小范围内。

在读取有效头部后,可能会读取负载,同时保持来自头部的大小约束。

读取器在读取头部的SSZ长度前缀n后,不应读取超过max_encoded_len(n)字节。

读取器应将以下情况视为无效输入:

  • 在读取n个SSZ字节后,仍有剩余字节。如果读取的字节超过要求,应该期待EOF。
  • 在完全读取声明的SSZ字节长度之前早期EOF。

在出现无效输入(头部或有效负载)情况下,读取器必须:

  • 从请求:返回错误消息,响应代码 InvalidRequest。请求本身被忽略。
  • 从响应:忽略响应,该响应必须视为坏服务器行为。

所有仅包含单个字段的消息必须直接作为该字段的类型进行编码,而不得作为SSZ容器编码。

消息

状态

协议ID: /account_abstraction/req/status/1/

请求、响应内容:

(
  chain_id: uint64
  block_hash: Bytes32
  block_number: uint64
)

字段如下,当客户端发送消息时所见:

  • chain_id - 捆绑器网络的链ID。有关社区策划的链ID列表,请参见 https://chainid.network
  • block_hash - 捆绑器所见的最后一个区块哈希。
  • block_number - 捆绑器所见的最后一个区块编号。

拨号客户端在连接后必须发送Status请求。

请求/响应必须作为单个SSZ字段进行编码。

响应必须由单个response_chunk组成。

客户在以下条件下必须立即断开连接:

  1. 如果chain_id与节点的本地chain_id不匹配,因为对等体在另一个网络上。这是配置错误。

客户端可以在以下条件下断开与对等体的连接:

  1. 对等体的block_number远远落后于当前本地区块号。这是为了检查对等体的活跃性和重组状态。实施者可以自由定义此限制,并且可能会依赖于网络。建议提供一些宽限期,因为节点以不同的速度发现新块。
再见

协议ID: /account_abstraction/req/goodbye/1/

请求、响应内容:

(
  uint64
)

客户端可以在断开连接时发送再见消息。理由字段可以是以下值之一:

  • 1: 客户端关闭。
  • 2: 无关网络。
  • 3: 故障/错误。

客户端可以使用超过128的原因代码来指示其他替代的、错误的请求特定响应。

范围 [4, 127] 保留用于未来使用。

请求/响应必须作为单个SSZ字段编码。

响应必须由单个response_chunk组成。

Ping

协议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组成。

PooledUserOpHashes

协议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组成。

PooledUserOpsByHash

协议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有效负载。

发现域:discv5

发现版本5(discv5)(协议版本v5.1)用于对等体发现。

discv5是一个独立的协议,在专用端口上运行UDP,仅用于对等体发现。 discv5支持自我认证的灵活对等记录(ENRs)和基于主题的广告,这些都是在此上下文中(或将在此上下文中)需要的。

集成到libp2p堆栈

discv5应通过实现适配器集成到捆绑器的libp2p堆栈中,以使其符合服务发现对等体路由的抽象和接口(go-libp2p链接提供)。

操作的输入包括对等体ID(在查找特定对等体时)或能力(在搜索具有特定能力的对等体时),输出将是从discv5后端返回的ENR记录转换的multiaddrs。

这种集成使libp2p堆栈能够随后与发现的对等体形成连接和流。

ENR结构

捆绑器将使用与以太坊共识客户端相同类型的以太坊节点记录(ENR)。基本上,它们必须包含以下条目 (排除序列号和签名,这在ENR中必须存在):

  • 压缩的secp256k1公钥,33字节(secp256k1字段)。

ENR可以包含以下条目:

  • IPv4地址(ip字段)和/或IPv6地址(ip6字段)。
  • 表示本地libp2p监听端口的TCP端口(tcp字段)。
  • 表示本地discv5监听端口的UDP端口(udp字段)。

这些参数的规范可以在ENR规范中找到。

链ID字段

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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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