该文档提出了一种名为BetterHash的新的比特币挖矿协议,旨在改进现有Stratum协议的不足,通过分离工作信息和矿池支付信息的通道,提高挖矿的去中心化程度,并提升矿池的效率和安全性。该协议包括工作协议和矿池协议,分别处理挖矿任务的分配和矿工的收益分配,同时定义了详细的消息格式和通信流程。
TheBlueMatt/ bips 公开
fork 自 bitcoin/bips
betterhash
在此仓库中搜索
/
复制路径
追溯更多文件操作
追溯更多文件操作
2018年7月18日
02f7040 · 2018年7月18日
打开提交详情
822 行 (736 loc) · 83.4 KB
/
顶部
预览
代码
追溯
822 行 (736 loc) · 83.4 KB
复制原始文件
下载原始文件
你必须登录才能进行或提议更改
更多编辑选项
大纲
编辑和原始操作
BIP: XXXX
Layer: API
Title: BetterHash Mining Protocol(s)
Author: Matt Corallo
Status: Draft
Type: Standards Track
Created: 2018-03-12
License: BSD-2-Clause
| ## 目录<br>固定链接: 目录<br>- 摘要<br>- 动机<br>- 协议概述<br>- 示例网络拓扑 <br> - 大型矿场<br> - 基于广播的矿场<br> - 简单但高效的矿池架构<br>- 工作协议规范 <br> - 消息定义 <br> - PROTOCOL_SUPPORT<br> - PROTOCOL_VERSION<br> - ADDITIONAL_COINBASE_LENGTH<br> - BLOCK_TEMPLATE<br> - WINNING_NONCE<br> - TRANSACTION_DATA_REQUEST<br> - TRANSACTION_DATA<br> - COINBASE_PREFIX_POSTFIX<br> - BLOCK_TEMPLATE_HEADER<br> - WINNING_NONCE_HEADER<br> - NEW_WORK_SERVER<br> - VENDOR_MESSAGE<br>- 广播媒介上的最终工作协议<br>- 矿池协议规范 <br> - 消息定义 <br> - PROTOCOL_SUPPORT<br> - PROTOCOL_VERSION<br> - PAYOUT_INFO<br> - USER_AUTH<br> - ACCEPT_USER_AUTH<br> - REJECT_USER_AUTH<br> - DROP_USER<br> - SHARE_DIFFICULTY<br> - SHARE<br> - WEAK_BLOCK<br> - WEAK_BLOCK_STATE_RESET<br> - SHARE_ACCEPTED<br> - SHARE_REJECTED<br> - NEW_POOL_SERVER<br> - VENDOR_MESSAGE<br>- 讨论<br>- 致谢 |
我们提出了两种新的挖矿协议,以重新思考比特币网络中生成工作的方式,从而有可能大幅提高有效挖矿的去中心化程度。
本文档中的关键词“必须”,“禁止”,“需要”,“应该”,“不应该”,“推荐”,“可以”和“可选”应按照 RFC 2119 中的描述进行解释。
比特币挖矿领域存在许多鼓励中心化的压力。 维持这些压力的一个载体是通过最广泛部署的挖矿协议:Stratum。 Stratum 协议难以实施且文档记录不完善,因此矿池运营商需要构建区块模板并将其分发给客户端。 如果没有各种矿工构建区块模板,网络的抗审查性将受到威胁(例如,矿池运营商可能会利用其权力来限制协议升级的流程)。 由于服务器和客户端之间缺少密码学认证的连接,进一步加剧了对这些中心化力量的担忧,这是一个时间攻击的载体(例如,MiTM),恶意方可以通过该载体以静默方式获得对算力的控制,直到矿池运营商和/或其他矿工进行干预。 此外,Stratum 客户端的简单实施要求每个矿工都指向一个常用的 Stratum 服务器,从而导致大量单独的连接涌入每个矿池。
还存在对 Bitcoin Core API 的广泛依赖,以向矿工提供新工作并监视区块更新,这是矿池运营商必须以非凡的实施复杂性来管理的负担。 例如,除非以不受支持的方式利用 Bitcoin Core API,并且依赖 Bitcoin Core 中通知的内部排序(从而损害了性能),否则 getblocktemplate 的长轮询的实现既效率低下又不可靠。 此外,getblocktemplate 提供的数据量和性质使协议升级变得复杂,并迫使矿池服务器对交易进行哈希处理以构建区块的 Merkle 树。 更糟糕的是,由于其基于轮询的架构,阻止了解决 getblocktemplate 的延迟问题的尝试。
该提案解决这些缺陷的主要机制是通过分离工作信息和矿池支付信息所承载的通道。 当直接传递到挖矿硬件时,工作承载协议将替换 getblocktemplate 和 Stratum,而支付协议会管理所有池<->客户端通信。 这些功能的隔离为矿池参与者提供了构建区块模板的能力,这些区块模板包含他们(或他们选择的另一个矿池)选择的交易,而矿池会监督支付的分配。 在此提案之前,希望构建自己的模板的矿工必须进行单人挖矿或加入 p2pool,这两种方式的支付差异都很大。 借助 BetterHash,矿工既可以降低其支付差异,又可以构建自己的区块模板。
BetterHash 的可扩展性也值得注意,因为它的结构更依赖 Bitcoin Core 中的模板逻辑,而不是其前身 getblocktemplate。 该切换提高了性能,简化了向新共识规则的过渡,并允许更强大的模板逻辑(例如,更好的mempool逐出)。 池的整体架构也简单得多,因为可以以更低的开销添加其他服务器(因为工作由客户端管理)。 除了对代理的一流支持之外,这还有助于缓解连接泛滥。
虽然完全替换 Stratum 协议可能并非实现既定目标所必需的,但所需的架构更改提供了一个解决 Stratum 长期存在的问题的机会。 在这种情况下,协议的整体重写似乎比额外的 Stratum 扩展更有利。
工作协议以三种模式之一将新的唯一工作推送到客户端。“非最终”工作模式是不包含支付的工作,并且描述了如何构建 coinbase 交易,以及涵盖区块中交易和区块头信息的 Merkle 路径。 客户端可以使用“非最终”工作来生成支付给矿池服务器的工作。“最终”工作信息非常相似,但包括完整的支付信息,旨在发送给硬件控制器进行挖矿。“头部”工作假设客户端能够滚动 nVersion 字段以及区块头部中的 nonce 足够多次,以生成整整一秒钟的工作,从而允许他们通过每秒滚动一次头部 nTime 字段来饱和他们的工作。 对于此类客户端,无需发送 coinbase 交易或 Merkle 路径信息,从而大大简化了协议(这显然意味着“最终”工作,因为客户端无法填写 coinbase 交易中的支付信息)。
矿池协议相对简单,主要侧重于将 PAYOUT_INFO 消息发送给可用于构建“最终”工作的客户端。 客户端将 SHARE 发送到矿池作为工作量证明,从而允许他们获得合并奖励。 矿池协议具有一个可选的附加功能,适用于希望优化区块传播的矿池,即客户端将 WEAK_BLOCK 发送到矿池服务器,以便高效上传完整区块。
两种协议都以 PROTOCOL_SUPPORT/PROTOCOL_VERSION 握手开始,服务器(矿池或工作)在握手完成后立即发送初始 COINBASE_PREFIX_POSTFIX+BLOCK_TEMPLATE(对于工作服务器)或初始 PAYOUT_INFO+SHARE_DIFFICULTY(对于矿池服务器)。 此后,服务器可以根据需要将更新推送到工作/支付/难度信息,客户端通过 WINNING_NONCE 或 SHARE/WEAK_BLOCK 消息提交有效份额。
本节不具有规范性,但提供了一些预期部署场景的示例。
ASICs ASICs ASICs ASICs
\ | | / (通过 stratum 或工作协议)
农场代理/挖矿控制器
/ (通过工作协议) \ (通过矿池协议)
本地 bitcoind 远程矿池
在此设置中,挖矿控制器(例如,廉价的基于 ARM 的服务器)接受来自 ASIC 矿机的传入连接。 这些连接可以使用 stratum(为了向后兼容)或新的工作协议(可选地以头部模式,这大大简化了 ASIC 级别的逻辑 - 允许 ASIC 固件甚至跳过实现 Merkle 路径哈希/coinbase 交易构造逻辑)。 该挖矿控制器通过工作协议(从不在头部模式)从本地 bitcoind(可选地在同一设备上,为了简单起见)获取工作,并使用矿池协议连接到一个或多个远程矿池。 借助来自 bitcoind 的 BLOCK_TEMPLATE 和来自矿池服务器的 PAYOUT_INFO,挖矿控制器可以构建独特的矿工,这些矿工向矿池支付费用,但包含本地节点选择的交易以进行分发给 ASIC。 满足矿池 SHARE_DIFFICULTY 的 share_target 的区块将(仅包含 coinbase 交易、Merkle 路径和区块头)发送到矿池,以进行验证和支付,并且满足比特币网络难度的区块将发送到本地 bitcoind。 矿池可以选择请求以辅助难度发送完整区块(使用基于弱区块的压缩方案),以便矿池也能收到用于中继的完整难度区块。
待办事项
待办事项:关于只有一个矿池服务器很棒,因为你可以通过矿池协议多路复用多个客户端,因此你可以在全球范围内拥有大量 stratum 代理 + bitcoind 以提高效率,然后只拥有一个(或两个,用于冗余/GFW 问题)集中/安全托管的矿池服务器,从而大大简化了矿池的管理和安全性。
| 比特 | 客户端支持 |
| 0b00 | 客户端希望在 coinbase 中添加自己的额外数据,以及自己的支付信息。 因此,PROTOCOL_VERSION flags 中的第 6 位和第 7 位必须为 0。 |
| 0b01 | 客户端必须在 coinbase 中填写自己的额外数据,但无法填写自己的支付信息。 因此,PROTOCOL_VERSION flags 中的第 6 位必须未设置,而 PROTOCOL_VERSION flags 中的第 7 位必须设置。 |
| 0b10 | 客户端可以选择构建自己的 coinbase 交易,但正确的操作不需要它,并且无法填写自己的支付信息。 因此,PROTOCOL_VERSION flags 中的第 6 位可以为任何值,而 PROTOCOL_VERSION flags 中的第 7 位必须设置。 |
| 0b11 | 客户端不支持构建自己的 coinbase 交易,因此无法填写自己的支付信息。 因此,第 6 位和第 7 位都必须设置(即 0b11)。 |
完成版本握手后,服务器可以向客户端发送一个可选的 COINBASE_PREFIX_POSTFIX。
在初始握手以及潜在的 COINBASE_PREFIX_POSTFIX 之后,必须从服务器发送到客户端一个初始的BLOCK_TEMPLATE(或BLOCK_TEMPLATE_HEADER)消息,当服务器认为当前最佳区块发生了变化时,或者当coinbase交易的总奖励发生了重大变化时,服务器应该发送新的BLOCK_TEMPLATE (或BLOCK_TEMPLATE_HEADER)消息。
客户端通过 BLOCK_TEMPLATE 构建 coinbase 交易和候选头部,如下所示:
客户端可以在 coinbase 字段中使用任何剩余的空间(最多 100 个字节)作为额外的 nonce 空间。 由于对 BLOCK_TEMPLATE 的 coinbase_prefix 和 coinbase_postfix、COINBASE_PREFIX_POSTFIX 的 coinbase_prefix_postfix 以及矿池 PAYOUT_INFO 的 coinbase_postfix 的限制,客户端保证至少有 8 个字节可用于此目的。
一旦客户端选择了其预期的额外 nonce 大小,就可以通过将最高时间戳的 BLOCK_TEMPLATE coinbase_prefix 与最高时间戳的 COINBASE_PREFIX_POSTFIX coinbase_prefix_postfix 连接起来,然后连接任何额外的 nonce 信息,然后连接 BLOCK_TEMPLATE coinbase_postfix。
一旦客户端构建了 coinbase,就可以通过将 coinbase_tx_version 与 coinbase 字段的长度、构建的 coinbase 字段和 coinbase_tx_input_nSequence 连接起来来构建 coinbase 交易的第一部分。
如果 coinbase_tx_remaining_value 非 0(意味着未设置 PROTOCOL_SUPPORT flags 中索引为 7 的位),则 coinbase 交易中第一个输出应该向客户端支付整个剩余值。
然后,通过附加来自 BLOCK_TEMPLATE 的 coinbase_tx_outputs_to_append 和 coinbase_locktime 来完成 coinbase 交易。 请注意,不接受任何剩余输出值的客户端可以通过简单地使用 coinbase_tx_remaining_data_len 作为长度指示符来附加消息中的所有剩余数据来执行此操作。
- 提供非最终工作的服务器(即 _coinbase\_tx\_remaining\_value_ 非0)**必须不允许** _coinbase\_tx\_remaining\_data\_len_ 超过32767字节,以便为池服务器提供的任何其他输出留出空间。
- 提供非最终工作的服务器**应该**将 _coinbase\_tx\_remaining\_data\_len_ 限制为133字节(即限制 _coinbase\_tx\_outputs\_to\_append_ 的长度为128字节),以确保具有0附加coinbase长度的最大尺寸的最终BLOCK\_TEMPLATE适合单个IPv6 TCP帧内(当使用1280字节的最小所需MTU和60字节的TCP标头时,这在现代Linux上由于SACK和时间戳选项而很常见)。
- 字段从相关消息字段复制到候选标头中,仅未提供nonce和merkle根,merkle根将通过使用双SHA256对生成的coinbase交易的txid与 _merkle\_path_ 条目进行哈希计算得出,这是Bitcoin共识规则所要求的。
- 客户端**可以**更改BIP YYYY(TODO:Drak的BIP)保留的块头版本字段位,并在生成工作时每秒将块头时间戳递增1。客户端**不得**更改块头版本字段中未被BIP YYYY(TODO)保留的任何位,也不得允许块头时间戳的递增速度快于挂钟时间。
希望包含大型coinbase交易的客户端可以向服务器发送一个ADDITIONAL_COINBASE_LENGTH消息:
additional_length 字段不得考虑任何coinbase长度(根据上述规定,最多58个字节),常规支付输出(具有最多255字节scriptPubKey的单个输出),也不得考虑描述coinbase交易中输出数量的字节。
因此,即使客户端使用的额外长度超过指定的 additional_length 的328字节,工作提供者必须始终提供有效的工作(具有255字节scriptPubKey的输出总长度为266字节,加上58字节的coinbase数据,再加上表示coinbase交易中输出数量所需的最多4个额外字节)。
如果服务器没有收到ADDITIONAL_COINBASE_LENGTH消息,则服务器应该表现得好像它收到了一个 additional_length 设置为0的ADDITIONAL_COINBASE_LENGTH消息。
客户端应该首选不使用额外的coinbase交易长度。
客户端通过以下方式从BLOCK_TEMPLATE_HEADER构建候选标头:
当客户端找到nonce的组合,导致块头哈希小于相应BLOCK_TEMPLATE/BLOCK_TEMPLATE_HEADER消息中的 target_hash 时,它们必须提交一个带有完整构建的coinbase交易(对于WINNING_NONCE)和nonce填充的WINNING_NONCE/WINNING_NONCE_HEADER消息。
需要正在处理的块的完整数据的客户端(例如,用于弱块中继到池以抽查此类工作的有效性),可以使用TRANSACTION_DATA_REQUEST消息请求它。
如果工作服务器需要将客户端迁移到新主机,则它可以发送一个NEW_WORK_SERVER消息,指示客户端应连接到哪里。
实现者可以选择性地通过处理VENDOR_MESSAGE消息来支持此协议的扩展。
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常量 1 | 消息类型 |
| message_length | uint32_t | 3 字节 | 字节 {0x06, 0x00, 0x00} | 消息的剩余长度,顺序为{低位字节,次低位字节,次高位字节},其中高位字节隐式为0 |
| max_version | uint16_t | 2 字节 | Little-Endian Integer | 客户端支持的最大协议版本(目前必须为1) |
| min_version | uint16_t | 2 字节 | Little-Endian Integer | 客户端支持的最小协议版本(目前必须为1) |
| flags | uint16_t | 2 字节 | 16 个标志位 | 指示客户端支持的可选协议功能的标志(目前仅定义了0到3之间的值,包括0和3) |
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常量 2 | 消息类型 |
| message_length | uint32_t | 3 字节 | 字节 {0x25, 0x00, 0x00} | 消息的剩余长度,顺序为{低位字节,次低位字节,次高位字节},其中高位字节隐式为0 |
| version | uint16_t | 2 字节 | Little-Endian Integer | 服务器选择使用的版本(目前始终为1) |
| flags | uint16_t | 2 字节 | 16 个标志位 | 指示服务器选择使用的可选协议功能的标志。 |
| public_key | secp256k1 Public Key | 33 字节 | “压缩”secp256k1公钥 | 将用于验证剩余消息的公钥 |
Permalink: ADDITIONAL_COINBASE_LENGTH
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常量 3 | 消息类型 |
| message_length | uint32_t | 3 字节 | 字节 {0x02, 0x00, 0x00} | 消息的剩余长度,顺序为{低位字节,次低位字节,次高位字节},其中高位字节隐式为0 |
| additional_length | uint16_t | 2 字节 | Little-Endian Integer | 需要为coinbase交易保留的额外大小,请参阅消息定义以了解更多信息。 |
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常量 4 | 消息类型 |
| message_length | uint32_t | 3 字节 | Little-Endian Integer | 消息的剩余长度,顺序为{低位字节,次低位字节,次高位字节},其中高位字节隐式为0 |
| signature | secp256k1 compact signature | 64 字节 | secp256k1 ECDSA签名,编码为R、S,均为大端字节序 | SHA256(4 加上此消息中的所有剩余数据)上的签名 |
| template_timestamp | uint64_t | 8 字节 | Little-Endian Integer | 生成此模板的时间戳,自1970年1月1日以来的毫秒数 |
| target_hash | uint256 | 32 字节 | Little-Endian Integer | 工作响应应匹配的目标哈希值(编码为小端字节序,块哈希应小于该值,当解释为小端字节序时) |
| default_header_version | uint32_t | 4 字节 | Little-Endian Integer | 块头中的默认版本字段 |
| previous_block | block hash | 32 字节 | 标准双SHA256输出的块哈希 | 当前最佳块的块哈希(应在其上构建新块) |
| default_header_time | uint32_t | 4 字节 | Little-Endian Integer | 块头中的默认时间戳字段 |
| header_nbits | uint32_t | 4 字节 | Little-Endian Integer | 块头中的“nBits”字段 |
| merkle_path | 32字节哈希数组 | 1 + N*32 字节 | 1个计数字节 + N个32字节的双SHA256哈希 | 到coinbase交易的Merkle路径 |
| coinbase_tx_remaining_value | uint64_t | 8 字节 | Little-Endian Integer | 要分配给本地支付地址的剩余值 |
| coinbase_tx_version | uint32_t | 4 字节 | Little-Endian Integer | 应在coinbase交易上设置的版本字段 |
| coinbase_prefix | byte array | 1-97 字节 | 1个长度字节 + N个字节 | 应放置在coinbase交易中coinbase字段开头的数据 |
| coinbase_postfix | byte array | 1-97 字节 | 1个长度字节 + N个字节 | 应放置在coinbase交易中coinbase字段末尾的数据 |
| coinbase_tx_input_nSequence | uint32_t | 4 字节 | Little-Endian Integer | coinbase交易的输入的nSequence字段 |
| coinbase_tx_remaining_data_len | uint16_t | 2 字节 | Little-Endian Integer | 剩余coinbase交易数据的长度(即输出的长度 + coinbase交易的锁定时间) |
| coinbase_tx_output_count | Compact Size | 1-5 字节 | 使用标准Bitcoin协议紧凑尺寸编码 | 要附加到coinbase交易末尾的输出数量 |
| coinbase_tx_outputs_to_append | Transaction Outputs | N*(8 + P + M) 字节 | compactsize lengthscriptPubKey) | 应包含在coinbase交易的输出列表末尾的输出 |
| coinbase_locktime | uint32_t | 4 字节 | Little-Endian Integer | coinbase交易的锁定时间字段 |
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常量 5 | 消息类型 |
| message_length | uint32_t | 3 字节 | Little-Endian Integer | 消息的剩余长度,顺序为{低位字节,次低位字节,次高位字节},其中高位字节隐式为0 |
| template_timestamp | uint64_t | 8 字节 | Little-Endian Integer | 从用于生成此工作的BLOCK_TEMPLATE消息复制的模板时间戳字段 |
| header_version | uint32_t | 4 字节 | Little-Endian Integer | 块头中的版本字段 |
| header_timestamp | uint32_t | 4 字节 | Little-Endian Integer | 块头中的时间戳字段 |
| header_nonce | uint32_t | 4 字节 | Little-Endian Integer | 块头中的nonce字段 |
| user_tag | bytes | 1-256 字节 | 长度字节,后跟N个字节 | 可以填充的自由标签,用于统计目的 |
| coinbase_tx_length | uin32_t | 4 字节 | Little-Endian Integer | coinbase交易的长度(以及此消息的其余部分) |
| coinbase_tx | Transaction | 47+ 字节 | 像任何其他Bitcoin交易一样 | 完全形成的编码的coinbase交易 |
Permalink: TRANSACTION_DATA_REQUEST
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常量 6 | 消息类型 |
| message_length | uint32_t | 3 字节 | 字节 {0x08, 0x00, 0x00} | 消息的剩余长度,顺序为{低位字节,次低位字节,次高位字节},其中高位字节隐式为0 |
| template_timestamp | uint64_t | 8 字节 | Little-Endian Integer | 客户端需要完整交易的BLOCK_TEMPLATE消息的模板时间戳字段 |
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常量 7 | 消息类型 |
| message_length | uint32_t | 3 字节 | Little-Endian Integer | 消息的剩余长度,顺序为{低位字节,次低位字节,次高位字节},其中高位字节隐式为0 |
| signature | secp256k1 compact signature | 64 字节 | secp256k1 ECDSA签名,编码为R、S,均为大端字节序 | SHA256(7 加上此消息中的所有剩余数据)上的签名 |
| template_timestamp | uint64_t | 8 字节 | Little-Endian Integer | 服务器为其提供完整交易的TRANSACTION_DATA_REQUEST消息的模板时间戳字段 |
| previous_header | Block header | 80 字节 | 序列化的Bitcoin块头 | 此工作基于的先前块的标头 |
| extra_block_data | bytes | 4+ 字节 | 4字节小端长度,后跟额外数据 | 块验证可能需要的不透明二进制数据 |
| tx_count | uint32_t | 4 字节 | Little-Endian Integer | 后续交易的数量(应该是候选块中的交易总数 - coinbase交易的1个) |
| transactions | opaque binary data blobs 列表 | N * (4 + tx len) 字节 | 4字节交易数据长度,后跟不透明的交易数据二进制blob,每个交易重复 | 交易本身 |
Permalink: COINBASE_PREFIX_POSTFIX
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常量 8 | 消息类型 |
| message_length | uint32_t | 3 字节 | Little-Endian Integer | 消息的剩余长度,顺序为{低位字节,次低位字节,次高位字节},其中高位字节隐式为0 |
| signature | secp256k1 compact signature | 64 字节 | secp256k1 ECDSA签名,编码为R、S,均为大端字节序 | SHA256(8 加上此消息中的所有剩余数据)上的签名 |
| message_timestamp | uint64_t | 8 字节 | Little-Endian Integer | 生成此消息的时间戳,自1970年1月1日以来的毫秒数 |
| coinbase_prefix_postfix | bytes | 1-97 字节 | 1个字节长度,后跟N个字节 | 应添加到BLOCK_TEMPLATE中提供的coinbase前缀的后缀,在客户端添加任何其他数据推送之前 |
Permalink: BLOCK_TEMPLATE_HEADER
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常量 9 | 消息类型 |
| message_length | uint32_t | 3 字节 | 字节 {0xbc, 0x00, 0x00} | 消息的剩余长度,顺序为{低位字节,次低位字节,次高位字节},其中高位字节隐式为0 |
| signature | secp256k1 compact signature | 64 字节 | secp256k1 ECDSA签名,编码为R、S,均为大端字节序 | SHA256(9 加上此消息中的所有剩余数据)上的签名 |
| template_timestamp | uint64_t | 8 字节 | Little-Endian Integer | 生成此模板的时间戳,自1970年1月1日以来的毫秒数 |
| template_variant | uint64_t | 8 字节 | Little-Endian Integer | 一个唯一的ID,用于标识此BLOCK_TEMPLATE_HEADER,以包含在相应的WINNIN_NONCE_HEADER消息中 |
| target_hash | uint256 | 32 字节 | Little-Endian Integer | 工作响应应匹配的目标哈希值(编码为小端字节序,块哈希应小于该值,当解释为小端字节序时) |
| default_header_version | uint32_t | 4 字节 | Little-Endian Integer | 块头中的默认版本字段 |
| previous_block | block hash | 32 字节 | 标准双SHA256输出的块哈希 | 当前最佳块的块哈希(应在其上构建新块) |
| merkle_root | uint256 | 32 字节 | 双 SHA256 哈希 | 块头中的merkle根字段 |
| default_header_time | uint32_t | 4 字节 | Little-Endian Integer | 块头中的默认时间戳字段 |
| header_nbits | uint32_t | 4 字节 | Little-Endian Integer | 块头中的“nBits”字段 |
Permalink: WINNING_NONCE_HEADER
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常量 10 | 消息类型 |
| message_length | uint32_t | 3 字节 | Little-Endian Integer | 消息的剩余长度,顺序为{低位字节,次低位字节,次高位字节},其中高位字节隐式为0 |
| template_timestamp | uint64_t | 8 字节 | Little-Endian Integer | 从用于生成此工作的BLOCK_TEMPLATE消息复制的模板时间戳字段 |
| template_variant | uint64_t | 8 字节 | Little-Endian Integer | 对应的BLOCK_TEMPLATE_HEADER消息中的 template_variant |
| header_version | uint32_t | 4 字节 | Little-Endian Integer | 块头中的版本字段 |
| header_timestamp | uint32_t | 4 字节 | Little-Endian Integer | 块头中的时间戳字段 |
| header_nonce | uint32_t | 4 字节 | Little-Endian Integer | 块头中的nonce字段 |
| user_tag | bytes | 1-256 字节 | 长度字节,后跟N个字节 | 可以填充的自由标签,用于统计目的 |
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常量 11 | 消息类型 |
| message_length | uint32_t | 3 字节 | Little-Endian Integer | 消息的剩余长度,顺序为{低位字节,次低位字节,次高位字节},其中高位字节隐式为0 |
| signature | secp256k1 compact signature | 64 字节 | secp256k1 ECDSA签名,编码为R、S,均为大端字节序 | SHA256(11 加上此消息中的所有剩余数据)上的签名 |
| new_host_port | String | 1 长度字节 + 最多 255 字节的字符串 | 字符串编码为长度字节,后跟 host:port | 用于此工作提供程序的新连接主机 |
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常量 12 | 消息类型 |
| message_length | uint32_t | 3 字节 | Little-Endian Integer | 消息的剩余长度,顺序为{低位字节,次低位字节,次高位字节},其中高位字节隐式为0 |
| flags | uint16_t | 1 byte1 | 8 个标志位 | 指示服务器选择使用的可选消息功能的标志。 |
| signature | secp256k1 compact signature | 0 或 64 字节 | secp256k1 ECDSA签名,编码为R、S,均为大端字节序 | SHA256(12 加上此消息中的所有剩余数据)上的签名,仅当 flags 的位 0 为 1 时存在。 |
| vendor | String | 1 长度字节 + 最多 255 字节的字符串 | 字符串编码为长度字节,后跟 vendor | 消息的供应商描述符 |
| message | bytes | N 字节,直至消息结尾 | 以供应商定义的方式编码的消息 | 供应商处理的消息本身 |
| 值 | 名称 | 含义 |
| 1 | STALE_PREVBLOCK | SHARE/WEAK_BLOCK 基于的先前区块与矿池知道的最高有效 total work 区块的 total work 不相同(或者矿池尚未收到给定的先前 block header)。 |
| 2 | BAD_HASH | header 未哈希到足够低的目标,以满足当前的 share_target 或 weak_block_target |
| 3 | DUPLICATE | 给定的 work 已经提交(请注意,对于已发送等效 SHARE 的 WEAK_BLOCK,矿池不得发送 REJECT_SHARE 消息) |
| 4 | BAD_PAYOUT_INFO | 给定的 SHARE/WEAK_BLOCK 未正确使用提供的 PAYOUT_INFO 和 ACCEPT_USER_AUTH |
| 5 | BAD_WORK | 给定的 SHARE/WEAK_BLOCK 由于某些其他原因而无效,最可能是 WEAK_BLOCK 未通过有效性检查。 |
| 字段名称 | 类型 | 大小 | 编码 | 目的 |
| message_type | 字节 | 1 字节 | 常数 1 | 消息类型 |
| message_length | uint32_t | 3 字节 | 字节 {0x06, 0x00, 0x00} | 消息的剩余长度,顺序为 {低位字节、次低位字节、倒数第二高位字节},高位字节隐式为 0 |
| max_version | uint16_t | 2 字节 | 小端整数 | 客户端支持的最高协议版本(当前必须为 1) |
| min_version | uint16_t | 2 字节 | 小端整数 | 客户端支持的最低协议版本(当前必须为 1) |
| flags | uint16_t | 2 字节 | 16 个标志位 | 指示客户端支持的可选协议功能的标志 |
| 字段名称 | 类型 | 大小 | 编码 | 目的 |
| message_type | 字节 | 1 字节 | 常数 2 | 消息类型 |
| message_length | uint32_t | 3 字节 | 字节 {0x25, 0x00, 0x00} | 消息的剩余长度,顺序为 {低位字节、次低位字节、倒数第二高位字节},高位字节隐式为 0 |
| version | uint16_t | 2 字节 | 小端整数 | 服务器已选择使用的版本(目前始终为 1) |
| flags | uint16_t | 2 字节 | 16 个标志位 | 指示服务器选择使用的可选协议功能的标志。 |
| public_key | secp256k1 公钥 | 33 字节 | “压缩” secp256k1 公钥 | 将用于验证剩余消息的公钥 |
| Permalink: PAYOUT_INFO | |||||
|---|---|---|---|---|---|
| 字段名 | 类型 | 大小 | 编码 | 目的 | |
| message_type | byte | 1 字节 | 常数 13 | 消息类型 | |
| message_length | uint32_t | 3 字节 | Little-Endian Integer(小端整数) | 消息的剩余长度,顺序为 {最低有效字节,次低有效字节,次高有效字节},最高有效字节隐式为 0 | |
| signature | secp256k1 compact signature(secp256k1 紧凑签名) | 64 字节 | secp256k1 ECDSA signature(secp256k1 ECDSA 签名)编码为 R, S,均为 big endian(大端) | SHA256(13 后面跟此消息中的所有剩余数据) 上的签名 | |
| message_timestamp | uint64_t | 8 字节 | Little-Endian Integer(小端整数) | 生成此消息的时间戳,自 1970 年 1 月 1 日以来的毫秒数 | |
| remaining_payout_script | Script(脚本) | 1-256 字节 | 1 个长度字节,后跟 N 个字节的 script(脚本) | 应该接收 coinbase 交易奖励的所有剩余值的 scriptPubKey | |
| appended_outputs_count | uint8_t | 1 字节 | Integer(整数) | 应该在 coinbase 交易中的 self 和 remaining payout outputs 之后出现的 output(输出)的数量(最多 250 个) | |
| appended_outputs | Transaction Outputs(交易输出) | N*(8 + 1 + M) 字节 | 1 字节 scriptPubKey(脚本公钥)长度,最多 252scriptPubKey) | output(输出)本身 |
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常数 14 | 消息类型 |
| message_length | uint32_t | 3 字节 | Little-Endian Integer(小端整数) | 消息的剩余长度,顺序为 {最低有效字节,次低有效字节,次高有效字节},最高有效字节隐式为 0 |
| suggested_target | uint256 | 32 字节 | Little-Endian Integer(小端整数) | 用户建议的 share_target |
| minimum_target | uint256 | 32 字节 | Little-Endian Integer(小端整数) | 用户支持的最小 share_target |
| user_id | String(字符串) | 1-256 字节 | 1 字节长度,后跟最多 255 字节的字符 | 用户的标识字符串 |
| user_auth | String(字符串) | 1-256 字节 | 1 字节长度,后跟最多 255 字节的字符 | 用户必须提供的任何其他数据以验证身份。 |
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常数 15 | 消息类型 |
| message_length | uint32_t | 3 字节 | Little-Endian Integer(小端整数) | 消息的剩余长度,顺序为 {最低有效字节,次低有效字节,次高有效字节},最高有效字节隐式为 0 |
| signature | secp256k1 compact signature(secp256k1 紧凑签名) | 64 字节 | secp256k1 ECDSA signature(secp256k1 ECDSA 签名)编码为 R, S,均为 big endian(大端) | SHA256(15 后面跟此消息中的所有剩余数据) 上的签名 |
| user_id | String(字符串) | 1-256 字节 | 1 字节长度,后跟最多 255 字节的字符 | 用户的标识字符串 |
| message_timestamp | uint64_t | 8 字节 | Little-Endian Integer(小端整数) | 生成此消息的时间戳,自 1970 年 1 月 1 日以来的毫秒数 |
| coinbase_postfix | bytes(字节) | 1-51 字节 | 长度字节,后跟 N 个字节 | 必须出现在 coinbase 字段末尾的数据 |
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常数 16 | 消息类型 |
| message_length | uint32_t | 3 字节 | Little-Endian Integer(小端整数) | 消息的剩余长度,顺序为 {最低有效字节,次低有效字节,次高有效字节},最高有效字节隐式为 0 |
| user_id | String(字符串) | 1-256 字节 | 1 字节长度,后跟最多 255 字节的字符 | 用户的标识字符串 |
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常数 17 | 消息类型 |
| message_length | uint32_t | 3 字节 | Little-Endian Integer(小端整数) | 消息的剩余长度,顺序为 {最低有效字节,次低有效字节,次高有效字节},最高有效字节隐式为 0 |
| user_id | String(字符串) | 1-256 字节 | 1 字节长度,后跟最多 255 字节的字符 | 用户的标识字符串 |
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常数 18 | 消息类型 |
| message_length | uint32_t | 3 字节 | Little-Endian Integer(小端整数) | 消息的剩余长度,顺序为 {最低有效字节,次低有效字节,次高有效字节},最高有效字节隐式为 0 |
| user_id | String(字符串) | 1-256 字节 | 1 字节长度,后跟最多 255 字节的字符 | 应该更新其 difficulty(难度)的用户的用户标识字符串 |
| message_timestamp | uint64_t | 8 字节 | Little-Endian Integer(小端整数) | 生成此消息的时间戳,自 1970 年 1 月 1 日以来的毫秒数 |
| share_target | uint256 | 32 字节 | Little-Endian Integer(小端整数) | 用于与 share(份额)的 block hash(区块哈希)进行比较的 share target |
| weak_block_target | uint256 | 32 字节 | Little-Endian Integer(小端整数) | 用于与必须提交的 share(份额)的 block hash(区块哈希)进行比较的 target |
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常数 19 | 消息类型 |
| message_length | uint32_t | 3 字节 | Little-Endian Integer(小端整数) | 消息的剩余长度,顺序为 {最低有效字节,次低有效字节,次高有效字节},最高有效字节隐式为 0 |
| header_version | uint32_t | 4 字节 | Little-Endian Integer(小端整数) | 区块头中的 version 字段 |
| previous_block | block hash(区块哈希) | 32 字节 | Block Hash(区块哈希)作为标准 double-SHA256(双 SHA256) output(输出) | 构建此 share(份额)的先前区块的 block hash(区块哈希) |
| header_timestamp | uint32_t | 4 字节 | Little-Endian Integer(小端整数) | 区块头中的 timestamp 字段 |
| header_nbits | uint32_t | 4 字节 | Little-Endian Integer(小端整数) | 区块头中的 "nBits" 字段 |
| header_nonce | uint32_t | 4 字节 | Little-Endian Integer(小端整数) | 区块头中的 nonce 字段 |
| merkle_path | array of 32-byte hashes(32 字节哈希数组) | 1 + N*32 字节 | 1 个计数字节 + N 个 32 字节 double-SHA256(双 SHA256)哈希 | 指向 coinbase 交易的 Merkle 路径 |
| coinbase_tx_length | uin32_t | 4 字节 | Little-Endian Integer(小端整数) | coinbase 交易的长度 |
| coinbase_tx | Transaction(交易) | 47+ 字节 | 像任何其他 Bitcoin 交易 | 完全形成的编码 coinbase 交易 |
| user_tag_1 | bytes(字节) | 1-256 字节 | 长度字节,后跟 N 个字节 | 可以填充以用于统计目的的自由标签 |
| user_tag_2 | bytes(字节) | 1-256 字节 | 长度字节,后跟 N 个字节 | 可以填充以用于统计目的的自由标签 |
| previous_header | Block header(区块头) | 0 或 80 字节 | 序列化的 Bitcoin Block Header(比特币区块头) | 此 work(工作)所基于的先前区块的头(仅在新时包含) |
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常数 20 | 消息类型 |
| message_length | uint32_t | 3 字节 | Little-Endian Integer(小端整数) | 消息的剩余长度,顺序为 {最低有效字节,次低有效字节,次高有效字节},最高有效字节隐式为 0 |
| header_version | uint32_t | 4 字节 | Little-Endian Integer(小端整数) | 区块头中的 version 字段 |
| previous_block | block hash(区块哈希) | 32 字节 | Block Hash(区块哈希)作为标准 double-SHA256(双 SHA256) output(输出) | 构建此 share(份额)的先前区块的 block hash(区块哈希) |
| header_timestamp | uint32_t | 4 字节 | Little-Endian Integer(小端整数) | 区块头中的 timestamp 字段 |
| header_nbits | uint32_t | 4 字节 | Little-Endian Integer(小端整数) | 区块头中的 "nBits" 字段 |
| header_nonce | uint32_t | 4 字节 | Little-Endian Integer(小端整数) | 区块头中的 nonce 字段 |
| merkle_path | array of 32-byte hashes(32 字节哈希数组) | 1 + N*32 字节 | 1 个计数字节 + N 个 32 字节 double-SHA256(双 SHA256)哈希 | 指向 coinbase 交易的 Merkle 路径 |
| user_tag_1 | bytes(字节) | 1-256 字节 | 长度字节,后跟 N 个字节 | 可以填充以用于统计目的的自由标签 |
| user_tag_2 | bytes(字节) | 1-256 字节 | 长度字节,后跟 N 个字节 | 可以填充以用于统计目的的自由标签 |
| extra_block_data | bytes(字节) | 4+ 字节 | 4 字节 Little-Endian(小端)长度,后跟额外数据 | 块验证可能需要的 Opaque(不透明)二进制数据 |
| txn_count | uint32_t | 4 字节 | Little-Endian Integer(小端整数) | 此块中的交易数量(如下) |
| txn_list | transaction pointer list(交易指针列表) | N 字节 | 指向上一个 WEAK_BLOCK 中的交易的 2 字节指针,或 0 表示完整交易 | 此块中包含的交易 |
Permalink: WEAK_BLOCK_STATE_RESET
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常数 21 | 消息类型 |
| message_length | uint32_t | 3 字节 | 字节 {0x00, 0x00, 0x00} | 消息的剩余长度,顺序为 {最低有效字节,次低有效字节,次高有效字节},最高有效字节隐式为 0 |
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常数 22 | 消息类型 |
| message_length | uint32_t | 3 字节 | Little-Endian Integer(小端整数) | 消息的剩余长度,顺序为 {最低有效字节,次低有效字节,次高有效字节},最高有效字节隐式为 0 |
| user_tag_1 | bytes(字节) | 1-256 字节 | 长度字节,后跟 N 个字节 | 相应 SHARE 或 WEAK_BLOCK 消息中提供的 user_tag_1 |
| user_tag_2 | bytes(字节) | 1-256 字节 | 长度字节,后跟 N 个字节 | 相应 SHARE 或 WEAK_BLOCK 消息中提供的 user_tag_2 |
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常数 23 | 消息类型 |
| message_length | uint32_t | 3 字节 | Little-Endian Integer(小端整数) | 消息的剩余长度,顺序为 {最低有效字节,次低有效字节,次高有效字节},最高有效字节隐式为 0 |
| reason | uint8_t | 1 字节 | 从可用原因中选择的值 | 原因指示器。 有关已定义原因的列表,请参见消息处理说明 |
| user_tag_1 | bytes(字节) | 1-256 字节 | 长度字节,后跟 N 个字节 | 相应 SHARE 或 WEAK_BLOCK 消息中提供的 user_tag_1 |
| user_tag_2 | bytes(字节) | 1-256 字节 | 长度字节,后跟 N 个字节 | 相应 SHARE 或 WEAK_BLOCK 消息中提供的 user_tag_2 |
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常数 11 | 消息类型 |
| message_length | uint32_t | 3 字节 | Little-Endian Integer(小端整数) | 消息的剩余长度,顺序为 {最低有效字节,次低有效字节,次高有效字节},最高有效字节隐式为 0 |
| signature | secp256k1 compact signature(secp256k1 紧凑签名) | 64 字节 | secp256k1 ECDSA signature(secp256k1 ECDSA 签名)编码为 R, S,均为 big endian(大端) | SHA256(11 后面跟此消息中的所有剩余数据) 上的签名 |
| new_host_port | String(字符串) | 1 长度字节 + 最多 255 字节的字符串 | 编码为长度字节,后跟 host:port 的字符串 | 要连接到此池的新主机 |
| 字段名 | 类型 | 大小 | 编码 | 目的 |
| message_type | byte | 1 字节 | 常数 12 | 消息类型 |
| message_length | uint32_t | 3 字节 | Little-Endian Integer(小端整数) | 消息的剩余长度,顺序为 {最低有效字节,次低有效字节,次高有效字节},最高有效字节隐式为 0 |
| flags | uint16_t | 1 byte1 | 8 flag bits(8 个标志位) | 指示服务器选择使用的可选消息功能的标志。 |
| signature | secp256k1 compact signature(secp256k1 紧凑签名) | 0 或 64 字节 | secp256k1 ECDSA signature(secp256k1 ECDSA 签名)编码为 R, S,均为 big endian(大端) | SHA256(12 后面跟此消息中的所有剩余数据) 上的签名,仅当 flags 的 bit 0 为 1 时才存在。 |
| vendor | String(字符串) | 1 长度字节 + 最多 255 字节的字符串 | 编码为长度字节的字符串,后跟 vendor | 消息的 Vendor 描述符 |
| message | bytes(字节) | N 字节,直到消息结尾 | 以 Vendor 定义的方式编码的消息 | Vendor 处理的消息本身 |
为什么使用 TCP? UDP 不是更适合低延迟中继吗?因为时间敏感数据包被设计为适合 1 个 TCP 帧,所以使用 TCP 可以显著降低可靠传输和 NAT 穿越的复杂性,同时仍提供低延迟交付。 对于使用广播介质减少带宽,显然 TCP 不适用,但是通过未拥塞的 LAN 和冗余数据包应该可以轻松完成可靠的交付。 具有严重数据包丢失问题的用户应考虑使用更本地的池服务器,并通过 FEC 生成代理(例如 QUIC)进行连接,尽管为了降低复杂性,本规范中有意将此类协议的使用保留为未定义。为什么增加 weak blocks(弱块)中继的复杂性?由于基于 PPS 的池已成为出于商业原因的常见做法,因此某些池非常关心能够优化支付给他们的块的中继。 因此,为了避免池严重依赖其客户端来执行自己的仔细优化,可以使客户端有效地将部分块中继到池,从而进一步允许池抽查客户端正在哈希处理的工作的有效性。 不需要基于 weak-block(弱块)的中继的池可以选择简单地将 weak_block_difficulty 设置为当前的块难度,并在每个 WEAK_BLOCK 之后发送 WEAK_BLOCK_STATE_RESET 消息,这意味着客户端几乎没有机会中继压缩的弱块。对于当今的矿工,预期的 UX 差异是什么?这在很大程度上取决于挖掘硬件打算支持的复杂程度。 对于同时支持两者的硬件,矿工现在可以有两组主机字段,而不是当前的配置,其中将挖掘固件定向到连接到一个或多个池以从其获取 work(工作)和 payout(支付)信息,一个用于 work(工作),一个用于池。 对于更简单的挖掘固件,他们可以选择仅支持 work(工作)提供商,这些提供商可以指向一个代理,该代理将来自 work(工作)提供商的 work(工作)与来自池的 payout(支付)信息合并到 BLOCK_TEMPLATE 的一个流中。 此类代理服务器的示例位于作者的 GitHub上,该 GitHub 还支持充当现有硬件连接到的 stratum 服务器。难道许多矿工不太可能简单地选择连接到同一运营商以获取 work(工作)和池 payout(支付)信息吗?实际上,这是可能的,但是降低矿工获取 work(工作)信息的转换成本相对于当今的设置而言已经是一个显着优势。 此外,将协议分为两部分可以大大简化池的操作 - 允许池运营商简单地将 Bitcoin Core 用作 work(工作)服务器可以简化构建池服务器的工作。没有可靠时间源的挖掘设备呢?请注意,尽管时间戳在协议中使用,但是客户端可以在完全不知道当前时间的情况下完全兼容。 尽管服务器必须提供它,但是如果所有下游客户端仅将 timestamp(时间戳)字段用作不断增加的 ID,则它们可以正常运行(尽管通常应避免这样做,以免可能生成永远无法覆盖的更新)。为什么某些消息已签名而另一些消息未签名?通常,重复的每个客户端消息都保持未签名状态,以确保在池和 work(工作)服务器上的低开销。 假设无论考虑什么协议功能,攻击者都能够阻止所有 share(份额)到达任何池/work(工作)服务器,强制用户退回到辅助配置的池/work(工作)服务器,并了解用户的算力和行为,因此没有努力提供针对此类攻击的安全性。 相反,仅针对以下攻击提供安全性:攻击者能够将已经在线的算力重定向到新的池/payout(支付)信息/work(工作)。 值得注意的是,SHARE_ACCEPTED/SHARE_REJECTED 消息未签名,从而允许攻击者通过删除 share(份额)来默默地降低用户的算力,同时通知用户它们已被接受,但是 MITM 攻击者即使在签名的 SHARE_ACCEPTED 消息的情况下也可以这样做,只是检测速度略快,而不是只能在事后通过协调份额计数来完成。 此外,REJECT_USER_AUTH/DROP 用户消息未签名,因为它们的行为方式与攻击者可以简单地删除与该用户关联的消息和/或重置 TCP 连接的方式非常相似。为什么 WEAK_BLOCK 消息是 per-connection(按连接)而不是 per-user(按用户),以及为什么池代理不能接受客户端生成的工作?这使池服务器的实现更加轻巧和简单。 如果允许代理服务器为多个生成 work(工作)的客户端提供服务,则它们必须按原样转发 WEAK_BLOCK 消息,以避免客户端彼此干扰 weak blocks(弱块)的效率,从而使池服务器复杂化并增加其资源使用量。 鉴于此类代理服务器的唯一重要用例是向后兼容的池运营 stratum 服务器,因此启用此类用例几乎没有优势。
感谢(按姓氏字母顺序排列)Jan Capek、Ivy Evans、James Hilliard、Greg Maxwell、Pavel Moravec 和 Alex Petrov 进行了多轮审查和反馈。 同时也感谢 Jonathan Cross、GitHub 上的 erikarvstedt、Jonas Nick、Jimmy Song 和其他一些人为副本编辑和文本建议提供的帮助。
- 原文链接: github.com/TheBlueMatt/b...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!