XMR门罗币新算法:RandomX设计说明(翻译,上篇)

(原文地址:https://github.com/tevador/RandomX/blob/master/doc/design.md, 总共有3章以及附录,本篇是第1章和第2章)


       为了最小化专用硬件的性能优势,工作量证明(proof of work, PoW)算法必须针对现有通用硬件的特定特性实现设备绑定。这是一项复杂的任务,因为我们必须面对大量来自不同制造商的不同架构的设备。
       有两类不同的通用处理设备:中央处理器(CPU)和图形处理器(GPU)。RandomX针对CPU的原因如下:

  •        CPU作为一种不太专用的设备,更加普遍和容易接触到。CPU绑定的算法更加平等,允许更多的参与者加入网络。这是原始的CryptoNote白皮书[1]中陈述的目标之一。
  •        在不同架构的CPU的专有指令集中存在一个较大的公共子集。但同样的说法就不适用于GPU了,例如,NVIDIA和AMD的GPU没有公共的整数乘法指令[2]。
  •        所有主流的CPU指令集都有详细的文档,还有多种开源编译器可用。相比之下,GPU指令集通常是私有的,可能需要供应商的特定闭源驱动程序才能获得最佳性能。

1. 设计的考量
       设计一种CPU绑定的工作量证明算法的最基本思想是:“工作”必须是动态的。这利用了CPU接受两种输入的事实:数据(主输入)和代码(指定对数据执行什么操作)。
       相反地,典型的密码学哈希函数[3]不代表对CPU而言的合适工作,因为它们唯一的输入是数据,而操作序列是固定的,可以通过专用集成电路更高效地执行。


1.1动态工作量证明
动态工作量证明算法一般包括以下4个步骤:

  •        生成一个随机程序。
  •        翻译成本地CPU的机器代码。
  •        执行程序。
  •        将程序的输出转换为密码学的安全值。

       实际“有用的”CPU绑定工作在步骤3中执行,因此必须对算法进行调优,以最小化其它步骤的开销。


1.1.1生成随机程序
       设计动态工作量证明的早期尝试是基于用高级语言(如C或Javascript)生成程序[4,5]。然而,这是非常低效的,主要有两个原因:

  •        高级语言有复杂的语法,生成一段有效的程序是相对较慢的,因为它需要创建一棵抽象语法树(ASL)。
  •        一旦生成了程序的源代码,编译器通常会将文本表示解析回ASL,这就让生成源代码的整个过程变得多余。

       生成随机程序的最快方法是使用无逻辑生成器——简单地用随机数据填充缓冲区。当然,这需要设计一种无语法的编程语言(或指令集),其中所有的随机比特串都表示有效的程序。


1.1.2将程序翻译成机器码
       这一步是不可避免的,因为我们不想将算法限制在一种特定的CPU架构中。为了尽可能快地生成机器码,我们需要指令集尽可能地接近本地硬件,同时仍然足够通用,以支持不同的架构。在代码编译期间,没有足够的时间进行代价高昂的优化。


1.1.3执行程序
       实际的程序执行应该使用尽可能多的CPU组件。程序中应该利用的一些特性是:

  •        多级缓存(L1, L2, L3)
  •        μop缓存[6]
  •        算术逻辑单元(ALU)
  •        浮点单元(FPU)
  •        内存控制器
  •        指令级并行[7]
                  超标量执行[8]
                  乱序执行[9]
                  推测执行[10]
                  寄存器重命名[11]

       第2章描述RandomX VM如何利用这些特性。


1.1.4计算最终结果
       Blake2b[12]是一个密码学中的安全哈希函数,它是专门为软件快速执行而设计的,特别是在现代64位处理器上,它的速度大约是SHA-3的三倍,并且能够以每字节输入消耗大约3个时钟周期的速度运行。此函数是一个对CPU友好的工作量证明算法的理想候选者。
       为了以密码学中安全的方式处理大量数据,高级加密标准(AES)[13]可以提供最快的处理速度,因为许多现代CPU支持这些操作的硬件加速。有关在RandomX中使用AES的更多细节,请参见第3章。


1.2“简单程序问题”
       当一个随机程序生成后,人们可以选择只在有利的时候执行它。这项策略是可行的,主要有两个原因:

  •        随机生成的程序的运行时间通常遵循对数正态分布[14](也请参阅附录C)。生成的程序可以被快速分析,如果它可能有高于平均值的运行时间,则可以跳过此程序的执行,转而生成一个新程序。这可以显著地提高性能,特别是在运行时间分布有一个沉重的尾部(许多长运行时间的异常值)和程序生成成本较低的情况下。
  •        可以选择优化程序执行所需的特征子集来实现。例如,可以去掉对某些操作(如除法)的支持,或者更有效地执行某些指令序列。生成的程序将会被分析,只有在符合优化实现的特定需求时才会被执行。

       这些搜索拥有特定属性的程序的策略偏离了工作量证明的目标,因此必须消除它们。这可以通过要求执行N个随机程序组成的一个序列来实现,序列中每个程序都是由前一个程序的输出生成的。最后程序的输出就作为结果。
                                 输入--> 程序1 --> 程序2 -->…--> 程序(N-1) --> 程序N --> 结果
       其原理是,在第一个程序被执行后,矿工必须二选一:或者完成整条链(可能包括不利的程序),或者重新开始,并浪费掉在未完成的链上已经花费的努力。附录A中给出了这会如何影响不同挖矿策略的哈希率的例子。
       此外,这种链式程序执行具有使整个链的运行时相等的优点,因为一组相同分布的运行时间总和的相对偏差减少了。


1.3验证时间
       由于工作量证明的目的是用于缺乏信任的对等网络,所以网络参与者必须能够快速验证一个工作量证明是否有效。这就对工作量证明算法的复杂度设定了一个上限。特别地,我们为RandomX设定了一个目标,它的验证速度至少应与它试图取代的CryptoNight哈希函数[15]一样快。


1.4 内存困难
       除了纯粹的计算资源(如ALU和FPU)外,CPU通常还可以访问大量DRAM[16]形式的内存。内存子系统的性能通常被调整以匹配计算能力,例如[17]:

  •        单通道内存用于嵌入式和低功耗CPU
  •        双通道内存用于桌面CPU
  •        三或四通道内存用于工作站CPU
  •        六或八通道内存用于高端服务器CPU

       为了利用外部内存和芯片上的内存控制器,工作量证明算法应当访问一个大的内存缓冲区(称为“数据集”)。数据集必须满足:

  •        大于芯片上可以存储的总量(需要外部内存)
  •        动态(需要可写内存)

       单块芯片上可制造的最大SRAM容量,16nm制程能超过512MiB,7nm制程能超过2GiB[18]。理想情况下,数据集的大小至少应该为4GiB。然而,由于验证时间的限制(见下文),RandomX使用的大小被选为2080MiB。虽然理论上可以用目前的技术(2019年的7nm)在单块芯片上制造此大小的SRAM,但这种解决方案的可行性值得怀疑,至少在未来短期内是这样。


1.4.1 轻量客户端验证
       虽然对于解决工作量证明的专用挖矿系统,要求内存大于2GiB是合理的,但是必须为轻量客户端提供一个选项,以便使用少得多的内存来验证工作量证明。
       “快速”和“轻量”模式所需的内存比例必须谨慎选择,以使轻量模式不适合挖矿。特别是轻量模式的面积-时间(Area-Time, AT)乘积不应小于快速模式的AT乘积。AT乘积的减少量是衡量交换攻击的常用方法[19]。(译者注:交换攻击是指用时间换内存空间,而内存空间正比于芯片面积。)
       根据前几章描述的约束条件,快速和轻量验证模式之间的最大可能的性能比被经验性地定为8。这是因为:

  •        进一步增加轻量验证时间将违反第1.3章提出的限制。
  •        进一步减少快速模式运行时间将违反第1.1章提出的约束条件,特别是程序生成和结果计算的开销会变得过高。

      另外,256 MiB被选为在轻客户机模式下所需的最大内存。这个数字是可以接受的,即使是树莓派之类的小型单板电脑。
       要保持恒定的内存时间乘积,快速模式的最大内存需求是:
                                                                  8 * 256 MiB = 2048 MiB
       这还可以进一步增加,因为对于超标量哈希函数,轻量模式需要额外的芯片面积(参见第3.4章和第6章)。假设,保守估计每个超标量哈希核需要0.2mm2的芯片面积, DRAM的面密度为0.149 Gb/mm2[20],则额外需要的内存为:
                                                        8 * 0.2 * 0.149 * 1024 / 8 = 30.5 MiB
或者32 MiB,即近似到最接近的2的整数次幂。在AT乘积大致是常数的情况下,快速模式的总内存需求可以是2080MiB。

2. 虚拟机架构
    本节描述RandomX虚拟机(VM)的设计。

2.1指令集
    RandomX使用了固定长度的指令编码,每条指令8个字节。这允许在指令字中包含一个32位的立即值。通过选择指令字比特位的解释,可使任意8字节的字都是有效的指令。这就允许非常高效的随机程序生成(参见1.1.1章)。

2.1.1指令复杂性
    此VM是一台复杂指令集机器,它同时允许寄存器和内存寻址操作。但是,每条RandomX指令仅转换成1-7条x86指令(平均1.8条)。保持指令复杂度相对较低是很重要的,这样可以最大限度地降低具有定制指令集的专用硬件的效率优势。

2.2程序
    经VM执行的程序是由256条随机指令组成的循环。

  •     256条指令已足够多,可提供大量可能的程序和足够的分支空间。能生成的不同程序的数量可达2^512 = 1.3e+154,这也是随机数生成器可能的种子值的数量。
  •     256条指令又足够少,因此,高性能CPU执行一次循环的耗时与从DRAM中获取数据的耗时相近。这是有利的,因为这允许数据集访问是同步的和完全可预取的(见2.9章)。
  •     由于程序是一个循环,它可以利用存在于某些x86 CPU中的μop缓存[6]。从μop缓存中运行循环允许CPU关闭x86指令解码器,这应该有助于均衡x86和具备同样指令解码器的架构之间的能效。

2.3寄存器
    VM使用8个整数寄存器和12个浮点寄存器。这是在x86-64架构中可以作为物理寄存器分配的最大值,在常见的64位CPU架构中,x86-64架构的结构寄存器最少。使用更多的寄存器会让x86 CPU处于劣势,因为它们必须使用内存来存储VM寄存器的内容。

2.4整数运算
    RandomX使用具有高输出熵的所有原始整数运算:加法(IADD_RS, IADD_M)、减法(ISUB_R, ISUB_M, INEG_R)、乘法(IMUL_R, IMUL_M, IMULH_M, ISMULH_R, ISMULH_M, IMUL_RCP)、异或(IXOR_R, IXOR_M)和循环移位(IROR_R, IROL_R)。

2.4.1 IADD_RS
    IADD_RS指令利用了CPU的地址计算逻辑,大多数CPU (x86 lea, ARM add)可以在一条硬件指令中执行。

2.4.2 IMUL_RCP
    由于整数除法在CPU中不是完全流水线执行的,并且在ASIC中可以更快地完成,所以IMUL_RCP指令仅要求每个程序进行一次除法来计算倒数。这就迫使ASIC必须包含一个硬件除法器,但除法器在程序执行期间又无法提供性能优势。

2.4.3 IROR_R / IROL_R
    循环移位指令分为右移和左移,比例为4:1。循环右移具有更高的频率,是因为一些架构(如ARM)不支持循环左移(必须使用循环右移来模拟)。

2.4.4 ISWAP_R
    这条指令可被支持寄存器重命名/移动消除的CPU高效执行。

2.5浮点运算
    RandomX使用双精度浮点运算,大多数CPU都是支持的,并且需要比单精度运算更复杂的硬件。所有运算都是作为128位向量运算执行的,所有主流的CPU架构也都支持这种操作。
    RandomX使用IEEE 754标准保证的五种运算来给出正确的舍入结果:加法、减法、乘法、除法和平方根。标准定义的全部4种舍入模式都被用上了。

2.5.1浮点寄存器组
    浮点运算的领域被分成使用F组寄存器的“加法”运算和使用E组寄存器的“乘法”运算。这样做是为了防止当一个较小的数与一个较大的数相加减时,加法/减法变成“无操作”。由于F组寄存器的范围大约被限制在±3.0e+14,因此,加或减一个绝对值大于1的浮点数总是会改变至少5个小数比特位。
    因为F组寄存器有限的范围会允许使用更高效的定点表示法(80bit的数),所以FSCAL指令操作浮点格式的二进制表示会使这种优化更加困难。
    E组寄存器被限制为正值,这避免了NaN结果(如负数的平方根或0 *∞)。除法只使用内存源操作数,以避免被优化为常数倒数的乘法。E组内存操作数的指数设定为-255到0之间的值,以避免被0除和乘,并增加可获得的数的范围。E组寄存器可能值的大致范围是1.7E-77到无穷大。
    每个程序循环结束后浮点寄存器值的近似分布如下图所示(左-F组,右-E组):

(注:柱状图中的立柱用区间的左边值标记,如:标记为1e-40的立柱包含从1e-40到1e-20的值)。
    F组寄存器在1e+14附近的少量数值是由FSCAL指令造成的,该指令大大增加了寄存器值的范围。
    E组寄存器值覆盖了一个非常大的范围。大约2%的程序产生至少一个无穷大的值。
    为了让熵最大化,也为了适应一个64字节的高速缓存行,在每次循环结束后浮点寄存器的结果被XOR运算组合,然后存储到暂存器中。

2.6分支
    现代CPU使用了大量的芯片面积和能量来处理分支。这包括:

  •     分支预测单元[21]
  •     允许CPU在分支预测错误时进行恢复的检查点/回滚状态。

    为了利用推测执行设计,随机程序应该包含分支。然而,如果分支预测失败,推测执行的指令将被丢弃,这将导致每次错误预测都会浪费一些能量。因此,我们应该尽量减少错误预测的次数。
    此外,代码中的分支是必不可少的,因为分支可以显著减少静态优化的数量。例如,考虑以下x86指令序列:

…
branch_target_00:
…
    xor r8, r9
    test r10, 2088960
    je branch_target_00
    xor r8, r9
…

    两次XOR运算的效果通常会抵消,但由于分支的存在而不能优化掉,因为如果执行了分支,结果将会不同。类似地,如果没有分支,ISWAP_R指令总是可以被静态优化去掉。
    一般来说,随机分支必须按如下方式设计:
    1.无限循环是不可能的。
    2.预测错误的分支数量很少。
    3.分支条件依赖于一个运行时值,以禁用静态分支优化。

2.6.1分支预测
    不幸的是,我们还没有找到在RandomX中使用分支预测的方法。因为RandomX是一个共识协议,所有的规则都必须提前设定,包括分支的规则。完全可预测的分支不能依赖于任何VM寄存器的运行时值(因为寄存器值是伪随机的和不可预测的),所以它们必须是静态的,因而可以通过专门的硬件轻松地进行优化。

2.6.2 CBRANCH指令
    因此,RandomX使用跳转概率为1/256的随机分支和依赖于整数寄存器值的条件分支,这些分支将被CPU预测为“不会执行”。在大多数CPU设计中,这样的分支是“空闲的”,除非它们被执行。虽然这并没有利用分支预测器,但与非推测性分支处理相比,推测性设计将获得显著的性能提升。更多信息请参见附录B。
    在选择分支条件和跳转目标时,必须满足RandomX代码中不可能存在无限循环,因为控制分支的寄存器在重复代码块中永远不会被修改。每条CBRANCH指令可以连续跳转两次。使用预测执行[22]来处理CBRANCH是不切实际的,因为大多数情况下该分支不会执行。

2.7指令级并行
    CPU使用一些技术来提高它们的性能,这些技术利用了执行代码的指令级并行性。这些技术包括:

  •     拥有多个可以并行执行操作的执行单元(超标量执行)。
  •     不按程序顺序执行指令,而是按操作数的可用性(乱序执行)。
  •     预测分支将以何种方式执行,以增加超标量和乱序执行的益处。

    RandomX可从这些优化中获益。详细分析见附录B。

2.8 暂存器
    暂存器被用作可读写内存。它的大小被选为完全匹配CPU缓存。

2.8.1 暂存器级别
    暂存器分为3个级别,用于模拟典型的CPU缓存层次结构[23]。大多数VM指令访问“L1”和“L2”暂存器,因为L1和L2缓存位于CPU执行单元附近,提供最低的随机访问延迟。从L1和L2中读取数据的比例是3:1,这与典型延迟时间的反比相匹配(见下表)。

CPU μ-architecture L1 延迟 L2 延迟 L3 延迟 资料来源
ARM Cortex 2 6 - [24]
AMD Zen+ 4 12 40 [25]
Intel Skylake 4 12 42 [26]

    L3缓存要大得多,并且位于离CPU核心更远的地方。因此,它的访问延迟要高得多,并可能导致程序执行的暂停。
    因此,RandomX在每个程序循环中只执行两次对“L3”暂存器的随机访问(参见第4.6.2章中的步骤2和步骤3)。来自给定循环的寄存器值被写入与加载它们相同的位置,这就保证了所需的高速缓存行已被移动到速度更快的L1或L2缓存中。
    此外,从固定地址读取的整数指令也会使用整个“L3”暂存器(参看表5.1.4),因为重复的访问将确保高速缓存行被放置在CPU的L1缓存中。这表明,暂存器级别并不总是直接对应于相同的CPU缓存级别。

2.8.2暂存器写入
    在VM执行过程中,有两种方法可以修改暂存器:
    1.在每次程序循环结束时,所有寄存器值都被写入“L3”暂存器(参见第4.6.2章,步骤9和步骤11)。每次循环将在两个64字节的块中总计写入128字节。
    2.ISTORE指令执行显式存储。平均每个程序有16次存储,其中2次存储会进入“L3”级别。每条ISTORE指令写入8个字节。
    下图展示了一个暂存器写入数据分布情况的例子。图像中的每个像素代表暂存器中的8个字节。红色像素表示在哈希计算期间至少被覆盖了一次的暂存器部分。“L1”和“L2”级位于左侧(几乎完全被覆盖)。暂存器的右侧代表底部的1792KiB。只有大约66%的部分被覆盖,但这些写入分布得均匀且随机。

关于暂存器熵的分析见附录D。

2.8.3读写比例
    每个程序循环对暂存器平均执行39次读取(IADD_M、ISUB_M、IMUL_M、IMULH_M、ISMULH_M、IXOR_M、FADD_M、FSUB_M、FDIV_M)和16次写入(ISTORE)。另外128个字节被隐式地读写以初始化和存储寄存器值。每次循环从数据集读取64字节的数据。总计:

  •     每次程序循环从内存中读取的平均数据量是:39 * 8 + 128 + 64 = 504字节
  •     每次程序循环向内存中写入的平均数据量是:16 * 8 + 128 = 256字节

    这接近2:1的读/写比例,也是CPU优化的目标。

2.9数据集
    由于暂存器通常存储在CPU缓存中,所以只有访问数据集时才使用内存控制器。
    RandomX在每个程序循环中随机读取数据集一次(每个哈希结果读取16384次)。由于数据集必须存储在DRAM中,于是它提供了一种自然的并行化限制,因为在每个存储库组中,DRAM无法执行超过2500万次/秒的随机访问。每个单独可寻址的存储库组允许大约1500Hash/s的吞吐量。
    所有数据集访问都读取一CPU高速缓存行(64字节),并且是完全预取的。第4.6.2章中描述的执行一个程序循环的时间与典型的DRAM访问延迟(50-100 ns)大致相同。

2.9.1缓存
    用于轻量验证和数据集构建的缓存近似是数据集的1/8大。为保持恒定的面积-时间乘积,每个数据集项由8个随机缓存访问构成。
    因为256MiB足够小,可以包含在芯片上,RandomX使用自定义的高延迟、高能耗的混合函数(“超标量哈希”),这抵消了使用低延迟内存的好处,并且计算超标量哈希所需的能量使得轻量模式挖矿非常低效(参见3.4章)。
    由于将抗交换攻击的Argon2d函数迭代了3次,使用少于256MiB的内存是不可能的。当使用3次迭代时,内存使用量减半将增加3423倍的计算成本,才能实现最佳的交换攻击[27]。

(原文中只看到三张和附录A~F,并未看到第4章以及其它章节。)


参考资料:
References
[1] CryptoNote whitepaper - https://cryptonote.org/whitepaper.pdf
[2] ProgPoW: Inefficient integer multiplications - https://github.com/ifdefelse/ProgPOW/issues/16
[3] Cryptographic Hashing function - https://en.wikipedia.org/wiki/Cryptographic_hash_function
[4] randprog - https://github.com/hyc/randprog
[5] RandomJS - https://github.com/tevador/RandomJS
[6] μop cache - https://en.wikipedia.org/wiki/CPU_cache#Micro-operation_(%CE%BCop_or_uop)_cache
[7] Instruction-level parallelism - https://en.wikipedia.org/wiki/Instruction-level_parallelism
[8] Superscalar processor - https://en.wikipedia.org/wiki/Superscalar_processor
[9] Out-of-order execution - https://en.wikipedia.org/wiki/Out-of-order_execution
[10] Speculative execution - https://en.wikipedia.org/wiki/Speculative_execution
[11] Register renaming - https://en.wikipedia.org/wiki/Register_renaming
[12] Blake2 hashing function - https://blake2.net/
[13] Advanced Encryption Standard - https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
[14] Log-normal distribution - https://en.wikipedia.org/wiki/Log-normal_distribution
[15] CryptoNight hash function - https://cryptonote.org/cns/cns008.txt
[16] Dynamic random-access memory - https://en.wikipedia.org/wiki/Dynamic_random-access_memory
[17] Multi-channel memory architecture - https://en.wikipedia.org/wiki/Multi-channel_memory_architecture
[18] Obelisk GRN1 chip details - https://www.grin-forum.org/t/obelisk-grn1-chip-details/4571
[19] Biryukov et al.: Tradeoff Cryptanalysis of Memory-Hard Functions - https://eprint.iacr.org/2015/227.pdf
[20] SK Hynix 20nm DRAM density - http://en.thelec.kr/news/articleView.html?idxno=20
[21] Branch predictor - https://en.wikipedia.org/wiki/Branch_predictor
[22] Predication - https://en.wikipedia.org/wiki/Predication_(computer_architecture)
[23] CPU cache - https://en.wikipedia.org/wiki/CPU_cache
[24] Cortex-A55 Microarchitecture - https://www.anandtech.com/show/11441/dynamiq-and-arms-new-cpus-cortex-a75-a55/4
[25] AMD Zen+ Microarchitecture - https://en.wikichip.org/wiki/amd/microarchitectures/zen%2B#Memory_Hierarchy
[26] Intel Skylake Microarchitecture - https://en.wikichip.org/wiki/intel/microarchitectures/skylake_(client)#Memory_Hierarchy
[27] Biryukov et al.: Fast and Tradeoff-Resilient Memory-Hard Functions for Cryptocurrencies and Password Hashing - https://eprint.iacr.org/2015/430.pdf Table 2, page 8
[28] J. Daemen, V. Rijmen: AES Proposal: Rijndael - https://csrc.nist.gov/csrc/media/projects/cryptographic-standards-and-guidelines/documents/aes-development/rijndael-ammended.pdf page 28
[29] 7-Zip File archiver - https://www.7-zip.org/
[30] TestU01 library - http://simul.iro.umontreal.ca/testu01/tu01.html

发布了45 篇原创文章 · 获赞 98 · 访问量 35万+

猜你喜欢

转载自blog.csdn.net/pijianzhirui/article/details/103885849