Cost of the second signature verifier: 2026-10-21 and 2026-10-22, ARM build machine
Cost of checking every signature with two independent verifiers, liboqs and qanum-pqverify, on an aarch64 machine: three stages of qanum-pqverify, with raw data.
This page reproduces docs/benchmarks/2026-10-21_arm_second-verifier/README.md from qanum-core v0.9.2 (release tag). Links to files that are not published on this site are shown as plain text until the source repository is public.
Contents
Produced by examples/pq_dual_bench. It measures what the node pays for
checking every post-quantum signature with two independent verifiers:
liboqs and qanum-pqverify. The same benchmark was run on three successive
states of qanum-pqverify, so this page compares them:
| Stage | Folder | qanum-pqverify code |
|---|---|---|
| 1 | 1-initial/ |
First version: SHAKE through the hashing API of the sha3 crate; ML-DSA arithmetic reduced with % after every operation. |
| 2 | 2-sponge/ |
The SHAKE sponge (FIPS 202) written in the crate over the Keccak permutation of the keccak crate. |
| 3 | 3-montgomery/ |
Stage 2, plus ML-DSA arithmetic with Montgomery multiplication in the NTT (FIPS 204 Appendix A), conditional reductions and one reduction per coefficient for sums of products. |
The verification results are the same in all three stages; only the speed
changes. The .jsonl files are the raw output (the commit fields of
meta are left out); this page is a summary of those files.
Machine and method
| Gold core | Silver core | |
|---|---|---|
| Files | gold-core6.jsonl |
silver-core2.jsonl |
Core (taskset -c) |
6 (CPU part 0xd0d) |
2 (CPU part 0x805) |
| Max frequency during the runs | 1.08 GHz (capped by the system) | 1.80 GHz |
| Run time, stages 1 / 2 / 3 | 76 s / 67 s / 67 s | 137 s / 123 s / 114 s |
- Machine: the same aarch64 big.LITTLE development machine as
2026-10-14_arm, Linux 4.19.113-27166950, governorschedutil, compilerrustc 1.98.1 (48a229cea 2026-09-01), liboqs 0.16.0 (linked),qanum-pqverify0.1.0. Load and per-core frequency before and after each run are in the.env.txtfiles. - Gold numbers are not comparable with
2026-10-14_arm: during all three runs the system held the Gold cores at 1.08 GHz instead of 2.42 GHz, so every Gold time is about 2.1 times slower. The ratios between verifiers are measured on the same core at the same time and do not depend on that. - liboqs as the yardstick: liboqs is the same in every stage. Its times moved by up to 4 % between runs on the same core (Silver ML-DSA-87: 541 µs, 541 µs, 565 µs), which also moves the percentages.
- One core, one job: each run pinned with
taskset, undernice -n 10. - Command:
QANUM_BENCH_COMMIT=<commit> taskset -c <core> target/release/examples/pq_dual_bench --out <file>.jsonl(cargo build --release --locked --example pq_dual_benchfirst). - Four ways of checking the same valid signatures, interleaved, three
rounds each (so a drift of the machine affects all four alike):
liboqs:OqsVerifier::verify_suite, the node without a second verifier;qanum-pqverify: the second verifier alone;dual enforce:DualVerifierin enforce mode (mainnet, regtest);dual shadow:DualVerifierin shadow mode (testnet).
- Statistics: median of all samples of the three rounds for valid signatures; median of the three round medians for invalid ones. Largest spread between rounds of the same way, stages 1 / 2 / 3: 101 / 174 / 122 basis points (Gold), 72 / 83 / 65 (Silver).
- No disagreement: each run made 28,868 checks with
qanum-pqverifyand the two verifiers never disagreed.
The second verifier alone
Median time per valid signature. The percentage is against liboqs in the
same run.
| Suite | Core | liboqs | Stage 1 | Stage 2 | Stage 3 |
|---|---|---|---|---|---|
| ML-DSA-44 | Gold | 121 µs | 278 µs (230 %) | 267 µs (221 %) | 265 µs (219 %) |
| Silver | 188 to 192 µs | 597 µs (316 %) | 574 µs (305 %) | 424 µs (222 %) | |
| ML-DSA-87 | Gold | 342 µs | 708 µs (208 %) | 667 µs (196 %) | 643 µs (189 %) |
| Silver | 541 to 565 µs | 1.48 ms (275 %) | 1.41 ms (261 %) | 1.06 ms (187 %) | |
| SLH-DSA-128s | Gold | 1.81 to 1.86 ms | 3.45 ms (191 %) | 1.78 ms (98 %) | 1.83 ms (99 %) |
| Silver | 3.02 to 3.13 ms | 5.54 ms (183 %) | 3.02 ms (97 %) | 2.89 ms (96 %) | |
| SLH-DSA-256s | Gold | 3.75 to 3.83 ms | 7.14 ms (191 %) | 3.91 ms (102 %) | 3.89 ms (104 %) |
| Silver | 6.35 to 6.72 ms | 11.65 ms (179 %) | 6.26 ms (99 %) | 6.64 ms (99 %) |
- Stage 2 (SHAKE): SLH-DSA makes thousands of short SHAKE256 calls per
signature (about 2,100 for SLH-DSA-128s and 4,400 for SLH-DSA-256s). The
hashing API of the
sha3crate cost more per call than the Keccak permutation itself; with the sponge in the crate SLH-DSA verifies as fast as liboqs. ML-DSA gains a little (its matrix A is SHAKE128 output). - Stage 3 (ML-DSA arithmetic): the NTT no longer reduces with
%after every addition, subtraction and product, and the hot operations cannot overflow, so the overflow checks of the release profile drop out of them. The gain is large on the Silver core, which executes instructions in order and pays the full latency of each step, and small on the Gold core, which already overlapped that work. - What remains for ML-DSA: about twice liboqs, whose ML-DSA uses
hand-written vector (NEON) assembly.
qanum-pqverifystays portable Rust with no assembly or vector code.
Both verifiers (valid signatures)
| Suite | Core | dual enforce, stage 1 | Stage 2 | Stage 3 |
|---|---|---|---|---|
| ML-DSA-44 | Gold | 400 µs (330 %) | 388 µs (321 %) | 387 µs (319 %) |
| Silver | 799 µs (423 %) | 770 µs (410 %) | 628 µs (328 %) | |
| ML-DSA-87 | Gold | 1.06 ms (310 %) | 1.01 ms (296 %) | 989 µs (290 %) |
| Silver | 2.09 ms (388 %) | 2.01 ms (372 %) | 1.66 ms (295 %) | |
| SLH-DSA-128s | Gold | 5.23 ms (289 %) | 3.55 ms (196 %) | 3.68 ms (198 %) |
| Silver | 8.61 ms (284 %) | 6.14 ms (197 %) | 5.92 ms (197 %) | |
| SLH-DSA-256s | Gold | 10.86 ms (290 %) | 7.81 ms (204 %) | 7.64 ms (204 %) |
| Silver | 18.18 ms (280 %) | 12.63 ms (200 %) | 13.37 ms (200 %) |
A valid signature costs the sum of both verifiers. dual shadow gives the
same times within 1 % in every stage (see the .jsonl files).
Invalid signatures (one byte flipped), stage 3
| Suite | liboqs Gold / Silver | dual enforce Gold / Silver | dual shadow Gold / Silver |
|---|---|---|---|
| ML-DSA-44 | 121.4 µs / 192.2 µs | 121.4 µs / 194.5 µs | 387.2 µs / 628.6 µs |
| ML-DSA-87 | 342.3 µs / 567.6 µs | 342.4 µs / 560.6 µs | 992.0 µs / 1.66 ms |
| SLH-DSA-128s | 1.85 ms / 3.08 ms | 1.88 ms / 3.07 ms | 3.69 ms / 6.04 ms |
| SLH-DSA-256s | 3.76 ms / 6.65 ms | 3.81 ms / 6.64 ms | 7.66 ms / 13.14 ms |
In enforce mode the second verifier is not called when liboqs rejects, so an invalid signature costs what it cost before, in every stage. Shadow mode runs both.
Verify Units under enforce mode
Estimate = ceil(100 × median / median of ML-DSA-44), both through
dual enforce. Gold / Silver.
| Suite | Stage 1 | Stage 2 | Stage 3 | Consensus VU |
|---|---|---|---|---|
| ML-DSA-44 | 100 / 100 | 100 / 100 | 100 / 100 | 100 |
| ML-DSA-87 | 265 / 263 | 261 / 261 | 256 / 265 | 290 |
| SLH-DSA-128s | 1309 / 1078 | 917 / 797 | 951 / 943 | 1600 |
| SLH-DSA-256s | 2719 / 2276 | 2015 / 1640 | 1977 / 2128 | 3600 |
Every constant covers its estimate in every stage. ML-DSA-87 has the least room (265 of 290).
Worst-case block estimate
A block holding as many signatures of one suite as MAX_BLOCK_VERIFY_UNITS
(200,000) allows, times the median of one signature, on one core. This is
an estimate, not a measured block. Block validation spreads signatures over
all cores, so a node with several cores takes less.
| Suite (signatures) | liboqs Gold / Silver | dual, stage 1 | dual, stage 3 |
|---|---|---|---|
| ML-DSA-44 (2000) | 243 ms / 384 ms | 799 ms / 1,598 ms | 774 ms / 1,257 ms |
| ML-DSA-87 (689) | 236 ms / 390 ms | 728 ms / 1,444 ms | 682 ms / 1,145 ms |
| SLH-DSA-128s (125) | 233 ms / 378 ms | 654 ms / 1,077 ms | 460 ms / 741 ms |
| SLH-DSA-256s (55) | 207 ms / 370 ms | 598 ms / 1,000 ms | 421 ms / 736 ms |
liboqs columns from stage 3. On the slowest core the worst block goes from about 0.4 s of signature checks with liboqs alone to about 1.3 s with both verifiers (1.6 s in stage 1).