From e9aea318f5b9bbb7a2cba55b2829378d26bd4245 Mon Sep 17 00:00:00 2001 From: mjthatch Date: Thu, 12 Nov 2026 15:43:19 +0000 Subject: [PATCH 1/6] site snapshot: read popovers and table bodies Popover text is hidden until opened, so it is read regardless of visibility. Header rows are skipped. Re-recorded against the unchanged pages. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QabhpThuWQEq1AZRZo24vm --- tests/site_snapshot.js | 17 +++++++++++------ tests/site_snapshot.json | 5 ----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/site_snapshot.js b/tests/site_snapshot.js index 0a7add7..29f0ba2 100644 --- a/tests/site_snapshot.js +++ b/tests/site_snapshot.js @@ -2,8 +2,8 @@ // Checks that the site pages still show the numbers recorded in tests/site_snapshot.json. // // Each page is opened in Chromium for a fixed list of share links. The script -// reads the result tables, info lines and inputs, and compares their numbers at the -// precision shown. +// reads the result tables, info lines, popovers and inputs, and compares their numbers +// at the precision shown. // // node tests/site_snapshot.js compare; exit 1 if a number changed // node tests/site_snapshot.js --update record what the pages show now @@ -39,7 +39,7 @@ const STATES = { const REGIONS = { 'index.html': ['#stateless-table', '#xmss-table', '#uxmss-table', '.tuple'], 'stateless.html': ['#initial-info', '#funnel', '#winner', '#custom-status', '#custom-result'], - 'stateful.html': ['#schnorr-ref', '#compare-table', '#sweep-info', '#sweep-table'], + 'stateful.html': ['#compare-table', '#sweep-info', '#sweep-table'], }; const MAX_ROWS = 25; @@ -49,13 +49,13 @@ function readPage({ regions, maxRows }) { const shown = el => el.getClientRects().length > 0 && getComputedStyle(el).visibility !== 'hidden'; // Visible text with spaces around elements ("5540.00×" reads as two numbers) // and "^" before superscripts (240 reads as 2^40). - const textOf = (root, skipTables) => { + const textOf = (root, skipTables, hiddenToo = false) => { let s = ''; (function walk(node) { for (const n of node.childNodes) { if (n.nodeType === Node.TEXT_NODE) { s += n.nodeValue; - } else if (n.nodeType === Node.ELEMENT_NODE && shown(n) && !n.matches('script, style') && + } else if (n.nodeType === Node.ELEMENT_NODE && (hiddenToo || shown(n)) && !n.matches('script, style') && !(skipTables && n.tagName === 'TABLE')) { s += n.tagName === 'SUP' ? ' ^' : ' '; walk(n); @@ -78,7 +78,7 @@ function readPage({ regions, maxRows }) { if (text) out[name] = text; const tables = el.tagName === 'TABLE' ? [el] : Array.from(el.querySelectorAll('table')); let r = 0; - for (const tr of tables.flatMap(t => Array.from(t.rows))) { + for (const tr of tables.flatMap(t => Array.from(t.tBodies).flatMap(b => Array.from(b.rows)))) { if (r === maxRows) break; if (!shown(tr)) continue; const cells = Array.from(tr.cells).filter(shown).map(c => textOf(c, false)); @@ -86,6 +86,11 @@ function readPage({ regions, maxRows }) { } }); } + // Popovers stay hidden until opened. + for (const el of document.querySelectorAll('[popover][id]')) { + const text = textOf(el, false, true); + if (/\d/.test(text)) out[`popover #${el.id}`] = text; + } // Inputs carry the state a share link restores, so hidden ones count too. for (const el of document.querySelectorAll('input[id], select[id]')) { if (el.type === 'range') continue; // mirrors the number box next to it diff --git a/tests/site_snapshot.json b/tests/site_snapshot.json index 91a31d7..0a93ff9 100644 --- a/tests/site_snapshot.json +++ b/tests/site_snapshot.json @@ -325,7 +325,6 @@ "input #cust-w": "16" }, "stateful.html": { - "#schnorr-ref": "The ρ ÷ Schnorr columns compare average-case C/B against BIP340 Schnorr verification ≈ 1.98 C/B (libsecp256k1, single verify over a 64 B wire signature; bip340-vs-sha256 : built-in 1.98 C/B — live kappa.json unreachable). SLH-DSA-128s baseline: 0.279 C/B = 0.141× Schnorr.", "#compare-table row 0": ["SLH-DSA-128s","7,856 B","292.4K","2.22M","—","2.2K","0.279","0.496","0.141×","2^64",""], "#compare-table row 1": ["UXMSS (WOTS, w=16) sig grows with index — worst case shown (255 auth nodes); capacity 256 sigs","594–4,665 B 0.59×","146.2K 0.50×","554 0.00×","4,096 B","536 0.24×","0.475→0.115 0.41×","0.884→0.167 0.34×","0.240→0.058×","256","×"], "#compare-table row 2": ["UXMSS (WOTS+C, w=16) sig grows with index — worst case shown (255 auth nodes); capacity 256 sigs","548–4,619 B 0.59×","133.6K 0.46×","599 0.00×","4,096 B","509 0.23×","0.465→0.110 0.39×","0.465→0.110 0.22×","0.235→0.056×","256","×"], @@ -378,7 +377,6 @@ "input #sw-W-cb-val": "1.0" }, "stateful.html#m=cold": { - "#schnorr-ref": "The ρ ÷ Schnorr columns compare average-case C/B against BIP340 Schnorr verification ≈ 1.98 C/B (libsecp256k1, single verify over a 64 B wire signature; bip340-vs-sha256 : built-in 1.98 C/B — live kappa.json unreachable). SLH-DSA-128s baseline: 0.279 C/B = 0.141× Schnorr.", "#compare-table row 0": ["SLH-DSA-128s","7,856 B","292.4K","2.22M","—","2.2K","0.279","0.496","0.141×","2^64",""], "#compare-table row 1": ["UXMSS (WOTS, w=16) sig grows with index — worst case shown (255 auth nodes); capacity 256 sigs","594–4,665 B 0.59×","146.2K 0.50×","145.9K 0.07×","—","536 0.24×","0.475→0.115 0.41×","0.884→0.167 0.34×","0.240→0.058×","256","×"], "#compare-table row 2": ["UXMSS (WOTS+C, w=16) sig grows with index — worst case shown (255 auth nodes); capacity 256 sigs","548–4,619 B 0.59×","133.6K 0.46×","133.5K 0.06×","—","509 0.23×","0.465→0.110 0.39×","0.465→0.110 0.22×","0.235→0.056×","256","×"], @@ -431,7 +429,6 @@ "input #sw-W-cb-val": "1.0" }, "stateful.html#b=0.5,20,20,2,20,20&w=5,1,1,2,1&sort=size.asc": { - "#schnorr-ref": "The ρ ÷ Schnorr columns compare average-case C/B against BIP340 Schnorr verification ≈ 1.98 C/B (libsecp256k1, single verify over a 64 B wire signature; bip340-vs-sha256 : built-in 1.98 C/B — live kappa.json unreachable). SLH-DSA-128s baseline: 0.279 C/B = 0.141× Schnorr.", "#compare-table row 0": ["SLH-DSA-128s","7,856 B","292.4K","2.22M","—","2.2K","0.279","0.496","0.141×","2^64",""], "#compare-table row 1": ["UXMSS (WOTS, w=16) sig grows with index — worst case shown (255 auth nodes); capacity 256 sigs","594–4,665 B 0.59×","146.2K 0.50×","554 0.00×","4,096 B","536 0.24×","0.475→0.115 0.41×","0.884→0.167 0.34×","0.240→0.058×","256","×"], "#compare-table row 2": ["UXMSS (WOTS+C, w=16) sig grows with index — worst case shown (255 auth nodes); capacity 256 sigs","548–4,619 B 0.59×","133.6K 0.46×","599 0.00×","4,096 B","509 0.23×","0.465→0.110 0.39×","0.465→0.110 0.22×","0.235→0.056×","256","×"], @@ -484,7 +481,6 @@ "input #sw-W-cb-val": "1.0" }, "stateful.html#b=1,20,20,1.5,20,30&w=5,1,1,2,1": { - "#schnorr-ref": "The ρ ÷ Schnorr columns compare average-case C/B against BIP340 Schnorr verification ≈ 1.98 C/B (libsecp256k1, single verify over a 64 B wire signature; bip340-vs-sha256 : built-in 1.98 C/B — live kappa.json unreachable). SLH-DSA-128s baseline: 0.279 C/B = 0.141× Schnorr.", "#compare-table row 0": ["SLH-DSA-128s","7,856 B","292.4K","2.22M","—","2.2K","0.279","0.496","0.141×","2^64",""], "#compare-table row 1": ["UXMSS (WOTS, w=16) sig grows with index — worst case shown (255 auth nodes); capacity 256 sigs","594–4,665 B 0.59×","146.2K 0.50×","554 0.00×","4,096 B","536 0.24×","0.475→0.115 0.41×","0.884→0.167 0.34×","0.240→0.058×","256","×"], "#compare-table row 2": ["UXMSS (WOTS+C, w=16) sig grows with index — worst case shown (255 auth nodes); capacity 256 sigs","548–4,619 B 0.59×","133.6K 0.46×","599 0.00×","4,096 B","509 0.23×","0.465→0.110 0.39×","0.465→0.110 0.22×","0.235→0.056×","256","×"], @@ -537,7 +533,6 @@ "input #sw-W-cb-val": "1.0" }, "stateful.html#cand=uxmss-WC-64,xmssmt-WC-64-20-1": { - "#schnorr-ref": "The ρ ÷ Schnorr columns compare average-case C/B against BIP340 Schnorr verification ≈ 1.98 C/B (libsecp256k1, single verify over a 64 B wire signature; bip340-vs-sha256 : built-in 1.98 C/B — live kappa.json unreachable). SLH-DSA-128s baseline: 0.279 C/B = 0.141× Schnorr.", "#compare-table row 0": ["SLH-DSA-128s","7,856 B","292.4K","2.22M","—","2.2K","0.279","0.496","0.141×","2^64",""], "#compare-table row 1": ["UXMSS (WOTS+C, w=64) sig grows with index — worst case shown (255 auth nodes); capacity 256 sigs","388–4,459 B 0.57×","362.2K 1.24×","1.2K 0.00×","4,096 B","959 0.44×","1.817→0.215 0.77×","1.817→0.215 0.43×","0.918→0.109×","256","×"], "#compare-table row 2": ["XMSS-2^20 (WOTS+C, w=64, d=1)","694 B 0.09×","1.48G 5.1e+3×","15.1K 0.01×","1,056 B","724 0.33×","1.043 3.74×","1.043 2.11×","0.527×","2^20","×"], From 77076109fa0798e5c53486f500bb0abc2ae3d8ff Mon Sep 17 00:00:00 2001 From: mjthatch Date: Thu, 12 Nov 2026 15:43:37 +0000 Subject: [PATCH 2/6] site: add shared number formats, popovers and glossary js/ui.js and style.css, used by the pages from the next commits on. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QabhpThuWQEq1AZRZo24vm --- site/js/ui.js | 102 +++++++++++++++++++++++++++++++++++++++++++++++++ site/style.css | 54 ++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 site/js/ui.js create mode 100644 site/style.css diff --git a/site/js/ui.js b/site/js/ui.js new file mode 100644 index 0000000..1deb126 --- /dev/null +++ b/site/js/ui.js @@ -0,0 +1,102 @@ +// Number formats, popovers and the glossary. +'use strict'; + +// Number formats + +// Compressions and bytes: exact up to six digits, compact above, with the exact value on hover. Returns HTML. +function fmtNum(n) { + if (!isFinite(n)) return '—'; + n = Math.round(n); + const exact = n.toLocaleString('en-US'); + if (n < 1e6) return exact; + for (const [unit, size] of [['M', 1e6], ['G', 1e9], ['T', 1e12], ['P', 1e15]]) { + const v = (n / size).toPrecision(3); + if (Number(v) < 1000) return `${v}${unit}`; + } + return `${n.toExponential(2)}`; +} + +// Ratios to a reference value, to three significant digits. +function fmtRatio(r) { + if (r >= 100) return Math.round(r).toLocaleString('en-US') + '×'; + return r.toPrecision(3) + '×'; +} + +// ρ, in compressions per signature byte. +function fmtRho(r) { + return r.toFixed(4); +} + +function fmtPow2(e) { + return `2${e}`; +} + +// Popovers: opens +//
…
. The browser opens and closes the popover; +// this code only places it next to its button. + +function infoButton(id, label) { + return ``; +} + +(function () { + if (!('popover' in HTMLElement.prototype)) return; // older browsers show the content inline + let opener = null; + document.addEventListener('click', e => { + const b = e.target.closest && e.target.closest('[popovertarget]'); + if (b) opener = b; + }, true); + + function place(pop) { + const btn = opener && opener.isConnected && opener.getAttribute('popovertarget') === pop.id + ? opener : document.querySelector(`[popovertarget="${pop.id}"]`); + if (!btn) return; + const r = btn.getBoundingClientRect(), gap = 6, edge = 8; + const left = Math.max(edge, Math.min(r.left, innerWidth - pop.offsetWidth - edge)); + let top = r.bottom + gap; + if (top + pop.offsetHeight > innerHeight - edge && r.top - gap - pop.offsetHeight >= edge) { + top = r.top - gap - pop.offsetHeight; + } + pop.style.left = `${left}px`; + pop.style.top = `${Math.max(edge, top)}px`; + } + + // toggle does not bubble, so listen in the capture phase. + document.addEventListener('toggle', e => { + if (e.newState === 'open' && e.target.classList.contains('pop')) place(e.target); + }, true); + const placeOpen = () => document.querySelectorAll('.pop:popover-open').forEach(place); + addEventListener('resize', placeOpen); + addEventListener('scroll', placeOpen, true); +})(); + +// Glossary + +document.body.insertAdjacentHTML('beforeend', ` +
+

Glossary

+
+
C
+
One SHA-256 compression. Costs count compressions; the PK.seed block is compressed once per key and reused.
+
B
+
Byte.
+
KeyGen, SigGen, SigVer
+
Compressions to generate a key pair, to sign a message and to verify a signature. SigVer is the average unless marked worst.
+
ρ
+
SigVer per signature byte, in C/B.
+
ρ ÷ Schnorr
+
ρ divided by the ρ of a BIP340 Schnorr verification (libsecp256k1, 64-byte signature).
+
SLH-DSA-128s
+
The FIPS 205 parameter set (h, d, k, a, w) = (63, 7, 14, 12, 16). Ratios (×) compare against it.
+
h, d, k, a, w
+
Hypertree height, number of layers, FORS trees, FORS tree height, Winternitz parameter. h′ = h/d is the height of one layer.
+
qs
+
The number of signatures per key that security is computed for: 240 for the stateless schemes.
+
WOTS, WOTS+C
+
Winternitz one-time signatures. WOTS (as in SLH-DSA) has checksum chains; WOTS+C replaces them with a counter that the signer grinds until the message digits have a fixed sum, so verification always costs the same.
+
XMSS, XMSS-MT, UXMSS
+
Stateful schemes: one balanced Merkle tree (BXMSS in SHRINCS), d layers of such trees, and the unbalanced SHRINCS tree, whose signatures grow by one node per signature.
+
Cached, cold
+
A cached signer keeps tree state between signatures (a BDS state for XMSS, a leaf cache for UXMSS); a cold signer keeps none and rebuilds its trees for every signature.
+
+
`); diff --git a/site/style.css b/site/style.css new file mode 100644 index 0000000..b34c2a4 --- /dev/null +++ b/site/style.css @@ -0,0 +1,54 @@ +/* Shared by the three site pages. Colours come from each page's :root. */ + +/* Page header */ +.crumbs { margin-bottom: 4px; font-size: 12px; color: var(--text-mute); } +.crumbs a { color: var(--accent); text-decoration: none; } +.crumbs a:hover { text-decoration: underline; } +.text-btn { + padding: 0; border: 0; background: none; font: inherit; + color: var(--accent); cursor: pointer; +} +.text-btn:hover { text-decoration: underline; } + +/* info button */ +.info { + display: inline-grid; place-items: center; + width: 15px; height: 15px; margin-left: 5px; padding: 0; + border: 1px solid currentColor; border-radius: 50%; + background: none; color: var(--text-mute); + font: italic 600 10px/1 Georgia, 'Times New Roman', serif; + letter-spacing: 0; text-transform: none; + vertical-align: 1px; cursor: pointer; +} +.info:hover, .info:focus-visible { color: var(--accent); } + +/* Popover: placed next to its button by js/ui.js. Inherited table-header styles are reset. */ +.pop { + position: fixed; inset: auto; margin: 0; + width: max-content; max-width: min(24rem, calc(100vw - 16px)); + max-height: calc(100vh - 16px); overflow: auto; + padding: 10px 12px; border: 1px solid var(--border); border-radius: 6px; + background: var(--panel); color: var(--text); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); + font: 400 13px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + text-align: left; text-transform: none; letter-spacing: normal; white-space: normal; +} +.pop-wide { max-width: min(34rem, calc(100vw - 16px)); } +.pop p { margin: 0; } +.pop p + p, .pop p + ul, .pop ul + p { margin-top: 6px; } +.pop ul { margin: 0; padding-left: 18px; } +.pop h3 { margin: 0 0 6px; font-size: 13px; font-weight: 600; } +.pop a { color: var(--accent); } +.pop code { font-family: var(--mono); font-size: 12px; } + +/* Glossary */ +.pop dl { margin: 0; display: grid; grid-template-columns: max-content 1fr; gap: 4px 12px; } +.pop dt { font-weight: 600; white-space: nowrap; } +.pop dd { margin: 0; color: var(--text-soft); } +@media (max-width: 520px) { + .pop dl { grid-template-columns: 1fr; gap: 0; } + .pop dd { margin-bottom: 6px; } +} + +/* Names whose case matters */ +.keep-case { text-transform: none; } From 5d347206d31218c41bf83ee5e6c689e41860468f Mon Sep 17 00:00:00 2001 From: mjthatch Date: Thu, 12 Nov 2026 15:44:07 +0000 Subject: [PATCH 3/6] index.html: popovers, metric names, number format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The footer moves into card popovers and the glossary. Rows use KeyGen, SigGen, SigVer and ρ with the unit in the label. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QabhpThuWQEq1AZRZo24vm --- site/index.html | 106 ++++++++++++++++++++------------------- tests/site_snapshot.json | 59 ++++++++++++---------- 2 files changed, 87 insertions(+), 78 deletions(-) diff --git a/site/index.html b/site/index.html index 6b112b0..6ac4701 100644 --- a/site/index.html +++ b/site/index.html @@ -4,9 +4,10 @@ - Pinned parameters for SHRINCS + SHRINCS pinned parameters +