KXCO measurement record: why use ML-DSA-87 Published with https://kxco.ai/developers/blog/why-use-ml-dsa-87 Measured on 7 October 2026 by KXCO engineering. Machine: Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz, 8 logical cores, Windows 10 Pro, x64 Runtime: Node.js v26.1.0 Library: kxco-post-quantum 1.8.0 from npm, backend OpenSSL 3.5.6 through node:crypto Chain: public endpoint https://chain.kxco.ai/rpc, eth_call only, no account Sizes are from FIPS 204 Table 2. Repetitions are from FIPS 204 Table 1. 1. Chain verdicts and a first benchmark (why87-facts.mjs) ---------------------------------------------------------- Script: // Live facts for the "why ML-DSA-87" dev blog, 7 Oct 2026. Public endpoints and published npm packages only. import { mlDsa as mlDsa65, mlDsa87, backend } from 'kxco-post-quantum' import { performance } from 'node:perf_hooks' const hex = b => Buffer.from(b).toString('hex') const rpc = async (method, params) => (await (await fetch('https://chain.kxco.ai/rpc', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }), })).json()).result const P87 = '0x4b58434f00000000000000000000000000000087', P65 = '0x000000000000000000000000000000000000000b' const master = crypto.getRandomValues(new Uint8Array(32)) const k87 = mlDsa87.keypairFromMaster(master, 'why87-facts-87') const k65 = mlDsa65.keypairFromMaster(master, 'why87-facts-65') const msg = new TextEncoder().encode('title deed 1187, transferred 7 October 2026') const s87 = mlDsa87.sign(k87.secretKey, msg), s65 = mlDsa65.sign(k65.secretKey, msg) console.log('sizes 87 pk', k87.publicKey.length, 'sig', s87.length / 2, '| 65 pk', k65.publicKey.length, 'sig', s65.length / 2) const bad = msg.slice(); bad[11] ^= 1 console.log('block ', parseInt(await rpc('eth_blockNumber', []), 16)) console.log('87 valid', await rpc('eth_call', [{ to: P87, data: '0x' + hex(k87.publicKey) + s87 + hex(msg) }, 'latest'])) console.log('87 1bit ', await rpc('eth_call', [{ to: P87, data: '0x' + hex(k87.publicKey) + s87 + hex(bad) }, 'latest'])) console.log('65 valid', await rpc('eth_call', [{ to: P65, data: '0x' + hex(k65.publicKey) + s65 + hex(msg) }, 'latest'])) console.log('87 at 65', await rpc('eth_call', [{ to: P65, data: '0x' + hex(k87.publicKey) + s87 + hex(msg) }, 'latest'])) // in-process benchmark, same library, same machine, median of N const N = 200 const med = a => a.sort((x, y) => x - y)[a.length >> 1] function bench(label, fn) { for (let i = 0; i < 20; i++) fn() const t = [] for (let i = 0; i < N; i++) { const a = performance.now(); fn(); t.push(performance.now() - a) } console.log('bench', label, med(t).toFixed(3), 'ms median of', N) } let i = 0 bench('87 keygen', () => mlDsa87.keypairFromMaster(master, 'b' + i++)) bench('65 keygen', () => mlDsa65.keypairFromMaster(master, 'b' + i++)) bench('87 sign ', () => mlDsa87.sign(k87.secretKey, msg)) bench('65 sign ', () => mlDsa65.sign(k65.secretKey, msg)) bench('87 verify', () => mlDsa87.verify(k87.publicKey, msg, s87)) bench('65 verify', () => mlDsa65.verify(k65.publicKey, msg, s65)) console.log('backend', typeof backend === 'function' ? backend() : backend, 'node', process.version, process.platform, process.arch) Output: sizes 87 pk 2592 sig 4627 | 65 pk 1952 sig 3309 block 5005334 87 valid 0x0000000000000000000000000000000000000000000000000000000000000001 87 1bit 0x0000000000000000000000000000000000000000000000000000000000000000 65 valid 0x0000000000000000000000000000000000000000000000000000000000000001 87 at 65 0x0000000000000000000000000000000000000000000000000000000000000000 bench 87 keygen 4.790 ms median of 200 bench 65 keygen 2.654 ms median of 200 bench 87 sign 2.087 ms median of 200 bench 65 sign 2.379 ms median of 200 bench 87 verify 1.187 ms median of 200 bench 65 verify 0.880 ms median of 200 backend { kind: 'openssl', library: 'node:crypto', openssl: '3.5.6', parameterSets: [ 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', 'ML-KEM-1024', 'ML-KEM-512', 'ML-KEM-768', 'SLH-DSA-SHA2-128f', 'SLH-DSA-SHA2-192s', 'SLH-DSA-SHAKE-256f' ] } node v26.1.0 win32 x64 bench 87 keygen 4.951 ms median of 200 bench 65 keygen 2.978 ms median of 200 bench 87 sign 2.781 ms median of 200 bench 65 sign 1.756 ms median of 200 bench 87 verify 1.034 ms median of 200 bench 65 verify 0.451 ms median of 200 bench 87 keygen 3.611 ms median of 200 bench 65 keygen 2.793 ms median of 200 bench 87 sign 1.873 ms median of 200 bench 65 sign 1.474 ms median of 200 bench 87 verify 0.680 ms median of 200 bench 65 verify 0.447 ms median of 200 The block benchmark above ran three times (the last two runs print only their bench lines). It runs each operation in a block of its own and drifted between runs, so the figures quoted in the article come from the interleaved benchmark below. 2. Interleaved benchmark (why87-bench.mjs), three runs ------------------------------------------------------- Script: // Interleaved benchmark: ML-DSA-65 and ML-DSA-87 alternate each iteration so machine drift hits both equally. import { mlDsa as m65, mlDsa87 as m87 } from 'kxco-post-quantum' import { performance } from 'node:perf_hooks' const N = 3000, master = crypto.getRandomValues(new Uint8Array(32)) const k = { 65: m65.keypairFromMaster(master, 'bench-65'), 87: m87.keypairFromMaster(master, 'bench-87') } const m = { 65: m65, 87: m87 } const msgs = Array.from({ length: 64 }, () => crypto.getRandomValues(new Uint8Array(256))) const sig = { 65: msgs.map(x => m65.sign(k[65].secretKey, x)), 87: msgs.map(x => m87.sign(k[87].secretKey, x)) } const t = { s65: [], s87: [], v65: [], v87: [] } for (let i = 0; i < 200; i++) for (const p of [65, 87]) { m[p].sign(k[p].secretKey, msgs[i & 63]); m[p].verify(k[p].publicKey, msgs[i & 63], sig[p][i & 63]) } for (let i = 0; i < N; i++) for (const p of (i & 1 ? [65, 87] : [87, 65])) { const x = msgs[i & 63] let a = performance.now(); m[p].sign(k[p].secretKey, x); t['s' + p].push(performance.now() - a) a = performance.now(); m[p].verify(k[p].publicKey, x, sig[p][i & 63]); t['v' + p].push(performance.now() - a) } const q = (a, f) => a.slice().sort((x, y) => x - y)[Math.floor(a.length * f)].toFixed(3) for (const [n, a] of Object.entries(t)) console.log(n, 'median', q(a, .5), 'p90', q(a, .9), 'ms, n =', a.length) Output, three consecutive runs: s65 median 1.775 p90 3.910 ms, n = 3000 s87 median 2.256 p90 4.696 ms, n = 3000 v65 median 0.876 p90 1.301 ms, n = 3000 v87 median 1.179 p90 1.742 ms, n = 3000 s65 median 1.697 p90 3.751 ms, n = 3000 s87 median 2.127 p90 4.466 ms, n = 3000 v65 median 0.843 p90 1.185 ms, n = 3000 v87 median 1.122 p90 1.583 ms, n = 3000 s65 median 1.972 p90 4.514 ms, n = 3000 s87 median 2.491 p90 5.459 ms, n = 3000 v65 median 0.967 p90 1.528 ms, n = 3000 v87 median 1.289 p90 2.066 ms, n = 3000 3. Hedged signing ----------------- Two signatures of one message under one key, compared: two signatures of one message identical: false Hedged ML-DSA mixes fresh randomness into each signature, so the two differ.