Monero 的 PoW 算法 RandomX 通过随机代码执行和大内存访问来抵抗 ASIC 中心化。
Monero 的工作量证明被称为 RandomX。
Monero 并不要求矿工反复运行同一个微小的哈希函数。它要求矿工在虚拟机上运行一个小型随机程序,在此过程中大量访问内存,然后对结果进行哈希。
比特币的工作量证明适合专用芯片,因为工作量从不改变。RandomX 的设计则相反。它试图让高效的挖矿看起来尽可能像普通的 CPU 工作负载。
以下是最简短的实用总结:
有趣的部分不是最终的“是/否”规则。每个工作量证明系统都有这个。有趣的部分在于 Monero 如何让每次哈希尝试在普通 CPU 擅长而定制芯片讨厌的方面变得昂贵。
如果你的工作量证明只是“在新输入上反复运行这个固定函数,直到得到幸运输出”,硬件设计师的任务很明确:构建能够以最低成本和最快速度运行该精确函数的硅片。
这就是比特币在 SHA-256 ASIC 上发生的事情。
Monero 不想走那条路。早在 RandomX 之前,该项目就明确指出专用挖矿硬件会造成中心化压力。更少的制造商变得更加重要。大型矿场变得更加重要。普通用户变得不那么重要。
Monero 早期的答案是 CryptoNight 系列。后来,在 2019 年末,Monero 切换到了 RandomX,其发布说明将其描述为一种新的工作量证明,“基于随机指令,适用于 CPU。”
因此,设计目标从 让内存变得重要 转变为 让整个 CPU 变得重要。
RandomX 从一个观察出发:CPU 不仅仅是算术盒子。它们是灵活的机器,专为运行不断变化的代码并同时处理大量硬件特性而构建。
现代 CPU 拥有:
普通的密码学哈希函数并没有充分利用这些多样性。它们大多只是通过固定的管道推送数据。
RandomX 试图将工作量证明与这些更广泛的 CPU 优势绑定。其设计文档指出,工作量必须是 动态的。这意味着矿工不仅是在输入新数据,还在获取要运行的新代码。
这就是 RandomX 基于 随机代码执行 的原因。
在 Monero 层面,RandomX 接收两个重要输入:
KH对于 Monero,K 来自一个较早的区块哈希,称为 密钥区块。RandomX 参考 README 建议每 2048 个区块 更改一次此密钥,并带有 64 个区块的延迟,而 Monero 正是这样实现的。
这个细节很重要,因为矿工 不会 为每个 nonce 重建庞大的共享内存结构。他们只在密钥更改时重建一次,大约每 2.8 天一次。
H 是候选区块的哈希数据块,包含选定的 nonce。这部分是矿工反复更改的内容。
因此,你可以这样理解 Monero 挖矿:
K 为你提供一个中期环境H 为你提供一个每次尝试的输入环境变化缓慢。尝试不断变化。
RandomX 首先对密钥 K 运行 Argon2d。
Argon2d 更常被称为密码哈希和密钥派生函数。它在这里有用的原因与在那里相同:它是 内存硬 的。它迫使机器以难以欺骗的方式访问大量内存。
在默认的 RandomX 参数下,这会生成一个 256 MiB 的缓存。
该缓存是 RandomX 中两个大型内存结构中较小的一个。它不是矿工想要直接用于最大速度的结构。它是用来构建更大结构的基础。
从该 256 MiB 缓存出发,RandomX 构建 数据集。
默认数据集大小为:
2,147,483,648 字节 基本大小33,554,368 字节 额外大小总共约为 2080 MiB,略大于 2 GiB。
这种看起来奇怪的尺寸是有意为之。它足够大,以至于会溢出片上内存进入 DRAM,而额外的非 2 的幂次尾部使得硬件设计者的工作更加麻烦。
数据集在哈希过程中是只读的。RandomX 利用它来强制产生常规的 DRAM 流量。设计文档指出,每次程序迭代会读取一个 64 字节的数据集项,并且在整个哈希结果过程中,这变成 16,384 次数据集读取。
这给了 RandomX 一个主要瓶颈:内存访问,而不仅仅是计算。
现在 RandomX 转向每个哈希的输入 H。
它使用 Blake2b 计算 Hash512(H)。这个 64 字节的结果用于种子化一个基于 AES 的生成器,该生成器填充 暂存器。
暂存器是虚拟机的工作内存。与大型数据集不同,它应该留在 CPU 缓存中,而不是 DRAM。
其默认大小为 2 MiB,拆分为模拟 CPU 缓存层级:
16 KiB L1256 KiB L22 MiB L3这是 RandomX 最聪明的部分之一。它同时使用了两个截然不同的内存结构:
这样既可以给内存子系统施压,也可以给 CPU 核心施压。
暂存器准备好后,RandomX 为其虚拟机生成一个程序。
这不是 C 程序或 JavaScript 程序。它是一个紧凑的虚拟机程序,拥有自己的指令集。
两个细节非常重要:
第二个选择意义重大。这意味着 RandomX 可以通过用随机字节填充缓冲区来生成程序。没有缓慢的解析器,也没有复杂的语法检查。
每个程序包含 256 条指令。
这些指令被选择为看起来像是真实 CPU 擅长的工作:
浮点部分并非装饰。RandomX 使用 IEEE 754 双精度运算,包括除法和平方根,并使用了所有四种标准舍入模式。这使得虚拟机更难被压缩成一个小型“主要是整数”的定制设计。
虚拟机以 2048 次迭代 的循环执行包含 256 条指令的程序。
在每次迭代中,它:
设计文档指出,平均每次迭代从内存读取约 504 字节,并写入约 256 字节。
这个数字暗示了 RandomX 实际在做什么。它不仅仅是“带有额外步骤的哈希”。它试图表现得像杂乱、混合的真实软件。
分支很容易被忽视,但它们很重要。
如果代码完全是直线型的,专用硬件可以更容易地优化掉更多部分。分支使得静态简化变得更加困难。
RandomX 谨慎地使用分支。分支被触发的概率约为 1/256,并且设计有意让这些分支通常被预测为“未触发”。这意味着在大多数情况下,它们对 CPU 来说是廉价的,同时仍然阻碍过度优化的硬件捷径。
重点不在于 RandomX 找到了某种神奇的分支预测技巧。它没有。重点是,即便只有很少的实际控制流,也能让工作负载看起来更像真实代码,而不是一个干净的硬件流水线。
单个随机程序是不够的。
如果矿工只需要运行一个随机程序,不诚实的矿工可以尝试先检查程序并跳过“坏”的程序。或者定制芯片设计者可以只支持对其硬件容易的程序子集。
RandomX 通过将 8 个程序 链式连接来阻止这一点。
一个程序的输出状态成为下一个程序的种子。因此,一旦开始,你无法预先知道整个链。你要么完成整个链,要么丢弃你已经付出的工作。
这是 RandomX 最简洁的设计之一。它将“也许我只做容易的工作”变成了一种糟糕的策略。
在 8 个程序中的最后一个完成后,RandomX 仍需将所有机器状态转化为一个最终的摘要。
它做两件事:
这个最终的 256 位结果就是 RandomX 的输出。
在 Monero 层面,矿工检查该结果是否低于当前的难度目标。如果是,则区块获胜。如果不是,矿工更改 nonce 并再次尝试。
RandomX 有两种模式:
2080 MiB 数据集256 MiB 缓存并动态计算数据集项两种模式给出 相同的答案。
验证必须与挖矿一致,但这两种模式的工作成本并不相同。
快速模式用于挖矿。轻量模式用于验证。参考 README 明确说明了这一点,这种分离是 RandomX 最优秀的设计选择之一。
如果每个验证者都需要超过 2 GiB 内存才能检查一个工作量证明,那会很麻烦。如果轻量模式便宜到可以竞争性地挖矿,那也很糟糕。RandomX 试图保持在中间位置:验证应该是可行的,但作为挖矿捷径不具有吸引力。
设计文档明确指出,轻量模式应该在内存-时间权衡上处于劣势。通俗地说,如果你节省了内存,你就应该用额外的工作来补偿这一优势。
“CPU 友好”听起来可能像营销话术,因此值得具体说明。
RandomX 有利于 CPU,因为它依赖于优秀 CPU 已经具备的特性:
参考实现甚至包含针对 x86-64、ARM64 和 RISCV64 的 JIT 编译器,因此虚拟机程序可以即时翻译成本机机器码,而不是逐条解释执行。
这并不意味着 ASIC 永远不可能。没有任何诚实的东西应该声称这一点。它所做的只是让定制芯片的工作变得更加棘手。一个优秀的 RandomX ASIC 开始看起来不像一个整洁的固定功能哈希引擎,而更像“构建一个带有大量内存的奇怪且昂贵的 CPU”。这缩小了优势差距。
RandomX 主要关心的不是原始速度。而是 硬件经济学。
Monero 希望工作量证明能够:
这就是为什么如果你期待一个正常的挖矿哈希,RandomX 看起来如此奇怪。
它并不试图在比特币的意义上优雅。它试图在专业化方面变得别扭。
如果比特币挖矿是“永远运行这一台小机器”,那么 Monero 挖矿就是“不断生成类似 CPU 的小型工作负载并承受内存流量”。
这是我所知道的最简短且仍然正确的心理模型。
在底层,RandomX 是一系列精心选择的堆叠:
将这些组合起来,你就得到了今天的 Monero 工作量证明。
它并不简单。但重点很简单:让挖矿看起来像通用计算,从而让专用硬件几乎没有空间在网络中占据主导。
- 原文链接: blog.alcazarsec.com/tech...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!
作者暂未设置收款二维码