Skip to content

HashWX 工作量证明

HashWX 是 Cap 默认的质询协议。它不使用 SHA-256 这样固定的哈希函数,而是为每个质询从种子生成一个新的单向函数,由整数运算和分支构成,其选取方式使得 GPU 跑起来不会比 CPU 快多少。

它由 tevador 设计,他也是 RandomX 和 HashX 的作者。Cap 内置了官方的 WebAssembly 参考实现。

提示

HashWX 是 Standalone 新建密钥的默认协议。在 cap-core 中它需要通过 format-2 API 手动启用,那里的默认仍然是 SHA-256 工作量证明。参见 HashWX 质询

为什么是 HashWX

SHA-256 工作量证明的问题在于吞吐量。GPU 用成千上万条通道同步跑同一个固定函数,因此每秒解出的质询数远多于 CPU。而这正是机器人防护真正在意的指标:攻击者不关心单个质询要花多久,只关心每小时能清掉多少个。

Cap 之前为此提供的是 RSW 时间锁谜题。RSW 赢在延迟上,因为单个谜题内部的顺序平方无法并行,但它在吞吐量上输得很惨,因为 GPU 可以同时跑几千个互不相关的谜题。与一块消费级 GPU 对比:

算法CPU,Ryzen 3700X,16 线程GPU,RTX 5060 TiGPU 优势
SHA-25641 MH/s6150 MH/s~150x
RSW26 H/s4400 H/s~170x
HashWX2.8 MH/s5.8 MH/s~2x

这些数字来自 tevador,用的是他自己未公开的 CUDA 实现。RSW 那一行我们独立复现过:M3 上每线程实测 2.112 H/s,折算到 16 个 Ryzen 线程正好是 26 H/s。

RSW 已被弃用。它仍然可以按密钥选用,现有密钥也继续可用,但不应再用于新的部署。

协议如何工作

铸造

服务端取 32 个随机字节作为质询 C,再定一个难度 d。这里没有密钥材料,也没有预计算,所以铸造就是一次随机读取加一次 JWT 签名。

客户端拿到 Cd,以及 n,也就是每个生成出来的函数覆盖多少个 nonce。

客户端求解

客户端要找到一个 64 位 nonce N,使得

H(N) <= (2^64 - 1) / d      where H = hashwx_make(sha256(C || u64le(N / n)))

每一块 n 个连续 nonce 共用一个生成出来的哈希函数。客户端构建这个函数,在整块上运行它,如果没有结果落到目标以下就换下一块。期望工作量是 d 次哈希。

Cap 用的是 n = 65536。参考协议给原生客户端用的是 463;在浏览器里,每一块都要通过 WebAssembly.Module 把函数重新 JIT 编译一遍,所以更大的块能摊薄这部分开销。tevador 明确指出了这个取舍:每个函数覆盖的 nonce 越多,协议就越容易被 JIT 编译的 GPU 内核追上,在这个取值下撑住抗 GPU 能力的是分支发散。上面那个约 2 倍的数字就是在 65536 下测的,已经把这一点算进去了。

抗 GPU 的原因

四个特性,都出自设计文档

每个实例是 32 个程序,每个程序都是一个循环,以 1/2 的概率跳回自己的开头,算下来每次哈希正好 256 次分支。在 CPU 上这只是几次分支预测失败。在 GPU 上它会把一个 warp 拆成若干发散路径,只能一条接一条地执行。

有一块 16 KB 的暂存区,而且对它的读取是刻意不对齐的。CPU 把它放在 L1 里,用乱序执行掩盖 3 到 4 个周期的延迟。GPU 只能把它放在由 L2 支撑的本地内存里,延迟约 100 个周期,而且大多数 GPU 架构还得把两次相邻读取拼起来,才能模拟出不对齐读取。

源寄存器取自交错排列的"浅"列表和"深"列表,因此 CPU 平均要处理 2.75 次相互依赖的读取。GPU 解释器只能按深列表来特化,因为大约 95% 的情况下,一个 warp 里至少有一个线程在跑深程序,于是每次都得吃下完整的 6 次依赖读取链。

指令集被限制在 WebAssembly 1.0 提供的范围内:64 位乘、加、减、XOR、OR、循环移位和移位,立即数为 6 位。正是这一点让同一套算法能在浏览器里跑起来。

服务端验证

服务端根据 C 和提交的 nonce 所对应的块索引重新算出种子,生成那一个哈希函数,运行一次,再和目标比较。程序生成的开销大约只有 HashX 的 1/5,这正是验证能保持在几十微秒的原因。

成本

每个质询的服务端成本,在 Apple M3 的单个核心上测得,通过 validateChallenge 做 200 次铸造和 40 次验证取中位数:

协议铸造验证合计
HashWX,1 个质询14 µs40 µs54 µs
HashWX,4 个子质询(默认)20 µs129 µs149 µs
SHA-256(50 个质询,难度 4)4 µs83 µs87 µs
RSW(t = 75,000)1522 µs14 µs1536 µs

单个 HashWX 质询是三者中往返开销最低的。默认的四个子质询约需 150 µs,比 SHA-256 高,但只有 RSW 的十分之一,因为 RSW 每次签发都要做四次真正的模幂运算。难度不影响这些数字:无论找到解有多难,验证每个子质询都只需要算一次哈希。

客户端开销是这笔交易的另一面。单个质询的求解时间服从指数分布,这让它像抽奖:同样的难度,一位访客 30 毫秒解完,下一位要三秒。所以 Cap 默认把难度拆成四个子质询,让求解时间更均匀。以下是在 8 核 M3 上用正式版 Chrome 通过验证组件测得的结果,每组 72 次求解,两种难度调到大致相同的中位数:

1 个质询,d = 1,330,0004 个子质询,d = 1,000,000
中位数536 ms490 ms
p901447 ms778 ms
72 次中最慢2378 ms1490 ms

右侧一列就是默认配置。对接 Standalone 密钥时,测得中位数 578 毫秒,p90 为 0.9 秒。

对于给定的难度,拆分并不改变攻击者要付出的代价,因为无论怎么拆,预期工作量都是 d 次哈希。改变的是分布的形状。单个质询的中位数只有均值的 0.69,而四个子质询把中位数推到均值的 0.92 左右,长尾也随之缩短。所以在难度相同时,拆分会让一次典型求解慢三分之一左右,p90 则减少约四分之一。上表是固定中位数来比较的,这让拆分方案的难度低了 25%,攻击者每次求解也少做 25% 的工作。验证组件自身的开销很小。worker 每 16 毫秒检查一次是否该停止,这决定了子质询之间交接的最长时间。

浏览器引擎

wasm 构建的速度大约是原生的 60%。三大引擎在单个 worker 上相差无几,所有核心都跑满时 Safari 会落后。在同一台 M3 上、同一次测试中测得,每个浏览器都处于无头模式或在前台:

引擎1 个 worker8 个 worker解释执行回退
Chrome 153440 KH/s2050 KH/s94 KH/s
Firefox 156420 KH/s1850 KH/s105 KH/s
Safari 27.2410 KH/s1480 KH/s105 KH/s

单个 worker 时三者相差不到 5%。8 个 worker 时,Safari 大约只有 Chrome 速率的 70%,所以调难度时要以 Safari 为准:在 d = 1,000,000 时,Chrome 上平均约 0.5 秒,Safari 上约 0.7 秒。

如果你要自己测,请用各浏览器的正式发布版,并让标签页保持在前台。Playwright 自带的 Firefox 跑任何 WebAssembly 负载都比正式版 Firefox 慢三到六倍,不只是 HashWX;而退到后台的标签页可能会被调度到能效核心上,让所有数字直接减半。

手机

以下是默认配置在 BrowserStack 真机上的结果,每台 15 次求解,Vivo 为 30 次。每轮都从刚加载的页面开始,和访客第一次打开页面时一样。页面先跑一分钟 HashWX 之后,Pixel 6 每次求解的时间少了 24%,Vivo 少了 29%,所以先预热再测的基准测试会比这里的数字好看。

设备系统全部核心中位数最慢
Galaxy S24Android 141238 KH/s1.1 秒1.8 秒
Pixel 9Android 15837 KH/s1.4 秒2.0 秒
Pixel 6Android 12746 KH/s1.9 秒2.4 秒
iPhone 15iOS 17未测1.9 秒4.3 秒
iPhone 12iOS 17620 KH/s2.0 秒3.3 秒
iPhone 13iOS 15678 KH/s2.2 秒4.1 秒
iPhone SE 2022iOS 15588 KH/s2.4 秒4.0 秒
Redmi Note 11Android 11456 KH/s2.4 秒4.8 秒
Galaxy M32Android 11444 KH/s2.8 秒6.1 秒
Vivo Y21Android 11316 KH/s5.9 秒11.0 秒

每次求解都包含两次到测试服务器的往返,各设备的中位数在 80 到 190 毫秒之间。入门级 Android 手机 Vivo 的耗时约为 M3 台式机的十倍。如果你的流量以移动端为主,请调低难度。求解时间与难度成正比,500,000 大约能让表中每个数字的求解部分减半。

没有 WebAssembly 的客户端根本解不了 HashWX,会直接拿到错误。如果你需要支持它们,请改用 SHA-256 工作量证明,它有纯 JS 的回退方案。在 iPhone 上,验证组件需要 iOS 15 或更高版本。

HashWX 防不住什么

和 RSW 防不住的是同一件事:定制芯片。为它专门打造的 FPGA 或 ASIC 仍然会赢过 CPU。对刷 CAPTCHA 来说这笔账算不过来,因为 ASIC 的一次性工程费用高达数百万美元,但这终究不是密码学层面的保证。

它同样拦不住人工打码农场,而且永远拦不住。工作量证明抬高的是每个请求的成本。请把它和 instrumentation 质询搭配使用,这样还会要求对方必须是真实的浏览器环境。

试一试

cap-core 的 API 接口记录在 HashWX 质询中。验证组件会自动检测 format-2 响应,因此只需升级服务端即可。

Cap Standalone 上,HashWX 是新建密钥的默认协议,也可以在控制台里按密钥切换。