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 Ti | GPU 优势 |
|---|---|---|---|
| SHA-256 | 41 MH/s | 6150 MH/s | ~150x |
| RSW | 26 H/s | 4400 H/s | ~170x |
| HashWX | 2.8 MH/s | 5.8 MH/s | ~2x |
这些数字来自 tevador,用的是他自己未公开的 CUDA 实现。RSW 那一行我们独立复现过:M3 上每线程实测 2.112 H/s,折算到 16 个 Ryzen 线程正好是 26 H/s。
RSW 已被弃用。它仍然可以按密钥选用,现有密钥也继续可用,但不应再用于新的部署。
协议如何工作
铸造
服务端取 32 个随机字节作为质询 C,再定一个难度 d。这里没有密钥材料,也没有预计算,所以铸造就是一次随机读取加一次 JWT 签名。
客户端拿到 C、d,以及 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 µs | 40 µs | 54 µs |
| HashWX,4 个子质询(默认) | 20 µs | 129 µs | 149 µs |
| SHA-256(50 个质询,难度 4) | 4 µs | 83 µs | 87 µs |
| RSW(t = 75,000) | 1522 µs | 14 µs | 1536 µs |
单个 HashWX 质询是三者中往返开销最低的。默认的四个子质询约需 150 µs,比 SHA-256 高,但只有 RSW 的十分之一,因为 RSW 每次签发都要做四次真正的模幂运算。难度不影响这些数字:无论找到解有多难,验证每个子质询都只需要算一次哈希。
客户端开销是这笔交易的另一面。单个质询的求解时间服从指数分布,这让它像抽奖:同样的难度,一位访客 30 毫秒解完,下一位要三秒。所以 Cap 默认把难度拆成四个子质询,让求解时间更均匀。以下是在 8 核 M3 上用正式版 Chrome 通过验证组件测得的结果,每组 72 次求解,两种难度调到大致相同的中位数:
| 1 个质询,d = 1,330,000 | 4 个子质询,d = 1,000,000 | |
|---|---|---|
| 中位数 | 536 ms | 490 ms |
| p90 | 1447 ms | 778 ms |
| 72 次中最慢 | 2378 ms | 1490 ms |
右侧一列就是默认配置。对接 Standalone 密钥时,测得中位数 578 毫秒,p90 为 0.9 秒。
对于给定的难度,拆分并不改变攻击者要付出的代价,因为无论怎么拆,预期工作量都是 d 次哈希。改变的是分布的形状。单个质询的中位数只有均值的 0.69,而四个子质询把中位数推到均值的 0.92 左右,长尾也随之缩短。所以在难度相同时,拆分会让一次典型求解慢三分之一左右,p90 则减少约四分之一。上表是固定中位数来比较的,这让拆分方案的难度低了 25%,攻击者每次求解也少做 25% 的工作。验证组件自身的开销很小。worker 每 16 毫秒检查一次是否该停止,这决定了子质询之间交接的最长时间。
浏览器引擎
wasm 构建的速度大约是原生的 60%。三大引擎在单个 worker 上相差无几,所有核心都跑满时 Safari 会落后。在同一台 M3 上、同一次测试中测得,每个浏览器都处于无头模式或在前台:
| 引擎 | 1 个 worker | 8 个 worker | 解释执行回退 |
|---|---|---|---|
| Chrome 153 | 440 KH/s | 2050 KH/s | 94 KH/s |
| Firefox 156 | 420 KH/s | 1850 KH/s | 105 KH/s |
| Safari 27.2 | 410 KH/s | 1480 KH/s | 105 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 S24 | Android 14 | 1238 KH/s | 1.1 秒 | 1.8 秒 |
| Pixel 9 | Android 15 | 837 KH/s | 1.4 秒 | 2.0 秒 |
| Pixel 6 | Android 12 | 746 KH/s | 1.9 秒 | 2.4 秒 |
| iPhone 15 | iOS 17 | 未测 | 1.9 秒 | 4.3 秒 |
| iPhone 12 | iOS 17 | 620 KH/s | 2.0 秒 | 3.3 秒 |
| iPhone 13 | iOS 15 | 678 KH/s | 2.2 秒 | 4.1 秒 |
| iPhone SE 2022 | iOS 15 | 588 KH/s | 2.4 秒 | 4.0 秒 |
| Redmi Note 11 | Android 11 | 456 KH/s | 2.4 秒 | 4.8 秒 |
| Galaxy M32 | Android 11 | 444 KH/s | 2.8 秒 | 6.1 秒 |
| Vivo Y21 | Android 11 | 316 KH/s | 5.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 是新建密钥的默认协议,也可以在控制台里按密钥切换。
