对于密码学货币而言,工作量证明(PoW)算法的安全性直接决定了网络的去中心化程度。Monero 在 2019 年启动的 RandomHash2 算法升级,本质上是一次针对专用挖矿硬件的防御性革命。与其前辈 CryptoNight 系列不同,RandomHash2 不再依赖隐晦的内存访问模式,而是通过一个完全透明的虚拟机构建了一套精密的「通用计算模拟」体系。这套体系的核心,是将 AES 加密指令嵌入虚拟机指令集,并配合近 2.1 GB 的只读数据集(Dataset)形成内存硬性约束。本文将从密码学原语层面,拆解 RandomHash2 的实现细节。
虚拟机的 AES 指令嵌入机制
RandomHash2 的核心创新在于它并不直接调用 CPU 的 AES-NI 指令集,而是在一个自定义虚拟机(Virtual Machine,VM)内部模拟了一个简化版的 AES 运算模型。这个虚拟机被设计为「任何随机的 8 字节数据都是一条合法指令」,这意味着生成一个有效的挖矿程序只需向程序缓冲区填充随机字节,无需任何语法解析逻辑。
虚拟机内部包含三组寄存器:8 个 64 位整数寄存器(r0 至 r7)、12 个 128 位浮点寄存器(细分为 f0-f3、e0-e3、a0-a3 两组),以及 4 个内部控制寄存器(ma、mx、mt、fprc)。其中,fprc 寄存器控制所有浮点运算的舍入模式,这与 IEEE 754 标准的四种模式(roundTiesToEven、roundTowardNegative、roundTowardPositive、roundTowardZero)一一对应。
在 AES 相关的密码学原语层面,RandomHash2 定义了三个基于 AES 的核心函数。AesGenerator1R 是轻量级伪随机数生成器,使用单轮 AES 变换处理 64 字节状态(分为 4 列,每列 16 字节)。其中第 0、2 列执行 AES 解密,第 1、3 列执行 AES 加密,单次迭代即可输出 64 字节随机数并更新内部状态。AesGenerator4R 则是其安全强化版本,对每列执行四轮 AES 变换(列 0、1 使用一组密钥,列 2、3 使用另一组密钥),更适合程序生成等对随机性要求更高的场景。AesHash1R 则用于对 Scratchpad 内存区域计算 512 位指纹摘要,同样采用四列结构,区别在于输入数据块被直接当作轮密钥使用,并在最后额外施加两轮 AES 变换作为输出混淆。
这种设计的精妙之处在于:虽然虚拟机在软件层面模拟 AES 运算,但当 CPU 支持 AES-NI 硬件指令时,底层编译器可以将这些模拟操作直接映射为高效的硬件指令,形成「软件定义 + 硬件加速」的双重优势。
程序数据集的构建与内存访问模式
RandomHash2 的内存硬性并非简单通过大容量缓存实现,而是通过一个精心设计的双层结构:Cache(缓存)与 Dataset(数据集)。Cache 是一个由 Argon2d 算法初始化的只读内存区域,默认配置使用 262,144 个 1 KiB 区块(总计 256 MB),经过 3 轮 Argon2d 迭代填充。Dataset 则是从 Cache 派生出的更大只读结构,默认基础大小为 2,147,483,648 字节(约 2 GB),额外空间为 33,554,368 字节(约 32 MB),总计超过 2.1 GB。
Dataset 的生成过程引入了 SuperscalarHash 函数 —— 这是一种专门为超标量 CPU 优化的自定义扩散函数。SuperscalarHash 的设计目标是最大化整数 ALU 的利用率,它模拟了 Intel Ivy Bridge 微架构的三发射端口(P0、P1、P5)执行模型,生成一组指令序列使得所有执行端口尽可能满载。每个 64 字节的 Dataset 条目通过 8 个独立的 SuperscalarHash 实例并行处理,并与从 Cache 中随机选取的数据进行 XOR 混合。值得注意的是,单个 Dataset 条目的生成需要 8 次 Cache 访问(RANDOMX_CACHE_ACCESSES = 8),这确保了即使攻击者试图优化 Dataset 读取,也会面临大量的内存带宽消耗。
在程序执行阶段,虚拟机通过两个内部寄存器 ma 和 mx 追踪 Dataset 访问地址。每次循环迭代中,虚拟机会预取下一个 Dataset 条目(地址为 datasetOffset + mp % RANDOMX_DATASET_BASE_SIZE),同时读取当前需要的条目并与整数寄存器进行 XOR 混合。这种设计使得每产生一个有效的 PoW 结果,都需要完整遍历近 2 GB 的只读数据集,ASIC 芯片在晶体管面积和功耗上都将面临巨大压力。
程序执行模型与抗 ASIC 原理
RandomHash2 的程序执行流程可以概括为:每轮哈希计算会经历 8 次独立的程序执行(由 RANDOMX_PROGRAM_COUNT = 8 控制),每次程序包含 256 条随机指令(由 RANDOMX_PROGRAM_SIZE = 256 指定),每条指令被执行 2048 次迭代(由 RANDOMX_PROGRAM_ITERATIONS = 2048 控制)。这意味着单次 PoW 计算需要执行 256 × 2048 × 8 = 4,194,304 条虚拟机指令。
指令集的设计刻意偏向通用 CPU 的常见操作:整数指令涵盖加法(IADD_RS、IADD_M)、减法(ISUB_R、ISUB_M)、乘法(IMUL_R、IMUL_M)、乘法高位(IMULH_R、IMULH_M)、异或(IXOR_R、IXOR_M)、移位(IROR_R、IROL_R)等 17 种指令类型,占全部 256 个操作码中的 120 个。浮点指令则包括加法、减法、乘法、除法、开方等 9 种类型,占 94 个操作码。控制流指令仅两种:CFROUND(修改浮点舍入模式)和 CBRANCH(条件跳转),但其跳转逻辑经过特殊设计,确保程序不会陷入无限循环 —— 跳转目标始终指向目标指令上次修改位置之后的下一条指令。
这种设计的抗 ASIC 逻辑可以从三个维度理解。其一是指令集的随机性和复杂性:任意 8 字节序列都是合法指令,意味着攻击者无法通过静态分析预编译的二进制代码来提取可优化的计算图,必须完整模拟虚拟机的完整状态转换。其二是内存访问的全地址覆盖:Scratchpad(工作内存)分为三级(L1: 16 KB,L2: 256 KB,L3: 2 MB),程序执行过程中会随机访问不同层级的内存位置,而 Dataset 的大小达到 2 GB 且访问地址通过寄存器值动态计算,这使得 ASIC 无法通过固定地址模式的专用缓存来加速。其三是对通用计算能力的高度依赖:浮点运算单元、整数乘法管线、内存层级结构等特性在不同 CPU 微架构间差异巨大,而 RandomHash2 的指令混合比例经过精细调整,使其在 Intel、AMD、ARM 等主流架构上都能高效运行,但对专用硬件的适配成本极高。
随机数种子与状态管理
整个 RandomHash2 算法的入口是一个可选的密钥 K(0-60 字节)和任意长度的待哈希数据 H。算法首先使用 Blake2b-512 将 H 转换为 64 字节种子 S,然后通过 AesGenerator1R 用 S 作为密钥初始化并填充 Scratchpad(2 MB)。紧接着使用 AesGenerator4R 从前一个生成器的最终状态派生出新的随机源,用于填充虚拟机的配置数据(128 字节)和程序缓冲区(8 × 256 = 2048 字节)。
程序执行完成后,寄存器的完整状态(256 字节)再次通过 Blake2b-512 哈希生成新的种子,用于后续程序的随机数生成。这种嵌套的「程序 → 状态 → 程序」迭代结构确保了每轮计算之间存在密码学强度的依赖关系,无法通过跳过中间步骤来降低计算量。
版本差异与工程实践参数
值得注意的是,RandomHash2 存在两个主要版本(v1 和 v2),在寄存器别名和浮点混合策略上存在关键差异。v1 版本中 mp 是 mx 的别名,freg 混合使用简单的 XOR 操作;v2 版本中 mp 是 ma 的别名,freg 混合改为 8 轮 AES 加密 / 解密操作(每次使用 e0-e3 中的一个作为轮密钥),这进一步提升了算法复杂度。
实际部署时的关键参数包括:Scratchpad L3 大小 2,097,152 字节(2 MB)、Dataset 基础大小 2,147,483,648 字节(2 GB)、程序迭代次数 2,048、每轮程序数量 8、Argon2 内存占用 256 MB、Argon2 迭代轮数 3。在工程实现中,这些参数均可通过编译配置动态调整,但默认配置已在安全性与性能之间取得了经过社区验证的平衡。
资料来源:RandomX 官方技术规范(https://github.com/tevador/RandomX/blob/master/doc/specs.md)