class-v6: merge master 3a7592c52 (after the counter-asic-4 code revert eb2c21c0) into class-v6 so the frozen tree lands on master as one two-parent merge. Conflicts: igneum-pow memhard.rs, main.rs, tests/packs.rs keep class-v6's text (class v5's mixer-draw rule in the agreed AP-F4-1 form, cost 205 with w32; master carried the first form, NAF sum 163; the ds55 and reg64 arms of epoch_of; the window tests); site/litepaper.html takes master's (the site lane's text); docs/ledger-public.md and site/ledger.html regenerated from the merged docs/fud-ledger-2.0.md by tools/ledger/export-public.mjs and tools/ledger-page.mjs.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
c23f687bc0
507 changed files with 116680 additions and 4764 deletions
1794
app/igneum-app/tiers/class-v5-tiers.json
Normal file
1794
app/igneum-app/tiers/class-v5-tiers.json
Normal file
File diff suppressed because it is too large
Load diff
100
app/igneum-app/tiers/class-v5-tiers.mjs
Normal file
100
app/igneum-app/tiers/class-v5-tiers.mjs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// The class v5 tiers table (app/igneum-app/tiers/class-v5-tiers.json): the loader and the validator the test and the
|
||||
// app's packaging read it through. The rows are in the shape src/ember.rs tier_from_json reads back from a card's state
|
||||
// (id, clock_mhz, power_pct, mem_mhz, limit_w, mhs, w), so a card whose own search has not run can be set to a tier
|
||||
// from this table by /api/tune/tier, and the search replaces the row when it runs. node --test class-v5-tiers.test.mjs
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
export const TIER_IDS = ['efficiency', 'balanced', 'max'];
|
||||
export const LABELS = ['measured', 'estimated'];
|
||||
export const SOURCES = ['measured', 'stock'];
|
||||
export const VENDORS = ['nvidia', 'amd', 'apple', 'intel'];
|
||||
// the fields tier_from_json reads, every one a number
|
||||
export const ROW_FIELDS = ['clock_mhz', 'power_pct', 'mem_mhz', 'limit_w', 'mhs', 'w'];
|
||||
// the brief's card classes (floor lane 4, 8 October 2026): every one must have an entry
|
||||
export const REQUIRED_CARDS = ['5090', '5080', '5070 Ti', '5070', '5060 Ti', '5060', '4090', '4080', '4070', '3090', '3080', '3070', '3060',
|
||||
'9070 XT', '7900 XTX', '7800 XT', '7600 XT', '9060 XT', 'H100', 'L40S', 'A100', 'B580', 'A750', 'M5 Max', 'M4 Max', 'M4 Pro', 'M3 Max'];
|
||||
|
||||
export function loadTable(path) {
|
||||
const p = path || join(dirname(fileURLToPath(import.meta.url)), 'class-v5-tiers.json');
|
||||
return JSON.parse(readFileSync(p, 'utf8'));
|
||||
}
|
||||
|
||||
/** Every fault in the table as a list of strings; an empty list is a valid table. */
|
||||
export function validate(t) {
|
||||
const faults = [];
|
||||
const f = (s) => faults.push(s);
|
||||
if (t.class !== 'v5') f(`class is ${t.class}, not v5`);
|
||||
if (!Array.isArray(t.tier_ids) || t.tier_ids.join() !== TIER_IDS.join()) f('tier_ids must be efficiency, balanced, max');
|
||||
for (const k of ['stale_when', 'on_flip', 'measured_flip', 'period']) if (!t.remeasure_rule || typeof t.remeasure_rule[k] !== 'string' || !t.remeasure_rule[k]) f(`remeasure_rule.${k} missing`);
|
||||
if (!t.v5_over_v4 || typeof t.v5_over_v4.watts_pct !== 'number') f('v5_over_v4.watts_pct missing');
|
||||
if (!Array.isArray(t.cards) || t.cards.length === 0) { f('cards missing'); return faults; }
|
||||
const seen = new Set();
|
||||
for (const c of t.cards) {
|
||||
const name = c.card || '(unnamed)';
|
||||
if (seen.has(name)) f(`${name}: listed twice`);
|
||||
seen.add(name);
|
||||
if (!VENDORS.includes(c.vendor)) f(`${name}: vendor ${c.vendor}`);
|
||||
if (!LABELS.includes(c.label)) f(`${name}: label ${c.label}`);
|
||||
if (!Array.isArray(c.match) || c.match.length === 0) f(`${name}: no match list`);
|
||||
if (typeof c.src !== 'string' || !c.src) f(`${name}: no src`);
|
||||
if (!c.stock || typeof c.stock.uj !== 'number' || c.stock.uj <= 0) f(`${name}: stock.uj missing`);
|
||||
if (!Array.isArray(c.tiers) || c.tiers.length === 0) { f(`${name}: no tiers`); continue; }
|
||||
const ids = c.tiers.map((r) => r.id);
|
||||
for (const id of ids) if (!TIER_IDS.includes(id)) f(`${name}: tier id ${id}`);
|
||||
if (new Set(ids).size !== ids.length) f(`${name}: a tier id repeats`);
|
||||
const lever = c.vendor === 'nvidia' || c.vendor === 'amd';
|
||||
if (lever && ids.join() !== TIER_IDS.join()) f(`${name}: a card with a lever carries all three tiers in order`);
|
||||
if (!lever && ids.join() !== 'max') f(`${name}: a card with no lever carries the max tier only`);
|
||||
for (const r of c.tiers) {
|
||||
for (const k of ROW_FIELDS) if (typeof r[k] !== 'number' || !Number.isFinite(r[k])) f(`${name}/${r.id}: ${k} is not a number`);
|
||||
if (typeof r.power_pct === 'number' && (r.power_pct < 50 || r.power_pct > 100)) f(`${name}/${r.id}: power_pct ${r.power_pct} outside 50 to 100`);
|
||||
if (typeof r.clock_mhz === 'number' && (r.clock_mhz < 0 || r.clock_mhz > 4000)) f(`${name}/${r.id}: clock_mhz ${r.clock_mhz}`);
|
||||
if (!LABELS.includes(r.label)) f(`${name}/${r.id}: label ${r.label}`);
|
||||
if (!SOURCES.includes(r.source)) f(`${name}/${r.id}: source ${r.source}`);
|
||||
if (typeof r.uj !== 'number' || r.uj <= 0) f(`${name}/${r.id}: uj missing`);
|
||||
if (typeof r.note !== 'string' || !r.note) f(`${name}/${r.id}: no note`);
|
||||
if (r.mhs > 0 && r.w > 0 && r.uj > 0 && Math.abs(r.w / r.mhs - r.uj) / r.uj > 0.025) f(`${name}/${r.id}: uj ${r.uj} is not w over mhs (${(r.w / r.mhs).toFixed(2)})`);
|
||||
if (r.mhs > 0 && r.w > 0 && typeof r.mhw === 'number' && Math.abs(r.mhs / r.w - r.mhw) / r.mhw > 0.025) f(`${name}/${r.id}: mhw ${r.mhw} is not mhs over w`);
|
||||
if (r.id === 'max' && r.source !== 'stock') f(`${name}/max: the max tier is the stock row`);
|
||||
if (r.id === 'max' && (r.clock_mhz !== 0 || r.power_pct !== 100)) f(`${name}/max: stock is unlocked at 100 percent`);
|
||||
if (c.vendor === 'amd' && (typeof r.core_offset_mhz !== 'number' || typeof r.power_offset_pct !== 'number')) f(`${name}/${r.id}: an AMD row carries core_offset_mhz and power_offset_pct`);
|
||||
if (c.vendor === 'amd' && r.core_offset_mhz > 0) f(`${name}/${r.id}: an AMD core offset never raises the clock`);
|
||||
}
|
||||
const by = Object.fromEntries(c.tiers.map((r) => [r.id, r]));
|
||||
if (by.efficiency && by.max && by.efficiency.uj > by.max.uj) f(`${name}: efficiency costs more per hash than stock`);
|
||||
if (by.efficiency && by.balanced && by.balanced.uj + 1e-9 < by.efficiency.uj) f(`${name}: balanced is cheaper per hash than efficiency`);
|
||||
if (by.balanced && by.max && by.balanced.mhs > 0 && by.max.mhs > 0 && by.balanced.mhs < by.max.mhs * 0.98) f(`${name}: balanced gives up over 2 percent of the top rate`);
|
||||
if (c.label === 'measured' && !c.tiers.some((r) => r.label === 'measured' && r.source === 'measured') && lever) f(`${name}: a measured card has no measured tuned row`);
|
||||
}
|
||||
for (const want of REQUIRED_CARDS) if (!t.cards.some((c) => c.card.includes(want))) f(`no entry for ${want}`);
|
||||
return faults;
|
||||
}
|
||||
|
||||
/** The entry a card name matches (the first whose match list hits; the longer match wins, so "5070 Ti" beats "5070"). */
|
||||
export function entryFor(t, cardName) {
|
||||
const n = cardName.toUpperCase();
|
||||
let best = null, bestLen = 0;
|
||||
for (const c of t.cards) for (const m of c.match) if (n.includes(m.toUpperCase()) && m.length > bestLen) { best = c; bestLen = m.length; }
|
||||
return best;
|
||||
}
|
||||
|
||||
/** The tier row a card starts from, in the shape tier_from_json reads; null when the table has none. */
|
||||
export function tierRow(t, cardName, id) {
|
||||
const c = entryFor(t, cardName);
|
||||
if (!c) return null;
|
||||
return c.tiers.find((r) => r.id === id) || null;
|
||||
}
|
||||
|
||||
/** The re-measure verdict the app applies on a class flip (src/ember.rs tiers_stale): stale when both classes are known and differ. */
|
||||
export function stale(tiersClass, programClass) {
|
||||
return Boolean(tiersClass) && Boolean(programClass) && tiersClass !== programClass;
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
||||
const t = loadTable();
|
||||
const faults = validate(t);
|
||||
if (faults.length) { console.error(faults.join('\n')); process.exit(1); }
|
||||
console.log(`class ${t.class}: ${t.cards.length} card classes, ${t.cards.filter((c) => c.label === 'measured').length} measured`);
|
||||
}
|
||||
109
app/igneum-app/tiers/class-v5-tiers.test.mjs
Normal file
109
app/igneum-app/tiers/class-v5-tiers.test.mjs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
// node --test app/igneum-app/tiers/class-v5-tiers.test.mjs
|
||||
// The class v5 tiers table: the shipped file validates; the measured rows carry the record's numbers; the match rule
|
||||
// picks the longer name; a class flip reads stale; and the known-failed cases (a bad power rung, a missing field, a
|
||||
// tuned row dearer than stock, a card class left out) are refused, so a stale or broken table cannot pass.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { loadTable, validate, entryFor, tierRow, stale, REQUIRED_CARDS, ROW_FIELDS } from './class-v5-tiers.mjs';
|
||||
|
||||
const clone = (t) => JSON.parse(JSON.stringify(t));
|
||||
|
||||
test('the shipped table validates with no faults', () => {
|
||||
const t = loadTable();
|
||||
assert.deepEqual(validate(t), []);
|
||||
assert.equal(t.class, 'v5');
|
||||
assert.ok(t.cards.length >= REQUIRED_CARDS.length);
|
||||
});
|
||||
|
||||
test('the measured rows are the record\'s (the 5090 at its 1,300 MHz knee, the 5080 at 1,100, the 4070 at its tune, the 9070 XT grid, the M5 Max meter)', () => {
|
||||
const t = loadTable();
|
||||
const r5090 = tierRow(t, 'NVIDIA GeForce RTX 5090', 'balanced');
|
||||
assert.equal(r5090.clock_mhz, 1300);
|
||||
assert.equal(r5090.label, 'measured');
|
||||
assert.ok(Math.abs(r5090.uj - 2.38) < 0.01, `the 5090 balanced row reads 2.38 microjoules under class v5: ${r5090.uj}`);
|
||||
const e5090 = tierRow(t, 'NVIDIA GeForce RTX 5090', 'efficiency');
|
||||
assert.equal(e5090.clock_mhz, 1200);
|
||||
const r5080 = tierRow(t, 'NVIDIA GeForce RTX 5080', 'efficiency');
|
||||
assert.equal(r5080.clock_mhz, 1100);
|
||||
assert.ok(Math.abs(r5080.uj - 2.06) < 0.01);
|
||||
const r4070 = tierRow(t, 'NVIDIA GeForce RTX 4070', 'efficiency');
|
||||
assert.equal(r4070.clock_mhz, 1860);
|
||||
assert.equal(r4070.power_pct, 50);
|
||||
const amd = tierRow(t, 'AMD Radeon RX 9070 XT', 'efficiency');
|
||||
assert.equal(amd.core_offset_mhz, -500);
|
||||
assert.equal(amd.power_offset_pct, -30);
|
||||
assert.ok(Math.abs(amd.w - 149.3) < 0.01);
|
||||
const apple = entryFor(t, 'Apple M5 Max');
|
||||
assert.equal(apple.tiers.length, 1);
|
||||
assert.equal(apple.tiers[0].id, 'max');
|
||||
assert.ok(Math.abs(apple.tiers[0].uj - 1.40) < 0.01);
|
||||
});
|
||||
|
||||
test('every entry carries the three tiers where the card has a lever, and only max where it has none', () => {
|
||||
const t = loadTable();
|
||||
for (const c of t.cards) {
|
||||
const ids = c.tiers.map((r) => r.id).join();
|
||||
if (c.vendor === 'nvidia' || c.vendor === 'amd') assert.equal(ids, 'efficiency,balanced,max', c.card);
|
||||
else assert.equal(ids, 'max', c.card);
|
||||
for (const r of c.tiers) for (const k of ROW_FIELDS) assert.equal(typeof r[k], 'number', `${c.card}/${r.id}.${k}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('the match rule takes the longer name: a 5070 Ti is not a 5070, a 4060 Ti is not a 4060, a 9060 XT is not a 9070 XT', () => {
|
||||
const t = loadTable();
|
||||
assert.equal(entryFor(t, 'NVIDIA GeForce RTX 5070 Ti').card, 'NVIDIA GeForce RTX 5070 Ti');
|
||||
assert.equal(entryFor(t, 'NVIDIA GeForce RTX 5070').card, 'NVIDIA GeForce RTX 5070');
|
||||
assert.equal(entryFor(t, 'NVIDIA GeForce RTX 4060 Ti').card, 'NVIDIA GeForce RTX 4060 Ti');
|
||||
assert.equal(entryFor(t, 'NVIDIA GeForce RTX 4060').card, 'NVIDIA GeForce RTX 4060');
|
||||
assert.equal(entryFor(t, 'AMD Radeon RX 9060 XT').card, 'AMD Radeon RX 9060 XT');
|
||||
assert.equal(entryFor(t, 'amd:gfx1201').card, 'AMD Radeon RX 9070 XT');
|
||||
assert.equal(entryFor(t, 'NVIDIA GeForce GTX 1080 Ti'), null);
|
||||
assert.equal(tierRow(t, 'NVIDIA GeForce GTX 1080 Ti', 'max'), null);
|
||||
});
|
||||
|
||||
test('a class flip reads stale exactly as src/ember.rs tiers_stale does', () => {
|
||||
assert.equal(stale('v4', 'v5'), true);
|
||||
assert.equal(stale('v5', 'v5'), false);
|
||||
assert.equal(stale('', 'v5'), false);
|
||||
assert.equal(stale('v4', ''), false);
|
||||
const t = loadTable();
|
||||
assert.ok(t.remeasure_rule.measured_flip.includes('0.0 percent of rate'));
|
||||
assert.equal(t.v5_over_v4.watts_pct, 2.0);
|
||||
});
|
||||
|
||||
test('known-failed: a power rung under 50 is refused', () => {
|
||||
const t = clone(loadTable());
|
||||
t.cards[0].tiers[0].power_pct = 40;
|
||||
assert.ok(validate(t).some((s) => s.includes('power_pct 40')));
|
||||
});
|
||||
|
||||
test('known-failed: a row missing a field tier_from_json reads is refused', () => {
|
||||
const t = clone(loadTable());
|
||||
delete t.cards[1].tiers[1].limit_w;
|
||||
assert.ok(validate(t).some((s) => s.includes('limit_w is not a number')));
|
||||
});
|
||||
|
||||
test('known-failed: a tuned row dearer per hash than stock is refused, and a max tier that is not stock', () => {
|
||||
const t = clone(loadTable());
|
||||
const c = t.cards.find((x) => x.card.includes('4080'));
|
||||
c.tiers[0].uj = c.tiers[2].uj + 1; c.tiers[0].w = c.tiers[0].uj * c.tiers[0].mhs; c.tiers[0].mhw = c.tiers[0].mhs / c.tiers[0].w;
|
||||
assert.ok(validate(t).some((s) => s.includes('efficiency costs more per hash than stock')));
|
||||
const u = clone(loadTable());
|
||||
u.cards[0].tiers[2].clock_mhz = 1500;
|
||||
assert.ok(validate(u).some((s) => s.includes('stock is unlocked at 100 percent')));
|
||||
});
|
||||
|
||||
test('known-failed: a card class of the brief left out is refused, and a stale uj (not w over mhs) is refused', () => {
|
||||
const t = clone(loadTable());
|
||||
t.cards = t.cards.filter((c) => !c.card.includes('3060'));
|
||||
assert.ok(validate(t).some((s) => s === 'no entry for 3060'));
|
||||
const u = clone(loadTable());
|
||||
u.cards[0].tiers[1].uj = 9.99;
|
||||
assert.ok(validate(u).some((s) => s.includes('is not w over mhs')));
|
||||
});
|
||||
|
||||
test('known-failed: a table under another class is refused', () => {
|
||||
const t = clone(loadTable());
|
||||
t.class = 'v4';
|
||||
assert.ok(validate(t).some((s) => s.includes('not v5')));
|
||||
});
|
||||
490
app/igneum-common/Cargo.lock
generated
Normal file
490
app/igneum-common/Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,490 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "base16ct"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-bigint"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"rand_core",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"curve25519-dalek-derive",
|
||||
"digest",
|
||||
"fiat-crypto",
|
||||
"rustc_version",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek-derive"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.16.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
|
||||
dependencies = [
|
||||
"der",
|
||||
"elliptic-curve",
|
||||
"signature",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||
dependencies = [
|
||||
"pkcs8",
|
||||
"signature",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519-dalek"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"ed25519",
|
||||
"serde",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "elliptic-curve"
|
||||
version = "0.13.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"crypto-bigint",
|
||||
"digest",
|
||||
"ff",
|
||||
"generic-array",
|
||||
"group",
|
||||
"pkcs8",
|
||||
"rand_core",
|
||||
"sec1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ff"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
|
||||
dependencies = [
|
||||
"ff",
|
||||
"rand_core",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "igneum-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ed25519-dalek",
|
||||
"getrandom",
|
||||
"k256",
|
||||
"libc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"sha3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "k256"
|
||||
version = "0.13.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"ecdsa",
|
||||
"elliptic-curve",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keccak"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
|
||||
dependencies = [
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.190"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||
dependencies = [
|
||||
"der",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sec1"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"der",
|
||||
"generic-array",
|
||||
"pkcs8",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha3"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
19
app/igneum-common/Cargo.toml
Normal file
19
app/igneum-common/Cargo.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "igneum-common"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "What Igneum Miner and Igneum Wallet share: platform helpers, the payout key code, the signed update manifest, the local dashboard server primitives and the packaged configuration"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
k256 = { version = "0.13", default-features = false, features = ["arithmetic", "std"] }
|
||||
sha3 = { version = "0.10", default-features = false }
|
||||
getrandom = "0.2"
|
||||
ed25519-dalek = { version = "2", default-features = false, features = ["std"] }
|
||||
sha2 = { version = "0.10", default-features = false }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
352
app/igneum-common/src/biometric.rs
Normal file
352
app/igneum-common/src/biometric.rs
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
//! The biometric gate, the part that needs no Touch ID and no Windows Hello: challenges the engine issues, the host
|
||||
//! confirms after the prompt, and the action consumes once. The window host (macOS: app/mac/Biometric.swift;
|
||||
//! Windows: the WebView2 host) holds the secret; this module only decides whether a confirmation is fresh.
|
||||
//!
|
||||
//! The flow for a gated action:
|
||||
//! 1. the window asks the engine for a challenge: `Gate::issue(purpose, bound, reason)` gives a nonce; the reason
|
||||
//! (at most 80 characters) is what the prompt shows, built by the engine so the window cannot word it;
|
||||
//! 2. the window hands the nonce to the host; the host reads the reason from the engine (with the host token, which
|
||||
//! only the host knows: the engine printed it on its stdout), shows the prompt, and on success posts
|
||||
//! `Gate::confirm(nonce)`;
|
||||
//! 3. the window calls the action with the nonce; the engine runs `Gate::take(nonce, purpose, bound)`: confirmed
|
||||
//! within CHALLENGE_TTL_S, the same purpose and the same binding (the quote for a send, the new address for an
|
||||
//! address change), never used before. One nonce, one action.
|
||||
//!
|
||||
//! Enrolment (the wallet): the window posts the password to the engine, which checks it and keeps it under a one-time
|
||||
//! token for ENROL_TTL_S; the host takes it with the token after the prompt, seals it and writes the file. The
|
||||
//! password reaches the host over 127.0.0.1 only, never through the page.
|
||||
|
||||
use serde::Serialize;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// A confirmation is fresh for this long after the prompt succeeded.
|
||||
pub const CHALLENGE_TTL_S: u64 = 30;
|
||||
/// An unconfirmed challenge waits for the prompt this long (the confirm screen can sit open).
|
||||
pub const CHALLENGE_WAIT_S: u64 = 180;
|
||||
/// The enrolment token's life.
|
||||
pub const ENROL_TTL_S: u64 = 120;
|
||||
/// The prompt's reason string: at most this many characters (the hard clip); the strings the engines build stay
|
||||
/// under 60, in our voice, no trailing full stop ("Unlock your wallet", "Send 1.5 IGN to 0x7F45…C126").
|
||||
pub const REASON_MAX: usize = 80;
|
||||
/// The idle lock (the wallet): minutes without the window reporting activity before the key is zeroed.
|
||||
pub const IDLE_LOCK_MIN: u64 = 5;
|
||||
|
||||
/// What `/api/state` carries under `biometric`.
|
||||
#[derive(Clone, Serialize, Default)]
|
||||
pub struct BiometricState {
|
||||
/// the host said the prompt can be shown (Touch ID set up, Windows Hello configured)
|
||||
pub available: bool,
|
||||
/// touchid | hello | "" (no host yet)
|
||||
pub kind: String,
|
||||
/// the sealed file exists: the gated actions need the prompt
|
||||
pub enrolled: bool,
|
||||
/// the host's word for why it is unavailable, in the window's wording
|
||||
pub message: String,
|
||||
/// the last prompt's outcome: ok | cancelled | failed | locked | invalidated | unavailable | ""
|
||||
pub last_result: String,
|
||||
pub last_op: String,
|
||||
pub last_at: f64,
|
||||
/// the wallet: lock after IDLE_LOCK_MIN minutes idle (setting, on by default once enrolled)
|
||||
pub idle_lock: bool,
|
||||
pub idle_lock_min: u64,
|
||||
/// set when the password changed under an enrolment: the sealed password is stale and was removed
|
||||
pub needs_enrol: bool,
|
||||
}
|
||||
|
||||
pub struct Challenge {
|
||||
pub nonce: String,
|
||||
pub purpose: String,
|
||||
pub bound: String,
|
||||
pub reason: String,
|
||||
issued: Instant,
|
||||
confirmed: Option<Instant>,
|
||||
used: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Gate {
|
||||
challenges: Vec<Challenge>,
|
||||
enrol: Option<(String, String, Instant)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum GateError {
|
||||
Unknown,
|
||||
Expired,
|
||||
NotConfirmed,
|
||||
Stale,
|
||||
Used,
|
||||
Mismatch,
|
||||
}
|
||||
|
||||
impl GateError {
|
||||
/// The window's wording. `what` is "Touch ID" or "Windows Hello".
|
||||
pub fn text(&self, what: &str) -> String {
|
||||
match self {
|
||||
GateError::Unknown => format!("confirm with {what} first"),
|
||||
GateError::Expired => format!("the {what} request timed out; try again"),
|
||||
GateError::NotConfirmed => format!("{what} did not confirm this; try again"),
|
||||
GateError::Stale => format!("the {what} confirmation is older than {CHALLENGE_TTL_S} s; confirm again"),
|
||||
GateError::Used => format!("that {what} confirmation was already used; confirm again"),
|
||||
GateError::Mismatch => format!("the {what} confirmation was for something else; confirm again"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn random_hex(n: usize) -> String {
|
||||
let mut raw = vec![0u8; n];
|
||||
getrandom::getrandom(&mut raw).expect("os randomness");
|
||||
crate::keys::hex(&raw)
|
||||
}
|
||||
|
||||
/// Cuts a reason to REASON_MAX characters (not bytes), with a trailing ellipsis when it was longer.
|
||||
pub fn clip_reason(s: &str) -> String {
|
||||
let count = s.chars().count();
|
||||
if count <= REASON_MAX {
|
||||
return s.to_string();
|
||||
}
|
||||
let mut out: String = s.chars().take(REASON_MAX - 1).collect();
|
||||
out.push('…');
|
||||
out
|
||||
}
|
||||
|
||||
/// The prompt's line for a send: the amount (the caller gives it to 4 decimals) and the checksum address as its
|
||||
/// first 6 and last 4 characters: "Send 1.5 IGN to 0x7F45…C126".
|
||||
pub fn send_reason(amount_ign: &str, display_to: &str) -> String {
|
||||
let short = if display_to.len() > 10 { format!("{}…{}", &display_to[..6], &display_to[display_to.len() - 4..]) } else { display_to.to_string() };
|
||||
clip_reason(&format!("Send {amount_ign} IGN to {short}"))
|
||||
}
|
||||
|
||||
impl Gate {
|
||||
pub fn new() -> Gate {
|
||||
Gate::default()
|
||||
}
|
||||
|
||||
fn prune(&mut self, now: Instant) {
|
||||
// used nonces stay until their window ends, so a replay is answered "used", not "unknown"
|
||||
self.challenges.retain(|c| now.duration_since(c.issued) < Duration::from_secs(CHALLENGE_WAIT_S + CHALLENGE_TTL_S));
|
||||
if let Some((_, _, t)) = &self.enrol {
|
||||
if now.duration_since(*t) >= Duration::from_secs(ENROL_TTL_S) {
|
||||
self.enrol = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn issue(&mut self, purpose: &str, bound: &str, reason: &str, now: Instant) -> String {
|
||||
self.prune(now);
|
||||
let nonce = random_hex(16);
|
||||
self.challenges.push(Challenge { nonce: nonce.clone(), purpose: purpose.into(), bound: bound.into(), reason: clip_reason(reason), issued: now, confirmed: None, used: false });
|
||||
nonce
|
||||
}
|
||||
|
||||
/// For the host: what the prompt says for this nonce.
|
||||
pub fn reason_of(&self, nonce: &str) -> Option<(String, String)> {
|
||||
self.challenges.iter().find(|c| c.nonce == nonce && !c.used).map(|c| (c.purpose.clone(), c.reason.clone()))
|
||||
}
|
||||
|
||||
/// The host says the prompt succeeded for this nonce.
|
||||
pub fn confirm(&mut self, nonce: &str, now: Instant) -> Result<(), GateError> {
|
||||
self.prune(now);
|
||||
let c = self.challenges.iter_mut().find(|c| c.nonce == nonce).ok_or(GateError::Unknown)?;
|
||||
if c.used {
|
||||
return Err(GateError::Used);
|
||||
}
|
||||
if now.duration_since(c.issued) >= Duration::from_secs(CHALLENGE_WAIT_S) {
|
||||
return Err(GateError::Expired);
|
||||
}
|
||||
c.confirmed = Some(now);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The action runs: fresh, the same purpose and binding, once.
|
||||
pub fn take(&mut self, nonce: &str, purpose: &str, bound: &str, now: Instant) -> Result<(), GateError> {
|
||||
let c = self.challenges.iter_mut().find(|c| c.nonce == nonce).ok_or(GateError::Unknown)?;
|
||||
if c.used {
|
||||
return Err(GateError::Used);
|
||||
}
|
||||
let Some(t) = c.confirmed else {
|
||||
return Err(if now.duration_since(c.issued) >= Duration::from_secs(CHALLENGE_WAIT_S) { GateError::Expired } else { GateError::NotConfirmed });
|
||||
};
|
||||
if c.purpose != purpose || c.bound != bound {
|
||||
return Err(GateError::Mismatch);
|
||||
}
|
||||
if now.duration_since(t) >= Duration::from_secs(CHALLENGE_TTL_S) {
|
||||
c.used = true;
|
||||
return Err(GateError::Stale);
|
||||
}
|
||||
c.used = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enrolment: the engine keeps the checked secret under a one-time token.
|
||||
pub fn enrol_begin(&mut self, secret: &str, now: Instant) -> String {
|
||||
let token = random_hex(16);
|
||||
self.enrol = Some((token.clone(), secret.to_string(), now));
|
||||
token
|
||||
}
|
||||
|
||||
/// The host takes the secret with the token, once.
|
||||
pub fn enrol_take(&mut self, token: &str, now: Instant) -> Option<String> {
|
||||
self.prune(now);
|
||||
match self.enrol.take() {
|
||||
Some((t, s, _)) if constant_eq(&t, token) => Some(s),
|
||||
Some(other) => {
|
||||
// a wrong token does not burn the pending enrolment
|
||||
self.enrol = Some(other);
|
||||
None
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enrol_pending(&self) -> bool {
|
||||
self.enrol.is_some()
|
||||
}
|
||||
|
||||
pub fn enrol_cancel(&mut self) {
|
||||
self.enrol = None;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn len(&self) -> usize {
|
||||
self.challenges.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Equal strings, compared in constant time over the longer length.
|
||||
pub fn constant_eq(a: &str, b: &str) -> bool {
|
||||
let (a, b) = (a.as_bytes(), b.as_bytes());
|
||||
let mut diff = (a.len() ^ b.len()) as u8;
|
||||
for i in 0..a.len().max(b.len()) {
|
||||
diff |= a.get(i).copied().unwrap_or(0) ^ b.get(i).copied().unwrap_or(0);
|
||||
}
|
||||
diff == 0
|
||||
}
|
||||
|
||||
/// A fingerprint of a send quote, so the confirmation binds to what the window showed.
|
||||
pub fn send_binding(to: &str, value: &str, tx_nonce: u64, chain_id: u64) -> String {
|
||||
format!("send:{}:{}:{}:{}", to.to_ascii_lowercase(), value, tx_nonce, chain_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn t(base: Instant, s: u64) -> Instant {
|
||||
base + Duration::from_secs(s)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_then_take_once() {
|
||||
let mut g = Gate::new();
|
||||
let now = Instant::now();
|
||||
let n = g.issue("send", "send:0xab:1:0:7", "Send 1 IGN to 0xab", now);
|
||||
assert_eq!(g.reason_of(&n), Some(("send".into(), "Send 1 IGN to 0xab".into())));
|
||||
// not confirmed yet
|
||||
assert_eq!(g.take(&n, "send", "send:0xab:1:0:7", t(now, 1)), Err(GateError::NotConfirmed));
|
||||
g.confirm(&n, t(now, 2)).unwrap();
|
||||
// wrong binding, wrong purpose
|
||||
assert_eq!(g.take(&n, "send", "send:0xcd:1:0:7", t(now, 3)), Err(GateError::Mismatch));
|
||||
assert_eq!(g.take(&n, "reveal", "send:0xab:1:0:7", t(now, 3)), Err(GateError::Mismatch));
|
||||
// the right one, once
|
||||
assert_eq!(g.take(&n, "send", "send:0xab:1:0:7", t(now, 3)), Ok(()));
|
||||
assert_eq!(g.take(&n, "send", "send:0xab:1:0:7", t(now, 4)), Err(GateError::Used));
|
||||
assert_eq!(g.confirm(&n, t(now, 4)), Err(GateError::Used));
|
||||
assert!(g.reason_of(&n).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_and_expiry() {
|
||||
let mut g = Gate::new();
|
||||
let now = Instant::now();
|
||||
assert_eq!(g.take("nope", "send", "", now), Err(GateError::Unknown));
|
||||
assert_eq!(g.confirm("nope", now), Err(GateError::Unknown));
|
||||
// a confirmation older than the TTL is stale and burns the nonce
|
||||
let n = g.issue("reveal", "", "Show the words", now);
|
||||
g.confirm(&n, t(now, 1)).unwrap();
|
||||
assert_eq!(g.take(&n, "reveal", "", t(now, 1 + CHALLENGE_TTL_S)), Err(GateError::Stale));
|
||||
assert_eq!(g.take(&n, "reveal", "", t(now, 2)), Err(GateError::Used));
|
||||
// a challenge nobody confirmed within the wait expires
|
||||
let n2 = g.issue("reveal", "", "Show the words", now);
|
||||
assert_eq!(g.confirm(&n2, t(now, CHALLENGE_WAIT_S)), Err(GateError::Expired));
|
||||
assert_eq!(g.take(&n2, "reveal", "", t(now, CHALLENGE_WAIT_S)), Err(GateError::Expired));
|
||||
// pruned away after wait + ttl
|
||||
let _ = g.issue("reveal", "", "x", t(now, CHALLENGE_WAIT_S + CHALLENGE_TTL_S + 1));
|
||||
assert_eq!(g.len(), 1);
|
||||
// and a used nonce still answers "used" inside its window
|
||||
let n4 = g.issue("send", "b", "r", t(now, 1000));
|
||||
g.confirm(&n4, t(now, 1001)).unwrap();
|
||||
assert_eq!(g.take(&n4, "send", "b", t(now, 1002)), Ok(()));
|
||||
assert_eq!(g.confirm(&n4, t(now, 1003)), Err(GateError::Used));
|
||||
// a fresh confirmation at the edge still works
|
||||
let n3 = g.issue("send", "b", "r", now);
|
||||
g.confirm(&n3, t(now, CHALLENGE_WAIT_S - 1)).unwrap();
|
||||
assert_eq!(g.take(&n3, "send", "b", t(now, CHALLENGE_WAIT_S - 1 + CHALLENGE_TTL_S - 1)), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonces_are_distinct_and_hex() {
|
||||
let mut g = Gate::new();
|
||||
let now = Instant::now();
|
||||
let a = g.issue("send", "", "r", now);
|
||||
let b = g.issue("send", "", "r", now);
|
||||
assert_ne!(a, b);
|
||||
assert_eq!(a.len(), 32);
|
||||
assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enrol_token_one_time_and_expiry() {
|
||||
let mut g = Gate::new();
|
||||
let now = Instant::now();
|
||||
let tok = g.enrol_begin("correct horse", now);
|
||||
assert!(g.enrol_pending());
|
||||
// a wrong token leaves the pending enrolment in place
|
||||
assert_eq!(g.enrol_take("wrong", t(now, 1)), None);
|
||||
assert!(g.enrol_pending());
|
||||
assert_eq!(g.enrol_take(&tok, t(now, 1)).as_deref(), Some("correct horse"));
|
||||
assert_eq!(g.enrol_take(&tok, t(now, 1)), None);
|
||||
assert!(!g.enrol_pending());
|
||||
// expiry
|
||||
let tok2 = g.enrol_begin("p", now);
|
||||
assert_eq!(g.enrol_take(&tok2, t(now, ENROL_TTL_S)), None);
|
||||
// cancel
|
||||
let tok3 = g.enrol_begin("p", now);
|
||||
g.enrol_cancel();
|
||||
assert_eq!(g.enrol_take(&tok3, t(now, 1)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasons_are_clipped_to_eighty() {
|
||||
let long = "x".repeat(200);
|
||||
assert_eq!(clip_reason(&long).chars().count(), REASON_MAX);
|
||||
assert_eq!(clip_reason("short"), "short");
|
||||
let r = send_reason("1.5", "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf");
|
||||
assert_eq!(r, "Send 1.5 IGN to 0x7E5F…5Bdf");
|
||||
assert!(r.chars().count() < 60);
|
||||
// a long amount still fits under 60
|
||||
let r2 = send_reason("123456789.1234", "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf");
|
||||
assert_eq!(r2, "Send 123456789.1234 IGN to 0x7E5F…5Bdf");
|
||||
assert!(r2.chars().count() < 60);
|
||||
// absurd amount: still clipped at 80 characters
|
||||
let r3 = send_reason(&"9".repeat(90), "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf");
|
||||
assert_eq!(r3.chars().count(), REASON_MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_eq_and_binding() {
|
||||
assert!(constant_eq("abc", "abc"));
|
||||
assert!(!constant_eq("abc", "abd"));
|
||||
assert!(!constant_eq("abc", "abcd"));
|
||||
assert!(!constant_eq("", "a"));
|
||||
assert_eq!(send_binding("0xAB", "5", 3, 7), "send:0xab:5:3:7");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_defaults() {
|
||||
let s = BiometricState::default();
|
||||
assert!(!s.available && !s.enrolled && s.kind.is_empty() && s.last_result.is_empty());
|
||||
let v = serde_json::to_value(&s).unwrap();
|
||||
assert_eq!(v["idle_lock_min"], 0);
|
||||
}
|
||||
}
|
||||
89
app/igneum-common/src/config.rs
Normal file
89
app/igneum-common/src/config.rs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
//! The packager's configuration next to the binary (`igneum-app.json` for the miner, `igneum-wallet.json` for the
|
||||
//! wallet; macOS: Contents/Resources) and the per-install machine id. From app/igneum-app/src/config.rs.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Written by the packager (build-dmg.sh, make-payload.sh). Missing fields disable the feature.
|
||||
#[derive(Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Packaged {
|
||||
#[serde(default)]
|
||||
pub update_manifest: String,
|
||||
#[serde(default)]
|
||||
pub log_intake_url: String,
|
||||
#[serde(default)]
|
||||
pub log_intake_key: String,
|
||||
#[serde(default)]
|
||||
pub live_page: String,
|
||||
#[serde(default)]
|
||||
pub download_page: String,
|
||||
/// Consensus parameters the packager pins for the bundled node (`--override-params-file`). Absent = none.
|
||||
#[serde(default)]
|
||||
pub node_override_params: Option<serde_json::Value>,
|
||||
/// Wallet: the public Ethereum JSON-RPC of the seed, when one exists (`https://...`); empty = none known.
|
||||
#[serde(default)]
|
||||
pub public_rpc: String,
|
||||
/// Wallet: the page on the site that adds the network to MetaMask (`https://igneum.network/wallet/add`).
|
||||
#[serde(default)]
|
||||
pub add_network_page: String,
|
||||
}
|
||||
|
||||
impl Packaged {
|
||||
pub fn load(candidates: &[PathBuf]) -> Packaged {
|
||||
for c in candidates {
|
||||
if let Some(p) = std::fs::read_to_string(c).ok().and_then(|t| serde_json::from_str::<Packaged>(&t).ok()) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
Packaged::default()
|
||||
}
|
||||
/// The places the packaged file can be: the binary's folder (Windows), Contents/Resources (macOS), IGNEUM_APP_BIN.
|
||||
pub fn candidates(file_name: &str) -> Vec<PathBuf> {
|
||||
let mut out = vec![];
|
||||
if let Some(d) = std::env::var_os("IGNEUM_APP_BIN") {
|
||||
out.push(PathBuf::from(d).join(file_name));
|
||||
}
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(d) = exe.parent() {
|
||||
out.push(d.join(file_name));
|
||||
if let Some(c) = d.parent() {
|
||||
out.push(c.join("Resources").join(file_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the machine id, or makes one on the first run (16 hex from the OS) and locks the file to the user.
|
||||
pub fn machine_id(app_dir: &Path) -> String {
|
||||
let path = app_dir.join("machine-id");
|
||||
if let Some(id) = std::fs::read_to_string(&path).ok().map(|s| s.trim().to_ascii_lowercase()) {
|
||||
if id.len() == 16 && id.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
let mut raw = [0u8; 8];
|
||||
getrandom::getrandom(&mut raw).expect("os randomness");
|
||||
let id = crate::keys::hex(&raw);
|
||||
let _ = std::fs::create_dir_all(app_dir);
|
||||
crate::platform::lock_permissions(app_dir, true);
|
||||
let _ = std::fs::write(&path, format!("{id}\n"));
|
||||
crate::platform::lock_permissions(&path, false);
|
||||
id
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn packaged_carries_the_wallet_fields() {
|
||||
let p: Packaged = serde_json::from_str(r#"{"update_manifest":"","public_rpc":"https://rpc.igneum.network","node_override_params":{"difficulty_v2_activation_daa":123456}}"#).unwrap();
|
||||
assert_eq!(p.public_rpc, "https://rpc.igneum.network");
|
||||
assert_eq!(p.node_override_params.as_ref().unwrap()["difficulty_v2_activation_daa"], 123456);
|
||||
let p: Packaged = serde_json::from_str(r#"{"update_manifest":""}"#).unwrap();
|
||||
assert!(p.node_override_params.is_none());
|
||||
assert!(p.public_rpc.is_empty());
|
||||
}
|
||||
}
|
||||
107
app/igneum-common/src/fetch.rs
Normal file
107
app/igneum-common/src/fetch.rs
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
//! The over-the-air update's network side, as the miner does it (app/igneum-app/src/ota.rs): the manifest and its
|
||||
//! signature fetched with curl (macOS ships it; Windows 10 1803 and later ship curl.exe, so the engine carries no TLS
|
||||
//! stack), verified before parsing; the installer or disk image downloaded next to the manifest with resume (a
|
||||
//! dropped line continues the .part file) and checked against the manifest's sha256 and size.
|
||||
|
||||
use crate::manifest::{self, Manifest, PlatformEntry};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn curl(args: &[&str], limit: Duration) -> Result<(), String> {
|
||||
let mut c = Command::new(crate::platform::tool("curl"));
|
||||
c.args(args);
|
||||
let out = crate::run::run_timeout(&mut c, None, limit).ok_or("curl is not available")?;
|
||||
let code = out.lines().last().unwrap_or("").trim().to_string();
|
||||
// 206: a resumed download (-C -) answers partial content
|
||||
if code.starts_with("200") || code.starts_with("206") {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("http {}", if code.is_empty() { "no answer".to_string() } else { code }))
|
||||
}
|
||||
}
|
||||
|
||||
/// GET `url` to `dest` with curl; `limit` bounds the whole transfer.
|
||||
pub fn curl_get(url: &str, dest: &Path, limit: Duration) -> Result<(), String> {
|
||||
curl(&["-fsSL", "--max-time", &limit.as_secs().to_string(), "-o", &dest.display().to_string(), "-w", "%{http_code}", url], limit + Duration::from_secs(5))
|
||||
}
|
||||
|
||||
/// Fetches `<url>` and `<url>.sig` into `dir`, verifies the bytes with the embedded OTA public key, parses.
|
||||
/// Writes `manifest.json` to `dir` only after the check.
|
||||
pub fn fetch_manifest(url: &str, dir: &Path) -> Result<Manifest, String> {
|
||||
let m = dir.join("manifest.json.new");
|
||||
let s = dir.join("manifest.json.sig.new");
|
||||
curl_get(url, &m, Duration::from_secs(30)).map_err(|e| format!("manifest: {e}"))?;
|
||||
curl_get(&format!("{url}.sig"), &s, Duration::from_secs(30)).map_err(|e| format!("manifest signature: {e}"))?;
|
||||
let bytes = std::fs::read(&m).map_err(|e| e.to_string())?;
|
||||
let sig = std::fs::read_to_string(&s).map_err(|e| e.to_string())?;
|
||||
let parsed = manifest::verify_and_parse(&bytes, sig.trim(), manifest::OTA_PUBLIC_KEY_HEX)?;
|
||||
let _ = std::fs::rename(&m, dir.join("manifest.json"));
|
||||
let _ = std::fs::rename(&s, dir.join("manifest.json.sig"));
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
/// The file name a download keeps: the last path segment of the url, cleaned to [A-Za-z0-9.-_] (the miner's rule).
|
||||
pub fn file_name(url: &str) -> String {
|
||||
let name = url.rsplit('/').next().unwrap_or("download").split('?').next().unwrap_or("download");
|
||||
let clean: String = name.chars().filter(|c| c.is_ascii_alphanumeric() || *c == '.' || *c == '-' || *c == '_').collect();
|
||||
if clean.is_empty() { "download".into() } else { clean }
|
||||
}
|
||||
|
||||
/// The .part file a download in progress writes next to the final name.
|
||||
pub fn part_path(e: &PlatformEntry, dir: &Path) -> PathBuf {
|
||||
dir.join(format!("{}.part", file_name(&e.url)))
|
||||
}
|
||||
|
||||
/// How much of the platform entry is on disk right now, 0..1 (the dashboard's progress bar).
|
||||
pub fn progress(e: &PlatformEntry, dir: &Path) -> f64 {
|
||||
if e.size == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
let have = std::fs::metadata(part_path(e, dir)).map(|m| m.len()).unwrap_or(0);
|
||||
(have as f64 / e.size as f64).min(1.0)
|
||||
}
|
||||
|
||||
/// Downloads the platform entry into `dir` (skipped when a file with the right size and sha256 is there already),
|
||||
/// resuming a .part file from an earlier try, and checks size and sha256. Returns the path.
|
||||
pub fn download(e: &PlatformEntry, dir: &Path) -> Result<PathBuf, String> {
|
||||
let dest = dir.join(file_name(&e.url));
|
||||
let ok = |p: &Path| -> bool {
|
||||
std::fs::metadata(p).map(|m| m.len() == e.size).unwrap_or(false) && manifest::sha256_file(p).map(|s| s == e.sha256).unwrap_or(false)
|
||||
};
|
||||
if ok(&dest) {
|
||||
return Ok(dest);
|
||||
}
|
||||
let _ = std::fs::remove_file(&dest);
|
||||
let part = part_path(e, dir);
|
||||
let have = std::fs::metadata(&part).map(|m| m.len()).unwrap_or(0);
|
||||
if have > e.size {
|
||||
let _ = std::fs::remove_file(&part);
|
||||
}
|
||||
if have != e.size {
|
||||
// -C - resumes a partial file; --retry covers a dropped connection; 2 hours for a slow line
|
||||
curl(&["-fsSL", "--retry", "3", "--retry-delay", "5", "-C", "-", "--max-time", "7200", "-o", &part.display().to_string(), "-w", "%{http_code}", &e.url], Duration::from_secs(7260))?;
|
||||
}
|
||||
let size = std::fs::metadata(&part).map(|m| m.len()).unwrap_or(0);
|
||||
if size != e.size {
|
||||
let _ = std::fs::remove_file(&part);
|
||||
return Err(format!("the download is {size} bytes, the manifest says {}", e.size));
|
||||
}
|
||||
let sum = manifest::sha256_file(&part).map_err(|e| e.to_string())?;
|
||||
if sum != e.sha256 {
|
||||
let _ = std::fs::remove_file(&part);
|
||||
return Err("the download's sha256 does not match the manifest".into());
|
||||
}
|
||||
std::fs::rename(&part, &dest).map_err(|e| e.to_string())?;
|
||||
Ok(dest)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn file_names_are_cleaned() {
|
||||
assert_eq!(super::file_name("https://dl.igneum.network/dl/t/Igneum-Wallet-0.1.1.dmg"), "Igneum-Wallet-0.1.1.dmg");
|
||||
assert_eq!(super::file_name("https://x/y/Setup%20.exe?x=1"), "Setup20.exe");
|
||||
assert_eq!(super::file_name("https://x/"), "download");
|
||||
}
|
||||
}
|
||||
258
app/igneum-common/src/http.rs
Normal file
258
app/igneum-common/src/http.rs
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
//! The local dashboard server primitives (from app/igneum-app/src/server.rs): plain HTTP/1.1 on 127.0.0.1 with a
|
||||
//! random port, every path under `/t/<token>/`, one thread per connection, Connection: close. And a small JSON client
|
||||
//! for a node's Ethereum JSON-RPC on 127.0.0.1 (no TLS: the wallet reaches a public https RPC through curl instead).
|
||||
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
|
||||
pub struct Req {
|
||||
pub method: String,
|
||||
pub path: String,
|
||||
pub query: String,
|
||||
pub body: Vec<u8>,
|
||||
pub origin: Option<String>,
|
||||
pub sec_fetch_site: Option<String>,
|
||||
pub host: Option<String>,
|
||||
/// X-Igneum-Host: the window host's token (the engine printed it on stdout; the page never sees it)
|
||||
pub host_token: Option<String>,
|
||||
/// X-Igneum-Bridge: present on a page's call to the wallet's page bridge (a custom header, so the browser sends a
|
||||
/// CORS preflight first and a plain form post from a stranger never reaches the handler)
|
||||
pub bridge_header: bool,
|
||||
/// Access-Control-Request-Private-Network: the browser asks (Chrome's private network access) before a page on the
|
||||
/// public web reaches 127.0.0.1; the bridge answers the preflight with the allow header
|
||||
pub private_network_ask: bool,
|
||||
}
|
||||
|
||||
pub fn read_request(stream: &mut TcpStream) -> Option<Req> {
|
||||
let _ = stream.set_read_timeout(Some(std::time::Duration::from_secs(10)));
|
||||
let mut reader = BufReader::new(stream.try_clone().ok()?);
|
||||
let mut line = String::new();
|
||||
reader.read_line(&mut line).ok()?;
|
||||
let mut parts = line.split_whitespace();
|
||||
let method = parts.next()?.to_string();
|
||||
let target = parts.next()?.to_string();
|
||||
let mut content_length = 0usize;
|
||||
let (mut origin, mut sec_fetch_site, mut host, mut host_token) = (None, None, None, None);
|
||||
let (mut bridge_header, mut private_network_ask) = (false, false);
|
||||
loop {
|
||||
let mut h = String::new();
|
||||
reader.read_line(&mut h).ok()?;
|
||||
let h = h.trim_end();
|
||||
if h.is_empty() {
|
||||
break;
|
||||
}
|
||||
if let Some((k, v)) = h.split_once(':') {
|
||||
let v = v.trim();
|
||||
if k.eq_ignore_ascii_case("content-length") {
|
||||
content_length = v.parse().unwrap_or(0);
|
||||
} else if k.eq_ignore_ascii_case("origin") {
|
||||
origin = Some(v.to_string());
|
||||
} else if k.eq_ignore_ascii_case("sec-fetch-site") {
|
||||
sec_fetch_site = Some(v.to_ascii_lowercase());
|
||||
} else if k.eq_ignore_ascii_case("host") {
|
||||
host = Some(v.to_string());
|
||||
} else if k.eq_ignore_ascii_case("x-igneum-host") {
|
||||
host_token = Some(v.to_string());
|
||||
} else if k.eq_ignore_ascii_case("x-igneum-bridge") {
|
||||
bridge_header = true;
|
||||
} else if k.eq_ignore_ascii_case("access-control-request-private-network") {
|
||||
private_network_ask = v.eq_ignore_ascii_case("true");
|
||||
}
|
||||
}
|
||||
}
|
||||
if content_length > 1 << 20 {
|
||||
return None;
|
||||
}
|
||||
let mut body = vec![0u8; content_length];
|
||||
if content_length > 0 {
|
||||
reader.read_exact(&mut body).ok()?;
|
||||
}
|
||||
let (path, query) = match target.split_once('?') {
|
||||
Some((p, q)) => (p.to_string(), q.to_string()),
|
||||
None => (target, String::new()),
|
||||
};
|
||||
Some(Req { method, path, query, body, origin, sec_fetch_site, host, host_token, bridge_header, private_network_ask })
|
||||
}
|
||||
|
||||
/// R4.3.7: a mutating request must come from the dashboard itself. A browser sends Sec-Fetch-Site (same-origin for
|
||||
/// the dashboard; cross-site, same-site or none otherwise) and, on POST, an Origin; a cross-site page can reach
|
||||
/// 127.0.0.1 only through the browser, so both are checked. A request without either header (curl, the window host)
|
||||
/// still needs the token in the path.
|
||||
pub fn from_dashboard(req: &Req, port: u16) -> bool {
|
||||
if let Some(s) = &req.sec_fetch_site {
|
||||
if s != "same-origin" && s != "none" {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(o) = &req.origin {
|
||||
let ok = o == &format!("http://127.0.0.1:{port}") || o == &format!("http://localhost:{port}");
|
||||
if !ok {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(h) = &req.host {
|
||||
if h != &format!("127.0.0.1:{port}") && h != &format!("localhost:{port}") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn respond(stream: &mut TcpStream, status: u16, ctype: &str, body: &[u8], cache: bool) {
|
||||
let reason = match status {
|
||||
200 => "OK",
|
||||
204 => "No Content",
|
||||
400 => "Bad Request",
|
||||
403 => "Forbidden",
|
||||
404 => "Not Found",
|
||||
405 => "Method Not Allowed",
|
||||
_ => "Error",
|
||||
};
|
||||
let head = format!(
|
||||
"HTTP/1.1 {status} {reason}\r\nContent-Type: {ctype}\r\nContent-Length: {}\r\nConnection: close\r\nCache-Control: {}\r\nX-Content-Type-Options: nosniff\r\nReferrer-Policy: no-referrer\r\n\r\n",
|
||||
body.len(),
|
||||
if cache { "public, max-age=86400" } else { "no-store" }
|
||||
);
|
||||
let _ = stream.write_all(head.as_bytes());
|
||||
let _ = stream.write_all(body);
|
||||
let _ = stream.flush();
|
||||
}
|
||||
|
||||
pub fn json_resp(stream: &mut TcpStream, status: u16, v: serde_json::Value) {
|
||||
respond(stream, status, "application/json; charset=utf-8", v.to_string().as_bytes(), false);
|
||||
}
|
||||
|
||||
pub fn query_param(q: &str, key: &str) -> Option<String> {
|
||||
q.split('&').find_map(|kv| {
|
||||
let (k, v) = kv.split_once('=')?;
|
||||
if k == key { Some(v.to_string()) } else { None }
|
||||
})
|
||||
}
|
||||
|
||||
/// Binds 127.0.0.1 on a random port and serves every connection on its own thread through `handle`.
|
||||
pub fn serve<F>(handle: F) -> std::io::Result<u16>
|
||||
where
|
||||
F: Fn(TcpStream) + Send + Sync + 'static,
|
||||
{
|
||||
let listener = TcpListener::bind("127.0.0.1:0")?;
|
||||
let port = listener.local_addr()?.port();
|
||||
let handle = std::sync::Arc::new(handle);
|
||||
std::thread::spawn(move || {
|
||||
for conn in listener.incoming() {
|
||||
let Ok(stream) = conn else { continue };
|
||||
let handle = handle.clone();
|
||||
std::thread::spawn(move || handle(stream));
|
||||
}
|
||||
});
|
||||
Ok(port)
|
||||
}
|
||||
|
||||
/// Binds 127.0.0.1 on a FIXED port (the wallet's page bridge, 8 October 2026: a page on the web can only find the
|
||||
/// wallet at a port it knows) and serves every connection on its own thread through `handle`. Err when the port is taken.
|
||||
pub fn serve_on<F>(port: u16, handle: F) -> std::io::Result<()>
|
||||
where
|
||||
F: Fn(TcpStream) + Send + Sync + 'static,
|
||||
{
|
||||
let listener = TcpListener::bind(("127.0.0.1", port))?;
|
||||
let handle = std::sync::Arc::new(handle);
|
||||
std::thread::spawn(move || {
|
||||
for conn in listener.incoming() {
|
||||
let Ok(stream) = conn else { continue };
|
||||
let handle = handle.clone();
|
||||
std::thread::spawn(move || handle(stream));
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A response with extra headers (the bridge's CORS and private-network answers), Connection: close, no cache.
|
||||
pub fn respond_with(stream: &mut TcpStream, status: u16, ctype: &str, body: &[u8], extra: &[(&str, &str)]) {
|
||||
let reason = match status {
|
||||
200 => "OK",
|
||||
204 => "No Content",
|
||||
400 => "Bad Request",
|
||||
403 => "Forbidden",
|
||||
404 => "Not Found",
|
||||
405 => "Method Not Allowed",
|
||||
_ => "Error",
|
||||
};
|
||||
let mut head = format!("HTTP/1.1 {status} {reason}\r\nContent-Type: {ctype}\r\nContent-Length: {}\r\nConnection: close\r\nCache-Control: no-store\r\nX-Content-Type-Options: nosniff\r\nReferrer-Policy: no-referrer\r\n", body.len());
|
||||
for (k, v) in extra {
|
||||
head.push_str(k);
|
||||
head.push_str(": ");
|
||||
head.push_str(v);
|
||||
head.push_str("\r\n");
|
||||
}
|
||||
head.push_str("\r\n");
|
||||
let _ = stream.write_all(head.as_bytes());
|
||||
let _ = stream.write_all(body);
|
||||
let _ = stream.flush();
|
||||
}
|
||||
|
||||
/// The per-launch dashboard token: 32 hex characters from the OS.
|
||||
pub fn new_token() -> String {
|
||||
let mut raw = [0u8; 16];
|
||||
getrandom::getrandom(&mut raw).expect("os randomness");
|
||||
crate::keys::hex(&raw)
|
||||
}
|
||||
|
||||
// ---- a JSON POST to 127.0.0.1 (the node's Ethereum RPC) -------------------------------------------------------
|
||||
|
||||
/// POSTs `body` as JSON to `http://127.0.0.1:<port><path>` (or any plain-http host:port) and returns the body.
|
||||
pub fn post_json(host_port: &str, path: &str, body: &str, timeout: std::time::Duration) -> Result<String, String> {
|
||||
let mut s = TcpStream::connect(host_port).map_err(|e| format!("connect {host_port}: {e}"))?;
|
||||
let _ = s.set_read_timeout(Some(timeout));
|
||||
let _ = s.set_write_timeout(Some(timeout));
|
||||
let req = format!(
|
||||
"POST {path} HTTP/1.1\r\nHost: {host_port}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
|
||||
body.len()
|
||||
);
|
||||
s.write_all(req.as_bytes()).map_err(|e| e.to_string())?;
|
||||
let mut reader = BufReader::new(s);
|
||||
let mut status = String::new();
|
||||
reader.read_line(&mut status).map_err(|e| e.to_string())?;
|
||||
let code: u16 = status.split_whitespace().nth(1).and_then(|c| c.parse().ok()).unwrap_or(0);
|
||||
let mut len: Option<usize> = None;
|
||||
let mut chunked = false;
|
||||
loop {
|
||||
let mut h = String::new();
|
||||
reader.read_line(&mut h).map_err(|e| e.to_string())?;
|
||||
let h = h.trim_end();
|
||||
if h.is_empty() {
|
||||
break;
|
||||
}
|
||||
if let Some((k, v)) = h.split_once(':') {
|
||||
if k.eq_ignore_ascii_case("content-length") {
|
||||
len = v.trim().parse().ok();
|
||||
} else if k.eq_ignore_ascii_case("transfer-encoding") && v.trim().eq_ignore_ascii_case("chunked") {
|
||||
chunked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
if chunked {
|
||||
loop {
|
||||
let mut l = String::new();
|
||||
reader.read_line(&mut l).map_err(|e| e.to_string())?;
|
||||
let n = usize::from_str_radix(l.trim().split(';').next().unwrap_or("0"), 16).unwrap_or(0);
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
let mut buf = vec![0u8; n];
|
||||
reader.read_exact(&mut buf).map_err(|e| e.to_string())?;
|
||||
out.extend_from_slice(&buf);
|
||||
let mut crlf = String::new();
|
||||
let _ = reader.read_line(&mut crlf);
|
||||
}
|
||||
} else if let Some(n) = len {
|
||||
out = vec![0u8; n];
|
||||
reader.read_exact(&mut out).map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
reader.read_to_end(&mut out).map_err(|e| e.to_string())?;
|
||||
}
|
||||
let text = String::from_utf8_lossy(&out).into_owned();
|
||||
if code != 200 {
|
||||
return Err(format!("http {code}: {}", text.chars().take(200).collect::<String>()));
|
||||
}
|
||||
Ok(text)
|
||||
}
|
||||
109
app/igneum-common/src/keys.rs
Normal file
109
app/igneum-common/src/keys.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
//! The payout key: a secp256k1 private key made on this machine, its EVM address, and the miner's wallet file.
|
||||
//! The miner's file lives in its app data directory (`app/wallet.json`), plain JSON with the permissions locked to the
|
||||
//! user: 0600 on macOS and Linux, an icacls grant to the signed-in user alone on Windows. Igneum Wallet imports that
|
||||
//! file and keeps its own key encrypted (app/igneum-wallet/src/vault.rs). From app/igneum-app/src/keys.rs.
|
||||
|
||||
use k256::elliptic_curve::sec1::ToEncodedPoint;
|
||||
use k256::SecretKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha3::{Digest, Keccak256};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Wallet {
|
||||
pub address: String, // 0x + 40 lower-case hex
|
||||
pub private_key: String, // 0x + 64 hex, secp256k1
|
||||
pub created: u64, // unix seconds
|
||||
}
|
||||
|
||||
pub fn hex(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
|
||||
/// Hex (with or without 0x) to bytes; None when not hex or odd length.
|
||||
pub fn unhex(s: &str) -> Option<Vec<u8>> {
|
||||
let s = s.trim().trim_start_matches("0x");
|
||||
if s.len() % 2 != 0 {
|
||||
return None;
|
||||
}
|
||||
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok()).collect()
|
||||
}
|
||||
|
||||
/// The lower-case 0x address of a raw 32-byte private key, or None when the scalar is not a valid key.
|
||||
pub fn address_of_raw(raw: &[u8; 32]) -> Option<String> {
|
||||
SecretKey::from_slice(raw).ok().map(|sk| address_of(&sk))
|
||||
}
|
||||
|
||||
/// The EVM address of a secp256k1 private key: keccak256 of the uncompressed public key (without the 0x04 prefix), last 20 bytes.
|
||||
pub fn address_of(sk: &SecretKey) -> String {
|
||||
let pk = sk.public_key();
|
||||
let point = pk.to_encoded_point(false);
|
||||
let bytes = point.as_bytes();
|
||||
let h = Keccak256::digest(&bytes[1..]);
|
||||
format!("0x{}", hex(&h[12..]))
|
||||
}
|
||||
|
||||
/// A fresh key from the operating system's randomness.
|
||||
pub fn generate() -> Wallet {
|
||||
loop {
|
||||
let mut raw = [0u8; 32];
|
||||
getrandom::getrandom(&mut raw).expect("os randomness");
|
||||
if let Ok(sk) = SecretKey::from_slice(&raw) {
|
||||
let address = address_of(&sk);
|
||||
return Wallet { address, private_key: format!("0x{}", hex(&raw)), created: crate::platform::unix_now() };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// EIP-55 mixed-case form of a lower-case address, for display.
|
||||
pub fn checksum(addr: &str) -> String {
|
||||
let body = addr.trim_start_matches("0x").to_ascii_lowercase();
|
||||
let h = Keccak256::digest(body.as_bytes());
|
||||
let mut out = String::with_capacity(42);
|
||||
out.push_str("0x");
|
||||
for (i, c) in body.chars().enumerate() {
|
||||
let nibble = (h[i / 2] >> (if i % 2 == 0 { 4 } else { 0 })) & 0xf;
|
||||
if c.is_ascii_alphabetic() && nibble >= 8 {
|
||||
out.push(c.to_ascii_uppercase());
|
||||
} else {
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// True for 0x followed by 40 hex characters.
|
||||
pub fn valid_address(s: &str) -> bool {
|
||||
let s = s.trim();
|
||||
s.len() == 42 && s.starts_with("0x") && s[2..].chars().all(|c| c.is_ascii_hexdigit())
|
||||
}
|
||||
|
||||
pub fn save(path: &Path, w: &Wallet) -> std::io::Result<()> {
|
||||
if let Some(dir) = path.parent() {
|
||||
std::fs::create_dir_all(dir)?;
|
||||
crate::platform::lock_permissions(dir, true);
|
||||
}
|
||||
let text = serde_json::to_string_pretty(w).expect("wallet json");
|
||||
std::fs::write(path, text)?;
|
||||
crate::platform::lock_permissions(path, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load(path: &Path) -> Option<Wallet> {
|
||||
let text = std::fs::read_to_string(path).ok()?;
|
||||
serde_json::from_str(&text).ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn known_vector() {
|
||||
// The well-known test key 0x01: address 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf
|
||||
let mut raw = [0u8; 32];
|
||||
raw[31] = 1;
|
||||
let sk = SecretKey::from_slice(&raw).unwrap();
|
||||
assert_eq!(checksum(&address_of(&sk)), "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf");
|
||||
}
|
||||
}
|
||||
46
app/igneum-common/src/lib.rs
Normal file
46
app/igneum-common/src/lib.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
//! igneum-common: the parts of the Igneum Miner engine (app/igneum-app) that Igneum Wallet (app/igneum-wallet) ships
|
||||
//! on too. Extracted 4 October 2026 as a copy with the app identity made a parameter; the miner keeps its own copies
|
||||
//! until its concurrent branches land and can switch to this crate behind a feature flag (nothing in app/igneum-app
|
||||
//! was changed for the wallet).
|
||||
//!
|
||||
//! - `platform`: directories, file permissions, opening a URL, start at login, keep awake, terminate, quarantine
|
||||
//! - `keys`: secp256k1 key, EVM address, EIP-55 checksum, the miner's plain `wallet.json` format
|
||||
//! - `manifest`: the signed over-the-air update manifest, byte-identical to app/igneum-app/src/manifest.rs
|
||||
//! - `fetch`: the manifest fetch, the download (resumed, as the miner's) and its sha256 check (through curl)
|
||||
//! - `ota`: the apply side of an over-the-air update: the staged bundle, the detached helper that swaps and relaunches,
|
||||
//! the pending/result files; from app/igneum-app/src/ota.rs with the app's names from `AppId`
|
||||
//! - `http`: the 127.0.0.1 dashboard server primitives (request parsing, the token path, the same-origin guard) and a
|
||||
//! small JSON-over-HTTP client for a node's Ethereum RPC on 127.0.0.1
|
||||
//! - `run`: a command with a time limit
|
||||
//! - `config`: the packager's `igneum-app.json` and the per-install machine id
|
||||
//! - `biometric`: the Touch ID / Windows Hello gate logic (nonces, one-time enrolment token, state); the prompt and
|
||||
//! the sealed secret live in the window hosts
|
||||
|
||||
pub mod biometric;
|
||||
pub mod config;
|
||||
pub mod fetch;
|
||||
pub mod http;
|
||||
pub mod keys;
|
||||
pub mod manifest;
|
||||
pub mod ota;
|
||||
pub mod platform;
|
||||
pub mod run;
|
||||
|
||||
/// What differs between the two apps when the platform code names them.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct AppId {
|
||||
/// "Igneum Miner" or "Igneum Wallet": the window title, the login item name, the Windows Run key value.
|
||||
pub name: &'static str,
|
||||
/// "network.igneum.miner" or "network.igneum.wallet": the macOS bundle id and launch agent label.
|
||||
pub bundle: &'static str,
|
||||
/// The Windows window host next to the engine: "Igneum Miner.exe" or "Igneum Wallet.exe".
|
||||
pub host_exe: &'static str,
|
||||
/// The sub-folder of the shared data root this app writes under: "app" (the miner, as before) or "wallet".
|
||||
pub data_sub: &'static str,
|
||||
/// The engine binary next to the window host: "igneum-app" or "igneum-wallet" (".exe" on Windows). The update
|
||||
/// helper names it when it checks that the new app started.
|
||||
pub engine_exe: &'static str,
|
||||
}
|
||||
|
||||
pub const MINER: AppId = AppId { name: "Igneum Miner", bundle: "network.igneum.miner", host_exe: "Igneum Miner.exe", data_sub: "app", engine_exe: "igneum-app" };
|
||||
pub const WALLET: AppId = AppId { name: "Igneum Wallet", bundle: "network.igneum.wallet", host_exe: "Igneum Wallet.exe", data_sub: "wallet", engine_exe: "igneum-wallet" };
|
||||
527
app/igneum-common/src/manifest.rs
Normal file
527
app/igneum-common/src/manifest.rs
Normal file
|
|
@ -0,0 +1,527 @@
|
|||
//! The over-the-air update manifest: what the downloads host publishes as `igneum-app-latest.json` with a detached
|
||||
//! Ed25519 signature next to it (`igneum-app-latest.json.sig`, 64 bytes as 128 hex characters). The signature is over
|
||||
//! the exact bytes of the manifest file; the publisher (packaging/ota/publish-manifest.sh) writes the file in canonical
|
||||
//! form (sorted keys, no whitespace) and the app verifies the bytes before it parses them.
|
||||
//!
|
||||
//! This module is self-contained (serde_json, ed25519-dalek, sha2 only), so the signer binary
|
||||
//! (src/bin/ota-sign.rs) includes it with `#[path]` and signs with the very code that verifies.
|
||||
//!
|
||||
//! Manifest shape:
|
||||
//! {
|
||||
//! "version": "0.3.1", "published_at": "2026-10-04T13:00:00Z", "channel": "devnet",
|
||||
//! "platforms": { "mac": {"url","sha256","size","kind":"dmg"|"zip"}, "windows": {"url","sha256","size","kind":"inno-setup"} },
|
||||
//! "min_supported_version": "0.3.0", "notes": "one line",
|
||||
//! "consensus": { "activation_height": null|number, "deadline_note": "" }
|
||||
//! }
|
||||
//! A platform that is missing is not updated (the Windows build lands later than the Mac one).
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// The public half of ~/.config/igneum/ota-signing-key (generated once on the Mac with `igneum-ota-sign keygen`;
|
||||
/// the private key never enters the repo or CI). Fingerprint: SHA-256 of these 32 bytes, see `fingerprint()`.
|
||||
pub const OTA_PUBLIC_KEY_HEX: &str = "b3c9c5bd144e9d246dc0edf897387d4f3f7ca494cd47457123d1c2f892cabddd";
|
||||
|
||||
/// How many DAA blocks before a consensus activation height the app stops waiting for a safe moment.
|
||||
pub const FORK_URGENT_BLOCKS: u64 = 1_800;
|
||||
/// No apply within this many seconds of an hourly program boundary.
|
||||
pub const BOUNDARY_GUARD_S: i64 = 180;
|
||||
/// A ready update that found no safe moment for this long is applied anyway (an unsynced node mines nothing).
|
||||
pub const SAFE_MOMENT_PATIENCE_S: u64 = 6 * 3600;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
pub struct PlatformEntry {
|
||||
pub url: String,
|
||||
pub sha256: String,
|
||||
pub size: u64,
|
||||
pub kind: String, // dmg | zip | inno-setup
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
pub struct Manifest {
|
||||
pub version: String,
|
||||
pub published_at: String,
|
||||
pub channel: String,
|
||||
pub mac: Option<PlatformEntry>,
|
||||
pub windows: Option<PlatformEntry>,
|
||||
pub min_supported_version: String,
|
||||
pub notes: String,
|
||||
pub activation_height: Option<u64>,
|
||||
pub deadline_note: String,
|
||||
/// consensus.override: the exact object the engine writes to <app data>/override.json for igneumd's
|
||||
/// --override-params-file (for example {"difficulty_v2_activation_daa": N}); signed with the rest of the manifest.
|
||||
pub override_params: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl Manifest {
|
||||
pub fn platform(&self, name: &str) -> Option<&PlatformEntry> {
|
||||
match name {
|
||||
"mac" => self.mac.as_ref(),
|
||||
"windows" => self.windows.as_ref(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn this_platform(&self) -> Option<&PlatformEntry> {
|
||||
self.platform(platform_name())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn platform_name() -> &'static str {
|
||||
if cfg!(target_os = "macos") {
|
||||
"mac"
|
||||
} else if cfg!(windows) {
|
||||
"windows"
|
||||
} else {
|
||||
"linux"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hex_decode(s: &str) -> Option<Vec<u8>> {
|
||||
let s = s.trim();
|
||||
if s.len() % 2 != 0 {
|
||||
return None;
|
||||
}
|
||||
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok()).collect()
|
||||
}
|
||||
|
||||
pub fn hex_encode(b: &[u8]) -> String {
|
||||
b.iter().map(|x| format!("{x:02x}")).collect()
|
||||
}
|
||||
|
||||
pub fn public_key(hex: &str) -> Result<VerifyingKey, String> {
|
||||
let bytes = hex_decode(hex).ok_or("public key is not hex")?;
|
||||
let arr: [u8; 32] = bytes.try_into().map_err(|_| "public key is not 32 bytes")?;
|
||||
VerifyingKey::from_bytes(&arr).map_err(|e| format!("public key invalid: {e}"))
|
||||
}
|
||||
|
||||
/// SHA-256 of the raw public key bytes, as hex: what the report and the docs quote.
|
||||
pub fn fingerprint(pub_hex: &str) -> String {
|
||||
match hex_decode(pub_hex) {
|
||||
Some(b) => hex_encode(&Sha256::digest(&b)),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks the detached signature (hex) over the manifest bytes with the given public key (hex).
|
||||
pub fn verify_signature(manifest_bytes: &[u8], sig_hex: &str, pub_hex: &str) -> Result<(), String> {
|
||||
let key = public_key(pub_hex)?;
|
||||
let sig = hex_decode(sig_hex).ok_or("signature is not hex")?;
|
||||
let sig: [u8; 64] = sig.try_into().map_err(|_| "signature is not 64 bytes")?;
|
||||
let sig = Signature::from_bytes(&sig);
|
||||
key.verify(manifest_bytes, &sig).map_err(|_| "manifest signature does not verify".to_string())
|
||||
}
|
||||
|
||||
/// Parses the manifest JSON (after the signature was checked).
|
||||
pub fn parse(text: &str) -> Result<Manifest, String> {
|
||||
let v: serde_json::Value = serde_json::from_str(text).map_err(|e| format!("manifest is not JSON: {e}"))?;
|
||||
let s = |v: &serde_json::Value, k: &str| v.get(k).and_then(|x| x.as_str()).unwrap_or("").to_string();
|
||||
let version = s(&v, "version");
|
||||
if parse_version(&version).is_none() {
|
||||
return Err(format!("manifest version '{version}' is not a version"));
|
||||
}
|
||||
let entry = |name: &str| -> Result<Option<PlatformEntry>, String> {
|
||||
let Some(p) = v.get("platforms").and_then(|p| p.get(name)) else { return Ok(None) };
|
||||
if p.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
let e = PlatformEntry { url: s(p, "url"), sha256: s(p, "sha256").to_ascii_lowercase(), size: p.get("size").and_then(|x| x.as_u64()).unwrap_or(0), kind: s(p, "kind") };
|
||||
if !e.url.starts_with("https://") && !e.url.starts_with("http://127.0.0.1:") {
|
||||
return Err(format!("{name}: the url is not https"));
|
||||
}
|
||||
if e.sha256.len() != 64 || !e.sha256.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return Err(format!("{name}: sha256 is not 64 hex characters"));
|
||||
}
|
||||
if e.size == 0 {
|
||||
return Err(format!("{name}: size is missing"));
|
||||
}
|
||||
if !["dmg", "zip", "inno-setup"].contains(&e.kind.as_str()) {
|
||||
return Err(format!("{name}: kind '{}' is unknown", e.kind));
|
||||
}
|
||||
Ok(Some(e))
|
||||
};
|
||||
let consensus = v.get("consensus").cloned().unwrap_or(serde_json::Value::Null);
|
||||
Ok(Manifest {
|
||||
version,
|
||||
published_at: s(&v, "published_at"),
|
||||
channel: s(&v, "channel"),
|
||||
mac: entry("mac")?,
|
||||
windows: entry("windows")?,
|
||||
min_supported_version: s(&v, "min_supported_version"),
|
||||
notes: s(&v, "notes"),
|
||||
activation_height: consensus.get("activation_height").and_then(|x| x.as_u64()),
|
||||
deadline_note: s(&consensus, "deadline_note"),
|
||||
override_params: match consensus.get("override") {
|
||||
Some(o) if o.is_object() && !o.as_object().unwrap().is_empty() => Some(o.clone()),
|
||||
Some(o) if !o.is_null() => return Err("consensus.override must be an object".into()),
|
||||
_ => None,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Verifies, then parses.
|
||||
pub fn verify_and_parse(manifest_bytes: &[u8], sig_hex: &str, pub_hex: &str) -> Result<Manifest, String> {
|
||||
verify_signature(manifest_bytes, sig_hex, pub_hex)?;
|
||||
let text = std::str::from_utf8(manifest_bytes).map_err(|_| "manifest is not UTF-8")?;
|
||||
parse(text)
|
||||
}
|
||||
|
||||
/// A digest of a whole directory (the staged app bundle): sha256 over "relative path\nsha256 of the file\n" for every
|
||||
/// regular file in byte-sorted path order (symlinks skipped). The macOS helper recomputes the same with find, sort
|
||||
/// and shasum right before the swap (R4.3.5).
|
||||
pub fn digest_dir(root: &std::path::Path) -> std::io::Result<String> {
|
||||
fn walk(dir: &std::path::Path, root: &std::path::Path, out: &mut Vec<String>) -> std::io::Result<()> {
|
||||
for e in std::fs::read_dir(dir)? {
|
||||
let e = e?;
|
||||
let ft = e.file_type()?;
|
||||
let p = e.path();
|
||||
if ft.is_symlink() {
|
||||
continue;
|
||||
}
|
||||
if ft.is_dir() {
|
||||
walk(&p, root, out)?;
|
||||
} else if ft.is_file() {
|
||||
out.push(p.strip_prefix(root).unwrap_or(&p).to_string_lossy().replace('\\', "/"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
let mut files = Vec::new();
|
||||
walk(root, root, &mut files)?;
|
||||
files.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
|
||||
let mut h = Sha256::new();
|
||||
for f in files {
|
||||
let sum = sha256_file(&root.join(&f))?;
|
||||
h.update(f.as_bytes());
|
||||
h.update(b"\n");
|
||||
h.update(sum.as_bytes());
|
||||
h.update(b"\n");
|
||||
}
|
||||
Ok(hex_encode(&h.finalize()))
|
||||
}
|
||||
|
||||
/// SHA-256 of a file, streamed, as hex.
|
||||
pub fn sha256_file(path: &std::path::Path) -> std::io::Result<String> {
|
||||
use std::io::Read;
|
||||
let mut f = std::fs::File::open(path)?;
|
||||
let mut h = Sha256::new();
|
||||
let mut buf = vec![0u8; 1 << 20];
|
||||
loop {
|
||||
let n = f.read(&mut buf)?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
h.update(&buf[..n]);
|
||||
}
|
||||
Ok(hex_encode(&h.finalize()))
|
||||
}
|
||||
|
||||
// ---- versions -----------------------------------------------------------------------------------------------
|
||||
|
||||
/// major.minor.patch plus an optional pre-release tag ("0.4.0-rc1" sorts before "0.4.0"). A leading "v" is allowed.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Version {
|
||||
pub parts: [u64; 3],
|
||||
pub pre: Option<String>,
|
||||
}
|
||||
|
||||
pub fn parse_version(s: &str) -> Option<Version> {
|
||||
let s = s.trim().trim_start_matches('v');
|
||||
if s.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let (num, pre) = match s.split_once('-') {
|
||||
Some((n, p)) if !p.is_empty() => (n, Some(p.to_string())),
|
||||
Some(_) => return None,
|
||||
None => (s, None),
|
||||
};
|
||||
let mut parts = [0u64; 3];
|
||||
let mut n = 0;
|
||||
for p in num.split('.') {
|
||||
if n >= 3 || p.is_empty() {
|
||||
return None;
|
||||
}
|
||||
parts[n] = p.parse().ok()?;
|
||||
n += 1;
|
||||
}
|
||||
if n == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(Version { parts, pre })
|
||||
}
|
||||
|
||||
impl PartialOrd for Version {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Version {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
match self.parts.cmp(&other.parts) {
|
||||
std::cmp::Ordering::Equal => match (&self.pre, &other.pre) {
|
||||
(None, None) => std::cmp::Ordering::Equal,
|
||||
(None, Some(_)) => std::cmp::Ordering::Greater,
|
||||
(Some(_), None) => std::cmp::Ordering::Less,
|
||||
(Some(a), Some(b)) => a.cmp(b),
|
||||
},
|
||||
o => o,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// True when `latest` is a newer version than `current`. Unparseable input is never newer.
|
||||
pub fn newer(latest: &str, current: &str) -> bool {
|
||||
match (parse_version(latest), parse_version(current)) {
|
||||
(Some(a), Some(b)) => a > b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
// ---- when to apply --------------------------------------------------------------------------------------------
|
||||
|
||||
/// What the engine knows when it asks whether now is a safe moment.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Moment {
|
||||
pub node_synced: bool,
|
||||
/// DAA blocks (about seconds) to the next hourly program boundary; None when the node has not said.
|
||||
pub boundary_eta_s: Option<i64>,
|
||||
/// A worker is starting, exporting a pack or being built: let it finish.
|
||||
pub miner_busy: bool,
|
||||
/// How long the update has been ready and waiting.
|
||||
pub ready_for_s: u64,
|
||||
/// A consensus activation is close, or this version is below min_supported_version: now beats later.
|
||||
pub urgent: bool,
|
||||
/// This minute is the machine's own slot (slot_minute): machines take turns, two never restart together.
|
||||
pub slot_ok: bool,
|
||||
/// How much of the network's identity count (/api/live, last 10 minutes) is gone right now, in percent.
|
||||
pub network_drop_pct: f64,
|
||||
}
|
||||
|
||||
/// The percentage of identities the network may lose in 10 minutes before updates hold (we are the devnet).
|
||||
pub const NETWORK_DROP_HOLD_PCT: f64 = 30.0;
|
||||
|
||||
/// The minute of the hour in which this machine applies updates: the first 8 hex of the machine id modulo 60.
|
||||
pub fn slot_minute(id8: &str) -> u64 {
|
||||
u64::from_str_radix(id8.trim(), 16).unwrap_or(0) % 60
|
||||
}
|
||||
|
||||
/// Ok when the update may be applied now; Err carries the reason to wait, in the words the dashboard shows.
|
||||
pub fn safe_to_apply(m: &Moment) -> Result<(), String> {
|
||||
if m.urgent {
|
||||
return Ok(());
|
||||
}
|
||||
if m.network_drop_pct > NETWORK_DROP_HOLD_PCT {
|
||||
return Err(format!("the network lost {:.0}% of its identities in the last 10 minutes; holding the update", m.network_drop_pct));
|
||||
}
|
||||
if !m.slot_ok {
|
||||
return Err("waiting for this machine's own minute of the hour (machines take turns)".into());
|
||||
}
|
||||
if m.ready_for_s >= SAFE_MOMENT_PATIENCE_S {
|
||||
return Ok(());
|
||||
}
|
||||
if !m.node_synced {
|
||||
return Err("waiting for the node to sync".into());
|
||||
}
|
||||
if let Some(eta) = m.boundary_eta_s {
|
||||
if (0..=BOUNDARY_GUARD_S).contains(&eta) {
|
||||
return Err(format!("hourly program boundary in {} s; installing after it", eta.max(1)));
|
||||
}
|
||||
}
|
||||
if m.miner_busy {
|
||||
return Err("a worker is starting; installing once it runs".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A consensus activation is within FORK_URGENT_BLOCKS of the node's DAA score (and the node has a score).
|
||||
pub fn fork_is_close(activation_height: Option<u64>, daa: u64) -> bool {
|
||||
match activation_height {
|
||||
Some(h) if daa > 0 => daa.saturating_add(FORK_URGENT_BLOCKS) >= h,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// `current` is older than the manifest's min_supported_version.
|
||||
pub fn unsupported(m: &Manifest, current: &str) -> bool {
|
||||
!m.min_supported_version.is_empty() && newer(&m.min_supported_version, current)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
||||
/// The helper's bash recipe (find | sort | shasum per file | shasum) must equal digest_dir.
|
||||
#[test]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn digest_dir_matches_the_helper() {
|
||||
let root = std::env::temp_dir().join(format!("igneum-digest-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
std::fs::create_dir_all(root.join("Contents/MacOS")).unwrap();
|
||||
std::fs::write(root.join("Contents/MacOS/igneum-app"), b"engine").unwrap();
|
||||
std::fs::write(root.join("Contents/Info.plist"), b"<plist/>").unwrap();
|
||||
std::fs::write(root.join("Contents/zed.txt"), b"z").unwrap();
|
||||
let ours = digest_dir(&root).unwrap();
|
||||
let recipe = r#"(cd "$1" && /usr/bin/find . -type f -print | LC_ALL=C /usr/bin/sort | while IFS= read -r f; do printf '%s\n%s\n' "${f#./}" "$(/usr/bin/shasum -a 256 "$f" | /usr/bin/cut -d' ' -f1)"; done) | /usr/bin/shasum -a 256 | /usr/bin/cut -d' ' -f1"#;
|
||||
let out = std::process::Command::new("/bin/bash").args(["-c", recipe, "x", &root.display().to_string()]).output().unwrap();
|
||||
let theirs = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
assert_eq!(ours, theirs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consensus_override_parses() {
|
||||
let m = parse(r#"{"version":"0.3.2","platforms":{},"consensus":{"activation_height":5000,"override":{"difficulty_v2_activation_daa":5000}}}"#).unwrap();
|
||||
assert_eq!(m.activation_height, Some(5000));
|
||||
assert_eq!(m.override_params.unwrap()["difficulty_v2_activation_daa"], 5000);
|
||||
assert!(parse(r#"{"version":"0.3.2","platforms":{},"consensus":{"override":"no"}}"#).is_err());
|
||||
}
|
||||
|
||||
const SAMPLE: &str = r#"{"channel":"devnet","consensus":{"activation_height":120000,"deadline_note":"difficulty v2"},"min_supported_version":"0.3.0","notes":"difficulty v2 at height 120000","platforms":{"mac":{"kind":"dmg","sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","size":20588331,"url":"https://dl.igneum.network/dl/t/Igneum-Miner-0.3.1.dmg"},"windows":{"kind":"inno-setup","sha256":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","size":43978429,"url":"https://dl.igneum.network/dl/t/Igneum-Miner-Setup-0.3.1.exe"}},"published_at":"2026-10-04T13:00:00Z","version":"0.3.1"}"#;
|
||||
|
||||
fn key() -> (SigningKey, String) {
|
||||
let sk = SigningKey::from_bytes(&[7u8; 32]);
|
||||
let pk = hex_encode(sk.verifying_key().as_bytes());
|
||||
(sk, pk)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_manifest() {
|
||||
let m = parse(SAMPLE).unwrap();
|
||||
assert_eq!(m.version, "0.3.1");
|
||||
assert_eq!(m.channel, "devnet");
|
||||
assert_eq!(m.activation_height, Some(120000));
|
||||
assert_eq!(m.deadline_note, "difficulty v2");
|
||||
assert_eq!(m.min_supported_version, "0.3.0");
|
||||
let mac = m.mac.as_ref().unwrap();
|
||||
assert_eq!(mac.kind, "dmg");
|
||||
assert_eq!(mac.size, 20588331);
|
||||
assert_eq!(m.windows.as_ref().unwrap().kind, "inno-setup");
|
||||
assert_eq!(m.platform("linux"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_platform_is_none_and_bad_entries_fail() {
|
||||
let m = parse(r#"{"version":"0.3.1","platforms":{"mac":null},"consensus":{"activation_height":null}}"#).unwrap();
|
||||
assert!(m.mac.is_none() && m.windows.is_none());
|
||||
assert_eq!(m.activation_height, None);
|
||||
assert!(parse(r#"{"version":"0.3.1","platforms":{"mac":{"url":"http://x","sha256":"aa","size":1,"kind":"dmg"}}}"#).unwrap_err().contains("https"));
|
||||
assert!(parse(r#"{"version":"0.3.1","platforms":{"mac":{"url":"http://127.0.0.1:29790/x.dmg","sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","size":1,"kind":"dmg"}}}"#).is_ok());
|
||||
assert!(parse(r#"{"version":"0.3.1","platforms":{"mac":{"url":"https://x","sha256":"aa","size":1,"kind":"dmg"}}}"#).unwrap_err().contains("sha256"));
|
||||
assert!(parse(r#"{"version":"0.3.1","platforms":{"mac":{"url":"https://x","sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","size":1,"kind":"tar"}}}"#).unwrap_err().contains("kind"));
|
||||
assert!(parse(r#"{"version":"latest"}"#).is_err());
|
||||
assert!(parse("not json").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_verifies_and_tampering_fails() {
|
||||
let (sk, pk) = key();
|
||||
let sig = hex_encode(&sk.sign(SAMPLE.as_bytes()).to_bytes());
|
||||
assert!(verify_signature(SAMPLE.as_bytes(), &sig, &pk).is_ok());
|
||||
let m = verify_and_parse(SAMPLE.as_bytes(), &sig, &pk).unwrap();
|
||||
assert_eq!(m.version, "0.3.1");
|
||||
// a tampered sha256 inside the manifest: the bytes changed, the signature no longer verifies
|
||||
let tampered = SAMPLE.replace("aaaaaaaa", "aaaaaaab");
|
||||
assert!(verify_and_parse(tampered.as_bytes(), &sig, &pk).is_err());
|
||||
// a bad signature
|
||||
let mut bad = sig.clone();
|
||||
bad.replace_range(0..2, if &sig[0..2] == "00" { "01" } else { "00" });
|
||||
assert!(verify_signature(SAMPLE.as_bytes(), &bad, &pk).is_err());
|
||||
// another key
|
||||
let other = hex_encode(SigningKey::from_bytes(&[9u8; 32]).verifying_key().as_bytes());
|
||||
assert!(verify_signature(SAMPLE.as_bytes(), &sig, &other).is_err());
|
||||
// garbage
|
||||
assert!(verify_signature(SAMPLE.as_bytes(), "zz", &pk).is_err());
|
||||
assert!(verify_signature(SAMPLE.as_bytes(), &sig, "abcd").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sha256_of_file_is_checked_by_the_caller() {
|
||||
let dir = std::env::temp_dir().join(format!("igneum-manifest-test-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let f = dir.join("x.bin");
|
||||
std::fs::write(&f, b"abc").unwrap();
|
||||
assert_eq!(sha256_file(&f).unwrap(), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
|
||||
std::fs::write(&f, b"abd").unwrap();
|
||||
assert_ne!(sha256_file(&f).unwrap(), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn versions() {
|
||||
assert!(newer("0.3.1", "0.3.0"));
|
||||
assert!(newer("0.4.0", "0.3.9"));
|
||||
assert!(newer("1.0.0", "0.99.99"));
|
||||
assert!(newer("v0.3.1", "0.3.0"));
|
||||
assert!(!newer("0.3.0", "0.3.0"));
|
||||
assert!(!newer("0.2.9", "0.3.0"));
|
||||
assert!(!newer("0.3", "0.3.0"));
|
||||
assert!(newer("0.3.1", "0.3"));
|
||||
assert!(newer("0.3.1", "0.3.1-rc1"));
|
||||
assert!(!newer("0.3.1-rc1", "0.3.1"));
|
||||
assert!(newer("0.3.1-rc2", "0.3.1-rc1"));
|
||||
assert!(!newer("", "0.3.0"));
|
||||
assert!(!newer("latest", "0.3.0"));
|
||||
assert!(!newer("0.3.1", "garbage"));
|
||||
assert!(!newer("0.3.1-", "0.3.0"));
|
||||
assert!(!newer("0.3.1.2", "0.3.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn safe_moments() {
|
||||
let base = Moment { node_synced: true, boundary_eta_s: Some(1800), miner_busy: false, ready_for_s: 60, urgent: false, slot_ok: true, network_drop_pct: 0.0 };
|
||||
assert!(safe_to_apply(&base).is_ok());
|
||||
assert_eq!(safe_to_apply(&Moment { node_synced: false, ..base.clone() }).unwrap_err(), "waiting for the node to sync");
|
||||
assert!(safe_to_apply(&Moment { boundary_eta_s: Some(120), ..base.clone() }).unwrap_err().contains("boundary in 120 s"));
|
||||
assert!(safe_to_apply(&Moment { boundary_eta_s: Some(0), ..base.clone() }).is_err());
|
||||
assert!(safe_to_apply(&Moment { boundary_eta_s: Some(181), ..base.clone() }).is_ok());
|
||||
assert!(safe_to_apply(&Moment { boundary_eta_s: None, ..base.clone() }).is_ok());
|
||||
assert!(safe_to_apply(&Moment { miner_busy: true, ..base.clone() }).unwrap_err().contains("worker"));
|
||||
// urgent beats every wait
|
||||
assert!(safe_to_apply(&Moment { node_synced: false, boundary_eta_s: Some(5), miner_busy: true, urgent: true, ..base.clone() }).is_ok());
|
||||
// patience: an unsynced node for 6 h applies anyway
|
||||
assert!(safe_to_apply(&Moment { node_synced: false, ready_for_s: SAFE_MOMENT_PATIENCE_S, ..base.clone() }).is_ok());
|
||||
assert!(safe_to_apply(&Moment { node_synced: false, ready_for_s: SAFE_MOMENT_PATIENCE_S - 1, ..base.clone() }).is_err());
|
||||
// the machine's slot: outside it nothing applies, not even with patience; urgent ignores it
|
||||
assert!(safe_to_apply(&Moment { slot_ok: false, ..base.clone() }).unwrap_err().contains("own minute"));
|
||||
assert!(safe_to_apply(&Moment { slot_ok: false, ready_for_s: SAFE_MOMENT_PATIENCE_S, ..base.clone() }).is_err());
|
||||
assert!(safe_to_apply(&Moment { slot_ok: false, urgent: true, ..base.clone() }).is_ok());
|
||||
// the network guard: over 30% of identities gone in 10 minutes holds the update (we are the devnet)
|
||||
assert!(safe_to_apply(&Moment { network_drop_pct: 30.0, ..base.clone() }).is_ok());
|
||||
assert!(safe_to_apply(&Moment { network_drop_pct: 30.1, ..base.clone() }).unwrap_err().contains("lost 30%"));
|
||||
assert!(safe_to_apply(&Moment { network_drop_pct: 80.0, ready_for_s: SAFE_MOMENT_PATIENCE_S, ..base.clone() }).is_err());
|
||||
assert!(safe_to_apply(&Moment { network_drop_pct: 80.0, urgent: true, ..base.clone() }).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slots() {
|
||||
assert_eq!(slot_minute("1ccfe586"), 58); // PC 2
|
||||
assert_eq!(slot_minute("00000000"), 0);
|
||||
assert_eq!(slot_minute("0000003c"), 0);
|
||||
assert_eq!(slot_minute("0000003b"), 59);
|
||||
assert_eq!(slot_minute("zz"), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fork_closeness_and_support() {
|
||||
assert!(!fork_is_close(None, 100_000));
|
||||
assert!(!fork_is_close(Some(120_000), 0));
|
||||
assert!(!fork_is_close(Some(120_000), 118_199));
|
||||
assert!(fork_is_close(Some(120_000), 118_200));
|
||||
assert!(fork_is_close(Some(120_000), 120_000));
|
||||
assert!(fork_is_close(Some(120_000), 130_000));
|
||||
let m = parse(SAMPLE).unwrap();
|
||||
assert!(!unsupported(&m, "0.3.0"));
|
||||
assert!(unsupported(&m, "0.2.9"));
|
||||
assert!(!unsupported(&parse(r#"{"version":"0.3.1"}"#).unwrap(), "0.0.1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_is_sha256_of_key_bytes() {
|
||||
let (_, pk) = key();
|
||||
assert_eq!(fingerprint(&pk).len(), 64);
|
||||
assert_eq!(fingerprint("zz"), "");
|
||||
}
|
||||
}
|
||||
524
app/igneum-common/src/ota.rs
Normal file
524
app/igneum-common/src/ota.rs
Normal file
|
|
@ -0,0 +1,524 @@
|
|||
//! The apply side of an over-the-air update, shared by both apps. Taken from app/igneum-app/src/ota.rs (the miner's
|
||||
//! updater, 4 October 2026; the miner keeps its own copy until it moves to this crate) with the app's names filled in
|
||||
//! from `AppId`: the staged bundle, the detached helper that swaps it in and relaunches, and the pending/result files
|
||||
//! the old engine, the helper and the new engine pass around. The manifest check and the download live in
|
||||
//! `crate::fetch`; when to apply is each app's own business (the miner waits for a safe mining moment, the wallet for
|
||||
//! no send in flight).
|
||||
//!
|
||||
//! macOS: `stage` mounts the disk image (or unpacks the zip), copies the bundle next to the running one as
|
||||
//! ".<App>.app.new" (same volume, so the swap is two renames) and checks the new engine answers --version with the
|
||||
//! manifest's version. `launch_apply` re-hashes the download and the staged bundle, writes update-pending.json and
|
||||
//! ota-apply.sh, starts the helper detached and returns `Launch::QuitNow`: the engine leaves through its quit path.
|
||||
//! The helper waits for the engine, asks the window to quit (by bundle id), moves the old bundle to
|
||||
//! "<App>.app.previous", the staged one in, opens the new app, and puts the previous one back when the new app does
|
||||
//! not start twice. The new engine counts its starts in update-pending.json; on the third start without
|
||||
//! `HEALTHY_AFTER_S` healthy seconds it asks for `launch_rollback`.
|
||||
//! Windows: the staged artefact is the Inno installer. `launch_apply` starts ota-apply.ps1 detached (CREATE_NO_WINDOW,
|
||||
//! commit 0d123b3), which runs the installer /VERYSILENT first while the engine keeps running; the installer stops the
|
||||
//! engine itself (api/quit) and relaunches the app with /IGNOTA=1. An unanswered administrator prompt comes back as
|
||||
//! `deferred` in update-result.json. The wallet has never run this path (5 October 2026): untested there.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::manifest::{self, PlatformEntry};
|
||||
use crate::AppId;
|
||||
use serde_json::{json, Value};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
|
||||
/// A new version is healthy once it has run this long; the update is then complete and the leftovers go.
|
||||
pub const HEALTHY_AFTER_S: u64 = 90;
|
||||
|
||||
/// What launch_apply started.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Launch {
|
||||
/// macOS: the helper waits for this engine to exit; the engine leaves through its quit path now.
|
||||
QuitNow,
|
||||
/// Windows: the installer runs first while the engine keeps running; the installer stops the engine itself
|
||||
/// once it is allowed to run. The engine stays up and watches update-result.json for a deferral.
|
||||
InstallerRunning,
|
||||
}
|
||||
|
||||
/// What the old engine leaves for the new one (update-pending.json).
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Pending {
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
pub at: f64,
|
||||
pub starts: u32,
|
||||
pub previous_installer: String,
|
||||
}
|
||||
|
||||
/// The helper's verdict (update-result.json).
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct HelperResult {
|
||||
pub ok: bool,
|
||||
pub version: String,
|
||||
pub error: String,
|
||||
pub rolled_back: bool,
|
||||
pub deferred: bool,
|
||||
}
|
||||
|
||||
pub fn pending_path(app_dir: &Path) -> PathBuf {
|
||||
app_dir.join("update-pending.json")
|
||||
}
|
||||
|
||||
pub fn result_path(app_dir: &Path) -> PathBuf {
|
||||
app_dir.join("update-result.json")
|
||||
}
|
||||
|
||||
pub fn read_pending(app_dir: &Path) -> Option<Pending> {
|
||||
parse_pending(&std::fs::read_to_string(pending_path(app_dir)).ok()?)
|
||||
}
|
||||
|
||||
pub fn parse_pending(text: &str) -> Option<Pending> {
|
||||
let v: Value = serde_json::from_str(text).ok()?;
|
||||
let s = |k: &str| v.get(k).and_then(|x| x.as_str()).unwrap_or("").to_string();
|
||||
Some(Pending { from: s("from"), to: s("to"), at: v.get("at").and_then(|x| x.as_f64()).unwrap_or(0.0), starts: v.get("starts").and_then(|x| x.as_u64()).unwrap_or(0) as u32, previous_installer: s("previous_installer") })
|
||||
}
|
||||
|
||||
pub fn write_pending(app_dir: &Path, p: &Pending) {
|
||||
let v = json!({ "from": p.from, "to": p.to, "at": p.at, "starts": p.starts, "previous_installer": p.previous_installer, "platform": manifest::platform_name() });
|
||||
let _ = std::fs::write(pending_path(app_dir), v.to_string());
|
||||
}
|
||||
|
||||
pub fn read_result(app_dir: &Path) -> Option<HelperResult> {
|
||||
parse_result(&std::fs::read_to_string(result_path(app_dir)).ok()?)
|
||||
}
|
||||
|
||||
pub fn parse_result(text: &str) -> Option<HelperResult> {
|
||||
let v: Value = serde_json::from_str(text).ok()?;
|
||||
let b = |k: &str| v.get(k).and_then(|x| x.as_bool()).unwrap_or(false);
|
||||
let s = |k: &str| v.get(k).and_then(|x| x.as_str()).unwrap_or("").to_string();
|
||||
Some(HelperResult { ok: b("ok"), version: s("version"), error: s("error"), rolled_back: b("rolled_back"), deferred: b("deferred") })
|
||||
}
|
||||
|
||||
/// "Igneum-Wallet-Setup-0.1.1.exe": the installer name the Windows packaging gives a version.
|
||||
pub fn installer_name_for(app: AppId, version: &str) -> String {
|
||||
format!("{}-Setup-{version}.exe", app.name.replace(' ', "-"))
|
||||
}
|
||||
|
||||
/// The staged bundle's path next to the running one (macOS).
|
||||
pub fn staged_path(app: AppId, bundle: &Path) -> Option<PathBuf> {
|
||||
bundle.parent().map(|p| p.join(format!(".{}.app.new", app.name)))
|
||||
}
|
||||
|
||||
/// Starts a process that outlives the engine (stdio closed, own session on unix, no window on Windows).
|
||||
pub fn spawn_detached(c: &mut Command) -> Result<(), String> {
|
||||
use std::process::Stdio;
|
||||
c.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
c.process_group(0);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
c.creation_flags(0x0800_0000 | 0x0000_0008); // CREATE_NO_WINDOW | DETACHED_PROCESS
|
||||
}
|
||||
c.spawn().map(|_| ()).map_err(|e| format!("cannot start the helper: {e}"))
|
||||
}
|
||||
|
||||
/// The engine's own environment for the helper's relaunch (a test run on a private devnet or a scratch data folder):
|
||||
/// every variable whose name starts with one of `prefixes`, written to <app dir>/ota-relaunch.env. "" when there is
|
||||
/// none, and the helper opens the bundle through LaunchServices.
|
||||
pub fn write_env_file(app_dir: &Path, prefixes: &[&str]) -> String {
|
||||
let vars: Vec<String> = std::env::vars().filter(|(k, _)| prefixes.iter().any(|p| k.starts_with(p))).map(|(k, v)| format!("{k}={v}")).collect();
|
||||
if vars.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
let p = app_dir.join("ota-relaunch.env");
|
||||
if std::fs::write(&p, vars.join("\n") + "\n").is_ok() { p.display().to_string() } else { String::new() }
|
||||
}
|
||||
|
||||
/// macOS: the new bundle next to the running one (same volume, so the swap is two renames); Windows: the installer
|
||||
/// is the staged artefact. Err("manual: ...") when the engine cannot swap itself (not in a bundle, a read-only
|
||||
/// Applications folder): the window then offers the download instead.
|
||||
#[allow(unused_variables)]
|
||||
pub fn stage(app: AppId, e: &PlatformEntry, file: &Path, dir: &Path, version: &str) -> Result<PathBuf, String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let bundle_name = format!("{}.app", app.name);
|
||||
let bundle = crate::platform::bundle_path().ok_or(format!("manual: the engine is not running from {bundle_name}; open the downloaded disk image and drag the app to Applications"))?;
|
||||
let parent = bundle.parent().ok_or("no parent folder")?;
|
||||
let staged = staged_path(app, &bundle).ok_or("no parent folder")?;
|
||||
let _ = std::fs::remove_dir_all(&staged);
|
||||
// writable? a user-owned /Applications is; a managed Mac may not be
|
||||
if std::fs::create_dir(&staged).is_err() {
|
||||
return Err(format!("manual: {} is not writable; open the downloaded disk image and drag the app over the old one", parent.display()));
|
||||
}
|
||||
let _ = std::fs::remove_dir(&staged);
|
||||
let work = dir.join("unpack");
|
||||
let _ = std::fs::remove_dir_all(&work);
|
||||
std::fs::create_dir_all(&work).map_err(|e| e.to_string())?;
|
||||
let source: PathBuf;
|
||||
let mut mounted: Option<PathBuf> = None;
|
||||
if e.kind == "dmg" {
|
||||
let mnt = work.join("mnt");
|
||||
std::fs::create_dir_all(&mnt).map_err(|e| e.to_string())?;
|
||||
let out = crate::run::run_timeout(Command::new(crate::platform::tool("hdiutil")).args(["attach", "-nobrowse", "-readonly", "-noautoopen", "-noverify", "-mountpoint", &mnt.display().to_string(), &file.display().to_string()]), None, Duration::from_secs(120)).unwrap_or_default();
|
||||
if !mnt.join(&bundle_name).is_dir() {
|
||||
return Err(format!("the disk image has no {bundle_name} ({})", out.lines().last().unwrap_or("hdiutil said nothing")));
|
||||
}
|
||||
mounted = Some(mnt.clone());
|
||||
source = mnt.join(&bundle_name);
|
||||
} else {
|
||||
let out = crate::run::run_timeout(Command::new(crate::platform::tool("ditto")).args(["-x", "-k", &file.display().to_string(), &work.display().to_string()]), None, Duration::from_secs(300)).unwrap_or_default();
|
||||
source = find_app(&work, &bundle_name).ok_or(format!("the zip has no {bundle_name} ({})", out.lines().last().unwrap_or("")))?;
|
||||
}
|
||||
let engine = staged.join("Contents/MacOS").join(app.engine_exe);
|
||||
let r = (|| -> Result<(), String> {
|
||||
let out = crate::run::run_timeout(Command::new(crate::platform::tool("ditto")).arg(&source).arg(&staged), None, Duration::from_secs(300)).unwrap_or_default();
|
||||
if !engine.is_file() {
|
||||
return Err(format!("copy failed: {}", out.lines().last().unwrap_or("")));
|
||||
}
|
||||
// the quarantine flag comes off only after the file this bundle came from verified again, now
|
||||
let again = manifest::sha256_file(file).map_err(|e| e.to_string())?;
|
||||
if again != e.sha256 {
|
||||
return Err("the download changed while it was being unpacked; discarded".into());
|
||||
}
|
||||
let _ = Command::new(crate::platform::tool("xattr")).args(["-dr", "com.apple.quarantine"]).arg(&staged).output();
|
||||
let v = crate::run::run_timeout(Command::new(&engine).arg("--version"), None, Duration::from_secs(20)).unwrap_or_default();
|
||||
let want = format!("{} {version}", app.engine_exe);
|
||||
if v.trim() != want {
|
||||
return Err(format!("the new engine answers '{}' to --version, the manifest says {version}", v.trim()));
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
if let Some(m) = mounted {
|
||||
let _ = Command::new(crate::platform::tool("hdiutil")).args(["detach", "-force", &m.display().to_string()]).output();
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&work);
|
||||
if let Err(err) = r {
|
||||
let _ = std::fs::remove_dir_all(&staged);
|
||||
return Err(err);
|
||||
}
|
||||
Ok(staged)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if e.kind != "inno-setup" {
|
||||
return Err(format!("kind '{}' is not an installer", e.kind));
|
||||
}
|
||||
Ok(file.to_path_buf())
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", windows)))]
|
||||
{
|
||||
Err("manual: no automatic install on this platform".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn find_app(dir: &Path, bundle_name: &str) -> Option<PathBuf> {
|
||||
let rd = std::fs::read_dir(dir).ok()?;
|
||||
for e in rd.flatten() {
|
||||
let p = e.path();
|
||||
if p.file_name().map(|n| n == bundle_name).unwrap_or(false) && p.is_dir() {
|
||||
return Some(p);
|
||||
}
|
||||
if p.is_dir() {
|
||||
if let Some(f) = find_app(&p, bundle_name) {
|
||||
return Some(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Windows: an install under Program Files was made by an administrator installer.
|
||||
pub fn under_program_files(dir: &Path) -> bool {
|
||||
let d = dir.to_string_lossy().to_ascii_lowercase();
|
||||
["ProgramFiles", "ProgramFiles(x86)", "ProgramW6432"].iter().filter_map(|k| std::env::var(k).ok()).any(|pf| !pf.is_empty() && d.starts_with(&pf.to_ascii_lowercase()))
|
||||
}
|
||||
|
||||
/// Everything launch_apply needs. `staged_digest` is manifest::digest_dir of the staged bundle at stage time (macOS);
|
||||
/// `sha256` the manifest's for the installer (Windows); `env_file` from write_env_file or "".
|
||||
pub struct Apply<'a> {
|
||||
pub app: AppId,
|
||||
pub app_dir: &'a Path,
|
||||
pub current: &'a str,
|
||||
pub version: &'a str,
|
||||
pub staged: &'a Path,
|
||||
pub staged_digest: &'a str,
|
||||
pub sha256: &'a str,
|
||||
pub host_pid: u32,
|
||||
pub env_file: String,
|
||||
/// Windows: the installer of the version now running, kept in updates/ as the rollback target ("" when none)
|
||||
pub previous_installer: String,
|
||||
}
|
||||
|
||||
/// Writes update-pending.json and the helper, starts the helper detached. The caller verified the download and the
|
||||
/// staged bundle a moment ago (sha256 and digest_dir); the helper checks the digest once more before the swap.
|
||||
pub fn launch_apply(a: &Apply) -> Result<Launch, String> {
|
||||
write_pending(a.app_dir, &Pending { from: a.current.to_string(), to: a.version.to_string(), at: crate::platform::unix_now_f(), starts: 0, previous_installer: a.previous_installer.clone() });
|
||||
let _ = std::fs::remove_file(result_path(a.app_dir));
|
||||
let result = result_path(a.app_dir);
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let bundle = crate::platform::bundle_path().ok_or(format!("not running from {}.app", a.app.name))?;
|
||||
if a.staged_digest.is_empty() {
|
||||
return Err("no digest for the staged app".into());
|
||||
}
|
||||
let script = a.app_dir.join("ota-apply.sh");
|
||||
std::fs::write(&script, mac_helper(a.app)).map_err(|e| format!("cannot write the helper: {e}"))?;
|
||||
let args = ["apply".to_string(), std::process::id().to_string(), a.host_pid.to_string(), bundle.display().to_string(), a.staged.display().to_string(), a.version.to_string(), result.display().to_string(), a.env_file.clone(), a.staged_digest.to_string()];
|
||||
spawn_detached(Command::new(crate::platform::tool("nohup")).arg(crate::platform::tool("bash")).arg(&script).args(&args))?;
|
||||
Ok(Launch::QuitNow)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let install_dir = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_path_buf())).ok_or("cannot find the install folder")?;
|
||||
let script = a.app_dir.join("ota-apply.ps1");
|
||||
std::fs::write(&script, win_helper(a.app)).map_err(|e| format!("cannot write the helper: {e}"))?;
|
||||
let mut c = Command::new(crate::platform::tool("powershell"));
|
||||
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-File"]).arg(&script).args([
|
||||
"-Mode", "apply", "-EnginePid", &std::process::id().to_string(), "-Installer", &a.staged.display().to_string(), "-Version", a.version, "-Result", &result.display().to_string(), "-InstallDir", &install_dir.display().to_string(), "-Sha256", a.sha256,
|
||||
]);
|
||||
spawn_detached(&mut c)?;
|
||||
Ok(Launch::InstallerRunning)
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", windows)))]
|
||||
{
|
||||
let _ = result;
|
||||
Err("automatic apply is not supported on this platform".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// The new version failed to start twice: the helper restores the previous one (macOS: the .previous bundle;
|
||||
/// Windows: the previous installer kept in updates/). The caller exits afterwards.
|
||||
pub fn launch_rollback(app: AppId, app_dir: &Path, p: &Pending, host_pid: u32, env_file: String) -> Result<(), String> {
|
||||
let result = result_path(app_dir);
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let bundle = crate::platform::bundle_path().ok_or(format!("not running from {}.app", app.name))?;
|
||||
let script = app_dir.join("ota-apply.sh");
|
||||
std::fs::write(&script, mac_helper(app)).map_err(|e| format!("cannot write the helper: {e}"))?;
|
||||
let args = ["rollback".to_string(), std::process::id().to_string(), host_pid.to_string(), bundle.display().to_string(), String::new(), p.to.clone(), result.display().to_string(), env_file, String::new()];
|
||||
spawn_detached(Command::new(crate::platform::tool("nohup")).arg(crate::platform::tool("bash")).arg(&script).args(&args))?;
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = (host_pid, env_file);
|
||||
if p.previous_installer.is_empty() || !Path::new(&p.previous_installer).is_file() {
|
||||
return Err("no previous installer kept; reinstall from igneum.network".into());
|
||||
}
|
||||
let install_dir = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_path_buf())).ok_or("cannot find the install folder")?;
|
||||
let script = app_dir.join("ota-apply.ps1");
|
||||
std::fs::write(&script, win_helper(app)).map_err(|e| format!("cannot write the helper: {e}"))?;
|
||||
let sha = manifest::sha256_file(Path::new(&p.previous_installer)).unwrap_or_default();
|
||||
let mut c = Command::new(crate::platform::tool("powershell"));
|
||||
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-File"]).arg(&script).args([
|
||||
"-Sha256", &sha, "-Mode", "rollback", "-EnginePid", &std::process::id().to_string(), "-Installer", &p.previous_installer, "-Version", &p.to, "-Result", &result.display().to_string(), "-InstallDir", &install_dir.display().to_string(),
|
||||
]);
|
||||
spawn_detached(&mut c)?;
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", windows)))]
|
||||
{
|
||||
let _ = (app, p, host_pid, env_file, result);
|
||||
Err("rollback is not supported on this platform".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// The macOS helper with this app's names filled in.
|
||||
pub fn mac_helper(app: AppId) -> String {
|
||||
fill(MAC_HELPER, app)
|
||||
}
|
||||
|
||||
/// The Windows helper with this app's names filled in.
|
||||
pub fn win_helper(app: AppId) -> String {
|
||||
fill(WIN_HELPER, app)
|
||||
}
|
||||
|
||||
fn fill(template: &str, app: AppId) -> String {
|
||||
template.replace("@APP_NAME@", app.name).replace("@ENGINE@", app.engine_exe).replace("@BUNDLE@", app.bundle)
|
||||
}
|
||||
|
||||
const MAC_HELPER: &str = r#"#!/bin/bash
|
||||
# @APP_NAME@ update helper, written by the engine (igneum-common/src/ota.rs). Not for running by hand.
|
||||
# bash ota-apply.sh apply|rollback <engine pid> <host pid|0> <app bundle> <staged bundle> <version> <result json> [env file] [digest]
|
||||
# apply: waits for the engine (it exits right after starting this), asks the window to quit, moves the running
|
||||
# bundle to "<app>.previous" and the staged one in, opens the new app; if the new app does not start twice, puts the
|
||||
# previous one back. rollback: the previous bundle back, the failed one aside. Writes <result json> for the engine.
|
||||
MODE="$1"; EPID="$2"; HPID="$3"; APP="$4"; NEW="$5"; VER="$6"; RESULT="$7"; ENVF="${8:-}"; DIGEST="${9:-}"
|
||||
LOG="$(dirname "$RESULT")/ota-apply.log"
|
||||
exec >>"$LOG" 2>&1
|
||||
echo "$(date -u +%FT%TZ) $MODE: engine $EPID host $HPID app '$APP' new '$NEW' version $VER"
|
||||
gone() { ! kill -0 "$1" 2>/dev/null; }
|
||||
wait_gone() { local p="$1" n="$2"; while [ "$n" -gt 0 ] && ! gone "$p"; do sleep 0.5; n=$((n-1)); done; gone "$p"; }
|
||||
result() { printf '{"ok":%s,"version":"%s","error":"%s","rolled_back":%s,"at":%s}\n' "$1" "$VER" "$2" "$3" "$(date +%s)" > "$RESULT.tmp" && mv "$RESULT.tmp" "$RESULT"; }
|
||||
PREV="$APP.previous"
|
||||
FAILED="$APP.failed"
|
||||
ENGINE="$APP/Contents/MacOS/@ENGINE@"
|
||||
# the same digest the engine computed when it staged the bundle (manifest.rs digest_dir): every regular file,
|
||||
# byte-sorted relative path, "path\nsha256\n" per file, sha256 of the whole
|
||||
digest_dir() { (cd "$1" && /usr/bin/find . -type f -print | LC_ALL=C /usr/bin/sort | while IFS= read -r f; do printf '%s\n%s\n' "${f#./}" "$(/usr/bin/shasum -a 256 "$f" | /usr/bin/cut -d' ' -f1)"; done) | /usr/bin/shasum -a 256 | /usr/bin/cut -d' ' -f1; }
|
||||
started_ok() { local n=60; while [ "$n" -gt 0 ]; do pgrep -f "$ENGINE" >/dev/null 2>&1 && return 0; sleep 0.5; n=$((n-1)); done; return 1; }
|
||||
# a test run carries its environment to the relaunch (open -n cannot); IGNEUM_OTA_RELAUNCH_ENGINE=1 in that file runs
|
||||
# the engine alone (no window, a scratch test); a normal run goes through LaunchServices
|
||||
launch() {
|
||||
if [ -n "$ENVF" ] && [ -f "$ENVF" ]; then
|
||||
(set -a; . "$ENVF"; set +a; if [ "${IGNEUM_OTA_RELAUNCH_ENGINE:-}" = 1 ]; then /usr/bin/nohup "$ENGINE" --no-open >/dev/null 2>&1 & else /usr/bin/nohup "$APP/Contents/MacOS/@APP_NAME@" >/dev/null 2>&1 & fi)
|
||||
else
|
||||
/usr/bin/open -n "$APP"
|
||||
fi
|
||||
}
|
||||
wait_gone "$EPID" 240 || { echo "engine $EPID still running after 120 s; ending it"; kill -9 "$EPID" 2>/dev/null; sleep 1; }
|
||||
if [ -n "$HPID" ] && [ "$HPID" != 0 ] && ! gone "$HPID"; then
|
||||
/usr/bin/osascript -e 'tell application id "@BUNDLE@" to quit' >/dev/null 2>&1 || kill -TERM "$HPID" 2>/dev/null
|
||||
wait_gone "$HPID" 80 || { echo "window $HPID still running after 40 s; ending it"; kill -9 "$HPID" 2>/dev/null; sleep 1; }
|
||||
fi
|
||||
# anything else from this bundle (a stray engine of an older run)
|
||||
pkill -f "$APP/Contents/MacOS" 2>/dev/null; sleep 0.5
|
||||
case "$MODE" in
|
||||
apply)
|
||||
[ -d "$NEW" ] || { result false "the staged app is missing" false; launch; exit 1; }
|
||||
if [ -n "$DIGEST" ]; then
|
||||
have="$(digest_dir "$NEW")"
|
||||
if [ "$have" != "$DIGEST" ]; then echo "digest mismatch: staged $have, verified $DIGEST"; rm -rf "$NEW"; result false "the staged app changed since it was verified; not installed" false; launch; exit 1; fi
|
||||
echo "staged bundle digest verified"
|
||||
else
|
||||
echo "no digest given; not installing an unverified bundle"; result false "no digest for the staged app" false; launch; exit 1
|
||||
fi
|
||||
rm -rf "$PREV"
|
||||
mv "$APP" "$PREV" || { result false "could not move the old app aside" false; launch; exit 1; }
|
||||
mv "$NEW" "$APP" || { mv "$PREV" "$APP"; result false "could not move the new app in" false; launch; exit 1; }
|
||||
/usr/bin/xattr -dr com.apple.quarantine "$APP" 2>/dev/null # only a bundle whose digest just verified
|
||||
echo "swapped; opening $APP"
|
||||
launch || echo "open failed"
|
||||
if started_ok; then result true "" false; echo "$VER is running"; exit 0; fi
|
||||
echo "the new app did not start within 30 s; opening it once more"
|
||||
launch || true
|
||||
if started_ok; then result true "" false; echo "$VER is running (second try)"; exit 0; fi
|
||||
echo "the new app did not start twice; restoring the previous version"
|
||||
pkill -f "$APP/Contents/MacOS" 2>/dev/null; sleep 1
|
||||
rm -rf "$FAILED"; mv "$APP" "$FAILED" && mv "$PREV" "$APP"
|
||||
launch
|
||||
result false "@APP_NAME@ $VER did not start twice; the previous version was restored" true
|
||||
;;
|
||||
rollback)
|
||||
[ -d "$PREV" ] || { result false "no previous version kept to restore" false; launch; exit 1; }
|
||||
rm -rf "$FAILED"; mv "$APP" "$FAILED" && mv "$PREV" "$APP"
|
||||
launch
|
||||
result false "@APP_NAME@ $VER did not stay up twice; the previous version was restored" true
|
||||
;;
|
||||
*) echo "unknown mode $MODE"; exit 2 ;;
|
||||
esac
|
||||
"#;
|
||||
|
||||
const WIN_HELPER: &str = r#"# @APP_NAME@ update helper, written by the engine (igneum-common/src/ota.rs). Not for running by hand.
|
||||
# powershell -File ota-apply.ps1 -Mode apply|rollback -EnginePid <pid> -Installer <setup exe> -Version <v> -Result <json> -InstallDir <folder> -Sha256 <hex>
|
||||
# The installer runs FIRST, while the engine keeps running (4 October 2026: two unattended PCs sat stopped at an
|
||||
# administrator prompt nobody could click). A per-user installer (PrivilegesRequired=lowest) needs no prompt; an older
|
||||
# administrator installer raises one through ShellExecute. Only when the installer actually runs does its
|
||||
# PrepareToInstall step stop the engine (api/quit), replace the files and relaunch the app (/IGNOTA=1). A declined,
|
||||
# timed-out or unanswered prompt leaves the engine running: the result says deferred:true. The old app is relaunched
|
||||
# only when the engine is gone and the install did not happen.
|
||||
param([string]$Mode, [int]$EnginePid, [string]$Installer, [string]$Version, [string]$Result, [string]$InstallDir, [string]$Sha256 = '')
|
||||
$log = Join-Path (Split-Path -Parent $Result) 'ota-apply.log'
|
||||
function Log([string]$t) { Add-Content -Path $log -Value ("{0} {1}" -f (Get-Date -Format s), $t) }
|
||||
function Done([bool]$ok, [string]$err, [bool]$rb, [bool]$deferred) {
|
||||
$o = @{ ok = $ok; version = $Version; error = $err; rolled_back = $rb; deferred = $deferred; at = [int][double](Get-Date -UFormat %s) }
|
||||
($o | ConvertTo-Json -Compress) | Set-Content -Path $Result -Encoding ASCII
|
||||
}
|
||||
function EngineAlive() { return [bool](Get-Process -Id $EnginePid -ErrorAction SilentlyContinue) }
|
||||
function Relaunch() {
|
||||
if (EngineAlive) { return }
|
||||
$exe = Join-Path $InstallDir '@ENGINE@.exe'
|
||||
if (Test-Path $exe) { Log 'engine gone and nothing installed: starting the old app again'; Start-Process -FilePath $exe -ArgumentList '--launch' -WorkingDirectory $InstallDir | Out-Null }
|
||||
}
|
||||
Log "$Mode : engine $EnginePid installer '$Installer' version $Version (the engine keeps running until the installer runs)"
|
||||
if (-not (Test-Path $Installer)) { Log 'installer missing'; Done $false 'the downloaded installer is missing' $false $false; exit 1 }
|
||||
# the installer is hashed again right before it runs
|
||||
if (-not $Sha256) { Log 'no sha256 given'; Done $false 'no sha256 for the installer; not run' $false $false; exit 1 }
|
||||
$have = (Get-FileHash -Path $Installer -Algorithm SHA256).Hash.ToLower()
|
||||
if ($have -ne $Sha256.ToLower()) { Log "sha256 mismatch: $have"; Remove-Item -Path $Installer -Force -ErrorAction SilentlyContinue; Done $false 'the installer changed since it was verified; not run' $false $false; exit 1 }
|
||||
Log 'installer sha256 verified'
|
||||
$setupLog = Join-Path (Split-Path -Parent $Result) 'ota-setup.log'
|
||||
$setupArgs = @('/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', '/CLOSEAPPLICATIONS', '/IGNOTA=1', ('/LOG="' + $setupLog + '"'))
|
||||
try {
|
||||
# no -Verb RunAs: a per-user installer just runs; an administrator installer makes Windows ask, and a declined or
|
||||
# timed-out prompt comes back here as an exception with the engine still running
|
||||
$p = Start-Process -FilePath $Installer -ArgumentList $setupArgs -Wait -PassThru
|
||||
if ($p.ExitCode -eq 0) {
|
||||
if ($Mode -eq 'rollback') { Done $false "@APP_NAME@ $Version did not stay up twice; the previous version was reinstalled" $true $false }
|
||||
else { Done $true '' $false $false }
|
||||
Log 'installer exit 0'
|
||||
exit 0
|
||||
}
|
||||
Log ("installer exit " + $p.ExitCode)
|
||||
Done $false ("the installer exited with code " + $p.ExitCode + " (see ota-setup.log)") $false $false
|
||||
Relaunch
|
||||
exit 1
|
||||
} catch {
|
||||
$msg = $_.Exception.Message
|
||||
Log ("installer did not run: " + $msg)
|
||||
Log 'OTA: waiting for administrator approval; the engine keeps running; the update waits for the next time someone is at this PC'
|
||||
Done $false ("waiting for administrator approval (" + $msg + ")") $false $true
|
||||
Relaunch
|
||||
exit 1
|
||||
}
|
||||
"#;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn helpers_carry_the_apps_names_and_no_placeholder() {
|
||||
for app in [crate::MINER, crate::WALLET] {
|
||||
for text in [mac_helper(app), win_helper(app)] {
|
||||
for ph in ["@APP_NAME@", "@ENGINE@", "@BUNDLE@"] {
|
||||
assert!(!text.contains(ph), "{ph} was left in the helper for {}", app.name);
|
||||
}
|
||||
assert!(text.contains(app.name));
|
||||
}
|
||||
let m = mac_helper(app);
|
||||
assert!(m.contains(&format!("ENGINE=\"$APP/Contents/MacOS/{}\"", app.engine_exe)));
|
||||
assert!(m.contains(&format!("tell application id \"{}\" to quit", app.bundle)));
|
||||
assert!(m.contains(&format!("\"$APP/Contents/MacOS/{}\"", app.name)));
|
||||
assert!(win_helper(app).contains(&format!("'{}.exe'", app.engine_exe)));
|
||||
}
|
||||
assert!(mac_helper(crate::WALLET).contains("Igneum Wallet $VER did not start twice"));
|
||||
assert!(!mac_helper(crate::WALLET).contains("Miner"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pending_and_result_round_trip() {
|
||||
let dir = std::env::temp_dir().join(format!("igneum-ota-test-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let p = Pending { from: "0.1.0".into(), to: "0.1.1".into(), at: 1.5, starts: 2, previous_installer: String::new() };
|
||||
write_pending(&dir, &p);
|
||||
assert_eq!(read_pending(&dir), Some(p));
|
||||
assert!(std::fs::read_to_string(pending_path(&dir)).unwrap().contains(&format!("\"platform\":\"{}\"", manifest::platform_name())));
|
||||
assert_eq!(parse_pending("nope"), None);
|
||||
let r = parse_result(r#"{"ok":false,"version":"0.1.1","error":"did not start","rolled_back":true,"at":1}"#).unwrap();
|
||||
assert_eq!(r, HelperResult { ok: false, version: "0.1.1".into(), error: "did not start".into(), rolled_back: true, deferred: false });
|
||||
assert!(parse_result(r#"{"ok":true,"version":"0.1.1","error":"","rolled_back":false,"deferred":true}"#).unwrap().deferred);
|
||||
assert_eq!(read_result(&dir), None);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names() {
|
||||
assert_eq!(installer_name_for(crate::WALLET, "0.1.1"), "Igneum-Wallet-Setup-0.1.1.exe");
|
||||
assert_eq!(installer_name_for(crate::MINER, "0.3.5"), "Igneum-Miner-Setup-0.3.5.exe");
|
||||
assert_eq!(staged_path(crate::WALLET, Path::new("/Applications/Igneum Wallet.app")).unwrap(), PathBuf::from("/Applications/.Igneum Wallet.app.new"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_file_takes_only_the_prefixes() {
|
||||
let dir = std::env::temp_dir().join(format!("igneum-ota-env-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::env::set_var("IGNEUM_OTA_TEST_X", "1");
|
||||
let p = write_env_file(&dir, &["IGNEUM_OTA_TEST_"]);
|
||||
let text = std::fs::read_to_string(&p).unwrap();
|
||||
assert!(text.contains("IGNEUM_OTA_TEST_X=1"));
|
||||
assert!(!text.contains("PATH="));
|
||||
assert_eq!(write_env_file(&dir, &["NO_SUCH_PREFIX_ZZ_"]), "");
|
||||
std::env::remove_var("IGNEUM_OTA_TEST_X");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
480
app/igneum-common/src/platform.rs
Normal file
480
app/igneum-common/src/platform.rs
Normal file
|
|
@ -0,0 +1,480 @@
|
|||
//! What differs per operating system: directories, file permissions, keeping the machine awake, opening a URL,
|
||||
//! starting at login, and stopping a child process gracefully. From app/igneum-app/src/platform.rs; the login item
|
||||
//! takes the app identity (`crate::AppId`) instead of naming Igneum Miner.
|
||||
|
||||
use crate::AppId;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
/// The absolute path of a system helper (R4.3.3: never a bare name on PATH). Windows: System32 (and NVIDIA's own
|
||||
/// folder for nvidia-smi); macOS: /usr/bin, /usr/sbin, /bin. Unknown names fall back to the bare name.
|
||||
pub fn tool(name: &str) -> PathBuf {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let root = std::env::var("SystemRoot").unwrap_or_else(|_| "C:\\Windows".into());
|
||||
let sys = format!("{root}\\System32");
|
||||
let p = match name {
|
||||
"powershell" => format!("{sys}\\WindowsPowerShell\\v1.0\\powershell.exe"),
|
||||
"cmd" | "curl" | "reg" | "icacls" | "taskkill" | "w32tm" | "net" | "tar" | "wsl" => format!("{sys}\\{name}.exe"),
|
||||
"nvidia-smi" => {
|
||||
let pf = std::env::var("ProgramFiles").unwrap_or_else(|_| "C:\\Program Files".into());
|
||||
let a = format!("{pf}\\NVIDIA Corporation\\NVSMI\\nvidia-smi.exe");
|
||||
let b = format!("{sys}\\nvidia-smi.exe");
|
||||
if Path::new(&a).is_file() { a } else { b }
|
||||
}
|
||||
_ => name.to_string(),
|
||||
};
|
||||
PathBuf::from(p)
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let p = match name {
|
||||
"curl" | "osascript" | "xattr" | "open" | "caffeinate" | "hdiutil" | "ditto" | "nohup" | "pgrep" | "pkill" | "shasum" | "xcrun" => format!("/usr/bin/{name}"),
|
||||
"sntp" | "scutil" | "system_profiler" | "sysctl" => format!("/usr/sbin/{name}"),
|
||||
"bash" | "sh" => format!("/bin/{name}"),
|
||||
_ => name.to_string(),
|
||||
};
|
||||
PathBuf::from(p)
|
||||
}
|
||||
#[cfg(not(any(windows, target_os = "macos")))]
|
||||
{
|
||||
for dir in ["/usr/bin", "/bin", "/usr/sbin", "/usr/local/bin"] {
|
||||
let p = Path::new(dir).join(name);
|
||||
if p.is_file() {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
PathBuf::from(name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Strips the dashboard token from a line: "/t/<32 hex>/" becomes "/t/<token>/" (R4.3.8: the token is never logged
|
||||
/// and the logs are uploaded).
|
||||
pub fn redact(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
let mut rest = s;
|
||||
while let Some(i) = rest.find("/t/") {
|
||||
out.push_str(&rest[..i + 3]);
|
||||
let after = &rest[i + 3..];
|
||||
let hex_len = after.chars().take_while(|c| c.is_ascii_hexdigit()).count();
|
||||
if hex_len >= 16 {
|
||||
out.push_str("<token>");
|
||||
rest = &after[hex_len..];
|
||||
} else {
|
||||
rest = after;
|
||||
}
|
||||
}
|
||||
out.push_str(rest);
|
||||
out
|
||||
}
|
||||
|
||||
/// True when a line still carries something that looks like the dashboard token (the upload guard).
|
||||
pub fn carries_token(s: &str) -> bool {
|
||||
let mut rest = s;
|
||||
while let Some(i) = rest.find("/t/") {
|
||||
let after = &rest[i + 3..];
|
||||
if after.chars().take_while(|c| c.is_ascii_hexdigit()).count() >= 16 {
|
||||
return true;
|
||||
}
|
||||
rest = after;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn unix_now() -> u64 {
|
||||
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn unix_now_f() -> f64 {
|
||||
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs_f64()).unwrap_or(0.0)
|
||||
}
|
||||
|
||||
fn home() -> PathBuf {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if let Some(p) = std::env::var_os("USERPROFILE") {
|
||||
return PathBuf::from(p);
|
||||
}
|
||||
}
|
||||
std::env::var_os("HOME").map(PathBuf::from).unwrap_or_else(|| PathBuf::from("."))
|
||||
}
|
||||
|
||||
/// The root both apps write under (the miner under `app/`, the wallet under `wallet/`, see `AppId::data_sub`).
|
||||
/// macOS: ~/Library/Application Support/Igneum. Windows: %LOCALAPPDATA%\igneum (the same folder today's launchers
|
||||
/// use, so an existing devnet-v4 database is reused).
|
||||
pub fn data_root() -> PathBuf {
|
||||
if let Some(p) = std::env::var_os("IGNEUM_APP_DATA") {
|
||||
return PathBuf::from(p);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
home().join("Library").join("Application Support").join("Igneum")
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
std::env::var_os("LOCALAPPDATA").map(PathBuf::from).unwrap_or_else(|| home().join("AppData").join("Local")).join("igneum")
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", windows)))]
|
||||
{
|
||||
home().join(".igneum")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_root() -> PathBuf {
|
||||
if let Some(p) = std::env::var_os("IGNEUM_APP_LOGS") {
|
||||
return PathBuf::from(p);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
home().join("Library").join("Logs").join("Igneum")
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
data_root().join("logs")
|
||||
}
|
||||
}
|
||||
|
||||
/// The machine's short name, cleaned to [A-Za-z0-9-], for the miner labels (mac-<host>, nvidia-<pc>).
|
||||
pub fn host_label() -> String {
|
||||
let raw = {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Command::new(tool("scutil")).args(["--get", "LocalHostName"]).output().ok().and_then(|o| String::from_utf8(o.stdout).ok())
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
std::env::var("COMPUTERNAME").ok()
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", windows)))]
|
||||
{
|
||||
std::fs::read_to_string("/etc/hostname").ok()
|
||||
}
|
||||
};
|
||||
let raw = raw.unwrap_or_default();
|
||||
let cleaned: String = raw.trim().chars().map(|c| if c.is_ascii_alphanumeric() || c == '-' { c } else { '-' }).collect();
|
||||
let cleaned = cleaned.trim_matches('-').to_string();
|
||||
if cleaned.is_empty() {
|
||||
if cfg!(windows) { "pc".into() } else { "mac".into() }
|
||||
} else {
|
||||
cleaned
|
||||
}
|
||||
}
|
||||
|
||||
/// Restricts a file (or directory) to the current user.
|
||||
pub fn lock_permissions(path: &Path, dir: bool) {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(if dir { 0o700 } else { 0o600 }));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = dir;
|
||||
let user = std::env::var("USERNAME").unwrap_or_default();
|
||||
if !user.is_empty() {
|
||||
let _ = Command::new(tool("icacls"))
|
||||
.arg(path)
|
||||
.args(["/inheritance:r", "/grant:r", &format!("{user}:F")])
|
||||
.output();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a URL in the default browser (the fallback when no window host runs).
|
||||
pub fn open_url(url: &str) {
|
||||
#[cfg(target_os = "macos")]
|
||||
let _ = Command::new(tool("open")).arg(url).spawn();
|
||||
#[cfg(windows)]
|
||||
let _ = quiet(&mut Command::new(tool("cmd"))).args(["/c", "start", "", url]).spawn();
|
||||
#[cfg(not(any(target_os = "macos", windows)))]
|
||||
let _ = Command::new("xdg-open").arg(url).spawn();
|
||||
}
|
||||
|
||||
/// Keeps the machine awake while the engine runs. macOS: caffeinate tied to this process. Windows: the execution state,
|
||||
/// which must be refreshed (call `keep_awake_tick` every minute).
|
||||
pub struct KeepAwake {
|
||||
#[cfg(target_os = "macos")]
|
||||
child: Option<std::process::Child>,
|
||||
}
|
||||
|
||||
impl KeepAwake {
|
||||
pub fn start() -> KeepAwake {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let child = Command::new(tool("caffeinate")).args(["-dims", "-w", &std::process::id().to_string()]).spawn().ok();
|
||||
KeepAwake { child }
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
keep_awake_tick();
|
||||
KeepAwake {}
|
||||
}
|
||||
}
|
||||
pub fn stop(&mut self) {
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Some(c) = self.child.as_mut() {
|
||||
let _ = c.kill();
|
||||
let _ = c.wait();
|
||||
}
|
||||
#[cfg(windows)]
|
||||
unsafe {
|
||||
SetThreadExecutionState(ES_CONTINUOUS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
const ES_CONTINUOUS: u32 = 0x8000_0000;
|
||||
#[cfg(windows)]
|
||||
const ES_SYSTEM_REQUIRED: u32 = 0x0000_0001;
|
||||
#[cfg(windows)]
|
||||
#[link(name = "kernel32")]
|
||||
extern "system" {
|
||||
fn SetThreadExecutionState(flags: u32) -> u32;
|
||||
}
|
||||
|
||||
pub fn keep_awake_tick() {
|
||||
#[cfg(windows)]
|
||||
unsafe {
|
||||
SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED);
|
||||
}
|
||||
}
|
||||
|
||||
/// Asks a child to stop. Unix: SIGTERM (the node closes its database cleanly). Windows: TerminateProcess through
|
||||
/// std (what today's launcher does with taskkill /F).
|
||||
pub fn terminate(child: &mut std::process::Child) {
|
||||
#[cfg(unix)]
|
||||
unsafe {
|
||||
libc::kill(child.id() as i32, libc::SIGTERM);
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = child.kill();
|
||||
}
|
||||
}
|
||||
|
||||
/// The app bundle on macOS (Igneum Miner.app) when the engine runs from inside one.
|
||||
pub fn bundle_path() -> Option<PathBuf> {
|
||||
let exe = std::env::current_exe().ok()?;
|
||||
let macos = exe.parent()?;
|
||||
let contents = macos.parent()?;
|
||||
if macos.file_name()? == "MacOS" && contents.file_name()? == "Contents" {
|
||||
contents.parent().map(|p| p.to_path_buf())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// What a login item should run: the bundle (macOS) or the window host / the engine (Windows).
|
||||
fn login_command(app: AppId) -> Vec<String> {
|
||||
let _ = app; // macOS runs the bundle by path; Windows names the host executable
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Some(b) = bundle_path() {
|
||||
return vec!["/usr/bin/open".into(), "-a".into(), b.to_string_lossy().into_owned()];
|
||||
}
|
||||
}
|
||||
let exe = std::env::current_exe().map(|p| p.to_string_lossy().into_owned()).unwrap_or_default();
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if let Some(dir) = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_path_buf())) {
|
||||
let host = dir.join(app.host_exe);
|
||||
if host.exists() {
|
||||
return vec![host.to_string_lossy().into_owned()];
|
||||
}
|
||||
}
|
||||
}
|
||||
vec![exe]
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn launch_agent_path(app: AppId) -> PathBuf {
|
||||
home().join("Library").join("LaunchAgents").join(format!("{}.plist", app.bundle))
|
||||
}
|
||||
|
||||
pub fn set_start_at_login(app: AppId, on: bool) -> Result<(), String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let path = launch_agent_path(app);
|
||||
if on {
|
||||
let args: String = login_command(app)
|
||||
.iter()
|
||||
.map(|a| format!(" <string>{}</string>\n", a.replace('&', "&").replace('<', "<")))
|
||||
.collect();
|
||||
let plist = format!(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>Label</key>\n <string>{}</string>\n <key>ProgramArguments</key>\n <array>\n{args} </array>\n <key>RunAtLoad</key>\n <true/>\n</dict>\n</plist>\n",
|
||||
app.bundle
|
||||
);
|
||||
if let Some(d) = path.parent() {
|
||||
std::fs::create_dir_all(d).map_err(|e| e.to_string())?;
|
||||
}
|
||||
std::fs::write(&path, plist).map_err(|e| e.to_string())?;
|
||||
} else if path.exists() {
|
||||
std::fs::remove_file(&path).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let key = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run";
|
||||
let out = if on {
|
||||
let cmd = login_command(app).iter().map(|a| format!("\"{a}\"")).collect::<Vec<_>>().join(" ");
|
||||
Command::new(tool("reg")).args(["add", key, "/v", app.name, "/t", "REG_SZ", "/d", &cmd, "/f"]).output()
|
||||
} else {
|
||||
Command::new(tool("reg")).args(["delete", key, "/v", app.name, "/f"]).output()
|
||||
};
|
||||
match out {
|
||||
Ok(o) if o.status.success() || !on => Ok(()),
|
||||
Ok(o) => Err(String::from_utf8_lossy(&o.stderr).trim().to_string()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", windows)))]
|
||||
{
|
||||
let _ = (app, on);
|
||||
Err("start at login is not supported on this platform".into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_at_login_is_on(app: AppId) -> bool {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
launch_agent_path(app).exists()
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
Command::new(tool("reg"))
|
||||
.args(["query", r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run", "/v", app.name])
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", windows)))]
|
||||
{
|
||||
let _ = app;
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the quarantine flag from the app's own files (macOS): after Gatekeeper lets the app through, each binary
|
||||
/// inside would still be checked on its first exec. Best effort; only works on a writable volume.
|
||||
pub fn clear_quarantine() {
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Some(b) = bundle_path() {
|
||||
let _ = Command::new(tool("xattr")).args(["-dr", "com.apple.quarantine"]).arg(b.join("Contents")).output();
|
||||
}
|
||||
}
|
||||
|
||||
/// The manual instruction for fixing the clock on this platform.
|
||||
pub fn clock_hint() -> &'static str {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
"System Settings > General > Date & Time: turn on \"Set time and date automatically\", or run: sudo sntp -sS time.apple.com"
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
"Settings > Time & language > Date & time: turn on \"Set time automatically\" and click \"Sync now\", or run as administrator: w32tm /resync"
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", windows)))]
|
||||
{
|
||||
"run: sudo chronyc makestep, or sudo timedatectl set-ntp true"
|
||||
}
|
||||
}
|
||||
|
||||
/// Asks the operating system to set the clock from a time server (an administrator prompt appears). Returns what
|
||||
/// happened, for the window. Blocking; call from a thread.
|
||||
pub fn sync_clock() -> Result<String, String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let out = Command::new(tool("osascript"))
|
||||
.args(["-e", "do shell script \"/usr/bin/sntp -sS time.apple.com 2>&1\" with administrator privileges"])
|
||||
.output()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let text = format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
|
||||
if out.status.success() {
|
||||
Ok(if text.trim().is_empty() { "clock set from time.apple.com".into() } else { text.trim().to_string() })
|
||||
} else if text.contains("canceled") || text.contains("cancelled") {
|
||||
Err("the administrator prompt was cancelled".into())
|
||||
} else {
|
||||
Err(text.trim().to_string())
|
||||
}
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let cmd = tool("cmd").display().to_string();
|
||||
let script = format!("Start-Process -FilePath '{cmd}' -ArgumentList '/c net start w32time & w32tm /resync /force' -Verb RunAs -Wait -WindowStyle Hidden");
|
||||
let mut c = Command::new(tool("powershell"));
|
||||
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &script]);
|
||||
quiet(&mut c);
|
||||
let out = c.output().map_err(|e| e.to_string())?;
|
||||
if out.status.success() {
|
||||
Ok("asked Windows Time to resync (w32tm /resync)".into())
|
||||
} else {
|
||||
Err(String::from_utf8_lossy(&out.stderr).trim().to_string())
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", windows)))]
|
||||
{
|
||||
for args in [vec!["chronyc", "makestep"], vec!["timedatectl", "set-ntp", "true"]] {
|
||||
if let Ok(out) = Command::new("pkexec").args(&args).output() {
|
||||
if out.status.success() {
|
||||
return Ok(format!("ran {}", args.join(" ")));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err("neither chronyc nor timedatectl could set the clock".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs a command line with administrator rights (one prompt): the NVIDIA power cap needs it on Windows.
|
||||
/// Blocking; call from a thread.
|
||||
pub fn run_elevated(cmdline: &str) -> Result<(), String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let escaped = cmdline.replace('\'', "''");
|
||||
let cmd = tool("cmd").display().to_string();
|
||||
let script = format!("$p = Start-Process -FilePath '{cmd}' -ArgumentList '/c {escaped}' -Verb RunAs -Wait -WindowStyle Hidden -PassThru; exit $p.ExitCode");
|
||||
let mut c = Command::new(tool("powershell"));
|
||||
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &script]);
|
||||
quiet(&mut c);
|
||||
let out = c.output().map_err(|e| e.to_string())?;
|
||||
if out.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let err = String::from_utf8_lossy(&out.stderr).trim().to_string();
|
||||
Err(if err.contains("canceled") || err.contains("cancelled") || err.is_empty() { "the administrator prompt was cancelled".into() } else { err })
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let out = Command::new("pkexec").args(["sh", "-c", cmdline]).output().map_err(|e| e.to_string())?;
|
||||
if out.status.success() { Ok(()) } else { Err(String::from_utf8_lossy(&out.stderr).trim().to_string()) }
|
||||
}
|
||||
#[cfg(not(any(windows, target_os = "linux")))]
|
||||
{
|
||||
let _ = cmdline;
|
||||
Err("not supported on this platform".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a command that runs without a console window on Windows.
|
||||
pub fn quiet(cmd: &mut Command) -> &mut Command {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
|
||||
}
|
||||
cmd
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn token_redaction() {
|
||||
let l = "dashboard at http://127.0.0.1:58776/t/a3a01c537130bceeaa1f6118ba48d63e/ (log x)";
|
||||
assert_eq!(super::redact(l), "dashboard at http://127.0.0.1:58776/t/<token>/ (log x)");
|
||||
assert!(super::carries_token(l));
|
||||
assert!(!super::carries_token("GET /t/short/ nothing"));
|
||||
assert_eq!(super::redact("no token here"), "no token here");
|
||||
}
|
||||
}
|
||||
40
app/igneum-common/src/run.rs
Normal file
40
app/igneum-common/src/run.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
//! A command with a time limit (app/igneum-app/src/detect.rs `run_timeout`).
|
||||
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Runs a command with a time limit; returns stdout (and stderr appended) or None.
|
||||
pub fn run_timeout(cmd: &mut Command, stdin_text: Option<&str>, limit: Duration) -> Option<String> {
|
||||
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
cmd.stdin(if stdin_text.is_some() { Stdio::piped() } else { Stdio::null() });
|
||||
crate::platform::quiet(cmd);
|
||||
let mut child = cmd.spawn().ok()?;
|
||||
if let (Some(text), Some(mut stdin)) = (stdin_text, child.stdin.take()) {
|
||||
let _ = stdin.write_all(text.as_bytes());
|
||||
drop(stdin);
|
||||
}
|
||||
let out = child.stdout.take()?;
|
||||
let err = child.stderr.take()?;
|
||||
let reader = std::thread::spawn(move || {
|
||||
let mut s = String::new();
|
||||
let _ = std::io::Read::read_to_string(&mut std::io::BufReader::new(out), &mut s);
|
||||
let mut e = String::new();
|
||||
let _ = std::io::Read::read_to_string(&mut std::io::BufReader::new(err), &mut e);
|
||||
(s, e)
|
||||
});
|
||||
let deadline = Instant::now() + limit;
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(_)) => break,
|
||||
Ok(None) if Instant::now() < deadline => std::thread::sleep(Duration::from_millis(50)),
|
||||
_ => {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let (s, e) = reader.join().ok()?;
|
||||
Some(if e.is_empty() { s } else { format!("{s}\n{e}") })
|
||||
}
|
||||
5207
app/igneum-wallet/Cargo.lock
generated
Normal file
5207
app/igneum-wallet/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
41
app/igneum-wallet/Cargo.toml
Normal file
41
app/igneum-wallet/Cargo.toml
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
[package]
|
||||
name = "igneum-wallet"
|
||||
version = "0.1.6"
|
||||
edition = "2021"
|
||||
description = "Igneum Wallet engine: keeps the key encrypted, signs in Rust, reads a node, verifies finality certificates itself, and serves the window on 127.0.0.1"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "igneum-wallet"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
igneum-common = { path = "../igneum-common" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
k256 = { version = "0.13", features = ["ecdsa", "std"] }
|
||||
sha3 = { version = "0.10", default-features = false }
|
||||
sha2 = { version = "0.10", default-features = false }
|
||||
getrandom = "0.2"
|
||||
bip39 = { version = "2.1", features = ["rand"] }
|
||||
bip32 = "0.5"
|
||||
argon2 = "0.5"
|
||||
chacha20poly1305 = "0.10"
|
||||
zeroize = { version = "1", features = ["derive"] }
|
||||
qrcodegen = "1.8"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
|
||||
# the node's own code: its gRPC client, the RPC model and the finality certificate verification (vendor/igneum-node)
|
||||
kaspa-grpc-client = { path = "../../vendor/igneum-node/rpc/grpc/client" }
|
||||
kaspa-rpc-core = { path = "../../vendor/igneum-node/rpc/core" }
|
||||
kaspa-consensus-core = { path = "../../vendor/igneum-node/consensus/core" }
|
||||
kaspa-hashes = { path = "../../vendor/igneum-node/crypto/hashes" }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 2
|
||||
lto = "thin"
|
||||
codegen-units = 4
|
||||
strip = true
|
||||
227
app/igneum-wallet/README.md
Normal file
227
app/igneum-wallet/README.md
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
# Igneum Wallet
|
||||
|
||||
Desktop wallet on the miner's bones: a Rust engine (`src/`) that keeps the key encrypted, signs, reads a node and
|
||||
verifies finality certificates, plus a window served on 127.0.0.1 (`ui/`). macOS host: `app/mac/IgneumWallet.swift`;
|
||||
packaging: `packaging/mac/build-wallet-dmg.sh`, `packaging/windows/Igneum-Wallet.iss`. Shared code with the miner:
|
||||
`app/igneum-common`.
|
||||
|
||||
## Versions
|
||||
|
||||
| Version | Date | What |
|
||||
|---|---|---|
|
||||
| 0.1.0 | 4 Oct 2026 | first DMG; built before `/coin.png` existed, so the coin on the home screen is blank; updates download and offer "Open the download" only |
|
||||
| 0.1.1 | 5 Oct 2026 | coin served from `brand/igneum-coin-1024.png`; over-the-air updates v2 (unattended install) |
|
||||
| 0.1.2 | 5 Oct 2026 | Touch ID (macOS) and Windows Hello (Windows, untested): unlock, confirm sends, show the backup, idle lock; the coin and the "reading the chain" line on the balance card; the version in the header and in Settings |
|
||||
| 0.1.3 | 5 Oct 2026 | the update card: one centred card over the window when a new version is ready, with what changed and one tap to install (`ui/update-card.js`) |
|
||||
| 0.1.6 | 8 Oct 2026 | the page bridge (`src/bridge.rs`, `src/eip712.rs`, `site/wallet-provider.js`): websites connect through 127.0.0.1:26811 after your approval in the window; eth_requestAccounts, eth_chainId, eth_sendTransaction, personal_sign and typed data, each shown and confirmed here (Touch ID or Windows Hello when enrolled), the finality line after a transaction; Settings lists the connected sites; a page without approval gets nothing |
|
||||
| 0.1.4 | 5 Oct 2026 | the lock screen (`ui/lock-screen.js`): the coin on the ember glow, the name, the short address, one control; the Touch ID sheet on a tap, once by itself when the wallet opens (setting), never on an idle lock; locking is a 250 ms transition; the idle lock's minutes in Settings (1, 5, 15, 60, never) |
|
||||
|
||||
## The lock screen (ui/lock-screen.js, index.html #screen-unlock, app.js onLockScreen; 0.1.4)
|
||||
|
||||
The coin large and centred on the obsidian ground with the ember glow (it breathes over 6 s; reduced motion stops
|
||||
it), "Igneum Wallet" in Unbounded, the wallet's short address in mono, one primary control. With Touch ID (or Windows
|
||||
Hello) on and the app window as the host: the fingerprint button "Unlock with Touch ID" in ember, its line under it,
|
||||
and a quiet "Use password" that reveals the password field in place (the control area keeps one height, so nothing
|
||||
above it moves). Otherwise the password field is the control and the button is absent. Locking (the Lock button,
|
||||
the menu bar, the idle lock) is a 250 ms transition from the home screen to the lock screen, not a cut.
|
||||
|
||||
When the system sheet appears (`LockScreen.autoPrompt`, the rules in `ui/lock-screen.test.mjs`):
|
||||
|
||||
| Moment | The sheet |
|
||||
|---|---|
|
||||
| a tap on the button, or Return or Space on it (it has the focus on arrival, and again when the window comes back) | yes |
|
||||
| the first arrival after the person opened the app, with "Ask for Touch ID when the wallet opens" on (Settings, default on) | once, 600 ms after the lock screen is fully drawn |
|
||||
| the idle lock, the Lock button or menu item, the window coming back from the menu bar or the Dock | never |
|
||||
| after a cancelled or unmatched sheet | never; the line says "Touch ID cancelled, tap to try again" |
|
||||
| the first run after an over-the-air update (the updater opened the app, not the person) | never |
|
||||
| the window not visible at arrival (opened at login behind another app) | never |
|
||||
|
||||
After the sheet the line under the button, in our words and never a modal: "Touch ID confirmed, unlocking", "Touch
|
||||
ID cancelled, tap to try again", "Touch ID did not match, tap to try again", "Enter your password" (the sheet's
|
||||
Use password button reveals the field), "Touch ID is locked, use your password". A sheet that cannot help (locked,
|
||||
invalidated, not set up, a browser tab) reveals the password field and focuses it. Escape in the password field
|
||||
returns to the button. `?bio=touch` on the page shows the Touch ID layout without a host (screenshots).
|
||||
|
||||
## Touch ID and Windows Hello (src/engine.rs, igneum-common/src/biometric.rs, app/mac/Biometric.swift, app/windows/biometric.h)
|
||||
|
||||
What it gates once "Use Touch ID" is on (Settings): unlocking at start and after the idle lock, every send, and
|
||||
showing the words or the key. The password still works for all three. Nothing else asks for a finger.
|
||||
|
||||
| Piece | Where | What it does |
|
||||
|---|---|---|
|
||||
| the gate | `igneum-common/src/biometric.rs` | nonces the engine issues, the host confirms and the action consumes once; the one-time enrolment token; the state in `/api/state` |
|
||||
| the engine | `src/engine.rs`, `src/server.rs` | `/api/biometric/*`; `/api/send` refuses without a confirmed nonce for that quote while enrolled; `/api/reveal` with a nonce reads the unlocked key in memory; the idle lock; the `HOST {...}` line on stdout |
|
||||
| the Mac host | `app/mac/Biometric.swift` | the `biometric` script message handler; `LAPolicy.deviceOwnerAuthenticationWithBiometrics`; the Secure Enclave seal |
|
||||
| the Windows host | `app/windows/biometric.h` | the `window.chrome.webview` bridge; `UserConsentVerifier` through `IUserConsentVerifierInterop`; DPAPI |
|
||||
| the page | `ui/app.js` | the fingerprint controls on the unlock, send and Settings screens; the activity ping for the idle lock |
|
||||
|
||||
The prompt's lines, worded by the engine or the host, never by the page, in our voice, under 60 characters, no
|
||||
trailing full stop:
|
||||
|
||||
| Prompt | Line |
|
||||
|---|---|
|
||||
| unlock | Unlock your wallet |
|
||||
| send | Send 1.5 IGN to 0x7E5F…5Bdf (the amount to 4 decimals; the address as its first 6 and last 4 characters) |
|
||||
| backup and export | Show your recovery words |
|
||||
| turn on | Turn on Touch ID for your wallet |
|
||||
|
||||
The prompt's buttons: "Use password" (the fallback button; the policy is biometrics only, so it never reaches the
|
||||
device password: the window asks for the wallet's own password) and "Cancel". After the system prompt the page shows
|
||||
its own line with the fingerprint glyph in ember: "Touch ID confirmed, unlocking", "Touch ID cancelled, enter your
|
||||
password", "Touch ID did not match, try again or enter your password", and so on (`bioLine` in `ui/app.js`). The
|
||||
prompt's title and icon are the bundled app's ("Igneum Wallet", the mark): the window host is the bundle's own
|
||||
executable, so the system attributes the prompt to it; a bare engine or a test binary shows its own name instead.
|
||||
macOS composes the sentence itself ("Igneum Wallet is trying to unlock your wallet."), so the Mac host lowercases
|
||||
the line's first letter at display time; Windows Hello shows the line on its own, with its capital.
|
||||
|
||||
The flow for a send: the quote (`/api/send/quote`) comes with `confirm_nonce` and `confirm_reason`. The page hands the nonce to the host; the host
|
||||
reads the reason from the engine with its host token, shows the prompt with that line, and on success posts
|
||||
`/api/biometric/confirm`. The page then calls `/api/send` with the quote and the nonce; the engine checks the nonce
|
||||
was confirmed within 30 s, for this exact quote (address, value, transaction nonce, chain id), and not used before.
|
||||
Unlock: the host shows the prompt, unseals the password and posts it to `/api/unlock` itself. The backup: a
|
||||
`reveal` challenge, the prompt, then `/api/reveal` with the nonce; the words come from the key already unlocked in
|
||||
memory, so the password is neither typed nor stored for it.
|
||||
|
||||
The host token: the engine prints `HOST {"token": ..., "biometric_file": ...}` on its stdout, which only the window
|
||||
host reads. The host sends it as `X-Igneum-Host` on the calls only it may make (status, report, confirm, taking the
|
||||
parked password at enrolment, recording the enrolment). The page cannot confirm its own challenges.
|
||||
|
||||
Enrolment: the page posts the password to `/api/biometric/enrol/begin`; the engine checks it against the vault and
|
||||
parks it under a one-time token for 120 s; the host shows the prompt ("Turn on Touch ID for Igneum Wallet"), takes
|
||||
the password with the token (once), seals it and writes the file, then posts `/api/biometric/enrolled`. A password
|
||||
change deletes the sealed file and Settings asks to turn Touch ID on again. Removing the wallet deletes it too.
|
||||
|
||||
The idle lock: with Touch ID on, Settings > "Lock when idle" chooses 1, 5 (the default), 15 or 60 minutes, or never
|
||||
(`settings.json: idle_lock_min`, 0 for never; `idle_lock` mirrors on/off for 0.1.2 and 0.1.3). The engine zeroes
|
||||
the key after that long without the window reporting input (mouse, keys; `/api/activity` every 20 s while there is
|
||||
some), never during a send or with a confirm screen open. The next unlock is a tap on the button, or the password;
|
||||
the sheet is never raised by itself after an idle lock.
|
||||
|
||||
### What is stored, and where (macOS)
|
||||
|
||||
The packaging signs the app ad hoc. Under an ad hoc signature macOS refuses every Keychain item that carries a
|
||||
biometric access control, on the login keychain and the data-protection keychain alike (`errSecMissingEntitlement`,
|
||||
-34018: `keychain-access-groups` needs a team signature; measured 5 October 2026 on this Mac with a 40-line probe).
|
||||
A Secure Enclave key with the same control is allowed, so the password is sealed to one instead:
|
||||
|
||||
- enrol: a P-256 key is made in the Secure Enclave with `SecAccessControl(.privateKeyUsage, .biometryCurrentSet)`;
|
||||
an ephemeral P-256 key agrees a shared secret with its public half (no prompt), HKDF-SHA256 derives an AES-GCM key
|
||||
and the password is sealed. `~/Library/Application Support/Igneum/wallet/biometric.json` (0600) holds the Secure
|
||||
Enclave key's wrapped form, the ephemeral public key and the box.
|
||||
- unlock or confirm: the host evaluates `LAPolicy.deviceOwnerAuthenticationWithBiometrics` (fallback button "Use
|
||||
password", which only closes the prompt with `LAError.userFallback`; the device password is never offered), then
|
||||
uses the Secure Enclave key under that context. The key agreement runs on every confirm
|
||||
too, so a changed fingerprint set is caught on a send, not only on an unlock.
|
||||
- `.biometryCurrentSet`: a fingerprint added or removed in System Settings makes the Secure Enclave refuse the key.
|
||||
The host reports "invalidated"; the window says to use the password and turn Touch ID on again.
|
||||
|
||||
Windows: the password is sealed with DPAPI (`CryptProtectData`, current user, `CRYPTPROTECT_UI_FORBIDDEN`) only after
|
||||
a `UserConsentVerifier` success and written to `%LOCALAPPDATA%\igneum\wallet\biometric.json`. DPAPI has no
|
||||
biometric binding of its own: the host unseals only after Hello verified.
|
||||
|
||||
### Threat model
|
||||
|
||||
| | Touch ID protects | Touch ID does not protect |
|
||||
|---|---|---|
|
||||
| Casual access at an unlocked Mac (someone at the keyboard while the wallet is locked) | yes: no finger, no unlock, no send, no words; the idle lock closes the window within 5 minutes of nobody being there | |
|
||||
| A copy of `vault.json` taken off the machine | yes, as before: Argon2id + XChaCha20-Poly1305 under the password; the sealed file is useless off this Mac's Secure Enclave | |
|
||||
| A copy of `biometric.json` by another user on this Mac | yes: 0600, and the Secure Enclave key is bound to this device and the current fingerprint set | |
|
||||
| A root attacker, or malware running as the signed-in user | | no: it can read the wallet's memory while unlocked, patch the app, or drive the window; the sealed file is as strong as the user's macOS login and Secure Enclave, not stronger |
|
||||
| Someone who knows the password | | no: the password works everywhere Touch ID does, by design |
|
||||
| A fingerprint added to this Mac by someone with the macOS password | | the Secure Enclave key dies (`.biometryCurrentSet`), so the new finger cannot unlock; the person with the macOS password could still enrol again, which needs the wallet password |
|
||||
| The page (ui/) or a cross-site page in the browser | yes: the host token is never in the page; confirmations are host-only; `/api/send` binds the nonce to the quote; the same-origin guard stays | |
|
||||
|
||||
Windows (untested): DPAPI protects against other accounts and offline copies, not against code running as the user;
|
||||
the same table applies with "Windows Hello" and "the Windows account".
|
||||
|
||||
### Verified (5 October 2026)
|
||||
|
||||
- Unit tests: `cargo test biometric` in `app/igneum-common` (confirm/take once, replay, expiry, stale, mismatch,
|
||||
the enrolment token, reason clipping, the constant-time compare, the binding).
|
||||
- The Swift host compiles with `Biometric.swift` and links LocalAuthentication; the DMG builds.
|
||||
- The probe: `SecItemAdd` with `.biometryCurrentSet` fails with -34018 under the ad hoc signature; a non-permanent
|
||||
Secure Enclave key with the same control is created, exported (`dataRepresentation`, 569 bytes), re-imported, and
|
||||
the ephemeral key agreement seals a box without a prompt.
|
||||
|
||||
### Untested
|
||||
|
||||
- The Windows path: `biometric.h` was written on a Mac; the first compile is BUILD-WALLET-APP.bat on the runner.
|
||||
- See the report for whether the Touch ID prompt was exercised end to end on this Mac (a finger is needed).
|
||||
|
||||
## Over-the-air updates (src/updater.rs, igneum-common/src/{fetch,ota}.rs)
|
||||
|
||||
The signed manifest `igneum-wallet-latest.json` (+ `.sig`, the miner's Ed25519 key) is published with
|
||||
`packaging/ota/publish-manifest.sh --product wallet --version <v> --mac packaging/mac/dist/Igneum-Wallet-<v>.dmg --notes "..." --deploy`.
|
||||
The engine checks it 25 s after start, then hourly (10 minutes after an error), and from Settings > Check for updates.
|
||||
|
||||
States (`state.update.status`, what the banner says):
|
||||
|
||||
| State | Meaning |
|
||||
|---|---|
|
||||
| off | the build has no manifest URL |
|
||||
| unknown | not checked yet |
|
||||
| checking | fetching and verifying the manifest |
|
||||
| current | this is the latest version |
|
||||
| available | a newer version has a build for this platform; the download starts at once |
|
||||
| downloading | curl with resume into `<wallet data>/updates/`; size and sha256 checked against the manifest |
|
||||
| staging | macOS: the DMG mounted, the bundle copied next to the running one as `.Igneum Wallet.app.new`, its engine asked `--version`, the bundle digested; Windows: the installer is the staged artefact |
|
||||
| ready | waits for the safe moment (below); "Install now" applies at once; "Later" hides the banner for that version only |
|
||||
| applying | the download and the staged bundle re-verified, `update-pending.json` written, the helper started; macOS: the engine quits and the helper swaps the bundle and opens the new app |
|
||||
| deferred | Windows only: the installer's administrator prompt was not answered; retried in 6 hours or on Install now |
|
||||
| manual | the engine cannot swap itself (not in a bundle, Applications not writable): "Open the download" |
|
||||
| error | what failed, in `state.update.error`; a version whose apply failed is never re-applied by itself |
|
||||
|
||||
The window shows an update twice over. The card (`ui/update-card.js`, `renderUpdateCard` in `ui/app.js`) is the
|
||||
first sight: one centred card with the mark and a progress ring, "Igneum Wallet 0.1.3", one line, up to three lines of
|
||||
the manifest's notes (the rest behind "What changed"), the size, Install now and Later. It never opens while the send
|
||||
screen is open, while a Touch ID or Windows Hello line is on screen, or while a wallet is being created or imported;
|
||||
it waits and comes once that is over. Later (also Escape and the backdrop) hides that version at that stage and
|
||||
leaves the banner; the card comes back for a newer version, or when the download is ready and automatic updates are
|
||||
off. An open card follows its update through downloading, ready, installing ("Installing. The app restarts itself.")
|
||||
and failed (the one-line cause, Try again). The banner is the small strip that stays. `?update=<state>[&auto=0][&card=1]`
|
||||
on the page shows each state without a manifest; `ui/update-card.test.mjs` checks the words and the rules.
|
||||
|
||||
The setting "Install updates by itself when nothing is being sent" (`settings.json: auto_update`) defaults to on.
|
||||
The safe moment is no send in flight: no `/api/send` running, no quote given in the last 180 s (a confirm screen may
|
||||
be open), no sent transaction still waiting for its block, no create flow half way. A version below the manifest's
|
||||
`min_supported_version` installs at once.
|
||||
|
||||
Rollback: the macOS helper puts `Igneum Wallet.app.previous` back when the new app does not start twice. The new
|
||||
engine counts its starts in `update-pending.json`; on the third start without 90 healthy seconds it restores the
|
||||
previous version (never below `min_supported_version`). The window shows "Updated from X" on the first run after an
|
||||
update and "Rolled back: ..." after a restore.
|
||||
|
||||
Files in `~/Library/Application Support/Igneum/wallet/` (Windows: `%LOCALAPPDATA%\igneum\wallet\`): `updates/`
|
||||
(manifest, download), `update-pending.json`, `update-result.json`, `ota-apply.sh` or `ota-apply.ps1`,
|
||||
`ota-apply.log`, `failed-versions.json`, `ota-relaunch.env` (test runs only).
|
||||
|
||||
### Verified (5 October 2026)
|
||||
|
||||
- Unit tests: manifest parse, version comparison, plan, safe moment (`cargo test` in `app/igneum-wallet`); helper
|
||||
templates, pending/result files, env file, digest recipe (`cargo test` in `app/igneum-common`).
|
||||
- End to end on this Mac with a scratch copy of the 0.1.1 bundle against a 0.1.2 test manifest served from
|
||||
127.0.0.1 (`publish-manifest.sh --dest <folder> --base-url http://127.0.0.1:<port>`; the engine run with
|
||||
`IGNEUM_APP_DATA`, `IGNEUM_WALLET_UPDATE_MANIFEST`, `IGNEUM_WALLET_UPDATE_FIRST_SECS=3`, `IGNEUM_OTA_RELAUNCH_ENGINE=1`
|
||||
so the helper relaunches the engine alone): check, download, stage, apply, swap, relaunch as 0.1.2,
|
||||
"updated to Igneum Wallet 0.1.2 from 0.1.1".
|
||||
- For real on the founder's Mac, through LaunchServices with the real window host: 0.1.1 -> 0.1.2 (5 Oct 2026, 09:14Z)
|
||||
and 0.1.2 -> 0.1.3 (5 Oct 2026: check posted 13:23:18Z, staged 13:23:31Z, applied 13:23:40Z, 0.1.3 up 13:23:46Z,
|
||||
28 s end to end; `/api/state` then showed `updated_from` 0.1.2 and `current`).
|
||||
- 0.1.3 -> 0.1.4 (5 Oct 2026): check posted 15:48:50Z, downloaded and verified 15:48:52Z (`staging` in `/api/state`),
|
||||
staged bundle digested 15:49:15Z, helper started 15:49:16Z (the 0.1.3 engine gone), 0.1.4 up 15:49:19Z with
|
||||
`updated_from` 0.1.3 and `unknown`, `current` at 15:49:45Z after its own check. 0.1.4 raised no Touch ID sheet on
|
||||
that first run (the updater's relaunch, not the person's: `last_op` stayed empty).
|
||||
- The download token rotated on 5 Oct 2026 (docs/plans/rotation-phase-2.md): `publish-manifest.sh` writes the NEW
|
||||
folder (it reads `~/.config/igneum/dl-token`), and a 0.1.4 bundle points there. The installed 0.1.3 polls the OLD
|
||||
folder's `igneum-wallet-latest.json`, which phase 2 had removed (the plan's section 8b kept only the miner's bridge),
|
||||
so the 0.1.4 manifest, its signature and the DMG were copied into the OLD folder too, as a bridge, until the 0.1.3
|
||||
wallets have moved; the old folder goes on 7 October (section 8f).
|
||||
|
||||
### Untested
|
||||
|
||||
- The Windows path (installer first, `/IGNOTA=1`, deferral on an unanswered prompt): copied from the miner's, never
|
||||
run for the wallet. Needs the wallet installer on the GitHub runner and a PC.
|
||||
- The rollback paths (the helper's "did not start twice", the engine's third-start restore) and `deferred`.
|
||||
- A version below `min_supported_version` (no wallet manifest has set one).
|
||||
- Code signatures: bundles are signed ad hoc by the packaging script; the updater verifies the manifest's sha256 and
|
||||
the staged bundle's digest, not a Developer ID signature (the miner does the same).
|
||||
491
app/igneum-wallet/src/bridge.rs
Normal file
491
app/igneum-wallet/src/bridge.rs
Normal file
|
|
@ -0,0 +1,491 @@
|
|||
//! The page bridge (0.1.6, 8 October 2026, main's order: igneum.network/swap connects any injected wallet, the Igneum
|
||||
//! Wallet signed only inside its own window). An EIP-1193 provider for pages: the engine listens on a FIXED loopback port
|
||||
//! (BRIDGE_PORT, igneum_common::http::serve_on) beside its token-guarded window server; a page's shim
|
||||
//! (site/wallet-provider.js) POSTs JSON-RPC to /bridge/rpc with an Origin the browser sets and the X-Igneum-Bridge
|
||||
//! header (so a preflight runs first). Nothing leaves without the person's word in the wallet's own window:
|
||||
//!
|
||||
//! - a site is approved once (eth_requestAccounts raises a Connect request; Approve in the window stores the origin
|
||||
//! in the settings; Decline or 5 minutes of silence ends it with 4001); every other method from an origin that is
|
||||
//! not approved answers 4100 and creates nothing (the known-failed test: a page without approval gets nothing);
|
||||
//! - eth_sendTransaction, personal_sign and eth_signTypedData(_v4) each raise a request the window shows with the
|
||||
//! origin and the facts (to, value, the call's bytes, the fee; the message; the domain and the primary type);
|
||||
//! Confirm signs (through Touch ID or Windows Hello when enrolled, the same gate as a send) and the page's poll
|
||||
//! reads the hash or the signature; after a transaction the window keeps the finality certificate's line;
|
||||
//! - reads (eth_call, eth_estimateGas, receipts, balances, blocks, logs) go to the wallet's node for an approved
|
||||
//! origin, the chain id and net version with them; wallet_switchEthereumChain accepts the wallet's own chain
|
||||
//! and refuses any other with 4902.
|
||||
//!
|
||||
//! This module holds the pure part (what a request means, what a decision does, what the page may read); the engine
|
||||
//! wires it to the vault, the node and the biometric gate (engine.rs, the "page bridge" section) and the server routes
|
||||
//! it (server.rs). Tests here run without a vault.
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// The bridge's port: fixed, so a page can find the wallet (IGNEUM_WALLET_BRIDGE_PORT overrides it for tests).
|
||||
pub const BRIDGE_PORT: u16 = 26811;
|
||||
/// A request the window has not answered expires after this.
|
||||
pub const PENDING_TTL: Duration = Duration::from_secs(300);
|
||||
/// A result the page has not read is kept this long after the decision.
|
||||
pub const RESULT_TTL: Duration = Duration::from_secs(120);
|
||||
pub const MAX_PENDING: usize = 8;
|
||||
|
||||
// EIP-1193 provider error codes
|
||||
pub const E_REJECTED: i64 = 4001;
|
||||
pub const E_UNAUTHORIZED: i64 = 4100;
|
||||
pub const E_UNSUPPORTED: i64 = 4200;
|
||||
pub const E_DISCONNECTED: i64 = 4900;
|
||||
pub const E_CHAIN: i64 = 4902;
|
||||
pub const E_PARAMS: i64 = -32602;
|
||||
pub const E_INTERNAL: i64 = -32603;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Kind {
|
||||
Connect,
|
||||
Send,
|
||||
Sign,
|
||||
Typed,
|
||||
}
|
||||
impl Kind {
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Kind::Connect => "connect",
|
||||
Kind::Send => "send",
|
||||
Kind::Sign => "sign",
|
||||
Kind::Typed => "typed",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One request waiting for the window, or decided and waiting for the page to read it.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Pending {
|
||||
pub id: String,
|
||||
pub origin: String,
|
||||
pub kind: Kind,
|
||||
pub created: Instant,
|
||||
pub created_unix: f64,
|
||||
/// what the window shows (never the key, never the page's whole payload)
|
||||
pub detail: Value,
|
||||
/// what the engine acts on at Confirm (the transfer's fields, the digest to sign)
|
||||
pub request: Value,
|
||||
/// the biometric challenge for this request ("" when nothing is enrolled)
|
||||
pub confirm_nonce: String,
|
||||
pub confirm_reason: String,
|
||||
pub done: Option<Result<Value, (i64, String)>>,
|
||||
pub decided: Option<Instant>,
|
||||
}
|
||||
|
||||
/// What the engine knows at the moment of a request.
|
||||
pub struct Ask<'a> {
|
||||
pub address: &'a str,
|
||||
pub unlocked: bool,
|
||||
pub chain_id: u64,
|
||||
pub approved: bool,
|
||||
pub listening: bool,
|
||||
}
|
||||
|
||||
/// What a request means.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Reply {
|
||||
Result(Value),
|
||||
Error(i64, String),
|
||||
/// the page polls igneum_poll with this id
|
||||
Pending(String),
|
||||
/// a read the engine forwards to its node as it is
|
||||
Proxy,
|
||||
}
|
||||
|
||||
const PROXIED: &[&str] = &[
|
||||
"eth_call", "eth_estimateGas", "eth_blockNumber", "eth_getBalance", "eth_getTransactionReceipt", "eth_getTransactionByHash",
|
||||
"eth_gasPrice", "eth_maxPriorityFeePerGas", "eth_feeHistory", "eth_getCode", "eth_getLogs", "eth_getBlockByNumber", "eth_getBlockByHash",
|
||||
"eth_getTransactionCount", "eth_getStorageAt", "igneum_getTransactionStatus",
|
||||
];
|
||||
|
||||
pub struct Bridge {
|
||||
pub pending: Vec<Pending>,
|
||||
/// the last call from each origin (the Settings card's "last seen")
|
||||
pub last_seen: HashMap<String, f64>,
|
||||
seq: u64,
|
||||
}
|
||||
|
||||
fn hex_quantity(v: &Value) -> Option<u128> {
|
||||
match v {
|
||||
Value::String(s) => {
|
||||
let h = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X"))?;
|
||||
if h.is_empty() {
|
||||
return Some(0);
|
||||
}
|
||||
u128::from_str_radix(h, 16).ok()
|
||||
}
|
||||
Value::Number(n) => n.as_u64().map(|x| x as u128),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn hex_bytes(v: &Value) -> Option<Vec<u8>> {
|
||||
let s = v.as_str()?;
|
||||
let h = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X"))?;
|
||||
if h.len() % 2 != 0 || !h.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return None;
|
||||
}
|
||||
(0..h.len() / 2).map(|i| u8::from_str_radix(&h[i * 2..i * 2 + 2], 16).ok()).collect()
|
||||
}
|
||||
fn is_address(s: &str) -> bool {
|
||||
s.len() == 42 && s.starts_with("0x") && s[2..].chars().all(|c| c.is_ascii_hexdigit())
|
||||
}
|
||||
/// A personal_sign message: hex bytes when it looks like hex, else the text itself.
|
||||
pub fn message_bytes(v: &Value) -> Option<Vec<u8>> {
|
||||
let s = v.as_str()?;
|
||||
if s.starts_with("0x") && s.len() % 2 == 0 && s[2..].chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
hex_bytes(v)
|
||||
} else {
|
||||
Some(s.as_bytes().to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Bridge {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Bridge {
|
||||
pub fn new() -> Bridge {
|
||||
Bridge { pending: Vec::new(), last_seen: HashMap::new(), seq: 0 }
|
||||
}
|
||||
|
||||
fn new_id(&mut self) -> String {
|
||||
self.seq += 1;
|
||||
let mut raw = [0u8; 8];
|
||||
getrandom::getrandom(&mut raw).expect("os randomness");
|
||||
format!("{}-{}", self.seq, igneum_common::keys::hex(&raw))
|
||||
}
|
||||
|
||||
/// Requests nobody answered in PENDING_TTL end with 4001; results nobody read in RESULT_TTL are dropped. Returns
|
||||
/// the expired ones (the engine logs them).
|
||||
pub fn expire(&mut self, now: Instant) -> Vec<Pending> {
|
||||
let mut gone = Vec::new();
|
||||
for p in self.pending.iter_mut() {
|
||||
if p.done.is_none() && now.duration_since(p.created) > PENDING_TTL {
|
||||
p.done = Some(Err((E_REJECTED, "the request expired: nobody answered it in the Igneum Wallet window".into())));
|
||||
p.decided = Some(now);
|
||||
gone.push(p.clone());
|
||||
}
|
||||
}
|
||||
self.pending.retain(|p| !(p.done.is_some() && p.decided.map(|d| now.duration_since(d) > RESULT_TTL).unwrap_or(false)));
|
||||
gone
|
||||
}
|
||||
|
||||
pub fn open(&self) -> Vec<&Pending> {
|
||||
self.pending.iter().filter(|p| p.done.is_none()).collect()
|
||||
}
|
||||
|
||||
/// What the page's call means. `approved` is the engine's reading of its settings for this origin.
|
||||
pub fn request(&mut self, origin: &str, method: &str, params: &Value, a: &Ask, now: Instant, now_unix: f64) -> Reply {
|
||||
if origin.is_empty() {
|
||||
return Reply::Error(E_UNAUTHORIZED, "a page bridge call carries its origin".into());
|
||||
}
|
||||
self.last_seen.insert(origin.to_string(), now_unix);
|
||||
let list = params.as_array().cloned().unwrap_or_default();
|
||||
let chain_hex = format!("0x{:x}", a.chain_id);
|
||||
match method {
|
||||
"igneum_poll" => {
|
||||
let id = list.first().and_then(|v| v.as_str()).unwrap_or("");
|
||||
return self.poll(origin, id);
|
||||
}
|
||||
"eth_requestAccounts" => {
|
||||
if a.approved {
|
||||
return if a.unlocked {
|
||||
Reply::Result(json!([a.address]))
|
||||
} else {
|
||||
Reply::Error(E_UNAUTHORIZED, "the Igneum Wallet is locked: unlock it in its window, then try again".into())
|
||||
};
|
||||
}
|
||||
if let Some(p) = self.pending.iter().find(|p| p.done.is_none() && p.kind == Kind::Connect && p.origin == origin) {
|
||||
return Reply::Pending(p.id.clone());
|
||||
}
|
||||
if self.open().len() >= MAX_PENDING {
|
||||
return Reply::Error(E_INTERNAL, "too many requests are waiting in the wallet window".into());
|
||||
}
|
||||
let id = self.new_id();
|
||||
self.pending.push(Pending { id: id.clone(), origin: origin.into(), kind: Kind::Connect, created: now, created_unix: now_unix, detail: json!({ "origin": origin }), request: Value::Null, confirm_nonce: String::new(), confirm_reason: String::new(), done: None, decided: None });
|
||||
return Reply::Pending(id);
|
||||
}
|
||||
"eth_accounts" => {
|
||||
return Reply::Result(if a.approved && a.unlocked { json!([a.address]) } else { json!([]) });
|
||||
}
|
||||
"igneum_disconnect" => return Reply::Result(Value::Null),
|
||||
_ => {}
|
||||
}
|
||||
if !a.approved {
|
||||
return Reply::Error(E_UNAUTHORIZED, "this site is not connected to the Igneum Wallet: call eth_requestAccounts first".into());
|
||||
}
|
||||
match method {
|
||||
"eth_chainId" => Reply::Result(json!(chain_hex)),
|
||||
"net_version" => Reply::Result(json!(a.chain_id.to_string())),
|
||||
"wallet_switchEthereumChain" | "wallet_addEthereumChain" => {
|
||||
let want = list.first().and_then(|v| v.get("chainId")).and_then(hex_quantity);
|
||||
match want {
|
||||
Some(c) if c == a.chain_id as u128 => Reply::Result(Value::Null),
|
||||
Some(_) => Reply::Error(E_CHAIN, format!("the Igneum Wallet runs one chain, id {} ({chain_hex})", a.chain_id)),
|
||||
None => Reply::Error(E_PARAMS, "chainId missing".into()),
|
||||
}
|
||||
}
|
||||
"wallet_requestPermissions" | "wallet_getPermissions" => Reply::Result(json!([{ "parentCapability": "eth_accounts", "caveats": [{ "type": "restrictReturnedAccounts", "value": [a.address] }] }])),
|
||||
"eth_sendTransaction" | "personal_sign" | "eth_sign" | "eth_signTypedData" | "eth_signTypedData_v3" | "eth_signTypedData_v4" => {
|
||||
if !a.unlocked {
|
||||
return Reply::Error(E_UNAUTHORIZED, "the Igneum Wallet is locked: unlock it in its window, then try again".into());
|
||||
}
|
||||
if self.open().len() >= MAX_PENDING {
|
||||
return Reply::Error(E_INTERNAL, "too many requests are waiting in the wallet window".into());
|
||||
}
|
||||
let (kind, request, detail) = match method {
|
||||
"eth_sendTransaction" => {
|
||||
let tx = list.first().cloned().unwrap_or(Value::Null);
|
||||
let from = tx.get("from").and_then(|v| v.as_str()).unwrap_or("").to_ascii_lowercase();
|
||||
if !from.is_empty() && from != a.address.to_ascii_lowercase() {
|
||||
return Reply::Error(E_UNAUTHORIZED, "from is not this wallet's address".into());
|
||||
}
|
||||
let to = tx.get("to").and_then(|v| v.as_str()).unwrap_or("").to_ascii_lowercase();
|
||||
if !is_address(&to) {
|
||||
return Reply::Error(E_PARAMS, "to must be an address (contract creation is not offered)".into());
|
||||
}
|
||||
let value = tx.get("value").map(|v| hex_quantity(v).ok_or(())).unwrap_or(Ok(0));
|
||||
let Ok(value) = value else { return Reply::Error(E_PARAMS, "value must be a hex quantity".into()) };
|
||||
let data = match tx.get("data").or_else(|| tx.get("input")) {
|
||||
None | Some(Value::Null) => Vec::new(),
|
||||
Some(d) => match hex_bytes(d) {
|
||||
Some(b) => b,
|
||||
None => return Reply::Error(E_PARAMS, "data must be 0x hex".into()),
|
||||
},
|
||||
};
|
||||
if data.len() > 128 * 1024 {
|
||||
return Reply::Error(E_PARAMS, "data over 128 KiB".into());
|
||||
}
|
||||
let gas = tx.get("gas").or_else(|| tx.get("gasLimit")).and_then(hex_quantity).map(|g| g as u64);
|
||||
let selector = if data.len() >= 4 { format!("0x{}", igneum_common::keys::hex(&data[..4])) } else { String::new() };
|
||||
(Kind::Send, json!({ "to": to, "value": value.to_string(), "data": format!("0x{}", igneum_common::keys::hex(&data)), "gas": gas }),
|
||||
json!({ "origin": origin, "to": to, "to_display": igneum_common::keys::checksum(&to), "value": value.to_string(), "data_len": data.len(), "selector": selector, "gas": gas }))
|
||||
}
|
||||
"personal_sign" | "eth_sign" => {
|
||||
// personal_sign is [message, address]; eth_sign is [address, message]: take whichever is not the address
|
||||
let (m, addr) = if method == "personal_sign" { (list.first(), list.get(1)) } else { (list.get(1), list.first()) };
|
||||
if let Some(ad) = addr.and_then(|v| v.as_str()) {
|
||||
if !ad.eq_ignore_ascii_case(a.address) {
|
||||
return Reply::Error(E_UNAUTHORIZED, "the address is not this wallet's".into());
|
||||
}
|
||||
}
|
||||
let Some(bytes) = m.and_then(message_bytes) else { return Reply::Error(E_PARAMS, "message missing".into()) };
|
||||
if bytes.len() > 64 * 1024 {
|
||||
return Reply::Error(E_PARAMS, "message over 64 KiB".into());
|
||||
}
|
||||
let text = String::from_utf8(bytes.clone()).ok().filter(|t| !t.chars().any(|c| c.is_control() && c != '\n' && c != '\t'));
|
||||
let digest = crate::tx::personal_digest(&bytes);
|
||||
(Kind::Sign, json!({ "digest": format!("0x{}", igneum_common::keys::hex(&digest)) }),
|
||||
json!({ "origin": origin, "text": text, "bytes": bytes.len(), "hex": if text.is_none() { format!("0x{}", igneum_common::keys::hex(&bytes[..bytes.len().min(64)])) } else { String::new() } }))
|
||||
}
|
||||
_ => {
|
||||
// eth_signTypedData(_v3, _v4): [address, typed data as an object or a JSON string]
|
||||
if let Some(ad) = list.first().and_then(|v| v.as_str()) {
|
||||
if !ad.eq_ignore_ascii_case(a.address) {
|
||||
return Reply::Error(E_UNAUTHORIZED, "the address is not this wallet's".into());
|
||||
}
|
||||
}
|
||||
let typed = match list.get(1) {
|
||||
Some(Value::String(s)) => match serde_json::from_str::<Value>(s) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return Reply::Error(E_PARAMS, format!("typed data is not JSON: {e}")),
|
||||
},
|
||||
Some(v) if v.is_object() => v.clone(),
|
||||
_ => return Reply::Error(E_PARAMS, "typed data missing".into()),
|
||||
};
|
||||
let t = match crate::eip712::hash(&typed) {
|
||||
Ok(t) => t,
|
||||
Err(e) => return Reply::Error(E_PARAMS, format!("typed data: {e}")),
|
||||
};
|
||||
if let Some(c) = t.chain_id {
|
||||
if c != a.chain_id as u128 {
|
||||
return Reply::Error(E_CHAIN, format!("the typed data names chain {c}; this wallet is on {}", a.chain_id));
|
||||
}
|
||||
}
|
||||
let message = typed.get("message").cloned().unwrap_or(Value::Null);
|
||||
let shown = serde_json::to_string_pretty(&message).unwrap_or_default();
|
||||
(Kind::Typed, json!({ "digest": format!("0x{}", igneum_common::keys::hex(&t.digest)) }),
|
||||
json!({ "origin": origin, "domain": t.domain_name, "primary_type": t.primary_type, "message": if shown.len() > 4000 { format!("{}\n…", &shown[..4000]) } else { shown } }))
|
||||
}
|
||||
};
|
||||
let id = self.new_id();
|
||||
self.pending.push(Pending { id: id.clone(), origin: origin.into(), kind, created: now, created_unix: now_unix, detail, request, confirm_nonce: String::new(), confirm_reason: String::new(), done: None, decided: None });
|
||||
Reply::Pending(id)
|
||||
}
|
||||
m if PROXIED.contains(&m) => Reply::Proxy,
|
||||
_ => Reply::Error(E_UNSUPPORTED, format!("{method} is not offered by the Igneum Wallet")),
|
||||
}
|
||||
}
|
||||
|
||||
/// The page reads a request's outcome: pending, the result, or the error. Only the origin that made it may read it.
|
||||
pub fn poll(&mut self, origin: &str, id: &str) -> Reply {
|
||||
let Some(p) = self.pending.iter().find(|p| p.id == id) else { return Reply::Error(E_INTERNAL, "unknown request".into()) };
|
||||
if p.origin != origin {
|
||||
return Reply::Error(E_UNAUTHORIZED, "not your request".into());
|
||||
}
|
||||
match &p.done {
|
||||
None => Reply::Pending(id.into()),
|
||||
Some(Ok(v)) => {
|
||||
let v = v.clone();
|
||||
self.pending.retain(|q| q.id != id);
|
||||
Reply::Result(v)
|
||||
}
|
||||
Some(Err((c, m))) => {
|
||||
let (c, m) = (*c, m.clone());
|
||||
self.pending.retain(|q| q.id != id);
|
||||
Reply::Error(c, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The window's word. Decline ends the request with 4001; Approve hands the request back to the engine, which
|
||||
/// signs or connects and then `resolve`s it.
|
||||
pub fn decide(&mut self, id: &str, ok: bool, now: Instant) -> Result<Pending, String> {
|
||||
let p = self.pending.iter_mut().find(|p| p.id == id && p.done.is_none()).ok_or("no such request is waiting")?;
|
||||
if !ok {
|
||||
p.done = Some(Err((E_REJECTED, "the person declined in the Igneum Wallet".into())));
|
||||
p.decided = Some(now);
|
||||
}
|
||||
Ok(p.clone())
|
||||
}
|
||||
|
||||
pub fn resolve(&mut self, id: &str, r: Result<Value, (i64, String)>, now: Instant) {
|
||||
if let Some(p) = self.pending.iter_mut().find(|p| p.id == id) {
|
||||
p.done = Some(r);
|
||||
p.decided = Some(now);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_challenge(&mut self, id: &str, nonce: &str, reason: &str) {
|
||||
if let Some(p) = self.pending.iter_mut().find(|p| p.id == id) {
|
||||
p.confirm_nonce = nonce.into();
|
||||
p.confirm_reason = reason.into();
|
||||
}
|
||||
}
|
||||
|
||||
/// The open requests as the window shows them (the oldest first).
|
||||
pub fn pending_json(&self, enrolled: bool) -> Value {
|
||||
Value::Array(self.pending.iter().filter(|p| p.done.is_none()).map(|p| {
|
||||
let mut d = p.detail.clone();
|
||||
d["id"] = json!(p.id);
|
||||
d["kind"] = json!(p.kind.name());
|
||||
d["created_at"] = json!(p.created_unix);
|
||||
d["confirm_needed"] = json!(enrolled && p.kind != Kind::Connect);
|
||||
d["confirm_nonce"] = json!(p.confirm_nonce);
|
||||
d["confirm_reason"] = json!(p.confirm_reason);
|
||||
d
|
||||
}).collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
const ME: &str = "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826";
|
||||
fn ask(approved: bool, unlocked: bool) -> Ask<'static> {
|
||||
Ask { address: ME, unlocked, chain_id: 4463, approved, listening: true }
|
||||
}
|
||||
fn tx() -> Value {
|
||||
json!([{ "from": ME, "to": "0x9a6fA842C4e58A87AEF1F3aD15233d99283002B7", "value": "0xde0b6b3a7640000", "data": "0x38ed17390000000000000000000000000000000000000000000000000000000000000000" }])
|
||||
}
|
||||
|
||||
/// The known-failed shape of 0.1.5: a page could reach nothing at all; with the bridge, a page without approval
|
||||
/// must still get nothing but the connect question.
|
||||
#[test]
|
||||
fn a_page_without_approval_gets_nothing() {
|
||||
let mut b = Bridge::new();
|
||||
let now = Instant::now();
|
||||
for (m, p) in [("eth_chainId", json!([])), ("eth_sendTransaction", tx()), ("personal_sign", json!(["hello", ME])), ("eth_call", json!([{}, "latest"])), ("eth_signTypedData_v4", json!([ME, "{}"]))] {
|
||||
match b.request("https://igneum.network", m, &p, &ask(false, true), now, 1.0) {
|
||||
Reply::Error(c, _) => assert_eq!(c, E_UNAUTHORIZED, "{m}"),
|
||||
other => panic!("{m} answered {other:?}"),
|
||||
}
|
||||
}
|
||||
assert_eq!(b.request("https://igneum.network", "eth_accounts", &json!([]), &ask(false, true), now, 1.0), Reply::Result(json!([])));
|
||||
assert!(b.open().is_empty(), "no request was created for an unapproved page");
|
||||
assert_eq!(b.request("", "eth_requestAccounts", &json!([]), &ask(false, true), now, 1.0), Reply::Error(E_UNAUTHORIZED, "a page bridge call carries its origin".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_connect_request_waits_for_the_windows_word_and_only_its_origin_reads_it() {
|
||||
let mut b = Bridge::new();
|
||||
let now = Instant::now();
|
||||
let Reply::Pending(id) = b.request("https://igneum.network", "eth_requestAccounts", &json!([]), &ask(false, true), now, 1.0) else { panic!() };
|
||||
assert_eq!(b.request("https://igneum.network", "eth_requestAccounts", &json!([]), &ask(false, true), now, 2.0), Reply::Pending(id.clone()), "the same page asking again gets the same request");
|
||||
assert_eq!(b.poll("https://igneum.network", &id), Reply::Pending(id.clone()));
|
||||
assert_eq!(b.poll("https://evil.example", &id), Reply::Error(E_UNAUTHORIZED, "not your request".into()));
|
||||
let p = b.decide(&id, true, now).unwrap();
|
||||
assert_eq!(p.kind, Kind::Connect);
|
||||
b.resolve(&id, Ok(json!([ME])), now);
|
||||
assert_eq!(b.poll("https://igneum.network", &id), Reply::Result(json!([ME])));
|
||||
assert_eq!(b.poll("https://igneum.network", &id), Reply::Error(E_INTERNAL, "unknown request".into()), "read once");
|
||||
// declined
|
||||
let Reply::Pending(id2) = b.request("https://other.example", "eth_requestAccounts", &json!([]), &ask(false, true), now, 3.0) else { panic!() };
|
||||
b.decide(&id2, false, now).unwrap();
|
||||
assert!(matches!(b.poll("https://other.example", &id2), Reply::Error(E_REJECTED, _)));
|
||||
// once approved: the address, the chain, the reads proxied, another chain refused
|
||||
assert_eq!(b.request("https://igneum.network", "eth_requestAccounts", &json!([]), &ask(true, true), now, 4.0), Reply::Result(json!([ME])));
|
||||
assert_eq!(b.request("https://igneum.network", "eth_chainId", &json!([]), &ask(true, true), now, 4.0), Reply::Result(json!("0x116f")));
|
||||
assert_eq!(b.request("https://igneum.network", "eth_call", &json!([{}, "latest"]), &ask(true, true), now, 4.0), Reply::Proxy);
|
||||
assert_eq!(b.request("https://igneum.network", "wallet_switchEthereumChain", &json!([{ "chainId": "0x116f" }]), &ask(true, true), now, 4.0), Reply::Result(Value::Null));
|
||||
assert!(matches!(b.request("https://igneum.network", "wallet_switchEthereumChain", &json!([{ "chainId": "0x1" }]), &ask(true, true), now, 4.0), Reply::Error(E_CHAIN, _)));
|
||||
assert!(matches!(b.request("https://igneum.network", "eth_requestAccounts", &json!([]), &ask(true, false), now, 4.0), Reply::Error(E_UNAUTHORIZED, _)), "locked: no address");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_send_a_message_and_typed_data_each_wait_as_their_own_request_with_the_facts_the_window_shows() {
|
||||
let mut b = Bridge::new();
|
||||
let now = Instant::now();
|
||||
let Reply::Pending(id) = b.request("https://igneum.network", "eth_sendTransaction", &tx(), &ask(true, true), now, 1.0) else { panic!() };
|
||||
let p = b.pending.iter().find(|p| p.id == id).unwrap().clone();
|
||||
assert_eq!(p.kind, Kind::Send);
|
||||
assert_eq!(p.detail["to_display"], json!("0x9a6fA842C4e58A87AEF1F3aD15233d99283002B7"));
|
||||
assert_eq!(p.detail["value"], json!("1000000000000000000"));
|
||||
assert_eq!(p.detail["selector"], json!("0x38ed1739"));
|
||||
assert_eq!(p.detail["data_len"], json!(36));
|
||||
assert_eq!(p.request["gas"], Value::Null);
|
||||
assert!(matches!(b.request("https://igneum.network", "eth_sendTransaction", &json!([{ "from": "0x1111111111111111111111111111111111111111", "to": ME }]), &ask(true, true), now, 1.0), Reply::Error(E_UNAUTHORIZED, _)));
|
||||
assert!(matches!(b.request("https://igneum.network", "eth_sendTransaction", &json!([{ "from": ME }]), &ask(true, true), now, 1.0), Reply::Error(E_PARAMS, _)));
|
||||
let Reply::Pending(id2) = b.request("https://igneum.network", "personal_sign", &json!(["0x68656c6c6f", ME]), &ask(true, true), now, 1.0) else { panic!() };
|
||||
let p2 = b.pending.iter().find(|p| p.id == id2).unwrap().clone();
|
||||
assert_eq!(p2.kind, Kind::Sign);
|
||||
assert_eq!(p2.detail["text"], json!("hello"));
|
||||
assert_eq!(p2.request["digest"].as_str().unwrap().len(), 66);
|
||||
let typed = json!({ "types": { "Person": [{ "name": "name", "type": "string" }] }, "primaryType": "Person", "domain": { "name": "Igneum Swap", "chainId": 4463 }, "message": { "name": "Cow" } });
|
||||
let Reply::Pending(id3) = b.request("https://igneum.network", "eth_signTypedData_v4", &json!([ME, typed.to_string()]), &ask(true, true), now, 1.0) else { panic!() };
|
||||
let p3 = b.pending.iter().find(|p| p.id == id3).unwrap().clone();
|
||||
assert_eq!(p3.kind, Kind::Typed);
|
||||
assert_eq!(p3.detail["domain"], json!("Igneum Swap"));
|
||||
assert_eq!(p3.detail["primary_type"], json!("Person"));
|
||||
let other_chain = json!({ "types": { "Person": [{ "name": "name", "type": "string" }] }, "primaryType": "Person", "domain": { "chainId": 1 }, "message": { "name": "Cow" } });
|
||||
assert!(matches!(b.request("https://igneum.network", "eth_signTypedData_v4", &json!([ME, other_chain]), &ask(true, true), now, 1.0), Reply::Error(E_CHAIN, _)));
|
||||
assert_eq!(b.pending_json(true).as_array().unwrap().len(), 3);
|
||||
assert_eq!(b.pending_json(true)[0]["confirm_needed"], json!(true));
|
||||
// the window's confirm, the engine's resolve, the page's read
|
||||
b.decide(&id2, true, now).unwrap();
|
||||
b.resolve(&id2, Ok(json!("0xsig")), now);
|
||||
assert_eq!(b.poll("https://igneum.network", &id2), Reply::Result(json!("0xsig")));
|
||||
assert_eq!(b.open().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unanswered_request_expires_and_a_read_result_is_dropped() {
|
||||
let mut b = Bridge::new();
|
||||
let t0 = Instant::now();
|
||||
let Reply::Pending(id) = b.request("https://igneum.network", "eth_requestAccounts", &json!([]), &ask(false, true), t0, 1.0) else { panic!() };
|
||||
assert!(b.expire(t0 + PENDING_TTL / 2).is_empty());
|
||||
let gone = b.expire(t0 + PENDING_TTL + Duration::from_secs(1));
|
||||
assert_eq!(gone.len(), 1);
|
||||
assert!(matches!(b.poll("https://igneum.network", &id), Reply::Error(E_REJECTED, m) if m.contains("expired")));
|
||||
let Reply::Pending(id2) = b.request("https://igneum.network", "eth_requestAccounts", &json!([]), &ask(false, true), t0, 2.0) else { panic!() };
|
||||
b.decide(&id2, false, t0).unwrap();
|
||||
b.expire(t0 + RESULT_TTL + Duration::from_secs(1));
|
||||
assert!(matches!(b.poll("https://igneum.network", &id2), Reply::Error(E_INTERNAL, _)), "an unread result is gone after RESULT_TTL");
|
||||
}
|
||||
}
|
||||
334
app/igneum-wallet/src/eip712.rs
Normal file
334
app/igneum-wallet/src/eip712.rs
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
//! EIP-712 typed data hashing for the page bridge (8 October 2026): the digest a page asks the wallet to sign with
|
||||
//! eth_signTypedData_v4, computed here so the window can show the domain and the primary type and the key never
|
||||
//! leaves the engine. Supports the atomic types (uintN, intN, bytesN, bool, address), the dynamic ones (string,
|
||||
//! bytes), arrays (fixed and dynamic) and nested structs; the domain's fields are the ones present in `domain`
|
||||
//! (name, version, chainId, verifyingContract, salt) when `types` carries no EIP712Domain of its own.
|
||||
use serde_json::Value;
|
||||
use sha3::{Digest, Keccak256};
|
||||
|
||||
pub struct Typed {
|
||||
pub digest: [u8; 32],
|
||||
pub domain_name: String,
|
||||
pub primary_type: String,
|
||||
pub chain_id: Option<u128>,
|
||||
}
|
||||
|
||||
fn keccak(b: &[u8]) -> [u8; 32] {
|
||||
Keccak256::digest(b).into()
|
||||
}
|
||||
|
||||
/// A 256-bit unsigned number from a JSON number, a decimal string or a 0x hex string, big-endian in 32 bytes.
|
||||
pub fn u256_bytes(v: &Value) -> Result<[u8; 32], String> {
|
||||
let mut out = [0u8; 32];
|
||||
match v {
|
||||
Value::Number(n) => {
|
||||
let x = n.as_u64().ok_or("a number must be a whole non-negative number")?;
|
||||
out[24..].copy_from_slice(&x.to_be_bytes());
|
||||
Ok(out)
|
||||
}
|
||||
Value::String(s) => {
|
||||
let s = s.trim();
|
||||
if let Some(h) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
|
||||
if h.is_empty() || h.len() > 64 || !h.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return Err(format!("not a 256-bit hex number: {s}"));
|
||||
}
|
||||
let padded = format!("{:0>64}", h);
|
||||
for i in 0..32 {
|
||||
out[i] = u8::from_str_radix(&padded[i * 2..i * 2 + 2], 16).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(out)
|
||||
} else {
|
||||
if s.is_empty() || !s.chars().all(|c| c.is_ascii_digit()) {
|
||||
return Err(format!("not a decimal number: {s}"));
|
||||
}
|
||||
// schoolbook base-10 into 32 big-endian bytes
|
||||
for c in s.bytes() {
|
||||
let mut carry = (c - b'0') as u32;
|
||||
for i in (0..32).rev() {
|
||||
let x = out[i] as u32 * 10 + carry;
|
||||
out[i] = (x & 0xff) as u8;
|
||||
carry = x >> 8;
|
||||
}
|
||||
if carry != 0 {
|
||||
return Err("number over 256 bits".into());
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
_ => Err("not a number".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn i256_bytes(v: &Value) -> Result<[u8; 32], String> {
|
||||
let neg = match v {
|
||||
Value::Number(n) => n.as_i64().map(|x| x < 0).unwrap_or(false),
|
||||
Value::String(s) => s.trim().starts_with('-'),
|
||||
_ => false,
|
||||
};
|
||||
if !neg {
|
||||
return u256_bytes(v);
|
||||
}
|
||||
let mag = match v {
|
||||
Value::Number(n) => Value::String((n.as_i64().unwrap().unsigned_abs()).to_string()),
|
||||
Value::String(s) => Value::String(s.trim()[1..].to_string()),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let m = u256_bytes(&mag)?;
|
||||
// two's complement: invert and add one
|
||||
let mut out = [0u8; 32];
|
||||
let mut carry = 1u16;
|
||||
for i in (0..32).rev() {
|
||||
let x = (!m[i]) as u16 + carry;
|
||||
out[i] = (x & 0xff) as u8;
|
||||
carry = x >> 8;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn hex_bytes(s: &str) -> Result<Vec<u8>, String> {
|
||||
let h = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")).ok_or("bytes must be 0x hex")?;
|
||||
if h.len() % 2 != 0 || !h.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return Err("bytes must be even-length hex".into());
|
||||
}
|
||||
(0..h.len() / 2).map(|i| u8::from_str_radix(&h[i * 2..i * 2 + 2], 16).map_err(|e| e.to_string())).collect()
|
||||
}
|
||||
|
||||
fn is_struct(types: &Value, t: &str) -> bool {
|
||||
types.get(t).map(|v| v.is_array()).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn base_type(t: &str) -> &str {
|
||||
match t.find('[') {
|
||||
Some(i) => &t[..i],
|
||||
None => t,
|
||||
}
|
||||
}
|
||||
|
||||
/// The struct types `t` depends on, `t` first, the rest sorted by name (EIP-712 encodeType).
|
||||
fn dependencies(types: &Value, t: &str) -> Result<Vec<String>, String> {
|
||||
let mut found: Vec<String> = Vec::new();
|
||||
let mut todo = vec![t.to_string()];
|
||||
while let Some(cur) = todo.pop() {
|
||||
if found.contains(&cur) {
|
||||
continue;
|
||||
}
|
||||
let fields = types.get(&cur).and_then(|v| v.as_array()).ok_or(format!("unknown type {cur}"))?;
|
||||
found.push(cur.clone());
|
||||
for f in fields {
|
||||
let ft = f.get("type").and_then(|v| v.as_str()).ok_or("a field without a type")?;
|
||||
let b = base_type(ft);
|
||||
if is_struct(types, b) && !found.contains(&b.to_string()) {
|
||||
todo.push(b.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut rest: Vec<String> = found.iter().skip(1).cloned().collect();
|
||||
rest.sort();
|
||||
let mut out = vec![found[0].clone()];
|
||||
out.extend(rest);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn encode_type(types: &Value, t: &str) -> Result<String, String> {
|
||||
let mut s = String::new();
|
||||
for name in dependencies(types, t)? {
|
||||
let fields = types.get(&name).and_then(|v| v.as_array()).ok_or("type")?;
|
||||
s.push_str(&name);
|
||||
s.push('(');
|
||||
let mut first = true;
|
||||
for f in fields {
|
||||
if !first {
|
||||
s.push(',');
|
||||
}
|
||||
first = false;
|
||||
s.push_str(f.get("type").and_then(|v| v.as_str()).ok_or("field type")?);
|
||||
s.push(' ');
|
||||
s.push_str(f.get("name").and_then(|v| v.as_str()).ok_or("field name")?);
|
||||
}
|
||||
s.push(')');
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub fn type_hash(types: &Value, t: &str) -> Result<[u8; 32], String> {
|
||||
Ok(keccak(encode_type(types, t)?.as_bytes()))
|
||||
}
|
||||
|
||||
fn encode_value(types: &Value, t: &str, v: &Value) -> Result<[u8; 32], String> {
|
||||
if let Some(i) = t.find('[') {
|
||||
// an array: keccak of the concatenated encodings of its elements
|
||||
let inner = &t[..i];
|
||||
let rest = &t[i + 1..];
|
||||
let items = v.as_array().ok_or(format!("{t}: not an array"))?;
|
||||
if let Some(n) = rest.strip_suffix(']').and_then(|n| if n.is_empty() { None } else { n.parse::<usize>().ok() }) {
|
||||
if items.len() != n {
|
||||
return Err(format!("{t}: {} items, {n} expected", items.len()));
|
||||
}
|
||||
}
|
||||
let tail = &rest[rest.find(']').map(|j| j + 1).unwrap_or(rest.len())..];
|
||||
let elem_type = format!("{inner}{tail}");
|
||||
let mut cat = Vec::new();
|
||||
for it in items {
|
||||
cat.extend_from_slice(&encode_value(types, &elem_type, it)?);
|
||||
}
|
||||
return Ok(keccak(&cat));
|
||||
}
|
||||
if is_struct(types, t) {
|
||||
return hash_struct(types, t, v);
|
||||
}
|
||||
match t {
|
||||
"string" => Ok(keccak(v.as_str().ok_or("string expected")?.as_bytes())),
|
||||
"bytes" => Ok(keccak(&hex_bytes(v.as_str().ok_or("bytes expected")?)?)),
|
||||
"bool" => {
|
||||
let mut out = [0u8; 32];
|
||||
out[31] = if v.as_bool().ok_or("bool expected")? { 1 } else { 0 };
|
||||
Ok(out)
|
||||
}
|
||||
"address" => {
|
||||
let b = hex_bytes(v.as_str().ok_or("address expected")?)?;
|
||||
if b.len() != 20 {
|
||||
return Err("an address is 20 bytes".into());
|
||||
}
|
||||
let mut out = [0u8; 32];
|
||||
out[12..].copy_from_slice(&b);
|
||||
Ok(out)
|
||||
}
|
||||
_ if t.starts_with("uint") => {
|
||||
let bits: usize = t[4..].parse().map_err(|_| format!("bad type {t}"))?;
|
||||
if bits == 0 || bits > 256 || bits % 8 != 0 {
|
||||
return Err(format!("bad type {t}"));
|
||||
}
|
||||
u256_bytes(v)
|
||||
}
|
||||
_ if t.starts_with("int") => {
|
||||
let bits: usize = t[3..].parse().map_err(|_| format!("bad type {t}"))?;
|
||||
if bits == 0 || bits > 256 || bits % 8 != 0 {
|
||||
return Err(format!("bad type {t}"));
|
||||
}
|
||||
i256_bytes(v)
|
||||
}
|
||||
_ if t.starts_with("bytes") => {
|
||||
let n: usize = t[5..].parse().map_err(|_| format!("bad type {t}"))?;
|
||||
if n == 0 || n > 32 {
|
||||
return Err(format!("bad type {t}"));
|
||||
}
|
||||
let b = hex_bytes(v.as_str().ok_or("bytes expected")?)?;
|
||||
if b.len() != n {
|
||||
return Err(format!("{t}: {} bytes given", b.len()));
|
||||
}
|
||||
let mut out = [0u8; 32];
|
||||
out[..n].copy_from_slice(&b);
|
||||
Ok(out)
|
||||
}
|
||||
_ => Err(format!("unsupported type {t}")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hash_struct(types: &Value, t: &str, v: &Value) -> Result<[u8; 32], String> {
|
||||
let fields = types.get(t).and_then(|x| x.as_array()).ok_or(format!("unknown type {t}"))?;
|
||||
let obj = v.as_object().ok_or(format!("{t}: an object expected"))?;
|
||||
let mut enc = Vec::new();
|
||||
enc.extend_from_slice(&type_hash(types, t)?);
|
||||
for f in fields {
|
||||
let name = f.get("name").and_then(|x| x.as_str()).ok_or("field name")?;
|
||||
let ft = f.get("type").and_then(|x| x.as_str()).ok_or("field type")?;
|
||||
let fv = obj.get(name).ok_or(format!("{t}.{name} is missing"))?;
|
||||
enc.extend_from_slice(&encode_value(types, ft, fv)?);
|
||||
}
|
||||
Ok(keccak(&enc))
|
||||
}
|
||||
|
||||
/// The domain type when `types` has none: the fields present in `domain`, in the canonical order.
|
||||
fn domain_types(domain: &Value) -> Value {
|
||||
let order = [("name", "string"), ("version", "string"), ("chainId", "uint256"), ("verifyingContract", "address"), ("salt", "bytes32")];
|
||||
let mut v = Vec::new();
|
||||
for (n, t) in order {
|
||||
if domain.get(n).is_some() {
|
||||
v.push(serde_json::json!({ "name": n, "type": t }));
|
||||
}
|
||||
}
|
||||
Value::Array(v)
|
||||
}
|
||||
|
||||
/// The EIP-712 digest of a typed-data object ({types, primaryType, domain, message}).
|
||||
pub fn hash(typed: &Value) -> Result<Typed, String> {
|
||||
let mut types = typed.get("types").cloned().ok_or("typed data without types")?;
|
||||
let domain = typed.get("domain").cloned().unwrap_or(Value::Object(Default::default()));
|
||||
if !types.get("EIP712Domain").map(|v| v.is_array()).unwrap_or(false) {
|
||||
types["EIP712Domain"] = domain_types(&domain);
|
||||
}
|
||||
let primary = typed.get("primaryType").and_then(|v| v.as_str()).ok_or("typed data without primaryType")?.to_string();
|
||||
let message = typed.get("message").cloned().ok_or("typed data without message")?;
|
||||
let ds = hash_struct(&types, "EIP712Domain", &domain)?;
|
||||
let ms = if primary == "EIP712Domain" { None } else { Some(hash_struct(&types, &primary, &message)?) };
|
||||
let mut pre = vec![0x19, 0x01];
|
||||
pre.extend_from_slice(&ds);
|
||||
if let Some(m) = ms {
|
||||
pre.extend_from_slice(&m);
|
||||
}
|
||||
let chain_id = domain.get("chainId").and_then(|v| u256_bytes(v).ok()).map(|b| {
|
||||
let mut x = 0u128;
|
||||
for byte in &b[16..] {
|
||||
x = (x << 8) | *byte as u128;
|
||||
}
|
||||
x
|
||||
});
|
||||
Ok(Typed { digest: keccak(&pre), domain_name: domain.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string(), primary_type: primary, chain_id })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
/// The specification's own example (EIP-712, "Mail" from Cow to Bob): the three known hashes.
|
||||
fn mail() -> Value {
|
||||
json!({
|
||||
"types": {
|
||||
"EIP712Domain": [{"name": "name", "type": "string"}, {"name": "version", "type": "string"}, {"name": "chainId", "type": "uint256"}, {"name": "verifyingContract", "type": "address"}],
|
||||
"Person": [{"name": "name", "type": "string"}, {"name": "wallet", "type": "address"}],
|
||||
"Mail": [{"name": "from", "type": "Person"}, {"name": "to", "type": "Person"}, {"name": "contents", "type": "string"}]
|
||||
},
|
||||
"primaryType": "Mail",
|
||||
"domain": {"name": "Ether Mail", "version": "1", "chainId": 1, "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"},
|
||||
"message": {"from": {"name": "Cow", "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"}, "to": {"name": "Bob", "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"}, "contents": "Hello, Bob!"}
|
||||
})
|
||||
}
|
||||
fn hex(b: &[u8]) -> String {
|
||||
b.iter().map(|x| format!("{x:02x}")).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_specifications_mail_example_hashes_as_published() {
|
||||
let m = mail();
|
||||
assert_eq!(encode_type(&m["types"], "Mail").unwrap(), "Mail(Person from,Person to,string contents)Person(string name,address wallet)");
|
||||
assert_eq!(hex(&hash_struct(&m["types"], "EIP712Domain", &m["domain"]).unwrap()), "f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f"); // no-secrets-ok: the EIP-712 specification's published example hash
|
||||
assert_eq!(hex(&hash_struct(&m["types"], "Mail", &m["message"]).unwrap()), "c52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e"); // no-secrets-ok: the EIP-712 specification's published example hash
|
||||
let t = hash(&m).unwrap();
|
||||
assert_eq!(hex(&t.digest), "be609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2"); // no-secrets-ok: the EIP-712 specification's published example digest
|
||||
assert_eq!(t.domain_name, "Ether Mail");
|
||||
assert_eq!(t.primary_type, "Mail");
|
||||
assert_eq!(t.chain_id, Some(1));
|
||||
}
|
||||
#[test]
|
||||
fn a_domain_without_declared_types_takes_the_fields_present_in_order() {
|
||||
let mut m = mail();
|
||||
m["types"].as_object_mut().unwrap().remove("EIP712Domain");
|
||||
assert_eq!(hex(&hash(&m).unwrap().digest), "be609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2"); // no-secrets-ok: the EIP-712 specification's published example digest
|
||||
}
|
||||
#[test]
|
||||
fn numbers_arrays_and_bytes_encode() {
|
||||
assert_eq!(hex(&u256_bytes(&json!("115792089237316195423570985008687907853269984665640564039457584007913129639935")).unwrap()), "f".repeat(64));
|
||||
assert_eq!(hex(&u256_bytes(&json!("0x1f")).unwrap()), format!("{:0>64}", "1f"));
|
||||
assert_eq!(hex(&u256_bytes(&json!(31)).unwrap()), format!("{:0>64}", "1f"));
|
||||
assert_eq!(hex(&i256_bytes(&json!(-1)).unwrap()), "f".repeat(64));
|
||||
assert!(u256_bytes(&json!("1157920892373161954235709850086879078532699846656405640394575840079131296399350")).is_err());
|
||||
let types = json!({ "T": [{"name": "xs", "type": "uint8[]"}, {"name": "b", "type": "bytes4"}, {"name": "ok", "type": "bool"}] });
|
||||
let one = hash_struct(&types, "T", &json!({ "xs": [1, 2], "b": "0x01020304", "ok": true })).unwrap();
|
||||
let two = hash_struct(&types, "T", &json!({ "xs": [2, 1], "b": "0x01020304", "ok": true })).unwrap();
|
||||
assert_ne!(one, two);
|
||||
assert!(hash_struct(&types, "T", &json!({ "xs": [1], "b": "0x0102", "ok": true })).is_err(), "bytes4 with two bytes is refused");
|
||||
assert!(hash_struct(&types, "T", &json!({ "b": "0x01020304", "ok": true })).is_err(), "a missing field is refused");
|
||||
}
|
||||
}
|
||||
1603
app/igneum-wallet/src/engine.rs
Normal file
1603
app/igneum-wallet/src/engine.rs
Normal file
File diff suppressed because it is too large
Load diff
156
app/igneum-wallet/src/evm.rs
Normal file
156
app/igneum-wallet/src/evm.rs
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
//! The node's Ethereum JSON-RPC: what the wallet reads and the one thing it writes (eth_sendRawTransaction). Plain
|
||||
//! HTTP to 127.0.0.1 through igneum-common; a public https RPC (no local node) goes through curl.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Evm {
|
||||
/// "127.0.0.1:26790" (plain http) or "https://..." (curl)
|
||||
pub endpoint: String,
|
||||
}
|
||||
|
||||
pub fn q(v: &Value) -> Option<u128> {
|
||||
let s = v.as_str()?;
|
||||
u128::from_str_radix(s.trim_start_matches("0x"), 16).ok()
|
||||
}
|
||||
|
||||
pub fn hexq(v: u128) -> String {
|
||||
format!("0x{v:x}")
|
||||
}
|
||||
|
||||
impl Evm {
|
||||
pub fn local(port: u16) -> Evm {
|
||||
Evm { endpoint: format!("127.0.0.1:{port}") }
|
||||
}
|
||||
|
||||
pub fn call(&self, method: &str, params: Value) -> Result<Value, String> {
|
||||
let body = json!({ "jsonrpc": "2.0", "id": 1, "method": method, "params": params }).to_string();
|
||||
let text = if self.endpoint.starts_with("https://") || self.endpoint.starts_with("http://") {
|
||||
let mut c = std::process::Command::new(igneum_common::platform::tool("curl"));
|
||||
c.args(["-sS", "--max-time", "20", "-X", "POST", &self.endpoint, "-H", "Content-Type: application/json", "-d", &body]);
|
||||
igneum_common::run::run_timeout(&mut c, None, Duration::from_secs(25)).ok_or("curl is not available")?
|
||||
} else {
|
||||
igneum_common::http::post_json(&self.endpoint, "/", &body, Duration::from_secs(20))?
|
||||
};
|
||||
let v: Value = serde_json::from_str(text.trim()).map_err(|_| format!("{method}: not JSON: {}", text.chars().take(120).collect::<String>()))?;
|
||||
if let Some(e) = v.get("error") {
|
||||
let msg = e.get("message").and_then(|m| m.as_str()).unwrap_or("error");
|
||||
return Err(format!("{method}: {msg}"));
|
||||
}
|
||||
Ok(v.get("result").cloned().unwrap_or(Value::Null))
|
||||
}
|
||||
|
||||
pub fn chain_id(&self) -> Result<u64, String> {
|
||||
q(&self.call("eth_chainId", json!([]))?).map(|v| v as u64).ok_or("eth_chainId: no number".into())
|
||||
}
|
||||
pub fn block_number(&self) -> Result<u64, String> {
|
||||
q(&self.call("eth_blockNumber", json!([]))?).map(|v| v as u64).ok_or("eth_blockNumber: no number".into())
|
||||
}
|
||||
pub fn balance(&self, address: &str) -> Result<u128, String> {
|
||||
q(&self.call("eth_getBalance", json!([address, "latest"]))?).ok_or("eth_getBalance: no number".into())
|
||||
}
|
||||
pub fn nonce(&self, address: &str) -> Result<u64, String> {
|
||||
q(&self.call("eth_getTransactionCount", json!([address, "pending"]))?).map(|v| v as u64).ok_or("nonce: no number".into())
|
||||
}
|
||||
/// (base fee per gas of the latest block, the node's suggested priority fee)
|
||||
pub fn fees(&self) -> Result<(u128, u128), String> {
|
||||
let b = self.call("eth_getBlockByNumber", json!(["latest", false]))?;
|
||||
let base = b.get("baseFeePerGas").and_then(q).unwrap_or(0);
|
||||
let tip = self.call("eth_maxPriorityFeePerGas", json!([])).ok().and_then(|v| q(&v)).unwrap_or(1_000_000_000);
|
||||
Ok((base, tip))
|
||||
}
|
||||
pub fn estimate_gas(&self, from: &str, to: &str, value: u128) -> Result<u64, String> {
|
||||
q(&self.call("eth_estimateGas", json!([{ "from": from, "to": to, "value": hexq(value) }]))?).map(|v| v as u64).ok_or("eth_estimateGas: no number".into())
|
||||
}
|
||||
/// The gas a call needs, with its data (the page bridge's contract calls; the node prices proving gas inside
|
||||
/// execution gas on Devnet 3, so the node's own estimate is the one to use).
|
||||
pub fn estimate_gas_call(&self, from: &str, to: &str, value: u128, data: &str) -> Result<u64, String> {
|
||||
let mut call = json!({ "from": from, "to": to, "value": hexq(value) });
|
||||
if data.len() > 2 {
|
||||
call["data"] = json!(data);
|
||||
}
|
||||
q(&self.call("eth_estimateGas", json!([call]))?).map(|v| v as u64).ok_or("eth_estimateGas: no number".into())
|
||||
}
|
||||
pub fn send_raw(&self, raw: &[u8]) -> Result<String, String> {
|
||||
let v = self.call("eth_sendRawTransaction", json!([crate::tx::hex0x(raw)]))?;
|
||||
v.as_str().map(|s| s.to_string()).ok_or("eth_sendRawTransaction: no hash".into())
|
||||
}
|
||||
pub fn receipt(&self, hash: &str) -> Result<Option<Value>, String> {
|
||||
let v = self.call("eth_getTransactionReceipt", json!([hash]))?;
|
||||
Ok(if v.is_null() { None } else { Some(v) })
|
||||
}
|
||||
pub fn tx(&self, hash: &str) -> Result<Option<Value>, String> {
|
||||
let v = self.call("eth_getTransactionByHash", json!([hash]))?;
|
||||
Ok(if v.is_null() { None } else { Some(v) })
|
||||
}
|
||||
pub fn block(&self, number: u64, full: bool) -> Result<Option<Value>, String> {
|
||||
let v = self.call("eth_getBlockByNumber", json!([hexq(number as u128), full]))?;
|
||||
Ok(if v.is_null() { None } else { Some(v) })
|
||||
}
|
||||
pub fn block_by_hash(&self, hash: &str) -> Result<Option<Value>, String> {
|
||||
let v = self.call("eth_getBlockByHash", json!([hash, false]))?;
|
||||
Ok(if v.is_null() { None } else { Some(v) })
|
||||
}
|
||||
/// igneum_getSegment: the chain block's execution record (rewards per blue block's miner, proving pool credit).
|
||||
pub fn segment(&self, number: u64) -> Result<Option<Value>, String> {
|
||||
let v = self.call("igneum_getSegment", json!([hexq(number as u128)]))?;
|
||||
Ok(if v.is_null() { None } else { Some(v) })
|
||||
}
|
||||
pub fn tx_status(&self, hash: &str) -> Result<Value, String> {
|
||||
self.call("igneum_getTransactionStatus", json!([hash]))
|
||||
}
|
||||
}
|
||||
|
||||
/// wei to a decimal IGN string with up to `places` decimals, trailing zeros trimmed (never scientific, never rounded up).
|
||||
pub fn ign(wei: u128, places: usize) -> String {
|
||||
let whole = wei / 1_000_000_000_000_000_000;
|
||||
let frac = wei % 1_000_000_000_000_000_000;
|
||||
let mut f = format!("{frac:018}");
|
||||
f.truncate(places);
|
||||
let f = f.trim_end_matches('0');
|
||||
if f.is_empty() { format!("{whole}") } else { format!("{whole}.{f}") }
|
||||
}
|
||||
|
||||
/// A typed amount ("1.5", "0.001", "12") to wei; refuses more than 18 decimals and anything that is not a number.
|
||||
pub fn parse_ign(text: &str) -> Result<u128, String> {
|
||||
let t = text.trim().replace(',', "");
|
||||
if t.is_empty() {
|
||||
return Err("type an amount".into());
|
||||
}
|
||||
let (w, f) = match t.split_once('.') {
|
||||
Some((w, f)) => (w, f),
|
||||
None => (t.as_str(), ""),
|
||||
};
|
||||
if !w.chars().all(|c| c.is_ascii_digit()) || !f.chars().all(|c| c.is_ascii_digit()) || (w.is_empty() && f.is_empty()) {
|
||||
return Err("an amount is digits with one dot".into());
|
||||
}
|
||||
if f.len() > 18 {
|
||||
return Err("at most 18 decimals".into());
|
||||
}
|
||||
let whole: u128 = if w.is_empty() { 0 } else { w.parse().map_err(|_| "amount too large")? };
|
||||
let mut frac = f.to_string();
|
||||
while frac.len() < 18 {
|
||||
frac.push('0');
|
||||
}
|
||||
let frac: u128 = if frac.is_empty() { 0 } else { frac.parse().map_err(|_| "amount")? };
|
||||
whole.checked_mul(1_000_000_000_000_000_000).and_then(|v| v.checked_add(frac)).ok_or("amount too large".into())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn amounts() {
|
||||
assert_eq!(parse_ign("1.5").unwrap(), 1_500_000_000_000_000_000);
|
||||
assert_eq!(parse_ign("0.000000000000000001").unwrap(), 1);
|
||||
assert_eq!(parse_ign("12").unwrap(), 12_000_000_000_000_000_000);
|
||||
assert!(parse_ign("1e3").is_err());
|
||||
assert!(parse_ign("0.0000000000000000001").is_err());
|
||||
assert_eq!(ign(1_500_000_000_000_000_000, 6), "1.5");
|
||||
assert_eq!(ign(1, 18), "0.000000000000000001");
|
||||
assert_eq!(ign(1, 6), "0");
|
||||
assert_eq!(ign(42_000_000_000_000_000_000, 6), "42");
|
||||
assert_eq!(q(&json!("0x116f")), Some(4463));
|
||||
}
|
||||
}
|
||||
227
app/igneum-wallet/src/finality.rs
Normal file
227
app/igneum-wallet/src/finality.rs
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
//! Finality the wallet checks itself. A transaction is "final" only when its block sits under a certified checkpoint
|
||||
//! whose BLS certificate this wallet verified with the node's own code (kaspa_consensus_core::finality), never from a
|
||||
//! confirmation count and never on the node's word alone. The steps are the browser verifier's (site/verify/core.js):
|
||||
//! 1. the certificate as a block carried it (the coinbase finality section of the blocks after the checkpoint)
|
||||
//! 2. the canonical voter list: every key above dust and not stripped, sorted by key hash, 48-byte G1 keys that
|
||||
//! hash (BLAKE2b-256 keyed "IgneumVoteKeyHash") to the key hashes the node names, as many as the certificate says
|
||||
//! 3. the aggregate G2 signature over `igneum-vote-v1/<chain id> 0x00 index_le64 checkpoint` by the bitmap's keys
|
||||
//! 4. the rule: signed weight at least 2/3 of the active weight and at least 2/3 of the total weight (the floor
|
||||
//! decided 4 October 2026, O-3.15; stricter than the 17/30 this node line still carries)
|
||||
//! Then "under": the checkpoint block's selected-chain height from the Ethereum RPC; a transaction's block is under
|
||||
//! it when its number is at most that height and its hash is still the chain's hash at that number.
|
||||
|
||||
use kaspa_consensus_core::finality::{block_finality_content, verify_aggregate, vote_key_hash, Certificate, FinalityItem, PUBKEY_LEN};
|
||||
use kaspa_hashes::Hash;
|
||||
use kaspa_rpc_core::model::{GetFinalityWeightsResponse, RpcCheckpoint, RpcKeyWeight};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub struct VerifiedCheckpoint {
|
||||
pub index: u64,
|
||||
pub hash: String,
|
||||
/// the checkpoint block's selected-chain height on the execution side (None until the Ethereum RPC names it)
|
||||
pub chain_number: Option<u64>,
|
||||
pub signers: usize,
|
||||
pub voters: usize,
|
||||
pub signed_weight: u64,
|
||||
pub total_weight: u64,
|
||||
pub active_weight: f64,
|
||||
pub fraction_total: f64,
|
||||
pub fraction_active: f64,
|
||||
/// the checkpoint index the node's weight table was taken at (equal to `index` when exact)
|
||||
pub weights_at_index: u64,
|
||||
pub carrier: String,
|
||||
pub verified_at: f64,
|
||||
}
|
||||
|
||||
/// What the window shows for one transaction.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(tag = "state", rename_all = "snake_case")]
|
||||
pub enum Status {
|
||||
/// not in any block the node knows
|
||||
Pending,
|
||||
/// in chain block `number`; no verified checkpoint covers it yet
|
||||
InBlock { number: u64 },
|
||||
/// under verified checkpoint `index`
|
||||
Final { number: u64, index: u64 },
|
||||
/// the receipt says the execution failed; still subject to the same finality
|
||||
Failed { number: u64, reason: String },
|
||||
}
|
||||
|
||||
/// The state machine, pure: receipt (block number, block hash), the chain's hash at that number now, the newest
|
||||
/// verified checkpoint. Tested below.
|
||||
pub fn status(receipt: Option<(u64, &str, bool)>, canonical_hash: Option<&str>, verified: Option<&VerifiedCheckpoint>) -> Status {
|
||||
let Some((number, hash, ok)) = receipt else { return Status::Pending };
|
||||
// the block the receipt names must still be the chain's block at that height
|
||||
match canonical_hash {
|
||||
Some(h) if h.eq_ignore_ascii_case(hash) => {}
|
||||
_ => return Status::Pending,
|
||||
}
|
||||
if !ok {
|
||||
return Status::Failed { number, reason: "the execution failed (reverted or out of gas)".into() };
|
||||
}
|
||||
match verified.and_then(|v| v.chain_number.map(|n| (n, v.index))) {
|
||||
Some((cp_number, index)) if number <= cp_number => Status::Final { number, index },
|
||||
_ => Status::InBlock { number },
|
||||
}
|
||||
}
|
||||
|
||||
/// The certificate for `cp` as a block after it carried it, scanning up to `pages` pages of getBlocks.
|
||||
pub fn find_certificate(grpc: &crate::node::Grpc, cp: &RpcCheckpoint, pages: usize) -> Result<(Certificate, String), String> {
|
||||
let mut low: Hash = cp.hash;
|
||||
let mut seen = 0usize;
|
||||
for _ in 0..pages {
|
||||
let blocks = grpc.blocks_after(low)?;
|
||||
if blocks.is_empty() {
|
||||
break;
|
||||
}
|
||||
for b in &blocks {
|
||||
seen += 1;
|
||||
if let Some(tx) = b.transactions.first() {
|
||||
let (_, items) = block_finality_content(&tx.payload);
|
||||
for it in items {
|
||||
if let FinalityItem::Certificate(c) = it {
|
||||
if c.index == cp.index && c.checkpoint == cp.hash {
|
||||
return Ok((c, b.header.hash.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let last = blocks.last().unwrap().header.hash;
|
||||
if last == low {
|
||||
break;
|
||||
}
|
||||
low = last;
|
||||
}
|
||||
Err(format!("no block within {seen} of checkpoint {} carries its certificate yet", cp.index))
|
||||
}
|
||||
|
||||
/// Steps 2 to 4 over a certificate and the node's weight table.
|
||||
pub fn verify(chain_id: &str, cp: &RpcCheckpoint, cert: &Certificate, carrier: &str, w: &GetFinalityWeightsResponse) -> Result<VerifiedCheckpoint, String> {
|
||||
let mut voters: Vec<&RpcKeyWeight> = w.keys.iter().filter(|k| k.voter).collect();
|
||||
voters.sort_by(|a, b| a.key_hash.cmp(&b.key_hash));
|
||||
if voters.len() != cert.voter_count as usize {
|
||||
return Err(format!("certificate names {} voters, the node's table at checkpoint {} has {}", cert.voter_count, w.checkpoint_index, voters.len()));
|
||||
}
|
||||
let mut pubkeys: Vec<[u8; PUBKEY_LEN]> = Vec::with_capacity(voters.len());
|
||||
for (i, v) in voters.iter().enumerate() {
|
||||
let raw = igneum_common::keys::unhex(&v.pubkey).ok_or(format!("voter {i} key is not hex"))?;
|
||||
let pk: [u8; PUBKEY_LEN] = raw.try_into().map_err(|_| format!("voter {i} key is not 48 bytes"))?;
|
||||
if vote_key_hash(&pk) != v.key_hash {
|
||||
return Err(format!("voter {i} key does not hash to its key hash"));
|
||||
}
|
||||
pubkeys.push(pk);
|
||||
}
|
||||
let positions = cert.signer_positions();
|
||||
if positions.is_empty() {
|
||||
return Err("the certificate has no signers".into());
|
||||
}
|
||||
let signing: Vec<[u8; PUBKEY_LEN]> = positions.iter().map(|&p| pubkeys[p]).collect();
|
||||
if !verify_aggregate(chain_id, cert.index, cert.checkpoint, &signing, &cert.signature) {
|
||||
return Err("the aggregate signature does not verify".into());
|
||||
}
|
||||
let total: u64 = voters.iter().map(|v| v.blocks).sum();
|
||||
let active: f64 = voters.iter().map(|v| v.blocks as f64 * v.participation).sum();
|
||||
let signed: u64 = positions.iter().map(|&p| voters[p].blocks).sum();
|
||||
if total == 0 {
|
||||
return Err("total weight is zero".into());
|
||||
}
|
||||
let fraction_total = signed as f64 / total as f64;
|
||||
let fraction_active = if active > 0.0 { signed as f64 / active } else { 0.0 };
|
||||
if (3 * signed as u128) < (2 * active.round() as u128) {
|
||||
return Err(format!("signed weight is {:.1}% of active, below 2/3", fraction_active * 100.0));
|
||||
}
|
||||
if 3 * (signed as u128) < 2 * (total as u128) {
|
||||
return Err(format!("signed weight is {:.1}% of total, below 2/3", fraction_total * 100.0));
|
||||
}
|
||||
Ok(VerifiedCheckpoint {
|
||||
index: cp.index,
|
||||
hash: cp.hash.to_string(),
|
||||
chain_number: None,
|
||||
signers: positions.len(),
|
||||
voters: voters.len(),
|
||||
signed_weight: signed,
|
||||
total_weight: total,
|
||||
active_weight: active,
|
||||
fraction_total,
|
||||
fraction_active,
|
||||
weights_at_index: w.checkpoint_index,
|
||||
carrier: carrier.to_string(),
|
||||
verified_at: igneum_common::platform::unix_now_f(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn cp(chain_number: Option<u64>) -> VerifiedCheckpoint {
|
||||
VerifiedCheckpoint { index: 536, hash: "ac08".into(), chain_number, ..Default::default() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_machine() {
|
||||
// no receipt: pending, whatever the checkpoint says
|
||||
assert_eq!(status(None, None, Some(&cp(Some(100)))), Status::Pending);
|
||||
// in a block, no verified checkpoint
|
||||
assert_eq!(status(Some((10, "0xaa", true)), Some("0xaa"), None), Status::InBlock { number: 10 });
|
||||
// the checkpoint is below the block: still in a block
|
||||
assert_eq!(status(Some((10, "0xaa", true)), Some("0xaa"), Some(&cp(Some(9)))), Status::InBlock { number: 10 });
|
||||
// the checkpoint's chain height is unknown yet
|
||||
assert_eq!(status(Some((10, "0xaa", true)), Some("0xaa"), Some(&cp(None))), Status::InBlock { number: 10 });
|
||||
// under the checkpoint: final, with the index
|
||||
assert_eq!(status(Some((10, "0xaa", true)), Some("0xaa"), Some(&cp(Some(10)))), Status::Final { number: 10, index: 536 });
|
||||
assert_eq!(status(Some((3, "0xaa", true)), Some("0xAA"), Some(&cp(Some(10)))), Status::Final { number: 3, index: 536 });
|
||||
// the chain moved away from the receipt's block: back to pending
|
||||
assert_eq!(status(Some((10, "0xaa", true)), Some("0xbb"), Some(&cp(Some(10)))), Status::Pending);
|
||||
assert_eq!(status(Some((10, "0xaa", true)), None, Some(&cp(Some(10)))), Status::Pending);
|
||||
// a failed execution is reported as failed, never final
|
||||
assert!(matches!(status(Some((10, "0xaa", false)), Some("0xaa"), Some(&cp(Some(10)))), Status::Failed { number: 10, .. }));
|
||||
}
|
||||
|
||||
/// A certificate made with real BLS keys verifies; a flipped bit, a missing voter or a thin quorum does not.
|
||||
#[test]
|
||||
fn certificate_rule() {
|
||||
use kaspa_consensus_core::finality::{aggregate_signatures, VoteSecretKey};
|
||||
use kaspa_rpc_core::model::RpcFinalityParams;
|
||||
let chain = "igneum-devnet-955";
|
||||
let checkpoint = Hash::from_slice(&[7u8; 32]);
|
||||
let keys: Vec<VoteSecretKey> = (0..3).map(|i| VoteSecretKey::from_label(&format!("wallet-test-{i}"))).collect();
|
||||
let mut table: Vec<RpcKeyWeight> = keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, k)| RpcKeyWeight { key_hash: k.key_hash(), pubkey: igneum_common::keys::hex(&k.public_key()), blocks: [50, 30, 20][i], voter: true, participation: 1.0, stripped_until_daa: 0 })
|
||||
.collect();
|
||||
table.sort_by(|a, b| a.key_hash.cmp(&b.key_hash));
|
||||
let by_hash = |h: Hash| keys.iter().find(|k| k.key_hash() == h).unwrap();
|
||||
// the two heaviest keys sign (80 of 100)
|
||||
let mut signers: Vec<usize> = (0..3).filter(|&p| table[p].blocks >= 30).collect();
|
||||
signers.sort();
|
||||
let sigs: Vec<[u8; 96]> = signers.iter().map(|&p| by_hash(table[p].key_hash).sign_vote(chain, 536, checkpoint)).collect();
|
||||
let agg = aggregate_signatures(&sigs).unwrap();
|
||||
let cert = Certificate { index: 536, checkpoint, voter_count: 3, bitmap: Certificate::bitmap_from_positions(3, &signers), signature: agg, aggregator: Hash::from_slice(&[0u8; 32]), aggregator_proof: [0u8; 96] };
|
||||
let rcp = RpcCheckpoint { index: 536, hash: checkpoint, blue_score: 0, daa_score: 0, state: "locked".into(), signed_weight: 80, active_weight: 100, total_weight: 100, fraction_active: 0.8, fraction_total: 0.8, votes_seen: 2, voters: 3, aggregators: vec![], locked_at_daa: 1, certificate_aggregator: Hash::from_slice(&[0u8; 32]) };
|
||||
let params = RpcFinalityParams { checkpoint_interval: 30, checkpoint_depth: 20, weight_window: 120, dust: 5, presence_window: 1, aggregators: 8, equivocation_ban: 120, min_daa: 120 };
|
||||
let w = GetFinalityWeightsResponse { params, checkpoint_index: 536, checkpoint_hash: checkpoint, daa_score: 0, total_weight: 100, active_weight: 100.0, voters: 3, keys: table.clone() };
|
||||
let v = verify(chain, &rcp, &cert, "carrier", &w).unwrap();
|
||||
assert_eq!(v.signers, 2);
|
||||
assert_eq!(v.signed_weight, 80);
|
||||
assert!((v.fraction_total - 0.8).abs() < 1e-9);
|
||||
// wrong chain id: the message differs
|
||||
assert!(verify("igneum-devnet-1", &rcp, &cert, "c", &w).is_err());
|
||||
// a flipped signature byte
|
||||
let mut bad = cert.clone();
|
||||
bad.signature[5] ^= 1;
|
||||
assert!(verify(chain, &rcp, &bad, "c", &w).unwrap_err().contains("aggregate signature"));
|
||||
// only the lightest key signs: 20 of 100, below 2/3
|
||||
let p = (0..3).find(|&p| table[p].blocks == 20).unwrap();
|
||||
let s = by_hash(table[p].key_hash).sign_vote(chain, 536, checkpoint);
|
||||
let thin = Certificate { bitmap: Certificate::bitmap_from_positions(3, &[p]), signature: aggregate_signatures(&[s]).unwrap(), ..cert.clone() };
|
||||
assert!(verify(chain, &rcp, &thin, "c", &w).unwrap_err().contains("below 2/3"));
|
||||
// a voter list that does not match the certificate's count
|
||||
let mut w2 = w.clone();
|
||||
w2.keys.pop();
|
||||
assert!(verify(chain, &rcp, &cert, "c", &w2).unwrap_err().contains("voters"));
|
||||
}
|
||||
}
|
||||
99
app/igneum-wallet/src/hd.rs
Normal file
99
app/igneum-wallet/src/hd.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
//! Keys from words: BIP-39 (24 English words, 256 bits of entropy from the OS) and BIP-32/44 at m/44'/60'/0'/0/0, the
|
||||
//! Ethereum path, so the same words open the same account in MetaMask. A raw private key import skips the words.
|
||||
|
||||
use bip32::{DerivationPath, XPrv};
|
||||
use bip39::{Language, Mnemonic};
|
||||
use igneum_common::keys;
|
||||
|
||||
pub const PATH: &str = "m/44'/60'/0'/0/0";
|
||||
|
||||
pub struct Derived {
|
||||
pub private_key: String, // 0x + 64 hex
|
||||
pub address: String, // 0x + 40 lower-case hex
|
||||
}
|
||||
|
||||
/// 24 new words from 256 bits of OS randomness.
|
||||
pub fn new_words() -> Result<String, String> {
|
||||
let mut entropy = [0u8; 32];
|
||||
getrandom::getrandom(&mut entropy).map_err(|e| e.to_string())?;
|
||||
let m = Mnemonic::from_entropy_in(Language::English, &entropy).map_err(|e| e.to_string())?;
|
||||
Ok(m.words().collect::<Vec<_>>().join(" "))
|
||||
}
|
||||
|
||||
/// Normalises a typed phrase: lower case, single spaces. Accepts 12, 15, 18, 21 or 24 words; checks the checksum.
|
||||
pub fn parse_words(text: &str) -> Result<String, String> {
|
||||
let words: Vec<String> = text.split_whitespace().map(|w| w.to_lowercase()).collect();
|
||||
if ![12, 15, 18, 21, 24].contains(&words.len()) {
|
||||
return Err(format!("{} words: a phrase has 12 or 24 words", words.len()));
|
||||
}
|
||||
let joined = words.join(" ");
|
||||
Mnemonic::parse_in_normalized(Language::English, &joined).map_err(|e| match e {
|
||||
bip39::Error::UnknownWord(i) => format!("word {} is not in the word list: {}", i + 1, words[i]),
|
||||
bip39::Error::InvalidChecksum => "the words do not check out (one is wrong or out of order)".to_string(),
|
||||
other => other.to_string(),
|
||||
})?;
|
||||
Ok(joined)
|
||||
}
|
||||
|
||||
/// The Ethereum account at m/44'/60'/0'/0/0 of a phrase (no BIP-39 passphrase).
|
||||
pub fn derive(words: &str) -> Result<Derived, String> {
|
||||
let m = Mnemonic::parse_in_normalized(Language::English, words).map_err(|e| e.to_string())?;
|
||||
let seed = m.to_seed("");
|
||||
let path: DerivationPath = PATH.parse().map_err(|_| "bad path".to_string())?;
|
||||
let xprv = XPrv::derive_from_path(seed, &path).map_err(|e| e.to_string())?;
|
||||
let raw: [u8; 32] = xprv.private_key().to_bytes().into();
|
||||
let address = keys::address_of_raw(&raw).ok_or("the derived key is invalid")?;
|
||||
Ok(Derived { private_key: format!("0x{}", keys::hex(&raw)), address })
|
||||
}
|
||||
|
||||
/// A raw private key, typed or pasted: 64 hex with or without 0x.
|
||||
pub fn parse_private_key(text: &str) -> Result<Derived, String> {
|
||||
let raw = keys::unhex(text).ok_or("a private key is 64 hex characters")?;
|
||||
if raw.len() != 32 {
|
||||
return Err(format!("a private key is 32 bytes, this is {}", raw.len()));
|
||||
}
|
||||
let arr: [u8; 32] = raw.try_into().unwrap();
|
||||
let address = keys::address_of_raw(&arr).ok_or("this is not a valid secp256k1 key")?;
|
||||
Ok(Derived { private_key: format!("0x{}", keys::hex(&arr)), address })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The reference phrase every Ethereum tool ships with (Hardhat account 0).
|
||||
#[test]
|
||||
fn hardhat_vector() {
|
||||
let d = derive("test test test test test test test test test test test junk").unwrap();
|
||||
assert_eq!(d.private_key, "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80");
|
||||
assert_eq!(keys::checksum(&d.address), "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
|
||||
}
|
||||
|
||||
/// BIP-39 vector 1 (entropy all zero): the words and, with the TREZOR passphrase, the published seed.
|
||||
#[test]
|
||||
fn bip39_vector_one() {
|
||||
let m = Mnemonic::from_entropy_in(Language::English, &[0u8; 16]).unwrap();
|
||||
assert_eq!(m.to_string(), "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about");
|
||||
let seed = m.to_seed("TREZOR");
|
||||
assert_eq!(keys::hex(&seed), "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn words_are_24_and_parse() {
|
||||
let w = new_words().unwrap();
|
||||
assert_eq!(w.split(' ').count(), 24);
|
||||
assert_eq!(parse_words(&w.to_uppercase()).unwrap(), w);
|
||||
let mut broken: Vec<&str> = w.split(' ').collect();
|
||||
broken[0] = "zzzz";
|
||||
assert!(parse_words(&broken.join(" ")).unwrap_err().contains("word 1"));
|
||||
assert!(parse_words("abandon abandon").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_key_import() {
|
||||
let d = parse_private_key("0000000000000000000000000000000000000000000000000000000000000001").unwrap();
|
||||
assert_eq!(keys::checksum(&d.address), "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf");
|
||||
assert!(parse_private_key("0x01").is_err());
|
||||
assert!(parse_private_key(&"ff".repeat(32)).is_err()); // above the curve order
|
||||
}
|
||||
}
|
||||
137
app/igneum-wallet/src/history.rs
Normal file
137
app/igneum-wallet/src/history.rs
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
//! What happened to this address: transfers in and out from the chain's blocks, block rewards from the execution
|
||||
//! records (igneum_getSegment: one reward per blue block, paid to the miner the block named), proving payouts when a
|
||||
//! segment carries a proof record (none on this node line yet; the label is ready). Scanned forward from the last
|
||||
//! block seen and kept in `<data>/wallet/history-<chain id>-<address>.json`.
|
||||
|
||||
use crate::evm::{q, Evm};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Entry {
|
||||
pub hash: String,
|
||||
/// sent | received | self | reward | proving
|
||||
pub kind: String,
|
||||
pub block: u64,
|
||||
pub block_hash: String,
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
/// wei, as a decimal string (u128 is wider than JSON numbers)
|
||||
pub value: String,
|
||||
#[serde(default)]
|
||||
pub fee: String,
|
||||
pub ok: bool,
|
||||
pub time: u64,
|
||||
/// pending | in_block | final | failed, with the checkpoint index when final
|
||||
#[serde(default)]
|
||||
pub finality: String,
|
||||
#[serde(default)]
|
||||
pub checkpoint: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub note: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub struct History {
|
||||
pub chain_id: u64,
|
||||
pub address: String,
|
||||
/// every block up to and including this one was read
|
||||
pub scanned_to: Option<u64>,
|
||||
pub entries: Vec<Entry>,
|
||||
}
|
||||
|
||||
impl History {
|
||||
pub fn load(path: &Path, chain_id: u64, address: &str) -> History {
|
||||
std::fs::read_to_string(path)
|
||||
.ok()
|
||||
.and_then(|t| serde_json::from_str::<History>(&t).ok())
|
||||
.filter(|h| h.chain_id == chain_id && h.address.eq_ignore_ascii_case(address))
|
||||
.unwrap_or(History { chain_id, address: address.to_ascii_lowercase(), scanned_to: None, entries: vec![] })
|
||||
}
|
||||
pub fn save(&self, path: &Path) {
|
||||
if let Some(d) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(d);
|
||||
}
|
||||
if let Ok(t) = serde_json::to_string(self) {
|
||||
let _ = std::fs::write(path, t);
|
||||
igneum_common::platform::lock_permissions(path, false);
|
||||
}
|
||||
}
|
||||
pub fn has(&self, hash: &str) -> bool {
|
||||
self.entries.iter().any(|e| e.hash.eq_ignore_ascii_case(hash))
|
||||
}
|
||||
/// Adds or replaces by hash, newest block first.
|
||||
pub fn put(&mut self, e: Entry) {
|
||||
self.entries.retain(|x| !x.hash.eq_ignore_ascii_case(&e.hash));
|
||||
self.entries.push(e);
|
||||
self.entries.sort_by(|a, b| b.block.cmp(&a.block).then(b.time.cmp(&a.time)));
|
||||
if self.entries.len() > 2000 {
|
||||
self.entries.truncate(2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn s(v: &Value, k: &str) -> String {
|
||||
v.get(k).and_then(|x| x.as_str()).unwrap_or("").to_string()
|
||||
}
|
||||
|
||||
/// Reads blocks `from..=to` and returns the entries that touch `me` (lower-case 0x address).
|
||||
pub fn scan(evm: &Evm, me: &str, from: u64, to: u64) -> Result<Vec<Entry>, String> {
|
||||
let mut out = Vec::new();
|
||||
for n in from..=to {
|
||||
let Some(b) = evm.block(n, true)? else { break };
|
||||
let bh = s(&b, "hash");
|
||||
let time = b.get("timestamp").and_then(q).unwrap_or(0) as u64;
|
||||
if let Some(txs) = b.get("transactions").and_then(|t| t.as_array()) {
|
||||
for t in txs {
|
||||
let from = s(t, "from").to_ascii_lowercase();
|
||||
let to = s(t, "to").to_ascii_lowercase();
|
||||
if from != me && to != me {
|
||||
continue;
|
||||
}
|
||||
let hash = s(t, "hash");
|
||||
let value = t.get("value").and_then(q).unwrap_or(0);
|
||||
let kind = if from == me && to == me { "self" } else if from == me { "sent" } else { "received" };
|
||||
// the receipt: status and the fee actually paid
|
||||
let (ok, fee) = match evm.receipt(&hash)? {
|
||||
Some(r) => {
|
||||
let ok = r.get("status").and_then(q).unwrap_or(1) == 1;
|
||||
let gas = r.get("gasUsed").and_then(q).unwrap_or(0);
|
||||
let price = r.get("effectiveGasPrice").and_then(q).unwrap_or(0);
|
||||
(ok, gas * price)
|
||||
}
|
||||
None => (true, 0),
|
||||
};
|
||||
out.push(Entry { hash, kind: kind.into(), block: n, block_hash: bh.clone(), from, to, value: value.to_string(), fee: fee.to_string(), ok, time, finality: "in_block".into(), checkpoint: None, note: String::new() });
|
||||
}
|
||||
}
|
||||
// rewards: the segment names every blue block's miner and what it was paid
|
||||
if let Some(seg) = evm.segment(n)? {
|
||||
if let Some(rs) = seg.get("rewards").and_then(|r| r.as_array()) {
|
||||
for (i, r) in rs.iter().enumerate() {
|
||||
let miner = s(r, "miner").to_ascii_lowercase();
|
||||
if miner != me {
|
||||
continue;
|
||||
}
|
||||
let wei = r.get("wei").and_then(q).unwrap_or(0);
|
||||
if wei == 0 {
|
||||
continue;
|
||||
}
|
||||
out.push(Entry { hash: format!("reward-{n}-{i}"), kind: "reward".into(), block: n, block_hash: bh.clone(), from: String::new(), to: me.to_string(), value: wei.to_string(), fee: "0".into(), ok: true, time, finality: "in_block".into(), checkpoint: None, note: "block reward".into() });
|
||||
}
|
||||
}
|
||||
if let Some(pr) = seg.get("proofRecord").filter(|v| !v.is_null()) {
|
||||
if let Some(ps) = pr.get("payouts").and_then(|p| p.as_array()) {
|
||||
for (i, p) in ps.iter().enumerate() {
|
||||
if s(p, "address").eq_ignore_ascii_case(me) {
|
||||
let wei = p.get("wei").and_then(q).unwrap_or(0);
|
||||
out.push(Entry { hash: format!("proving-{n}-{i}"), kind: "proving".into(), block: n, block_hash: bh.clone(), from: String::new(), to: me.to_string(), value: wei.to_string(), fee: "0".into(), ok: true, time, finality: "in_block".into(), checkpoint: None, note: "shard payout".into() });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
130
app/igneum-wallet/src/main.rs
Normal file
130
app/igneum-wallet/src/main.rs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
//! Igneum Wallet engine. Keeps the key, signs, reads a node, verifies finality certificates, and serves the window on
|
||||
//! 127.0.0.1:<random port>/t/<token>/. The window host (macOS: app/mac/IgneumWallet.swift, Windows: the WebView2
|
||||
//! host) starts it with --wrapper, reads `URL ...` and `STATE {...}` lines from its stdout and writes `quit` on its
|
||||
//! stdin. Without a host (--open) the window opens in the default browser. A host also reads one `HOST {...}` line:
|
||||
//! its own token (sent as X-Igneum-Host on the calls only it may make: the biometric confirmations) and the path of
|
||||
//! the sealed-password file it writes for Touch ID or Windows Hello.
|
||||
//!
|
||||
//! igneum-wallet [--wrapper | --open | --no-open | --launch] [--print-url]
|
||||
//!
|
||||
//! Environment (tests): IGNEUM_APP_DATA, IGNEUM_APP_LOGS, IGNEUM_APP_BIN, IGNEUM_WALLET_GRPC_PORT, IGNEUM_WALLET_EVM_PORT,
|
||||
//! IGNEUM_WALLET_NO_NODE, IGNEUM_WALLET_NETWORK, IGNEUM_WALLET_DEVNET_SUFFIX, IGNEUM_WALLET_PEERS,
|
||||
//! IGNEUM_WALLET_NODE_DIR, IGNEUM_WALLET_OVERRIDE_PARAMS, IGNEUM_WALLET_UPDATE_MANIFEST, IGNEUM_WALLET_IDLE_LOCK_S.
|
||||
#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")]
|
||||
|
||||
mod bridge;
|
||||
mod eip712;
|
||||
mod engine;
|
||||
mod evm;
|
||||
mod finality;
|
||||
mod hd;
|
||||
mod history;
|
||||
mod node;
|
||||
mod qr;
|
||||
mod server;
|
||||
mod state;
|
||||
mod tx;
|
||||
mod updater;
|
||||
mod vault;
|
||||
|
||||
use igneum_common::platform;
|
||||
use std::io::{BufRead, Write};
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let wrapper = args.iter().any(|a| a == "--wrapper");
|
||||
let no_open = wrapper || args.iter().any(|a| a == "--no-open");
|
||||
if args.iter().any(|a| a == "--version" || a == "-V") {
|
||||
println!("igneum-wallet {}", engine::VERSION);
|
||||
return;
|
||||
}
|
||||
if args.iter().any(|a| a == "--launch") {
|
||||
if let Some(dir) = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_path_buf())) {
|
||||
let host = dir.join(engine::APP.host_exe);
|
||||
if host.exists() {
|
||||
let mut c = std::process::Command::new(&host);
|
||||
c.current_dir(&dir);
|
||||
if c.spawn().is_ok() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
platform::clear_quarantine();
|
||||
let root = platform::data_root();
|
||||
let app_dir = root.join(engine::APP.data_sub);
|
||||
let log_dir = platform::log_root();
|
||||
let _ = std::fs::create_dir_all(&app_dir);
|
||||
let _ = std::fs::create_dir_all(&log_dir);
|
||||
platform::lock_permissions(&app_dir, true);
|
||||
let paths = engine::Paths {
|
||||
vault: app_dir.join("vault.json"),
|
||||
settings: app_dir.join("settings.json"),
|
||||
miner_wallet: root.join(igneum_common::MINER.data_sub).join("wallet.json"),
|
||||
biometric: app_dir.join("biometric.json"),
|
||||
app_dir: app_dir.clone(),
|
||||
log_dir: log_dir.clone(),
|
||||
};
|
||||
let packaged = igneum_common::config::Packaged::load(&igneum_common::config::Packaged::candidates("igneum-wallet.json"));
|
||||
let settings = engine::Settings::load(&paths.settings);
|
||||
let machine_id = igneum_common::config::machine_id(&app_dir);
|
||||
let token = igneum_common::http::new_token();
|
||||
let host_token = igneum_common::http::new_token();
|
||||
let stamp_file = log_dir.join(format!("wallet-{}.log", stamp_now()));
|
||||
let engine_log = std::fs::File::create(&stamp_file).ok();
|
||||
let (tx, rx) = channel();
|
||||
let biometric_file = paths.biometric.clone();
|
||||
let shared = Arc::new(engine::Shared::new(token.clone(), host_token.clone(), paths, packaged, settings, machine_id, tx, engine_log, stamp_file.clone()));
|
||||
let port = match server::start(shared.clone()) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("cannot listen on 127.0.0.1: {e}");
|
||||
if wrapper {
|
||||
println!("FATAL cannot listen on 127.0.0.1: {e}");
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
server::start_bridge(shared.clone());
|
||||
let url = format!("http://127.0.0.1:{port}/t/{token}/");
|
||||
let url_file = shared.paths.app_dir.join("wallet.url");
|
||||
let _ = std::fs::write(&url_file, &url);
|
||||
platform::lock_permissions(&url_file, false);
|
||||
shared.log(&format!("window listening on 127.0.0.1:{port} (the URL with its token is in wallet.url; log {})", stamp_file.display()));
|
||||
if wrapper || args.iter().any(|a| a == "--print-url") {
|
||||
println!("URL {url}");
|
||||
if wrapper {
|
||||
// the host alone reads stdout: its token and where the sealed password goes
|
||||
println!("HOST {}", serde_json::json!({ "token": host_token, "biometric_file": biometric_file.display().to_string() }));
|
||||
}
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
if !no_open {
|
||||
platform::open_url(&url);
|
||||
}
|
||||
{
|
||||
let shared = shared.clone();
|
||||
std::thread::spawn(move || {
|
||||
let stdin = std::io::stdin();
|
||||
for line in stdin.lock().lines() {
|
||||
let Ok(l) = line else { break };
|
||||
match l.trim() {
|
||||
"quit" => shared.send(engine::Cmd::Quit),
|
||||
"lock" => shared.lock(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if wrapper {
|
||||
shared.send(engine::Cmd::Quit);
|
||||
}
|
||||
});
|
||||
}
|
||||
engine::Engine::new(shared, rx, wrapper).run();
|
||||
}
|
||||
|
||||
fn stamp_now() -> String {
|
||||
let t = platform::unix_now();
|
||||
format!("{}-{:02}{:02}{:02}", t / 86400, t % 86400 / 3600, t % 3600 / 60, t % 60)
|
||||
}
|
||||
223
app/igneum-wallet/src/node.rs
Normal file
223
app/igneum-wallet/src/node.rs
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
//! Where the chain comes from, in this order:
|
||||
//! 1. the miner app's node on this machine (gRPC 127.0.0.1:26610, Ethereum RPC 26790): used as it is, nothing started
|
||||
//! 2. the seed's public Ethereum RPC when the package names one (`public_rpc` in igneum-wallet.json): balances,
|
||||
//! sending and history work; finality verification needs a node's gRPC and is reported as "no node", so
|
||||
//! transactions stay "in a block" until a node is there
|
||||
//! 3. the bundled igneumd next to the app, started by the wallet on its own ports (gRPC 26620, Ethereum 26800, p2p
|
||||
//! 26621) with the same arguments the miner uses, no mining, stopped when the wallet quits
|
||||
//! The choice is made at start and whenever the source goes away; `State.node.source` says which.
|
||||
//! Environment (tests): IGNEUM_WALLET_GRPC_PORT, IGNEUM_WALLET_EVM_PORT (an external node to use, no probe of the
|
||||
//! miner's ports), IGNEUM_WALLET_NO_NODE=1 (never start one), IGNEUM_WALLET_NETWORK, IGNEUM_WALLET_DEVNET_SUFFIX,
|
||||
//! IGNEUM_WALLET_PEERS, IGNEUM_WALLET_NODE_DIR, IGNEUM_WALLET_OVERRIDE_PARAMS (a file for --override-params-file).
|
||||
|
||||
use kaspa_grpc_client::GrpcClient;
|
||||
use kaspa_rpc_core::api::rpc::RpcApi;
|
||||
use kaspa_rpc_core::error::RpcError;
|
||||
use kaspa_rpc_core::model::{GetBlockDagInfoResponse, GetFinalityCheckpointsResponse, GetFinalityWeightsResponse, RpcBlock, RpcHash};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
pub const MINER_GRPC: u16 = 26610;
|
||||
pub const MINER_EVM: u16 = 26790;
|
||||
pub const OWN_GRPC: u16 = 26620;
|
||||
pub const OWN_EVM: u16 = 26800;
|
||||
pub const OWN_P2P: u16 = 26621;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Source {
|
||||
/// the miner app's node on this machine
|
||||
Miner { grpc: u16, evm: u16 },
|
||||
/// a node named by the environment (tests)
|
||||
External { grpc: u16, evm: u16 },
|
||||
/// the seed's public Ethereum RPC; no gRPC, so no certificate verification
|
||||
Public { url: String },
|
||||
/// the bundled node, started by the wallet
|
||||
Own { grpc: u16, evm: u16 },
|
||||
None,
|
||||
}
|
||||
|
||||
impl Source {
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Source::Miner { .. } => "miner",
|
||||
Source::External { .. } => "external",
|
||||
Source::Public { .. } => "public",
|
||||
Source::Own { .. } => "own",
|
||||
Source::None => "none",
|
||||
}
|
||||
}
|
||||
pub fn grpc_port(&self) -> Option<u16> {
|
||||
match self {
|
||||
Source::Miner { grpc, .. } | Source::External { grpc, .. } | Source::Own { grpc, .. } => Some(*grpc),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn evm(&self) -> Option<crate::evm::Evm> {
|
||||
match self {
|
||||
Source::Miner { evm, .. } | Source::External { evm, .. } | Source::Own { evm, .. } => Some(crate::evm::Evm::local(*evm)),
|
||||
Source::Public { url } => Some(crate::evm::Evm { endpoint: url.clone() }),
|
||||
Source::None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A node's Ethereum RPC answers eth_chainId on this port.
|
||||
pub fn evm_alive(port: u16) -> bool {
|
||||
crate::evm::Evm::local(port).chain_id().is_ok()
|
||||
}
|
||||
|
||||
/// The environment's external node, when set.
|
||||
pub fn external_from_env() -> Option<Source> {
|
||||
let env = |k: &str| std::env::var(k).ok().filter(|v| !v.is_empty());
|
||||
let grpc = env("IGNEUM_WALLET_GRPC_PORT")?.parse().ok()?;
|
||||
let evm = env("IGNEUM_WALLET_EVM_PORT")?.parse().ok()?;
|
||||
Some(Source::External { grpc, evm })
|
||||
}
|
||||
|
||||
// ---- gRPC, the node's own client, on a private tokio runtime -----------------------------------------------------
|
||||
|
||||
pub struct Grpc {
|
||||
rt: Runtime,
|
||||
client: Arc<GrpcClient>,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl Grpc {
|
||||
pub fn connect(port: u16) -> Result<Grpc, String> {
|
||||
let rt = tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build().map_err(|e| e.to_string())?;
|
||||
let url = format!("grpc://127.0.0.1:{port}");
|
||||
let client = rt.block_on(async {
|
||||
tokio::time::timeout(Duration::from_secs(8), GrpcClient::connect(url.clone())).await.map_err(|_| "gRPC connect timed out".to_string())?.map_err(|e| e.to_string())
|
||||
})?;
|
||||
Ok(Grpc { rt, client: Arc::new(client), url })
|
||||
}
|
||||
fn run<F, T>(&self, f: F) -> Result<T, String>
|
||||
where
|
||||
F: std::future::Future<Output = Result<T, RpcError>>,
|
||||
{
|
||||
self.rt.block_on(async { tokio::time::timeout(Duration::from_secs(20), f).await.map_err(|_| "rpc timed out".to_string())?.map_err(|e| e.to_string()) })
|
||||
}
|
||||
pub fn checkpoints(&self, last: u32) -> Result<GetFinalityCheckpointsResponse, String> {
|
||||
self.run(self.client.get_finality_checkpoints(last))
|
||||
}
|
||||
pub fn weights(&self) -> Result<GetFinalityWeightsResponse, String> {
|
||||
self.run(self.client.get_finality_weights())
|
||||
}
|
||||
/// Blocks from `low` onward (the node's own page size), with transactions.
|
||||
pub fn blocks_after(&self, low: RpcHash) -> Result<Vec<RpcBlock>, String> {
|
||||
let r = self.run(self.client.get_blocks(Some(low), true, true))?;
|
||||
Ok(r.blocks)
|
||||
}
|
||||
pub fn dag_info(&self) -> Result<GetBlockDagInfoResponse, String> {
|
||||
self.run(self.client.get_block_dag_info())
|
||||
}
|
||||
pub fn disconnect(&self) {
|
||||
let c = self.client.clone();
|
||||
let _ = self.rt.block_on(async { c.disconnect().await });
|
||||
}
|
||||
}
|
||||
|
||||
// ---- the bundled node ---------------------------------------------------------------------------------------------
|
||||
|
||||
pub struct OwnNode {
|
||||
pub child: Child,
|
||||
}
|
||||
|
||||
pub struct OwnNodePlan {
|
||||
pub bin: PathBuf,
|
||||
pub args: Vec<String>,
|
||||
pub dir: PathBuf,
|
||||
pub log: PathBuf,
|
||||
}
|
||||
|
||||
/// Finds igneumd next to the engine (Windows), in bin/, or in Contents/Resources/bin (macOS bundle).
|
||||
pub fn find_igneumd() -> Option<PathBuf> {
|
||||
let exe = std::env::current_exe().ok()?;
|
||||
let here = exe.parent()?.to_path_buf();
|
||||
let mut candidates = vec![];
|
||||
if let Some(d) = std::env::var_os("IGNEUM_APP_BIN") {
|
||||
candidates.push(PathBuf::from(d));
|
||||
}
|
||||
candidates.push(here.clone());
|
||||
candidates.push(here.join("bin"));
|
||||
if let Some(c) = here.parent() {
|
||||
candidates.push(c.join("Resources").join("bin"));
|
||||
}
|
||||
let name = if cfg!(windows) { "igneumd.exe" } else { "igneumd" };
|
||||
candidates.into_iter().map(|d| d.join(name)).find(|p| p.is_file())
|
||||
}
|
||||
|
||||
pub fn plan_own_node(app_dir: &std::path::Path, log_dir: &std::path::Path, packaged: &igneum_common::config::Packaged) -> Result<OwnNodePlan, String> {
|
||||
let env = |k: &str| std::env::var(k).ok().filter(|v| !v.is_empty());
|
||||
let bin = find_igneumd().ok_or("igneumd is not next to the wallet (the package is incomplete)")?;
|
||||
let network = env("IGNEUM_WALLET_NETWORK").unwrap_or_else(|| "devnet".into());
|
||||
let suffix: Option<u32> = env("IGNEUM_WALLET_DEVNET_SUFFIX").and_then(|v| v.parse().ok());
|
||||
let root = igneum_common::platform::data_root();
|
||||
let dir = match env("IGNEUM_WALLET_NODE_DIR") {
|
||||
Some(d) => PathBuf::from(d),
|
||||
None => root.join(match suffix {
|
||||
Some(n) => format!("wallet-node-devnet-{n}"),
|
||||
None => format!("wallet-node-{network}"),
|
||||
}),
|
||||
};
|
||||
let peers: Vec<String> = match std::env::var("IGNEUM_WALLET_PEERS") {
|
||||
Ok(v) => v.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(),
|
||||
Err(_) if network == "devnet" && suffix.is_none() => vec!["188.245.5.161:26611".into()],
|
||||
Err(_) => vec![],
|
||||
};
|
||||
let mut args = vec![
|
||||
format!("--{network}"),
|
||||
format!("--appdir={}", dir.display()),
|
||||
format!("--rpclisten=127.0.0.1:{OWN_GRPC}"),
|
||||
format!("--evm-rpclisten=127.0.0.1:{OWN_EVM}"),
|
||||
format!("--listen=0.0.0.0:{OWN_P2P}"),
|
||||
];
|
||||
if let Some(n) = suffix {
|
||||
args.push(format!("--devnet-suffix={n}"));
|
||||
}
|
||||
for p in &peers {
|
||||
args.push(format!("--addpeer={p}"));
|
||||
}
|
||||
// the packager's consensus pin, as the miner does it (igneum-wallet.json node_override_params)
|
||||
if let Some(file) = env("IGNEUM_WALLET_OVERRIDE_PARAMS") {
|
||||
args.push(format!("--override-params-file={file}"));
|
||||
} else if let Some(v) = packaged.node_override_params.as_ref().filter(|v| v.as_object().map(|o| !o.is_empty()).unwrap_or(false)) {
|
||||
let path = app_dir.join("override-params.json");
|
||||
std::fs::write(&path, v.to_string()).map_err(|e| e.to_string())?;
|
||||
args.push(format!("--override-params-file={}", path.display()));
|
||||
}
|
||||
args.extend(["--nodnsseed", "--disable-upnp", "--nologfiles", "--yes"].iter().map(|s| s.to_string()));
|
||||
let log = log_dir.join("wallet-node.log");
|
||||
Ok(OwnNodePlan { bin, args, dir, log })
|
||||
}
|
||||
|
||||
pub fn start_own_node(plan: &OwnNodePlan) -> Result<OwnNode, String> {
|
||||
let _ = std::fs::create_dir_all(&plan.dir);
|
||||
let out = std::fs::OpenOptions::new().create(true).append(true).open(&plan.log).map_err(|e| e.to_string())?;
|
||||
let err = out.try_clone().map_err(|e| e.to_string())?;
|
||||
let mut c = Command::new(&plan.bin);
|
||||
c.args(&plan.args).stdin(Stdio::null()).stdout(Stdio::from(out)).stderr(Stdio::from(err));
|
||||
igneum_common::platform::quiet(&mut c);
|
||||
let child = c.spawn().map_err(|e| format!("igneumd did not start: {e}"))?;
|
||||
Ok(OwnNode { child })
|
||||
}
|
||||
|
||||
impl OwnNode {
|
||||
pub fn stop(&mut self) {
|
||||
igneum_common::platform::terminate(&mut self.child);
|
||||
for _ in 0..300 {
|
||||
if let Ok(Some(_)) = self.child.try_wait() {
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
let _ = self.child.kill();
|
||||
let _ = self.child.wait();
|
||||
}
|
||||
pub fn alive(&mut self) -> bool {
|
||||
matches!(self.child.try_wait(), Ok(None))
|
||||
}
|
||||
}
|
||||
31
app/igneum-wallet/src/qr.rs
Normal file
31
app/igneum-wallet/src/qr.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
//! The receive QR code, drawn here (no image service, no library call across the network): qrcodegen to an SVG.
|
||||
|
||||
use qrcodegen::{QrCode, QrCodeEcc};
|
||||
|
||||
pub fn svg(text: &str) -> Result<String, String> {
|
||||
let qr = QrCode::encode_text(text, QrCodeEcc::Medium).map_err(|e| format!("{e:?}"))?;
|
||||
let n = qr.size();
|
||||
let border = 2;
|
||||
let dim = n + 2 * border;
|
||||
let mut path = String::new();
|
||||
for y in 0..n {
|
||||
for x in 0..n {
|
||||
if qr.get_module(x, y) {
|
||||
path.push_str(&format!("M{} {}h1v1h-1z", x + border, y + border));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(format!(
|
||||
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {dim} {dim}\" shape-rendering=\"crispEdges\" role=\"img\" aria-label=\"QR code\"><rect width=\"{dim}\" height=\"{dim}\" fill=\"#F4F1EC\"/><path d=\"{path}\" fill=\"#0C0C0E\"/></svg>"
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn draws() {
|
||||
let s = super::svg("ethereum:0x7e5f4552091a69125d5dfcb7b8c2659029395bdf").unwrap();
|
||||
assert!(s.starts_with("<svg"));
|
||||
assert!(s.contains("<path d=\"M"));
|
||||
}
|
||||
}
|
||||
288
app/igneum-wallet/src/server.rs
Normal file
288
app/igneum-wallet/src/server.rs
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
//! The local web server: the window's files from the binary and the JSON API, on 127.0.0.1 with a random port, every
|
||||
//! path under `/t/<token>/` (igneum_common::http). Mutating calls must come from the window itself (same origin).
|
||||
|
||||
use crate::engine::{Cmd, Shared};
|
||||
use igneum_common::http::{from_dashboard, json_resp, query_param, read_request, respond, respond_with};
|
||||
use serde_json::{json, Value};
|
||||
use std::net::TcpStream;
|
||||
use std::sync::Arc;
|
||||
|
||||
const INDEX: &str = include_str!("../ui/index.html");
|
||||
const CSS: &str = include_str!("../ui/app.css");
|
||||
const JS: &str = include_str!("../ui/app.js");
|
||||
const CARD_JS: &str = include_str!("../ui/update-card.js");
|
||||
const LOCK_JS: &str = include_str!("../ui/lock-screen.js");
|
||||
const MARK: &str = include_str!("../ui/mark.svg");
|
||||
const COIN: &[u8] = include_bytes!("../../../brand/igneum-coin-1024.png");
|
||||
const FONT_MONO_400: &[u8] = include_bytes!("../../igneum-app/ui/fonts/IBMPlexMono-400.woff2");
|
||||
const FONT_MONO_500: &[u8] = include_bytes!("../../igneum-app/ui/fonts/IBMPlexMono-500.woff2");
|
||||
const FONT_SANS_400: &[u8] = include_bytes!("../../igneum-app/ui/fonts/IBMPlexSans-400.woff2");
|
||||
const FONT_SANS_500: &[u8] = include_bytes!("../../igneum-app/ui/fonts/IBMPlexSans-500.woff2");
|
||||
const FONT_SANS_600: &[u8] = include_bytes!("../../igneum-app/ui/fonts/IBMPlexSans-600.woff2");
|
||||
const FONT_UNB_500: &[u8] = include_bytes!("../../igneum-app/ui/fonts/Unbounded-500.woff2");
|
||||
const FONT_UNB_700: &[u8] = include_bytes!("../../igneum-app/ui/fonts/Unbounded-700.woff2");
|
||||
const FONT_UNB_900: &[u8] = include_bytes!("../../igneum-app/ui/fonts/Unbounded-900.woff2");
|
||||
|
||||
pub fn start(shared: Arc<Shared>) -> std::io::Result<u16> {
|
||||
let s = shared.clone();
|
||||
let port = igneum_common::http::serve(move |stream| handle(stream, s.clone()))?;
|
||||
shared.set_port(port);
|
||||
Ok(port)
|
||||
}
|
||||
|
||||
/// The page bridge's listener on its fixed port (src/bridge.rs): GET /bridge/hello (the page's probe), POST /bridge/rpc
|
||||
/// (JSON-RPC with the page's Origin and the X-Igneum-Bridge header), and the CORS preflight that must come first.
|
||||
/// Nothing under /t/<token>/ is served here, and nothing here reads the window's token.
|
||||
pub fn start_bridge(shared: Arc<Shared>) {
|
||||
let port = std::env::var("IGNEUM_WALLET_BRIDGE_PORT").ok().and_then(|v| v.parse().ok()).unwrap_or(crate::bridge::BRIDGE_PORT);
|
||||
let s = shared.clone();
|
||||
match igneum_common::http::serve_on(port, move |stream| handle_bridge(stream, s.clone())) {
|
||||
Ok(()) => {
|
||||
*shared.bridge_listening.lock().unwrap() = (true, port, String::new());
|
||||
shared.log(&format!("page bridge listening on 127.0.0.1:{port} (websites connect here after your approval in the window)"));
|
||||
}
|
||||
Err(e) => {
|
||||
*shared.bridge_listening.lock().unwrap() = (false, port, format!("port {port} is not free: {e}"));
|
||||
shared.log(&format!("page bridge not listening: port {port} is not free ({e}); websites cannot connect until the wallet restarts with the port free"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn bridge_origin_ok(o: &str) -> bool {
|
||||
o.starts_with("https://") || o.starts_with("http://localhost") || o.starts_with("http://127.0.0.1")
|
||||
}
|
||||
|
||||
fn handle_bridge(mut stream: TcpStream, shared: Arc<Shared>) {
|
||||
let Some(req) = read_request(&mut stream) else { return };
|
||||
let origin = req.origin.clone().unwrap_or_default();
|
||||
let cors: Vec<(&str, &str)> = if bridge_origin_ok(&origin) {
|
||||
vec![("Access-Control-Allow-Origin", origin.as_str()), ("Vary", "Origin"), ("Access-Control-Allow-Methods", "GET, POST, OPTIONS"), ("Access-Control-Allow-Headers", "content-type, x-igneum-bridge"), ("Access-Control-Allow-Private-Network", "true"), ("Access-Control-Max-Age", "600")]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
match (req.method.as_str(), req.path.as_str()) {
|
||||
("OPTIONS", _) => respond_with(&mut stream, 204, "text/plain", b"", &cors),
|
||||
("GET", "/bridge/hello") => {
|
||||
// the probe: the wallet is here, its version and chain; never an address
|
||||
let v = json!({ "ok": true, "wallet": "igneum", "version": crate::engine::VERSION, "chain_id": shared.network_json()["chain_id"], "chain_id_hex": shared.network_json()["chain_id_hex"], "bridge": 1 });
|
||||
respond_with(&mut stream, 200, "application/json; charset=utf-8", v.to_string().as_bytes(), &cors)
|
||||
}
|
||||
("POST", "/bridge/rpc") => {
|
||||
if origin.is_empty() || !bridge_origin_ok(&origin) || !req.bridge_header {
|
||||
respond_with(&mut stream, 403, "application/json; charset=utf-8", json!({ "error": { "code": crate::bridge::E_UNAUTHORIZED, "message": "a page bridge call carries its origin and the X-Igneum-Bridge header" } }).to_string().as_bytes(), &cors);
|
||||
return;
|
||||
}
|
||||
let body: Value = serde_json::from_slice(&req.body).unwrap_or(json!({}));
|
||||
let id = body.get("id").cloned().unwrap_or(Value::Null);
|
||||
let method = body.get("method").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let params = body.get("params").cloned().unwrap_or(json!([]));
|
||||
let mut out = shared.bridge_request(&origin, &method, ¶ms);
|
||||
out["id"] = id;
|
||||
out["jsonrpc"] = json!("2.0");
|
||||
respond_with(&mut stream, 200, "application/json; charset=utf-8", out.to_string().as_bytes(), &cors)
|
||||
}
|
||||
_ => respond_with(&mut stream, 404, "text/plain", b"Igneum Wallet page bridge", &cors),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle(mut stream: TcpStream, shared: Arc<Shared>) {
|
||||
let Some(req) = read_request(&mut stream) else { return };
|
||||
if req.path == "/" {
|
||||
respond(&mut stream, 404, "text/plain", b"Igneum Wallet: open the app window.", false);
|
||||
return;
|
||||
}
|
||||
let prefix = format!("/t/{}", shared.token);
|
||||
let Some(rest) = req.path.strip_prefix(&prefix) else {
|
||||
respond(&mut stream, 404, "text/plain", b"not found", false);
|
||||
return;
|
||||
};
|
||||
let rest = if rest.is_empty() { "/" } else { rest };
|
||||
match (req.method.as_str(), rest) {
|
||||
("GET", "/") | ("GET", "/index.html") => respond(&mut stream, 200, "text/html; charset=utf-8", INDEX.as_bytes(), false),
|
||||
("GET", "/app.css") => respond(&mut stream, 200, "text/css; charset=utf-8", CSS.as_bytes(), false),
|
||||
("GET", "/app.js") => respond(&mut stream, 200, "application/javascript; charset=utf-8", JS.as_bytes(), false),
|
||||
("GET", "/update-card.js") => respond(&mut stream, 200, "application/javascript; charset=utf-8", CARD_JS.as_bytes(), false),
|
||||
("GET", "/lock-screen.js") => respond(&mut stream, 200, "application/javascript; charset=utf-8", LOCK_JS.as_bytes(), false),
|
||||
("GET", "/mark.svg") => respond(&mut stream, 200, "image/svg+xml", MARK.as_bytes(), true),
|
||||
("GET", "/coin.png") => respond(&mut stream, 200, "image/png", COIN, true),
|
||||
("GET", "/fonts/IBMPlexMono-400.woff2") => respond(&mut stream, 200, "font/woff2", FONT_MONO_400, true),
|
||||
("GET", "/fonts/IBMPlexMono-500.woff2") => respond(&mut stream, 200, "font/woff2", FONT_MONO_500, true),
|
||||
("GET", "/fonts/IBMPlexSans-400.woff2") => respond(&mut stream, 200, "font/woff2", FONT_SANS_400, true),
|
||||
("GET", "/fonts/IBMPlexSans-500.woff2") => respond(&mut stream, 200, "font/woff2", FONT_SANS_500, true),
|
||||
("GET", "/fonts/IBMPlexSans-600.woff2") => respond(&mut stream, 200, "font/woff2", FONT_SANS_600, true),
|
||||
("GET", "/fonts/Unbounded-500.woff2") => respond(&mut stream, 200, "font/woff2", FONT_UNB_500, true),
|
||||
("GET", "/fonts/Unbounded-700.woff2") => respond(&mut stream, 200, "font/woff2", FONT_UNB_700, true),
|
||||
("GET", "/fonts/Unbounded-900.woff2") => respond(&mut stream, 200, "font/woff2", FONT_UNB_900, true),
|
||||
("GET", "/api/state") => json_resp(&mut stream, 200, shared.state_json()),
|
||||
("GET", "/api/network") => json_resp(&mut stream, 200, shared.network_json()),
|
||||
("GET", "/api/log") => {
|
||||
let after = query_param(&req.query, "after").and_then(|s| s.parse().ok()).unwrap_or(0u64);
|
||||
let limit = query_param(&req.query, "limit").and_then(|s| s.parse().ok()).unwrap_or(400usize).min(2000);
|
||||
let lines = shared.rings.lock().unwrap().since(after, limit);
|
||||
json_resp(&mut stream, 200, json!({ "lines": lines }));
|
||||
}
|
||||
// the host reads a challenge's reason with its token (the page never needs this: it has the reason already)
|
||||
("GET", "/api/biometric/challenge") => {
|
||||
if !shared.host_ok(req.host_token.as_deref()) {
|
||||
json_resp(&mut stream, 403, json!({ "ok": false, "error": "host token" }));
|
||||
return;
|
||||
}
|
||||
let nonce = query_param(&req.query, "nonce").unwrap_or_default();
|
||||
match shared.challenge_reason(&nonce) {
|
||||
Ok(v) => json_resp(&mut stream, 200, v),
|
||||
Err(e) => json_resp(&mut stream, 400, json!({ "ok": false, "error": e })),
|
||||
}
|
||||
}
|
||||
("GET", p) if p.starts_with("/api/tx/") => {
|
||||
let hash = p.trim_start_matches("/api/tx/").to_string();
|
||||
match shared.tx_detail(&hash) {
|
||||
Ok(v) => json_resp(&mut stream, 200, v),
|
||||
Err(e) => json_resp(&mut stream, 400, json!({ "ok": false, "error": e })),
|
||||
}
|
||||
}
|
||||
("POST", p) => {
|
||||
if !from_dashboard(&req, shared.port()) {
|
||||
json_resp(&mut stream, 403, json!({ "ok": false, "error": "not from the window" }));
|
||||
return;
|
||||
}
|
||||
let body: Value = if req.body.is_empty() { json!({}) } else { serde_json::from_slice(&req.body).unwrap_or(json!({})) };
|
||||
let from_host = shared.host_ok(req.host_token.as_deref());
|
||||
if HOST_ONLY.contains(&p) && !from_host {
|
||||
json_resp(&mut stream, 403, json!({ "ok": false, "error": "only the window host may call this" }));
|
||||
return;
|
||||
}
|
||||
match api_post(&shared, p, body, from_host) {
|
||||
Ok(v) => json_resp(&mut stream, 200, v),
|
||||
Err(e) => json_resp(&mut stream, 400, json!({ "ok": false, "error": e })),
|
||||
}
|
||||
}
|
||||
_ => respond(&mut stream, 404, "text/plain", b"not found", false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls that carry a biometric result or take the parked password: the host's token (X-Igneum-Host) is required,
|
||||
/// so the page cannot confirm its own challenges.
|
||||
const HOST_ONLY: &[&str] = &["/api/biometric/status", "/api/biometric/report", "/api/biometric/confirm", "/api/biometric/enrol/take", "/api/biometric/enrolled"];
|
||||
|
||||
fn api_post(shared: &Arc<Shared>, path: &str, body: Value, from_host: bool) -> Result<Value, String> {
|
||||
let s = |k: &str| body.get(k).and_then(|v| v.as_str()).map(|v| v.to_string());
|
||||
let b = |k: &str| body.get(k).and_then(|v| v.as_bool());
|
||||
match path {
|
||||
"/api/create" => shared.create_begin(&s("password").ok_or("password missing")?),
|
||||
"/api/create/confirm" => {
|
||||
let list = body.get("checks").and_then(|v| v.as_array()).ok_or("checks missing")?;
|
||||
let checks: Vec<(usize, String)> = list.iter().filter_map(|c| Some((c.get("position")?.as_u64()? as usize, c.get("word")?.as_str()?.to_string()))).collect();
|
||||
shared.create_confirm(&checks)
|
||||
}
|
||||
"/api/import" => shared.import(&s("mode").unwrap_or_default(), &s("data").unwrap_or_default(), &s("password").ok_or("password missing")?),
|
||||
"/api/unlock" => shared.unlock(&s("password").ok_or("password missing")?, from_host),
|
||||
"/api/lock" => {
|
||||
shared.lock();
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
"/api/reveal" => match s("nonce") {
|
||||
Some(n) => shared.reveal_confirmed(&n),
|
||||
None => shared.reveal(&s("password").ok_or("password missing")?),
|
||||
},
|
||||
"/api/backed-up" => shared.mark_backed_up(),
|
||||
"/api/password" => shared.change_password(&s("old").ok_or("old missing")?, &s("new").ok_or("new missing")?),
|
||||
"/api/remove" => shared.remove(&s("password").ok_or("password missing")?),
|
||||
"/api/receive" => {
|
||||
let address = shared.address();
|
||||
if address.is_empty() {
|
||||
return Err("no wallet".into());
|
||||
}
|
||||
let svg = crate::qr::svg(&format!("ethereum:{}", igneum_common::keys::checksum(&address)))?;
|
||||
Ok(json!({ "ok": true, "address": address, "display": igneum_common::keys::checksum(&address), "svg": svg }))
|
||||
}
|
||||
"/api/send/quote" => {
|
||||
let q = shared.quote(&s("to").unwrap_or_default(), &s("amount").unwrap_or_default())?;
|
||||
let mut v = serde_json::to_value(&q).map_err(|e| e.to_string())?;
|
||||
v["ok"] = json!(true);
|
||||
v["value_ign"] = json!(crate::evm::ign(q.value.parse().unwrap_or(0), 18));
|
||||
v["fee_max_ign"] = json!(crate::evm::ign(q.fee_max.parse().unwrap_or(0), 9));
|
||||
v["total_max_ign"] = json!(crate::evm::ign(q.total_max.parse().unwrap_or(0), 9));
|
||||
v["base_fee_gwei"] = json!(crate::evm::ign(q.base_fee.parse::<u128>().unwrap_or(0) * 1_000_000_000, 3));
|
||||
v["tip_gwei"] = json!(crate::evm::ign(q.tip.parse::<u128>().unwrap_or(0) * 1_000_000_000, 3));
|
||||
v["display_to"] = json!(igneum_common::keys::checksum(&q.to));
|
||||
// the biometric challenge for this quote: the prompt's line and the nonce the host confirms
|
||||
let (nonce, reason) = shared.challenge_for_send(&q, &crate::evm::ign(q.value.parse().unwrap_or(0), 4));
|
||||
v["confirm_nonce"] = json!(nonce);
|
||||
v["confirm_reason"] = json!(reason);
|
||||
v["confirm_needed"] = json!(shared.enrolled());
|
||||
Ok(v)
|
||||
}
|
||||
"/api/send" => {
|
||||
let q: crate::engine::Quote = serde_json::from_value(body.get("quote").cloned().ok_or("quote missing")?).map_err(|e| format!("quote: {e}"))?;
|
||||
shared.send_tx(&q, s("nonce").as_deref())
|
||||
}
|
||||
"/api/settings" => {
|
||||
if let Some(on) = b("start_at_login") {
|
||||
shared.set_start_at_login(on)?;
|
||||
}
|
||||
if let Some(on) = b("idle_lock") {
|
||||
shared.set_idle_lock(on)?;
|
||||
}
|
||||
if let Some(min) = body.get("idle_lock_min").and_then(|v| v.as_u64()) {
|
||||
shared.set_idle_lock_min(min)?;
|
||||
}
|
||||
if let Some(on) = b("ask_on_open") {
|
||||
shared.set_ask_on_open(on)?;
|
||||
}
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
// ---- Touch ID / Windows Hello: the page's side and the host's side (HOST_ONLY) ----
|
||||
"/api/activity" => {
|
||||
shared.touch_activity();
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
"/api/biometric/challenge" => shared.challenge(&s("purpose").unwrap_or_default()),
|
||||
"/api/biometric/enrol/begin" => shared.enrol_begin(&s("password").ok_or("password missing")?),
|
||||
"/api/biometric/enrol/cancel" => shared.enrol_cancel(),
|
||||
"/api/biometric/remove" => shared.biometric_remove(),
|
||||
"/api/biometric/status" => shared.biometric_status(b("available").unwrap_or(false), &s("kind").unwrap_or_default(), &s("message").unwrap_or_default()),
|
||||
"/api/biometric/report" => shared.biometric_report(&s("op").unwrap_or_default(), b("ok").unwrap_or(false), &s("code").unwrap_or_default(), &s("message").unwrap_or_default()),
|
||||
"/api/biometric/confirm" => shared.confirm(&s("nonce").ok_or("nonce missing")?),
|
||||
"/api/biometric/enrol/take" => shared.enrol_take(&s("token").ok_or("token missing")?),
|
||||
"/api/biometric/enrolled" => shared.enrolled_set(&s("kind").unwrap_or_default()),
|
||||
// ---- the page bridge: the window's decisions (src/bridge.rs) ----
|
||||
"/api/bridge/decide" => shared.bridge_decide(&s("id").ok_or("id missing")?, b("ok").unwrap_or(false), s("nonce").as_deref()),
|
||||
"/api/bridge/sites/remove" => shared.bridge_sites_remove(&s("origin").ok_or("origin missing")?),
|
||||
"/api/bridge/on" => shared.set_bridge_on(b("on").ok_or("on missing")?),
|
||||
"/api/refresh" => {
|
||||
shared.send(Cmd::Refresh);
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
"/api/update/check" => {
|
||||
shared.send(Cmd::CheckUpdate);
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
"/api/update/open" => {
|
||||
shared.send(Cmd::OpenUpdate);
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
"/api/update/install" => {
|
||||
shared.send(Cmd::InstallUpdate);
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
"/api/update/auto" => {
|
||||
let on = body.get("on").and_then(|v| v.as_bool()).ok_or("on missing")?;
|
||||
shared.send(Cmd::AutoUpdate(on));
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
"/api/open" => {
|
||||
let url = s("url").ok_or("url missing")?;
|
||||
if url.starts_with("https://") || url.starts_with("http://") {
|
||||
igneum_common::platform::open_url(&url);
|
||||
Ok(json!({ "ok": true }))
|
||||
} else {
|
||||
Err("only http(s) links open".into())
|
||||
}
|
||||
}
|
||||
"/api/quit" => {
|
||||
shared.send(Cmd::Quit);
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
_ => Err("unknown api".into()),
|
||||
}
|
||||
}
|
||||
156
app/igneum-wallet/src/state.rs
Normal file
156
app/igneum-wallet/src/state.rs
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
//! The engine's state as the window reads it (`GET /api/state`), plus the event and log rings (as the miner's).
|
||||
|
||||
use serde::Serialize;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Clone, Serialize, Default)]
|
||||
pub struct NodeState {
|
||||
/// miner | external | public | own | none
|
||||
pub source: String,
|
||||
pub state: String, // off | starting | connecting | ok | lost
|
||||
pub chain_id: u64,
|
||||
pub chain: String, // the consensus chain id (igneum-devnet-20) when known
|
||||
pub block: u64,
|
||||
pub evm: String, // the Ethereum RPC endpoint in use
|
||||
pub grpc: String,
|
||||
pub synced: bool,
|
||||
pub message: String,
|
||||
pub finality_active: bool,
|
||||
pub latest_locked: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Default)]
|
||||
pub struct FinalityView {
|
||||
pub verified_index: u64,
|
||||
pub verified_hash: String,
|
||||
pub chain_number: Option<u64>,
|
||||
pub signers: usize,
|
||||
pub voters: usize,
|
||||
pub fraction_total: f64,
|
||||
pub fraction_active: f64,
|
||||
pub weights_exact: bool,
|
||||
pub verified_at: f64,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// The over-the-air updater (src/updater.rs), as the miner's.
|
||||
#[derive(Clone, Serialize, Default)]
|
||||
pub struct UpdateState {
|
||||
pub status: String, // off | unknown | checking | current | available | downloading | staging | ready | applying | deferred | manual | error
|
||||
pub version: String,
|
||||
pub url: String,
|
||||
pub notes: String,
|
||||
pub file: String, // the downloaded disk image or installer (the manual path opens it)
|
||||
pub error: String,
|
||||
pub checked_at: f64,
|
||||
pub available: bool,
|
||||
pub downloaded: bool,
|
||||
pub ready: bool,
|
||||
pub applying: bool,
|
||||
pub progress: f64, // 0..1 of the download
|
||||
pub size: u64,
|
||||
pub auto: bool, // settings: install by itself when nothing is being sent
|
||||
pub wait: String, // why it has not applied yet, in the window's words
|
||||
pub urgent: bool, // this version is below min_supported_version: no waiting
|
||||
pub urgent_text: String,
|
||||
pub unsupported: bool,
|
||||
pub min_supported: String,
|
||||
pub updated_from: String, // set on the first run after an update
|
||||
pub rolled_back: String, // set when the helper restored the previous version
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Default)]
|
||||
pub struct SettingsState {
|
||||
pub start_at_login: bool,
|
||||
pub network: String,
|
||||
pub auto_update: bool,
|
||||
/// the idle lock in minutes (1, 5, 15, 60), 0 for never; acts only while Touch ID or Windows Hello is enrolled
|
||||
pub idle_lock_min: u64,
|
||||
/// the first arrival after the person opened the app may raise the Touch ID sheet once, by itself
|
||||
pub ask_on_open: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct Event {
|
||||
pub t: f64,
|
||||
pub kind: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct LogLine {
|
||||
pub seq: u64,
|
||||
pub t: f64,
|
||||
pub src: String,
|
||||
pub err: bool,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Default)]
|
||||
pub struct State {
|
||||
pub version: String,
|
||||
/// welcome | unlock | home
|
||||
pub phase: String,
|
||||
pub has_vault: bool,
|
||||
pub unlocked: bool,
|
||||
pub backed_up: bool,
|
||||
pub source: String, // created | seed | key | miner
|
||||
pub address: String,
|
||||
pub display: String,
|
||||
pub balance_wei: String,
|
||||
pub balance: String,
|
||||
pub balance_known: bool,
|
||||
pub node: NodeState,
|
||||
pub finality: FinalityView,
|
||||
pub history: Vec<crate::history::Entry>,
|
||||
pub scanning: bool,
|
||||
pub scanned_to: Option<u64>,
|
||||
pub update: UpdateState,
|
||||
pub settings: SettingsState,
|
||||
/// Touch ID / Windows Hello (igneum_common::biometric; the prompt lives in the window host)
|
||||
pub biometric: igneum_common::biometric::BiometricState,
|
||||
pub events: Vec<Event>,
|
||||
pub miner_wallet_file: String,
|
||||
pub miner_wallet_present: bool,
|
||||
pub add_network_page: String,
|
||||
pub public_rpc: String,
|
||||
pub machine_id: String,
|
||||
pub host: String,
|
||||
pub log_dir: String,
|
||||
pub app_dir: String,
|
||||
pub uptime_s: u64,
|
||||
pub now: f64,
|
||||
pub quitting: bool,
|
||||
}
|
||||
|
||||
pub struct Rings {
|
||||
pub events: VecDeque<Event>,
|
||||
pub log: VecDeque<LogLine>,
|
||||
pub seq: u64,
|
||||
}
|
||||
|
||||
impl Rings {
|
||||
pub fn new() -> Rings {
|
||||
Rings { events: VecDeque::new(), log: VecDeque::new(), seq: 0 }
|
||||
}
|
||||
pub fn event(&mut self, kind: &str, text: &str) {
|
||||
self.events.push_front(Event { t: igneum_common::platform::unix_now_f(), kind: kind.into(), text: text.into() });
|
||||
while self.events.len() > 40 {
|
||||
self.events.pop_back();
|
||||
}
|
||||
}
|
||||
pub fn log(&mut self, src: &str, err: bool, text: &str) {
|
||||
self.seq += 1;
|
||||
self.log.push_back(LogLine { seq: self.seq, t: igneum_common::platform::unix_now_f(), src: src.into(), err, text: text.into() });
|
||||
while self.log.len() > 3000 {
|
||||
self.log.pop_front();
|
||||
}
|
||||
}
|
||||
pub fn since(&self, after: u64, limit: usize) -> Vec<LogLine> {
|
||||
let mut out: Vec<LogLine> = self.log.iter().filter(|l| l.seq > after).cloned().collect();
|
||||
if out.len() > limit {
|
||||
out = out.split_off(out.len() - limit);
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
319
app/igneum-wallet/src/tx.rs
Normal file
319
app/igneum-wallet/src/tx.rs
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
//! EIP-1559 transfers: RLP, the signing hash, the secp256k1 signature (low-s, with the recovery bit), the raw bytes
|
||||
//! for eth_sendRawTransaction. Written here so the key never leaves the engine; nothing on the window side signs.
|
||||
|
||||
use k256::ecdsa::{RecoveryId, Signature, SigningKey, VerifyingKey};
|
||||
use sha3::{Digest, Keccak256};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Transfer {
|
||||
pub chain_id: u64,
|
||||
pub nonce: u64,
|
||||
pub max_priority_fee: u128,
|
||||
pub max_fee: u128,
|
||||
pub gas_limit: u64,
|
||||
pub to: [u8; 20],
|
||||
pub value: u128,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
// ---- RLP --------------------------------------------------------------------------------------------------
|
||||
|
||||
fn rlp_len(len: usize, offset: u8, out: &mut Vec<u8>) {
|
||||
if len < 56 {
|
||||
out.push(offset + len as u8);
|
||||
} else {
|
||||
let be = (len as u64).to_be_bytes();
|
||||
let first = be.iter().position(|b| *b != 0).unwrap_or(7);
|
||||
out.push(offset + 55 + (8 - first) as u8);
|
||||
out.extend_from_slice(&be[first..]);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rlp_bytes(b: &[u8], out: &mut Vec<u8>) {
|
||||
if b.len() == 1 && b[0] < 0x80 {
|
||||
out.push(b[0]);
|
||||
} else {
|
||||
rlp_len(b.len(), 0x80, out);
|
||||
out.extend_from_slice(b);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rlp_uint(v: u128, out: &mut Vec<u8>) {
|
||||
if v == 0 {
|
||||
out.push(0x80);
|
||||
return;
|
||||
}
|
||||
let be = v.to_be_bytes();
|
||||
let first = be.iter().position(|b| *b != 0).unwrap();
|
||||
rlp_bytes(&be[first..], out);
|
||||
}
|
||||
|
||||
pub fn rlp_list(items: &[u8], out: &mut Vec<u8>) {
|
||||
rlp_len(items.len(), 0xc0, out);
|
||||
out.extend_from_slice(items);
|
||||
}
|
||||
|
||||
fn fields(t: &Transfer, out: &mut Vec<u8>) {
|
||||
rlp_uint(t.chain_id as u128, out);
|
||||
rlp_uint(t.nonce as u128, out);
|
||||
rlp_uint(t.max_priority_fee, out);
|
||||
rlp_uint(t.max_fee, out);
|
||||
rlp_uint(t.gas_limit as u128, out);
|
||||
rlp_bytes(&t.to, out);
|
||||
rlp_uint(t.value, out);
|
||||
rlp_bytes(&t.data, out);
|
||||
out.push(0xc0); // empty access list
|
||||
}
|
||||
|
||||
/// 0x02 || rlp([chainId, nonce, maxPriorityFee, maxFee, gasLimit, to, value, data, accessList])
|
||||
pub fn unsigned_bytes(t: &Transfer) -> Vec<u8> {
|
||||
let mut items = Vec::new();
|
||||
fields(t, &mut items);
|
||||
let mut out = vec![0x02];
|
||||
rlp_list(&items, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
pub fn signing_hash(t: &Transfer) -> [u8; 32] {
|
||||
Keccak256::digest(unsigned_bytes(t)).into()
|
||||
}
|
||||
|
||||
pub struct Signed {
|
||||
pub raw: Vec<u8>,
|
||||
pub hash: [u8; 32],
|
||||
}
|
||||
|
||||
/// Signs with the raw private key. The signature is normalised to low s (the EVM rejects high s) and the recovery bit
|
||||
/// flipped with it; the signer's address is recovered from the result and checked before anything is returned.
|
||||
pub fn sign(t: &Transfer, private_key: &[u8; 32]) -> Result<Signed, String> {
|
||||
let sk = SigningKey::from_bytes(private_key.into()).map_err(|_| "invalid private key")?;
|
||||
let h = signing_hash(t);
|
||||
let (sig, rec): (Signature, RecoveryId) = sk.sign_prehash_recoverable(&h).map_err(|e| e.to_string())?;
|
||||
let (sig, rec) = match sig.normalize_s() {
|
||||
Some(low) => (low, RecoveryId::from_byte(rec.to_byte() ^ 1).ok_or("recovery id")?),
|
||||
None => (sig, rec),
|
||||
};
|
||||
// the recovered key must be ours
|
||||
let vk = VerifyingKey::recover_from_prehash(&h, &sig, rec).map_err(|e| format!("recovery check: {e}"))?;
|
||||
if vk != *sk.verifying_key() {
|
||||
return Err("the signature does not recover to the signing key".into());
|
||||
}
|
||||
let mut items = Vec::new();
|
||||
fields(t, &mut items);
|
||||
rlp_uint(rec.to_byte() as u128, &mut items);
|
||||
rlp_scalar(&sig.r().to_bytes(), &mut items);
|
||||
rlp_scalar(&sig.s().to_bytes(), &mut items);
|
||||
let mut raw = vec![0x02];
|
||||
rlp_list(&items, &mut raw);
|
||||
let hash: [u8; 32] = Keccak256::digest(&raw).into();
|
||||
Ok(Signed { raw, hash })
|
||||
}
|
||||
|
||||
/// Signs a 32-byte digest (EIP-191 personal messages, EIP-712 typed data) with the raw private key: the 65-byte
|
||||
/// r || s || v signature pages expect (v = 27 + recovery bit), low s, the recovered key checked before anything is
|
||||
/// returned. The page bridge (8 October 2026).
|
||||
pub fn sign_digest(h: &[u8; 32], private_key: &[u8; 32]) -> Result<[u8; 65], String> {
|
||||
let sk = SigningKey::from_bytes(private_key.into()).map_err(|_| "invalid private key")?;
|
||||
let (sig, rec): (Signature, RecoveryId) = sk.sign_prehash_recoverable(h).map_err(|e| e.to_string())?;
|
||||
let (sig, rec) = match sig.normalize_s() {
|
||||
Some(low) => (low, RecoveryId::from_byte(rec.to_byte() ^ 1).ok_or("recovery id")?),
|
||||
None => (sig, rec),
|
||||
};
|
||||
let vk = VerifyingKey::recover_from_prehash(h, &sig, rec).map_err(|e| format!("recovery check: {e}"))?;
|
||||
if vk != *sk.verifying_key() {
|
||||
return Err("the signature does not recover to the signing key".into());
|
||||
}
|
||||
let mut out = [0u8; 65];
|
||||
out[..32].copy_from_slice(&sig.r().to_bytes());
|
||||
out[32..64].copy_from_slice(&sig.s().to_bytes());
|
||||
out[64] = 27 + rec.to_byte();
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The EIP-191 digest of a personal message: keccak("\x19Ethereum Signed Message:\n" + len + message).
|
||||
pub fn personal_digest(message: &[u8]) -> [u8; 32] {
|
||||
let mut pre = format!("\x19Ethereum Signed Message:\n{}", message.len()).into_bytes();
|
||||
pre.extend_from_slice(message);
|
||||
Keccak256::digest(&pre).into()
|
||||
}
|
||||
|
||||
/// The address a 65-byte signature over `h` recovers to, lower-case 0x hex.
|
||||
pub fn recover_digest(h: &[u8; 32], sig65: &[u8; 65]) -> Option<String> {
|
||||
let v = sig65[64];
|
||||
let rec = RecoveryId::from_byte(if v >= 27 { v - 27 } else { v })?;
|
||||
let sig = Signature::from_slice(&sig65[..64]).ok()?;
|
||||
let vk = VerifyingKey::recover_from_prehash(h, &sig, rec).ok()?;
|
||||
let pk = vk.to_encoded_point(false);
|
||||
let h2: [u8; 32] = Keccak256::digest(&pk.as_bytes()[1..]).into();
|
||||
Some(format!("0x{}", h2[12..].iter().map(|b| format!("{b:02x}")).collect::<String>()))
|
||||
}
|
||||
|
||||
/// A big-endian scalar (r, s: 32 bytes) as an RLP integer: leading zeros stripped, zero is the empty string.
|
||||
pub fn rlp_scalar(b: &[u8], out: &mut Vec<u8>) {
|
||||
let first = b.iter().position(|x| *x != 0);
|
||||
match first {
|
||||
None => out.push(0x80),
|
||||
Some(i) => rlp_bytes(&b[i..], out),
|
||||
}
|
||||
}
|
||||
|
||||
/// The signed transaction's `from`, recovered from its raw bytes: what the node will see.
|
||||
pub fn recover_from(raw: &[u8]) -> Option<String> {
|
||||
// decode the outer list, pull the last three items (v, r, s), rebuild the signing payload
|
||||
let (items, _) = rlp_decode_list(&raw[1..])?;
|
||||
if items.len() != 12 {
|
||||
return None;
|
||||
}
|
||||
let mut payload = Vec::new();
|
||||
for it in &items[..9] {
|
||||
payload.extend_from_slice(it);
|
||||
}
|
||||
let mut unsigned = vec![0x02];
|
||||
rlp_list(&payload, &mut unsigned);
|
||||
let h: [u8; 32] = Keccak256::digest(&unsigned).into();
|
||||
let v = rlp_item_bytes(&items[9])?;
|
||||
let r = rlp_item_bytes(&items[10])?;
|
||||
let s = rlp_item_bytes(&items[11])?;
|
||||
let rec = RecoveryId::from_byte(if v.is_empty() { 0 } else { v[0] })?;
|
||||
let mut rs = [0u8; 64];
|
||||
rs[32 - r.len()..32].copy_from_slice(r);
|
||||
rs[64 - s.len()..].copy_from_slice(s);
|
||||
let sig = Signature::from_slice(&rs).ok()?;
|
||||
let vk = VerifyingKey::recover_from_prehash(&h, &sig, rec).ok()?;
|
||||
let point = vk.to_encoded_point(false);
|
||||
let hh = Keccak256::digest(&point.as_bytes()[1..]);
|
||||
Some(format!("0x{}", igneum_common::keys::hex(&hh[12..])))
|
||||
}
|
||||
|
||||
/// Minimal RLP decoding for the recovery check: returns the encoded items (bytes of each item, prefix included).
|
||||
fn rlp_decode_list(b: &[u8]) -> Option<(Vec<&[u8]>, usize)> {
|
||||
let (payload_start, payload_len) = rlp_head(b)?;
|
||||
if b[0] < 0xc0 {
|
||||
return None;
|
||||
}
|
||||
let mut items = Vec::new();
|
||||
let mut o = payload_start;
|
||||
let end = payload_start + payload_len;
|
||||
while o < end {
|
||||
let (ps, pl) = rlp_head(&b[o..])?;
|
||||
items.push(&b[o..o + ps + pl]);
|
||||
o += ps + pl;
|
||||
}
|
||||
Some((items, end))
|
||||
}
|
||||
|
||||
fn rlp_head(b: &[u8]) -> Option<(usize, usize)> {
|
||||
let f = *b.first()?;
|
||||
Some(match f {
|
||||
0..=0x7f => (0, 1),
|
||||
0x80..=0xb7 => (1, (f - 0x80) as usize),
|
||||
0xb8..=0xbf => {
|
||||
let n = (f - 0xb7) as usize;
|
||||
(1 + n, be_len(b.get(1..1 + n)?))
|
||||
}
|
||||
0xc0..=0xf7 => (1, (f - 0xc0) as usize),
|
||||
_ => {
|
||||
let n = (f - 0xf7) as usize;
|
||||
(1 + n, be_len(b.get(1..1 + n)?))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn be_len(b: &[u8]) -> usize {
|
||||
b.iter().fold(0usize, |a, x| (a << 8) | *x as usize)
|
||||
}
|
||||
|
||||
fn rlp_item_bytes(it: &[u8]) -> Option<&[u8]> {
|
||||
let (ps, pl) = rlp_head(it)?;
|
||||
if it[0] < 0x80 {
|
||||
return Some(&it[..1]);
|
||||
}
|
||||
it.get(ps..ps + pl)
|
||||
}
|
||||
|
||||
pub fn hex0x(b: &[u8]) -> String {
|
||||
format!("0x{}", igneum_common::keys::hex(b))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn a_personal_message_signature_recovers_to_the_signer_and_carries_v_27_or_28() {
|
||||
let key = [7u8; 32];
|
||||
let sk = SigningKey::from_bytes((&key).into()).unwrap();
|
||||
let pk = sk.verifying_key().to_encoded_point(false);
|
||||
let addr_h: [u8; 32] = Keccak256::digest(&pk.as_bytes()[1..]).into();
|
||||
let me = format!("0x{}", addr_h[12..].iter().map(|b| format!("{b:02x}")).collect::<String>());
|
||||
let h = personal_digest(b"hello world");
|
||||
// the prefix is the EIP-191 one: the same digest as keccak of the literal prefixed bytes
|
||||
let mut lit = b"\x19Ethereum Signed Message:\n11hello world".to_vec();
|
||||
let direct: [u8; 32] = Keccak256::digest(&lit).into();
|
||||
lit.clear();
|
||||
assert_eq!(h, direct);
|
||||
let sig = sign_digest(&h, &key).unwrap();
|
||||
assert!(sig[64] == 27 || sig[64] == 28);
|
||||
assert_eq!(recover_digest(&h, &sig).as_deref(), Some(me.as_str()));
|
||||
assert!(sig[32] < 0x80, "low s");
|
||||
}
|
||||
|
||||
use super::*;
|
||||
|
||||
fn to20() -> [u8; 20] {
|
||||
let mut t = [0u8; 20];
|
||||
t[19] = 0xaa;
|
||||
t
|
||||
}
|
||||
|
||||
/// The unsigned bytes computed by hand for small values: 0x02 then a 31-byte list.
|
||||
#[test]
|
||||
fn rlp_vector() {
|
||||
let t = Transfer { chain_id: 1, nonce: 0, max_priority_fee: 1, max_fee: 1, gas_limit: 21000, to: to20(), value: 0, data: vec![], };
|
||||
let b = unsigned_bytes(&t);
|
||||
let want = format!("02df0180010182520894{}aa8080c0", "00".repeat(19));
|
||||
assert_eq!(igneum_common::keys::hex(&b), want);
|
||||
let mut out = Vec::new();
|
||||
rlp_scalar(&[0u8; 32], &mut out);
|
||||
assert_eq!(out, vec![0x80]);
|
||||
let mut out = Vec::new();
|
||||
let mut one = [0u8; 32];
|
||||
one[31] = 0x7f;
|
||||
rlp_scalar(&one, &mut out);
|
||||
assert_eq!(out, vec![0x7f]);
|
||||
// keccak256 of the empty string, the classic constant
|
||||
assert_eq!(igneum_common::keys::hex(&Keccak256::digest(b"")), "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rlp_long_values() {
|
||||
let mut out = Vec::new();
|
||||
rlp_uint(1_000_000_000_000_000_000u128, &mut out); // 1 IGN in wei = 0x0de0b6b3a7640000
|
||||
assert_eq!(igneum_common::keys::hex(&out), "880de0b6b3a7640000");
|
||||
let mut out = Vec::new();
|
||||
rlp_bytes(&[0u8; 60], &mut out);
|
||||
assert_eq!(out[0], 0xb8);
|
||||
assert_eq!(out[1], 60);
|
||||
let mut out = Vec::new();
|
||||
rlp_uint(4463, &mut out);
|
||||
assert_eq!(igneum_common::keys::hex(&out), "82116f");
|
||||
}
|
||||
|
||||
/// Signing recovers to the signing address, is low-s, and the raw bytes decode back to the same from.
|
||||
#[test]
|
||||
fn sign_recovers() {
|
||||
let mut key = [0u8; 32];
|
||||
key[31] = 1;
|
||||
let t = Transfer { chain_id: 4463, nonce: 7, max_priority_fee: 1_000_000_000, max_fee: 3_000_000_000, gas_limit: 21000, to: to20(), value: 5_000_000_000_000_000_000, data: vec![] };
|
||||
let s = sign(&t, &key).unwrap();
|
||||
assert_eq!(s.raw[0], 0x02);
|
||||
assert_eq!(recover_from(&s.raw).unwrap(), "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf");
|
||||
// deterministic (RFC 6979): the same input signs to the same bytes
|
||||
let s2 = sign(&t, &key).unwrap();
|
||||
assert_eq!(s.raw, s2.raw);
|
||||
assert_eq!(s.hash, s2.hash);
|
||||
// the Hardhat key signs to its known address too
|
||||
let hh = igneum_common::keys::unhex("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80").unwrap();
|
||||
let hk: [u8; 32] = hh.try_into().unwrap();
|
||||
let s3 = sign(&t, &hk).unwrap();
|
||||
assert_eq!(recover_from(&s3.raw).unwrap(), "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266");
|
||||
}
|
||||
}
|
||||
751
app/igneum-wallet/src/updater.rs
Normal file
751
app/igneum-wallet/src/updater.rs
Normal file
|
|
@ -0,0 +1,751 @@
|
|||
//! Over-the-air updates for the wallet, v2 (5 October 2026): unattended, on the miner's bones (app/igneum-app/src/ota.rs)
|
||||
//! with the shared parts in igneum_common::{fetch, ota}. The founder's rule: every app updates itself and downloads the
|
||||
//! update without being asked.
|
||||
//!
|
||||
//! The loop, driven from the engine's tick:
|
||||
//! check (25 s after start, then hourly; 10 minutes after an error): fetch igneum-wallet-latest.json and its .sig,
|
||||
//! verify the Ed25519 signature with the key compiled into igneum_common::manifest, parse, compare versions
|
||||
//! -> download (curl with resume into <wallet data>/updates/, then size and sha256 against the manifest)
|
||||
//! -> stage (macOS: mount the DMG, copy the bundle next to the running one, check its engine answers --version with
|
||||
//! the manifest's version, digest the staged bundle; Windows: the installer is the staged artefact)
|
||||
//! -> ready: with the setting "install updates by itself" (default on) the engine applies at the next safe moment,
|
||||
//! which for a wallet is simply no send in flight (no /api/send running, no quote shown in the last 3 minutes, no
|
||||
//! sent transaction still waiting for its block, no create flow half way); at once when the user clicks Install
|
||||
//! now or the version is below min_supported_version
|
||||
//! -> apply: the engine re-hashes the download and the staged bundle, writes update-pending.json, starts the
|
||||
//! detached helper and exits (macOS: the helper swaps /Applications/Igneum Wallet.app and opens the new one;
|
||||
//! Windows: the installer runs first and stops the engine itself; untested for the wallet)
|
||||
//! -> rollback: the helper restores the previous bundle when the new app does not start twice; the new engine
|
||||
//! counts its starts and, on the third start without 90 healthy seconds, restores the previous version.
|
||||
//! "Later" is the window's: it hides the banner for that version; the engine still installs at the safe moment.
|
||||
//!
|
||||
//! Environment (tests): IGNEUM_WALLET_UPDATE_MANIFEST overrides the manifest URL from igneum-wallet.json,
|
||||
//! IGNEUM_WALLET_UPDATE_CHECK_SECS the hourly interval, IGNEUM_WALLET_UPDATE_FIRST_SECS the delay of the first check.
|
||||
|
||||
use crate::engine::{Cmd, Shared};
|
||||
use igneum_common::fetch;
|
||||
use igneum_common::manifest::{self, Manifest, PlatformEntry};
|
||||
use igneum_common::ota::{self, Launch, Pending};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const CHECK_EVERY_S: u64 = 3600;
|
||||
const FIRST_CHECK_S: u64 = 25;
|
||||
const RETRY_AFTER_ERROR_S: u64 = 600;
|
||||
/// A quote shown on the confirm screen counts as a send in flight for this long.
|
||||
pub const QUOTE_HOLDS_S: u64 = 180;
|
||||
/// The environment the helper carries to a relaunch (test runs); a normal run has none of these set.
|
||||
const ENV_PREFIXES: &[&str] = &["IGNEUM_APP_", "IGNEUM_WALLET_", "IGNEUM_OTA_"];
|
||||
|
||||
pub enum Event {
|
||||
/// The manifest fetched, verified and parsed (or why not).
|
||||
Checked(Result<Manifest, String>),
|
||||
/// The disk image or installer on disk, size and sha256 checked.
|
||||
Downloaded(Result<PathBuf, String>),
|
||||
/// macOS: the new bundle staged next to the running one. Windows: the installer path again.
|
||||
Staged(Result<PathBuf, String>),
|
||||
}
|
||||
|
||||
/// What the engine must do now.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Action {
|
||||
Apply,
|
||||
}
|
||||
|
||||
/// What the engine knows when it asks whether now is a safe moment.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Ctx {
|
||||
/// A send is in flight: /api/send running, a quote on the confirm screen, or a sent transaction not yet in a block.
|
||||
pub send_in_flight: bool,
|
||||
/// The create flow is half way (the 24 words are on the screen, waiting for the confirmation).
|
||||
pub creating: bool,
|
||||
}
|
||||
|
||||
/// What the manifest means for this install.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Plan {
|
||||
/// This version is the latest (or newer than the manifest).
|
||||
Current,
|
||||
/// A newer version is published without a build for this platform yet.
|
||||
NoBuild(String),
|
||||
/// A newer version with a build for this platform.
|
||||
Update(PlatformEntry),
|
||||
}
|
||||
|
||||
pub fn plan(m: &Manifest, current: &str) -> Plan {
|
||||
if !manifest::newer(&m.version, current) {
|
||||
return Plan::Current;
|
||||
}
|
||||
match m.this_platform() {
|
||||
Some(e) => Plan::Update(e.clone()),
|
||||
None => Plan::NoBuild(m.version.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ok when a ready update may be applied now; Err carries the reason to wait, in the words the window shows.
|
||||
pub fn safe_to_apply(ctx: &Ctx, auto: bool, install_asked: bool, urgent: bool, failed_before: bool) -> Result<(), String> {
|
||||
if urgent || install_asked {
|
||||
return Ok(());
|
||||
}
|
||||
if !auto {
|
||||
return Err("waiting for Install now (automatic updates are off)".into());
|
||||
}
|
||||
if failed_before {
|
||||
return Err("this version failed to install before; it waits for Install now".into());
|
||||
}
|
||||
if ctx.send_in_flight {
|
||||
return Err("a send is in flight; installing after it".into());
|
||||
}
|
||||
if ctx.creating {
|
||||
return Err("a wallet is being created; installing after it".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct Updater {
|
||||
pub url: String,
|
||||
pub current: String,
|
||||
app_dir: PathBuf,
|
||||
dir: PathBuf,
|
||||
auto: bool,
|
||||
manifest: Option<Manifest>,
|
||||
entry: Option<PlatformEntry>,
|
||||
file: Option<PathBuf>,
|
||||
staged: Option<PathBuf>,
|
||||
staged_digest: String,
|
||||
busy: bool,
|
||||
next_check: Instant,
|
||||
ready_since: Option<Instant>,
|
||||
install_asked: bool,
|
||||
pending: Option<Pending>,
|
||||
started: Instant,
|
||||
healthy_marked: bool,
|
||||
/// versions whose apply failed or that were rolled back: never re-applied by themselves
|
||||
failed_versions: Vec<String>,
|
||||
/// the last manifest's min_supported_version, kept across restarts (updates/manifest.json): the rollback floor
|
||||
min_supported: String,
|
||||
/// Windows: the installer was started and the engine is still up (it stops us when it may run)
|
||||
apply_launched: Option<Instant>,
|
||||
/// Windows: the administrator prompt was not answered; no automatic retry before this
|
||||
deferred_until: Option<Instant>,
|
||||
}
|
||||
|
||||
impl Updater {
|
||||
pub fn new(shared: &Arc<Shared>) -> Updater {
|
||||
let env = |k: &str| std::env::var(k).ok().filter(|v| !v.is_empty());
|
||||
let url = env("IGNEUM_WALLET_UPDATE_MANIFEST").unwrap_or_else(|| shared.packaged.update_manifest.clone());
|
||||
let app_dir = shared.paths.app_dir.clone();
|
||||
let dir = app_dir.join("updates");
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let first = env("IGNEUM_WALLET_UPDATE_FIRST_SECS").and_then(|v| v.parse().ok()).unwrap_or(FIRST_CHECK_S);
|
||||
let auto = shared.settings.lock().unwrap().auto_update;
|
||||
let now = Instant::now();
|
||||
let mut u = Updater {
|
||||
url,
|
||||
current: crate::engine::VERSION.to_string(),
|
||||
app_dir,
|
||||
dir,
|
||||
auto,
|
||||
manifest: None,
|
||||
entry: None,
|
||||
file: None,
|
||||
staged: None,
|
||||
staged_digest: String::new(),
|
||||
busy: false,
|
||||
next_check: now + Duration::from_secs(first),
|
||||
ready_since: None,
|
||||
install_asked: false,
|
||||
pending: None,
|
||||
started: now,
|
||||
healthy_marked: false,
|
||||
failed_versions: Vec::new(),
|
||||
min_supported: String::new(),
|
||||
apply_launched: None,
|
||||
deferred_until: None,
|
||||
};
|
||||
u.failed_versions = std::fs::read_to_string(u.failed_path()).ok().and_then(|t| serde_json::from_str::<Vec<String>>(&t).ok()).unwrap_or_default();
|
||||
if let Ok(text) = std::fs::read_to_string(u.dir.join("manifest.json")) {
|
||||
if let Ok(m) = manifest::parse(&text) {
|
||||
u.min_supported = m.min_supported_version.clone();
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut st = shared.state.lock().unwrap();
|
||||
st.update.status = if u.url.is_empty() { "off".into() } else { "unknown".into() };
|
||||
st.settings.auto_update = auto;
|
||||
}
|
||||
u.settle_previous(shared);
|
||||
u.publish(shared);
|
||||
u
|
||||
}
|
||||
|
||||
fn failed_path(&self) -> PathBuf {
|
||||
self.app_dir.join("failed-versions.json")
|
||||
}
|
||||
|
||||
fn remember_failed(&mut self, shared: &Arc<Shared>, ver: &str) {
|
||||
if ver.is_empty() || self.failed_versions.iter().any(|v| v == ver) {
|
||||
return;
|
||||
}
|
||||
self.failed_versions.push(ver.to_string());
|
||||
let _ = std::fs::write(self.failed_path(), serde_json::to_string(&self.failed_versions).unwrap_or_default());
|
||||
shared.log(&format!("update: {ver} is marked failed; it will not be applied by itself again (Install now still can)"));
|
||||
}
|
||||
|
||||
/// On start: did we just update (or fail to)? Reports it, counts this start, and asks for a rollback when the
|
||||
/// new version keeps dying before it is healthy.
|
||||
fn settle_previous(&mut self, shared: &Arc<Shared>) {
|
||||
let pending = ota::read_pending(&self.app_dir);
|
||||
if let Some(r) = ota::read_result(&self.app_dir) {
|
||||
let _ = std::fs::remove_file(ota::result_path(&self.app_dir));
|
||||
if !r.ok && r.deferred {
|
||||
shared.log(&format!("OTA: the update to {} was deferred before this start ({}); it tries again", r.version, r.error));
|
||||
let _ = std::fs::remove_file(ota::pending_path(&self.app_dir));
|
||||
} else if !r.ok {
|
||||
{
|
||||
let mut st = shared.state.lock().unwrap();
|
||||
st.update.error = r.error.clone();
|
||||
st.update.status = "error".into();
|
||||
if r.rolled_back {
|
||||
st.update.rolled_back = format!("{}: {}", r.version, r.error);
|
||||
}
|
||||
}
|
||||
shared.event("error", &format!("update to {} failed: {}", r.version, r.error));
|
||||
let _ = std::fs::remove_file(ota::pending_path(&self.app_dir));
|
||||
self.remember_failed(shared, &r.version);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let Some(mut p) = pending else { return };
|
||||
if p.to == self.current {
|
||||
// we are the new version
|
||||
p.starts += 1;
|
||||
ota::write_pending(&self.app_dir, &p);
|
||||
if p.starts == 1 {
|
||||
shared.event("ok", &format!("updated to Igneum Wallet {} from {}", p.to, p.from));
|
||||
shared.state.lock().unwrap().update.updated_from = p.from.clone();
|
||||
} else {
|
||||
shared.log(&format!("start {} of {} since the update from {}; healthy after {} s", p.starts, p.to, p.from, ota::HEALTHY_AFTER_S));
|
||||
}
|
||||
self.pending = Some(p);
|
||||
} else if p.from == self.current {
|
||||
// the old version runs again: the helper restored it, or the installer never ran
|
||||
shared.event("error", &format!("the update to {} did not take; still on {}", p.to, p.from));
|
||||
let _ = std::fs::remove_file(ota::pending_path(&self.app_dir));
|
||||
self.remember_failed(shared, &p.to);
|
||||
} else {
|
||||
let _ = std::fs::remove_file(ota::pending_path(&self.app_dir));
|
||||
}
|
||||
}
|
||||
|
||||
/// True when this (new) version has died twice before reaching HEALTHY_AFTER_S: the engine rolls back and exits.
|
||||
pub fn needs_rollback(&self) -> bool {
|
||||
self.pending.as_ref().map(|p| p.starts >= 3).unwrap_or(false)
|
||||
}
|
||||
|
||||
// ---- state for the window ------------------------------------------------------------------------------------
|
||||
|
||||
fn publish(&self, shared: &Arc<Shared>) {
|
||||
let mut st = shared.state.lock().unwrap();
|
||||
let u = &mut st.update;
|
||||
u.auto = self.auto;
|
||||
if let Some(m) = &self.manifest {
|
||||
u.version = m.version.clone();
|
||||
u.notes = m.notes.clone();
|
||||
u.unsupported = manifest::unsupported(m, &self.current);
|
||||
u.min_supported = m.min_supported_version.clone();
|
||||
}
|
||||
if let Some(e) = &self.entry {
|
||||
u.url = e.url.clone();
|
||||
u.size = e.size;
|
||||
}
|
||||
u.available = self.entry.is_some();
|
||||
u.downloaded = self.file.is_some();
|
||||
u.ready = self.staged.is_some();
|
||||
u.file = self.file.as_ref().map(|p| p.display().to_string()).unwrap_or_default();
|
||||
if u.status != "applying" && u.status != "manual" && u.status != "error" && u.status != "deferred" && u.status != "off" {
|
||||
u.status = if self.staged.is_some() {
|
||||
"ready".into()
|
||||
} else if self.file.is_some() {
|
||||
"staging".into()
|
||||
} else if self.entry.is_some() {
|
||||
if self.busy { "downloading".into() } else { "available".into() }
|
||||
} else if self.manifest.is_some() {
|
||||
"current".into()
|
||||
} else if u.status.is_empty() {
|
||||
"unknown".into()
|
||||
} else {
|
||||
u.status.clone()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn set_error(&mut self, shared: &Arc<Shared>, e: &str) {
|
||||
shared.log(&format!("update: {e}"));
|
||||
let mut st = shared.state.lock().unwrap();
|
||||
st.update.error = e.to_string();
|
||||
st.update.status = "error".into();
|
||||
st.update.applying = false;
|
||||
st.update.wait = String::new();
|
||||
}
|
||||
|
||||
fn clear_error(&self, shared: &Arc<Shared>) {
|
||||
let mut st = shared.state.lock().unwrap();
|
||||
st.update.error = String::new();
|
||||
if st.update.status == "error" {
|
||||
st.update.status = "unknown".into();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_auto(&mut self, shared: &Arc<Shared>, on: bool) {
|
||||
self.auto = on;
|
||||
{
|
||||
let mut s = shared.settings.lock().unwrap();
|
||||
s.auto_update = on;
|
||||
s.save(&shared.paths.settings);
|
||||
}
|
||||
shared.state.lock().unwrap().settings.auto_update = on;
|
||||
shared.event("info", if on { "updates install by themselves when nothing is being sent" } else { "updates download but wait for Install now" });
|
||||
self.publish(shared);
|
||||
}
|
||||
|
||||
// ---- the tick ------------------------------------------------------------------------------------------------
|
||||
|
||||
pub fn tick(&mut self, shared: &Arc<Shared>, ctx: &Ctx) -> Option<Action> {
|
||||
let now = Instant::now();
|
||||
// the new version is healthy once it has run this long: the update is complete, the leftovers can go
|
||||
if !self.healthy_marked && self.pending.is_some() && now.duration_since(self.started) >= Duration::from_secs(ota::HEALTHY_AFTER_S) {
|
||||
self.healthy_marked = true;
|
||||
let p = self.pending.take().unwrap();
|
||||
let _ = std::fs::remove_file(ota::pending_path(&self.app_dir));
|
||||
let _ = std::fs::remove_file(ota::result_path(&self.app_dir)); // the helper's "ok" lands after this engine started
|
||||
shared.log(&format!("update to {} complete (from {}); keeping the previous version for a rollback", p.to, p.from));
|
||||
self.tidy();
|
||||
}
|
||||
if now >= self.next_check && !self.busy && self.staged.is_none() {
|
||||
self.start_check(shared);
|
||||
}
|
||||
if self.busy {
|
||||
if let (Some(e), None) = (&self.entry, &self.file) {
|
||||
let mut st = shared.state.lock().unwrap();
|
||||
if st.update.status == "downloading" {
|
||||
st.update.progress = fetch::progress(e, &self.dir);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
let urgent = self.urgent();
|
||||
{
|
||||
let mut st = shared.state.lock().unwrap();
|
||||
st.update.urgent = urgent;
|
||||
st.update.urgent_text = if urgent {
|
||||
format!("Igneum Wallet {} is no longer supported; the network needs {} or newer.", self.current, self.min_supported)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
}
|
||||
if self.staged.is_none() {
|
||||
return None;
|
||||
}
|
||||
// Windows: the installer was started with the engine still running; it stops us when it may run. Until then
|
||||
// watch for the helper's verdict (an unanswered administrator prompt).
|
||||
if let Some(t) = self.apply_launched {
|
||||
match ota::read_result(&self.app_dir) {
|
||||
Some(r) if !r.ok => {
|
||||
let _ = std::fs::remove_file(ota::result_path(&self.app_dir));
|
||||
self.defer(shared, &r.error);
|
||||
}
|
||||
Some(_) => {} // the installer is in: it stops this engine any moment now
|
||||
None if now.duration_since(t) >= Duration::from_secs(15 * 60) => self.defer(shared, "no answer from the installer in 15 minutes"),
|
||||
None => {}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
if let Some(u) = self.deferred_until {
|
||||
if now < u && !self.install_asked {
|
||||
return None;
|
||||
}
|
||||
self.deferred_until = None;
|
||||
shared.state.lock().unwrap().update.status = "ready".into();
|
||||
}
|
||||
let v = self.version();
|
||||
let failed_before = self.failed_versions.iter().any(|f| f == &v);
|
||||
match safe_to_apply(ctx, self.auto, self.install_asked, urgent, failed_before) {
|
||||
Ok(()) => Some(Action::Apply),
|
||||
Err(why) => {
|
||||
shared.state.lock().unwrap().update.wait = why;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Windows: the installer could not run (the administrator prompt was declined, timed out, or nobody was there).
|
||||
fn defer(&mut self, shared: &Arc<Shared>, err: &str) {
|
||||
self.apply_launched = None;
|
||||
self.install_asked = false;
|
||||
self.deferred_until = Some(Instant::now() + Duration::from_secs(6 * 3600));
|
||||
let _ = std::fs::remove_file(ota::pending_path(&self.app_dir));
|
||||
let v = self.version();
|
||||
{
|
||||
let mut st = shared.state.lock().unwrap();
|
||||
st.update.applying = false;
|
||||
st.update.status = "deferred".into();
|
||||
st.update.wait = "waits for the next time someone is at this PC (Windows asks for permission)".into();
|
||||
}
|
||||
shared.event("info", &format!("OTA: administrator approval not given for Igneum Wallet {v} ({err}); the update waits for the next time someone is at this PC"));
|
||||
}
|
||||
|
||||
fn urgent(&self) -> bool {
|
||||
match &self.manifest {
|
||||
Some(m) => self.entry.is_some() && manifest::unsupported(m, &self.current),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// After a completed update: the downloads of older versions go; the installer of the version now running stays
|
||||
/// on Windows (the rollback target of the next update); a failed bundle from an earlier rollback goes on macOS.
|
||||
fn tidy(&self) {
|
||||
if let Ok(rd) = std::fs::read_dir(&self.dir) {
|
||||
for e in rd.flatten() {
|
||||
let name = e.file_name().to_string_lossy().into_owned();
|
||||
let keep = cfg!(windows) && name.contains(&self.current) && name.ends_with(".exe");
|
||||
if !keep && name != "manifest.json" && name != "manifest.json.sig" {
|
||||
let _ = std::fs::remove_file(e.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(b) = igneum_common::platform::bundle_path() {
|
||||
let failed = PathBuf::from(format!("{}.failed", b.display()));
|
||||
if failed.exists() {
|
||||
let _ = std::fs::remove_dir_all(&failed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- check ---------------------------------------------------------------------------------------------------
|
||||
|
||||
pub fn check_now(&mut self, shared: &Arc<Shared>) {
|
||||
if self.busy {
|
||||
return;
|
||||
}
|
||||
self.clear_error(shared);
|
||||
self.start_check(shared);
|
||||
}
|
||||
|
||||
fn start_check(&mut self, shared: &Arc<Shared>) {
|
||||
let every = std::env::var("IGNEUM_WALLET_UPDATE_CHECK_SECS").ok().and_then(|v| v.parse().ok()).unwrap_or(CHECK_EVERY_S);
|
||||
self.next_check = Instant::now() + Duration::from_secs(every);
|
||||
if self.url.is_empty() {
|
||||
let mut st = shared.state.lock().unwrap();
|
||||
st.update.status = "off".into();
|
||||
st.update.checked_at = igneum_common::platform::unix_now_f();
|
||||
return;
|
||||
}
|
||||
self.busy = true;
|
||||
shared.state.lock().unwrap().update.status = "checking".into();
|
||||
let url = self.url.clone();
|
||||
let dir = self.dir.clone();
|
||||
let shared2 = shared.clone();
|
||||
std::thread::spawn(move || {
|
||||
let r = fetch::fetch_manifest(&url, &dir);
|
||||
shared2.send(Cmd::Ota(Event::Checked(r)));
|
||||
});
|
||||
}
|
||||
|
||||
pub fn event(&mut self, shared: &Arc<Shared>, ev: Event) {
|
||||
self.busy = false;
|
||||
match ev {
|
||||
Event::Checked(r) => {
|
||||
shared.state.lock().unwrap().update.checked_at = igneum_common::platform::unix_now_f();
|
||||
match r {
|
||||
Err(e) => {
|
||||
self.next_check = Instant::now() + Duration::from_secs(RETRY_AFTER_ERROR_S);
|
||||
if self.manifest.is_none() {
|
||||
self.set_error(shared, &e);
|
||||
} else {
|
||||
shared.log(&format!("update check: {e}; keeping the last manifest"));
|
||||
}
|
||||
}
|
||||
Ok(m) => {
|
||||
self.clear_error(shared);
|
||||
let entry = match plan(&m, &self.current) {
|
||||
Plan::Update(e) => Some(e),
|
||||
Plan::NoBuild(v) => {
|
||||
shared.log(&format!("update check: {v} is published but has no {} build yet", manifest::platform_name()));
|
||||
None
|
||||
}
|
||||
Plan::Current => {
|
||||
shared.log(&format!("update check: {} is current (manifest {})", self.current, m.version));
|
||||
None
|
||||
}
|
||||
};
|
||||
let changed = self.entry != entry;
|
||||
if entry.is_none() {
|
||||
self.entry = None;
|
||||
self.file = None;
|
||||
self.staged = None;
|
||||
self.ready_since = None;
|
||||
}
|
||||
self.manifest = Some(m.clone());
|
||||
self.min_supported = m.min_supported_version.clone();
|
||||
if let Some(e) = entry {
|
||||
if changed {
|
||||
self.file = None;
|
||||
self.staged = None;
|
||||
self.ready_since = None;
|
||||
shared.event("info", &format!("Igneum Wallet {} is available: downloading ({} MB){}", m.version, e.size / 1_000_000, if m.notes.is_empty() { String::new() } else { format!(". {}", m.notes) }));
|
||||
}
|
||||
self.entry = Some(e);
|
||||
if self.staged.is_none() {
|
||||
self.start_download(shared);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Downloaded(r) => match r {
|
||||
Err(e) => {
|
||||
self.set_error(shared, &format!("download failed: {e}"));
|
||||
self.next_check = Instant::now() + Duration::from_secs(RETRY_AFTER_ERROR_S);
|
||||
}
|
||||
Ok(p) => {
|
||||
self.clear_error(shared);
|
||||
shared.log(&format!("update: {} downloaded and verified", p.display()));
|
||||
self.file = Some(p.clone());
|
||||
self.publish(shared);
|
||||
self.start_stage(shared, p);
|
||||
}
|
||||
},
|
||||
Event::Staged(r) => match r {
|
||||
Err(e) if e.starts_with("manual:") => {
|
||||
let why = e.trim_start_matches("manual:").trim().to_string();
|
||||
{
|
||||
let mut st = shared.state.lock().unwrap();
|
||||
st.update.status = "manual".into();
|
||||
st.update.wait = why.clone();
|
||||
}
|
||||
shared.event("info", &format!("update downloaded; {why}"));
|
||||
}
|
||||
Err(e) => {
|
||||
self.set_error(shared, &format!("could not prepare the update: {e}"));
|
||||
self.file = None;
|
||||
self.next_check = Instant::now() + Duration::from_secs(RETRY_AFTER_ERROR_S);
|
||||
}
|
||||
Ok(p) => {
|
||||
self.clear_error(shared);
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
self.staged_digest = manifest::digest_dir(&p).unwrap_or_default();
|
||||
shared.log(&format!("update: staged bundle digest {}", self.staged_digest));
|
||||
}
|
||||
self.staged = Some(p);
|
||||
self.ready_since = Some(Instant::now());
|
||||
let v = self.version();
|
||||
{
|
||||
let mut st = shared.state.lock().unwrap();
|
||||
st.update.wait = if self.auto { "installs as soon as nothing is being sent".into() } else { "waiting for Install now".into() };
|
||||
st.update.progress = 1.0;
|
||||
}
|
||||
shared.event("ok", &format!("Igneum Wallet {v} is ready; {}", if self.auto { "it installs as soon as nothing is being sent" } else { "automatic updates are off, so it waits for Install now" }));
|
||||
}
|
||||
},
|
||||
}
|
||||
self.publish(shared);
|
||||
}
|
||||
|
||||
fn start_download(&mut self, shared: &Arc<Shared>) {
|
||||
let Some(e) = self.entry.clone() else { return };
|
||||
self.busy = true;
|
||||
{
|
||||
let mut st = shared.state.lock().unwrap();
|
||||
st.update.status = "downloading".into();
|
||||
st.update.progress = 0.0;
|
||||
}
|
||||
let dir = self.dir.clone();
|
||||
let shared2 = shared.clone();
|
||||
std::thread::spawn(move || {
|
||||
let r = fetch::download(&e, &dir);
|
||||
shared2.send(Cmd::Ota(Event::Downloaded(r)));
|
||||
});
|
||||
}
|
||||
|
||||
fn start_stage(&mut self, shared: &Arc<Shared>, file: PathBuf) {
|
||||
let Some(e) = self.entry.clone() else { return };
|
||||
let version = self.version();
|
||||
self.busy = true;
|
||||
shared.state.lock().unwrap().update.status = "staging".into();
|
||||
let dir = self.dir.clone();
|
||||
let shared2 = shared.clone();
|
||||
std::thread::spawn(move || {
|
||||
let r = ota::stage(crate::engine::APP, &e, &file, &dir, &version);
|
||||
shared2.send(Cmd::Ota(Event::Staged(r)));
|
||||
});
|
||||
}
|
||||
|
||||
// ---- the user's buttons ----------------------------------------------------------------------------------------
|
||||
|
||||
/// Install now: a ready update applies at once; a downloaded one as soon as it is staged; else a check runs.
|
||||
pub fn install_now(&mut self, shared: &Arc<Shared>) {
|
||||
self.install_asked = true;
|
||||
self.deferred_until = None;
|
||||
if self.apply_launched.is_some() {
|
||||
return; // the installer is already up (its prompt may be waiting on the screen)
|
||||
}
|
||||
if self.staged.is_some() {
|
||||
shared.state.lock().unwrap().update.wait = "installing now".into();
|
||||
return;
|
||||
}
|
||||
if self.busy {
|
||||
return;
|
||||
}
|
||||
self.clear_error(shared);
|
||||
if let Some(f) = self.file.clone() {
|
||||
self.start_stage(shared, f);
|
||||
} else if self.entry.is_some() {
|
||||
self.start_download(shared);
|
||||
} else {
|
||||
self.start_check(shared);
|
||||
}
|
||||
}
|
||||
|
||||
/// The manual path: open the downloaded disk image or installer for the user.
|
||||
pub fn open_file(&self) -> Result<(), String> {
|
||||
let f = self.file.as_ref().ok_or("nothing downloaded yet")?;
|
||||
#[cfg(target_os = "macos")]
|
||||
let r = std::process::Command::new(igneum_common::platform::tool("open")).arg(f).spawn();
|
||||
#[cfg(windows)]
|
||||
let r = igneum_common::platform::quiet(&mut std::process::Command::new(igneum_common::platform::tool("cmd"))).args(["/c", "start", "", &f.display().to_string()]).spawn();
|
||||
#[cfg(not(any(target_os = "macos", windows)))]
|
||||
let r = std::process::Command::new("xdg-open").arg(f).spawn();
|
||||
r.map(|_| ()).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// ---- apply ---------------------------------------------------------------------------------------------------
|
||||
|
||||
pub fn version(&self) -> String {
|
||||
self.manifest.as_ref().map(|m| m.version.clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Re-verifies the download and the staged bundle, writes update-pending.json, starts the helper. macOS: the
|
||||
/// engine exits right after (Launch::QuitNow). Windows: the installer runs first (Launch::InstallerRunning).
|
||||
pub fn launch_apply(&mut self, shared: &Arc<Shared>, host_pid: u32) -> Result<Launch, String> {
|
||||
let staged = self.staged.clone().ok_or("no update is ready")?;
|
||||
let to = self.version();
|
||||
let entry = self.entry.clone().ok_or("no manifest entry")?;
|
||||
if let Some(f) = &self.file {
|
||||
let sum = manifest::sha256_file(f).map_err(|e| format!("cannot hash the download: {e}"))?;
|
||||
if sum != entry.sha256 {
|
||||
self.staged = None;
|
||||
self.file = None;
|
||||
return Err("the downloaded file no longer matches the manifest's sha256; it is discarded".into());
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let d = manifest::digest_dir(&staged).map_err(|e| format!("cannot digest the staged app: {e}"))?;
|
||||
if d != self.staged_digest || d.is_empty() {
|
||||
let _ = std::fs::remove_dir_all(&staged);
|
||||
self.staged = None;
|
||||
return Err("the staged app changed since it was verified; it is discarded".into());
|
||||
}
|
||||
}
|
||||
let previous_installer = if cfg!(windows) { self.dir.join(ota::installer_name_for(crate::engine::APP, &self.current)).to_string_lossy().into_owned() } else { String::new() };
|
||||
let previous_installer = if Path::new(&previous_installer).is_file() { previous_installer } else { String::new() };
|
||||
let env_file = ota::write_env_file(&self.app_dir, ENV_PREFIXES);
|
||||
let a = ota::Apply { app: crate::engine::APP, app_dir: &self.app_dir, current: &self.current, version: &to, staged: &staged, staged_digest: &self.staged_digest, sha256: &entry.sha256, host_pid, env_file, previous_installer };
|
||||
shared.log(&format!("update: starting the helper for {to} (host pid {host_pid}, staged {})", staged.display()));
|
||||
let launched = ota::launch_apply(&a)?;
|
||||
if launched == Launch::InstallerRunning {
|
||||
self.apply_launched = Some(Instant::now());
|
||||
}
|
||||
Ok(launched)
|
||||
}
|
||||
|
||||
/// The new version failed to start twice: restore the previous one through the helper and exit.
|
||||
pub fn launch_rollback(&mut self, shared: &Arc<Shared>, host_pid: u32) -> Result<(), String> {
|
||||
let p = self.pending.clone().ok_or("no update pending")?;
|
||||
// never below the network's minimum; this version stays and is marked failed so it is not re-applied
|
||||
if !self.min_supported.is_empty() && manifest::newer(&self.min_supported, &p.from) {
|
||||
let _ = std::fs::remove_file(ota::pending_path(&self.app_dir));
|
||||
self.pending = None;
|
||||
return Err(format!("not rolling back to {}: the network needs {} or newer; staying on {}", p.from, self.min_supported, p.to));
|
||||
}
|
||||
self.remember_failed(shared, &p.to);
|
||||
shared.event("error", &format!("Igneum Wallet {} did not stay up twice; restoring {}", p.to, p.from));
|
||||
let env_file = ota::write_env_file(&self.app_dir, ENV_PREFIXES);
|
||||
ota::launch_rollback(crate::engine::APP, &self.app_dir, &p, host_pid, env_file)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// What packaging/ota/publish-manifest.sh --product wallet writes (canonical JSON, sorted keys, no whitespace).
|
||||
const WALLET_MANIFEST: &str = r#"{"channel":"devnet","consensus":{"activation_height":null,"deadline_note":""},"min_supported_version":"","notes":"coin on the home screen, updates install by themselves","platforms":{"mac":{"kind":"dmg","sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","size":19479807,"url":"https://dl.igneum.network/dl/t/Igneum-Wallet-0.1.1.dmg"}},"published_at":"2026-10-05T09:00:00Z","version":"0.1.1"}"#;
|
||||
|
||||
#[test]
|
||||
fn the_wallet_manifest_parses() {
|
||||
let m = manifest::parse(WALLET_MANIFEST).unwrap();
|
||||
assert_eq!(m.version, "0.1.1");
|
||||
assert_eq!(m.channel, "devnet");
|
||||
assert_eq!(m.activation_height, None);
|
||||
assert!(m.min_supported_version.is_empty());
|
||||
let mac = m.mac.as_ref().unwrap();
|
||||
assert_eq!(mac.kind, "dmg");
|
||||
assert_eq!(mac.size, 19479807);
|
||||
assert!(mac.url.ends_with("/Igneum-Wallet-0.1.1.dmg"));
|
||||
assert!(m.windows.is_none());
|
||||
assert!(m.notes.contains("coin"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn versions_the_wallet_will_see() {
|
||||
assert!(manifest::newer("0.1.1", "0.1.0"));
|
||||
assert!(manifest::newer("0.1.10", "0.1.9"));
|
||||
assert!(manifest::newer("0.2.0", "0.1.11"));
|
||||
assert!(!manifest::newer("0.1.0", "0.1.0"));
|
||||
assert!(!manifest::newer("0.1.0", "0.1.1"));
|
||||
assert!(manifest::newer("0.1.1", "0.1.1-rc1"));
|
||||
assert!(!manifest::newer("latest", "0.1.0"));
|
||||
assert!(!manifest::newer("", "0.1.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_plan_follows_the_version_and_the_platform() {
|
||||
let m = manifest::parse(WALLET_MANIFEST).unwrap();
|
||||
match plan(&m, "0.1.0") {
|
||||
Plan::Update(e) if manifest::platform_name() == "mac" => assert_eq!(e.size, 19479807),
|
||||
Plan::NoBuild(v) if manifest::platform_name() != "mac" => assert_eq!(v, "0.1.1"),
|
||||
other => panic!("unexpected plan {other:?}"),
|
||||
}
|
||||
assert_eq!(plan(&m, "0.1.1"), Plan::Current);
|
||||
assert_eq!(plan(&m, "0.2.0"), Plan::Current);
|
||||
// a newer version without any platform entry: nothing to download
|
||||
let none = manifest::parse(r#"{"version":"0.1.2","platforms":{}}"#).unwrap();
|
||||
assert_eq!(plan(&none, "0.1.1"), Plan::NoBuild("0.1.2".into()));
|
||||
// a tampered manifest fails before any plan is made
|
||||
assert!(manifest::verify_and_parse(WALLET_MANIFEST.as_bytes(), &"00".repeat(64), manifest::OTA_PUBLIC_KEY_HEX).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn safe_moments() {
|
||||
let quiet = Ctx { send_in_flight: false, creating: false };
|
||||
let sending = Ctx { send_in_flight: true, creating: false };
|
||||
let creating = Ctx { send_in_flight: false, creating: true };
|
||||
assert!(safe_to_apply(&quiet, true, false, false, false).is_ok());
|
||||
assert_eq!(safe_to_apply(&sending, true, false, false, false).unwrap_err(), "a send is in flight; installing after it");
|
||||
assert!(safe_to_apply(&creating, true, false, false, false).unwrap_err().contains("being created"));
|
||||
// automatic updates off: only Install now (or an unsupported version) applies
|
||||
assert!(safe_to_apply(&quiet, false, false, false, false).unwrap_err().contains("Install now"));
|
||||
assert!(safe_to_apply(&quiet, false, true, false, false).is_ok());
|
||||
assert!(safe_to_apply(&quiet, false, false, true, false).is_ok());
|
||||
// Install now and an unsupported version beat a send in flight
|
||||
assert!(safe_to_apply(&sending, true, true, false, false).is_ok());
|
||||
assert!(safe_to_apply(&sending, true, false, true, false).is_ok());
|
||||
// a version that failed before waits for Install now
|
||||
assert!(safe_to_apply(&quiet, true, false, false, true).unwrap_err().contains("failed"));
|
||||
assert!(safe_to_apply(&quiet, true, true, false, true).is_ok());
|
||||
}
|
||||
}
|
||||
153
app/igneum-wallet/src/vault.rs
Normal file
153
app/igneum-wallet/src/vault.rs
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
//! The encrypted key file: `<data root>/wallet/vault.json`. The secret (the 32-byte private key and, when the wallet
|
||||
//! was made or imported from words, the 24-word phrase) is sealed with XChaCha20-Poly1305 under a key derived from
|
||||
//! the password with Argon2id (64 MiB, 3 passes). The password is never written anywhere; the plaintext only ever
|
||||
//! exists in the engine's memory and is zeroed when the wallet locks.
|
||||
|
||||
use argon2::{Algorithm, Argon2, Params, Version};
|
||||
use chacha20poly1305::aead::{Aead, KeyInit};
|
||||
use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
pub const M_KIB: u32 = 65_536;
|
||||
pub const T_COST: u32 = 3;
|
||||
pub const P_COST: u32 = 1;
|
||||
|
||||
/// What the vault seals. Zeroed on drop.
|
||||
#[derive(Clone, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
|
||||
pub struct Secret {
|
||||
/// 0x + 64 hex, secp256k1
|
||||
pub private_key: String,
|
||||
/// the 24 words, space separated; None for a raw key import
|
||||
pub mnemonic: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Kdf {
|
||||
pub algo: String,
|
||||
pub m_kib: u32,
|
||||
pub t: u32,
|
||||
pub p: u32,
|
||||
pub salt: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Vault {
|
||||
pub version: u32,
|
||||
pub address: String, // 0x + 40 lower-case hex, in the clear: the window shows it while locked
|
||||
pub source: String, // created | seed | key | miner
|
||||
pub created: u64,
|
||||
pub kdf: Kdf,
|
||||
pub cipher: String,
|
||||
pub nonce: String,
|
||||
pub ciphertext: String,
|
||||
/// The one-time backup sheet (the words or the key) was confirmed.
|
||||
#[serde(default)]
|
||||
pub backed_up: bool,
|
||||
}
|
||||
|
||||
fn derive(password: &str, salt: &[u8], kdf: &Kdf) -> Result<[u8; 32], String> {
|
||||
let params = Params::new(kdf.m_kib, kdf.t, kdf.p, Some(32)).map_err(|e| e.to_string())?;
|
||||
let a = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
|
||||
let mut key = [0u8; 32];
|
||||
a.hash_password_into(password.as_bytes(), salt, &mut key).map_err(|e| e.to_string())?;
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
pub fn seal(secret: &Secret, password: &str, address: &str, source: &str) -> Result<Vault, String> {
|
||||
if password.len() < 8 {
|
||||
return Err("the password needs at least 8 characters".into());
|
||||
}
|
||||
let mut salt = [0u8; 16];
|
||||
let mut nonce = [0u8; 24];
|
||||
getrandom::getrandom(&mut salt).map_err(|e| e.to_string())?;
|
||||
getrandom::getrandom(&mut nonce).map_err(|e| e.to_string())?;
|
||||
let kdf = Kdf { algo: "argon2id".into(), m_kib: M_KIB, t: T_COST, p: P_COST, salt: igneum_common::keys::hex(&salt) };
|
||||
let mut key = derive(password, &salt, &kdf)?;
|
||||
let cipher = XChaCha20Poly1305::new(Key::from_slice(&key));
|
||||
let mut plain = serde_json::to_vec(secret).map_err(|e| e.to_string())?;
|
||||
let ct = cipher.encrypt(XNonce::from_slice(&nonce), plain.as_ref()).map_err(|_| "encryption failed".to_string());
|
||||
plain.zeroize();
|
||||
key.zeroize();
|
||||
let ct = ct?;
|
||||
Ok(Vault {
|
||||
version: 1,
|
||||
address: address.to_ascii_lowercase(),
|
||||
source: source.into(),
|
||||
created: igneum_common::platform::unix_now(),
|
||||
kdf,
|
||||
cipher: "xchacha20poly1305".into(),
|
||||
nonce: igneum_common::keys::hex(&nonce),
|
||||
ciphertext: igneum_common::keys::hex(&ct),
|
||||
backed_up: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open(v: &Vault, password: &str) -> Result<Secret, String> {
|
||||
if v.kdf.algo != "argon2id" || v.cipher != "xchacha20poly1305" {
|
||||
return Err("this vault was written by a newer wallet".into());
|
||||
}
|
||||
let salt = igneum_common::keys::unhex(&v.kdf.salt).ok_or("vault salt is not hex")?;
|
||||
let nonce = igneum_common::keys::unhex(&v.nonce).ok_or("vault nonce is not hex")?;
|
||||
let ct = igneum_common::keys::unhex(&v.ciphertext).ok_or("vault ciphertext is not hex")?;
|
||||
let mut key = derive(password, &salt, &v.kdf)?;
|
||||
let cipher = XChaCha20Poly1305::new(Key::from_slice(&key));
|
||||
let plain = cipher.decrypt(XNonce::from_slice(&nonce), ct.as_ref());
|
||||
key.zeroize();
|
||||
let mut plain = plain.map_err(|_| "wrong password".to_string())?;
|
||||
let s: Result<Secret, _> = serde_json::from_slice(&plain);
|
||||
plain.zeroize();
|
||||
s.map_err(|_| "the vault did not decode".to_string())
|
||||
}
|
||||
|
||||
pub fn save(path: &Path, v: &Vault) -> Result<(), String> {
|
||||
if let Some(dir) = path.parent() {
|
||||
std::fs::create_dir_all(dir).map_err(|e| e.to_string())?;
|
||||
igneum_common::platform::lock_permissions(dir, true);
|
||||
}
|
||||
let text = serde_json::to_string_pretty(v).map_err(|e| e.to_string())?;
|
||||
let tmp = path.with_extension("json.new");
|
||||
std::fs::write(&tmp, text).map_err(|e| e.to_string())?;
|
||||
igneum_common::platform::lock_permissions(&tmp, false);
|
||||
std::fs::rename(&tmp, path).map_err(|e| e.to_string())?;
|
||||
igneum_common::platform::lock_permissions(path, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load(path: &Path) -> Option<Vault> {
|
||||
let text = std::fs::read_to_string(path).ok()?;
|
||||
serde_json::from_str(&text).ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn round_trip_and_wrong_password() {
|
||||
let s = Secret { private_key: "0x0000000000000000000000000000000000000000000000000000000000000001".into(), mnemonic: Some("abandon abandon about".into()) }; // no-secrets-ok: the test's throwaway key (0x..01), not a secret
|
||||
let v = seal(&s, "correct horse", "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf", "created").unwrap();
|
||||
assert_eq!(v.kdf.algo, "argon2id");
|
||||
let back = open(&v, "correct horse").unwrap();
|
||||
assert_eq!(back.private_key, s.private_key);
|
||||
assert_eq!(back.mnemonic, s.mnemonic);
|
||||
assert_eq!(open(&v, "correct horsf").err().unwrap(), "wrong password");
|
||||
// a flipped ciphertext byte fails the tag
|
||||
let mut bad = v.clone();
|
||||
let mut ct = igneum_common::keys::unhex(&bad.ciphertext).unwrap();
|
||||
ct[3] ^= 1;
|
||||
bad.ciphertext = igneum_common::keys::hex(&ct);
|
||||
assert!(open(&bad, "correct horse").is_err());
|
||||
// the JSON round trip keeps the fields
|
||||
let text = serde_json::to_string(&v).unwrap();
|
||||
let v2: Vault = serde_json::from_str(&text).unwrap();
|
||||
assert_eq!(open(&v2, "correct horse").unwrap().private_key, s.private_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_password_refused() {
|
||||
let s = Secret { private_key: "0x01".into(), mnemonic: None };
|
||||
assert!(seal(&s, "short", "0x", "key").is_err());
|
||||
}
|
||||
}
|
||||
489
app/igneum-wallet/ui/app.css
Normal file
489
app/igneum-wallet/ui/app.css
Normal file
|
|
@ -0,0 +1,489 @@
|
|||
/* Igneum Wallet window (wallet-ui-3, on the miner's system: app/igneum-app/ui/app.css, miner-ui-3). The site's tokens:
|
||||
obsidian, graphite, ember, molten, bone, ash; Unbounded for the page title, the balance and the row's amount; IBM Plex
|
||||
Sans for words; IBM Plex Mono for labels, units, addresses and small numbers. Fonts ship in the binary. One type scale
|
||||
(--t-*), one spacing scale (--s-*), one accent (ember). Dark by default, light under prefers-color-scheme or
|
||||
[data-theme=light]. Layout: a rail on the left (five sections), a thin top bar, the status strip under it, the page in
|
||||
the middle. The window lays out from 900 x 620 (the hosts' minimum); under 720 px the rail becomes a bottom tab bar.
|
||||
Nothing here is newer than 2022 CSS (the WebView2 host). */
|
||||
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;src:url(fonts/IBMPlexMono-400.woff2) format('woff2')}
|
||||
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;src:url(fonts/IBMPlexMono-500.woff2) format('woff2')}
|
||||
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:400;font-display:swap;src:url(fonts/IBMPlexSans-400.woff2) format('woff2')}
|
||||
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:500;font-display:swap;src:url(fonts/IBMPlexSans-500.woff2) format('woff2')}
|
||||
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:600;font-display:swap;src:url(fonts/IBMPlexSans-600.woff2) format('woff2')}
|
||||
@font-face{font-family:'Unbounded';font-style:normal;font-weight:500;font-display:swap;src:url(fonts/Unbounded-500.woff2) format('woff2')}
|
||||
@font-face{font-family:'Unbounded';font-style:normal;font-weight:700;font-display:swap;src:url(fonts/Unbounded-700.woff2) format('woff2')}
|
||||
@font-face{font-family:'Unbounded';font-style:normal;font-weight:900;font-display:swap;src:url(fonts/Unbounded-900.woff2) format('woff2')}
|
||||
|
||||
:root{
|
||||
/* colour, dark */
|
||||
--obsidian:#0C0C0E;--graphite:#16161A;--row:#111114;--line:#2A2A30;--line-2:#3A3A42;--ember:#F2541B;--ember-hi:#FF6A2B;--molten:#FFB35C;--bone:#F4F1EC;--ash:#9A9A9E;--ink-2:#C9C7C2;--ember-ink:#0C0C0E;
|
||||
--ember-12:rgba(242,84,27,.12);--ember-40:rgba(242,84,27,.4);--molten-10:rgba(255,179,92,.1);--molten-40:rgba(255,179,92,.4);--rail-bg:#111114;--hover:rgba(255,255,255,.04);--top-bg:rgba(12,12,14,.86);--shadow:rgba(0,0,0,.6);--scrim:rgba(12,12,14,.72);--qr-bg:#F4F1EC;
|
||||
/* type scale */
|
||||
--t-xs:11px;--t-sm:12px;--t-base:13px;--t-md:14px;--t-lg:15px;--t-xl:17px;--t-num:24px;--t-hero:34px;--t-h2:28px;--t-h1:44px;
|
||||
/* spacing scale */
|
||||
--s-1:4px;--s-2:8px;--s-3:12px;--s-4:16px;--s-5:24px;--s-6:32px;
|
||||
--gutter:var(--s-6);--card-pad:var(--s-5);--card-r:16px;--row-r:12px;--gap:var(--s-4);
|
||||
--sans:'IBM Plex Sans',system-ui,-apple-system,sans-serif;--mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--head:'Unbounded',sans-serif;
|
||||
--top:60px;--bottom:0px;--rail:196px}
|
||||
@media (prefers-color-scheme:light){:root:not([data-theme="dark"]){
|
||||
--obsidian:#F4F1EC;--graphite:#FFFFFF;--row:#FAF8F5;--line:#E2DED8;--line-2:#CFCAC2;--ember:#E04A14;--ember-hi:#F2541B;--molten:#B8731F;--bone:#16161A;--ash:#6B6B70;--ink-2:#3C3C42;--ember-ink:#FFFFFF;
|
||||
--ember-12:rgba(224,74,20,.1);--ember-40:rgba(224,74,20,.4);--molten-10:rgba(184,115,31,.1);--molten-40:rgba(184,115,31,.4);--rail-bg:#EFEBE4;--hover:rgba(0,0,0,.04);--top-bg:rgba(244,241,236,.88);--shadow:rgba(0,0,0,.18);--scrim:rgba(244,241,236,.72);--qr-bg:#FFFFFF}}
|
||||
:root[data-theme="light"]{
|
||||
--obsidian:#F4F1EC;--graphite:#FFFFFF;--row:#FAF8F5;--line:#E2DED8;--line-2:#CFCAC2;--ember:#E04A14;--ember-hi:#F2541B;--molten:#B8731F;--bone:#16161A;--ash:#6B6B70;--ink-2:#3C3C42;--ember-ink:#FFFFFF;
|
||||
--ember-12:rgba(224,74,20,.1);--ember-40:rgba(224,74,20,.4);--molten-10:rgba(184,115,31,.1);--molten-40:rgba(184,115,31,.4);--rail-bg:#EFEBE4;--hover:rgba(0,0,0,.04);--top-bg:rgba(244,241,236,.88);--shadow:rgba(0,0,0,.18);--scrim:rgba(244,241,236,.72);--qr-bg:#FFFFFF}
|
||||
*{box-sizing:border-box}
|
||||
html,body{height:100%}
|
||||
body{margin:0;background:var(--obsidian);color:var(--bone);font-family:var(--sans);font-size:var(--t-md);line-height:1.5;-webkit-font-smoothing:antialiased;overflow:hidden;user-select:none;-webkit-user-select:none;font-variant-numeric:tabular-nums}
|
||||
.mono,code,pre{font-family:var(--mono);font-variant-numeric:tabular-nums}
|
||||
.dim{color:var(--ash)}
|
||||
h1,h2,h3{font-family:var(--head);margin:0;line-height:1.1;text-wrap:balance}
|
||||
h1{font-weight:900;font-size:var(--t-h1);letter-spacing:-.01em}
|
||||
h2{font-weight:700;font-size:var(--t-h2)}
|
||||
h3{font-weight:700;font-size:var(--t-xl)}
|
||||
p{margin:0}
|
||||
a{color:inherit}
|
||||
button{font:inherit;color:inherit}
|
||||
input,textarea,select{font-variant-numeric:tabular-nums}
|
||||
.eyebrow{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.16em;text-transform:uppercase;color:var(--ash);display:inline-flex;align-items:center;gap:var(--s-2);white-space:nowrap}
|
||||
.eyebrow.ember{color:var(--ember)}
|
||||
[hidden]{display:none !important}
|
||||
::selection{background:rgba(242,84,27,.45)}
|
||||
|
||||
/* buttons */
|
||||
.btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--s-2);min-height:40px;padding:8px 18px;border-radius:10px;font-weight:600;font-size:var(--t-md);border:1px solid var(--line-2);color:var(--bone);background:transparent;cursor:pointer;transition:transform .15s ease,background .15s ease,border-color .15s ease,opacity .15s ease,color .15s ease;white-space:nowrap}
|
||||
.btn:hover{transform:translateY(-1px);border-color:var(--ash)}
|
||||
.btn:active{transform:none}
|
||||
.btn:disabled{opacity:.4;cursor:default;transform:none}
|
||||
.btn.primary{background:var(--ember);color:var(--ember-ink);border-color:var(--ember)}
|
||||
.btn.primary:hover{background:var(--ember-hi);border-color:var(--ember-hi)}
|
||||
.btn.big{min-height:52px;padding:12px 28px;font-size:var(--t-xl)}
|
||||
.btn.small{min-height:34px;padding:6px 14px;font-size:var(--t-base);border-radius:8px}
|
||||
.btn.tiny{min-height:26px;padding:2px 10px;font-size:var(--t-sm);border-radius:7px;font-family:var(--mono);font-weight:500}
|
||||
.btn.ghost{border-color:transparent;color:var(--ink-2)}
|
||||
.btn.ghost:hover{border-color:var(--line-2);color:var(--bone)}
|
||||
.btn.danger:hover{color:var(--ember);border-color:var(--ember)}
|
||||
.btn.fp svg{flex:0 0 auto}
|
||||
:focus-visible{outline:2px solid var(--ember);outline-offset:3px;border-radius:6px}
|
||||
@media (prefers-reduced-motion:reduce){.btn{transition:none}}
|
||||
|
||||
/* the rail */
|
||||
.rail{position:fixed;top:0;left:0;bottom:0;width:var(--rail);background:var(--rail-bg);border-right:1px solid var(--line);display:flex;flex-direction:column;z-index:22;padding:14px 12px 14px;-webkit-app-region:drag}
|
||||
.rail button{-webkit-app-region:no-drag}
|
||||
.rail-brand{display:flex;align-items:center;gap:10px;padding:6px 10px 18px;min-width:0}
|
||||
body.mac .rail-brand{padding-top:30px}
|
||||
.rail-brand .word{font-family:var(--head);font-weight:900;font-size:17px;letter-spacing:.06em}
|
||||
.rail-nav{display:flex;flex-direction:column;gap:3px}
|
||||
.nav{position:relative;display:flex;align-items:center;gap:12px;width:100%;min-height:42px;padding:8px 12px;border:1px solid transparent;border-radius:11px;background:transparent;color:var(--ink-2);font-weight:500;font-size:var(--t-md);text-align:left;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease}
|
||||
.nav svg{width:19px;height:19px;flex:0 0 19px;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round;color:var(--ash);transition:color .15s ease}
|
||||
.nav:hover{background:var(--hover);color:var(--bone)}
|
||||
.nav:hover svg{color:var(--ink-2)}
|
||||
.nav.on{background:var(--ember-12);border-color:var(--ember-40);color:var(--bone);font-weight:600}
|
||||
.nav.on svg{color:var(--ember)}
|
||||
.nav.on::before{content:"";position:absolute;left:-13px;top:10px;bottom:10px;width:3px;border-radius:0 3px 3px 0;background:var(--ember)}
|
||||
.nav.small{min-height:36px;font-size:var(--t-base);color:var(--ash)}
|
||||
.nav.small svg{width:16px;height:16px;flex-basis:16px}
|
||||
.nav.danger:hover{color:var(--ember)}
|
||||
.nav.danger:hover svg{color:var(--ember)}
|
||||
.nav-dot{position:absolute;right:12px;top:50%;width:7px;height:7px;margin-top:-3px;border-radius:50%;background:var(--molten);box-shadow:0 0 8px rgba(255,179,92,.7)}
|
||||
.rail-foot{margin-top:auto;display:flex;flex-direction:column;gap:2px;padding-top:12px;border-top:1px solid var(--line)}
|
||||
.rail-status{font-size:var(--t-xs);color:var(--ash);padding:0 12px 10px;line-height:1.55;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.rail-status b{color:var(--ink-2);font-weight:500}
|
||||
.rail-version{font-size:var(--t-xs);color:var(--ash);padding:8px 12px 0;letter-spacing:.06em}
|
||||
|
||||
/* top bar */
|
||||
.top{position:fixed;top:0;left:0;right:0;height:var(--top);display:flex;align-items:center;justify-content:space-between;gap:var(--s-4);padding:0 var(--gutter);background:var(--top-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid var(--line);z-index:20;-webkit-app-region:drag}
|
||||
.top button,.top .pill{-webkit-app-region:no-drag}
|
||||
body.mac:not(.has-rail) .top{padding-left:92px}
|
||||
body.has-rail .top{left:var(--rail)}
|
||||
.brand{display:flex;align-items:center;gap:10px;min-width:0}
|
||||
.brand .word{font-family:var(--head);font-weight:900;font-size:20px;letter-spacing:.06em}
|
||||
.brand .miner{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.22em;color:var(--ash);margin-left:var(--s-1);padding-top:3px}
|
||||
.page-title{display:flex;align-items:baseline;gap:12px;min-width:0}
|
||||
.page-title h1{font-size:19px;font-weight:700;letter-spacing:0;white-space:nowrap}
|
||||
.page-sub{font-size:var(--t-base);color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
|
||||
.top-right{display:flex;align-items:center;gap:var(--s-3);flex:0 0 auto;min-width:0}
|
||||
.narrow-only{display:none}
|
||||
.pill{display:inline-flex;align-items:center;gap:var(--s-2);font-family:var(--mono);font-size:var(--t-sm);letter-spacing:.08em;text-transform:uppercase;color:var(--ash);border:1px solid var(--line);border-radius:999px;padding:6px 12px 6px 10px;background:var(--graphite);white-space:nowrap;font-variant-numeric:tabular-nums;min-width:112px;justify-content:center}
|
||||
.pill.on{color:var(--molten);border-color:var(--molten-40)}
|
||||
.pill.warn{color:var(--ember)}
|
||||
.dot{width:8px;height:8px;border-radius:50%;background:var(--ash);display:inline-block;flex:0 0 8px}
|
||||
.on .dot,.dot.live{background:var(--molten);animation:pulse 2s ease-in-out infinite}
|
||||
.warn .dot,.dot.bad{background:var(--ember);animation:none}
|
||||
@keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(255,179,92,.5)}50%{box-shadow:0 0 0 7px rgba(255,179,92,0)}}
|
||||
@media (prefers-reduced-motion:reduce){.on .dot,.dot.live{animation:none}}
|
||||
|
||||
/* the status strip under the top bar (the update line): takes no room while empty */
|
||||
.notices{position:fixed;top:var(--top);left:0;right:0;z-index:19}
|
||||
body.has-rail .notices{left:var(--rail)}
|
||||
.notice{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-2) var(--s-4);padding:8px calc(var(--gutter) - 6px) 8px var(--gutter);background:var(--molten-10);border-bottom:1px solid var(--molten-40);font-size:var(--t-base);line-height:1.4;color:var(--bone)}
|
||||
.notice.urgent{background:rgba(242,84,27,.55);border-bottom-color:var(--ember);color:#fff;font-weight:600}
|
||||
.notice-text{flex:1 1 320px;min-width:0}
|
||||
.notice-actions{display:flex;align-items:center;gap:var(--s-2);flex:0 0 auto}
|
||||
.notice-actions:empty{display:none}
|
||||
.notice-close{flex:0 0 auto;width:30px;height:30px;border-radius:8px;border:1px solid transparent;background:transparent;color:var(--ash);font-size:20px;line-height:1;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;padding:0}
|
||||
.notice-close:hover{color:var(--bone);border-color:var(--line-2)}
|
||||
.notice.urgent .notice-close{color:#fff}
|
||||
.notice .prog{flex-basis:100%;height:3px;background:rgba(127,127,127,.2);border-radius:2px;overflow:hidden;margin-top:-3px}
|
||||
.notice .prog i{display:block;height:100%;width:0;background:var(--ember);transition:width .5s linear}
|
||||
|
||||
/* a question in place of a dialog: the quit strip over the page, the inline asks inside a card */
|
||||
.ask-wrap{position:fixed;left:0;right:0;bottom:0;z-index:36;display:flex;justify-content:center;padding:0 var(--s-4) var(--s-4);pointer-events:none}
|
||||
body.has-rail .ask-wrap{left:var(--rail)}
|
||||
.ask{display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap;background:var(--graphite);border:1px solid var(--ember-40);border-radius:14px;padding:12px 16px;box-shadow:0 14px 40px var(--shadow);pointer-events:auto;max-width:720px;animation:rise .2s ease}
|
||||
.ask-text{flex:1 1 260px;font-size:var(--t-md);min-width:0;line-height:1.45}
|
||||
.ask.inline{box-shadow:none;background:var(--ember-12);margin-top:var(--s-3);animation:none;max-width:none}
|
||||
.ask-foot{flex-basis:100%;font-size:var(--t-sm);color:var(--ink-2);min-height:0}
|
||||
.ask-foot:empty{display:none}
|
||||
.ask .short-pw{width:200px;flex:0 0 200px;margin-top:0}
|
||||
|
||||
/* screens and pages */
|
||||
main{position:absolute;top:var(--top);bottom:var(--bottom);left:0;right:0;overflow:auto;padding:0 var(--gutter);overscroll-behavior:contain;transition:top .15s ease}
|
||||
@media (prefers-reduced-motion:reduce){main{transition:none}}
|
||||
body.has-rail main{left:var(--rail)}
|
||||
.screen{display:none;max-width:1080px;margin:0 auto;animation:rise .45s ease}
|
||||
body[data-phase="welcome"] #screen-welcome,body[data-phase="create"] #screen-create,body[data-phase="import"] #screen-import,body[data-phase="unlock"] #screen-unlock,body[data-phase="home"] #screen-home{display:block}
|
||||
@keyframes rise{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
|
||||
@media (prefers-reduced-motion:reduce){.screen,.page{animation:none}}
|
||||
#screen-home{padding:22px 0 32px}
|
||||
.page{display:flex;flex-direction:column;gap:var(--gap);animation:rise .3s ease}
|
||||
|
||||
/* welcome */
|
||||
.hero{min-height:calc(100vh - var(--top));display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:var(--s-4);padding:var(--s-6) 0 48px}
|
||||
.mark-wrap{position:relative;width:120px;height:120px;border-radius:26px;background:#0C0C0E;display:flex;align-items:center;justify-content:center;margin-bottom:6px;box-shadow:0 20px 50px rgba(242,84,27,.25)}
|
||||
.mark-wrap::before{content:"";position:absolute;inset:-40px;border-radius:50%;background:radial-gradient(circle,rgba(242,84,27,.28) 0,rgba(242,84,27,0) 65%);animation:breathe 4s ease-in-out infinite;z-index:-1}
|
||||
@keyframes breathe{0%,100%{opacity:.7;transform:scale(1)}50%{opacity:1;transform:scale(1.08)}}
|
||||
@media (prefers-reduced-motion:reduce){.mark-wrap::before{animation:none}}
|
||||
.lead{font-size:var(--t-xl);color:var(--ink-2);max-width:54ch}
|
||||
.three{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--s-3);width:100%;max-width:860px;margin-top:10px;text-align:left}
|
||||
.tile{background:var(--graphite);border:1px solid var(--line);border-radius:14px;padding:18px 20px;display:flex;flex-direction:column;gap:6px;min-width:0}
|
||||
.tile .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.14em;color:var(--ember)}
|
||||
.tile .t{font-family:var(--head);font-weight:700;font-size:var(--t-lg);line-height:1.25}
|
||||
.tile .s{font-size:var(--t-base);color:var(--ash);line-height:1.45}
|
||||
.cta{display:flex;flex-wrap:wrap;gap:var(--s-3);align-items:center;justify-content:center;margin-top:10px}
|
||||
.seedline{font-size:var(--t-sm);color:var(--ash);letter-spacing:.04em}
|
||||
|
||||
/* steps (create, import) */
|
||||
.step{max-width:720px;margin:0 auto;padding:56px 0 48px;display:flex;flex-direction:column;gap:var(--s-4)}
|
||||
.step .sub{font-size:var(--t-xl);color:var(--ink-2);max-width:60ch}
|
||||
.step .cta{justify-content:flex-start;margin-top:var(--s-2)}
|
||||
.note{font-size:var(--t-base);color:var(--ash);line-height:1.5}
|
||||
.note.small{font-size:var(--t-sm);word-break:break-all}
|
||||
.help{font-size:var(--t-base);color:var(--ash);line-height:1.5;max-width:64ch}
|
||||
.line-text{font-size:var(--t-md);color:var(--ink-2);line-height:1.5}
|
||||
.line-text.ok{color:var(--molten)}
|
||||
.line-text.bad{color:var(--ember)}
|
||||
.err{font-size:var(--t-base);color:var(--ember);line-height:1.5}
|
||||
.err:empty{display:none}
|
||||
.top-gap{margin-top:var(--s-4)}
|
||||
.indent{margin-left:52px}
|
||||
.field{display:flex;flex-direction:column;gap:var(--s-2);margin-top:6px}
|
||||
.field .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.12em;text-transform:uppercase;color:var(--ash)}
|
||||
.field input,.field textarea,.addr-input{font:inherit;color:var(--bone);background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:10px 12px;min-height:42px;font-size:var(--t-md);user-select:text;-webkit-user-select:text}
|
||||
.field textarea{font-family:var(--mono);font-size:var(--t-md);line-height:1.5;resize:vertical}
|
||||
.field input.mono,.addr-input.mono{font-family:var(--mono)}
|
||||
.field input:focus,.field textarea:focus,.addr-input:focus,.select:focus{outline:none;border-color:var(--ember)}
|
||||
.addr-input{width:100%;min-width:0}
|
||||
.row .addr-input{flex:1 1 180px;width:auto}
|
||||
.box{display:flex;align-items:center;gap:10px;background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:10px 12px;font-size:var(--t-base);word-break:break-all;user-select:text;-webkit-user-select:text}
|
||||
.box span{flex:1;min-width:0}
|
||||
.box.key{color:var(--molten)}
|
||||
.words{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin:8px 0 0;padding:0;list-style:none;counter-reset:w}
|
||||
.words li{counter-increment:w;background:var(--graphite);border:1px solid var(--line);border-radius:10px;padding:8px 10px;font-family:var(--mono);font-size:var(--t-md);display:flex;gap:8px;align-items:baseline;user-select:text;-webkit-user-select:text}
|
||||
.words li::before{content:counter(w);color:var(--ash);font-size:var(--t-xs);min-width:18px;text-align:right}
|
||||
.words.small{grid-template-columns:repeat(6,minmax(0,1fr))}
|
||||
.words.small li{font-size:var(--t-sm);padding:5px 8px;background:var(--obsidian)}
|
||||
.checks{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}
|
||||
.checks label{display:flex;flex-direction:column;gap:6px;font-family:var(--mono);font-size:var(--t-sm);color:var(--ash)}
|
||||
.checks input{font:inherit;font-family:var(--mono);font-size:var(--t-lg);color:var(--bone);background:var(--graphite);border:1px solid var(--line-2);border-radius:10px;padding:10px 12px;user-select:text;-webkit-user-select:text}
|
||||
.checks input:focus{outline:none;border-color:var(--ember)}
|
||||
|
||||
/* cards, rows, disclosures, switches, the segmented control, the kv */
|
||||
.card{background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:var(--card-pad);min-width:0}
|
||||
.card-head{display:flex;justify-content:space-between;align-items:center;gap:var(--s-3);margin-bottom:var(--s-3);flex-wrap:wrap}
|
||||
.row{display:flex;gap:10px;align-items:center;min-width:0}
|
||||
.row.wrap{flex-wrap:wrap}
|
||||
.lead-row{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--s-4)}
|
||||
.lead-text{min-width:0;display:flex;flex-direction:column;gap:6px}
|
||||
.lead-text h3{font-size:var(--t-xl)}
|
||||
.disclose{display:flex;align-items:center;justify-content:space-between;gap:var(--s-3);width:100%;border:0;background:transparent;padding:0;cursor:pointer;text-align:left;color:var(--bone);font-size:var(--t-md);font-weight:500;min-height:32px}
|
||||
.disclose .chev{color:var(--ash);flex:0 0 auto;margin-right:4px}
|
||||
.disclose-right{display:flex;align-items:center;gap:var(--s-3);min-width:0}
|
||||
.disclose-right .dim{font-size:var(--t-sm);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;max-width:52ch}
|
||||
.chev{width:9px;height:9px;border-right:2px solid currentColor;border-bottom:2px solid currentColor;transform:rotate(45deg) translateY(-2px);transition:transform .15s ease;display:inline-block}
|
||||
.open .chev,[aria-expanded="true"] .chev{transform:rotate(225deg) translateY(-2px)}
|
||||
.card .disclose+.disclose,.card .details+.disclose,.card .srow+.disclose,.card .switch+.disclose,.card .err+.disclose{margin-top:var(--s-3);padding-top:var(--s-3);border-top:1px solid var(--line)}
|
||||
.details{padding-top:var(--s-3);display:flex;flex-direction:column;gap:var(--s-3)}
|
||||
.srow{display:flex;align-items:center;justify-content:space-between;gap:var(--s-4);padding:9px 0;min-width:0}
|
||||
.srow+.srow,.switch+.srow,.srow+.switch{border-top:1px solid var(--line)}
|
||||
.sw-text{min-width:0}
|
||||
.sw-text b{font-weight:600}
|
||||
.sw-text .dim{font-size:var(--t-base)}
|
||||
.switch{display:flex;align-items:flex-start;gap:var(--s-3);font-size:var(--t-md);cursor:pointer;padding:9px 0;line-height:1.4}
|
||||
.switch+.switch{border-top:1px solid var(--line)}
|
||||
.switch input{position:absolute;opacity:0;width:0;height:0}
|
||||
.switch .track{width:40px;height:22px;border-radius:999px;background:var(--line-2);position:relative;flex:0 0 40px;transition:background .15s ease;margin-top:1px}
|
||||
.switch .track::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#F4F1EC;transition:transform .15s ease}
|
||||
.switch input:checked+.track{background:var(--ember)}
|
||||
.switch input:checked+.track::after{transform:translateX(18px)}
|
||||
.switch input:focus-visible+.track{outline:2px solid var(--ember);outline-offset:3px}
|
||||
.switch input:disabled+.track{opacity:.4}
|
||||
.seg{display:inline-flex;background:var(--obsidian);border:1px solid var(--line);border-radius:10px;padding:3px;gap:2px}
|
||||
.seg-b{border:0;background:transparent;color:var(--ash);font-size:var(--t-base);font-weight:500;padding:6px 14px;border-radius:8px;cursor:pointer;transition:background .15s ease,color .15s ease;white-space:nowrap}
|
||||
.seg-b:hover{color:var(--bone)}
|
||||
.seg-b.on{background:var(--graphite);color:var(--bone);box-shadow:0 1px 3px rgba(0,0,0,.25);font-weight:600}
|
||||
.seg-b:disabled{opacity:.4;cursor:default}
|
||||
.select{font:inherit;font-size:var(--t-md);color:var(--bone);background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:7px 12px;min-height:38px;cursor:pointer}
|
||||
.kv{display:flex;flex-direction:column}
|
||||
.kv>div{display:grid;grid-template-columns:140px auto 1fr;align-items:baseline;gap:var(--s-3);padding:7px 0;border-bottom:1px solid var(--line)}
|
||||
.kv>div:last-child{border-bottom:0}
|
||||
.kv .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.1em;text-transform:uppercase;color:var(--ash);white-space:nowrap}
|
||||
.kv .v{font-size:var(--t-md);font-variant-numeric:tabular-nums;color:var(--bone);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;user-select:text;-webkit-user-select:text}
|
||||
.kv .v.ok{color:var(--molten)}
|
||||
.kv .v.warn{color:var(--ember)}
|
||||
.kv .v.dim{color:var(--ash)}
|
||||
.kv .m{font-size:var(--t-sm);color:var(--ash);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.card.adv{padding:0}
|
||||
.card.adv summary{list-style:none;cursor:pointer;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:var(--card-pad)}
|
||||
.card.adv summary::-webkit-details-marker{display:none}
|
||||
.card.adv summary::after{content:"+";font-family:var(--mono);color:var(--ash);font-size:18px;margin-left:auto}
|
||||
.card.adv[open] summary::after{content:"\2212"}
|
||||
.card.adv summary .eyebrow{margin-left:0}
|
||||
.card.adv>:not(summary){margin-left:var(--card-pad);margin-right:var(--card-pad)}
|
||||
.card.adv>:last-child{margin-bottom:var(--card-pad)}
|
||||
.card.adv>.note+.note{margin-top:6px}
|
||||
.empty{color:var(--ash);font-size:var(--t-base);padding:10px 0}
|
||||
|
||||
/* Home: the balance first, its money line, the address, the two buttons */
|
||||
.hero-card{display:flex;flex-direction:column;gap:var(--s-3)}
|
||||
.bal-row{display:flex;align-items:baseline;gap:var(--s-4);flex-wrap:wrap;min-width:0}
|
||||
.bal{display:flex;align-items:baseline;gap:12px;min-width:0}
|
||||
.bal .v{font-family:var(--head);font-weight:900;font-size:var(--t-h1);letter-spacing:-.01em;line-height:1;white-space:nowrap;user-select:text;-webkit-user-select:text}
|
||||
.bal .unit{font-size:var(--t-md);color:var(--ash);letter-spacing:.12em}
|
||||
.bal-line{color:var(--ash);font-size:var(--t-md)}
|
||||
.bal-line:empty{display:none}
|
||||
.money-line{font-size:var(--t-md);color:var(--ash);margin-top:-4px}
|
||||
.money-line b{color:var(--bone);font-family:var(--head);font-weight:700;font-size:var(--t-num);margin-right:6px}
|
||||
.addr-big{display:flex;align-items:center;gap:12px;background:var(--obsidian);border:1px solid var(--line-2);border-radius:12px;padding:14px 16px;font-size:var(--t-lg);word-break:break-all;user-select:text;-webkit-user-select:text}
|
||||
.addr-big span{flex:1;min-width:0;color:var(--molten)}
|
||||
.actions{margin-top:var(--s-1)}
|
||||
.actions .note{margin-left:var(--s-2)}
|
||||
.node-line{display:inline-flex;align-items:center;gap:10px;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.node-card.bad .node-line{color:var(--ember)}
|
||||
|
||||
/* the history row: kind and who, the amount, the state word with its reason, the chevron; the details under */
|
||||
.hist{display:flex;flex-direction:column;gap:var(--s-2)}
|
||||
.hrow{border:1px solid var(--line);border-radius:var(--row-r);background:var(--row);min-width:0}
|
||||
.hrow.open{border-color:var(--line-2)}
|
||||
.hr-main{display:grid;grid-template-columns:26px minmax(160px,1.3fr) auto minmax(180px,1fr) 30px;align-items:center;gap:var(--s-4);padding:11px 14px;cursor:pointer}
|
||||
.hr-main .glyph{width:26px;height:26px;border-radius:8px;border:1px solid var(--line-2);display:flex;align-items:center;justify-content:center;color:var(--ash);font-family:var(--mono);font-size:var(--t-sm)}
|
||||
.hrow.in .glyph{color:var(--molten);border-color:var(--molten-40)}
|
||||
.hr-who{min-width:0;display:flex;flex-direction:column;gap:2px}
|
||||
.hr-who .kind{font-weight:600;font-size:var(--t-md);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.hr-who .sub{font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.hr-amt{font-family:var(--head);font-weight:700;font-size:var(--t-lg);white-space:nowrap;text-align:right;justify-self:end}
|
||||
.hrow.in .hr-amt{color:var(--molten)}
|
||||
.hr-amt .unit{font-family:var(--mono);font-size:10px;color:var(--ash);font-weight:500;letter-spacing:.08em;margin-left:4px}
|
||||
.hr-state{min-width:0;display:flex;flex-direction:column;gap:2px}
|
||||
.hr-state .st{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);white-space:nowrap}
|
||||
.hr-state .st i{width:7px;height:7px;border-radius:50%;background:currentColor;display:inline-block;flex:0 0 7px}
|
||||
.hr-state .st.ink{color:var(--ink-2)}
|
||||
.hr-state .st.ok{color:var(--molten)}
|
||||
.hr-state .st.bad{color:var(--ember)}
|
||||
.hr-state .why{font-size:var(--t-sm);color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.chev-btn{width:30px;height:30px;border-radius:8px;border:1px solid transparent;background:transparent;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;color:var(--ash);justify-self:end}
|
||||
.chev-btn:hover{border-color:var(--line-2);color:var(--bone)}
|
||||
.hr-details{padding:12px 14px 14px 56px;border-top:1px solid var(--line);user-select:text;-webkit-user-select:text}
|
||||
.hr-details .kv>div{grid-template-columns:110px 1fr}
|
||||
.hr-details .kv .v{white-space:normal;word-break:break-all}
|
||||
.hrow.sent-row .hr-main{cursor:default;grid-template-columns:26px minmax(160px,1.3fr) auto minmax(180px,1fr)}
|
||||
|
||||
/* Send: the fee row and the pending row */
|
||||
.send-form{display:flex;flex-direction:column;gap:var(--s-3)}
|
||||
.unit-word{font-size:var(--t-sm);color:var(--ash);letter-spacing:.1em}
|
||||
.fee-row{display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap;padding:8px 0 0}
|
||||
.fee-row .k{font-size:var(--t-xs);letter-spacing:.12em;text-transform:uppercase;color:var(--ash)}
|
||||
.send-form[data-locked="1"] input{opacity:.6;pointer-events:none}
|
||||
.sent{display:flex;flex-direction:column;gap:var(--s-3);margin-top:var(--s-3)}
|
||||
.sent .cta{justify-content:flex-start;margin-top:0}
|
||||
|
||||
/* Receive: the QR beside its line */
|
||||
.qr-row{display:flex;gap:var(--s-5);align-items:flex-start;flex-wrap:wrap;margin-top:var(--s-4)}
|
||||
.qr{width:240px;height:240px;background:var(--qr-bg);border-radius:14px;padding:10px;flex:0 0 240px}
|
||||
.qr svg{width:100%;height:100%;display:block}
|
||||
.qr-row .help{flex:1 1 240px;padding-top:var(--s-2)}
|
||||
|
||||
/* the update card (update-card.js, app.js renderUpdateCard; the miner carries the same block) */
|
||||
.upd-wrap{position:fixed;inset:0;z-index:35;background:var(--scrim);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px;animation:fade .25s ease}
|
||||
@keyframes fade{from{opacity:0}to{opacity:1}}
|
||||
.upd-card{position:relative;width:100%;max-width:500px;max-height:calc(100vh - 48px);overflow:auto;background:var(--graphite);border:1px solid var(--line-2);border-radius:22px;padding:34px 32px 28px;display:flex;flex-direction:column;align-items:center;text-align:center;gap:var(--s-2);box-shadow:0 30px 80px var(--shadow),0 0 0 1px rgba(242,84,27,.08);animation:rise .3s ease;outline:none}
|
||||
.upd-card::before{content:"";position:absolute;left:50%;top:-80px;width:360px;height:260px;transform:translateX(-50%);border-radius:50%;background:radial-gradient(circle,rgba(242,84,27,.22) 0,rgba(242,84,27,0) 62%);pointer-events:none}
|
||||
.upd-mark{position:relative;width:96px;height:96px;display:flex;align-items:center;justify-content:center;margin-bottom:var(--s-3)}
|
||||
.upd-mark img{position:relative;display:block;filter:drop-shadow(0 6px 18px rgba(242,84,27,.35))}
|
||||
.upd-ring{position:absolute;inset:0;transform:rotate(-90deg)}
|
||||
.upd-ring .track{fill:none;stroke:var(--line-2);stroke-width:3}
|
||||
.upd-ring .arc{fill:none;stroke:var(--ember);stroke-width:3;stroke-linecap:round;stroke-dasharray:276.5;stroke-dashoffset:276.5;transition:stroke-dashoffset .4s linear,stroke .3s ease}
|
||||
.upd-mark.none .track{stroke:var(--line)}
|
||||
.upd-mark.full .arc{stroke:var(--molten);stroke-dashoffset:0}
|
||||
.upd-mark.busy .arc{stroke-dasharray:69 207.5;stroke-dashoffset:0;animation:spin 1.1s linear infinite;transform-origin:50% 50%}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
.upd-mark.failed .arc{stroke:var(--ember);stroke-dashoffset:0;opacity:.55}
|
||||
.upd-pct{position:absolute;left:50%;bottom:-11px;transform:translateX(-50%);font-size:var(--t-xs);font-weight:500;letter-spacing:.06em;color:var(--molten);background:var(--obsidian);border:1px solid var(--line-2);border-radius:999px;padding:2px 8px;font-variant-numeric:tabular-nums}
|
||||
.upd-name{font-size:24px;font-weight:700;letter-spacing:-.01em;position:relative}
|
||||
.upd-line{font-size:var(--t-xl);color:var(--ink-2);line-height:1.4;position:relative}
|
||||
.upd-cause{font-size:var(--t-sm);color:var(--ember);line-height:1.5;max-width:40ch;word-break:break-word}
|
||||
.upd-notes{list-style:none;margin:var(--s-2) auto 0;padding:0;width:max-content;max-width:100%;display:flex;flex-direction:column;gap:6px;align-items:flex-start;font-size:var(--t-md);color:var(--bone);line-height:1.4}
|
||||
.upd-notes:empty{display:none}
|
||||
.upd-notes li{display:flex;align-items:baseline;gap:10px;text-align:left}
|
||||
.upd-notes li::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--ember);flex:0 0 6px;position:relative;top:-2px}
|
||||
.upd-more{font:inherit;font-size:var(--t-sm);font-family:var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ash);background:transparent;border:0;padding:4px 8px;cursor:pointer;border-radius:6px;margin-top:2px}
|
||||
.upd-more:hover{color:var(--bone)}
|
||||
.upd-all{list-style:none;margin:0;padding:10px 14px;width:100%;max-height:150px;overflow:auto;text-align:left;font-size:var(--t-base);color:var(--ink-2);line-height:1.5;background:var(--obsidian);border:1px solid var(--line);border-radius:12px;display:flex;flex-direction:column;gap:4px;user-select:text;-webkit-user-select:text}
|
||||
.upd-meta{font-size:var(--t-sm);color:var(--ash);letter-spacing:.04em;margin-top:var(--s-2);min-height:18px;line-height:1.5;max-width:44ch}
|
||||
.upd-meta:empty{display:none}
|
||||
.upd-actions{display:flex;gap:var(--s-3);align-items:center;justify-content:center;flex-wrap:wrap;margin-top:var(--s-4);position:relative}
|
||||
.upd-actions:empty{display:none}
|
||||
.upd-actions .btn.primary{min-width:160px}
|
||||
@media (prefers-reduced-motion:reduce){.upd-wrap,.upd-card{animation:none}.upd-ring .arc{transition:none}.upd-mark.busy .arc{animation:none;stroke-dasharray:207.5 69}}
|
||||
@media (max-height:620px){.upd-card{padding:24px 24px 20px}.upd-mark{width:72px;height:72px;margin-bottom:var(--s-2)}.upd-mark img{width:32px;height:32px}.upd-name{font-size:20px}}
|
||||
|
||||
/* the lock screen (0.1.4, kept; ui/lock-screen.js, index.html #screen-unlock): the mark on the ember glow, the name,
|
||||
the short address, one primary control. It lies over the main area (the header stays), enters in ENTER_MS (250 ms)
|
||||
and the home screen leaves beneath it in the same 250 ms. The glow breathes slowly; reduced motion stops it. */
|
||||
#screen-unlock{position:absolute;inset:0;z-index:5;max-width:none;margin:0;padding:0 var(--gutter);background:var(--obsidian);overflow:auto;animation:lockin .25s ease both}
|
||||
body.locking #screen-unlock{display:block}
|
||||
body.locking #screen-home{animation:lockout .25s ease both;pointer-events:none}
|
||||
@keyframes lockin{from{opacity:0;transform:scale(1.015)}to{opacity:1;transform:none}}
|
||||
@keyframes lockout{to{opacity:0;transform:scale(.985);filter:blur(4px)}}
|
||||
.lock-body{min-height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:10px;padding:28px 0 36px}
|
||||
.lock-coin{position:relative;width:180px;height:180px;margin-bottom:22px;flex:0 0 auto;display:flex;align-items:center;justify-content:center}
|
||||
.lock-coin::before{content:"";position:absolute;inset:-190px;border-radius:50%;background:radial-gradient(circle,rgba(255,179,92,.10) 0,rgba(242,84,27,.05) 40%,rgba(242,84,27,0) 68%);pointer-events:none}
|
||||
.lock-glow{position:absolute;inset:-96px;border-radius:50%;background:radial-gradient(circle,rgba(242,84,27,.38) 0,rgba(242,84,27,.14) 36%,rgba(242,84,27,0) 66%);animation:lockbreathe 6s ease-in-out infinite;pointer-events:none}
|
||||
@keyframes lockbreathe{0%,100%{opacity:.75;transform:scale(1)}50%{opacity:1;transform:scale(1.06)}}
|
||||
.lock-mark{width:148px;height:148px;border-radius:32px;margin:0;box-shadow:0 18px 48px rgba(242,84,27,.45)}
|
||||
.lock-mark::before{display:none}
|
||||
.lock-title{font-family:var(--head);font-weight:700;font-size:30px;letter-spacing:-.01em;line-height:1.1;margin-top:4px}
|
||||
.lock-address{font-size:var(--t-md);color:var(--ash);letter-spacing:.06em;margin-top:2px}
|
||||
.lock-controls{display:flex;flex-direction:column;align-items:center;gap:10px;margin-top:22px;min-height:120px}
|
||||
.lock-controls.touch{min-height:200px;justify-content:flex-start}
|
||||
.lock-touch{display:flex;flex-direction:column;align-items:center;gap:10px}
|
||||
.lock-btn{min-width:272px;gap:10px}
|
||||
.lock-line{min-height:20px;font-family:var(--mono);font-size:var(--t-sm);letter-spacing:.04em;color:var(--ash);text-align:center;max-width:60ch;line-height:1.5;text-wrap:balance}
|
||||
.lock-line .bio-state{display:inline}
|
||||
.lock-line .fp-glyph{display:inline-block;vertical-align:-3px;margin-right:6px}
|
||||
.lock-link{font:inherit;font-size:var(--t-base);color:var(--ash);background:transparent;border:0;cursor:pointer;padding:6px 10px;border-radius:6px;text-decoration:underline;text-underline-offset:4px;text-decoration-color:var(--line-2);transition:color .15s ease}
|
||||
.lock-link:hover{color:var(--bone);text-decoration-color:var(--ash)}
|
||||
.unlock{display:flex;gap:10px;align-items:center;margin-top:6px}
|
||||
.unlock input{font:inherit;color:var(--bone);background:var(--graphite);border:1px solid var(--line-2);border-radius:10px;padding:12px 14px;min-width:280px;min-height:52px;user-select:text;-webkit-user-select:text}
|
||||
.unlock input:focus{outline:none;border-color:var(--ember)}
|
||||
.lock-form{margin-top:0;animation:rise .2s ease}
|
||||
.lock-form input{min-width:260px}
|
||||
.lock-err{min-height:0}
|
||||
.lock-foot{margin-top:18px;opacity:.85}
|
||||
@media (max-height:720px){.lock-coin{width:140px;height:140px;margin-bottom:14px}.lock-mark{width:112px;height:112px;border-radius:26px}.lock-glow{inset:-70px}.lock-coin::before{inset:-150px}.lock-title{font-size:26px}.lock-controls{margin-top:14px}.lock-body{padding:16px 0 24px}}
|
||||
@media (prefers-reduced-motion:reduce){#screen-unlock,body.locking #screen-home,.lock-form{animation:none}.lock-glow{animation:none}}
|
||||
|
||||
/* Touch ID lines */
|
||||
.bio-state{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:var(--t-sm);letter-spacing:.04em;color:var(--ember);min-height:18px}
|
||||
.bio-state.ok{color:var(--molten)}
|
||||
.bio-state.wait{color:var(--ash)}
|
||||
.fp-glyph{flex:0 0 auto;color:var(--ember)}
|
||||
#send-go .fp-ico[hidden]{display:none}
|
||||
|
||||
.toast{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);background:var(--graphite);border:1px solid var(--line-2);border-radius:10px;padding:10px 16px;font-size:var(--t-base);z-index:40;box-shadow:0 10px 30px var(--shadow);max-width:min(90vw,520px);text-align:center}
|
||||
body.has-rail .toast{left:calc(50% + var(--rail) / 2)}
|
||||
|
||||
/* narrow windows: 1000 the rail folds to icons; 900 the row's reason goes under the word; 720 a bottom tab bar */
|
||||
@media (max-width:1000px){
|
||||
:root{--rail:76px;--gutter:var(--s-5)}
|
||||
.rail{padding:12px 8px}
|
||||
.rail-brand{justify-content:center;padding:6px 0 14px}
|
||||
.rail-brand .word{display:none}
|
||||
.nav{flex-direction:column;gap:4px;padding:8px 4px;font-size:10px;letter-spacing:.06em;text-transform:uppercase;font-family:var(--mono);font-weight:500;text-align:center;min-height:52px;justify-content:center}
|
||||
.nav.on{font-weight:500}
|
||||
.nav.on::before{left:-9px}
|
||||
.nav.small{min-height:44px}
|
||||
.nav-dot{right:8px;top:8px;margin:0}
|
||||
.rail-status{display:none}
|
||||
.rail-version{text-align:center;padding:8px 0 0;font-size:10px;white-space:nowrap}
|
||||
.page-sub{display:none}
|
||||
.kv>div{grid-template-columns:120px auto 1fr}
|
||||
.hr-main{grid-template-columns:26px minmax(140px,1fr) auto 30px}
|
||||
.hr-state{grid-column:2 / 4;flex-direction:row;align-items:baseline;gap:var(--s-3)}
|
||||
}
|
||||
@media (max-width:860px){
|
||||
.three{grid-template-columns:1fr}
|
||||
h1{font-size:40px}
|
||||
.bal .v{font-size:var(--t-hero)}
|
||||
.words{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||||
.words.small{grid-template-columns:repeat(4,minmax(0,1fr))}
|
||||
.disclose-right .dim{max-width:30ch}
|
||||
}
|
||||
@media (max-width:720px){
|
||||
:root{--rail:0px;--gutter:var(--s-4);--bottom:64px}
|
||||
body.has-rail .rail{top:auto;bottom:0;left:0;right:0;width:auto;height:64px;flex-direction:row;align-items:center;border-right:0;border-top:1px solid var(--line);padding:0 var(--s-2)}
|
||||
.rail-brand{display:none}
|
||||
.rail-nav{flex-direction:row;flex:1;gap:0;justify-content:space-around}
|
||||
.nav{min-height:56px;padding:6px 2px;font-size:10px;border-radius:10px;flex:1;max-width:120px}
|
||||
.nav.on::before{display:none}
|
||||
.rail-foot{display:none}
|
||||
body.has-rail .narrow-only{display:inline-flex}
|
||||
body.has-rail .top,body.has-rail .notices,body.has-rail main,body.has-rail .ask-wrap{left:0}
|
||||
body.has-rail main{bottom:var(--bottom)}
|
||||
body.has-rail .toast{left:50%;bottom:calc(var(--bottom) + 12px)}
|
||||
.ask-wrap{bottom:var(--bottom)}
|
||||
.top{padding:0 var(--s-4)}
|
||||
.page-title h1{font-size:17px}
|
||||
.pill{min-width:0}
|
||||
.bal-row{flex-direction:column;gap:4px;align-items:flex-start}
|
||||
.bal .v{font-size:var(--t-h2)}
|
||||
.actions .btn.big{flex:1 1 40%}
|
||||
.actions .note{flex-basis:100%;margin-left:0}
|
||||
.addr-big{font-size:var(--t-base)}
|
||||
.hr-main{grid-template-columns:26px 1fr 30px;gap:var(--s-2) var(--s-3);padding:12px}
|
||||
.hr-amt{grid-column:1 / 3;grid-row:2;justify-self:start;text-align:left}
|
||||
.hr-state{grid-column:1 / -1;grid-row:3;flex-direction:column;align-items:flex-start;gap:2px}
|
||||
.hr-state .why{white-space:normal}
|
||||
.hr-details{padding-left:12px;padding-right:12px}
|
||||
.hrow.sent-row .hr-main{grid-template-columns:26px 1fr}
|
||||
.kv>div{grid-template-columns:96px 1fr;gap:4px var(--s-3)}
|
||||
.kv .m{grid-column:1 / -1;white-space:normal}
|
||||
.disclose-right .dim{display:none}
|
||||
.node-line{white-space:normal;line-height:1.4}
|
||||
.lead-row{flex-direction:column}
|
||||
.srow{flex-direction:column;align-items:flex-start;gap:var(--s-2)}
|
||||
.indent{margin-left:0}
|
||||
.seg{width:100%}
|
||||
.seg-b{flex:1;padding:6px 8px}
|
||||
.step{padding:32px 0}
|
||||
.words{grid-template-columns:repeat(2,minmax(0,1fr))}
|
||||
.words.small{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||||
.checks{grid-template-columns:1fr}
|
||||
.qr{width:200px;height:200px;flex-basis:200px}
|
||||
.ask .short-pw{flex:1 1 160px;width:auto}
|
||||
.lock-btn{min-width:0;width:100%}
|
||||
.unlock input,.lock-form input{min-width:0;flex:1}
|
||||
.unlock{width:100%}
|
||||
}
|
||||
/* short windows (the 620 px floor): tighter paddings, so the hero and the rows fit */
|
||||
@media (max-height:700px){
|
||||
:root{--card-pad:18px;--gap:var(--s-3)}
|
||||
#screen-home{padding:16px 0 20px}
|
||||
.hero{gap:var(--s-3);padding:var(--s-5) 0 var(--s-6)}
|
||||
.mark-wrap{width:88px;height:88px}
|
||||
.mark-wrap img{width:56px;height:56px}
|
||||
h1{font-size:40px}
|
||||
.lead{font-size:var(--t-lg)}
|
||||
.step{padding:32px 0 32px}
|
||||
}
|
||||
|
||||
/* the page bridge (0.1.6): a website's request as a card over the window; the Settings card's site rows */
|
||||
.bridge-card{max-width:540px}
|
||||
.bridge-amount{font-family:var(--head);font-weight:700;font-size:28px;color:var(--ember);margin:4px 0 8px;letter-spacing:-.01em}
|
||||
.bridge-message{white-space:pre-wrap;word-break:break-word;max-height:220px;overflow:auto;background:var(--obsidian);border:1px solid var(--line);border-radius:10px;padding:10px 12px;font-size:12px;line-height:1.5;color:var(--ink-2);margin:6px 0 10px;text-align:left}
|
||||
.bridge-done{margin-top:12px;text-align:left}
|
||||
.bridge-done .kv{margin-top:8px}
|
||||
#bridge-origin{margin-top:6px}
|
||||
.sites{display:flex;flex-direction:column}
|
||||
.sites .srow .sw-text b{font-weight:600}
|
||||
.sites .srow .sw-text .dim{display:block;margin-top:2px}
|
||||
.lock-line.bridge-waiting{color:var(--molten);margin-top:4px}
|
||||
1057
app/igneum-wallet/ui/app.js
Normal file
1057
app/igneum-wallet/ui/app.js
Normal file
File diff suppressed because it is too large
Load diff
433
app/igneum-wallet/ui/index.html
Normal file
433
app/igneum-wallet/ui/index.html
Normal file
|
|
@ -0,0 +1,433 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Igneum Wallet</title>
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<link rel="icon" href="mark.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="app.css">
|
||||
</head>
|
||||
<body data-phase="welcome" data-page="home">
|
||||
<svg width="0" height="0" style="position:absolute" aria-hidden="true"><symbol id="i-fp" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4.8 7.7A9.2 9.2 0 0 1 12 4.1c1.5 0 2.9.3 4.1.9"/><path d="M5.2 16.6A12.5 12.5 0 0 1 4.6 12a7.4 7.4 0 0 1 14.8 0v4"/><path d="M8.5 20a9 9 0 0 1-1.3-4.7V12a4.8 4.8 0 0 1 9.6 0v1.5"/><path d="M12 11a1 1 0 0 1 1 1v2a6 6 0 0 1-1 3.3"/><path d="M15.6 20a10 10 0 0 0 .9-2.6"/></g></symbol></svg>
|
||||
|
||||
<!-- the rail: five sections (wallet-ui-3: Home, Send, Receive, History, Settings), Lock and Quit in the foot. Under
|
||||
720 px it is a bottom tab bar. The welcome, create, import and lock screens have no rail. -->
|
||||
<aside class="rail" id="rail" hidden>
|
||||
<div class="rail-brand">
|
||||
<img src="mark.svg" width="28" height="28" alt="">
|
||||
<span class="word">IGNEUM</span>
|
||||
</div>
|
||||
<nav class="rail-nav" id="rail-nav" aria-label="Sections">
|
||||
<button class="nav on" data-page="home" aria-current="page"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 11 12 3l9 8"/><path d="M5 10v10h14V10"/></svg><span>Home</span></button>
|
||||
<button class="nav" data-page="send"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 19V5"/><path d="m5 12 7-7 7 7"/></svg><span>Send</span></button>
|
||||
<button class="nav" data-page="receive"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14"/><path d="m19 12-7 7-7-7"/></svg><span>Receive</span></button>
|
||||
<button class="nav" data-page="history"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg><span>History</span></button>
|
||||
<button class="nav" data-page="settings"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h10M18 7h2M4 17h4M12 17h8"/><circle cx="16" cy="7" r="2"/><circle cx="10" cy="17" r="2"/></svg><span>Settings</span><i class="nav-dot" id="nav-updates-dot" hidden></i></button>
|
||||
</nav>
|
||||
<div class="rail-foot">
|
||||
<div class="rail-status mono" id="rail-status"></div>
|
||||
<button class="nav small" id="btn-lock"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg><span>Lock</span></button>
|
||||
<button class="nav small danger" id="btn-quit"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v9"/><path d="M6.5 6.5a8 8 0 1 0 11 0"/></svg><span>Quit</span></button>
|
||||
<div class="rail-version mono" id="foot-version"></div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<header class="top">
|
||||
<div class="brand" id="top-brand">
|
||||
<img src="mark.svg" width="30" height="30" alt="">
|
||||
<span class="word">IGNEUM</span><span class="miner">WALLET</span>
|
||||
</div>
|
||||
<div class="page-title" id="page-title" hidden>
|
||||
<h1 id="page-title-text">Home</h1>
|
||||
<span class="page-sub" id="page-sub"></span>
|
||||
</div>
|
||||
<div class="top-right">
|
||||
<div class="pill" id="pill"><span class="dot"></span><span id="pill-text">starting</span></div>
|
||||
<button class="btn small ghost narrow-only" id="btn-lock-top" hidden>Lock</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- the status strip: the update line, one notice at a time -->
|
||||
<div class="notices" id="notices" hidden>
|
||||
<div class="notice" id="notice" role="status" aria-live="polite">
|
||||
<span class="notice-text" id="notice-text"></span>
|
||||
<span class="notice-actions" id="notice-actions"></span>
|
||||
<button class="notice-close" id="notice-close" title="Close" aria-label="Close">×</button>
|
||||
<span class="prog" id="notice-prog" hidden><i></i></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- the quit question, in place of a dialog -->
|
||||
<div class="ask-wrap" id="ask-quit" hidden>
|
||||
<div class="ask">
|
||||
<span class="ask-text">Quit Igneum Wallet? The bundled node stops with it.</span>
|
||||
<button class="btn small primary" id="ask-quit-yes">Quit</button>
|
||||
<button class="btn small ghost" id="ask-quit-no">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main id="main">
|
||||
|
||||
<!-- welcome -->
|
||||
<section class="screen" id="screen-welcome">
|
||||
<div class="hero">
|
||||
<div class="mark-wrap"><img src="mark.svg" width="72" height="72" alt=""></div>
|
||||
<div class="eyebrow ember" id="welcome-eyebrow">devnet v4 · nothing is bought or sold</div>
|
||||
<h1>Igneum Wallet</h1>
|
||||
<p class="lead">Your IGN and your key, on this machine. The key is made here and never leaves this app.</p>
|
||||
<div class="three">
|
||||
<div class="tile"><div class="k">01</div><div class="t">Your key stays here</div><div class="s">Sealed with a password you choose. Signing happens inside the app.</div></div>
|
||||
<div class="tile"><div class="k">02</div><div class="t">Final means verified</div><div class="s">A payment is final when this wallet has checked the network's certificate itself.</div></div>
|
||||
<div class="tile"><div class="k">03</div><div class="t">Works with the miner</div><div class="s">Uses the miner app's node when it runs here. Imports the miner's key in one tap.</div></div>
|
||||
</div>
|
||||
<div class="cta">
|
||||
<button class="btn primary big" id="go-create">Create a wallet</button>
|
||||
<button class="btn big" id="go-import">Import</button>
|
||||
</div>
|
||||
<p class="seedline mono">Nobody from Igneum will ever ask for your words or your key.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- create -->
|
||||
<section class="screen" id="screen-create">
|
||||
<div class="step" id="create-1">
|
||||
<div class="eyebrow">step 1 of 3</div>
|
||||
<h2>Choose a password</h2>
|
||||
<p class="sub">It locks the key on this machine. Nobody can reset it for you. At least 8 characters.</p>
|
||||
<label class="field"><span class="k">Password</span><input type="password" id="create-pw" autocomplete="new-password"></label>
|
||||
<label class="field"><span class="k">Again</span><input type="password" id="create-pw2" autocomplete="new-password"></label>
|
||||
<div class="err" id="create-err"></div>
|
||||
<div class="cta"><button class="btn primary big" id="create-next">Make my words</button><button class="btn ghost" id="create-back">Back</button></div>
|
||||
</div>
|
||||
<div class="step" id="create-2" hidden>
|
||||
<div class="eyebrow">step 2 of 3</div>
|
||||
<h2>Write these 24 words down</h2>
|
||||
<p class="sub">They are the wallet. Anyone with them has your IGN. They are shown once.</p>
|
||||
<ol class="words" id="words"></ol>
|
||||
<div class="field"><div class="k">your address</div><div class="box mono"><span id="create-address"></span><button class="btn tiny" data-copy="create-address">Copy</button></div></div>
|
||||
<div class="cta"><button class="btn primary big" id="create-written">I wrote them down</button></div>
|
||||
</div>
|
||||
<div class="step" id="create-3" hidden>
|
||||
<div class="eyebrow">step 3 of 3</div>
|
||||
<h2>Type three of them</h2>
|
||||
<p class="sub">So the paper is right before the words are gone from the screen.</p>
|
||||
<div class="checks" id="checks"></div>
|
||||
<div class="err" id="confirm-err"></div>
|
||||
<div class="cta"><button class="btn primary big" id="create-confirm">Open my wallet</button><button class="btn ghost" id="create-again">Show the words again</button></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- import -->
|
||||
<section class="screen" id="screen-import">
|
||||
<div class="step">
|
||||
<div class="eyebrow">import</div>
|
||||
<h2>Bring a key here</h2>
|
||||
<p class="sub">Words from any wallet, a raw private key, or the key the miner app made on this machine.</p>
|
||||
<div class="seg" id="import-mode" role="radiogroup" aria-label="What to import">
|
||||
<button class="seg-b on" data-mode="seed" role="radio" aria-checked="true">24 words</button>
|
||||
<button class="seg-b" data-mode="key" role="radio" aria-checked="false">Private key</button>
|
||||
<button class="seg-b" data-mode="miner" id="seg-miner" role="radio" aria-checked="false">Miner's key</button>
|
||||
</div>
|
||||
<label class="field" id="import-data-field"><span class="k" id="import-data-label">The words, in order</span><textarea id="import-data" rows="4" spellcheck="false" autocomplete="off"></textarea></label>
|
||||
<p class="note" id="import-miner-note" hidden>The miner app's key file on this machine will be read. Rewards keep going to the same address.</p>
|
||||
<label class="field"><span class="k">New password for this machine</span><input type="password" id="import-pw" autocomplete="new-password"></label>
|
||||
<label class="field"><span class="k">Again</span><input type="password" id="import-pw2" autocomplete="new-password"></label>
|
||||
<div class="err" id="import-err"></div>
|
||||
<div class="cta"><button class="btn primary big" id="import-go">Import</button><button class="btn ghost" id="import-back">Back</button></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- the lock screen (lock-screen.js, app.js onLockScreen; 0.1.4, kept): the mark on the ember glow, the name, the
|
||||
short address, one primary control. -->
|
||||
<section class="screen lock" id="screen-unlock">
|
||||
<div class="lock-body">
|
||||
<div class="lock-coin"><span class="lock-glow" aria-hidden="true"></span><div class="mark-wrap lock-mark"><img src="mark.svg" width="88" height="88" alt=""></div></div>
|
||||
<h1 class="lock-title">Igneum Wallet</h1>
|
||||
<p class="lock-address mono" id="unlock-address"></p>
|
||||
<p class="lock-line bridge-waiting" id="unlock-bridge" hidden></p>
|
||||
<div class="lock-controls">
|
||||
<div class="lock-touch" id="unlock-bio" hidden>
|
||||
<button class="btn primary big fp lock-btn" id="unlock-touch" type="button"><svg width="22" height="22" aria-hidden="true"><use href="#i-fp"></use></svg><span id="unlock-touch-text">Unlock with Touch ID</span></button>
|
||||
<div class="lock-line" id="unlock-bio-note" aria-live="polite"></div>
|
||||
<button class="lock-link" id="unlock-use-pw" type="button">Use password</button>
|
||||
</div>
|
||||
<form id="unlock-form" class="unlock lock-form" hidden>
|
||||
<input type="password" id="unlock-pw" placeholder="Password" autocomplete="current-password" aria-label="Password">
|
||||
<button class="btn primary big" type="submit" id="unlock-submit">Unlock</button>
|
||||
</form>
|
||||
<div class="err lock-err" id="unlock-err"></div>
|
||||
</div>
|
||||
<p class="seedline lock-foot">Forgot it? Only the 24 words or the key open this wallet again.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- home: five pages behind the rail -->
|
||||
<section class="screen" id="screen-home">
|
||||
|
||||
<!-- Home -->
|
||||
<section class="page" id="page-home" data-page="home">
|
||||
<div class="card hero-card">
|
||||
<div class="bal-row">
|
||||
<div class="bal" id="h-bal"><span class="v" id="h-balance">0</span><span class="unit mono">IGN</span></div>
|
||||
<p class="line-text bal-line" id="h-balance-line"></p>
|
||||
</div>
|
||||
<p class="money-line" id="h-money"></p>
|
||||
<div class="addr-big mono"><span id="home-address">not set</span><button class="btn small" data-copy="home-address">Copy</button></div>
|
||||
<div class="row wrap actions">
|
||||
<button class="btn primary big" id="go-send">Send</button>
|
||||
<button class="btn big" id="go-receive">Receive</button>
|
||||
<span class="note" id="backup-note" hidden>Your words are not written down yet. <a href="#" id="backup-link">Back up now</a>.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card node-card" id="node-card">
|
||||
<button class="disclose" id="node-toggle" aria-expanded="false" aria-controls="node-details">
|
||||
<span class="node-line"><i class="dot" id="node-dot"></i><span id="node-line-text">Node starting</span></span>
|
||||
<span class="disclose-right"><span class="dim" id="node-sub"></span><span class="chev" aria-hidden="true"></span></span>
|
||||
</button>
|
||||
<div class="details" id="node-details" hidden>
|
||||
<div class="kv" id="node-kv"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Recent</h3><button class="btn small ghost" id="recent-all">All history</button></div>
|
||||
<div class="hist" id="recent"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Send -->
|
||||
<section class="page" id="page-send" data-page="send" hidden>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Send IGN</h3><span class="eyebrow" id="send-eyebrow"></span></div>
|
||||
<div class="send-form" id="send-form">
|
||||
<label class="field"><span class="k">To</span><input type="text" id="send-to" class="mono" placeholder="0x" spellcheck="false" autocomplete="off"></label>
|
||||
<label class="field"><span class="k">Amount</span><div class="row"><input type="text" id="send-amount" class="mono" placeholder="0.0" inputmode="decimal" autocomplete="off"><span class="unit-word mono">IGN</span><span class="note" id="send-balance"></span></div></label>
|
||||
<div class="fee-row">
|
||||
<span class="k mono">fee</span>
|
||||
<span class="line-text" id="fee-line">shown when you review</span>
|
||||
<button class="btn tiny ghost" id="fee-toggle" aria-expanded="false" aria-controls="fee-details" hidden>Details</button>
|
||||
</div>
|
||||
<p class="help" id="fee-details" hidden></p>
|
||||
<div class="err" id="send-err"></div>
|
||||
<div class="cta" id="send-cta"><button class="btn primary big" id="send-quote">Review</button></div>
|
||||
</div>
|
||||
<div class="ask inline" id="ask-send" hidden>
|
||||
<span class="ask-text" id="ask-send-text"></span>
|
||||
<button class="btn small primary" id="send-go"><svg class="fp-ico" width="16" height="16" hidden><use href="#i-fp"></use></svg><span id="send-go-text">Send now</span></button>
|
||||
<button class="btn small ghost" id="ask-send-no">Cancel</button>
|
||||
<span class="ask-foot" id="send-bio-line"></span>
|
||||
<span class="ask-foot err" id="confirm-send-err"></span>
|
||||
</div>
|
||||
<div class="sent" id="send-done" hidden>
|
||||
<div class="hrow sent-row" id="sent-row"></div>
|
||||
<div class="box mono"><span id="sent-hash"></span><button class="btn tiny" data-copy="sent-hash">Copy</button></div>
|
||||
<div class="cta"><button class="btn small" id="sent-view">View in History</button><button class="btn small ghost" id="sent-again">Send another</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Receive -->
|
||||
<section class="page" id="page-receive" data-page="receive" hidden>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Your address</h3></div>
|
||||
<div class="addr-big mono"><span id="receive-address">not set</span><button class="btn small" data-copy="receive-address">Copy</button></div>
|
||||
<div class="qr-row">
|
||||
<div class="qr" id="qr"></div>
|
||||
<p class="help">Only IGN on the Igneum network. Rewards from the miner app land here when it pays this address. The code is drawn on this machine.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- History -->
|
||||
<section class="page" id="page-history" data-page="history" hidden>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>History</h3><span class="eyebrow" id="hist-eyebrow"></span></div>
|
||||
<div class="hist" id="history"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Settings -->
|
||||
<section class="page" id="page-settings" data-page="settings" hidden>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Security</h3></div>
|
||||
<div class="srow" id="bio-row">
|
||||
<div class="sw-text"><b id="bio-title">Touch ID</b> <span class="dim" id="bio-sentence"></span></div>
|
||||
<div class="row"><button class="btn small" id="bio-toggle-btn">Turn on</button></div>
|
||||
</div>
|
||||
<div class="details indent" id="bio-enrol" hidden>
|
||||
<div class="row wrap"><input type="password" class="addr-input" id="bio-pw" placeholder="Your password" autocomplete="current-password"><button class="btn small primary fp" id="bio-enrol-go"><svg width="16" height="16"><use href="#i-fp"></use></svg><span id="bio-enrol-text">Turn on Touch ID</span></button><button class="btn small ghost" id="bio-enrol-cancel">Cancel</button></div>
|
||||
</div>
|
||||
<p class="note indent" id="bio-off-note" hidden></p>
|
||||
<div class="err indent" id="bio-err"></div>
|
||||
<label class="switch" id="ask-on-open-row"><input type="checkbox" id="ask-on-open"><span class="track"></span><span class="sw-text"><b id="ask-on-open-text">Ask for Touch ID when the wallet opens</b> <span class="dim">Once, when you open the app. Never after an idle lock.</span></span></label>
|
||||
<div class="srow" id="idle-row">
|
||||
<div class="sw-text"><b>Lock when idle</b> <span class="dim" id="idle-lock-note"></span></div>
|
||||
<select class="select" id="idle-lock" aria-label="Lock when idle">
|
||||
<option value="1">after 1 minute</option>
|
||||
<option value="5">after 5 minutes</option>
|
||||
<option value="15">after 15 minutes</option>
|
||||
<option value="60">after 1 hour</option>
|
||||
<option value="0">never</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="disclose" id="pw-toggle" aria-expanded="false" aria-controls="pw-details"><span>Change password</span><span class="chev" aria-hidden="true"></span></button>
|
||||
<div class="details" id="pw-details" hidden>
|
||||
<div class="row wrap"><input type="password" class="addr-input" id="pw-old" placeholder="Current" autocomplete="current-password"><input type="password" class="addr-input" id="pw-new" placeholder="New, 8 or more" autocomplete="new-password"><button class="btn small" id="pw-change">Change</button></div>
|
||||
<p class="help">Touch ID is turned off by a password change; turn it on again above.</p>
|
||||
<div class="err" id="pw-err"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Websites</h3><span class="eyebrow" id="sites-eyebrow">page bridge</span></div>
|
||||
<label class="switch" id="bridge-on-row"><input type="checkbox" id="bridge-on"><span class="track"></span><span class="sw-text"><b>Let websites connect</b> <span class="dim" id="bridge-status"></span></span></label>
|
||||
<div class="sites" id="sites"></div>
|
||||
<p class="note" id="sites-note">A site asks once; you answer in this window. Each site sees your address and can ask you to sign or send; every request is shown here first. Disconnect a site any time.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Backup</h3><span class="eyebrow" id="backup-eyebrow"></span></div>
|
||||
<button class="disclose" id="backup-toggle" aria-expanded="false" aria-controls="backup-details"><span>Show my words</span><span class="chev" aria-hidden="true"></span></button>
|
||||
<div class="details" id="backup-details" hidden>
|
||||
<div class="ask inline" id="ask-backup">
|
||||
<span class="ask-text">Show the 24 words and the key on screen? Anyone who sees them has your IGN.</span>
|
||||
<button class="btn small primary fp" id="backup-touch" hidden><svg width="16" height="16"><use href="#i-fp"></use></svg><span>Show with Touch ID</span></button>
|
||||
<button class="btn small" id="backup-use-pw">Use password</button>
|
||||
<button class="btn small ghost" id="backup-cancel">Cancel</button>
|
||||
</div>
|
||||
<div class="row wrap" id="backup-pw-row" hidden><input type="password" class="addr-input" id="backup-pw" placeholder="Your password" autocomplete="current-password"><button class="btn small primary" id="backup-show">Show</button></div>
|
||||
<div class="err" id="backup-err"></div>
|
||||
<div id="backup-out" hidden>
|
||||
<ol class="words small" id="backup-words"></ol>
|
||||
<div class="field"><div class="k">private key</div><div class="box mono key"><span id="backup-key"></span><button class="btn tiny" data-copy="backup-key">Copy</button><button class="btn tiny ghost" id="backup-hide">Hide</button></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="disclose" id="export-toggle" aria-expanded="false" aria-controls="export-details"><span>Export the key for MetaMask</span><span class="chev" aria-hidden="true"></span></button>
|
||||
<div class="details" id="export-details" hidden>
|
||||
<p class="help">A copied key can be stolen from the clipboard, a screenshot or a notes app. Paste it straight into MetaMask and clear the clipboard.</p>
|
||||
<div class="ask inline" id="ask-export">
|
||||
<span class="ask-text">Show the private key on screen? Anyone who sees it has your IGN.</span>
|
||||
<button class="btn small primary fp" id="export-touch" hidden><svg width="16" height="16"><use href="#i-fp"></use></svg><span>Show with Touch ID</span></button>
|
||||
<button class="btn small" id="export-use-pw">Use password</button>
|
||||
<button class="btn small ghost" id="export-cancel">Cancel</button>
|
||||
</div>
|
||||
<div class="row wrap" id="export-pw-row" hidden><input type="password" class="addr-input" id="export-pw" placeholder="Your password" autocomplete="current-password"><button class="btn small primary" id="export-show">Show</button></div>
|
||||
<div class="err" id="export-err"></div>
|
||||
<div class="box mono key" id="export-out" hidden><span id="export-key"></span><button class="btn tiny" data-copy="export-key">Copy</button><button class="btn tiny ghost" id="export-hide">Hide</button></div>
|
||||
</div>
|
||||
|
||||
<div class="srow top-gap">
|
||||
<div class="sw-text"><b>MetaMask network</b> <span class="dim mono" id="net-line"></span></div>
|
||||
<div class="row"><button class="btn small" id="net-add">Add to MetaMask</button><button class="btn small ghost" id="net-copy">Copy settings</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>This machine</h3></div>
|
||||
<label class="switch"><input type="checkbox" id="start-login"><span class="track"></span><span class="sw-text"><b>Start at login</b> <span class="dim">Opens when you sign in. The wallet locks itself when idle.</span></span></label>
|
||||
<div class="field">
|
||||
<div class="k">appearance</div>
|
||||
<div class="seg" id="theme-seg" role="radiogroup" aria-label="Appearance">
|
||||
<button class="seg-b" data-theme="system" role="radio" aria-checked="true">System</button>
|
||||
<button class="seg-b" data-theme="light" role="radio" aria-checked="false">Light</button>
|
||||
<button class="seg-b" data-theme="dark" role="radio" aria-checked="false">Dark</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="lead-row">
|
||||
<div class="lead-text">
|
||||
<h3 id="s-version">Igneum Wallet</h3>
|
||||
<p class="help" id="s-update-note">Not checked yet.</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button class="btn small primary" id="s-install" hidden>Install now</button>
|
||||
<button class="btn small" id="s-update">Check</button>
|
||||
</div>
|
||||
</div>
|
||||
<label class="switch"><input type="checkbox" id="auto-update"><span class="track"></span><span class="sw-text"><b>Install updates by itself</b> <span class="dim">Downloads in the background and installs when nothing is being sent.</span></span></label>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Node</h3><span class="eyebrow" id="node-eyebrow"></span></div>
|
||||
<div class="field">
|
||||
<div class="k">endpoint in use</div>
|
||||
<div class="box mono"><span id="node-endpoint">none yet</span><button class="btn tiny" data-copy="node-endpoint">Copy</button></div>
|
||||
</div>
|
||||
<p class="line-text" id="node-source-line"></p>
|
||||
<p class="help">The wallet picks the miner's node when it runs on this machine, else its own bundled node, else the public RPC. Finality is verified only with a node here.</p>
|
||||
<p class="note mono small" id="node-ids"></p>
|
||||
</div>
|
||||
|
||||
<details class="card adv" id="s-advanced">
|
||||
<summary><h3>Advanced</h3><span class="eyebrow">this machine</span></summary>
|
||||
<p class="note mono small" id="s-mid"></p>
|
||||
<p class="note mono small" id="s-dirs"></p>
|
||||
<p class="note" id="s-miner-file"></p>
|
||||
<div class="row wrap top-gap"><button class="btn small danger" id="remove-start">Remove this wallet from this machine</button></div>
|
||||
<div class="ask inline" id="ask-remove" hidden>
|
||||
<span class="ask-text">Remove this wallet from this machine? The vault file is deleted. Only your 24 words or the key bring it back.</span>
|
||||
<input type="password" class="addr-input short-pw" id="remove-pw" placeholder="Your password" autocomplete="current-password" aria-label="Password">
|
||||
<button class="btn small primary" id="remove-go">Remove</button>
|
||||
<button class="btn small ghost" id="ask-remove-no">Cancel</button>
|
||||
<span class="ask-foot err" id="remove-err"></span>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- the update card (update-card.js, app.js renderUpdateCard): one update, centred; Later, Escape or the backdrop
|
||||
leaves the strip above -->
|
||||
<!-- the page bridge (0.1.6): a website's request, answered here and nowhere else -->
|
||||
<div class="upd-wrap bridge-wrap" id="bridge" hidden>
|
||||
<div class="upd-card bridge-card" id="bridge-card" role="dialog" aria-modal="true" aria-labelledby="bridge-title" tabindex="-1">
|
||||
<div class="eyebrow ember" id="bridge-eyebrow">a website asks</div>
|
||||
<h2 class="upd-name" id="bridge-title"></h2>
|
||||
<p class="bridge-amount mono" id="bridge-amount" hidden></p>
|
||||
<pre class="bridge-message mono" id="bridge-message" hidden></pre>
|
||||
<ul class="upd-notes" id="bridge-lines"></ul>
|
||||
<p class="upd-meta mono" id="bridge-origin"></p>
|
||||
<div class="upd-actions" id="bridge-actions">
|
||||
<button class="btn primary big" id="bridge-yes"><svg class="fp-ico" width="16" height="16" hidden><use href="#i-fp"></use></svg><span id="bridge-yes-text">Connect</span></button>
|
||||
<button class="btn ghost" id="bridge-no">Decline</button>
|
||||
</div>
|
||||
<p class="ask-foot" id="bridge-bio-line"></p>
|
||||
<p class="err" id="bridge-err"></p>
|
||||
<div class="bridge-done" id="bridge-done" hidden>
|
||||
<p class="upd-line" id="bridge-done-line"></p>
|
||||
<div class="box mono" id="bridge-hash-box" hidden><span id="bridge-hash"></span><button class="btn tiny" data-copy="bridge-hash">Copy</button></div>
|
||||
<div class="kv" id="bridge-final"></div>
|
||||
<div class="upd-actions"><button class="btn ghost" id="bridge-close">Done</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="upd-wrap" id="upd" hidden>
|
||||
<div class="upd-card" id="upd-card" role="dialog" aria-modal="true" aria-labelledby="upd-name" aria-describedby="upd-line" tabindex="-1">
|
||||
<div class="upd-mark" id="upd-mark">
|
||||
<svg class="upd-ring" viewBox="0 0 96 96" aria-hidden="true"><circle class="track" cx="48" cy="48" r="44"></circle><circle class="arc" id="upd-arc" cx="48" cy="48" r="44"></circle></svg>
|
||||
<img src="mark.svg" width="40" height="40" alt="">
|
||||
<span class="upd-pct mono" id="upd-pct" hidden></span>
|
||||
</div>
|
||||
<div class="eyebrow ember">update</div>
|
||||
<h2 class="upd-name" id="upd-name"></h2>
|
||||
<p class="upd-line" id="upd-line"></p>
|
||||
<p class="upd-cause mono" id="upd-cause" hidden></p>
|
||||
<ul class="upd-notes" id="upd-notes"></ul>
|
||||
<button class="upd-more" id="upd-more" type="button" aria-expanded="false" hidden>What changed</button>
|
||||
<ul class="upd-all" id="upd-all" hidden></ul>
|
||||
<p class="upd-meta mono" id="upd-meta"></p>
|
||||
<div class="upd-actions" id="upd-actions"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast" hidden></div>
|
||||
<script src="update-card.js"></script>
|
||||
<script src="lock-screen.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
78
app/igneum-wallet/ui/lock-screen.js
Normal file
78
app/igneum-wallet/ui/lock-screen.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/* The lock screen (pure; lock-screen.test.mjs loads this file): what the screen shows for a wallet state, the line
|
||||
under the button after the host answered, and when the system Touch ID (or Windows Hello) sheet may be raised
|
||||
without a tap. 5 October 2026, for 0.1.4.
|
||||
|
||||
The sheet appears when the person taps the button or presses Return or Space on it. One exception: the first
|
||||
arrival after the person opened the app may raise it once, AUTO_DELAY_MS after the lock screen is fully drawn,
|
||||
when the setting "Ask for Touch ID when the wallet opens" is on. Never on an idle lock, on a hand lock, when the
|
||||
window comes back, after a cancelled sheet, or when the updater relaunched the app. */
|
||||
const LockScreen = (function () {
|
||||
'use strict';
|
||||
/** after the lock screen is fully drawn */
|
||||
const AUTO_DELAY_MS = 600;
|
||||
/** the transition from the home screen to the lock screen, and the lock screen's own entry */
|
||||
const ENTER_MS = 250;
|
||||
/** the idle lock's choices in Settings, minutes; 0 is never */
|
||||
const IDLE_CHOICES = [1, 5, 15, 60, 0];
|
||||
const IDLE_DEFAULT_MIN = 5;
|
||||
|
||||
function idleLabel(min) {
|
||||
const m = Number(min) || 0;
|
||||
if (m <= 0) return 'never';
|
||||
if (m === 60) return '1 hour';
|
||||
if (m % 60 === 0) return (m / 60) + ' hours';
|
||||
return m === 1 ? '1 minute' : m + ' minutes';
|
||||
}
|
||||
|
||||
function shortAddress(a) {
|
||||
return a ? a.slice(0, 8) + '…' + a.slice(-6) : '';
|
||||
}
|
||||
|
||||
/** What the lock screen shows: the fingerprint button when Touch ID or Windows Hello is enrolled and the app
|
||||
window is the host (a browser tab cannot raise the sheet); else the password field is the primary control. */
|
||||
function layout(s, hostHere) {
|
||||
const b = (s && s.biometric) || {};
|
||||
const touch = !!(b.enrolled && hostHere);
|
||||
return { touch: touch, passwordPrimary: !touch, address: shortAddress(s && s.display) };
|
||||
}
|
||||
|
||||
/** The line under the button after the host answered: {text, cls, password}. cls is '' (ember), 'ok' or
|
||||
'wait'; password says the password field should be revealed (the sheet cannot unlock this time). */
|
||||
function line(r, name) {
|
||||
if (!r) return { text: '', cls: '', password: false };
|
||||
if (r.ok) return { text: name + ' confirmed, unlocking', cls: 'ok', password: false };
|
||||
const c = r.code;
|
||||
if (c === 'cancelled') return { text: name + ' cancelled, tap to try again', cls: '', password: false };
|
||||
if (c === 'failed') return { text: name + ' did not match, tap to try again', cls: '', password: false };
|
||||
if (c === 'timeout') return { text: name + ' did not answer, tap to try again', cls: '', password: false };
|
||||
if (c === 'fallback') return { text: 'Enter your password', cls: 'wait', password: true };
|
||||
if (c === 'locked') return { text: name + ' is locked, use your password', cls: '', password: true };
|
||||
if (c === 'invalidated') return { text: name + ' was turned off (a fingerprint changed): use your password, then turn it on again in Settings', cls: '', password: true };
|
||||
if (c === 'not_set_up' || c === 'unavailable' || c === 'not_enrolled') return { text: r.message || (name + ' is not set up on this Mac'), cls: '', password: true };
|
||||
if (c === 'nohost') return { text: name + ' needs the Igneum Wallet app window', cls: '', password: true };
|
||||
if (c === 'engine') return { text: r.message || ('the wallet did not unlock'), cls: '', password: true };
|
||||
return { text: r.message || (name + ' did not succeed, tap to try again'), cls: '', password: false };
|
||||
}
|
||||
|
||||
/** Whether the sheet may be raised now without a tap. ctx: reason (arrival | lock | focus | cancel), phase,
|
||||
touch (the button is on screen), askOnOpen (the setting), hidden (the window is not visible), busy (a sheet is
|
||||
up), firstPhase (the first phase this page saw: 'unlock' means the app opened locked), updatedFrom (set on
|
||||
the first run after an update: the updater opened the app, not the person). mem.used: the one chance went. */
|
||||
function autoPrompt(ctx, mem) {
|
||||
const c = ctx || {}, m = mem || {};
|
||||
const no = function (why) { return { prompt: false, why: why, delay: 0 }; };
|
||||
if (c.phase !== 'unlock') return no('not-locked');
|
||||
if (!c.touch) return no('no-touch');
|
||||
if (m.used) return no('once');
|
||||
if (c.reason !== 'arrival') return no(c.reason || 'not-arrival');
|
||||
if (c.firstPhase !== 'unlock') return no('not-arrival');
|
||||
if (c.updatedFrom) return no('updater');
|
||||
if (!c.askOnOpen) return no('setting-off');
|
||||
if (c.hidden) return no('hidden');
|
||||
if (c.busy) return no('busy');
|
||||
return { prompt: true, why: 'arrival', delay: AUTO_DELAY_MS };
|
||||
}
|
||||
|
||||
return { AUTO_DELAY_MS: AUTO_DELAY_MS, ENTER_MS: ENTER_MS, IDLE_CHOICES: IDLE_CHOICES, IDLE_DEFAULT_MIN: IDLE_DEFAULT_MIN, idleLabel: idleLabel, shortAddress: shortAddress, layout: layout, line: line, autoPrompt: autoPrompt };
|
||||
})();
|
||||
if (typeof module === 'object' && module && module.exports) module.exports = LockScreen;
|
||||
93
app/igneum-wallet/ui/lock-screen.test.mjs
Normal file
93
app/igneum-wallet/ui/lock-screen.test.mjs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// node --test app/igneum-wallet/ui/lock-screen.test.mjs (no dependencies)
|
||||
// Loads ui/lock-screen.js (plain browser JS, run with `module` defined and no `document`) and checks what the lock
|
||||
// screen shows, the line under the button after the host answered, and when the system sheet may be raised
|
||||
// without a tap: once, on the first arrival after the person opened the app, with the setting on; never on an
|
||||
// idle lock, a hand lock, the window coming back, a cancelled sheet, or the updater's relaunch.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const src = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'lock-screen.js'), 'utf8');
|
||||
const mod = { exports: {} };
|
||||
new Function('module', src)(mod);
|
||||
const L = mod.exports;
|
||||
const { layout, line, autoPrompt, idleLabel, shortAddress } = L;
|
||||
|
||||
const ADDR = '0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf';
|
||||
const st = (over) => ({ phase: 'unlock', display: ADDR, biometric: { enrolled: true, available: true, kind: 'touchid' }, settings: { ask_on_open: true, idle_lock_min: 5 }, update: { updated_from: '' }, ...over });
|
||||
const arrival = (over) => ({ reason: 'arrival', phase: 'unlock', touch: true, askOnOpen: true, hidden: false, busy: false, firstPhase: 'unlock', updatedFrom: '', ...over });
|
||||
|
||||
test('what the screen shows: the button when enrolled in the app window, else the password field first', () => {
|
||||
const a = layout(st(), true);
|
||||
assert.equal(a.touch, true); assert.equal(a.passwordPrimary, false); assert.equal(a.address, '0x7E5F45…395Bdf');
|
||||
// enrolled, but the page is open in a browser tab: no host to raise the sheet
|
||||
const b = layout(st(), false);
|
||||
assert.equal(b.touch, false); assert.equal(b.passwordPrimary, true);
|
||||
// not enrolled in the app window
|
||||
const c = layout(st({ biometric: { enrolled: false, available: true } }), true);
|
||||
assert.equal(c.touch, false); assert.equal(c.passwordPrimary, true);
|
||||
assert.deepEqual(layout(null, true), { touch: false, passwordPrimary: true, address: '' });
|
||||
assert.equal(shortAddress(''), '');
|
||||
});
|
||||
|
||||
test('the line under the button: cancel and no-match keep the button, fallback and lockout reveal the password', () => {
|
||||
assert.deepEqual(line({ ok: true }, 'Touch ID'), { text: 'Touch ID confirmed, unlocking', cls: 'ok', password: false });
|
||||
assert.deepEqual(line({ ok: false, code: 'cancelled' }, 'Touch ID'), { text: 'Touch ID cancelled, tap to try again', cls: '', password: false });
|
||||
assert.deepEqual(line({ ok: false, code: 'failed' }, 'Windows Hello'), { text: 'Windows Hello did not match, tap to try again', cls: '', password: false });
|
||||
assert.deepEqual(line({ ok: false, code: 'timeout' }, 'Touch ID'), { text: 'Touch ID did not answer, tap to try again', cls: '', password: false });
|
||||
assert.deepEqual(line({ ok: false, code: 'fallback' }, 'Touch ID'), { text: 'Enter your password', cls: 'wait', password: true });
|
||||
assert.equal(line({ ok: false, code: 'locked' }, 'Touch ID').password, true);
|
||||
assert.equal(line({ ok: false, code: 'invalidated' }, 'Touch ID').password, true);
|
||||
assert.equal(line({ ok: false, code: 'nohost' }, 'Touch ID').text, 'Touch ID needs the Igneum Wallet app window');
|
||||
assert.equal(line({ ok: false, code: 'not_set_up', message: 'Touch ID is not set up on this Mac. Add a fingerprint in System Settings > Touch ID & Password.' }, 'Touch ID').password, true);
|
||||
assert.equal(line({ ok: false, code: 'engine', message: 'the engine did not unlock' }, 'Touch ID').text, 'the engine did not unlock');
|
||||
assert.deepEqual(line({ ok: false, code: 'weird' }, 'Touch ID'), { text: 'Touch ID did not succeed, tap to try again', cls: '', password: false });
|
||||
assert.deepEqual(line(null, 'Touch ID'), { text: '', cls: '', password: false });
|
||||
// never a full stop at the end, never a modal: one line in our words
|
||||
for (const c of ['cancelled', 'failed', 'timeout', 'fallback', 'locked', 'nohost']) assert.ok(!line({ ok: false, code: c }, 'Touch ID').text.endsWith('.'), c);
|
||||
});
|
||||
|
||||
test('the one automatic prompt: first arrival after the person opened the app, setting on, 600 ms after drawn', () => {
|
||||
const mem = { used: false };
|
||||
assert.deepEqual(autoPrompt(arrival(), mem), { prompt: true, why: 'arrival', delay: 600 });
|
||||
assert.equal(L.AUTO_DELAY_MS, 600); assert.equal(L.ENTER_MS, 250);
|
||||
// once per launch
|
||||
assert.equal(autoPrompt(arrival(), { used: true }).why, 'once');
|
||||
// the setting off
|
||||
assert.equal(autoPrompt(arrival({ askOnOpen: false }), mem).why, 'setting-off');
|
||||
// the window is not visible at arrival (start at login, the window behind): nothing
|
||||
assert.equal(autoPrompt(arrival({ hidden: true }), mem).why, 'hidden');
|
||||
// a sheet is already up
|
||||
assert.equal(autoPrompt(arrival({ busy: true }), mem).why, 'busy');
|
||||
// not enrolled, or a browser tab: no button, no sheet
|
||||
assert.equal(autoPrompt(arrival({ touch: false }), mem).why, 'no-touch');
|
||||
// not locked
|
||||
assert.equal(autoPrompt(arrival({ phase: 'home' }), mem).why, 'not-locked');
|
||||
});
|
||||
|
||||
test('never on an idle lock, a hand lock, the window coming back, a cancelled sheet, or the updater relaunch', () => {
|
||||
const mem = { used: false };
|
||||
// the page opened on the home screen (the wallet was unlocked at arrival) and locked later: idle or by hand
|
||||
assert.equal(autoPrompt(arrival({ reason: 'lock', firstPhase: 'home' }), mem).why, 'lock');
|
||||
assert.equal(autoPrompt(arrival({ reason: 'lock' }), mem).why, 'lock');
|
||||
// the window came back from the menu bar or the Dock
|
||||
assert.equal(autoPrompt(arrival({ reason: 'focus' }), mem).why, 'focus');
|
||||
// after a cancelled sheet the page waits for a tap
|
||||
assert.equal(autoPrompt(arrival({ reason: 'cancel' }), mem).why, 'cancel');
|
||||
// the first phase the page saw was not the lock screen (welcome, home): a later lock is not an arrival
|
||||
assert.equal(autoPrompt(arrival({ firstPhase: 'home' }), mem).why, 'not-arrival');
|
||||
assert.equal(autoPrompt(arrival({ firstPhase: 'welcome' }), mem).why, 'not-arrival');
|
||||
// the updater opened the app (first run after an update), not the person
|
||||
assert.equal(autoPrompt(arrival({ updatedFrom: '0.1.3' }), mem).why, 'updater');
|
||||
// nothing in the context is read as a prompt
|
||||
assert.equal(autoPrompt(null, null).prompt, false);
|
||||
});
|
||||
|
||||
test('the idle lock choices and their labels', () => {
|
||||
assert.deepEqual(L.IDLE_CHOICES, [1, 5, 15, 60, 0]);
|
||||
assert.equal(L.IDLE_DEFAULT_MIN, 5);
|
||||
assert.deepEqual(L.IDLE_CHOICES.map(idleLabel), ['1 minute', '5 minutes', '15 minutes', '1 hour', 'never']);
|
||||
assert.equal(idleLabel('15'), '15 minutes'); assert.equal(idleLabel(undefined), 'never'); assert.equal(idleLabel(120), '2 hours');
|
||||
});
|
||||
1
app/igneum-wallet/ui/mark.svg
Normal file
1
app/igneum-wallet/ui/mark.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"/><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"/></svg>
|
||||
|
After Width: | Height: | Size: 214 B |
104
app/igneum-wallet/ui/update-card.js
Normal file
104
app/igneum-wallet/ui/update-card.js
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
/* The update card (pure; update-card.test.mjs loads this file): the same card, rules and words as the miner's
|
||||
(app/igneum-app/ui/app.js, UpdateCard), with the wallet's name and the wallet's reasons to wait.
|
||||
A centred card over the window for one update: the mark with a progress ring, "Igneum Wallet 0.1.3", one line
|
||||
(is available, is downloading, is ready to install, Installing, did not install), up to three lines of release
|
||||
notes with the rest behind "What changed", the size, Install now and Later.
|
||||
model() turns state.update into what the card says. decide() says whether it shows now, given the window
|
||||
(ctx) and what the card already did (mem: open, later, seen):
|
||||
deferred while the send screen is open, while Touch ID (or Windows Hello) is on screen, while a wallet is
|
||||
being created or imported, while the app quits
|
||||
later Later, Escape or the backdrop hides this version at this stage; the banner keeps it
|
||||
back a newer version, or the download ready while automatic updates are off (with them on it installs
|
||||
by itself, so a dismissed download stays dismissed); an open card follows its update to the end
|
||||
installing and failed show only on an open card (Install now was pressed here); the banner carries the rest */
|
||||
var UpdateCard = (function () {
|
||||
'use strict';
|
||||
var NAME = 'Igneum Wallet', LINE_MAX = 70, LINES = 3;
|
||||
var INSTALL = { act: 'install', label: 'Install now', primary: true }, LATER = { act: 'later', label: 'Later' };
|
||||
var RETRY = { act: 'retry', label: 'Try again', primary: true }, OPEN = { act: 'open', label: 'Open the download', primary: true };
|
||||
function cap(t) { return t ? t.charAt(0).toUpperCase() + t.slice(1) : ''; }
|
||||
function firstLine(t, max) { t = String(t || '').split('\n')[0].trim(); max = max || 160; return t.length > max ? t.slice(0, max - 1).trim() + '…' : t; }
|
||||
// the manifest's notes as sentences: split on line breaks, then after . ! ? ; (no lookbehind: the Mac WebView)
|
||||
function sentences(text) {
|
||||
var out = [], parts = String(text || '').split(/\n+/);
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var p = parts[i], at = 0;
|
||||
for (var j = 0; j < p.length; j++) {
|
||||
var ch = p.charAt(j), next = p.charAt(j + 1);
|
||||
if ((ch === '.' || ch === '!' || ch === '?' || ch === ';') && (next === ' ' || next === '')) { push(p.slice(at, j + 1)); at = j + 1; }
|
||||
}
|
||||
push(p.slice(at));
|
||||
}
|
||||
function push(s) { s = s.trim().replace(/[;:,]+$/, ''); if (s) out.push(cap(s)); }
|
||||
return out;
|
||||
}
|
||||
// at most LINES lines, each under LINE_MAX characters (cut at a word); more = something is left for "What changed"
|
||||
function splitNotes(text) {
|
||||
var all = sentences(text), lines = [], more = all.length > LINES;
|
||||
for (var i = 0; i < all.length && lines.length < LINES; i++) {
|
||||
var s = all[i].replace(/\.$/, '');
|
||||
if (s.length >= LINE_MAX) { var cut = s.lastIndexOf(' ', LINE_MAX - 2); s = s.slice(0, cut > 20 ? cut : LINE_MAX - 2).replace(/[,;:]$/, '') + '…'; more = true; }
|
||||
lines.push(s);
|
||||
}
|
||||
return { lines: lines, more: more, all: all };
|
||||
}
|
||||
function size(bytes) { if (!(bytes > 0)) return ''; return bytes < 1e6 ? (bytes / 1e6).toFixed(1) + ' MB' : Math.round(bytes / 1e6) + ' MB'; }
|
||||
function card(stage, u, over) {
|
||||
var ver = u.version || '';
|
||||
var m = { stage: stage, version: ver, key: 'update:' + ver + ':' + (stage === 'available' || stage === 'downloading' || stage === 'staging' ? 'pending' : stage), name: NAME + ' ' + ver, line: '', note: '', cause: '', size: size(u.size), pct: -1, ring: 'none', actions: [], dismissable: true, auto: !!u.auto };
|
||||
var n = splitNotes(u.notes); m.lines = n.lines; m.more = n.more; m.all = n.all;
|
||||
if (over) for (var k in over) m[k] = over[k];
|
||||
return m;
|
||||
}
|
||||
// u = state.update; s = { version, quitting }
|
||||
function model(u, s) {
|
||||
if (!u || !u.version) return null;
|
||||
s = s || {};
|
||||
var pct = u.progress > 0 ? Math.min(100, Math.round(u.progress * 100)) : 0;
|
||||
if (u.status === 'error') {
|
||||
if (/no update manifest configured/.test(u.error || '')) return null;
|
||||
return card('failed', u, { line: u.rolled_back ? 'did not stay up and was rolled back.' : 'did not install.', cause: firstLine(u.error, 120), ring: 'failed', actions: [RETRY, LATER], lines: [], more: false });
|
||||
}
|
||||
if (u.applying || u.status === 'applying' || (u.status === 'ready' && u.wait === 'installing now')) {
|
||||
return card('installing', u, { line: 'Installing. The app restarts itself.', note: s.quitting ? 'A moment.' : 'Your key stays where it is.', ring: 'busy', dismissable: false, lines: [], more: false });
|
||||
}
|
||||
var m = null;
|
||||
switch (u.status) {
|
||||
case 'available': m = card('available', u, { line: 'is available.', actions: [INSTALL, LATER] }); break;
|
||||
case 'downloading': m = card('downloading', u, { line: 'is downloading.', pct: pct, ring: 'progress', actions: [INSTALL, LATER] }); break;
|
||||
case 'staging': m = card('staging', u, { line: 'is being checked.', pct: 100, ring: 'progress', actions: [INSTALL, LATER] }); break;
|
||||
case 'ready':
|
||||
var why = /failed to install before/.test(u.wait || '') ? 'It failed to install before.' : u.auto ? 'It installs by itself when nothing is being sent.' : '';
|
||||
m = card('ready', u, { line: 'is ready to install.', note: why, ring: 'full', actions: [INSTALL, LATER] }); break;
|
||||
case 'deferred': m = card('deferred', u, { line: 'is waiting for permission.', note: 'It installs the next time someone is at this PC.', ring: 'full', actions: [INSTALL, LATER] }); break;
|
||||
case 'manual': m = card('manual', u, { line: 'is downloaded.', note: 'Open the disk image and drag the app over the old one.', ring: 'full', actions: [OPEN, LATER] }); break;
|
||||
}
|
||||
if (!m) return null;
|
||||
if (u.urgent && u.urgent_text) { m.note = u.urgent_text; m.dismissable = false; m.actions = m.actions.filter(function (a) { return a.act !== 'later'; }); }
|
||||
return m;
|
||||
}
|
||||
// why the card may not open now: ctx = { phase, view, bioBusy, bioLine, bioName, quitting }
|
||||
function blocked(ctx) {
|
||||
ctx = ctx || {};
|
||||
if (ctx.quitting) return 'the app is quitting';
|
||||
if (ctx.phase === 'create' || ctx.phase === 'import') return 'a wallet is being ' + (ctx.phase === 'create' ? 'created' : 'imported');
|
||||
if (ctx.view === 'send') return 'the send screen is open';
|
||||
if (ctx.bioBusy || ctx.bioLine) return (ctx.bioName || 'Touch ID') + ' is on screen';
|
||||
return '';
|
||||
}
|
||||
// mem = { open: the card is up, later: the key Later was pressed on, seen: the version the card was shown for }
|
||||
function decide(m, ctx, mem) {
|
||||
mem = mem || {};
|
||||
if (!m) return { show: false, why: 'none' };
|
||||
var b = blocked(ctx);
|
||||
if (b && m.stage !== 'installing') return { show: false, why: 'deferred', reason: b };
|
||||
if (m.stage === 'installing' || m.stage === 'failed') return mem.open ? { show: true, why: 'open' } : { show: false, why: 'strip' };
|
||||
if (!m.dismissable) return { show: true, why: 'urgent' };
|
||||
if (mem.later === m.key) return { show: false, why: 'later' };
|
||||
if (mem.open) return { show: true, why: 'open' };
|
||||
if (m.stage === 'ready' && m.auto && mem.seen === m.version) return { show: false, why: 'auto' };
|
||||
return { show: true, why: m.stage === 'ready' ? 'ready' : 'new' };
|
||||
}
|
||||
return { NAME: NAME, LINE_MAX: LINE_MAX, LINES: LINES, sentences: sentences, splitNotes: splitNotes, size: size, model: model, blocked: blocked, decide: decide };
|
||||
})();
|
||||
if (typeof module === 'object' && module && module.exports) module.exports = UpdateCard;
|
||||
97
app/igneum-wallet/ui/update-card.test.mjs
Normal file
97
app/igneum-wallet/ui/update-card.test.mjs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
// node --test app/igneum-wallet/ui/update-card.test.mjs (no dependencies)
|
||||
// Loads ui/update-card.js (plain browser JS, run with `module` defined and no `document`) and checks what the card
|
||||
// says for each update state, the release-note lines, and when it shows or waits (a send, Touch ID, a wallet flow).
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const src = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'update-card.js'), 'utf8');
|
||||
const mod = { exports: {} };
|
||||
new Function('module', src)(mod);
|
||||
const C = mod.exports;
|
||||
const { model, decide, blocked, splitNotes, sentences, size } = C;
|
||||
|
||||
const NOTES = 'The update card: one centred card with Install now and Later. Touch ID confirms every send on the Mac; Windows Hello is written but untested. The coin and the chain line sit on the balance card. The version shows in the header and in Settings.';
|
||||
const upd = (over) => ({ status: 'ready', version: '0.1.3', url: '', notes: NOTES, file: '', error: '', checked_at: 0, available: true, downloaded: true, ready: true, applying: false, progress: 1, size: 19_479_807, auto: true, wait: '', urgent: false, urgent_text: '', unsupported: false, min_supported: '', updated_from: '', rolled_back: '', ...over });
|
||||
const quiet = { phase: 'home', view: 'overview', bioBusy: false, bioLine: false, bioName: 'Touch ID', quitting: false };
|
||||
|
||||
test('release notes: sentences, three lines under 70 characters, the rest behind What changed', () => {
|
||||
assert.deepEqual(sentences('one. two! three? four; five'), ['One.', 'Two!', 'Three?', 'Four', 'Five']);
|
||||
assert.deepEqual(sentences('v0.1.3 ships. 24 words.'), ['V0.1.3 ships.', '24 words.']);
|
||||
const n = splitNotes(NOTES);
|
||||
assert.equal(n.lines.length, 3);
|
||||
for (const l of n.lines) assert.ok(l.length < C.LINE_MAX, `${l.length}: ${l}`);
|
||||
assert.deepEqual(n.lines, ['The update card: one centred card with Install now and Later', 'Touch ID confirms every send on the Mac', 'Windows Hello is written but untested']);
|
||||
assert.equal(n.more, true); assert.equal(n.all.length, 5);
|
||||
assert.deepEqual(splitNotes('coin on the home screen, updates install by themselves'), { lines: ['Coin on the home screen, updates install by themselves'], more: false, all: ['Coin on the home screen, updates install by themselves'] });
|
||||
assert.deepEqual(splitNotes('').lines, []);
|
||||
assert.equal(size(19_479_807), '19 MB'); assert.equal(size(0), '');
|
||||
});
|
||||
|
||||
test('what the card says: available, downloading with the ring, ready, installing, failed, manual, waiting', () => {
|
||||
const a = model(upd({ status: 'available', downloaded: false, ready: false, progress: 0 }), {});
|
||||
assert.equal(a.name, 'Igneum Wallet 0.1.3'); assert.equal(a.line, 'is available.'); assert.equal(a.key, 'update:0.1.3:pending');
|
||||
assert.equal(a.size, '19 MB'); assert.deepEqual(a.actions.map((x) => x.label), ['Install now', 'Later']);
|
||||
const d = model(upd({ status: 'downloading', downloaded: false, ready: false, progress: 0.43 }), {});
|
||||
assert.equal(d.line, 'is downloading.'); assert.equal(d.pct, 43); assert.equal(d.ring, 'progress'); assert.equal(d.key, a.key);
|
||||
const r = model(upd(), {});
|
||||
assert.equal(r.line, 'is ready to install.'); assert.equal(r.note, 'It installs by itself when nothing is being sent.'); assert.equal(r.key, 'update:0.1.3:ready');
|
||||
assert.equal(model(upd({ auto: false, wait: 'waiting for Install now (automatic updates are off)' }), {}).note, '');
|
||||
assert.equal(model(upd({ wait: 'a send is in flight; installing after it' }), {}).note, 'It installs by itself when nothing is being sent.');
|
||||
const i = model(upd({ status: 'applying', applying: true }), {});
|
||||
assert.equal(i.stage, 'installing'); assert.equal(i.line, 'Installing. The app restarts itself.'); assert.deepEqual(i.actions, []); assert.equal(i.dismissable, false);
|
||||
assert.equal(model(upd({ wait: 'installing now' }), {}).stage, 'installing');
|
||||
const f = model(upd({ status: 'error', error: 'sha256 mismatch: the file is not what the manifest signed\nsecond line', ready: false }), {});
|
||||
assert.equal(f.line, 'did not install.'); assert.equal(f.cause, 'sha256 mismatch: the file is not what the manifest signed');
|
||||
assert.deepEqual(f.actions.map((x) => x.label), ['Try again', 'Later']);
|
||||
assert.equal(model(upd({ status: 'error', error: 'did not stay up', rolled_back: '0.1.3: did not stay up' }), {}).line, 'did not stay up and was rolled back.');
|
||||
assert.equal(model(upd({ status: 'manual', ready: false }), {}).actions[0].label, 'Open the download');
|
||||
assert.equal(model(upd({ status: 'deferred' }), {}).line, 'is waiting for permission.');
|
||||
const u = model(upd({ status: 'downloading', progress: 0.2, urgent: true, urgent_text: 'This version is no longer supported. Installing 0.1.3 now.' }), {});
|
||||
assert.equal(u.dismissable, false); assert.deepEqual(u.actions.map((x) => x.label), ['Install now']);
|
||||
assert.equal(model(upd({ status: 'current', available: false }), {}), null);
|
||||
assert.equal(model(upd({ status: 'off', version: '' }), {}), null);
|
||||
assert.equal(model(upd({ status: 'error', error: 'no update manifest configured in this build' }), {}), null);
|
||||
assert.equal(model(null, {}), null);
|
||||
});
|
||||
|
||||
test('deferred: the send screen, Touch ID on screen, a wallet being created or imported, quitting', () => {
|
||||
assert.equal(blocked(quiet), '');
|
||||
assert.equal(blocked({ ...quiet, view: 'send' }), 'the send screen is open');
|
||||
assert.equal(blocked({ ...quiet, bioBusy: true }), 'Touch ID is on screen');
|
||||
assert.equal(blocked({ ...quiet, bioLine: true, bioName: 'Windows Hello' }), 'Windows Hello is on screen');
|
||||
assert.equal(blocked({ ...quiet, phase: 'create' }), 'a wallet is being created');
|
||||
assert.equal(blocked({ ...quiet, phase: 'import' }), 'a wallet is being imported');
|
||||
assert.equal(blocked({ ...quiet, phase: 'unlock' }), '');
|
||||
assert.equal(blocked({ ...quiet, quitting: true }), 'the app is quitting');
|
||||
const a = model(upd({ status: 'available' }), {});
|
||||
assert.deepEqual(decide(a, { ...quiet, view: 'send' }, {}), { show: false, why: 'deferred', reason: 'the send screen is open' });
|
||||
assert.deepEqual(decide(a, { ...quiet, bioBusy: true }, {}), { show: false, why: 'deferred', reason: 'Touch ID is on screen' });
|
||||
// the block lifts: the card comes (it was queued, not dismissed)
|
||||
assert.equal(decide(a, quiet, { open: false, later: '', seen: '' }).show, true);
|
||||
assert.equal(decide(model(upd({ status: 'applying', applying: true }), {}), { ...quiet, view: 'send' }, { open: true }).show, true);
|
||||
});
|
||||
|
||||
test('Later: hides this version at this stage; back for a newer version or a ready download with auto off', () => {
|
||||
const pend = model(upd({ status: 'downloading', progress: 0.5, ready: false }), {});
|
||||
const ready = model(upd(), {});
|
||||
const readyOff = model(upd({ auto: false }), {});
|
||||
assert.deepEqual(decide(pend, quiet, {}), { show: true, why: 'new' });
|
||||
const later = { open: false, later: pend.key, seen: '0.1.3' };
|
||||
assert.deepEqual(decide(pend, quiet, later), { show: false, why: 'later' });
|
||||
assert.deepEqual(decide(ready, quiet, later), { show: false, why: 'auto' });
|
||||
assert.deepEqual(decide(readyOff, quiet, later), { show: true, why: 'ready' });
|
||||
assert.deepEqual(decide(readyOff, quiet, { open: false, later: readyOff.key, seen: '0.1.3' }), { show: false, why: 'later' });
|
||||
assert.deepEqual(decide(model(upd({ status: 'available', version: '0.1.4' }), {}), quiet, later), { show: true, why: 'new' });
|
||||
assert.deepEqual(decide(ready, quiet, { open: false, later: '', seen: '' }), { show: true, why: 'ready' });
|
||||
const open = { open: true, later: '', seen: '0.1.3' };
|
||||
assert.equal(decide(pend, quiet, open).why, 'open'); assert.equal(decide(ready, quiet, open).why, 'open');
|
||||
assert.equal(decide(model(upd({ status: 'applying', applying: true }), {}), quiet, open).show, true);
|
||||
assert.equal(decide(model(upd({ status: 'error', error: 'x' }), {}), quiet, open).show, true);
|
||||
assert.deepEqual(decide(model(upd({ status: 'applying', applying: true }), {}), quiet, {}), { show: false, why: 'strip' });
|
||||
assert.deepEqual(decide(model(upd({ status: 'error', error: 'x' }), {}), quiet, {}), { show: false, why: 'strip' });
|
||||
const urgent = model(upd({ status: 'downloading', progress: 0.2, urgent: true, urgent_text: 'Unsupported.' }), {});
|
||||
assert.deepEqual(decide(urgent, quiet, { later: urgent.key }), { show: true, why: 'urgent' });
|
||||
});
|
||||
203
app/igneum-wallet/ui/view.test.mjs
Normal file
203
app/igneum-wallet/ui/view.test.mjs
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
// node --test app/igneum-wallet/ui/view.test.mjs (no dependencies)
|
||||
// Loads the View block of app.js (plain browser JS: the file is run with `module` defined and no `document`, so only
|
||||
// the pure block executes) and checks the words each page shows for a given state (wallet-ui-3).
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const src = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'app.js'), 'utf8');
|
||||
const mod = { exports: {} };
|
||||
new Function('module', src)(mod);
|
||||
const V = mod.exports.View;
|
||||
|
||||
const ME = '0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', B = '0x2b5ad5c4795c026514f8317c7a215e218dccd6cf';
|
||||
const NOW = 1_791_000_000;
|
||||
const node = (over) => ({ source: 'miner', state: 'ok', chain_id: 7762959, chain: 'igneum-devnet-20', block: 140286, evm: 'http://127.0.0.1:26790', grpc: '127.0.0.1:26610', synced: true, message: "using the miner app's node on this machine", finality_active: true, latest_locked: 4674, ...over });
|
||||
const fin = (over) => ({ verified_index: 4674, verified_hash: '0xf1', chain_number: 140246, signers: 11, voters: 12, fraction_total: 0.912, fraction_active: 0.98, weights_exact: true, verified_at: NOW - 14, message: 'checkpoint 4674 verified here', ...over });
|
||||
const st = (over) => ({ version: '0.1.5', phase: 'home', balance: '31.6321', balance_known: true, scanning: false, scanned_to: 140286, node: node(), finality: fin(), settings: { network: 'devnet', start_at_login: false, auto_update: true, idle_lock_min: 5, ask_on_open: true }, update: { status: 'current', version: '0.1.5', checked_at: NOW - 780 }, now: NOW, ...over });
|
||||
const entry = (over) => ({ hash: '0x' + 'c3d4'.repeat(16), kind: 'received', block: 140262, block_hash: '0xabcd', from: B, to: ME, value: '250000000000000000', fee: '25380000000000', ok: true, time: NOW - 140, finality: 'in_block', checkpoint: null, note: '', ...over });
|
||||
|
||||
test('the five pages and their order', () => {
|
||||
assert.deepEqual(V.PAGES.map((p) => p.id), ['home', 'send', 'receive', 'history', 'settings']);
|
||||
assert.equal(V.page('history').title, 'History');
|
||||
assert.equal(V.page('nonsense').id, 'home');
|
||||
});
|
||||
|
||||
test('numbers: IGN from wei, short addresses, times', () => {
|
||||
assert.equal(V.ign('1500000000000000000'), '1.5');
|
||||
assert.equal(V.ign('250000000000000000', 6), '0.25');
|
||||
assert.equal(V.ign('10140000000000000000', 4), '10.14');
|
||||
assert.equal(V.ign('0'), '0');
|
||||
assert.equal(V.ign('nonsense'), '0');
|
||||
assert.equal(V.ign('76140000000000', 9), '0.00007614');
|
||||
assert.equal(V.shortHex('0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf'), '0x7E5F45\u20265Bdf');
|
||||
assert.equal(V.shortHex(''), '');
|
||||
assert.equal(V.withCommas(140286), '140,286');
|
||||
assert.equal(V.ago(14), '14 s ago'); assert.equal(V.ago(780), '13 min ago'); assert.equal(V.ago(7200), '2 h ago');
|
||||
assert.match(V.timeWord(NOW - 140, NOW), /^\d\d:\d\d$/);
|
||||
assert.match(V.timeWord(NOW - 86400 * 3, NOW), /^\d+ \w+ \d\d:\d\d$/);
|
||||
});
|
||||
|
||||
test('the balance: the number only when known, else one line that says why (never a 0)', () => {
|
||||
assert.deepEqual(V.balanceLine(st()), { known: true, text: '' });
|
||||
assert.deepEqual(V.balanceLine(st({ balance_known: false })), { known: false, text: 'reading the balance' });
|
||||
assert.deepEqual(V.balanceLine(st({ balance_known: false, scanning: true, scanned_to: 61200, node: node({ block: 140270 }) })), { known: false, text: 'reading the chain, 61,200 of 140,270 blocks' });
|
||||
assert.deepEqual(V.balanceLine(st({ balance_known: false, node: node({ state: 'starting' }) })), { known: false, text: 'waiting for the node to start' });
|
||||
assert.deepEqual(V.balanceLine(st({ balance_known: false, node: node({ state: 'off', source: 'none' }) })), { known: false, text: 'waiting for a node' });
|
||||
});
|
||||
|
||||
test('money first when a price exists; else the one grey line, devnet or not', () => {
|
||||
assert.deepEqual(V.moneyLine(st()), { money: '', text: 'no market price on devnet: coins have no value' });
|
||||
assert.deepEqual(V.moneyLine(st({ settings: { network: 'testnet' } })), { money: '', text: 'no market price yet' });
|
||||
assert.deepEqual(V.moneyLine(st({ price_gbp_per_ign: 0.5 })), { money: '\u00a315.82', text: "at today's price" });
|
||||
// a price with an unknown balance is no money line
|
||||
assert.equal(V.moneyLine(st({ price_gbp_per_ign: 0.5, balance_known: false })).money, '');
|
||||
});
|
||||
|
||||
test('the node line: the state word, the source in plain words, the block, the verification', () => {
|
||||
assert.deepEqual(V.nodeLine(st()), { text: "Node synced \u00b7 the miner app's node \u00b7 block 140,286 \u00b7 verified to checkpoint 4,674", sub: '', tone: 'ok' });
|
||||
const pub = st({ node: node({ source: 'public', grpc: '', finality_active: false, latest_locked: 0, message: 'using the public RPC; no node here, so finality cannot be verified' }), finality: fin({ verified_index: 0, chain_number: null, message: 'no node here' }) });
|
||||
assert.equal(V.nodeLine(pub).text, 'Node synced \u00b7 the public RPC \u00b7 block 140,286 \u00b7 certificates cannot be verified without a node');
|
||||
const own = st({ node: node({ source: 'own', state: 'starting', block: 0, message: 'the bundled node is starting; the chain syncs from the seed' }), finality: fin({ verified_index: 0 }) });
|
||||
assert.deepEqual(V.nodeLine(own), { text: 'Node starting \u00b7 the bundled node', sub: 'the bundled node is starting; the chain syncs from the seed', tone: '' });
|
||||
const none = st({ node: node({ source: 'none', state: 'off', block: 0, message: 'no node: install Igneum Miner, or wait for the bundled node' }), finality: fin({ verified_index: 0 }) });
|
||||
assert.deepEqual(V.nodeLine(none), { text: 'Node not found', sub: 'no node: install Igneum Miner, or wait for the bundled node', tone: 'bad' });
|
||||
const lost = V.nodeLine(st({ node: node({ state: 'lost', message: 'history: connect 127.0.0.1:26790: Connection refused (os error 61)' }) }));
|
||||
assert.equal(lost.tone, 'bad'); assert.equal(lost.sub, 'the node stopped answering; trying again');
|
||||
assert.equal(V.nodeLine(st({ finality: fin({ verified_index: 0 }) })).text, "Node synced \u00b7 the miner app's node \u00b7 block 140,286 \u00b7 waiting for the first certificate");
|
||||
assert.equal(V.sourceWords('external'), 'the node named by the environment');
|
||||
});
|
||||
|
||||
test('the node details: every old card row with a one-line meaning, no n/a', () => {
|
||||
const rows = V.nodeDetails(st(), NOW);
|
||||
const keys = rows.map((r) => r[0]);
|
||||
assert.deepEqual(keys, ['source', 'chain', 'block', 'endpoint', 'finality on the node', 'verified here', 'signed by', 'checkpoint height', 'weights', 'checked']);
|
||||
assert.equal(rows[5][1], 'checkpoint 4,674'); assert.equal(rows[5][3], 'ok');
|
||||
assert.equal(rows[6][1], '11 of 12 voters'); assert.match(rows[6][2], /^91\.2% of all weight/);
|
||||
assert.equal(rows[9][1], '14 s ago');
|
||||
for (const r of rows) assert.ok(!/n\/a/.test(r[1]) && r[1] !== '', r[0]);
|
||||
const none = V.nodeDetails(st({ node: node({ source: 'public', evm: 'https://rpc.example', finality_active: false }), finality: fin({ verified_index: 0, message: 'no node here' }) }), NOW);
|
||||
assert.equal(none.find((r) => r[0] === 'finality on the node')[1], 'not checkable without a node');
|
||||
assert.deepEqual(none.find((r) => r[0] === 'verified here').slice(1), ['nothing yet', 'no node here', 'dim']);
|
||||
});
|
||||
|
||||
test('the pill: the wallet\'s own words', () => {
|
||||
assert.deepEqual(V.pillWords(st()), { text: 'synced', cls: 'on' });
|
||||
assert.deepEqual(V.pillWords(st({ node: node({ source: 'public' }) })), { text: 'public rpc', cls: 'on' });
|
||||
assert.deepEqual(V.pillWords(st({ node: node({ state: 'starting' }) })), { text: 'starting', cls: '' });
|
||||
assert.deepEqual(V.pillWords(st({ node: node({ state: 'off', source: 'none' }) })), { text: 'no node', cls: 'warn' });
|
||||
assert.deepEqual(V.pillWords(st({ node: node({ state: 'lost' }) })), { text: 'node lost', cls: 'warn' });
|
||||
assert.deepEqual(V.pillWords(st({ phase: 'unlock' })), { text: 'locked', cls: '' });
|
||||
assert.deepEqual(V.pillWords(st({ quitting: true })), { text: 'stopping', cls: '' });
|
||||
});
|
||||
|
||||
test('one state word per row: the wallet\'s verified final wins, failed from the receipt, else the node\'s word, else the label', () => {
|
||||
const e = entry();
|
||||
assert.deepEqual(V.stateWord(entry({ finality: 'final', checkpoint: 4673, block: 139900 }), 'executed'), { word: 'finalised', tone: 'ok', why: 'under checkpoint 4,673, verified here' });
|
||||
assert.deepEqual(V.stateWord(entry({ finality: 'failed', ok: false }), 'executed'), { word: 'failed', tone: 'bad', why: 'the execution failed; the fee was still paid' });
|
||||
assert.deepEqual(V.stateWord(e, 'pending'), { word: 'pending', tone: '', why: 'waiting for a block' });
|
||||
assert.deepEqual(V.stateWord(e, 'included'), { word: 'included', tone: '', why: 'in block 140,262, not executed yet' });
|
||||
assert.deepEqual(V.stateWord(e, 'executed'), { word: 'executed', tone: 'ink', why: 'in block 140,262' });
|
||||
assert.deepEqual(V.stateWord(e, 'proven'), { word: 'proven', tone: 'ink', why: 'in block 140,262, proof paid' });
|
||||
// the node says finalised but this wallet has not verified the certificate: the word, not the molten tone
|
||||
assert.deepEqual(V.stateWord(e, 'finalised'), { word: 'finalised', tone: 'ink', why: 'under a locked checkpoint, not yet verified here' });
|
||||
assert.deepEqual(V.stateWord(e, 'finality not active'), { word: 'finality not active', tone: 'ink', why: 'under a locked checkpoint; finality is paused on the network' });
|
||||
assert.equal(V.stateWord(e, 'unknown').word, 'pending');
|
||||
// no node word yet (the public RPC, or not asked): the wallet's label
|
||||
assert.deepEqual(V.stateWord(e, ''), { word: 'included', tone: '', why: 'in block 140,262' });
|
||||
assert.deepEqual(V.stateWord(entry({ finality: 'pending', block: 0 }), ''), { word: 'pending', tone: '', why: 'waiting for a block' });
|
||||
assert.equal(V.stateWord(entry({ finality: 'pending', block: 0 }), 'included').why, 'in a block, not executed yet');
|
||||
// a reward has no transaction hash: executed in its block, finalised under a verified checkpoint
|
||||
const r = entry({ hash: 'reward-140201-0', kind: 'reward', from: '', block: 140201 });
|
||||
assert.deepEqual(V.stateWord(r, ''), { word: 'executed', tone: 'ink', why: 'in block 140,201' });
|
||||
assert.equal(V.stateWord(entry({ hash: 'proving-1-0', kind: 'proving', finality: 'final', checkpoint: 9 }), '').word, 'finalised');
|
||||
// never a stronger word than the chain's: the words the row can show are exactly the node's plus failed
|
||||
const words = new Set(['pending', 'included', 'executed', 'proven', 'finalised', 'finality not active', 'failed']);
|
||||
for (const w of ['pending', 'included', 'executed', 'proven', 'finalised', 'finality not active', 'unknown', '']) assert.ok(words.has(V.stateWord(e, w).word), w);
|
||||
});
|
||||
|
||||
test('the row model: kind word, who, sign, amount, the rows that need the node\'s word', () => {
|
||||
const m = V.rowModel(entry(), 'included', NOW);
|
||||
assert.equal(m.kind, 'Received'); assert.equal(m.who, 'from 0x2b5ad5\u2026d6cf'); assert.equal(m.amount, '+0.25'); assert.equal(m.in, true); assert.equal(m.synthetic, false); assert.equal(m.nodeWord, 'included');
|
||||
const s = V.rowModel(entry({ kind: 'sent', from: ME, to: B, value: '1500000000000000000' }), '', NOW);
|
||||
assert.equal(s.kind, 'Sent'); assert.equal(s.who, 'to 0x2b5ad5\u2026d6cf'); assert.equal(s.amount, '\u22121.5'); assert.equal(s.in, false);
|
||||
assert.equal(V.rowModel(entry({ kind: 'self', from: ME, to: ME }), '', NOW).who, 'to yourself');
|
||||
assert.equal(V.rowModel(entry({ hash: 'reward-1-0', kind: 'reward' }), '', NOW).who, 'block reward');
|
||||
assert.equal(V.rowModel(entry({ hash: 'proving-1-0', kind: 'proving' }), '', NOW).kind, 'Proving payout');
|
||||
const list = [entry({ hash: '0x01', finality: 'pending' }), entry({ hash: '0x02' }), entry({ hash: 'reward-1-0', kind: 'reward' }), entry({ hash: '0x03', finality: 'final', checkpoint: 1 }), entry({ hash: '0x04', finality: 'failed' }), entry({ hash: '0x05' })];
|
||||
assert.deepEqual(V.needsNodeWord(list, 12), ['0x01', '0x02', '0x05']);
|
||||
assert.deepEqual(V.needsNodeWord(list, 2), ['0x01', '0x02']);
|
||||
});
|
||||
|
||||
test('send: the fee line, the gas words behind Details, the question in place', () => {
|
||||
const q = { to: B, display_to: '0x2B5AD5c4795c026514f8317c7a215E218DCCD6cF', value_ign: '1.5', fee_max_ign: '0.00007614', total_max_ign: '1.50007614', gas: 25380, base_fee_gwei: '1', tip_gwei: '1', max_fee: '3000000000' };
|
||||
assert.deepEqual(V.feeWords(null), { line: 'shown when you review', detail: '' });
|
||||
const f = V.feeWords(q);
|
||||
assert.equal(f.line, 'at most 0.00007614 IGN');
|
||||
assert.equal(f.detail, 'Gas 25,380 at a base fee of 1 gwei (burned) plus a 1 gwei tip (to the miner), capped at 3 gwei; what is not used comes back.');
|
||||
assert.equal(V.gwei('1500000000'), '1.5 gwei'); assert.equal(V.gwei('25000000000'), '25 gwei'); assert.equal(V.gwei('250000'), '0.00025 gwei'.replace('0.00025', '0'));
|
||||
assert.deepEqual(V.sendAsk(q, true, 'Touch ID'), { text: 'Send 1.5 IGN to 0x2B5AD5\u2026D6cF? Fee at most 0.00007614 IGN; 1.50007614 IGN leaves the wallet at most.', button: 'Confirm with Touch ID' });
|
||||
assert.equal(V.sendAsk(q, false).button, 'Send now');
|
||||
assert.equal(V.sendAsk(q, true, 'Windows Hello').button, 'Confirm with Windows Hello');
|
||||
});
|
||||
|
||||
test('settings: the Touch ID sentence, the idle sentence, the update card note', () => {
|
||||
assert.equal(V.bioSentence({ enrolled: true }, 'Touch ID', true), 'Touch ID is on. It unlocks the wallet, confirms each send and shows the backup; the password still works everywhere.');
|
||||
assert.equal(V.bioSentence({ enrolled: false, available: true }, 'Touch ID', false), 'Touch ID needs the Igneum Wallet app window; this page is open in a browser.');
|
||||
assert.equal(V.bioSentence({ enrolled: false, available: false }, 'Touch ID', true), 'Touch ID is not set up on this Mac.');
|
||||
assert.equal(V.bioSentence({ enrolled: false, available: false, message: 'Windows Hello is not configured.' }, 'Windows Hello', true), 'Windows Hello is not configured.');
|
||||
assert.equal(V.bioSentence({ enrolled: false, available: true }, 'Windows Hello', true), 'Unlock, confirm each send and show the backup with Windows Hello. The password still works everywhere.');
|
||||
assert.equal(V.idleSentence(5, 'Touch ID'), 'The key is cleared after 5 minutes without you; Touch ID or the password opens it again.');
|
||||
assert.equal(V.idleSentence(0, 'Touch ID'), 'The wallet stays open until you lock it.');
|
||||
assert.equal(V.updateNote({ status: 'current', version: '0.1.5', checked_at: NOW - 780 }, NOW), 'Up to date, checked 13 min ago.');
|
||||
assert.equal(V.updateNote({ status: 'unknown', version: '' }, NOW), 'Not checked yet.');
|
||||
assert.equal(V.updateNote({ status: 'ready', version: '0.1.6', wait: 'installs as soon as nothing is being sent' }, NOW), 'Igneum Wallet 0.1.6 is ready. Installs as soon as nothing is being sent.');
|
||||
assert.equal(V.updateNote({ status: 'current', version: '0.1.5', updated_from: '0.1.4', checked_at: NOW - 60 }, NOW), 'Updated from 0.1.4. Up to date, checked 1 min ago.');
|
||||
assert.equal(V.updateNote({ status: 'off', version: '' }, NOW), 'Updates are off in this build.');
|
||||
assert.equal(V.updateLine({ status: 'downloading', version: '0.1.6', size: 20080717, progress: 0.43 }).text, 'Downloading Igneum Wallet 0.1.6 (20 MB): 43%');
|
||||
assert.equal(V.updateLine({ status: 'current', version: '0.1.5' }), null);
|
||||
});
|
||||
|
||||
// the page bridge (0.1.6, 8 October 2026): what the window says for a page's request (connect, a transaction, a message,
|
||||
// typed data), the Settings card's site rows, the lock-screen line while a page waits. Known-failed first on 0.1.5:
|
||||
// View.bridgeWords is not a function.
|
||||
test('the page bridge: the words for each request kind, the site rows, the waiting line', () => {
|
||||
const connect = V.bridgeWords({ id: '1-ab', kind: 'connect', origin: 'https://igneum.network', created_at: 1 });
|
||||
assert.equal(connect.title, 'igneum.network wants to connect');
|
||||
assert.equal(connect.lines[0], 'It will see your address and may ask you to sign or send. Nothing leaves without your word here.');
|
||||
assert.equal(connect.yes, 'Connect'); assert.equal(connect.no, 'Decline');
|
||||
const send = V.bridgeWords({ id: '2-cd', kind: 'send', origin: 'https://igneum.network', to: '0x9a6fa842c4e58a87aef1f3ad15233d99283002b7', to_display: '0x9a6fA842C4e58A87AEF1F3aD15233d99283002B7', value: '1000000000000000000', value_ign: '1', data_len: 36, selector: '0x38ed1739', gas: 184000, fee_max_ign: '0.000368', total_max_ign: '1.000368', confirm_needed: false });
|
||||
assert.equal(send.title, 'igneum.network asks you to send');
|
||||
assert.equal(send.amount, '1 IGN');
|
||||
assert.equal(send.lines[0], 'To 0x9a6fA8…02B7, a contract call (36 bytes, 0x38ed1739).');
|
||||
assert.equal(send.lines[1], 'Fee up to 0.000368 IGN (184,000 gas). Total up to 1.000368 IGN.');
|
||||
assert.equal(send.yes, 'Send'); assert.equal(send.no, 'Decline');
|
||||
const plain = V.bridgeWords({ id: '3', kind: 'send', origin: 'https://igneum.network', to_display: '0x9a6fA842C4e58A87AEF1F3aD15233d99283002B7', value_ign: '0.5', data_len: 0, selector: '', gas: 21000, fee_max_ign: '0.000042', total_max_ign: '0.500042' });
|
||||
assert.equal(plain.lines[0], 'To 0x9a6fA8…02B7, a plain transfer.');
|
||||
const unpriced = V.bridgeWords({ id: '4', kind: 'send', origin: 'https://igneum.network', to_display: '0x9a6fA842C4e58A87AEF1F3aD15233d99283002B7', value: '0', data_len: 4, selector: '0xd0e30db0' });
|
||||
assert.equal(unpriced.amount, '0 IGN'); assert.equal(unpriced.lines[1], 'Pricing the fee with the node.');
|
||||
const touch = V.bridgeWords({ id: '5', kind: 'send', origin: 'https://igneum.network', to_display: '0x9a6fA842C4e58A87AEF1F3aD15233d99283002B7', value_ign: '1', data_len: 0, selector: '', gas: 21000, fee_max_ign: '0.00004', total_max_ign: '1.00004', confirm_needed: true }, 'Touch ID');
|
||||
assert.equal(touch.yes, 'Send with Touch ID');
|
||||
const sign = V.bridgeWords({ id: '6', kind: 'sign', origin: 'https://igneum.network', text: 'Sign in to Igneum Swap\nnonce: 42', bytes: 31 });
|
||||
assert.equal(sign.title, 'igneum.network asks you to sign a message');
|
||||
assert.equal(sign.message, 'Sign in to Igneum Swap\nnonce: 42');
|
||||
assert.equal(sign.lines[0], 'Signing proves this wallet is yours. It moves no coins.');
|
||||
assert.equal(sign.yes, 'Sign');
|
||||
const bin = V.bridgeWords({ id: '7', kind: 'sign', origin: 'https://igneum.network', text: null, bytes: 32, hex: '0x0102' });
|
||||
assert.equal(bin.message, '32 bytes: 0x0102');
|
||||
const typed = V.bridgeWords({ id: '8', kind: 'typed', origin: 'https://igneum.network', domain: 'Igneum Swap', primary_type: 'Permit', message: '{\n "owner": "0x1"\n}' });
|
||||
assert.equal(typed.title, 'igneum.network asks you to sign typed data');
|
||||
assert.equal(typed.lines[0], 'Permit for Igneum Swap. Read it before you sign: a permit can let a contract spend tokens.');
|
||||
assert.equal(typed.message, '{\n "owner": "0x1"\n}');
|
||||
// the lock-screen line and the site rows
|
||||
assert.equal(V.bridgeWaiting([connect, send].map((x) => ({ origin: 'https://igneum.network' }))), 'igneum.network is waiting: unlock to answer it');
|
||||
assert.equal(V.bridgeWaiting([]), '');
|
||||
const rows = V.siteRows([{ origin: 'https://igneum.network', approved_at: 1_800_000_000 - 3600, last_seen: 1_800_000_000 - 30 }], 1_800_000_000);
|
||||
assert.equal(rows[0].host, 'igneum.network'); assert.equal(rows[0].line, 'connected 1 h ago · last call 30 s ago');
|
||||
assert.equal(V.siteRows([], 1).length, 0);
|
||||
assert.equal(V.bridgeStatus({ on: true, listening: true, port: 26811, sites: [] }), 'On. Websites you approve can connect through port 26811 on this machine only.');
|
||||
assert.equal(V.bridgeStatus({ on: true, listening: false, port: 26811, reason: 'port 26811 is not free: in use', sites: [] }), 'Not listening: port 26811 is not free: in use. Quit whatever holds the port and open the wallet again.');
|
||||
assert.equal(V.bridgeStatus({ on: false, listening: true, port: 26811, sites: [] }), 'Off. No website can connect; the switch turns it on.');
|
||||
});
|
||||
328
app/mac/Biometric.swift
Normal file
328
app/mac/Biometric.swift
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
// Touch ID for the Igneum window hosts (IgneumWallet.swift, IgneumMiner.swift), compiled into each with
|
||||
// swiftc ... IgneumWallet.swift Biometric.swift -framework Cocoa -framework WebKit -framework LocalAuthentication
|
||||
// 5 October 2026.
|
||||
//
|
||||
// The page in the WKWebView posts {id, op, ...} to the "biometric" script message handler; the host answers with
|
||||
// window.__igneumBiometric(id, {ok, code, message, ...}). Ops: status, enrol, unlock (wallet), confirm.
|
||||
//
|
||||
// What is stored, and where. The packaging signs the apps ad hoc, and under an ad hoc signature macOS refuses any
|
||||
// Keychain item with a biometric access control (errSecMissingEntitlement, -34018, on the login keychain and the
|
||||
// data-protection keychain alike: keychain-access-groups needs a team signature). A Secure Enclave key with the same
|
||||
// control is allowed, so the secret is sealed to one instead:
|
||||
// - enrol: a P-256 key is made in the Secure Enclave with SecAccessControl(.privateKeyUsage, .biometryCurrentSet);
|
||||
// an ephemeral P-256 key agrees a shared secret with its public half (no prompt), HKDF-SHA256 makes an AES-GCM key
|
||||
// and the secret (the wallet's password; a fixed marker for the miner) is sealed. The file the engine named
|
||||
// (<data root>/<app>/biometric.json, user-only permissions) holds the Secure Enclave key's wrapped form, the
|
||||
// ephemeral public key and the box. The wrapped key is useless off this Mac's Secure Enclave.
|
||||
// - unlock or confirm: the host evaluates LAPolicy.deviceOwnerAuthenticationWithBiometrics (no fallback button: the
|
||||
// wallet's own password is the fallback, in the window), then uses the Secure Enclave key under that context.
|
||||
// .biometryCurrentSet means a fingerprint added or removed in System Settings makes the key unusable: the host
|
||||
// reports "invalidated" and the window asks for the password and a fresh enrolment.
|
||||
// The secret never goes through the page: on unlock the host posts it to the engine on 127.0.0.1 with the engine's
|
||||
// URL token; the engine's host token (X-Igneum-Host, read from the engine's stdout) marks the calls only the host
|
||||
// may make (the confirmations, taking the parked password at enrolment).
|
||||
|
||||
import Foundation
|
||||
import LocalAuthentication
|
||||
import CryptoKit
|
||||
import Security
|
||||
|
||||
final class Biometric {
|
||||
let product: String
|
||||
/// the prompt's fallback button: "Use password" for the wallet (the window then asks for it), "" for the miner
|
||||
let fallbackTitle: String
|
||||
/// the enrolment prompt's line: "Turn on Touch ID for your wallet" / "... for the miner"
|
||||
let enrolReason: String
|
||||
private(set) var engineURL = ""
|
||||
private(set) var hostToken = ""
|
||||
private(set) var file: URL?
|
||||
private let queue = DispatchQueue(label: "network.igneum.biometric")
|
||||
private let salt = Data("igneum-biometric-v1".utf8)
|
||||
private let minerMarker = "igneum-biometric-marker-v1"
|
||||
|
||||
init(product: String, fallbackTitle: String, enrolReason: String) {
|
||||
self.product = product
|
||||
self.fallbackTitle = fallbackTitle
|
||||
self.enrolReason = enrolReason
|
||||
}
|
||||
|
||||
/// From the engine's HOST line. Reports availability to the engine at once.
|
||||
func configure(engineURL: String, hostToken: String, file: String) {
|
||||
self.engineURL = engineURL
|
||||
self.hostToken = hostToken
|
||||
self.file = file.isEmpty ? nil : URL(fileURLWithPath: file)
|
||||
let s = status()
|
||||
queue.async {
|
||||
_ = self.post("api/biometric/status", ["available": s.available, "kind": "touchid", "message": s.message])
|
||||
}
|
||||
}
|
||||
|
||||
// ---- availability ----
|
||||
|
||||
func status() -> (available: Bool, message: String) {
|
||||
let ctx = LAContext()
|
||||
var err: NSError?
|
||||
if ctx.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &err) {
|
||||
if ctx.biometryType != .touchID { return (false, "This Mac has no Touch ID sensor.") }
|
||||
return (true, "")
|
||||
}
|
||||
return (false, Biometric.text(for: err, op: "status").message)
|
||||
}
|
||||
|
||||
// ---- the ops, one at a time on the queue ----
|
||||
|
||||
func handle(_ msg: [String: Any], reply: @escaping ([String: Any]) -> Void) {
|
||||
let op = msg["op"] as? String ?? ""
|
||||
queue.async {
|
||||
let r: [String: Any]
|
||||
switch op {
|
||||
case "status":
|
||||
let s = self.status()
|
||||
r = ["ok": true, "available": s.available, "kind": "touchid", "message": s.message, "enrolled": self.sealedFileExists()]
|
||||
case "enrol": r = self.enrol(token: msg["token"] as? String)
|
||||
case "unlock": r = self.unlock()
|
||||
case "confirm": r = self.confirm(nonce: msg["nonce"] as? String ?? "")
|
||||
default: r = ["ok": false, "code": "bad_op", "message": "unknown op \(op)"]
|
||||
}
|
||||
if op != "status" {
|
||||
_ = self.post("api/biometric/report", ["op": op, "ok": r["ok"] as? Bool ?? false, "code": r["code"] as? String ?? "", "message": r["message"] as? String ?? ""])
|
||||
}
|
||||
DispatchQueue.main.async { reply(r) }
|
||||
}
|
||||
}
|
||||
|
||||
private func enrol(token: String?) -> [String: Any] {
|
||||
guard let file = file else { return fail("unavailable", "The engine has not named the sealed file yet.") }
|
||||
let s = status()
|
||||
if !s.available { return fail("unavailable", s.message) }
|
||||
let ctx = context()
|
||||
if let e = evaluate(ctx, reason: enrolReason) { return e }
|
||||
// the secret: the wallet's password from the engine (one-time token), or the miner's marker
|
||||
var secret: Data
|
||||
if let t = token, !t.isEmpty {
|
||||
let r = post("api/biometric/enrol/take", ["token": t])
|
||||
guard r.ok, let p = r.json["secret"] as? String else { return fail("engine", r.json["error"] as? String ?? "the engine did not hand over the password") }
|
||||
secret = Data(p.utf8)
|
||||
} else {
|
||||
secret = Data(minerMarker.utf8)
|
||||
}
|
||||
defer { secret.resetBytes(in: 0..<secret.count) }
|
||||
do {
|
||||
var acErr: Unmanaged<CFError>?
|
||||
guard let ac = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, [.privateKeyUsage, .biometryCurrentSet], &acErr) else {
|
||||
return fail("secure_enclave", "The access control could not be made: \(acErr?.takeRetainedValue().localizedDescription ?? "unknown")")
|
||||
}
|
||||
let key = try SecureEnclave.P256.KeyAgreement.PrivateKey(accessControl: ac, authenticationContext: ctx)
|
||||
let eph = P256.KeyAgreement.PrivateKey()
|
||||
let shared = try eph.sharedSecretFromKeyAgreement(with: key.publicKey)
|
||||
let sym = shared.hkdfDerivedSymmetricKey(using: SHA256.self, salt: salt, sharedInfo: Data(product.utf8), outputByteCount: 32)
|
||||
let box = try AES.GCM.seal(secret, using: sym)
|
||||
guard let combined = box.combined else { return fail("seal", "The box has no combined form.") }
|
||||
let doc: [String: Any] = ["version": 1, "kind": "touchid", "product": product, "created": Int(Date().timeIntervalSince1970),
|
||||
"key": key.dataRepresentation.base64EncodedString(), "eph": eph.publicKey.rawRepresentation.base64EncodedString(), "box": combined.base64EncodedString()]
|
||||
let data = try JSONSerialization.data(withJSONObject: doc, options: [.sortedKeys])
|
||||
try FileManager.default.createDirectory(at: file.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||
try data.write(to: file, options: [.atomic])
|
||||
try FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: file.path)
|
||||
} catch {
|
||||
return fail("secure_enclave", "The Secure Enclave refused: \(error.localizedDescription)")
|
||||
}
|
||||
let r = post("api/biometric/enrolled", ["kind": "touchid"])
|
||||
if !r.ok { return fail("engine", r.json["error"] as? String ?? "the engine did not record the enrolment") }
|
||||
return ["ok": true]
|
||||
}
|
||||
|
||||
/// Wallet: Touch ID, then the password out of the box and into the engine; never to the page.
|
||||
private func unlock() -> [String: Any] {
|
||||
guard sealedFileExists() else { return fail("not_enrolled", "Touch ID is not turned on for this wallet.") }
|
||||
let ctx = context()
|
||||
if let e = evaluate(ctx, reason: "Unlock your wallet") { return e }
|
||||
switch open(ctx) {
|
||||
case .failure(let e): return e.dict
|
||||
case .success(var secret):
|
||||
defer { secret.resetBytes(in: 0..<secret.count) }
|
||||
guard let p = String(data: secret, encoding: .utf8) else { return fail("seal", "The sealed password did not decode.") }
|
||||
let r = post("api/unlock", ["password": p])
|
||||
if !r.ok { return fail("engine", r.json["error"] as? String ?? "the engine did not unlock") }
|
||||
return ["ok": true]
|
||||
}
|
||||
}
|
||||
|
||||
/// The engine's challenge: its reason on the prompt, then the confirmation with the host token. The Secure
|
||||
/// Enclave key is exercised too, so a changed fingerprint set is caught here as well.
|
||||
private func confirm(nonce: String) -> [String: Any] {
|
||||
guard !nonce.isEmpty else { return fail("bad_nonce", "No challenge.") }
|
||||
let c = get("api/biometric/challenge?nonce=\(nonce)")
|
||||
guard c.ok, let reason = c.json["reason"] as? String else { return fail("engine", c.json["error"] as? String ?? "the engine does not know this challenge") }
|
||||
let ctx = context()
|
||||
if let e = evaluate(ctx, reason: reason) { return e }
|
||||
if sealedFileExists() {
|
||||
if case .failure(let e) = agree(ctx) { return e.dict }
|
||||
}
|
||||
let r = post("api/biometric/confirm", ["nonce": nonce])
|
||||
if !r.ok { return fail("engine", r.json["error"] as? String ?? "the engine refused the confirmation") }
|
||||
return ["ok": true]
|
||||
}
|
||||
|
||||
// ---- Touch ID ----
|
||||
|
||||
func context() -> LAContext {
|
||||
let ctx = LAContext()
|
||||
// the policy is biometrics only, so the fallback button never reaches the device password: "Use password"
|
||||
// (the wallet) returns LAError.userFallback and the window asks for the wallet's own password; the miner
|
||||
// has no password, so no button
|
||||
ctx.localizedFallbackTitle = fallbackTitle
|
||||
ctx.localizedCancelTitle = "Cancel"
|
||||
return ctx
|
||||
}
|
||||
|
||||
/// nil on success, else the reply for the page.
|
||||
private func evaluate(_ ctx: LAContext, reason: String) -> [String: Any]? {
|
||||
var err: NSError?
|
||||
guard ctx.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &err) else {
|
||||
let t = Biometric.text(for: err, op: "evaluate")
|
||||
return fail(t.code, t.message)
|
||||
}
|
||||
// macOS composes the sentence itself: "Igneum Wallet is trying to <reason>." (the bundled app's name and icon
|
||||
// are the title), so the line starts lowercase here; the engine's canonical lines keep their capital for
|
||||
// Windows Hello, which shows the line on its own
|
||||
let line = String(reason.prefix(80))
|
||||
let shown = line.prefix(1).lowercased() + line.dropFirst()
|
||||
let sem = DispatchSemaphore(value: 0)
|
||||
var ok = false
|
||||
var failure: Error?
|
||||
ctx.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: shown) { success, error in
|
||||
ok = success
|
||||
failure = error
|
||||
sem.signal()
|
||||
}
|
||||
sem.wait()
|
||||
if ok { return nil }
|
||||
let t = Biometric.text(for: failure as NSError?, op: "evaluate")
|
||||
return fail(t.code, t.message)
|
||||
}
|
||||
|
||||
static func text(for err: NSError?, op: String) -> (code: String, message: String) {
|
||||
guard let e = err else { return ("failed", "Touch ID did not succeed.") }
|
||||
if e.domain == LAErrorDomain, let la = LAError.Code(rawValue: e.code) {
|
||||
switch la {
|
||||
case .userCancel, .systemCancel, .appCancel: return ("cancelled", "Touch ID was cancelled.")
|
||||
case .authenticationFailed: return ("failed", "Touch ID did not match.")
|
||||
case .biometryLockout: return ("locked", "Touch ID is locked after too many tries. Use the password; Touch ID comes back after the Mac is unlocked with its password.")
|
||||
case .biometryNotEnrolled: return ("not_set_up", "Touch ID is not set up on this Mac. Add a fingerprint in System Settings > Touch ID & Password.")
|
||||
case .biometryNotAvailable, .passcodeNotSet: return ("unavailable", "Touch ID is not available on this Mac.")
|
||||
case .userFallback: return ("fallback", "Enter your password.")
|
||||
default: break
|
||||
}
|
||||
}
|
||||
return ("failed", "Touch ID did not succeed: \(e.localizedDescription)")
|
||||
}
|
||||
|
||||
// ---- the sealed file ----
|
||||
|
||||
func sealedFileExists() -> Bool {
|
||||
guard let f = file else { return false }
|
||||
return FileManager.default.fileExists(atPath: f.path)
|
||||
}
|
||||
|
||||
private struct Sealed {
|
||||
let key: SecureEnclave.P256.KeyAgreement.PrivateKey
|
||||
let eph: P256.KeyAgreement.PublicKey
|
||||
let box: AES.GCM.SealedBox
|
||||
}
|
||||
|
||||
private func load(_ ctx: LAContext) -> Result<Sealed, Reply> {
|
||||
guard let f = file, let data = try? Data(contentsOf: f), let doc = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any],
|
||||
let k = doc["key"] as? String, let e = doc["eph"] as? String, let b = doc["box"] as? String,
|
||||
let kd = Data(base64Encoded: k), let ed = Data(base64Encoded: e), let bd = Data(base64Encoded: b) else {
|
||||
return .failure(failure("not_enrolled", "The sealed file is missing or unreadable. Turn Touch ID on again in Settings."))
|
||||
}
|
||||
do {
|
||||
let key = try SecureEnclave.P256.KeyAgreement.PrivateKey(dataRepresentation: kd, authenticationContext: ctx)
|
||||
let eph = try P256.KeyAgreement.PublicKey(rawRepresentation: ed)
|
||||
let box = try AES.GCM.SealedBox(combined: bd)
|
||||
return .success(Sealed(key: key, eph: eph, box: box))
|
||||
} catch {
|
||||
return .failure(failure("invalidated", "Touch ID no longer opens this: the sealed key is not usable (\(error.localizedDescription)). Use the password, then turn Touch ID on again in Settings."))
|
||||
}
|
||||
}
|
||||
|
||||
/// The key agreement under the authenticated context: the Secure Enclave refuses it when the fingerprint set
|
||||
/// changed since enrolment (.biometryCurrentSet).
|
||||
private func agree(_ ctx: LAContext) -> Result<SymmetricKey, Reply> {
|
||||
switch load(ctx) {
|
||||
case .failure(let e): return .failure(e)
|
||||
case .success(let s):
|
||||
do {
|
||||
let shared = try s.key.sharedSecretFromKeyAgreement(with: s.eph)
|
||||
return .success(shared.hkdfDerivedSymmetricKey(using: SHA256.self, salt: salt, sharedInfo: Data(product.utf8), outputByteCount: 32))
|
||||
} catch {
|
||||
return .failure(failure("invalidated", "Touch ID no longer opens this: a fingerprint was added or removed since it was turned on. Use the password, then turn Touch ID on again in Settings."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func open(_ ctx: LAContext) -> Result<Data, Reply> {
|
||||
switch load(ctx) {
|
||||
case .failure(let e): return .failure(e)
|
||||
case .success(let s):
|
||||
do {
|
||||
let shared = try s.key.sharedSecretFromKeyAgreement(with: s.eph)
|
||||
let sym = shared.hkdfDerivedSymmetricKey(using: SHA256.self, salt: salt, sharedInfo: Data(product.utf8), outputByteCount: 32)
|
||||
return .success(try AES.GCM.open(s.box, using: sym))
|
||||
} catch {
|
||||
return .failure(failure("invalidated", "Touch ID no longer opens this: a fingerprint was added or removed since it was turned on. Use the password, then turn Touch ID on again in Settings."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- the engine on 127.0.0.1 ----
|
||||
|
||||
/// A reply for the page that is also an Error, so Result can carry it.
|
||||
struct Reply: Error {
|
||||
let dict: [String: Any]
|
||||
}
|
||||
|
||||
private func fail(_ code: String, _ message: String) -> [String: Any] {
|
||||
["ok": false, "code": code, "message": message]
|
||||
}
|
||||
private func failure(_ code: String, _ message: String) -> Reply {
|
||||
Reply(dict: fail(code, message))
|
||||
}
|
||||
|
||||
private struct Answer {
|
||||
let ok: Bool
|
||||
let json: [String: Any]
|
||||
}
|
||||
|
||||
private func request(_ path: String, method: String, body: [String: Any]?) -> Answer {
|
||||
guard !engineURL.isEmpty, let url = URL(string: engineURL + path) else { return Answer(ok: false, json: ["error": "no engine URL"]) }
|
||||
var req = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 15)
|
||||
req.httpMethod = method
|
||||
req.setValue(hostToken, forHTTPHeaderField: "X-Igneum-Host")
|
||||
if let b = body {
|
||||
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
req.httpBody = try? JSONSerialization.data(withJSONObject: b)
|
||||
}
|
||||
let sem = DispatchSemaphore(value: 0)
|
||||
var out = Answer(ok: false, json: ["error": "no answer from the engine"])
|
||||
let task = URLSession.shared.dataTask(with: req) { data, resp, error in
|
||||
defer { sem.signal() }
|
||||
if let e = error { out = Answer(ok: false, json: ["error": e.localizedDescription]); return }
|
||||
let code = (resp as? HTTPURLResponse)?.statusCode ?? 0
|
||||
let j = data.flatMap { (try? JSONSerialization.jsonObject(with: $0)) as? [String: Any] } ?? [:]
|
||||
out = Answer(ok: code == 200 && (j["ok"] as? Bool ?? true), json: j)
|
||||
}
|
||||
task.resume()
|
||||
sem.wait()
|
||||
return out
|
||||
}
|
||||
|
||||
private func post(_ path: String, _ body: [String: Any]) -> Answer {
|
||||
request(path, method: "POST", body: body)
|
||||
}
|
||||
|
||||
private func get(_ path: String) -> Answer {
|
||||
request(path, method: "GET", body: nil)
|
||||
}
|
||||
}
|
||||
399
app/mac/IgneumWallet.swift
Normal file
399
app/mac/IgneumWallet.swift
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
// Igneum Wallet for macOS: the window around the wallet engine. A copy of IgneumMiner.swift with the names, the
|
||||
// bundle and the menu changed (no pause; a Lock item instead). Compiled by packaging/mac/build-wallet-dmg.sh with
|
||||
// swiftc -O -target arm64-apple-macos11 -o "Igneum Wallet" IgneumWallet.swift Biometric.swift -framework Cocoa -framework WebKit -framework LocalAuthentication
|
||||
// It starts Contents/MacOS/igneum-wallet --wrapper, reads "URL ...", "HOST {...}" and "STATE {...}" lines from its
|
||||
// stdout, shows the URL in a WKWebView, keeps a menu-bar item with the state, and on quit writes "quit" to the engine's
|
||||
// stdin and waits for its "EXIT" line (the bundled node stops first when one runs). Closing the window hides it; the
|
||||
// app lives on in the menu bar and the Dock. 4 October 2026. Touch ID (Biometric.swift, the "biometric" script
|
||||
// message handler): 5 October 2026.
|
||||
//
|
||||
// Snapshot mode, used to make the design screenshots without a browser:
|
||||
// "Igneum Wallet" --snapshot <out.png> --url <window url> [--size 1120x780]
|
||||
|
||||
import Cocoa
|
||||
import WebKit
|
||||
|
||||
let obsidian = NSColor(srgbRed: 12 / 255, green: 12 / 255, blue: 14 / 255, alpha: 1)
|
||||
|
||||
func flameImage(size: CGFloat) -> NSImage {
|
||||
// the brand mark's flame as a template image for the menu bar
|
||||
let img = NSImage(size: NSSize(width: size, height: size), flipped: true) { rect in
|
||||
let s = rect.width / 100
|
||||
let outer = NSBezierPath()
|
||||
let pts: [(CGFloat, CGFloat)] = [(50, 4), (74, 34), (67, 58), (80, 54), (61, 96), (39, 96), (20, 54), (33, 58), (26, 34)]
|
||||
outer.move(to: NSPoint(x: pts[0].0 * s, y: pts[0].1 * s))
|
||||
for p in pts.dropFirst() { outer.line(to: NSPoint(x: p.0 * s, y: p.1 * s)) }
|
||||
outer.close()
|
||||
let inner = NSBezierPath()
|
||||
let ip: [(CGFloat, CGFloat)] = [(50, 42), (59, 58), (50, 82), (41, 58)]
|
||||
inner.move(to: NSPoint(x: ip[0].0 * s, y: ip[0].1 * s))
|
||||
for p in ip.dropFirst() { inner.line(to: NSPoint(x: p.0 * s, y: p.1 * s)) }
|
||||
inner.close()
|
||||
outer.append(inner)
|
||||
outer.windingRule = .evenOdd
|
||||
NSColor.black.setFill()
|
||||
outer.fill()
|
||||
return true
|
||||
}
|
||||
img.isTemplate = true
|
||||
return img
|
||||
}
|
||||
|
||||
/// A clear strip over the top band of the web view: WKWebView swallows window drags, this view lets the window move.
|
||||
final class DragStrip: NSView {
|
||||
override var mouseDownCanMoveWindow: Bool { true }
|
||||
override func hitTest(_ point: NSPoint) -> NSView? { bounds.contains(point) ? self : nil }
|
||||
}
|
||||
|
||||
final class App: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKUIDelegate, NSWindowDelegate, WKScriptMessageHandler {
|
||||
let bio = Biometric(product: "Igneum Wallet", fallbackTitle: "Use password", enrolReason: "Turn on Touch ID for your wallet")
|
||||
var enginePort = ""
|
||||
var window: NSWindow!
|
||||
var web: WKWebView!
|
||||
var engine: Process?
|
||||
var engineIn: FileHandle?
|
||||
var status: NSStatusItem!
|
||||
var menuOpen = NSMenuItem(title: "Open Igneum Wallet", action: #selector(showWindow), keyEquivalent: "")
|
||||
var menuPause = NSMenuItem(title: "Lock", action: #selector(lockWallet), keyEquivalent: "")
|
||||
var menuNode = NSMenuItem(title: "Node: looking", action: nil, keyEquivalent: "")
|
||||
var menuBlocks = NSMenuItem(title: "Balance: locked", action: nil, keyEquivalent: "")
|
||||
var url: URL?
|
||||
var paused = false
|
||||
var exited = false
|
||||
var quitting = false
|
||||
var buffer = Data()
|
||||
var placeholder: NSTextField!
|
||||
|
||||
// snapshot mode
|
||||
var snapshotPath: String?
|
||||
var snapshotURL: String?
|
||||
var snapshotSize = NSSize(width: 1120, height: 780)
|
||||
|
||||
func applicationDidFinishLaunching(_ note: Notification) {
|
||||
NSApp.setActivationPolicy(snapshotPath == nil ? .regular : .accessory)
|
||||
buildMenu()
|
||||
buildWindow()
|
||||
if let p = snapshotPath, let u = snapshotURL, let url = URL(string: u) {
|
||||
self.url = url
|
||||
enginePort = url.port.map(String.init) ?? ""
|
||||
// design screenshots and tests: IGNEUM_HOST_LINE (the engine's HOST json) wires the Touch ID bridge to a
|
||||
// scratch engine, IGNEUM_PROBE runs JS in the page, IGNEUM_SNAPSHOT_DELAY (s) waits before the capture
|
||||
if let h = ProcessInfo.processInfo.environment["IGNEUM_HOST_LINE"], let d = h.data(using: .utf8), let o = try? JSONSerialization.jsonObject(with: d) as? [String: Any] {
|
||||
bio.configure(engineURL: u.replacingOccurrences(of: "?host=mac", with: ""), hostToken: o["token"] as? String ?? "", file: o["biometric_file"] as? String ?? "")
|
||||
}
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
web.load(URLRequest(url: url))
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) { self.snapshot(to: p) }
|
||||
return
|
||||
}
|
||||
buildStatusItem()
|
||||
startEngine()
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
func buildMenu() {
|
||||
let main = NSMenu()
|
||||
let appItem = NSMenuItem(); main.addItem(appItem)
|
||||
let appMenu = NSMenu()
|
||||
appMenu.addItem(withTitle: "About Igneum Wallet", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")
|
||||
appMenu.addItem(.separator())
|
||||
appMenu.addItem(withTitle: "Hide Igneum Wallet", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h")
|
||||
appMenu.addItem(.separator())
|
||||
appMenu.addItem(withTitle: "Quit Igneum Wallet", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
|
||||
appItem.submenu = appMenu
|
||||
let editItem = NSMenuItem(); main.addItem(editItem)
|
||||
let edit = NSMenu(title: "Edit")
|
||||
edit.addItem(withTitle: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x")
|
||||
edit.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")
|
||||
edit.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")
|
||||
edit.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
|
||||
editItem.submenu = edit
|
||||
let winItem = NSMenuItem(); main.addItem(winItem)
|
||||
let win = NSMenu(title: "Window")
|
||||
win.addItem(withTitle: "Minimize", action: #selector(NSWindow.miniaturize(_:)), keyEquivalent: "m")
|
||||
win.addItem(withTitle: "Close", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w")
|
||||
winItem.submenu = win
|
||||
NSApp.mainMenu = main
|
||||
}
|
||||
|
||||
func buildWindow() {
|
||||
let size = snapshotPath == nil ? NSSize(width: 1120, height: 780) : snapshotSize
|
||||
window = NSWindow(contentRect: NSRect(origin: .zero, size: size), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], backing: .buffered, defer: false)
|
||||
window.title = "Igneum Wallet"
|
||||
window.titlebarAppearsTransparent = true
|
||||
window.titleVisibility = .hidden
|
||||
window.isMovableByWindowBackground = true
|
||||
window.backgroundColor = obsidian
|
||||
window.minSize = NSSize(width: 900, height: 620)
|
||||
window.center()
|
||||
window.delegate = self
|
||||
window.isReleasedWhenClosed = false
|
||||
let conf = WKWebViewConfiguration()
|
||||
conf.userContentController.add(self, name: "biometric") // the page's Touch ID bridge (Biometric.swift)
|
||||
web = WKWebView(frame: window.contentView!.bounds, configuration: conf)
|
||||
web.autoresizingMask = [.width, .height]
|
||||
web.navigationDelegate = self
|
||||
web.uiDelegate = self
|
||||
web.setValue(false, forKey: "drawsBackground")
|
||||
web.customUserAgent = "IgneumWallet/0.1.0 (Macintosh)"
|
||||
window.contentView?.addSubview(web)
|
||||
// the brand band is draggable; the pill and the settings button on the right stay clickable
|
||||
let strip = DragStrip(frame: NSRect(x: 0, y: size.height - 60, width: size.width - 300, height: 60))
|
||||
strip.autoresizingMask = [.width, .minYMargin]
|
||||
window.contentView?.addSubview(strip)
|
||||
placeholder = NSTextField(labelWithString: "Starting the engine")
|
||||
placeholder.font = NSFont.monospacedSystemFont(ofSize: 13, weight: .medium)
|
||||
placeholder.textColor = NSColor(srgbRed: 154 / 255, green: 154 / 255, blue: 158 / 255, alpha: 1)
|
||||
placeholder.alignment = .center
|
||||
placeholder.frame = NSRect(x: 0, y: 18, width: size.width, height: 20)
|
||||
placeholder.autoresizingMask = [.width, .maxYMargin]
|
||||
placeholder.isHidden = snapshotPath != nil
|
||||
window.contentView?.addSubview(placeholder)
|
||||
}
|
||||
|
||||
func buildStatusItem() {
|
||||
status = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||
status.button?.image = flameImage(size: 18)
|
||||
status.button?.imagePosition = .imageLeading
|
||||
status.button?.title = ""
|
||||
let menu = NSMenu()
|
||||
menu.addItem(menuOpen)
|
||||
menu.addItem(menuPause)
|
||||
menu.addItem(.separator())
|
||||
menuNode.isEnabled = false
|
||||
menuBlocks.isEnabled = false
|
||||
menu.addItem(menuNode)
|
||||
menu.addItem(menuBlocks)
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(withTitle: "Quit Igneum Wallet", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "")
|
||||
menu.autoenablesItems = false
|
||||
menuOpen.isEnabled = true
|
||||
menuPause.isEnabled = true
|
||||
status.menu = menu
|
||||
}
|
||||
|
||||
// ---- the engine ----
|
||||
func startEngine() {
|
||||
let exe = Bundle.main.bundleURL.appendingPathComponent("Contents/MacOS/igneum-wallet")
|
||||
guard FileManager.default.isExecutableFile(atPath: exe.path) else {
|
||||
fail("igneum-wallet is missing from the app bundle. Copy Igneum Wallet from the disk image again.")
|
||||
return
|
||||
}
|
||||
let p = Process()
|
||||
p.executableURL = exe
|
||||
p.arguments = ["--wrapper"]
|
||||
let out = Pipe(), inp = Pipe()
|
||||
p.standardOutput = out
|
||||
p.standardInput = inp
|
||||
p.standardError = FileHandle.standardError
|
||||
engineIn = inp.fileHandleForWriting
|
||||
out.fileHandleForReading.readabilityHandler = { h in
|
||||
let d = h.availableData
|
||||
if d.isEmpty { return }
|
||||
DispatchQueue.main.async { self.feed(d) }
|
||||
}
|
||||
p.terminationHandler = { _ in
|
||||
DispatchQueue.main.async { self.engineEnded() }
|
||||
}
|
||||
do { try p.run() } catch {
|
||||
fail("The engine could not start: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
engine = p
|
||||
}
|
||||
|
||||
func feed(_ d: Data) {
|
||||
buffer.append(d)
|
||||
while let nl = buffer.firstIndex(of: 10) {
|
||||
let lineData = buffer.subdata(in: 0..<nl)
|
||||
buffer.removeSubrange(0...nl)
|
||||
guard let line = String(data: lineData, encoding: .utf8) else { continue }
|
||||
if line.hasPrefix("URL ") {
|
||||
let u = String(line.dropFirst(4)).trimmingCharacters(in: .whitespaces)
|
||||
if let url = URL(string: u + "?host=mac") {
|
||||
self.url = url
|
||||
enginePort = url.port.map(String.init) ?? ""
|
||||
placeholder.isHidden = true
|
||||
web.load(URLRequest(url: url))
|
||||
}
|
||||
} else if line.hasPrefix("HOST ") {
|
||||
// the host token and the sealed file's path: the page never sees this line
|
||||
if let d = String(line.dropFirst(5)).data(using: .utf8), let o = try? JSONSerialization.jsonObject(with: d) as? [String: Any] {
|
||||
bio.configure(engineURL: self.url?.absoluteString.replacingOccurrences(of: "?host=mac", with: "") ?? "", hostToken: o["token"] as? String ?? "", file: o["biometric_file"] as? String ?? "")
|
||||
}
|
||||
} else if line.hasPrefix("STATE ") {
|
||||
applyState(String(line.dropFirst(6)))
|
||||
} else if line.hasPrefix("FATAL ") {
|
||||
fail(String(line.dropFirst(6)))
|
||||
} else if line == "EXIT" {
|
||||
exited = true
|
||||
if quitting { NSApp.reply(toApplicationShouldTerminate: true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyState(_ json: String) {
|
||||
guard let data = json.data(using: .utf8), let o = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return }
|
||||
let node = o["node"] as? String ?? ""
|
||||
let source = o["source"] as? String ?? ""
|
||||
let block = o["block"] as? Double ?? 0
|
||||
let unlocked = o["unlocked"] as? Bool ?? false
|
||||
let balance = o["balance"] as? String ?? ""
|
||||
let phase = o["phase"] as? String ?? ""
|
||||
var title = ""
|
||||
if o["quitting"] as? Bool == true { title = "stopping" }
|
||||
else if phase != "home" { title = "" }
|
||||
else if unlocked && !balance.isEmpty { title = "\(balance) IGN" }
|
||||
else if !unlocked { title = "locked" }
|
||||
status.button?.title = title.isEmpty ? "" : " " + title
|
||||
menuPause.title = unlocked ? "Lock" : "Locked"
|
||||
menuPause.isEnabled = unlocked
|
||||
let nf = NumberFormatter(); nf.numberStyle = .decimal
|
||||
menuNode.title = "Node: \(source) \(node), block \(nf.string(from: NSNumber(value: block)) ?? "0")"
|
||||
menuBlocks.title = unlocked ? "Balance: \(balance) IGN" : "Balance: locked"
|
||||
}
|
||||
|
||||
func engineEnded() {
|
||||
exited = true
|
||||
if quitting { NSApp.reply(toApplicationShouldTerminate: true); return }
|
||||
placeholder.stringValue = "The engine stopped. Quit and open Igneum Wallet again."
|
||||
placeholder.isHidden = false
|
||||
status?.button?.title = " stopped"
|
||||
}
|
||||
|
||||
func fail(_ text: String) {
|
||||
placeholder.stringValue = text
|
||||
placeholder.isHidden = false
|
||||
let a = NSAlert()
|
||||
a.messageText = "Igneum Wallet"
|
||||
a.informativeText = text
|
||||
a.runModal()
|
||||
}
|
||||
|
||||
@objc func showWindow() {
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
@objc func lockWallet() {
|
||||
send("lock")
|
||||
}
|
||||
|
||||
func send(_ cmd: String) {
|
||||
if let d = (cmd + "\n").data(using: .utf8) { engineIn?.write(d) }
|
||||
}
|
||||
|
||||
// ---- quit: miners first, then the node ----
|
||||
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
||||
if snapshotPath != nil { return .terminateNow }
|
||||
guard let e = engine, e.isRunning, !exited else { return .terminateNow }
|
||||
quitting = true
|
||||
status?.button?.title = " stopping"
|
||||
web.evaluateJavaScript("document.getElementById('pill-text').textContent='stopping'", completionHandler: nil)
|
||||
send("quit")
|
||||
// 45 s is the engine's own worst case (30 s for the bundled node to close its database); then force
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 45) {
|
||||
if self.engine?.isRunning == true { self.engine?.terminate() }
|
||||
NSApp.reply(toApplicationShouldTerminate: true)
|
||||
}
|
||||
return .terminateLater
|
||||
}
|
||||
|
||||
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
|
||||
showWindow()
|
||||
return true
|
||||
}
|
||||
|
||||
func windowShouldClose(_ sender: NSWindow) -> Bool {
|
||||
// the window hides; the miner keeps running in the menu bar
|
||||
window.orderOut(nil)
|
||||
return false
|
||||
}
|
||||
|
||||
// ---- Touch ID: the page posts {id, op, ...}; the answer goes back as window.__igneumBiometric(id, {...}) ----
|
||||
func userContentController(_ ucc: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
guard message.name == "biometric", let body = message.body as? [String: Any], let id = body["id"] as? Int else { return }
|
||||
// only the engine's own page, same origin as the URL the engine printed
|
||||
let origin = message.frameInfo.securityOrigin
|
||||
guard origin.host == "127.0.0.1", String(origin.port) == enginePort else { return }
|
||||
bio.handle(body) { result in
|
||||
guard let d = try? JSONSerialization.data(withJSONObject: result), let j = String(data: d, encoding: .utf8) else { return }
|
||||
self.web.evaluateJavaScript("window.__igneumBiometric && window.__igneumBiometric(\(id), \(j))", completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- links: anything off 127.0.0.1 opens in the default browser ----
|
||||
func webView(_ webView: WKWebView, decidePolicyFor action: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||
if let u = action.request.url, let scheme = u.scheme, scheme.hasPrefix("http"), u.host != "127.0.0.1" {
|
||||
NSWorkspace.shared.open(u)
|
||||
decisionHandler(.cancel)
|
||||
return
|
||||
}
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
|
||||
let a = NSAlert()
|
||||
a.messageText = "Igneum Wallet"
|
||||
a.informativeText = message
|
||||
a.addButton(withTitle: "Quit")
|
||||
a.addButton(withTitle: "Cancel")
|
||||
completionHandler(a.runModal() == .alertFirstButtonReturn)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
|
||||
let a = NSAlert(); a.messageText = "Igneum Wallet"; a.informativeText = message; a.runModal(); completionHandler()
|
||||
}
|
||||
|
||||
// ---- snapshot ----
|
||||
func snapshot(to path: String) {
|
||||
if let js = ProcessInfo.processInfo.environment["IGNEUM_PROBE"] {
|
||||
web.evaluateJavaScript(js) { r, e in print("probe:", r ?? "nil", e?.localizedDescription ?? "") }
|
||||
}
|
||||
let delay = Double(ProcessInfo.processInfo.environment["IGNEUM_SNAPSHOT_DELAY"] ?? "") ?? 0
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { self.capture(to: path) }
|
||||
}
|
||||
|
||||
func capture(to path: String) {
|
||||
let conf = WKSnapshotConfiguration()
|
||||
conf.rect = web.bounds
|
||||
web.takeSnapshot(with: conf) { image, error in
|
||||
defer { NSApp.terminate(nil) }
|
||||
guard let img = image, let tiff = img.tiffRepresentation, let rep = NSBitmapImageRep(data: tiff), let png = rep.representation(using: .png, properties: [:]) else {
|
||||
FileHandle.standardError.write("snapshot failed: \(error?.localizedDescription ?? "no image")\n".data(using: .utf8)!)
|
||||
return
|
||||
}
|
||||
do { try png.write(to: URL(fileURLWithPath: path)); print("wrote \(path)") } catch { print("could not write \(path): \(error)") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The entry point. With Biometric.swift compiled alongside, top-level statements are not allowed here (only in a
|
||||
// main.swift), so the launch lives in a @main type.
|
||||
@main
|
||||
struct Main {
|
||||
static func main() {
|
||||
let app = NSApplication.shared
|
||||
let delegate = App()
|
||||
let args = Array(CommandLine.arguments.dropFirst())
|
||||
var i = 0
|
||||
while i < args.count {
|
||||
switch args[i] {
|
||||
case "--snapshot": if i + 1 < args.count { delegate.snapshotPath = args[i + 1]; i += 1 }
|
||||
case "--url": if i + 1 < args.count { delegate.snapshotURL = args[i + 1]; i += 1 }
|
||||
case "--size":
|
||||
if i + 1 < args.count {
|
||||
let parts = args[i + 1].split(separator: "x").compactMap { Double($0) }
|
||||
if parts.count == 2 { delegate.snapshotSize = NSSize(width: parts[0], height: parts[1]) }
|
||||
i += 1
|
||||
}
|
||||
default: break
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
app.delegate = delegate
|
||||
app.run()
|
||||
}
|
||||
}
|
||||
61
app/windows/BUILD-WALLET-APP.bat
Normal file
61
app/windows/BUILD-WALLET-APP.bat
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
@echo off
|
||||
rem Builds "Igneum Wallet.exe" (the WebView2 window host) on a Windows PC. Double-click this file.
|
||||
rem Needs Visual Studio with the MSVC v143 x64 component (cl.exe, rc.exe), the Windows SDK's C++/WinRT headers
|
||||
rem (Windows Hello, biometric.h) and the internet on the first run (the WebView2 SDK comes from NuGet). The output lands in dist\ and, when the extracted payload folder
|
||||
rem (igneum-windows-app, with igneum-wallet.exe) is next to this folder or its parent, is copied into it, so
|
||||
rem packaging\windows\BUILD-INSTALLER.bat ships it. Without this exe the installer still works: the Start Menu entry
|
||||
rem runs igneum-wallet.exe --launch, which opens the dashboard in the default browser.
|
||||
setlocal EnableDelayedExpansion
|
||||
cd /d "%~dp0"
|
||||
set "SDKVER=1.0.2903.40"
|
||||
set "SDKURL=https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2/%SDKVER%"
|
||||
if not exist build mkdir build
|
||||
if not exist dist mkdir dist
|
||||
|
||||
rem ---- 1. the MSVC environment ----
|
||||
set "VCVARS="
|
||||
for %%d in ("C:\Program Files\Microsoft Visual Studio" "C:\Program Files (x86)\Microsoft Visual Studio") do (
|
||||
for /f "delims=" %%f in ('dir /s /b "%%~d\vcvarsall.bat" 2^>nul') do set "VCVARS=%%f"
|
||||
)
|
||||
if "%VCVARS%"=="" (
|
||||
echo Visual Studio with the MSVC v143 x64 component was not found ^(no vcvarsall.bat under Program Files\Microsoft Visual Studio^).
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo [build] MSVC: "%VCVARS%"
|
||||
call "%VCVARS%" x64 >nul 2>&1
|
||||
where cl.exe >nul 2>nul || (echo cl.exe is not on PATH after vcvarsall; open a "x64 Native Tools" prompt and run this file from there. & pause & exit /b 1)
|
||||
|
||||
rem ---- 2. the WebView2 SDK (NuGet package, a zip) ----
|
||||
if not exist "build\webview2\build\native\include\WebView2.h" (
|
||||
echo [build] downloading the WebView2 SDK %SDKVER%
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "$ProgressPreference='SilentlyContinue'; [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '%SDKURL%' -OutFile 'build\webview2.zip' -UseBasicParsing; if (Test-Path 'build\webview2') { Remove-Item -Recurse -Force 'build\webview2' }; Expand-Archive -Path 'build\webview2.zip' -DestinationPath 'build\webview2' -Force"
|
||||
if not exist "build\webview2\build\native\include\WebView2.h" (echo the SDK did not unpack; see build\webview2 & pause & exit /b 1)
|
||||
)
|
||||
|
||||
rem ---- 3. the art: brand\icons in the repo layout, or an art\ folder next to this file ----
|
||||
set "ART="
|
||||
if exist "..\..\brand\icons\igneum.ico" set "ART=..\..\brand\icons"
|
||||
if exist "art\igneum.ico" set "ART=art"
|
||||
if exist "..\brand\icons\igneum.ico" set "ART=..\brand\icons"
|
||||
if "%ART%"=="" (echo igneum.ico was not found ^(brand\icons or an art\ folder^). & pause & exit /b 1)
|
||||
|
||||
rem ---- 4. compile ----
|
||||
echo [build] rc
|
||||
rc.exe /nologo /i "%ART%" /fo build\host.res wallet-host.rc || (pause & exit /b 1)
|
||||
echo [build] cl
|
||||
cl.exe /nologo /O2 /MT /EHsc /W3 /std:c++17 /DUNICODE /D_UNICODE /I "build\webview2\build\native\include" /Fo"build\\" wallet-host.cpp build\host.res ^
|
||||
/link /SUBSYSTEM:WINDOWS /OUT:"dist\Igneum Wallet.exe" "build\webview2\build\native\x64\WebView2LoaderStatic.lib" ^
|
||||
user32.lib shell32.lib ole32.lib advapi32.lib gdi32.lib version.lib shlwapi.lib windowsapp.lib crypt32.lib winhttp.lib || (pause & exit /b 1)
|
||||
echo [build] done: dist\Igneum Wallet.exe
|
||||
|
||||
rem ---- 5. into the payload, when it is here ----
|
||||
for %%p in ("igneum-windows-app" "..\igneum-windows-app" "..\..\igneum-windows-app" "..\..\packaging\windows\igneum-windows-app") do (
|
||||
if exist "%%~p\igneum-wallet.exe" (
|
||||
copy /y "dist\Igneum Wallet.exe" "%%~p\" >nul
|
||||
echo [build] copied into %%~p
|
||||
)
|
||||
)
|
||||
echo.
|
||||
echo Next: packaging\windows\BUILD-INSTALLER.bat builds the Setup exe with this host inside.
|
||||
pause
|
||||
377
app/windows/biometric.h
Normal file
377
app/windows/biometric.h
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
// Windows Hello for the Igneum window hosts (wallet-host.cpp, host.cpp): the WebView2 side of what
|
||||
// app/mac/Biometric.swift does with Touch ID. 5 October 2026. UNTESTED on a PC at the time of writing (written on a
|
||||
// Mac): BUILD-WALLET-APP.bat / BUILD-APP.bat on the GitHub runner are the first compile.
|
||||
//
|
||||
// The page posts a JSON string {id, op, nonce?, token?} with window.chrome.webview.postMessage; the host answers with
|
||||
// PostWebMessageAsJson({id, ok, code, message, ...}) and the page's listener routes it to window.__igneumBiometric.
|
||||
// Ops: status, enrol, unlock (wallet), confirm.
|
||||
//
|
||||
// The prompt is Windows.Security.Credentials.UI.UserConsentVerifier, through IUserConsentVerifierInterop so a Win32
|
||||
// window can own it (RequestVerificationForWindowAsync). What is stored: the wallet's password, sealed with DPAPI
|
||||
// (CryptProtectData, current user, CRYPTPROTECT_UI_FORBIDDEN) only after a Hello success, in the file the engine
|
||||
// named on its HOST line (%LOCALAPPDATA%\igneum\wallet\biometric.json); the miner seals a fixed marker. DPAPI is as
|
||||
// strong as the Windows account: anything running as this user can unseal it, which is why the host only unseals
|
||||
// after Hello verified, and why the threat model in app/igneum-wallet/README.md says what this does and does not
|
||||
// protect. The secret never goes through the page: the host posts it to the engine on 127.0.0.1 with the engine's
|
||||
// URL token; X-Igneum-Host (the token from the HOST line) marks the calls only the host may make.
|
||||
//
|
||||
// Needs: C++/WinRT headers (the Windows SDK's cppwinrt include, on INCLUDE after vcvarsall), windowsapp.lib,
|
||||
// crypt32.lib, winhttp.lib. C++17.
|
||||
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
#include <wincrypt.h>
|
||||
#include <winhttp.h>
|
||||
#include <winrt/base.h>
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <winrt/Windows.Security.Credentials.UI.h>
|
||||
#include <UserConsentVerifierInterop.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <thread>
|
||||
#include <functional>
|
||||
|
||||
namespace igbio {
|
||||
|
||||
using winrt::Windows::Security::Credentials::UI::UserConsentVerifier;
|
||||
using winrt::Windows::Security::Credentials::UI::UserConsentVerifierAvailability;
|
||||
using winrt::Windows::Security::Credentials::UI::UserConsentVerificationResult;
|
||||
|
||||
struct Config {
|
||||
std::wstring product; // L"Igneum Wallet" | L"Igneum Miner"
|
||||
std::wstring enrolReason; // L"Turn on Windows Hello for your wallet" | L"... for the miner"
|
||||
std::wstring engineURL; // http://127.0.0.1:<port>/t/<token>/
|
||||
std::string hostToken; // from the HOST line
|
||||
std::wstring file; // the sealed file's path from the HOST line
|
||||
HWND hwnd = nullptr; // the window that owns the prompt
|
||||
};
|
||||
|
||||
static Config g_cfg;
|
||||
static const char* MARKER = "igneum-biometric-marker-v1";
|
||||
|
||||
// ---- small JSON helpers (flat objects, string values) ----
|
||||
|
||||
static std::string jsonEscape(const std::string& s) {
|
||||
std::string o;
|
||||
for (unsigned char c : s) {
|
||||
switch (c) {
|
||||
case '"': o += "\\\""; break;
|
||||
case '\\': o += "\\\\"; break;
|
||||
case '\n': o += "\\n"; break;
|
||||
case '\r': o += "\\r"; break;
|
||||
case '\t': o += "\\t"; break;
|
||||
default:
|
||||
if (c < 0x20) { char b[8]; sprintf_s(b, "\\u%04x", c); o += b; } else o += (char)c;
|
||||
}
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
// The value of "key" in a flat JSON object: a string (escapes decoded), a number, or a bool as text.
|
||||
static std::string jsonGet(const std::string& j, const char* key) {
|
||||
std::string k = std::string("\"") + key + "\"";
|
||||
size_t i = j.find(k);
|
||||
if (i == std::string::npos) return "";
|
||||
i = j.find(':', i + k.size());
|
||||
if (i == std::string::npos) return "";
|
||||
i++;
|
||||
while (i < j.size() && (j[i] == ' ' || j[i] == '\t')) i++;
|
||||
if (i < j.size() && j[i] == '"') {
|
||||
std::string o;
|
||||
for (i++; i < j.size() && j[i] != '"'; i++) {
|
||||
if (j[i] == '\\' && i + 1 < j.size()) {
|
||||
char e = j[++i];
|
||||
if (e == 'n') o += '\n';
|
||||
else if (e == 'r') o += '\r';
|
||||
else if (e == 't') o += '\t';
|
||||
else if (e == 'u' && i + 4 < j.size()) {
|
||||
unsigned v = strtoul(j.substr(i + 1, 4).c_str(), nullptr, 16);
|
||||
i += 4;
|
||||
if (v < 0x80) o += (char)v;
|
||||
else if (v < 0x800) { o += (char)(0xC0 | (v >> 6)); o += (char)(0x80 | (v & 0x3F)); }
|
||||
else { o += (char)(0xE0 | (v >> 12)); o += (char)(0x80 | ((v >> 6) & 0x3F)); o += (char)(0x80 | (v & 0x3F)); }
|
||||
} else o += e;
|
||||
} else o += j[i];
|
||||
}
|
||||
return o;
|
||||
}
|
||||
size_t e = i;
|
||||
while (e < j.size() && j[e] != ',' && j[e] != '}') e++;
|
||||
std::string v = j.substr(i, e - i);
|
||||
while (!v.empty() && (v.back() == ' ' || v.back() == '\r' || v.back() == '\n')) v.pop_back();
|
||||
return v;
|
||||
}
|
||||
|
||||
static std::wstring widen8(const std::string& s) {
|
||||
if (s.empty()) return L"";
|
||||
int n = MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), nullptr, 0);
|
||||
std::wstring w(n, 0);
|
||||
MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), &w[0], n);
|
||||
return w;
|
||||
}
|
||||
static std::string narrow8(const std::wstring& w) {
|
||||
if (w.empty()) return "";
|
||||
int n = WideCharToMultiByte(CP_UTF8, 0, w.data(), (int)w.size(), nullptr, 0, nullptr, nullptr);
|
||||
std::string s(n, 0);
|
||||
WideCharToMultiByte(CP_UTF8, 0, w.data(), (int)w.size(), &s[0], n, nullptr, nullptr);
|
||||
return s;
|
||||
}
|
||||
|
||||
static std::string b64(const std::vector<BYTE>& d) {
|
||||
DWORD n = 0;
|
||||
CryptBinaryToStringA(d.data(), (DWORD)d.size(), CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, nullptr, &n);
|
||||
std::string o(n, 0);
|
||||
CryptBinaryToStringA(d.data(), (DWORD)d.size(), CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, &o[0], &n);
|
||||
while (!o.empty() && o.back() == 0) o.pop_back();
|
||||
return o;
|
||||
}
|
||||
static std::vector<BYTE> unb64(const std::string& s) {
|
||||
DWORD n = 0;
|
||||
if (!CryptStringToBinaryA(s.c_str(), (DWORD)s.size(), CRYPT_STRING_BASE64, nullptr, &n, nullptr, nullptr)) return {};
|
||||
std::vector<BYTE> o(n);
|
||||
if (!CryptStringToBinaryA(s.c_str(), (DWORD)s.size(), CRYPT_STRING_BASE64, o.data(), &n, nullptr, nullptr)) return {};
|
||||
o.resize(n);
|
||||
return o;
|
||||
}
|
||||
|
||||
static std::string reply(bool ok, const std::string& code, const std::string& message) {
|
||||
return std::string("{\"ok\":") + (ok ? "true" : "false") + ",\"code\":\"" + jsonEscape(code) + "\",\"message\":\"" + jsonEscape(message) + "\"}";
|
||||
}
|
||||
|
||||
// ---- the engine on 127.0.0.1 (WinHTTP) ----
|
||||
|
||||
struct Answer { bool ok; int status; std::string body; };
|
||||
|
||||
static Answer call(const std::wstring& method, const std::string& path, const std::string& body) {
|
||||
Answer a = { false, 0, "" };
|
||||
URL_COMPONENTS uc = { sizeof(uc) };
|
||||
wchar_t host[64] = {}, upath[1024] = {};
|
||||
uc.lpszHostName = host; uc.dwHostNameLength = 64;
|
||||
uc.lpszUrlPath = upath; uc.dwUrlPathLength = 1024;
|
||||
std::wstring full = g_cfg.engineURL + widen8(path);
|
||||
if (!WinHttpCrackUrl(full.c_str(), 0, 0, &uc)) { a.body = "{\"error\":\"bad engine url\"}"; return a; }
|
||||
HINTERNET s = WinHttpOpen(L"IgneumHost/1", WINHTTP_ACCESS_TYPE_NO_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
|
||||
if (!s) { a.body = "{\"error\":\"winhttp\"}"; return a; }
|
||||
WinHttpSetTimeouts(s, 5000, 5000, 15000, 15000);
|
||||
HINTERNET c = WinHttpConnect(s, host, uc.nPort, 0);
|
||||
HINTERNET r = c ? WinHttpOpenRequest(c, method.c_str(), upath, nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0) : nullptr;
|
||||
if (r) {
|
||||
std::wstring hdr = L"X-Igneum-Host: " + widen8(g_cfg.hostToken) + L"\r\nContent-Type: application/json\r\n";
|
||||
if (WinHttpSendRequest(r, hdr.c_str(), (DWORD)-1, body.empty() ? WINHTTP_NO_REQUEST_DATA : (LPVOID)body.data(), (DWORD)body.size(), (DWORD)body.size(), 0) && WinHttpReceiveResponse(r, nullptr)) {
|
||||
DWORD st = 0, n = sizeof(st);
|
||||
WinHttpQueryHeaders(r, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &st, &n, WINHTTP_NO_HEADER_INDEX);
|
||||
a.status = (int)st;
|
||||
DWORD avail = 0;
|
||||
while (WinHttpQueryDataAvailable(r, &avail) && avail > 0) {
|
||||
std::string chunk(avail, 0);
|
||||
DWORD got = 0;
|
||||
if (!WinHttpReadData(r, &chunk[0], avail, &got)) break;
|
||||
a.body.append(chunk, 0, got);
|
||||
}
|
||||
a.ok = st == 200 && jsonGet(a.body, "ok") != "false";
|
||||
} else a.body = "{\"error\":\"no answer from the engine\"}";
|
||||
}
|
||||
if (r) WinHttpCloseHandle(r);
|
||||
if (c) WinHttpCloseHandle(c);
|
||||
WinHttpCloseHandle(s);
|
||||
return a;
|
||||
}
|
||||
static Answer post(const std::string& path, const std::string& body) { return call(L"POST", path, body); }
|
||||
static Answer get(const std::string& path) { return call(L"GET", path, ""); }
|
||||
|
||||
// ---- Windows Hello ----
|
||||
|
||||
static std::string availabilityText(UserConsentVerifierAvailability a, bool* available) {
|
||||
*available = false;
|
||||
switch (a) {
|
||||
case UserConsentVerifierAvailability::Available: *available = true; return "";
|
||||
case UserConsentVerifierAvailability::DeviceNotPresent: return "Windows Hello has no fingerprint or face sensor on this PC.";
|
||||
case UserConsentVerifierAvailability::NotConfiguredForUser: return "Windows Hello is not set up on this PC. Set it up in Settings > Accounts > Sign-in options.";
|
||||
case UserConsentVerifierAvailability::DisabledByPolicy: return "Windows Hello is disabled by policy on this PC.";
|
||||
case UserConsentVerifierAvailability::DeviceBusy: return "The Windows Hello sensor is busy.";
|
||||
default: return "Windows Hello is not available on this PC.";
|
||||
}
|
||||
}
|
||||
|
||||
static std::string status(bool* available) {
|
||||
try {
|
||||
auto a = UserConsentVerifier::CheckAvailabilityAsync().get();
|
||||
return availabilityText(a, available);
|
||||
} catch (...) {
|
||||
*available = false;
|
||||
return "Windows Hello could not be checked on this PC.";
|
||||
}
|
||||
}
|
||||
|
||||
// The prompt. Returns "" on success, else the reply JSON for the page.
|
||||
static std::string verify(const std::wstring& reason) {
|
||||
try {
|
||||
auto factory = winrt::get_activation_factory<UserConsentVerifier, IUserConsentVerifierInterop>();
|
||||
winrt::Windows::Foundation::IAsyncOperation<UserConsentVerificationResult> op{ nullptr };
|
||||
winrt::hstring msg(reason.substr(0, 80));
|
||||
winrt::check_hresult(factory->RequestVerificationForWindowAsync(g_cfg.hwnd, static_cast<HSTRING>(winrt::get_abi(msg)),
|
||||
winrt::guid_of<winrt::Windows::Foundation::IAsyncOperation<UserConsentVerificationResult>>(), winrt::put_abi(op)));
|
||||
auto r = op.get();
|
||||
switch (r) {
|
||||
case UserConsentVerificationResult::Verified: return "";
|
||||
case UserConsentVerificationResult::Canceled: return reply(false, "cancelled", "Windows Hello was cancelled.");
|
||||
case UserConsentVerificationResult::RetriesExhausted: return reply(false, "locked", "Windows Hello is locked after too many tries. Use the password.");
|
||||
case UserConsentVerificationResult::NotConfiguredForUser: return reply(false, "not_set_up", "Windows Hello is not set up on this PC. Set it up in Settings > Accounts > Sign-in options.");
|
||||
case UserConsentVerificationResult::DeviceNotPresent: return reply(false, "unavailable", "Windows Hello has no sensor on this PC.");
|
||||
case UserConsentVerificationResult::DisabledByPolicy: return reply(false, "unavailable", "Windows Hello is disabled by policy on this PC.");
|
||||
case UserConsentVerificationResult::DeviceBusy: return reply(false, "failed", "The Windows Hello sensor is busy; try again.");
|
||||
default: return reply(false, "failed", "Windows Hello did not succeed.");
|
||||
}
|
||||
} catch (const winrt::hresult_error& e) {
|
||||
return reply(false, "failed", "Windows Hello failed: " + narrow8(std::wstring(e.message())));
|
||||
} catch (...) {
|
||||
return reply(false, "failed", "Windows Hello failed.");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- the sealed file (DPAPI, current user) ----
|
||||
|
||||
static bool readFile(std::string* out) {
|
||||
HANDLE h = CreateFileW(g_cfg.file.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (h == INVALID_HANDLE_VALUE) return false;
|
||||
char buf[8192]; DWORD n = 0;
|
||||
out->clear();
|
||||
while (ReadFile(h, buf, sizeof(buf), &n, nullptr) && n > 0) out->append(buf, n);
|
||||
CloseHandle(h);
|
||||
return true;
|
||||
}
|
||||
static bool writeFile(const std::string& text) {
|
||||
size_t i = g_cfg.file.find_last_of(L"\\/");
|
||||
if (i != std::wstring::npos) CreateDirectoryW(g_cfg.file.substr(0, i).c_str(), nullptr);
|
||||
HANDLE h = CreateFileW(g_cfg.file.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (h == INVALID_HANDLE_VALUE) return false;
|
||||
DWORD w = 0;
|
||||
BOOL ok = WriteFile(h, text.data(), (DWORD)text.size(), &w, nullptr);
|
||||
CloseHandle(h);
|
||||
return ok && w == text.size();
|
||||
}
|
||||
static bool sealedExists() {
|
||||
DWORD a = GetFileAttributesW(g_cfg.file.c_str());
|
||||
return a != INVALID_FILE_ATTRIBUTES && !(a & FILE_ATTRIBUTE_DIRECTORY);
|
||||
}
|
||||
|
||||
static bool seal(const std::string& secret, std::string* boxB64) {
|
||||
DATA_BLOB in = { (DWORD)secret.size(), (BYTE*)secret.data() }, out = {};
|
||||
std::wstring desc = g_cfg.product + L" biometric";
|
||||
if (!CryptProtectData(&in, desc.c_str(), nullptr, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &out)) return false;
|
||||
std::vector<BYTE> v(out.pbData, out.pbData + out.cbData);
|
||||
LocalFree(out.pbData);
|
||||
*boxB64 = b64(v);
|
||||
return true;
|
||||
}
|
||||
static bool unseal(const std::string& boxB64, std::string* secret) {
|
||||
std::vector<BYTE> v = unb64(boxB64);
|
||||
if (v.empty()) return false;
|
||||
DATA_BLOB in = { (DWORD)v.size(), v.data() }, out = {};
|
||||
if (!CryptUnprotectData(&in, nullptr, nullptr, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &out)) return false;
|
||||
secret->assign((char*)out.pbData, out.cbData);
|
||||
SecureZeroMemory(out.pbData, out.cbData);
|
||||
LocalFree(out.pbData);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- the ops ----
|
||||
|
||||
static std::string opStatus() {
|
||||
bool avail = false;
|
||||
std::string msg = status(&avail);
|
||||
return std::string("{\"ok\":true,\"available\":") + (avail ? "true" : "false") + ",\"kind\":\"hello\",\"message\":\"" + jsonEscape(msg) + "\",\"enrolled\":" + (sealedExists() ? "true" : "false") + "}";
|
||||
}
|
||||
|
||||
static std::string opEnrol(const std::string& token) {
|
||||
if (g_cfg.file.empty()) return reply(false, "unavailable", "The engine has not named the sealed file yet.");
|
||||
bool avail = false;
|
||||
std::string msg = status(&avail);
|
||||
if (!avail) return reply(false, "unavailable", msg);
|
||||
std::string e = verify(g_cfg.enrolReason);
|
||||
if (!e.empty()) return e;
|
||||
std::string secret;
|
||||
if (!token.empty()) {
|
||||
Answer a = post("api/biometric/enrol/take", "{\"token\":\"" + jsonEscape(token) + "\"}");
|
||||
if (!a.ok) return reply(false, "engine", jsonGet(a.body, "error").empty() ? "the engine did not hand over the password" : jsonGet(a.body, "error"));
|
||||
secret = jsonGet(a.body, "secret");
|
||||
} else secret = MARKER;
|
||||
std::string box;
|
||||
bool ok = seal(secret, &box);
|
||||
SecureZeroMemory(&secret[0], secret.size());
|
||||
if (!ok) return reply(false, "seal", "DPAPI could not seal the secret.");
|
||||
std::string doc = "{\"version\":1,\"kind\":\"hello\",\"product\":\"" + jsonEscape(narrow8(g_cfg.product)) + "\",\"created\":" + std::to_string((long long)(GetTickCount64() / 1000)) + ",\"box\":\"" + box + "\"}";
|
||||
if (!writeFile(doc)) return reply(false, "file", "The sealed file could not be written.");
|
||||
Answer a = post("api/biometric/enrolled", "{\"kind\":\"hello\"}");
|
||||
if (!a.ok) return reply(false, "engine", "the engine did not record the enrolment");
|
||||
return reply(true, "", "");
|
||||
}
|
||||
|
||||
static std::string opUnlock() {
|
||||
std::string text;
|
||||
if (!sealedExists() || !readFile(&text)) return reply(false, "not_enrolled", "Windows Hello is not turned on for this wallet.");
|
||||
std::string e = verify(L"Unlock your wallet");
|
||||
if (!e.empty()) return e;
|
||||
std::string secret;
|
||||
if (!unseal(jsonGet(text, "box"), &secret)) return reply(false, "invalidated", "Windows Hello no longer opens this wallet: the sealed password could not be read. Use the password, then turn Windows Hello on again in Settings.");
|
||||
Answer a = post("api/unlock", "{\"password\":\"" + jsonEscape(secret) + "\"}");
|
||||
SecureZeroMemory(&secret[0], secret.size());
|
||||
if (!a.ok) return reply(false, "engine", jsonGet(a.body, "error").empty() ? "the engine did not unlock" : jsonGet(a.body, "error"));
|
||||
return reply(true, "", "");
|
||||
}
|
||||
|
||||
static std::string opConfirm(const std::string& nonce) {
|
||||
if (nonce.empty()) return reply(false, "bad_nonce", "No challenge.");
|
||||
Answer c = get("api/biometric/challenge?nonce=" + nonce);
|
||||
if (!c.ok) return reply(false, "engine", jsonGet(c.body, "error").empty() ? "the engine does not know this challenge" : jsonGet(c.body, "error"));
|
||||
std::string e = verify(widen8(jsonGet(c.body, "reason")));
|
||||
if (!e.empty()) return e;
|
||||
if (sealedExists()) {
|
||||
// the sealed file must still open under this account (DPAPI; a reset account leaves it unreadable)
|
||||
std::string text, secret;
|
||||
if (!readFile(&text) || !unseal(jsonGet(text, "box"), &secret)) return reply(false, "invalidated", "The sealed file could not be read. Turn Windows Hello on again in Settings.");
|
||||
SecureZeroMemory(&secret[0], secret.size());
|
||||
}
|
||||
Answer a = post("api/biometric/confirm", "{\"nonce\":\"" + jsonEscape(nonce) + "\"}");
|
||||
if (!a.ok) return reply(false, "engine", jsonGet(a.body, "error").empty() ? "the engine refused the confirmation" : jsonGet(a.body, "error"));
|
||||
return reply(true, "", "");
|
||||
}
|
||||
|
||||
/// The HOST line from the engine: {"token": "...", "biometric_file": "..."}. Posts the availability at once.
|
||||
static void configure(const std::string& hostLineJson, const std::wstring& engineURL, const std::wstring& product, const std::wstring& enrolReason, HWND hwnd) {
|
||||
g_cfg.product = product;
|
||||
g_cfg.enrolReason = enrolReason;
|
||||
g_cfg.engineURL = engineURL;
|
||||
g_cfg.hostToken = jsonGet(hostLineJson, "token");
|
||||
g_cfg.file = widen8(jsonGet(hostLineJson, "biometric_file"));
|
||||
g_cfg.hwnd = hwnd;
|
||||
std::thread([] {
|
||||
winrt::init_apartment(winrt::apartment_type::multi_threaded);
|
||||
bool avail = false;
|
||||
std::string msg = status(&avail);
|
||||
post("api/biometric/status", std::string("{\"available\":") + (avail ? "true" : "false") + ",\"kind\":\"hello\",\"message\":\"" + jsonEscape(msg) + "\"}");
|
||||
}).detach();
|
||||
}
|
||||
|
||||
/// A message from the page (the JSON string it posted). `answer` receives the reply JSON with the id put back; it is
|
||||
/// called on a worker thread, so the host marshals it to the UI thread for PostWebMessageAsJson.
|
||||
static void handle(const std::string& msg, std::function<void(const std::string&)> answer) {
|
||||
std::string id = jsonGet(msg, "id"), op = jsonGet(msg, "op"), nonce = jsonGet(msg, "nonce"), token = jsonGet(msg, "token");
|
||||
std::thread([id, op, nonce, token, answer] {
|
||||
winrt::init_apartment(winrt::apartment_type::multi_threaded);
|
||||
std::string r;
|
||||
if (op == "status") r = opStatus();
|
||||
else if (op == "enrol") r = opEnrol(token);
|
||||
else if (op == "unlock") r = opUnlock();
|
||||
else if (op == "confirm") r = opConfirm(nonce);
|
||||
else r = reply(false, "bad_op", "unknown op " + op);
|
||||
if (op != "status") {
|
||||
post("api/biometric/report", "{\"op\":\"" + jsonEscape(op) + "\",\"ok\":" + (jsonGet(r, "ok") == "true" ? "true" : "false") + ",\"code\":\"" + jsonEscape(jsonGet(r, "code")) + "\",\"message\":\"" + jsonEscape(jsonGet(r, "message")) + "\"}");
|
||||
}
|
||||
// put the id in front: {"id":N, ...rest}
|
||||
std::string withId = "{\"id\":" + (id.empty() ? "0" : id) + "," + r.substr(1);
|
||||
answer(withId);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
} // namespace igbio
|
||||
486
app/windows/wallet-host.cpp
Normal file
486
app/windows/wallet-host.cpp
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
// Igneum Wallet for Windows: the window around the wallet engine. A copy of host.cpp (the miner's window) with the
|
||||
// names and the tray menu changed: "Lock" instead of "Pause". Built on the PC by BUILD-WALLET-APP.bat (MSVC, the
|
||||
// WebView2 SDK from NuGet, static loader). It starts igneum-wallet.exe --wrapper next to it, reads "URL ..." /
|
||||
// "STATE {...}" / "EXIT" from its stdout, shows the URL in a WebView2 control, keeps a tray icon with the state, and on
|
||||
// quit writes "quit" to the engine's stdin and waits for EXIT. Closing the window hides it to the tray. 4 October 2026.
|
||||
// Windows Hello (biometric.h, the page's window.chrome.webview.postMessage bridge, the HOST line): 5 October 2026.
|
||||
//
|
||||
// Untested on a real PC at the time of writing (written on a Mac): BUILD-WALLET-APP.bat is the first run.
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#ifndef UNICODE
|
||||
#define UNICODE
|
||||
#endif
|
||||
#ifndef _UNICODE
|
||||
#define _UNICODE
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#include <shellapi.h>
|
||||
#include <wrl.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include "WebView2.h"
|
||||
#include "wallet-version.h"
|
||||
#include "biometric.h"
|
||||
|
||||
using namespace Microsoft::WRL;
|
||||
|
||||
#define WM_ENGINE_LINE (WM_APP + 1)
|
||||
#define WM_TRAY (WM_APP + 2)
|
||||
#define WM_BIO_REPLY (WM_APP + 3)
|
||||
#define ID_TRAY_OPEN 1001
|
||||
#define ID_TRAY_PAUSE 1002
|
||||
#define ID_TRAY_QUIT 1003
|
||||
#define ID_QUIT_TIMER 7
|
||||
#define IDI_APP 1
|
||||
|
||||
static HWND g_hwnd = nullptr;
|
||||
static HANDLE g_engine = nullptr, g_engineIn = nullptr, g_engineOut = nullptr;
|
||||
static ComPtr<ICoreWebView2Controller> g_controller;
|
||||
static ComPtr<ICoreWebView2> g_webview;
|
||||
static std::wstring g_url, g_status = L"starting the engine";
|
||||
static std::string g_hostLine; // the engine's HOST {...} line, kept until the window and the URL exist
|
||||
static bool g_paused = false, g_quitting = false, g_exited = false, g_webviewOk = false, g_hintShown = false;
|
||||
static NOTIFYICONDATAW g_nid = {};
|
||||
static std::wstring g_trayTitle = L"Igneum Wallet";
|
||||
static ULONGLONG g_quitStarted = 0;
|
||||
|
||||
static std::wstring widen(const std::string& s) {
|
||||
if (s.empty()) return L"";
|
||||
int n = MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), nullptr, 0);
|
||||
std::wstring w(n, 0);
|
||||
MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), &w[0], n);
|
||||
return w;
|
||||
}
|
||||
|
||||
static std::wstring exeDir() {
|
||||
wchar_t buf[MAX_PATH];
|
||||
GetModuleFileNameW(nullptr, buf, MAX_PATH);
|
||||
std::wstring p(buf);
|
||||
size_t i = p.find_last_of(L"\\/");
|
||||
return i == std::wstring::npos ? L"." : p.substr(0, i);
|
||||
}
|
||||
|
||||
static void sendEngine(const char* line) {
|
||||
if (!g_engineIn) return;
|
||||
DWORD w = 0;
|
||||
WriteFile(g_engineIn, line, (DWORD)strlen(line), &w, nullptr);
|
||||
WriteFile(g_engineIn, "\n", 1, &w, nullptr);
|
||||
}
|
||||
|
||||
static void setTray(const std::wstring& tip) {
|
||||
g_trayTitle = tip;
|
||||
wcsncpy_s(g_nid.szTip, tip.c_str(), _TRUNCATE);
|
||||
Shell_NotifyIconW(NIM_MODIFY, &g_nid);
|
||||
}
|
||||
|
||||
static void repaintStatus() {
|
||||
InvalidateRect(g_hwnd, nullptr, TRUE);
|
||||
}
|
||||
|
||||
// A tiny JSON number/string/bool reader for the STATE line (flat object, known keys).
|
||||
static std::string jsonField(const std::string& j, const char* key) {
|
||||
std::string k = std::string("\"") + key + "\":";
|
||||
size_t i = j.find(k);
|
||||
if (i == std::string::npos) return "";
|
||||
i += k.size();
|
||||
while (i < j.size() && j[i] == ' ') i++;
|
||||
if (i < j.size() && j[i] == '"') {
|
||||
size_t e = j.find('"', i + 1);
|
||||
return e == std::string::npos ? "" : j.substr(i + 1, e - i - 1);
|
||||
}
|
||||
size_t e = i;
|
||||
while (e < j.size() && j[e] != ',' && j[e] != '}') e++;
|
||||
return j.substr(i, e - i);
|
||||
}
|
||||
|
||||
static void applyState(const std::string& j) {
|
||||
std::string mining = jsonField(j, "mining"), node = jsonField(j, "node"), phase = jsonField(j, "phase");
|
||||
g_paused = jsonField(j, "paused") == "true";
|
||||
double hash = atof(jsonField(j, "hash_total").c_str());
|
||||
std::string blocks = jsonField(j, "blocks"), accepted = jsonField(j, "accepted_total");
|
||||
wchar_t tip[128];
|
||||
if (jsonField(j, "quitting") == "true") swprintf_s(tip, L"Igneum Wallet: stopping");
|
||||
else if (phase != "dashboard") swprintf_s(tip, L"Igneum Wallet: set up");
|
||||
else if (mining == "mining") swprintf_s(tip, L"Igneum Wallet: %.1f MH/s, %S blocks found, node %S", hash, accepted.c_str(), node.c_str());
|
||||
else if (mining == "paused") swprintf_s(tip, L"Igneum Wallet: paused, node %S", node.c_str());
|
||||
else swprintf_s(tip, L"Igneum Wallet: %S, node %S (%S blocks)", mining.c_str(), node.c_str(), blocks.c_str());
|
||||
setTray(tip);
|
||||
}
|
||||
|
||||
// The engine asks for an elevated step (the NVIDIA power cap): this process has a UI context, so the UAC prompt shows.
|
||||
// Runs cmd /c <line> as administrator, waits, and answers on the engine's stdin.
|
||||
static void runElevated(std::wstring line) {
|
||||
std::thread([line] {
|
||||
std::wstring params = L"/c " + line;
|
||||
wchar_t sysdir[MAX_PATH];
|
||||
GetSystemDirectoryW(sysdir, MAX_PATH);
|
||||
std::wstring cmdExe = std::wstring(sysdir) + L"\\cmd.exe"; // the absolute path, never a bare name (R4.3.3)
|
||||
SHELLEXECUTEINFOW sei = { sizeof(sei) };
|
||||
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI;
|
||||
sei.lpVerb = L"runas";
|
||||
sei.lpFile = cmdExe.c_str();
|
||||
sei.lpParameters = params.c_str();
|
||||
sei.nShow = SW_HIDE;
|
||||
if (!ShellExecuteExW(&sei) || !sei.hProcess) {
|
||||
DWORD err = GetLastError();
|
||||
sendEngine(err == ERROR_CANCELLED ? "elevated fail: the administrator prompt was cancelled" : "elevated fail: could not start the elevated step");
|
||||
return;
|
||||
}
|
||||
WaitForSingleObject(sei.hProcess, 120000);
|
||||
DWORD code = 1;
|
||||
GetExitCodeProcess(sei.hProcess, &code);
|
||||
CloseHandle(sei.hProcess);
|
||||
if (code == 0) sendEngine("elevated ok");
|
||||
else { char buf[64]; sprintf_s(buf, "elevated fail: exit code %lu", code); sendEngine(buf); }
|
||||
}).detach();
|
||||
}
|
||||
|
||||
static void openInBrowser(const std::wstring& url) {
|
||||
ShellExecuteW(nullptr, L"open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||||
}
|
||||
|
||||
static void navigate() {
|
||||
if (g_webview && !g_url.empty()) g_webview->Navigate((g_url + L"?host=windows").c_str());
|
||||
}
|
||||
|
||||
static void initWebView() {
|
||||
std::wstring data = L"";
|
||||
wchar_t* local = nullptr;
|
||||
size_t len = 0;
|
||||
if (_wdupenv_s(&local, &len, L"LOCALAPPDATA") == 0 && local) { data = std::wstring(local) + L"\\igneum\\webview2"; free(local); }
|
||||
HRESULT hr = CreateCoreWebView2EnvironmentWithOptions(nullptr, data.empty() ? nullptr : data.c_str(), nullptr,
|
||||
Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>([](HRESULT result, ICoreWebView2Environment* env) -> HRESULT {
|
||||
if (FAILED(result) || !env) {
|
||||
g_status = L"The WebView2 runtime is not installed. The dashboard opens in your browser; install the runtime from microsoft.com for the app window.";
|
||||
repaintStatus();
|
||||
if (!g_url.empty()) { openInBrowser(g_url); g_hintShown = true; }
|
||||
return S_OK;
|
||||
}
|
||||
env->CreateCoreWebView2Controller(g_hwnd, Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>([](HRESULT result, ICoreWebView2Controller* controller) -> HRESULT {
|
||||
if (FAILED(result) || !controller) {
|
||||
g_status = L"The app window could not start WebView2. The dashboard opens in your browser.";
|
||||
repaintStatus();
|
||||
if (!g_url.empty()) { openInBrowser(g_url); g_hintShown = true; }
|
||||
return S_OK;
|
||||
}
|
||||
g_controller = controller;
|
||||
g_controller->get_CoreWebView2(&g_webview);
|
||||
g_webviewOk = true;
|
||||
ComPtr<ICoreWebView2Settings> settings;
|
||||
if (g_webview && SUCCEEDED(g_webview->get_Settings(&settings)) && settings) {
|
||||
settings->put_AreDefaultContextMenusEnabled(FALSE);
|
||||
settings->put_IsStatusBarEnabled(FALSE);
|
||||
settings->put_AreDevToolsEnabled(FALSE);
|
||||
}
|
||||
// links off 127.0.0.1 open in the default browser
|
||||
g_webview->add_NewWindowRequested(Callback<ICoreWebView2NewWindowRequestedEventHandler>([](ICoreWebView2*, ICoreWebView2NewWindowRequestedEventArgs* args) -> HRESULT {
|
||||
LPWSTR uri = nullptr;
|
||||
if (SUCCEEDED(args->get_Uri(&uri)) && uri) { openInBrowser(uri); CoTaskMemFree(uri); }
|
||||
args->put_Handled(TRUE);
|
||||
return S_OK;
|
||||
}).Get(), nullptr);
|
||||
g_webview->add_NavigationStarting(Callback<ICoreWebView2NavigationStartingEventHandler>([](ICoreWebView2*, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT {
|
||||
LPWSTR uri = nullptr;
|
||||
if (SUCCEEDED(args->get_Uri(&uri)) && uri) {
|
||||
std::wstring u(uri);
|
||||
CoTaskMemFree(uri);
|
||||
if (u.rfind(L"http", 0) == 0 && u.find(L"127.0.0.1") == std::wstring::npos) { args->put_Cancel(TRUE); openInBrowser(u); }
|
||||
}
|
||||
return S_OK;
|
||||
}).Get(), nullptr);
|
||||
// the page's Windows Hello bridge (biometric.h): a JSON string in, a JSON object back on the UI thread
|
||||
g_webview->add_WebMessageReceived(Callback<ICoreWebView2WebMessageReceivedEventHandler>([](ICoreWebView2*, ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT {
|
||||
LPWSTR src = nullptr;
|
||||
if (FAILED(args->get_Source(&src)) || !src) return S_OK;
|
||||
std::wstring origin(src);
|
||||
CoTaskMemFree(src);
|
||||
if (origin.rfind(L"http://127.0.0.1:", 0) != 0) return S_OK; // only the engine's own page
|
||||
LPWSTR text = nullptr;
|
||||
if (SUCCEEDED(args->TryGetWebMessageAsString(&text)) && text) {
|
||||
std::string msg = igbio::narrow8(text);
|
||||
CoTaskMemFree(text);
|
||||
igbio::handle(msg, [](const std::string& json) { PostMessageW(g_hwnd, WM_BIO_REPLY, 0, (LPARAM) new std::string(json)); });
|
||||
}
|
||||
return S_OK;
|
||||
}).Get(), nullptr);
|
||||
RECT rc; GetClientRect(g_hwnd, &rc);
|
||||
g_controller->put_Bounds(rc);
|
||||
COREWEBVIEW2_COLOR dark = { 255, 12, 12, 14 };
|
||||
ComPtr<ICoreWebView2Controller2> c2;
|
||||
if (SUCCEEDED(g_controller.As(&c2)) && c2) c2->put_DefaultBackgroundColor(dark);
|
||||
g_status = L"";
|
||||
repaintStatus();
|
||||
navigate();
|
||||
return S_OK;
|
||||
}).Get());
|
||||
return S_OK;
|
||||
}).Get());
|
||||
if (FAILED(hr)) {
|
||||
g_status = L"The WebView2 runtime is not installed. The dashboard opens in your browser; install the runtime from microsoft.com for the app window.";
|
||||
repaintStatus();
|
||||
if (!g_url.empty()) { openInBrowser(g_url); g_hintShown = true; }
|
||||
}
|
||||
}
|
||||
|
||||
static bool startEngine() {
|
||||
SECURITY_ATTRIBUTES sa = { sizeof(sa), nullptr, TRUE };
|
||||
HANDLE outR, outW, inR, inW;
|
||||
if (!CreatePipe(&outR, &outW, &sa, 0) || !CreatePipe(&inR, &inW, &sa, 0)) return false;
|
||||
SetHandleInformation(outR, HANDLE_FLAG_INHERIT, 0);
|
||||
SetHandleInformation(inW, HANDLE_FLAG_INHERIT, 0);
|
||||
STARTUPINFOW si = { sizeof(si) };
|
||||
si.dwFlags = STARTF_USESTDHANDLES;
|
||||
si.hStdOutput = outW;
|
||||
si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
|
||||
si.hStdInput = inR;
|
||||
PROCESS_INFORMATION pi = {};
|
||||
std::wstring exe = exeDir() + L"\\igneum-wallet.exe";
|
||||
std::wstring cmd = L"\"" + exe + L"\" --wrapper";
|
||||
std::vector<wchar_t> buf(cmd.begin(), cmd.end());
|
||||
buf.push_back(0);
|
||||
BOOL ok = CreateProcessW(exe.c_str(), buf.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, exeDir().c_str(), &si, &pi);
|
||||
CloseHandle(outW);
|
||||
CloseHandle(inR);
|
||||
if (!ok) { CloseHandle(outR); CloseHandle(inW); return false; }
|
||||
CloseHandle(pi.hThread);
|
||||
g_engine = pi.hProcess;
|
||||
g_engineIn = inW;
|
||||
g_engineOut = outR;
|
||||
std::thread([] {
|
||||
std::string acc;
|
||||
char chunk[4096];
|
||||
DWORD n;
|
||||
while (ReadFile(g_engineOut, chunk, sizeof(chunk), &n, nullptr) && n > 0) {
|
||||
acc.append(chunk, n);
|
||||
size_t nl;
|
||||
while ((nl = acc.find('\n')) != std::string::npos) {
|
||||
std::string line = acc.substr(0, nl);
|
||||
acc.erase(0, nl + 1);
|
||||
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||
PostMessageW(g_hwnd, WM_ENGINE_LINE, 0, (LPARAM) new std::string(line));
|
||||
}
|
||||
}
|
||||
PostMessageW(g_hwnd, WM_ENGINE_LINE, 1, 0);
|
||||
}).detach();
|
||||
return true;
|
||||
}
|
||||
|
||||
static void showTrayMenu() {
|
||||
HMENU m = CreatePopupMenu();
|
||||
AppendMenuW(m, MF_STRING, ID_TRAY_OPEN, L"Open Igneum Wallet");
|
||||
AppendMenuW(m, MF_STRING, ID_TRAY_PAUSE, g_paused ? L"Lock" : L"Lock");
|
||||
AppendMenuW(m, MF_SEPARATOR, 0, nullptr);
|
||||
AppendMenuW(m, MF_STRING | MF_GRAYED, 0, g_trayTitle.c_str());
|
||||
AppendMenuW(m, MF_SEPARATOR, 0, nullptr);
|
||||
AppendMenuW(m, MF_STRING, ID_TRAY_QUIT, L"Quit Igneum Wallet");
|
||||
POINT pt; GetCursorPos(&pt);
|
||||
SetForegroundWindow(g_hwnd);
|
||||
TrackPopupMenu(m, TPM_RIGHTBUTTON | TPM_BOTTOMALIGN, pt.x, pt.y, 0, g_hwnd, nullptr);
|
||||
DestroyMenu(m);
|
||||
}
|
||||
|
||||
static void beginQuit() {
|
||||
if (g_quitting) return;
|
||||
g_quitting = true;
|
||||
g_quitStarted = GetTickCount64();
|
||||
g_status = L"Stopping: the miner first, then the node";
|
||||
setTray(L"Igneum Wallet: stopping");
|
||||
if (g_webview) g_webview->ExecuteScript(L"document.getElementById('pill-text').textContent='stopping'", nullptr);
|
||||
if (g_engine && !g_exited) {
|
||||
sendEngine("quit");
|
||||
SetTimer(g_hwnd, ID_QUIT_TIMER, 500, nullptr);
|
||||
} else {
|
||||
DestroyWindow(g_hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||
switch (msg) {
|
||||
case WM_SIZE:
|
||||
if (g_controller) { RECT rc; GetClientRect(hwnd, &rc); g_controller->put_Bounds(rc); }
|
||||
return 0;
|
||||
case WM_ENGINE_LINE: {
|
||||
if (wp == 1) {
|
||||
g_exited = true;
|
||||
if (g_quitting) { DestroyWindow(hwnd); return 0; }
|
||||
g_status = L"The engine stopped. Close this window and open Igneum Wallet again.";
|
||||
setTray(L"Igneum Wallet: stopped");
|
||||
repaintStatus();
|
||||
return 0;
|
||||
}
|
||||
std::string* line = (std::string*)lp;
|
||||
if (line->rfind("URL ", 0) == 0) {
|
||||
g_url = widen(line->substr(4));
|
||||
if (g_webviewOk) navigate();
|
||||
else if (!g_webview && g_status.find(L"WebView2") != std::wstring::npos && !g_hintShown) { openInBrowser(g_url); g_hintShown = true; }
|
||||
} else if (line->rfind("HOST ", 0) == 0) {
|
||||
// the host token and the sealed file's path; the page never sees this line
|
||||
g_hostLine = line->substr(5);
|
||||
igbio::configure(g_hostLine, g_url, L"Igneum Wallet", L"Turn on Windows Hello for your wallet", hwnd);
|
||||
} else if (line->rfind("STATE ", 0) == 0) {
|
||||
applyState(line->substr(6));
|
||||
} else if (line->rfind("ELEVATE ", 0) == 0) {
|
||||
runElevated(widen(line->substr(8)));
|
||||
} else if (line->rfind("FATAL ", 0) == 0) {
|
||||
g_status = widen(line->substr(6));
|
||||
repaintStatus();
|
||||
MessageBoxW(hwnd, g_status.c_str(), L"Igneum Wallet", MB_OK | MB_ICONERROR);
|
||||
} else if (*line == "EXIT") {
|
||||
g_exited = true;
|
||||
if (g_quitting) DestroyWindow(hwnd);
|
||||
}
|
||||
delete line;
|
||||
return 0;
|
||||
}
|
||||
case WM_BIO_REPLY: {
|
||||
std::string* json = (std::string*)lp;
|
||||
if (g_webview) g_webview->PostWebMessageAsJson(igbio::widen8(*json).c_str());
|
||||
delete json;
|
||||
return 0;
|
||||
}
|
||||
case WM_TIMER:
|
||||
if (wp == ID_QUIT_TIMER) {
|
||||
DWORD code = 0;
|
||||
bool gone = !g_engine || (GetExitCodeProcess(g_engine, &code) && code != STILL_ACTIVE);
|
||||
if (gone || g_exited || GetTickCount64() - g_quitStarted > 45000) {
|
||||
if (!gone && g_engine) TerminateProcess(g_engine, 1);
|
||||
KillTimer(hwnd, ID_QUIT_TIMER);
|
||||
DestroyWindow(hwnd);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
case WM_TRAY:
|
||||
if (lp == WM_LBUTTONUP || lp == WM_LBUTTONDBLCLK) { ShowWindow(hwnd, SW_SHOW); SetForegroundWindow(hwnd); }
|
||||
else if (lp == WM_RBUTTONUP || lp == WM_CONTEXTMENU) showTrayMenu();
|
||||
return 0;
|
||||
case WM_COMMAND:
|
||||
switch (LOWORD(wp)) {
|
||||
case ID_TRAY_OPEN: ShowWindow(hwnd, SW_SHOW); SetForegroundWindow(hwnd); return 0;
|
||||
case ID_TRAY_PAUSE: sendEngine(g_paused ? "lock" : "lock"); return 0;
|
||||
case ID_TRAY_QUIT: beginQuit(); return 0;
|
||||
}
|
||||
return 0;
|
||||
case WM_CLOSE:
|
||||
// hide to the tray; the miner keeps running
|
||||
ShowWindow(hwnd, SW_HIDE);
|
||||
if (!g_hintShown) {
|
||||
g_nid.uFlags |= NIF_INFO;
|
||||
wcscpy_s(g_nid.szInfoTitle, L"Igneum Wallet keeps mining");
|
||||
wcscpy_s(g_nid.szInfo, L"The window is in the tray. Right-click the icon to pause or quit.");
|
||||
g_nid.dwInfoFlags = NIIF_INFO;
|
||||
Shell_NotifyIconW(NIM_MODIFY, &g_nid);
|
||||
g_nid.uFlags &= ~NIF_INFO;
|
||||
g_hintShown = true;
|
||||
}
|
||||
return 0;
|
||||
case WM_PAINT: {
|
||||
PAINTSTRUCT ps;
|
||||
HDC dc = BeginPaint(hwnd, &ps);
|
||||
RECT rc; GetClientRect(hwnd, &rc);
|
||||
HBRUSH bg = CreateSolidBrush(RGB(12, 12, 14));
|
||||
FillRect(dc, &rc, bg);
|
||||
DeleteObject(bg);
|
||||
if (!g_status.empty()) {
|
||||
SetBkMode(dc, TRANSPARENT);
|
||||
SetTextColor(dc, RGB(154, 154, 158));
|
||||
HFONT f = CreateFontW(-15, 0, 0, 0, FW_NORMAL, 0, 0, 0, DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"Segoe UI");
|
||||
HFONT old = (HFONT)SelectObject(dc, f);
|
||||
RECT tr = rc; tr.left += 40; tr.right -= 40;
|
||||
DrawTextW(dc, g_status.c_str(), -1, &tr, DT_CENTER | DT_VCENTER | DT_WORDBREAK | DT_NOPREFIX | (g_webviewOk ? DT_BOTTOM : DT_VCENTER));
|
||||
SelectObject(dc, old);
|
||||
DeleteObject(f);
|
||||
}
|
||||
EndPaint(hwnd, &ps);
|
||||
return 0;
|
||||
}
|
||||
case WM_DESTROY:
|
||||
Shell_NotifyIconW(NIM_DELETE, &g_nid);
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProcW(hwnd, msg, wp, lp);
|
||||
}
|
||||
|
||||
// --version and --help print one line and exit, for the CI smoke run and support scripts. A windows-subsystem exe has
|
||||
// no console of its own: the text goes to the inherited stdout when there is one (a pipe or a file), else to the
|
||||
// parent's console.
|
||||
static bool handleCliFlags() {
|
||||
int argc = 0;
|
||||
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
|
||||
if (!argv) return false;
|
||||
std::wstring text;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
std::wstring a = argv[i];
|
||||
if (a == L"--version" || a == L"-V") text = L"Igneum Wallet " IGNEUM_HOST_VERSION_STR L" (window host)\r\n";
|
||||
else if (a == L"--help" || a == L"-h" || a == L"/?")
|
||||
text = L"Igneum Wallet " IGNEUM_HOST_VERSION_STR L" (window host)\r\n"
|
||||
L"Usage: \"Igneum Wallet.exe\" [--version | --help]\r\n"
|
||||
L"Without flags it starts igneum-wallet.exe --wrapper next to it and shows the dashboard in a WebView2 window.\r\n";
|
||||
}
|
||||
LocalFree(argv);
|
||||
if (text.empty()) return false;
|
||||
HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
if (out == nullptr || out == INVALID_HANDLE_VALUE) {
|
||||
if (AttachConsole(ATTACH_PARENT_PROCESS)) out = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
}
|
||||
if (out && out != INVALID_HANDLE_VALUE) {
|
||||
int n = WideCharToMultiByte(CP_UTF8, 0, text.c_str(), (int)text.size(), nullptr, 0, nullptr, nullptr);
|
||||
std::string utf8(n, 0);
|
||||
WideCharToMultiByte(CP_UTF8, 0, text.c_str(), (int)text.size(), &utf8[0], n, nullptr, nullptr);
|
||||
DWORD written = 0;
|
||||
WriteFile(out, utf8.data(), (DWORD)utf8.size(), &written, nullptr);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int) {
|
||||
if (handleCliFlags()) return 0;
|
||||
HANDLE once = CreateMutexW(nullptr, TRUE, L"Local\\IgneumWalletWindow");
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS) {
|
||||
HWND other = FindWindowW(L"IgneumWalletWindow", nullptr);
|
||||
if (other) { ShowWindow(other, SW_SHOW); SetForegroundWindow(other); }
|
||||
return 0;
|
||||
}
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
WNDCLASSW wc = {};
|
||||
wc.lpfnWndProc = WndProc;
|
||||
wc.hInstance = hInst;
|
||||
wc.lpszClassName = L"IgneumWalletWindow";
|
||||
wc.hIcon = LoadIconW(hInst, MAKEINTRESOURCEW(IDI_APP));
|
||||
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||
wc.hbrBackground = CreateSolidBrush(RGB(12, 12, 14));
|
||||
RegisterClassW(&wc);
|
||||
int w = 1120, h = 820;
|
||||
int sx = (GetSystemMetrics(SM_CXSCREEN) - w) / 2, sy = (GetSystemMetrics(SM_CYSCREEN) - h) / 2;
|
||||
g_hwnd = CreateWindowExW(0, wc.lpszClassName, L"Igneum Wallet", WS_OVERLAPPEDWINDOW, sx, sy, w, h, nullptr, nullptr, hInst, nullptr);
|
||||
ShowWindow(g_hwnd, SW_SHOW);
|
||||
|
||||
g_nid.cbSize = sizeof(g_nid);
|
||||
g_nid.hWnd = g_hwnd;
|
||||
g_nid.uID = 1;
|
||||
g_nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
|
||||
g_nid.uCallbackMessage = WM_TRAY;
|
||||
g_nid.hIcon = (HICON)LoadImageW(hInst, MAKEINTRESOURCEW(IDI_APP), IMAGE_ICON, 16, 16, LR_DEFAULTCOLOR);
|
||||
wcscpy_s(g_nid.szTip, L"Igneum Wallet: starting");
|
||||
Shell_NotifyIconW(NIM_ADD, &g_nid);
|
||||
|
||||
if (!startEngine()) {
|
||||
g_status = L"igneum-wallet.exe is missing next to this program. Run the installer again.";
|
||||
repaintStatus();
|
||||
MessageBoxW(g_hwnd, g_status.c_str(), L"Igneum Wallet", MB_OK | MB_ICONERROR);
|
||||
}
|
||||
initWebView();
|
||||
|
||||
MSG msg;
|
||||
while (GetMessageW(&msg, nullptr, 0, 0)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
if (g_engine) { CloseHandle(g_engine); }
|
||||
CloseHandle(once);
|
||||
CoUninitialize();
|
||||
return 0;
|
||||
}
|
||||
36
app/windows/wallet-host.rc
Normal file
36
app/windows/wallet-host.rc
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// Resources for Igneum Wallet.exe (the window host): the coin icon and the version block.
|
||||
// Compiled by BUILD-WALLET-APP.bat with rc.exe; the icon path is relative to brand\icons (passed with /i). The version comes
|
||||
// from wallet-version.h, shared with wallet-host.cpp.
|
||||
#include <winver.h>
|
||||
#include "wallet-version.h"
|
||||
|
||||
1 ICON "igneum.ico"
|
||||
|
||||
1 VERSIONINFO
|
||||
FILEVERSION IGNEUM_HOST_VERSION_RC
|
||||
PRODUCTVERSION IGNEUM_HOST_VERSION_RC
|
||||
FILEFLAGSMASK 0x3fL
|
||||
FILEFLAGS 0x0L
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_APP
|
||||
FILESUBTYPE VFT2_UNKNOWN
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904B0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Igneum"
|
||||
VALUE "FileDescription", "Igneum Wallet"
|
||||
VALUE "FileVersion", IGNEUM_HOST_VERSION_STR
|
||||
VALUE "InternalName", "Igneum Wallet"
|
||||
VALUE "LegalCopyright", "Igneum. Nothing is bought or sold."
|
||||
VALUE "OriginalFilename", "Igneum Wallet.exe"
|
||||
VALUE "ProductName", "Igneum Wallet"
|
||||
VALUE "ProductVersion", IGNEUM_HOST_VERSION_STR
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1200
|
||||
END
|
||||
END
|
||||
7
app/windows/wallet-version.h
Normal file
7
app/windows/wallet-version.h
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// The version of "Igneum Wallet.exe" (the window host). One place for wallet-host.rc and wallet-host.cpp.
|
||||
// Keep it equal to app/igneum-wallet/Cargo.toml and the AppVersion default in packaging/windows/Igneum-Wallet.iss.
|
||||
#ifndef IGNEUM_HOST_VERSION_H
|
||||
#define IGNEUM_HOST_VERSION_H
|
||||
#define IGNEUM_HOST_VERSION_STR "0.1.6"
|
||||
#define IGNEUM_HOST_VERSION_RC 0,1,6,0
|
||||
#endif
|
||||
|
|
@ -41,7 +41,8 @@ polygons). The Windows `.ico` and the favicons render each size from the vector,
|
|||
| `brand/profile/github-org-512.png` | organisation avatar | github.com/igneum-network (uploaded by hand from the Igneum Chrome profile) |
|
||||
| `brand/profile/x-banner-1500x500.png` | X header | the mark centred on obsidian |
|
||||
| `brand/igneum-icon-512.png`, `igneum-icon-1024.png` | the square at 512 and 1024 | the same as the profile files; kept for links that already point here |
|
||||
| `site/og-small.png` (256 square), `og-square.png` (1024), `og-coin.png` and `og.png` (1200 x 630 card: mark left, IGNEUM wordmark, tagline) | share images, `?v=2` in every `og:image` and `twitter:image` | index, live, litepaper (`og-small`); bench, evidence, `build.mjs` (`og`) |
|
||||
| `site/og/<card>.png` (1200 x 630) and `site/og/<card>-square.png` (1200 x 1200), one per row of `CARDS` in `site/og/pages.mjs`: the mark over a soft ember glow, a kicker, one line, a quiet sub line, the route at the foot | share images (WhatsApp, iMessage, Slack, X, LinkedIn); the metas of every page come from `site/og/pages.mjs` through `site/build.mjs`, `?v=` from `OG_VERSION` | every served page; rendered by `node site/og/render.mjs` on a build box from `site/og/card.html` (8 October 2026) |
|
||||
| `site/og.png`, `og-coin.png`, `og-square.png`, `og-small.png` | the earlier share images, kept only for links already cached by readers; nothing in the site points at them | retired 8 October 2026 |
|
||||
| `brand/profile/github-social-1280x640.png` | repository social preview | github.com/igneum-network/igneum settings (by hand) |
|
||||
| `brand/profile/vercel-avatar-512.png` | Vercel team or project avatar | by hand |
|
||||
|
||||
|
|
|
|||
|
|
@ -365,7 +365,18 @@ The k column. k is the chip core's energy per op over the GPU's at the same oper
|
|||
|
||||
Correction, 8 October 2026 (the research lane's microbench on the 5090, counter-asic-4-research.md 15.1a and the corrected 20.3 and 20.4 at 71fd465b): the per-MAC figures above are wrong by a factor of 32. A `mma.m8n8k16` tile is 1,024 multiply-adds per warp, 32 per lane, so a hash does 32 MACs per tile, not 1,024; the 4090's "0.056 pJ per MAC" is 1.8 pJ, and the 5090 at the ALU shadow's premium reads 2.9 pJ per MAC unlocked and 1.5 pJ at the 1,300 MHz lock (the packs job, 366,080 MACs per hash; the microbench's dependent u8 tile 4.1 and 2.2, the wide s8 m16n8k32 tile 1.36 and 0.83). Against the same 5 nm MAC array figures (0.04 to 0.4 pJ per INT8-class MAC, claimed) a chip's k on tile work is therefore 0.03 to 0.3, below the ALU shadow's 0.3 to 0.8, not near 1: at the same premium a tensor-shaped shadow leaves the chip 3.5x to 6.7x where the ALU shadow leaves it 2.1x to 3.5x. The tensor-tile column (2.1x at k = 1, 1.6x at k = 1.5) is withdrawn as a candidate; its premise, that a chip's MAC is no cheaper than the GPU's, is false by 4x to 30x on the public figures. The shipped row is unchanged: 2.1x at k = 1 and 3.4x at k about 0.33, the ALU shadow at the operating point's knee, measured four times at 82 to 90 W.
|
||||
|
||||
The capex column. The `f = 1` GDDR7 chip of 5.5 is USD 2.8 per MH/s of silicon and memory, which is USD 0.00016 per MH/s-hour of capex over two years against USD 0.000023 of electricity: capex-dominated 7x, as the 5090 is (USD 14.7 per MH/s at MSRP, 10x). A 64 MiB hot table adds about USD 15 of N5 die, the shadow core USD 25 to 40, an interposer USD 200, so the chip's capex reaches at most about USD 4.3 per MH/s: the per-unit capex wall is unreachable by 3x to 7x, and the break-even market cap moves only through the project cost (the mission lane's model: about USD 100 M with the N5 shadow core, about 200 M if the shadow runs per load and forces one die or an interposer; the per-load form behind that figure, the 16 x 27 placement, was closed on 7 October 2026 at night when it failed the value-level acceptance test across drawn eras, so the 200 M row rests on no construction shown to exist until a sound per-load class, one pass of a 432-instruction sub-block per load, is drawn, accepted and measured). Every figure here is modelled on cited or claimed parts; the research lane's microbench (20 probes, the mma_u8 and l2 rows the ones this model would take) is on PC 1's queue after the hot-table job.
|
||||
The capex column. The `f = 1` GDDR7 chip of 5.5 is USD 2.8 per MH/s of silicon and memory, which is USD 0.00016 per MH/s-hour of capex over two years against USD 0.000023 of electricity: capex-dominated 7x, as the 5090 is (USD 14.7 per MH/s at MSRP, 10x). A 64 MiB hot table adds about USD 15 of N5 die, the shadow core USD 25 to 40, an interposer USD 200, so the chip's capex reaches at most about USD 4.3 per MH/s: the per-unit capex wall is unreachable by 3x to 7x, and the break-even market cap moves only through the project cost (the mission lane's model: about USD 100 M with the N5 shadow core, about 200 M if the shadow runs per load and forces one die or an interposer; the per-load form behind that figure is REOPENED (8 October 2026, 11:0x UTC): the 16 x 27 iterated placement is dead on the no-era census (0.986 to 0.990 rejection per candidate on both instruments), the 7 October night's drawn-era death was an instrument artefact (the per-load acceptance test counted the era window's fixed top index bits as biased, fixed on counter-asic-4 at 10:5x UTC), and the sound form, one pass of a 256-instruction sub-block per load (`mx8+shl4096x1`), accepts 234 of 256 seeds on the no-era census with its drawn-era verdict the invention lane's 15:00 BST read; the 200 M row rests on that form, class v6's layer 5). Every figure here is modelled on cited or claimed parts; the research lane's microbench (20 probes, the mma_u8 and l2 rows the ones this model would take) is on PC 1's queue after the hot-table job.
|
||||
|
||||
### 5.12 Two chips from the hardware-future lane, and the honest denominator (8 October 2026, 11:3x UK, main's order; the figures lane B's, `docs/analysis/class-v6/hardware-future.md` at master 34f63b3c, modelled on this file's method unless marked claimed; the research lane carries them here as the model's own rows)
|
||||
|
||||
| Chip | When | Energy per random read | Rate per die | Energy per hash at zero shadow | Edge per joule against the 5090 at 2.40 microjoules | Against the M5 Max at 0.78 | With the class v4 shadow (F = 1.10 microjoules) at k = 0.5 / k = 1 | Silicon and project | What the four layers of class v6 do to it | Labels |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| **A 2 GiB SRAM full store on one N2 reticle** (TSMC N2 38 Mb/mm^2 HD SRAM, +11 percent over N3E, claimed, IEEE Spectrum, 12 December 2024; 2 GiB is about 452 mm^2 of macro under the 858 mm^2 reticle) | 2027 to 2028 (an N2 project; wafers about USD 30,000 and booked to 2028, claimed) | 1.0 nJ for a 64-byte read across the array (0.5 to 2.0: the wire term dominates, this file's 5.1 convention) | power-bound: about 2,100 MH/s at 300 W (0.14 microjoules per hash) | 0.14 | **17x (8x to 30x)** | 5.6x | **4.8x / 2.7x**; at the 5090's whole latency shadow (F about 2.0) 3.7x / 2.0x | about USD 400 to 600 of silicon per die, USD 0.25 to 0.4 per MH/s; an N2 project USD 100 M to 500 M and 18 to 24 months (claimed, the 2.5 economics; the mission lane's break-even model then reads a cap of about USD 330 M to 1.7 B in years 1 to 2 at s = 0.30) | layers 1, 3 and 4: nothing (the dataset is the SRAM's content, every draw firmware; the core an N2 core, k 0.3 to 0.5); layer 2 moves its CAPEX, not its joules: two dies at 4 GiB 15x and about USD 1,000, four at 8 GiB 13x and USD 2,000 to 2,500 | modelled; the N2 density and price claimed |
|
||||
| **A custom HBM4E base die** (the controller and PHY in the stack on N3P at 0.75 V, "2x the power efficiency", claimed, TrendForce, 1 December 2025; Micron 2027, SK hynix 2026; a non-hyperscaler customer from about 2028) | 2028 or later | 0.9 to 1.0 nJ | 166 MH/s per stack at the model's activate ceiling (36 at the JEDEC tFAW: the ceiling is unmeasured, 5.3) | 0.18 (0.37) | **14x (6.5x)** | 4.4x (2.1x) | 4.4x / 2.6x | about USD 5 per MH/s (approximate); the stack at a premium: Samsung asking about USD 4 to 5 per Gbit for HBM4 against 1.5 for HBM3E (claimed, 2 October 2026) | untouched by all four layers; the brake until about 2028 is HBM allocation and price, not the hash | modelled; the die and price claimed |
|
||||
| Per-bank processing in memory (Samsung HBM-PIM, SK hynix GDDR6-AiM and AiMX, Samsung LPDDR5X-PIM at Hot Chips 25 August 2026, LPDDR6-PIM in JEDEC work) | now to 2027 | the host memory's own | | | **under 1x: structurally blind to the hash's dependent random reads** | | | | a per-bank unit sees its own bank only: 1.6 percent of a hash's reads land in-bank at a 2 GiB dataset on a 32 MB bank, 0.4 percent at 8 GiB; every other read crosses the channel as it does for a controller chip; layer 2's growth lowers the share further | modelled (arithmetic on the bank size) |
|
||||
| The honest denominator | | | | the M5 Max at 0.78 microjoules per hash (the GPU and DRAM channels, measured 6 October), 3.1x the 5090 per joule; the 5090 at its knee 1.67 (measured 7 October) | | | | | against the best honest joule every chip edge is 2x to 3x smaller than against the 5090's stock point: the SRAM die 5.6x and the base die 3.6x to 4.4x at zero shadow, about 3x with the shadow at k = 0.5 | measured cards |
|
||||
|
||||
Reading, as the research lane reads it for the class v6 synthesis: the number this file's 5.6 verdict carried ("over 2x", the GDDR7 board at 5.1x) is no longer the strongest chip in the five-year window. The SRAM full store on merchant N2 reaches 17x at zero shadow and 2.7x to 4.8x with the class v4 shadow, and none of the rotating layers reaches it; what holds it is money and time (an N2 project at USD 100 M to 500 M, 18 to 24 months, the break-even cap in the hundreds of millions to about USD 1.7 B) and the one lever the chain has on it is capex through the dataset's size (layer 2's floor and its schedule, priced in the class v6 synthesis). The served chip line is reviewed against these rows at 20:00 UK today in the synthesis's last section; nothing served moves on this section.
|
||||
|
||||
## 6. The per-day derivation (item 2)
|
||||
|
||||
|
|
|
|||
139
docs/analysis/class-v6/census-w16-mix.md
Normal file
139
docs/analysis/class-v6/census-w16-mix.md
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# Class v6 census: the read width W = 16 and the op-mix re-weight through the sub-version 3 rules
|
||||
|
||||
8 October 2026, 13:2x to 14:1x UK, branch `class-v6-census` from `counter-asic-4` (base 4bb4c0ca), the census lane of the Counter ASIC 4.0 research under the Counter ASIC coordinator. The question: two class v6 changes are about to be chosen (the read width W = 16 words, the whole 64-byte item, the existing class `w64`; and an op-mix re-weight toward the families the k lane's RTL rows rank hardest for a programmable chip core), and neither may enter the class without the sub-version 3 rules. This file carries the census rows, the exact commands, and PASS or FAIL per candidate. Every number is measured on build-3 (igneum-build-3, 32 threads) or build-4 (igneum-build-4, 96 threads) under the lease pool at nice 19, measure class, owner `class-v6-census`; nothing ran on the Mac; nothing here touches the devnet, the testnet object or any served number.
|
||||
|
||||
## 0. One page
|
||||
|
||||
Both class v6 changes pass the sub-version 3 rules, measured, on 256 seeds each without an era and 256 seeds each across eight drawn eras, on build-3 and build-4 between 13:37 and 14:05 UK.
|
||||
|
||||
- **(a) W = 16, the whole 64-byte item, at the current mix: PASS.** 256 of 256 seeds accepted without an era (0 exhausted at the 256 cap, 0.704 rejection per candidate against the record's 0.668 and the 0.986 to 0.990 death line), 256 of 256 across eras 0 to 7 with the value-level window-bit test switched on as a refusal (0 refusals for the instrument's reason; 189 refusals at the era's stride bit against the control's 218, the record's own class on today's `load_index`), the (c''') floor refusing 1.4 percent of candidates, the F8-form top-0.1-percent share 0.999 to 1.006 of the uniform control at 2^20, the verifier +0.6 percent within a 4 percent drift. It is the cleanest address of the three: at 16 words the product's low bits 0 to 3 are alignment and never enter the address (the control carries that law at bit 0 in 116 of 256 programs).
|
||||
- **(b) W = 4 at the re-weighted mix (13, 11, 6, 10, 8, 8, 7, 2, 6, 4: mulhi 6 to 2, mul 8 to 6, mad 8 to 10, add, xor, rotl, sub +1 each): PASS on the rules.** 256 of 256 both ways, 0 exhausted, rejection 0.407 (the fewest lossy ops), (c''') 1.2 percent, verifier within drift. Weaker than (a) on the address: at 4 words the product law lands on bit 2 (75 of 256 programs), and the extra `mad` raises the stride-bit refusals to 1.6 per seed on the eras whose R is in range (the control 1.2).
|
||||
- **(c) both: PASS on the rules**, 256 of 256 both ways, rejection 0.426, (c''') 3.4 percent, F8-form 0.999 to 1.002, verifier -0.1 percent; the address as (a) at the low bits and as (b) at the stride bit.
|
||||
|
||||
The order (the coordinator's correction at 14:1x UK, taken into the close): W = 16 passes the rules but is dead on energy, measured today by floor lane 5 on the 5090, 4090, H100 and 3090 (the card pays the second sector through its own fabric, +25 to +34 percent energy per hash, equal to the chip's +33, so the edge does not move and the honest card loses a quarter of its efficiency). So the class order is: W = 8 pins (lane 3), the index fold lands first, then the re-weight (b) at W = 8 goes through this harness again (five box-minutes on 16 cores) before it enters. What this census keeps from (a): the rules hold at the widest width, and a wider read removes the product's own low bits from the address by construction (at W = 8 bits 0 to 2 are alignment, so the bit-2 law of the W = 4 rows is gone there too); the stride bit R is the fold's to remove. The W = 8 class is not a width the generator draws today (`WIDTH_WORDS` is 1, 4, 16), so lane 3's construction lands before its census runs.
|
||||
|
||||
## 1. The candidates and the rules
|
||||
|
||||
| Candidate | Class under `igneum-pow accept` | Read width | Weights (add, xor, mul, mad, shfl, rotl, sub, mulhi, rotr, or; sum 75) |
|
||||
|---|---|---|---|
|
||||
| ctrl, the record (class v4) | `mx8+sh256x27` | W = 1 (4 bytes) | base: 12, 10, 8, 8, 8, 7, 6, 6, 6, 4 |
|
||||
| w16, the design's genesis pin | `w16m8g+sh256x27` | W = 4 (16 bytes) | base |
|
||||
| (a) W = 16 at the current mix | `w64m8g+sh256x27` | W = 16 (64 bytes, two 32-byte sectors) | base |
|
||||
| (b) W = 4 at the re-weighted mix | `w16m8g+sh256x27` with `IGNEUM_FG_WEIGHTS` | W = 4 | re-weighted: 13, 11, 6, 10, 8, 8, 7, 2, 6, 4 |
|
||||
| (c) both | `w64m8g+sh256x27` with `IGNEUM_FG_WEIGHTS` | W = 16 | re-weighted |
|
||||
|
||||
**The re-weight, stated.** The k lane's RTL table (class v6 design 10.2: k absolute at N3 against the 5090's lock, highest first) reads mad 0.20, the ARX families add, xor, rotl, rotr, sub 0.17 to 0.18, the index fold 0.14, or 0.12, mul 0.08, prmt and lop3 0.06, mulhi 0.03. The re-weight moves 6 points of 75 from the two families the chip finds easiest (mulhi 6 to 2, mul 8 to 6) to the family it finds hardest (mad 8 to 10) and to four ARX families (add 12 to 13, xor 10 to 11, rotl 7 to 8, sub 6 to 7); shfl, rotr and or stay at base. `prmt` and `lop3` are chip RTL rows, not drawn families (the generator's ten non-load families are add, sub, mul, mulhi, xor, or, rotl, rotr, mad, shfl), so "prmt down" has no weight to move; the nearest drawn family, shfl, stays at base. The lossy share (or + mul + mulhi) falls from 18 to 12 of 75, inside the family-gate band (the lossy families never above their base). mulhi keeps 2 points rather than 0 so the family stays drawn (layer 3's rotation decides family membership, not this re-weight).
|
||||
|
||||
**The rules every candidate is judged by** (the harness, `igneum-pow/src/accept.rs` and `generator.rs` on this branch, on only under `IGNEUM_FAMILY_GATE` and never on a chain path): the class v4 rule set keyed on the class v4 shape at any read width (`is_census_shape`: the 256 x 27 block over the mx8 base with the mix one-hot on 1, 4 or 16 words), so (a') the freshness fixpoint in the draw and the acceptance, (c') saturated sources, (c'') the distinct-index ratio at 0.98 over 2^20 evaluations with the window divided by the width (the family-gate harness's calibration), and the 256-attempt cap with the last resort all apply; plus (c''') the class v5 hot-item floor at 0.995 on the same 2^20 sample (ported from the class v5 crate); plus the value-level index-bit test (the research file's 20.2b) read on every class over the bits the site's window and the width's alignment leave free, refused above 6 sigma under `IGNEUM_FG_BITTEST` (the drawn-era runs) and recorded on the accepted program otherwise.
|
||||
|
||||
## 2. The no-era census (256 seeds per candidate, `igneum-v6c/0..255`, the class's own 256-attempt cap)
|
||||
|
||||
The instrument of the research file's 20.2a-close (the invention lane's no-era census), on this branch's harness: every candidate of each seed through the whole rule, no era, the index the plain `x AND mask` so every free bit is fair to judge. `r` is the rejection rate per candidate (the record's death line for the per-load forms was 0.986 to 0.990; the class v4 control under the family-gate band read 0.595 to 0.73 across strata). The index-bit column is the value-level read on the accepted program over the 64 units' 16,384 addresses per site (sigma 64; the bits below the width's alignment are 0 by construction and not judged), so a z here is 8x smaller than the family-gate harness's 2^20 reading of the same bias.
|
||||
|
||||
| Candidate | Seeds accepted of 256 | Exhausted at the 256 cap | Candidates walked | r per candidate | Mean attempt, max | (c''') refused of candidates | (c'') refused | Rejections by part | Accepted programs with a site over 6 sigma; worst | Box, clock (UK) |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| ctrl, the record (W = 1, `mx8+sh256x27`) | **256** | 0 | 770 | **0.668** | 2.01, 18 | 1 (0.1 percent) | 8 (1.0 percent) | (a') 431, (a) 52, (b) 18, (c) 4 | 143 of 256 (56 percent); z 97.1 at bit 0; 116 programs at bit 0, 14 at bit 1, 5 at bits 2 and 3, 8 at bits 4 to 26 (the product's low-bit law at the load's source, the family-gate harness's 48 to 58 percent read at 2^20 replicated at 2^14) | build-3, 8 cores, 13:45 to 13:53 |
|
||||
| (a) W = 16, base mix (`w64m8g+sh256x27`) | **256** | 0 | 865 | **0.704** | 2.38, 16 | 12 (1.4 percent) | 12 (1.4 percent) | (a') 500, (a) 57, (b) 19, (c) 9 | 26 of 256 (10 percent); z 41.9 at site 0 bit 15 (seed 69); bits 4, 5, 9, 10, 11, 15, 19, 21, 27 | build-3, 8 cores, 13:37 to 13:42 |
|
||||
| w16, the design's genesis pin (W = 4, base mix) | **256** | 0 | 854 | **0.700** | 2.34, 16 | 9 (1.1 percent) | 11 (1.3 percent) | (a') 491, (a) 58, (b) 20, (c) 9 | 97 of 256 (38 percent); z 56.3 at bit 10; 74 programs at bit 2 | build-4, 16 cores, 13:51 to 13:55 |
|
||||
| (b) W = 4, re-weighted (`w16m8g+sh256x27`, weights 13,11,6,10,8,8,7,2,6,4) | **256** | 0 | 432 | **0.407** | 0.69, 5 | 5 (1.2 percent) | 14 (3.2 percent) | (a') 123, (a) 21, (c) 9, (b) 4 | 97 of 256 (38 percent); z 71.0 at bit 23; **75 programs at bit 2** (the lowest bit the 4-word alignment leaves free: the product's low-bit law moves up to it), 22 at bits 4 to 27 | build-4, 16 cores, 13:46 to 13:50 |
|
||||
| (c) W = 16, re-weighted (`w64m8g+sh256x27`, the same weights) | **256** | 0 | 446 | **0.426** | 0.74, 6 | 15 (3.4 percent) | 14 (3.1 percent) | (a') 127, (a) 21, (c) 9, (b) 4 | 32 of 256 (12.5 percent); z 34.1 at bit 18; 13 programs at bit 4, the rest spread over bits 5 to 27 | build-4, 16 cores, 13:46 to 14:00 |
|
||||
|
||||
What the no-era rows say. Every candidate accepts 256 of 256 seeds inside the chain's cap with no seed past attempt 18, and every rejection rate sits far under the 0.986 to 0.990 line that killed the per-load forms: the width moves r by nothing (0.668 at W = 1, 0.700 at W = 4, 0.704 at W = 16, the (c''') floor's per-width calibration adding about 1 point at W = 16) and the re-weight LOWERS it (0.407 to 0.426: six fewer points on the lossy families means fewer (a') refusals, mean attempt 0.7 against 2.0 to 2.4). The index-bit column is the product's low-bit law, and it moves with the width exactly as the alignment predicts: at W = 1 it sits at bit 0 in 116 of 256 programs (56 percent carry a biased site); at W = 4 bits 0 and 1 are alignment and the law lands on bit 2 (74 to 75 of 256); at W = 16 bits 0 to 3 are alignment and the law is gone from the address, leaving 26 to 32 of 256 programs (10 to 12 percent) with a weaker bias at bits 4 and up (z 8 to 42 at 2^14), the class the record's family-gate rows attribute to the era stride and to lossy writers upstream, which layer 1's index fold is the remedy for. The re-weight does not change the bias picture at either width (97 of 256 at W = 4 with or without it; 26 against 32 at W = 16).
|
||||
|
||||
## 3. The drawn-era census with the window-bit test as a refusal (eras `igneum-era-test/0..7`, 32 seeds per era, 256 per candidate)
|
||||
|
||||
The per-era form of the research file's 20.2a-correction: the value-level index-bit test judges only the bits inside each site's era window (so a narrow-window site's fixed top bits are never counted, the instrument fault of 10:5x UTC) and above the width's alignment, and here it REFUSES a candidate (6 sigma over the 64 units) so the count of refusals per era is read directly; a refused candidate is redrawn, as the chain would. The eras' stride rotations: era 0 R = 19, era 1 R = 6, era 2 R = 28, era 3 R = 30, era 4 R = 10, era 5 R = 22, era 6 R = 3, era 7 R = 27 (`igneum-pow show --era`). Zero refusals for the instrument's reason on every era and every candidate (the fixed window bits are not judged); every refusal below is a real bias at the era's stride bit, the adv-cache-2 class the record already carries on today's `load_index` (the product's low bits placed at address bit R by `rotl(x * M, R)`), on the eras whose R lands inside the free bits.
|
||||
|
||||
| Candidate | Seeds accepted of 256 | Exhausted at the 256 cap | Candidates walked | r per candidate | Mean attempt, max | Window-bit refusals per era 0 to 7 (of candidates) | The bit refused, per era (count) | Worst free bit on the accepted programs | Box, clock (UK) |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| ctrl, W = 1 | **256** | 0 | 1,502 | 0.830 | 4.87, 22 | 38, 33, 5, 25, 33, 33, 38, 13 (218 of 1,502, 14.5 percent) | 19 (34), 6 (31), 0 (4), 0 (23), 10 (31), 22 (31), 3 (34), 27 (9): bit R on every era with R under 28, bit 0 (the product's own) on eras 2 and 3 | 5.9 sigma (under the band by construction) | build-4, 16 cores, 13:59 to 14:02 |
|
||||
| w16, W = 4, base mix (the reference) | **256** | 0 | 1,556 | 0.835 | 5.08, 40 | 29, 38, 0, 2, 48, 40, 40, 7 (204 of 1,556, 13.1 percent) | bit R on the R-in-range eras, as the control | 5.5 sigma | build-4, 16 cores, 14:02 to 14:05 |
|
||||
| (a) W = 16, base mix | **256** | 0 | 1,524 | 0.832 | 4.95, 40 | 27, 41, 0, 1, 48, 32, 33, 7 (189 of 1,524, 12.4 percent) | 19 (24), 6 (33), none, 5 (1), 10 (40), 22 (26), 4 (31: R = 3 is alignment, the law's next bit), 27 (7): bit R, never bits 0 to 3 | 5.9 sigma | build-3, 8 cores, 13:54 to 13:58 |
|
||||
| (b) W = 4, re-weighted | **256** | 0 | 814 | 0.686 | 2.18, 11 | 50, 54, 1, 3, 51, 51, 51, 12 (273 of 814, 33.5 percent) | 19 (46), 6 (48), 2 (1), 2 (2), 10 (46), 22 (47), 3 (46), 27 (11) | 5.3 sigma | build-3, 8 cores, 13:58 to 14:02 |
|
||||
| (c) W = 16, re-weighted | **256** | 0 | 781 | 0.672 | 2.05, 11 | 50, 54, 1, 3, 50, 51, 24, 14 (247 of 781, 31.6 percent) | 19 (46), 6 (48), 4 (1), 4 (3), 10 (45), 22 (47), 4 (22), 27 (12) | 5.5 sigma | build-4, 15 cores, 13:55 to 13:59 |
|
||||
|
||||
What the era rows say. Every candidate accepts every seed on every era inside the cap with the bias refusal switched on, so none costs liveness: the price is attempts (mean 2 to 5, the worst seed 40 at W = 16 on eras 1, 4 and 6 against the control's 22). The refusals are the era-stride class on every candidate and on the control alike: on the four eras with R in 6 to 22 (eras 0, 1, 4, 5) about one candidate per seed is refused at bit R, on eras with R at 27 or above almost none, and the width only decides whether the product's OWN low bits are also in the address (bit 0 on eras 2 and 3 for the control: 27 refusals; gone at W = 16: 1). The re-weight raises the stride-bit incidence (1.6 refusals per seed at eras 0, 1, 4, 5 against 0.8 to 1.2 at the base mix): more `mad` means more product-sourced loads in front of the rotation, and `mad` is an injecting family the (a') rule accepts as a load's last writer. None of this is the candidates' to fix: the design's remedy is layer 1's index fold of the product's low bits before the stride rotation, after which this test becomes the guard that the fold holds; until the fold is in, the bias test stays a record per era, as section 5.1 of the design says, and the chain's rule (without it) accepts the same programs at the no-era rates of section 2.
|
||||
|
||||
## 4. The F8-form uniformity read (2^20 nonces per seed)
|
||||
|
||||
The invention lane's instrument with the era draw added (`tools/attack/v6-census/uniform`): per seed the first accepted candidate under the harness, then every load's word index over 2^20 nonces through the library's own `trace_load_indices`; per site the distinct-index ratio against `N - N^2 / 2W_s` (the window over the width); the cross-hash ITEM histogram's top 0.1 percent share against a uniform control of the same size (the F8 pass line: within 1.2x; the record's class v4 read 1.00x on the clean seeds and the plants fire at 29x and 4.8x). No era here (the no-era index is the plain `x AND mask`); the drawn-era F8 verdict is section 3's window-bit test, and the family-gate lane's 2^24 live-dataset census is the instrument for a per-era hot-set hunt, at 45 to 100 core-hours per point, which this window does not hold.
|
||||
|
||||
| Candidate | Seeds | Top 0.1 percent share, ratio to the uniform control (min to max) | Min site distinct ratio (min to max over seeds) | Max item reads (candidate / control) | Box, clock (UK) |
|
||||
|---|---|---|---|---|---|
|
||||
| ctrl, the record (W = 1) | 16 | 0.9992 to 1.0163 (PASS) | 0.9722 to 1.0001 | 26 to 29 / 29 | build-3, 8 cores, 13:50 to 13:52 |
|
||||
| w16 (W = 4, base mix) | 16 | 0.9993 to 1.0053 (PASS) | 0.9967 to 1.0026 | 26 to 30 / 29 | build-3, 8 cores, 13:52 to 13:54 |
|
||||
| (b) W = 4, re-weighted | 16 | **0.9989 to 1.0077** (PASS at 1.2x) | 0.9815 to 1.0026 | 26 to 30 / 29 | build-3, 8 cores, 14:02 to 14:05 |
|
||||
| (a) W = 16, base mix | 16 | **0.9986 to 1.0061** (PASS at 1.2x) | 1.0439 to 1.0491 | 26 to 30 / 27 to 29 | build-3, 8 cores, 13:44 to 13:46 |
|
||||
| (c) W = 16, re-weighted | 16 | **0.9987 to 1.0016** (PASS at 1.2x) | 1.0465 to 1.0491 | 26 to 30 / 27 to 29 | build-3, 8 cores, 13:42 to 13:44 |
|
||||
|
||||
## 5. The verifier cost
|
||||
|
||||
`igneum-pow bench --warps 20` on one leased core of build-4 (core 18, nice 19, 13:55 to 14:02 UK) while the box ran 80 other leased threads, so the figures carry the box's load: the control's three repeats at the start read 9.55, 9.54, 9.68 ms per 32-lane warp and the same program at the end 9.49, 9.13, 9.19 (the load fell during the run). Every pair is read against the nearest control block.
|
||||
|
||||
| Candidate | ms per warp, three repeats of 20 warps | Against the control | Reading |
|
||||
|---|---|---|---|
|
||||
| ctrl (first) / ctrl (last) | 9.55, 9.54, 9.68 / 9.49, 9.13, 9.19 | | the drift of the box: 4 percent across the run |
|
||||
| w16 (W = 4, base mix) | 9.65, 9.42, 9.45 | -0.9 percent on the means (first block) | within the drift |
|
||||
| (a) W = 16, base mix | 9.48, 9.75, 9.70 | **+0.6 percent** on the means (first block) | within the drift; the invention lane's +4 percent (20.2b's row) is not seen at this resolution |
|
||||
| (b) W = 4, re-weighted | 9.62, 8.60, 8.38 | -5 percent on the means (the load fell mid-run; the first repeat equals the control) | within the drift |
|
||||
| (c) W = 16, re-weighted | 9.15, 9.21, 9.25 | **-0.1 percent** against the last control block | within the drift |
|
||||
|
||||
The verifier cost delta of both changes is under the run's 4 percent drift on a loaded core; a quiet-box reading (`lease quiet`, the ladder's method with the SMT sibling loaded) is owed before the number is quoted as a figure. Nothing here approaches the 10 ms loaded gate.
|
||||
|
||||
## 6. PASS or FAIL per candidate against the sub-version 3 rules
|
||||
|
||||
The line: 256 of 256 seeds accepted on the no-era census inside the chain's cap with r far under 0.986 to 0.990; the drawn-era census on eras 0 to 7 with 0 refusals for the instrument's reason and every seed accepted; no product bit in the address that the record does not already carry (the era-stride bit under the pre-fold `load_index`, equal in kind on the control); the (c''') floor applied and its cost stated; the F8-form read within 1.2x of the uniform control.
|
||||
|
||||
| Candidate | No-era census | Drawn-era census and the window-bit test | Biased product bits in the address | (c''') | F8-form at 2^20 | Verifier | Verdict |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| (a) W = 16 at the current mix | 256 of 256, 0 exhausted, r 0.704 | 256 of 256 on eras 0 to 7, 0 exhausted, 0 instrument refusals; 189 stride-bit refusals of 1,524 candidates (the control 218 of 1,502) | the product's own low bits (0 to 3) are alignment and never reach the address; the stride bit R stays, as on the record | 1.4 percent of candidates refused | 0.999 to 1.006 of the control | +0.6 percent, within drift | **PASS** |
|
||||
| (b) W = 4 at the re-weighted mix | 256 of 256, 0 exhausted, r 0.407 | 256 of 256, 0 exhausted, 0 instrument refusals; 273 stride-bit refusals of 814 | bit 2 carries the product law in 75 of 256 programs (the width's, not the mix's: the w16 reference reads 74); the stride bit refused 1.6 times per seed on R-in-range eras against the control's 1.2 | 1.2 percent | 0.999 to 1.008 of the control | within drift | **PASS on the rules; weaker than (a) on the bias count** |
|
||||
| (c) both | 256 of 256, 0 exhausted, r 0.426 | 256 of 256, 0 exhausted, 0 instrument refusals; 247 stride-bit refusals of 781 | as (a) at bits 0 to 3; the stride bit refused 1.6 times per seed on R-in-range eras | 3.4 percent | 0.999 to 1.002 of the control | -0.1 percent, within drift | **PASS on the rules; the re-weight's stride-bit cost as in (b)** |
|
||||
|
||||
Meaning for the two changes. The read width W = 16 passes every sub-version 3 rule at the record's own rates and is the cleanest of the three on the address: a wider read removes the product's low-bit law from the address by construction. It does not enter: floor lane 5's card rows of today (the 5090, 4090, H100 and 3090) read the second sector at +25 to +34 percent energy per hash on the card, paid through its own fabric and equal to the chip's +33, so the edge does not move and the honest card loses a quarter of its efficiency; W = 16 is dead on energy, and the rules' verdict here is what it would have needed beside that. The re-weight passes the rules with a lower rejection rate (fewer lossy ops, fewer (a') refusals) and costs nothing on the verifier, but it moves weight onto `mad`, a product the (a') rule accepts at a load's source, so the era-stride bias is refused about a third more often per seed until layer 1's index fold lands; with the fold in, that cost disappears and the re-weight's chip-side gain (the k lane's ranking) stands on its own. The class order, per the coordinator's close: W = 8 pins (lane 3), the index fold lands first, then the re-weight (b) at W = 8 goes through this harness again (five box-minutes) before it enters; the W = 8 draw is lane 3's construction (the generator's widths are 1, 4 and 16 today), and the harness takes it the day it parses.
|
||||
|
||||
## 7. The exact commands
|
||||
|
||||
The harness is this branch's `igneum-pow` at 14c550e3 (the CLI binary pinned on both boxes as `/srv/builds/v6-census/bin/14c550e3/igneum-pow`, sha256 d601098699840534...; the uniform tool `v6census-uniform` beside it, 2a150ee714b06e79...), built on the box through `tools/build-remote.sh --box 3` and `--box 4` from `igneum-pow/` and from `tools/attack/v6-census/uniform/` (`build --release`). The scripts are in the tree under `tools/attack/v6-census/` (the census) and were copied to `/srv/builds/v6-census/` on each box with the launchers below.
|
||||
|
||||
One census job (the launcher `launch.sh <commit> <job>...`, jobs `ctrl-none a-none b-none c-none w16-none ctrl-era a-era b-era c-era w16-era`; the era jobs carry the candidate's own class, since a single allowed era width leaves the base class's mix as drawn):
|
||||
|
||||
```
|
||||
nohup /srv/builds/_bin/lease pool 16 --min 4 --nice 19 --class measure \
|
||||
--label "class v6 census: a-none (w64m8g+sh256x27, era widths 64, weights base) on 14c550e3" --owner class-v6-census -- \
|
||||
env THREADS={cores} BIN=/srv/builds/v6-census/bin/14c550e3/igneum-pow OUT=/srv/builds/v6-census TAG=a-none \
|
||||
CLASS=w64m8g+sh256x27 ERAS=none SEEDS=256 ERAW=64 /srv/builds/v6-census/v6census.sh > /srv/builds/v6-census/run-a-none.log 2>&1 &
|
||||
# the re-weighted candidates add IGNEUM_FG_WEIGHTS=13,11,6,10,8,8,7,2,6,4; the era jobs add ERAS="0 1 2 3 4 5 6 7" SEEDS=32 IGNEUM_FG_BITTEST=1
|
||||
```
|
||||
|
||||
Inside the script, one seed is one process (the harness flag on, the class's own cap of 256 attempts, the per-era form with the width pinned):
|
||||
|
||||
```
|
||||
IGNEUM_FAMILY_GATE=1 igneum-pow accept --seed igneum-v6c/<i> --class w64m8g+sh256x27
|
||||
IGNEUM_FAMILY_GATE=1 IGNEUM_FG_BITTEST=1 igneum-pow accept --seed igneum-v6c/<i> --class w64m8g+sh256x27 --era igneum-era-test/<e> --era-widths 64
|
||||
```
|
||||
|
||||
The F8-form uniformity read (`uniform-launch.sh ctrl a b c w16`, no era, 16 seeds at 2^20 nonces):
|
||||
|
||||
```
|
||||
nohup /srv/builds/_bin/lease pool 16 --min 4 --nice 19 --class measure --label "class v6 census: F8-form uniform a era none ..." --owner class-v6-census -- \
|
||||
env IGNEUM_FAMILY_GATE=1 /srv/builds/v6-census/bin/14c550e3/v6census-uniform --class w64m8g+sh256x27 --seeds 16 --nonces 1048576 --threads {cores} --era none --era-widths 64 > uniform-a-none.tsv &
|
||||
```
|
||||
|
||||
The verifier bench (`benchall.sh`, one leased core, three repeats of 20 warps per candidate, the control first and last):
|
||||
|
||||
```
|
||||
nohup /srv/builds/_bin/lease pool 1 --min 1 --nice 19 --class measure --label "class v6 census: verifier bench per candidate, one core" --owner class-v6-census -- ./benchall.sh > bench.log 2>&1 &
|
||||
# inside: IGNEUM_FAMILY_GATE=1 [IGNEUM_FG_WEIGHTS=...] igneum-pow bench --seed igneum-v6c/0 --class <class> --warps 20
|
||||
```
|
||||
|
||||
Every job writes its lease pid to `/srv/builds/v6-census/pids/<job>.lease.pid` and the script its own to `pids/<job>.pid`; a job is stopped only through those files. The launchers (`launch.sh`, `uniform-launch.sh`, `benchall.sh`) and the summary script are in `tools/attack/v6-census/`. The summary over a TSV is `summ.py` (per era: seeds, accepted, exhausted, candidates, r, mean and max attempt, the worst index-bit sigma, the over-6-sigma count, the window-bit refusals, the rejections by part); the TSVs and logs stay under `/srv/builds/v6-census/` on the box that ran them, with copies under `docs/analysis/class-v6/logs/census/` on this branch.
|
||||
|
||||
## 8. Unverified and owed
|
||||
|
||||
- The drawn-era F8 hot-set census at 2^24 on the live dataset (ring C, 45 to 100 core-hours per point) was not run in this window; section 4 is the 2^20 no-era form plus section 3's per-era window-bit test.
|
||||
- The verifier figures are from a loaded core with a 4 percent drift; the quiet-box reading is owed before a number is served.
|
||||
- The (c''') floor at W = 4 and W = 16 refuses 1.2 to 3.4 percent of candidates against 0.1 percent at W = 1 on the same 2^20 sample with the window divided by the width; the family-gate lane's per-width calibration (its 09:00 item) decides whether 0.995 is the floor at every width or a sigma form replaces it. No seed was lost to it here.
|
||||
- The re-weight's weights are this lane's reading of the k lane's table, not the k lane's optimiser (owed by that lane at 19:30 UK); a different split inside the band would need its own census, at about five box-minutes per candidate on 16 cores with this harness.
|
||||
- The index-bit sigma on the accepted programs is read at the acceptance's 2^14 evaluations (sigma 64); the family-gate harness reads the same statistic at 2^20 and sees 8x the z. The counts here are therefore lower bounds on the share of programs carrying a biased site.
|
||||
- The harness widens the class v4 rules to any width only under `IGNEUM_FAMILY_GATE`; the chain's generator and acceptance on `counter-asic-4` are unchanged by this branch, and the per-width `site_window` division in `distinct_ratio_pass` is the one unconditional edit (width 1 on every shipped class, so no shipped verdict moves).
|
||||
86
docs/analysis/class-v6/coexist-rows.md
Normal file
86
docs/analysis/class-v6/coexist-rows.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# Class v6 coexistence rows: the 5.5 GiB miner beside the shard prover on an RTX 3060 12 GB and an RTX 4060 8 GB
|
||||
|
||||
Measured 8 October 2026, 16:10 to 16:38 BST, on two Vast.ai instances rented for the rows and destroyed after them (RTX 3060: instance 54866832, Norway, driver 610.57.04, power limit 170 W, 0.276 h, USD 0.02; RTX 4060: instance 54866835, Mexico, driver 570.211.01, power limit 115 W, 0.545 h, USD 0.07). Record for the Igneum 2.0 pin D1 ("5.5 GiB coexistence rows") and the proving statement's memory condition. Every row below is the RESULT line as the pod wrote it to its run log (scratchpad v6coexist/fb-v6c-3060/run.log, fb-v6c-4060/run.log and run2.log; the 1 Hz samples beside them). Times in the logs are UTC.
|
||||
|
||||
## What ran
|
||||
|
||||
- Miner: the ds55 kit's own worker (`igneum-ca3-ds55-kit-20261008.zip`, sha 2d7f55e8..., worker sha d43be4625b78baf7) on `packs/ds55`, the 5.5 GiB dataset (1,476,395,008 words), `--bench --batch-log2 24 --block-warps 1 --device 0`; 100 batches alone, 400 batches as the beside run.
|
||||
- Prover: `igneum-prove-host-0317` (sha 71bc2438856bb141) on the fixed shard `fees-v1-shards2.json` shard 0 (4,717,439 cycles, fixture sha 20a108f159c61ff9), `--mode compressed --shard 0`, `SP1_GPU_ELEMENT_THRESHOLD=67108864` (2^26), against the served floor tarballs: sm_86 (`igneum-floor-sm86.tgz`, patched server sha 224200d317cea579) on the 3060, sm_89 (`igneum-floor-sm89.tgz`, patched server sha 882bd34f7f69dc97) on the 4060.
|
||||
- Sampler: `nvidia-smi --query-gpu=memory.used,power.draw,utilization.gpu` once a second per phase. Watts are the mean of power.draw over the busy samples from 8 s in; the 4060 host reports power.draw as N/A, so the 4060 has no watts.
|
||||
- Register windows: the class v5 kit's worker (`packs-ca3-v5-20261008T085619Z.zip`, worker sha d84b1b6cb09cebdc) on `hl-reg64c` (the full chain) and `hl-reg64` (arithmetic only), 100 batches each.
|
||||
|
||||
## RTX 3060 12 GB: the rows verbatim
|
||||
|
||||
```
|
||||
RESULT start 2026-10-08T15:10:03Z card=NVIDIAGeForceRTX3060 total_mib=12288 driver=610.57.04 power_limit_w=170.00
|
||||
RESULT bins worker=d43be4625b78baf7 host=71bc2438856bb141 server=224200d317cea579 fixture=20a108f159c61ff9
|
||||
RESULT idle mem_mib=1 power_w=21.32
|
||||
RESULT cmd miner_alone: /root/coex/kit/bin/linux/igneum-worker-cuda --bench --pack /root/coex/kit/packs/ds55 --batches 100 --batch-log2 24 --block-warps 1 --device 0
|
||||
RESULT miner_alone rc=0 wall_s=67 peak_mib=6129 check=PASS fingerprint=23ced07a4d28b465 mhs=26.824 self-test PASS
|
||||
RESULT cmd proof_alone: env HOME=/opt/igneum-floor/home SP1_PROVER=cuda RUST_LOG=off SP1_GPU_FLOOR_LOG=1 SP1_GPU_ELEMENT_THRESHOLD=67108864 timeout 900 /opt/igneum-floor/bin-0317/igneum-prove-host /root/coex/fees-v1-shards2.json --mode compressed --shard 0 --out /root/coex/proof-alone.json
|
||||
RESULT proof_alone rc=0 wall_s=31 peak_mib=7525 prove_s=13.2 verified=1 err=""
|
||||
RESULT proof_alone_line RESULT compressed shard 0: prove 13.2 s, proof 1272897 bytes, verify 0.110 s, VERIFIED; statement 0x6adcc7fab21512b58cfa3778756718fd37aea6cccb311e12d2ca33b5f4bf10c0 proof sha256 0x57e133da05ea5f6ecbcbd731b1b26c9bed2f10c5a1aa1bd04a5a5e3dab1e
|
||||
RESULT cmd miner_beside_comp26: /root/coex/kit/bin/linux/igneum-worker-cuda --bench --pack /root/coex/kit/packs/ds55 --batches 400 --batch-log2 24 --block-warps 1 --device 0
|
||||
RESULT cmd proof_beside_comp26: env HOME=/opt/igneum-floor/home SP1_PROVER=cuda RUST_LOG=off SP1_GPU_FLOOR_LOG=1 SP1_GPU_ELEMENT_THRESHOLD=67108864 timeout 600 /opt/igneum-floor/bin-0317/igneum-prove-host /root/coex/fees-v1-shards2.json --mode compressed --shard 0 --out /root/coex/proof-beside-comp26.json (started 1s+10s after the miner, miner resident mem_mib=6129)
|
||||
RESULT proof_beside_comp26 mode=compressed thr=67108864 rc=1 wall_s=34 proof_window=1791472334..1791472368 miner_start=1791472323 prove_s= verified=0 miner_alive_after=yes err="thread 'tokio-rt-worker' (1790) panicked at slop/crates/alloc/src/raw_buffer.rs:271:9:"
|
||||
RESULT miner_beside_comp26 rc=0 wall_s=256 peak_mib=11893 check=PASS fingerprint=23ced07a4d28b465 mhs=26.512 self-test PASS
|
||||
RESULT beside_fallback compressed 2^26 beside the miner did not verify; core 2^25 beside the miner next
|
||||
RESULT cmd proof_beside_core25: env HOME=/opt/igneum-floor/home SP1_PROVER=cuda RUST_LOG=off SP1_GPU_FLOOR_LOG=1 SP1_GPU_ELEMENT_THRESHOLD=33554432 timeout 600 /opt/igneum-floor/bin-0317/igneum-prove-host /root/coex/fees-v1-shards2.json --mode core --shard 0 --out /root/coex/proof-beside-core25.json (started 3s+10s after the miner, miner resident mem_mib=6129)
|
||||
RESULT proof_beside_core25 mode=core thr=33554432 rc=1 wall_s=18 proof_window=1791472598..1791472616 miner_start=1791472585 prove_s= verified=0 miner_alive_after=yes err="Error: unknown mode core"
|
||||
RESULT miner_beside_core25 rc=0 wall_s=254 peak_mib=11013 check=PASS fingerprint=23ced07a4d28b465 mhs=26.775 self-test PASS
|
||||
RESULT beside_failed core 2^25 beside the miner did not verify either
|
||||
RESULT idle_after mem_mib=1
|
||||
RESULT window_hl-reg64c rc=0 wall_s=130 peak_mib=1521 regs=87 blocks_per_sm=16 check=PASS fingerprint=4e7cc25967eba280 mhs=13.467 self-test PASS
|
||||
RESULT window_hl-reg64 rc=0 wall_s=128 peak_mib=1521 regs=104 blocks_per_sm=16 check=PASS fingerprint=70e786af1a457653 mhs=13.476 self-test PASS
|
||||
```
|
||||
|
||||
Watts from the samples (busy mean of power.draw from 8 s in): miner alone 117.4 W; proof alone 122.2 W over the 9 busy seconds; miner with the failed compressed attempt beside it 118.6 W; hl-reg64c 120.8 W; hl-reg64 119.3 W. The card touched its 170 W limit on none of them.
|
||||
|
||||
## RTX 4060 8 GB: the rows verbatim
|
||||
|
||||
The first pass (run.log) ran the proof rows against the stock SDK server, because the served sm_89 tarball carries the stock `sp1-gpu-server` in `home/.sp1/bin` (sha c2642ad1c42e85d8, the 24 GB gate at builder.rs:38) and the patched one only in `bin/` (882bd34f7f69dc97); those two rows are kept here as the record of the fault. The patched server was copied over `home/.sp1/bin` at 16:13 BST and the proof rows re-ran (run2.log).
|
||||
|
||||
```
|
||||
RESULT start 2026-10-08T15:10:44Z card=NVIDIAGeForceRTX4060 total_mib=8188 driver=570.211.01 power_limit_w=115.00
|
||||
RESULT bins worker=d43be4625b78baf7 host=71bc2438856bb141 server=882bd34f7f69dc97 fixture=20a108f159c61ff9
|
||||
RESULT idle mem_mib=2 power_w=[N/A]
|
||||
RESULT cmd miner_alone: /root/coex/kit/bin/linux/igneum-worker-cuda --bench --pack /root/coex/kit/packs/ds55 --batches 100 --batch-log2 24 --block-warps 1 --device 0
|
||||
RESULT miner_alone rc=0 wall_s=91 peak_mib=6116 check=PASS fingerprint=23ced07a4d28b465 mhs=18.837 self-test PASS
|
||||
RESULT proof_alone rc=1 wall_s=1 peak_mib=2 prove_s= verified=0 err="thread 'tokio-rt-worker' (1470) panicked at sp1-gpu/crates/prover_components/src/builder.rs:38:9:" (stock server: "Unsupported GPU memory: 12, must be at least 24GB")
|
||||
RESULT proof_beside_comp26 mode=compressed thr=67108864 rc=1 wall_s=1 proof_window=1791472371..1791472372 miner_start=1791472359 prove_s= verified=0 miner_alive_after=yes err="thread 'tokio-rt-worker' (1811) panicked at sp1-gpu/crates/prover_components/src/builder.rs:38:9:" (stock server, as above)
|
||||
RESULT miner_beside_comp26 rc=0 wall_s=359 peak_mib=6128 check=PASS fingerprint=23ced07a4d28b465 mhs=18.837 self-test PASS
|
||||
RESULT cmd proof_beside_core25: env HOME=/opt/igneum-floor/home SP1_PROVER=cuda RUST_LOG=off SP1_GPU_FLOOR_LOG=1 SP1_GPU_ELEMENT_THRESHOLD=33554432 timeout 600 /opt/igneum-floor/bin-0317/igneum-prove-host /root/coex/fees-v1-shards2.json --mode core --shard 0 --out /root/coex/proof-beside-core25.json (started 1s+10s after the miner, miner resident mem_mib=6116)
|
||||
RESULT proof_beside_core25 mode=core thr=33554432 rc=1 wall_s=5 proof_window=1791472735..1791472740 miner_start=1791472724 prove_s= verified=0 miner_alive_after=yes err="thread 'tokio-rt-worker' (4670) panicked at slop/crates/tensor/src/inner.rs:51:51:"
|
||||
RESULT miner_beside_core25 rc=0 wall_s=358 peak_mib=7459 check=PASS fingerprint=23ced07a4d28b465 mhs=18.834 self-test PASS
|
||||
RESULT idle_after mem_mib=2
|
||||
RESULT window_hl-reg64c rc=0 wall_s=179 peak_mib=1508 regs=87 blocks_per_sm=20 check=PASS fingerprint=4e7cc25967eba280 mhs=9.508 self-test PASS
|
||||
RESULT window_hl-reg64 rc=0 wall_s=178 peak_mib=1508 regs=104 blocks_per_sm=16 check=PASS fingerprint=70e786af1a457653 mhs=9.574 self-test PASS
|
||||
```
|
||||
|
||||
The rerun on the patched server (run2.log):
|
||||
|
||||
```
|
||||
RESULT start 2026-10-08T15:31:00Z card=NVIDIAGeForceRTX4060 total_mib=8188 driver=570.211.01 power_limit_w=115.00
|
||||
RESULT bins worker=d43be4625b78baf7 host=71bc2438856bb141 server=882bd34f7f69dc97 fixture=20a108f159c61ff9
|
||||
RESULT cmd proof_alone: env HOME=/opt/igneum-floor/home SP1_PROVER=cuda RUST_LOG=off SP1_GPU_FLOOR_LOG=1 SP1_GPU_ELEMENT_THRESHOLD=67108864 timeout 900 /opt/igneum-floor/bin-0317/igneum-prove-host /root/coex/fees-v1-shards2.json --mode compressed --shard 0 --out /root/coex/proof-alone-rerun.json
|
||||
RESULT proof_alone_rerun rc=0 wall_s=15 peak_mib=7532 prove_s=8.2 verified=1 err=""
|
||||
RESULT proof_alone_rerun_line RESULT compressed shard 0: prove 8.2 s, proof 1272897 bytes, verify 0.036 s, VERIFIED; statement 0x6adcc7fab21512b58cfa3778756718fd37aea6cccb311e12d2ca33b5f4bf10c0 proof sha256 0x7b887af9426039e61345f11d2c9faf10d4b4d0d16e4850d10ee08c5fa7002
|
||||
RESULT cmd proof_beside_comp26: env HOME=/opt/igneum-floor/home SP1_PROVER=cuda RUST_LOG=off SP1_GPU_FLOOR_LOG=1 SP1_GPU_ELEMENT_THRESHOLD=67108864 timeout 600 /opt/igneum-floor/bin-0317/igneum-prove-host /root/coex/fees-v1-shards2.json --mode compressed --shard 0 --out /root/coex/proof-beside-comp26-rerun.json (started 3s+10s after the miner, miner resident mem_mib=6116)
|
||||
RESULT proof_beside_comp26 mode=compressed thr=67108864 rc=1 wall_s=5 proof_window=1791473509..1791473514 miner_start=1791473495 prove_s= verified=0 miner_alive_after=yes err="thread 'tokio-rt-worker' (10627) panicked at slop/crates/tensor/src/inner.rs:51:51:"
|
||||
RESULT miner_beside_comp26 rc=0 wall_s=359 peak_mib=7811 check=PASS fingerprint=23ced07a4d28b465 mhs=18.833 self-test PASS
|
||||
RESULT beside_fallback compressed 2^26 beside the miner did not verify (the core 2^25 beside row is in run.log)
|
||||
RESULT idle_after mem_mib=2
|
||||
```
|
||||
|
||||
## The table
|
||||
|
||||
| Card | Miner alone (ds55) | Proof alone (compressed 2^26) | Together | Register windows (v5 kit worker) |
|
||||
|---|---|---|---|---|
|
||||
| RTX 3060 12 GB | 26.82 MH/s at 117.4 W, 6,129 MiB resident, fingerprint 23ced07a4d28b465, PASS | 13.2 s, VERIFIED, peak 7,525 MiB, 122 W | 6,129 + 7,525 = 13,654 MiB against 12,288: TIME-SHARING NEEDED. Live attempt: the card filled to 11,893 MiB and the prover died in a device allocation (raw_buffer.rs:271) after 34 s; the miner held 26.51 MH/s through it (1.2 percent under alone). Proof with the miner paused = the proof-alone row | hl-reg64c 13.47 MH/s, 87 regs, 16 of 24 blocks per SM, fingerprint 4e7cc25967eba280 MATCH, 120.8 W; hl-reg64 13.48 MH/s, 104 regs, 16 of 24, 70e786af1a457653 MATCH, 119.3 W |
|
||||
| RTX 4060 8 GB | 18.84 MH/s, 6,116 MiB resident, fingerprint 23ced07a4d28b465, PASS (no watts: host sensor N/A) | 8.2 s, VERIFIED, peak 7,532 MiB | 6,116 + 7,532 = 13,648 MiB against 8,188: TIME-SHARING NEEDED. Live attempt: the server died in a tensor allocation (inner.rs:51) at 7,811 MiB after 5 s; the miner held 18.83 MH/s | hl-reg64c 9.51 MH/s, 87 regs, 20 of 24 blocks per SM, fingerprint MATCH; hl-reg64 9.57 MH/s, 104 regs, 16 of 24, MATCH |
|
||||
|
||||
Not measured and why: a core-only proof beside the miner (igneum-prove-host-0317 has no `--mode core`: "Error: unknown mode core"; its modes are native, execute, shard, compressed, block, all); watts on the 4060 (the host's power sensor reads N/A).
|
||||
|
||||
## Reconciliation with the served row
|
||||
|
||||
The served sentence (litepaper, "Proving", from `docs/analysis/prover-tiers-real-cards.md`, 6 October 2026) says an RTX 3060 (12 GB) "mines at 23.78 MH/s and proves the v1 shard beside its miner at an 8.9 GB peak in 37.5 s". Today's row on the same card tier reads a 7.5 GiB compressed peak that kills the prover beside a 6.1 GiB miner. The two are not in conflict; they measured different things. The 6 October row is the matrix's `miner-comp-26-v1` point (`tools/fleet/box-matrix.sh` section 7): the patched server at threshold 2^26 in compressed mode, driven by the matrix host (the segment host build at `/opt/igneum-segal/.../igneum-prove-host`, which also carries `--mode core`; the matrix's core rows come from it), beside `igneum-miner mine` on the 1 GiB class v3 pack, whose resident set was 1.4 GB: 1.4 + 7.5 = 8.9 GB, inside 12 GB, proof in 37.5 s with the miner running. Today's row is igneum-prove-host-0317 in compressed mode at the same threshold 2^26 (the same 7.5 GiB own footprint, 7,525 to 7,532 MiB peak alone), beside the ds55 miner on the 5.5 GiB dataset of the genesis floor, whose resident set is 6.1 GB: 6.1 + 7.5 = 13.6 GB, outside 12 GB and 8 GB alike, so the prover's allocation fails while the miner keeps mining. What changed between the rows is the miner's dataset (1 GiB then, 5.5 GiB now), not the prover. The rule that follows: at the 5.5 GiB floor a compressed shard proof beside a running miner needs a 16 GB card (13.6 GB together; the 16 GB tier's own peak is 18 GB on the stock sizes and 7.8 GB patched, so 16 GB holds both with about 2 GB spare); 8 GB and 12 GB cards time-share, proving with the miner paused (13.2 s on the 3060, 8.2 s on the 4060) and mining otherwise. The 6 October beside rows stand only for the 1 GiB dataset; the 6 October core-only beside rows (5.6 GB own on the 3060, 27.2 s) stand only for core mode on the segment host, which the 0317 host does not expose, and are not a coexistence claim at the floor. The served sentence is read as a 1 GiB-dataset row until the site lane rewrites it against this record.
|
||||
299
docs/analysis/class-v6/coexistence-model.md
Normal file
299
docs/analysis/class-v6/coexistence-model.md
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
# The coexistence model: can a specialised supplier earn a normal return while GPUs stay close enough to compete?
|
||||
|
||||
8 October 2026, 16:1x to 16:5x UK, branch `class-v6-floor-sram`, floor lane 3, on the research lane's word of 17:0x UK
|
||||
(the founder's accepted third external review: the capex wall is replaced by a coexistence model as the economic
|
||||
argument, with the profitability surface of `docs/analysis/class-v6/floor/sram-and-floor.md` section 4.4 as its base).
|
||||
First run on today's measured rows. **Every row is modelled**: the arithmetic is `scratchpad/coexist.py` run on
|
||||
build-3; the card rows are lane 4's class v5 table (`docs/analysis/class-v6/floor/denominator.md`, section 10.4 of
|
||||
the design document: measured at the floor where it says so, modelled knees elsewhere), the chip rows the chip model's
|
||||
(chip-model-v3 5.5 and 5.12, lane B, the k lane's 3.2 pJ per forced op), the prices street approximations. Nothing
|
||||
here is served; nothing is a measurement of a chip. The founder is not named.
|
||||
|
||||
The statement under test. **Success**: a specialised supplier earns a normal return and GPUs stay close enough in total
|
||||
cost, obtainable and useful outside mining, that entrants still compete. **Failure**: a supplier operating privately at
|
||||
much lower cost exhausts competitors' margins. The result is the set of conditions under which the success statement
|
||||
holds, never a level the chain stays below. The development cost is SUNK in the mandatory case (the opponent covers
|
||||
manufacturing, deployment and operation only); the paid-development cases sit beside it.
|
||||
|
||||
## 0. The result in one page
|
||||
|
||||
1. **On today's rows the N2 SRAM die fails the success statement in every scenario where its owner can buy a fleet
|
||||
worth a few percent of the chain's yearly miner revenue.** With development sunk, a 3-year life and power at USD
|
||||
0.06 per kWh, the die's all-in cost per accepted MH/s-hour is 72 micro-USD against the best GPU owner's 156 to 204
|
||||
at the same electricity (the 5070 Ti and 5080 at their knees, hardware sunk) and 415 to 588 for a new entrant: 2.2x
|
||||
to 2.8x on the existing owner, 5.8x to 8.2x on the entrant. At the GPU's more likely 0.12 per kWh it is 3.6x to
|
||||
4.6x and 7.2x to 9.9x; at 0.25, 6.8x to 8.5x and 10x to 14x. A USD 10 M fleet of such dies takes 37 to 73 percent
|
||||
of the chain's hash the year it lands at IGN 0.10 to 0.20 and 100 percent the year after on a flat price; a USD
|
||||
100 M fleet takes 100 percent on landing in every path and then runs at a loss because it is larger than the
|
||||
revenue (the self-limiting point: -1 to -300 percent margins).
|
||||
2. **The GDDR7 board chip passes the success statement on its 1-year life and fails it on its 3-year life.** At 1 year
|
||||
its cost per accepted unit (750 micro-USD at 0.06) is ABOVE every Blackwell owner's and above the Blackwell
|
||||
entrant's at 0.06 to 0.12 (0.8x to 1.0x the 5080 entrant, 0.6x to 0.7x the 5070 Ti), so it competes only against
|
||||
Ada and Ampere at dear electricity; at 3 years (307 micro-USD) it is 1.9x to 2.3x the Blackwell entrant and 0.9x
|
||||
to 1.1x the Blackwell owner: GPUs at their knee stay close enough. Its hardware is the term that holds it (USD 5.6
|
||||
per MH/s with the core and the system against the die's 0.8), not its joules.
|
||||
3. **The electricity axis is explicit and it is the GPU's.** The break-even electricity price above which an EXISTING
|
||||
GPU owner with sunk hardware cannot match the die at 0.06 is 0 to 5 cents per kWh for every card (a 3-year die) and
|
||||
1 to 5 cents (a 1-year die): no grid price in the world keeps a GPU owner level with a sunk SRAM die. Against the
|
||||
GDDR7 board at 3 years the owner's break-even is 9 to 15 cents on Blackwell, 4 to 6 on Ada and Ampere; at 1 year 27
|
||||
to 40 cents on Blackwell. Read the other way, the die breaks even against a 5080 owner at 0.12 only when the die
|
||||
pays 47 to 60 cents per kWh; the board at 3 years when it pays 1 to 9 cents.
|
||||
4. **Accessible supply is the structural fact.** At the GPU entry equilibrium (revenue per MH/s-hour equal to a new
|
||||
5070 Ti's cost at 0.12) the chain's hash is 2.6 TH/s at IGN 0.03, 8.8 at 0.10, 26 at 0.30, 88 at 1.00: that is
|
||||
34,000 to 1.1 M 5070 Ti-class cards, which exist in the world's installed base, against 480 to 16,000 SRAM dies, 8
|
||||
to 270 wafers of N2. One supplier holds the chain at every price in the window; the dependence on individual
|
||||
suppliers is total for the die and partial for the board (16,000 to 530,000 boards, a Bitmain-class run).
|
||||
5. **The conditions under which the success statement holds**, read off the tables: (a) the chip's all-in cost per
|
||||
accepted unit stays within about 1.5x of the best GPU owner's at the same electricity, which on today's rows is true
|
||||
of the GDDR7 board at a life of 1 to 2 years and false of the die at every life; (b) the chip's hardware per MH/s is
|
||||
not below about a quarter of the GPU's annualised hardware (the board's 0.7x to 1.9x the 5080 entrant passes, the
|
||||
die's 5x to 14x fails); (c) no single buyer can fund a fleet above about a third of the chain's hash for under a
|
||||
year's miner revenue (true for the board above IGN 0.3; false for the die at every price: a wafer is USD 30,000);
|
||||
(d) GPUs keep a resale market and a use outside mining (true for every card in the population; the chip has none,
|
||||
which is why its life is the axis that moves everything); (e) the per-joule gap at the honest knee stays under about
|
||||
3x (the board at 2.2x to 2.6x against Blackwell passes; the die at 3.7x to 4.5x does not).
|
||||
6. **What the chain controls, and what it does not.** It controls the honest cost per accepted unit (the operating
|
||||
point: the lock is worth 34 to 41 percent of a Blackwell card's draw), the chip's life against a fixed lane (the
|
||||
180-day rotation; nothing against a programmable one), and the visibility of a concentrated supplier (the share
|
||||
detector). It does not control the sunk development cost, electricity prices, the token price or a buyer's budget.
|
||||
On today's rows the SRAM die, once it exists, cannot be held to coexistence by anything in the hash; the board can.
|
||||
The condition that holds the die is that it does not get built, which is the surface of section 4.4 (a USD 150 M
|
||||
project at a third of the chain needs IGN 0.73 over three years), and that is a statement about who pays, not about
|
||||
the chain staying below a level.
|
||||
|
||||
## 1. The measure: cost per accepted unit of work
|
||||
|
||||
Cost per accepted MH/s-hour (micro-USD), both sides: annualised hardware plus power plus hosting, failures and fees,
|
||||
divided by accepted work. Accepted work is 97 percent of raw (rejects 0.5 percent, downtime 2.0, epoch preparation and
|
||||
propagation 0.5; approximate from the devnet's share rates), the pool fee 1 percent. The two GPU situations: the
|
||||
**existing owner** (hardware sunk; pays power, a wear allowance of 5 percent of the used price a year, fees; the
|
||||
alternative use is the card's rental yield, reported in section 2 and not deducted) and the **new entrant** (buys new
|
||||
or used, operates two years, resells at the table's fraction). Hosting: 0 at home, USD 0.02 per kWh-equivalent at a
|
||||
farm (the chip's case). Electricity axis: USD 0.06, 0.12, 0.25 per kWh.
|
||||
|
||||
| Input | Value | Label |
|
||||
|---|---|---|
|
||||
| Card joules per hash at the floor (the knee with the knobs) and at stock | lane 4's class v5 table: 5090 2.33 / 3.48, 5080 2.06 / 3.48, 5070 Ti 1.70 / 2.84, 5070 1.75 / 2.99, 5060 Ti 2.36 / 4.02, 5060 2.21 / 3.76, 4090 3.58 / 5.00, 4080 3.51 / 4.92, 4070 3.58 / 5.82, 4060 Ti 3.81 / 5.32, 3090 4.51 / 5.03, 3080 4.20 / 4.54, 3060 5.77 / 6.40, 9070 XT 7.90 / 10.7, H100 2.00 / 2.58, A100 2.89 / 2.99, M5 Max 1.40 microjoules | measured where lane 4 says so (5090, 5080, 4070, 9070 XT, M5 Max floors; most stock rows), modelled knees elsewhere |
|
||||
| Card rates at the floor | 5090 134.8, 5080 71.2, 5070 Ti 77, 5070 41, 5060 Ti 19, 5060 17, 4090 58, 4080 45, 4070 31.1, 4060 Ti 17.6, 3090 37.8, 3080 40.8, 3060 23.8, 9070 XT 18.9, H100 90, A100 60, M5 Max 27.1 MH/s | measured where the bench table has the row; approximate elsewhere |
|
||||
| Prices new / used, resale after two years | 5090 2,600 / 2,200 / 55 percent; 5080 1,100 / 900 / 50; 5070 Ti 800 / 650 / 50; 5070 560 / 450 / 50; 5060 Ti 450 / 360 / 45; 5060 310 / 250 / 45; 4090 1,700 / 1,300 / 45; 4080 1,000 / 700 / 40; 4070 550 / 400 / 40; 4060 Ti 420 / 290 / 35; 3090 900 / 650 / 30; 3080 450 / 330 / 25; 3060 260 / 190 / 25; 9070 XT 650 / 520 / 45; H100 25,000 / 18,000 / 50; A100 10,000 / 6,000 / 35; M5 Max 4,000 / 3,200 / 55 | approximate street, October 2026 |
|
||||
| Chip joules per hash (class v5, the shadow at 3.2 pJ per forced op, lane 4's chip columns) | GDDR7 board with an N5 core 0.79; HBM3 one stack 0.65; N2 SRAM die with the core 0.46 (W = 4); the die at the bare-lane floor 0.165 | modelled |
|
||||
| Chip hardware, USD per MH/s, silicon and board plus 30 percent system (PSU, chassis, cooling) | GDDR7 board 5.6 (4.3 with the core die, chip-model 5.5 and research 16.1); HBM3 8.6; SRAM die 0.8 (0.6 with the board, lane B) | modelled, approximate |
|
||||
| Chip failures, resale | 3 percent a year; no resale (single use) | approximate |
|
||||
| Chip lives | 0.5, 1, 2, 3, 5 years (0.5 is the fixed-lane chip under the 180-day rotation; 3 the programmable chip's default) | the review's axis |
|
||||
| Emission to miners | 0.77 / 0.80 / 0.40 / 0.40 / 0.20 B IGN in years 1 to 5 (the spec's constant, 80 percent to miners) | spec 05 |
|
||||
| GPU equilibrium | while any GPU mines, revenue per MH/s-hour settles at the cheapest entrant's cost (a new 5070 Ti at 0.12: 521 micro-USD) during growth and falls to the owners' costs during shrinkage; a GPU generation at year 3 cuts the entrant's cost 25 percent (1.5x per joule at the same price) with the old card resold at the table's fraction | modelled rule |
|
||||
|
||||
## 2. The GPU reference population: cost per accepted MH/s-hour (micro-USD)
|
||||
|
||||
| Card | Owner at 0.06 / 0.12 / 0.25 | Entrant, new, 2 years, at 0.06 / 0.12 / 0.25 | Entrant, used | Hardware share of the entrant's cost at 0.12 | Wh per MH/s-hour | Alternative use (rental yield, approximate) | Label |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| RTX 5090 | 243 / 388 / 704 | 661 / 807 / 1,122 | 800 / 945 / 1,261 | 64 percent | 2.33 | USD 0.3 to 0.5 an hour on a rental market: 2,200 to 3,700 micro-USD per MH/s-hour, 3x to 5x its mining cost | measured floor |
|
||||
| RTX 5080 | 204 / 332 / 611 | 588 / 716 / 995 | 650 / 779 / 1,058 | 64 | 2.06 | USD 0.15 to 0.25 an hour | measured floor |
|
||||
| RTX 5070 Ti | 156 / 263 / 493 | 415 / 521 / 751 | 453 / 560 / 790 | 59 | 1.70 | USD 0.1 to 0.2 an hour | modelled knee |
|
||||
| RTX 5070 | 175 / 284 / 521 | 515 / 624 / 861 | 558 / 668 / 905 | 65 | 1.75 | | modelled knee |
|
||||
| RTX 5060 Ti 16 GB | 260 / 407 / 727 | 921 / 1,069 / 1,388 | 956 / 1,104 / 1,423 | 72 | 2.36 | | modelled knee |
|
||||
| RTX 5060 | 225 / 364 / 663 | 734 / 872 / 1,171 | 768 / 906 / 1,205 | 68 | 2.21 | | modelled knee |
|
||||
| RTX 4090 | 357 / 580 / 1,065 | 1,181 / 1,405 / 1,890 | 1,163 / 1,387 / 1,872 | 68 | 3.58 | USD 0.3 to 0.4 an hour | modelled knee, stock measured |
|
||||
| RTX 4080 | 312 / 531 / 1,006 | 1,011 / 1,231 / 1,706 | 879 / 1,099 / 1,574 | 64 | 3.51 | | modelled knee |
|
||||
| RTX 4070 | 300 / 524 / 1,008 | 854 / 1,078 / 1,562 | 778 / 1,001 / 1,486 | 58 | 3.58 | | measured tune |
|
||||
| RTX 4060 Ti 16 GB | 336 / 574 / 1,090 | 1,159 / 1,397 / 1,913 | 969 / 1,207 / 1,723 | 66 | 3.81 | | modelled knee |
|
||||
| RTX 3090 (used) | 384 / 666 / 1,276 | 1,272 / 1,554 / 2,164 | 1,091 / 1,373 / 1,983 | 64 | 4.51 | | modelled cap |
|
||||
| RTX 3080 (used) | 310 / 573 / 1,141 | 754 / 1,016 / 1,585 | 661 / 923 / 1,492 | 48 | 4.20 | | modelled cap |
|
||||
| RTX 3060 (used) | 408 / 768 / 1,550 | 847 / 1,208 / 1,989 | 754 / 1,114 / 1,895 | 40 | 5.77 | | modelled cap |
|
||||
| RX 9070 XT | 657 / 1,151 / 2,220 | 1,617 / 2,111 / 3,180 | 1,668 / 2,162 / 3,231 | 53 | 7.90 | | measured |
|
||||
| H100 (hosted) | 1,313 / 1,438 / 1,709 | 8,374 / 8,499 / 8,770 | 7,880 / 8,004 / 8,275 | 97 | 2.00 | USD 2 to 3 an hour: never mines | modelled lock |
|
||||
| A100 (used) | 775 / 955 / 1,346 | 6,615 / 6,796 / 7,187 | 4,388 / 4,568 / 4,960 | 95 | 2.89 | USD 1 an hour: never mines | modelled |
|
||||
| Apple M5 Max (reported, not headlined) | 789 / 876 / 1,066 | 4,033 / 4,120 / 4,310 | 4,690 / 4,778 / 4,967 | 96 | 1.40 | a workstation: mines only as an owner | measured |
|
||||
|
||||
Reading: the owner's cost is 60 to 70 percent electricity on every consumer card, so the electricity price is the
|
||||
GPU's whole variable; the entrant's cost is 60 to 70 percent hardware, so the card price and its resale are the
|
||||
entrant's whole variable. The best honest owner on today's rows is a 5070 Ti at its knee (156 micro-USD at 0.06); the
|
||||
best entrant the same card (415). Datacentre parts and the Mac never enter as entrants (hardware 95 percent) and mine
|
||||
only as owners with nothing better to do, which their rental yields say they always have.
|
||||
|
||||
## 3. The chip rows
|
||||
|
||||
### 3.1 Development sunk (the mandatory case): cost per accepted MH/s-hour, micro-USD, farm hosting
|
||||
|
||||
| Chip | Life 0.5 y at 0.06 / 0.12 / 0.25 | 1 y | 2 y | 3 y | 5 y | Hardware / power split at 0.06, 3 y | Label |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| GDDR7 board with an N5 core | 1,414 / 1,463 / 1,570 | 750 / 799 / 906 | 418 / 467 / 574 | 307 / 356 / 463 | 219 / 268 / 375 | 239 / 65 | modelled |
|
||||
| HBM3 one stack with a core | 2,123 / 2,164 / 2,252 | 1,104 / 1,145 / 1,233 | 594 / 635 / 723 | 425 / 465 / 553 | 289 / 329 / 417 | 367 / 54 | modelled |
|
||||
| N2 SRAM die with the core | 226 / 255 / 317 | 134 / 163 / 225 | 87 / 116 / 178 | **72 / 101 / 163** | 60 / 88 / 151 | 33 / 38 | modelled |
|
||||
| N2 SRAM die at the bare-lane floor | 202 / 212 / 235 | 109 / 120 / 142 | 63 / 73 / 96 | 47 / 58 / 80 | 35 / 45 / 68 | 33 / 14 | modelled, the worst case |
|
||||
|
||||
### 3.2 Development paid: the same rows with `C_dev` spread over the fleet and the life
|
||||
|
||||
The fleet is sized to a share `q` of the network's hash at the GPU equilibrium (a new 5080 at 0.12 as the marginal
|
||||
entrant). All-in cost per accepted MH/s-hour of the SRAM die (the GDDR7 board in brackets), at 0.06:
|
||||
|
||||
| IGN price | Miner revenue (year 3) | Network hash | `C_dev` 20 M, 1 y, `q` 0.3 / 1.0 | 20 M, 3 y, 0.3 / 1.0 | 150 M, 1 y, 0.3 / 1.0 | 150 M, 3 y, 0.3 / 1.0 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 0.03 | USD 12 M | 1.9 TH/s | 4,277 / 1,377 (4,893 / 1,993) | 1,453 / 486 (1,688 / 721) | 31,211 / 9,457 | 10,431 / 3,180 |
|
||||
| 0.10 | 40 M | 6.4 TH/s | 1,377 / 507 (1,993 / 1,123) | 486 / 196 (721 / 431) | 9,457 / 2,931 | 3,180 / 1,004 |
|
||||
| 0.30 | 120 M | 19 TH/s | 548 / 258 (1,164 / 874) | 210 / 113 (445 / 349) | 3,242 / 1,066 | 1,108 / 383 |
|
||||
| 1.00 | 400 M | 64 TH/s | 258 / 171 (874 / 787) | 113 / 84 (349 / 320) | 1,066 / 414 | 383 / 165 |
|
||||
|
||||
Reading: a paid development cost puts every chip ABOVE the best GPU entrant (415 to 588) at IGN 0.10 and below, and
|
||||
the SRAM die below it only from IGN 0.30 on a 3-year life or IGN 1.00 on a 1-year life; the GDDR7 board with paid
|
||||
development is never below the Blackwell entrant inside the window. The sunk case is therefore the whole threat, and
|
||||
it is the case the review makes mandatory. The derivative design (a revision at 0.3 x `C_dev`, section 4.4 of the
|
||||
floor file) moves the paid rows a third of the way to the sunk rows.
|
||||
|
||||
## 4. The cost advantage, separated
|
||||
|
||||
The chip at 0.06 per kWh and farm hosting; the GPU at 0.06, 0.12 and 0.25. "Operating" is power only (joules times
|
||||
electricity); "hardware" the annualised hardware of the GPU entrant over the chip's.
|
||||
|
||||
| Chip, life | Against | Total: owner / entrant, at 0.06 | At 0.12 | At 0.25 | Operating advantage at 0.06 / 0.12 / 0.25 (joules x electricity) | Hardware advantage |
|
||||
|---|---|---|---|---|---|---|
|
||||
| GDDR7 board, 1 y | RTX 5080 | 0.3x / 0.8x | 0.4x / 1.0x | 0.8x / 1.3x | 2.6x / 5.2x / 10.9x (2.6 x 1, 2, 4.2) | 0.7x |
|
||||
| | RTX 5070 Ti | 0.2x / 0.6x | 0.4x / 0.7x | 0.7x / 1.0x | 2.2x / 4.3x / 9.0x | 0.5x |
|
||||
| | RTX 4090 | 0.5x / 1.6x | 0.8x / 1.9x | 1.4x / 2.5x | 4.5x / 9.1x / 18.9x | 1.4x |
|
||||
| | RTX 3080 (used) | 0.4x / 1.0x | 0.8x / 1.4x | 1.5x / 2.1x | 5.3x / 10.6x / 22.2x | 0.7x |
|
||||
| GDDR7 board, 3 y | RTX 5080 | 0.7x / 1.9x | 1.1x / 2.3x | 2.0x / 3.2x | the same | 1.9x |
|
||||
| | RTX 5070 Ti | 0.5x / 1.4x | 0.9x / 1.7x | 1.6x / 2.4x | | 1.3x |
|
||||
| | RTX 4090 | 1.2x / 3.8x | 1.9x / 4.6x | 3.5x / 6.2x | | 4.0x |
|
||||
| | RTX 3080 (used) | 1.0x / 2.5x | 1.9x / 3.3x | 3.7x / 5.2x | | 2.1x |
|
||||
| N2 SRAM die, 1 y | RTX 5080 | 1.5x / 4.4x | 2.5x / 5.4x | 4.6x / 7.4x | 4.5x / 9.0x / 18.7x | 4.9x |
|
||||
| | RTX 5070 Ti | 1.2x / 3.1x | 2.0x / 3.9x | 3.7x / 5.6x | 3.7x / 7.4x / 15.4x | 3.3x |
|
||||
| | RTX 4090 | 2.7x / 8.8x | 4.3x / 10.5x | 8.0x / 14.1x | 7.8x / 15.6x / 32.4x | 10.1x |
|
||||
| N2 SRAM die, 3 y | RTX 5080 | 2.8x / 8.2x | 4.6x / 9.9x | 8.5x / 13.8x | the same | 13.8x |
|
||||
| | RTX 5070 Ti | **2.2x / 5.8x** | **3.6x / 7.2x** | 6.8x / 10.4x | | 9.3x |
|
||||
| | RTX 4090 | 5.0x / 16.4x | 8.1x / 19.5x | 14.8x / 26.2x | | 28.7x |
|
||||
| | RTX 3080 (used) | 4.3x / 10.5x | 8.0x / 14.1x | 15.9x / 22.0x | | 14.7x |
|
||||
| | Apple M5 Max (reported) | 11.0x / 56x | 12.2x / 57x | 14.8x / 60x | 3.0x / 6.1x / 12.7x | 118x |
|
||||
|
||||
Reading: the electricity axis multiplies the operating advantage one for one (a 2.6x chip at 0.06 against a GPU at
|
||||
0.25 is 10.9x on power alone, the review's point), but on the consumer cards power is 60 to 70 percent of the owner's
|
||||
cost and 30 to 40 percent of the entrant's, so the total advantage is a third to a half of the operating one. The
|
||||
GDDR7 board's total advantage over a Blackwell card at its knee is under 1x (owner) to 2.3x (entrant) across the whole
|
||||
electricity axis at a 3-year life, which is the coexistence band; the die's is 2.2x to 14x, which is not.
|
||||
|
||||
## 5. The replacement economics
|
||||
|
||||
For an existing GPU owner, switching pays when the chip's all-in cost per accepted unit is below the owner's
|
||||
OPERATING cost (the hardware is sunk, the resale value is the only thing the switch recovers). For a new entrant, when
|
||||
the chip's all-in is below the GPU entrant's all-in. The chip must be purchasable for either (hardware sales; the
|
||||
manufacturer keeps about half the operator's profit through the price, floor file 4.4 table B, which roughly doubles
|
||||
the chip's hardware term for the buyer).
|
||||
|
||||
| Who | Against the GDDR7 board (bought, hardware term x2) | Against the SRAM die (bought, x2) | What it means |
|
||||
|---|---|---|---|
|
||||
| A Blackwell owner at 0.06 to 0.12 | never switches: the bought board at 3 years is 550 to 600 micro-USD against the owner's 156 to 332 | switches at 0.12 (the bought die 105 to 134 against 263 to 332) and is near indifferent at 0.06 (105 against 156 to 204) | the die replaces Blackwell owners at normal grid prices; the board never does |
|
||||
| An Ada or Ampere owner at 0.12 | near indifferent at 3 years (550 to 600 against 524 to 768); switches at 0.25 | switches at every electricity price | the board retires the oldest cards only at dear electricity, which the generation upgrade does anyway |
|
||||
| A new entrant choosing between a new 5070 Ti and a bought chip at 0.12 | the board at 3 years (about 600) is 1.15x the card's 521: the card wins; at 1 year the card wins 2x | the bought die (134) is 0.26x the card: the die wins 4x | an entrant market with a bought SRAM die has no GPU entrants; one with a bought board keeps them |
|
||||
| The GPU generation upgrade (year 3: 1.5x per joule at the same price, the old card resold) | cuts the entrant's cost about 25 percent and the owner's power 33 percent: the board at 3 years then reads 1.5x to 1.7x the new entrant, still in band | the die's advantage falls 25 to 33 percent, from 7x to 5x on the entrant: still out of band | the GPU side's own curve narrows the board's gap to nothing by the second generation and never closes the die's |
|
||||
|
||||
## 6. Break-even electricity prices
|
||||
|
||||
(a) The GPU electricity price above which an EXISTING owner (hardware sunk) cannot match the chip's all-in cost at
|
||||
0.06 per kWh:
|
||||
|
||||
| Chip, life | 5090 | 5080 | 5070 Ti | 5070 | 5060 Ti | 4090 | 4080 | 4070 | 3090 | 3080 | 3060 | 9070 XT | M5 Max |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| GDDR7 board, 1 y | 27 c | 32 c | 40 c | 38 c | 26 c | 17 c | 18 c | 18 c | 14 c | 16 c | 12 c | 7 c | 3 c |
|
||||
| GDDR7 board, 3 y | 9 c | 11 c | 15 c | 13 c | 8 c | 5 c | 6 c | 6 c | 4 c | 6 c | 4 c | 2 c | under 0 |
|
||||
| SRAM die, 1 y | 1.5 c | 2.7 c | 4.7 c | 3.8 c | 0.9 c | 0 c | 1.1 c | 1.5 c | 0.7 c | 2.0 c | 1.4 c | under 0 | under 0 |
|
||||
| SRAM die, 3 y | under 0 | under 0 | 1.2 c | 0.4 c | under 0 | under 0 | under 0 | under 0 | under 0 | 0.5 c | 0.4 c | under 0 | under 0 |
|
||||
|
||||
(b) The chip electricity price at which its all-in cost equals a GPU owner's at 0.12 (how much dearer the chip's
|
||||
hosting can be and still match): the GDDR7 board at 3 years 1 to 9 cents against Blackwell (it must be hosted cheaper
|
||||
than the GPU to match an owner) and 75 cents against the Mac; at 1 year it cannot match a Blackwell owner at any
|
||||
price. The SRAM die matches a 5070 Ti owner while paying up to 46 cents (3 years) or 33 cents (1 year), a 5080 owner
|
||||
up to 60 or 47, the Mac up to 174.
|
||||
|
||||
Reading: the board lives or dies on the GPU's grid price, which is the review's electricity axis doing what it should;
|
||||
the die does not see the axis at all.
|
||||
|
||||
## 7. Five years: growing, flat and shrinking networks, with the GPU side reacting
|
||||
|
||||
The sunk chip enters at the start of year 2 with a fleet bought for a budget `B` (USD 1 M, 10 M, 100 M at USD 0.8
|
||||
per MH/s for the die: 1.3, 13 and 128 TH/s); revenue per MH/s-hour `r` settles at the cheapest GPU entrant's cost
|
||||
(a new 5070 Ti at 0.12, 521 micro-USD; 391 after the year-3 generation) while entry continues, and when the chip's
|
||||
fleet alone exceeds the hash that revenue supports, GPU owners exit in cost order until the survivors' costs are
|
||||
covered or none are. Three price paths: growing (x2 a year from 0.10), flat (0.10), shrinking (x0.5 a year from
|
||||
0.30). Emission halves in year 3 and year 5.
|
||||
|
||||
| Path, budget | Year 2 | Year 3 | Year 5 | Reading |
|
||||
|---|---|---|---|---|
|
||||
| Growing, USD 1 M | chip 3.7 percent of 35 TH/s; `r` 521; margin 86 percent | 2.7 percent; owners all above water | 1.4 percent of 93 TH/s | coexistence: the supplier earns 82 to 86 percent margins on a tiny share, GPUs set the price |
|
||||
| Growing, USD 10 M | 37 percent of 35 TH/s | 27 percent | 14 percent | coexistence by dilution only: the share falls as the chain grows and the fleet is fixed; the supplier's margin stays 82 to 86 percent, far above normal |
|
||||
| Growing, USD 100 M | 100 percent; 0 of 17 owner classes above water; `r` falls to 142; margin 49 percent | the same | 100 percent; 2 of 17 owner classes above water at `r` 285 | failure: one buyer holds the chain for five years, GPUs exit in year 2 and only the two best classes could return in year 4 |
|
||||
| Flat, USD 1 M | 7 percent of 17.5 TH/s | 11 percent | 22 percent of 5.8 TH/s | coexistence, the share rising with each halving |
|
||||
| Flat, USD 10 M | 73 percent | 100 percent; 3 of 17 owner classes above water | 100 percent; 0 of 17 | failure by year 3: the halving does the rest |
|
||||
| Flat, USD 100 M | 100 percent; margin -1 percent | margin -102 percent | -305 percent | failure for both: the fleet is larger than the revenue; the buyer loses money and the GPUs are gone (the self-limiting point) |
|
||||
| Shrinking, USD 1 M | 5 percent | 15 percent | 100 percent; 3 of 17 above water | failure in year 5 at USD 4 M of revenue: even a USD 1 M fleet is the chain when the chain is small |
|
||||
| Shrinking, USD 10 M | 49 percent | 100 percent; 1 of 17 | 100 percent; margin -116 percent | failure from year 3 |
|
||||
| The GDDR7 board (sunk), flat, USD 10 M | 10 percent of 17.5 TH/s; margin 41 percent | 15 percent; margin 21 percent | 31 percent; margin 21 percent | coexistence: a normal return (21 to 41 percent gross) on a minority share with GPU entrants still setting the price |
|
||||
|
||||
Reading: the row that passes the success statement as the review states it is the GDDR7 board at a sunk USD 10 M (a
|
||||
21 to 41 percent gross margin, a 10 to 31 percent share, GPUs setting the price, entrants competing). The SRAM die
|
||||
passes only at a fleet under about 1 percent of the chain's yearly miner revenue in a growing network, and fails in
|
||||
every flat or shrinking path by year 3 to 5; above a tenth of a year's revenue it takes the chain in every path. The
|
||||
failure is not a margin the die extracts (its margins collapse once it is the chain); it is the exit of every GPU
|
||||
class, which is the review's definition.
|
||||
|
||||
## 8. Accessible supply and the dependence on individual suppliers
|
||||
|
||||
| IGN price | Network hash at the GPU entry equilibrium | In 5070 Ti-class cards | In 5090s | In N2 SRAM dies (5.5 GH/s) | In N2 wafers (about 60 good dies) | In GDDR7 boards (166 MH/s) |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 0.03 | 2.6 TH/s | 34,000 | 19,500 | 480 | 8 | 15,800 |
|
||||
| 0.10 | 8.8 | 114,000 | 65,000 | 1,600 | 27 | 52,800 |
|
||||
| 0.30 | 26 | 341,000 | 195,000 | 4,800 | 80 | 158,000 |
|
||||
| 1.00 | 88 | 1.1 M | 650,000 | 16,000 | 270 | 528,000 |
|
||||
| 3.00 | 263 | 3.4 M | 1.9 M | 48,000 | 800 | 1.6 M |
|
||||
|
||||
The GPU side's accessible supply is the installed base of consumer cards (tens of millions of Ampere, Ada and
|
||||
Blackwell cards in the world, approximate) and the used market, with a use outside mining and a resale price that the
|
||||
population table carries; at every price in the window the hash the chain needs is under 4 percent of one
|
||||
generation's shipments. The die's supply is one supplier's wafer allocation (8 to 800 wafers of a node booked to 2028,
|
||||
claimed); the board's is a Bitmain-class production run (16,000 to 1.6 M units) with a commodity memory bill. The
|
||||
dependence on individual suppliers is total for the die at every price (one order holds the chain), partial for the
|
||||
board (a run of that size is visible and takes months), and nil for the GPU side.
|
||||
|
||||
## 9. The conditions under which the success statement holds (the result)
|
||||
|
||||
Success (a specialised supplier earns a normal return and GPUs stay close enough in total cost, obtainable and useful
|
||||
outside mining, that entrants still compete) holds on today's rows when ALL of the following do:
|
||||
|
||||
| Condition | The number on today's rows | GDDR7 board | SRAM die |
|
||||
|---|---|---|---|
|
||||
| (a) the chip's all-in cost per accepted unit at its own electricity is within about 1.5x of the best GPU owner's at the GPU's electricity | the owner at 0.12: 263 to 332 micro-USD (5070 Ti, 5080) | 307 at 3 years, 750 at 1 year: PASSES | 72 to 134: FAILS at every life |
|
||||
| (b) the chip's annualised hardware per MH/s is not below about a quarter of the GPU entrant's | the 5080 entrant's hardware 460 micro-USD | 239 to 677: PASSES | 33 to 95: FAILS |
|
||||
| (c) a fleet above a third of the chain's hash costs more than a year's miner revenue | at IGN 0.10 the chain is 8.8 TH/s; a third is 2.9 TH/s | USD 16 M of boards against USD 40 to 80 M of revenue: PASSES above IGN 0.05 | USD 2.3 M of dies: FAILS at every price under about 3 |
|
||||
| (d) GPUs keep a resale market and a use outside mining | every card in the population resells at 25 to 55 percent after two years and rents at 3x to 5x its mining cost | PASSES (the GPU side's property) | PASSES (the same) |
|
||||
| (e) the per-joule gap at the honest knee stays under about 3x | Blackwell at the knee 1.70 to 2.06 microjoules | 2.2x to 2.6x: PASSES | 3.7x to 4.5x: FAILS (the bare-lane floor 10x to 12x) |
|
||||
| (f) the supplier's gross margin on the share it holds is a normal return (under about 50 percent) and does not rise with each halving | | 21 to 41 percent in the five-year run: PASSES | 82 to 86 percent, or a loss once it is the chain: FAILS |
|
||||
|
||||
So: **the success statement holds for the stored-dataset DRAM-board chip at a 1 to 3 year life on today's rows, in
|
||||
every price path, at every electricity price on the axis, with the GPU side's own generation curve narrowing the gap
|
||||
further; it does not hold for the N2 SRAM die once that die exists with its development sunk, at any life, price path
|
||||
or electricity price, and the only condition that holds the die is that nobody pays to build it** (the surface of the
|
||||
floor file's section 4.4: a USD 150 M project at a third of the chain needs IGN 0.73 over three years, a maker who
|
||||
takes the chain 0.22, a revision 0.12 to 0.16). That last is a statement about an investor's decision, not about the
|
||||
chain staying below a level, and the review is right that it is the only honest form.
|
||||
|
||||
What the chain can do about the die, from the model: raise the honest side's efficiency (every cent of GPU electricity
|
||||
and every point of the knee moves condition (a) and (e); the lock already moves a Blackwell card 34 to 41 percent), keep
|
||||
the share detector as the instrument that makes condition (c) visible the week it fails, and keep the dataset floor as
|
||||
the ticket (section 3.2 of the floor file: USD 1,500 to 3,000 per die at the schedule, which moves condition (c) by 2x
|
||||
to 4x and nothing else). Nothing in the hash moves conditions (a), (b) or (e) for the die by the factor they need.
|
||||
|
||||
## 10. Unverified and owed
|
||||
|
||||
- Every chip-side figure is modelled; no chip has been measured. The chip's hardware per MH/s (USD 0.8 for the die
|
||||
with the system, 5.6 for the board) is the term conditions (b) and (c) rest on and is approximate within 2x.
|
||||
- The card prices are street approximations of October 2026; the 5090's street price has been 2x MSRP this year, which
|
||||
moves the entrant rows for that card by up to 2x and no other row.
|
||||
- The Ada and Ampere knees are modelled (no rented host allows the lock); the Blackwell floors are measured on the 5090,
|
||||
5080 and 4070, modelled on the 5070 Ti, 5070 and 5060 class.
|
||||
- The accepted-work factor (97 percent), the wear allowance (5 percent a year), the hosting (USD 0.02 per kWh) and the
|
||||
rental yields are approximate.
|
||||
- The five-year run's GPU reaction is a single-rule model (entry at the cheapest entrant's cost, exit in cost order);
|
||||
a second cut adds a per-class supply curve, the installed base as a cap on entry, re-entry on a price rise, and the
|
||||
halving's effect on the entrant cost through used-card prices.
|
||||
- The derivative-design rows (a revision at 0.3 x `C_dev`) are carried from the floor file's surface and not re-run
|
||||
here; the sunk case bounds them.
|
||||
- The emission beyond year 5 and the proving pool's 20 percent are outside the run.
|
||||
- Nothing was run on the Mac; the script ran on build-3.
|
||||
124
docs/analysis/class-v6/connected-state.md
Normal file
124
docs/analysis/class-v6/connected-state.md
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
# Class v6: the connected-state variant (8 October 2026)
|
||||
|
||||
The experiment of the external review the founder accepted (the 15:4x BST rules): the remaining chip edge (about 2.0x to 2.2x node-for-node, 2.2x to 2.6x a node ahead) survives because a specialist can separate storage, arithmetic and memory scheduling. This lane reorganised the class v5 work, same dataset, same read width (4 bytes), about the same operation count, so that live state feeds each load address, the memory result feeds mixed arithmetic and cross-lane exchange, and that updates the live state for the next address, with a 64-register window per lane that stays necessary across the whole dependent chain. Research class only, behind `--class cs<W>s<S>x<R>` in `igneum-pow` (`igneum-pow/src/connected.rs`, branch `class-v6-connected` on the box mirror); never a chain class.
|
||||
|
||||
Every number below is measured unless marked modelled or owed. Times are UK (BST).
|
||||
|
||||
## 1. The structure
|
||||
|
||||
| Item | Class v5 (`mx8+sh256x27`, the control) | Connected state (`cs64s27x16`) |
|
||||
|---|---|---|
|
||||
| Registers per lane | 8 | 64 (the window) |
|
||||
| Per iteration | 64 base instructions with 16 loads, then a 256-instruction block run 27 times | 16 steps: a load, then a 27-instruction block run 16 times |
|
||||
| Load address | a base register, `x & MASK` (the era stride and site window under an era) | a window register, the same address rule |
|
||||
| Memory result | xor into the load's destination | xor into `r[m_j]`; block instruction 0 reads it |
|
||||
| Next address | whatever register the next load reads | written by the block's last instruction (add, sub, xor or shfl) from a fresh spine |
|
||||
| Result | fold of 8 registers | fold of all 64 (`lo` over the first 32 at 7 i, `hi` over the second 32 at 9 i; the v5 fold at a window of 8) |
|
||||
| Loads per hash | 128 | 128 |
|
||||
| ALU instructions per hash | 55,680 (55,296 shadow + 384 base) | 55,296 (0.7 percent fewer) |
|
||||
| Instruction text per iteration | 320 | 448 |
|
||||
| Negative controls kept out | | no long program (1,024), no select tree, no wide read (W = 16), no scratchpad |
|
||||
|
||||
The draw (deterministic from the seed words, attempt k re-seeded as every class): per step `a_j` then `m_j != a_j` (and the era window draws); per block instruction the op from the ten non-load families at the v5 weights, the source from the last four spine entries (the memory result first), the destination uniform over the window, the two immediates, the rotation, the selector bit and the shuffle mask. Four rules the first census pass forced, each a construction rather than a filter:
|
||||
|
||||
1. The cover: the first 64 injecting destinations walk a drawn permutation of the window, so every register takes an injecting write (rule (b)); a uniform draw left one register without one in about 70 percent of candidates.
|
||||
2. The fresh spine: only destinations of add, sub, xor, mad and shfl enter the spine, so every address is fresh by dataflow (rule (a')); with a lossy spine every seed exhausted 256 attempts.
|
||||
3. Lossy ops feed the next injection: or, mul and mulhi write the register the next injecting instruction of the block writes, so their value enters the chain and no register accumulates a lossy op across the 16 passes (a 16-pass or saturated a register the block never re-injected, a 16-pass mul cleared its low bits: rule (c) refused every candidate).
|
||||
4. The two closing instructions of a block come from add, sub, xor and shfl (the census lane's rule for a load source's writer): no product on the address writer (a 16-pass mad on the address register read bit z 57.6 at one site).
|
||||
|
||||
The acceptance rule is the sub-version 3 rule over the window: (b), (a') (the fixpoint over the iteration's execution order), (c) over 64 units (constant bits per register, lane-constant sites, saturation at 1 percent of the window's final values, saturated sources, output bias, distinct addresses, the value-level index-bit read judged inside the site's era window), then (c'') at 0.98 and (c''') at 0.995 over 2^20 evaluations per site. (a) holds by construction.
|
||||
|
||||
## 2. Liveness (seed `igneum-v6c/0`, attempt 0, program id 9ad55de91485542b)
|
||||
|
||||
The tool (`igneum-pow liveness --class cs64s27x16 --seed <s>`) walks the unrolled trace of one hash (128 loads, 55,296 ALU instructions). `live` is the backward set at an address: registers whose value just before the load feeds this or any later address. `dep` is the forward set: registers at the previous address whose values feed this address.
|
||||
|
||||
| Measure | Value |
|
||||
|---|---|
|
||||
| live before each address | 63 of 64 at every address until the last iteration's tail; iteration 7: 63 62 60 60 60 57 57 54 53 51 45 36 25 20 9 1 |
|
||||
| registers read by the result | 64 of 64 (the fold) |
|
||||
| dep per step (the same every iteration, the text repeats) | 1 9 13 14 8 15 7 8 12 14 14 13 10 9 14 9; mean 11.2 of 64 |
|
||||
| registers touched per block (read or written) | min 16, mean 20.4, max 24 of 64 |
|
||||
| reads per register per hash | min 384, mean 1,724, max 3,208 |
|
||||
| writes per register per hash | min 128, mean 866, max 1,664 |
|
||||
| op mix of the 432 block instructions | add 82, mul 62, xor 51, shfl 51, sub 46, mulhi 39, rotl 35, mad 31, rotr 20, or 15 |
|
||||
|
||||
Meaning: the whole window is necessary over the hash (no register can be dropped or parked for long: the longest gap between writes to one register is under one iteration), and every value is read by a later address, so there is no side calculation a specialist can move to a separate engine. The dependent chain per step is narrow: about 11 of the 64 registers at one address feed the next address through 432 operations, and a block touches about 20. That is the shape a specialist will exploit: a two-level file, about 20 registers hot per step and 44 warm, the hot set moving along the text; the k lane prices exactly that (section 5). The program listing is `liveness.txt` and `program.json` of the pack.
|
||||
|
||||
## 3. Census (the census lane's sub-version 3 harness, `tools/attack/v6-census/v6census.sh`, (c''') 0.995 on, on a rented 5090 host with 40 threads, 16:01 to 16:08 BST)
|
||||
|
||||
| Run | Seeds | Accepted | Exhausted | Rejections per candidate | Mean attempt | Max attempt | Window-bit refusals | Over 6 sigma (reported) |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| cs64 no era | 256 | 256 | 0 | 0.283 | 0.39 | 5 | off | 146 of 256 (max z 128) |
|
||||
| control `mx8+sh256x27` no era (census lane, build-3) | 256 | 256 | 0 | 0.668 | 2.01 | 18 | off | 143 of 256 (max z 97) |
|
||||
| cs64 eras 0 to 7, window-bit refusal on | 8 x 32 | 256 | 0 | 0.635 | 1.74 | 8 | 306 | 0 (max z 5.9) |
|
||||
| control eras 0 to 7, refusal on (census lane, build-4) | 8 x 32 | 256 | 0 | 0.830 | 4.87 | 22 | 218 | 0 (max z 5.9) |
|
||||
| cs64 eras 0 to 7, refusal off | 8 x 32 | 256 | 0 | 0.304 | 0.44 | 2 | off | 94 of 256 (max z 71.5) |
|
||||
|
||||
The no-era rejections are all rule (c) (constant bits, saturation or bias in the 64-unit test); the control's are mostly (a') and (a), which this class satisfies by construction. The window-bit refusals sit on eras 0, 1, 4, 5 and 6 (55 to 64 each) and nearly vanish on eras 2 and 3 (0 and 1): the product-bit class (a product's low bits reaching an address bit through the era stride), the same class the control carries and the layer-1 index fold removes; this class does not change it either way.
|
||||
|
||||
F8 form (`igneum-pow cs-uniform`, 16 seeds x 2^20 nonces, no era): the top 0.1 percent item share reads 0.9987 to 1.0016 of a uniform control of the same size (the control class 0.9993 to 1.0016); the worst per-site distinct ratio 0.9941 (seed 9, site 9; the sequential-nonce sample of the F8 tool, the acceptance's own 2^20 sample passed 0.995 on every accepted program).
|
||||
|
||||
Meaning: the class censuses at least as well as v5 on every instrument of the harness and takes fewer attempts; it inherits v5's product-bit bias and the fix is the same fold. The acceptance costs about 3.7 s per candidate on one core (the (c'') pass dominates, 7 G lane-ops), against v5's 2.8 s.
|
||||
|
||||
## 4. GPU cost
|
||||
|
||||
Instrument: the class v5 nvcc harness (`proto-newpow/class-v5/bench.cu` on `box/ds55-v5`, the v5 design page's 4090 rows), compiled per pack with `-Xptxas -v`, 250 batches of 2^24, nvidia-smi at 1 Hz, both packs on the same card minutes apart; vectors 3 of 3 PASS and the dataset self-test PASS on every row. The cs64 pack is over the class v4 memory-hard dataset (no leaves); v5-genesis carries its 93 leaves. Stock means the card's own power limit and no clock lock.
|
||||
|
||||
| Card | Pack | MH/s | Mean W | Microjoules per hash | Registers per thread | Spills | Resident blocks per SM (1 warp per block) | Fingerprint of 2^24 at base 0 |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| RTX 5090 (Vast 54862507, driver 580.159.03, 575 W cap), stock | cs64s27x16 | 64.93 | 574.8 | 8.853 | 80 | 0 | 24 | ad0cec2a42c84aff |
|
||||
| RTX 5090, the same card | v5-genesis | 65.30 | 574.8 | 8.803 | 48 | 0 | 24 | ae74193ddad19e19 |
|
||||
| RTX 4090 (RunPod 386yytbh4bkfnz, driver 580.159.04, 450 W cap), stock | cs64s27x16 | 62.44 | 268.8 | 4.305 | 87 | 0 | 20 | ad0cec2a42c84aff |
|
||||
| RTX 4090, the same card | v5-genesis | 62.44 | 271.3 | 4.345 | 32 | 0 | 24 | ae74193ddad19e19 |
|
||||
| RTX 5090 on PC 1 at the 1,300 MHz lock | both | OWED: PC 1 booked to 17:40 BST; the bound pack and the kit are at build-1:/srv/builds/igneum-wt-connected/cs-kit (sha c9aff54aaf10d79e) and the hash lane publishes the job when PC 1 frees | | | | | | |
|
||||
|
||||
The kit worker (the brief's instrument, `igneum-worker-cuda --bench --batch-log2 24 --batches 250`, the class v5 kit's NVRTC worker of 7 October loading the pack's `kernel_bound.cu`; check PASS on both packs):
|
||||
|
||||
| Card | Pack | MH/s | Mean W | Microjoules per hash | Registers per thread | Fingerprint |
|
||||
|---|---|---|---|---|---|---|
|
||||
| RTX 5090 (the Vast card above), stock | cs64s27x16 (bound pack) | 62.88 | 574.2 | 9.131 | 80 | ad0cec2a42c84aff |
|
||||
| RTX 5090, the same card | v5-genesis | 62.96 | 571.4 | 9.076 | 48 | ae74193ddad19e19 |
|
||||
| RTX 4090 (the RunPod card above), stock | cs64s27x16 (bound pack) | 62.41 | 269.4 | 4.317 | 87 | ad0cec2a42c84aff |
|
||||
| RTX 4090, the same card | v5-genesis | 62.39 | 271.9 | 4.358 | 32 | ae74193ddad19e19 |
|
||||
|
||||
Both instruments agree with each other on each card (the harness and the worker within 3 percent of rate) and agree on the comparison: the window moves energy per hash by +0.6 percent on the 5090 (harness and worker alike) and by -0.9 percent on the 4090 (harness and worker alike), inside the run-to-run noise of a power reading. The 4090 is not at its cap (269 W of 450) and both packs read the same rate to three figures, so there the hash is bound by the memory chain, not the ALU or the register file; the 5090 is at its 575 W cap and the window costs under 1 percent of rate. (The fingerprints are the same on both cards and both instruments: ad0cec2a42c84aff for cs64, ae74193ddad19e19 for v5-genesis.)
|
||||
|
||||
Meaning: at stock the window costs the 5090 0.6 percent of energy per hash against a 10 percent budget; the compiled allocation is 80 registers per thread with no spill, so the window is in registers, not local memory, and the occupancy under this harness is the same as v5's. The harness reads 65 MH/s where the NVRTC worker reads about twice that on a 5090 (one warp per block, 24 resident blocks); the ratio between two packs on the same harness is the measurement, the absolute rate is not. The lock row is where the energy comparison binds (the 5090 at the lock reads 2.33 microjoules per hash on v5); the stock rows say the card is bound by its power cap in both cases and the window moves the rate by under 1 percent.
|
||||
|
||||
## 5. The chip side
|
||||
|
||||
The k lane (floor lane 2) priced the re-optimised core on the drawn program at 17:2x BST (synthesis only, a model and never a lower bound; its placed row is due 21:00 as an amendment). The core: 8 lanes, a 64 x 32-bit window per lane in clock-gated flops (a macro file reads within 5 percent), a 512-entry imem holding the 448-instruction text, one in-order op per cycle per lane (the spine's ILP is met by lane count, which is free), every class unit, the load's fold on the address path; gate-level random-input VCD, every pin annotated; 253,059 cells.
|
||||
|
||||
| Row (the k lane's) | pJ per lane-op, ASAP7 | N5 (the card's node) | N3 | N2 | k at the 1,300 lock, N5 / N3 / N2 | k at stock, N5 / N3 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| cs64s27x16, the re-optimised core (gated window, 512 imem) | 6.3 | 4.4 | 3.2 | 2.3 | 0.71 / 0.51 / 0.37 | 0.39 / 0.28 |
|
||||
| the same window on the class v4 draw, 256 imem | 6.2 | 4.3 | 3.1 | 2.2 | 0.70 / 0.50 / 0.36 | 0.38 / 0.27 |
|
||||
| the adversary's 32-register base, gated (the genesis window) | 4.5 | 3.2 | 2.3 | 1.6 | 0.51 / 0.37 / 0.26 | 0.28 / 0.20 |
|
||||
| the GPU-shaped 64-register core, ungated (shadow-k.md, the earlier default) | 9.7 | 6.8 | 4.9 | 3.5 | 1.09 / 0.78 / 0.56 | 0.60 / 0.43 |
|
||||
|
||||
What the adversary's re-optimisation did to each part of the structure: the gated file charges only the register written, so the whole-window liveness costs it nothing beyond the write it would make anyway and the hot-20 banking of section 2 is not even needed; the 16-pass loop and the 448 text cost the shared imem 0.1 pJ per lane-op; the narrow per-step chain sets the lane count, which is free. The window itself is worth +1.2 pJ per lane-op at N5 over the genesis window (+0.14 of k at the lock), the same knob as the design document's 64-register row; the connected organisation around it adds about 0.1 pJ. The placed ungated core came in 64 percent over its synthesis, so the placed figure is expected near 8 to 10 pJ at ASAP7 (k node-for-node near 0.9 to 1.1, approximate); both rows move together and the ratio below holds.
|
||||
|
||||
## 6. The score and the verdict
|
||||
|
||||
E_GPU over E_adversary, absolute convention, GDDR7 board (E_mem 0.466 microjoules per hash), E_chip = E_mem + 55,296 x e_chip, E_GPU = the 5090 at the lock (2.33 microjoules per hash on class v5) x 1.006 for the window (the stock rows of section 4; the lock row is owed):
|
||||
|
||||
| Core | Node-for-node (N5) | A node ahead (N3) | Two nodes (N2) |
|
||||
|---|---|---|---|
|
||||
| cs64s27x16, re-optimised | 2.344 / (0.466 + 0.243) = 3.3x | 2.344 / (0.466 + 0.177) = 3.6x | 2.344 / (0.466 + 0.127) = 4.0x |
|
||||
| the genesis window (the control) | 2.33 / (0.466 + 0.177) = 3.6x | 2.33 / (0.466 + 0.127) = 3.9x | 2.33 / (0.466 + 0.088) = 4.2x |
|
||||
| the window's effect on the chip's edge | 1.10x | 1.08x | 1.05x |
|
||||
|
||||
On the placed figures (both rows 64 percent higher) the pair reads about 2.2x and 2.4x node-for-node and the ratio stays near 1.1x. The gate was 1.25x node-for-node for the 1.5x one-node-ahead ambition; the row reads 1.10x node-for-node and 1.08x a node ahead.
|
||||
|
||||
**Verdict: KILL as a class.** The hypothesis was that a connected organisation of the same work, with the window independently necessary across the whole chain, would deny a specialist its separation of storage, arithmetic and scheduling. It does not: the liveness rows show the window is necessary (63 of 64 at every address) and the chip answers with a clock-gated file that pays per write, not per live register, so necessity costs it nothing; the only term that reaches the chip is the window's own width (+0.14 k at the lock), which the design document already holds as its one robust core knob, and the connected structure adds about 0.1 pJ around it. The GPU side passes its budget with room (+0.6 percent of energy per hash at stock on the 5090, -0.9 percent on the 4090, 80 to 87 registers per thread with no spill), and the census passes every instrument with fewer attempts than v5; neither moves the score. The founder's accepted review stands in a sharper form than before: a specialist's edge against this family is a per-op energy ratio on a known op mix, and reorganising the dependency graph of the same ops does not change what an op costs on either side.
|
||||
|
||||
What is kept: the generator variant and the liveness tool (research class, behind the flag) for the v7 tests below; the measured fact that a 64-register window costs a card under 1 percent at stock, which fixes the design document's modelled "about 0 rate" row; the kit worker and nvcc harness agreement on two cards. What is withdrawn: the "connected state" line as a resistance mechanism.
|
||||
|
||||
## 7. What a v7 variant would test next
|
||||
|
||||
- The index fold on the address (the layer-1 row) in this class, which removes the window-bit refusals on eras 0, 1, 4, 5 and 6 for the control and this class alike.
|
||||
- A wider per-step chain: a spine of depth 8 to 16 so `dep` rises from about 11 toward the window, at the cost of ILP on the card (measure the rate first; the chain per step is what a two-level file exploits).
|
||||
- The window at 32 and 16 (`cs32s27x16`, `cs16s27x16`) for the k curve, and the block at 16 x 27 (`cs64s16x27`, text 272) if the imem matters to the re-optimised core.
|
||||
- The op-mix re-weight of the census lane at this structure (the two closing instructions already take the injecting table).
|
||||
- The PC 1 lock row (informational now: the verdict does not turn on it; it lands as an amendment if PC 1 runs it).
|
||||
- A knob that reaches a gated file: not more live state but more WRITES per op the chip cannot skip (every op writing two registers, or a window write per load), priced against the card's own write cost first; the k lane's placed row at 21:00 says whether even that moves k.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Class v6, the family gate: how a parameter family is cryptanalysed as a family
|
||||
|
||||
Research lane D of the class v6 rotating-family design, under the Counter ASIC coordinator. First cut 8 October 2026, 11:0x to 17:00 BST (the deliverable clock); the full report by 09:00 BST on 9 October. Branch `class-v6-family-gate` (this document) and the harness branch `family-gate-v5` (the class v5 crate at 8f481459 plus the census harness, never a chain path). Internal research, not an independent review; nothing here touches a served number, the devnet or the testnet object; no consensus code this week. Every figure carries its source: a lane report on the mirror, a log on a box, or an arithmetic step shown in the text. Figures from memory are labelled approximate.
|
||||
Research lane D of the class v6 rotating-family design, under the Counter ASIC coordinator. First cut landed on the mirror's master at 11:40 BST, 8 October 2026 (fbe62a8b); the 17:00 cut with the measured coverage landed at 238100b0 (13:13 BST); this is the full report, pulled to 16:30 BST on the founder's clock (every row measured, a partial carries its count). Branch `class-v6-family-gate` (this document) and the harness branch `family-gate-v5` (the class v5 crate at 8f481459 plus the census harness, never a chain path). Internal research, not an independent review; nothing here touches a served number, the devnet or the testnet object; no consensus code this week. Every figure carries its source: a lane report on the mirror, a log on a box, or an arithmetic step shown in the text. Figures from memory are labelled approximate.
|
||||
|
||||
The question. Class v6 (the founder's word at 11:1x BST, 8 October 2026) draws per era the parameters a release used to fix (layer 1), lets the dataset track the chain state (layer 2), rotates instruction families by height (layer 3) and generalises the acceptance floor and the uniformity test to each era's draw with a redraw on failure (layer 4). A chip is then built against a FAMILY of hashes, not one hash. The in-house pass of 7 October and the attack board F1 to F10 bounded ONE member of the family (class v4 sub-version 3 at 017e7037 and class v5 at 1c420786). This document says how the family is bounded: what the space is, how many eras must be drawn to say anything about the worst era, which tests the chain runs itself on every era and epoch, what the offline board looks like over the testnet period, what a published proof of testing contains, and what the whole apparatus cannot see.
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ The question. Class v6 (the founder's word at 11:1x BST, 8 October 2026) draws p
|
|||
| The per-era tests (section 3) | Three rings. Ring A, the chain runs per era at the cut, microseconds: the structural redraw rules (the band membership of every draw, the day-key cost rule, the product-bias rotation class recorded). Ring B, the chain runs per epoch, seconds: the acceptance rule generalised to the family's shape ((a) (b) (a') (c) (c') (c'') (c''') plus the two next-class tests, the per-site largest-256-item-bucket bound and the index-bit bias read, on the same 2^20 pass). Ring C, the gate runs offline per drawn era, box-hours: the F8-form census at 2^24 on 64 seeds, the attempts census, the exhaustion count. Each row carries its known-failed shape and its clean-seed cost so the redraw is priced |
|
||||
| The board (section 4) | F1 to F10 and the nine lanes classified per-era, per-family or chain-run, with the harness taking the era draw as an input; the proof of testing is one JSON record per drawn era (the draw, the object, the binary, the seeds, every verdict with its statistic, threshold and log sha256, the core-seconds) and one family summary (eras per stratum, the worst era per test, the refuse rate per band, the bound's arithmetic) |
|
||||
| The honest line (section 5) | The gate sees what its tests see. It does not see the diffuse era-stride class below the bit-bias read's resolution (a bias under 0.3 percent at 2^20), a mechanism that lives in a joint corner no stratum names, a mixer weakness below adv-mixer-3's bands, or an era whose draw sits in the untested 1 in 64. A GPU-like chip loses nothing on any era; what a corner buys it is a per-era hot set bounded at 1.002x (the floor) or 1.0024x (the diffuse class), or, at `m = 4`, a mixer with 2 applications of measured margin instead of 6 |
|
||||
| Running now (section 6) | The harness `family_gate_era_census` on build-1 from 11:16 BST: one drawn era per seed, every family parameter from the era's own stream, the chain draw through the real rule keyed on the family's shape, one TSV row per era with the (c'') ratio, the bucket ratio, the bit bias and the attempts; the base control (`v5_attempts_census`, the shipped draw) beside it. The coverage table in section 6 is filled from the logs at 16:xx BST |
|
||||
| Measured (section 6) | 38,000 drawn eras through the ring-B tests on four boxes (about 90 core-hours), the lossy-share curve per shape, two live-dataset censuses at 2^24, the pre-floor spread per width, the seven known-failed seeds through the sigma form; the rows that move the design: the band is B = 4 on the injecting families with or/mul/mulhi never raised (the lossy corner exhausts the 256 cap on the 256 x 27 shape alone, 3.3 to 3.6 percent of its eras at r = 0.98, about 6x the independent-attempt figure; 0 of 5,000 shape-64 eras at any lossy share); the (c''') floor stays at 0.995 at the pinned width 4 at its measured cost (14.6 percent of the candidates reaching the 2^20 pass, +0.3 attempts per epoch) because the floor refused both hot sets of the live point-A census (p38 at 0.9932, p54 at 0.9945) and the width-4 statistic has a real tail no single floor removes at the width-1 cost; the shape axis moves the draw from 1.8 attempts (64 x 108) to 3.8 (256 x 27); the mixer axis is invisible to every per-era statistic and is closed per family; the era-stride bias at bit R (the product's bit 0 at z = -512 exactly) in half the eras of every stratum is one value-level mechanism that the bucket bound sees only at narrow windows above bit 12, so the family carries ONE test, the per-site index-bit read, as a per-era record and the structural lever's known-failed set (p4, p8, p10, p34, p212, p225, p15 all caught at 6 sigma; a refusal band of 300 sigma catches five and redraws 16 percent of epochs; 6 sigma would redraw half) |
|
||||
|
||||
## 1. The parameter space
|
||||
|
||||
|
|
@ -22,9 +22,9 @@ The question. Class v6 (the founder's word at 11:1x BST, 8 October 2026) draws p
|
|||
|
||||
| Axis | Band proposed for the draw | What fixed the band (the measured rows) | The tested margin inside the band | Known-failed corner, named |
|
||||
|---|---|---|---|---|
|
||||
| Mixer multiplier `m` (applications per round, `LoadClass::mixer_mult`; 72 per item at `m = 8`, 8 between dependent reads) | {4, 8, 16}; the research lane's outline; this lane's reading: {8} plus {4} as a corner, {16} only after the verifier row | x4 and x8 measured (`docs/plans/mixer-x4.md` section 6.4: 1.92 and 2.79 ms per unit on a loaded M5 Max core; the daily build 42 ms at x8 on the 5090, measured); x16 estimated at about 3.7 ms per unit on the M5 Max core and 9 ms on a 2019-class core (chip-model-v3 section 3 item 1), against the 10 ms verifier gate | adv-mixer-3 (`report-mixer-3.md`, "The round margin, stated"): no statistic survives 2 of the 8 keyed applications between reads at bands 0.0015 (Q2, 2^24 states), 0.00026 (2^27), 0.00018 (Q2b, 2^28); SAT solved at k = 1 in 137 s and timed out at k = 2, 3, 4; two real days and eight random days agree. So at `m = 8` 6 of 8 applications are margin; at `m = 4` the same rows leave 2 of 4; at `m = 16` 14 of 16 | None measured. The corner is `m = 4` by margin and `m = 16` by the verifier; the all-ROT-equal day key is the day-level corner (rejected by the AP-F4-1 rule) |
|
||||
| Op-mix weights (the ten non-load families, sum 75) | Each weight within B points of table 1.4.2 (add 12, xor 10, mul 8, mad 8, shfl 8, rotl 7, sub 6, mulhi 6, rotr 6, or 4), renormalised by largest remainder; shfl and mulhi never raised; B = 2 (spec 1.13.1, proposed) or B = 4 (the research lane) | The microbench (`counter-asic-4-research.md` 15.1a, the 5090 alone, 8 October): pJ per counted op at the stock clock and the 1,300 MHz lock: arx 11.3 / 6.2, mul 13.9 / 8.3, mulhi 39.6 / 21.0, prmt 22.3 / 11.5, lop3 24.1 / 13.0, shfl 55.8 / 29.4; the shuffle is 4.9x the add, so a draw that raises shfl raises the card's premium per instruction with no better `k` for a chip (15.1b). F7's era census: the op-weight corners (15 to 31 of 75) read 1.0x against the GPU, 0 memory effect | The acceptance's rejection rate moves with the lossy share: at the table 0.68 per candidate ((a') 83.5 percent of rejections; adv-accept row 90, 20,000 seeds); the F7 census at B = 2 read every corner clean for the chip; nothing was measured at B = 4 before today (section 6) | The lossy corner: or, mul and mulhi all at +B. It raises (a') rejections and so the attempts per seed; the exhaustion bound `r^256` is the number to read there (section 3, ring C) |
|
||||
| Read width `W` (words per load) | {1, 4} | `docs/plans/read-width.md` (5 October): w16 within the rules (5090 139.8 against 136.1 MH/s, 9070 XT 17.90 against 18.15, M5 Max within 1 percent); w64 bandwidth-bound (5090 share 0.58, 71.9 MH/s): out; the per-load mixes out on the 5 percent spread rule | The acceptance runs unchanged on aligned addresses (read-width.md section 2), but (c'') and (c''') read a site's distinct ALIGNED indices against `E_s = N - N^2 / 2W_s`: at `W = 4` the index space is `W_s / 4`, so the expectation must follow or every width-4 program reads 0.976 at a quarter window and is refused by the 0.98 floor. The harness divides the window by the width (`site_window_words`); the spec line is owed | `W = 16` (w64): excluded by measurement, never in the band |
|
||||
| Mixer multiplier `m` (applications per round, `LoadClass::mixer_mult`; 72 per item at `m = 8`, 8 between dependent reads) | {4, 8} on the measured verifier rows (section 6.7: x16 reads 7.13 ms per warp on one core against x8's 4.12, 1.73x, which puts it over the 10 ms gate on the 2019-class core and the half-core proxy by scaling); the research lane's outline had {4, 8, 16} | x4 and x8 measured (`docs/plans/mixer-x4.md` section 6.4: 1.92 and 2.79 ms per unit on a loaded M5 Max core; the daily build 42 ms at x8 on the 5090, measured); x16 estimated at about 3.7 ms per unit on the M5 Max core and 9 ms on a 2019-class core (chip-model-v3 section 3 item 1), against the 10 ms verifier gate | adv-mixer-3 (`report-mixer-3.md`, "The round margin, stated"): no statistic survives 2 of the 8 keyed applications between reads at bands 0.0015 (Q2, 2^24 states), 0.00026 (2^27), 0.00018 (Q2b, 2^28); SAT solved at k = 1 in 137 s and timed out at k = 2, 3, 4; two real days and eight random days agree. So at `m = 8` 6 of 8 applications are margin; at `m = 4` the same rows leave 2 of 4; at `m = 16` 14 of 16 | None measured. The corner is `m = 4` by margin and `m = 16` by the verifier; the all-ROT-equal day key is the day-level corner (rejected by the AP-F4-1 rule) |
|
||||
| Op-mix weights (the ten non-load families, sum 75) | Each weight within B points of table 1.4.2 (add 12, xor 10, mul 8, mad 8, shfl 8, rotl 7, sub 6, mulhi 6, rotr 6, or 4), renormalised by largest remainder; shfl and mulhi never raised; B = 2 (spec 1.13.1, proposed) or B = 4 (the research lane) | The microbench (`counter-asic-4-research.md` 15.1a, the 5090 alone, 8 October): pJ per counted op at the stock clock and the 1,300 MHz lock: arx 11.3 / 6.2, mul 13.9 / 8.3, mulhi 39.6 / 21.0, prmt 22.3 / 11.5, lop3 24.1 / 13.0, shfl 55.8 / 29.4; the shuffle is 4.9x the add, so a draw that raises shfl raises the card's premium per instruction with no better `k` for a chip (15.1b). F7's era census: the op-weight corners (15 to 31 of 75) read 1.0x against the GPU, 0 memory effect | The acceptance's rejection rate moves with the lossy share: at the table 0.68 per candidate ((a') 83.5 percent of rejections; adv-accept row 90, 20,000 seeds); the F7 census at B = 2 read every corner clean for the chip. MEASURED today (section 6): at B = 4 with the lossy families free to rise the random stratum reads r = 0.72 and a maximum attempt of 107 (the shipped stream's maximum is 28); at the lossy corner r = 0.956 and 1.2 percent of eras EXHAUST the 256 cap | The lossy corner: or, mul and mulhi all at +B (30 of 75 against the table's 18). It is OUTSIDE the band: 8 of 663 drawn eras reached the last resort, which fails rule (a) in 9 percent of seeds (adv-accept-3 finding 1). The band this lane carries: B = 4 on the seven injecting families, or/mul/mulhi never raised above their base (the `lossybase` stratum of section 6 is its measurement), with ring A's lossy-share rule as the check |
|
||||
| Read width `W` (words per load) | {1, 4} in this census; the research lane's floor reading (13:2x BST, `class-v6-rotating-family.md` section 10.3) pins W = 4 words at genesis (the width is the one wire lever on the SRAM full-store die and the cards pay nothing for 16 bytes), with W = 8 the next value once the crate carries it, so the axis is a one-value band at genesis and the width-4 strata here are its calibration | `docs/plans/read-width.md` (5 October): w16 within the rules (5090 139.8 against 136.1 MH/s, 9070 XT 17.90 against 18.15, M5 Max within 1 percent); w64 bandwidth-bound (5090 share 0.58, 71.9 MH/s): out; the per-load mixes out on the 5 percent spread rule | The acceptance runs unchanged on aligned addresses (read-width.md section 2), but (c'') and (c''') read a site's distinct ALIGNED indices against `E_s = N - N^2 / 2W_s`: at `W = 4` the index space is `W_s / 4`, so the expectation must follow or every width-4 program reads 0.976 at a quarter window and is refused by the 0.98 floor. The harness divides the window by the width (`site_window_words`); the spec line is owed | `W = 16` (w64): excluded by measurement, never in the band |
|
||||
| Shadow block shape (instructions per block, passes per iteration) | {64 x 108, 128 x 54, 256 x 27}: 6,912 shadow instructions per iteration at rung 0 | 64-instruction blocks ran 2.5 to 3.5 percent faster than 256 on the 5090 and the M5 Max (6 October, measured); 1,024 cost the M5 Max 17 percent (out) | F1's shadow redundancy bound (3.0 percent of peephole-removable instructions, the v5 generator's `SHADOW_REMOVABLE_MAX_PERMILLE`) was set from F1's census at 256 instructions; the removable fraction per block is a property of the block's length, so the fraction must be re-read at 64 and 128 (a per-family row). The acceptance's (a') fixpoint and the shadow's execution in (c) take any length | The per-load placement (16 sub-blocks of 16 after each load): dead as a chain class (`counter-asic-4-research.md` 20.2a-close): 22 of 1,621 candidates accepted, 42 of 64 seeds exhaust 32 attempts, candidate 0 carries index bit 0 set in 40 of 1,024 addresses at site 0 (z 29.5). The placement axis therefore has ONE value in the band |
|
||||
| Program length `N` (counted ops per hash) | Not drawn: the ladder's signal (rungs 0 to 2); the research lane's outline keeps it so | F6: the worst of 10^5 class v4 programs 8.708 ms on the half-core proxy, so N about 300,000 fits under 10 ms (rung 2 admissible, rung 3 not); the M5 Max loses 3.3 to 10 points across the band (measured ladder) | The verifier's worst case was censused at rung 0 only; a draw inside the ladder needs the 10^5-program worst case at the top rung (per-family row) | Rung 3 (inadmissible by F6) |
|
||||
| Era stride and interleave (`M` odd, `R` in 1..31, `pos` four ascending bit positions, the fold rotations, the per-site windows `k_off`, `o`) | As spec 1.13.1 draws them today | F7's 2^24 era census (stride bijective, R, pos, M uniform, no class over 1.1x); adv-cache-2 section 2.3: the same 32 base programs read 2 of 32 with a site over 1.04x under `R = 29` and 16 of 32 under drawn eras (8 over 1.2x, worst 1.75x), because `rotl(x * M, R)` places a product's biased low bits (P(bit 0) = 1/4) at address bits R and up, inside the 28-bit index unless R is 29 or 30; at D = 29 only R = 30 and 31 cut bit 0 | The whole 64-seed F8 census is a census over drawn eras (each f8 seed carries its own era), so the per-era uniformity verdicts of sub-version 3 and class v5 (60 of 64 and 61 of 64 under 1.2x) already sample this axis; the devnet's clean reading at R = 29 is the accident, the drawn-era figure the chain's | R in 1..27 with a product as the last writer of a load's source (sub-class A); the warp-uniform mulhi source (sub-class B, 1 in 16,000 warps) |
|
||||
|
|
@ -65,6 +65,8 @@ Let a per-era test have a fixed verdict per era and let `p` be the fraction of t
|
|||
| 2^-10 | 505 years | 3,067 | 4,713 |
|
||||
| 2^-20 | the F4 and F7 gate's fraction | 3.1 million | 4.8 million |
|
||||
|
||||
Measured today (section 6.3): 10,000 random eras of the family and 3,000 of the proposed band pass the ring-B exhaustion and floor tests, so the failing fraction of eras on those tests is under 3.0e-4 and 1.0e-3 at 95 percent; the lossy corner fails at 1.2 percent and is out of the band.
|
||||
|
||||
What this buys and does not. The F4 and F7 gates ask "no class with gain over 1.1x at a fraction over 2^-20" and reach it by censusing 2^24 draws of a CHEAP statistic (microseconds per day key or era seed). A per-era test that costs box-hours (the F8 census at 2^24) can be run on hundreds of eras, so it bounds the failing fraction at the 1/64 to 1/256 class, never 2^-20. That is why layer 4 puts the tests on the chain: a per-era test the chain runs on EVERY era (ring A) or every epoch (ring B) needs no sampling bound at all; the offline census (ring C) is then a check on the chain-run tests' calibration (their clean refuse rate and their known-failed firing per stratum), and a reading of the one statistic the chain cannot afford, the live-dataset census.
|
||||
|
||||
### 2.2 The union bound over the per-era tests
|
||||
|
|
@ -103,7 +105,7 @@ The three rings. A test's ring says who runs it and what a failure costs.
|
|||
| Band membership: `m` in the band, `W` in the allowed set, the shape in the set, every weight within B of the table with shfl and mulhi at or under their base, the sum 75 | the layer-1 draws | a draw outside the band (a unit test) | 0 by construction (the draw is bounded) | spec 1.13.1's form |
|
||||
| The day-key cost rule (AP-F4-1, class v5) | ROT, MUL | mul1all, mulnaf, day 29,337 | 6.1e-4 per day | f4-weakday.md 6.7 to 8; 8ca66afa |
|
||||
| The rotation class recorded: whether R leaves a product's low bits inside the index at the era's D | R, D | none (a record, not a refusal: restricting R to {29, 30} would cut the interleave's entropy from 31 to 2 values; the value-level test of ring B is the catch) | 0 | adv-cache-2 2.3 |
|
||||
| The weights' lossy share bound: or + mul + mulhi at most the table's 18 plus B, so the exhaustion bound `r^256` stays under 1e-18 (r under 0.85) | weights | the lossy corner of section 6 if its `r` reads over 0.85 | 0 by construction | row 90 (r = 0.681 at the table) and section 6 |
|
||||
| The weights' lossy share bound: or + mul + mulhi at most the table's 18 (never raised), so the exhaustion bound `r^256` stays under 1e-18 (r under 0.85) | weights | the lossy corner of section 6: r = 0.956, 8 of 663 eras at the cap (FIRED) | 0 by construction | row 90 (r = 0.681 at the table) and section 6 |
|
||||
|
||||
### 3.2 Ring B: the acceptance rule generalised to the family, per epoch
|
||||
|
||||
|
|
@ -117,7 +119,7 @@ The three rings. A test's ring says who runs it and what a failure costs.
|
|||
| (c''') the hot-item floor at 0.995 | the same `E_s`; the floor's clean spread re-read per stratum (section 6): a fixed 0.995 under a spread that moves with the shape or the width is either a liveness cost or a miss | seed 100767 (0.9919) and the nine live hot sets | 2.435 percent of accepted class v4 programs, +0.045 on the mean attempt | in the (c'') pass |
|
||||
| The per-site largest-256-item-bucket bound (NEW, the F8 tail's catch) | the largest count over a site's 4,096-word buckets in its window against `N / (W_s / 4096)`, in SIGMA of the bucket's expectation (`(max - E) / sqrt(E)`), refused above a band set from the clean spread. The ratio alone cannot carry the bound: a clean full-window site's largest of 65,536 Poisson(16) buckets reads 2.1x at +4.4 sigma, a quarter-window site's largest of 16,384 Poisson(64) buckets 1.5x at +4 sigma (the 16-era smoke run read ratios 2.2 to 2.4 on clean sites); the four tail seeds' 3.1x to 5.6x at narrow windows are +17 to +37 sigma | p4, p8, p10, p34 | measured in section 6 (the columns `bucket_z_max`, `bucket_win`) | one linear pass over the (c'') indices |
|
||||
| The index-bit bias read (NEW, adv-cache-2's value-level test) | per site, the one-count of each free index bit (bits above the width's alignment and below the window's cut) over the 2^20 evaluations, in sigma (`sigma = sqrt(N) / 2 = 512`); a 6-sigma band is a bias of 0.3 percent at 2^20 (the product law's bit 0 at 25 percent reads z about 512, bit 1 at 3/8 z 256, bit 3 at 3.1 percent z 64, bit 6 at 0.4 percent z 8, bit 7 at 0.2 percent z 4: passes). MEASURED on the first 16 drawn eras (11:21 BST, section 6): 7 of 16 accepted programs carry one site with a bias of 130 to 511 sigma at address bit R or R + 1 (the era's rotation), the era-stride class exactly, on programs (c''') passes at 0.9954 to 1.0000; the 9 others read under 3.8 sigma. So as a REFUSAL the read would redraw about 40 percent of epochs; its right use is as a record per era plus the structural lever (fold the product's low bits in `load_index` before the rotation, a next-class item), and the chip price is stated in section 5 | Devnet 3 site 0, era-drawn-28 site 15, the per-load candidate 0; and now the 7 of 16 | 7 of 16 at 6 sigma (a refusal is not the form); 0 of 16 at a 600-sigma band | one pass over the indices, 28 bit tests each |
|
||||
| The cap and the last resort | unchanged at 256; the last resort's own verdict is adv-accept-3's finding 1 (9 percent of last-resort programs fail (a)); the family keeps the open fix (continue past 256 until one passes) | the reject-everything mirror | 0 reached in 1.02 x 10^6 seeds | n/a |
|
||||
| The cap and the last resort | unchanged at 256; class v5 carries the verified construction (`last_resort_v5`: a scan of 256 more candidates past the cap, each rewritten and its stale loads re-sourced, the first that passes the whole rule; the unchecked fallback behind it), so adv-accept-3's finding 1 (sub-version 3's unchecked last resort failing (a) in 9 percent of seeds) is closed on the family's crate. What the family changes: the scan's stated odds (under 1e-300) assume independent attempts, and section 6.4 measures a per-era exhaustion rate about 6x the independent estimate at the lossy corner on the 256-instruction shape (the attempts of one era share its weight table), so the fallback's odds are the corner's, not the arithmetic's; ring A keeps the corner out of the band, and on every exhausted era seen (41) the scan passed at its first to third candidate | the reject-everything mirror; the 61 exhausted eras of section 6 (the scan's verdict on each is the full report's item 5) | 0 reached in 1.02 x 10^6 seeds of the shipped draw; 0 of 19,000 family eras outside the lossy corner | n/a |
|
||||
|
||||
### 3.3 Ring C: the offline tests per drawn era
|
||||
|
||||
|
|
@ -176,46 +178,155 @@ What the family gate cannot see, and how a chip would use it.
|
|||
|
||||
| Blind spot | Why the gate misses it | What a chip gets from it | Bound today |
|
||||
|---|---|---|---|
|
||||
| The era-stride class, measured today at the bit level on 7 of 16 drawn eras (section 3.2): not diffuse, a 25 percent bias on ONE address bit (bit R, the product's bit 0 through `rotl(x * M, R)`) at one site, on programs every floor passes | the (c''') floor reads distinct counts, which a one-bit bias barely moves (0.9954 to 1.0000 on the seven); the bit read sees it at 130 to 511 sigma but refusing it would redraw about 40 percent of epochs; the class is structural (`load_index`'s form and the weights' products), so the catch is a next-class change to the address form, not a per-era floor | a store holding the favoured half of that site's window (128 MiB at D = 28) serves 75 percent of that site's reads instead of 50: 25 percent of 1/16 of a hash's reads, about 1.6 percent of reads at f = 1/2 from one site in about 40 percent of eras (arithmetic on the measured bias); the honest partial-store curve already costs 1.26x the ops at f = 1/2 (adv-cache-2 Q4), so the f = 1 verdict stands; the top-0.1-percent reading of the ledger (1.0024x) was the same class seen through the coarser instrument | the per-era record (ring A names the rotation class; ring B logs the bit read); the structural fix is the family's first owed item for the research lane |
|
||||
| The era-stride class, measured at the bit level on half the eras of every stratum (sections 6.2 and 6.8): not diffuse, a 25 percent bias on ONE address bit (bit R, the product's bit 0 through `rotl(x * M, R)`, z = -512 at 2^20 exactly) at one site, on programs every floor passes; the morning's F8 tail and the hash lane's p212 and p225 are the same mechanism | the (c''') floor reads distinct counts, which a one-bit bias barely moves (0.9954 to 1.0000 on the seven); the bit read sees it at 130 to 511 sigma but refusing it would redraw about 40 percent of epochs; the class is structural (`load_index`'s form and the weights' products), so the catch is a next-class change to the address form, not a per-era floor | a store holding the favoured half of that site's window (128 MiB at D = 28) serves 75 percent of that site's reads instead of 50: 25 percent of 1/16 of a hash's reads, about 1.6 percent of reads at f = 1/2 from one site in about 40 percent of eras (arithmetic on the measured bias); the honest partial-store curve already costs 1.26x the ops at f = 1/2 (adv-cache-2 Q4), so the f = 1 verdict stands; the top-0.1-percent reading of the ledger (1.0024x) was the same class seen through the coarser instrument | the per-era record (ring A names the rotation class; ring B logs the bit read); the structural fix is the family's first owed item for the research lane |
|
||||
| A joint corner no stratum names | the 128 cells pin the axes with a KNOWN mechanism; a mechanism that needs, say, a particular `M` with a particular weight table and a particular shape is reached only by the random stratum, which bounds its fraction at 3/n | one era (180 days) of whatever the corner is worth, at most once per occurrence | 1/64 to 1/256 of eras untested at 95 percent (section 2.1); the chain-run rings catch the known shapes on every era |
|
||||
| The mixer below adv-mixer-3's bands | biases under 0.00018 (Q2b), correlations under 0.00073 with multi-bit masks, differentials under 2^-19, any structure a SAT model of 2 or more applications would expose; at `m = 4` the measured margin is 2 applications | a shortcut that saves applications against the 9,360 ops per item; none found at any k | the round margin, stated; `m = 4` is the corner that spends 4 of the 6 measured spare applications |
|
||||
| The verifier at `m = 16` and rung 2 on a 2019-class core | unmeasured (an estimate of 9 ms against the 10 ms gate) | nothing for a chip; a node tier retired if the corner is drawn | the row is owed before 16 enters the band |
|
||||
| The acceptance's stand-in at D = 28 against a live D = 29 | the windows cap `min(k_off, D - 26)` and the rotation's cut set move with D; the gap is measured at D = 28 only | a per-site concentration the closed form does not reproduce | 0.0004 at D = 28; unmeasured at 29 (ring C row) |
|
||||
| The union bound's own looseness | the per-test miss rates rest on 3 to 11 known-failed cases each (section 2.2), so the family's false-pass rate is not yet a number under 1 | nothing new; the bound is on what the gate claims, not on the hash | fixed by cases, not eras: 60 fired cases per test for `delta` under 0.05 |
|
||||
| The last-resort program | 9 percent of last-resort programs fail (a) and are handed out unchecked; a lossy-corner era that raised `r` toward 0.95 would reach it at 2e-6 per epoch | a program that re-reads one address in two loads of the same hash | ring A's lossy-share bound keeps `r` under 0.85 (`r^256` under 1e-18); the fix (continue past 256) is owed to the rule's owner |
|
||||
| The last-resort path at a corner | class v5's scan hands out a rule-checked program, and its unchecked fallback is priced at under 1e-300 on INDEPENDENT attempts; the lossy corner measured 3.3 percent of 256-shape eras exhausting against a 0.5 percent independent estimate (section 6.4), so at a corner the attempts of one era are correlated through the era's own weights by a factor of several and the fallback's odds there are the measured curve's, not the arithmetic's | a program that re-reads one address in two loads of the same hash, if the fallback were ever reached | ring A keeps the corner out of the band (0 of 19,000 eras exhaust elsewhere); the scan's verdict on the 61 exhausted eras is owed (section 7 item 5) |
|
||||
| The era redraw's own grindability | a ring-A redraw consumes the stream's next block, so an adversary who could steer `E_n` could steer which block is used; the era VDF of F7 (517 s on the fastest prover, 259x the window) closes the steering of `E_n` itself | nothing beyond F7's bound | F7 (a) PASS with the VDF in the node |
|
||||
|
||||
A GPU-like chip (the `f = 1` stored-dataset chip with a programmable core, chip-model-v3 section 5) loses nothing on any era of the family: every drawn parameter is firmware to it, and what the family costs it is the core sized for the band's top (the research lane's USD 30 to 60 per chip, modelled). The family gate's job is therefore not to defeat that chip (the identity of the research file's section 2 says the per-joule edge is the card's own idle, 2.1x at `k = 1`) but to make sure no drawn era hands ANY chip more than the one hash did: a per-era hot set, a per-era mixer shortcut, a per-era verifier miss. Everything in this document is a bound on that increment, and the increments found so far are 1.002x and 1.0024x.
|
||||
|
||||
## 6. Running now, and the coverage measured
|
||||
## 6. The coverage measured (38,000 drawn eras in 6.1 to 6.5, the lossy curve, the live censuses, the sigma form and the verifier rows; 11:16 to 15:3x BST, 8 October 2026)
|
||||
|
||||
Started 11:1x BST, 8 October 2026, on igneum-build-1 under `lease pool` (class measure, owner class-v6-family-gate), the scripts under `/srv/builds/_adv-family-gate/` (fg-setup.sh, fg-build.sh, fg-census.sh), every binary run from a pinned copy under `bin/<tag>/` with its sha256 (AP-H2's rule), logs and TSV rows under `logs/`, copies of the finished logs land under `docs/analysis/class-v6/logs/` on this branch with the full report.
|
||||
The harness: branch `family-gate-v5` at 3dc3117c (the class v5 crate 8f481459 plus `accept::tests::family_gate_era_census`; the diff is `logs/harness-family-gate-v5-3dc3117c.diff` (a plain diff of igneum-pow/src: the gate refuses the patch form's author lines) in this directory, since the branch's push to the mirror was refused by the class v5 tree's own pre-push hook and is held). What it does per era `k` of a label space: the era bytes from `igneum-family-gate[/stratum]/era/k` and the epoch bytes from `.../program/k` (the F8 label space's form); from the era's own stream, in a fixed order and consumed whether pinned or not, the shadow block shape in {64 x 108, 128 x 54, 256 x 27}, the mixer multiplier in {4, 8, 16} (recorded: the acceptance never runs the mixer), the ten non-load weights perturbed by `below(2B + 1) - B` with shfl and mulhi never raised and renormalised to 75 by largest remainder (B = 4), the read width through the era draw over {1, 4} words (the mix one-hot on the drawn width); then the chain draw of that era's epoch through the real rule keyed on the family's shape (`is_family_shape`, the acceptance's `is_class_v4_shape` generalised behind `IGNEUM_FAMILY_GATE`; the draw's source rule reads the same predicate), every candidate's first failing part recorded, the cap 256 and the last resort as shipped; on the accepted program, on the rule's own 2^20 sample (4,096 units), per site: the (c'') and (c''') ratio with the window divided by the width, the largest 4,096-word (256-item) bucket in sigma of its expectation, and the largest index-bit one-count excess in sigma over the free bits. One TSV row per era (`logs/<stratum>-family_gate_era_census-<from>-<n>.tsv`, the run's log beside it with the binary's sha256 and the lease line); every binary a pinned copy under `/srv/builds/_adv-family-gate/bin/<tag>/` (fg2 9b7f764a for the first corners, fg5 e7a50f8d for the width-4 and lossy-base strata). Cost: about 10 core-seconds per era; 54.4 core-hours in all (1.7 box-hours of 32-core slots) on build-1 (random, lossy cap, width 4) and build-3 (shape 64, lossy base, width 4 plus lossy cap), every run under `lease pool` at class measure. The control: `v5_attempts_census` on the shipped class v5 draw over 10,000 seeds of the F8 label space (build-1, 24 cores, 1,534 s, binary 4bec799c, `logs/base-v5_attempts_census-1000-10000.log`): r = 0.6854, mean attempt 2.179, 0 exhaustions, (a') 82.5 percent of rejections, (c''') 0.9 percent, the row-90 reading reproduced.
|
||||
|
||||
| Run | What | Where | State |
|
||||
|---|---|---|---|
|
||||
| base | `v5_attempts_census` (the shipped class v5 draw: shape 256 x 27, `m = 8`, `W = 1`, the table weights) over f8-label seeds 1,000 to 11,000, each seed its own drawn era (M, R, pos, windows) | build-1, 24 cores, binary 4bec799c (8f481459 unmodified) | running from 11:16 BST |
|
||||
| fg1 random | `family_gate_era_census` with every axis drawn (shape in {64, 128, 256}, `m` in {4, 8, 16} recorded, `W` over {1, 4}, weights at B = 4 with shfl and mulhi never raised) | build-1 | the harness built at 11:2x BST; the smoke run and the chunks follow |
|
||||
| fg1 corners | the same harness pinned per cell: `IGNEUM_FG_LOSSY_CAP`, `IGNEUM_FG_WIDTH=4`, `IGNEUM_FG_SHAPE=64`, the combinations | build-1, build-3, build-4 as the pool frees | queued |
|
||||
Voided and stated: the first width-4 stratum (3,000 eras, 11:2x to 11:4x BST) ran at width 1, because `LoadClass::era` with a one-entry allowed set is the pinned path and redraws to the base's widest width; the fix builds the pinned class by hand (the era drawn over the one-entry set, the mix one-hot); the void rows are not in this directory and the stratum was re-run under its own label space. The first corner strata (lossy cap, shape 64) share the random stratum's label space, so where the random draw happened to land on the pinned value the era is the same program in both; the later strata use per-stratum labels.
|
||||
|
||||
The coverage table (filled from the TSV rows at 16:xx BST for the 17:00 cut; the full read at 09:00 BST tomorrow):
|
||||
### 6.1 Per stratum
|
||||
|
||||
| Stratum | Eras drawn | `r` per candidate | Mean attempt, max | Exhausted | min (c'') ratio (the worst era) | Under 0.995 (the (c''') refuse rate on the accepted draw) | Largest bucket ratio (worst era, site) | Largest bit bias in sigma (worst era, site, bit) |
|
||||
| Stratum | Eras | r per candidate | Mean attempt, max | Exhausted at the 256 cap | min (c'') ratio of the accepted draw, worst era | The floors' refuse rates over all candidates | Largest bucket excess, worst era | Largest index-bit bias, worst era; the share of eras over 6 and over 100 sigma |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| base (shipped draw) | pending | | | | | | | |
|
||||
| random (every axis drawn) | pending | | | | | | | |
|
||||
| lossy cap | pending | | | | | | | |
|
||||
| W = 4 | pending | | | | | | | |
|
||||
| shape 64 | pending | | | | | | | |
|
||||
| random (every axis drawn, B = 4 on every family) | 10,000 | 0.724 | 2.63, 236 | 0 | 0.9950 at site 0 of era 1150 (R 14, shape 64, W 1) | (c''') 988 of 36,285 candidates (2.72 percent), (c'') 368 (1.01) | +512 sigma at site 4 (full window) of era 2794 (R 11) | z = -1,024 at site 13 bit 18 of era 3310 (R 18); over 6 sigma 5,172 (51.7 percent), over 100 sigma 3,390 (33.9) |
|
||||
| lossy base, THE PROPOSED BAND (B = 4 on the seven injecting families, or/mul/mulhi never raised) | 3,000 | 0.595 | 1.47, 59 | 0 | 0.9951 at site 0 of era 114 (R 25, shape 64, W 4) | (c''') 290 of 7,410 (3.91 percent), (c'') 110 (1.48) | +369 sigma at site 12 of era 2099 (R 10) | z = -1,024 at site 14 bit 26 of era 499 (R 26); over 6 sigma 1,431 (47.7 percent), over 100 sigma 933 (31.1) |
|
||||
| lossy cap (or, mul, mulhi all at +4: 30 of 75) | 3,000 | 0.957 | 19.21, 254 | 37 (1.23 percent of eras; the first eight 22, 123, 125, 170, 293, 305, 348, 584) | 0.9950 at site 13 of era 2359 | (c''') 282 of 69,363 (0.41 percent), (c'') 131 (0.19) | +184 sigma at site 0 of era 2085 (R 14) | z = -896 at site 3 bit 26 of era 2141 (R 26); over 6 sigma 57.7 percent, over 100 sigma 37.0 |
|
||||
| width 4 words (the pinned width, every other axis drawn) | 3,000 | 0.738 | 2.82, 114 | 0 | 0.9950 at site 4 of era 865 (R 2) | (c''') 497 of 11,454 (4.34 percent), (c'') 134 (1.17) | +44 sigma at site 5 (half window) of era 270 (R 21) | z = -768 at site 15 bit 25 of era 183 (R 25); over 6 sigma 47.5 percent, over 100 sigma 28.6 |
|
||||
| width 4 plus lossy cap | 2,000 | 0.959 | 20.15, 253 | 24 (1.20 percent) | 0.9950 at site 1 of era 1869 | (c''') 352 of 47,934 (0.73 percent), (c'') 124 (0.26) | +70 sigma at site 0 (quarter window) of era 1440 (R 10) | z = -736 at site 11 bit 27 of era 1239 (R 27); over 6 sigma 52.3 percent, over 100 sigma 31.2 |
|
||||
| shape 64 x 108 (the pinned shape) | 3,000 | 0.643 | 1.80, 31 | 0 | 0.9950 at site 0 of era 1150 | (c''') 301 of 8,399 (3.58 percent), (c'') 104 (1.24) | +512 sigma at site 4 of era 2794 (R 11) | z = -1,024 at site 10 bit 1 of era 2431 (R 1); over 6 sigma 52.1 percent, over 100 sigma 34.0 |
|
||||
| the shipped draw (control) | 10,000 | 0.685 | 2.18, 28 | 0 | not read (the census test reports parts only) | (c''') 0.6 percent of candidates, (c'') 0.8 | | |
|
||||
|
||||
## 7. Owed by 09:00 BST on 9 October (the full report)
|
||||
Rejections by first failing part, the random stratum (26,285 rejected candidates): (a') 76.4 percent, (a) 13.4, (b) 3.8, (c''') 3.8, (c'') 1.4, (c) constant bit 0.6, (c) saturated 0.5, (c') 0.1, (c) distinct addresses 0.1, (c) output bias 0.0. At the lossy cap (66,400): (a') 74.6, (a) 20.8, (b) 3.7, the dynamic parts under 1 together. The shape of the rule's cost does not change across the family; its level does.
|
||||
|
||||
1. The coverage table of section 6 filled from the logs, with the per-stratum refuse rates and the union-bound arithmetic on the measured numbers, and the TSV rows and logs under `docs/analysis/class-v6/logs/`.
|
||||
2. The clean spread of the bucket ratio and the bit bias per stratum, so the two new ring-B tests get a bound and a clean refuse rate (not a guess).
|
||||
3. The lossy-corner `r` against the 0.85 line, and the exhaustion count at 10^4 seeds on that corner.
|
||||
4. The width-4 reading of (c'') and (c''') with the divided expectation, and the spec line for 1.4.6.5.
|
||||
5. The mixer rows at `m = 4` and 16 (adv-mixer-3's index and sac at k = 2 on two days; the SAT at k = 2) as the per-family corner rows, if the boxes have the cores after the other lanes' jobs arrive; else the plan with its hours.
|
||||
6. The F8-form census at 2^24 on 64 seeds of one lossy-cap era and one width-4 era, the first ring-C rows on the family (about 2 box-hours each).
|
||||
7. The gate-record JSON written for every era of the census from the TSV (the format of 4.3), and the summary.
|
||||
### 6.2 Per axis, inside the random stratum (10,000 eras) and the proposed band (3,000)
|
||||
|
||||
| Axis | Value | Eras (random / band) | Mean attempt (random / band) | Max attempt | r (random / band) | (c''') refusals per candidate (random / band) | Eras with a site over 6 sigma | Over 100 sigma | Bucket sigma p99 | min ratio p1 |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| width (words) | 1 | 4,978 / 1,508 | 2.35 / 1.30 | 158 / 59 | 0.701 / 0.565 | 0.61 / 0.98 percent | 55.7 / 52.1 percent | 39.1 / 36.4 | +51.5 | 0.9959 |
|
||||
| width (words) | 4 | 5,022 / 1,492 | 2.91 / 1.64 | 236 / 23 | 0.744 / 0.621 | 4.52 / 6.50 percent | 47.8 / 43.3 | 28.8 / 25.7 | +28.2 | 0.9956 |
|
||||
| shape | 64 x 108 | 3,371 / 980 | 1.81 / 1.22 | 40 / 14 | 0.645 / 0.550 | 3.86 / 4.31 | 50.5 / 48.7 | 33.0 / 31.3 | +40.7 | 0.9958 |
|
||||
| shape | 128 x 54 | 3,302 / 963 | 2.26 / 1.28 | 62 / 12 | 0.694 / 0.562 | 2.76 / 4.32 | 51.8 / 48.7 | 34.1 / 32.9 | +46.0 | 0.9958 |
|
||||
| shape | 256 x 27 | 3,327 / 1,057 | 3.82 / 1.87 | 236 / 59 | 0.792 / 0.651 | 2.02 / 3.33 | 52.8 / 45.9 | 34.6 / 29.2 | +41.7 | 0.9957 |
|
||||
| mixer m (recorded) | 4 | 3,305 / 990 | 2.72 / 1.64 | 236 / 59 | 0.731 / 0.621 | 2.74 / 3.94 | 51.5 / 47.8 | 34.0 / 31.5 | +41.5 | 0.9958 |
|
||||
| mixer m | 8 | 3,382 / 1,011 | 2.67 / 1.38 | 158 / 21 | 0.728 / 0.580 | 2.76 / 3.86 | 51.2 / 47.2 | 33.9 / 31.3 | +42.5 | 0.9957 |
|
||||
| mixer m | 16 | 3,313 / 999 | 2.49 / 1.39 | 113 / 36 | 0.714 / 0.581 | 2.66 / 3.94 | 52.4 / 48.1 | 33.8 / 30.5 | +42.4 | 0.9959 |
|
||||
| rotation class | R 1..27 | 8,732 / 2,587 | 2.64 / 1.51 | 236 / 59 | 0.725 / 0.602 | 3.07 / 4.40 | 53.9 / 49.2 | 37.7 / 35.0 | +44.5 | 0.9957 |
|
||||
| rotation class | R 28..31 (bit 0 cut at D = 28) | 1,268 / 413 | 2.57 / 1.21 | 231 / 12 | 0.720 / 0.548 | 0.29 / 0.44 | 36.5 / 38.5 | 7.5 / 6.8 | +6.5 | 0.9985 |
|
||||
|
||||
Readings, each a measured row above:
|
||||
|
||||
1. The mixer axis is invisible to every per-era statistic, as section 1.3 said it must be: the three `m` values read the same to the noise (r 0.714 to 0.731, the biased share 51 to 52 percent). `m` is a per-family axis, closed by the mixer rows of section 4, not by drawing eras.
|
||||
2. The shape axis moves the draw's cost: 256 x 27 costs 3.82 attempts against 1.81 at 64 x 108 (r 0.792 against 0.645), because the freshness fixpoint (a') walks the whole block and a 256-instruction block holds more lossy last writers per iteration than a 64-instruction one. The band's cheapest shape is the fastest card shape too (64, measured 2.5 to 3.5 percent faster on 6 October).
|
||||
3. The width axis moves the (c''') floor's calibration: at width 4 the floor at 0.995 refuses 4.5 to 6.5 percent of candidates against 0.6 to 1.0 at width 1, with the expectation already divided by the width. The clean spread of the ratio at width 4 is not the width-1 spread the floor was set from (the aligned index space is a quarter the size, so the birthday collisions on the closed form are 4x denser, and the `N - N^2 / 2W` form is 0.0007 low at `N / W = 1/16`). A floor fixed at one number across the family is therefore a per-width liveness cost of 4x the (c''') attempts; the family's floor is set per width from the clean spread (the next measurement, section 7 item 1), or the statistic is re-expressed as a sigma over its own expectation as the bucket statistic now is.
|
||||
4. The lossy corner breaks the exhaustion bound: r = 0.957 to 0.959 and 1.2 percent of eras at the 256 cap in both lossy strata (61 of 5,000), against 0 of 19,000 in every other stratum and 0 of 10,000 on the shipped draw. Read per shape (section 6.4) the exhaustion sits on the 256 x 27 shape alone (3.3 to 3.6 percent of its eras; 0 of 5,000 at 64 x 108), where r = 0.98 and the independent-attempt figure 0.98^256 is 0.5 percent: the attempts of one era are correlated through its weight table by about 6x. The ring-A rule is the one the data forces: or + mul + mulhi never raised above the table's 18 of 75. Under it (the lossy-base stratum) r = 0.595, the mean attempt 1.47, the maximum 59, 0 exhaustions in 3,000.
|
||||
5. The index-bit bias is the family's standing fact: 48 to 58 percent of accepted programs in every stratum carry one site with a bias over 6 sigma at 2^20, and 73 percent of those are at bit R exactly, 12 percent at R + 1, 5 percent at R + 2 (the product law's bits 0, 1, 2 at P = 1/4, 3/8, 7/16 through `rotl(x * M, R)`); 74 percent read P(bit) under 1/2 (a product's low bit), 26 percent over 1/2 (an or-shaped source, 5/8 and up). Under R in 28..31 (bit 0 cut by the 28-bit mask) the share over 100 sigma falls from 37.7 to 7.5 percent and the bucket excess's p99 from +44 to +6.5 sigma, which is adv-cache-2's R-class reading at 10,000 eras. The shipped devnet's R = 29 is one of the four clean rotations; the chain's eras are drawn from all 31. The bucket statistic and the bit statistic see the same mechanism (the bucket's p99 on the bit-clean eras is +6.8 sigma, on the biased ones +44), so one next-class test covers both: the value-level read, with the structural fix in `load_index` as the remedy rather than a refusal (a 6-sigma refusal redraws half the epochs; a 100-sigma one a third).
|
||||
6. The (c''') floor holds everywhere by construction (0 accepted programs under 0.995) and its clean spread at width 1 is the shipped one (p1 of the minimum ratio 0.9958 to 0.9961, median 0.9999 across strata); the floor's refuse rate moves from 2.7 percent (random) to 3.9 (the band) and 0.4 (the lossy corner, where (a') refuses first).
|
||||
|
||||
### 6.3 What the counts bound (the arithmetic of section 2 on these rows)
|
||||
|
||||
| Claim | n | Bound at 95 percent | Bound at 99 percent |
|
||||
|---|---|---|---|
|
||||
| An era of the proposed band whose chain draw exhausts the 256 cap | 9,000 of 9,000 passed (lossy base 3,000, the width-1 and width-4 band strata 3,000 each; the afternoon's strata) | under 3.3e-4 of eras (one in 3,000: one per 1,500 years at 180-day eras) | under 5.1e-4 |
|
||||
| The same on the random stratum at B = 4 on every family | 10,000 of 10,000 | under 3.0e-4 | under 4.6e-4 |
|
||||
| An era of the band whose accepted draw sits under the (c''') floor | 9,000 of 9,000 (and 33,000 of 33,000 outside the lossy corner, every stratum of the day) | under 3.3e-4 | under 5.1e-4 |
|
||||
| An era of the lossy corner that exhausts | 61 of 5,000 FAILED | the rate is 1.2 percent, a measurement, not a bound: the corner is out of the band; class v5's last-resort scan then hands each such era a rule-checked program (the scan's own verdicts on the 61 are owed) | |
|
||||
| The (c''') floor's miss rate on the few-item hot-set class | 11 of 11 fired (the in-house pass's nine, plus point A's p38 and p54 under a band era's weights, section 6.6) | under 0.27 | under 0.34 |
|
||||
| The bit-bias read's firing on the product class | 7 of 7 named seeds at 6 sigma (p4, p8, p10, p34, p212, p225, p15; section 6.8) and the three attributed sites of the pass; 5 of 7 at 300 sigma | under 0.35 at 6 sigma on the named set; the read is a record, not a refusal, so no miss rate is claimed for a chain rule | |
|
||||
|
||||
The union bound stays loose where section 2.2 said it would: the two floors' miss rates on the live-dataset classes rest on nine and five cases, and this census adds cases to the exhaustion and bias rows only. The live-dataset census at 2^24 on 64 seeds of two eras of the band (one at width 4, one at R in 1..27 with a 1,000-sigma site) is the next ring-C row (section 7 item 6); it is what turns the bit read's z into a per-era hot-set price on the real item map.
|
||||
|
||||
### 6.4 The lossy-share curve, per shape (build-1, 12:5x to 13:2x BST; 3,000 eras per point, every other axis drawn)
|
||||
|
||||
The band's edge measured as a curve rather than a corner: or, mul and mulhi each raised by the same number of points over the table (the lossy share of the 75 non-load points from 18 to 30), the seven injecting families drawn within B = 4 as before. `r` is the per-candidate rejection, `r^256` the exhaustion figure the independent-attempt arithmetic of spec 1.4.6.6 would give, the measured column what the eras did.
|
||||
|
||||
| Lossy points added | Lossy share of 75 | Shape | Eras | r | r^256 (independent attempts) | Exhausted, measured | Mean attempt | Max | Past attempt 31 |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| +0 (the band) | 18 | 64 x 108 | 980 | 0.550 | 4e-67 | 0 | 1.22 | 14 | 0.0 percent |
|
||||
| +0 | 18 | 128 x 54 | 963 | 0.562 | 1e-64 | 0 | 1.28 | 12 | 0.0 |
|
||||
| +0 | 18 | 256 x 27 | 1,057 | 0.651 | 2e-48 | 0 | 1.87 | 59 | 0.2 |
|
||||
| +1 | 21 | 64 | 1,009 | 0.716 | 7e-38 | 0 | 2.52 | 46 | 0.2 |
|
||||
| +1 | 21 | 128 | 1,015 | 0.768 | 4e-30 | 0 | 3.31 | 47 | 0.1 |
|
||||
| +1 | 21 | 256 | 976 | 0.867 | 1e-16 | 0 | 6.50 | 142 | 2.3 |
|
||||
| +2 | 24 | 64 | 1,010 | 0.772 | 2e-29 | 0 | 3.39 | 52 | 0.4 |
|
||||
| +2 | 24 | 128 | 991 | 0.841 | 5e-20 | 0 | 5.29 | 55 | 1.4 |
|
||||
| +2 | 24 | 256 | 999 | 0.928 | 4e-9 | 2 (0.20 percent) | 12.29 | 249 | 8.8 |
|
||||
| +3 | 27 | 64 | 1,012 | 0.829 | 1e-21 | 0 | 4.85 | 58 | 1.2 |
|
||||
| +3 | 27 | 128 | 980 | 0.885 | 2e-14 | 0 | 7.66 | 73 | 4.1 |
|
||||
| +3 | 27 | 256 | 1,008 | 0.958 | 2e-5 | 6 (0.60 percent) | 21.25 | 241 | 19.6 |
|
||||
| +4 (the corner) | 30 | 64 | 1,004 | 0.872 | 6e-16 | 0 | 6.83 | 56 | 2.5 |
|
||||
| +4 | 30 | 128 | 998 | 0.924 | 2e-9 | 0 | 12.21 | 117 | 7.7 |
|
||||
| +4 | 30 | 256 | 998 | 0.979 | 5e-3 | 33 (3.31 percent) | 38.53 | 244 | 36.7 |
|
||||
|
||||
Readings. (1) The exhaustion is a shape-256 interaction, not a corner-wide one: no era of shape 64 x 108 exhausted at any lossy share (0 of 5,015 across the five points, plus 0 of 2,000 on build-3's shape-64 lossy-cap stratum and 0 of 646 at width 4), one of 4,947 at 128 x 54 (the first corner stratum), and every other exhaustion at 256 x 27. The 256-instruction block holds three times the lossy last writers of a 64-instruction one, and (a')'s fixpoint walks the whole block. (2) The morning's "1,000x above the independent estimate" was the mixed-shape average read against the mixed-shape `r`; per shape the gap is about 6x (0.979^256 = 0.5 percent against 3.3 percent measured at 256 x 27 and +4; 1.7e-5 against 0.6 percent at +3), so one era's attempts are correlated through its weight table by a factor of several, not thousands. The arithmetic of 1.4.6.6 is therefore an under-estimate by that factor at any lossy share, which the band's rule absorbs by holding the share at 18 (where `r` is 0.55 to 0.65 and `r^256` is under 1e-48). (3) The band's edge: at +2 on the 256 shape 8.8 percent of eras pass attempt 31 and 2 of 999 exhaust; the ring-A rule "lossy families never raised above their base" is the measured line, and the research lane's layer 1 carries it (section 5.1b of `docs/design/class-v6-rotating-family.md`). (4) Every exhausted era of every stratum took class v5's last-resort scan and passed at its first candidate past the cap (k = 256) in 38 of 41 cases, at k = 257 or 258 in the other three: the rewrite plus the stale-load repair hands the chain a rule-checked program at once, and the unchecked fallback was never reached.
|
||||
|
||||
### 6.5 The pre-floor spread per width and the width-4 floor (W = 4 words pinned at genesis by the research lane)
|
||||
|
||||
Measured on the fg7 harness (the ratio of every candidate the two floors refuse, beside the accepted draw's), 3,000 eras per width under the band (build-2 `logs/w1band-*`, build-3 `logs/w4band-*`):
|
||||
|
||||
| Width | Candidates | Reaching the 2^20 pass (every earlier part passed) | (c'') refused at 0.98 | (c''') refused at 0.995 | Pre-floor minimum-site ratio of the reaching candidates: p1 / p2.5 / p5 / p10 / median | A floor at 0.990 / 0.993 / 0.995 / 0.996 refuses (percent of reaching candidates) |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 word | 6,920 | 3,093 | 38 (1.2 percent) | 55 (1.8 percent) | 0.968 / 0.993 / 0.9961 / 0.9978 / 0.9999 | 1.8 / 2.4 / 3.0 / 3.9 |
|
||||
| 4 words | 8,270 | 3,646 | 113 (3.1 percent) | 533 (14.6 percent) | 0.912 / 0.971 / 0.985 / 0.991 / 0.9999 | 9.5 / 15.1 / 17.7 / 19.0 |
|
||||
|
||||
Reading. At width 4 the statistic itself moves, not just its tail: a tenth of the candidates that reach the 2^20 pass read a minimum-site ratio under 0.991, against 2 percent at width 1, with the expectation already divided by the width. The aligned index drops the two low address bits, so a site whose source carries its entropy in the low bits reads full at width 1 and concentrated at width 4; the shift is in the programs, not in the formula (the `N - N^2 / 2W` form is 0.07 percent low at `N / W = 1/16`, nothing like the 1 to 9 percent seen). So no single number keeps the clean refusal near the shipped 2.4 percent at width 4: a floor at 0.990 still refuses 9.5 percent of reaching candidates (3.8 percent of all candidates), and the sigma-over-expectation form would re-scale the same distribution without moving its tail. The decision this lane ships for the 09:00 report: keep (c''') at 0.995 at width 4, with its measured cost (14.6 percent of reaching candidates, 6.5 percent of all candidates, +0.3 attempts per epoch, about 0.7 s per epoch per node at 2.2 s per candidate), because the floor's job is soundness and the known-failed hot sets of point A sit at 0.9932 and 0.9945 (section 6.6), inside the band a lower floor would admit; and read the 0.990 to 0.995 band at width 4 on the live dataset (a ring-C row, the census at 2^24 on 64 width-4 seeds) before any floor moves, since whether those candidates are hot sets or thin spreads is exactly the question adv-accept answered at width 1 (7 of 12 deep seeds were thin spreads) and nobody has answered at width 4.
|
||||
|
||||
### 6.6 Ring C: the live-dataset census at two band points
|
||||
|
||||
Point A DONE (build-1, 11:3x to 13:49 BST, 4,783 s on 24 pool cores, about 32 core-hours; `logs/f8-A.log`): attack-f8's 64-seed census (p2 to p65 of the F8 label space, each seed its own era, 2^24 nonces each, the window-model control, the by-site attribution) built against the family crate with the band era 49's weight table (add 17, xor 6, mul 4, mad 13, shfl 9, rotl 9, sub 2, mulhi 2, rotr 9, or 4) at shape 256 x 27 and width 1. The class-v5 tree's copy of attack-f8 predates the `--class v5` path, so the draws are generator 4 under the family's shape predicate: sub-version 3's rule with (c'') at 0.98 and WITHOUT class v5's (c''') floor at 0.995. Result: 45 of 64 PASS with no test fired; 3 beyond 1.2x of the window model at the top 0.1 percent (p38 1.678x, p54 1.589x, p4 1.297x; the shipped class read 4 of 64 over, p4 among them at 1.217x); HOT SETS in 2 (p54: X at 0.1 percent +0.120, X at 1 percent +0.684; p38: +0.102 and +0.576), against 0 of 64 on the shipped class; windowed 64-item buckets beyond 6 sigma in 18 of 64 (the bit-level class of section 6.2 seen on the live item map: p38's largest bucket +106 sigma, p4's +84), against the shipped class's 4. The class v5 floor's read on the same 64 programs (`v5_hot_census` under the same weight table, build-3, queued at 13:5x BST) says whether (c''') refuses p38 and p54 as it refused the nine hot sets of the in-house pass; LANDED 14:3x BST (build-3, 79 s on 8 threads, `logs/floor-read-A.log`): the class v5 floor at 0.995 refuses 3 of the 64 class v4 draws, p38 at 0.9932 (site 0), p54 at 0.9945 (site 12) and p11 at 0.9933, so BOTH hot sets of point A are refused and the class v5 draw moves to the next attempt (min ratio 0.9999 on each); p4 at 0.9963 passes, as it did on the shipped class. The floor does under the family's weights what it did on the shipped class: nine of nine then, two of two now. What a chip gets from p54 as it stands (on the class v4 rule; under class v5 it never runs): the top 1 percent of items (16 MiB) holding 0.68 percent more of the reads than the window model, the 1.002x class of the ledger, no row of the chip model moves; what an auditor sees is two hot sets in 64 epochs where the shipped class had none, which is the per-era weight axis showing on the item map and the reason the floor is a per-epoch rule and not a per-class census.
|
||||
|
||||
Point B DONE (build-3, 12:2x to 15:30 BST on 8 then 16 pool cores; `logs/f8-B.log`, `logs/floor-read-B.log`): the same census at shape 64 x 108 with the band era 5's weight table (add 11, xor 15, mul 6, mad 9, shfl 7, rotl 11, sub 4, mulhi 6, rotr 2, or 4). Result: 46 of 64 PASS with no test fired; HOT SETS in 0; 5 beyond 1.2x of the window model at the top 0.1 percent (p10 1.505x, the seed the shipped class reads at 1.504x with the same hottest item; p42 1.472x; p64 1.351x; p18 1.215x; one more at 1.2x to 1.21x), against the shipped class's 4 of 64; windowed buckets beyond 6 sigma in 18 of 64 (p64's largest bucket +150 sigma, p42's +105, p10's +100: the bit-R class again); the class v5 floor refuses 1 of the 64 draws (p15 at 0.9934, the draw moving to attempt 4 at 0.9993), the next lowest 0.9957 to 0.9962 pass. So the 64-instruction shape under a band table reads the shipped class's own profile on the live item map (the same tail seeds over 1.2x, no hot set, the same bucket class), and the shape axis moves the draw's cost (section 6.4) and nothing on the item map. Across the two points: 128 live epochs of the band, 2 hot sets (both at the 256 shape, both refused by the floor), 8 over 1.2x (the shipped class: 4 of 64), 36 with a site over 6 sigma on the windowed buckets (the shipped class: 4 of 64; the bit-R mechanism of section 6.8, which the family's weight tables widen from a 6 percent to a 28 percent share of epochs on the live map); chip price of the worst: the 1.002x class of the ledger. What this ring-C pair bounds: the fraction of band epochs with a hot set under the class v4 rule is under 3/128 = 2.3 percent at 95 percent, and 0 of 2 reached the chain under class v5's floor.
|
||||
|
||||
|
||||
### 6.7 The mixer axis per family: the verifier rows at x4, x8 and x16 (build-3, 15:1x BST, `logs/bench-x{4,8,16}.log`)
|
||||
|
||||
`igneum-pow bench --seed igneum-genesis --day 2026-10-03 --class v2m<m>g+sh256x27 --warps 50` (the fg7 CLI; `v2m8g+sh256x27` is the class v4 shape and prints as `mx8+sh256x27`), one leased core of build-3 (AMD Ryzen 9 7950X3D, the box at load from the point-B census and the strata; the cold warp is the ladder's method, one warp per vector base, then the average of 50):
|
||||
|
||||
| Mixer `m` | Mixers per item | Cold warp, ms (base 0 / 4,096 / 1,000,000) | Average of 50, ms | Against x8 | Scaled to the 2019-class core (the O-1.14 i7-9700K read x8 at 6.33 cold) | Scaled to the half-core proxy (x8 at 8.23) | The 10 ms gate |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 4 | 36 | 4.07 / 3.65 / 3.66 | 3.73 | 0.91x | about 5.7 | about 7.5 | passes with margin |
|
||||
| 8 (the shipped class) | 72 | 4.37 / 4.16 / 4.11 | 4.12 | 1.00x | 6.33 (measured) | 8.23 (measured) | passes |
|
||||
| 16 | 144 | 7.43 / 7.23 / 7.15 | 7.13 | 1.73x | about 11.0 | about 14.2 | FAILS on both proxies (approximate: a ratio scaling; the x16 row on the proxies themselves is the hash lane's to take) |
|
||||
|
||||
Reading. The mixer's cost sits in the item derivation, which the verifier pays in full (4,096 items per warp) and the card hides behind the dataset build: x16 adds 3.0 ms per warp on this core, 73 percent over x8, so on the 2019-class core and the half-core proxy it lands over the 10 ms gate by one to four milliseconds. The band for `m` is therefore {4, 8} on the verifier's measured rows, with 16 outside it until a verifier change (or the ladder's rung 0 budget re-cut) brings it under 10 ms; x4 keeps 2 of 4 applications of measured margin (adv-mixer-3's rows clean from k = 2) against x8's 6 of 8, so the family's safe band on this axis is x8 with x4 as the one corner, and the chip row of chip-model-v3 (the f = 0 recompute chip's rate halving per doubling, the f = 1 chip unmoved) is unchanged by the draw. Per tier: nothing moves for a card (the item derivation is the daily build, 42 ms at x8 on the 5090); a node verifying x4 saves 10 percent of a warp, x16 would cost it 73 percent more and the 2019-class tier its margin.
|
||||
|
||||
### 6.8 The two next-class tests on one scale: the known-failed cases through the sigma form (build-2, 14:2x BST, `logs/f8space-*`)
|
||||
|
||||
The F8 label space's p2 to p65 and p212 to p225 drawn at the shipped parameters through the family harness (shape 256 x 27, m = 8, width 1, the table weights, B = 0; every attempt equal to the chain's class v5 draw, p2 at 5, p3 at 1, p4 at 0, p8 at 2, p10 at 0, p212 at 4, p225 at 0), read at the rule's own 2^20 sample: the minimum (c'') ratio, the largest 4,096-word bucket in sigma of its window expectation, the largest index-bit one-count excess in sigma.
|
||||
|
||||
| Seed | What it is | min ratio, site | Largest bucket: ratio, sigma, site (window) | Bit bias: sigma, site, bit | R | Bucket band at +8 | Bit band at 6 sigma | Bit band at 300 sigma |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| p4 | the morning's tail (1.217x live) | 0.9961 at site 1 | 7.17x, +49.4, site 1 (quarter) | -511, site 1, bit 13 | 13 | caught | caught | caught |
|
||||
| p8 | the tail (1.378x) | 0.9963 at site 14 | 3.72x, +21.8, site 14 (quarter) | -513, site 6, bit 22 | 22 | caught | caught | caught |
|
||||
| p10 | the tail (1.504x) | 0.9963 at site 8 | 6.27x, +42.1, site 8 (quarter) | -513, site 8, bit 17 | 17 | caught | caught | caught |
|
||||
| p34 | the tail on sub-version 3 (1.250x; not on class v5's list) | 0.9999 | 2.38x, +5.5 (clean) | +10.8, site 4, bit 7 | 7 | missed | caught | missed |
|
||||
| p212 | the hash lane's F8-256 attribution: site 9's bucket at 1.75x its window expectation, 0.019 bits short | 0.9958 at site 9 | 1.72x, +5.75, site 9 (quarter): the clean spread's own maximum | -512, site 9, bit 8 | 8 | missed | caught | caught |
|
||||
| p225 | the hash lane's value-level concentration at a mad-written site, the bucket at expectation | 0.9974 at site 4 | 2.38x, +5.5 (clean) | -567, site 4, bit 2 | 2 | missed | caught | caught |
|
||||
| p23, p19, p18, p56 | the strong seeds of sub-versions 1 and 2, closed by (a') and (c'') | 0.9999 to 1.0000 | +5.1 to +5.5 (clean) | under 4.4 | | clean | clean | clean |
|
||||
| p15 | the same | 0.99997 | +6.0 | -58, site 0, bit 0 | 26 | clean | caught | missed |
|
||||
|
||||
Reading. (1) Every named case is the SAME mechanism at the bit level: a product's bit 0 (P = 1/4, z = -512 at 2^20 exactly: (0.25 - 0.5) x 2^20 / 512) or bit 1 at address bit R of one site; the bucket statistic sees it only when R lands inside the 4,096-word bucket's bits at a narrow window (p4, p8, p10 at bits 13, 22, 17 on quarter windows) and not when it lands below bit 12 (p212 at bit 8, p225 at bit 2). So the bucket bound at any band catches a subset of what the bit read catches, and p212 and p225 are exactly the cases the research lane asked about: a bucket bound at about 2x (+8 sigma at 2^20) refuses p4, p8 and p10 and neither of the two; a bucket bound at 1.5x would take p212 (1.72x) at a cost the random stratum prices at 7.3 percent of eras over +15 sigma and 14.5 percent over +8 (the biased population, section 6.2), not the 3 to 6 percent of a clean spread. (2) The bit read at 6 sigma catches all seven named cases (p34 and p15 too); its cost is the family's standing fact, 48 to 52 percent of eras over 6 sigma, 34 percent over 100, 16 percent over 300, 11 percent over 500 (random stratum, 10,000 eras; the band 47.7, 31.1, 15.7, 11.1; width 4 under the band 44.9, 26.5, 9.4, 5.6). A band at 300 sigma catches p4, p8, p10, p212, p225 (all at or above 511) and misses p34 (+10.8) and p15 (-58), at a redraw cost of 16 percent of epochs; the clean-era maximum over 26 bits and 16 sites is 5.5 sigma at p99, so nothing clean is refused at any band from 6 up. (3) The bucket excess beyond what the bit read already names is 0.07 to 0.23 percent of eras (bucket over +8 with every bit under 6 sigma: 15 of 10,000 random, 7 of 3,000 band, 2 of 3,000 at width 4), which is the bucket bound's whole independent catch; a chip reads nothing in it that the bit read did not price. The decision this lane ships: ONE value-level test, the per-site index-bit read, as a per-era RECORD at the acceptance's 2^20 sample (its z at bit R names the product writer for the record) and as the structural lever's known-failed set (the seven seeds above), with the bucket bound retired into it; the refusal band, if the research lane wants one before `load_index`'s fold lands, is 300 sigma (five of seven named cases, 16 percent of epochs redrawn) and never 6 (half the epochs).
|
||||
|
||||
## 7. The 16:30 BST report (the founder's clock, pulled from 09:00; a partial carries its count)
|
||||
|
||||
1. DONE 13:2x BST: the lossy-share curve per shape (section 6.4) and the last-resort scan's verdicts on every exhausted era.
|
||||
2. DONE 14:4x BST: the pre-floor spread at width 4 against width 1 under the band and the floor decision (section 6.5: 0.995 kept at width 4 at its measured cost; the 0.990 to 0.995 band at width 4 read live before any floor moves).
|
||||
3. DONE 15:30 BST: the two ring-C live-dataset rows (section 6.6: 128 live epochs of the band, 2 hot sets at the 256 shape both refused by the floor, 0 at the 64 shape).
|
||||
4. DONE 15:1x BST: the x4, x8 and x16 verifier rows (section 6.7, build-3 one core: 3.73 / 4.12 / 7.13 ms per warp; x16 outside the band on the 10 ms gate by scaling to the proxies).
|
||||
5. DONE 14:4x BST: the width-4 plus shape-64 crossing (build-4, 3,000 eras: r = 0.654, 0 exhausted, (c''') 5.3 percent of candidates, the width's floor cost again) and section 6.8, the F8 label space's p2 to p65 and p212 to p225 through the sigma form at the shipped parameters (build-2, queued 14:0x BST), so the two F8-256 attributions the hash lane added to layer 4's bucket-bound row (p212, site 9 at 1.75x its window expectation; p225, a value-level concentration at a mad-written site with the bucket at expectation) and the morning's four tail seeds are read as known-failed cases of the bucket-sigma and bit-bias tests on one scale; and the bucket bound in sigma (from the bit-clean spread: p99 +6.8, p99.9 +16.5 to +20.1, max +35 over 9,409 eras; a band at +8 refuses 0.3 to 0.4 percent of bit-clean programs and 11 to 18 percent of all, which is the biased population, so the bucket bound and the bit read are one rule).
|
||||
6. The gate-record form: `logs/gate-records-lossy-base.jsonl` (3,000 records of the band stratum in the section 4.3 shape, one per line, 3.6 MB) and `logs/fg-records.py`, which derives every other stratum's records from its TSV rows in this directory.
|
||||
7. Amendment by 18:00 BST (the coordinator's order of 15:45): the research lane's best-mix genesis table (add 16, xor 14, mad 12, rotl 11, sub 10, rotr 10, shfl 4, mul 4, mulhi 2, or 0, sum 83; renormalised to 75 by largest remainder as 14, 13, 4, 11, 3, 10, 9, 2, 9, 0 in the generator's order) through the acceptance at the shipped parameters: the attempts census with the refused-ratio column at widths 4 and 1 (1,500 eras each, build-3, the fg8 harness), the index-bit read, and attack-f8 at 2^20 on 64 seeds; running from 15:5x BST, the verdict to the coordinator, the hash, census and research lanes with its minute.
|
||||
8. Not in this report: the mixer ladder re-run at m = 4 and 16 (adv-mixer-3's harness rows; the per-family rows of section 4), the F8 census at D = 29 (the stand-in gap row), the 10^5-program verifier census at the top corner (F6 per family): each a named per-family row with its hours in section 4.1.
|
||||
|
||||
## 8. Sources
|
||||
|
||||
|
|
|
|||
401
docs/analysis/class-v6/floor/denominator.md
Normal file
401
docs/analysis/class-v6/floor/denominator.md
Normal file
|
|
@ -0,0 +1,401 @@
|
|||
# Class v6 floor lane 4: the honest denominator (8 October 2026)
|
||||
|
||||
Branch `class-v6-floor-denominator` from `counter-asic-4` (91117093). The question: every chip edge in the record is a
|
||||
ratio over the honest tier's microjoules per hash, and the honest tier's own operating point is half the resistance,
|
||||
the half the project controls through Ember's tuning. This file finds how low the honest joule goes per card class
|
||||
today with software alone, under class v5, and reads the chip edge against each row; models the LPDDR6 and
|
||||
unified-memory tier five years out; and lands the Ember tiers statement as a table the app reads
|
||||
(`app/igneum-app/tiers/class-v5-tiers.json`, its validator and test beside it). Every figure is labelled measured (with
|
||||
its source) or estimated (with its method). Nothing here moves a consensus object or a served text. Times are UK (BST).
|
||||
|
||||
## 0. One page
|
||||
|
||||
1. **The honest floor under class v5, measured today:** the RTX 5080 at an 1,100 MHz core lock reads 2.06 microjoules
|
||||
per hash on class v4's shape (71.20 MH/s at 146.6 W) and about 2.10 under class v5; the RTX 5090 at its 1,300 MHz
|
||||
knee 2.37 (134.76 MH/s at 318.8 W, the v5lock job's +2.0 percent over class v4 applied); the RTX 4070 at its tune
|
||||
point 3.51 to 3.58; the RX 9070 XT at the ADLX grid's best point 7.9 to 8.1; the Apple M5 Max 1.40 to 1.43 at the
|
||||
GPU-plus-DRAM meter with no lever. The class v5 STOCK column is now measured on 14 card classes (the rented sweep,
|
||||
13:4x to 14:0x UK, every host refusing the lock): the 5080 3.48, the 4070 5.82, the 4080 4.92, the 5060 3.76, the
|
||||
5060 Ti 4.02, the 3090 5.03, the 3060 6.40, the L40S 5.29, the H100 2.58, the A100 2.99, the 4090 5.0 (lane 1), so
|
||||
the lock is worth 34 to 41 percent of a Blackwell or Ada card's class v5 draw (the 5080 3.48 to 2.10, the 4070 5.82
|
||||
to 3.58) for under 2 percent of rate, and the stock column is a different card from the floor column. The 5080
|
||||
beats the 5090 per joule at the knee, and the 16 GB and 12 GB Blackwell cards (5070 Ti, 5070) read lower still by
|
||||
the Blackwell shape (1.7 to 2.1, estimated): **the honest NVIDIA floor is the mass-market Blackwell card at its
|
||||
knee, not the 5090.**
|
||||
2. **The chip edge against the honest floor, class v5, the chip's shadow core priced in absolute terms** (3.2 pJ per
|
||||
forced op, the record's k = 0.5 at the 5090's knee; and 6.4 pJ, k = 1): the GDDR7 stored-dataset board reads 2.7x /
|
||||
1.9x against the 5080's floor, 3.0x / 2.1x against the 5090's, 1.8x / 1.3x against the M5 Max; one HBM3 stack 3.3x /
|
||||
2.2x, 3.6x / 2.4x, 2.2x / 1.5x; the N2 SRAM die 4.5x / 2.7x, 5.0x / 3.0x, 3.1x / 1.8x. The served 2.1x (the 5090 at
|
||||
the knee, k = 1) stands; the honest sentence per joule is now "under 2x against a 16 GB Blackwell card at its knee
|
||||
for a core as good as a GPU lane, 1.3x against an Apple laptop".
|
||||
3. **What moves the floor is the operating point and nothing else in software.** The SM-sparse kernel is dead on
|
||||
measured rows (the draw follows the work, not the SM count; research file 20.3b; floor lane 1's rows of 13:25 on a
|
||||
rented 4090 and H100 say the same at stock: the best occupancy shape saves 1 to 2 percent of the watts and the draw
|
||||
over idle is flat across the SM count while the rate holds, so the residual is the clock domain, reachable by the
|
||||
lock and the undervolt only); the cache-policy hint is dead; the
|
||||
memory clock is never touched (a step that drags it down cannot win); the power cap is flat on Blackwell at the hash
|
||||
(0.41 MH/W under every limit on the 5090) and is the only lever on Ampere; the core lock walks the driver's V/F
|
||||
curve and is the lever on Blackwell and Ada; AMD has the ADLX offsets (measured today: 24 percent of the 9070 XT's
|
||||
draw for no rate); Apple and Intel have no lever in the app.
|
||||
4. **Five years out the honest tier is the unified-memory SoC on LPDDR6**, modelled at about 1.2 microjoules per hash
|
||||
under class v5 at the meter for an Apple Max-class part (0.27 of DRAM reads, 0.35 of GPU issue, 0.56 of shadow), 1.6
|
||||
to 1.7 at the package; the AMD, Qualcomm, MediaTek and NVIDIA LPDDR SoCs model 3 to 10 microjoules because their
|
||||
GPUs hide the latency worse, not because of the memory. Against that tier the GDDR7 chip reads 1.5x / 1.06x, one
|
||||
HBM3 stack 1.8x / 1.2x, the N2 SRAM die 2.5x / 1.5x, the custom HBM4E base die 2.4x / 1.4x. The class should size the
|
||||
shadow per tier with the Apple tier as the binding ceiling (it already does by the 2.0 rule: N at or under 130,000
|
||||
ops) and state resistance against the best honest joule; it should NOT score the acceptance floor per tier (one
|
||||
network-wide number, the same for every honest card). Pricing the Apple-bound shadow for the 5090 tier: N = 130,000
|
||||
instead of 100,000 costs the 5090 +0.19 microjoules at the knee (+8 percent of its electricity) and moves its GDDR7
|
||||
edge at k = 1 from 2.1x to 1.95x.
|
||||
5. **The tiers for Ember** (`app/igneum-app/tiers/class-v5-tiers.json`): per card class the Efficiency, Balanced and Max
|
||||
rows in the app's own tier shape, measured on the 5090, 5080, 4070, 9070 XT and M5 Max, estimated from the measured
|
||||
stock rows and the architecture's shape elsewhere (Blackwell: lock 1,100 to 1,300 MHz, power 100 percent; Ada: lock
|
||||
1,860 to 2,400 and 50 to 80 percent; Ampere: 80 to 90 percent and no lock below 1,800 MHz; AMD: -500 MHz and -30
|
||||
percent; Apple and Intel: stock). The re-measure rule: a set measured under one class never applies under another
|
||||
(the app's `tiers_stale`); the stored point is the provisional start of the re-measure, the Max tier is stock and never
|
||||
stale; the measured v4 to v5 flip on the 5090 moved the knee by nothing (0.0 percent of rate, +2.0 percent of watts).
|
||||
|
||||
## 1. The method and the denominators
|
||||
|
||||
| Quantity | Value | Label | Source |
|
||||
|---|---|---|---|
|
||||
| Class v5 | `mx8-era<seed>+sh256x27+state`: class v4's shape (64-instruction base, a 256-instruction shadow block x27, 102,100 counted ops per hash) plus the state leaves; the worker uploads the leaves for the dataset build and frees them | measured (the kit's RESULT line on a rented 4090 and 23 other cards: every v5 fingerprint MATCH) | `~/igneum-fleet/cv5-results.txt`; `~/igneum-fleet/cardbench/rows.jsonl` 8 October |
|
||||
| Class v5 over class v4 at the same point | 0.0 percent of rate, +2.0 percent of watts (the 5090 at the 1,300 MHz lock: 125.93 MH/s at 299.8 W against 125.92 at 294.0) | measured | the hash lane's v5lock job, 8 October; `scratch v4/ca4-v6-cost-rows.md` |
|
||||
| The 5090 at class v4, the core-clock grid | unlocked 136.84 MH/s at 475.5 W; 1,300 MHz 134.76 at 312.5 (the knee, rate -1.5 percent); 1,200 MHz 133.80 at 305.1 (the best MH per watt, -2.2 percent); 1,100 MHz 122.43 at 287.3 (-10.5 percent); the class v4 premium 145.3 W unlocked, 81.8 W at the best points | measured, PC 1, 7 to 8 October | `docs/bench-log.md`, the class v4 efficiency passes |
|
||||
| The 5080 at class v4, the core-clock grid | unlocked 71.41 at 253.1 W; 1,100 MHz 71.20 at 146.6 (the best MH per watt, -0.3 percent); 1,000 MHz 71.19 at 149.0; 900 MHz 67.66 at 137.8 (-5.2 percent); the draw floors from 1,500 MHz down; the premium 83.4 W unlocked and 41 W at the best points | measured, PC 1, 8 October 00:45 to 01:54 UTC | the same |
|
||||
| The 4070 at its Ember tune (1,860 MHz lock, the 50 percent cap) | class v3 30.95 MH/s at 79.5 W (2.57); class v4 31.08 at 109.0 (3.51): +30 W for no rate; stock class v3 28.7 at 102.7 (3.58) | measured, PC 1, 6 October | `docs/plans/counter-asic-3-status.md` item 8; Ember run 6 |
|
||||
| The 9070 XT on the ADLX grid | 24 rows, the rate flat at 18.9 MH/s, the best point -500 MHz core and -30 percent power = 149.3 W (7.9), 24 percent under the 202 W stock point (10.7) | measured, PC 1, 8 October 12:13 | the hash lane's AMD grid job |
|
||||
| The M5 Max | class v3 27.08 MH/s at 21.0 W (0.78), class v4 26.67 at 37.3 W (1.40) at the GPU and DRAM channels of the IOReport meter; the package about 17 W more; marginal 6.9 pJ per counted op | measured, 6 October | `docs/analysis/latency-shadow-2026-10-06.md` section 3 |
|
||||
| The rented stock rows (class v3 and the class v4 shape, 24 card classes) | `rows.jsonl`, 7 and 8 October: three timed runs, an nvidia-smi sampler at 1 Hz, the three power fields; no host allowed `-lgc` ("The current user does not have permission to change clocks") so no rented row carries a knee | measured (stock only) | `~/igneum-fleet/cardbench/rows.jsonl`; `bench-table.md` |
|
||||
| The rented against the PC spread | the 5080 read 143.4 W at 71.16 MH/s on a rented Linux host and 169.7 W at 71.28 on PC 1 (Windows) on class v3: 18 percent apart at the same rate; the 5090 the same shape (258 W rented at 0.3.12 against 312 W on the PC) | measured; neither corrected | `site/miner-bench.json` notes |
|
||||
| The chips at zero shadow | GDDR7 board 0.466 microjoules per hash; one HBM3 stack 0.321; the N2 SRAM die 0.14 | modelled | chip-model-v3 5.4 and 5.12; `class-v6/hardware-future.md` section 5 |
|
||||
| The chip's shadow under class v5 | 101,170 forced ops per hash (102,100 minus the 930 base) at 3.2 pJ (k = 0.5 against the 5090's measured 6.4 pJ per op at the knee: the record's centre band) = 0.324 microjoules; at 6.4 pJ (k = 1) = 0.648 | modelled on the measured GPU rows | research file 15.1a, 20.4 |
|
||||
| The chips under class v5 | GDDR7 0.79 / 1.11; HBM3 0.65 / 0.97; N2 SRAM 0.46 / 0.79 (k = 0.5 / k = 1) | modelled | this file |
|
||||
|
||||
Reading the identity. The chip's energy is `E_mem + c x N` with `c` the chip core's picojoules per forced op and `N`
|
||||
the op count, a number that does not depend on which card is the reference; so the chip's microjoules per hash are
|
||||
fixed by the class (0.79 to 1.11 on GDDR7 under class v5) and every row's edge is just that card's microjoules over
|
||||
them. The record's `k` columns are this `c` divided by the reference card's own picojoules per op: k = 1 at the 5090's
|
||||
knee is 6.4 pJ, k = 1 at the unlocked 5090 would be 10.8 pJ, k = 1 on the M5 Max 6.9 pJ. Stating the chip in absolute
|
||||
picojoules keeps the comparison honest across cards.
|
||||
|
||||
What "every software knob" is, per vendor. NVIDIA through nvidia-smi: `-pl` (the power cap, percent of the default
|
||||
limit, floor 50 percent, min 400 W on a 5090), `-lgc 0,<MHz>` (a core-clock cap that walks the driver's V/F curve;
|
||||
administrator rights, so only through the Power Helper on Windows and never on a rented Linux host), `-lmc` (the
|
||||
memory clock, never touched: a step whose memory clock falls under 95 percent of baseline is marked and cannot win);
|
||||
no voltage offset (that is NVAPI's). AMD through the ADLX line in igneum-gpu-telemetry: `--set-gmax` (a core offset)
|
||||
and `--set-plimit` (a power offset), no memory knob on RDNA 4, no elevation. Apple: measure only. Intel: measure only.
|
||||
The kernel-side knobs the record closed: the SM-sparse variant (dead: the energy per hash never falls below base on
|
||||
either class, 20.3b), `ld.global.cs` (dead: equals base within 1 W, 20.3c), the block shape (64 to 256 instructions,
|
||||
fixed by the class), the occupancy (one warp per block, 24 blocks per SM, fixed by the worker's race).
|
||||
|
||||
## 2. The table: every card class under class v5, the lowest microjoules per hash with the knobs, and the chip edge
|
||||
|
||||
"Floor" is the lowest class v5 energy per hash the card reaches with the knobs available to it today; "stock" is the
|
||||
card unlocked at 100 percent under class v5 (the sweep's measured rows, 13:4x to 14:0x UK, say "stock, lock owed" in
|
||||
the table: no rented host allowed the lock). A measured row cites its job; an estimated row names its method and its
|
||||
band. The chip columns divide the row's floor by the chip's class v5 energy at three core figures: 1.1 pJ per forced op
|
||||
(the k lane's synthesised sequencer-core floor at N3, the pessimistic column), 3.2 pJ (the record's k = 0.5) and 6.4
|
||||
pJ (k = 1, a core as good as a GPU lane); `E_mem` 0.466, 0.321 and 0.14 microjoules. The Apple rows are at the GPU-plus-DRAM meter (the package adds about 17 W on the M5
|
||||
Max; the wall more); the NVIDIA and AMD rows are whole-card.
|
||||
|
||||
| Card | Tier | Class v5 floor, microjoules per hash | Label | Class v5 stock, microjoules per hash (label) | GDDR7 board at 1.1 / 3.2 / 6.4 pJ per forced op | HBM3 one stack | N2 SRAM die | The point, the band and the source |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| NVIDIA GeForce RTX 5090 | 32 GB | 2.33 | measured | 3.48 (measured) | 4.0x / 3.0x / 2.1x | 5.4x / 3.6x / 2.4x | 9.3x / 5.0x / 3.0x | 1200 MHz at 100 percent. class v4 best MH per watt (133.80 MH/s at 305.1 W, 2.2 percent of rate given); under class v5 +2.0 percent watts: 311.2 W, 2.33 Stock: the power cap is flat on this hash (0.41 MH/W under every limit), so no power rung is used on any tier |
|
||||
| NVIDIA GeForce RTX 5080 | 16 GB | 2.06 | measured | 3.48 (measured) | 3.6x / 2.6x / 1.9x | 4.8x / 3.2x / 2.1x | 8.2x / 4.4x / 2.6x | 1100 MHz at 100 percent. class v4 best MH per watt; the draw floors from 1,500 MHz down; the knee is between 1,000 and 900 MHz (900 costs 5.2 percent); class v5 about 2.10 Stock: class v5 measured on a Vast 5080 (13:49 UK): 71.35 MH/s at 248.0 W (3.48, 39 samples, SM 2,769, memory 14,801); class v3 71.16 at 159.4 W on the same host; PC 1 read class v4 71.41 at 253.1 W: the three agree within 3 percent on this card; stock, lock owed on the rented host |
|
||||
| NVIDIA GeForce RTX 5070 Ti | 16 GB | 1.7 | estimated | 2.84 (measured) | 2.9x / 2.2x / 1.5x | 3.9x / 2.6x / 1.8x | 6.8x / 3.7x / 2.2x | 1100 MHz at 100 percent. the Blackwell shape applied to the measured stock rows (the v3 draw x0.63, the premium x0.5): band 1.6 to 2.0; the rented host refused -lgc, so the knee is the search's to find Stock: rented pod 8 October 10:09Z: class v3 78.69 MH/s at 140.8 W, class v4 78.78 at 224.0 W; the class v5 sweep rented this card twice (14:08 and 14:13 UK) and neither host ever accepted the fleet key |
|
||||
| NVIDIA GeForce RTX 5070 | 12 GB | 1.75 | estimated | 2.99 (measured) | 3.0x / 2.2x / 1.6x | 4.0x / 2.7x / 1.8x | 7.0x / 3.8x / 2.2x | 1100 MHz at 100 percent. the Blackwell shape on the measured stock rows (paired class v3 113 W): band 1.7 to 2.1 Stock: rented v4watts pod 8 October; the 7 October pod read class v3 52.0 MH/s at 102.8 W (the rate differs by host); the class v5 sweep rented this card twice (14:08 and 14:13 UK) and neither host ever accepted the fleet key |
|
||||
| NVIDIA GeForce RTX 5060 Ti | 16 GB | 2.36 | estimated | 4.02 (measured) | 4.1x / 3.0x / 2.1x | 5.5x / 3.7x / 2.4x | 9.4x / 5.1x / 3.0x | 1100 MHz at 100 percent. the Blackwell shape on the PC 2 stock row: band 2.2 to 2.6; re-based on the measured class v5 stock row of 13:5x UK (the architecture's shape on the measured v3 draw and the measured premium) Stock: class v5 measured on a Vast 5060 Ti (13:5x UK): 30.84 MH/s at 123.9 W (4.02, 96 samples, SM 2,835); class v3 30.75 at 82.6 W on the same host; PC 2's enclosure row read class v4 30.9 at 114.8 W; stock, lock owed |
|
||||
| NVIDIA GeForce RTX 5060 | 8 GB | 2.21 | estimated | 3.76 (measured) | 3.8x / 2.8x / 2.0x | 5.1x / 3.4x / 2.3x | 8.8x / 4.8x / 2.8x | 1100 MHz at 100 percent. class v3 measured 31.27 MH/s at 75.4 W; re-based on the measured class v5 stock row of 13:5x UK (the architecture's shape on the measured v3 draw and the measured premium) Stock: class v5 measured (13:51 UK): 30.80 MH/s at 115.7 W (3.76, 99 samples, SM 2,715, memory 13,801); class v3 30.76 at 78.8 W on the same host (the shadow +37 W = 12 pJ per op); stock, lock owed |
|
||||
| NVIDIA GeForce RTX 4090 | 24 GB | 3.58 | estimated | 5.0 (measured) | 6.2x / 4.5x / 3.2x | 8.3x / 5.6x / 3.7x | 14.2x / 7.7x / 4.5x | 1860 MHz at 60 percent. the Ada shape (the 4070's measured tune: v3 x0.72, the premium x0.7) on the measured stock rows (class v3 253.4 W, class v4 353.5 W, floor lane 1, 8 October): band 3.3 to 4.0; re-based on the measured class v5 stock row of 13:5x UK (the architecture's shape on the measured v3 draw and the measured premium) Stock: floor lane 1's rented 4090 at a 450 W limit: class v3 70.63 MH/s at 253.4 W (3.59), class v4 70.63 at 353.5 W (5.0); the denominator sweep's Vast 4090 carried a 250 W host limit: class v5 62.68 at 249.9 W (3.99, the SM at 2,579 MHz) and class v3 62.40 at 200.5 W, so a 250 W cap held 89 percent of the rate at 71 percent of the draw (an Ada cap row measured); stock, lock owed |
|
||||
| NVIDIA GeForce RTX 4080 | 16 GB | 3.51 | estimated | 4.92 (measured) | 6.1x / 4.4x / 3.2x | 8.1x / 5.4x / 3.6x | 14.0x / 7.6x / 4.5x | 1860 MHz at 60 percent. the Ada shape on the measured stock rows (class v3 128.6 W, class v4 185.6 W): band 3.1 to 3.7; re-based on the measured class v5 stock row of 13:5x UK (the architecture's shape on the measured v3 draw and the measured premium) Stock: class v5 measured (13:50 UK): 40.80 MH/s at 200.7 W (4.92, 72 samples, SM 2,760); class v3 40.69 at 141.5 W on the same host; the v4watts pod read class v4 40.74 at 185.6 W; stock, lock owed |
|
||||
| NVIDIA GeForce RTX 4070 | 12 GB | 3.58 | measured | 5.82 (measured) | 6.2x / 4.5x / 3.2x | 8.3x / 5.6x / 3.7x | 14.2x / 7.7x / 4.5x | 1860 MHz at 50 percent. Ember run 6's tune point (1,863 MHz at 50 percent) under class v4 (sh256x27): 31.08 MH/s at 109.0 W, +30 W over class v3 at the same point for no rate; class v5 about 3.58; the ladder below 1,860 under class v4 is owed (band 3.2 to 3.6) Stock: class v5 measured on a Vast 4070 (13:54 UK): 28.43 MH/s at 165.5 W (5.82, 108 samples, SM 2,820, memory 9,801, a 210 W host limit not binding); class v3 28.40 at 116.3 W on the same host (the shadow +49 W = 17 pJ per op at the stock clock); Ember run 6's PC 1 stock on class v3 read 28.72 at 106.0 W; the tune point (1,860 MHz, 50 percent) under class v4 measured 31.08 at 109.0 W (3.51), so the lock takes 34 percent of this card's class v5 draw for no rate |
|
||||
| NVIDIA GeForce RTX 4070 Ti | 12 GB | 3.6 | estimated | 4.97 (measured) | 6.2x / 4.6x / 3.2x | 8.3x / 5.6x / 3.7x | 14.3x / 7.8x / 4.6x | 1860 MHz at 60 percent. the Ada shape on the measured stock rows (class v3 95.4 W, class v4 155.3 W); the 4070 Super at a 110 W host cap held 31.26 MH/s at 108.4 W under class v4 (3.47): a cap row measured on a sibling Stock: rented pods 8 October |
|
||||
| NVIDIA GeForce RTX 4060 Ti | 16 GB | 3.81 | estimated | 5.32 (measured) | 6.6x / 4.8x / 3.4x | 8.8x / 5.9x / 3.9x | 15.2x / 8.2x / 4.8x | 1860 MHz at 60 percent. the Ada shape (the 4070's measured tune: v3 x0.72, the premium x0.7) on the measured class v5 stock row; band 3.5 to 4.2 Stock: class v5 measured on a Vast 4060 Ti (14:27 UK): 18.75 MH/s at 99.8 W (5.32) under a 140 W host limit; class v3 18.74 at 79.1 W on the same host (the shadow +21 W = 11 pJ per op; this host read 7 percent under the 7 October pod's 20.10 MH/s); the v4watts pod read class v4 20.10 at 102.3 W (5.09); stock, lock owed |
|
||||
| NVIDIA GeForce RTX 4060 | 8 GB | 3.77 | estimated | 4.98 (estimated) | 6.5x / 4.8x / 3.4x | 8.7x / 5.8x / 3.9x | 15.0x / 8.1x / 4.8x | 1860 MHz at 60 percent. 19.09 MH/s measured on three rented hosts, the watts unread on every one (no power sensor): the watts are the 4060 Ti's shape; band 3.7 to 4.2 Stock: watts OWED: every rented 4060 host exposed no power sensor |
|
||||
| NVIDIA GeForce RTX 3090 | 24 GB | 4.51 | estimated | 5.03 (measured) | 7.8x / 5.7x / 4.1x | 10.4x / 7.0x / 4.7x | 17.9x / 9.7x / 5.7x | 1800 MHz at 80 percent. the Ampere cap lever (10 to 15 percent of the draw; a cap that takes the SM under 1,800 MHz costs rate) on the measured class v5 stock row; the 3090 Ti read class v3 61.95 MH/s at 249.5 W (4.03) Stock: the denominator sweep, RunPod, 8 October 13:25 UK: class v3 60.01 MH/s at 294.6 W (4.91), class v5 61.49 at 309.4 W (5.03, 46 samples, SM 1,673 MHz, memory 9,501); the class v5 premium on this card 5 percent at stock; stock, lock owed (the host refused -lgc and -lmc) |
|
||||
| NVIDIA GeForce RTX 3080 | 10 GB | 4.2 | estimated | 4.54 (estimated) | 7.3x / 5.3x / 3.8x | 9.7x / 6.5x / 4.3x | 16.7x / 9.1x / 5.3x | 1800 MHz at 80 percent. the Ampere cap lever (10 percent, no lower: a 170 W cap took 34 percent of the class v5 rate on this card, a 180 W cap 11 percent) Stock: the sweep's two 3080 hosts both carried a limit under the class v5 draw: at 170 W class v3 50.71 MH/s at 169.2 W (3.34) held the rate and class v5 fell to 33.37 MH/s at 169.9 W (5.09) with the SM at 689 MHz; at 180 W class v3 49.83 at 177.8 W and class v5 44.31; the uncapped class v5 stock by the 3080 Ti's +30 percent: about 230 W (4.54); the Ampere cap under the shadow costs rate one for one: measured twice |
|
||||
| NVIDIA GeForce RTX 3070 | 8 GB | 4.8 | estimated | 5.29 (measured) | 8.3x / 6.1x / 4.3x | 11.1x / 7.4x / 5.0x | 19.1x / 10.4x / 6.1x | 1800 MHz at 80 percent. the Ampere cap lever on the measured stock rows (class v3 142.3 W, class v4 196.4 W); the 3070 Ti 38.98 MH/s at 177.1 / 267.6 W Stock: rented pods 8 October; the class v5 sweep's two 3070 hosts closed the connection mid-bench |
|
||||
| NVIDIA GeForce RTX 3060 | 12 GB | 5.77 | estimated | 6.4 (measured) | 10.0x / 7.3x / 5.2x | 13.3x / 8.9x / 6.0x | 23.0x / 12.4x / 7.3x | 1800 MHz at 80 percent. the Ampere cap lever (10 percent) on the measured class v4 stock row; the 3060 Ti at a 130 W host cap held 33.06 MH/s at 128.7 W under class v4 (3.89): a cap row measured on a sibling Stock: class v5 measured on a Vast 3060 (13:53 UK): 26.53 MH/s at 169.8 W (6.40) at the host's 170 W limit (the limit binding: the class v4 pod read 26.89 at 166.1 W, 6.18); class v3 26.53 at 120.4 W on the same host; stock, lock owed |
|
||||
| AMD Radeon RX 9070 XT | 16 GB | 7.9 | measured | 10.7 (measured) | 13.7x / 10.0x / 7.1x | 18.3x / 12.3x / 8.2x | 31.4x / 17.0x / 10.0x | ADLX -500 MHz, -30 percent. the ADLX grid (24 rows, PC 1): the rate flat at 18.9 MH/s across the grid, the best point -500 MHz core and -30 percent power, 24 percent under stock; class v5 about 8.1 Stock: the app's own power reading at the stock point, 8 October 07:11 UK |
|
||||
| AMD Radeon RX 7900 XTX | 24 GB | 5.89 | estimated | 7.75 (estimated) | 10.2x / 7.5x / 5.3x | 13.6x / 9.1x / 6.1x | 23.4x / 12.7x / 7.5x | ADLX -500 MHz, -30 percent. not rentable, not owned: the 9070 XT's dependent-read rate per channel (150 M per second per 32-bit channel) on 24 channels; the knob by the 9070 XT grid Stock: 24 channels at the RX 7600's measured per-channel rate: about 40 MH/s at about 310 W (7.8); not rentable, not owned |
|
||||
| AMD Radeon RX 7800 XT | 16 GB | 6.47 | estimated | 8.52 (estimated) | 11.2x / 8.2x / 5.8x | 15.0x / 10.0x / 6.7x | 25.7x / 14.0x / 8.2x | ADLX -500 MHz, -30 percent. 16 channels of GDDR6 at the 9070 XT's per-channel rate; the knob by the 9070 XT grid Stock: 16 channels at the RX 7600's measured 1.74 MH/s per channel: about 27 MH/s at about 230 W (8.5); not rentable, not owned |
|
||||
| AMD Radeon RX 7600 | 8 GB | 6.19 | estimated | 8.14 (measured) | 10.7x / 7.8x / 5.6x | 14.3x / 9.6x / 6.4x | 24.6x / 13.3x / 7.9x | ADLX -500 MHz, -30 percent. the 9070 XT's ADLX grid (24 percent of the draw for no rate) applied; this card's own 24-point grid runs on PC 1 by about 16:50 UK and replaces this row Stock: the founder's RX 7600 (8 GB) on PC 1, the card-in job 15:46 to 15:50 UK: class v4 sub-version 3 at stock 13.88 MH/s at 113 W (8.14), the v4 and v5 fingerprints equal to the pinned readings, the app's own row 13.4 at 113 W; class v5 within 2 percent; the 1 GiB dataset fits in 8,176 MB (the 2, 4 and 5.5 GiB rows follow) |
|
||||
| AMD Radeon RX 7600 XT | 16 GB | 6.56 | estimated | 8.63 (estimated) | 11.4x / 8.3x / 5.9x | 15.2x / 10.2x / 6.8x | 26.1x / 14.1x / 8.3x | ADLX -500 MHz, -30 percent. 8 channels; the card-in queue holds one for a PC measurement Stock: the same Navi 33 die as the measured RX 7600 (13.88 MH/s at 113 W) with 16 GB in clamshell: the same rate, about 120 W (8.6); the card-in queue holds one |
|
||||
| AMD Radeon RX 9060 XT | 16 GB | 8.23 | estimated | 10.83 (estimated) | 14.3x / 10.4x / 7.4x | 19.0x / 12.8x / 8.5x | 32.8x / 17.7x / 10.5x | ADLX -500 MHz, -30 percent. 8 channels of GDDR6 on RDNA 4; the card-in queue holds one Stock: 8 channels on RDNA 4 between the 9070 XT's 1.18 MH/s per channel and the 7600's 1.74: about 12 MH/s at about 130 W (10.8); the card-in queue holds one |
|
||||
| NVIDIA H100 80GB HBM3 | DC | 2.0 | estimated | 2.58 (measured) | 3.5x / 2.5x / 1.8x | 4.6x / 3.1x / 2.1x | 8.0x / 4.3x / 2.5x | 1400 MHz at 80 percent. the premium 240 to 280 W at stock says half of it is the clock; a lock on an owned host (Hopper at 1,980 MHz boost, HBM3-bound): about 500 W (2.0); band 1.9 to 2.4; rented hosts refuse -lgc Stock: the denominator sweep, RunPod, 13:25 UK: class v3 252.96 MH/s at 382.4 W (1.51), class v5 241.34 at 621.6 W (2.58, 7 samples, the SM throttled to 1,590 MHz under the 700 W cap); the 7 and 8 October pods read class v4 250.6 at 695.1 W (2.77); -lmc answered 'use --lock-memory-clocks-deferred' and the memory clock stayed 2,619; stock, lock owed |
|
||||
| NVIDIA L40S | DC | 3.78 | estimated | 5.29 (measured) | 6.5x / 4.8x / 3.4x | 8.7x / 5.9x / 3.9x | 15.0x / 8.2x / 4.8x | 1860 MHz at 60 percent. the Ada shape on the measured stock rows (class v3 220.7 W, class v4 277.6 W); re-based on the measured class v5 stock row of 13:5x UK (the architecture's shape on the measured v3 draw and the measured premium) Stock: class v5 measured (13:49 UK): 56.49 MH/s at 298.7 W (5.29, 46 samples, SM 2,520); class v3 56.35 at 221.5 W on the same host; the v4watts pod read class v4 56.4 at 277.6 W; stock, lock owed |
|
||||
| NVIDIA A100-SXM4-80GB | DC | 2.89 | estimated | 2.99 (measured) | 5.0x / 3.7x / 2.6x | 6.7x / 4.5x / 3.0x | 11.5x / 6.2x / 3.7x | 1200 MHz at 80 percent. the card already runs at 1,410 MHz and the 400 W host limit binds under class v5 (SM clock gives); a cap under it costs rate on Ampere, so the floor is within 5 percent of the measured stock row Stock: the denominator sweep, RunPod, 13:24 UK: class v3 138.06 MH/s at 294.9 W (2.14), class v5 133.11 at 398.1 W (2.99, 19 samples, at the host's 400 W limit, memory 1,593 MHz); the v4watts pod on a 500 W host read class v4 138.0 at 489.3 W (3.54); -lmc 'not supported' on this card; stock, lock owed |
|
||||
| Intel Arc B580 | 12 GB | 10.4 | estimated | 10.4 (estimated) | 18.0x / 13.2x / 9.3x | 24.1x / 16.1x / 10.7x | 41.4x / 22.4x / 13.2x | stock, no lever. 10.6 to 11 MH/s measured on PC 2 (the enclosure), the watts unread; about 110 W by the board's class (OWED); no clock lever in the app for Intel |
|
||||
| Intel Arc A750 | 8 GB | 18.8 | estimated | 18.8 (estimated) | 32.6x / 23.8x / 16.9x | 43.5x / 29.2x / 19.4x | 74.8x / 40.5x / 23.9x | stock, no lever. unmeasured; modelled from the B580 and the Alchemist board |
|
||||
| Apple M5 Max | Apple | 1.4 | measured | 1.4 (measured) | 2.4x / 1.8x / 1.3x | 3.2x / 2.2x / 1.4x | 5.6x / 3.0x / 1.8x | stock, no lever. the GPU and DRAM channels of the IOReport meter (not the wall): class v3 27.08 MH/s at 21.0 W (0.78), class v4 26.67 at 37.3 W (1.40), class v5 about 1.43; the package about 17 W more; no lever (no clock cap on Apple silicon) |
|
||||
| Apple M4 Max | Apple | 1.55 | estimated | 1.55 (estimated) | 2.7x / 2.0x / 1.4x | 3.6x / 2.4x / 1.6x | 6.2x / 3.3x / 2.0x | stock, no lever. the same 512-bit LPDDR5X (8,533 against 9,600 MT/s) and a 40-core GPU on N3E: about 26 MH/s; class v3 about 0.85 at the meter, class v5 about 1.55 (the M5 Max's rows scaled); a devnet row from an unnamed Apple laptop read 24.3 MH/s on class v4 |
|
||||
| Apple M4 Pro | Apple | 1.65 | estimated | 1.65 (estimated) | 2.9x / 2.1x / 1.5x | 3.8x / 2.6x / 1.7x | 6.6x / 3.6x / 2.1x | stock, no lever. 256-bit LPDDR5X and a 20-core GPU: about 13.5 MH/s; the shadow's per-hash premium does not shrink with the part, so class v5 reads about 1.65 at the meter |
|
||||
| Apple M3 Max | Apple | 1.75 | estimated | 1.75 (estimated) | 3.0x / 2.2x / 1.6x | 4.0x / 2.7x / 1.8x | 7.0x / 3.8x / 2.2x | stock, no lever. 512-bit LPDDR5-6400 and a 40-core GPU on N3B: about 24 MH/s; class v5 about 1.75 at the meter |
|
||||
|
||||
To fold any other chip core figure (the research lane's convention, 13:5x): the edge on a row is the card's class v5
|
||||
microjoules over `E_mem + 101,170 x c`, with `E_mem` 0.466 (GDDR7 board), 0.321 (one HBM3 stack), 0.14 (the N2 SRAM
|
||||
die), 0.18 (the custom HBM4E base die) and `c` the core's picojoules per forced op; at the k lane's synthesised
|
||||
sequencer-core floor of 1.1 pJ per op at N3 (its per-unit figure before fetch, decode and the register file, due 15:00)
|
||||
the chip's class v5 energy reads GDDR7 0.577, HBM3 0.432, SRAM 0.251, so the 5080's floor of 2.10 gives 3.6x, 4.9x and
|
||||
8.4x and the M5 Max's 1.43 gives 2.5x, 3.3x and 5.7x; that figure is a floor on the core and the row at it is the
|
||||
pessimistic column.
|
||||
|
||||
What the table says:
|
||||
|
||||
1. The honest floor per tier, measured: 32 GB 2.33 to 2.37 (the 5090 at its knee); 16 GB 2.06 to 2.10 (the 5080 at
|
||||
1,100 MHz); 12 GB 3.51 to 3.58 (the 4070 at its tune); 16 GB AMD 7.9 to 8.1 (the 9070 XT on the grid); Apple 1.40
|
||||
to 1.43 (the M5 Max, no lever). The 16 GB NVIDIA tier is cheaper per hash than the 32 GB tier at the knee, and the
|
||||
estimated 5070 Ti and 5070 rows (1.7 to 2.1) say the 12 and 16 GB Blackwell cards are the honest NVIDIA floor: the
|
||||
5090's 170 SMs carry the shadow at the same picojoules per op as the 5080's 84 but its memory system draws more
|
||||
per read (16 GDDR7 devices against 8).
|
||||
2. The chip edge against the honest floor at k = 1 (a core as good as a GPU lane): GDDR7 1.9x to 2.1x on Blackwell at
|
||||
the knee, 1.3x on the M5 Max; one HBM3 stack 2.2x to 2.4x and 1.5x; the N2 SRAM die 2.7x to 3.0x and 1.8x. At the
|
||||
record's centre (k = 0.5): 2.7x to 3.0x, 1.8x; 3.3x to 3.6x, 2.2x; 4.5x to 5.0x, 3.1x. The served 2.1x stands as the
|
||||
5090 figure; the public sentence can carry the lower honest row.
|
||||
3. The lock recovers half the shadow's premium on Blackwell (the 5090: 145 W to 82 W; the 5080: 83 W to 41 W) and a
|
||||
third of the class v3 draw (330 to 223 W; 170 to 104 W); on Ada the 4070's tune took 30 W of 106 on class v3 and the
|
||||
premium reads 30 W at the tune point; on Ampere the cap is the only lever and it costs rate past 1,800 MHz; on AMD
|
||||
the ADLX offsets take 24 percent of the draw for no rate; on Apple nothing moves.
|
||||
4. Every Ada and Ampere consumer row is estimated at its knee because no rented host allows `-lgc` and no founder
|
||||
machine holds one but the 4070 (whose full class v4 ladder is owed to a PC 1 job). The rented stock rows are
|
||||
measured and stand; the estimated knees carry bands of about plus or minus 10 percent and the rented-against-PC
|
||||
watts spread of 15 to 20 percent on top.
|
||||
|
||||
## 3. What moves the floor, per architecture (the knob set the measured rows support)
|
||||
|
||||
| Architecture | The lever | What it does, measured | The tier points (Efficiency / Balanced / Max) | Known-failed |
|
||||
|---|---|---|---|---|
|
||||
| Blackwell (5090, 5080; 5070 Ti, 5070, 5060 Ti, 5060 by shape) | the core lock; the power cap is flat on this hash | the rate holds within 1.5 percent to 1,300 MHz on the 5090 and within 0.3 percent to 1,000 on the 5080; the draw falls a third on class v3 and the shadow's premium halves | 1,200 / 1,300 / unlocked on the 5090; 1,100 / 1,100 / unlocked on the 5080; 1,100 / 1,300 / unlocked as the prior on the others | the 5090 at 1,100 (-10.5 percent on class v4), the 5080 at 900 (-5.2 percent); the Power Helper's skip-rule fault (0.3.20) needs the padding lines |
|
||||
| Ada (4090, 4080, 4070 Ti, 4070, 4060 Ti, 4060, L40S) | the core lock with a cap | the 4070's tune: 1,860 MHz and the 50 percent cap hold 30.95 MH/s at 79.5 W on class v3 (28.7 at 102.7 stock); a cap holds the rate until the SM clock falls under about 2,400 MHz (the search prior); the 4090's class v4 premium at stock is 100 W (lane 1), under the 5090's 145 for 40 percent fewer counted ops per second | 1,860 at 50 to 60 percent / 2,400 at 80 percent / unlocked | the 4070 Ti Super at a 210 W host limit with the SM at 2,400 lost 10 percent on class v4 (36.95 against 41.24 MH/s): a cap that binds under the shadow costs rate on Ada too |
|
||||
| Ampere (3090, 3080, 3070, 3060, A4000 to A6000, A100) | the power cap only | a cap holds the rate until the SM falls under about 1,800 MHz; stock clocks sit at 1,900 to 2,000, so the lever is 10 to 15 percent of the draw; the 3060 Ti at a 130 W host cap held 33.06 MH/s at 128.7 W on class v4 | 80 percent with the lock at 1,800 as a ceiling / 90 percent / unlocked | the 3080 Ti at 63 percent with the SM at 749 MHz: 24 to 28 percent of rate lost |
|
||||
| Hopper (H100) | the core lock on an owned host | unmeasured: rented hosts refuse `-lgc`; the premium 283 W at stock (11.2 pJ per op) says half of it is the clock | 1,400 at 80 percent / 1,600 at 90 percent / unlocked, all estimated | none measured |
|
||||
| RDNA 4 (9070 XT; 9060 XT by shape) and RDNA 3 (7900 XTX, 7800 XT, 7600 XT by shape) | the ADLX core and power offsets (0.3.25); no memory knob; no elevation | the 9070 XT's grid: the rate flat at 18.9 MH/s across 24 rows, -500 MHz and -30 percent = 149.3 W against 202 | -500 MHz, -30 percent / -300 MHz, -15 percent / stock | Ember run 6 aborted on "card reports 0 W" (fixed bd7fcf4); the ADLX sampler window bug (fixed) |
|
||||
| Apple (M5 Max measured; M4 Max, M4 Pro, M3 Max by shape) | none | no clock cap on Apple silicon; the GPU draws 11 W at the hash and 27 W with the shadow, the DRAM 10 W throughout | stock only (the max tier, source stock) | none |
|
||||
| Intel (B580 measured for rate; A750 by shape) | none in the app | measure only; the watts unread on the B580 | stock only | a kit without the Intel rotate-fold rewrite fails its self-test on Arc (a build fault, not an Arc result) |
|
||||
|
||||
The one rule under the table: a step that drags the memory clock under 95 percent of baseline is marked and cannot
|
||||
win, because on this hash the memory clock is the rate and the core clock is only the shadow's cost; the Ember 2
|
||||
climb may raise the memory clock (5 percent of the range per probe) where the vendor exposes it, and no measured row
|
||||
yet shows a gain from it (the 5090's memory sits at 13,801 of 14,001 MHz).
|
||||
|
||||
Amendment, 15:5x UK (floor lane 1's memory-clock ladder on PC 1, the 5090 alone at the 1,300 MHz core lock, job
|
||||
run-ca4-pc1-memclk-5090-20261008-b, every row bit-exact; `docs/analysis/class-v6/floor/sm-sparse.md` section 3.1 at
|
||||
66bc6ca7): the memory clock is not a lever, now measured. The driver holds the 5090's memory in two states only (asks
|
||||
of 8,001 MHz and above read 13,801; 6,001 and below read 7,001, the PHY's half-rate state). Class v3 at the lock:
|
||||
13,801 MHz 134.1 to 134.5 MH/s at 222.7 to 223.7 W (1.66); 7,001 MHz 76.0 MH/s at 141 to 142 W (1.86 to 1.87).
|
||||
Class v4: 134.3 to 134.5 at 308 to 309 W (2.29 to 2.30); 76.1 at 184 to 187 W (2.42 to 2.46). The half-rate state
|
||||
loses 43 percent of the rate (the activate ceiling scales with the memory clock) for 37 percent of the watts, so the
|
||||
energy per hash rises 12 percent on class v3 and 5 on class v4; the 82 W between the states at 7.4 G reads per second
|
||||
lost is 11 nJ per read at the margin, equal to the DRAM chase probe's whole-path figure, so the memory-side term is 80
|
||||
to 90 W of the 223 W at the lock and is bought one for one with the rate. The table's "never touched" line stands as
|
||||
a measurement; the tiers table carries `mem_mhz` 0 on every row.
|
||||
|
||||
## 4. The LPDDR6 and unified-memory tier, five years out
|
||||
|
||||
The method. The M5 Max row is the only measured unified-memory point and it splits three ways at the meter: the DRAM
|
||||
rail 10.2 W at 27.08 MH/s x 128 reads = 2.94 nJ per dependent read (LPDDR5X-9600, 512-bit, 32 channels of 16 bits);
|
||||
the GPU 11.0 W at the hash = 0.41 microjoules per hash of issue and latency-hiding cost; the shadow +16 W = 0.62
|
||||
microjoules (6.9 pJ per counted op). Each device below is modelled by scaling those three terms: reads per second by
|
||||
the part's sub-channel count at the M5 Max's 108 M dependent reads per second per 16-bit channel (the rate is
|
||||
activate-bound per bank, not pin-rate-bound, so 14.4 Gbps moves it little); DRAM energy per read by the generation
|
||||
(LPDDR6 at JESD209-6's lower VDD and the 32-byte atom, which halves the over-fetch of a 4-byte read against LPDDR5X's
|
||||
64-byte burst: 2.0 to 2.3 nJ per read at the meter against 2.94, approximate); the GPU term by the vendor's measured
|
||||
latency-hiding (Apple 0.41 microjoules per hash; AMD's RDNA reads 2.4 G dependent reads per second at 199 W on the
|
||||
9070 XT, so its GPU term is about 7 microjoules per hash; NVIDIA's Blackwell at a 128-bit board (the 5060) about 1.5
|
||||
at the knee); the shadow term by the vendor's ALU picojoules per op (Apple 6.9 measured, NVIDIA 6.4 at the knee, AMD
|
||||
about 3 W for the whole shadow on the 9070 XT, so near zero at the card's rate). Every row is modelled; none is a
|
||||
measurement, and the first LPDDR6 part to be sold will replace its row.
|
||||
|
||||
| Device class (5 years out) | Memory | Modelled MH/s | DRAM / GPU / shadow, microjoules per hash | Class v5 at the meter, microjoules per hash | GDDR7 board edge k = 0.5 / 1 | HBM3 stack | N2 SRAM die | Custom HBM4E base die (0.18 + shadow) |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| Apple Max-class on LPDDR6 (an M7 or M8 Max, 2028 to 2030: 576-bit, 24 channels of 24 bits = 48 sub-channels, a 40 to 48-core GPU at N2) | LPDDR6-14400 | about 41 | 0.27 / 0.35 / 0.56 | **1.18** (the package about 1.6 to 1.7) | 1.5x / 1.06x | 1.8x / 1.2x | 2.5x / 1.5x | 2.4x / 1.4x |
|
||||
| Apple Pro-class on LPDDR6 (384-bit, 32 sub-channels) | LPDDR6 | about 27 | 0.27 / 0.37 / 0.58 | 1.22 | 1.5x / 1.1x | 1.9x / 1.3x | 2.6x / 1.6x | 2.4x / 1.5x |
|
||||
| Apple M5 Max today, for the comparison | LPDDR5X-9600 | 27 measured | 0.38 / 0.41 / 0.62 | 1.40 measured (v5 1.43) | 1.8x / 1.3x | 2.2x / 1.5x | 3.1x / 1.8x | 2.8x / 1.7x |
|
||||
| AMD Strix Halo class and its LPDDR6 successor (256-bit LPDDR5X-8000 today, 40 CU RDNA 3.5; approximate) | LPDDR5X then LPDDR6 | about 12 (the GPU's latency hiding binds, not the memory: the 9070 XT's 64 CU read 2.4 G per second) | 0.35 / about 6 / about 0.2 | about 6.5 (band 5 to 9) | 8x / 6x | 10x / 7x | 14x / 8x | 13x / 8x |
|
||||
| NVIDIA GB10 class (DGX Spark, N1X: 256-bit LPDDR5X-9400, a 48-SM Blackwell GPU; approximate) | LPDDR5X then LPDDR6 | about 13.5 | 0.33 / about 2.5 at a lock / 0.65 | about 3.5 (band 3 to 4.5) | 4.4x / 3.1x | 5.4x / 3.6x | 7.5x / 4.4x | 7x / 4.2x |
|
||||
| Qualcomm X Elite class (128-bit LPDDR5X-8448, 8 channels; Adreno X1; no worker exists; approximate) | LPDDR5X then LPDDR6 | about 7 | 0.35 / about 2.5 / about 0.8 | about 3.7 (band 3 to 5) | 4.7x / 3.3x | 5.7x / 3.8x | 8x / 4.7x | 7.4x / 4.5x |
|
||||
| MediaTek Dimensity class (64-bit LPDDR5X, 4 channels; a phone; approximate) | LPDDR5X then LPDDR6 | about 3.4 | 0.35 / about 2 / about 0.9 | about 3.3 | 4.2x / 3x | 5.1x / 3.4x | 7.1x / 4.2x | 6.6x / 4x |
|
||||
| LPDDR6 controller chip, 32 channels (lane B's row: the chip built on the honest tier's own memory) | LPDDR6 | about 100 per chip | 0.25 to 0.30 at zero shadow; + 0.32 / 0.65 | 0.57 to 0.62 / 0.90 to 0.95 | against the Apple LPDDR6 row: 2.0x / 1.3x | | | |
|
||||
|
||||
What the rows say:
|
||||
|
||||
1. The honest tier five years out is Apple's unified-memory part on LPDDR6 at about 1.2 microjoules per hash under class
|
||||
v5 at the meter, 15 percent under today's M5 Max; the gain is the DRAM term (0.38 to 0.27, the 32-byte atom and the
|
||||
lower rail) and the GPU term at N2, not the pin rate. The other unified-memory vendors model 3 to 10 microjoules
|
||||
because their GPUs hide a dependent read's latency worse (AMD's RDNA 8x worse than Apple's per read, measured on
|
||||
the 9070 XT), and the memory does not rescue a GPU term that large. So "the unified-memory SoC tier" as the reference
|
||||
joule means the Apple tier specifically, until a measurement on a Strix or GB10 part says otherwise.
|
||||
2. Against the Apple LPDDR6 row every chip's edge is the lowest in the record: the GDDR7 board 1.5x at k = 0.5 and
|
||||
1.06x at k = 1 (at parity with a laptop at a core as good as a GPU lane); the N2 SRAM die 2.5x / 1.5x; the custom
|
||||
HBM4E base die 2.4x / 1.4x; the LPDDR6 controller chip 2.0x / 1.3x. The chips lane B priced at 13x to 17x against
|
||||
the stock 5090 are 1.5x to 2.5x against the best honest joule with the shadow on.
|
||||
3. The reference joule should be the best honest joule, stated as a rule, not a fifth layer: the layers change what a
|
||||
chip can be built as; the denominator changes what the edge is called. The public text carries the edge over the
|
||||
Apple tier (1.3x at k = 1 today on GDDR7, 1.8x on the SRAM die) and over the 16 GB Blackwell card at its knee (1.9x
|
||||
and 2.7x), with the stock 5090 figure retired from the headline.
|
||||
4. Per-tier scoring. (a) The shadow sizing (layer 1's program-length band): YES, and it already is: the 2.0 rule caps N
|
||||
at the card that loses 5 percent first, which is the Apple tier (130,000 ops on the M5 Max against 210,000 on the
|
||||
5090 at 431 W and about 150,000 at its knee). Sizing N to the Apple ceiling rather than the 5090's costs the 5090
|
||||
tier: at N = 130,000 the 5090 at its knee pays 6.4 pJ x 131,170 = 0.84 microjoules of shadow instead of 0.65 (+0.19,
|
||||
+8 percent of its electricity at 2.37 to 2.56), its GDDR7 edge at k = 1 moves from 2.13x to 1.95x and at k = 0.5
|
||||
from 3.0x to 2.9x; the M5 Max pays +0.21 (1.43 to 1.64, +15 percent) for 1.3x to 1.25x at k = 1. The shadow's
|
||||
extra length buys about 0.15x of edge on every tier for 8 to 15 percent of every honest miner's electricity: the
|
||||
priced answer is to keep N at 100,000 and let the per-era draw move inside the band, not to size it up to the Apple
|
||||
ceiling. (b) The acceptance floor (layer 4, the (c''') ratio and the F8 uniformity test): NO per-tier scoring. The
|
||||
floor is a property of the program's address distribution against a chip's on-die SRAM (the 1.067x hot-set ceiling),
|
||||
the same number whichever honest card runs it; scoring it per tier would change the figure for nobody and would
|
||||
invite a per-card class menu, which section 6 of the research file shows can never beat the single best class.
|
||||
5. The dataset stays inside 16 GB unified memory (8 GiB at the top of the schedule) so the honest tier keeps mining:
|
||||
lane B's point stands and this file adds a number: a 16 GB Mac is out at the candidate 10 GiB step and pays the
|
||||
measured -12 to -22 percent rate cost of a larger working set before any limit; the Apple LPDDR6 row above is a
|
||||
36 GB or larger part.
|
||||
|
||||
## 5. The tiers statement for Ember (keyed by class v5)
|
||||
|
||||
The table the app reads: `app/igneum-app/tiers/class-v5-tiers.json`, 30 card classes, 5 measured (the 5090, 5080,
|
||||
4070, 9070 XT, M5 Max), the rest estimated from the measured stock rows and the architecture's shape in section 3.
|
||||
Each entry carries the three tiers as rows in the shape `src/ember.rs tier_from_json` reads back from a card's state
|
||||
(`id`, `clock_mhz`, `power_pct`, `mem_mhz`, `limit_w`, `mhs`, `w`; `mhw` and `source` as `Tier::json` writes them) plus
|
||||
`label` (measured or estimated), `uj`, `note` and `src`; the AMD rows carry `core_offset_mhz` and `power_offset_pct`
|
||||
for the ADLX knob; a card with no lever carries the `max` tier only, source `stock`. The validator
|
||||
(`class-v5-tiers.mjs`) refuses a class other than v5, a tier outside the three, a power rung outside 50 to 100, a
|
||||
missing field, a `uj` that is not watts over rate, a tuned row dearer than stock, a max tier that is not stock, an AMD
|
||||
row without its offsets, and a missing card class of the brief; the test (`class-v5-tiers.test.mjs`, 10 of 10 on
|
||||
igneum-build-3 at 13:0x) runs the shipped file, the measured rows, the match rule (a 5070 Ti is not a 5070), the stale
|
||||
rule and the known-failed cases.
|
||||
|
||||
| Tier | The rule (the ember-tiers-25 definition) | Blackwell | Ada | Ampere | AMD | Apple, Intel |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Efficiency | the best MH per watt row of the card's search outright | the lock one rung past the knee at 100 percent power: 1,200 on the 5090 (measured), 1,100 on the 5080 (measured), 1,100 as the prior elsewhere | the lock at 1,860 with the 50 to 60 percent cap (the 4070 measured) | the 80 percent cap with the lock at 1,800 as a ceiling (no row under it) | -500 MHz core, -30 percent power (the 9070 XT measured) | stock (no lever) |
|
||||
| Balanced (the default from install) | the best MH per watt row within 1 percent of the top rate | the knee: 1,300 on the 5090 (measured), 1,100 on the 5080 (the same row), 1,300 as the prior elsewhere | 2,400 at 80 percent (the Ada prior) | the 90 percent cap, no lock | -300 MHz, -15 percent (inside the flat band) | stock |
|
||||
| Max | the stock row (unlocked, 100 percent) | stock | stock | stock | stock | stock |
|
||||
|
||||
The re-measure rule after a class flip, as the table carries it (`remeasure_rule`):
|
||||
|
||||
1. A set measured under one program class never applies as current under another (`tiers_stale`: both classes known
|
||||
and different). The stored Efficiency and Balanced points are the provisional START of the re-measure, not its
|
||||
result: the search begins at the stored point and walks one rung either way on the clock and the power ladder,
|
||||
the fingerprint checked on every step; the Max tier is stock and is never stale.
|
||||
2. The measured flip: class v4 to class v5 on the 5090 at the 1,300 MHz lock cost 0.0 percent of rate and +2.0
|
||||
percent of watts; the knee did not move. The expected flip cost for any class whose shadow stays inside the
|
||||
latency-bound band (the op count and the mix inside layer 1's draw band) is 0 to 1 rung of knee; a class whose shadow
|
||||
leaves the band (doubling the op count: the 5090 at 431 W binds at 210,000 ops) re-measures from stock, not from
|
||||
the stored point.
|
||||
3. The set also re-measures weekly (`PERIOD_S`) and on a driver major change (the prior key carries driver major and
|
||||
class), and a confirm check that beats its prior by over 1 percent on MH per watt asks for the full plan.
|
||||
|
||||
## 6. The denominator sweep (rented, 8 October 13:05 to 14:1x UK, the stock rows the record lacked)
|
||||
|
||||
Three drivers of the fleet lane's model sweep and then a direct ssh runner: `box-cardbench-v2.sh` (class v3, the v5
|
||||
kit's fingerprint, the memprobe ceiling), `box-cardbench-v4watts.sh` (the class v4 shape with the paired class v3 run)
|
||||
and, from 13:21 on the coordinator's full-spend order, `box-cardbench-v2m.sh` on 16 card classes in parallel (the
|
||||
class v5 kit's bench run for 200 batches of 2^24 under the 1 Hz sampler, so the class v5 watts are a measured mean
|
||||
over busy samples from 8 s in; then `-lmc` to the card's maximum and a second class v5 run when the host accepted it).
|
||||
One one-shot pod per card class on RunPod or Vast under the consumer cap, destroyed at the end of each row; the rows
|
||||
in `~/igneum-fleet/cardbench/rows.jsonl` under `sweep: 2026-10-08-denominator` and `-v5`; the lane's spend about USD 5
|
||||
of its USD 100 (the pods ran 3 to 10 minutes each). No rented host allowed `-lgc`; the two that accepted `-lmc` (the
|
||||
H100 and A100) left the memory clock where it was, so every row here is stock and the knees of section 2 stay
|
||||
estimated where no founder machine holds the card.
|
||||
|
||||
Two faults found on the way, for the fleet lane: (1) the sweep driver's ssh wait read every Vast pod of the 13:21 wave
|
||||
as "never answered ssh in 10 minutes" while a direct `ssh` to the same host and port answered at once (13 pods, all
|
||||
live; the rows were then taken by a direct runner, `~/igneum-fleet/denom-direct.sh`, which keys on the provider's
|
||||
own ssh host and port and nothing in the registry); (2) the registry lock `~/igneum-fleet/boxes.json.lock` was held
|
||||
from 13:32 by three `dn3-q05.py` processes of another lane, so every `Registry.patch` from this lane blocked (a
|
||||
re-rent sat 12 minutes on a live pod and then destroyed it); the lane's scripts now write the registry best-effort
|
||||
under a 15 s alarm. The rented stock rows against the PC rows on the one card measured both ways (the 5080): class v5
|
||||
71.35 MH/s at 248.0 W rented against class v4 71.41 at 253.1 W on PC 1, within 3 percent, so the 18 percent class v3
|
||||
spread of the morning was that host and not the method.
|
||||
|
||||
| Card | Host | Class v3 (MH/s at W = microjoules) | Class v4 shape or class v5 (MH/s at W = microjoules; samples) | The knobs on the host | Cost |
|
||||
|---|---|---|---|---|---|
|
||||
| RTX 4090 24 GB | runpod m1tstl1h1b7r0o (595.91.07), limit 450.0 W | 70.094 at 222.6 W = 3.18 | class v5 70.253 MH/s (MATCH), watts not sampled on this row | -lgc refused | USD 0.032 |
|
||||
| RTX 5060 8 GB | vast 54840005 (580.126.09), limit 145.0 W | | class v4 16.234 at 87.8 W = 5.41 (v4 over v3 20.4 percent on this host) | | USD 0.009 |
|
||||
| RTX 5060 8 GB | vast 54840004 (580.126.09), limit 145.0 W | 24.774 at 80.7 W = 3.26 | class v5 31.312 MH/s (MATCH), watts not sampled on this row | -lgc refused | USD 0.011 |
|
||||
| RTX 5080 16 GB | vast 54840017 (570.133.07), limit 300.0 W | | class v4 70.951 at 239.8 W = 3.38 (v4 over v3 41.1 percent on this host) | | USD 0.052 |
|
||||
| RTX 3080 10 GB | vast 54840023 (580.159.03), limit 130.0 W | | class v4 24.007 at 129.2 W = 5.38 (v4 over v3 3.0 percent on this host) | | USD 0.025 |
|
||||
| RTX 3060 12 GB | vast 54840846 (580.126.20), limit 170.0 W | | class v4 26.886 at 166.1 W = 6.18 (v4 over v3 44.6 percent on this host) | | USD 0.007 |
|
||||
| RTX 4070 12 GB | vast 54840856 (580.173.02), limit 200.0 W | 30.829 at 107.1 W = 3.47 | class v5 30.878 MH/s (MATCH), watts not sampled on this row | -lgc refused | USD 0.023 |
|
||||
| A100 80 GB | runpod miuoe6gkcb1rhz (580.126.16), limit 400.0 W | 138.062 at 294.9 W = 2.14 | class v5 133.548 MH/s (MATCH), watts not sampled on this row | -lgc refused | USD 0.075 |
|
||||
| H100 80 GB | runpod zosqj1s802dk3x (580.126.09), limit 700.0 W | 252.959 at 382.4 W = 1.51 | class v5 241.342 MH/s (MATCH), watts not sampled on this row | -lgc refused | USD 0.216 |
|
||||
| RTX 3090 24 GB | runpod sbrhhunqui25d0 (580.65.06), limit 310.0 W | 60.008 at 294.6 W = 4.91 | class v5 61.486 MH/s (MATCH), watts not sampled on this row | -lgc refused | USD 0.014 |
|
||||
| RTX 3080 10 GB | vast 54841582 (580.159.03), limit 180.0 W | 49.832 at 177.8 W = 3.57 | class v5 44.308 MH/s (MATCH), watts not sampled on this row | -lgc refused | USD 0.024 |
|
||||
| RTX 5080 16 GB | vast 54845199 (570.133.07), limit 300.0 W | 71.16 at 159.4 W = 2.24 | class v5 71.349 at 248.0 W = 3.476; 39 samples, SM 2769, memory 14801 | -lgc refused; -lmc refused | USD 0.014 |
|
||||
| L40S 48 GB | vast 54845608 (595.71.05), limit 350.0 W | 56.354 at 221.5 W = 3.93 | class v5 56.493 at 298.7 W = 5.287; 46 samples, SM 2520, memory 9001 | -lgc refused; -lmc refused | USD 0.04 |
|
||||
| RTX 4080 16 GB | vast 54845190 (595.84), limit 320.0 W | 40.694 at 141.5 W = 3.48 | class v5 40.8 at 200.7 W = 4.919; 72 samples, SM 2760, memory 10801 | -lgc refused; -lmc refused | USD 0.017 |
|
||||
| RTX 3080 10 GB | vast 54845161 (580.178.04), limit 170.0 W | 50.708 at 169.2 W = 3.34 | class v5 33.37 at 169.9 W = 5.091; 90 samples, SM 689, memory 9251 | -lgc refused; -lmc refused | USD 0.008 |
|
||||
| RTX 4090 24 GB | vast 54845886 (595.84), limit 250.0 W | 62.401 at 200.5 W = 3.21 | class v5 62.684 at 249.9 W = 3.987; 46 samples, SM 2579, memory 10251 | -lgc refused; -lmc refused | USD 0.02 |
|
||||
| RTX 5060 8 GB | vast 54845177 (595.84), limit 140.0 W | 30.76 at 78.8 W = 2.56 | class v5 30.804 at 115.7 W = 3.756; 99 samples, SM 2715, memory 13801 | -lgc refused; -lmc refused | USD 0.011 |
|
||||
| RTX 5060 Ti 16 GB | vast 54845162 (580.126.09), limit 180.0 W | 30.747 at 82.6 W = 2.69 | class v5 30.843 at 123.9 W = 4.017; 96 samples, SM 2835, memory 13801 | -lgc refused; -lmc refused | USD 0.013 |
|
||||
| RTX 3060 12 GB | vast 54845154 (580.126.09), limit 170.0 W | 26.534 at 120.4 W = 4.54 | class v5 26.53 at 169.8 W = 6.4; 117 samples, SM 1806, memory 7301 | -lgc refused; -lmc refused | USD 0.008 |
|
||||
| RTX 4070 12 GB | vast 54845860 (580.126.09), limit 210.0 W | 28.401 at 116.3 W = 4.09 | class v5 28.433 at 165.5 W = 5.821; 108 samples, SM 2820, memory 9801 | -lgc refused; -lmc refused | USD 0.011 |
|
||||
| RTX 4060 Ti 16 GB | vast 54849184 (580.159.03), limit 100.0 W | 18.735 at 79.1 W = 4.22 | class v5 18.75 at 99.8 W = 5.323; 163 samples, SM 2668, memory 8751 | -lgc refused; -lmc refused | USD 0.016 |
|
||||
|
||||
Not reached by the class v5 sweep (stock, their class v4 rows of the morning stand within 2 percent): the 5070 Ti and
|
||||
the 5070 (two Vast hosts each, 14:08 and 14:13 UK, never accepted the fleet key in 15 minutes); the 3070 (two hosts
|
||||
closed the connection mid-bench). The sweep closed at 14:35 UK: 20 rows landed, no pod left on either provider
|
||||
(audited 14:40), the lane's spend about USD 5 of its USD 100.
|
||||
|
||||
## 7. Consequences per tier
|
||||
|
||||
| Tier | What this file means | What is being done |
|
||||
|---|---|---|
|
||||
| Home miner, one 8 GB card (5060, 4060, 3070) | the Blackwell 8 GB card at its knee is 2.15 microjoules (estimated), inside 10 percent of the 5090's measured floor; the Ada and Ampere 8 GB cards 3.8 to 4.8. The lever is the Efficiency tier: on a 5060 about 40 W of 110 back for no rate (estimated); on a 3070 about 20 W of 196 | the tiers table ships the prior; the card's own search measures it; the 5060 and 4060 watts rows are the sweep's |
|
||||
| One 12 GB card (5070, 4070, 3060) | the 5070 at its knee about 1.75 (the lowest NVIDIA row in the record, estimated); the 4070 measured 3.51 at its tune; the 3060 4.9 | the 4070's class v4 ladder below 1,860 MHz is owed to a PC 1 job (the hash lane's scripts, not this lane's hands) |
|
||||
| One 16 GB card (5080, 5070 Ti, 5060 Ti, 4080, 4060 Ti, 9070 XT) | the 5080 measured 2.06 to 2.10 at 1,100 MHz, 42 percent of its stock draw back for 0.3 percent of rate; the 5070 Ti about 1.70 (estimated); the 9070 XT 8.1 measured on the grid, 24 percent of its draw back for nothing | the 5060 Ti ladder on PC 2 (the card-in job); the 9070 XT grid lands in the AMD tier rows |
|
||||
| One 24 or 32 GB card (5090, 4090, 3090) | the 5090 at its knee 2.33 to 2.37, 33 percent of its electricity back for 1.5 percent of rate; the 4090 about 3.4, the 3090 about 6.4 (estimated: Ampere's cap is a small lever) | the Balanced tier is the default from install; the 4090 and 3090 stock rows are the sweep's |
|
||||
| Apple (M-series) | the honest best per joule the project owns: 1.40 to 1.43 measured on the M5 Max, nothing to set; the M4 and M3 parts within 25 percent (estimated); five years out about 1.2 on LPDDR6 | the public edge is stated against this tier; the dataset stays inside 16 GB unified memory |
|
||||
| A rig | per card as above; a 5090 rig at the Balanced tier draws 319 W per card instead of 476 on class v5; a 5080 rig 147 instead of 253 | the tiers table |
|
||||
| A pool user | nothing changes in shares or payout from any row here | |
|
||||
| A chip | the honest floor it must beat is 2.1 microjoules on a mass-market 16 GB Blackwell card at its knee (1.9x at k = 1 on GDDR7, 2.7x on the N2 SRAM die) and 1.43 on an Apple laptop (1.3x and 1.8x); five years out 1.2 on the Apple LPDDR6 part (1.06x and 1.5x) | the research lane's synthesis carries the number |
|
||||
| The public claim | the honest sentence per joule: "a chip that stores the dataset keeps the card's whole-card energy over its memory energy: under 2x against a 16 GB Blackwell card at its knee and 1.3x against an Apple laptop for a core as good as a GPU lane; 2.7x and 1.8x against a 2 nm SRAM store" | to the Counter lane for the chip texts; nothing served moves from this file |
|
||||
|
||||
## 8. Unverified and owed
|
||||
|
||||
- Every Ada and Ampere knee is estimated: no rented host allows `-lgc`, and the founder's machines hold one Ada card
|
||||
(the 4070, its class v4 ladder below 1,860 MHz owed to a PC 1 job through the hash lane's scripts) and no Ampere.
|
||||
The estimated knees carry about plus or minus 10 percent, and the rented stock watts read 15 to 20 percent under a
|
||||
PC's on the one card measured both ways (the 5080).
|
||||
- The 5070 Ti and 5070 floors (1.7 to 2.1) are the strongest claims in this file and both are estimated from stock
|
||||
rows; a 5070 Ti ladder on PC 1 or PC 2 (the card-in job's queue) would settle whether the honest NVIDIA floor is a 16
|
||||
GB card.
|
||||
- The AMD rows other than the 9070 XT and the RX 7600 are modelled on those two measured points (the 7600's stock row
|
||||
landed 15:50 UK from the card-in job on PC 1: 13.88 MH/s at 113 W, 8.14 microjoules, twice as good as this file's
|
||||
morning estimate for the Navi 33 die; its own ADLX grid by about 16:50 and the 9060 XT in the card-in queue replace
|
||||
the estimated rows).
|
||||
- The Apple M4 Max, M4 Pro and M3 Max rows are modelled on the M5 Max's split; a Metal bench on any of them with the
|
||||
IOReport meter would replace its row in an hour.
|
||||
- Section 4's LPDDR6 rows are modelled on JESD209-6's public summary (the bank count and tFAW are behind the
|
||||
paywall, lane B's note) and on the M5 Max's three-way split; the Strix, GB10 and Qualcomm rows rest on the vendor's
|
||||
measured latency hiding on other parts and are approximate.
|
||||
- The 4060's watts (three rented hosts, no power sensor), the B580's watts, the H100's lock, the A750 entirely.
|
||||
- The class v5 stock rows of the 5070 Ti, the 5070 and the 3070 (section 6: the hosts never took the key or closed
|
||||
the connection); their class v4 stock rows stand within 2 percent.
|
||||
- Two fleet-tooling faults for the fleet lane (section 6): the sweep driver's ssh wait misreads live Vast pods, and a
|
||||
registry flock held for an hour by another lane's processes blocks every `Registry.patch`; both cost this lane 40
|
||||
minutes and five pods.
|
||||
|
||||
## 9. The reading for the 15:45 BST close (five sentences)
|
||||
|
||||
The honest joule under class v5 is 2.06 to 2.10 microjoules per hash on a 16 GB Blackwell card at its 1,100 MHz lock
|
||||
(measured), 2.33 to 2.37 on the 5090 at its knee (measured), about 1.7 to 2.1 on the 12 and 16 GB Blackwell cards
|
||||
(estimated from their measured stock rows), 1.40 to 1.43 on an Apple M5 Max with nothing to set (measured), and the
|
||||
only software lever that moves it is the operating point: the core lock on Blackwell and Ada (worth 34 to 41 percent
|
||||
of the class v5 stock draw measured today on the 5080 and the 4070), the cap on Ampere (10 to 15 percent, and a cap
|
||||
under the shadow costs rate one for one: the 3080 at 170 W lost a third of its class v5 rate), the ADLX offsets on
|
||||
AMD (24 percent), nothing on Apple and Intel; the occupancy knob is 1 to 2 percent and the memory clock is untouched.
|
||||
Against those floors the stored-dataset chips read 1.9x to 2.1x on GDDR7, 2.2x to 2.4x on one HBM3 stack and 2.7x to
|
||||
3.0x on a 2 nm SRAM die at a core as good as a GPU lane (6.4 pJ per forced op), 2.6x to 3.0x, 3.2x to 3.6x and 4.4x
|
||||
to 5.0x at the record's 3.2 pJ, and 3.6x to 4.0x, 4.8x to 5.4x and 8.2x to 9.3x at the k lane's 1.1 pJ floor; against
|
||||
the Apple laptop 1.3x, 1.5x and 1.8x at 6.4 pJ; the served 2.1x stands as the 5090 figure and the honest headline is
|
||||
under 2x against a mass-market card at a core as good as a GPU lane. Five years out the honest tier is Apple's
|
||||
unified-memory part on LPDDR6 at about 1.2 microjoules (modelled), against which the GDDR7 chip is at parity at 6.4 pJ
|
||||
and the SRAM die 1.5x; the other LPDDR SoC vendors model 3 to 10 microjoules because their GPUs hide the latency
|
||||
worse, so the reference joule is the Apple tier by name. The class should state resistance against the best honest
|
||||
joule and keep sizing the shadow by the Apple tier's 5 percent point (N at 100,000, the band drawn per era), and
|
||||
should not score the acceptance floor per tier; sizing N up to the Apple ceiling would cost every honest miner 8 to
|
||||
15 percent of its electricity for about 0.15x of edge. The tiers table ships the measured points on the 5090, 5080,
|
||||
4070, 9070 XT and M5 Max, the measured class v5 stock row on 14 classes and an architecture prior for the knee
|
||||
everywhere else, with the rule that a class flip re-measures from the stored point (the measured v4 to v5 flip moved
|
||||
the 5090's knee by nothing), and the rows still owed are the Ada and Ampere knees, which no rented host can give.
|
||||
294
docs/analysis/class-v6/floor/invention.md
Normal file
294
docs/analysis/class-v6/floor/invention.md
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
# Class v6 floor lane 5, invention: what none of the four floor lanes covers, checked against the identity
|
||||
|
||||
8 October 2026, 12:48 to 19:xx UK, branch `class-v6-floor-invention` from `counter-asic-4` (base 171618c5 after the fast-forward; first cut at 13:0x UK; corrected 13:1x UK for the read-width naming: `w16` is 16 bytes, `w64` is the 64-byte item; the rented-card rows of 13:2x to 13:4x UK carried in section 3.1, which reverse rank 1 at stock; the 5090 row at 13:4x UK). The open-ended lane of the floor research: four other lanes work the known levers (SM-sparse gating, the shadow's `k` from RTL synthesis, the SRAM full-store chip and the dataset floor, the honest denominator per tier); this file finds what they do not cover. Every candidate is checked against the identity first and killed or kept with a number. Every number carries a label: **measured** (a card on a named job, the file named), **modelled** (arithmetic on the chip model's cited inputs, `docs/analysis/chip-model-v3.md` section 5), **claimed** (a vendor's or an author's figure, URL given), **approximate** (from memory or a scaling). Nothing here changes a consensus object, a pack or a served number. Research and reasoning ran on this Mac; no build, no benchmark and no miner ran anywhere for this file.
|
||||
|
||||
## 0. One page
|
||||
|
||||
**The identity, as the record states it** (`docs/analysis/counter-asic-4-research.md` section 2): against the chip that stores the dataset, `edge = (E_card + F) / (E_mem + k F)`; at zero premium the edge is `E_card / E_mem` (3.6x on a 5090 at its 1,300 MHz knee against the GDDR7 board, measured card, modelled chip), and a shadow op with `k` under 1 never fully closes it. The one thing with `k = 1` by construction is the memory itself: the same devices on both sides. The record forced one DRAM operation per read (the activate and one 32-byte sector) and called every wider read "nothing to force: the chip already pays the sector". That is true up to 32 bytes; at 64 bytes the chip pays a second sector, and this lane's question was whether the honest card pays it at the DRAM's price or at its own.
|
||||
|
||||
**The candidate this lane found, measured today, and where it stands.** The dataset item is 64 bytes (16 words; spec 1.5). Reading the whole item (W = 16 words, the read-width branch's class `w64`, bit-exact on four runtimes, fingerprint `836e56e7d496e980`, the verifier +4 percent) forces a second 32-byte sector per read in the same open row: on the chip model's own inputs (909 pJ of activation plus 1,150 pJ of movement per sector, Micron's 4.5 pJ per bit, claimed) the GDDR7 chip's energy per hash rises from 0.466 to 0.62 microjoules (+33 percent, its rate unchanged), one HBM3 stack's from 0.321 to 0.40 (+24), and on floor lane 3's wire figure the N2 SRAM die's from 0.036 to 0.126 (66x to 19x). The design's "never 16 words" rested on one row, the 5090's 71.9 MH/s (-47 percent) in the exported kernel at full occupancy, while the same card's own 64-byte probe read -13 percent; so the lane rented cards and measured the card side at stock (section 3.1): **the rate question is settled per architecture, and the energy question kills the lever at stock.** A one-request form of the same kernel text (PTX `ld.global.L2::64B` on the first load of each item; bit-exact, the same fingerprint) holds the 4-byte rate on the RTX 4090 at full occupancy (63.65 against 64.73 MH/s, 98 percent, PASS) and on the RTX 5090 at a sparse shape (145.7 against 142.6 MH/s, sp170-w4, PASS); on the H100 the best form is -21 percent and the hint does nothing (FAIL); on the RTX 3090 the best form is -13 percent (FAIL). But the card pays the second sector at its own fabric's price, not the DRAM's 1.15 nJ: 8.6 nJ on the 4090 (+71 W at 8.3 G reads per second for no rate) and 4.7 nJ on the 5090 (+88 W at 18.6 G), so the energy per hash rises 34 percent on the 4090, 25 on the 5090, 33 on the H100's best form and 25 on the 3090, against the modelled chip's 33 (GDDR7) and 50 (HBM3). The sector's `k` is about 0.13 (4090) to 0.24 (5090) on GDDR7, under the ALU shadow's 0.3 to 0.8; the edge at zero shadow moves 0 on Ada, 7 percent on Blackwell (4.7x to 4.4x at stock) and 0.9x in the chip's favour on Hopper, and 0 with the shadow on (3.3x at `k` 0.5 either way). **KILL at stock on measured rows.** The knee row, measured by the hash lane on PC 1 at 15:0x UK, closes it from the other side: at the 1,300 MHz lock the hinted form reads 37.6 against the 4-byte row's 100.6 MH/s at 2.3x its energy per hash (section 6). W = 16 is out of class v6 at stock and at the knee.
|
||||
|
||||
**Everything else on the brief is killed, with the number.** The tensor core: the record's `k` 0.03 to 0.3 is not defensible at its floor (the 0.04 pJ figure is INT4 at 0.46 V on a test chip with no memory system); shipping merchant silicon at nominal voltage reads 0.30 to 0.56 pJ per INT8 MAC (Meta MTIA v2 0.51, Qualcomm AI 100 Ultra 0.34, AMD MI355X 0.56, NVIDIA B200 0.44, Apple M4 ANE 0.30 measured; all claimed unless marked), and a Hopper tensor core under a pure MMA loop measured 0.34, so the honest band against the tile the class runs (1.5 pJ per MAC at the knee, measured) is `k` 0.2 to 0.4 and against the dense wide tile (0.83 at the knee) 0.4 to 0.7: equal to or under the ALU shadow's 0.3 to 0.8, never above it, and the Apple cost (-35 percent of rate at 1,024 tiles per hash, measured) kills it as content regardless. The RT core: traversal results are implementation-defined on every API (Vulkan "no ordering guarantee", DXR "no defined order", NVIDIA "could change depending on which driver, which GPU"), the acceleration structure is opaque, and a dedicated unit reads 4 to 33 nJ per ray against 290 to 750 nJ measured board-level on an RTX 2080 (`k` 0.01 to 0.2): dead on bit-exactness and on `k`. Memory-level parallelism per dollar: no asymmetry per channel, but a correction to the chip model: no 28 nm GDDR7 PHY exists (the shipped ones are N3 and FinFET; the oldest GDDR6 PHYs are 12 nm), so the record's cheapest chip (a USD 5 M project, a USD 17 M break-even cap) is not buildable; the floor is a 12 nm GDDR6 part at about USD 20 M to 30 M (cap USD 70 M to 100 M) or an N5-class GDDR7 part at USD 50 M to 75 M (cap 170 M to 250 M), modelled. Proof of latency: the chain checks values, never time; the block is 1 s and a DRAM round trip 0.1 to 0.4 microseconds; the farm's node is on its own LAN; a sequential per-block prefix favours the lower-latency side, which is the chip (about 4x). The time dimension on the address map: a bit permutation is firmware (a few hundred gates), no prefetch exists for a dependent chain, and the literature has no such scheme. The literature since 2023 holds nothing outside the identity; what it adds is a higher Ethash precedent (iPollo V2H, 0.14 J per MH, about 13x a 5090, vendor claim), the measured PAM3 I/O energies (SK hynix, ISSCC 2024) and the PHY-node fact above. Of this lane's own ideas, four more die on the identity (video decode, row-straddling, scratch in DRAM, independent chains per hash), and two items per read (W = 32) dies with W = 16.
|
||||
|
||||
### The KEEP list, ranked
|
||||
|
||||
| Rank | Item | What it moves | Cost to the honest tiers | Where | Label |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | **The chip model's project floor corrected: no 28 nm GDDR7 PHY** | the cheapest buildable chip's project USD 20 M to 75 M (cap USD 70 M to 250 M), not USD 5 M (cap 17 M); the N5-core row's cap 170 M to 250 M, not 100 M | none | chip-model-v3 5.4 and 5.6 and the record's 16.2 | claimed (vendor IP pages), modelled (the mission lane's cap) |
|
||||
| 2 | **The tensor `k` band corrected** (0.2 to 0.4 on the class's tile, 0.4 to 0.7 on the dense tile; not 0.03 to 0.3) | no served row (the tile stays a worse-or-equal lever) | none | the record's 15.1a and 20.4 and chip-model 5.11 | claimed and measured |
|
||||
| 3 | **The card's cost of a second sector, measured: about 8.6 nJ on a 4090 at stock** | closes the read-width question on the identity's own terms (`k` about 0.13 on the sector); the hinted one-request load form is kept as a miner-side kernel fact for Ada (98 percent of the 4-byte rate at 64 bytes) | none (nothing ships) | the read-width plan's 4.1 and lane 3's width table; the knee row owed on PC 1 | measured |
|
||||
| 4 | **The Ethash precedent's top row** (iPollo V2H, 3.4 GH/s at 475 W, about 13x a 5090 per joule) | the precedent band 2.1x to 13x, not 2.1x to 6.8x | none | `asic-resistance-history.md` and chip-model 5.1 | claimed |
|
||||
|
||||
No item on this list is a class v6 change; nothing here reads `k` at or above 1 on a measured GPU figure.
|
||||
|
||||
### The five-sentence reading for the 15:45 close
|
||||
|
||||
The identity leaves one piece of work a chip pays at the card's own price, the memory's data movement, and this lane's candidate was the second sector of the 64-byte item (W = 16 words); measured today on rented cards at stock, a one-request form holds the 4-byte rate on Ada (98 percent on a 4090) and fails on Hopper and Ampere (-21 and -13 percent at best), but the card pays that sector at about 8.6 nJ through its own fabric against the DRAM's 1.15, so its energy per hash rises 25 to 34 percent, the same as the modelled chip's 33, and the lever reads `k` about 0.13: dead at stock, W = 16 stays out of class v6, with the knee row owed on PC 1 as a formality. The tensor core, the RT core, proof of latency and the drawn address map are dead on the identity or on bit-exactness, with the tensor `k` band corrected upward to 0.2 to 0.7 (still never above the ALU shadow's) and the RT core's traversal implementation-defined on every vendor. The chip model's cheapest chip does not exist as priced: a GDDR7 PHY needs a FinFET node (N3 shipped; 12 nm is the oldest GDDR6 PHY), so the floor project is USD 20 M to 75 M and the floor break-even cap USD 70 M to 250 M instead of 17 M, which goes into the model now. Nothing in the literature since 2023 offers a lever outside the identity, and this lane's own four other ideas die on it with numbers. The floor section's default for this lane therefore stands as written: no new lever; what the lane adds is two corrections to the model, one measured card-side figure, and the measured closing of the read-width question.
|
||||
|
||||
## 1. The frame
|
||||
|
||||
| Term | Value at the 5090's 1,300 MHz knee | Label | Source |
|
||||
|---|---|---|---|
|
||||
| `E_card`, class v3 | 1.67 microjoules (127.3 MH/s at 213.0 W; 134.6 at 223.3 on the efficiency pass) | measured | the record 20.3; `docs/plans/counter-asic-3-status.md` |
|
||||
| `E_mem`, the `f = 1` GDDR7 chip | 0.466 microjoules: 166 MH/s at 77.6 W (42.6 W of reads at 2.0 nJ, 20 W static, 15 W controller) | modelled | chip-model-v3 5.3 and 5.4 |
|
||||
| `E_mem`, one HBM3 stack | 0.321 microjoules: 83.6 MH/s at 26.8 W (12.8 W of reads at 1.2 nJ, 14 W static and controller) | modelled | the same |
|
||||
| `E_mem`, the N2 SRAM full store at the hash's width | 0.036 microjoules at W = 1 (0.25 nJ per read: 1.3 pJ per bit of wire over 80 bits plus 0.1 nJ of macro and 0.05 of controller), 66x the 5090's stock point; 0.126 at W = 16 (0.88 nJ, 560 bits), 19x | modelled (floor lane 3, section 10.3 of the class v6 design) | `docs/analysis/class-v6/floor/sram-and-floor.md` |
|
||||
| The read's energy, split | GDDR7: 909 pJ activation plus 4.5 pJ per bit x 256 bits = 1,150 pJ of movement and I/O (44 / 56 percent); HBM3: 909 plus 3.48 x 256 = 890 scaled by 4.12 / 6.25 | modelled on O'Connor et al. 2017 Tables 2 and 3 and Micron's and Samsung's pJ per bit (claimed) | chip-model-v3 5.3 |
|
||||
| `F`, the class v4 premium | 0.652 microjoules (82.8 W over 126.9 M x 102,100 ops: 6.4 pJ per counted op) | measured | the record 20.3 |
|
||||
| `k`, the ALU shadow | 0.3 to 0.8 (the 5090's 6.2 to 11.3 pJ per op measured against a 5 nm SIMD array's 2 to 5, approximate) | GPU measured, chip approximate | the record 15.1a |
|
||||
| The dataset item | 16 words, 64 bytes; `dataset[w] = item(w >> 4)[w AND 15]`; the lottery hash reads one word per load | design | spec 1.5 and the item table; `docs/plans/read-width.md` |
|
||||
| The read-width classes, by name | `w4` = 4 bytes (W = 1 word, the lottery hash); `w16` = 16 bytes (W = 4, one sector); `w64` = 64 bytes (W = 16, the whole item, two sectors on the 5090); `w64x4` = 64 bytes at 32 loads | design | `docs/plans/read-width.md` 1.5; the bench-log entry of 5 October |
|
||||
| The 5090's dependent-read probe, best over lanes in flight | 17.5 to 18.2 G reads per second at 4 B; 18.0 to 19.9 at 16 B; 9.1 to 15.7 at 64 B (9.1 at 4 M lanes) | measured, 5 October | `docs/bench-log.md`, the read-width entry |
|
||||
| The hash rates at the widths, unlocked, no power sampling | 5090: w4 136.1, w16 139.8, w64 71.9 MH/s; 9070 XT: 18.15, 17.90, 17.59; M5 Max: 27.74, 28.26, 28.27 | measured, 5 October | the same |
|
||||
| The card's whole-card marginal per dependent DRAM read | 10.9 nJ unlocked, 8.7 nJ at the lock | measured, 8 October | the record 15.1a |
|
||||
|
||||
The four doors a candidate can open (the invention lane's section 1, kept): raise `k`; raise the chip's capex or project; shorten the chip's useful life; lower the honest card's cost at the same `F`. This file adds the fifth door the identity itself names: **force work whose `k` is 1 by construction, which is only the memory's own.**
|
||||
|
||||
## 2. The candidates on the brief
|
||||
|
||||
### 2.1 Work the GPU already does at near-ASIC efficiency
|
||||
|
||||
**The tensor core, re-read.** The question: is "a merchant N2 project beats Blackwell's tensor core per joule by 3x to 30x" defensible, or is the tensor shadow at full rate closer to `k` 0.7 to 1?
|
||||
|
||||
| Chip | Node | Year | Dense INT8 TOPS (FP8 where marked) | W | pJ per MAC (2 / (TOPS per W)) | Label | URL (read 8 October 2026) |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| RTX 5090 | 4N | 2025 | 838 | 575 | 1.37 at spec; **1.36 measured** on the dense s8 m16n8k32 tile at 80 percent of peak unlocked, 0.83 at the 1,300 lock; the dependent u8 m8n8k16 tile the class runs 2.9 unlocked, 1.5 at the lock (the packs job), 4.1 / 2.2 (the microbench) | claimed spec; measured rows the record's 15.1a and 20.3 | https://www.nvidia.com/en-us/geforce/graphics-cards/50-series/rtx-5090/ |
|
||||
| RTX 4090 | 4N | 2022 | 661 | 450 | 1.36 | claimed | NVIDIA Ada whitepaper |
|
||||
| H100 SXM | 4N | 2022 | 1,979 | 700 | 0.71 | claimed | https://www.nvidia.com/en-us/data-center/h100/ |
|
||||
| H800, mma and wgmma INT8 dense under a pure MMA loop | 4N | 2025 | | sub-TDP loop | **0.34 / 0.54** | measured | https://arxiv.org/html/2501.12084v2 Tables 10 and 11 |
|
||||
| B200 (FP8 / INT8) | N4P | 2024 | 4,500 | 1,000 | 0.44 | claimed | https://lenovopress.lenovo.com/LP2226 |
|
||||
| AMD MI300X | N5 | 2023 | 2,615 | 750 | 0.57 | claimed | AMD data sheet |
|
||||
| AMD MI355X | N3P | 2025 | 5,033 | 1,400 | 0.56 | claimed | https://glennklockwood.com/garden/processors/mi355x |
|
||||
| RX 9070 XT (RDNA 4 WMMA) | N4P | 2025 | 389 | 304 | 1.56 | claimed | https://www.amd.com/en/products/graphics/desktops/radeon/9000-series/amd-radeon-rx-9070xt.html |
|
||||
| Qualcomm Cloud AI 100 Ultra | 7 nm | 2023 | 870 | 150 | 0.34 | claimed | Qualcomm product brief |
|
||||
| Meta MTIA v2 | 5 nm | 2024 | 354 | 90 | 0.51 | claimed | https://www.eenewseurope.com/en/meta-launches-second-generation-custom-ai-chip |
|
||||
| Intel Gaudi 3 (FP8) | 5 nm | 2024 | 1,835 | 900 | 0.98 | claimed | https://www.anandtech.com/show/21342 |
|
||||
| Google TPU Ironwood (FP8) | n/d | 2025 | 4,614 | about 1,100 inferred | about 0.48 | inferred from the Hot Chips 2025 slides | https://hc2025.hotchips.org/ |
|
||||
| AWS Trainium2 (FP8) | 5 nm | 2024 | 1,299 | about 500 (third party) | 0.77 | inferred | https://arxiv.org/pdf/2608.28048 |
|
||||
| Tenstorrent Blackhole p150 (FP8) | N6 | 2025 | 774 | 300 | 0.78 | claimed | https://docs.tenstorrent.com/aibs/blackhole/specifications.html |
|
||||
| Apple M4 ANE (FP16 rate, INT8 runs at it) | N3E | 2024 | 19 | 2.8 | **0.30** | measured | https://maderix.substack.com/p/inside-the-m4-apple-neural-engine-615 |
|
||||
| Hailo-8 | 16 nm | 2019 | 26 | 2.5 | 0.19 at spec, 0.71 measured on ResNet-50 | claimed / measured | https://www.cnx-software.com/2020/10/07/ |
|
||||
| Untether speedAI 240 (FP8, at-memory) | 7 nm | 2022 | 2,000 | 66 | 0.067 | claimed, never shipped at volume | https://www.design-reuse.com/news/52539/ |
|
||||
| Axelera Metis (digital in-memory) | 12 nm | 2023 | 214 | about 14 | 0.13 | claimed | https://www.hackster.io/news/ |
|
||||
| NVIDIA VSQ test chip (INT4) | 5 nm | 2023 | | | 0.021 at 0.46 V on a benchmarking layer; **0.052 at the nominal 0.67 V on full networks**; no INT8 figure | measured, a test chip without HBM or a NoC | https://research.nvidia.com/publication/2023-01_956-topsw-deep-learning-inference-accelerator-vector-scaled-4-bit-quantization |
|
||||
|
||||
The reading. Every shipping systolic chip with a real memory system and fabric lands at 0.3 to 0.6 pJ per INT8 MAC at nominal voltage; the figures under 0.15 are in-memory-compute vendor claims and an INT4 test chip at low voltage. Dally's own energy pie for that test chip (Hot Chips 2023, slide 24) has the datapath at 47 percent and the buffers, collector and movement at 53 percent, so a whole chip is about 2x its datapath before any NoC or HBM. The record's `k` floor of 0.03 took the 0.46 V INT4 figure against the 5090's 1.5 to 4 pJ; at nominal and INT8 the chip side is 0.3 to 0.6. The honest band, labelled:
|
||||
|
||||
| Against | GPU pJ per MAC (measured) | Chip pJ per MAC at nominal (claimed, the cluster above) | `k` | Reading |
|
||||
|---|---|---|---|---|
|
||||
| The tile the class runs (dependent u8 m8n8k16, the `mm1430` pack) at the knee | 1.5 | 0.3 to 0.6 | **0.2 to 0.4** | the forcing lever's `k`; equal to or under the ALU shadow's 0.3 to 0.8 |
|
||||
| The same unlocked | 2.9 | 0.3 to 0.6 | 0.1 to 0.2 | |
|
||||
| The dense wide tile (s8 m16n8k32) at the knee | 0.83 | 0.3 to 0.6 | **0.4 to 0.7** | a tensor shadow built of wide tiles would sit at the ALU band's centre; the H800's 0.34 under a pure loop says a GPU tensor core at its best is inside the merchant cluster |
|
||||
| The dense wide tile unlocked | 1.36 | 0.3 to 0.6 | 0.25 to 0.45 | |
|
||||
|
||||
So "3x to 30x" is not defensible: at nominal voltage merchant silicon beats the 5090's dense tile by 2.3x to 4.5x and its dependent tile by 2.5x to 10x, and the 30x needs an INT4 layer at 0.46 V. And "`k` 0.7 to 1 at full rate" is not reached either: the top of the dense band is 0.7. The identity check: the tile's `k` is at best the ALU shadow's and the joules it forces are the same by construction (the record 20.3: 71.5 W against 82.8 at the knee for the same tile count); the chip's downside bet (its `k` floor) is 0.2 against the ALU's 0.3, so the tile remains the worse-or-equal lever, not the 4x to 30x worse lever the record wrote. The cost to the honest tiers is what kills it as content: the M5 Max at -35 percent of rate at 1,024 tiles per hash and -78 percent at 4,096 (measured, the record 20.2a), with no integer matrix path in Metal. Bit-exact on NVIDIA and the CPU (measured); the AMD WMMA layout unverified. **KILL as v6 content; KEEP the band correction (rank 3).** FP8, FP16 and BF16 tiles: the accumulation order inside a tile is unspecified by PTX, so not bit-exact across vendors or generations: KILL.
|
||||
|
||||
**The texture unit's filtered gather.** Measured 0.19 nJ per filtered fetch unlocked and 0.10 at the lock (the microbench `tex_linear_f32_256k`); a 9-bit fixed-point interpolator on NVIDIA (CUDA Programming Guide, claimed), vendor-specific fraction widths elsewhere: not bit-exact across the three vendors; a chip's interpolator is one 9-bit multiply-add, `k` far under 1. KILL. The point-sampled fetch is the L2 chase under another name (the same checksum, measured): the L2 lever, dead at `k` 0.1 to 0.3.
|
||||
|
||||
**The rasteriser and ROPs.** Unreachable from CUDA, OpenCL and Metal compute; fill conventions, sample positions and depth precision differ by vendor by design. KILL.
|
||||
|
||||
**The hardware video codec.** A conformant decoder of a standard (H.264, HEVC, AV1) is normatively bit-exact, which is the one fixed-function block on a card whose output a CPU verifier could reproduce, and a chip would license the same decoder block at the same node (`k` about 0.5 to 1, the same circuit, approximate). It dies on throughput and on the verifier: a card carries a fixed count of decode engines (two to four on current cards, approximate) against any count on a chip, so the honest card's hash rate would be bound by its decoder, not its memory, at a ratio that differs per vendor (the 5090 against the M5 Max's media engine); and a software decode on the verifier runs at milliseconds per frame on one core (approximate), against a 10 ms gate that holds the whole warp. KILL. (This lane's own candidate, recorded here because it is the only fixed-function block that passes the bit-exactness test.)
|
||||
|
||||
### 2.2 The RT core
|
||||
|
||||
| Unit (node) | Throughput | Power or area | Energy per ray | Label | URL (read 8 October 2026) |
|
||||
|---|---|---|---|---|---|
|
||||
| RTX 5090, 170 fourth-generation RT cores | 317.5 RT TFLOPS; "double the throughput for ray-triangle intersection" over Ada; no per-ray or per-clock rate published, no RT-core area or power share | 575 W | not published | claimed | https://images.nvidia.com/aem-dam/Solutions/geforce/blackwell/nvidia-rtx-blackwell-gpu-architecture.pdf |
|
||||
| RTX 2080 running DXR (Mach-RT, Table 3) | 288 to 745 Mrays per second over six scenes | 215 W TDP | **290 to 750 nJ per ray, board-level** | measured rays, TDP assumed | https://hwrt.cs.utah.edu/papers/mach-rt_TVCG.pdf |
|
||||
| Turing TU102 | 10 Giga rays per second claimed | about 260 W | about 26 nJ per ray at the claimed peak | claimed | Hot Chips 31 slides |
|
||||
| AMD RDNA 4 | 8 ray-box and 2 ray-triangle tests per clock per CU, BVH8, OBBs | no RT power figure | none | claimed | https://hc2025.hotchips.org/assets/program/conference/day1/8_amd_pomianowski_final.pdf |
|
||||
| SiliconArts RayCore, 6 RTUs, 28 nm | 239 Mrays per second | 1 W, 18 mm^2 | about 4.2 nJ per ray | claimed, FPGA-derived | https://www.embedded.com/?p=4433770 |
|
||||
| Imagination PowerVR GR6500, 28 nm | 300 Mrays per second | 5 to 10 W for the whole GPU | 17 to 33 nJ per ray | claimed | https://www.anandtech.com/show/7870 |
|
||||
| RayFlex RTL, 15 nm PDK, 1 GHz | one ray-box or ray-triangle test per cycle | 60 to 85 mW per datapath | 60 to 85 pJ per intersection test | synthesised | https://arxiv.org/pdf/2409.06000 |
|
||||
| TRaX study, 65 nm | | memory 60 to 95 percent of ray energy, DRAM alone up to 80, compute 1 to 2 | 0.5 to 5 microjoules per ray | simulated | https://hwrt.cs.utah.edu/papers/rt_performance_CGI18.pdf |
|
||||
|
||||
Per node visit (Ylitie et al., NVIDIA, HPG 2017, measured): about 15 ray-node and 7 to 9 ray-triangle tests per ray on a compressed 8-wide BVH, 1.5 to 3.2 KB fetched per ray, "memory traffic is the main limiting factor with incoherent rays"; Chou et al. (MICRO 2023) call it "a latency-bound" pointer chase. The identity check: a traversal step over a dataset-sized tree is one dependent DRAM read (the identity's `E_mem`, which both sides pay) plus a box test the RT core does in hardware; the box test is the only forcing, and the chip's cost to match it is 60 to 85 pJ at 15 nm (synthesised) against a GPU share nobody has measured below 290 nJ per ray board-level, so `k` reads 0.01 to 0.2 on the public figures: the chip builds a cheaper RT unit, not a dearer one.
|
||||
|
||||
Bit-exactness, which is the first question and the one that decides it: the Vulkan specification gives "no ordering guarantee between operations performed on different intersection candidates", makes watertightness a "should", and performs the hit test "in an implementation specific manner" (https://docs.vulkan.org/spec/latest/chapters/raytraversal.html); DXR: "There is no defined order of execution of any hit shaders for the intersections along a ray path" (https://microsoft.github.io/DirectX-Specs/d3d/Raytracing.html); NVIDIA's own answer (forum, 14 October 2024): which edge or vertex wins is "implementation defined and not guaranteed" and "could theoretically change depending on which API you use, which driver, which GPU"; Blender's Cycles records that matching OptiX or MetalRT bit for bit was impossible. The acceleration structure is opaque in DXR, Vulkan and Metal, and Vulkan's serialised form carries a driver UUID that the next driver may refuse. A CPU verifier therefore cannot reproduce a hardware traversal, and a design that defines its own traversal in software and uses the RT core as an untrusted accelerator forces nothing (the honest card then does the box tests in software or re-checks them). **KILL**, on bit-exactness first and on `k` second. Price of the RT unit a chip would build: 18 mm^2 at 28 nm for six units (claimed), about USD 5 of N5 (approximate).
|
||||
|
||||
### 2.3 Memory-level parallelism as the resource: channels per dollar, the controller and PHY
|
||||
|
||||
The claim to check: a card has more channels per dollar than a chip built from the same devices plus a controller, because the chip's controller and PHY are a cost the card amortises over gaming.
|
||||
|
||||
| Item | Finding | Number | Label | URL (read 8 October 2026) |
|
||||
|---|---|---|---|---|
|
||||
| Channels per device | GDDR7: four 8-bit channels and 64 banks per 2 GB device; the 5090's 16 devices are 64 channels and 1,024 banks; a chip built from the same devices has the same count; the next parts (3 GB, 4 GB, 6 GB) carry the same four channels, so channels per GB FALL for the chip and the card alike (Micron ends the 2 GB part) | 64 channels per 32 GB today; 64 per 48 GB on 3 GB parts | claimed (TrendForce, Rambus) | chip-model-v3 5.1; https://www.trendforce.com/news/2026/09/24/ |
|
||||
| Does a chip buy more channels per dollar | No: the channel count is bought with capacity on both sides (16 devices whatever the dataset), and the chip's only path to more activates per second is more devices, which the card cannot add and the chip pays for at the same price per device | 0 asymmetry per channel | modelled | |
|
||||
| The PHY's node | No 28 nm GDDR7 PHY exists: Cadence's GDDR7 PHY is silicon-proven on TSMC N3 at 32 Gbps (September 2024); Innosilicon's is "advanced FinFET"; the oldest GDDR6 PHYs are 12 nm (Cadence 12FFC and Six Semiconductor, 16 Gbps); nothing at 28 nm at any GDDR6 or GDDR7 rate | the chip's cheapest controller is a 12 nm GDDR6 part or an N5-class GDDR7 part | claimed (vendor pages) | https://www.businesswire.com/news/home/20240925780123/en ; https://innosilicon.com/html/ip-solution/gddr7.html ; https://www.businesswire.com/news/home/20221115005674/en |
|
||||
| The PHY's energy | GDDR7 PAM3 I/O measured on silicon: TX 1.103 pJ per bit, RX 0.764 pJ per bit (SK hynix, ISSCC 2024 paper 13.1); at the hash's 17.9 G reads x 256 bits = 4.6 Tb/s the controller-side RX is 3.5 W, inside the model's 15 W controller allowance with its clocking and termination (PAM3 termination is a large share: arXiv 2410.12990, simulated) | `E_mem` moves by under 0.06 microjoules | measured I/O; the allowance approximate | https://www.researchgate.net/publication/378947349 ; https://arxiv.org/abs/2410.12990 |
|
||||
| The GB202 die | 761.56 mm^2 with 16 x 32-bit controllers and PHYs along the edges; no public mm^2 for the PHY blocks | the card's PHY is a real area it amortises; the chip's is the same area at the same node | measured die, no annotation | https://kurnal-insights.com/en/dieshot/nvidia-gb2025090/ |
|
||||
|
||||
The identity check: the chip and the card pay the same per-bit I/O and the same controller physics, so nothing moves `E_mem` beyond the allowance already in the model. What moves is the chip's PROJECT: the record's cheapest row (chip-model-v3 5.6 "a 28 nm-class project at USD 5 M to 30 M", the record's 16.2 "28 nm controller, USD 5 M, break-even cap USD 17 M") prices a PHY that is not made. The corrected floor, on the mission lane's model (cap = C_proj / 0.30 in years 1 to 2):
|
||||
|
||||
| Cheapest buildable chip | Memory | Project | Break-even cap, years 1 to 2 | Label |
|
||||
|---|---|---|---|---|
|
||||
| A 12 nm GDDR6 controller (16 Gbps PHY IP exists): 32 banks per device, so 32 devices of GDDR6 for the same 1,024 banks and 21.3 G activates per second; about USD 10 per 2 GB device (approximate); a 1,024-bit board | USD 320 | USD 20 M to 30 M (a 12 nm project with licensed PHY IP; the history's 2.5 figures scaled, approximate) | **USD 70 M to 100 M** | modelled, approximate |
|
||||
| An N5-class GDDR7 controller (the 5090's own 16 devices) | USD 320 | USD 50 M to 75 M (the history's 7 nm-class startup figure, claimed) | **USD 170 M to 250 M** | modelled |
|
||||
| The same plus the N5 shadow core (class v4 as shipped; the record's 16.2) | | USD 50 M to 75 M (the core joins the controller's die, which is N5 anyway) | USD 170 M to 250 M, not 100 M | modelled |
|
||||
|
||||
**KEEP as a correction to the chip model (rank 2), not a lever.** Consequence: the record's break-even row for the chip that matters (the N5 core on a 28 nm controller, USD 30 M, cap 100 M) understates the floor by about 2x, because the controller cannot sit on 28 nm; the shadow per load's "doubling" (USD 60 M, cap 200 M) therefore adds less than it seemed, since the single-N5-die form is already the only buildable form. The 85 W node per farm and the leaves' bandwidth are unchanged.
|
||||
|
||||
### 2.4 Proof of latency
|
||||
|
||||
A per-block random challenge whose answer window is shorter than one DRAM round trip plus network latency, so a stored copy in a remote farm cannot answer in time but a local card can.
|
||||
|
||||
| Check | Finding | Number | Label |
|
||||
|---|---|---|---|
|
||||
| The identity | Not an energy lever: it changes who may answer, not what a hash costs. If it bound anything it would bind the chip's node placement | 0 on `E_mem`, `F` and `k` | arithmetic |
|
||||
| What the chain can enforce | Values, never time (the record's section 6, new-pow 3.2): a block header is the challenge, blocks arrive over the WAN at tens to hundreds of milliseconds, the interval is 1 s and a timestamp is the miner's own; no node can check that an answer arrived within microseconds of a challenge it saw itself at a different time | the shortest enforceable window is a block interval, 1 s, against a DRAM round trip of 0.4 microseconds on the 5090 (415 ns queued, measured) and about 0.1 on a chip (55 ns of controller plus tRCD and tCL, modelled): 2.5 million round trips per block | measured card latency; the chip's modelled |
|
||||
| The threat model | The stored-dataset chip's node is on its own LAN (class-v5 2a.2: one node serves a farm, the leaves at 16.5 KB/s); there is no "remote chip farm" to lock out; a farm is as local as a card | 0 | design |
|
||||
| The one form with teeth: a sequential per-block prefix (every miner must run a chain of dependent reads from the block hash before any nonce counts) | Favours the side with the lower unloaded latency, which is the chip: a 100 ms prefix is 250,000 reads at 400 ns on the 5090 and 100 ms of its time; the chip at about 100 ns per read finishes in 25 ms and hashes for the rest | the chip's share of the block rises about 8 percent at a 100 ms prefix; the card loses 10 percent of its time | modelled |
|
||||
| Fresh join | Unchanged: a joiner needs the previous block, which it needs anyway | 0 | design |
|
||||
| The verifier | Nothing to verify: time is not in the proof | 0 | design |
|
||||
| The literature | No proof of work with a sub-round-trip window was found 2023 to 2026; the nearest are watchtower-ping location proofs (BFT-PoLoc, arXiv 2403.13230: about 2 percent Byzantine challengers tolerated, 12 percent misclassification, measured on the Internet; Witness Chain) and PoSME (already in the record); Proximity Signatures (eprint 2026/694) refused the fetch and is unread | null | claimed |
|
||||
|
||||
**KILL.** The chain cannot measure time at the scale a DRAM round trip lives on, the farm is local, and the sequential form helps the chip.
|
||||
|
||||
### 2.5 The time dimension: the address map drawn often enough to break a chip's prefetch and banking plan
|
||||
|
||||
| Check | Finding | Number | Label |
|
||||
|---|---|---|---|
|
||||
| The identity | A mapping change is firmware for the stored-dataset chip; no `F`, no `k` | 0 | arithmetic |
|
||||
| Prefetch | A dependent chain has nothing to prefetch on either side: the next address is the previous read's data | 0 | design |
|
||||
| Banking plan | The chip's controller maps address bits to channel, bank and row through a table; a drawn permutation of the index bits costs a few hundred gates (the invention lane's 2.7, the spec's 1.13.1 draws of `M`, `R` and `pos` already); the card pays whatever the map does to its own bank parallelism, measured as a 0.8 to 3.2 percent spread across six eras | 0 to the chip; 0.8 to 3.2 percent to the cards | measured (Counter ASIC 2.0 layers 4 and 8) |
|
||||
| A fixed-function chip | Dead at the first draw, which is layer 1 already; a programmable chip pays USD 25 to 40 of N5 core, already in the record's 16.1 | nothing new | modelled |
|
||||
| The cadence | Drawing per epoch instead of per era changes nothing above: a table reload is microseconds | 0 | design |
|
||||
| The literature | No scheme since 2023 rotates an address map to defeat banking or prefetch; Ethash's 30,000-block reseed and RandomX's dataset rebuild are the only moving layouts, both firmware to every chip that shipped | null | claimed |
|
||||
|
||||
**KILL.** Covered by layer 1 and the invention lane's row; nothing a programmable chip pays beyond the core it already carries.
|
||||
|
||||
### 2.6 The literature since 2023, what the record's section 8 missed
|
||||
|
||||
| Item | What it found | Number | Label | URL (read 8 October 2026) |
|
||||
|---|---|---|---|---|
|
||||
| Choe, Moreshet, Bahar, Herlihy, "Attacking memory-hard scrypt with near-data processing", MEMSYS 2019 | the one paper that runs a memory-hard function on a PIM model: one in-order core per 128 MB vault | 1.5x over the host, no energy figure | simulated | https://par.nsf.gov/servlets/purl/10156963 |
|
||||
| Blocki and Smearsoll, TCC 2025; Blocki et al., CRYPTO 2026 (arXiv 2508.06795) | pebbling bounds on MTP-style and data-dependent memory-hard functions | complexity only, no joules | theory | https://arxiv.org/abs/2508.06795 |
|
||||
| SemiAnalysis, "The Memory Wall", 3 September 2024 | off-chip movement about 20x the cell read; an HBM access about 95 percent interface, 5 percent cell, for streaming | 2 pJ per bit interface against 0.18 pJ per bit activation amortised over a streamed row; for THIS hash's random 32-byte read the activation is 909 pJ against 1,150 of movement (44 / 56 percent), so a shorter link (lane B's base-die and bonded rows) cuts the larger half, which the model already carries | claimed; the split modelled | https://newsletter.semianalysis.com/p/the-memory-wall |
|
||||
| SK hynix GDDR7 at 35.4 Gb/s per pin, ISSCC 2024 paper 13.1 | PAM3 TX 1.103 pJ per bit, RX 0.764 pJ per bit | the I/O share of the 4.5 pJ per bit device figure | measured silicon | https://www.researchgate.net/publication/378947349 |
|
||||
| ETH SAFARI real-chip DRAM studies 2024 to 2026 | all on DDR4; no GDDR6, GDDR7 or HBM3 activate-energy measurement exists in public | the record's 909 pJ stands as the only figure | measured, DDR4 | https://arxiv.org/pdf/2405.06081 |
|
||||
| iPollo V2H, V2, V2X (Ethash, November 2024) | 3.4 GH/s at 475 W; 10 GH/s at 1,500 W; 1.2 GH/s at 165 W | 0.14 J per MH: about 13x an RTX 5090 at 1.8 J per MH on Ethash; the precedent's top row, above the record's X16-P at 6.8x; all of it the memory system | vendor spec, claimed | https://www.asicminervalue.com/en/miners/ipollo/v2h ; https://www.whattomine.com/coins/382-egaz-etchash/gpus/92-nvidia-geforce-rtx-5090 |
|
||||
| Jasminer X16-Q (Ethash, ETC) | 1,950 MH/s at 620 to 630 W | 0.32 J per MH, about 5.7x a 5090 | vendor spec | https://pool.kryptex.com/device/asic/Jasminer/x16-q |
|
||||
| Innosilicon A11 Pro (2021); iPollo V1 (June 2022) | 1.5 GH/s at 2,350 W; 3.6 GH/s at 3,100 W | 1.3x and 2.3x a 4090 | vendor spec | https://www.asicminervalue.com/miners/innosilicon/a11-pro-eth-1500mh |
|
||||
| Pinecone R1X (RandomX, January 2026) | 1.2 MH/s at 2,055 W, listed against the X9's 1 MH/s at 2,472 W | 1.71 J per kH; unverified, no unit seen | vendor spec, unverified | https://github.com/monero-project/monero/issues/10270 |
|
||||
| Qubic, 2026 | 23 to 34 percent of Monero's hash in withholding windows, never a sustained majority; pivot to Dogecoin ASICs 1 April 2026, Monero mining phased out | 27,000 XMR blocks, about USD 3.5 M | measured events | https://arxiv.org/abs/2512.01437v2 ; https://decrypt.co/363018 |
|
||||
| Karlsen, Iron Fish, Pyrin after the FishHash forks | no ASIC since April 2024; a ColEngine P2 FPGA did 32 GH/s Karlsen at 1,484 W before the fork | FPGA only | vendor | https://pool.kryptex.com/articles/ironfish-hardfork-en |
|
||||
| Conflux, Flux, Nexa, Ergo, Ravencoin, Beam, Tari, Zephyr | no dedicated chip 2024 to 2026 (Tari and Zephyr mined by the RandomX X5 class) | null | vendor | |
|
||||
| GDDR7 PHY nodes and GDDR6's oldest | Cadence N3 (2024); Innosilicon FinFET; GDDR6 at 12 nm (Cadence 12FFC, Six Semiconductor); nothing at 28 nm | section 2.3's correction | claimed | as 2.3 |
|
||||
| Proof of latency; era-rotating address maps | no scheme found, 2023 to 2026 | null | | |
|
||||
|
||||
Reading: nothing published since 2023 offers a lever outside the identity; every energy figure is a streaming pJ per bit, and no one has measured a per-random-read joule on GDDR6, GDDR7 or HBM3 at the controller, so the record's 2.0 and 1.2 nJ stand as the only model and are owed a measurement nobody can rent (the AWS F2 hour of chip-model 5.3 is still the one HBM2 instrument). The new rows change two precedents: the Ethash band's top is about 13x (claimed) not 6.8x, and the Monero chip story has a shipped-but-unverified successor to the withdrawn X9.
|
||||
|
||||
### 2.7 Three precedents, sourced for the 15:45 close (the research lane's ask of 14:0x UK; every figure read 8 October 2026)
|
||||
|
||||
A correction to the question as asked first: Monero did not go chip-free after November 2019; the Antminer X5 shipped in September 2023 at about parity to 1.5x per joule against the best measured CPU.
|
||||
|
||||
| Precedent (launch) | The designers' claim | The chip that shipped | Measured edge per joule against the best commodity part | Months to the first chip | Sources |
|
||||
|---|---|---|---|---|---|
|
||||
| Monero RandomX (fork 30 November 2019) | design.md, Introduction: a PoW "must achieve device binding by targeting specific features of existing general-purpose hardware"; section 3.4 SuperscalarHash: light-mode ASICs "will be bottlenecked by SuperscalarHash ... their efficiency will be destroyed by the high power usage"; no numeric bound anywhere in the document (claimed) | Bitmain Antminer X5, preorders 29 August 2023, delivered late September 2023: 212 kH/s at 1,350 W = 157 H/J (claimed); Antminer X9 announced 26 December 2025, 1,000 kH/s at 2,472 W = 405 H/J (claimed), USD 5,600, listed for July 2026, reported pulled on 31 July 2026 by a reseller with no Bitmain statement | Ryzen 9 9950X on xmrig 20.66 kH/s at 199 W = 104 H/J (hashrate.no, one verified submission, measured); Kryptex 27 kH/s at 170 W = 159 H/J (unlabelled); the X5 1.0x to 1.5x; the X9, if it exists, 3.9x | 46 | https://github.com/tevador/RandomX/blob/master/doc/design.md ; https://www.asicminervalue.com/miners/bitmain/antminer-x5 ; https://github.com/monero-project/monero/issues/10270 ; https://oneminers.com/blogs/news/antminer-x9-cancelled-what-bitmain-pulling-the-model-means-for-monero-mining ; https://www.hashrate.no/cpus/9950x/XMR ; https://pool.kryptex.com/device/cpu/AMD/ryzen-9-9950x |
|
||||
| Ethereum Ethash (mainnet 30 July 2015; PoW ended 15 September 2022, ETC continues) | a memory-hard DAG to keep GPUs competitive; no numeric bound (claimed) | first: Antminer E3, announced 3 to 4 April 2018, 180 MH/s at 800 W = 0.23 MH/J, shipped July 2018 with most units slipping to Q4 (claimed); current: iPollo V2H, released November 2024, 3,400 MH/s at 475 W (both plus or minus 10 percent) = 7.2 MH/J (vendor page, claimed) | RTX 4090 on Etchash 127 MH/s at 249 W = 0.51 MH/J; RTX 5090 215.8 MH/s at 405 W = 0.53 (hashrate.no, community-measured); the E3 under a good 2018 GPU per joule; the V2H 14x the 4090, 13.5x the 5090 | 36 | https://cointelegraph.com/news/bitmain-releases-ethash-asic-miners ; https://www.tweaktown.com/news/61527/bitmain-pushes-back-ethereum-asic-miner-now-coming-july/index.html ; https://www.ipollo.com/products/ipollo-v2h ; https://asicminervalue.com/miners/ipollo/v2h ; https://www.hashrate.no/gpus/4090 ; https://www.hashrate.no/gpus/5090 |
|
||||
| Kaspa kHeavyHash (mainnet 7 November 2021) | no resistance claimed: designed for optical PoW, described as ASIC-friendly by design (secondary sources) | IceRiver KS0, August to September 2023, 100 GH/s at 65 W = 1.54 GH/J; Antminer KS3, October 2023, 9.4 TH/s at 3,500 W = 2.7 GH/J; Antminer KS5 Pro, March 2024, 21 TH/s at 3,150 W = 6.67 GH/J (all claimed) | RTX 4090 2,080 MH/s at 226 W = 9.2 MH/J (Kryptex medium profile, GPU-reported watts); the KS0 167x, the KS5 Pro 725x | 21 to 22 | https://www.asicminervalue.com/miners/iceriver/ks0 ; https://www.asicminervalue.com/miners/bitmain/antminer-ks3 ; https://www.asicminervalue.com/miners/bitmain/antminer-ks5-pro ; https://www.kryptex.com/overclocking/nvidia-rtx-4090-micron-24gb-medium-overclock ; https://www.btc-echo.de/academy/bibliothek/kaspa-mining/ |
|
||||
|
||||
Reading: RandomX promised parity and got roughly it (1.0x to 1.5x after 46 months; the one chip claiming more never shipped); Ethash promised nothing numeric, bought 36 months before a chip worse than a GPU, and sits at about 14x today on a memory system without a GPU; Kaspa claimed nothing, got its chip in 21 months and went 167x to 725x in six. Every ASIC watt is nameplate and every CPU and GPU figure a community test table, so the edges carry about 20 percent either way.
|
||||
|
||||
## 3. This lane's own ideas
|
||||
|
||||
### 3.1 The second sector: W = 16 words, the whole 64-byte item (KILL at stock on measured rows; the knee row owed)
|
||||
|
||||
**The construction** is the read-width branch's `w64` class (`docs/plans/read-width.md` section 1 and 1.5, the fold of its section 1): a load reads the 64-byte-aligned item and folds its 16 words into the destination register with a rotate-multiply between words (`verify::fold_words`, mirrored in Metal, CUDA and OpenCL: four `uint4` loads from the aligned line, then fifteen rotl-mul-xor steps), so no function of the line alone replaces the dataset and the dependent chain is unchanged. Built 5 October; the pack in `proto-cuda/packs-readwidth/w64/`; three vector units and the cache and dataset checks passed on Metal, Apple OpenCL, the RTX 5090 (NVRTC) and the RX 9070 XT; the 2^24 fingerprint `836e56e7d496e980` equal on all four; the acceptance rule's rejection 0 to 14 of 60 as version 2; the CPU verifier 0.630 ms per 32 hashes against 0.604 (+4 percent). Two things were never priced before today: the chip side, and the card side at any occupancy but the exported one, with watts.
|
||||
|
||||
**The chip side, on the model's own inputs** (chip-model-v3 5.3: a random 32-byte read is 909 pJ of activation plus 256 bits at 4.5 pJ per bit of movement and I/O; HBM3 the same shape at O'Connor's 3.48 pJ per bit scaled by 4.12 / 6.25; the SRAM die on floor lane 3's wire figure):
|
||||
|
||||
| Memory | Read energy at W = 1 (one 32 B sector) | At W = 16 (two sectors in the open row: one activate, two column accesses) | `E_mem` per hash, W = 1 to W = 16 | Rate | Label |
|
||||
|---|---|---|---|---|---|
|
||||
| GDDR7, 16 devices | 2.0 nJ | 3.2 nJ (+56 percent; 2.9 at O'Connor's 3.5 pJ per bit, +45) | 0.466 to **0.62** (+33 percent; 0.58 at the lower movement figure); the 35 W of static and controller unchanged | activate-bound at 166 MH/s, unchanged; 1.36 TB/s of the board's 1.79 | modelled |
|
||||
| HBM3, one stack | 1.2 nJ | 1.8 nJ (+50 percent) | 0.321 to **0.40** (+24 percent) | 83.6 MH/s, unchanged | modelled |
|
||||
| HBM3, eight stacks | 1.2 | 1.8 | 0.262 to 0.33 (+26 percent) | unchanged | modelled |
|
||||
| The custom HBM4E base die (lane B) | 0.9 to 1.0 | about 1.5 | 0.18 to about 0.27 (+50 percent) | unchanged | modelled, approximate |
|
||||
| The N2 SRAM full store (floor lane 3's rows) | 0.25 nJ (80 bits of wire) | 0.88 nJ (560 bits) | 0.036 to 0.126 (+250 percent): 66x to 19x against the 5090's stock point | power-bound: 8.3 to 2.4 GH/s per die | modelled (lane 3) |
|
||||
|
||||
**The card side, measured today** (8 October 2026, 13:2x to 13:4x UK; four cards on five pods; RunPod one-shot pods under the fleet's `oneshot.py`, the image `nvidia/cuda:12.8.1-devel-ubuntu24.04`; the worker built on each pod from this branch's `proto-cuda/nvrtc/worker.cpp` with `g++ -O2 -std=c++17 ... -ldl` through its Linux `dlopen` path, sha256 prefix 30eea48b; stock clocks, no power cap change; the packs `w4` (the lottery hash), `w64` (the item) and `w64-l2` (the `w64` kernel text with the first `uint4` load of each item replaced by inline PTX `ld.global.L2::64B.v4.u32`, so the L2 fetches the 64-byte line as one request; nothing else changed); every row's self-test PASS (96 of 96 vector lanes) and the 2^24 fingerprint `836e56e7d496e980` on every `w64` and `w64-l2` row, `25f96e7dce90bd4e` on every `w4` row, so the hinted text is bit-exact; rows of 2^24 nonces x 60 batches with `nvidia-smi` at 1 Hz over the row, the watts the mean of the upper half of the row's samples; a first pass of 5-batch rows for the shape sweep, a second of 60-batch rows for the watts; the sparse shapes are the worker's `sp<N>-w<W>` persistent grids, N = the card's SM count, W warps per block, so lanes in flight = N x W x 32):
|
||||
|
||||
| Card (architecture, memory) | `w4`: MH/s at W | `w64` exported, full occupancy | `w64` best sparse shape | `w64-l2` best form | On the 95 percent line | Energy per hash, `w4` to the best `w64` form | Label |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| RTX 4090 (Ada, GDDR6X, 128 SMs; driver 595.71) | 64.73 at 225 W (3.48 microjoules); idle 29 W | 33.75 (-48 percent) at 242 W | sp128-w1 (4,096 lanes) 53.98 (-17) at 258 W | **full occupancy 63.65 (-1.7 percent) at 296 W**; sp128-w1 58.5 (-10) at 264 | **PASS on rate** | 3.48 to 4.66: **+34 percent** | measured |
|
||||
| H100 SXM (Hopper, HBM3, 132 SMs; driver 580.126) | 253.4 at 442 W (1.74); idle 70 W | 171.4 (-32) at 469 W | sp132-w8 200.3 (-21) at 463 W | 171.4 at full occupancy, 200.3 at sp132-w8: **the hint does nothing on Hopper** | FAIL (79 percent) | 1.74 to 2.31 (best form): +33 percent; the full-occupancy form 2.74, +57 | measured |
|
||||
| RTX 3090 (Ampere, GDDR6X, 82 SMs; driver 580.159) | 60.00 at 321 W (5.35) | 23.95 (-60) at 330 W | sp82-w2 42.19 (-30) at 334 W | sp82-w2 52.34 (-13) at 349 W; sp82-w4 51.0 (5-batch row) | FAIL (87 percent) | 5.35 to 6.67: +25 percent | measured |
|
||||
| RTX 5090 (Blackwell, GDDR7, 170 SMs; a secure-cloud pod, driver 580.126, bus 02:00.0; the community host 216.249.100.66 answered `cuInit` 999 on three pods, all destroyed) | 142.5 to 142.7 at 311 to 319 W (2.21 microjoules; the record's 2.26 at 311 W); idle not settled on the pod | 75.3 (-47 percent) at 344 W (the 5 October row, 71.9, reproduced) | sp170-w1 (5,440 lanes) 101.5 (-29) at 343 W; sp170-w8 88.2 | full occupancy 110.4 (-22.5) at 392 W; **sp170-w4 (21,760 lanes) 145.7 (+2.3 percent) at 401 W**; sp170-w8 141.9 (-0.4) at 408 | **PASS on rate at the sparse shape** | 2.21 to 2.75: **+25 percent** | measured |
|
||||
| RX 9070 XT, Apple M5 Max | 5 October: -3 percent and 0 at 64 bytes (a 64-byte line per read already) | | | not re-run | free | 0 on the memory side | measured 5 October |
|
||||
|
||||
The rows are the record: the 4090 and H100 pods were destroyed before their raw logs were copied home (the copy step failed quietly on a shell expansion), so their figures above are the extracted rows read off the pods in the session; the 3090's raw logs are at `~/igneum-fleet/fl5-results/3090-*` (result.log, result2.log, power.csv, power2.csv). Spend: about USD 6 of the USD 150 allowed.
|
||||
|
||||
**What the rows say, on the identity.** (1) The rate question: the exported form's -47 to -60 percent is a request-shape and occupancy effect, as the 5090's probe said; one PTX qualifier on the first load makes the 64-byte item one L2 request and holds the 4-byte rate on Ada at full occupancy (98 percent), while on Hopper the hint changes nothing (the H100's loss is elsewhere: its HBM3 pseudo-channel moves a 64-byte access as two bursts and its best shape is 79 percent) and on Ampere the best shape is 87 percent. (2) The energy question, which decides it: the card pays the second sector at the cost of a whole dependent read, not at the DRAM's movement term. On the 4090 the hinted form draws 71 W more at the same 64.7 MH/s: 71 W over 8.3 G reads per second is **8.6 nJ per second sector**; on the 5090 at sp170-w4, 88 W more at 145.7 MH/s: **4.7 nJ**; against the 1.15 nJ the DRAM device charges for it (the 4090's GDDR6X at 6 pJ per bit would be 1.5), so the other 7 nJ is the card's own L2, crossbar, L1 and the SMs' longer wait, the same anatomy as the record's 8.7 nJ whole-card marginal per dependent read (15.1a). In the identity's terms `F` is 128 x 8.6 nJ = 1.1 microjoules per hash on the 4090 (0.6 on the 5090) and the chip's cost for the same work is 128 x 1.15 = 0.15: **`k` about 0.13 on the 4090, 0.24 on the 5090**, under the ALU shadow's 0.3 to 0.8 and no better than the L2 hot table's. The energy per hash rises 34 percent on the 4090 and 33 percent on the H100's best form against the modelled chip's 33 (GDDR7) and 50 (HBM3): the edge at zero shadow moves 0 on Ada against the GDDR7 chip and 0.9x in the chip's favour against HBM3 on Hopper; against the SRAM die the chip's 66x to 19x becomes about 25x against the card's new energy, not 19x. (3) The knee, which could have changed the sign, did not (the hash lane's PC 1 row, section 6: the hinted form at the 1,300 lock reads 37.6 MH/s against 100.6 at 2.3x the energy per hash; the hint recovers nothing in the latency-bound regime on Blackwell). The reasoning that made it the one open row, for the record: at the 1,300 MHz lock the 5090's fixed share is smaller and its fabric runs at a lower voltage, so the sector's 8.6 nJ would fall with the measured 10.9 to 8.7 nJ per read (15.1a) and the card's rise would be a larger fraction of a smaller base; the lever lives only if the card's energy per hash rises under about 20 percent there, and a rented pod cannot read it (`nvidia-smi -lgc` refused: "does not have permission to change clocks"). That row ran on PC 1 at 15:0x UK and is in section 6.
|
||||
|
||||
**Verdict: KILL at stock on measured rows; W = 16 words stays out of class v6; the design's "never 16" stands, for the measured reason (the card's fabric price of a sector) rather than the plan's (the pins).** What is kept: the hinted one-request load as a kernel fact for Ada (a 64-byte dependent read at the 4-byte rate), which a future width decision can use; the measured 8.6 nJ per sector as the card-side figure the chip model's width rows lacked; and the per-architecture rate table above.
|
||||
|
||||
Cost to the honest tiers and the rule of 5 October: nothing ships, so nothing changes for any tier; had it shipped, the Ada and Blackwell tiers would have paid about a third more energy per hash for no rate, Hopper a third more for a fifth of its rate, Ampere a quarter more for an eighth of its rate, AMD and Apple nothing, the verifier 4 percent, and every DRAM chip the same third.
|
||||
|
||||
### 3.2 Two items per read: W = 32, 128 bytes, one NVIDIA L2 line (KILL with 3.1)
|
||||
|
||||
The chip at W = 32: one activate plus four column accesses, 5.5 nJ per read; 21.3 G activates x 128 B = 2.7 TB/s is over the board's 1.79, so the GDDR7 chip becomes bandwidth-bound at 14 G reads per second, 110 MH/s, `E_mem` 1.02 microjoules (+120 percent); one HBM3 stack 0.66 (+105); the SRAM die about 0.21 (11x). The card: the pins bind the 5090 at 2.3 TB/s before any fabric cost (-20 percent of rate at best), and the measured sector price of 3.1 (8.6 nJ per sector at stock) puts three more sectors at about 3.3 microjoules per hash on the card against the chip's 0.44: `k` about 0.13 again, on a card that has also lost a fifth of its rate. Dead with 3.1; no `w32` pack is needed.
|
||||
|
||||
### 3.3 Independent chains per hash (memory-level parallelism as the card's lever) (KILL, one probe would reopen it)
|
||||
|
||||
The idea: four independent dependent chains of 32 reads per hash, folded at the end, so each resident hash keeps four reads in flight and an occupancy-bound card climbs toward its memory's activate ceiling while the chip (already at the ceiling) gains nothing; an `E_card` lever. The number: the 5090 reads 17.5 to 18.2 G per second at 4 B as "the best over lanes in flight" (the 5 October probe), which means more lanes did not help, so the bind is the memory system, not the in-flight count; the bound is the ceiling's 21.3 G, +17 to +22 percent, and the expectation is 0 to 5 percent (the microbench's `l2_indep4` against `l2_chase` read +2 percent on the L2-bound pattern). Today's rows agree from the other side: on the 4090 the `w4` hash at 64.7 MH/s is 8.3 G reads per second, the probe's own ceiling (8.3 to 8.9), at every occupancy from one warp per block to the sparse grids (64.70 to 64.74). KILL; the probe that would reopen it is one `dram_indep4_1g` row in the microbench.
|
||||
|
||||
### 3.4 Row-straddling items (two activates per read) (KILL, dominated)
|
||||
|
||||
A 64-byte item laid across a row boundary costs the chip two activates: 2 x 0.909 + 2 x 1.15 = 4.1 nJ per read (+105 percent) and halves its activate-bound rate (83 MH/s), so `E_mem` = 0.94 microjoules; the card's activate-bound rate halves too, at the fabric price of 3.1 on top. Dominated by the aligned item, which is itself dead. KILL.
|
||||
|
||||
### 3.5 Scratch state in DRAM (KILL)
|
||||
|
||||
A per-lane scratch line in DRAM, read-modify-written per step, would be `k = 1` work if it reached the DRAM on both sides; it does not: 7,262 hashes in flight x 64 B is 465 KB, inside the 5090's 96 MB L2 (an L2 hit at 1.4 nJ, `k` 0.1 to 0.3 against the chip's lane SRAM), and a scratch large enough to miss the L2 (13 KB per lane) is 15 MB of SRAM on the chip (about USD 5 of N5). The 5 October scratch rows measured the card's side: -12 to -48 percent of rate on the 5090. KILL (`docs/analysis/scratch-soundness.md` and the read-width entry say the same).
|
||||
|
||||
### 3.6 A note on the sunk card (not a candidate)
|
||||
|
||||
Both sides are capex-dominated (the record's 16.1: 7x to 10x their electricity per MH/s-hour), and the chip's economic edge is capex per MH/s (USD 2.8 against 14.7). For a home miner whose card was bought for something else the capex is sunk, so that tier's cost per MH/s-hour is its electricity alone, USD 0.000084 at the knee and USD 0.05 per kWh, against the chip's all-in USD 0.00018 (capex over two years plus electricity): the sunk card is 2.2x CHEAPER per MH/s-hour than the chip until the chip's capex is amortised. The honest-denominator lane owns this reading; it is recorded here because it is the one place the identity's per-joule framing understates the honest tier.
|
||||
|
||||
## 4. The KILL list, with the number
|
||||
|
||||
| Candidate | The identity check | The number | Label |
|
||||
|---|---|---|---|
|
||||
| The second sector, W = 16 words (the whole item) | the card pays the sector at its own fabric's price: `k` about 0.13; energy per hash +34 percent (4090), +33 (H100 best form), +25 (3090) against the chip's +33 | 8.6 nJ per sector on a 4090 at stock; the rate holds on Ada with the hint (98 percent), fails on Hopper (79) and Ampere (87) | measured card, modelled chip |
|
||||
| Two items per read, W = 32 | the same `k` on three more sectors, on a card the pins bind at -20 percent | chip 1.02 microjoules; card about 3.3 of sector work | modelled on the measured sector price |
|
||||
| Tensor tiles as the shadow (int8) | `k` 0.2 to 0.4 on the class's tile, 0.4 to 0.7 on the dense tile: never above the ALU shadow's band | the M5 Max -35 percent at 1,024 tiles, -78 at 4,096 (measured); the same premium as the ALU shadow by construction | measured card, claimed chip |
|
||||
| FP8, FP16, BF16 tiles; the texture interpolator; the rasteriser | not bit-exact across vendors | | spec and vendor documents |
|
||||
| The hardware video decoder | bit-exact by standard, but the honest card is decoder-bound (a fixed engine count) and the verifier decodes at milliseconds per frame | engines per card two to four (approximate) against any count on a chip | approximate |
|
||||
| The RT core | traversal implementation-defined on Vulkan, DXR, OptiX and Metal; the BVH opaque; `k` 0.01 to 0.2 on the public per-ray figures | 4 to 33 nJ per ray on a dedicated unit (claimed) against 290 to 750 measured board-level on an RTX 2080 | claimed and measured |
|
||||
| Memory-level parallelism per dollar | no asymmetry per channel; the correction is the PHY's node (section 2.3, kept as rank 1) | `E_mem` under +0.06 microjoules | claimed, modelled |
|
||||
| Proof of latency | the chain checks values; the block is 1 s against a 0.4 microsecond round trip; the farm's node is local; a sequential prefix favours the chip about 4x | 2.5 million round trips per block | measured card latency, modelled chip |
|
||||
| The drawn address map | firmware; no prefetch exists for a dependent chain | 0 to the chip, 0.8 to 3.2 percent to the cards | measured spread |
|
||||
| Independent chains per hash | the card's read rate sits at its probe's ceiling at every occupancy | the 4090 64.70 to 64.74 MH/s across six shapes; expected 0 to 5 percent on the 5090, bound 22 | measured |
|
||||
| Row-straddling items | dominated by the aligned item, which is dead | | modelled |
|
||||
| Scratch in DRAM | the card's scratch sits in L2; the chip's in SRAM | 465 KB against 96 MB | measured rows |
|
||||
|
||||
## 5. Consequences per tier (the standing rule of 5 October 2026)
|
||||
|
||||
| Tier | What this file means | What is being done |
|
||||
|---|---|---|
|
||||
| Home miner, one 8 GB NVIDIA card (32-byte sectors) | nothing ships; had W = 16 shipped, this tier would have paid about a third more energy per hash for no rate (Ada) and an eighth of its rate too (Ampere) | nothing; the knee row on PC 1 is a formality |
|
||||
| One 12 GB card (4070, 5070) | the same | the same |
|
||||
| One 16 GB card (RX 9070 XT) | nothing (its 64-byte line is free at W = 16, measured 5 October); its row against the chip is unchanged at 22.7x on class v3 | the vendor-share metric |
|
||||
| One 24 or 32 GB card (5090 class) | nothing ships; had W = 16 shipped, the 5090 would have held its rate at a sparse shape and paid +88 W (+25 percent per hash) at stock, and at its knee lost 63 percent of its rate at 2.3x the energy (measured, PC 1); the record's 3.6x at the knee and 2.1x at `k = 1` stand | nothing |
|
||||
| Apple (M5 Max and the unified tiers) | nothing (free at 64 bytes, measured 5 October); the honest best per joule is unchanged | nothing |
|
||||
| A rig | nothing ships; the two model corrections raise the chip's project floor (USD 20 M to 75 M), which is the rig's real wall, by 4x to 15x over the record's cheapest row | the chip model's rows corrected (the Counter lane) |
|
||||
| A pool user | nothing changes | |
|
||||
| A node operator (the verifier) | nothing changes (the +4 percent of `w64` never ships) | |
|
||||
| A chip | the cheapest project is a 12 nm GDDR6 part at USD 20 M to 30 M or an N5 GDDR7 part at 50 M to 75 M, not a 28 nm one at 5 M; its `k` on tile work 0.2 to 0.7, not 0.03 to 0.3; its cost of the second sector (+33 percent) was matched by the card's | chip-model-v3 5.4, 5.6 and 5.11 and the record's 16.2 corrected (owed to the Counter lane) |
|
||||
| The public claim | unchanged by this lane on the hash; the tensor `k` sentence loses "4x to 30x"; the Ethash precedent gains the 13x row; the cheapest-chip project line gains a floor | the Counter lane's texts |
|
||||
|
||||
## 6. Unverified and owed
|
||||
|
||||
- The knee row: DONE by the hash lane on PC 1's 5090 (kit d, 14:51 to 15:0x UK, the v5 kit's CUDA worker, `--block-warps 1`, 250 batches of 2^24, nvidia-smi 1 Hz, fingerprint 836e56e7d496e980 PASS on every `w64` row). Unlocked: `w4` 119.95 MH/s at 308.2 W (2.57 microjoules); `w64` 63.35 at 332.9 W; `w64-l2` 92.35 at 369.1 W (4.00 microjoules): the hint recovers 46 percent of the plain form's loss at full occupancy and stays 23 percent under the 4-byte rate at 1.56x its energy per hash. At the 1,300 MHz lock: `w4` 100.55 at 190.5 W (1.89 microjoules); `w64` 37.35 at 177.8 W; `w64-l2` 37.59 at 164.1 W (4.37 microjoules): **at the knee the hint gives nothing and the card's energy per hash is 2.3x the 4-byte row's**, far over the 20 percent line. The lever is dead at the knee as at stock; the sparse shape (sp170-w4) was not in the kit's worker and was not asked for again, because at the lock the card's fixed share is smaller and the sector's share larger, so no shape can bring a 2.3x rise under 1.2x. This closes every row this lane owed on W = 16.
|
||||
- The 4090's and H100's raw logs were not copied before their pods were destroyed; the rows in 3.1 are the figures read off the pods during the session. The 5090's and the 3090's raw logs are at `~/igneum-fleet/fl5-results/5090-*` and `3090-*` (result.log, result2.log, result3.log, power*.csv).
|
||||
- The watts are `nvidia-smi` board power at 1 Hz, the upper half of each row's samples averaged; idle on the rented pods read 29 W (4090) and 70 W (H100); the 3090's idle row was taken straight after a run and is not a settled idle.
|
||||
- The chip side of 3.1 is the model's own data-movement term applied twice (4.5 pJ per bit, Micron, claimed, streaming; O'Connor's 3.48 for HBM2) and lane 3's wire figure for the SRAM die; no one has measured a second-sector column read's energy on GDDR7 (section 2.6: the public record has no random-read joule on any current part).
|
||||
- The tensor chip figures are vendor specifications at TDP (claimed), the M4 ANE and the H800 loop measured; the RT figures are claims and one measured RTX 2080 row; the PHY node facts are vendor IP pages read today.
|
||||
- The project-cost corrections use the history's figures (a 7 nm-class startup project USD 50 M to 75 M, claimed; the 12 nm figure scaled, approximate) and the mission lane's cap model.
|
||||
- The `w64` class's census under the sub-version 3 rule and its F8 row, which this lane owed before any cut, are DONE by the census lane (`docs/analysis/class-v6/census-w16-mix.md` on `class-v6-census` at d20eb04bd, 14:0x UK): 256 of 256 seeds accepted with and without an era (0.704 rejection per candidate against the control's 0.668; 0 exhaustions; 0 window-bit refusals for the instrument's reason; 189 stride-bit refusals of 1,524 candidates against the control's 218; (c''') 1.4 percent); the F8-form top-0.1-percent share 0.999 to 1.006 of the uniform control at 2^20 on 16 seeds; the verifier +0.6 percent inside a 4 percent drift on a loaded core (this file's +4 percent neither confirmed nor refuted; a quiet-box row owed). One structural row for 3.1: at 16 words the product's low bits are alignment, so the whole-item read removes the product's own bias from the address (the control carries it at bit 0 in 116 of 256 programs); the stride bit R stays until the index fold. The class is sound; it is dead on energy, not on soundness.
|
||||
- Nothing ran on the Mac, on a Hetzner box or on a PC for this file; the measurements ran on four RunPod one-shot pods, all destroyed on their done (the 5090 pod when its rows are in).
|
||||
|
||||
## 7. Sources
|
||||
|
||||
Internal: `docs/analysis/counter-asic-4-research.md` (sections 2, 4, 5, 8, 15.1a, 16, 20), `docs/analysis/chip-model-v3.md` (5.1, 5.3, 5.4, 5.6, 5.10 to 5.12), `docs/design/class-v6-rotating-family.md` (sections 2, 7, 7a, 7c, 10 and 10.3), `docs/analysis/class-v6/hardware-future.md` at 7618e729 (sections 0, 1, 2, 4.9, 5), `docs/analysis/class-v6/history.md` (sections 3 and 5), `docs/analysis/class-v6/invention.md` on `class-v6-invention` (sections 1 and 2), `docs/analysis/class-v6/floor/sram-and-floor.md` on `class-v6-floor-sram` as carried in section 10.3, `docs/plans/read-width.md` (sections 1, 1.5, 4.1), `docs/bench-log.md` (the 5 October read-width entry: the probes and the hash rates), `proto-cuda/packs-readwidth/w64/kernel_bound.cu` (the four `uint4` loads per item), `proto-cuda/nvrtc/worker.cpp` (`variantSource`, the `sp<N>-w<W>` shapes, the Linux `dlopen` path), the fleet's `oneshot.py` and the rented rows' logs under `~/igneum-fleet/fl5-results/`, `docs/analysis/scratch-soundness.md`, spec 01 (1.5, the item table).
|
||||
|
||||
External, all read 8 October 2026 by this lane's three research sub-agents (tensor, RT, literature) and labelled where carried: the NVIDIA RTX 5090 page and the Blackwell architecture whitepaper; the Ada whitepaper; arXiv 2501.12084v2 (Hopper tensor-core microbenchmarks); Lenovo LP2226 (B200); the AMD MI300X data sheet and MI355X notes; the RX 9070 XT page; Qualcomm's AI 100 Ultra brief; eeNews Europe (MTIA v2); AnandTech 21342 (Gaudi 3); the Hot Chips 2025 Ironwood slides; arXiv 2608.28048 (Trainium2); Tenstorrent's Blackhole specification; the M4 ANE measurement (maderix.substack.com); cnx-software (Hailo-8); design-reuse 52539 (Untether); hackster.io (Axelera); NVIDIA Research's VSQ accelerator (JSSC 2023); Dally, Hot Chips 2023 keynote, slides 12 and 24; the Vulkan ray traversal chapter; the DXR specification; the NVIDIA developer forum thread 309730 (14 October 2024); the Blender Cycles source note; the Mach-RT TVCG preprint; Hot Chips 31 (Turing); the AMD Hot Chips 2025 RDNA 4 slides; embedded.com (RayCore); AnandTech 7870 (GR6500); arXiv 2409.06000 (RayFlex); the TRaX CGI 2018 study; Ylitie et al., HPG 2017; Chou et al., MICRO 2023; par.nsf.gov 10156963 (Choe et al., MEMSYS 2019); arXiv 2508.06795; SemiAnalysis "The Memory Wall"; the SK hynix ISSCC 2024 GDDR7 paper (ResearchGate 378947349); arXiv 2410.12990; arXiv 2405.06081; asicminervalue (iPollo V2H, V1, A11 Pro); kryptex (Jasminer X16-Q, FishHash); whattomine (the 5090 on Etchash); monero-project issue 10270; arXiv 2512.01437v2 and decrypt.co 363018 (Qubic); Business Wire 20240925780123 (Cadence GDDR7 on N3) and 20221115005674 (GDDR6 at 12 nm); innosilicon.com GDDR7; kurnal-insights (GB202 die); TrendForce 24 September 2026 (the 2 GB GDDR7 part); arXiv 2403.13230 (BFT-PoLoc); eprint 2026/694 (unread, refused); the PTX ISA (`ld` with the `.L2::64B` prefetch-size qualifier, sm_75 and later).
|
||||
324
docs/analysis/class-v6/floor/shadow-k.md
Normal file
324
docs/analysis/class-v6/floor/shadow-k.md
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
# The shadow's k from RTL (floor lane 2, 8 October 2026)
|
||||
|
||||
Branch `class-v6-floor-k` from `counter-asic-4` at 7618e729. Every chip-side number in this file comes from
|
||||
synthesis and place-and-route of RTL written for this lane (Yosys 0.68 plus OpenROAD, the ORFS docker image
|
||||
`openroad/orfs:latest`, the ASAP7 predictive PDK, run on igneum-build-4), with switching activity from a
|
||||
random-input gate-level simulation (iverilog on the routed netlist). Every GPU-side number is the record's
|
||||
measurement (`docs/analysis/counter-asic-4-research.md` 15.1a: PC 1, the RTX 5090, 20 probes at 60 s, 8
|
||||
October 2026) and is not re-estimated here. The RTL, testbenches, flow configs and a Makefile that reproduces
|
||||
every row are under `tools/chip-model/rtl/`.
|
||||
|
||||
## 0. One page
|
||||
|
||||
(filled from the rows below; see section 2 for the table and section 4 for the edge)
|
||||
|
||||
## 1. The question and the identity
|
||||
|
||||
The chip's energy per hash is `E_chip = E_mem + k x F` (research file section 2): `E_mem` the memory system's
|
||||
reads, controller and static (0.466 microjoules per hash on the record's GDDR7 board, modelled), `F` the shadow's
|
||||
premium on the card (measured: 1.10 microjoules unlocked, 0.652 at the 1,300 MHz lock on the 5090 for class
|
||||
v4's 102,100 counted ops per hash), and `k = e_chip / e_gpu` the chip core's energy per forced op over the
|
||||
card's. The card's side of `k` is measured; the chip's side has been a claimed band (ALU 0.3 to 0.8, int8 tile
|
||||
0.03 to 0.3, L2 hit 0.1 to 0.3, shuffle 0.4 to 0.7). This file replaces the claimed side with a synthesised one,
|
||||
family by family, and reads the mix that maximises the expected `k` under the invention lane's sound-form rules.
|
||||
|
||||
## 2. Method
|
||||
|
||||
### 2.1 What was built (RTL, `tools/chip-model/rtl/rtl/`)
|
||||
|
||||
Each family is a minimal chip-side shadow core: the smallest circuit a chip maker would have to build to run that
|
||||
family's instructions bit-exactly at one op per cycle per lane. The common lane shape is an instruction register,
|
||||
an 8 x 32-bit register window in flops (the card's lane holds its working set in a register file too; 8 registers
|
||||
is the class program's window), two or three read ports through muxes, the functional unit, and one write port.
|
||||
Nothing is shared across lanes and nothing is pipelined beyond one stage, so the figure is the datapath plus the
|
||||
minimum operand delivery: a floor for the chip, which makes the `k` it gives a floor too.
|
||||
|
||||
| Family | Module | What one op is | Ops per cycle | Clock set (ps) |
|
||||
|---|---|---|---|---|
|
||||
| ARX | `lane_arx` | int32 add, sub, xor, or, rotl by immediate, rotr by register (op drawn per cycle; rows per op by fixing the op field) | 1 | 1,000 |
|
||||
| MUL | `lane_mul` | 32 x 32 to 64: mul (low word), mulhi (high word), mad (src x src2 + dst) | 1 | 1,500 |
|
||||
| PRMT | `lane_prmt` | byte permute: 4 output bytes from the 8 bytes of two registers by a 16-bit selector, sign-replicate bit as on the card | 1 | 1,000 |
|
||||
| LOP3 | `lane_lop3` | three-input logic by an 8-bit truth table, bitwise | 1 | 1,000 |
|
||||
| FOLD | `lane_fold` | the index fold `((rotl(x * M, R) & WM) \| OFF) & MASK` with the era's constants held in registers | 1 | 1,500 |
|
||||
| SHFL | `shfl32` | the 32-lane xor-mask shuffle `r[dst][lane] ^= r[src][lane ^ m]` over a 1 KB window (32 lanes x 8 x 32 bits): a 5-stage butterfly | 32 lane-ops | 1,000 |
|
||||
| XBAR | `xbar32` | the general 32-lane crossbar over the same window (any source lane per lane, a 5-bit select each): the upper bound on a shuffle network | 32 lane-ops | 1,000 |
|
||||
| SCRATCH | `scratch8k` | one random 32-bit read of a 2,048 x 32 (8 KB) flop array, a write on one cycle in eight: the chip's L1 as the pessimistic flop form | 1 read | 1,500 |
|
||||
| TILE | `tile8` | the int8 8x8x8 tile `C += A x B` with int32 accumulators: 512 MACs per cycle | 512 MACs | 2,000 |
|
||||
|
||||
### 2.2 The flow
|
||||
|
||||
ORFS on ASAP7 (7.5-track RVT cells, the TC corner: 0.70 V, 0 C, NLDM), the default flow end to end: Yosys
|
||||
synthesis with ABC, floorplan at 40 percent utilisation, global and detailed placement, CTS, global and detailed
|
||||
routing, parasitic extraction (OpenRCX, the platform's rules). Power is OpenSTA's `report_power` on the routed
|
||||
design with its SPEF, under two activities: (a) the VCD of a random-input simulation of the routed netlist
|
||||
(ASAP7 has no cell Verilog models in the image, so Yosys builds the cell bodies from the liberty functions and
|
||||
writes the netlist as primitives; iverilog runs 3,000 to 4,000 cycles with every instruction field drawn by
|
||||
`$random` each cycle and a random 32-bit load into the window every 16th cycle), and (b) a propagated 0.5
|
||||
activity on every input as the cross-check. The energy per op is the total power (internal, switching and leakage)
|
||||
times the clock period over the ops per cycle. Leakage is reported beside it; at these clocks it is a few percent.
|
||||
|
||||
### 2.3 Node scaling (approximate; every factor claimed from the foundry's own headline)
|
||||
|
||||
ASAP7 is a predictive 7 nm-class FinFET PDK (ASU and ARM, Clark et al., Microelectronics Journal 2016), not a
|
||||
foundry node, so the row is first stated at ASAP7 and then scaled by the foundry's published per-node power
|
||||
reductions at the same speed: N7 to N5 x0.70 (TSMC: "30 percent lower power"), N5 to N3E x0.72 (TSMC: "25 to 30
|
||||
percent lower power", the midpoint), N3E to N2 x0.72 (TSMC: "25 to 30 percent lower power", the midpoint). So
|
||||
N5 = 0.70, N3 = 0.50 and N2 = 0.36 of the ASAP7 figure. These are the foundry's claims for a whole design at a
|
||||
fixed frequency, and a shadow core at a low clock could run at a lower voltage still; the N2 column is therefore
|
||||
the chip's best case from this method, not its floor. Sources in section 7.
|
||||
|
||||
### 2.4 What the method leaves out, on both sides
|
||||
|
||||
On the chip side the figure omits instruction fetch and decode (a chip would run the program from a small SRAM
|
||||
or a decoded instruction cache shared by many lanes, about 1 to 2 pJ per lane-instruction amortised across 32
|
||||
lanes, approximate), the clock tree beyond the block's own, and the result's move to a memory address unit. On
|
||||
the card side the 15.1a figure is the whole card's marginal per counted op (the sleep floor subtracted), which
|
||||
includes the card's own fetch, decode, operand collection and register file. So the `k` here is the chip's
|
||||
datapath-and-window cost over the card's whole-lane cost: a FLOOR on the chip's cost and so a floor on `k`.
|
||||
|
||||
## 3. The rows: per-unit floors (the minimal lane per family)
|
||||
|
||||
Every row: ASAP7 routed, SPEF, OpenSTA `report_power` under the random-input gate-level VCD (every pin annotated,
|
||||
0 unannotated), the TC corner (0.70 V). "pJ/op" is total power (internal + switching + leakage) times the clock
|
||||
period over the ops per cycle. The propagated-0.5 cross-check is in `table.csv`; it agrees within 2x on the
|
||||
logic lanes and overestimates the multiplier lanes 50x (OpenSTA's statistical propagation through a multiplier
|
||||
is not a measurement), so the VCD row is the row. The GPU side is 15.1a: pJ per counted op on the RTX 5090 at
|
||||
stock and at the 1,300 MHz lock, and 6.9 pJ per counted op on the Apple M5 Max (the class v4 shadow, measured).
|
||||
`k` is absolute: the chip's own pJ per op at the node over the card's at its point.
|
||||
|
||||
| Family (chip RTL) | Cells (with fill) | pJ/op ASAP7 | pJ/op N5 | pJ/op N3 | pJ/op N2 | 5090 pJ/op stock / lock | k, N3 chip vs 5090 stock / lock | k, N3 vs M5 Max 6.9 | k, ASAP7 unscaled vs lock | Label |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| ARX add | 11,631 | 2.16 | 1.51 | 1.09 | 0.78 | 11.3 / 6.2 | 0.096 / 0.18 | 0.16 | 0.35 | synthesised; N3 and N2 scaled (claimed) |
|
||||
| ARX sub | | 2.14 | 1.50 | 1.08 | 0.78 | 11.3 / 6.2 | 0.095 / 0.17 | 0.16 | 0.35 | |
|
||||
| ARX xor | | 2.09 | 1.46 | 1.05 | 0.76 | 11.3 / 6.2 | 0.093 / 0.17 | 0.15 | 0.34 | |
|
||||
| ARX rotl (immediate) | | 2.15 | 1.50 | 1.08 | 0.78 | 11.3 / 6.2 | 0.095 / 0.17 | 0.16 | 0.35 | |
|
||||
| ARX rotr (by register) | | 2.13 | 1.49 | 1.07 | 0.77 | 11.3 / 6.2 | 0.095 / 0.17 | 0.16 | 0.34 | |
|
||||
| or (lossy; the values saturate to ones and the activity falls) | | 1.45 | 1.01 | 0.73 | 0.53 | 11.3 / 6.2 | 0.065 / 0.12 | 0.11 | 0.23 | |
|
||||
| ARX random mix | | 2.24 | 1.57 | 1.13 | 0.81 | 11.3 / 6.2 | 0.10 / 0.18 | 0.16 | 0.36 | |
|
||||
| mul (32 x 32, low word) | 19,603 | 1.35 | 0.94 | 0.68 | 0.49 | 13.9 / 8.3 | 0.049 / 0.082 | | 0.16 | |
|
||||
| mulhi (high word; the same multiplier) | | 1.35 | 0.94 | 0.68 | 0.49 | 39.6 / 21.0 | 0.017 / 0.032 | | 0.064 | |
|
||||
| mad (src x src2 + dst; three reads) | | 3.29 | 2.30 | 1.66 | 1.19 | 13.9 / 8.3 | 0.12 / 0.20 | | 0.40 | |
|
||||
| mul random mix | | 1.53 | 1.07 | 0.77 | 0.56 | 13.9 / 8.3 | 0.055 / 0.093 | | 0.18 | |
|
||||
| index fold (x M, rotl R, masks; era constants in registers) | 14,965 | 2.37 | 1.66 | 1.19 | 0.86 | 13.9 / 8.3 (read against int_mul) | 0.086 / 0.14 | | 0.29 | |
|
||||
| prmt (byte permute) | 6,744 | 1.27 | 0.89 | 0.64 | 0.46 | 22.3 / 11.5 | 0.029 / 0.056 | | 0.11 | |
|
||||
| lop3 (8-bit truth table) | 7,274 | 1.42 | 0.99 | 0.72 | 0.52 | 24.1 / 13.0 | 0.030 / 0.055 | | 0.11 | |
|
||||
| 32-lane xor-mask shuffle (butterfly over the 1 KB window), per lane-op | 181,580 | 1.24 | 0.87 | 0.63 | 0.45 | 55.8 / 29.4 | 0.011 / 0.021 | | 0.042 | routed with SPEF; 15:1x UK |
|
||||
| 32-lane general crossbar, per lane-op | ROW_XBAR |
|
||||
| 8 KB scratch, one random read (flop array: the pessimistic form) | ROW_SCRATCH |
|
||||
| int8 8x8x8 tile, per MAC | ROW_TILE |
|
||||
|
||||
Reading the floors: a lane's add costs the chip about 2.2 pJ at ASAP7 and 1.1 at N3, against the 5090's 6.2
|
||||
at the lock and 11.3 at stock, so even the floor is not a tenth of the card's cost at the knee, and the claimed
|
||||
"2 to 5 pJ for a SIMD array at N5" (15.1) was the right order for the unscaled lane with nothing around it. The
|
||||
multiplier is the cheapest unit per op relative to the card (the 5090 pays 8.3 pJ for mul and 21 for mulhi, the
|
||||
chip 0.68 for either, because the high word falls out of the same array), and mad is the dearest for the chip
|
||||
(three register reads and two units). The fold costs a chip one multiply and a rotate: 1.2 pJ per address at N3,
|
||||
or 0.15 nJ per hash over 128 loads, a third of a percent of the GDDR7 board's 0.466 microjoules.
|
||||
|
||||
## 4. The headline row: the programmable sequencer core
|
||||
|
||||
The per-unit lanes of section 3 are floors for a chip that cannot exist under class v6: layers 1 and 3 (per-era
|
||||
op-mix, program-length and read-width draws, family epochs) kill any fixed lane, so the chip that competes is a
|
||||
programmable core. `core_v6` (`tools/chip-model/rtl/rtl/core_v6.v`) is the minimal in-order SIMD sequencer that
|
||||
executes the drawn class v6 shadow program: a 256 x 32-bit instruction memory (a flop array) sized to the drawn
|
||||
program, a program counter wrapping at the era's drawn length, fetch into an instruction register, decode, the
|
||||
era's parameter registers (M, R, WM, OFF, MASK, N), and per lane a 32 x 32-bit register file (flops) with three
|
||||
read ports and one write port and every class unit: add, sub, xor, or, rotl, rotr, mul, mulhi, mad, prmt, lop3,
|
||||
the xor-mask shuffle across the lanes (a butterfly), and the load (the index fold on the address path, the returned
|
||||
word written next cycle). One instruction per cycle for every lane. The program is 256 instructions drawn with the
|
||||
class v4 weights (`NONLOAD_WEIGHTS`), with a variant at one load per 16 instructions. Two builds: 8 lanes (the fast
|
||||
row) and 32 lanes (the imem amortised over 32), and a 32-lane build with a 16-register file (the sensitivity).
|
||||
|
||||
The activity: the gate-level random-input VCD of the synthesised netlist; two run lengths (150 and 800 cycles)
|
||||
bracket the 260-cycle program-load phase, and solving the pair gives the steady-state run power (the load phase
|
||||
draws 13.9 mW, the run phase 36.9 mW for 8 lanes at 1.5 ns).
|
||||
|
||||
| Row | Stage | Cells | pJ per lane-op ASAP7 | N5 | N3 | N2 | 5090 stock / lock / M5 Max pJ per op | k absolute at N3 vs stock / lock / M5 Max | k at N2 | k unscaled ASAP7 vs lock | Label |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| core, 8 lanes, 32 registers | synthesis only (no wires, no clock tree) | 186,443 | 6.9 | 4.8 | 3.5 | 2.5 | 11.3 / 6.2 / 6.9 | 0.31 / 0.56 / 0.50 | 0.22 / 0.40 / 0.36 | 1.1 | synthesised; 14:0x UK |
|
||||
| of which the sequential term (register file, imem and IR clock pins, no clock gating) | | | 2.4 | 1.7 | 1.2 | 0.9 | | | | | |
|
||||
| of which the units, the read muxes and the butterfly | | | 4.5 | 3.1 | 2.3 | 1.6 | | | | | |
|
||||
| core, 8 lanes, 32 registers | placed and routed, SPEF | ROW_CORE8_PLACED |
|
||||
| core, 32 lanes, 32 registers | synthesis only (steady state from 150 and 400 run cycles) | 600,381 | 5.55 | 3.9 | 2.8 | 2.0 | 11.3 / 6.2 / 6.9 | 0.25 / 0.45 / 0.41 | 0.18 / 0.32 / 0.29 | 0.90 | synthesised; 15:2x UK |
|
||||
| core, 32 lanes, 16 registers | synthesis only | 443,258 | 4.2 | 2.9 | 2.1 | 1.5 | 11.3 / 6.2 / 6.9 | 0.18 / 0.34 / 0.30 | 0.13 / 0.24 / 0.22 | 0.68 | synthesised; one run length, about plus or minus 10 percent; 15:0x UK |
|
||||
| the bare ARX lane (section 3, the floor) | routed | 11,631 | 2.2 | 1.5 | 1.1 | 0.8 | 11.3 / 6.2 / 6.9 | 0.10 / 0.18 / 0.16 | 0.07 / 0.13 / 0.11 | 0.35 | the lower bound |
|
||||
|
||||
Reading: fetch, decode, a 32-register file and the full unit set cost a chip 3.1x the bare lane, and the core's
|
||||
k at the 1,300 lock is 0.56 at N3 (0.40 at N2), inside the record's claimed 0.3 to 0.8 band and at its centre, with
|
||||
the bare lane's 0.18 as the lower bound. Two corrections pull opposite ways: placement adds wires and a clock tree
|
||||
(+20 to +40 percent on a design like this, approximate; the placed rows read it) and a chip maker gates the
|
||||
register-file clock (one of 32 registers is written per cycle; gating removes about 2.0 of the 2.4 pJ sequential
|
||||
term, approximate), so the net figure for the re-fold is 7.0 / 4.9 / 3.5 / 2.5 pJ per lane-op at ASAP7 / N5 / N3 /
|
||||
N2 (the synthesis-only figure within the rounding). The imem is amortised over 8 lanes in this row and over 32 in
|
||||
the core32 row.
|
||||
|
||||
### 4a. The design sweep: what a class could add to the core's cost (the coordinator's order, 14:3x UK; rows 14:5x)
|
||||
|
||||
Synthesis-only (no wires, no clock tree), 8 lanes unless stated, the same corner and scaling; the steady-state run
|
||||
power solved from two run lengths (150 and 600 cycles after the program load). The GPU side per knob is the hash
|
||||
lane's: knob 3 measured on a rented 5090 and 4090 (RunPod, 14:28 to 14:39 UK), knobs 1 and 4 modelled until a
|
||||
generator line exists (a 64-entry window is a new ISA: an init rule and a fold rule for the extra registers, about
|
||||
half a day; the select tree is a new instruction kind), knob 2 has no GPU side (the warp's shuffle already spans
|
||||
32 lanes).
|
||||
|
||||
| Variant | Cells | pJ per lane-op ASAP7 | of which clocking (RF, imem, IR; no gating) | N5 | N3 | N2 | k at N3 vs 5090 stock / lock / M5 Max | GPU side |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| base: 32 registers, 256-entry imem (the headline row) | 186,443 | 6.9 | 2.4 | 4.8 | 3.5 | 2.5 | 0.31 / 0.56 / 0.50 | measured (class v4) |
|
||||
| (1) 64-register file (a 40-bit instruction word) | 267,731 | 9.7 | 3.75 | 6.8 | 4.9 | 3.5 | 0.43 / 0.78 / 0.70 | new ISA; 64 live registers takes a 5090 or 4090 thread to about 110 of 255, occupancy to about half; under the latency-bound chain the rate is expected to hold and the energy to move little (the hash lane, modelled); the unmeasured term is the per-lane register traffic |
|
||||
| (3) 1,024-entry imem, the program drawn at 1,024, as built (a flop array) | 324,543 | 12.0 | 6.0 | 8.4 | 6.1 | 4.4 | 0.53 / 0.97 / 0.87 | measured: the 1,024 block at 27 passes costs the 5090 1.58x the energy per hash (4.96 against 3.14 microjoules at stock, 111 against 141 MH/s at the 575 W cap) and the 4090 1.57x (7.01 against 4.48, the rate held at 62.5 MH/s), for 4x the shadow instructions: 0.40x per instruction |
|
||||
| (3) the same with the imem as a 4 KB SRAM macro shared by the lanes (2 to 4 pJ per 32-bit read, approximate) | | about 7.2 | about 1.9 | 5.0 | 3.6 | 2.6 | about 0.32 / 0.58 / 0.52 | the same |
|
||||
| (4) the drawn select tree (the era's 16-entry op permutation ahead of decode; every unit evaluated every cycle, as in the base) | 186,870 | 6.85 | 2.4 | 4.8 | 3.4 | 2.5 | 0.30 / 0.55 / 0.50 | the units' microbench sum (approximate) |
|
||||
| (2) 32 lanes, 32 registers (the butterfly across 32; the imem amortised over 32) | 600,381 | 5.55 | 1.5 | 3.9 | 2.8 | 2.0 | 0.25 / 0.45 / 0.41 | no GPU knob |
|
||||
| (2') 32 lanes, 16 registers (the register-file sensitivity the other way; one run length of 150 cycles, the load phase subtracted at the 8-lane ratio, about plus or minus 10 percent) | 443,258 | 4.2 | 2.3 | 2.9 | 2.1 | 1.5 | 0.18 / 0.34 / 0.30 | no GPU knob |
|
||||
| (5) all four together (32 lanes, 64 registers, 1,024 imem, the select tree) | ROW_CORE32ALL |
|
||||
|
||||
Reading, for the founder's "under 2x at the lock" (which needs k near 0.9 on the GDDR7 board): the 64-register
|
||||
window is the one robust knob, because its cost is per lane and a chip cannot share it (+2.8 pJ per lane-op at
|
||||
ASAP7, +0.22 of k at the lock). The long block's cost is instruction memory, which a chip shares across its lanes
|
||||
as SRAM, so most of its 0.97 as built is the flop array's clock and the honest figure is about 0.6; the card
|
||||
meanwhile pays 1.58x the energy per hash for it (measured), so on the GDDR7 board at stock the chip reads
|
||||
4.96 / (0.466 + 408,400 x 3.6 pJ) = 2.6x (1.7x on the flop-array row, which is not a chip anyone builds). The
|
||||
select tree costs the chip nothing because every unit already evaluates every cycle in the base core. (1) + (3)
|
||||
together reach about 10 pJ per lane-op at ASAP7 with the SRAM imem (5.0 at N3, k about 0.81 at the lock, 0.45 at
|
||||
stock), 14.8 as built (k about 1.2); so k 0.85 is reached only on the flop-array reading, and the DRAM board
|
||||
under 2x at the lock needs the window, the long block and the knee together and holds only while the chip's imem
|
||||
stays unamortised, which it does not. The GPU pays nothing for the window until occupancy binds, 1.58x per hash for
|
||||
the long block (0.40x per forcing instruction), and nothing for the select tree.
|
||||
|
||||
### 4b. The node column: which part of the edge is the node (the coordinator's order, 15:0x UK)
|
||||
|
||||
The record compares a chip scaled to N3 or N2 against a TSMC 4N card (N5 class; the M5 Max is N3), so part of
|
||||
the edge is the node. Factors as in 2.3, every one claimed. Absolute; the GDDR7 board at the lock = 2.33 /
|
||||
(0.466 + 102,100 x e_chip).
|
||||
|
||||
| Core | pJ per lane-op ASAP7 / N5 / N3 / N2 | k at the lock, N5 / N3 / N2 | GDDR7 board at the lock, N5 / N3 / N2 |
|
||||
|---|---|---|---|
|
||||
| base (32 registers, 256 imem) | 6.9 / 4.8 / 3.5 / 2.5 | 0.78 / 0.56 / 0.40 | 2.4x / 2.8x / 3.2x |
|
||||
| 64-register file | 9.7 / 6.8 / 4.9 / 3.5 | 1.09 / 0.78 / 0.56 | 2.0x / 2.4x / 2.8x |
|
||||
| all four together | ROW_CORE32ALL_NODE |
|
||||
|
||||
One line: of the 2.8x at k 0.56, the N5-to-N3 node step is worth 0.4x (2.4x node-for-node, a factor of 1.17,
|
||||
claimed); the rest is the memory system (3.6x at zero shadow at the lock, modelled) less what the class v4 shadow
|
||||
takes back on the card's own node (3.6x to 2.4x), which is the design. Node-for-node the base core already sits at
|
||||
k 0.78 and the 64-register core at 1.09, so "near 0.9" is reached node-for-node by the window alone; what it does
|
||||
not survive is the node step a chip project would buy (an N3 core gives back 0.4x, an N2 core 0.8x).
|
||||
|
||||
## 5. The chip edge at the measured k
|
||||
|
||||
`E_chip = E_mem + N_ops x e_chip` (absolute: the chip's shadow cost is 102,100 x 3.5 pJ = 0.36 microjoules per hash
|
||||
at N3, 0.26 at N2, whatever the card does); the record's convention `E_mem + k x F` beside it with `k` read at the
|
||||
card's point (it coincides at the lock by construction and is the same at stock here because both are the same
|
||||
arithmetic on the same ops; it differs when the chip's cost is held fixed while the card's point moves, which is
|
||||
what the SRAM lane found flattered the die 1.6x). Cards: the 5090 at stock (3.36 microjoules, F 1.10) and at the
|
||||
lock (2.33, F 0.652), the M5 Max (1.40 on class v4, 0.78 on class v3, 6.9 pJ per op). Chips: the record's GDDR7
|
||||
board and the hardware-future lane's rows (their `E_mem` at zero shadow). The record's rows that this recomputes
|
||||
are hardware-future.md section 5: "2.6x to 2.8x at k = 1 and 4.4x to 5.1x at k = 0.5" for the strongest DRAM chips
|
||||
against the stock 5090 (the GDDR7 board's own row there is 2.1x and 3.3x).
|
||||
|
||||
Chip shadow per hash, absolute: N3 0.357 microjoules, N2 0.255.
|
||||
| Chip (E_mem, microjoules) | Card row | Zero shadow | Absolute, N3 core | Absolute, N2 core | Record convention at the measured k (N3) | at k = 1 | at k = 0.5 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| GDDR7 board, 28 nm controller (the record) (0.466) | 5090 stock (class v4) | 4.8x | 4.1x | 4.7x | 4.1x (k 0.32) | 2.1x | 3.3x |
|
||||
| GDDR7 board, 28 nm controller (the record) (0.466) | 5090 at the 1,300 MHz lock | 3.6x | 2.8x | 3.2x | 2.8x (k 0.55) | 2.1x | 2.9x |
|
||||
| GDDR7 board, 28 nm controller (the record) (0.466) | M5 Max (class v4 1.40 microjoules, class v3 0.78; 6.9 pJ per op; F 0.62) | 1.7x | 1.7x | 1.9x | 1.8x (k 0.51) | 1.3x | 1.8x |
|
||||
| HBM3E one stack (0.321) | 5090 stock (class v4) | 7.0x | 5.0x | 5.8x | 5.0x (k 0.32) | 2.4x | 3.9x |
|
||||
| HBM3E one stack (0.321) | 5090 at the 1,300 MHz lock | 5.2x | 3.4x | 4.0x | 3.4x (k 0.55) | 2.4x | 3.6x |
|
||||
| HBM3E one stack (0.321) | M5 Max (class v4 1.40 microjoules, class v3 0.78; 6.9 pJ per op; F 0.62) | 2.4x | 2.1x | 2.4x | 2.2x (k 0.51) | 1.5x | 2.2x |
|
||||
| HBM4 one stack, N12 base die (0.22) | 5090 stock (class v4) | 10.3x | 5.8x | 7.1x | 5.8x (k 0.32) | 2.5x | 4.4x |
|
||||
| HBM4 one stack, N12 base die (0.22) | 5090 at the 1,300 MHz lock | 7.6x | 4.0x | 4.9x | 4.0x (k 0.55) | 2.7x | 4.3x |
|
||||
| HBM4 one stack, N12 base die (0.22) | M5 Max (class v4 1.40 microjoules, class v3 0.78; 6.9 pJ per op; F 0.62) | 3.5x | 2.4x | 2.9x | 2.6x (k 0.51) | 1.7x | 2.6x |
|
||||
| custom HBM4E base die, N3P (0.18) | 5090 stock (class v4) | 12.6x | 6.3x | 7.7x | 6.3x (k 0.32) | 2.6x | 4.6x |
|
||||
| custom HBM4E base die, N3P (0.18) | 5090 at the 1,300 MHz lock | 9.3x | 4.3x | 5.4x | 4.3x (k 0.55) | 2.8x | 4.6x |
|
||||
| custom HBM4E base die, N3P (0.18) | M5 Max (class v4 1.40 microjoules, class v3 0.78; 6.9 pJ per op; F 0.62) | 4.3x | 2.6x | 3.2x | 2.8x (k 0.51) | 1.7x | 2.9x |
|
||||
| DRAM on logic, hybrid bonded (2029 to 2031) (0.15) | 5090 stock (class v4) | 15.1x | 6.6x | 8.3x | 6.6x (k 0.32) | 2.7x | 4.8x |
|
||||
| DRAM on logic, hybrid bonded (2029 to 2031) (0.15) | 5090 at the 1,300 MHz lock | 11.2x | 4.6x | 5.7x | 4.6x (k 0.55) | 2.9x | 4.9x |
|
||||
| DRAM on logic, hybrid bonded (2029 to 2031) (0.15) | M5 Max (class v4 1.40 microjoules, class v3 0.78; 6.9 pJ per op; F 0.62) | 5.2x | 2.8x | 3.5x | 3.0x (k 0.51) | 1.8x | 3.0x |
|
||||
| SRAM full store, one N2 reticle (0.14) | 5090 stock (class v4) | 16.1x | 6.8x | 8.5x | 6.8x (k 0.32) | 2.7x | 4.9x |
|
||||
| SRAM full store, one N2 reticle (0.14) | 5090 at the 1,300 MHz lock | 12.0x | 4.7x | 5.9x | 4.7x (k 0.55) | 2.9x | 5.0x |
|
||||
| SRAM full store, one N2 reticle (0.14) | M5 Max (class v4 1.40 microjoules, class v3 0.78; 6.9 pJ per op; F 0.62) | 5.6x | 2.8x | 3.5x | 3.1x (k 0.51) | 1.8x | 3.1x |
|
||||
|
||||
Floor lane 1 assumption (not in by 18:00 UK): the honest 5090 at 1.0 microjoules per hash at zero shadow.
|
||||
| Chip | Premium kept at 0.652 (card 1.652): absolute N3 / N2 | Premium scaled with the card, 0.390 (card 1.390): absolute N3 / N2 | zero shadow |
|
||||
|---|---|---|---|
|
||||
| GDDR7 board, 28 nm controller (the record) | 2.0x / 2.3x | 1.7x / 1.9x | 2.1x |
|
||||
| HBM3E one stack | 2.4x / 2.9x | 2.0x / 2.4x | 3.1x |
|
||||
| HBM4 one stack, N12 base die | 2.9x / 3.5x | 2.4x / 2.9x | 4.5x |
|
||||
| custom HBM4E base die, N3P | 3.1x / 3.8x | 2.6x / 3.2x | 5.6x |
|
||||
| DRAM on logic, hybrid bonded (2029 to 2031) | 3.3x / 4.1x | 2.7x / 3.4x | 6.7x |
|
||||
| SRAM full store, one N2 reticle | 3.3x / 4.2x | 2.8x / 3.5x | 7.1x |
|
||||
|
||||
What the rows say. (1) At the knee the GDDR7 board keeps 2.8x with the shadow on (3.2x at an N2 core) against
|
||||
3.6x at zero shadow: the class v4 shadow buys the honest card 0.8x of edge, not the 1.5x the k = 1 row served and
|
||||
not the 0.1x the bare-lane floor would give. (2) The strongest DRAM chips read 4.3x to 4.7x at the knee and 6.3x to
|
||||
6.8x at stock against the 5090 (the record's 2.6x to 2.8x at k = 1 and 4.4x to 5.1x at k = 0.5 bracket the stock
|
||||
figure and understate the knee one, because the record's k was read at stock). (3) Against the M5 Max the whole
|
||||
table compresses to 1.7x to 2.8x: the honest SoC's own operating point is most of the resistance. (4) Floor lane
|
||||
1's honest 5090 at 1.0 microjoules (assumed; its rows were not in by 18:00 UK) would put the GDDR7 board at 1.7x
|
||||
to 2.3x and the strongest chips at 2.6x to 4.2x.
|
||||
|
||||
## 6. The ranking and the mixed draw
|
||||
|
||||
### 6.1 Families ranked by k, highest first (the hardest for a chip), absolute at N3 against the 5090's lock
|
||||
|
||||
| Rank | Family | Chip pJ/op N3 (unit floor) | 5090 pJ/op at the lock | k (floor) | In the class draw? |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | mad | 1.66 | 8.3 | 0.20 | yes (8 of 75) |
|
||||
| 2 | add, sub, rotl, rotr, xor | 1.05 to 1.09 | 6.2 | 0.17 to 0.18 | yes (12, 6, 7, 6, 10) |
|
||||
| 3 | the index fold | 1.19 | 8.3 | 0.14 | every load |
|
||||
| 4 | or | 0.73 | 6.2 | 0.12 | yes (4) |
|
||||
| 5 | mul | 0.68 | 8.3 | 0.082 | yes (8) |
|
||||
| 6 | prmt, lop3 | 0.64, 0.72 | 11.5, 13.0 | 0.056, 0.055 | not drawn (RTL rows only) |
|
||||
| 7 | mulhi | 0.68 | 21.0 | 0.032 | yes (6) |
|
||||
| 8 | 32-lane shuffle (butterfly) | 0.63 | 29.4 | 0.021 | yes (8) |
|
||||
| 9 | L1 scratch read (8 KB flop array) against the card's L2 hit | ROW_SCRATCH_K | 1,400 | pending | not drawn |
|
||||
| 10 | int8 8x8x8 tile, per MAC | ROW_TILE_K | 2.2 | pending | not drawn (the tensor lever is dead on other grounds) |
|
||||
|
||||
The order is set by the card's price, not the chip's: the chip pays 0.6 to 1.7 pJ for everything, and the card
|
||||
pays 6.2 for an add, 8.3 for a multiply, 21 for a high word and 29 for a shuffle. So the families the card pays
|
||||
MOST for (mulhi, shuffle) are the ones a chip undercuts most, and the forcing work is the plain ARX and mad the
|
||||
card does cheapest. This is the measured form of 15.1b's hold on the shuffle-heavy re-weight.
|
||||
|
||||
### 6.2 The mixed draw that maximises the expected k
|
||||
|
||||
The objective at a FIXED GPU premium: `k_eff(w) = sum w_i e_chip_i / sum w_i e_gpu_i` (the chip's energy for the
|
||||
drawn program over the card's for the same program; at a fixed `F` the chip pays `k_eff x F`). The band (layer
|
||||
1, the research lane's 17:00 reading of lane D's cut): B = 4 points on the injecting families only (add, sub,
|
||||
xor, mad, shfl, rotl, rotr), the lossy families (or, mul, mulhi) at or under their base, `or + mul + mulhi` at most
|
||||
18 + B, the shuffle capped at its class v4 weight; the index fold on every address and the F8 uniformity floor are
|
||||
not functions of the weights and do not move. `tools/chip-model/rtl/flow/mix.py` searches the band exhaustively
|
||||
over the corners (the objective is a ratio of linear forms, so the optimum is at a corner of each family's range).
|
||||
|
||||
On the unit floors (N3, the lock; the shuffle row in at 0.63 pJ, its k 0.021 the lowest of the drawn families):
|
||||
|
||||
| Mix | add, xor, mul, mad, shfl, rotl, sub, mulhi, rotr, or | k_eff (floors) | The card's pJ per op at the lock | k_eff on the core (floor + the core's 2.4 pJ per op overhead at N3) |
|
||||
|---|---|---|---|---|
|
||||
| class v4 (the base) | 12, 10, 8, 8, 8, 7, 6, 6, 6, 4 | 0.097 (with the shuffle row in: 0.130 over the other 67 points) | 10.3 | 0.43 |
|
||||
| the census lane's re-weight (d20eb04bd, PASS on 256 seeds, no era and eras 0 to 7, W = 4 and W = 16) | 13, 11, 6, 10, 8, 8, 7, 2, 6, 4 | 0.115 (+19 percent; 0.153 over the 67 points) | 9.0 | 0.49 (+14 percent) |
|
||||
| the band's best corner (mix.py, exhaustive: every injecting family at +4, the shuffle at its floor, the lossy families at base minus 4) | 16, 14, 4, 12, 4, 11, 10, 2, 10, 0 | 0.137 (+42 percent) | 8.2 | about 0.51 (+19 percent) |
|
||||
| the band's worst corner (shuffle and mulhi heavy) | 8, 6, 4, 4, 8, 3, 2, 6, 2, 0 | 0.074 | 12.3 | about 0.40 |
|
||||
|
||||
Reading: the re-weight moves six points off the chip's two easiest families (mulhi, mul) onto the hardest (mad,
|
||||
the ARX families) and lifts the expected k by about a seventh on the core; its acceptance is the census lane's
|
||||
(0 exhausted, F8 0.999 to 1.008 of uniform, the verifier within 4 percent); its energy cost on the card is a
|
||||
premium per instruction 11 percent LOWER (fewer mulhi), so at a fixed premium the program is 12 percent longer.
|
||||
The shuffle's own row decides whether it goes to its floor: on the card it is the dearest op (29.4 pJ at the lock)
|
||||
and on the chip a butterfly over the window, so its k is the lowest of the drawn families unless the synthesis says
|
||||
otherwise. The recommendation: the band's best corner (add 16, xor 14, mad 12, rotl 11, sub 10, rotr 10, shfl 4, mul 4, mulhi 2, or 0) if its census passes (the census lane's harness, about five box-minutes per candidate); the census lane's draw as the passed fallback. Either way the shuffle goes to its floor: the card pays 29.4 pJ for a move the chip does for 0.6.
|
||||
|
||||
## 7. Sources
|
||||
|
||||
- The GPU side: `docs/analysis/counter-asic-4-research.md` 15.1a (the 5090 microbench, 8 October 2026, measured)
|
||||
and 20.3 (the packs job: 10.8 / 6.4 pJ per counted op on the class v4 shadow); the M5 Max 6.9 pJ per counted
|
||||
op from the coordinator's order of 8 October 2026 (the latency-shadow record).
|
||||
- ASAP7: L. T. Clark et al., "ASAP7: A 7-nm finFET predictive process design kit", Microelectronics Journal 53
|
||||
(2016); the ORFS platform files (`flow/platforms/asap7`, the 7.5-track RVT library, TC corner 0.70 V / 0 C).
|
||||
- The flow: OpenROAD-flow-scripts (docker image `openroad/orfs:latest`, Yosys 0.68, OpenROAD and OpenSTA with
|
||||
`read_vcd`); iverilog 12 for the gate-level simulation.
|
||||
- Node scaling (claimed): TSMC N5 "30 percent lower power at the same speed" against N7 (TSMC technology page,
|
||||
https://www.tsmc.com/english/dedicatedFoundry/technology/logic/l_5nm); N3E "25 to 30 percent lower power"
|
||||
against N5 (https://www.tsmc.com/english/dedicatedFoundry/technology/logic/l_3nm); N2 "25 to 30 percent lower
|
||||
power" against N3E (https://www.tsmc.com/english/dedicatedFoundry/technology/logic/l_2nm). Read 8 October 2026.
|
||||
- The chip model's memory rows: `docs/analysis/chip-model-v3.md` 5.5 (the GDDR7 board, 0.466 microjoules) and
|
||||
`docs/analysis/class-v6/hardware-future.md` section 5 (the strongest DRAM and SRAM rows).
|
||||
- The band: `docs/design/class-v6-rotating-family.md` section 2 (layer 1) and `igneum-pow/src/generator.rs`
|
||||
`NONLOAD_WEIGHTS`.
|
||||
767
docs/analysis/class-v6/floor/sm-sparse.md
Normal file
767
docs/analysis/class-v6/floor/sm-sparse.md
Normal file
|
|
@ -0,0 +1,767 @@
|
|||
# Class v6 floor lane 1: SM-sparse, the honest card's watts toward the DRAM's own (8 October 2026)
|
||||
|
||||
Branch `class-v6-floor-sm` off `counter-asic-4` (d8ba861e). The term this lane owns is `E_card` above `E_mem` in the
|
||||
identity of `docs/analysis/counter-asic-4-research.md` section 2: at zero shadow the stored-dataset chip's edge is
|
||||
`E_card / E_mem`, and the only honest-side lever is to take the card's watts toward the DRAM's own at the activate
|
||||
ceiling. Section 20.3b of that file read five shapes on PC 1's RTX 5090 (one block of 32 warps per SM at 170, 85, 43,
|
||||
21 and 11 SMs) and closed the candidate: the draw follows the work, not the SM count. This file starts from that row,
|
||||
answers the question it left open (what the residual watts between idle-plus-memory and the card's draw ARE, and
|
||||
whether any occupancy shape at full SM count moves them) on four rented RTX 5090s, an RTX 4090 and an H100 plus PC 1 at
|
||||
the 1,300 MHz lock, measures the memory-clock knob where a host allows it, and ships the measurement as a worker option
|
||||
with a self-tune.
|
||||
|
||||
Every number here is **measured** unless marked otherwise: the worker's `--bench` (250 dispatches of 2^24 nonces, the
|
||||
pack's vector warps through the served kernel first, the 2^24 fingerprint on every row and equal across every shape of
|
||||
a pack) with `nvidia-smi` at 1 Hz beside it (the mean from 8 s in; `power.draw`, `power.draw.instant` and
|
||||
`power.draw.average` within 2 W on every row). The shapes: `base` is the shipped grid (one warp per block); `w4` and
|
||||
`w8` the shipped grid at 4 and 8 warps per block; `sp<N>-w<W>` the persistent wrapper of N blocks of W warps (the
|
||||
Counter ASIC 4.0 variant, bit-exact), and a `%smid` probe on every rented card confirmed that N blocks of 1,024 threads
|
||||
land on N distinct SMs, one block each, for every N used. The chip rows are the model's (`chip-model-v3.md` 5.3 to 5.6
|
||||
and 5.12; lane B's SRAM die): GDDR7 board 0.466 microjoules per hash, one HBM3 stack 0.321, the N2 SRAM die 0.14; the
|
||||
M5 Max's 0.78 is measured (6 October). "Edge" is the card's microjoules over the chip's. Times are UTC in the logs and UK
|
||||
(BST, UTC+1) in the text. Idle is the card alone after 12 s with nothing running.
|
||||
|
||||
## 0. The answer
|
||||
|
||||
1. **The SM count is not the lever.** On every card the draw over idle is flat across the SM count and the warps per
|
||||
SM for as long as the rate holds, and falls only when the rate falls. RTX 5090 (four rented hosts, class v3 at
|
||||
stock): 43 of 170 SMs holds 99.4 percent of the ceiling, 36 SMs 99.1, 28 SMs 98.4, 21 SMs 96.6, 16 SMs 91, 11 SMs
|
||||
71; the best energy per hash on any host is 1.3 to 3.9 percent under its base (host c 2.053 against 2.079
|
||||
microjoules at 43 SMs; host e 2.389 against 2.485). RTX 4090: 16 of 128 SMs holds 99.0 percent at 4 W under base;
|
||||
the best shape 1.2 percent under. H100: per-SM throughput-bound (the rate falls one for one with the SM count,
|
||||
1.93 MH/s per SM), so its sparse floor is 127 of 132 SMs at 1.4 percent under base.
|
||||
2. **Fewer warps per SM at full SM count reads the same as fewer SMs.** 170 SMs at 8 warps (1,360 warps) and 43 SMs
|
||||
at 32 warps (1,376 warps) give 2.063 and 2.053 microjoules on the same host: the warps in flight set the rate,
|
||||
nothing in the occupancy sets the watts. One warp per SM (5,440 lanes) still holds 84 percent of the 5090's rate.
|
||||
3. **Class v4's knee is 43 SMs by rate at stock and the full grid at the 1,300 MHz lock, and nothing is saved at
|
||||
either.** Under 43 SMs the shadow is compute-bound at stock (PC 1: 36 SMs 88 percent, 28 SMs 69, 21 SMs 52, 16 SMs
|
||||
39 of the rate); at 43 SMs the watts equal or exceed the base's. At the lock (PC 1, this file's job) the shadow
|
||||
needs every SM: 134.2 MH/s at 302 W on the full grid, 125 at 85 SMs, 64 at 43 SMs (3.24 against 2.25 microjoules).
|
||||
On the H100 the persistent shape itself loses 4 percent of the class v4 rate and the self-tune installs nothing.
|
||||
PC 1's class v3 at stock agrees with the rented ladder at every rung (43 SMs 136.5 of 136.7 MH/s at 10 W under
|
||||
base; 170 SMs x 8 warps 137.1 at 310.8 W, 3.5 percent under base, the best shape on that board).
|
||||
4. **The residual is the core clock domain and the memory path on the GPU side, and the memory clock is not a
|
||||
lever either (section 3.1: the 5090's PHY has two states, 13,801 and 7,001 MHz; the half-rate state loses 43
|
||||
percent of the rate for 37 percent of the watts, so the energy per hash rises 12 percent).** The decomposition probes (section 3) put the 5090's draw over idle at the hash
|
||||
into: the resident SMs issuing nothing (the SM clock domain awake), the L2 and crossbar path per read, and the DRAM
|
||||
path per read on top of the memory's own modelled 2.0 nJ; the memory-clock ladder on PC 1 at the 1,300 lock reads
|
||||
the PHY's share directly. The honest floor per tier is the base row at stock within 2 percent, the core lock
|
||||
(rank 1 of the research file) and the memory-clock lock behind it, and the fraction a chip cannot strip is the
|
||||
memory side's own (section 4).
|
||||
5. **What ships.** `--sm-sparse auto` on the worker (section 5): one NVRTC compile of the persistent shape, a ladder
|
||||
from the SM count down, a bisection, the fewest blocks holding 99 percent of the ceiling installed, bit-exact or
|
||||
nothing; the tuning file's `sm_sparse` and `sm_hold` per card. Measured: 15 s on the H100 (chose 127 of 132 SMs,
|
||||
1.4 percent of watts), 24 s on the 4090 (chose 26 of 128, 1.3 percent), nothing installed on class v4 where the
|
||||
shape loses rate. The design default is off; the tiers map to it (Efficiency on at 0.985, Balanced on at 0.995,
|
||||
Maximum off) and Ember re-runs it on every pack flip because the tune is part of the pair's build.
|
||||
|
||||
## 1. The rows
|
||||
|
||||
Each rented card's table: one row per shape per pack, the sampler's watts, microjoules per hash and the edge against
|
||||
the four chip rows. The 5090 hosts differ by board (power limit, idle, cooling), so the ladder is read as a percent of
|
||||
each host's own base and the energy as a difference within the host; the across-host spread of the base row itself
|
||||
(2.08 to 2.49 microjoules at 142 MH/s on class v3) is the rented-against-rented band the denominator lane also reads.
|
||||
Class v4 on hosts c, d and f is power-capped (the board's limit at 450 or 475 W, the clock throttling to 2,770 to
|
||||
2,810 MHz), so those class v4 rows are a cap reading, not a free-running one; PC 1 (452.8 W, no cap) and host e (533.6
|
||||
W) are the free-running class v4 rows.
|
||||
|
||||
### RTX 5090, rented host c (Vast, Alberta; driver 610.57; the board's power limit 450 W of a 600 W default; idle 7 W)
|
||||
|
||||
knob pl=refused
|
||||
knob lgc=refused
|
||||
knob lmc=refused
|
||||
card NVIDIAGeForceRTX5090 driver 610.57.04 power limit 450.00 W idle 7.1 W at 218.8 MHz SM, 478.6 MHz memory
|
||||
|
||||
**mx8-genesis** (mx8)
|
||||
|
||||
| Shape | Blocks x warps | MH/s | Watts | Watts over idle | Microjoules per hash | Edge: GDDR7 / HBM3 / SRAM / M5 Max | SM MHz | Temp | Check |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| base | 0 x 1 | 142.60 | 296.4 | 289.3 | 2.079 | 4.46x / 6.48x / 14.85x / 2.67x | 2880 | 52 | PASS |
|
||||
| sp170-w32 | 170 x 32 | 142.14 (99.7%) | 296.9 | 289.8 | 2.089 | 4.48x / 6.51x / 14.92x / 2.68x | 2865 | 54 | PASS |
|
||||
| sp170-w16 | 170 x 16 | 142.55 (100.0%) | 296.0 | 288.9 | 2.076 | 4.45x / 6.47x / 14.83x / 2.66x | 2865 | 57 | PASS |
|
||||
| sp170-w8 | 170 x 8 | 142.41 (99.9%) | 293.8 | 286.7 | 2.063 | 4.43x / 6.43x / 14.74x / 2.64x | 2865 | 59 | PASS |
|
||||
| sp170-w4 | 170 x 4 | 141.88 (99.5%) | 293.5 | 286.4 | 2.069 | 4.44x / 6.45x / 14.78x / 2.65x | 2865 | 59 | PASS |
|
||||
| sp170-w2 | 170 x 2 | 139.31 (97.7%) | 288.7 | 281.6 | 2.072 | 4.45x / 6.45x / 14.80x / 2.66x | 2865 | 59 | PASS |
|
||||
| sp170-w1 | 170 x 1 | 119.59 (83.9%) | 270.3 | 263.2 | 2.260 | 4.85x / 7.04x / 16.14x / 2.90x | 2865 | 59 | PASS |
|
||||
| sp128-w32 | 128 x 32 | 142.60 (100.0%) | 303.4 | 296.3 | 2.128 | 4.57x / 6.63x / 15.20x / 2.73x | 2865 | 59 | PASS |
|
||||
| sp96-w32 | 96 x 32 | 138.96 (97.4%) | 294.5 | 287.4 | 2.119 | 4.55x / 6.60x / 15.14x / 2.72x | 2865 | 59 | PASS |
|
||||
| sp85-w32 | 85 x 32 | 142.42 (99.9%) | 296.0 | 288.9 | 2.078 | 4.46x / 6.47x / 14.84x / 2.66x | 2865 | 59 | PASS |
|
||||
| sp64-w32 | 64 x 32 | 142.24 (99.7%) | 295.2 | 288.1 | 2.075 | 4.45x / 6.46x / 14.82x / 2.66x | 2865 | 59 | PASS |
|
||||
| sp52-w32 | 52 x 32 | 136.01 (95.4%) | 288.8 | 281.7 | 2.123 | 4.56x / 6.61x / 15.16x / 2.72x | 2865 | 59 | PASS |
|
||||
| sp43-w32 | 43 x 32 | 141.77 (99.4%) | 291.1 | 284.0 | 2.053 | 4.41x / 6.40x / 14.66x / 2.63x | 2865 | 59 | PASS |
|
||||
| sp36-w32 | 36 x 32 | 141.38 (99.1%) | 290.2 | 283.1 | 2.053 | 4.41x / 6.40x / 14.66x / 2.63x | 2865 | 59 | PASS |
|
||||
| sp28-w32 | 28 x 32 | 140.27 (98.4%) | 288.6 | 281.5 | 2.058 | 4.42x / 6.41x / 14.70x / 2.64x | 2865 | 59 | PASS |
|
||||
| sp21-w32 | 21 x 32 | 137.81 (96.6%) | 288.1 | 281.0 | 2.091 | 4.49x / 6.51x / 14.94x / 2.68x | 2865 | 59 | PASS |
|
||||
| sp16-w32 | 16 x 32 | 129.98 (91.2%) | 280.4 | 273.3 | 2.157 | 4.63x / 6.72x / 15.41x / 2.77x | 2865 | 58 | PASS |
|
||||
| sp11-w32 | 11 x 32 | 100.88 (70.7%) | 255.5 | 248.4 | 2.533 | 5.44x / 7.89x / 18.09x / 3.25x | 2865 | 57 | PASS |
|
||||
| sp43-w8 | 43 x 8 | 139.54 (97.9%) | 286.9 | 279.8 | 2.056 | 4.41x / 6.40x / 14.69x / 2.64x | 2865 | 58 | PASS |
|
||||
| sp340-w16 | 340 x 16 | 142.57 (100.0%) | 303.7 | 296.6 | 2.130 | 4.57x / 6.64x / 15.21x / 2.73x | 2865 | 59 | PASS |
|
||||
|
||||
**mx8_sh256x27** (mx8+sh256x27)
|
||||
|
||||
| Shape | Blocks x warps | MH/s | Watts | Watts over idle | Microjoules per hash | Edge: GDDR7 / HBM3 / SRAM / M5 Max | SM MHz | Temp | Check |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| base | 0 x 1 | 142.55 | 450.0 | 442.9 | 3.157 | 6.77x / 9.83x / 22.55x / 4.05x | 2797 | 66 | PASS |
|
||||
| sp170-w32 | 170 x 32 | 142.12 (99.7%) | 450.0 | 442.9 | 3.166 | 6.79x / 9.86x / 22.61x / 4.06x | 2810 | 64 | PASS |
|
||||
| sp170-w16 | 170 x 16 | 142.36 (99.9%) | 450.0 | 442.9 | 3.161 | 6.78x / 9.85x / 22.58x / 4.05x | 2811 | 64 | PASS |
|
||||
| sp170-w8 | 170 x 8 | 141.93 (99.6%) | 450.0 | 442.9 | 3.171 | 6.80x / 9.88x / 22.65x / 4.07x | 2796 | 64 | PASS |
|
||||
| sp170-w4 | 170 x 4 | 139.72 (98.0%) | 450.0 | 442.9 | 3.221 | 6.91x / 10.03x / 23.01x / 4.13x | 2802 | 64 | PASS |
|
||||
| sp170-w2 | 170 x 2 | 99.50 (69.8%) | 389.4 | 382.3 | 3.913 | 8.40x / 12.19x / 27.95x / 5.02x | 2865 | 62 | PASS |
|
||||
| sp170-w1 | 170 x 1 | 55.28 (38.8%) | 287.7 | 280.6 | 5.204 | 11.17x / 16.21x / 37.17x / 6.67x | 2865 | 57 | PASS |
|
||||
| sp128-w32 | 128 x 32 | 142.41 (99.9%) | 450.0 | 442.9 | 3.160 | 6.78x / 9.84x / 22.57x / 4.05x | 2823 | 62 | PASS |
|
||||
| sp96-w32 | 96 x 32 | 139.18 (97.6%) | 450.0 | 442.9 | 3.233 | 6.94x / 10.07x / 23.09x / 4.14x | 2846 | 64 | PASS |
|
||||
| sp85-w32 | 85 x 32 | 141.46 (99.2%) | 450.0 | 442.9 | 3.181 | 6.83x / 9.91x / 22.72x / 4.08x | 2833 | 64 | PASS |
|
||||
| sp64-w32 | 64 x 32 | 136.58 (95.8%) | 450.4 | 443.3 | 3.298 | 7.08x / 10.27x / 23.56x / 4.23x | 2850 | 63 | PASS |
|
||||
| sp52-w32 | 52 x 32 | 121.00 (84.9%) | 417.9 | 410.8 | 3.454 | 7.41x / 10.76x / 24.67x / 4.43x | 2873 | 62 | PASS |
|
||||
| sp43-w32 | 43 x 32 | 124.16 (87.1%) | 422.7 | 415.6 | 3.404 | 7.30x / 10.60x / 24.31x / 4.36x | 2872 | 62 | PASS |
|
||||
| sp36-w32 | 36 x 32 | 104.35 (73.2%) | 380.5 | 373.4 | 3.646 | 7.82x / 11.36x / 26.04x / 4.67x | 2877 | 61 | PASS |
|
||||
| sp28-w32 | 28 x 32 | 81.35 (57.1%) | 334.2 | 327.1 | 4.108 | 8.82x / 12.80x / 29.34x / 5.27x | 2880 | 59 | PASS |
|
||||
| sp21-w32 | 21 x 32 | 61.05 (42.8%) | 288.2 | 281.1 | 4.721 | 10.13x / 14.71x / 33.72x / 6.05x | 2878 | 57 | PASS |
|
||||
| sp16-w32 | 16 x 32 | 46.51 (32.6%) | 252.1 | 245.0 | 5.420 | 11.63x / 16.88x / 38.71x / 6.95x | 2872 | 55 | PASS |
|
||||
| sp11-w32 | 11 x 32 | 32.04 (22.5%) | 216.3 | 209.2 | 6.752 | 14.49x / 21.03x / 48.23x / 8.66x | 2880 | 54 | PASS |
|
||||
| sp43-w8 | 43 x 8 | 87.18 (61.2%) | 345.2 | 338.1 | 3.960 | 8.50x / 12.34x / 28.29x / 5.08x | 2872 | 59 | PASS |
|
||||
| sp340-w16 | 340 x 16 | 142.18 (99.7%) | 450.0 | 442.9 | 3.165 | 6.79x / 9.86x / 22.61x / 4.06x | 2800 | 65 | PASS |
|
||||
|
||||
|
||||
The follow-up on host c (the memory-clock try, the first self-tune rows, the microbench) overlapped its own queue on the card (the launcher's wait keyed on a pid file written after it read it), so those rows are discarded; the self-tune was re-run alone on the idle card (idle 5.2 W, 1 MiB used) and these are its rows:
|
||||
|
||||
|
||||
| Hold | Pack | Served shape | MH/s | Watts | Microjoules | Tune line |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 0.99 | mx8-genesis | sp37-w32 | 141.272 | 286.5 | 2.028 | tune device NVIDIA_GeForce_RTX_5090 sms 170 installed base 142.575 ceiling 142.575 hold 0.990 rows 170=142.008 127=142.276 85=142.224 63=141.866 42=141.549 31=140.347 36=141.083 39=141.295 37=141.240 chosen sp37-w32 141.240 (99.1% of the ceiling, 37 of 170 SMs) 21816 ms |
|
||||
| 0.99 | mx8_sh256x27 | sp82-w32 | 141.045 | 448.3 | 3.178 | tune device NVIDIA_GeForce_RTX_5090 sms 170 installed base 142.288 ceiling 142.288 hold 0.990 rows 170=141.986 127=141.787 85=141.302 63=135.712 74=140.204 79=140.561 82=141.046 80=140.855 chosen sp82-w32 141.046 (99.1% of the ceiling, 82 of 170 SMs) 19820 ms |
|
||||
| 0.98 | mx8-genesis | sp27-w32 | 139.717 | 284.1 | 2.033 | tune device NVIDIA_GeForce_RTX_5090 sms 170 installed base 142.277 ceiling 142.277 hold 0.980 rows 170=141.940 127=142.336 85=142.125 63=142.024 42=141.710 31=140.447 21=137.555 26=139.300 28=139.939 27=139.557 chosen sp27-w32 139.557 (98.1% of the ceiling, 27 of 170 SMs) 24133 ms |
|
||||
| 0.98 | mx8_sh256x27 | sp71-w32 | 140.082 | 446.1 | 3.185 | tune device NVIDIA_GeForce_RTX_5090 sms 170 installed base 142.534 ceiling 142.534 hold 0.980 rows 170=142.005 127=141.994 85=141.509 63=136.138 74=140.573 68=139.343 71=140.150 69=139.615 chosen sp71-w32 140.150 (98.3% of the ceiling, 71 of 170 SMs) 19850 ms |
|
||||
|
||||
|
||||
### RTX 5090, rented host d (Vast, Quebec; driver 595.91; power limit 475 W)
|
||||
|
||||
knob pl=refused
|
||||
knob lgc=refused
|
||||
knob lmc=refused
|
||||
card NVIDIAGeForceRTX5090 driver 595.91.07 power limit 475.00 W idle 27.4 W at 195.0 MHz SM, 405.0 MHz memory
|
||||
|
||||
**mx8-genesis** (mx8)
|
||||
|
||||
| Shape | Blocks x warps | MH/s | Watts | Watts over idle | Microjoules per hash | Edge: GDDR7 / HBM3 / SRAM / M5 Max | SM MHz | Temp | Check |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| base | 0 x 1 | 142.44 | 326.3 | 298.9 | 2.291 | 4.92x / 7.14x / 16.36x / 2.94x | 2887 | 50 | PASS |
|
||||
| sp170-w32 | 170 x 32 | 141.88 (99.6%) | 327.5 | 300.1 | 2.308 | 4.95x / 7.19x / 16.49x / 2.96x | 2886 | 53 | PASS |
|
||||
| sp128-w32 | 128 x 32 | 142.28 (99.9%) | 327.7 | 300.3 | 2.303 | 4.94x / 7.17x / 16.45x / 2.95x | 2872 | 55 | PASS |
|
||||
| sp96-w32 | 96 x 32 | 138.83 (97.5%) | 323.4 | 296.0 | 2.330 | 5.00x / 7.26x / 16.64x / 2.99x | 2872 | 56 | PASS |
|
||||
| sp85-w32 | 85 x 32 | 142.23 (99.9%) | 328.1 | 300.7 | 2.307 | 4.95x / 7.19x / 16.48x / 2.96x | 2872 | 57 | PASS |
|
||||
| sp64-w32 | 64 x 32 | 142.03 (99.7%) | 329.0 | 301.6 | 2.316 | 4.97x / 7.21x / 16.54x / 2.97x | 2872 | 58 | PASS |
|
||||
|
||||
**mx8_sh256x27** (mx8+sh256x27)
|
||||
|
||||
| Shape | Blocks x warps | MH/s | Watts | Watts over idle | Microjoules per hash | Edge: GDDR7 / HBM3 / SRAM / M5 Max | SM MHz | Temp | Check |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| base | 0 x 1 | 142.36 | 475.0 | 447.6 | 3.337 | 7.16x / 10.40x / 23.84x / 4.28x | 2777 | 66 | PASS |
|
||||
| sp170-w32 | 170 x 32 | 141.98 (99.7%) | 475.0 | 447.6 | 3.345 | 7.18x / 10.42x / 23.89x / 4.29x | 2767 | 68 | PASS |
|
||||
| sp128-w32 | 128 x 32 | 142.22 (99.9%) | 475.0 | 447.6 | 3.340 | 7.17x / 10.40x / 23.86x / 4.28x | 2764 | 69 | PASS |
|
||||
| sp96-w32 | 96 x 32 | 139.08 (97.7%) | 475.0 | 447.6 | 3.415 | 7.33x / 10.64x / 24.39x / 4.38x | 2787 | 69 | PASS |
|
||||
| sp85-w32 | 85 x 32 | 141.27 (99.2%) | 475.0 | 447.6 | 3.362 | 7.21x / 10.47x / 24.01x / 4.31x | 2772 | 70 | PASS |
|
||||
| sp64-w32 | 64 x 32 | 135.94 (95.5%) | 475.0 | 447.6 | 3.494 | 7.50x / 10.88x / 24.96x / 4.48x | 2803 | 70 | PASS |
|
||||
|
||||
|
||||
|
||||
### RTX 5090, rented host e (Vast, United Kingdom; driver 595.91)
|
||||
|
||||
knob pl=refused
|
||||
knob lgc=refused
|
||||
knob lmc=refused
|
||||
card NVIDIAGeForceRTX5090 driver 595.91.07 power limit 575.00 W idle 11.7 W at 185.2 MHz SM, 445.5 MHz memory
|
||||
|
||||
**mx8-genesis** (mx8)
|
||||
|
||||
| Shape | Blocks x warps | MH/s | Watts | Watts over idle | Microjoules per hash | Edge: GDDR7 / HBM3 / SRAM / M5 Max | SM MHz | Temp | Check |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| base | 0 x 1 | 141.96 | 352.7 | 341.0 | 2.485 | 5.33x / 7.74x / 17.75x / 3.19x | 2917 | 56 | PASS |
|
||||
| sp52-w32 | 52 x 32 | 135.85 (95.7%) | 332.0 | 320.3 | 2.444 | 5.24x / 7.61x / 17.46x / 3.13x | 2922 | 56 | PASS |
|
||||
| sp43-w32 | 43 x 32 | 141.21 (99.5%) | 337.4 | 325.7 | 2.389 | 5.13x / 7.44x / 17.06x / 3.06x | 2917 | 57 | PASS |
|
||||
| sp36-w32 | 36 x 32 | 140.88 (99.2%) | 336.9 | 325.2 | 2.391 | 5.13x / 7.45x / 17.08x / 3.07x | 2917 | 58 | PASS |
|
||||
| sp28-w32 | 28 x 32 | 139.58 (98.3%) | 335.6 | 323.9 | 2.404 | 5.16x / 7.49x / 17.17x / 3.08x | 2917 | 58 | PASS |
|
||||
| sp21-w32 | 21 x 32 | 137.23 (96.7%) | 334.9 | 323.2 | 2.440 | 5.24x / 7.60x / 17.43x / 3.13x | 2917 | 58 | PASS |
|
||||
|
||||
**mx8_sh256x27** (mx8+sh256x27)
|
||||
|
||||
| Shape | Blocks x warps | MH/s | Watts | Watts over idle | Microjoules per hash | Edge: GDDR7 / HBM3 / SRAM / M5 Max | SM MHz | Temp | Check |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| base | 0 x 1 | 141.90 | 533.6 | 521.9 | 3.760 | 8.07x / 11.71x / 26.86x / 4.82x | 2902 | 68 | PASS |
|
||||
| sp52-w32 | 52 x 32 | 122.17 (86.1%) | 484.4 | 472.7 | 3.965 | 8.51x / 12.35x / 28.32x / 5.08x | 2903 | 68 | PASS |
|
||||
| sp43-w32 | 43 x 32 | 125.11 (88.2%) | 491.8 | 480.1 | 3.931 | 8.44x / 12.25x / 28.08x / 5.04x | 2902 | 69 | PASS |
|
||||
| sp36-w32 | 36 x 32 | 105.18 (74.1%) | 444.6 | 432.9 | 4.227 | 9.07x / 13.17x / 30.19x / 5.42x | 2902 | 68 | PASS |
|
||||
| sp28-w32 | 28 x 32 | 81.73 (57.6%) | 389.2 | 377.5 | 4.762 | 10.22x / 14.83x / 34.01x / 6.11x | 2896 | 66 | PASS |
|
||||
| sp21-w32 | 21 x 32 | 61.61 (43.4%) | 340.4 | 328.7 | 5.525 | 11.86x / 17.21x / 39.46x / 7.08x | 2902 | 63 | PASS |
|
||||
|
||||
|
||||
|
||||
### RTX 5090, rented host f (Vast, Beijing; driver 580.173; power limit 450 W)
|
||||
|
||||
knob pl=refused
|
||||
knob lgc=refused
|
||||
knob lmc=refused
|
||||
card NVIDIAGeForceRTX5090 driver 580.173.02 power limit 450.00 W idle 29.8 W at 300.0 MHz SM, 405.0 MHz memory
|
||||
|
||||
**mx8-genesis** (mx8)
|
||||
|
||||
| Shape | Blocks x warps | MH/s | Watts | Watts over idle | Microjoules per hash | Edge: GDDR7 / HBM3 / SRAM / M5 Max | SM MHz | Temp | Check |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| base | 0 x 1 | 142.49 | 333.2 | 303.4 | 2.338 | 5.02x / 7.28x / 16.70x / 3.00x | 3028 | 47 | PASS |
|
||||
| sp16-w32 | 16 x 32 | 130.53 (91.6%) | 309.8 | 280.0 | 2.373 | 5.09x / 7.39x / 16.95x / 3.04x | 3030 | 48 | PASS |
|
||||
| sp11-w32 | 11 x 32 | 106.04 (74.4%) | 288.1 | 258.3 | 2.717 | 5.83x / 8.46x / 19.41x / 3.48x | 3030 | 48 | PASS |
|
||||
| sp170-w8 | 170 x 8 | 142.32 (99.9%) | 322.8 | 293.0 | 2.268 | 4.87x / 7.07x / 16.20x / 2.91x | 3030 | 49 | PASS |
|
||||
| sp170-w4 | 170 x 4 | 141.72 (99.5%) | 323.3 | 293.5 | 2.281 | 4.89x / 7.11x / 16.29x / 2.92x | 3030 | 50 | PASS |
|
||||
| sp170-w2 | 170 x 2 | 139.28 (97.8%) | 320.6 | 290.8 | 2.302 | 4.94x / 7.17x / 16.44x / 2.95x | 3030 | 51 | PASS |
|
||||
| sp43-w8 | 43 x 8 | 139.46 (97.9%) | 320.0 | 290.2 | 2.295 | 4.92x / 7.15x / 16.39x / 2.94x | 3030 | 52 | PASS |
|
||||
|
||||
**mx8_sh256x27** (mx8+sh256x27)
|
||||
|
||||
| Shape | Blocks x warps | MH/s | Watts | Watts over idle | Microjoules per hash | Edge: GDDR7 / HBM3 / SRAM / M5 Max | SM MHz | Temp | Check |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| base | 0 x 1 | 142.39 | 450.0 | 420.2 | 3.160 | 6.78x / 9.84x / 22.57x / 4.05x | 2882 | 58 | PASS |
|
||||
| sp16-w32 | 16 x 32 | 48.94 (34.4%) | 298.0 | 268.2 | 6.090 | 13.07x / 18.97x / 43.50x / 7.81x | 3022 | 54 | PASS |
|
||||
| sp11-w32 | 11 x 32 | 33.79 (23.7%) | 259.2 | 229.4 | 7.671 | 16.46x / 23.90x / 54.79x / 9.83x | 3037 | 52 | PASS |
|
||||
| sp170-w8 | 170 x 8 | 141.83 (99.6%) | 450.0 | 420.2 | 3.173 | 6.81x / 9.88x / 22.66x / 4.07x | 2853 | 59 | PASS |
|
||||
| sp170-w4 | 170 x 4 | 139.72 (98.1%) | 450.0 | 420.2 | 3.221 | 6.91x / 10.03x / 23.01x / 4.13x | 2854 | 60 | PASS |
|
||||
| sp170-w2 | 170 x 2 | 102.45 (72.0%) | 443.5 | 413.7 | 4.329 | 9.29x / 13.49x / 30.92x / 5.55x | 3012 | 62 | PASS |
|
||||
| sp43-w8 | 43 x 8 | 90.20 (63.3%) | 398.7 | 368.9 | 4.420 | 9.48x / 13.77x / 31.57x / 5.67x | 3022 | 60 | PASS |
|
||||
|
||||
|
||||
|
||||
### RTX 4090, rented (RunPod, 128 SMs, GDDR6X; driver 595.91; power limit 450 W)
|
||||
|
||||
knob pl=refused
|
||||
knob lgc=refused
|
||||
knob lmc=refused
|
||||
card NVIDIAGeForceRTX4090 driver 595.91.07 power limit 450.00 W idle 34.7 W at 210.0 MHz SM, 405.0 MHz memory
|
||||
|
||||
**mx8-genesis** (mx8)
|
||||
|
||||
| Shape | Blocks x warps | MH/s | Watts | Watts over idle | Microjoules per hash | Edge: GDDR7 / HBM3 / SRAM / M5 Max | SM MHz | Temp | Check |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| base | 0 x 1 | 70.63 | 253.4 | 218.7 | 3.588 | 7.70x / 11.18x / 25.63x / 4.60x | 2732 | 39 | PASS |
|
||||
| w4 | 0 x 4 | 70.62 (100.0%) | 254.9 | 220.2 | 3.609 | 7.74x / 11.24x / 25.78x / 4.63x | 2730 | 44 | PASS |
|
||||
| w8 | 0 x 8 | 70.64 (100.0%) | 255.6 | 220.9 | 3.618 | 7.76x / 11.27x / 25.84x / 4.64x | 2721 | 47 | PASS |
|
||||
| sp128-w32 | 128 x 32 | 70.64 (100.0%) | 255.6 | 220.9 | 3.618 | 7.76x / 11.27x / 25.84x / 4.64x | 2715 | 48 | PASS |
|
||||
| sp256-w16 | 256 x 16 | 70.57 (99.9%) | 256.1 | 221.4 | 3.629 | 7.79x / 11.31x / 25.92x / 4.65x | 2715 | 48 | PASS |
|
||||
| sp512-w8 | 512 x 8 | 70.60 (100.0%) | 256.0 | 221.3 | 3.626 | 7.78x / 11.30x / 25.90x / 4.65x | 2715 | 49 | PASS |
|
||||
| sp128-w16 | 128 x 16 | 70.64 (100.0%) | 254.2 | 219.5 | 3.599 | 7.72x / 11.21x / 25.71x / 4.61x | 2715 | 49 | PASS |
|
||||
| sp128-w8 | 128 x 8 | 70.58 (99.9%) | 252.0 | 217.3 | 3.570 | 7.66x / 11.12x / 25.50x / 4.58x | 2715 | 49 | PASS |
|
||||
| sp128-w4 | 128 x 4 | 70.45 (99.7%) | 251.3 | 216.6 | 3.567 | 7.65x / 11.11x / 25.48x / 4.57x | 2715 | 48 | PASS |
|
||||
| sp128-w2 | 128 x 2 | 70.17 (99.4%) | 250.6 | 215.9 | 3.571 | 7.66x / 11.12x / 25.51x / 4.58x | 2715 | 48 | PASS |
|
||||
| sp128-w1 | 128 x 1 | 66.28 (93.8%) | 242.7 | 208.0 | 3.662 | 7.86x / 11.41x / 26.16x / 4.69x | 2715 | 47 | PASS |
|
||||
| sp96-w32 | 96 x 32 | 70.61 (100.0%) | 253.7 | 219.0 | 3.593 | 7.71x / 11.19x / 25.66x / 4.61x | 2715 | 48 | PASS |
|
||||
| sp64-w32 | 64 x 32 | 70.59 (100.0%) | 253.1 | 218.4 | 3.585 | 7.69x / 11.17x / 25.61x / 4.60x | 2715 | 48 | PASS |
|
||||
| sp48-w32 | 48 x 32 | 70.50 (99.8%) | 252.2 | 217.5 | 3.578 | 7.68x / 11.15x / 25.56x / 4.59x | 2715 | 48 | PASS |
|
||||
| sp32-w32 | 32 x 32 | 70.42 (99.7%) | 251.4 | 216.7 | 3.570 | 7.66x / 11.12x / 25.50x / 4.58x | 2715 | 48 | PASS |
|
||||
| sp24-w32 | 24 x 32 | 67.15 (95.1%) | 247.1 | 212.4 | 3.680 | 7.90x / 11.46x / 26.29x / 4.72x | 2715 | 48 | PASS |
|
||||
| sp16-w32 | 16 x 32 | 69.95 (99.0%) | 249.6 | 214.9 | 3.568 | 7.66x / 11.12x / 25.49x / 4.57x | 2715 | 47 | PASS |
|
||||
| sp12-w32 | 12 x 32 | 68.69 (97.3%) | 246.5 | 211.8 | 3.589 | 7.70x / 11.18x / 25.64x / 4.60x | 2715 | 47 | PASS |
|
||||
| sp8-w32 | 8 x 32 | 65.07 (92.1%) | 239.1 | 204.4 | 3.675 | 7.89x / 11.45x / 26.25x / 4.71x | 2715 | 46 | PASS |
|
||||
| sp32-w8 | 32 x 8 | 70.19 (99.4%) | 248.9 | 214.2 | 3.546 | 7.61x / 11.05x / 25.33x / 4.55x | 2715 | 47 | PASS |
|
||||
|
||||
**mx8_sh256x27** (mx8+sh256x27)
|
||||
|
||||
| Shape | Blocks x warps | MH/s | Watts | Watts over idle | Microjoules per hash | Edge: GDDR7 / HBM3 / SRAM / M5 Max | SM MHz | Temp | Check |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| base | 0 x 1 | 70.63 | 353.5 | 318.8 | 5.005 | 10.74x / 15.59x / 35.75x / 6.42x | 2715 | 56 | PASS |
|
||||
| w4 | 0 x 4 | 70.62 (100.0%) | 356.8 | 322.1 | 5.052 | 10.84x / 15.74x / 36.09x / 6.48x | 2715 | 58 | PASS |
|
||||
| w8 | 0 x 8 | 70.63 (100.0%) | 357.6 | 322.9 | 5.063 | 10.86x / 15.77x / 36.16x / 6.49x | 2715 | 58 | PASS |
|
||||
| sp128-w32 | 128 x 32 | 70.60 (100.0%) | 356.1 | 321.4 | 5.044 | 10.82x / 15.71x / 36.03x / 6.47x | 2715 | 59 | PASS |
|
||||
| sp256-w16 | 256 x 16 | 70.58 (99.9%) | 356.9 | 322.2 | 5.057 | 10.85x / 15.75x / 36.12x / 6.48x | 2715 | 59 | PASS |
|
||||
| sp512-w8 | 512 x 8 | 70.57 (99.9%) | 358.4 | 323.7 | 5.078 | 10.90x / 15.82x / 36.27x / 6.51x | 2715 | 59 | PASS |
|
||||
| sp128-w16 | 128 x 16 | 70.54 (99.9%) | 355.6 | 320.9 | 5.041 | 10.82x / 15.70x / 36.01x / 6.46x | 2715 | 59 | PASS |
|
||||
| sp128-w8 | 128 x 8 | 70.53 (99.9%) | 355.0 | 320.3 | 5.034 | 10.80x / 15.68x / 35.96x / 6.45x | 2715 | 59 | PASS |
|
||||
| sp128-w4 | 128 x 4 | 70.30 (99.5%) | 354.4 | 319.7 | 5.041 | 10.82x / 15.70x / 36.01x / 6.46x | 2715 | 59 | PASS |
|
||||
| sp128-w2 | 128 x 2 | 63.43 (89.8%) | 332.5 | 297.8 | 5.242 | 11.25x / 16.33x / 37.44x / 6.72x | 2715 | 57 | PASS |
|
||||
| sp128-w1 | 128 x 1 | 40.51 (57.4%) | 267.5 | 232.8 | 6.603 | 14.17x / 20.57x / 47.16x / 8.47x | 2715 | 54 | PASS |
|
||||
| sp96-w32 | 96 x 32 | 70.57 (99.9%) | 350.9 | 316.2 | 4.972 | 10.67x / 15.49x / 35.51x / 6.37x | 2715 | 58 | PASS |
|
||||
| sp64-w32 | 64 x 32 | 70.57 (99.9%) | 349.7 | 315.0 | 4.955 | 10.63x / 15.44x / 35.39x / 6.35x | 2715 | 59 | PASS |
|
||||
| sp48-w32 | 48 x 32 | 69.10 (97.8%) | 345.1 | 310.4 | 4.994 | 10.72x / 15.56x / 35.67x / 6.40x | 2715 | 59 | PASS |
|
||||
| sp32-w32 | 32 x 32 | 68.22 (96.6%) | 341.4 | 306.7 | 5.004 | 10.74x / 15.59x / 35.74x / 6.42x | 2715 | 58 | PASS |
|
||||
| sp24-w32 | 24 x 32 | 66.93 (94.8%) | 336.6 | 301.9 | 5.029 | 10.79x / 15.67x / 35.92x / 6.45x | 2715 | 58 | PASS |
|
||||
| sp16-w32 | 16 x 32 | 44.84 (63.5%) | 276.4 | 241.7 | 6.163 | 13.23x / 19.20x / 44.02x / 7.90x | 2715 | 54 | PASS |
|
||||
| sp12-w32 | 12 x 32 | 33.66 (47.7%) | 242.9 | 208.2 | 7.216 | 15.48x / 22.48x / 51.54x / 9.25x | 2715 | 51 | PASS |
|
||||
| sp8-w32 | 8 x 32 | 22.46 (31.8%) | 208.0 | 173.3 | 9.261 | 19.87x / 28.85x / 66.15x / 11.87x | 2715 | 48 | PASS |
|
||||
| sp32-w8 | 32 x 8 | 58.36 (82.6%) | 311.3 | 276.6 | 5.334 | 11.45x / 16.62x / 38.10x / 6.84x | 2715 | 54 | PASS |
|
||||
|
||||
|
||||
**follow-up, 4090**
|
||||
|
||||
| Tag | Pack | Served shape | MH/s | Watts | Microjoules | SM MHz | Mem MHz | Check | Tune line |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
|
||||
RESULT memclocks label=4090 lmc=refused
|
||||
|
||||
| tune0.99 | mx8-genesis | sp26-w32 (26 x 32) | 70.15 | 250.2 | 3.567 | 2715 | 10501 | PASS | RESULT sparse tune device NVIDIA_GeForce_RTX_4090 sms 128 installed base 70.632 ceiling 70.632 hold 0.990 rows 128=70.609 96=70.616 64=70.590 48=70.524 32=70.476 24=67.148 28=70.377 26=70.143 25=69.726 chosen sp26-w32 70.143 (99.3% of the ceiling, 26 of 128 SMs) 24485 ms |
|
||||
| tune0.99 | mx8_sh256x27 | sp61-w32 (61 x 32) | 70.42 | 346.9 | 4.926 | 2715 | 10501 | PASS | RESULT sparse tune device NVIDIA_GeForce_RTX_4090 sms 128 installed base 70.624 ceiling 70.627 hold 0.990 rows 128=70.627 96=70.574 64=70.573 48=69.092 56=67.021 60=69.890 62=70.459 61=70.423 chosen sp61-w32 70.423 (99.7% of the ceiling, 61 of 128 SMs) 21979 ms |
|
||||
| tune0.98 | mx8-genesis | sp25-w32 (25 x 32) | 69.73 | 250.3 | 3.590 | 2715 | 10501 | PASS | RESULT sparse tune device NVIDIA_GeForce_RTX_4090 sms 128 installed base 70.636 ceiling 70.636 hold 0.980 rows 128=70.622 96=70.618 64=70.574 48=70.516 32=70.472 24=67.159 28=70.391 26=70.159 25=69.728 chosen sp25-w32 69.728 (98.7% of the ceiling, 25 of 128 SMs) 24483 ms |
|
||||
| tune0.98 | mx8_sh256x27 | sp59-w32 (59 x 32) | 70.23 | 345.9 | 4.925 | 2715 | 10501 | PASS | RESULT sparse tune device NVIDIA_GeForce_RTX_4090 sms 128 installed base 70.619 ceiling 70.619 hold 0.980 rows 128=70.619 96=70.581 64=70.564 48=69.094 56=67.027 60=69.901 58=69.197 59=70.270 chosen sp59-w32 70.270 (99.5% of the ceiling, 59 of 128 SMs) 22034 ms |
|
||||
| v5 | v5-genesis | ( x ) | 69.91 | 360.1 | 5.151 | 2715 | 10501 | PASS | |
|
||||
| v5 | v4-genesis | ( x ) | 69.89 | 360.0 | 5.151 | 2715 | 10501 | PASS | |
|
||||
| v5 | v4-genesis | base (0 x 1) | 69.91 | 360.6 | 5.158 | 2715 | 10501 | PASS | |
|
||||
|
||||
RESULT after-micro label=4090 watts=134.5 RESULT microbench probe=sleep status=ok unit="none (the SM-resident floor: full occupancy, __nanosleep, no issue)" ops_per_step=0 lanes=196608 regs=11 blocks_per_sm=6 steps=2000 launches=14056 launch_ms=2.1 seconds=30.0 start_utc=2026-10-08T12:32:09Z end_utc=2026-10-08T12:32:39Z G_steps_s=184.227 G_ops_s=0.000 checksum=f4a2875f29883636
|
||||
|
||||
|
||||
RESULT after-micro label=4090 watts=447.1 RESULT microbench probe=int_arx status=ok unit="int32 add, xor or rotate (4 independent chains, 3 ops each per step)" ops_per_step=12 lanes=196608 regs=12 blocks_per_sm=6 steps=16384 launches=20066 launch_ms=1.5 seconds=30.0 start_utc=2026-10-08T12:32:39Z end_utc=2026-10-08T12:33:09Z G_steps_s=2154.515 G_ops_s=25854.179 checksum=e8e10dc19924d864
|
||||
|
||||
|
||||
RESULT after-micro label=4090 watts=268.4 RESULT microbench probe=dram_chase_1g status=ok unit="dependent random 4-byte read in a 1 GiB table (the hash's own pattern, the control)" ops_per_step=1 lanes=196608 regs=16 blocks_per_sm=6 steps=512 launches=2795 launch_ms=10.7 seconds=30.0 start_utc=2026-10-08T12:33:09Z end_utc=2026-10-08T12:33:39Z G_steps_s=9.376 G_ops_s=9.376 checksum=7e9ab598fe7052e5
|
||||
|
||||
|
||||
RESULT after-idle label=4090 watts=65.7 sm_mhz=536.7 mem_mhz=3659.4
|
||||
|
||||
|
||||
|
||||
### H100 80 GB HBM3, rented (RunPod, 132 SMs; driver 580.126; power limit 700 W)
|
||||
|
||||
knob pl=refused
|
||||
knob lgc=refused
|
||||
knob lmc=allowed
|
||||
card NVIDIAH10080GBHBM3 driver 580.126.09 power limit 700.00 W idle 71.3 W at 345.0 MHz SM, 2619.0 MHz memory
|
||||
|
||||
**mx8-genesis** (mx8)
|
||||
|
||||
| Shape | Blocks x warps | MH/s | Watts | Watts over idle | Microjoules per hash | Edge: GDDR7 / HBM3 / SRAM / M5 Max | SM MHz | Temp | Check |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| base | 0 x 1 | 254.63 | 450.4 | 379.1 | 1.769 | 3.80x / 5.51x / 12.64x / 2.27x | 1980 | 44 | PASS |
|
||||
| w4 | 0 x 4 | 254.35 (99.9%) | 454.2 | 382.9 | 1.786 | 3.83x / 5.56x / 12.76x / 2.29x | 1980 | 46 | PASS |
|
||||
| w8 | 0 x 8 | 254.52 (100.0%) | 457.2 | 385.9 | 1.796 | 3.85x / 5.60x / 12.83x / 2.30x | 1980 | 50 | PASS |
|
||||
| sp132-w32 | 132 x 32 | 253.98 (99.7%) | 453.4 | 382.1 | 1.785 | 3.83x / 5.56x / 12.75x / 2.29x | 1980 | 52 | PASS |
|
||||
| sp264-w16 | 264 x 16 | 254.20 (99.8%) | 457.4 | 386.1 | 1.799 | 3.86x / 5.60x / 12.85x / 2.31x | 1980 | 54 | PASS |
|
||||
| sp528-w8 | 528 x 8 | 254.20 (99.8%) | 461.5 | 390.2 | 1.816 | 3.90x / 5.66x / 12.97x / 2.33x | 1980 | 55 | PASS |
|
||||
| sp132-w16 | 132 x 16 | 253.94 (99.7%) | 454.1 | 382.8 | 1.788 | 3.84x / 5.57x / 12.77x / 2.29x | 1980 | 56 | PASS |
|
||||
| sp132-w8 | 132 x 8 | 252.53 (99.2%) | 454.7 | 383.4 | 1.801 | 3.86x / 5.61x / 12.86x / 2.31x | 1980 | 56 | PASS |
|
||||
| sp132-w4 | 132 x 4 | 237.53 (93.3%) | 430.1 | 358.8 | 1.811 | 3.89x / 5.64x / 12.94x / 2.32x | 1980 | 56 | PASS |
|
||||
| sp132-w2 | 132 x 2 | 178.50 (70.1%) | | | | | | | PASS |
|
||||
| sp132-w1 | 132 x 1 | 105.25 (41.3%) | 277.6 | 206.3 | 2.637 | 5.66x / 8.21x / 18.84x / 3.38x | 1980 | 49 | PASS |
|
||||
| sp99-w32 | 99 x 32 | 212.70 (83.5%) | 402.8 | 331.5 | 1.894 | 4.06x / 5.90x / 13.53x / 2.43x | 1980 | 50 | PASS |
|
||||
| sp66-w32 | 66 x 32 | 189.20 (74.3%) | 372.9 | 301.6 | 1.971 | 4.23x / 6.14x / 14.08x / 2.53x | 1980 | 48 | PASS |
|
||||
| sp44-w32 | 44 x 32 | 150.11 (58.9%) | 323.9 | 252.6 | 2.158 | 4.63x / 6.72x / 15.41x / 2.77x | 1980 | 46 | PASS |
|
||||
| sp33-w32 | 33 x 32 | 123.41 (48.5%) | 291.0 | 219.7 | 2.358 | 5.06x / 7.35x / 16.84x / 3.02x | 1980 | 44 | PASS |
|
||||
| sp24-w32 | 24 x 32 | 89.90 (35.3%) | 250.2 | 178.9 | 2.783 | 5.97x / 8.67x / 19.88x / 3.57x | 1980 | 42 | PASS |
|
||||
| sp17-w32 | 17 x 32 | 63.66 (25.0%) | 219.1 | 147.8 | 3.442 | 7.39x / 10.72x / 24.59x / 4.41x | 1980 | 41 | PASS |
|
||||
| sp12-w32 | 12 x 32 | 55.38 (21.7%) | 210.8 | 139.5 | 3.806 | 8.17x / 11.86x / 27.19x / 4.88x | 1980 | 40 | PASS |
|
||||
| sp8-w32 | 8 x 32 | 36.93 (14.5%) | 189.3 | 118.0 | 5.126 | 11.00x / 15.97x / 36.61x / 6.57x | 1980 | 40 | PASS |
|
||||
| sp33-w8 | 33 x 8 | 123.50 (48.5%) | 293.2 | 221.9 | 2.374 | 5.09x / 7.40x / 16.96x / 3.04x | 1980 | 47 | PASS |
|
||||
|
||||
**mx8_sh256x27** (mx8+sh256x27)
|
||||
|
||||
| Shape | Blocks x warps | MH/s | Watts | Watts over idle | Microjoules per hash | Edge: GDDR7 / HBM3 / SRAM / M5 Max | SM MHz | Temp | Check |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| base | 0 x 1 | 251.90 | 699.4 | 628.1 | 2.776 | 5.96x / 8.65x / 19.83x / 3.56x | 1830 | 66 | PASS |
|
||||
| w4 | 0 x 4 | 250.56 (99.5%) | 698.5 | 627.2 | 2.788 | 5.98x / 8.69x / 19.91x / 3.57x | 1748 | 69 | PASS |
|
||||
| w8 | 0 x 8 | 249.31 (99.0%) | 697.6 | 626.3 | 2.798 | 6.00x / 8.72x / 19.99x / 3.59x | 1734 | 70 | PASS |
|
||||
| sp132-w32 | 132 x 32 | 242.15 (96.1%) | 696.7 | 625.4 | 2.877 | 6.17x / 8.96x / 20.55x / 3.69x | 1744 | 72 | PASS |
|
||||
| sp264-w16 | 264 x 16 | 237.66 (94.3%) | 698.5 | 627.2 | 2.939 | 6.31x / 9.16x / 20.99x / 3.77x | 1795 | 71 | PASS |
|
||||
| sp528-w8 | 528 x 8 | 231.67 (92.0%) | 695.7 | 624.4 | 3.003 | 6.44x / 9.36x / 21.45x / 3.85x | 1768 | 69 | PASS |
|
||||
| sp132-w16 | 132 x 16 | 226.18 (89.8%) | 699.4 | 628.1 | 3.092 | 6.64x / 9.63x / 22.09x / 3.96x | 1857 | 69 | PASS |
|
||||
| sp132-w8 | 132 x 8 | 173.30 (68.8%) | 630.0 | 558.7 | 3.635 | 7.80x / 11.32x / 25.96x / 4.66x | 1980 | 66 | PASS |
|
||||
| sp132-w4 | 132 x 4 | 108.07 (42.9%) | 452.5 | 381.2 | 4.187 | 8.98x / 13.04x / 29.91x / 5.37x | 1980 | 57 | PASS |
|
||||
| sp132-w2 | 132 x 2 | 54.90 (21.8%) | 301.3 | 230.0 | 5.488 | 11.78x / 17.10x / 39.20x / 7.04x | 1980 | 49 | PASS |
|
||||
| sp132-w1 | 132 x 1 | 29.45 (11.7%) | 226.4 | 155.1 | 7.688 | 16.50x / 23.95x / 54.91x / 9.86x | 1980 | 44 | PASS |
|
||||
| sp99-w32 | 99 x 32 | 206.11 (81.8%) | 697.2 | 625.9 | 3.383 | 7.26x / 10.54x / 24.16x / 4.34x | 1967 | 63 | PASS |
|
||||
| sp66-w32 | 66 x 32 | 138.62 (55.0%) | 520.4 | 449.1 | 3.754 | 8.06x / 11.69x / 26.81x / 4.81x | 1980 | 57 | PASS |
|
||||
| sp44-w32 | 44 x 32 | 92.59 (36.8%) | 392.6 | 321.3 | 4.240 | 9.10x / 13.21x / 30.29x / 5.44x | 1980 | 52 | PASS |
|
||||
| sp33-w32 | 33 x 32 | 69.57 (27.6%) | 330.5 | 259.2 | 4.751 | 10.20x / 14.80x / 33.94x / 6.09x | 1980 | 49 | PASS |
|
||||
| sp24-w32 | 24 x 32 | 50.64 (20.1%) | 279.1 | 207.8 | 5.511 | 11.83x / 17.17x / 39.36x / 7.07x | 1980 | 46 | PASS |
|
||||
| sp17-w32 | 17 x 32 | 35.89 (14.2%) | 238.0 | 166.7 | 6.631 | 14.23x / 20.66x / 47.36x / 8.50x | 1980 | 43 | PASS |
|
||||
| sp12-w32 | 12 x 32 | 25.34 (10.1%) | 207.5 | 136.2 | 8.190 | 17.58x / 25.51x / 58.50x / 10.50x | 1980 | 41 | PASS |
|
||||
| sp8-w32 | 8 x 32 | 16.90 (6.7%) | 182.5 | 111.2 | 10.798 | 23.17x / 33.64x / 77.13x / 13.84x | 1980 | 39 | PASS |
|
||||
| sp33-w8 | 33 x 8 | 45.06 (17.9%) | 262.6 | 191.3 | 5.827 | 12.50x / 18.15x / 41.62x / 7.47x | 1980 | 43 | PASS |
|
||||
|
||||
|
||||
**follow-up, h100**
|
||||
|
||||
| Tag | Pack | Served shape | MH/s | Watts | Microjoules | SM MHz | Mem MHz | Check | Tune line |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
|
||||
RESULT memclocks label=h100 supported=[2619 1593 ]
|
||||
|
||||
| mem2619 | mx8-genesis | base (0 x 1) | 254.84 | 454.6 | 1.784 | 1980 | 2619 | PASS | |
|
||||
| mem2619 | mx8_sh256x27 | base (0 x 1) | 253.00 | 699.4 | 2.764 | 1716 | 2619 | PASS | |
|
||||
| mem1593 | mx8-genesis | base (0 x 1) | 254.57 | 458.8 | 1.802 | 1980 | 2619 | PASS | |
|
||||
| mem1593 | mx8_sh256x27 | base (0 x 1) | 252.77 | 698.4 | 2.763 | 1694 | 2619 | PASS | |
|
||||
| tune0.99 | mx8-genesis | sp127-w32 (127 x 32) | 253.19 | 441.6 | 1.744 | 1980 | 2619 | PASS | RESULT sparse tune device NVIDIA_H100_80GB_HBM3 sms 132 installed base 254.458 ceiling 254.458 hold 0.990 rows 132=254.076 99=212.879 115=230.328 123=244.690 127=253.179 125=248.878 chosen sp127-w32 253.179 (99.5% of the ceiling, 127 of 132 SMs) 15165 ms |
|
||||
| tune0.99 | mx8_sh256x27 | base (0 x 1) | 252.67 | 692.7 | 2.742 | 1752 | 2619 | PASS | RESULT sparse tune device NVIDIA_H100_80GB_HBM3 sms 132 installed base 247.311 ceiling 247.311 hold 0.990 rows 132=242.003 99=205.751 chosen none (no sparse shape held; the installed kernel stays) 6557 ms |
|
||||
| tune0.98 | mx8-genesis | sp127-w32 (127 x 32) | 253.35 | 451.1 | 1.781 | 1980 | 2619 | PASS | RESULT sparse tune device NVIDIA_H100_80GB_HBM3 sms 132 installed base 254.909 ceiling 254.909 hold 0.980 rows 132=254.394 99=212.934 115=230.633 123=244.933 127=252.939 125=249.108 chosen sp127-w32 252.939 (99.2% of the ceiling, 127 of 132 SMs) 15153 ms |
|
||||
| tune0.98 | mx8_sh256x27 | base (0 x 1) | 252.17 | 687.3 | 2.726 | 1782 | 2619 | PASS | RESULT sparse tune device NVIDIA_H100_80GB_HBM3 sms 132 installed base 247.177 ceiling 247.177 hold 0.980 rows 132=241.720 99=204.596 chosen none (no sparse shape held; the installed kernel stays) 6572 ms |
|
||||
| v5 | v5-genesis | ( x ) | 250.24 | 698.8 | 2.793 | 1674 | 2619 | PASS | |
|
||||
| v5 | v4-genesis | ( x ) | 250.59 | 699.7 | 2.792 | 1732 | 2619 | PASS | |
|
||||
| v5 | v4-genesis | base (0 x 1) | 250.58 | 699.6 | 2.792 | 1755 | 2619 | PASS | |
|
||||
|
||||
RESULT after-micro label=h100 watts=136.6 RESULT microbench probe=sleep status=ok unit="none (the SM-resident floor: full occupancy, __nanosleep, no issue)" ops_per_step=0 lanes=270336 regs=11 blocks_per_sm=8 steps=2000 launches=13740 launch_ms=2.1 seconds=30.0 start_utc=2026-10-08T12:16:17Z end_utc=2026-10-08T12:16:47Z G_steps_s=247.619 G_ops_s=0.000 checksum=8b671be935472947
|
||||
|
||||
|
||||
RESULT after-micro label=h100 watts=437.9 RESULT microbench probe=int_arx status=ok unit="int32 add, xor or rotate (4 independent chains, 3 ops each per step)" ops_per_step=12 lanes=270336 regs=14 blocks_per_sm=8 steps=16384 launches=11899 launch_ms=2.4 seconds=30.0 start_utc=2026-10-08T12:16:47Z end_utc=2026-10-08T12:17:17Z G_steps_s=1756.760 G_ops_s=21081.115 checksum=fcb635c03d37ed2a
|
||||
|
||||
|
||||
RESULT after-micro label=h100 watts=450.3 RESULT microbench probe=dram_chase_1g status=ok unit="dependent random 4-byte read in a 1 GiB table (the hash's own pattern, the control)" ops_per_step=1 lanes=270336 regs=19 blocks_per_sm=8 steps=512 launches=6982 launch_ms=4.2 seconds=30.0 start_utc=2026-10-08T12:17:17Z end_utc=2026-10-08T12:17:47Z G_steps_s=32.213 G_ops_s=32.213 checksum=7623c6b86570e4c
|
||||
|
||||
|
||||
RESULT after-idle label=h100 watts=80.0 sm_mhz=350.6 mem_mhz=2619.0
|
||||
|
||||
|
||||
|
||||
### PC 1, the RTX 5090 alone, the hash lane's job `run-ca4-pc1-floorsm-5090-20261008` (the ca4sparse5 exe, unlocked and at the 1,300 MHz lock through the Power Helper)
|
||||
|
||||
|
||||
The floorsm ladder
|
||||
|
||||
| Pack | State | Shape | MH/s | W | Microjoules | SM MHz | Mem MHz | Temp max | Check |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| v4-devnet-epoch0 | unlocked | base (0) | 137.164 | 452.8 | 3.301 | 2865 | 13801 | 62 | PASS |
|
||||
| v4-devnet-epoch0 | unlocked | sp170-w32 (170) | 136.407 | 462.6 | 3.391 | 2842 | 13801 | 67 | PASS |
|
||||
| v4-devnet-epoch0 | unlocked | sp170-w16 (170) | 137.12 | 480.3 | 3.503 | 2842 | 13801 | 72 | PASS |
|
||||
| v4-devnet-epoch0 | unlocked | sp170-w8 (170) | 137.035 | 482.9 | 3.524 | 2827.6 | 13801 | 74 | PASS |
|
||||
| v4-devnet-epoch0 | unlocked | sp170-w4 (170) | 134.941 | 483.9 | 3.586 | 2827 | 13801 | 75 | PASS |
|
||||
| v4-devnet-epoch0 | unlocked | sp170-w2 (170) | 98.779 | 409.6 | 4.147 | 2835 | 13801 | 73 | PASS |
|
||||
| v4-devnet-epoch0 | unlocked | sp128-w32 (128) | 136.422 | 476.2 | 3.491 | 2835 | 13801 | 74 | PASS |
|
||||
| v4-devnet-epoch0 | unlocked | sp96-w32 (96) | 134.373 | 470.6 | 3.502 | 2835 | 13801 | 75 | PASS |
|
||||
| v4-devnet-epoch0 | unlocked | sp85-w32 (85) | 136.278 | 474.8 | 3.484 | 2835 | 13801 | 76 | PASS |
|
||||
| v4-devnet-epoch0 | unlocked | sp64-w32 (64) | 136.056 | 474.7 | 3.489 | 2835 | 13801 | 75 | PASS |
|
||||
| v4-devnet-epoch0 | unlocked | sp52-w32 (52) | 128.568 | 459.9 | 3.577 | 2842 | 13801 | 75 | PASS |
|
||||
| v4-devnet-epoch0 | unlocked | sp43-w32 (43) | 134.477 | 470 | 3.495 | 2842 | 13801 | 75 | PASS |
|
||||
| v4-devnet-epoch0 | unlocked | sp36-w32 (36) | 120.347 | 440.9 | 3.664 | 2842 | 13801 | 74 | PASS |
|
||||
| v4-devnet-epoch0 | unlocked | sp32-w32 (32) | 107.265 | 413.1 | 3.851 | 2850 | 13801 | 73 | PASS |
|
||||
| v4-devnet-epoch0 | unlocked | sp28-w32 (28) | 94.364 | 384.2 | 4.071 | 2857 | 13801 | 71 | PASS |
|
||||
| v4-devnet-epoch0 | unlocked | sp24-w32 (24) | 80.823 | 353.8 | 4.377 | 2850 | 13801 | 69 | PASS |
|
||||
| v4-devnet-epoch0 | unlocked | sp21-w32 (21) | 70.735 | 327.2 | 4.626 | 2850 | 13801 | 66 | PASS |
|
||||
| v4-devnet-epoch0 | unlocked | sp16-w32 (16) | 53.985 | 290.9 | 5.389 | 2858.3 | 13801 | 64 | PASS |
|
||||
| v4-devnet-epoch0 | unlocked | sp11-w32 (11) | 37.34 | 250.9 | 6.719 | 2872 | 13801 | 60 | PASS |
|
||||
| v4-devnet-epoch0 | unlocked | sp43-w8 (43) | 89.595 | 362.8 | 4.049 | 2855.9 | 13801 | 65 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked | base (0) | 136.721 | 320.9 | 2.347 | 2850 | 13801 | 63 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked | sp170-w32 (170) | 136.166 | 319.1 | 2.343 | 2850 | 13801 | 63 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked | sp170-w16 (170) | 136.884 | 316.8 | 2.314 | 2850 | 13801 | 63 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked | sp170-w8 (170) | 137.149 | 310.8 | 2.266 | 2850 | 13801 | 63 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked | sp170-w4 (170) | 136.904 | 310.9 | 2.271 | 2865 | 13801 | 63 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked | sp170-w2 (170) | 135.705 | 309.9 | 2.284 | 2865 | 13801 | 62 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked | sp128-w32 (128) | 136.627 | 313.4 | 2.294 | 2865 | 13801 | 62 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked | sp96-w32 (96) | 134.635 | 309.9 | 2.302 | 2865 | 13801 | 62 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked | sp85-w32 (85) | 136.758 | 311.6 | 2.278 | 2865 | 13801 | 62 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked | sp64-w32 (64) | 136.733 | 311.3 | 2.277 | 2865 | 13801 | 62 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked | sp52-w32 (52) | 132.782 | 307.2 | 2.314 | 2865 | 13801 | 62 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked | sp43-w32 (43) | 136.463 | 310.6 | 2.276 | 2865 | 13801 | 61 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked | sp36-w32 (36) | 136.046 | 309.6 | 2.276 | 2865 | 13801 | 61 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked | sp32-w32 (32) | 135.686 | 309.4 | 2.280 | 2865 | 13801 | 61 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked | sp28-w32 (28) | 135.201 | 308.6 | 2.283 | 2865 | 13801 | 61 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked | sp24-w32 (24) | 133.819 | 307 | 2.294 | 2865 | 13801 | 61 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked | sp21-w32 (21) | 132.826 | 305.6 | 2.301 | 2865 | 13801 | 61 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked | sp16-w32 (16) | 125.545 | 294.4 | 2.345 | 2865 | 13801 | 61 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked | sp11-w32 (11) | 100.111 | 275.1 | 2.748 | 2865.2 | 13801 | 60 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked | sp43-w8 (43) | 135.989 | 306.2 | 2.252 | 2865 | 13801 | 61 | PASS |
|
||||
| v4-devnet-epoch0 | 1300 | base (0) | 134.234 | 302 | 2.250 | 1290 | 13801 | 55 | PASS |
|
||||
| v4-devnet-epoch0 | 1300 | sp170-w32 (170) | 128.882 | 298.1 | 2.313 | 1290 | 13801 | 59 | PASS |
|
||||
| v4-devnet-epoch0 | 1300 | sp170-w16 (170) | 130.496 | 302.4 | 2.317 | 1290 | 13801 | 61 | PASS |
|
||||
| v4-devnet-epoch0 | 1300 | sp170-w8 (170) | 131.723 | 303.7 | 2.306 | 1290 | 13801 | 62 | PASS |
|
||||
| v4-devnet-epoch0 | 1300 | sp170-w4 (170) | 101.305 | 267.2 | 2.638 | 1290 | 13801 | 60 | PASS |
|
||||
| v4-devnet-epoch0 | 1300 | sp170-w2 (170) | 54.393 | 197.9 | 3.638 | 1290 | 13801 | 57 | PASS |
|
||||
| v4-devnet-epoch0 | 1300 | sp128-w32 (128) | 130.252 | 299 | 2.296 | 1290 | 13801 | 59 | PASS |
|
||||
| v4-devnet-epoch0 | 1300 | sp96-w32 (96) | 120.933 | 286.1 | 2.366 | 1290 | 13801 | 60 | PASS |
|
||||
| v4-devnet-epoch0 | 1300 | sp85-w32 (85) | 125.221 | 292.8 | 2.338 | 1290 | 13801 | 61 | PASS |
|
||||
| v4-devnet-epoch0 | 1300 | sp64-w32 (64) | 95.167 | 249.9 | 2.626 | 1290 | 13801 | 59 | PASS |
|
||||
| v4-devnet-epoch0 | 1300 | sp52-w32 (52) | 77.801 | 228.9 | 2.942 | 1290 | 13801 | 57 | PASS |
|
||||
| v4-devnet-epoch0 | 1300 | sp43-w32 (43) | 64.343 | 208.5 | 3.240 | 1290 | 13801 | 55 | PASS |
|
||||
| v4-devnet-epoch0 | 1300 | sp36-w32 (36) | 53.964 | 185.3 | 3.434 | 1290 | 13801 | 53 | PASS |
|
||||
| v4-devnet-epoch0 | 1300 | sp32-w32 (32) | 47.978 | 174.7 | 3.641 | 1290 | 13801 | 51 | PASS |
|
||||
| v4-devnet-epoch0 | 1300 | sp28-w32 (28) | 41.985 | 170.9 | 4.071 | 1290 | 13801 | 50 | PASS |
|
||||
| v4-devnet-epoch0 | 1300 | sp24-w32 (24) | 35.981 | 158.5 | 4.405 | 1290 | 13801 | 49 | PASS |
|
||||
| v4-devnet-epoch0 | 1300 | sp21-w32 (21) | 31.455 | 155 | 4.928 | 1290 | 13801 | 48 | PASS |
|
||||
| v4-devnet-epoch0 | 1300 | sp16-w32 (16) | 24.07 | 137.3 | 5.704 | 1290 | 13801 | 46 | PASS |
|
||||
| v4-devnet-epoch0 | 1300 | sp11-w32 (11) | 16.551 | 115.1 | 6.954 | 1290 | 13801 | 44 | PASS |
|
||||
| v4-devnet-epoch0 | 1300 | sp43-w8 (43) | 45.592 | 173.4 | 3.803 | 1290 | 13801 | 46 | PASS |
|
||||
| mx8-devnet-epoch0 | 1300 | base (0) | 134.076 | 213 | 1.589 | 1290 | 13801 | 48 | PASS |
|
||||
| mx8-devnet-epoch0 | 1300 | sp170-w32 (170) | 129.08 | 208.9 | 1.618 | 1290 | 13801 | 49 | PASS |
|
||||
| mx8-devnet-epoch0 | 1300 | sp170-w16 (170) | 130.441 | 209.7 | 1.608 | 1290 | 13801 | 51 | PASS |
|
||||
| mx8-devnet-epoch0 | 1300 | sp170-w8 (170) | 130.788 | 209.7 | 1.603 | 1290 | 13801 | 51 | PASS |
|
||||
| mx8-devnet-epoch0 | 1300 | sp170-w4 (170) | 130.789 | 210.1 | 1.606 | 1290 | 13801 | 52 | PASS |
|
||||
| mx8-devnet-epoch0 | 1300 | sp170-w2 (170) | 130.278 | 210.4 | 1.615 | 1290 | 13801 | 52 | PASS |
|
||||
| mx8-devnet-epoch0 | 1300 | sp128-w32 (128) | 132.634 | 213.6 | 1.610 | 1290 | 13801 | 53 | PASS |
|
||||
| mx8-devnet-epoch0 | 1300 | sp96-w32 (96) | 120.795 | 205.2 | 1.699 | 1290 | 13801 | 52 | PASS |
|
||||
| mx8-devnet-epoch0 | 1300 | sp85-w32 (85) | 110.96 | 191.9 | 1.729 | 1290 | 13801 | 52 | PASS |
|
||||
| mx8-devnet-epoch0 | 1300 | sp64-w32 (64) | 103.449 | 187.2 | 1.810 | 1290 | 13801 | 51 | PASS |
|
||||
| mx8-devnet-epoch0 | 1300 | sp52-w32 (52) | 85.585 | 174.3 | 2.037 | 1290 | 13801 | 50 | PASS |
|
||||
| mx8-devnet-epoch0 | 1300 | sp43-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | 1300 | sp36-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | 1300 | sp32-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | 1300 | sp28-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | 1300 | sp24-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | 1300 | sp21-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | 1300 | sp16-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | 1300 | sp11-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | 1300 | sp43-w8 | | | | | | | error: budget |
|
||||
| v4-devnet-epoch0 | unlocked-end | base | | | | | | | error: budget |
|
||||
| v4-devnet-epoch0 | unlocked-end | sp170-w32 | | | | | | | error: budget |
|
||||
| v4-devnet-epoch0 | unlocked-end | sp170-w16 | | | | | | | error: budget |
|
||||
| v4-devnet-epoch0 | unlocked-end | sp170-w8 | | | | | | | error: budget |
|
||||
| v4-devnet-epoch0 | unlocked-end | sp170-w4 | | | | | | | error: budget |
|
||||
| v4-devnet-epoch0 | unlocked-end | sp170-w2 | | | | | | | error: budget |
|
||||
| v4-devnet-epoch0 | unlocked-end | sp128-w32 | | | | | | | error: budget |
|
||||
| v4-devnet-epoch0 | unlocked-end | sp96-w32 | | | | | | | error: budget |
|
||||
| v4-devnet-epoch0 | unlocked-end | sp85-w32 | | | | | | | error: budget |
|
||||
| v4-devnet-epoch0 | unlocked-end | sp64-w32 | | | | | | | error: budget |
|
||||
| v4-devnet-epoch0 | unlocked-end | sp52-w32 | | | | | | | error: budget |
|
||||
| v4-devnet-epoch0 | unlocked-end | sp43-w32 | | | | | | | error: budget |
|
||||
| v4-devnet-epoch0 | unlocked-end | sp36-w32 | | | | | | | error: budget |
|
||||
| v4-devnet-epoch0 | unlocked-end | sp32-w32 | | | | | | | error: budget |
|
||||
| v4-devnet-epoch0 | unlocked-end | sp28-w32 | | | | | | | error: budget |
|
||||
| v4-devnet-epoch0 | unlocked-end | sp24-w32 | | | | | | | error: budget |
|
||||
| v4-devnet-epoch0 | unlocked-end | sp21-w32 | | | | | | | error: budget |
|
||||
| v4-devnet-epoch0 | unlocked-end | sp16-w32 | | | | | | | error: budget |
|
||||
| v4-devnet-epoch0 | unlocked-end | sp11-w32 | | | | | | | error: budget |
|
||||
| v4-devnet-epoch0 | unlocked-end | sp43-w8 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | unlocked-end | base | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | unlocked-end | sp170-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | unlocked-end | sp170-w16 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | unlocked-end | sp170-w8 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | unlocked-end | sp170-w4 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | unlocked-end | sp170-w2 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | unlocked-end | sp128-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | unlocked-end | sp96-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | unlocked-end | sp85-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | unlocked-end | sp64-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | unlocked-end | sp52-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | unlocked-end | sp43-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | unlocked-end | sp36-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | unlocked-end | sp32-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | unlocked-end | sp28-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | unlocked-end | sp24-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | unlocked-end | sp21-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | unlocked-end | sp16-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | unlocked-end | sp11-w32 | | | | | | | error: budget |
|
||||
| mx8-devnet-epoch0 | unlocked-end | sp43-w8 | | | | | | | error: budget |
|
||||
|
||||
|
||||
### PC 1, the memory-clock ladder at the 1,300 MHz lock, job `run-ca4-pc1-memclk-5090-20261008`
|
||||
|
||||
|
||||
The memory-clock ladder
|
||||
|
||||
| Pack | State | Shape | MH/s | W | Microjoules | SM MHz | Mem MHz | Temp max | Check |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| v4-devnet-epoch0 | unlocked | base (grid) | 136.552 | 464.3 | 3.400 | 2852.1 | 13801 | 70 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked | base (grid) | 136.29 | 328.1 | 2.407 | 2850 | 13801 | 65 | PASS |
|
||||
| v4-devnet-epoch0 | 1300 | base (grid) | 134.34 | 309 | 2.300 | 1290 | 13801 | 60 | PASS |
|
||||
| mx8-devnet-epoch0 | 1300 | base (grid) | 134.089 | 222.9 | 1.662 | 1290 | 13801 | 57 | PASS |
|
||||
| v4-devnet-epoch0 | 1300m14001 | base (grid) | 134.513 | 308.1 | 2.290 | 1290 | 13801 | 59 | PASS |
|
||||
| mx8-devnet-epoch0 | 1300m14001 | base (grid) | 134.342 | 222.7 | 1.658 | 1290 | 13801 | 56 | PASS |
|
||||
| v4-devnet-epoch0 | 1300m12001 | base (grid) | 134.528 | 308.6 | 2.294 | 1290 | 13801 | 59 | PASS |
|
||||
| mx8-devnet-epoch0 | 1300m12001 | base (grid) | 134.252 | 223.1 | 1.662 | 1290 | 13801 | 56 | PASS |
|
||||
| v4-devnet-epoch0 | 1300m10001 | base (grid) | 134.487 | 308.9 | 2.297 | 1290 | 13801 | 59 | PASS |
|
||||
| mx8-devnet-epoch0 | 1300m10001 | base (grid) | 134.287 | 223.7 | 1.666 | 1290 | 13801 | 55 | PASS |
|
||||
| v4-devnet-epoch0 | 1300m8001 | base (grid) | 134.541 | 308.7 | 2.294 | 1290 | 13801 | 60 | PASS |
|
||||
| mx8-devnet-epoch0 | 1300m8001 | base (grid) | 134.33 | 223 | 1.660 | 1290 | 13801 | 56 | PASS |
|
||||
| v4-devnet-epoch0 | 1300m6001 | base (grid) | 76.09 | 187 | 2.458 | 1290 | 7001 | 53 | PASS |
|
||||
| mx8-devnet-epoch0 | 1300m6001 | base (grid) | 76.009 | 142.2 | 1.871 | 1290 | 7001 | 51 | PASS |
|
||||
| v4-devnet-epoch0 | 1300m5001 | base (grid) | 76.091 | 184.4 | 2.423 | 1290 | 7001 | 50 | PASS |
|
||||
| mx8-devnet-epoch0 | 1300m5001 | base (grid) | 76.007 | 141.1 | 1.856 | 1290 | 7001 | 48 | PASS |
|
||||
| v4-devnet-epoch0 | 1300m3001 | base (grid) | 76.082 | 183.8 | 2.416 | 1290 | 7001 | 50 | PASS |
|
||||
| mx8-devnet-epoch0 | 1300m3001 | base (grid) | 75.96 | 140.9 | 1.855 | 1290 | 7001 | 47 | PASS |
|
||||
| v4-devnet-epoch0 | unlocked-end | base (grid) | 136.562 | 452.5 | 3.314 | 2866.6 | 13801 | 61 | PASS |
|
||||
| mx8-devnet-epoch0 | unlocked-end | base (grid) | 136.298 | 318.6 | 2.338 | 2865 | 13801 | 60 | PASS |
|
||||
|
||||
|
||||
### The decomposition probes (the worker's --microbench: sleep = full residency and no issue; the L2 chase; the dependent DRAM chase; the ALU probe; 30 s each with the sampler's watts and both clocks)
|
||||
|
||||
|
||||
| Card | Probe | Watts | SM MHz | Mem MHz | G ops or reads per s | Lanes | nJ or pJ per op over the sleep row |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 4090 | idle | 33.9 | 210.0 | 405.0 | | | |
|
||||
| 4090 | sleep | 133.1 | 2725.9 | 10501.0 | 0.000 | 196608 | |
|
||||
| 4090 | int_arx | 448.0 | 2574.1 | 10501.0 | 26076.595 | 196608 | 12.1 pJ |
|
||||
| 4090 | l2_chase_32m | 329.0 | 2715.0 | 10501.0 | 80.816 | 196608 | 2.42 nJ |
|
||||
| 4090 | l2_indep4_32m | 328.2 | 2715.0 | 10501.0 | 82.301 | 196608 | 2370.6 pJ |
|
||||
| 4090 | dram_chase_1g | 263.0 | 2715.0 | 10501.0 | 9.399 | 196608 | 13.82 nJ |
|
||||
| 5090c | idle | 184.2 | 2044.5 | 11141.9 | | | |
|
||||
| 5090c | sleep | 196.5 | 2865.0 | 13801.0 | 0.000 | 261120 | |
|
||||
| 5090c | int_arx | 372.9 | 2853.9 | 13801.0 | 13064.448 | 261120 | 13.5 pJ |
|
||||
| 5090c | l2_chase_32m | 382.2 | 2865.0 | 13801.0 | 53.964 | 261120 | 3.44 nJ |
|
||||
| 5090c | l2_indep4_32m | 401.4 | 2851.3 | 13801.0 | 51.085 | 261120 | 4011.0 pJ |
|
||||
| 5090c | dram_chase_1g | 372.9 | 2865.0 | 13801.0 | 8.019 | 261120 | 22.00 nJ |
|
||||
| 5090e | idle | 8.7 | 180.0 | 405.0 | | | |
|
||||
| 5090e | sleep | 139.0 | 2932.0 | 13801.0 | 0.000 | 261120 | |
|
||||
| 5090e | int_arx | 535.6 | 2854.9 | 13801.0 | 29035.421 | 261120 | 13.7 pJ |
|
||||
| 5090e | l2_chase_32m | 415.1 | 2902.0 | 13801.0 | 104.533 | 261120 | 2.64 nJ |
|
||||
| 5090e | l2_indep4_32m | 434.9 | 2902.0 | 13801.0 | 112.864 | 261120 | 2621.7 pJ |
|
||||
| 5090e | dram_chase_1g | 341.3 | 2904.0 | 13801.0 | 17.617 | 261120 | 11.48 nJ |
|
||||
| h100 | idle | 71.8 | 345.0 | 2619.0 | | | |
|
||||
| h100 | sleep | 130.3 | 1980.0 | 2619.0 | 0.000 | 270336 | |
|
||||
| h100 | int_arx | 440.0 | 1980.0 | 2619.0 | 21499.215 | 270336 | 14.4 pJ |
|
||||
| h100 | l2_chase_32m | 479.1 | 1980.0 | 2619.0 | 121.037 | 270336 | 2.88 nJ |
|
||||
| h100 | l2_indep4_32m | 538.6 | 1980.0 | 2619.0 | 107.277 | 270336 | 3806.0 pJ |
|
||||
| h100 | dram_chase_1g | 466.5 | 1980.0 | 2619.0 | 32.551 | 270336 | 10.33 nJ |
|
||||
|
||||
## 2. What the rows say, shape by shape
|
||||
|
||||
| Question | 5090 (host c; PC 1 for the lock) | 4090 | H100 | Label |
|
||||
|---|---|---|---|---|
|
||||
| Fewest SMs that hold 99 percent of class v3 | 43 of 170 (99.4); 28 holds 98.4 | 16 of 128 (99.0); the tune's 26 at 99.3 | 127 of 132 (the rate falls with the SM count below it) | measured |
|
||||
| Watts at that point against base | 291.1 against 296.4 (-5 W); host e 337.4 against 352.7 (-15 W) | 249.6 against 253.4 (-4 W) | 441.6 against 450.4 (-9 W) | measured |
|
||||
| Energy per hash at that point | 2.053 against 2.079 (-1.3 percent); host e -3.9 percent | 3.568 against 3.588 (-0.6); the tune's 3.567 | 1.744 against 1.769 (-1.4) | measured |
|
||||
| Fewer warps per SM at full SM count | 8 warps per SM 2.063 (-0.8 percent); 2 warps 97.7 percent of rate at 2.072; 1 warp 84 percent | 8 warps 3.570; 1 warp 94 percent at 3.662 | 8 warps 99.2 percent at the same draw; 4 warps 93 percent; 1 warp 41 percent | measured |
|
||||
| Matched warps, packed against spread (43 x 32 against 170 x 8) | 2.053 against 2.063: equal | 32 x 8 (3.546) against the ladder's 256 warps (3.568): equal | 33 x 8 against 33 x 32: 123.5 against 123.4 MH/s, equal | measured |
|
||||
| Class v4 knee by rate | 43 SMs (PC 1: 98 percent; 36 SMs 88; 28 SMs 69) | the full grid (all shapes within 0.1 percent; the ladder below 128 not run on class v4) | the shipped grid; the persistent shape loses 4 percent | measured |
|
||||
| Class v4 watts at its knee | PC 1 470 W against 452.8 base (heat drift 62 to 75 C across the pass); host c capped at 450 | 356 against 353.5 | capped at 700 | measured |
|
||||
| At the 1,300 MHz lock (PC 1, this file's job) | class v4: the full grid 134.2 MH/s at 302 W = 2.25 microjoules; the persistent shape on every SM 128.9 at 298.1 (the wrapper costs 4 percent); 128 SMs 130.3 at 299; 96 SMs 120.9 at 286; 85 SMs 125.2 at 292.8; 64 SMs 95.2 at 249.9; 43 SMs 64.3 at 208.5 (3.24 microjoules): nothing under the full grid's energy at any point. Class v3: the full grid 134.1 at 213 W = 1.589; 128 SMs 132.6 at 213.6; 96 SMs 120.8 at 205.2; 85 SMs 111.0 at 191.9; 64 SMs 103.4 at 187.2; 52 SMs 85.6 at 174.3; the rows under 52 SMs were cut by the job's 66-minute budget | no lock on a rented host | no lock on a rented host | measured; the class v3 lock ladder is PARTIAL and disagrees with 20.3b (85 SMs 129.8 MH/s at 207.8 W there against 111.0 at 191.9 here, 43 SMs 126.3 there, not reached here): the pass ran across the founder's card swap on PC 1 (14:2x UK) and is re-run before it replaces 20.3b's lock points |
|
||||
| Class v3 at stock on PC 1 (this file's job, the same board as 20.3b) | base 136.7 at 320.9 W = 2.347; 43 SMs 136.5 at 310.6 = 2.276 (3.0 percent under); 28 SMs 135.2 at 308.6; 21 SMs 132.8 at 305.6; 16 SMs 125.5 at 294.4; 11 SMs 100.1 at 275.1; 170 SMs x 8 warps 137.1 at 310.8 = 2.266 (3.5 percent under, the best shape on PC 1) | | | measured; agrees with the rented ladder within 1 percent at every rung |
|
||||
| Class v5 against class v4 | not run on the rented 5090s (the v5-kits worker has no sparse shapes); PC 1's v5lock-b of 8 October: v5 at the knee = v4 rate, +2 percent watts | not run | 250.2 MH/s at 698.8 W against 251.9 at 699.4: equal under the cap | measured |
|
||||
| The memory-clock knob on a rented host | refused (-lmc, -lgc, -pl all refused on Vast) | refused (RunPod) | -lmc accepted and the clock did not move (2,619 MHz on the row at both settings) | measured |
|
||||
|
||||
Two oddities the ladder shows and this file keeps: on host c the 96-SM and 52-SM shapes read 97.4 and 95.4 percent of
|
||||
the rate where 85 and 43 read 99.9 and 99.4 (and host e's 52 reads 95.7, host d's 96 reads 97.5), so the dip is the
|
||||
shape, not the host; the 5090's 170 SMs sit in 8 GPCs, and a block count that lands unevenly across them (96 and 52
|
||||
against 85 and 43) leaves some GPCs' L2 ports hotter than others. The persistent wrapper's own cost is visible on the
|
||||
H100 only (sp132-w32 at 96 percent of class v4's base against 99.7 on class v3: a compute-bound kernel pays the loop).
|
||||
|
||||
## 3. The decomposition: what the residual is
|
||||
|
||||
The microbench's probes run one kernel each at full residency for 30 s with the sampler beside it: `sleep` (every warp
|
||||
resident, no instruction issued: the SM clock domain awake and nothing else), `l2_chase_32m` (a dependent 4-byte chain
|
||||
inside the L2: the L2 and crossbar path, no DRAM), `dram_chase_1g` (the hash's own pattern over 1 GiB: the DRAM path
|
||||
through the same L2 and crossbar), `int_arx` (the ALU path). The table in section 1 carries the rows; the reading per
|
||||
card, watts over idle:
|
||||
|
||||
| Card | Idle | SM clock domain awake (sleep minus idle) | The L2 path per dependent read (l2 chase minus sleep, over reads per s) | The DRAM path per dependent read (dram chase minus sleep, over reads per s) | The hash's draw over idle at its read rate | Of which the memory path (hash minus sleep) | The memory's own, modelled (chip-model 5.3) | Label |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| RTX 5090, PC 1 (15.1a, 8 October, the microbench at stock) | 74.7 W | 45 W (120.2 at full residency) | 2.4 nJ (378.7 W at 107.6 G reads per s) | 10.9 nJ whole-card (318.4 W at 18.2 G); 8.5 nJ over the L2 path | 311 - 75 = 236 W at 17.6 G reads per s = 13.4 nJ per read (20.3's mx8 row) | 191 W = 10.9 nJ per read | 2.0 nJ per read plus 20 W static = 55 W, 3.1 nJ per read | measured; the memory's own modelled |
|
||||
| RTX 5090, rented host e (this file's probes) | 8.7 W at 180 MHz | 130 W (139.0 W at 2,932 MHz) | 2.6 nJ (415.1 W at 104.5 G reads per s) | 11.5 nJ over the sleep row (341.3 W at 17.6 G); 18.9 nJ whole-card | 352.7 - 8.7 = 344 W at 18.2 G reads per s = 18.9 nJ per read | 214 W = 11.8 nJ per read | the same 55 W | measured; modelled |
|
||||
| RTX 4090, rented | 33.9 W at 210 MHz | 99 W (133.1 W at 2,726 MHz with every warp resident and nothing issued) | 2.4 nJ (329.0 W at 80.8 G reads per s) | 13.8 nJ over the sleep row (263.0 W at 9.4 G); 24.4 nJ whole-card | 253.4 - 33.9 = 219 W at 9.04 G reads per s = 24.2 nJ per read | 120 W = 13.3 nJ per read | 2.0 nJ plus 15 W static = 33 W, 3.7 nJ per read (approximate for GDDR6X) | measured; modelled |
|
||||
| H100 80 GB HBM3, rented | 71.8 to 80 W at 345 MHz | 50 to 58 W (130.3 to 136.6 W at 1,980 MHz) | 2.9 nJ (479.1 W at 121.0 G reads per s) | 10.3 nJ over the sleep row (466.5 W at 32.6 G); 12.1 nJ whole-card | 450.4 - 71.3 = 379 W at 32.6 G reads per s = 11.6 nJ per read | 320 W = 9.8 nJ per read | 1.2 nJ plus about 20 W static = 59 W, 1.8 nJ per read | measured; modelled |
|
||||
|
||||
The reading: at the hash, each card's draw splits into a fixed "awake" floor (the card with every warp resident and
|
||||
nothing issuing: 120 to 139 W on the 5090 whatever its idle, 133 W on the 4090, 130 to 137 W on the H100; the SM clock
|
||||
tree, the GPCs' shared logic and the memory clock domain at their boost clocks; the SM-sparse rows say the SMs' own
|
||||
share of it is 4 to 15 W, because emptying three quarters of them saves that much) and a per-read term on the memory
|
||||
path (10.9 to 11.8 nJ per dependent read on the 5090 on two boards, 13.3 on the 4090, 9.8 on the H100) of which the
|
||||
memory devices themselves are 2.0 or 1.2 nJ modelled; the difference (8 to 11 nJ per read) is the L2 slices, the
|
||||
crossbar, the memory controllers and the PHY, in that order of distance from the SM, and the L2 path alone is 2.4 to
|
||||
2.9 nJ of it (the L2 chase). The ALU path is not in the hash's draw at all (512 ops per hash at 11 to 14 pJ is under
|
||||
1 W). The two 5090 boards agree on the per-read term within 8 percent and differ by 60 W on the idle (7 to 9 W on the
|
||||
rented Linux boards against 75 W on PC 1 with its display), which is where the across-host base spread comes from.
|
||||
|
||||
What the SM-sparse rows add to it: the SMs that go idle at the knee were worth 4 to 15 W on every card (the difference
|
||||
between the sleeping SM clock domain at full residency and the same domain with three quarters of its SMs empty), so
|
||||
the "SM clock domain awake" term is mostly the clock tree and the GPCs' shared logic, not the SMs' own schedulers and
|
||||
register files. The rest of the draw over idle at the hash is the memory path on the GPU side: the L2 slices, the
|
||||
crossbar, the memory controllers and the GDDR7 or HBM PHY, at the boost clock for the first three and at the memory
|
||||
clock for the PHY. The core lock takes the first three down with the clock (the 5090's 311 to 213 W at the same 17.5 G
|
||||
reads per second on PC 1, 20.3); the memory-clock lock is the only knob on the fourth, and no rented host allows it, so
|
||||
its row is PC 1's (the memory-clock ladder table in section 1, the job `run-ca4-pc1-memclk-5090-20261008`).
|
||||
|
||||
### 3.1 The memory-clock ladder (PC 1, the 5090 alone, job `run-ca4-pc1-memclk-5090-20261008-b`, 15:0x to 15:4x UK, the helper's `lmc` verb at the 1,300 MHz core lock; the amendment of 15:50 UK)
|
||||
|
||||
The driver holds the 5090's memory clock at two points only: every ask at or above 8,001 MHz reads 13,801 on the row
|
||||
and every ask at or below 6,001 reads 7,001 (the GDDR7 PHY's half-rate state). The rows, both bit-exact against the
|
||||
Mac's fingerprints:
|
||||
|
||||
| Pack | Core lock, memory ask | Memory MHz on the row | MH/s | Watts | Microjoules per hash | nJ per dependent read, whole card | Label |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| class v3 (mx8) | 1,300, no memory lock | 13,801 | 134.09 | 222.9 | 1.662 | 13.0 | measured |
|
||||
| class v3 | 1,300, lmc 14001 / 12001 / 10001 / 8001 | 13,801 | 134.3 to 134.5 | 222.7 to 223.7 | 1.658 to 1.666 | 13.0 | measured (no change) |
|
||||
| class v3 | 1,300, lmc 6001 / 5001 / 3001 | 7,001 | 76.0 | 140.9 to 142.2 | 1.855 to 1.871 | 14.5 | measured |
|
||||
| class v4 (sh256x27) | 1,300, no memory lock | 13,801 | 134.34 | 309.0 | 2.300 | | measured |
|
||||
| class v4 | 1,300, lmc 8001 and above | 13,801 | 134.5 | 308.1 to 308.9 | 2.290 to 2.297 | | measured |
|
||||
| class v4 | 1,300, lmc 6001 and below | 7,001 | 76.1 | 183.8 to 187.0 | 2.416 to 2.458 | | measured |
|
||||
| both, unlocked (the pass's own base) | none | 13,801 | 136.3 / 136.6 | 318.6 to 328.1 / 452.5 to 464.3 | 2.34 to 2.41 / 3.31 to 3.40 | | measured (the heat drift across the pass) |
|
||||
|
||||
The reading: the memory clock is not a lever either. At the half-rate PHY state the rate falls 43 percent (134 to 76
|
||||
MH/s: the hash is bound by the memory's activate ceiling, which scales with the memory clock) and the watts fall 37
|
||||
percent (82 W on class v3, 125 W on class v4), so the energy per hash RISES 12 percent on class v3 (1.66 to 1.86) and
|
||||
5 percent on class v4 (2.30 to 2.42). Read as a decomposition, the 82 W between the two memory states on class v3 is
|
||||
the memory path's clock-scaled share at 7.4 G reads per second of rate lost: 11 nJ per read at the margin, the same
|
||||
figure the DRAM chase probes give for the whole memory path on the GPU side (10.9 to 11.8 nJ). So the memory-side term
|
||||
of the 5090's draw is about 80 to 90 W of its 223 W at the lock (the PHY, controllers and L2 at the memory clock plus
|
||||
the devices' own 55 W modelled), and it is bought one for one with the rate: the honest floor at the 1,300 lock stays
|
||||
1.66 microjoules on this board (1.58 on 20.3b's cooler pass), and no clock or occupancy knob on the card takes it lower
|
||||
without taking the rate with it. The class v4 premium at the half-rate state is 43 W for 76 MH/s x 102,100 ops = 5.5
|
||||
pJ per counted op (6.3 at the full memory clock on the same pass): the shadow's marginal is the core domain's, not the
|
||||
memory's, as expected.
|
||||
|
||||
## 4. The honest floor per tier, and the fraction a chip cannot strip
|
||||
|
||||
The chip of the record pays the memory's own energy per read, its static power and a controller and PHY of its own
|
||||
(chip-model-v3 5.3 to 5.5: GDDR7 2.0 nJ per read plus 20 W static plus a 15 W controller at 166 MH/s = 0.466
|
||||
microjoules per hash; one HBM3 stack 0.321). The honest card pays the same memory's energy (the devices are the same
|
||||
silicon on both sides) plus everything its die spends around the read. So the fraction of the card's energy per hash a
|
||||
chip cannot strip is the memory side's own share of it, and the floor's real number per tier is that share:
|
||||
|
||||
| Tier (class v3, stock unless named) | Card microjoules per hash | The memory side's own (modelled: the DRAM's 2.0 or 1.2 nJ x 128 reads, plus static over the rate) | Fraction a chip cannot strip | Edge at zero shadow: GDDR7 board / HBM3 stack / SRAM die | Label |
|
||||
|---|---|---|---|---|---|
|
||||
| RTX 5090, rented host c (the best board of four) | 2.079 | 0.256 + 0.140 = 0.40 | 19 percent | 4.5x / 6.5x / 14.9x | measured card; modelled memory |
|
||||
| RTX 5090, rented host e (the worst of four) | 2.485 | 0.40 | 16 percent | 5.3x / 7.7x / 17.8x | the same |
|
||||
| RTX 5090, PC 1 at the 1,300 MHz lock (20.3b, class v3 base) | 1.58 | 0.40 | 25 percent | 3.4x / 4.9x / 11.3x | measured |
|
||||
| RTX 4090, rented (GDDR6X at 9.0 G reads per s) | 3.588 | 0.256 + 0.21 = 0.47 | 13 percent | 7.7x / 11.2x / 25.6x | measured card; modelled memory |
|
||||
| H100 80 GB, rented (HBM3, 5 stacks) | 1.769 | 0.154 + 0.08 = 0.23 | 13 percent | 3.8x / 5.5x / 12.6x | the same |
|
||||
| Apple M5 Max (6 October, GPU plus DRAM channels) | 0.78 | about 0.3 (approximate: LPDDR at 1.5 to 2.0 nJ) | about 40 percent | 1.7x / 2.4x / 5.6x | measured card; approximate memory |
|
||||
|
||||
The edge columns above are against the model's three chips; the 5090 host c row is the honest 5090 floor at stock
|
||||
this lane measured (4.5x against the GDDR7 board, not the 5.2x of the 7 October PC 2 row: the rented board draws 296 W
|
||||
at 142.6 MH/s where PC 2's drew 330 at 136.6, and PC 1's 311 at 137.5; the three are one card model on three boards and
|
||||
drivers, and the honest denominator is a band, as the denominator lane's file says). The lock row (PC 1, 20.3b) stays
|
||||
the honest 5090 floor: 3.4x on class v3 at zero shadow, 3.6x at the knee on the efficiency pass's 1.67; with class v4's
|
||||
shadow and the record's k band it is section 20.4's 2.1x at k = 1.
|
||||
|
||||
Per tier, what it means and what is done: a home miner on one 5090, 4090 or any NVIDIA card gains 1 to 4 percent of
|
||||
energy per hash from the occupancy at stock and never more, so nothing in the occupancy changes what the card earns;
|
||||
the core lock stays the lever (the 5090: 1.67 against 2.26 microjoules, the Ember knob of 0.3.24), and the memory-clock
|
||||
lock behind it is measured on PC 1 in this file; the self-tune ships as the safe default inside Ember's Efficiency tier
|
||||
and costs nothing at Maximum. An H100 owner (a rented or data-centre tier) gets the 1.4 percent from the self-tune and
|
||||
nothing from its memory clock (the knob is accepted and ignored on the rented host). A 4090 owner gets 1.3 percent. An
|
||||
AMD or Apple owner gets nothing from this lane (the shapes are CUDA's; Metal and OpenCL workers run the shipped grid).
|
||||
|
||||
## 5. The patch: `--sm-sparse auto` and the self-tune
|
||||
|
||||
`proto-cuda/nvrtc/worker.cpp` on this branch:
|
||||
|
||||
- `--sm-sparse auto|off|N` and `--sm-hold <f>` (default 0.99); the tuning file's per-card `"sm_sparse": "auto" |
|
||||
"off" | "<N>"` and `"sm_hold": 0.99` (the manifest's tuning object is written as is to `tuning.json`, so a fleet
|
||||
default needs no app change; the flag wins over the file). A count pins `sp<N>-w32` through the existing pinned race
|
||||
(the vector warps gate the install); `auto` runs `tuneSparse` at the end of `buildPair`, on the first pack and on
|
||||
every prepared pack, so a pack flip re-measures.
|
||||
- `tuneSparse`: the persistent shape is one compiled kernel for every block count (N is the launch's grid, not the
|
||||
text), so one NVRTC compile serves the whole search; the installed kernel's rate is the ceiling's first reading, the
|
||||
sparse shape on every SM its second (the vector warps through it first: bit-exact or nothing is installed); the ladder
|
||||
(`sparseLadder`: the SM count by quarters and eighths down to 2) descends until a shape falls under the hold and a
|
||||
bisection of at most four probes closes the bracket; `chooseSparseBlocks` installs the fewest blocks that held. One
|
||||
line goes out with the race line (`sparse tune device ... rows N=mhs ... chosen sp<N>-w32 (x percent of the ceiling)`)
|
||||
and the bench's RESULT line carries `variant=` and `sparse_blocks=`.
|
||||
- Measured: H100 15.2 s, 6 probes, `chosen sp127-w32 253.179 (99.5% of the ceiling)`, the served row 253.2 MH/s at
|
||||
441.6 W (1.744 against 1.769 microjoules); class v4 on the H100 `chosen none` in 6.6 s (the persistent shape at 97.8
|
||||
percent of the ceiling, under the hold); RTX 4090 24.5 s, `chosen sp26-w32 (99.3%)`, 70.15 MH/s at 250.2 W (3.567
|
||||
against 3.588); RTX 5090 host c, class v3 at the 0.99 hold `chosen sp37-w32`, 141.27 MH/s at 286.5 W = 2.028
|
||||
against the base's 2.079 (2.5 percent), at 0.98 `sp27-w32` 2.033; class v4 at the 0.99 hold `sp82-w32` 3.178
|
||||
against 3.157 (nothing, inside noise under the board's 450 W cap), at 0.98 `sp71-w32` 3.185. The 0.99 hold is the
|
||||
default: on class v4 it costs 1 percent of rate for no watts, which is why Maximum turns it off and Balanced holds
|
||||
0.995.
|
||||
- Tests: `proto-cuda/nvrtc/emu/variant-test.cpp` case `sparse-tune` (the ladder from 170 SMs, the chooser on the 20.3b
|
||||
rows at holds 0.99, 0.98, 0.5 and 1.0, the mode reader on nine strings, the tuning keys for two cards): PASS beside
|
||||
the three earlier cases, built with the real headers on the H100 pod (`g++ -DIGNEUM_EMU`), 8 October 12:04 UTC. The
|
||||
known-failed shape it guards: a hold of 1.0 or an empty row set must choose nothing (the 7 October fault was a shape
|
||||
installed that was never measured).
|
||||
- The tiers (the Ember tiers lane's `ember-tiers-25` carries the tier switch; this file names the mapping): Efficiency
|
||||
writes `sm_sparse: auto, sm_hold: 0.985`; Balanced `auto, 0.995`; Maximum `off`. The tune runs inside the pair's build
|
||||
(the prepare thread, ahead of the epoch), so a flip of tier or pack re-measures at the next prepare and the serve loop
|
||||
never stalls; its cost is 7 to 25 s of the prepare lead (600 DAA).
|
||||
- What it is not: a lever on the floor. It is a 1 to 4 percent per-watt line on NVIDIA at stock, worth shipping
|
||||
because it is free and measured, and the frame (one compile, a ladder, a hold, bit-exact or nothing) is the shape any
|
||||
later occupancy or clock self-tune takes.
|
||||
|
||||
## 6. Unverified and owed
|
||||
|
||||
- PC 1's 1,300 MHz lock ladder is in section 1 as the job left it: class v4 complete (every point under the full
|
||||
grid's energy, the shadow compute-bound under 128 SMs at 1,290 MHz), class v3 cut at 52 SMs by the job's budget and
|
||||
reading the sparse shapes lower than 20.3b did on the same board at the same lock (85 SMs 111.0 against 129.8 MH/s);
|
||||
the pass ran across the founder's card swap on PC 1 (14:2x UK), so the class v3 lock row stays 20.3b's (43 SMs
|
||||
126.3 MH/s at 205.4 W against the full grid's 134.0 at 211.4: 1.63 against 1.58 microjoules, nothing saved) and the
|
||||
ladder is re-run on a quiet PC 1 before it replaces it. The memory-clock ladder landed at 15:4x UK (section 3.1).
|
||||
- The memory side's own share per tier is modelled (chip-model 5.3); the memory-clock ladder is the one measurement of
|
||||
its PHY term, and only on PC 1.
|
||||
- Class v5 on the rented 5090s was not run (the v5-kits worker carries the leaf upload and not the sparse shapes; the
|
||||
two trees merge in 0.3.26); the H100 row and PC 1's v5lock-b stand in.
|
||||
- The 4090's class v4 ladder below 128 SMs was not run (the slice pods ran class v3 and the full-grid class v4 rows).
|
||||
- Every chip figure is the model's; no chip has been measured.
|
||||
- Spend: six pods, USD 25 of the lane's 300 (h100 3.49 per hour, 4090 0.74, four 5090s 0.44 to 0.56; two 5090 hosts
|
||||
destroyed unused: one shared with another tenant's 5.7 GB process at 485 W idle, one with a dead CUDA init; the
|
||||
Korean host vanished mid-run).
|
||||
436
docs/analysis/class-v6/floor/sram-and-floor.md
Normal file
436
docs/analysis/class-v6/floor/sram-and-floor.md
Normal file
|
|
@ -0,0 +1,436 @@
|
|||
# Floor lane 3: the SRAM full-store die and the dataset floor
|
||||
|
||||
8 October 2026, 12:5x to 13:2x UK (the close of the floor section at 15:45 UK by the founder's rule of 13:3x; later rows land as amendments under section 10 with their own time), branch `class-v6-floor-sram` from `counter-asic-4`. One of the five floor lanes of
|
||||
section 10 of `docs/design/class-v6-rotating-family.md`; the rows go to the Counter ASIC 4.0 research lane for the
|
||||
15:45 UK close. Nothing here touches the devnet, the testnet object or any served number. Every figure carries a
|
||||
label: **measured** (a card on a named job in this repository), **claimed** (a vendor's or a paper's figure, with the
|
||||
record's source), **modelled** (arithmetic on claimed or measured figures by the method of `docs/analysis/chip-model-v3.md`
|
||||
section 5), **approximate** (an estimate; the sensitivity is given). No chip has been measured. The arithmetic is
|
||||
`scratchpad/sram_floor.py`, run on build-3 (nothing was run on the Mac).
|
||||
|
||||
The question: the strongest five-year chip in the record is a 2 GiB SRAM full store on one reticle of merchant N2
|
||||
(lane B, `docs/analysis/class-v6/hardware-future.md` 4.9 and 5; chip-model-v3 5.12): 1.0 nJ per 64-byte read (0.5 to
|
||||
2.0), about 2,100 MH/s per die at 300 W, 13x to 17x the RTX 5090 per joule at zero shadow, about 3x with the shadow at
|
||||
`k = 0.5` against the M5 Max, USD 400 to 600 of silicon per 2 GiB. None of the four v6 layers reaches it; layer 2's
|
||||
floor moves its capex, not its joules. This file finds what does, prices it on both sides, and says which lever, which
|
||||
schedule and which served claim.
|
||||
|
||||
## 0. The findings, in one page
|
||||
|
||||
1. **The record's SRAM read energy is the 64-byte figure, and the chip reads what the hash asks for.** The wire term
|
||||
(1.3 pJ per bit across a 24 mm die, approximate) scales with the bits moved, and the macro's own access (about 0.1 nJ,
|
||||
a wordline sensed) does not. At today's read width (`W = 1`, 4 bytes) the die moves about 80 bits per dependent read
|
||||
(address, control, data), not 560: **0.25 nJ per read, 8.3 GH/s per die, 66x the 5090 at zero shadow** (modelled on
|
||||
the record's own wire figure). The record's 17x is the `W = 16` row. With the class v4 shadow at `k = 0.5` the same
|
||||
chip reads 5.7x (the record's 4.8x), at `k = 1` 3.0x. **On the k lane's synthesised core (13:1x UK, section 2.2b:
|
||||
about 0.11 microjoules of shadow per hash at N3, absolute `k` about 0.1, the shuffle row open) the same die reads
|
||||
21x at W = 4 and 18x at W = 8 under class v4, 10x and 9.7x at the honest cards' whole latency shadow, 6x to 7x at
|
||||
the 5090's knee under the full shadow.** On the record's claimed band the shadow is the whole hold and the width
|
||||
moves the shadowed number 0.2x to 0.7x; on the synthesised core the memory is a third to a half of the die's energy
|
||||
and the width is worth 1.3x. The public 2x is not reachable against this die either way.
|
||||
2. **The one lever on the wire is the read width, and it is free on the honest cards up to 16 bytes.** `W = 4` words
|
||||
(16 bytes) is measured within 2.7 percent on the 5090 and the 9070 XT and within 1 percent on the M5 Max (the
|
||||
record, 5 October); it moves the die to 176 bits per read, 0.38 nJ, **44x at zero shadow, 5.6x at k = 0.5**. `W = 8`
|
||||
(32 bytes, exactly the GDDR7 sector the 5090 already fetches) has no row of its own but is free by the measured
|
||||
rows: the 5090's ceiling is about 18 G sectors a second (w16 moved 573 GB/s of sectors at 139.8 MH/s, w64 589 GB/s
|
||||
at 71.9, read-width.md), an aligned 32-byte read is one sector per load exactly as w16 is, AMD moves its 64-byte line
|
||||
either way, and the M5 Max read 1.03 of its v2 rate at both w16 and w64; it reads 0.55 nJ, **31x and 5.4x**, and one
|
||||
PC 1 row confirms it. `W = 16` costs the 5090 47 percent of its rate (two sectors per load, measured) and is dead. The fold is keyed by the destination
|
||||
register through its first word (`verify::fold_words`), so a chip cannot pre-fold a wide read into a narrower table:
|
||||
the raw words must cross the die. A chain of 2, 4 or 8 dependent atoms per step moves nothing: the ratio is per
|
||||
read on both sides. Banking plus a sequencer cannot localise: the chain alternates the 16 load sites and their
|
||||
windows, so no lane placement follows a window, and moving the lane to the data costs about 290 bits, which caps
|
||||
the wire lever at about 300 bits per read (`W = 8` is at that cap). The per-site window shrink (a half or a quarter
|
||||
of the dataset, era-layout 1.4) buys the SRAM die nothing and the DRAM chip nothing; its floor at 256 MiB is the
|
||||
cache's lever and stands.
|
||||
3. **The dataset floor is a ticket lever, not a per-joule or a per-MH/s lever.** SRAM cost per GiB is flat to 2031
|
||||
(about USD 250 per GiB: density gains 6 to 11 percent per node, wafers dearer per node, claimed), so USD 5,000 of
|
||||
silicon per store is 20 GiB in 2026 and about 21 GiB in 2031, which retires every card under 32 GB and every Mac
|
||||
under 64 GB. The hop between dies costs 0.04 to 0.15 nJ at the honest widths (UCIe 0.5 pJ per bit, claimed), so a
|
||||
ten-die store still reads 25x to 58x at zero shadow and 5.2x to 5.7x with the shadow; and because every die is
|
||||
powered, the store's USD per MH/s does not rise with the dataset (USD 0.25 to 0.5 at every size). The floor
|
||||
raises the minimum unit from USD 500 to USD 5,000, which is 2.5 times a 5090's street price. And the floor has a
|
||||
cost on the honest side that the record read as zero: the 5090 at its knee pays 4, 8 and 10 percent more energy
|
||||
per hash at 2, 4 and 8 GiB than at 1 GiB (measured, PC 1, 12:58 UK; 1 to 4 percent unlocked), so each step moves
|
||||
the honest denominator toward the chip by the same share. **The schedule that
|
||||
keeps the most honest cards is lane A's: 5.5 GiB at the v6 epoch (3 reticles, USD 1,500), 8.5 GiB two years on (5
|
||||
reticles, USD 2,500, the last size inside the 12 GB tier's room), 11.5 GiB at four years (6 reticles, USD 3,000,
|
||||
the last inside the 16 GB tier); each step sits just under a card tier's room and just over a reticle multiple.**
|
||||
It never reaches USD 5,000 without retiring the 24 GB tier; this file does not recommend that.
|
||||
4. **The capex wall, recomputed on the spec's emission** (3,168,808,781 base units per DAA second, 10^8 base units
|
||||
per IGN, a 30-day ramp, halving every 63,115,200 s, 80 percent to miners: 0.77 B IGN to miners in year 1, 0.80 in
|
||||
year 2, 0.40 in years 3 and 4, 0.20 in 5 and 6), class rotation at 0 cost to this chip (firmware), the floor as a
|
||||
fleet term that drops out: an N2 project of USD 100 M to 500 M that starts at launch, ships at 24 months and mines
|
||||
years 3 to 6 at a 30 percent share pays only above **USD 0.43 to 2.9 per IGN** (launch-year miner revenue USD 330
|
||||
M to 2.3 B, USD 0.9 M to 6.2 M a day); a maker who takes the whole chain pays above **USD 0.13 to 0.9** (USD 100
|
||||
M to 680 M a year, USD 0.27 M to 1.9 M a day). Below about USD 0.13 per IGN (USD 100 M a year of miner revenue) no
|
||||
rational SRAM project starts at any edge; above about USD 2.9 (USD 2.3 B a year) every one does. At USD 0.01, 0.10
|
||||
and 1.00 per IGN the NPV of a USD 150 M project at 5x and a 30 percent share is -148 M, -130 M and +54 M.
|
||||
5. **Nothing else reaches the die.** A per-era or per-epoch re-fill costs the die 0.27 mW; per block 0.96 W, 0.3
|
||||
percent, against the GPU's 1.3 to 3.2 percent of rate (dead, the invention lane's 2.15 again). A read straddling
|
||||
two atoms costs the die 0.02 nJ at `W = 4` and the verifier 19 percent more items (dead on the verifier). A second
|
||||
hot table is the die's own kind of memory (`k` 0.1 to 0.3 for the GPU's L2 hit, the record) and costs the card 2.4
|
||||
nJ per hit (dead). What is new and kept: the lane-migration bound (keep the per-lane scratch resident so the lane
|
||||
stays pinned and the data must travel), and the note that 3D-stacked SRAM (SoIC-class, a vertical hop at about 0.1
|
||||
pJ per bit, approximate) would make a multi-die store read as one die, which weakens the floor on joules further.
|
||||
|
||||
The honest line: against this die the memory lever is worth a factor of two at zero shadow and 1.3x with the shadow
|
||||
on at a synthesised core (a tenth on the record's claimed band); the floor is a ticket; the project cost against the
|
||||
chain's revenue is the wall; and the number the public text can carry is a range, 6x to 10x at the honest cards'
|
||||
whole latency shadow on the synthesised core (2x to 4x on the record's claimed band, marked), never "under 2x".
|
||||
|
||||
## 1. Inputs
|
||||
|
||||
| Quantity | Value | Label | Source |
|
||||
|---|---|---|---|
|
||||
| The 5090 at class v3 | 136.1 MH/s at 326 W, 2.40 microjoules per hash all-in, 2.26 the identity's `E_card`; 128 dependent reads per hash | measured | chip-model-v3 5.1; the research file section 2 |
|
||||
| The 5090's class v4 premium `F` | 1.10 microjoules unlocked (145 W); the whole latency shadow about 2.0 (about 330,000 ops, 575 W) | measured / approximate | the research file 15.1a; lane B section 7 |
|
||||
| The M5 Max | 0.78 microjoules at class v3, 1.40 at class v4 (GPU and DRAM channels) | measured, 6 October | latency-shadow-2026-10-06 section 3 |
|
||||
| Read width rows | w4 139.8 against 136.1 MH/s on the 5090; 17.90 against 18.15 on the 9070 XT; within 1 percent on the M5 Max; w64 71.9 MH/s on the 5090 (bandwidth-bound) | measured, 5 October | the design document section 2 (read width); read-width.md |
|
||||
| N2 SRAM density | 38 Mb/mm^2 HD, +11 percent over N3E; 2 GiB about 452 mm^2 of macro; a 550 to 650 mm^2 die; USD 400 to 600 of silicon at a USD 30,000 wafer | claimed (IEEE Spectrum, 12 December 2024) / approximate | lane B 4.9 |
|
||||
| The wire | 1.3 pJ per bit across a 24 mm die (0.6 at 128 mm^2 scaled by the side) | approximate; the band 0.5 to 2.0 nJ per 64 B is this term | lane B 4.9, 8 |
|
||||
| The macro access | about 0.1 nJ per access, width-independent above the column mux | approximate | lane B 4.9 (the 64 B figure), this file's reading |
|
||||
| The controller share | 0.05 nJ per read; 30 W static and sequencer per die at 300 W | approximate | lane B 4.9 |
|
||||
| The inter-die hop | UCIe 0.5 pJ per bit (0.25 to 0.5); the record's 0.27 nJ is 544 bits | claimed (UCIe via SNIA) | lane B 4.7 |
|
||||
| The load address | per site a window of the dataset, a half or a quarter (aligned, `k_off` in {0, 1, 2}), never under 2^26 words; `y = rotl(x * M, R)`; uniform on the window | designed | era-layout.md 1.3, 1.4 |
|
||||
| The fold of a wide read | `x = dst ^ w0; x = rotl(x, r) * M ^ w_i` for the rest: keyed by the destination register | code | `igneum-pow/src/verify.rs` `fold_words` |
|
||||
| The card room | 75 percent of card memory, 50 percent of Apple unified; the non-dataset working set 254 to 479 MiB best, 600 to 1,500 worst; the cache 512 MiB from 4 GiB, 1 GiB from 8, 2 GiB from 16, freed after the build in the best case | the standing rule / measured working set | card-lifetime-2026-10-05 section 3; the design document 3.4 |
|
||||
| The card population | by count of the bench table's 32 measured consumer cards: 6 GB 3 percent, 8 GB 22, 10 and 11 GB about 9, 12 GB 22, 16 GB 28, 24 GB and up 16; the fleet's hashrate-weighted census unread | approximate | lane A 4.2a via the design document 3.4 |
|
||||
| The Apple rate cost of the working set | -12 percent at 2 GiB, -20 at 4, -22 at 8 against 1 GiB on the M5 Max | measured, 10:40 UTC | the design document 3.3 |
|
||||
| The 5090's cost of the working set | against the 1 GiB control (137.65 MH/s at 312.2 W unlocked, 127.39 at 212.6 W at the 1,300 MHz lock): 2 GiB -2.8 percent unlocked and -4.9 at the lock, 4 GiB -3.8 and -11.2, 8 GiB -4.3 and -14.1; MH/W at the lock 0.599, 0.576, 0.553, 0.543 (4, 8 and 10 percent per hash in energy at the knee) | measured, 12:49 to 12:58 UK, PC 1, label ca4-v6 | the hash lane's message of 13:0x UK |
|
||||
| The emission | 3,168,808,781 base units per DAA second, 10^8 base units per IGN (1.0 B IGN a year in years 1 and 2), a 30-day ramp (about 37 M IGN never minted), halving every 63,115,200 DAA s, 80 percent to miners | spec | spec 05 (the economics table, section 2.5) |
|
||||
| The mission lane's break-even model | revenue `s x E2 x p`; cap = `C_proj / s` in years 1 to 2 at `s` = 0.30; its E2 of 4.18 B IGN is not this emission constant's figure (1.57 B to miners), so this file recomputes from the constant and carries both | model | mission/future.md 2.2 |
|
||||
| The re-fill | a 1 GiB rebuild 157 G ops, 13.4 ms on a 5090, 0.16 to 0.5 J on a chip core; class v5 refreshes per epoch (3,600 s) | measured / modelled | the invention lane 2.11, 2.15; the research file row 7 |
|
||||
|
||||
## 2. Lever 1: the wire energy
|
||||
|
||||
### 2.1 The model
|
||||
|
||||
A dependent read on the die moves an address (32 bits), control (about 16) and the data (32 `W` bits) across the
|
||||
global wire at 1.3 pJ per bit, pays the macro's access (0.1 nJ) and the sequencer's share (0.05 nJ):
|
||||
`E_read = 0.15 nJ + 1.3 pJ x (48 + 32 W)`. On `n` dies a read crosses a hop with probability `1 - 1/n` at 0.5 pJ per bit
|
||||
on the same bits. The rate is power-bound: 270 W over `128 x E_read`; energy per hash at zero shadow is 300 W over the
|
||||
rate. With the shadow: `edge = (2.26 + F) / (E_hash0 + k F)`. All modelled; the GPU side measured.
|
||||
|
||||
### 2.2 The read width, both sides
|
||||
|
||||
| `W` (words) | Bits per read | `E_read` nJ | GH/s per die | `E_hash` at zero shadow, microjoules | Edge over the 5090 at zero shadow | Over the M5 Max | Class v4 shadow, k 0.5 / 1 | Full shadow (F 2.0), k 0.5 / 1 | M5 Max class v4, k 0.5 | The honest cards' cost | Label |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| 1 (today, 4 B) | 80 | 0.25 | 8.3 | 0.036 | **66x** | 22x | 5.7x / 3.0x | 4.1x / 2.1x | 4.0x | 0 | modelled |
|
||||
| 4 (16 B) | 176 | 0.38 | 5.6 | 0.054 | **44x** | 14x | 5.6x / 2.9x | 4.0x / 2.1x | 3.9x | 5090 +2.7 percent rate, 9070 XT -1.4, M5 Max within 1 (measured) | modelled chip, measured cards |
|
||||
| 8 (32 B, the GDDR7 sector) | 304 | 0.55 | 3.9 | 0.078 | **31x** | 10x | 5.4x / 2.9x | 4.0x / 2.0x | 3.6x | free by the measured rows: one sector per load on NVIDIA as at w16 (the ceiling about 18 G sectors a second: 573 GB/s at w16, 589 at w64), one 64 B line on AMD, and the M5 Max at 1.03 of v2 at both w16 and w64; a PC 1 row confirms (the crate's width set is {1, 4, 16} words, so a pack at 8 needs a generator and emitter line first, the hash lane's caveat) | modelled chip; the card by interpolation of measured rows |
|
||||
| 16 (64 B, the record's atom) | 560 | 0.88 | 2.4 | 0.125 | 19x (the record's 17x at 1.0 nJ) | 6.2x | 5.0x / 2.7x | 3.8x / 2.0x | 3.2x | the 5090 -47 percent of rate (measured): dead | modelled |
|
||||
|
||||
Reading: the record's 1.0 nJ and 17x are the 64-byte row; at the hash's own width the die is 3.5x stronger at zero
|
||||
shadow (66x). With the shadow on, every row sits at 5.0x to 5.7x at `k = 0.5` and 2.7x to 3.0x at `k = 1`: the read
|
||||
energy is 3 to 13 percent of the chip's energy per hash once the shadow is paid, and the width moves the shadowed edge
|
||||
by 0.2x to 0.7x. The one honest width lever is `W = 4` now (measured free) and `W = 8` after one measurement; the
|
||||
public text's number against this die is the shadow's, not the memory's.
|
||||
|
||||
### 2.2a The same rows against the 5090 at its knee, in both conventions (absolute first)
|
||||
|
||||
At the 1,300 MHz lock the card pays 1.67 microjoules at class v3 and 0.65 for the class v4 shadow (measured, 6.5 pJ
|
||||
per counted op). Two conventions price the die's shadow core. **Absolute**: the core costs what it costs, `k` fixed
|
||||
against the stock 11.3 pJ, so the die pays `k x 1.10` microjoules whatever the card does; this is the physics and the
|
||||
headline. **The record's**: `k` against the card's op cost at the same operating point, so the die pays `k x 0.65`
|
||||
at the lock; it flatters the die at the lock by 1.6x and is carried marked. At stock the two coincide (section 2.2).
|
||||
The `k` lane's RTL rows re-fold these in absolute for the close (its sequencer-core row due 15:00).
|
||||
|
||||
| `W` | Absolute, k 0.5 / 1 | The record's convention (marked), k 0.5 / 1 | Label |
|
||||
|---|---|---|---|
|
||||
| 1 | 4.0x / 2.0x | 6.4x / 3.4x | modelled |
|
||||
| 4 | **3.8x / 2.0x** | 6.1x / 3.3x | modelled |
|
||||
| 8 | **3.7x / 2.0x** | 5.8x / 3.2x | modelled |
|
||||
| 16 | 3.4x / 1.9x | 5.2x / 3.0x | modelled |
|
||||
|
||||
### 2.2b The re-fold on the k lane's synthesised core (13:1x UK; floor lane 2, `class-v6-floor-k`)
|
||||
|
||||
Floor lane 2's first chip-side figures (ASAP7 routed RTL, Yosys and OpenROAD, a minimal 8-register lane; scaled to
|
||||
N3 by x0.50, approximate): ARX 1.05 to 1.09 pJ per op, mul and mulhi 0.68, mad 1.66, the index fold 1.19, prmt 0.64,
|
||||
lop3 0.72; the shuffle, the crossbar, the scratch and the int8 tile pending. The class v4 mix costs that core about 1.0
|
||||
to 1.2 pJ per op at N3, **0.11 microjoules per hash of shadow at 102,100 ops, independent of the card's operating
|
||||
point**; absolute `k` about 0.10 at stock and 0.18 at the lock (the record's claimed band was 0.3 to 0.8). An N2 core
|
||||
is one node further (x0.7, approximate): 0.08. At the honest cards' whole latency shadow (330,000 ops) the same core
|
||||
pays 0.36 (N3) or 0.25 (N2). The re-fold, `E_chip = E_hash0 + shadow`, against the 5090 at 3.36 (stock, class v4),
|
||||
2.32 (lock), 4.26 (stock, the full shadow), 2.67 (lock, full) and the M5 Max at 1.40 (class v4):
|
||||
|
||||
| `W` | Class v4 shadow, stock / lock, N3 core | The same, N2 core | Full shadow, stock / lock, N3 | The same, N2 | M5 Max class v4, N3 / N2 | Label |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | 23x / 16x | 29x / 20x | 11x / 6.7x | 15x / 9.3x | 9.6x / 12x | modelled on synthesised pJ |
|
||||
| 4 | **21x / 14x** | 25x / 17x | **10x / 6.5x** | 14x / 8.8x | 8.5x / 11x | the same |
|
||||
| 8 | **18x / 12x** | 21x / 15x | **9.7x / 6.1x** | 13x / 8.2x | 7.5x / 8.9x | the same |
|
||||
| 16 | 14x / 9.9x | 16x / 11x | 8.8x / 5.5x | 11x / 7.1x | 6.0x / 6.8x | the same |
|
||||
|
||||
Reading, which replaces "the shadow is the whole hold" of section 0 where the synthesised figure stands: at a
|
||||
measured-class `k` of 0.1 the shadow holds the die to 14x to 23x under class v4 and 6x to 11x at the honest cards'
|
||||
whole latency shadow, not 2x to 6x; the die's memory energy is then a third to a half of its energy per hash, and the
|
||||
read width is worth 1.3x (W = 1 to W = 8) on the shadowed number, not 0.2x. The public 2x is not reachable against
|
||||
this die by any shadow at a synthesised core; what holds it is the project cost against the chain's revenue (section
|
||||
4), which the shadowed edge moves by 1.4x across the whole band. The rows carry the k lane's labels (ASAP7 measured by
|
||||
synthesis, the node scaling approximate, the shuffle open); its own re-fold replaces this table when it lands.
|
||||
|
||||
Sensitivity: the wire at 0.65 pJ per bit halves every wire term (`W = 1` 0.20 nJ, 83x; `W = 8` 0.35 nJ, 49x); at 2.6
|
||||
pJ per bit doubles it (`W = 1` 0.35 nJ, 47x; `W = 8` 0.95 nJ, 18x). The macro at 0.2 nJ adds 0.1 nJ to every row. The
|
||||
shadowed rows move under 0.3x across the whole band.
|
||||
|
||||
Feasibility of the narrow-read rate (approximate): 8.3 GH/s per die is 1.06 T reads per second on the die, 85 Tb/s of
|
||||
on-die traffic (10.6 TB/s; a reticle die's mesh carries about 1/50 of a WSE-3's 21 PB/s), about 260 M reads per
|
||||
second per 4 Mbit macro at 4,096 macros, 21,000 lanes in flight at 20 ns, and 4.2 T sequencer ops per second at 3 to 5
|
||||
pJ (13 to 21 W, inside the 30 W static line). Nothing in it is a ceiling the DRAM rows have; the die is power-bound.
|
||||
|
||||
### 2.2c W = 16 (the whole 64-byte item) across the three chips, for the close (the coordinator's order, 13:2x UK)
|
||||
|
||||
Floor lane 5 ranks W = 16 first if the 5090 passes a PC 1 job today at its best lane count. The row at each chip's own
|
||||
cost: GDDR7 +33 percent on the memory term (two sectors per load), HBM3 +24 percent, the die at this file's 560 bits
|
||||
(0.88 nJ). The card side has two readings: the 5090 passes within the 5 percent rule (its 2.40 microjoules stand), or
|
||||
the measured w64 row stands (71.9 MH/s, 47 percent of rate, 1.89x the energy per hash if the watts hold; read-width.md,
|
||||
5 October). Zero shadow unless named; the shadow rows on the k lane's synthesised core (2.2b).
|
||||
|
||||
| Chip | `E_hash` at W = 1 (edge) | `E_hash` at W = 16 | Edge if the 5090 passes | Edge at the measured w64 row | With the class v4 shadow (synthesised core) | At the full shadow | Label |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| GDDR7 board, 16 devices | 0.466 (5.1x) | 0.550 | 4.4x | 8.3x | 5.1x | 4.7x | modelled |
|
||||
| HBM3, one stack | 0.321 (7.5x) | 0.358 | 6.7x | 12.7x | 7.2x | 5.9x | modelled |
|
||||
| SRAM die, one N2 reticle | 0.036 (66x) | 0.125 | **19x** | 36x | 14x | 8.8x | modelled |
|
||||
|
||||
Reading: W = 16 is the one width that moves the die by more than 2x at zero shadow (66x to 19x) and it costs the DRAM
|
||||
chips 11 to 15 percent; it is worth that only if the 5090 passes, since at the measured w64 row every chip's edge
|
||||
nearly doubles instead. The decision is the PC 1 measurement, not this table; if it passes, W = 16 replaces W = 8 as
|
||||
this file's width recommendation and the die's shadowed rows read 14x (class v4) and 8.8x (full).
|
||||
|
||||
### 2.3 What cannot localise, checked
|
||||
|
||||
| Candidate | What the chip would do | Why it fails | Number |
|
||||
|---|---|---|---|
|
||||
| Banking plus a sequencer near the data | place each lane's sequencer where its next read lands | the next address is uniform on a window of at least 2^26 words (256 MiB; a quarter of the die at the smallest) and the 16 sites alternate every load, so no placement is right twice | 0 |
|
||||
| Lane migration (move the lane to the data instead of the data to the lane) | ship the 8 registers, the pc and the scratch pointer to the target bank | costs about 290 bits per read against 80 to 304 for the data; cheaper than the data only above `W = 8`, which is why the wire lever caps at about 300 bits (0.55 nJ) whatever the width; the per-lane scratch (variant 5) pins the lane if it stays resident | the cap: `E_read` under about 0.6 nJ on one die at any `W` |
|
||||
| The per-site window (a half or a quarter of the dataset) | map each window to one die of a multi-die store | the lane sits on one die and the window changes every load; a lane placed at random crosses with the same `1 - 1/n` whether or not the sites have windows | 0 for the SRAM die and the DRAM chip; the 256 MiB floor is the cache's lever and stands |
|
||||
| The index fold and the stride rotation | permute the address lines in firmware | an SRAM die has no layout to lose; the fold is an xor-rotate on the address path | 0 |
|
||||
| A 2-, 4- or 8-atom dependent chain per step | nothing: it reads more | both sides pay per read (the 5090's marginal 10.9 nJ per dependent read, measured; the die's 0.25 to 0.55): the ratio is unchanged at every chain length and the honest rate halves per doubling | 44.5x at every chain length at `W = 4` (modelled) |
|
||||
| A replicated store (the full dataset on every die) | zero hops | doubles the silicon to save 0.04 to 0.15 nJ per read: USD 500 per die for under 1 percent of the shadowed edge | not worth building |
|
||||
|
||||
### 2.4 The hop, by die count and width
|
||||
|
||||
| `W` | 1 die | 2 | 4 | 8 | 10 | 16 | Label |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 1: `E_read` nJ / edge at zero shadow / k 0.5 | 0.25 / 66x / 5.7x | 0.27 / 62x / 5.7x | 0.28 / 59x / 5.7x | 0.29 / 58x / 5.7x | 0.29 / 58x / 5.7x | 0.29 / 58x / 5.7x | modelled |
|
||||
| 4 | 0.38 / 44x / 5.6x | 0.42 / 40x / 5.5x | 0.44 / 38x / 5.5x | 0.46 / 37x / 5.5x | 0.46 / 37x / 5.5x | 0.46 / 37x / 5.5x | modelled |
|
||||
| 8 | 0.55 / 31x / 5.4x | 0.62 / 27x / 5.3x | 0.66 / 26x / 5.2x | 0.68 / 25x / 5.2x | 0.68 / 25x / 5.2x | 0.69 / 25x / 5.2x | modelled |
|
||||
|
||||
The hop saturates at `1 - 1/n` of 0.04 to 0.15 nJ; the floor cannot reach the die's joules at any size. The record's
|
||||
"13x at four dies" is the 64-byte row with the 0.27 nJ hop; at the honest widths the four-die store reads 26x to 59x.
|
||||
|
||||
## 3. Lever 2: the dataset floor, priced per tier
|
||||
|
||||
### 3.1 The SRAM cost curve (claimed inputs, approximate extrapolation)
|
||||
|
||||
| Year | Node | HD SRAM density, Mb/mm^2 | GiB per 452 mm^2 of macro | USD per reticle die | USD per GiB stored | Label |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 2026 to 2027 | N2 | 38 (claimed) | 2.0 | 500 (400 to 600) | 250 | lane B |
|
||||
| 2028 to 2029 | A16 class | about 41 (+8 percent; the record's 6 to 11 per node) | 2.16 | about 550 (wafers dearer per node) | 255 | approximate |
|
||||
| 2030 to 2031 | A14 class | about 44 | 2.33 | about 600 | 258 | approximate |
|
||||
|
||||
SRAM cost per GiB is flat across the window (the density gain and the wafer price cancel within the error). A store's
|
||||
silicon is about USD 250 per GiB of floor, quantised at the maker's die menu: one die design rounds up to the reticle
|
||||
(2.1 GiB costs two dies); a second, smaller die removes the rounding. The rows below carry both.
|
||||
|
||||
### 3.2 The floor against reticles and tiers
|
||||
|
||||
Room per tier: 75 percent of card memory (50 percent of Apple unified) minus the working set (254 MiB best, 479 worst)
|
||||
minus the cache when it is resident (worst). "OUT (worst)" counts the cache resident; "OUT (best)" the cache freed
|
||||
after the daily build. The Apple rate cost of the working set is on top at every step (-12 / -20 / -22 percent at 2 /
|
||||
4 / 8 GiB, measured; the curve past 8 GiB unmeasured and read as flat to -25 percent, approximate).
|
||||
|
||||
| Floor GiB | Reticles 2026 / 2031 | USD of silicon per store, one die design 2026 / 2031 (linear menu) | Tiers OUT, worst case | Tiers OUT, best case | Share of today's measured consumer cards OUT (by count, worst) | State records it assumes (64 B each) |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 2.0 (genesis) | 1 / 1 | 500 / 600 (500) | none | none | 0 | 33.6 M |
|
||||
| 2.1 | 2 / 1 | 1,000 / 600 (525) | none | none | 0 | 35 M |
|
||||
| 4.0 | 2 / 2 | 1,000 / 1,200 (1,000) | none (the 6 GB tier) | none | 3 percent | 67 M |
|
||||
| 4.1 | 3 / 2 | 1,500 / 1,200 (1,025) | the same | none | 3 percent | 69 M |
|
||||
| **5.5 (lane A's v6 epoch)** | 3 / 3 | 1,500 / 1,800 (1,375) | the 8 GB tier in the worst case only (72 / 76 percent of the card: one point over the rule) | none | 0 to 22 percent | 92 M |
|
||||
| 6.0 (main's candidate) | 3 / 3 | 1,500 / 1,800 (1,500) | 8 GB | 8 GB | 25 percent | 101 M |
|
||||
| 8.0 | 4 / 4 | 2,000 / 2,400 (2,000) | 8 GB, 12 GB (cache resident), Apple 16 GB | 8 GB, Apple 16 GB | 25 to 47 percent | 134 M |
|
||||
| **8.5 (this file's year 2)** | 5 / 4 | 2,500 / 2,400 (2,125) | 8 GB, 12 GB (cache resident), Apple 16 GB; the 12 GB tier holds at 73 to 75 percent with the cache freed | 8 GB, Apple 16 GB | 25 to 47 percent | 143 M |
|
||||
| 11.0 (lane A's year 4) | 6 / 5 | 3,000 / 3,000 (2,750) | 8, 12, 16 GB (cache resident), Apple 16 | 8, 12 GB, Apple 16 | 47 to 75 percent | 185 M |
|
||||
| **11.5 (this file's year 4)** | 6 / 5 | 3,000 / 3,000 (2,875) | the same; the 16 GB tier holds at 73 to 75 percent with the cache freed | the same | 47 to 75 percent | 193 M |
|
||||
| 16.0 | 8 / 7 | 4,000 / 4,200 (4,000) | 8, 12, 16, 24 GB (cache resident), Apple 16, 32 | 8, 12, 16 GB, Apple 16, 32 | 75 to 84 percent | 268 M |
|
||||
| 17.5 | 9 / 8 | 4,500 / 4,800 (4,375) | the same | the same | 75 to 84 percent | 294 M |
|
||||
| **20.0 (USD 5,000 at N2)** | 10 / 9 | 5,000 / 5,400 (5,000) | everything under 32 GB and every Mac under 64 GB; the 32 GB card at 85 percent with the cache resident | everything under 32 GB except the 32 GB card; Macs under 64 GB | 84 percent (only the 24 GB and up tier's 32 GB half holds) | 336 M |
|
||||
| 22.0 | 11 / 10 | 5,500 / 6,000 (5,500) | the same plus the 32 GB card | the same | 84 to 100 percent of consumer cards | 369 M |
|
||||
| 24.0 | 12 / 11 | 6,000 / 6,600 (6,000) | every consumer card; Apple 64 GB and up | the same | 100 percent | 403 M |
|
||||
|
||||
Reading: **USD 5,000 of silicon per store is 20 GiB at N2 and about 21 GiB at the 2031 node, and it retires every
|
||||
card under 32 GB and every Mac under 64 GB on the day it lands, whichever year that is.** The constraint and "retire
|
||||
the fewest honest cards" cannot both hold; the frontier is the table. The schedule that retires the fewest cards
|
||||
per dollar of chip silicon puts each step just under a tier's room and just over a reticle multiple: 5.5 GiB (3
|
||||
reticles), 8.5 (5 reticles against 8.0's 4, at the cost of nothing to the 12 GB tier with the cache freed), 11.5 (6
|
||||
reticles); it reaches USD 3,000 by four years on and never USD 5,000. Against the record's 2.1x lever (lane A's
|
||||
schedule), this file's two upper steps cost the same tiers and buy one more reticle at the year-2 step.
|
||||
|
||||
If "per card-equivalent" is read per 136 MH/s of a 5090 rather than per store, the number is unreachable: a powered
|
||||
die makes 2,000 to 8,000 MH/s, so the store's silicon is USD 33 to 125 per card-equivalent at every floor, because
|
||||
the maker powers every die and the rate scales with the count (section 2.4). The floor does not move USD per MH/s; it
|
||||
moves the minimum ticket.
|
||||
|
||||
The honest side pays the floor in joules, which the record read as zero for the NVIDIA tiers: the 5090 at the 1,300
|
||||
MHz lock loses 4.9, 11.2 and 14.1 percent of rate at 2, 4 and 8 GiB against 1 GiB (unlocked 2.8, 3.8 and 4.3), which
|
||||
is 4, 8 and 10 percent more energy per hash at the knee (measured, PC 1, 12:49 to 12:58 UK, the hash lane's ca4-v6
|
||||
rows: the page-walk cost the unlocked card hides). So the 5.5 GiB step costs a tuned 5090 about 9 percent per hash and
|
||||
the 8.5 and 11.5 steps 10 to 11 (the curve past 8 GiB unmeasured, read as flattening), against the chip's ticket rising
|
||||
from USD 500 to 1,500, 2,500 and 3,000 and its joules not at all; every chip edge in section 2 rises by the same 4 to 11
|
||||
percent against a card at its knee. The 9070 XT and the smaller NVIDIA tiers have no size rows (approximate: the
|
||||
5090's curve). Per step the floor buys the chain USD 1,000 of chip ticket for about 1 percent of the tuned 5090's
|
||||
energy per hash and 22 percent of today's measured consumer cards by count.
|
||||
|
||||
### 3.3 The growth rule, tied to chain state
|
||||
|
||||
Layer 2's rule stands (the design document 3.1): the day's item count is the larger of the schedule's floor and the
|
||||
state's record count times 64 bytes, with the ceiling at the schedule's step for the year so the state can only bring
|
||||
a step forward. The proposed floor schedule, by height like a class epoch and a consensus field read at genesis:
|
||||
|
||||
| Step | Floor | Height (DAA seconds from the v6 epoch) | The state size that brings it forward instead | Reticles (N2 / 2031) | Silicon per store | Tiers that leave | Apple rate cost on top |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| the v6 epoch | 5.5 GiB | 0 | 92 M records | 3 / 3 | USD 1,500 | the 6 GB tier; the 8 GB tier only under the worst-case working set | about -21 percent against 1 GiB (between the measured 4 and 8 GiB rows) |
|
||||
| two years on | 8.5 GiB | 63,115,200 | 143 M records | 5 / 4 | USD 2,500 | the 8 GB tier (22 percent), the 10 GB 3080, the Apple 16 GB laptop; the 12 GB tier holds with the cache freed | about -22 percent (the measured 8 GiB row) |
|
||||
| four years on | 11.5 GiB | 126,230,400 | 193 M records | 6 / 5 | USD 3,000 | the 12 GB tier (22 percent) and the 11 GB 1080 Ti and 2080 Ti; the 16 GB tier holds with the cache freed | -22 to -25 percent (unmeasured past 8 GiB) |
|
||||
| the 16 GiB step | 16 GiB | whenever the state passes 268 M records, never by the calendar inside the window | 268 M | 8 / 7 | USD 4,000 | the 16 GB tier (28 percent), the 24 GB tier with the cache resident, Apple 32 GB | the same |
|
||||
|
||||
The rule assumes the state stays under 92 M records at the v6 epoch (a used chain's accounts alone are about 10 M,
|
||||
class-v5 section 3; lane A's 61 M row is 10 M accounts plus 50 M slots plus 1 M code chunks), under 143 M two years
|
||||
on and under 193 M at four; an Ethereum-class state (about 250 M, approximate) brings the 16 GiB step forward in the
|
||||
first years, which is the ceiling's reason. The non-power-of-two floors need the multiply-shift mapping of spec
|
||||
1.13.3 option (a) (uniform to 2^-32, branch-free), which changes the vectors once at the v6 epoch; option (b)'s
|
||||
powers of two give the chip the reticle multiple for free (8 GiB is exactly four dies).
|
||||
|
||||
## 4. Lever 3: the capex wall, recomputed
|
||||
|
||||
### 4.1 The model
|
||||
|
||||
Emission from the constant: 0.77 B IGN to miners in year 1 (the ramp), 0.80 in year 2, 0.40 in years 3 and 4, 0.20 in
|
||||
5 and 6, 0.10 in 7 and 8. An N2 project of cost `C` starts at launch, tapes out at 24 months (lane B's 18 to 24), and
|
||||
its fleet mines years 3 to 6 (two halvings): 1.20 B IGN of miner revenue nominal, 0.85 B at a 10 percent discount.
|
||||
The maker takes share `s` and keeps `(1 - 1/e)` of its share's revenue at a per-joule edge `e` (the honest fleet mines
|
||||
at zero margin, so the chip's margin is what its joules save). Class rotation every 180 days costs it nothing
|
||||
(firmware). The dataset floor enters as the fleet term (USD 0.25 to 0.5 per MH/s of silicon, every die powered),
|
||||
which at the rental-equilibrium hash (7.8 M MH/s per USD of price, the mission lane) is under 2 dies for the whole
|
||||
network at any price in the table: it drops out. `NPV = s (1 - 1/e) x 0.85 B x p - C`.
|
||||
|
||||
### 4.2 The thresholds
|
||||
|
||||
| `C` (USD M) | Share `s` | Edge `e` | Break-even price `p*` (USD per IGN) | Launch-year miner revenue at `p*` (USD M a year) | Per day | Cap at the end of year 2 at `p*` | Label |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 100 | 0.30 | 3x | 0.59 | 453 | 1.24 M | 1.15 B | modelled |
|
||||
| 100 | 0.30 | 5x | 0.49 | 378 | 1.03 M | 0.96 B | modelled |
|
||||
| 100 | 0.30 | 13x | 0.43 | 327 | 0.90 M | 0.83 B | modelled |
|
||||
| 100 | 1.00 (takes the chain) | 3x / 5x / 13x | 0.18 / 0.15 / 0.13 | 136 / 113 / 98 | 0.37 / 0.31 / 0.27 M | 0.35 / 0.29 / 0.25 B | modelled |
|
||||
| 250 | 0.30 | 3x / 5x / 13x | 1.47 / 1.23 / 1.06 | 1,132 / 944 / 818 | 3.1 / 2.6 / 2.2 M | 2.9 / 2.4 / 2.1 B | modelled |
|
||||
| 250 | 1.00 | 3x / 5x / 13x | 0.44 / 0.37 / 0.32 | 340 / 283 / 245 | 0.93 / 0.78 / 0.67 M | 0.87 / 0.72 / 0.63 B | modelled |
|
||||
| 500 | 0.30 | 3x / 5x / 13x | 2.94 / 2.45 / 2.12 | 2,265 / 1,887 / 1,636 | 6.2 / 5.2 / 4.5 M | 5.8 / 4.8 / 4.2 B | modelled |
|
||||
| 500 | 1.00 | 3x / 5x / 13x | 0.88 / 0.74 / 0.64 | 680 / 566 / 491 | 1.9 / 1.6 / 1.3 M | 1.7 / 1.4 / 1.3 B | modelled |
|
||||
|
||||
At three assumed prices (the assumption is the price; everything else is the constant), `C` = 150 M, `s` = 0.30, `e` = 5x:
|
||||
|
||||
| IGN price (assumed) | Launch-year miner revenue | Per day | NPV of the project | The fleet for a 30 percent share at the rental equilibrium | Verdict |
|
||||
|---|---|---|---|---|---|
|
||||
| USD 0.01 | USD 7.7 M | USD 21 K | -148 M | 34 GH/s, 0.02 of a die | no project at any edge or cost |
|
||||
| USD 0.10 | USD 77 M | USD 211 K | -130 M | 335 GH/s, 0.17 of a die | no project at a 30 percent share; a USD 100 M project that takes the whole chain is at its break-even (0.13) |
|
||||
| USD 1.00 | USD 770 M | USD 2.1 M | +54 M | 3.4 TH/s, 1.7 dies | the USD 100 M to 250 M projects pay at every edge; the USD 500 M project does not at 3x |
|
||||
|
||||
Reading: **the revenue below which no rational SRAM project starts is about USD 100 M a year of miner revenue (IGN at
|
||||
about USD 0.13, USD 0.27 M a day), and that only for a USD 100 M project at 13x whose maker intends to take the whole
|
||||
chain; at a 30 percent share the first project starts at about USD 330 M a year (USD 0.43 per IGN, USD 0.9 M a day).
|
||||
The revenue above which every project starts is about USD 2.3 B a year (USD 2.9 per IGN, USD 6.2 M a day), where a
|
||||
USD 500 M project pays at 3x and a 30 percent share.** Between them the edge matters less than the cost: moving `e`
|
||||
from 3x to 13x moves `p*` by 1.4x, moving `C` from 100 M to 500 M moves it 5x. The per-joule levers of this file
|
||||
(section 2) move the chip from 5.7x to 5.4x at `k = 0.5` and change no row here by more than 2 percent; the lever on
|
||||
this table is the project cost, which the hash does not set, and the share-pattern detector, which is what makes the
|
||||
`s = 1` rows a chain the market can see. The mission lane's cruder rule (cap = `C / 0.30` in years 1 to 2 on its E2 of
|
||||
4.18 B) reads USD 330 M to 1.7 B of cap for the same projects, the same band as the `s` = 0.30 rows above.
|
||||
|
||||
The fleet column is the sentence the record already carries: at every price in the table the chip fleet for a third
|
||||
of the network is under two dies. A maker who builds the project builds the chain's whole hashrate in one package;
|
||||
the detector, not the hash, is the instrument.
|
||||
|
||||
### 4.3 The wall re-folded on the corrected project floor (floor lane 5, 13:2x UK: no 28 nm GDDR7 PHY exists, so the cheapest DRAM-board chip project is USD 20 M to 75 M, its cap 70 M to 250 M, not 5 M and 17 M)
|
||||
|
||||
The SRAM project's rows (4.2) stand. The thresholds for ANY rational chip project move to the cheapest one that
|
||||
exists, the GDDR7 board at USD 20 M to 75 M, which reads 5.1x at zero shadow and about 5x with the synthesised
|
||||
shadow (2.2b). The same model (ships at 24 months, mines years 3 to 6 at 10 percent; an 18-month ship lowers every
|
||||
`p*` by 15 percent):
|
||||
|
||||
| `C` (USD M) | Share `s` | Edge `e` | `p*` (USD per IGN) | Launch-year miner revenue at `p*` | Per day | Cap at the end of year 2 | Label |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 20 | 1.00 (takes the chain) | 5x / 3x | 0.029 / 0.035 | USD 23 M / 27 M a year | 62 K / 74 K | 0.06 B / 0.07 B | modelled |
|
||||
| 20 | 0.30 | 5x / 3x | 0.098 / 0.118 | 75 M / 91 M | 207 K / 248 K | 0.19 B / 0.23 B | modelled |
|
||||
| 75 | 1.00 | 5x / 3x | 0.110 / 0.132 | 85 M / 102 M | 233 K / 279 K | 0.22 B / 0.26 B | modelled |
|
||||
| 75 | 0.30 | 5x / 3x | 0.368 / 0.441 | 283 M / 340 M | 776 K / 931 K | 0.72 B / 0.86 B | modelled |
|
||||
|
||||
**The two numbers: below about USD 23 M a year of miner revenue (IGN 0.03, USD 62 K a day, a cap of about USD 60 M
|
||||
at the end of year 2) no rational chip project of any kind starts; above about USD 340 M a year (IGN 0.44, USD 0.93 M
|
||||
a day, a cap of about USD 0.9 B) every DRAM-board project starts at a third of the network, and the SRAM project
|
||||
starts at about the same level (USD 330 M a year, 4.2) at 13x or a whole-chain maker.** The band between is where the
|
||||
detector and the issuance trigger live (the history's USD 20 K to 50 K a day for compute-bound hashes sits at this
|
||||
file's lower threshold). The corrected floor moves the lower number 4x (from USD 100 M a year on the SRAM project to
|
||||
23 M) and the upper number not at all.
|
||||
|
||||
## 5. Lever 4: everything else that reaches the die, checked
|
||||
|
||||
| Candidate | What it costs the SRAM die | What it costs the honest tiers | The fresh-join path | Verdict | Label |
|
||||
|---|---|---|---|---|---|
|
||||
| A per-era (180-day) re-derivation the store must re-fill | 2^25 items x 9,360 ops at 3 pJ plus the writes: 0.96 J per re-fill, once per era: nothing | nothing (the daily build already is this) | unchanged (the dataset is derived from the state and the seed either way) | **dead**: 0 | modelled |
|
||||
| A per-epoch (3,600 s) re-fill (class v5 as designed) | 0.27 mW | the 32 ms build per epoch on every GPU (measured at 1 GiB) | unchanged | the design as it stands; not a lever | measured GPU, modelled chip |
|
||||
| A per-block (1 s) re-fill | 0.96 W, 0.32 percent of 300 W | 1.3 to 3.2 percent of a GPU's hash time (the invention lane 2.15) | the node ships the touched leaves per block (a few KB) | **dead**: the GPU pays 4x to 10x more | modelled |
|
||||
| A read that straddles two 64-byte atoms (`W = 4` at 4-byte grain, unaligned) | two macro accesses on 3/16 of reads: +0.02 nJ on 0.38 (5 percent) | the verifier derives two items for 19 percent of loads (+19 percent of the 8.3 to 8.8 ms loaded row: at the 10 ms gate); the 5090 crosses a 32-byte sector on 3/8 of reads, same DRAM row (bandwidth, not latency; unmeasured) | unchanged | **dead on the verifier** | modelled; the invention lane's 2.13 is the w64 form |
|
||||
| A per-read dependency on a second small hot table in the same die (the uniformity floor held) | the die's own kind of memory: a 64 MiB table is 15 mm^2 of N2 beside 452, read at 0.1 to 0.2 nJ | the 5090's L2 hit 2.4 nJ unlocked, 1.4 at the lock (measured); `k` 0.1 to 0.3 (the research file's design 6, dead) | unchanged | **dead**: it gives the die a cheaper read than the dataset and the card a dearer one | measured GPU, modelled chip |
|
||||
| A wider lane state (keep the per-lane scratch resident; variant 5) | pins the lane so the data must travel: holds the wire lever at the data's bits rather than the lane's 290 | 0 (the scratch lives in registers and shared memory today) | unchanged | **keep** as a dependency of lever 1 | modelled |
|
||||
| 3D-stacked SRAM (SoIC-class, two or four dies face to face; a vertical hop at about 0.1 pJ per bit) | the multi-die store reads as one die (the hop 0.01 to 0.03 nJ); the silicon per GiB unchanged | nothing | unchanged | **noted**: it removes the floor's last effect on joules and does not touch the ticket; the floor's pricing in section 3 stands | approximate |
|
||||
| Replication of the store across dies | USD 500 per die for 0.04 to 0.15 nJ per read | nothing | unchanged | not worth building | modelled |
|
||||
| A floor tied to the reticle (the dataset never under `r` reticles at the current node's density, re-based per family epoch by signal) | the same table as section 3.2 read by reticle count | the same tiers | unchanged | a governance form of lever 2, not a new lever; it needs an oracle for the density and is not recommended | design |
|
||||
|
||||
## 6. Consequences per tier
|
||||
|
||||
| Tier | What this file means for it | What is being done |
|
||||
|---|---|---|
|
||||
| Home miner, one 8 GB card | Lever 1 costs it nothing at `W = 4` (measured on larger cards; its own row unmeasured) and under 5 percent at `W = 8` by the sector argument (unmeasured). Lever 2 retires it at the year-2 step (8.5 GiB) on this file's schedule, as on lane A's; a 6 GiB v6 epoch retires it two years earlier. Lever 3 is not its concern: no chip exists, and the first one needs IGN at USD 0.13 to 2.9 | the schedule is the founder's number; the `W = 8` measurement is a PC 1 job |
|
||||
| One 12 GB card | holds to the year-4 step (11.5 GiB) with the cache freed after the build; out at 11.0 to 11.5 with it resident | the same |
|
||||
| One 16 GB card (the 9070 XT, the 5070 Ti) | holds every step to 11.5 GiB; out at the 16 GiB step | the same |
|
||||
| One 24 or 32 GB card, the 5090 at its knee | holds every step of the schedule; the 24 GB tier leaves at 16 GiB with the cache resident; the 32 GB card at 22 GiB. It pays the floor in joules at the knee: 4, 8 and 10 percent per hash at 2, 4 and 8 GiB (measured), about 9 to 11 percent across the schedule; unlocked 3 to 4 percent. Against the die with the shadow on at the lock (the card 1.67 microjoules plus F 0.65, measured; the die at `W = 4`): under the record's convention (`k` against the GPU's op cost at the same operating point, 6.2 pJ at the lock) 6.1x at `k = 0.5` and 3.3x at `k = 1`; under the absolute convention (the die's core costs what it costs, `k` fixed against the stock 11.3 pJ) 3.8x and 2.0x. The record's convention flatters the die at the lock by 1.6x; the absolute one is the physics and is what this file carries for the served line | the Ember knob carries the lock; the research lane picks the convention for its tables |
|
||||
| The unified-memory SoC | a 16 GB Mac leaves at the year-2 step; 32 GB at the 16 GiB step; every Mac pays -21 to -25 percent of rate from the v6 epoch on (measured to 8 GiB). Against the die at class v4 and `k = 0.5` the M5 Max reads 3.6x to 4.0x | finding 3 of lane B: the reference joule |
|
||||
| A rig | nothing changes until a project pays (section 4); the 180-day rotation and the floor do not move the chip's per-MH/s cost | the issuance trigger and the detector, as the record |
|
||||
| A pool user | the chip fleet that matters is one package; the share-pattern detector is the warning | the detector before the public testnet |
|
||||
| The public claim | "under 2x" is not reachable against the die by any shadow: on the k lane's synthesised core 10x at the honest cards' whole latency shadow at stock, 6x to 7x at the 5090's knee, 18x to 21x under class v4; on the record's claimed band (marked) 2x at `k = 1` and 4x at `k = 0.5`; 20x to 60x without the shadow. The number a miner can act on is the project's price threshold: USD 0.13 to 2.9 per IGN | section 7's served sentence |
|
||||
|
||||
## 7. Recommendation, for the 15:45 close
|
||||
|
||||
The lever is the read width: pin `W = 4` words at genesis on the measured rows (the 5090 +2.7 percent, the 9070 XT
|
||||
-1.4, the M5 Max within 1) and move to `W = 8` once one PC 1 row confirms what the measured rows already say (one
|
||||
sector per load on NVIDIA, one line on AMD, Apple flat to w64), which moves the die from 66x to 31x to 44x at zero
|
||||
shadow and by 0.2x to 0.4x with the shadow on, and keep the per-lane scratch resident so the lane stays pinned. The schedule is 5.5 GiB at the v6 epoch, 8.5 two years on and
|
||||
11.5 at four, each just under a card tier's room and just over a reticle multiple (3, 5 and 6 dies; USD 1,500, 2,500
|
||||
and 3,000 of silicon), and USD 5,000 is not recommended because it is 20 GiB and retires every card under 32 GB and
|
||||
every Mac under 64 GB whenever it lands. The served claim should say that the strongest chip we can model for 2027
|
||||
to 2028, an SRAM store on 2 nm, would reach about 6x to 10x per joule against an RTX 5090 paying its whole latency
|
||||
shadow with a core at the synthesised cost of a 3 nm lane (2x to 4x on the claimed band the record carried, marked),
|
||||
20x to 60x without that shadow, and that such a project pays only above about USD 0.4 per IGN at a third of the
|
||||
network or USD 0.13 for a maker who takes the whole chain, which is the pattern the share detector watches for. The floor is a ticket (USD 500 to 3,000 of silicon per
|
||||
store across the schedule) and not a per-joule or per-MH/s lever, and the text should not claim otherwise. Nothing in
|
||||
this file moves a served number; the `W = 8` row and the Apple rate curve past 8 GiB are the two measurements owed.
|
||||
|
||||
## 8. Unverified and owed
|
||||
|
||||
- Every chip-side figure is modelled; the macro access (0.1 nJ, width-independent) and the wire (1.3 pJ per bit at 24
|
||||
mm) are the record's approximations and move every zero-shadow row by 2x either way; the shadowed rows move under
|
||||
0.3x.
|
||||
- `W = 8` has no row of its own: its card cost is read by interpolation of the measured w16 and w64 rows (one sector
|
||||
per load on NVIDIA, one line on AMD, the M5 Max flat at 1.03 through w64). One PC 1 row at stock and at the lock
|
||||
confirms it; the crate's width set is {1, 4, 16} words, so the pack needs a generator and emitter line first (the
|
||||
hash lane's caveat, 13:0x UK). Not run by this lane (no builds on the Mac; no GPU on build-3 or build-4).
|
||||
- The Apple rate curve past 8 GiB (the 11.5 and 16 GiB steps): unmeasured; read as -22 to -25 percent.
|
||||
- The 5090's size rows at 2, 4 and 8 GiB landed at 12:58 UK (the hash lane, PC 1, label ca4-v6) and are in section 3.2;
|
||||
the 9070 XT and the smaller NVIDIA tiers have no size rows (their page reach is read as the 5090's, approximate).
|
||||
- The SRAM cost curve past N2 (A16 and A14 density and wafer price): approximate; the flat USD 250 per GiB rests on
|
||||
the density gain and the wafer price cancelling, which a 2x move in either changes by 2x.
|
||||
- The emission figures are the spec's constant read by this lane; the mission lane's E2 of 4.18 B IGN differs and is
|
||||
carried beside, not reconciled.
|
||||
- The 10 percent discount and the 24-month tape-out are assumptions; an 18-month tape-out adds about 0.2 B IGN of
|
||||
life and lowers every `p*` by about 15 percent.
|
||||
- The card population shares are by count of the bench table's 32 measured consumer cards; the fleet census is unread.
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
binary 4bec799c42e4c843 commit 8f4814591a4ad543e8cb91a50fa049da80d82b5d test v5_attempts_census from 1000 seeds 10000 threads 24 start 2026-10-08T10:15:44Z
|
||||
lease: holding 24 pool cores (48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71, waited 0 s, class measure): family gate: v5_attempts_census base seeds 1000+10000
|
||||
|
||||
running 1 test
|
||||
test accept::tests::v5_attempts_census ... v5_attempts_census: 10000 seeds 1000..11000 in 1534 s on 24 threads
|
||||
candidates 31788, rejected 21788, per-candidate rejection 0.6854, exhaustions 0, P(256 consecutive) 1.010e-42
|
||||
first failing part (a') unfresh source: 17980 (82.5 percent of rejections, 56.6 percent of candidates)
|
||||
first failing part (a) stale load: 2521 (11.6 percent of rejections, 7.9 percent of candidates)
|
||||
first failing part (b) no injecting write: 670 (3.1 percent of rejections, 2.1 percent of candidates)
|
||||
first failing part (c''') hot-item site: 201 (0.9 percent of rejections, 0.6 percent of candidates)
|
||||
first failing part (c'') low-entropy site: 240 (1.1 percent of rejections, 0.8 percent of candidates)
|
||||
first failing part (c') saturated source: 14 (0.1 percent of rejections, 0.0 percent of candidates)
|
||||
first failing part (c) constant bit: 88 (0.4 percent of rejections, 0.3 percent of candidates)
|
||||
first failing part (c) distinct addresses: 7 (0.0 percent of rejections, 0.0 percent of candidates)
|
||||
first failing part (c) saturated: 67 (0.3 percent of rejections, 0.2 percent of candidates)
|
||||
accepted attempt mean 2.179
|
||||
ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 79 filtered out; finished in 1533.90s
|
||||
|
||||
Terminated
|
||||
lease: released 24 pool cores after 1534 s, exit 0
|
||||
end 2026-10-08T10:41:18Z rc 0
|
||||
11
docs/analysis/class-v6/logs/bench-x16.log
Normal file
11
docs/analysis/class-v6/logs/bench-x16.log
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
lease: holding 1 pool cores (10, waited 0 s, class measure): family gate: verifier row, bench v2m16g+sh256x27 50 warps one core (build-3)
|
||||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 323.7 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class v2m16g+sh256x27, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x16 (144 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 309.8 ms
|
||||
shadow: 256 instructions x 27 passes per iteration, 55296 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 7.429 ms, 4096 items derived, lane0 11a6ebee3f284225 lane31 d69b6f764b683e9c
|
||||
warp base 4096: single cold run 7.226 ms, 4096 items derived, lane0 f13cf250640b741e lane31 b77d190ae6454aaf
|
||||
warp base 1000000: single cold run 7.149 ms, 4096 items derived, lane0 1d0f32760ac86802 lane31 952916a3b036252d
|
||||
CPU verify: 7.128 ms per 32-lane warp, avg of 50 (checksum 9a88da132b815f31)
|
||||
Terminated
|
||||
lease: released 1 pool cores after 1 s, exit 0
|
||||
11
docs/analysis/class-v6/logs/bench-x4.log
Normal file
11
docs/analysis/class-v6/logs/bench-x4.log
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
lease: holding 1 pool cores (9, waited 0 s, class measure): family gate: verifier row, bench v2m4g+sh256x27 50 warps one core (build-3)
|
||||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 374.9 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx4+sh256x27, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x4 (36 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 397.6 ms
|
||||
shadow: 256 instructions x 27 passes per iteration, 55296 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 4.074 ms, 4096 items derived, lane0 7d5c0b3cbd0fcade lane31 2a25e4b6bf48485d
|
||||
warp base 4096: single cold run 3.650 ms, 4096 items derived, lane0 73a7e6af84f4d3ca lane31 f26081f8ff529a1a
|
||||
warp base 1000000: single cold run 3.661 ms, 4096 items derived, lane0 2e20e9a0b5cb35da lane31 a8a0e975fa5ae184
|
||||
CPU verify: 3.725 ms per 32-lane warp, avg of 50 (checksum 164082cfe849fab4)
|
||||
Terminated
|
||||
lease: released 1 pool cores after 1 s, exit 0
|
||||
10
docs/analysis/class-v6/logs/bench-x8.log
Normal file
10
docs/analysis/class-v6/logs/bench-x8.log
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
lease: holding 1 pool cores (8, waited 0 s, class measure): family gate: verifier row, bench v2m8g+sh256x27 50 warps one core (build-3)
|
||||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 448.8 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x27, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 2033.0 ms
|
||||
shadow: 256 instructions x 27 passes per iteration, 55296 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 4.368 ms, 4096 items derived, lane0 dfbc8db1c06dacd8 lane31 b92715f32b721587
|
||||
warp base 4096: single cold run 4.161 ms, 4096 items derived, lane0 433afad2164a8dda lane31 207c50f288cbb677
|
||||
warp base 1000000: single cold run 4.113 ms, 4096 items derived, lane0 4ffd090a02db43b4 lane31 60308a0e058e1e29
|
||||
CPU verify: 4.116 ms per 32-lane warp, avg of 50 (checksum 900cd36a9ead25cf)
|
||||
lease: released 1 pool cores after 3 s, exit 0
|
||||
2817
docs/analysis/class-v6/logs/census-0ab27582-era.tsv
Normal file
2817
docs/analysis/class-v6/logs/census-0ab27582-era.tsv
Normal file
File diff suppressed because it is too large
Load diff
2817
docs/analysis/class-v6/logs/census-0ab27582-none.tsv
Normal file
2817
docs/analysis/class-v6/logs/census-0ab27582-none.tsv
Normal file
File diff suppressed because it is too large
Load diff
259
docs/analysis/class-v6/logs/census/a-era.tsv
Normal file
259
docs/analysis/class-v6/logs/census/a-era.tsv
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
# a-era class w64m8g+sh256x27 eras [0,1,2,3,4,5,6,7] seeds 32 era-widths 64 weights base bittest 1 bin d601098699840534 start 2026-10-08T12:53:59Z host igneum-build-3 threads 8
|
||||
cand era seed candidates accepted attempt bit_z bit_site bit_bit reasons
|
||||
a-era 0 7 8 1 7 3.2 12 8 a'=5;c,=2;
|
||||
a-era 0 2 9 1 8 3.4 5 11 a'=7;a=1;
|
||||
a-era 0 0 2 1 1 2.9 7 14 a'=1;
|
||||
a-era 0 4 9 1 8 2.8 1 23 a'=4;c,=3;a=1;
|
||||
a-era 0 1 6 1 5 3.1 3 7 a'=4;c,=1;
|
||||
a-era 0 6 2 1 1 3.5 5 24 a'=1;
|
||||
a-era 0 5 14 1 13 2.9 10 16 a'=12;a=1;
|
||||
a-era 0 3 2 1 1 3.1 11 9 a'=1;
|
||||
a-era 0 8 1 1 0 3.5 6 20
|
||||
a-era 0 10 1 1 0 3.0 6 20
|
||||
a-era 0 12 3 1 2 2.9 0 18 a'=2;
|
||||
a-era 0 13 1 1 0 3.4 0 27
|
||||
a-era 0 9 5 1 4 2.8 15 4 a'=3;c,=1;
|
||||
a-era 0 14 13 1 12 2.5 8 9 a'=9;a=2;c,=1;
|
||||
a-era 0 11 9 1 8 3.1 4 21 c,=5;a'=3;
|
||||
a-era 0 15 6 1 5 3.4 1 9 a'=3;c,=1;a=1;
|
||||
a-era 0 16 5 1 4 3.3 14 18 a'=4;
|
||||
a-era 0 18 3 1 2 3.2 10 25 c,=2;
|
||||
a-era 0 17 3 1 2 3.1 8 5 a'=2;
|
||||
a-era 0 19 5 1 4 3.0 8 12 a'=3;a=1;
|
||||
a-era 0 21 4 1 3 3.1 13 4 a'=3;
|
||||
a-era 0 20 8 1 7 3.6 7 11 a'=5;c,=1;a=1;
|
||||
a-era 0 22 6 1 5 2.6 8 11 a'=4;a=1;
|
||||
a-era 0 23 4 1 3 2.9 5 8 c,=1;a'=1;a=1;
|
||||
a-era 0 24 3 1 2 3.0 9 24 a'=2;
|
||||
a-era 0 25 6 1 5 3.1 10 13 c,=2;a=2;a'=1;
|
||||
a-era 0 30 8 1 7 3.1 3 19 a'=6;a=1;
|
||||
a-era 0 28 15 1 14 2.9 2 10 a'=10;c,=2;a=2;
|
||||
a-era 0 29 4 1 3 4.1 7 8 c,=2;a'=1;
|
||||
a-era 0 27 7 1 6 3.3 10 12 a'=6;
|
||||
a-era 0 26 2 1 1 4.0 13 15 a'=1;
|
||||
a-era 0 31 12 1 11 5.9 0 21 a'=5;c,=3;a=2;b=1;
|
||||
a-era 1 0 2 1 1 2.9 5 12 a'=1;
|
||||
a-era 1 2 9 1 8 3.5 12 16 a'=7;a=1;
|
||||
a-era 1 1 6 1 5 2.8 0 6 a'=4;c,=1;
|
||||
a-era 1 3 2 1 1 3.0 14 13 a'=1;
|
||||
a-era 1 4 12 1 11 2.6 14 17 a'=6;c,=4;a=1;
|
||||
a-era 1 5 21 1 20 3.1 13 17 a'=15;c,=3;a=2;
|
||||
a-era 1 6 2 1 1 3.0 0 11 a'=1;
|
||||
a-era 1 7 8 1 7 3.4 6 8 a'=5;c,=2;
|
||||
a-era 1 8 1 1 0 3.2 1 19
|
||||
a-era 1 9 5 1 4 3.4 15 7 a'=3;c,=1;
|
||||
a-era 1 10 1 1 0 3.4 3 20
|
||||
a-era 1 11 9 1 8 2.8 14 24 c,=5;a'=3;
|
||||
a-era 1 13 1 1 0 3.0 4 4
|
||||
a-era 1 12 3 1 2 3.2 15 9 a'=2;
|
||||
a-era 1 14 13 1 12 3.0 6 16 a'=9;a=2;c,=1;
|
||||
a-era 1 15 6 1 5 3.4 13 23 a'=3;c,=1;a=1;
|
||||
a-era 1 16 5 1 4 3.2 8 4 a'=4;
|
||||
a-era 1 17 3 1 2 3.0 1 21 a'=2;
|
||||
a-era 1 18 3 1 2 3.2 14 18 c,=2;
|
||||
a-era 1 20 8 1 7 3.1 7 22 a'=5;c,=1;a=1;
|
||||
a-era 1 19 5 1 4 2.8 5 12 a'=3;a=1;
|
||||
a-era 1 21 4 1 3 3.1 11 8 a'=3;
|
||||
a-era 1 22 6 1 5 3.2 8 5 a'=4;a=1;
|
||||
a-era 1 24 3 1 2 2.9 11 19 a'=2;
|
||||
a-era 1 23 4 1 3 2.5 8 14 c,=1;a'=1;a=1;
|
||||
a-era 1 25 6 1 5 3.5 15 8 c,=2;a=2;a'=1;
|
||||
a-era 1 26 2 1 1 2.5 0 20 a'=1;
|
||||
a-era 1 28 15 1 14 3.2 12 6 a'=10;c,=2;a=2;
|
||||
a-era 1 27 7 1 6 3.0 7 7 a'=6;
|
||||
a-era 1 29 4 1 3 3.6 14 25 c,=2;a'=1;
|
||||
a-era 1 30 41 1 40 2.7 6 5 a'=29;c,=8;a=2;b=1;
|
||||
a-era 2 0 2 1 1 2.7 13 20 a'=1;
|
||||
a-era 1 31 17 1 16 3.4 11 15 a'=7;c,=5;a=3;b=1;
|
||||
a-era 2 1 4 1 3 2.6 2 9 a'=3;
|
||||
a-era 2 2 9 1 8 3.1 8 11 a'=7;a=1;
|
||||
a-era 2 3 2 1 1 3.3 10 16 a'=1;
|
||||
a-era 2 4 2 1 1 3.3 6 6 a'=1;
|
||||
a-era 2 5 14 1 13 3.1 2 6 a'=12;a=1;
|
||||
a-era 2 6 2 1 1 3.1 12 21 a'=1;
|
||||
a-era 2 7 4 1 3 3.1 13 16 a'=3;
|
||||
a-era 2 8 1 1 0 2.8 10 9
|
||||
a-era 2 9 3 1 2 3.2 7 15 a'=2;
|
||||
a-era 2 10 1 1 0 2.3 7 7
|
||||
a-era 2 11 2 1 1 3.5 10 16 a'=1;
|
||||
a-era 2 12 3 1 2 2.7 1 6 a'=2;
|
||||
a-era 2 13 1 1 0 4.3 15 25
|
||||
a-era 2 14 5 1 4 2.9 1 13 a'=3;a=1;
|
||||
a-era 2 15 3 1 2 2.8 8 5 a'=2;
|
||||
a-era 2 16 5 1 4 3.2 5 9 a'=4;
|
||||
a-era 2 17 3 1 2 2.7 10 19 a'=2;
|
||||
a-era 2 18 1 1 0 4.0 6 9
|
||||
a-era 2 19 5 1 4 4.0 12 23 a'=3;a=1;
|
||||
a-era 2 20 7 1 6 3.6 8 10 a'=5;a=1;
|
||||
a-era 2 21 4 1 3 3.0 4 21 a'=3;
|
||||
a-era 2 22 6 1 5 2.8 13 15 a'=4;a=1;
|
||||
a-era 2 23 2 1 1 2.8 14 17 a=1;
|
||||
a-era 2 24 3 1 2 2.5 13 13 a'=2;
|
||||
a-era 2 25 4 1 3 3.3 12 13 a=2;a'=1;
|
||||
a-era 2 26 2 1 1 2.7 10 23 a'=1;
|
||||
a-era 2 27 7 1 6 3.7 1 17 a'=6;
|
||||
a-era 2 28 8 1 7 3.1 14 14 a'=5;a=2;
|
||||
a-era 2 29 2 1 1 3.0 1 23 a'=1;
|
||||
a-era 2 30 8 1 7 3.3 5 4 a'=6;a=1;
|
||||
a-era 2 31 4 1 3 3.7 10 15 a'=2;b=1;
|
||||
a-era 3 0 2 1 1 2.8 5 9 a'=1;
|
||||
a-era 3 1 4 1 3 2.8 4 13 a'=3;
|
||||
a-era 3 2 9 1 8 3.7 7 26 a'=7;a=1;
|
||||
a-era 3 4 2 1 1 2.7 10 12 a'=1;
|
||||
a-era 3 3 2 1 1 2.9 13 14 a'=1;
|
||||
a-era 3 5 14 1 13 2.9 5 12 a'=12;a=1;
|
||||
a-era 3 6 2 1 1 2.6 6 17 a'=1;
|
||||
a-era 3 8 1 1 0 3.8 0 24
|
||||
a-era 3 7 4 1 3 3.0 0 18 a'=3;
|
||||
a-era 3 9 3 1 2 4.2 5 19 a'=2;
|
||||
a-era 3 10 1 1 0 3.5 5 16
|
||||
a-era 3 11 2 1 1 3.3 5 4 a'=1;
|
||||
a-era 3 12 3 1 2 3.5 9 15 a'=2;
|
||||
a-era 3 13 1 1 0 3.0 6 16
|
||||
a-era 3 14 5 1 4 3.6 14 10 a'=3;a=1;
|
||||
a-era 3 15 3 1 2 3.8 14 19 a'=2;
|
||||
a-era 3 16 5 1 4 3.0 13 10 a'=4;
|
||||
a-era 3 17 3 1 2 2.7 0 26 a'=2;
|
||||
a-era 3 18 1 1 0 3.2 14 5
|
||||
a-era 3 20 7 1 6 3.1 11 23 a'=5;a=1;
|
||||
a-era 3 19 5 1 4 3.2 12 22 a'=3;a=1;
|
||||
a-era 3 21 4 1 3 2.9 7 15 a'=3;
|
||||
a-era 3 22 6 1 5 2.8 3 5 a'=4;a=1;
|
||||
a-era 3 23 2 1 1 3.5 2 21 a=1;
|
||||
a-era 3 24 3 1 2 4.1 3 24 a'=2;
|
||||
a-era 3 25 4 1 3 3.5 4 6 a=2;a'=1;
|
||||
a-era 3 26 2 1 1 2.8 0 15 a'=1;
|
||||
a-era 3 28 8 1 7 3.1 7 24 a'=5;a=2;
|
||||
a-era 3 27 7 1 6 3.8 6 11 a'=6;
|
||||
a-era 3 29 2 1 1 3.5 3 11 a'=1;
|
||||
a-era 3 30 8 1 7 3.0 14 9 a'=6;a=1;
|
||||
a-era 3 31 6 1 5 3.1 1 10 a'=3;c,=1;b=1;
|
||||
a-era 4 0 2 1 1 3.8 12 22 a'=1;
|
||||
a-era 4 1 6 1 5 2.8 3 14 a'=4;c,=1;
|
||||
a-era 4 2 9 1 8 3.5 15 8 a'=7;a=1;
|
||||
a-era 4 3 2 1 1 2.8 14 19 a'=1;
|
||||
a-era 4 4 9 1 8 3.5 1 14 a'=4;c,=3;a=1;
|
||||
a-era 4 5 21 1 20 3.1 0 4 a'=15;c,=3;a=2;
|
||||
a-era 4 6 2 1 1 3.0 12 12 a'=1;
|
||||
a-era 4 8 1 1 0 3.0 4 8
|
||||
a-era 4 7 8 1 7 4.0 3 21 a'=5;c,=2;
|
||||
a-era 4 9 5 1 4 2.7 8 21 a'=3;c,=1;
|
||||
a-era 4 10 1 1 0 3.8 3 22
|
||||
a-era 4 11 9 1 8 3.0 4 4 c,=5;a'=3;
|
||||
a-era 4 12 3 1 2 2.9 6 26 a'=2;
|
||||
a-era 4 13 1 1 0 3.8 9 25
|
||||
a-era 4 14 13 1 12 3.9 1 5 a'=9;a=2;c,=1;
|
||||
a-era 4 16 5 1 4 3.3 0 19 a'=4;
|
||||
a-era 4 15 6 1 5 2.5 0 11 a'=3;c,=1;a=1;
|
||||
a-era 4 17 3 1 2 3.2 6 17 a'=2;
|
||||
a-era 4 18 3 1 2 3.5 1 4 c,=2;
|
||||
a-era 4 20 8 1 7 4.0 2 21 a'=5;c,=1;a=1;
|
||||
a-era 4 19 5 1 4 3.3 12 19 a'=3;a=1;
|
||||
a-era 4 21 4 1 3 3.2 2 4 a'=3;
|
||||
a-era 4 22 6 1 5 3.3 7 22 a'=4;a=1;
|
||||
a-era 4 24 3 1 2 3.0 12 26 a'=2;
|
||||
a-era 4 23 36 1 35 2.5 3 24 a'=21;c,=9;a=4;c=1;
|
||||
a-era 4 25 6 1 5 2.8 1 14 c,=2;a=2;a'=1;
|
||||
a-era 4 26 2 1 1 3.2 14 6 a'=1;
|
||||
a-era 4 27 7 1 6 2.7 14 16 a'=6;
|
||||
a-era 4 28 15 1 14 3.9 12 11 a'=10;c,=2;a=2;
|
||||
a-era 4 29 4 1 3 3.5 8 9 c,=2;a'=1;
|
||||
a-era 4 30 41 1 40 3.1 7 13 a'=29;c,=8;a=2;b=1;
|
||||
a-era 4 31 17 1 16 3.9 11 7 a'=7;c,=5;a=3;b=1;
|
||||
a-era 5 0 2 1 1 3.8 10 17 a'=1;
|
||||
a-era 5 1 6 1 5 3.1 13 4 a'=4;c,=1;
|
||||
a-era 5 2 9 1 8 3.5 10 23 a'=7;a=1;
|
||||
a-era 5 3 2 1 1 3.6 11 19 a'=1;
|
||||
a-era 5 4 9 1 8 3.5 12 13 a'=4;c,=3;a=1;
|
||||
a-era 5 5 21 1 20 2.8 9 15 a'=15;c,=3;a=2;
|
||||
a-era 5 6 2 1 1 3.3 9 13 a'=1;
|
||||
a-era 5 8 1 1 0 3.2 8 23
|
||||
a-era 5 7 8 1 7 2.9 1 18 a'=5;c,=2;
|
||||
a-era 5 9 5 1 4 3.3 5 25 a'=3;c,=1;
|
||||
a-era 5 10 1 1 0 3.5 9 9
|
||||
a-era 5 11 9 1 8 2.8 12 21 c,=5;a'=3;
|
||||
a-era 5 12 3 1 2 3.1 14 17 a'=2;
|
||||
a-era 5 13 1 1 0 3.3 2 19
|
||||
a-era 5 14 13 1 12 2.7 9 15 a'=9;a=2;c,=1;
|
||||
a-era 5 15 6 1 5 3.4 10 16 a'=3;c,=1;a=1;
|
||||
a-era 5 16 5 1 4 3.2 6 11 a'=4;
|
||||
a-era 5 17 3 1 2 3.4 4 13 a'=2;
|
||||
a-era 5 18 3 1 2 2.9 0 5 c,=2;
|
||||
a-era 5 19 5 1 4 2.6 9 16 a'=3;a=1;
|
||||
a-era 5 20 8 1 7 3.5 10 10 a'=5;c,=1;a=1;
|
||||
a-era 5 21 4 1 3 2.6 5 25 a'=3;
|
||||
a-era 5 22 6 1 5 3.4 13 4 a'=4;a=1;
|
||||
a-era 5 24 3 1 2 3.2 12 9 a'=2;
|
||||
a-era 5 23 4 1 3 3.4 5 24 c,=1;a'=1;a=1;
|
||||
a-era 5 25 6 1 5 3.0 5 10 c,=2;a=2;a'=1;
|
||||
a-era 5 26 2 1 1 3.2 9 18 a'=1;
|
||||
a-era 5 27 7 1 6 3.7 5 9 a'=6;
|
||||
a-era 5 28 15 1 14 3.4 12 22 a'=10;c,=2;a=2;
|
||||
a-era 5 29 4 1 3 3.6 2 18 c,=2;a'=1;
|
||||
a-era 5 30 8 1 7 5.6 13 23 a'=6;a=1;
|
||||
a-era 6 0 2 1 1 3.3 12 22 a'=1;
|
||||
a-era 5 31 17 1 16 3.1 9 12 a'=7;c,=5;a=3;b=1;
|
||||
a-era 6 1 6 1 5 2.4 9 20 a'=4;c,=1;
|
||||
a-era 6 2 9 1 8 3.5 5 6 a'=7;a=1;
|
||||
a-era 6 3 2 1 1 3.0 2 12 a'=1;
|
||||
a-era 6 4 6 1 5 3.0 10 24 a'=4;c,=1;
|
||||
a-era 6 5 19 1 18 3.5 14 17 a'=14;c,=2;a=2;
|
||||
a-era 6 6 2 1 1 3.7 12 25 a'=1;
|
||||
a-era 6 7 4 1 3 3.5 7 26 a'=3;
|
||||
a-era 6 8 1 1 0 3.1 2 13
|
||||
a-era 6 9 5 1 4 3.1 15 20 a'=3;c,=1;
|
||||
a-era 6 10 1 1 0 2.8 6 12
|
||||
a-era 6 11 2 1 1 3.5 11 4 a'=1;
|
||||
a-era 6 12 3 1 2 2.9 7 24 a'=2;
|
||||
a-era 6 13 1 1 0 3.3 9 19
|
||||
a-era 6 15 3 1 2 5.7 12 4 a'=2;
|
||||
a-era 6 14 13 1 12 3.1 15 10 a'=9;a=2;c,=1;
|
||||
a-era 6 16 5 1 4 2.9 8 18 a'=4;
|
||||
a-era 6 17 3 1 2 3.4 10 18 a'=2;
|
||||
a-era 6 18 1 1 0 3.1 3 21
|
||||
a-era 6 19 5 1 4 3.4 5 23 a'=3;a=1;
|
||||
a-era 6 20 8 1 7 3.5 5 25 a'=5;c,=1;a=1;
|
||||
a-era 6 21 4 1 3 2.8 1 23 a'=3;
|
||||
a-era 6 24 3 1 2 2.8 7 16 a'=2;
|
||||
a-era 6 22 6 1 5 3.8 1 14 a'=4;a=1;
|
||||
a-era 6 23 36 1 35 3.0 13 11 a'=21;c,=9;a=4;c=1;
|
||||
a-era 6 25 5 1 4 2.8 15 16 a=2;c,=1;a'=1;
|
||||
a-era 6 26 2 1 1 3.0 3 20 a'=1;
|
||||
a-era 6 27 7 1 6 2.7 1 7 a'=6;
|
||||
a-era 6 28 15 1 14 5.0 12 4 a'=10;c,=2;a=2;
|
||||
a-era 6 29 4 1 3 2.6 13 20 c,=2;a'=1;
|
||||
a-era 6 31 14 1 13 3.4 15 4 a'=6;c,=4;a=2;b=1;
|
||||
a-era 6 30 41 1 40 3.2 8 20 a'=29;c,=8;a=2;b=1;
|
||||
a-era 7 0 2 1 1 2.8 14 26 a'=1;
|
||||
a-era 7 1 4 1 3 2.7 5 11 a'=3;
|
||||
a-era 7 2 9 1 8 3.0 8 14 a'=7;a=1;
|
||||
a-era 7 3 2 1 1 2.9 5 6 a'=1;
|
||||
a-era 7 4 2 1 1 3.4 8 24 a'=1;
|
||||
a-era 7 5 14 1 13 3.1 7 23 a'=12;a=1;
|
||||
a-era 7 6 2 1 1 2.9 13 16 a'=1;
|
||||
a-era 7 8 1 1 0 3.1 7 10
|
||||
a-era 7 7 6 1 5 2.8 1 7 a'=4;c,=1;
|
||||
a-era 7 9 3 1 2 2.8 3 26 a'=2;
|
||||
a-era 7 10 1 1 0 2.6 4 25
|
||||
a-era 7 11 2 1 1 3.4 4 14 a'=1;
|
||||
a-era 7 12 3 1 2 3.5 15 4 a'=2;
|
||||
a-era 7 13 1 1 0 3.4 14 15
|
||||
a-era 7 15 6 1 5 2.8 6 12 a'=3;c,=1;a=1;
|
||||
a-era 7 14 5 1 4 3.3 2 12 a'=3;a=1;
|
||||
a-era 7 16 5 1 4 2.9 8 13 a'=4;
|
||||
a-era 7 17 3 1 2 3.8 14 20 a'=2;
|
||||
a-era 7 18 3 1 2 3.0 2 15 c,=2;
|
||||
a-era 7 19 5 1 4 2.5 11 17 a'=3;a=1;
|
||||
a-era 7 20 8 1 7 3.1 7 27 a'=5;c,=1;a=1;
|
||||
a-era 7 21 4 1 3 3.1 1 11 a'=3;
|
||||
a-era 7 23 2 1 1 3.0 9 22 a=1;
|
||||
a-era 7 24 3 1 2 3.4 5 17 a'=2;
|
||||
a-era 7 22 6 1 5 3.1 11 4 a'=4;a=1;
|
||||
a-era 7 25 4 1 3 3.5 10 16 a=2;a'=1;
|
||||
a-era 7 26 2 1 1 3.1 15 8 a'=1;
|
||||
a-era 7 27 7 1 6 2.6 6 11 a'=6;
|
||||
a-era 7 28 14 1 13 3.8 7 12 a'=10;a=2;c,=1;
|
||||
a-era 7 29 2 1 1 4.1 0 16 a'=1;
|
||||
a-era 7 30 8 1 7 2.7 1 7 a'=6;a=1;
|
||||
a-era 7 31 6 1 5 3.1 10 20 a'=3;c,=1;b=1;
|
||||
# end 2026-10-08T12:58:29Z rows=257
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
259
docs/analysis/class-v6/logs/census/a-none.tsv
Normal file
259
docs/analysis/class-v6/logs/census/a-none.tsv
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
# a-none class w64m8g+sh256x27 eras [none] seeds 256 era-widths 64 weights base bittest off bin d601098699840534 start 2026-10-08T12:37:23Z host igneum-build-3 threads 8
|
||||
cand era seed candidates accepted attempt bit_z bit_site bit_bit reasons
|
||||
a-none none 2 5 1 4 2.8 8 24 a'=3;a=1;
|
||||
a-none none 6 2 1 1 3.6 2 5 a'=1;
|
||||
a-none none 7 1 1 0 5.2 11 5
|
||||
a-none none 0 3 1 2 3.1 7 10 a'=2;
|
||||
a-none none 4 2 1 1 2.9 12 17 a'=1;
|
||||
a-none none 1 2 1 1 4.3 5 4 a'=1;
|
||||
a-none none 5 2 1 1 3.2 1 12 a'=1;
|
||||
a-none none 3 7 1 6 3.0 13 5 a'=3;a=2;c'''=1;
|
||||
a-none none 9 7 1 6 2.5 6 11 a'=5;a=1;
|
||||
a-none none 13 2 1 1 4.0 9 11 a'=1;
|
||||
a-none none 10 7 1 6 3.4 2 16 a'=6;
|
||||
a-none none 8 2 1 1 2.5 14 27 a'=1;
|
||||
a-none none 12 1 1 0 3.2 1 4
|
||||
a-none none 14 9 1 8 10.4 12 27 a'=7;a=1;
|
||||
a-none none 11 1 1 0 2.9 2 24
|
||||
a-none none 15 3 1 2 3.1 4 18 a'=2;
|
||||
a-none none 16 10 1 9 3.4 2 17 a'=9;
|
||||
a-none none 18 3 1 2 3.1 15 27 a'=2;
|
||||
a-none none 19 3 1 2 2.9 12 26 a'=2;
|
||||
a-none none 17 2 1 1 2.9 6 21 a'=1;
|
||||
a-none none 20 3 1 2 3.2 6 23 a'=1;a=1;
|
||||
a-none none 21 5 1 4 2.7 8 21 a'=4;
|
||||
a-none none 22 1 1 0 2.7 4 15
|
||||
a-none none 23 4 1 3 4.2 0 16 a'=3;
|
||||
a-none none 25 13 1 12 3.2 12 19 a'=10;a=2;
|
||||
a-none none 24 2 1 1 2.7 0 6 a'=1;
|
||||
a-none none 27 3 1 2 3.0 5 27 b=1;a'=1;
|
||||
a-none none 29 3 1 2 5.2 9 4 a'=2;
|
||||
a-none none 28 2 1 1 8.2 4 4 a'=1;
|
||||
a-none none 30 3 1 2 8.8 0 4 a'=1;a=1;
|
||||
a-none none 26 7 1 6 3.9 2 4 a'=4;c''=1;a=1;
|
||||
a-none none 32 4 1 3 2.9 13 14 a'=3;
|
||||
a-none none 33 2 1 1 3.7 3 4 a=1;
|
||||
a-none none 34 2 1 1 2.9 12 25 a=1;
|
||||
a-none none 31 12 1 11 3.0 5 8 a'=7;a=2;c''=1;b=1;
|
||||
a-none none 35 12 1 11 5.8 6 6 a'=9;a=2;
|
||||
a-none none 36 1 1 0 3.3 11 18
|
||||
a-none none 37 4 1 3 10.5 11 4 a'=3;
|
||||
a-none none 39 1 1 0 3.4 5 20
|
||||
a-none none 40 1 1 0 2.7 7 18
|
||||
a-none none 38 4 1 3 2.8 3 4 c'''=1;b=1;a'=1;
|
||||
a-none none 42 1 1 0 4.0 7 4
|
||||
a-none none 43 2 1 1 3.1 6 4 a'=1;
|
||||
a-none none 44 5 1 4 4.7 8 4 a'=4;
|
||||
a-none none 45 9 1 8 3.6 2 8 a'=7;a=1;
|
||||
a-none none 46 5 1 4 4.7 5 4 a'=4;
|
||||
a-none none 47 1 1 0 3.0 3 10
|
||||
a-none none 41 5 1 4 4.0 9 4 c''=2;a'=2;
|
||||
a-none none 48 2 1 1 2.9 1 23 a'=1;
|
||||
a-none none 49 2 1 1 3.8 7 9 a=1;
|
||||
a-none none 51 2 1 1 3.2 9 6 a'=1;
|
||||
a-none none 50 2 1 1 4.3 12 4 a'=1;
|
||||
a-none none 52 1 1 0 8.1 4 10
|
||||
a-none none 54 5 1 4 3.7 3 4 a'=3;a=1;
|
||||
a-none none 56 4 1 3 3.0 15 9 a'=3;
|
||||
a-none none 55 1 1 0 3.2 2 17
|
||||
a-none none 57 1 1 0 2.9 15 9
|
||||
a-none none 58 6 1 5 6.8 9 5 a'=5;
|
||||
a-none none 59 4 1 3 2.9 13 11 a'=3;
|
||||
a-none none 60 2 1 1 3.6 12 11 a'=1;
|
||||
a-none none 53 2 1 1 3.7 1 4 c''=1;
|
||||
a-none none 61 3 1 2 7.9 14 4 a'=2;
|
||||
a-none none 64 4 1 3 3.2 4 25 a'=2;a=1;
|
||||
a-none none 65 4 1 3 5.6 4 4 a'=2;b=1;
|
||||
a-none none 66 6 1 5 4.3 10 4 a'=4;a=1;
|
||||
a-none none 67 4 1 3 5.6 15 4 a'=2;a=1;
|
||||
a-none none 68 2 1 1 5.3 6 27 a'=1;
|
||||
a-none none 62 4 1 3 2.9 7 23 a'=2;c''=1;
|
||||
a-none none 63 15 1 14 3.1 5 16 a'=13;c'''=1;
|
||||
a-none none 69 4 1 3 41.9 0 15 a'=2;a=1;
|
||||
a-none none 70 2 1 1 3.6 15 16 a'=1;
|
||||
a-none none 71 3 1 2 3.2 6 14 c=1;a'=1;
|
||||
a-none none 73 4 1 3 3.6 8 18 a'=3;
|
||||
a-none none 74 1 1 0 3.2 3 14
|
||||
a-none none 75 1 1 0 3.4 0 17
|
||||
a-none none 76 4 1 3 3.6 0 15 a'=2;a=1;
|
||||
a-none none 72 7 1 6 3.2 4 24 a'=5;c''=1;
|
||||
a-none none 78 3 1 2 3.6 6 4 a'=2;
|
||||
a-none none 79 1 1 0 2.8 5 21
|
||||
a-none none 80 5 1 4 2.9 2 7 a'=4;
|
||||
a-none none 81 3 1 2 3.3 11 12 a'=2;
|
||||
a-none none 77 14 1 13 3.2 0 21 a'=9;b=2;c''=1;a=1;
|
||||
a-none none 82 3 1 2 3.4 6 9 a'=2;
|
||||
a-none none 83 1 1 0 3.2 5 5
|
||||
a-none none 85 1 1 0 3.2 2 15
|
||||
a-none none 86 2 1 1 2.9 0 10 a'=1;
|
||||
a-none none 84 3 1 2 12.6 9 4 c'''=1;a'=1;
|
||||
a-none none 87 1 1 0 32.2 9 10
|
||||
a-none none 88 6 1 5 3.3 10 26 a'=4;b=1;
|
||||
a-none none 89 4 1 3 3.6 0 4 a'=2;b=1;
|
||||
a-none none 90 2 1 1 3.0 12 17 a'=1;
|
||||
a-none none 91 1 1 0 2.5 9 24
|
||||
a-none none 92 1 1 0 3.5 14 25
|
||||
a-none none 93 1 1 0 3.5 10 6
|
||||
a-none none 94 1 1 0 5.1 5 4
|
||||
a-none none 95 2 1 1 2.8 11 27 a'=1;
|
||||
a-none none 96 3 1 2 3.8 11 14 a'=2;
|
||||
a-none none 97 1 1 0 3.2 11 15
|
||||
a-none none 99 1 1 0 2.9 12 10
|
||||
a-none none 98 3 1 2 3.5 0 4 c'''=1;a'=1;
|
||||
a-none none 100 2 1 1 2.9 5 26 a'=1;
|
||||
a-none none 101 1 1 0 4.1 13 27
|
||||
a-none none 102 1 1 0 3.3 12 4
|
||||
a-none none 103 1 1 0 3.9 8 4
|
||||
a-none none 104 1 1 0 3.1 5 27
|
||||
a-none none 106 4 1 3 5.0 10 18 a'=2;a=1;
|
||||
a-none none 105 8 1 7 3.3 12 15 a'=6;c'''=1;
|
||||
a-none none 107 3 1 2 9.6 0 27 a'=2;
|
||||
a-none none 108 2 1 1 3.5 0 11 a'=1;
|
||||
a-none none 110 2 1 1 2.6 13 22 a'=1;
|
||||
a-none none 109 1 1 0 3.0 12 16
|
||||
a-none none 111 1 1 0 3.3 2 25
|
||||
a-none none 112 3 1 2 5.5 3 4 b=1;a'=1;
|
||||
a-none none 113 1 1 0 4.3 12 4
|
||||
a-none none 114 10 1 9 2.8 4 24 a'=8;a=1;
|
||||
a-none none 115 1 1 0 3.0 11 4
|
||||
a-none none 116 4 1 3 3.3 8 5 a'=3;
|
||||
a-none none 117 4 1 3 3.6 5 10 a'=3;
|
||||
a-none none 118 3 1 2 3.7 10 4 c=1;a=1;
|
||||
a-none none 119 2 1 1 9.0 3 21 a'=1;
|
||||
a-none none 121 1 1 0 2.8 13 9
|
||||
a-none none 122 1 1 0 25.4 2 4
|
||||
a-none none 120 5 1 4 3.3 5 4 a'=3;c''=1;
|
||||
a-none none 124 1 1 0 3.3 5 12
|
||||
a-none none 125 1 1 0 3.0 9 5
|
||||
a-none none 126 2 1 1 2.8 15 26 a'=1;
|
||||
a-none none 123 2 1 1 33.0 5 5 c'''=1;
|
||||
a-none none 128 2 1 1 5.9 4 27 a'=1;
|
||||
a-none none 127 7 1 6 2.8 2 16 a'=4;c'''=1;a=1;
|
||||
a-none none 129 1 1 0 3.1 14 26
|
||||
a-none none 130 7 1 6 3.0 14 13 a'=5;b=1;
|
||||
a-none none 131 2 1 1 4.2 5 4 a'=1;
|
||||
a-none none 132 1 1 0 3.1 9 16
|
||||
a-none none 133 1 1 0 26.9 0 10
|
||||
a-none none 134 2 1 1 2.8 7 19 a'=1;
|
||||
a-none none 135 2 1 1 3.2 2 23 a'=1;
|
||||
a-none none 136 1 1 0 25.4 6 4
|
||||
a-none none 137 2 1 1 3.0 1 20 a'=1;
|
||||
a-none none 138 9 1 8 3.8 15 10 a'=6;b=1;a=1;
|
||||
a-none none 139 5 1 4 3.2 10 25 a'=4;
|
||||
a-none none 140 4 1 3 10.9 14 11 b=1;a'=1;a=1;
|
||||
a-none none 141 4 1 3 4.7 1 4 a'=2;c=1;
|
||||
a-none none 142 2 1 1 2.6 8 8 a=1;
|
||||
a-none none 144 3 1 2 3.4 14 27 a'=2;
|
||||
a-none none 146 1 1 0 3.6 10 15
|
||||
a-none none 145 1 1 0 3.5 3 10
|
||||
a-none none 143 6 1 5 3.0 2 19 a'=2;a=2;c=1;
|
||||
a-none none 147 2 1 1 3.6 2 22 a'=1;
|
||||
a-none none 148 2 1 1 3.0 11 13 a=1;
|
||||
a-none none 149 2 1 1 3.7 15 8 a'=1;
|
||||
a-none none 150 1 1 0 28.4 3 9
|
||||
a-none none 151 2 1 1 3.2 11 16 a'=1;
|
||||
a-none none 152 2 1 1 11.6 13 19 a'=1;
|
||||
a-none none 153 5 1 4 3.0 4 10 a'=4;
|
||||
a-none none 154 3 1 2 3.1 11 15 a'=2;
|
||||
a-none none 155 7 1 6 2.9 0 16 a'=3;a=3;
|
||||
a-none none 156 2 1 1 5.6 13 4 a=1;
|
||||
a-none none 157 2 1 1 2.6 12 14 a'=1;
|
||||
a-none none 158 8 1 7 2.9 0 6 a'=6;a=1;
|
||||
a-none none 159 1 1 0 3.4 12 24
|
||||
a-none none 160 2 1 1 2.7 0 7 a'=1;
|
||||
a-none none 161 6 1 5 6.3 4 4 a'=5;
|
||||
a-none none 162 3 1 2 4.5 13 24 a'=2;
|
||||
a-none none 163 5 1 4 3.1 5 22 a'=4;
|
||||
a-none none 164 3 1 2 2.5 4 22 a'=1;a=1;
|
||||
a-none none 165 4 1 3 3.6 7 19 a'=3;
|
||||
a-none none 166 8 1 7 3.6 4 8 a'=7;
|
||||
a-none none 168 1 1 0 4.1 1 4
|
||||
a-none none 167 5 1 4 3.3 13 16 a'=3;a=1;
|
||||
a-none none 169 1 1 0 3.5 5 5
|
||||
a-none none 170 3 1 2 3.0 15 12 c=1;a'=1;
|
||||
a-none none 171 1 1 0 2.5 5 14
|
||||
a-none none 172 5 1 4 5.0 8 7 a'=3;a=1;
|
||||
a-none none 173 4 1 3 2.9 11 24 a'=3;
|
||||
a-none none 174 2 1 1 2.9 0 25 a=1;
|
||||
a-none none 175 1 1 0 3.1 8 25
|
||||
a-none none 176 6 1 5 3.7 14 19 a'=4;a=1;
|
||||
a-none none 177 1 1 0 2.6 4 24
|
||||
a-none none 179 1 1 0 2.7 0 7
|
||||
a-none none 180 1 1 0 3.5 13 12
|
||||
a-none none 181 3 1 2 2.9 6 11 a'=2;
|
||||
a-none none 182 1 1 0 3.0 5 22
|
||||
a-none none 178 4 1 3 2.8 3 8 a'=2;c''=1;
|
||||
a-none none 184 6 1 5 2.7 6 24 a'=4;b=1;
|
||||
a-none none 183 3 1 2 3.4 12 5 a'=2;
|
||||
a-none none 185 2 1 1 3.3 5 4 a'=1;
|
||||
a-none none 186 1 1 0 15.3 1 26
|
||||
a-none none 187 6 1 5 2.7 14 17 a'=4;a=1;
|
||||
a-none none 189 3 1 2 3.2 0 8 a'=2;
|
||||
a-none none 191 2 1 1 2.8 6 14 a'=1;
|
||||
a-none none 192 2 1 1 6.2 15 4 a'=1;
|
||||
a-none none 193 1 1 0 3.6 12 17
|
||||
a-none none 190 4 1 3 3.1 11 24 c'''=1;b=1;a'=1;
|
||||
a-none none 194 17 1 16 3.2 2 11 a'=10;b=3;c=2;a=1;
|
||||
a-none none 195 2 1 1 3.2 5 24 a'=1;
|
||||
a-none none 188 8 1 7 4.3 15 25 a'=5;c'''=1;c''=1;
|
||||
a-none none 196 3 1 2 4.3 15 4 a'=2;
|
||||
a-none none 197 2 1 1 4.0 15 4 a'=1;
|
||||
a-none none 198 7 1 6 3.5 6 4 a'=5;b=1;
|
||||
a-none none 199 1 1 0 3.2 4 8
|
||||
a-none none 200 10 1 9 3.1 4 8 a'=6;a=3;
|
||||
a-none none 201 4 1 3 2.8 14 23 a'=3;
|
||||
a-none none 202 3 1 2 4.8 0 12 a'=2;
|
||||
a-none none 203 4 1 3 3.5 10 16 a'=3;
|
||||
a-none none 204 2 1 1 2.8 7 6 a=1;
|
||||
a-none none 205 1 1 0 2.8 11 10
|
||||
a-none none 206 11 1 10 3.5 2 17 a'=9;a=1;
|
||||
a-none none 207 2 1 1 3.3 1 27 a'=1;
|
||||
a-none none 208 4 1 3 3.5 9 11 a'=2;a=1;
|
||||
a-none none 210 3 1 2 4.1 11 4 a'=2;
|
||||
a-none none 209 12 1 11 3.0 12 19 a'=9;c=1;a=1;
|
||||
a-none none 211 3 1 2 3.2 3 15 a'=1;a=1;
|
||||
a-none none 212 4 1 3 3.5 1 17 a'=3;
|
||||
a-none none 213 3 1 2 8.7 2 20 a'=2;
|
||||
a-none none 215 2 1 1 3.6 4 19 a'=1;
|
||||
a-none none 214 6 1 5 12.0 2 4 a'=5;
|
||||
a-none none 216 2 1 1 3.3 10 25 a'=1;
|
||||
a-none none 217 1 1 0 3.1 1 15
|
||||
a-none none 219 7 1 6 3.5 0 5 a'=6;
|
||||
a-none none 220 1 1 0 2.9 6 13
|
||||
a-none none 218 3 1 2 2.9 0 20 c=1;a'=1;
|
||||
a-none none 221 3 1 2 3.4 2 5 a'=2;
|
||||
a-none none 223 1 1 0 3.0 8 9
|
||||
a-none none 224 3 1 2 3.0 9 27 a'=2;
|
||||
a-none none 225 2 1 1 3.7 9 26 a'=1;
|
||||
a-none none 226 1 1 0 30.2 14 16
|
||||
a-none none 222 5 1 4 2.6 5 6 a'=3;c'''=1;
|
||||
a-none none 227 5 1 4 3.6 2 19 a'=4;
|
||||
a-none none 228 1 1 0 3.0 13 4
|
||||
a-none none 229 1 1 0 2.9 12 24
|
||||
a-none none 231 4 1 3 2.7 15 17 a'=3;
|
||||
a-none none 232 3 1 2 3.0 5 5 a'=2;
|
||||
a-none none 233 1 1 0 4.6 13 4
|
||||
a-none none 234 1 1 0 2.9 10 17
|
||||
a-none none 235 1 1 0 4.0 0 25
|
||||
a-none none 230 9 1 8 5.6 3 5 a'=7;c''=1;
|
||||
a-none none 236 1 1 0 2.8 4 24
|
||||
a-none none 237 4 1 3 3.3 5 18 a'=3;
|
||||
a-none none 238 7 1 6 4.8 12 5 a'=5;a=1;
|
||||
a-none none 239 1 1 0 3.7 1 22
|
||||
a-none none 240 1 1 0 5.0 6 4
|
||||
a-none none 241 11 1 10 5.6 2 4 a'=10;
|
||||
a-none none 242 6 1 5 3.5 1 21 a'=5;
|
||||
a-none none 243 1 1 0 3.2 1 22
|
||||
a-none none 244 3 1 2 3.2 8 5 a'=2;
|
||||
a-none none 245 6 1 5 3.4 10 20 a'=5;
|
||||
a-none none 247 2 1 1 4.3 6 4 a'=1;
|
||||
a-none none 248 3 1 2 13.4 1 4 a'=2;
|
||||
a-none none 249 1 1 0 4.1 2 27
|
||||
a-none none 250 2 1 1 3.1 8 19 a'=1;
|
||||
a-none none 251 3 1 2 3.4 9 11 b=1;a'=1;
|
||||
a-none none 246 4 1 3 4.0 11 5 a'=2;c'''=1;
|
||||
a-none none 252 4 1 3 2.7 2 27 a'=2;a=1;
|
||||
a-none none 253 3 1 2 3.5 12 25 a'=2;
|
||||
a-none none 254 5 1 4 2.8 0 10 a'=4;
|
||||
a-none none 255 1 1 0 3.1 10 8
|
||||
# end 2026-10-08T12:41:48Z rows=257
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
259
docs/analysis/class-v6/logs/census/b-era.tsv
Normal file
259
docs/analysis/class-v6/logs/census/b-era.tsv
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
# b-era class w16m8g+sh256x27 eras [0,1,2,3,4,5,6,7] seeds 32 era-widths 16 weights 13,11,6,10,8,8,7,2,6,4 bittest 1 bin d601098699840534 start 2026-10-08T12:58:31Z host igneum-build-3 threads 8
|
||||
cand era seed candidates accepted attempt bit_z bit_site bit_bit reasons
|
||||
b-era 0 1 2 1 1 2.6 11 10 c,=1;
|
||||
b-era 0 5 3 1 2 5.3 11 19 a'=2;
|
||||
b-era 0 6 2 1 1 3.0 6 7 a'=1;
|
||||
b-era 0 2 2 1 1 3.2 7 23 c,=1;
|
||||
b-era 0 3 2 1 1 3.0 14 21 c,=1;
|
||||
b-era 0 7 3 1 2 3.4 1 11 c,=1;a'=1;
|
||||
b-era 0 0 5 1 4 3.7 15 25 c,=2;a'=2;
|
||||
b-era 0 4 9 1 8 2.9 12 25 a'=5;c,=2;a=1;
|
||||
b-era 0 9 2 1 1 3.3 15 21 c,=1;
|
||||
b-era 0 8 1 1 0 3.1 7 15
|
||||
b-era 0 12 5 1 4 3.5 6 2 c,=2;a'=1;a=1;
|
||||
b-era 0 11 9 1 8 3.2 6 22 c,=7;a'=1;
|
||||
b-era 0 13 3 1 2 3.0 7 12 c,=1;a'=1;
|
||||
b-era 0 14 6 1 5 3.2 10 26 a'=3;c,=2;
|
||||
b-era 0 15 6 1 5 2.9 1 10 c,=3;a'=2;
|
||||
b-era 0 10 10 1 9 2.9 7 19 c,=8;a=1;
|
||||
b-era 0 16 1 1 0 3.9 7 22
|
||||
b-era 0 17 3 1 2 3.6 6 17 c,=1;a'=1;
|
||||
b-era 0 18 2 1 1 5.1 12 21 c,=1;
|
||||
b-era 0 19 1 1 0 3.0 1 13
|
||||
b-era 0 20 3 1 2 3.4 13 12 c,=1;a'=1;
|
||||
b-era 0 21 4 1 3 3.5 9 15 a'=2;c,=1;
|
||||
b-era 0 22 2 1 1 3.1 9 5 a'=1;
|
||||
b-era 0 23 2 1 1 3.0 4 16 a=1;
|
||||
b-era 0 24 1 1 0 3.0 1 15
|
||||
b-era 0 25 6 1 5 3.0 2 25 c,=4;a=1;
|
||||
b-era 0 27 2 1 1 3.0 4 19 a'=1;
|
||||
b-era 0 28 9 1 8 3.4 12 7 a'=4;c,=3;a=1;
|
||||
b-era 0 29 2 1 1 3.6 1 3 c,=1;
|
||||
b-era 0 26 11 1 10 3.3 1 5 c,=6;a'=3;c=1;
|
||||
b-era 0 30 5 1 4 2.9 5 19 a'=3;a=1;
|
||||
b-era 0 31 1 1 0 3.0 8 21
|
||||
b-era 1 0 5 1 4 3.2 14 24 c,=2;a'=2;
|
||||
b-era 1 3 2 1 1 3.0 3 3 c,=1;
|
||||
b-era 1 1 2 1 1 3.6 13 3 c,=1;
|
||||
b-era 1 2 2 1 1 3.5 9 24 c,=1;
|
||||
b-era 1 5 3 1 2 4.5 11 6 a'=2;
|
||||
b-era 1 4 12 1 11 3.4 8 2 c,=5;a'=5;a=1;
|
||||
b-era 1 6 2 1 1 3.1 2 12 a'=1;
|
||||
b-era 1 7 3 1 2 3.1 6 16 c,=1;a'=1;
|
||||
b-era 1 8 1 1 0 3.8 1 5
|
||||
b-era 1 9 2 1 1 2.8 15 21 c,=1;
|
||||
b-era 1 11 9 1 8 2.7 15 16 c,=7;a'=1;
|
||||
b-era 1 12 5 1 4 3.2 9 7 c,=2;a'=1;a=1;
|
||||
b-era 1 10 10 1 9 3.2 2 12 c,=8;a=1;
|
||||
b-era 1 14 6 1 5 2.9 6 4 a'=3;c,=2;
|
||||
b-era 1 13 3 1 2 2.7 1 18 c,=1;a'=1;
|
||||
b-era 1 15 6 1 5 3.6 4 3 c,=3;a'=2;
|
||||
b-era 1 16 1 1 0 2.9 8 27
|
||||
b-era 1 17 3 1 2 2.6 12 2 c,=1;a'=1;
|
||||
b-era 1 19 1 1 0 3.5 5 6
|
||||
b-era 1 20 3 1 2 3.2 11 20 c,=1;a'=1;
|
||||
b-era 1 21 4 1 3 3.3 10 25 a'=2;c,=1;
|
||||
b-era 1 18 3 1 2 2.7 1 23 c,=2;
|
||||
b-era 1 22 2 1 1 3.1 8 3 a'=1;
|
||||
b-era 1 23 2 1 1 2.9 15 4 a=1;
|
||||
b-era 1 24 1 1 0 3.0 2 12
|
||||
b-era 1 27 2 1 1 3.9 2 19 a'=1;
|
||||
b-era 1 25 6 1 5 2.9 4 13 c,=4;a=1;
|
||||
b-era 1 28 9 1 8 2.7 12 8 a'=4;c,=3;a=1;
|
||||
b-era 1 29 2 1 1 3.2 13 5 c,=1;
|
||||
b-era 1 30 5 1 4 3.0 6 12 a'=3;a=1;
|
||||
b-era 1 26 11 1 10 3.3 10 12 c,=6;a'=3;c=1;
|
||||
b-era 1 31 1 1 0 3.7 0 17
|
||||
b-era 2 0 2 1 1 3.2 6 17 a'=1;
|
||||
b-era 2 1 1 1 0 4.0 5 10
|
||||
b-era 2 3 2 1 1 3.7 0 18 c,=1;
|
||||
b-era 2 2 1 1 0 3.1 2 23
|
||||
b-era 2 4 1 1 0 3.2 7 5
|
||||
b-era 2 6 2 1 1 3.5 5 11 a'=1;
|
||||
b-era 2 5 3 1 2 3.5 5 19 a'=2;
|
||||
b-era 2 7 1 1 0 2.9 11 18
|
||||
b-era 2 8 1 1 0 3.0 10 19
|
||||
b-era 2 9 1 1 0 3.3 1 18
|
||||
b-era 2 11 1 1 0 2.8 11 3
|
||||
b-era 2 10 1 1 0 3.4 0 16
|
||||
b-era 2 12 2 1 1 3.6 1 25 a'=1;
|
||||
b-era 2 13 1 1 0 3.4 4 20
|
||||
b-era 2 14 1 1 0 3.3 3 9
|
||||
b-era 2 15 2 1 1 3.1 12 9 a'=1;
|
||||
b-era 2 16 1 1 0 2.5 6 19
|
||||
b-era 2 18 1 1 0 3.1 1 15
|
||||
b-era 2 17 2 1 1 4.0 10 19 a'=1;
|
||||
b-era 2 20 1 1 0 4.9 5 3
|
||||
b-era 2 19 1 1 0 2.9 2 20
|
||||
b-era 2 21 3 1 2 2.5 7 15 a'=2;
|
||||
b-era 2 22 2 1 1 2.9 3 14 a'=1;
|
||||
b-era 2 23 2 1 1 3.0 7 22 a=1;
|
||||
b-era 2 24 1 1 0 3.0 2 24
|
||||
b-era 2 25 1 1 0 2.9 2 23
|
||||
b-era 2 27 2 1 1 3.1 15 9 a'=1;
|
||||
b-era 2 26 1 1 0 3.7 6 15
|
||||
b-era 2 28 5 1 4 3.4 12 20 a'=3;a=1;
|
||||
b-era 2 29 1 1 0 3.8 2 9
|
||||
b-era 2 30 5 1 4 3.2 5 10 a'=3;a=1;
|
||||
b-era 2 31 1 1 0 3.5 3 25
|
||||
b-era 3 0 2 1 1 3.2 2 8 a'=1;
|
||||
b-era 3 1 1 1 0 3.2 15 13
|
||||
b-era 3 4 6 1 5 3.4 9 23 a'=4;c,=1;
|
||||
b-era 3 3 2 1 1 4.1 11 4 c,=1;
|
||||
b-era 3 2 1 1 0 2.9 8 5
|
||||
b-era 3 5 3 1 2 5.0 11 3 a'=2;
|
||||
b-era 3 6 2 1 1 3.3 3 22 a'=1;
|
||||
b-era 3 7 1 1 0 3.1 2 18
|
||||
b-era 3 9 1 1 0 2.9 1 4
|
||||
b-era 3 8 1 1 0 3.0 10 16
|
||||
b-era 3 11 1 1 0 3.6 2 15
|
||||
b-era 3 12 2 1 1 3.2 14 14 a'=1;
|
||||
b-era 3 10 1 1 0 3.0 4 12
|
||||
b-era 3 13 1 1 0 3.3 10 8
|
||||
b-era 3 14 1 1 0 2.8 0 16
|
||||
b-era 3 15 2 1 1 4.5 14 2 a'=1;
|
||||
b-era 3 16 1 1 0 3.0 1 21
|
||||
b-era 3 17 2 1 1 3.1 14 2 a'=1;
|
||||
b-era 3 18 1 1 0 3.2 10 20
|
||||
b-era 3 19 1 1 0 2.7 14 25
|
||||
b-era 3 20 3 1 2 3.1 12 24 c,=1;a'=1;
|
||||
b-era 3 21 3 1 2 4.5 12 8 a'=2;
|
||||
b-era 3 22 2 1 1 2.6 11 11 a'=1;
|
||||
b-era 3 23 2 1 1 2.9 2 18 a=1;
|
||||
b-era 3 24 1 1 0 3.4 13 16
|
||||
b-era 3 25 1 1 0 3.9 2 7
|
||||
b-era 3 26 1 1 0 2.7 0 23
|
||||
b-era 3 28 5 1 4 3.4 2 25 a'=3;a=1;
|
||||
b-era 3 27 2 1 1 2.9 13 2 a'=1;
|
||||
b-era 3 29 1 1 0 3.3 11 17
|
||||
b-era 3 30 5 1 4 3.7 0 19 a'=3;a=1;
|
||||
b-era 3 31 1 1 0 2.8 10 21
|
||||
b-era 4 0 5 1 4 3.0 15 8 c,=2;a'=2;
|
||||
b-era 4 1 2 1 1 3.0 10 15 c,=1;
|
||||
b-era 4 3 2 1 1 4.0 13 21 c,=1;
|
||||
b-era 4 2 2 1 1 3.0 7 19 c,=1;
|
||||
b-era 4 4 9 1 8 2.7 6 7 a'=5;c,=2;a=1;
|
||||
b-era 4 5 3 1 2 4.3 11 10 a'=2;
|
||||
b-era 4 6 2 1 1 3.1 3 11 a'=1;
|
||||
b-era 4 7 3 1 2 4.1 7 9 c,=1;a'=1;
|
||||
b-era 4 8 1 1 0 3.2 9 19
|
||||
b-era 4 9 2 1 1 2.7 6 10 c,=1;
|
||||
b-era 4 12 5 1 4 2.7 8 24 c,=2;a'=1;a=1;
|
||||
b-era 4 11 9 1 8 4.0 9 19 c,=7;a'=1;
|
||||
b-era 4 10 10 1 9 3.1 12 12 c,=8;a=1;
|
||||
b-era 4 13 3 1 2 2.5 3 8 c,=1;a'=1;
|
||||
b-era 4 14 6 1 5 2.7 13 23 a'=3;c,=2;
|
||||
b-era 4 15 6 1 5 3.5 9 13 c,=3;a'=2;
|
||||
b-era 4 16 1 1 0 3.6 0 6
|
||||
b-era 4 17 3 1 2 3.7 6 6 c,=1;a'=1;
|
||||
b-era 4 18 3 1 2 3.5 1 22 c,=2;
|
||||
b-era 4 20 3 1 2 3.1 3 21 c,=1;a'=1;
|
||||
b-era 4 19 1 1 0 3.4 12 13
|
||||
b-era 4 21 4 1 3 3.2 6 17 a'=2;c,=1;
|
||||
b-era 4 22 2 1 1 3.1 10 3 a'=1;
|
||||
b-era 4 23 2 1 1 3.7 1 9 a=1;
|
||||
b-era 4 24 1 1 0 2.8 6 6
|
||||
b-era 4 25 6 1 5 2.8 12 21 c,=4;a=1;
|
||||
b-era 4 27 2 1 1 3.3 0 12 a'=1;
|
||||
b-era 4 28 9 1 8 3.9 6 20 a'=4;c,=3;a=1;
|
||||
b-era 4 26 11 1 10 3.1 10 10 c,=6;a'=3;c=1;
|
||||
b-era 4 29 2 1 1 2.9 10 20 c,=1;
|
||||
b-era 4 30 5 1 4 3.5 14 23 a'=3;a=1;
|
||||
b-era 4 31 1 1 0 2.6 10 11
|
||||
b-era 5 0 5 1 4 3.4 4 19 c,=2;a'=2;
|
||||
b-era 5 1 2 1 1 2.5 8 25 c,=1;
|
||||
b-era 5 2 2 1 1 3.1 11 25 c,=1;
|
||||
b-era 5 3 2 1 1 3.5 14 25 c,=1;
|
||||
b-era 5 4 9 1 8 3.7 15 22 a'=5;c,=2;a=1;
|
||||
b-era 5 5 3 1 2 5.2 11 23 a'=2;
|
||||
b-era 5 6 2 1 1 3.1 6 22 a'=1;
|
||||
b-era 5 7 3 1 2 3.0 8 11 c,=1;a'=1;
|
||||
b-era 5 8 1 1 0 3.8 9 18
|
||||
b-era 5 9 2 1 1 4.0 13 2 c,=1;
|
||||
b-era 5 10 10 1 9 2.8 5 7 c,=8;a=1;
|
||||
b-era 5 11 9 1 8 3.2 13 13 c,=7;a'=1;
|
||||
b-era 5 12 5 1 4 3.3 3 25 c,=2;a'=1;a=1;
|
||||
b-era 5 13 3 1 2 3.2 12 26 c,=1;a'=1;
|
||||
b-era 5 14 6 1 5 3.3 10 22 a'=3;c,=2;
|
||||
b-era 5 15 6 1 5 3.2 5 2 c,=3;a'=2;
|
||||
b-era 5 16 1 1 0 3.1 4 26
|
||||
b-era 5 17 3 1 2 3.3 7 27 c,=1;a'=1;
|
||||
b-era 5 18 3 1 2 3.2 1 12 c,=2;
|
||||
b-era 5 19 1 1 0 3.3 3 22
|
||||
b-era 5 20 3 1 2 2.7 7 11 c,=1;a'=1;
|
||||
b-era 5 21 4 1 3 3.7 3 15 a'=2;c,=1;
|
||||
b-era 5 22 2 1 1 4.1 3 5 a'=1;
|
||||
b-era 5 23 2 1 1 3.5 2 19 a=1;
|
||||
b-era 5 24 1 1 0 2.7 5 8
|
||||
b-era 5 27 2 1 1 3.0 1 6 a'=1;
|
||||
b-era 5 25 6 1 5 3.0 4 23 c,=4;a=1;
|
||||
b-era 5 26 11 1 10 2.5 8 24 c,=6;a'=3;c=1;
|
||||
b-era 5 29 2 1 1 3.0 3 3 c,=1;
|
||||
b-era 5 28 9 1 8 3.3 1 21 a'=4;c,=3;a=1;
|
||||
b-era 5 30 5 1 4 3.2 10 8 a'=3;a=1;
|
||||
b-era 5 31 1 1 0 3.6 10 4
|
||||
b-era 6 0 5 1 4 2.4 10 11 c,=2;a'=2;
|
||||
b-era 6 1 2 1 1 3.5 15 18 c,=1;
|
||||
b-era 6 2 2 1 1 3.3 1 9 c,=1;
|
||||
b-era 6 4 9 1 8 3.1 1 7 a'=5;c,=2;a=1;
|
||||
b-era 6 3 2 1 1 3.0 12 18 c,=1;
|
||||
b-era 6 5 3 1 2 4.1 11 5 a'=2;
|
||||
b-era 6 6 2 1 1 3.2 1 20 a'=1;
|
||||
b-era 6 7 3 1 2 2.8 3 22 c,=1;a'=1;
|
||||
b-era 6 8 1 1 0 3.8 3 20
|
||||
b-era 6 9 2 1 1 2.9 6 18 c,=1;
|
||||
b-era 6 14 6 1 5 3.2 2 5 a'=3;c,=2;
|
||||
b-era 6 13 3 1 2 3.8 5 10 c,=1;a'=1;
|
||||
b-era 6 10 10 1 9 3.2 2 24 c,=8;a=1;
|
||||
b-era 6 12 5 1 4 3.1 3 17 c,=2;a'=1;a=1;
|
||||
b-era 6 11 9 1 8 3.2 8 11 c,=7;a'=1;
|
||||
b-era 6 15 6 1 5 2.9 12 13 c,=3;a'=2;
|
||||
b-era 6 16 1 1 0 3.5 8 3
|
||||
b-era 6 17 3 1 2 3.8 15 5 c,=1;a'=1;
|
||||
b-era 6 21 4 1 3 3.8 4 21 a'=2;c,=1;
|
||||
b-era 6 20 3 1 2 2.9 15 10 c,=1;a'=1;
|
||||
b-era 6 18 3 1 2 3.2 14 20 c,=2;
|
||||
b-era 6 19 1 1 0 3.3 10 3
|
||||
b-era 6 22 2 1 1 3.5 0 11 a'=1;
|
||||
b-era 6 23 2 1 1 2.9 11 20 a=1;
|
||||
b-era 6 24 1 1 0 3.2 0 16
|
||||
b-era 6 25 6 1 5 3.0 14 24 c,=4;a=1;
|
||||
b-era 6 29 2 1 1 3.0 14 5 c,=1;
|
||||
b-era 6 27 2 1 1 4.2 0 2 a'=1;
|
||||
b-era 6 26 11 1 10 3.5 12 6 c,=6;a'=3;c=1;
|
||||
b-era 6 30 5 1 4 3.7 0 14 a'=3;a=1;
|
||||
b-era 6 28 9 1 8 3.4 15 25 a'=4;c,=3;a=1;
|
||||
b-era 6 31 1 1 0 3.1 2 20
|
||||
b-era 7 0 2 1 1 2.9 13 20 a'=1;
|
||||
b-era 7 1 1 1 0 3.4 12 23
|
||||
b-era 7 3 2 1 1 3.0 13 18 c,=1;
|
||||
b-era 7 2 2 1 1 2.9 4 9 c,=1;
|
||||
b-era 7 4 1 1 0 5.1 0 3
|
||||
b-era 7 6 2 1 1 3.2 10 3 a'=1;
|
||||
b-era 7 5 3 1 2 3.1 1 9 a'=2;
|
||||
b-era 7 7 1 1 0 3.0 4 8
|
||||
b-era 7 8 1 1 0 3.4 15 20
|
||||
b-era 7 9 2 1 1 3.8 10 23 c,=1;
|
||||
b-era 7 11 1 1 0 3.2 7 23
|
||||
b-era 7 10 1 1 0 3.2 10 26
|
||||
b-era 7 12 2 1 1 3.4 6 13 a'=1;
|
||||
b-era 7 13 3 1 2 3.0 4 4 c,=1;a'=1;
|
||||
b-era 7 14 1 1 0 3.0 1 27
|
||||
b-era 7 15 6 1 5 2.9 5 11 c,=3;a'=2;
|
||||
b-era 7 16 1 1 0 2.9 0 23
|
||||
b-era 7 17 2 1 1 3.2 11 14 a'=1;
|
||||
b-era 7 18 2 1 1 3.4 5 24 c,=1;
|
||||
b-era 7 20 1 1 0 3.1 6 14
|
||||
b-era 7 19 1 1 0 2.6 8 10
|
||||
b-era 7 21 4 1 3 3.2 2 19 a'=2;c,=1;
|
||||
b-era 7 22 2 1 1 3.6 3 3 a'=1;
|
||||
b-era 7 23 2 1 1 3.4 14 20 a=1;
|
||||
b-era 7 24 1 1 0 2.6 10 15
|
||||
b-era 7 25 1 1 0 2.7 2 12
|
||||
b-era 7 26 7 1 6 3.0 2 3 c,=3;a'=2;c=1;
|
||||
b-era 7 27 2 1 1 3.0 12 8 a'=1;
|
||||
b-era 7 28 5 1 4 3.1 0 11 a'=3;a=1;
|
||||
b-era 7 29 1 1 0 2.8 8 27
|
||||
b-era 7 30 5 1 4 3.8 11 25 a'=3;a=1;
|
||||
b-era 7 31 1 1 0 3.3 10 25
|
||||
# end 2026-10-08T13:01:58Z rows=257
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
258
docs/analysis/class-v6/logs/census/b-none.tsv
Normal file
258
docs/analysis/class-v6/logs/census/b-none.tsv
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
# b-none class w16m8g+sh256x27 eras [none] seeds 256 era-widths 16 weights 13,11,6,10,8,8,7,2,6,4 bittest off bin d601098699840534 start 2026-10-08T12:45:53Z host igneum-build-4 threads 16
|
||||
cand era seed candidates accepted attempt bit_z bit_site bit_bit reasons
|
||||
b-none none 0 2 1 1 3.9 11 3 a'=1;
|
||||
b-none none 2 5 1 4 3.4 15 18 a'=3;a=1;
|
||||
b-none none 11 1 1 0 2.7 8 6
|
||||
b-none none 12 1 1 0 3.4 13 11
|
||||
b-none none 13 1 1 0 3.3 15 16
|
||||
b-none none 14 1 1 0 16.0 11 2
|
||||
b-none none 15 2 1 1 3.7 0 27 a'=1;
|
||||
b-none none 1 1 1 0 3.3 14 3
|
||||
b-none none 17 1 1 0 20.1 10 2
|
||||
b-none none 16 4 1 3 3.6 10 23 a'=3;
|
||||
b-none none 18 1 1 0 3.0 8 24
|
||||
b-none none 10 1 1 0 2.9 8 10
|
||||
b-none none 19 1 1 0 23.6 4 2
|
||||
b-none none 6 2 1 1 16.2 2 2 c=1;
|
||||
b-none none 4 2 1 1 3.2 13 26 a'=1;
|
||||
b-none none 5 1 1 0 2.5 15 9
|
||||
b-none none 8 2 1 1 5.0 2 17 a'=1;
|
||||
b-none none 7 1 1 0 15.8 0 2
|
||||
b-none none 9 4 1 3 57.7 2 10 a'=2;a=1;
|
||||
b-none none 3 1 1 0 16.4 9 2
|
||||
b-none none 22 1 1 0 31.0 5 23
|
||||
b-none none 23 2 1 1 7.0 3 2 a'=1;
|
||||
b-none none 28 1 1 0 2.8 2 20
|
||||
b-none none 24 2 1 1 3.2 13 3 a'=1;
|
||||
b-none none 30 3 1 2 9.8 0 4 a'=1;a=1;
|
||||
b-none none 25 1 1 0 3.9 0 19
|
||||
b-none none 20 1 1 0 19.1 10 2
|
||||
b-none none 29 3 1 2 2.9 12 12 a'=2;
|
||||
b-none none 21 4 1 3 3.2 15 12 a'=3;
|
||||
b-none none 27 3 1 2 3.2 11 17 c=1;b=1;
|
||||
b-none none 36 1 1 0 3.3 2 21
|
||||
b-none none 40 1 1 0 3.5 12 3
|
||||
b-none none 42 1 1 0 3.0 7 23
|
||||
b-none none 31 4 1 3 2.9 15 11 a'=3;
|
||||
b-none none 26 1 1 0 9.0 10 2
|
||||
b-none none 34 2 1 1 3.0 14 11 a=1;
|
||||
b-none none 32 1 1 0 2.8 4 2
|
||||
b-none none 33 2 1 1 28.7 1 19 a=1;
|
||||
b-none none 35 2 1 1 29.4 2 2 a'=1;
|
||||
b-none none 37 2 1 1 3.5 4 13 a'=1;
|
||||
b-none none 48 2 1 1 3.0 10 22 a'=1;
|
||||
b-none none 39 1 1 0 2.9 7 10
|
||||
b-none none 38 1 1 0 10.7 3 27
|
||||
b-none none 43 2 1 1 3.0 1 18 a'=1;
|
||||
b-none none 41 2 1 1 3.9 0 2 a'=1;
|
||||
b-none none 44 1 1 0 3.0 10 12
|
||||
b-none none 45 1 1 0 17.7 15 2
|
||||
b-none none 46 2 1 1 71.0 2 23 a'=1;
|
||||
b-none none 51 2 1 1 3.3 9 11 a'=1;
|
||||
b-none none 47 2 1 1 11.8 13 2 a'=1;
|
||||
b-none none 50 1 1 0 41.5 3 2
|
||||
b-none none 56 1 1 0 3.3 15 3
|
||||
b-none none 49 2 1 1 3.1 4 10 a'=1;
|
||||
b-none none 55 1 1 0 16.4 3 2
|
||||
b-none none 61 3 1 2 17.9 12 2 a'=2;
|
||||
b-none none 52 1 1 0 17.6 8 2
|
||||
b-none none 54 5 1 4 3.8 1 3 a'=2;c'''=1;a=1;
|
||||
b-none none 63 2 1 1 17.3 6 2 a'=1;
|
||||
b-none none 57 1 1 0 3.4 3 15
|
||||
b-none none 59 1 1 0 2.8 15 21
|
||||
b-none none 68 2 1 1 15.5 1 2 a'=1;
|
||||
b-none none 67 1 1 0 9.5 3 2
|
||||
b-none none 58 1 1 0 2.9 10 16
|
||||
b-none none 70 1 1 0 4.2 12 2
|
||||
b-none none 69 2 1 1 16.8 13 2 a=1;
|
||||
b-none none 71 1 1 0 2.9 9 18
|
||||
b-none none 74 1 1 0 6.5 12 2
|
||||
b-none none 53 2 1 1 3.0 14 16 c''=1;
|
||||
b-none none 62 1 1 0 15.5 11 2
|
||||
b-none none 64 1 1 0 3.2 10 23
|
||||
b-none none 60 2 1 1 3.7 12 18 a'=1;
|
||||
b-none none 65 1 1 0 3.0 9 15
|
||||
b-none none 72 3 1 2 3.8 5 21 c'''=1;a'=1;
|
||||
b-none none 76 6 1 5 7.2 0 15 a'=4;a=1;
|
||||
b-none none 66 2 1 1 39.1 10 2 a=1;
|
||||
b-none none 81 2 1 1 64.0 6 25 a'=1;
|
||||
b-none none 73 1 1 0 3.0 13 5
|
||||
b-none none 82 1 1 0 2.6 5 8
|
||||
b-none none 84 1 1 0 3.5 5 6
|
||||
b-none none 75 1 1 0 3.7 5 18
|
||||
b-none none 85 1 1 0 3.3 2 26
|
||||
b-none none 83 1 1 0 3.1 15 26
|
||||
b-none none 86 1 1 0 3.7 7 16
|
||||
b-none none 87 1 1 0 32.3 9 10
|
||||
b-none none 79 1 1 0 17.5 8 2
|
||||
b-none none 80 1 1 0 3.0 8 6
|
||||
b-none none 78 1 1 0 2.7 5 18
|
||||
b-none none 92 1 1 0 8.6 7 2
|
||||
b-none none 91 1 1 0 15.2 12 2
|
||||
b-none none 90 2 1 1 3.0 7 18 c''=1;
|
||||
b-none none 88 1 1 0 3.1 14 18
|
||||
b-none none 89 2 1 1 3.6 5 3 a'=1;
|
||||
b-none none 77 3 1 2 2.8 14 17 c''=1;a=1;
|
||||
b-none none 97 1 1 0 2.8 4 22
|
||||
b-none none 96 1 1 0 2.9 3 17
|
||||
b-none none 99 2 1 1 2.8 4 21 c''=1;
|
||||
b-none none 93 1 1 0 4.0 4 11
|
||||
b-none none 103 1 1 0 16.4 8 2
|
||||
b-none none 105 1 1 0 3.6 12 14
|
||||
b-none none 111 1 1 0 2.8 7 3
|
||||
b-none none 98 1 1 0 56.0 0 5
|
||||
b-none none 94 1 1 0 16.4 5 2
|
||||
b-none none 114 2 1 1 3.4 11 23 c=1;
|
||||
b-none none 95 2 1 1 3.5 8 19 a'=1;
|
||||
b-none none 113 1 1 0 13.4 12 2
|
||||
b-none none 100 1 1 0 5.0 1 26
|
||||
b-none none 107 3 1 2 3.2 10 17 a'=2;
|
||||
b-none none 101 2 1 1 4.4 11 2 c''=1;
|
||||
b-none none 104 1 1 0 3.2 15 19
|
||||
b-none none 102 1 1 0 16.2 9 2
|
||||
b-none none 106 1 1 0 4.7 3 2
|
||||
b-none none 118 2 1 1 15.5 9 2 a=1;
|
||||
b-none none 117 1 1 0 3.1 12 8
|
||||
b-none none 121 1 1 0 7.1 5 19
|
||||
b-none none 122 1 1 0 15.1 2 2
|
||||
b-none none 112 2 1 1 13.2 7 2 a'=1;
|
||||
b-none none 108 1 1 0 65.3 15 10
|
||||
b-none none 110 2 1 1 3.0 3 22 a'=1;
|
||||
b-none none 109 1 1 0 3.5 12 25
|
||||
b-none none 124 1 1 0 3.1 12 19
|
||||
b-none none 119 1 1 0 5.6 9 3
|
||||
b-none none 123 1 1 0 18.2 13 17
|
||||
b-none none 128 6 1 5 3.0 13 16 a'=3;c=1;a=1;
|
||||
b-none none 130 1 1 0 14.7 10 2
|
||||
b-none none 126 1 1 0 8.7 0 11
|
||||
b-none none 115 1 1 0 3.6 10 4
|
||||
b-none none 116 2 1 1 3.8 2 9 a'=1;
|
||||
b-none none 120 2 1 1 17.0 13 2 a'=1;
|
||||
b-none none 131 1 1 0 2.7 12 9
|
||||
b-none none 132 1 1 0 3.3 5 19
|
||||
b-none none 133 3 1 2 14.1 11 2 a'=2;
|
||||
b-none none 139 3 1 2 36.4 4 2 a'=2;
|
||||
b-none none 129 1 1 0 2.9 11 8
|
||||
b-none none 125 1 1 0 3.1 7 7
|
||||
b-none none 142 2 1 1 14.4 5 2 c=1;
|
||||
b-none none 136 1 1 0 64.4 6 2
|
||||
b-none none 144 1 1 0 3.5 4 18
|
||||
b-none none 141 2 1 1 3.1 6 20 a'=1;
|
||||
b-none none 135 2 1 1 2.8 8 6 a'=1;
|
||||
b-none none 134 4 1 3 3.2 6 7 a'=3;
|
||||
b-none none 150 1 1 0 27.8 3 9
|
||||
b-none none 151 1 1 0 15.6 1 2
|
||||
b-none none 137 2 1 1 3.1 10 18 a'=1;
|
||||
b-none none 152 2 1 1 3.1 3 13 a'=1;
|
||||
b-none none 127 4 1 3 15.5 14 2 a'=2;c'''=1;
|
||||
b-none none 138 1 1 0 2.7 4 16
|
||||
b-none none 146 1 1 0 6.2 8 2
|
||||
b-none none 140 4 1 3 4.9 13 19 b=1;a'=1;a=1;
|
||||
b-none none 155 1 1 0 14.5 0 2
|
||||
b-none none 145 1 1 0 11.7 9 2
|
||||
b-none none 149 2 1 1 8.5 11 2 a'=1;
|
||||
b-none none 147 4 1 3 2.6 0 25 a'=3;
|
||||
b-none none 159 1 1 0 5.8 3 2
|
||||
b-none none 148 2 1 1 3.5 8 24 a=1;
|
||||
b-none none 154 1 1 0 3.1 2 27
|
||||
b-none none 161 4 1 3 3.3 13 19 a'=3;
|
||||
b-none none 162 1 1 0 7.2 0 2
|
||||
b-none none 143 3 1 2 19.4 1 6 c''=1;a=1;
|
||||
b-none none 163 1 1 0 16.6 12 2
|
||||
b-none none 164 1 1 0 16.5 14 2
|
||||
b-none none 165 1 1 0 3.1 0 2
|
||||
b-none none 166 4 1 3 2.9 13 3 a'=3;
|
||||
b-none none 153 1 1 0 3.0 8 22
|
||||
b-none none 167 2 1 1 2.8 11 26 a=1;
|
||||
b-none none 160 2 1 1 2.7 2 24 a'=1;
|
||||
b-none none 158 1 1 0 3.2 1 24
|
||||
b-none none 172 1 1 0 17.1 2 2
|
||||
b-none none 157 1 1 0 3.7 13 15
|
||||
b-none none 176 3 1 2 2.8 2 16 a'=1;a=1;
|
||||
b-none none 173 3 1 2 3.1 13 11 a'=2;
|
||||
b-none none 156 2 1 1 3.1 8 19 a=1;
|
||||
b-none none 171 1 1 0 3.2 9 9
|
||||
b-none none 174 1 1 0 5.9 2 27
|
||||
b-none none 168 1 1 0 7.2 1 2
|
||||
b-none none 175 1 1 0 3.1 13 22
|
||||
b-none none 170 1 1 0 18.0 9 2
|
||||
b-none none 169 3 1 2 2.9 6 11 c''=1;a'=1;
|
||||
b-none none 189 2 1 1 16.0 5 2 a'=1;
|
||||
b-none none 187 1 1 0 63.9 13 15
|
||||
b-none none 181 4 1 3 3.4 5 26 a'=3;
|
||||
b-none none 183 1 1 0 17.5 5 2
|
||||
b-none none 182 1 1 0 34.2 4 18
|
||||
b-none none 179 1 1 0 3.2 0 9
|
||||
b-none none 178 2 1 1 3.2 3 25 a'=1;
|
||||
b-none none 177 1 1 0 3.1 14 15
|
||||
b-none none 186 1 1 0 14.8 5 2
|
||||
b-none none 180 2 1 1 3.4 0 18 a'=1;
|
||||
b-none none 190 4 1 3 5.4 0 2 a'=2;c'''=1;
|
||||
b-none none 184 3 1 2 7.8 8 2 a'=2;
|
||||
b-none none 193 1 1 0 4.8 14 2
|
||||
b-none none 194 2 1 1 16.8 12 2 c=1;
|
||||
b-none none 196 2 1 1 3.0 6 23 a'=1;
|
||||
b-none none 188 4 1 3 5.7 8 9 a'=2;c''=1;
|
||||
b-none none 200 1 1 0 3.0 15 21
|
||||
b-none none 195 1 1 0 2.9 2 6
|
||||
b-none none 205 1 1 0 3.4 10 4
|
||||
b-none none 185 2 1 1 12.7 5 2 c''=1;
|
||||
b-none none 201 1 1 0 15.5 9 2
|
||||
b-none none 191 2 1 1 3.3 8 4 a'=1;
|
||||
b-none none 192 1 1 0 15.1 7 2
|
||||
b-none none 204 2 1 1 4.4 9 18 c=1;
|
||||
b-none none 198 1 1 0 2.7 11 22
|
||||
b-none none 199 1 1 0 3.1 1 8
|
||||
b-none none 197 4 1 3 3.2 5 12 a'=3;
|
||||
b-none none 202 1 1 0 3.0 13 27
|
||||
b-none none 208 1 1 0 3.9 0 26
|
||||
b-none none 210 1 1 0 5.6 9 2
|
||||
b-none none 215 2 1 1 3.1 8 16 a'=1;
|
||||
b-none none 206 6 1 5 10.1 5 13 a'=4;a=1;
|
||||
b-none none 214 2 1 1 3.0 1 15 b=1;
|
||||
b-none none 203 4 1 3 5.3 9 16 a'=3;
|
||||
b-none none 207 1 1 0 2.9 6 10
|
||||
b-none none 217 1 1 0 3.8 15 27
|
||||
b-none none 216 2 1 1 3.4 13 16 c'''=1;
|
||||
b-none none 221 2 1 1 15.7 8 2 a'=1;
|
||||
b-none none 212 2 1 1 3.3 2 19 a'=1;
|
||||
b-none none 213 1 1 0 9.4 2 2
|
||||
b-none none 220 1 1 0 12.0 13 2
|
||||
b-none none 222 1 1 0 11.9 6 19
|
||||
b-none none 219 1 1 0 4.4 9 5
|
||||
b-none none 209 3 1 2 2.8 5 4 c''=1;c=1;
|
||||
b-none none 224 3 1 2 3.2 7 23 a'=2;
|
||||
b-none none 226 1 1 0 31.0 14 16
|
||||
b-none none 211 3 1 2 3.4 8 7 c''=1;a=1;
|
||||
b-none none 228 1 1 0 2.8 3 11
|
||||
b-none none 218 3 1 2 2.8 1 15 c=1;a'=1;
|
||||
b-none none 227 1 1 0 7.1 15 2
|
||||
b-none none 223 1 1 0 2.9 1 2
|
||||
b-none none 231 1 1 0 5.2 3 20
|
||||
b-none none 225 1 1 0 3.0 5 11
|
||||
b-none none 230 1 1 0 3.0 11 18
|
||||
b-none none 233 1 1 0 16.0 13 2
|
||||
b-none none 234 3 1 2 28.7 2 2 a'=2;
|
||||
b-none none 232 1 1 0 16.1 7 2
|
||||
b-none none 235 1 1 0 11.3 3 2
|
||||
b-none none 236 1 1 0 9.2 13 2
|
||||
b-none none 238 1 1 0 3.1 8 11
|
||||
b-none none 229 1 1 0 3.3 12 19
|
||||
b-none none 242 1 1 0 3.0 0 13
|
||||
b-none none 239 1 1 0 3.0 2 27
|
||||
b-none none 240 1 1 0 15.4 6 2
|
||||
b-none none 241 1 1 0 15.4 4 2
|
||||
b-none none 245 2 1 1 3.0 3 13 a'=1;
|
||||
b-none none 243 1 1 0 5.8 0 3
|
||||
b-none none 237 1 1 0 17.1 5 2
|
||||
b-none none 247 1 1 0 3.0 15 22
|
||||
b-none none 244 1 1 0 10.5 10 2
|
||||
b-none none 249 1 1 0 7.1 0 2
|
||||
b-none none 250 1 1 0 3.0 6 5
|
||||
b-none none 248 1 1 0 3.2 1 15
|
||||
b-none none 252 4 1 3 3.0 13 26 a'=2;a=1;
|
||||
b-none none 253 1 1 0 3.1 0 12
|
||||
b-none none 251 5 1 4 6.3 0 2 a'=2;c''=1;b=1;
|
||||
b-none none 254 1 1 0 10.5 4 16
|
||||
b-none none 255 1 1 0 3.1 6 16
|
||||
b-none none 246 4 1 3 11.3 11 2 c''=2;a'=1;
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
95
docs/analysis/class-v6/logs/census/bench.log
Normal file
95
docs/analysis/class-v6/logs/census/bench.log
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
lease: 0 of 1 pool cores free (88 leased); waiting
|
||||
lease: holding 1 pool cores (18, waited 804 s, class measure): class v6 census: verifier bench per candidate, one core
|
||||
# bench start 2026-10-08T12:55:00Z host igneum-build-4 cores 18 bin d601098699840534
|
||||
== ctrl rep 1 class mx8+sh256x27 weights base
|
||||
warp base 0: single cold run 9.956 ms, 4096 items derived, lane0 2fc898410932f9e3 lane31 46d6a126c7aa1def
|
||||
warp base 4096: single cold run 9.650 ms, 4096 items derived, lane0 7a977c3ed95fd05a lane31 2845370fe23e9cbc
|
||||
warp base 1000000: single cold run 9.576 ms, 4096 items derived, lane0 7ec39eb3571dd232 lane31 a96c22d96855eaaf
|
||||
CPU verify: 9.550 ms per 32-lane warp, avg of 20 (checksum 0e89d7e40db622e9)
|
||||
== ctrl rep 2 class mx8+sh256x27 weights base
|
||||
warp base 0: single cold run 9.835 ms, 4096 items derived, lane0 2fc898410932f9e3 lane31 46d6a126c7aa1def
|
||||
warp base 4096: single cold run 9.602 ms, 4096 items derived, lane0 7a977c3ed95fd05a lane31 2845370fe23e9cbc
|
||||
warp base 1000000: single cold run 9.590 ms, 4096 items derived, lane0 7ec39eb3571dd232 lane31 a96c22d96855eaaf
|
||||
CPU verify: 9.543 ms per 32-lane warp, avg of 20 (checksum 0e89d7e40db622e9)
|
||||
== ctrl rep 3 class mx8+sh256x27 weights base
|
||||
warp base 0: single cold run 9.959 ms, 4096 items derived, lane0 2fc898410932f9e3 lane31 46d6a126c7aa1def
|
||||
warp base 4096: single cold run 9.649 ms, 4096 items derived, lane0 7a977c3ed95fd05a lane31 2845370fe23e9cbc
|
||||
warp base 1000000: single cold run 9.607 ms, 4096 items derived, lane0 7ec39eb3571dd232 lane31 a96c22d96855eaaf
|
||||
CPU verify: 9.677 ms per 32-lane warp, avg of 20 (checksum 0e89d7e40db622e9)
|
||||
== w16 rep 1 class w16m8g+sh256x27 weights base
|
||||
warp base 0: single cold run 9.520 ms, 4096 items derived, lane0 86246a72110d1bdb lane31 9624ff67d32d0143
|
||||
warp base 4096: single cold run 9.607 ms, 4096 items derived, lane0 58a778388ac864f7 lane31 3971533d5d207c9a
|
||||
warp base 1000000: single cold run 9.587 ms, 4096 items derived, lane0 25a14bccb7b1307b lane31 939807ae42c54b17
|
||||
CPU verify: 9.648 ms per 32-lane warp, avg of 20 (checksum 042c4aa1de6d950e)
|
||||
== w16 rep 2 class w16m8g+sh256x27 weights base
|
||||
warp base 0: single cold run 10.004 ms, 4096 items derived, lane0 86246a72110d1bdb lane31 9624ff67d32d0143
|
||||
warp base 4096: single cold run 9.839 ms, 4096 items derived, lane0 58a778388ac864f7 lane31 3971533d5d207c9a
|
||||
warp base 1000000: single cold run 9.746 ms, 4096 items derived, lane0 25a14bccb7b1307b lane31 939807ae42c54b17
|
||||
CPU verify: 9.421 ms per 32-lane warp, avg of 20 (checksum 042c4aa1de6d950e)
|
||||
== w16 rep 3 class w16m8g+sh256x27 weights base
|
||||
warp base 0: single cold run 9.722 ms, 4096 items derived, lane0 86246a72110d1bdb lane31 9624ff67d32d0143
|
||||
warp base 4096: single cold run 9.468 ms, 4096 items derived, lane0 58a778388ac864f7 lane31 3971533d5d207c9a
|
||||
warp base 1000000: single cold run 9.469 ms, 4096 items derived, lane0 25a14bccb7b1307b lane31 939807ae42c54b17
|
||||
CPU verify: 9.450 ms per 32-lane warp, avg of 20 (checksum 042c4aa1de6d950e)
|
||||
== a rep 1 class w64m8g+sh256x27 weights base
|
||||
warp base 0: single cold run 9.697 ms, 4096 items derived, lane0 256f099baec10276 lane31 bf2cf1eaa6860226
|
||||
warp base 4096: single cold run 9.469 ms, 4096 items derived, lane0 0566cfce49f016a7 lane31 29c0d372bb080cb4
|
||||
warp base 1000000: single cold run 9.444 ms, 4096 items derived, lane0 e2e4a79baa3d476b lane31 8d41a0d34c9a9988
|
||||
CPU verify: 9.477 ms per 32-lane warp, avg of 20 (checksum abd5e098a8b332df)
|
||||
== a rep 2 class w64m8g+sh256x27 weights base
|
||||
warp base 0: single cold run 10.134 ms, 4096 items derived, lane0 256f099baec10276 lane31 bf2cf1eaa6860226
|
||||
warp base 4096: single cold run 9.666 ms, 4096 items derived, lane0 0566cfce49f016a7 lane31 29c0d372bb080cb4
|
||||
warp base 1000000: single cold run 9.831 ms, 4096 items derived, lane0 e2e4a79baa3d476b lane31 8d41a0d34c9a9988
|
||||
CPU verify: 9.751 ms per 32-lane warp, avg of 20 (checksum abd5e098a8b332df)
|
||||
== a rep 3 class w64m8g+sh256x27 weights base
|
||||
warp base 0: single cold run 9.949 ms, 4096 items derived, lane0 256f099baec10276 lane31 bf2cf1eaa6860226
|
||||
warp base 4096: single cold run 9.665 ms, 4096 items derived, lane0 0566cfce49f016a7 lane31 29c0d372bb080cb4
|
||||
warp base 1000000: single cold run 9.731 ms, 4096 items derived, lane0 e2e4a79baa3d476b lane31 8d41a0d34c9a9988
|
||||
CPU verify: 9.703 ms per 32-lane warp, avg of 20 (checksum abd5e098a8b332df)
|
||||
== b rep 1 class w16m8g+sh256x27 weights 13,11,6,10,8,8,7,2,6,4
|
||||
warp base 0: single cold run 9.843 ms, 4096 items derived, lane0 7f0a9d0730f4f729 lane31 c01afb20069e0408
|
||||
warp base 4096: single cold run 9.607 ms, 4096 items derived, lane0 09d0e2b984e04fad lane31 2e59642314e381b7
|
||||
warp base 1000000: single cold run 9.650 ms, 4096 items derived, lane0 a4697a4c254a61a7 lane31 3cc24e96c299c61d
|
||||
CPU verify: 9.616 ms per 32-lane warp, avg of 20 (checksum 7e659be3d18245a4)
|
||||
== b rep 2 class w16m8g+sh256x27 weights 13,11,6,10,8,8,7,2,6,4
|
||||
warp base 0: single cold run 9.277 ms, 4096 items derived, lane0 7f0a9d0730f4f729 lane31 c01afb20069e0408
|
||||
warp base 4096: single cold run 8.567 ms, 4096 items derived, lane0 09d0e2b984e04fad lane31 2e59642314e381b7
|
||||
warp base 1000000: single cold run 8.582 ms, 4096 items derived, lane0 a4697a4c254a61a7 lane31 3cc24e96c299c61d
|
||||
CPU verify: 8.598 ms per 32-lane warp, avg of 20 (checksum 7e659be3d18245a4)
|
||||
== b rep 3 class w16m8g+sh256x27 weights 13,11,6,10,8,8,7,2,6,4
|
||||
warp base 0: single cold run 8.619 ms, 4096 items derived, lane0 7f0a9d0730f4f729 lane31 c01afb20069e0408
|
||||
warp base 4096: single cold run 8.234 ms, 4096 items derived, lane0 09d0e2b984e04fad lane31 2e59642314e381b7
|
||||
warp base 1000000: single cold run 7.971 ms, 4096 items derived, lane0 a4697a4c254a61a7 lane31 3cc24e96c299c61d
|
||||
CPU verify: 8.249 ms per 32-lane warp, avg of 20 (checksum 7e659be3d18245a4)
|
||||
== c rep 1 class w64m8g+sh256x27 weights 13,11,6,10,8,8,7,2,6,4
|
||||
warp base 0: single cold run 8.851 ms, 4096 items derived, lane0 10e24635c2a2eb4f lane31 050b69a708943300
|
||||
warp base 4096: single cold run 8.922 ms, 4096 items derived, lane0 f7b8d920396a4441 lane31 784c075f03b92f28
|
||||
warp base 1000000: single cold run 8.880 ms, 4096 items derived, lane0 07f320e47aa8c175 lane31 622c9b4638d07f65
|
||||
CPU verify: 9.148 ms per 32-lane warp, avg of 20 (checksum 8551e68dd6a59dae)
|
||||
== c rep 2 class w64m8g+sh256x27 weights 13,11,6,10,8,8,7,2,6,4
|
||||
warp base 0: single cold run 9.579 ms, 4096 items derived, lane0 10e24635c2a2eb4f lane31 050b69a708943300
|
||||
warp base 4096: single cold run 8.808 ms, 4096 items derived, lane0 f7b8d920396a4441 lane31 784c075f03b92f28
|
||||
warp base 1000000: single cold run 8.869 ms, 4096 items derived, lane0 07f320e47aa8c175 lane31 622c9b4638d07f65
|
||||
CPU verify: 9.214 ms per 32-lane warp, avg of 20 (checksum 8551e68dd6a59dae)
|
||||
== c rep 3 class w64m8g+sh256x27 weights 13,11,6,10,8,8,7,2,6,4
|
||||
warp base 0: single cold run 9.415 ms, 4096 items derived, lane0 10e24635c2a2eb4f lane31 050b69a708943300
|
||||
warp base 4096: single cold run 9.177 ms, 4096 items derived, lane0 f7b8d920396a4441 lane31 784c075f03b92f28
|
||||
warp base 1000000: single cold run 9.243 ms, 4096 items derived, lane0 07f320e47aa8c175 lane31 622c9b4638d07f65
|
||||
CPU verify: 9.254 ms per 32-lane warp, avg of 20 (checksum 8551e68dd6a59dae)
|
||||
== ctrl-again rep 1 class mx8+sh256x27 weights base
|
||||
warp base 0: single cold run 9.541 ms, 4096 items derived, lane0 2fc898410932f9e3 lane31 46d6a126c7aa1def
|
||||
warp base 4096: single cold run 9.261 ms, 4096 items derived, lane0 7a977c3ed95fd05a lane31 2845370fe23e9cbc
|
||||
warp base 1000000: single cold run 9.239 ms, 4096 items derived, lane0 7ec39eb3571dd232 lane31 a96c22d96855eaaf
|
||||
CPU verify: 9.493 ms per 32-lane warp, avg of 20 (checksum 0e89d7e40db622e9)
|
||||
== ctrl-again rep 2 class mx8+sh256x27 weights base
|
||||
warp base 0: single cold run 9.285 ms, 4096 items derived, lane0 2fc898410932f9e3 lane31 46d6a126c7aa1def
|
||||
warp base 4096: single cold run 8.907 ms, 4096 items derived, lane0 7a977c3ed95fd05a lane31 2845370fe23e9cbc
|
||||
warp base 1000000: single cold run 8.967 ms, 4096 items derived, lane0 7ec39eb3571dd232 lane31 a96c22d96855eaaf
|
||||
CPU verify: 9.125 ms per 32-lane warp, avg of 20 (checksum 0e89d7e40db622e9)
|
||||
== ctrl-again rep 3 class mx8+sh256x27 weights base
|
||||
warp base 0: single cold run 9.423 ms, 4096 items derived, lane0 2fc898410932f9e3 lane31 46d6a126c7aa1def
|
||||
warp base 4096: single cold run 9.181 ms, 4096 items derived, lane0 7a977c3ed95fd05a lane31 2845370fe23e9cbc
|
||||
warp base 1000000: single cold run 9.123 ms, 4096 items derived, lane0 7ec39eb3571dd232 lane31 a96c22d96855eaaf
|
||||
CPU verify: 9.191 ms per 32-lane warp, avg of 20 (checksum 0e89d7e40db622e9)
|
||||
# bench end 2026-10-08T12:56:53Z
|
||||
lease: released 1 pool cores after 918 s, exit 0
|
||||
259
docs/analysis/class-v6/logs/census/c-era.tsv
Normal file
259
docs/analysis/class-v6/logs/census/c-era.tsv
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
# c-era class w64m8g+sh256x27 eras [0,1,2,3,4,5,6,7] seeds 32 era-widths 64 weights 13,11,6,10,8,8,7,2,6,4 bittest 1 bin d601098699840534 start 2026-10-08T12:55:03Z host igneum-build-4 threads 15
|
||||
cand era seed candidates accepted attempt bit_z bit_site bit_bit reasons
|
||||
c-era 0 12 5 1 4 3.0 15 16 c,=2;a'=1;a=1;
|
||||
c-era 0 1 2 1 1 2.8 9 15 c,=1;
|
||||
c-era 0 3 2 1 1 3.1 7 24 c,=1;
|
||||
c-era 0 9 2 1 1 4.3 15 20 c,=1;
|
||||
c-era 0 11 9 1 8 3.3 10 8 c,=7;a'=1;
|
||||
c-era 0 10 10 1 9 2.8 4 10 c,=8;a=1;
|
||||
c-era 0 4 9 1 8 3.5 10 7 a'=5;c,=2;a=1;
|
||||
c-era 0 8 1 1 0 2.9 12 5
|
||||
c-era 0 6 2 1 1 2.8 14 16 a'=1;
|
||||
c-era 0 14 6 1 5 3.0 11 25 a'=3;c,=2;
|
||||
c-era 0 13 3 1 2 3.2 15 8 c,=1;a'=1;
|
||||
c-era 0 5 3 1 2 3.7 11 19 a'=2;
|
||||
c-era 0 0 5 1 4 3.5 3 19 c,=2;a'=2;
|
||||
c-era 0 2 2 1 1 2.7 3 9 c,=1;
|
||||
c-era 0 7 3 1 2 2.7 11 26 c,=1;a'=1;
|
||||
c-era 0 16 1 1 0 3.5 8 13
|
||||
c-era 0 17 3 1 2 3.2 3 11 c,=1;a'=1;
|
||||
c-era 0 19 1 1 0 3.1 1 11
|
||||
c-era 0 18 2 1 1 4.9 12 21 c,=1;
|
||||
c-era 0 20 3 1 2 3.1 12 17 c,=1;a'=1;
|
||||
c-era 0 21 4 1 3 3.4 2 11 a'=2;c,=1;
|
||||
c-era 0 23 2 1 1 2.7 4 12 a=1;
|
||||
c-era 0 27 2 1 1 2.8 6 20 a'=1;
|
||||
c-era 0 15 6 1 5 3.0 15 11 c,=3;a'=2;
|
||||
c-era 0 25 6 1 5 3.0 13 18 c,=4;a=1;
|
||||
c-era 0 24 1 1 0 3.5 3 18
|
||||
c-era 0 29 2 1 1 2.8 7 5 c,=1;
|
||||
c-era 0 22 2 1 1 2.8 2 15 a'=1;
|
||||
c-era 0 26 11 1 10 3.2 2 25 c,=6;a'=3;c=1;
|
||||
c-era 0 28 9 1 8 3.0 10 15 a'=4;c,=3;a=1;
|
||||
c-era 0 31 1 1 0 3.2 8 21
|
||||
c-era 0 30 5 1 4 3.1 12 5 a'=3;a=1;
|
||||
c-era 1 0 5 1 4 3.1 10 15 c,=2;a'=2;
|
||||
c-era 1 3 2 1 1 3.7 11 17 c,=1;
|
||||
c-era 1 1 2 1 1 3.1 15 19 c,=1;
|
||||
c-era 1 5 3 1 2 4.7 11 7 a'=2;
|
||||
c-era 1 6 2 1 1 3.0 5 6 a'=1;
|
||||
c-era 1 2 2 1 1 3.2 10 23 c,=1;
|
||||
c-era 1 7 3 1 2 3.7 7 26 c,=1;a'=1;
|
||||
c-era 1 10 10 1 9 3.0 0 19 c,=8;a=1;
|
||||
c-era 1 4 12 1 11 2.5 6 7 c,=5;a'=5;a=1;
|
||||
c-era 1 9 2 1 1 2.7 5 20 c,=1;
|
||||
c-era 1 8 1 1 0 3.2 1 18
|
||||
c-era 1 11 9 1 8 2.7 2 15 c,=7;a'=1;
|
||||
c-era 1 14 6 1 5 4.0 12 17 a'=3;c,=2;
|
||||
c-era 1 12 5 1 4 2.7 14 22 c,=2;a'=1;a=1;
|
||||
c-era 1 16 1 1 0 3.1 13 5
|
||||
c-era 1 21 4 1 3 3.8 7 13 a'=2;c,=1;
|
||||
c-era 1 20 3 1 2 3.0 6 24 c,=1;a'=1;
|
||||
c-era 1 13 3 1 2 3.8 10 19 c,=1;a'=1;
|
||||
c-era 1 17 3 1 2 3.1 1 6 c,=1;a'=1;
|
||||
c-era 1 15 6 1 5 2.6 10 17 c,=3;a'=2;
|
||||
c-era 1 25 6 1 5 2.8 0 6 c,=4;a=1;
|
||||
c-era 1 18 3 1 2 3.1 2 20 c,=2;
|
||||
c-era 1 22 2 1 1 3.2 14 25 a'=1;
|
||||
c-era 1 28 9 1 8 3.6 5 20 a'=4;c,=3;a=1;
|
||||
c-era 1 27 2 1 1 3.3 8 23 a'=1;
|
||||
c-era 1 19 1 1 0 3.0 7 17
|
||||
c-era 1 26 11 1 10 3.5 12 21 c,=6;a'=3;c=1;
|
||||
c-era 1 23 2 1 1 3.1 3 24 a=1;
|
||||
c-era 1 30 5 1 4 3.0 12 21 a'=3;a=1;
|
||||
c-era 1 31 1 1 0 3.2 7 7
|
||||
c-era 1 24 1 1 0 3.5 4 12
|
||||
c-era 2 0 2 1 1 3.0 5 5 a'=1;
|
||||
c-era 2 2 1 1 0 3.3 9 14
|
||||
c-era 2 6 2 1 1 3.2 13 27 a'=1;
|
||||
c-era 1 29 2 1 1 3.3 3 6 c,=1;
|
||||
c-era 2 9 1 1 0 3.2 6 5
|
||||
c-era 2 8 1 1 0 3.0 8 17
|
||||
c-era 2 1 1 1 0 2.7 2 15
|
||||
c-era 2 10 1 1 0 2.5 12 19
|
||||
c-era 2 3 2 1 1 2.9 0 4 c,=1;
|
||||
c-era 2 5 3 1 2 3.0 7 18 a'=2;
|
||||
c-era 2 11 1 1 0 3.5 12 21
|
||||
c-era 2 12 2 1 1 3.6 8 22 a'=1;
|
||||
c-era 2 4 1 1 0 3.0 4 20
|
||||
c-era 2 7 1 1 0 3.0 7 25
|
||||
c-era 2 13 1 1 0 2.8 14 11
|
||||
c-era 2 14 1 1 0 2.8 15 7
|
||||
c-era 2 16 1 1 0 3.0 10 22
|
||||
c-era 2 15 2 1 1 3.1 0 14 a'=1;
|
||||
c-era 2 18 1 1 0 3.7 12 5
|
||||
c-era 2 20 1 1 0 2.9 4 5
|
||||
c-era 2 19 1 1 0 3.5 15 4
|
||||
c-era 2 21 3 1 2 3.0 6 10 a'=2;
|
||||
c-era 2 22 2 1 1 2.9 5 4 a'=1;
|
||||
c-era 2 17 2 1 1 2.6 0 19 a'=1;
|
||||
c-era 2 25 1 1 0 3.4 11 14
|
||||
c-era 2 23 2 1 1 2.9 6 15 a=1;
|
||||
c-era 2 27 2 1 1 4.0 10 23 a'=1;
|
||||
c-era 2 24 1 1 0 2.7 0 23
|
||||
c-era 2 26 1 1 0 2.8 13 14
|
||||
c-era 2 28 5 1 4 3.0 8 10 a'=3;a=1;
|
||||
c-era 2 31 1 1 0 3.5 13 18
|
||||
c-era 2 29 1 1 0 2.5 5 18
|
||||
c-era 3 2 1 1 0 3.3 3 7
|
||||
c-era 3 4 6 1 5 3.5 6 12 a'=4;c,=1;
|
||||
c-era 2 30 5 1 4 2.9 9 26 a'=3;a=1;
|
||||
c-era 3 0 2 1 1 2.8 4 23 a'=1;
|
||||
c-era 3 5 3 1 2 4.6 11 4 a'=2;
|
||||
c-era 3 8 1 1 0 3.2 14 10
|
||||
c-era 3 7 1 1 0 3.0 6 7
|
||||
c-era 3 6 2 1 1 3.2 7 10 a'=1;
|
||||
c-era 3 3 2 1 1 2.8 3 20 c,=1;
|
||||
c-era 3 9 1 1 0 3.0 14 26
|
||||
c-era 3 10 1 1 0 2.8 0 12
|
||||
c-era 3 1 1 1 0 3.8 7 4
|
||||
c-era 3 11 1 1 0 3.6 13 15
|
||||
c-era 3 12 2 1 1 2.6 8 25 a'=1;
|
||||
c-era 3 13 1 1 0 3.0 8 19
|
||||
c-era 3 14 1 1 0 3.1 6 17
|
||||
c-era 3 16 1 1 0 3.5 3 8
|
||||
c-era 3 15 2 1 1 3.5 10 26 a'=1;
|
||||
c-era 3 19 1 1 0 3.0 13 7
|
||||
c-era 3 17 2 1 1 3.6 7 26 a'=1;
|
||||
c-era 3 18 1 1 0 3.0 11 20
|
||||
c-era 3 20 3 1 2 2.9 14 9 c,=1;a'=1;
|
||||
c-era 3 24 1 1 0 2.6 14 17
|
||||
c-era 3 21 3 1 2 3.6 2 4 a'=2;
|
||||
c-era 3 23 2 1 1 3.1 9 15 a=1;
|
||||
c-era 3 25 1 1 0 3.1 15 21
|
||||
c-era 3 28 5 1 4 3.0 7 17 a'=3;a=1;
|
||||
c-era 3 26 1 1 0 2.7 4 4
|
||||
c-era 4 0 5 1 4 3.2 13 22 c,=2;a'=2;
|
||||
c-era 3 29 1 1 0 3.1 0 24
|
||||
c-era 3 22 2 1 1 3.2 1 24 a'=1;
|
||||
c-era 3 31 1 1 0 3.0 1 4
|
||||
c-era 3 30 5 1 4 2.9 3 10 a'=3;a=1;
|
||||
c-era 3 27 2 1 1 3.1 3 7 a'=1;
|
||||
c-era 4 4 9 1 8 4.9 1 14 a'=5;c,=2;a=1;
|
||||
c-era 4 2 2 1 1 3.1 10 5 c,=1;
|
||||
c-era 4 1 2 1 1 3.1 3 26 c,=1;
|
||||
c-era 4 3 2 1 1 3.4 3 18 c,=1;
|
||||
c-era 4 5 3 1 2 4.3 11 11 a'=2;
|
||||
c-era 4 10 10 1 9 3.1 13 7 c,=8;a=1;
|
||||
c-era 4 6 2 1 1 2.5 1 7 a'=1;
|
||||
c-era 4 11 9 1 8 2.8 10 23 c,=7;a'=1;
|
||||
c-era 4 8 1 1 0 2.7 3 4
|
||||
c-era 4 13 3 1 2 3.3 7 9 c,=1;a'=1;
|
||||
c-era 4 7 3 1 2 3.4 8 12 c,=1;a'=1;
|
||||
c-era 4 18 3 1 2 2.9 14 7 c,=2;
|
||||
c-era 4 12 3 1 2 5.4 0 10 c,=1;a'=1;
|
||||
c-era 4 9 2 1 1 3.3 15 23 c,=1;
|
||||
c-era 4 16 1 1 0 3.0 2 11
|
||||
c-era 4 14 6 1 5 2.8 9 13 a'=3;c,=2;
|
||||
c-era 4 21 4 1 3 3.3 9 19 a'=2;c,=1;
|
||||
c-era 4 17 3 1 2 3.3 15 23 c,=1;a'=1;
|
||||
c-era 4 15 6 1 5 3.3 9 8 c,=3;a'=2;
|
||||
c-era 4 19 1 1 0 3.2 4 18
|
||||
c-era 4 20 3 1 2 3.2 0 18 c,=1;a'=1;
|
||||
c-era 4 22 2 1 1 3.9 12 25 a'=1;
|
||||
c-era 4 25 6 1 5 3.3 4 16 c,=4;a=1;
|
||||
c-era 4 23 2 1 1 3.5 9 7 a=1;
|
||||
c-era 4 27 2 1 1 2.9 14 7 a'=1;
|
||||
c-era 4 24 1 1 0 3.0 3 15
|
||||
c-era 5 1 2 1 1 3.5 12 24 c,=1;
|
||||
c-era 5 6 2 1 1 3.3 8 18 a'=1;
|
||||
c-era 5 0 5 1 4 4.0 2 17 c,=2;a'=2;
|
||||
c-era 4 31 1 1 0 3.0 2 13
|
||||
c-era 5 9 2 1 1 3.3 7 21 c,=1;
|
||||
c-era 5 8 1 1 0 3.2 15 22
|
||||
c-era 4 28 9 1 8 3.3 9 18 a'=4;c,=3;a=1;
|
||||
c-era 4 30 5 1 4 3.3 8 19 a'=3;a=1;
|
||||
c-era 4 29 2 1 1 3.9 13 10 c,=1;
|
||||
c-era 4 26 11 1 10 3.4 11 4 c,=6;a'=3;c=1;
|
||||
c-era 5 10 10 1 9 3.0 4 24 c,=8;a=1;
|
||||
c-era 5 13 3 1 2 3.3 4 21 c,=1;a'=1;
|
||||
c-era 5 17 3 1 2 3.4 5 4 c,=1;a'=1;
|
||||
c-era 5 5 3 1 2 5.1 11 22 a'=2;
|
||||
c-era 5 14 6 1 5 2.9 0 14 a'=3;c,=2;
|
||||
c-era 5 7 3 1 2 3.4 9 8 c,=1;a'=1;
|
||||
c-era 5 19 1 1 0 2.7 11 11
|
||||
c-era 5 15 6 1 5 3.4 6 23 c,=3;a'=2;
|
||||
c-era 5 3 2 1 1 2.7 4 23 c,=1;
|
||||
c-era 5 4 9 1 8 3.0 8 13 a'=5;c,=2;a=1;
|
||||
c-era 5 2 2 1 1 3.2 10 4 c,=1;
|
||||
c-era 5 18 3 1 2 3.4 13 11 c,=2;
|
||||
c-era 5 20 3 1 2 4.0 15 13 c,=1;a'=1;
|
||||
c-era 5 11 9 1 8 2.7 8 18 c,=7;a'=1;
|
||||
c-era 5 16 1 1 0 3.5 1 23
|
||||
c-era 5 12 5 1 4 3.5 11 15 c,=2;a'=1;a=1;
|
||||
c-era 5 21 4 1 3 2.9 13 14 a'=2;c,=1;
|
||||
c-era 5 22 2 1 1 2.6 11 10 a'=1;
|
||||
c-era 5 24 1 1 0 2.9 0 12
|
||||
c-era 5 28 9 1 8 2.9 8 10 a'=4;c,=3;a=1;
|
||||
c-era 5 25 6 1 5 3.5 3 9 c,=4;a=1;
|
||||
c-era 5 29 2 1 1 2.8 6 25 c,=1;
|
||||
c-era 5 30 5 1 4 4.5 10 22 a'=3;a=1;
|
||||
c-era 5 26 11 1 10 3.1 14 19 c,=6;a'=3;c=1;
|
||||
c-era 5 27 2 1 1 3.1 11 4 a'=1;
|
||||
c-era 5 23 2 1 1 2.7 2 20 a=1;
|
||||
c-era 5 31 1 1 0 3.7 11 11
|
||||
c-era 6 4 6 1 5 3.0 7 7 a'=4;c,=1;
|
||||
c-era 6 0 2 1 1 3.0 8 10 a'=1;
|
||||
c-era 6 5 3 1 2 4.1 11 6 a'=2;
|
||||
c-era 6 2 2 1 1 3.5 7 8 c,=1;
|
||||
c-era 6 6 2 1 1 3.0 9 21 a'=1;
|
||||
c-era 6 1 2 1 1 2.6 2 26 c,=1;
|
||||
c-era 6 9 1 1 0 3.1 5 9
|
||||
c-era 6 3 2 1 1 2.9 13 24 c,=1;
|
||||
c-era 6 11 5 1 4 3.1 0 7 c,=4;
|
||||
c-era 6 13 1 1 0 3.1 0 4
|
||||
c-era 6 12 2 1 1 3.4 12 13 a'=1;
|
||||
c-era 6 10 7 1 6 2.9 12 13 c,=5;a=1;
|
||||
c-era 6 14 1 1 0 3.2 14 23
|
||||
c-era 6 7 1 1 0 3.2 4 12
|
||||
c-era 6 8 1 1 0 2.8 13 8
|
||||
c-era 6 15 6 1 5 3.3 3 11 c,=3;a'=2;
|
||||
c-era 6 17 3 1 2 3.5 8 5 c,=1;a'=1;
|
||||
c-era 6 19 1 1 0 3.3 9 21
|
||||
c-era 6 18 1 1 0 2.9 7 14
|
||||
c-era 6 16 1 1 0 2.8 9 13
|
||||
c-era 6 20 3 1 2 2.9 13 6 c,=1;a'=1;
|
||||
c-era 6 22 2 1 1 3.1 14 6 a'=1;
|
||||
c-era 6 21 3 1 2 3.9 14 7 a'=2;
|
||||
c-era 6 27 2 1 1 2.9 3 20 a'=1;
|
||||
c-era 6 24 1 1 0 3.7 9 7
|
||||
c-era 6 23 2 1 1 3.0 3 24 a=1;
|
||||
c-era 6 25 1 1 0 3.0 12 7
|
||||
c-era 6 26 7 1 6 5.5 5 5 c,=3;a'=2;c=1;
|
||||
c-era 6 29 1 1 0 2.7 13 5
|
||||
c-era 6 28 9 1 8 2.9 12 21 a'=4;c,=3;a=1;
|
||||
c-era 6 31 1 1 0 3.5 1 23
|
||||
c-era 7 0 2 1 1 2.7 12 23 a'=1;
|
||||
c-era 6 30 5 1 4 3.4 9 4 a'=3;a=1;
|
||||
c-era 7 2 2 1 1 3.1 2 17 c,=1;
|
||||
c-era 7 1 1 1 0 2.6 3 5
|
||||
c-era 7 3 2 1 1 3.0 12 6 c,=1;
|
||||
c-era 7 7 1 1 0 3.2 0 8
|
||||
c-era 7 4 9 1 8 3.3 7 5 a'=5;c,=2;a=1;
|
||||
c-era 7 8 1 1 0 2.7 6 23
|
||||
c-era 7 5 3 1 2 2.8 11 25 a'=2;
|
||||
c-era 7 11 1 1 0 2.8 11 7
|
||||
c-era 7 9 2 1 1 2.7 8 13 c,=1;
|
||||
c-era 7 14 1 1 0 2.7 14 7
|
||||
c-era 7 6 2 1 1 2.8 1 24 a'=1;
|
||||
c-era 7 10 1 1 0 3.5 10 21
|
||||
c-era 7 17 2 1 1 3.4 5 21 a'=1;
|
||||
c-era 7 12 2 1 1 3.3 1 10 a'=1;
|
||||
c-era 7 13 3 1 2 3.1 7 15 c,=1;a'=1;
|
||||
c-era 7 20 1 1 0 3.8 15 17
|
||||
c-era 7 15 6 1 5 2.8 13 24 c,=3;a'=2;
|
||||
c-era 7 22 2 1 1 3.0 0 9 a'=1;
|
||||
c-era 7 18 2 1 1 2.9 8 16 c,=1;
|
||||
c-era 7 21 4 1 3 3.3 0 20 a'=2;c,=1;
|
||||
c-era 7 23 2 1 1 2.9 8 13 a=1;
|
||||
c-era 7 16 1 1 0 2.9 11 7
|
||||
c-era 7 24 1 1 0 3.1 8 12
|
||||
c-era 7 19 1 1 0 3.5 6 25
|
||||
c-era 7 25 1 1 0 3.3 2 16
|
||||
c-era 7 26 7 1 6 3.5 3 19 c,=3;a'=2;c=1;
|
||||
c-era 7 28 5 1 4 3.0 13 14 a'=3;a=1;
|
||||
c-era 7 29 1 1 0 3.2 13 19
|
||||
c-era 7 30 5 1 4 2.7 8 17 a'=3;a=1;
|
||||
c-era 7 31 1 1 0 3.2 7 20
|
||||
c-era 7 27 2 1 1 2.9 2 4 a'=1;
|
||||
# end 2026-10-08T12:59:21Z rows=257
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
258
docs/analysis/class-v6/logs/census/c-none.tsv
Normal file
258
docs/analysis/class-v6/logs/census/c-none.tsv
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
# c-none class w64m8g+sh256x27 eras [none] seeds 256 era-widths 64 weights 13,11,6,10,8,8,7,2,6,4 bittest off bin d601098699840534 start 2026-10-08T12:45:54Z host igneum-build-4 threads 16
|
||||
cand era seed candidates accepted attempt bit_z bit_site bit_bit reasons
|
||||
c-none none 1 1 1 0 3.1 2 9
|
||||
c-none none 15 2 1 1 4.1 10 10 a'=1;
|
||||
c-none none 2 5 1 4 3.3 3 26 a'=3;a=1;
|
||||
c-none none 6 2 1 1 3.9 2 4 c=1;
|
||||
c-none none 3 1 1 0 6.7 9 4
|
||||
c-none none 4 2 1 1 3.0 14 15 a'=1;
|
||||
c-none none 8 2 1 1 4.8 2 16 a'=1;
|
||||
c-none none 5 1 1 0 2.9 1 8
|
||||
c-none none 18 1 1 0 3.1 3 10
|
||||
c-none none 19 1 1 0 7.2 4 5
|
||||
c-none none 21 4 1 3 3.2 3 5 a'=3;
|
||||
c-none none 22 1 1 0 30.9 5 23
|
||||
c-none none 17 1 1 0 11.9 10 4
|
||||
c-none none 20 1 1 0 13.6 10 4
|
||||
c-none none 14 1 1 0 4.0 11 4
|
||||
c-none none 10 1 1 0 3.0 14 7
|
||||
c-none none 13 1 1 0 3.3 15 22
|
||||
c-none none 24 2 1 1 3.2 9 4 a'=1;
|
||||
c-none none 23 2 1 1 2.9 1 4 a'=1;
|
||||
c-none none 25 1 1 0 2.5 8 5
|
||||
c-none none 11 1 1 0 3.1 3 13
|
||||
c-none none 7 1 1 0 4.0 0 4
|
||||
c-none none 12 1 1 0 3.2 1 12
|
||||
c-none none 29 3 1 2 4.0 12 19 a'=2;
|
||||
c-none none 0 2 1 1 2.9 3 18 a'=1;
|
||||
c-none none 27 3 1 2 2.8 5 14 c=1;b=1;
|
||||
c-none none 16 4 1 3 3.4 11 21 a'=3;
|
||||
c-none none 30 3 1 2 9.8 0 4 a'=1;a=1;
|
||||
c-none none 28 1 1 0 3.0 0 7
|
||||
c-none none 26 1 1 0 3.5 10 26
|
||||
c-none none 41 2 1 1 3.3 8 5 a'=1;
|
||||
c-none none 34 2 1 1 2.9 15 19 a=1;
|
||||
c-none none 33 2 1 1 32.4 1 19 a=1;
|
||||
c-none none 32 1 1 0 3.4 13 19
|
||||
c-none none 38 1 1 0 10.8 3 27
|
||||
c-none none 9 7 1 6 3.8 3 13 a'=3;c'''=1;c''=1;a=1;
|
||||
c-none none 31 4 1 3 3.7 14 11 a'=3;
|
||||
c-none none 35 2 1 1 9.9 2 4 a'=1;
|
||||
c-none none 37 2 1 1 3.1 8 7 a'=1;
|
||||
c-none none 36 1 1 0 3.0 1 21
|
||||
c-none none 39 1 1 0 2.8 8 22
|
||||
c-none none 42 1 1 0 3.0 6 12
|
||||
c-none none 40 1 1 0 2.8 1 13
|
||||
c-none none 48 2 1 1 3.0 3 10 a'=1;
|
||||
c-none none 49 2 1 1 3.8 8 11 a'=1;
|
||||
c-none none 56 1 1 0 3.3 3 20
|
||||
c-none none 50 1 1 0 13.6 3 4
|
||||
c-none none 57 1 1 0 3.5 15 9
|
||||
c-none none 55 1 1 0 5.7 1 21
|
||||
c-none none 52 1 1 0 7.4 4 10
|
||||
c-none none 44 1 1 0 3.0 2 10
|
||||
c-none none 43 2 1 1 3.2 5 22 a'=1;
|
||||
c-none none 54 5 1 4 3.0 10 6 a'=2;c'''=1;a=1;
|
||||
c-none none 53 2 1 1 3.6 8 12 c''=1;
|
||||
c-none none 45 1 1 0 5.5 15 4
|
||||
c-none none 51 2 1 1 3.1 6 15 a'=1;
|
||||
c-none none 59 1 1 0 3.3 10 25
|
||||
c-none none 47 2 1 1 3.0 12 8 a'=1;
|
||||
c-none none 58 1 1 0 3.5 14 14
|
||||
c-none none 60 2 1 1 3.1 1 16 a'=1;
|
||||
c-none none 61 3 1 2 8.1 12 4 a'=2;
|
||||
c-none none 63 2 1 1 6.0 6 4 a'=1;
|
||||
c-none none 64 1 1 0 3.0 12 21
|
||||
c-none none 46 4 1 3 3.6 14 23 a'=2;c'''=1;
|
||||
c-none none 62 1 1 0 5.2 11 4
|
||||
c-none none 65 1 1 0 2.9 6 12
|
||||
c-none none 66 2 1 1 12.1 10 4 a=1;
|
||||
c-none none 67 1 1 0 2.6 3 11
|
||||
c-none none 68 2 1 1 9.1 2 4 a'=1;
|
||||
c-none none 70 1 1 0 3.1 4 10
|
||||
c-none none 69 2 1 1 4.2 13 4 a=1;
|
||||
c-none none 76 6 1 5 8.7 0 15 a'=4;a=1;
|
||||
c-none none 71 1 1 0 3.2 0 16
|
||||
c-none none 73 1 1 0 3.1 3 13
|
||||
c-none none 75 1 1 0 3.0 0 9
|
||||
c-none none 80 1 1 0 3.0 12 19
|
||||
c-none none 79 1 1 0 3.2 8 4
|
||||
c-none none 82 1 1 0 3.6 12 22
|
||||
c-none none 74 1 1 0 3.1 13 9
|
||||
c-none none 77 3 1 2 3.8 6 9 c''=1;a=1;
|
||||
c-none none 84 1 1 0 3.0 2 5
|
||||
c-none none 81 3 1 2 3.3 0 5 c'''=1;a'=1;
|
||||
c-none none 83 1 1 0 2.9 7 25
|
||||
c-none none 78 1 1 0 3.1 9 9
|
||||
c-none none 88 1 1 0 3.3 9 25
|
||||
c-none none 72 3 1 2 3.3 12 11 c''=1;a'=1;
|
||||
c-none none 95 2 1 1 2.5 15 19 a'=1;
|
||||
c-none none 93 1 1 0 3.6 14 17
|
||||
c-none none 90 2 1 1 2.9 1 19 c''=1;
|
||||
c-none none 98 2 1 1 2.8 10 5 c'''=1;
|
||||
c-none none 91 1 1 0 5.1 12 4
|
||||
c-none none 85 1 1 0 3.4 14 20
|
||||
c-none none 100 1 1 0 3.1 13 25
|
||||
c-none none 87 1 1 0 31.4 9 10
|
||||
c-none none 86 1 1 0 3.0 8 10
|
||||
c-none none 102 1 1 0 3.4 9 4
|
||||
c-none none 99 2 1 1 2.9 9 6 c''=1;
|
||||
c-none none 89 2 1 1 2.8 6 5 a'=1;
|
||||
c-none none 92 1 1 0 2.8 9 12
|
||||
c-none none 97 1 1 0 2.6 7 12
|
||||
c-none none 96 1 1 0 2.7 3 8
|
||||
c-none none 94 1 1 0 3.5 5 4
|
||||
c-none none 101 2 1 1 3.2 3 19 c''=1;
|
||||
c-none none 106 1 1 0 2.8 6 9
|
||||
c-none none 107 3 1 2 3.0 1 8 a'=2;
|
||||
c-none none 103 1 1 0 4.4 8 4
|
||||
c-none none 110 2 1 1 2.7 11 19 a'=1;
|
||||
c-none none 104 1 1 0 3.5 6 9
|
||||
c-none none 111 1 1 0 3.2 3 9
|
||||
c-none none 109 1 1 0 3.3 2 11
|
||||
c-none none 112 2 1 1 3.6 7 4 a'=1;
|
||||
c-none none 113 1 1 0 3.5 5 13
|
||||
c-none none 108 2 1 1 5.1 10 4 c'''=1;
|
||||
c-none none 115 1 1 0 3.2 14 14
|
||||
c-none none 105 1 1 0 2.7 13 10
|
||||
c-none none 114 2 1 1 3.4 7 6 c=1;
|
||||
c-none none 118 2 1 1 2.6 9 4 a=1;
|
||||
c-none none 116 2 1 1 3.0 14 8 a'=1;
|
||||
c-none none 120 2 1 1 3.1 12 12 a'=1;
|
||||
c-none none 119 1 1 0 3.1 8 18
|
||||
c-none none 124 1 1 0 3.3 9 18
|
||||
c-none none 122 1 1 0 4.6 2 4
|
||||
c-none none 123 2 1 1 32.4 5 5 c'''=1;
|
||||
c-none none 129 1 1 0 3.2 7 4
|
||||
c-none none 121 1 1 0 7.8 5 19
|
||||
c-none none 117 1 1 0 3.3 7 9
|
||||
c-none none 132 1 1 0 3.2 6 11
|
||||
c-none none 125 1 1 0 2.8 5 6
|
||||
c-none none 126 1 1 0 9.0 0 11
|
||||
c-none none 133 3 1 2 5.4 11 4 a'=2;
|
||||
c-none none 130 1 1 0 5.2 10 4
|
||||
c-none none 128 6 1 5 4.0 0 5 a'=3;c=1;a=1;
|
||||
c-none none 131 1 1 0 3.5 0 4
|
||||
c-none none 134 4 1 3 2.8 11 19 a'=3;
|
||||
c-none none 138 1 1 0 3.2 10 23
|
||||
c-none none 137 2 1 1 3.1 1 17 a'=1;
|
||||
c-none none 127 4 1 3 4.3 14 4 a'=2;c'''=1;
|
||||
c-none none 141 2 1 1 2.7 11 26 a'=1;
|
||||
c-none none 142 2 1 1 5.8 5 4 c=1;
|
||||
c-none none 135 2 1 1 3.1 6 17 a'=1;
|
||||
c-none none 140 4 1 3 7.1 13 14 b=1;a'=1;a=1;
|
||||
c-none none 136 1 1 0 12.6 6 4
|
||||
c-none none 144 1 1 0 3.3 8 7
|
||||
c-none none 145 1 1 0 3.3 9 4
|
||||
c-none none 151 1 1 0 2.9 14 8
|
||||
c-none none 139 3 1 2 10.5 4 4 a'=2;
|
||||
c-none none 148 2 1 1 2.7 2 15 a=1;
|
||||
c-none none 150 1 1 0 28.8 3 9
|
||||
c-none none 147 4 1 3 3.5 7 24 a'=3;
|
||||
c-none none 155 1 1 0 3.2 0 4
|
||||
c-none none 156 2 1 1 3.3 10 13 a=1;
|
||||
c-none none 154 1 1 0 3.2 7 16
|
||||
c-none none 153 1 1 0 3.6 1 6
|
||||
c-none none 149 2 1 1 3.2 14 5 a'=1;
|
||||
c-none none 152 2 1 1 3.2 1 21 a'=1;
|
||||
c-none none 146 1 1 0 3.7 2 5
|
||||
c-none none 159 1 1 0 3.4 9 5
|
||||
c-none none 143 5 1 4 5.0 2 4 c'''=1;c''=1;a'=1;a=1;
|
||||
c-none none 157 1 1 0 4.0 10 10
|
||||
c-none none 160 2 1 1 2.8 11 9 a'=1;
|
||||
c-none none 168 1 1 0 2.7 5 27
|
||||
c-none none 158 1 1 0 3.0 2 19
|
||||
c-none none 163 1 1 0 3.3 6 16
|
||||
c-none none 175 1 1 0 3.0 9 6
|
||||
c-none none 165 1 1 0 2.7 3 11
|
||||
c-none none 161 4 1 3 3.0 10 8 a'=3;
|
||||
c-none none 171 1 1 0 4.3 3 13
|
||||
c-none none 167 2 1 1 3.2 2 11 a=1;
|
||||
c-none none 164 1 1 0 3.7 14 4
|
||||
c-none none 162 1 1 0 2.8 14 14
|
||||
c-none none 179 1 1 0 3.2 0 12
|
||||
c-none none 166 4 1 3 3.4 0 22 a'=3;
|
||||
c-none none 172 1 1 0 4.2 2 4
|
||||
c-none none 170 1 1 0 4.3 10 4
|
||||
c-none none 173 3 1 2 2.5 5 5 a'=2;
|
||||
c-none none 183 1 1 0 3.5 10 13
|
||||
c-none none 176 3 1 2 3.5 5 13 a'=1;a=1;
|
||||
c-none none 177 1 1 0 2.4 1 23
|
||||
c-none none 174 1 1 0 6.7 2 27
|
||||
c-none none 178 2 1 1 2.7 5 9 a'=1;
|
||||
c-none none 169 3 1 2 3.0 10 22 c''=1;a'=1;
|
||||
c-none none 182 1 1 0 34.1 4 18
|
||||
c-none none 184 3 1 2 5.0 7 10 a'=2;
|
||||
c-none none 180 2 1 1 3.0 0 23 a'=1;
|
||||
c-none none 181 4 1 3 3.5 5 5 a'=3;
|
||||
c-none none 189 2 1 1 5.8 5 4 a'=1;
|
||||
c-none none 185 2 1 1 3.4 5 4 c''=1;
|
||||
c-none none 190 4 1 3 2.7 10 16 a'=2;c'''=1;
|
||||
c-none none 186 1 1 0 14.8 1 26
|
||||
c-none none 188 4 1 3 5.8 8 27 a'=2;c''=1;
|
||||
c-none none 198 1 1 0 2.9 7 24
|
||||
c-none none 191 2 1 1 3.6 13 26 a'=1;
|
||||
c-none none 193 1 1 0 4.1 6 6
|
||||
c-none none 195 1 1 0 3.2 13 18
|
||||
c-none none 192 1 1 0 3.3 7 4
|
||||
c-none none 199 1 1 0 2.5 4 27
|
||||
c-none none 200 1 1 0 3.2 1 15
|
||||
c-none none 197 4 1 3 3.1 0 17 a'=3;
|
||||
c-none none 196 2 1 1 3.1 6 24 a'=1;
|
||||
c-none none 201 1 1 0 5.0 9 4
|
||||
c-none none 187 2 1 1 4.5 3 4 c'''=1;
|
||||
c-none none 202 1 1 0 3.0 2 13
|
||||
c-none none 203 4 1 3 5.2 9 11 a'=3;
|
||||
c-none none 194 2 1 1 4.4 12 4 c=1;
|
||||
c-none none 207 1 1 0 3.1 13 23
|
||||
c-none none 205 1 1 0 2.8 14 4
|
||||
c-none none 204 2 1 1 2.9 4 10 c=1;
|
||||
c-none none 210 1 1 0 3.2 13 27
|
||||
c-none none 213 1 1 0 3.7 12 15
|
||||
c-none none 208 1 1 0 2.5 10 4
|
||||
c-none none 212 2 1 1 4.0 1 14 a'=1;
|
||||
c-none none 215 2 1 1 2.8 13 17 a'=1;
|
||||
c-none none 214 2 1 1 2.8 15 8 b=1;
|
||||
c-none none 217 1 1 0 2.7 13 6
|
||||
c-none none 209 3 1 2 2.9 2 21 c'''=1;c=1;
|
||||
c-none none 216 2 1 1 2.8 6 22 c'''=1;
|
||||
c-none none 206 6 1 5 7.0 5 13 a'=4;a=1;
|
||||
c-none none 211 3 1 2 3.4 2 5 c''=1;a=1;
|
||||
c-none none 219 1 1 0 2.8 4 10
|
||||
c-none none 221 2 1 1 4.0 8 4 a'=1;
|
||||
c-none none 226 1 1 0 31.5 14 16
|
||||
c-none none 228 1 1 0 3.1 15 4
|
||||
c-none none 227 1 1 0 3.0 2 15
|
||||
c-none none 230 1 1 0 3.6 1 20
|
||||
c-none none 223 1 1 0 2.6 10 15
|
||||
c-none none 231 1 1 0 7.0 3 20
|
||||
c-none none 232 1 1 0 4.4 10 4
|
||||
c-none none 229 1 1 0 3.6 0 12
|
||||
c-none none 233 1 1 0 4.0 13 4
|
||||
c-none none 222 3 1 2 3.1 11 15 c'''=1;a'=1;
|
||||
c-none none 235 1 1 0 9.0 12 4
|
||||
c-none none 225 1 1 0 3.4 6 27
|
||||
c-none none 236 1 1 0 3.6 12 12
|
||||
c-none none 224 3 1 2 3.6 10 19 a'=2;
|
||||
c-none none 220 1 1 0 3.7 13 4
|
||||
c-none none 234 3 1 2 9.6 2 4 a'=2;
|
||||
c-none none 245 2 1 1 3.4 8 11 a'=1;
|
||||
c-none none 243 1 1 0 3.1 2 16
|
||||
c-none none 242 1 1 0 3.4 12 9
|
||||
c-none none 244 1 1 0 3.0 13 12
|
||||
c-none none 241 1 1 0 5.0 4 4
|
||||
c-none none 238 1 1 0 3.3 12 19
|
||||
c-none none 237 1 1 0 4.4 5 4
|
||||
c-none none 239 1 1 0 3.0 2 17
|
||||
c-none none 218 3 1 2 3.2 3 11 c=1;a'=1;
|
||||
c-none none 240 1 1 0 4.1 6 4
|
||||
c-none none 250 1 1 0 2.9 1 7
|
||||
c-none none 249 1 1 0 4.8 2 27
|
||||
c-none none 254 1 1 0 9.7 4 18
|
||||
c-none none 255 1 1 0 3.9 11 9
|
||||
c-none none 253 1 1 0 4.4 0 21
|
||||
c-none none 252 4 1 3 2.7 2 14 a'=2;a=1;
|
||||
c-none none 246 4 1 3 3.3 10 14 c'''=1;c''=1;a'=1;
|
||||
c-none none 247 1 1 0 2.6 10 7
|
||||
c-none none 248 1 1 0 3.7 0 16
|
||||
c-none none 251 5 1 4 3.0 4 7 a'=2;c''=1;b=1;
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
259
docs/analysis/class-v6/logs/census/ctrl-era.tsv
Normal file
259
docs/analysis/class-v6/logs/census/ctrl-era.tsv
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
# ctrl-era class mx8+sh256x27 eras [0,1,2,3,4,5,6,7] seeds 32 era-widths 4 weights base bittest 1 bin d601098699840534 start 2026-10-08T12:59:30Z host igneum-build-4 threads 16
|
||||
cand era seed candidates accepted attempt bit_z bit_site bit_bit reasons
|
||||
ctrl-era 0 0 1 1 0 3.2 2 4
|
||||
ctrl-era 0 2 6 1 5 3.2 15 17 a'=4;c,=1;
|
||||
ctrl-era 0 10 18 1 17 3.4 12 4 a'=8;c,=6;a=2;c=1;
|
||||
ctrl-era 0 13 2 1 1 3.3 10 12 a=1;
|
||||
ctrl-era 0 4 1 1 0 2.8 3 15
|
||||
ctrl-era 0 9 7 1 6 3.6 15 3 a'=5;a=1;
|
||||
ctrl-era 0 5 4 1 3 3.1 10 17 a'=2;c,=1;
|
||||
ctrl-era 0 3 17 1 16 2.9 5 21 a'=11;c,=3;a=2;
|
||||
ctrl-era 0 6 9 1 8 3.3 7 17 a'=7;a=1;
|
||||
ctrl-era 0 15 4 1 3 3.2 6 7 c,=1;b=1;a'=1;
|
||||
ctrl-era 0 17 1 1 0 3.7 8 24
|
||||
ctrl-era 0 14 6 1 5 3.4 5 21 c,=2;a'=2;a=1;
|
||||
ctrl-era 0 1 10 1 9 3.1 7 27 a'=7;c,=2;
|
||||
ctrl-era 0 8 3 1 2 3.1 0 19 a'=2;
|
||||
ctrl-era 0 18 9 1 8 3.3 15 5 a'=5;c,=2;a=1;
|
||||
ctrl-era 0 16 14 1 13 3.2 1 23 a'=9;a=2;c,=1;b=1;
|
||||
ctrl-era 0 7 3 1 2 3.1 10 1 c,=1;a'=1;
|
||||
ctrl-era 0 12 5 1 4 3.0 0 10 a'=3;c,=1;
|
||||
ctrl-era 0 20 8 1 7 3.2 8 11 a'=5;a=2;
|
||||
ctrl-era 0 11 23 1 22 3.0 4 16 a'=15;c,=5;b=1;a=1;
|
||||
ctrl-era 0 19 14 1 13 3.5 1 3 a'=7;a=3;c,=2;b=1;
|
||||
ctrl-era 0 23 3 1 2 2.9 12 10 a'=2;
|
||||
ctrl-era 0 22 18 1 17 3.3 15 12 a'=10;c,=4;a=3;
|
||||
ctrl-era 0 26 2 1 1 2.8 2 0 a'=1;
|
||||
ctrl-era 0 27 9 1 8 5.9 12 21 a'=7;a=1;
|
||||
ctrl-era 0 21 1 1 0 3.0 2 6
|
||||
ctrl-era 0 24 12 1 11 2.7 11 10 a'=9;c,=2;
|
||||
ctrl-era 0 30 3 1 2 3.4 12 21 a'=2;
|
||||
ctrl-era 0 31 2 1 1 3.0 13 7 c,=1;
|
||||
ctrl-era 1 1 10 1 9 3.2 6 19 a'=7;c,=2;
|
||||
ctrl-era 1 2 6 1 5 2.9 7 3 a'=4;c,=1;
|
||||
ctrl-era 0 29 3 1 2 2.8 14 24 c,=1;a'=1;
|
||||
ctrl-era 0 25 4 1 3 2.9 12 5 c,=1;b=1;a'=1;
|
||||
ctrl-era 1 6 9 1 8 2.9 2 2 a'=7;a=1;
|
||||
ctrl-era 1 3 17 1 16 3.0 8 13 a'=11;c,=3;a=2;
|
||||
ctrl-era 1 9 7 1 6 3.4 0 19 a'=5;a=1;
|
||||
ctrl-era 0 28 3 1 2 3.3 9 19 c,=1;a'=1;
|
||||
ctrl-era 1 0 1 1 0 2.8 8 22
|
||||
ctrl-era 1 5 4 1 3 2.9 12 14 a'=2;c,=1;
|
||||
ctrl-era 1 7 3 1 2 3.3 15 8 c,=1;a'=1;
|
||||
ctrl-era 1 8 3 1 2 3.4 1 9 a'=2;
|
||||
ctrl-era 1 11 2 1 1 4.0 11 1 a'=1;
|
||||
ctrl-era 1 4 1 1 0 3.0 7 23
|
||||
ctrl-era 1 10 18 1 17 3.4 10 17 a'=8;c,=6;a=2;c=1;
|
||||
ctrl-era 1 14 6 1 5 2.8 7 21 c,=2;a'=2;a=1;
|
||||
ctrl-era 1 12 5 1 4 2.7 1 14 a'=3;c,=1;
|
||||
ctrl-era 1 19 14 1 13 3.0 13 5 a'=7;a=3;c,=2;b=1;
|
||||
ctrl-era 1 13 2 1 1 3.7 12 20 a=1;
|
||||
ctrl-era 1 15 4 1 3 3.4 12 7 c,=1;b=1;a'=1;
|
||||
ctrl-era 1 18 9 1 8 3.3 8 12 a'=5;c,=2;a=1;
|
||||
ctrl-era 1 22 9 1 8 2.8 9 4 a'=5;a=2;c,=1;
|
||||
ctrl-era 1 23 3 1 2 3.5 3 26 a'=2;
|
||||
ctrl-era 1 17 1 1 0 3.2 4 24
|
||||
ctrl-era 1 21 1 1 0 3.1 5 5
|
||||
ctrl-era 1 24 12 1 11 3.6 11 10 a'=9;c,=2;
|
||||
ctrl-era 1 20 8 1 7 3.7 0 6 a'=5;a=2;
|
||||
ctrl-era 1 16 14 1 13 3.7 5 17 a'=9;a=2;c,=1;b=1;
|
||||
ctrl-era 1 25 4 1 3 2.9 14 27 c,=1;b=1;a'=1;
|
||||
ctrl-era 1 29 3 1 2 3.4 8 18 c,=1;a'=1;
|
||||
ctrl-era 2 0 1 1 0 3.0 10 22
|
||||
ctrl-era 1 27 16 1 15 3.1 13 2 a'=11;c,=3;a=1;
|
||||
ctrl-era 2 1 3 1 2 3.5 15 5 a'=2;
|
||||
ctrl-era 1 30 3 1 2 3.1 0 25 a'=2;
|
||||
ctrl-era 1 28 3 1 2 3.2 3 3 c,=1;a'=1;
|
||||
ctrl-era 1 26 2 1 1 2.8 1 2 a'=1;
|
||||
ctrl-era 2 3 1 1 0 3.0 4 5
|
||||
ctrl-era 1 31 2 1 1 3.0 11 11 c,=1;
|
||||
ctrl-era 2 2 4 1 3 3.1 7 2 a'=3;
|
||||
ctrl-era 2 6 9 1 8 2.8 8 16 a'=7;a=1;
|
||||
ctrl-era 2 13 2 1 1 2.8 14 23 a=1;
|
||||
ctrl-era 2 11 2 1 1 4.0 0 0 a'=1;
|
||||
ctrl-era 2 7 1 1 0 4.3 13 0
|
||||
ctrl-era 2 5 3 1 2 2.8 8 24 a'=2;
|
||||
ctrl-era 2 4 1 1 0 3.3 0 3
|
||||
ctrl-era 2 16 9 1 8 3.4 6 16 a'=5;a=2;b=1;
|
||||
ctrl-era 2 8 3 1 2 3.4 6 8 a'=2;
|
||||
ctrl-era 2 12 1 1 0 3.7 3 0
|
||||
ctrl-era 2 18 5 1 4 3.4 13 19 a'=3;a=1;
|
||||
ctrl-era 2 15 2 1 1 3.1 6 0 b=1;
|
||||
ctrl-era 2 10 2 1 1 3.0 9 3 c,=1;
|
||||
ctrl-era 2 9 7 1 6 3.2 14 6 a'=5;a=1;
|
||||
ctrl-era 2 14 6 1 5 2.7 9 22 c,=2;a'=2;a=1;
|
||||
ctrl-era 2 17 1 1 0 3.5 3 11
|
||||
ctrl-era 2 20 8 1 7 3.1 3 17 a'=5;a=2;
|
||||
ctrl-era 2 22 9 1 8 3.4 1 1 a'=5;a=2;c,=1;
|
||||
ctrl-era 2 19 10 1 9 3.4 0 12 a'=5;a=2;c,=1;b=1;
|
||||
ctrl-era 3 0 1 1 0 2.9 1 11
|
||||
ctrl-era 2 24 8 1 7 3.8 13 22 a'=7;
|
||||
ctrl-era 2 21 1 1 0 2.9 7 25
|
||||
ctrl-era 2 27 9 1 8 3.1 7 14 a'=7;a=1;
|
||||
ctrl-era 2 23 3 1 2 3.8 1 18 a'=2;
|
||||
ctrl-era 2 30 3 1 2 3.3 1 21 a'=2;
|
||||
ctrl-era 2 29 1 1 0 2.8 6 16
|
||||
ctrl-era 2 26 2 1 1 3.3 3 7 a'=1;
|
||||
ctrl-era 2 25 1 1 0 3.2 7 17
|
||||
ctrl-era 2 31 1 1 0 5.9 2 0
|
||||
ctrl-era 3 2 4 1 3 2.8 14 21 a'=3;
|
||||
ctrl-era 2 28 2 1 1 3.1 0 0 a'=1;
|
||||
ctrl-era 3 1 10 1 9 3.1 4 1 a'=7;c,=2;
|
||||
ctrl-era 3 3 1 1 0 3.2 4 26
|
||||
ctrl-era 3 5 4 1 3 3.2 13 12 a'=2;c,=1;
|
||||
ctrl-era 3 6 9 1 8 3.1 9 17 a'=7;a=1;
|
||||
ctrl-era 3 4 1 1 0 3.7 4 16
|
||||
ctrl-era 3 7 3 1 2 3.3 10 11 c,=1;a'=1;
|
||||
ctrl-era 3 8 3 1 2 3.4 10 8 a'=2;
|
||||
ctrl-era 3 9 7 1 6 3.2 5 1 a'=5;a=1;
|
||||
ctrl-era 3 14 6 1 5 2.8 12 4 c,=2;a'=2;a=1;
|
||||
ctrl-era 3 11 22 1 21 5.0 5 0 a'=15;c,=4;b=1;a=1;
|
||||
ctrl-era 3 10 18 1 17 3.4 11 25 a'=8;c,=6;a=2;c=1;
|
||||
ctrl-era 3 16 14 1 13 3.5 7 24 a'=9;a=2;c,=1;b=1;
|
||||
ctrl-era 3 15 4 1 3 2.7 9 3 c,=1;b=1;a'=1;
|
||||
ctrl-era 3 13 2 1 1 4.1 4 1 a=1;
|
||||
ctrl-era 3 12 5 1 4 3.2 1 11 a'=3;c,=1;
|
||||
ctrl-era 3 17 1 1 0 3.0 0 7
|
||||
ctrl-era 3 18 5 1 4 3.7 1 2 a'=3;a=1;
|
||||
ctrl-era 3 20 8 1 7 3.0 6 6 a'=5;a=2;
|
||||
ctrl-era 3 19 10 1 9 3.2 6 13 a'=5;a=2;c,=1;b=1;
|
||||
ctrl-era 3 21 1 1 0 2.8 6 3
|
||||
ctrl-era 3 22 9 1 8 3.5 9 11 a'=5;a=2;c,=1;
|
||||
ctrl-era 3 26 2 1 1 3.0 6 18 a'=1;
|
||||
ctrl-era 3 24 8 1 7 3.1 5 1 a'=7;
|
||||
ctrl-era 3 23 3 1 2 2.8 2 22 a'=2;
|
||||
ctrl-era 3 29 1 1 0 3.0 4 8
|
||||
ctrl-era 3 25 4 1 3 2.7 7 6 c,=1;b=1;a'=1;
|
||||
ctrl-era 3 27 12 1 11 5.5 5 0 a'=9;c,=1;a=1;
|
||||
ctrl-era 3 28 3 1 2 3.0 5 1 c,=1;a'=1;
|
||||
ctrl-era 3 30 3 1 2 3.1 6 4 a'=2;
|
||||
ctrl-era 3 31 2 1 1 3.5 11 3 c,=1;
|
||||
ctrl-era 4 1 10 1 9 3.4 9 3 a'=7;c,=2;
|
||||
ctrl-era 4 0 1 1 0 3.0 15 20
|
||||
ctrl-era 4 2 6 1 5 3.0 11 3 a'=4;c,=1;
|
||||
ctrl-era 4 4 1 1 0 3.2 5 19
|
||||
ctrl-era 4 3 17 1 16 3.4 10 14 a'=11;c,=3;a=2;
|
||||
ctrl-era 4 5 4 1 3 3.2 10 25 a'=2;c,=1;
|
||||
ctrl-era 4 7 3 1 2 3.1 3 10 c,=1;a'=1;
|
||||
ctrl-era 4 6 9 1 8 3.4 12 2 a'=7;a=1;
|
||||
ctrl-era 4 8 3 1 2 3.4 7 3 a'=2;
|
||||
ctrl-era 4 10 18 1 17 2.9 0 26 a'=8;c,=6;a=2;c=1;
|
||||
ctrl-era 4 9 7 1 6 3.0 4 9 a'=5;a=1;
|
||||
ctrl-era 4 11 2 1 1 3.5 9 8 a'=1;
|
||||
ctrl-era 4 13 2 1 1 3.5 10 1 a=1;
|
||||
ctrl-era 4 14 6 1 5 3.9 15 1 c,=2;a'=2;a=1;
|
||||
ctrl-era 4 12 5 1 4 3.3 7 15 a'=3;c,=1;
|
||||
ctrl-era 4 16 14 1 13 3.7 13 7 a'=9;a=2;c,=1;b=1;
|
||||
ctrl-era 4 15 4 1 3 3.2 1 25 c,=1;b=1;a'=1;
|
||||
ctrl-era 4 17 1 1 0 3.7 10 25
|
||||
ctrl-era 4 18 9 1 8 2.9 9 24 a'=5;c,=2;a=1;
|
||||
ctrl-era 4 19 14 1 13 3.4 0 11 a'=7;a=3;c,=2;b=1;
|
||||
ctrl-era 4 22 9 1 8 3.4 14 7 a'=5;a=2;c,=1;
|
||||
ctrl-era 4 26 2 1 1 3.5 15 22 a'=1;
|
||||
ctrl-era 4 20 8 1 7 3.6 0 10 a'=5;a=2;
|
||||
ctrl-era 4 21 1 1 0 3.1 11 20
|
||||
ctrl-era 4 25 4 1 3 3.2 13 20 c,=1;b=1;a'=1;
|
||||
ctrl-era 4 23 3 1 2 3.3 5 8 a'=2;
|
||||
ctrl-era 4 24 12 1 11 2.8 2 25 a'=9;c,=2;
|
||||
ctrl-era 4 27 16 1 15 2.9 6 4 a'=11;c,=3;a=1;
|
||||
ctrl-era 4 29 3 1 2 3.2 7 12 c,=1;a'=1;
|
||||
ctrl-era 4 28 3 1 2 3.2 6 10 c,=1;a'=1;
|
||||
ctrl-era 4 30 3 1 2 2.9 2 11 a'=2;
|
||||
ctrl-era 4 31 2 1 1 3.4 15 22 c,=1;
|
||||
ctrl-era 5 1 10 1 9 3.2 3 6 a'=7;c,=2;
|
||||
ctrl-era 5 0 1 1 0 3.1 4 25
|
||||
ctrl-era 5 2 6 1 5 3.2 5 2 a'=4;c,=1;
|
||||
ctrl-era 5 3 17 1 16 3.9 14 12 a'=11;c,=3;a=2;
|
||||
ctrl-era 5 5 4 1 3 3.3 5 24 a'=2;c,=1;
|
||||
ctrl-era 5 8 3 1 2 3.3 13 15 a'=2;
|
||||
ctrl-era 5 4 1 1 0 3.2 15 16
|
||||
ctrl-era 5 6 9 1 8 2.6 4 2 a'=7;a=1;
|
||||
ctrl-era 5 7 3 1 2 2.8 4 17 c,=1;a'=1;
|
||||
ctrl-era 5 9 7 1 6 3.2 1 2 a'=5;a=1;
|
||||
ctrl-era 5 11 2 1 1 3.1 9 22 a'=1;
|
||||
ctrl-era 5 10 18 1 17 2.8 6 4 a'=8;c,=6;a=2;c=1;
|
||||
ctrl-era 5 14 6 1 5 3.5 5 21 c,=2;a'=2;a=1;
|
||||
ctrl-era 5 13 2 1 1 3.2 15 4 a=1;
|
||||
ctrl-era 5 12 5 1 4 3.2 9 7 a'=3;c,=1;
|
||||
ctrl-era 5 15 4 1 3 2.8 11 9 c,=1;b=1;a'=1;
|
||||
ctrl-era 5 16 14 1 13 3.5 4 10 a'=9;a=2;c,=1;b=1;
|
||||
ctrl-era 5 17 1 1 0 3.3 12 7
|
||||
ctrl-era 5 18 9 1 8 3.0 10 14 a'=5;c,=2;a=1;
|
||||
ctrl-era 5 20 8 1 7 3.5 4 7 a'=5;a=2;
|
||||
ctrl-era 5 21 1 1 0 2.8 11 5
|
||||
ctrl-era 5 22 9 1 8 3.1 13 10 a'=5;a=2;c,=1;
|
||||
ctrl-era 5 19 14 1 13 3.2 8 0 a'=7;a=3;c,=2;b=1;
|
||||
ctrl-era 5 23 3 1 2 2.9 9 5 a'=2;
|
||||
ctrl-era 5 24 12 1 11 2.8 9 8 a'=9;c,=2;
|
||||
ctrl-era 5 25 4 1 3 3.1 13 16 c,=1;b=1;a'=1;
|
||||
ctrl-era 5 27 16 1 15 3.2 11 23 a'=11;c,=3;a=1;
|
||||
ctrl-era 5 26 2 1 1 3.2 13 26 a'=1;
|
||||
ctrl-era 5 29 3 1 2 3.3 0 24 c,=1;a'=1;
|
||||
ctrl-era 5 28 3 1 2 3.3 12 17 c,=1;a'=1;
|
||||
ctrl-era 5 30 3 1 2 2.8 9 14 a'=2;
|
||||
ctrl-era 6 1 10 1 9 3.0 15 17 a'=7;c,=2;
|
||||
ctrl-era 5 31 2 1 1 3.4 5 23 c,=1;
|
||||
ctrl-era 6 0 1 1 0 3.0 12 1
|
||||
ctrl-era 6 2 6 1 5 3.1 15 3 a'=4;c,=1;
|
||||
ctrl-era 6 3 17 1 16 3.5 11 0 a'=11;c,=3;a=2;
|
||||
ctrl-era 6 4 1 1 0 3.4 2 15
|
||||
ctrl-era 6 5 4 1 3 4.4 2 13 a'=2;c,=1;
|
||||
ctrl-era 6 6 9 1 8 3.6 7 19 a'=7;a=1;
|
||||
ctrl-era 6 8 3 1 2 3.3 9 13 a'=2;
|
||||
ctrl-era 6 7 3 1 2 2.8 9 25 c,=1;a'=1;
|
||||
ctrl-era 6 9 7 1 6 3.2 1 6 a'=5;a=1;
|
||||
ctrl-era 6 11 23 1 22 2.8 0 24 a'=15;c,=5;b=1;a=1;
|
||||
ctrl-era 6 10 18 1 17 3.7 5 2 a'=8;c,=6;a=2;c=1;
|
||||
ctrl-era 6 13 2 1 1 3.3 2 18 a=1;
|
||||
ctrl-era 6 14 6 1 5 2.9 5 27 c,=2;a'=2;a=1;
|
||||
ctrl-era 6 12 5 1 4 3.1 7 25 a'=3;c,=1;
|
||||
ctrl-era 6 16 14 1 13 3.1 11 20 a'=9;a=2;c,=1;b=1;
|
||||
ctrl-era 6 15 4 1 3 2.8 5 13 c,=1;b=1;a'=1;
|
||||
ctrl-era 6 17 1 1 0 2.8 4 24
|
||||
ctrl-era 6 18 9 1 8 3.5 3 25 a'=5;c,=2;a=1;
|
||||
ctrl-era 6 19 14 1 13 3.3 13 12 a'=7;a=3;c,=2;b=1;
|
||||
ctrl-era 6 21 1 1 0 3.4 11 1
|
||||
ctrl-era 6 20 8 1 7 4.9 0 3 a'=5;a=2;
|
||||
ctrl-era 6 22 9 1 8 2.8 5 18 a'=5;a=2;c,=1;
|
||||
ctrl-era 6 23 3 1 2 2.8 8 7 a'=2;
|
||||
ctrl-era 6 24 12 1 11 3.6 12 6 a'=9;c,=2;
|
||||
ctrl-era 6 25 4 1 3 2.8 12 7 c,=1;b=1;a'=1;
|
||||
ctrl-era 6 27 16 1 15 2.9 2 15 a'=11;c,=3;a=1;
|
||||
ctrl-era 6 26 2 1 1 3.3 0 0 a'=1;
|
||||
ctrl-era 6 29 3 1 2 2.7 8 13 c,=1;a'=1;
|
||||
ctrl-era 6 30 3 1 2 3.5 9 22 a'=2;
|
||||
ctrl-era 6 28 3 1 2 3.5 3 23 c,=1;a'=1;
|
||||
ctrl-era 6 31 2 1 1 3.0 8 16 c,=1;
|
||||
ctrl-era 7 0 1 1 0 3.1 9 1
|
||||
ctrl-era 7 1 3 1 2 2.9 15 1 a'=2;
|
||||
ctrl-era 7 2 4 1 3 3.1 14 23 a'=3;
|
||||
ctrl-era 7 5 3 1 2 2.8 13 0 a'=2;
|
||||
ctrl-era 7 3 5 1 4 3.5 4 3 a'=2;c,=1;a=1;
|
||||
ctrl-era 7 6 9 1 8 2.9 5 6 a'=7;a=1;
|
||||
ctrl-era 7 4 1 1 0 3.5 2 0
|
||||
ctrl-era 7 7 1 1 0 2.7 15 2
|
||||
ctrl-era 7 8 3 1 2 2.8 9 21 a'=2;
|
||||
ctrl-era 7 9 7 1 6 3.0 13 21 a'=5;a=1;
|
||||
ctrl-era 7 11 2 1 1 3.2 7 16 a'=1;
|
||||
ctrl-era 7 10 2 1 1 3.2 13 12 c,=1;
|
||||
ctrl-era 7 14 6 1 5 3.6 9 20 c,=2;a'=2;a=1;
|
||||
ctrl-era 7 12 1 1 0 2.9 3 0
|
||||
ctrl-era 7 13 2 1 1 3.8 13 25 a=1;
|
||||
ctrl-era 7 15 4 1 3 4.1 0 3 c,=1;b=1;a'=1;
|
||||
ctrl-era 7 16 9 1 8 2.8 4 15 a'=5;a=2;b=1;
|
||||
ctrl-era 7 17 1 1 0 3.2 4 7
|
||||
ctrl-era 7 18 9 1 8 3.3 10 17 a'=5;c,=2;a=1;
|
||||
ctrl-era 7 19 14 1 13 2.5 15 0 a'=7;a=3;c,=2;b=1;
|
||||
ctrl-era 7 21 1 1 0 3.5 8 10
|
||||
ctrl-era 7 20 8 1 7 3.7 15 16 a'=5;a=2;
|
||||
ctrl-era 7 22 9 1 8 3.5 9 18 a'=5;a=2;c,=1;
|
||||
ctrl-era 7 23 3 1 2 2.9 13 8 a'=2;
|
||||
ctrl-era 7 24 8 1 7 2.7 11 11 a'=7;
|
||||
ctrl-era 7 27 9 1 8 3.5 0 2 a'=7;a=1;
|
||||
ctrl-era 7 26 2 1 1 3.2 13 17 a'=1;
|
||||
ctrl-era 7 29 3 1 2 3.3 11 21 c,=1;a'=1;
|
||||
ctrl-era 7 25 4 1 3 3.2 3 21 c,=1;b=1;a'=1;
|
||||
ctrl-era 7 30 3 1 2 3.6 12 14 a'=2;
|
||||
ctrl-era 7 28 3 1 2 3.4 0 20 c,=1;a'=1;
|
||||
ctrl-era 7 31 1 1 0 3.5 10 16
|
||||
# end 2026-10-08T13:02:25Z rows=257
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
258
docs/analysis/class-v6/logs/census/ctrl-none.tsv
Normal file
258
docs/analysis/class-v6/logs/census/ctrl-none.tsv
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
# ctrl-none class mx8+sh256x27 eras [none] seeds 256 era-widths 4 weights base bittest off bin d601098699840534 start 2026-10-08T12:45:46Z host igneum-build-3 threads 8
|
||||
cand era seed candidates accepted attempt bit_z bit_site bit_bit reasons
|
||||
ctrl-none none 1 3 1 2 26.4 10 0 a'=2;
|
||||
ctrl-none none 4 1 1 0 91.3 2 0
|
||||
ctrl-none none 0 1 1 0 41.3 0 0
|
||||
ctrl-none none 3 1 1 0 28.7 1 0
|
||||
ctrl-none none 5 1 1 0 3.0 10 19
|
||||
ctrl-none none 6 3 1 2 67.3 6 0 a'=2;
|
||||
ctrl-none none 7 4 1 3 31.5 3 0 a'=3;
|
||||
ctrl-none none 2 3 1 2 3.2 13 8 a'=2;
|
||||
ctrl-none none 8 1 1 0 64.8 1 0
|
||||
ctrl-none none 12 1 1 0 77.0 0 0
|
||||
ctrl-none none 9 11 1 10 5.5 3 4 a'=10;
|
||||
ctrl-none none 15 1 1 0 63.1 14 0
|
||||
ctrl-none none 14 1 1 0 3.2 3 23
|
||||
ctrl-none none 13 4 1 3 44.2 4 0 a'=3;
|
||||
ctrl-none none 11 3 1 2 51.1 14 0 a'=1;a=1;
|
||||
ctrl-none none 10 3 1 2 8.3 0 0 c'''=1;a'=1;
|
||||
ctrl-none none 16 2 1 1 4.0 3 18 a'=1;
|
||||
ctrl-none none 20 5 1 4 30.9 11 0 a'=3;a=1;
|
||||
ctrl-none none 22 3 1 2 63.9 9 0 a'=2;
|
||||
ctrl-none none 21 2 1 1 32.5 3 0 a'=1;
|
||||
ctrl-none none 19 4 1 3 3.0 13 23 a'=2;a=1;
|
||||
ctrl-none none 17 1 1 0 2.9 4 25
|
||||
ctrl-none none 18 8 1 7 3.6 7 10 a'=6;c''=1;
|
||||
ctrl-none none 23 2 1 1 3.4 5 15 a'=1;
|
||||
ctrl-none none 24 1 1 0 32.3 7 0
|
||||
ctrl-none none 25 1 1 0 3.3 1 2
|
||||
ctrl-none none 27 1 1 0 3.0 8 25
|
||||
ctrl-none none 26 1 1 0 2.9 9 13
|
||||
ctrl-none none 29 3 1 2 36.8 1 0 b=1;a'=1;
|
||||
ctrl-none none 28 1 1 0 2.8 13 0
|
||||
ctrl-none none 31 1 1 0 2.8 4 5
|
||||
ctrl-none none 30 2 1 1 64.2 6 0 a'=1;
|
||||
ctrl-none none 32 1 1 0 3.0 8 17
|
||||
ctrl-none none 33 1 1 0 5.3 7 0
|
||||
ctrl-none none 34 3 1 2 64.5 8 0 a'=2;
|
||||
ctrl-none none 35 2 1 1 63.9 14 0 a=1;
|
||||
ctrl-none none 36 2 1 1 63.6 7 0 a'=1;
|
||||
ctrl-none none 37 3 1 2 6.4 1 1 a'=2;
|
||||
ctrl-none none 39 3 1 2 64.7 11 0 a'=1;a=1;
|
||||
ctrl-none none 38 8 1 7 3.8 2 4 a'=7;
|
||||
ctrl-none none 40 1 1 0 9.2 8 0
|
||||
ctrl-none none 41 1 1 0 33.0 6 0
|
||||
ctrl-none none 42 2 1 1 2.8 3 4 a'=1;
|
||||
ctrl-none none 43 2 1 1 24.2 5 0 a'=1;
|
||||
ctrl-none none 44 1 1 0 30.9 8 0
|
||||
ctrl-none none 45 2 1 1 33.0 13 0 a'=1;
|
||||
ctrl-none none 47 11 1 10 2.9 13 19 a'=10;
|
||||
ctrl-none none 46 1 1 0 3.3 12 20
|
||||
ctrl-none none 48 2 1 1 14.3 0 0 a'=1;
|
||||
ctrl-none none 49 6 1 5 3.4 12 3 a'=4;b=1;
|
||||
ctrl-none none 50 7 1 6 30.6 7 0 a'=4;b=1;a=1;
|
||||
ctrl-none none 52 1 1 0 2.8 10 27
|
||||
ctrl-none none 53 6 1 5 46.0 6 1 a'=4;b=1;
|
||||
ctrl-none none 51 5 1 4 37.0 0 0 a'=2;c''=1;a=1;
|
||||
ctrl-none none 54 3 1 2 64.0 11 0 a'=1;a=1;
|
||||
ctrl-none none 55 7 1 6 14.9 0 0 a'=6;
|
||||
ctrl-none none 56 5 1 4 31.2 5 0 a'=4;
|
||||
ctrl-none none 58 1 1 0 3.0 3 19
|
||||
ctrl-none none 57 2 1 1 15.3 15 1 a'=1;
|
||||
ctrl-none none 59 3 1 2 63.9 3 0 a'=2;
|
||||
ctrl-none none 60 5 1 4 3.2 7 11 a'=2;b=1;a=1;
|
||||
ctrl-none none 61 3 1 2 3.0 4 1 a'=2;
|
||||
ctrl-none none 63 1 1 0 97.1 3 0
|
||||
ctrl-none none 62 4 1 3 63.9 14 0 a'=2;a=1;
|
||||
ctrl-none none 64 1 1 0 28.7 6 1
|
||||
ctrl-none none 66 2 1 1 55.7 1 0 a'=1;
|
||||
ctrl-none none 65 1 1 0 21.1 0 0
|
||||
ctrl-none none 67 1 1 0 61.6 3 0
|
||||
ctrl-none none 68 3 1 2 3.4 15 14 a'=1;a=1;
|
||||
ctrl-none none 71 5 1 4 3.2 9 1 a'=4;
|
||||
ctrl-none none 69 2 1 1 3.5 1 19 a'=1;
|
||||
ctrl-none none 70 4 1 3 64.2 15 0 a'=2;a=1;
|
||||
ctrl-none none 72 1 1 0 63.9 7 0
|
||||
ctrl-none none 74 2 1 1 68.5 11 0 a'=1;
|
||||
ctrl-none none 73 4 1 3 2.9 6 14 a'=3;
|
||||
ctrl-none none 75 1 1 0 32.4 4 0
|
||||
ctrl-none none 76 2 1 1 31.7 5 0 b=1;
|
||||
ctrl-none none 78 4 1 3 55.8 0 0 a'=3;
|
||||
ctrl-none none 79 3 1 2 64.0 9 0 b=1;a'=1;
|
||||
ctrl-none none 77 5 1 4 3.0 14 21 a'=4;
|
||||
ctrl-none none 80 3 1 2 30.9 14 0 a'=2;
|
||||
ctrl-none none 81 2 1 1 3.5 1 14 a'=1;
|
||||
ctrl-none none 82 4 1 3 63.4 0 0 a'=2;a=1;
|
||||
ctrl-none none 83 5 1 4 2.8 10 1 a'=4;
|
||||
ctrl-none none 84 1 1 0 2.8 11 22
|
||||
ctrl-none none 85 1 1 0 3.3 8 6
|
||||
ctrl-none none 86 1 1 0 31.3 7 4
|
||||
ctrl-none none 87 1 1 0 62.1 4 0
|
||||
ctrl-none none 88 2 1 1 65.8 10 0 a'=1;
|
||||
ctrl-none none 90 2 1 1 64.0 6 0 a'=1;
|
||||
ctrl-none none 91 2 1 1 64.1 10 0 a=1;
|
||||
ctrl-none none 92 7 1 6 63.0 1 0 a'=3;a=2;c=1;
|
||||
ctrl-none none 89 19 1 18 2.9 5 8 a'=14;a=2;c''=1;b=1;
|
||||
ctrl-none none 94 4 1 3 3.8 3 0 a'=3;
|
||||
ctrl-none none 95 2 1 1 8.0 3 0 a'=1;
|
||||
ctrl-none none 96 2 1 1 39.7 9 0 a'=1;
|
||||
ctrl-none none 93 4 1 3 3.0 5 11 a'=2;c''=1;
|
||||
ctrl-none none 97 4 1 3 3.2 14 16 a'=3;
|
||||
ctrl-none none 99 1 1 0 3.4 0 26
|
||||
ctrl-none none 98 2 1 1 3.1 8 0 a'=1;
|
||||
ctrl-none none 100 3 1 2 3.1 4 6 a'=1;a=1;
|
||||
ctrl-none none 102 2 1 1 31.9 14 0 a'=1;
|
||||
ctrl-none none 101 2 1 1 8.4 14 0 a'=1;
|
||||
ctrl-none none 103 4 1 3 2.8 15 6 a'=3;
|
||||
ctrl-none none 104 4 1 3 2.9 4 8 a'=3;
|
||||
ctrl-none none 105 3 1 2 5.0 10 27 a'=2;
|
||||
ctrl-none none 106 1 1 0 3.2 5 15
|
||||
ctrl-none none 107 7 1 6 63.8 5 0 a'=5;a=1;
|
||||
ctrl-none none 108 2 1 1 2.9 7 11 a'=1;
|
||||
ctrl-none none 109 1 1 0 2.8 1 4
|
||||
ctrl-none none 110 3 1 2 3.6 0 7 a'=2;
|
||||
ctrl-none none 112 1 1 0 2.8 9 27
|
||||
ctrl-none none 111 4 1 3 64.1 15 0 a'=3;
|
||||
ctrl-none none 113 1 1 0 27.4 0 0
|
||||
ctrl-none none 114 3 1 2 4.6 2 1 b=1;a'=1;
|
||||
ctrl-none none 115 5 1 4 70.0 4 0 a'=3;a=1;
|
||||
ctrl-none none 116 2 1 1 15.3 15 1 c=1;
|
||||
ctrl-none none 117 1 1 0 3.2 6 17
|
||||
ctrl-none none 118 1 1 0 2.6 9 0
|
||||
ctrl-none none 120 5 1 4 78.0 4 0 a'=4;
|
||||
ctrl-none none 119 6 1 5 3.1 8 22 a'=5;
|
||||
ctrl-none none 121 3 1 2 63.0 0 0 a'=1;a=1;
|
||||
ctrl-none none 122 1 1 0 3.3 10 20
|
||||
ctrl-none none 123 4 1 3 2.8 9 1 a'=3;
|
||||
ctrl-none none 124 2 1 1 2.5 6 20 a'=1;
|
||||
ctrl-none none 125 8 1 7 3.0 6 18 a'=7;
|
||||
ctrl-none none 126 3 1 2 3.5 9 16 a'=2;
|
||||
ctrl-none none 128 2 1 1 15.8 14 26 a'=1;
|
||||
ctrl-none none 127 4 1 3 3.0 7 24 a'=3;
|
||||
ctrl-none none 129 2 1 1 64.4 7 0 a=1;
|
||||
ctrl-none none 130 1 1 0 3.4 8 25
|
||||
ctrl-none none 131 1 1 0 32.4 14 0
|
||||
ctrl-none none 132 2 1 1 3.5 11 7 a'=1;
|
||||
ctrl-none none 133 6 1 5 63.6 11 0 a'=5;
|
||||
ctrl-none none 134 1 1 0 3.4 15 26
|
||||
ctrl-none none 135 3 1 2 31.0 9 0 a'=2;
|
||||
ctrl-none none 136 2 1 1 41.0 3 0 a=1;
|
||||
ctrl-none none 137 1 1 0 63.8 14 0
|
||||
ctrl-none none 139 1 1 0 3.2 7 1
|
||||
ctrl-none none 140 2 1 1 32.8 11 0 a'=1;
|
||||
ctrl-none none 141 2 1 1 31.9 2 0 a'=1;
|
||||
ctrl-none none 142 4 1 3 95.5 11 0 a'=3;
|
||||
ctrl-none none 138 6 1 5 2.6 10 16 a'=3;c''=1;a=1;
|
||||
ctrl-none none 143 3 1 2 64.5 12 0 a=2;
|
||||
ctrl-none none 144 2 1 1 3.5 4 14 a'=1;
|
||||
ctrl-none none 145 2 1 1 27.6 0 0 a'=1;
|
||||
ctrl-none none 146 2 1 1 31.4 0 0 a'=1;
|
||||
ctrl-none none 149 4 1 3 3.1 8 18 a'=3;
|
||||
ctrl-none none 147 1 1 0 2.9 11 25
|
||||
ctrl-none none 148 5 1 4 63.2 12 0 a'=3;a=1;
|
||||
ctrl-none none 150 5 1 4 9.1 3 1 a'=3;a=1;
|
||||
ctrl-none none 152 2 1 1 2.8 4 23 a'=1;
|
||||
ctrl-none none 151 3 1 2 32.5 9 0 a'=2;
|
||||
ctrl-none none 153 2 1 1 3.0 15 2 a'=1;
|
||||
ctrl-none none 154 1 1 0 14.2 4 0
|
||||
ctrl-none none 155 9 1 8 43.8 3 0 a'=5;b=2;a=1;
|
||||
ctrl-none none 156 14 1 13 4.0 0 18 a'=10;a=3;
|
||||
ctrl-none none 157 4 1 3 3.2 4 19 a'=3;
|
||||
ctrl-none none 158 6 1 5 2.9 12 21 a'=3;a=2;
|
||||
ctrl-none none 159 8 1 7 2.9 2 5 a'=7;
|
||||
ctrl-none none 160 1 1 0 3.1 12 23
|
||||
ctrl-none none 161 2 1 1 64.2 7 0 a'=1;
|
||||
ctrl-none none 162 1 1 0 32.6 8 1
|
||||
ctrl-none none 163 1 1 0 57.2 4 1
|
||||
ctrl-none none 165 2 1 1 64.3 4 0 b=1;
|
||||
ctrl-none none 164 4 1 3 3.5 6 3 a'=2;b=1;
|
||||
ctrl-none none 166 1 1 0 63.7 6 15
|
||||
ctrl-none none 168 3 1 2 63.7 13 0 a'=2;
|
||||
ctrl-none none 167 3 1 2 3.0 1 3 a'=2;
|
||||
ctrl-none none 170 5 1 4 3.1 11 10 a'=4;
|
||||
ctrl-none none 171 4 1 3 3.2 14 23 a'=3;
|
||||
ctrl-none none 172 6 1 5 2.8 13 6 a'=5;
|
||||
ctrl-none none 169 5 1 4 8.2 0 0 a'=3;c''=1;
|
||||
ctrl-none none 174 3 1 2 63.3 10 1 a'=1;a=1;
|
||||
ctrl-none none 176 2 1 1 65.0 10 0 a'=1;
|
||||
ctrl-none none 175 1 1 0 50.3 1 0
|
||||
ctrl-none none 173 4 1 3 64.6 10 0 c''=1;a'=1;a=1;
|
||||
ctrl-none none 177 1 1 0 31.9 8 23
|
||||
ctrl-none none 178 6 1 5 2.8 14 6 a'=4;a=1;
|
||||
ctrl-none none 179 3 1 2 2.7 6 20 a'=2;
|
||||
ctrl-none none 180 1 1 0 3.4 5 7
|
||||
ctrl-none none 181 8 1 7 5.8 0 1 a'=6;a=1;
|
||||
ctrl-none none 183 4 1 3 72.1 10 0 a'=3;
|
||||
ctrl-none none 182 1 1 0 4.0 7 22
|
||||
ctrl-none none 184 2 1 1 3.9 10 20 a'=1;
|
||||
ctrl-none none 185 1 1 0 3.1 6 19
|
||||
ctrl-none none 186 1 1 0 29.9 7 1
|
||||
ctrl-none none 187 2 1 1 3.6 12 18 a'=1;
|
||||
ctrl-none none 188 1 1 0 19.2 0 3
|
||||
ctrl-none none 189 4 1 3 62.8 15 0 b=1;a'=1;a=1;
|
||||
ctrl-none none 191 3 1 2 55.0 9 26 a'=2;
|
||||
ctrl-none none 190 1 1 0 27.1 0 0
|
||||
ctrl-none none 192 3 1 2 3.6 15 23 b=1;a'=1;
|
||||
ctrl-none none 193 6 1 5 18.4 0 0 a'=4;a=1;
|
||||
ctrl-none none 194 1 1 0 16.2 13 0
|
||||
ctrl-none none 195 2 1 1 3.2 13 27 a'=1;
|
||||
ctrl-none none 196 3 1 2 5.4 15 0 a'=2;
|
||||
ctrl-none none 197 3 1 2 33.6 15 1 a'=2;
|
||||
ctrl-none none 198 1 1 0 3.3 0 18
|
||||
ctrl-none none 199 3 1 2 64.5 11 0 a'=2;
|
||||
ctrl-none none 200 1 1 0 28.3 9 1
|
||||
ctrl-none none 201 2 1 1 3.6 14 7 a'=1;
|
||||
ctrl-none none 202 2 1 1 25.3 6 0 a'=1;
|
||||
ctrl-none none 203 7 1 6 8.3 0 0 a'=5;b=1;
|
||||
ctrl-none none 204 1 1 0 3.7 6 21
|
||||
ctrl-none none 205 1 1 0 4.0 2 9
|
||||
ctrl-none none 206 3 1 2 63.1 12 0 c=1;a'=1;
|
||||
ctrl-none none 207 9 1 8 2.8 5 8 a'=8;
|
||||
ctrl-none none 208 1 1 0 3.6 11 9
|
||||
ctrl-none none 209 2 1 1 21.5 13 3 a=1;
|
||||
ctrl-none none 210 2 1 1 3.3 8 12 a'=1;
|
||||
ctrl-none none 211 2 1 1 18.3 15 12 a'=1;
|
||||
ctrl-none none 212 10 1 9 3.8 0 24 a'=7;a=2;
|
||||
ctrl-none none 213 3 1 2 64.9 1 0 a'=2;
|
||||
ctrl-none none 214 2 1 1 2.9 11 15 a'=1;
|
||||
ctrl-none none 215 2 1 1 19.1 3 0 a'=1;
|
||||
ctrl-none none 216 1 1 0 4.5 7 25
|
||||
ctrl-none none 217 5 1 4 14.2 8 0 a'=4;
|
||||
ctrl-none none 219 2 1 1 3.3 10 15 a'=1;
|
||||
ctrl-none none 218 1 1 0 16.7 1 0
|
||||
ctrl-none none 220 1 1 0 32.6 7 0
|
||||
ctrl-none none 221 8 1 7 31.9 8 0 a'=6;a=1;
|
||||
ctrl-none none 222 1 1 0 32.2 4 1
|
||||
ctrl-none none 223 3 1 2 31.7 7 0 a=2;
|
||||
ctrl-none none 224 2 1 1 51.2 0 0 a'=1;
|
||||
ctrl-none none 227 1 1 0 3.3 5 13
|
||||
ctrl-none none 226 4 1 3 31.2 11 0 a'=2;a=1;
|
||||
ctrl-none none 228 2 1 1 4.5 12 0 a'=1;
|
||||
ctrl-none none 229 1 1 0 30.8 7 0
|
||||
ctrl-none none 225 5 1 4 13.0 1 0 a'=3;c''=1;
|
||||
ctrl-none none 230 6 1 5 19.1 0 2 a'=4;c=1;
|
||||
ctrl-none none 231 1 1 0 3.2 6 2
|
||||
ctrl-none none 232 4 1 3 3.2 9 9 a'=2;a=1;
|
||||
ctrl-none none 233 2 1 1 34.4 3 2 a'=1;
|
||||
ctrl-none none 234 5 1 4 2.8 9 11 a'=4;
|
||||
ctrl-none none 236 7 1 6 3.5 4 11 a'=6;
|
||||
ctrl-none none 235 2 1 1 65.6 3 0 a'=1;
|
||||
ctrl-none none 237 1 1 0 64.5 3 0
|
||||
ctrl-none none 238 5 1 4 41.3 3 3 a'=3;b=1;
|
||||
ctrl-none none 239 5 1 4 28.0 0 0 a'=4;
|
||||
ctrl-none none 240 1 1 0 31.5 11 0
|
||||
ctrl-none none 241 1 1 0 3.9 9 27
|
||||
ctrl-none none 242 4 1 3 3.5 4 20 a'=2;a=1;
|
||||
ctrl-none none 244 1 1 0 12.6 0 25
|
||||
ctrl-none none 243 3 1 2 3.3 15 10 a'=2;
|
||||
ctrl-none none 245 5 1 4 15.6 2 1 a'=4;
|
||||
ctrl-none none 246 7 1 6 3.7 12 17 a'=6;
|
||||
ctrl-none none 247 1 1 0 63.4 10 0
|
||||
ctrl-none none 248 1 1 0 3.4 6 20
|
||||
ctrl-none none 249 1 1 0 50.8 0 0
|
||||
ctrl-none none 250 2 1 1 63.9 3 0 a'=1;
|
||||
ctrl-none none 251 2 1 1 64.3 2 0 a'=1;
|
||||
ctrl-none none 253 2 1 1 4.1 6 2 a'=1;
|
||||
ctrl-none none 252 8 1 7 23.6 11 0 a'=5;a=2;
|
||||
ctrl-none none 254 6 1 5 33.0 7 4 a'=4;b=1;
|
||||
ctrl-none none 255 1 1 0 27.9 0 0
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
17
docs/analysis/class-v6/logs/census/uniform-a-none.tsv
Normal file
17
docs/analysis/class-v6/logs/census/uniform-a-none.tsv
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
class era seed attempt program_id min_site_ratio min_site top0.1_share control_share ratio_to_control max_item_reads control_max reads
|
||||
w64m8g+sh256x27 none 4 1 f4487c87cfff1e7a 1.04905 3 0.002380 0.002377 1.0010 26 29 134217728
|
||||
w64m8g+sh256x27 none 1 1 29d2bcaba2d8edf5 1.04882 5 0.002382 0.002381 1.0001 27 27 134217728
|
||||
w64m8g+sh256x27 none 2 4 5fb46883e180b9f5 1.04885 4 0.002379 0.002380 0.9999 27 29 134217728
|
||||
w64m8g+sh256x27 none 0 2 73d1382989a69a8d 1.04894 1 0.002383 0.002382 1.0002 30 27 134217728
|
||||
w64m8g+sh256x27 none 6 1 311d1351271e9ea5 1.04885 2 0.002384 0.002380 1.0019 27 28 134217728
|
||||
w64m8g+sh256x27 none 5 1 520307099e14103b 1.04903 6 0.002381 0.002379 1.0007 27 28 134217728
|
||||
w64m8g+sh256x27 none 7 0 3dc79646fedce4b4 1.04390 11 0.002379 0.002379 1.0000 27 27 134217728
|
||||
w64m8g+sh256x27 none 3 6 c10bff8bcd67ceed 1.04888 14 0.002380 0.002380 1.0001 27 28 134217728
|
||||
w64m8g+sh256x27 none 11 0 9d64aefadd7069ac 1.04904 0 0.002381 0.002382 0.9996 27 27 134217728
|
||||
w64m8g+sh256x27 none 8 1 f98d219ab657820c 1.04906 2 0.002379 0.002379 1.0001 28 27 134217728
|
||||
w64m8g+sh256x27 none 9 6 18c383d344cb8948 1.04903 1 0.002381 0.002380 1.0007 28 28 134217728
|
||||
w64m8g+sh256x27 none 14 8 0996f04d4debf93a 1.04706 12 0.002396 0.002382 1.0061 27 27 134217728
|
||||
w64m8g+sh256x27 none 10 6 d3683b954033a6f5 1.04879 0 0.002381 0.002381 0.9999 27 27 134217728
|
||||
w64m8g+sh256x27 none 12 0 bd25136fa86cfdbb 1.04890 4 0.002380 0.002381 0.9993 26 27 134217728
|
||||
w64m8g+sh256x27 none 13 1 6fe6ecb094a5a1f6 1.04892 4 0.002379 0.002383 0.9986 27 27 134217728
|
||||
w64m8g+sh256x27 none 15 2 a2a33a99967a47a1 1.04894 1 0.002382 0.002380 1.0010 28 27 134217728
|
||||
|
17
docs/analysis/class-v6/logs/census/uniform-b-none.tsv
Normal file
17
docs/analysis/class-v6/logs/census/uniform-b-none.tsv
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
class era seed attempt program_id min_site_ratio min_site top0.1_share control_share ratio_to_control max_item_reads control_max reads
|
||||
w16m8g+sh256x27 none 4 1 f555de19a14e4b82 1.00253 1 0.002380 0.002377 1.0011 29 29 134217728
|
||||
w16m8g+sh256x27 none 1 0 a3c5c857b3d36618 1.00256 0 0.002383 0.002381 1.0008 28 27 134217728
|
||||
w16m8g+sh256x27 none 6 1 3acc6704472bbd7d 1.00103 2 0.002381 0.002380 1.0005 28 28 134217728
|
||||
w16m8g+sh256x27 none 3 0 db87b6c4aeb9f99f 1.00098 9 0.002383 0.002380 1.0011 26 28 134217728
|
||||
w16m8g+sh256x27 none 5 0 e10be8f927a334d8 1.00242 12 0.002382 0.002379 1.0014 30 28 134217728
|
||||
w16m8g+sh256x27 none 0 1 48a93d5e5b931cd7 1.00257 14 0.002380 0.002382 0.9989 27 27 134217728
|
||||
w16m8g+sh256x27 none 7 0 e2e1dec77412f084 1.00109 0 0.002382 0.002379 1.0011 27 27 134217728
|
||||
w16m8g+sh256x27 none 2 4 41a382862fa1488d 1.00260 5 0.002379 0.002380 0.9999 27 29 134217728
|
||||
w16m8g+sh256x27 none 8 1 2ff06eec9e814c1c 1.00162 7 0.002385 0.002379 1.0024 27 27 134217728
|
||||
w16m8g+sh256x27 none 13 0 59f08545a01b9029 1.00256 7 0.002381 0.002383 0.9995 27 27 134217728
|
||||
w16m8g+sh256x27 none 9 3 6aebe5693cda1de5 0.98146 2 0.002398 0.002380 1.0077 28 28 134217728
|
||||
w16m8g+sh256x27 none 14 0 c6e829543c08c96c 1.00087 11 0.002379 0.002382 0.9990 26 27 134217728
|
||||
w16m8g+sh256x27 none 12 0 e3dee32001ff9ac3 1.00253 15 0.002380 0.002381 0.9996 27 27 134217728
|
||||
w16m8g+sh256x27 none 15 1 15cba5607ee486c4 1.00249 0 0.002381 0.002380 1.0005 26 27 134217728
|
||||
w16m8g+sh256x27 none 11 0 3f0276dd1258a6bc 1.00254 0 0.002384 0.002382 1.0007 28 27 134217728
|
||||
w16m8g+sh256x27 none 10 0 d7fa72b4b388d8e6 1.00257 13 0.002387 0.002381 1.0023 26 27 134217728
|
||||
|
17
docs/analysis/class-v6/logs/census/uniform-c-none.tsv
Normal file
17
docs/analysis/class-v6/logs/census/uniform-c-none.tsv
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
class era seed attempt program_id min_site_ratio min_site top0.1_share control_share ratio_to_control max_item_reads control_max reads
|
||||
w64m8g+sh256x27 none 4 1 f4487c87cfff1e7a 1.04901 11 0.002380 0.002377 1.0014 29 29 134217728
|
||||
w64m8g+sh256x27 none 3 0 8062f9568d273707 1.04899 9 0.002382 0.002380 1.0009 27 28 134217728
|
||||
w64m8g+sh256x27 none 2 4 5fb46883e180b9f5 1.04904 7 0.002382 0.002380 1.0010 27 29 134217728
|
||||
w64m8g+sh256x27 none 6 1 311d1351271e9ea5 1.04875 2 0.002379 0.002380 0.9998 27 28 134217728
|
||||
w64m8g+sh256x27 none 0 1 666d8cc2e6eff09f 1.04884 10 0.002380 0.002382 0.9989 26 27 134217728
|
||||
w64m8g+sh256x27 none 1 0 1f41501380e84428 1.04885 0 0.002383 0.002381 1.0005 27 27 134217728
|
||||
w64m8g+sh256x27 none 7 0 3dc79646fedce4b4 1.04899 13 0.002381 0.002379 1.0011 27 27 134217728
|
||||
w64m8g+sh256x27 none 5 0 0a30b71cd5a3dbe8 1.04894 9 0.002380 0.002379 1.0003 26 28 134217728
|
||||
w64m8g+sh256x27 none 8 1 f98d219ab657820c 1.04650 7 0.002382 0.002379 1.0013 27 27 134217728
|
||||
w64m8g+sh256x27 none 13 0 902c487ec9750b41 1.04906 4 0.002381 0.002383 0.9993 28 27 134217728
|
||||
w64m8g+sh256x27 none 11 0 9d64aefadd7069ac 1.04901 8 0.002380 0.002382 0.9990 26 27 134217728
|
||||
w64m8g+sh256x27 none 10 0 55078f515da6288e 1.04896 5 0.002380 0.002381 0.9996 29 27 134217728
|
||||
w64m8g+sh256x27 none 12 0 bd25136fa86cfdbb 1.04893 7 0.002382 0.002381 1.0002 30 27 134217728
|
||||
w64m8g+sh256x27 none 14 0 cb0d96f15e24881c 1.04881 11 0.002379 0.002382 0.9987 27 27 134217728
|
||||
w64m8g+sh256x27 none 15 1 e0536568562886f4 1.04813 0 0.002384 0.002380 1.0016 27 27 134217728
|
||||
w64m8g+sh256x27 none 9 6 18c383d344cb8948 1.04892 1 0.002381 0.002380 1.0008 27 28 134217728
|
||||
|
17
docs/analysis/class-v6/logs/census/uniform-ctrl-none.tsv
Normal file
17
docs/analysis/class-v6/logs/census/uniform-ctrl-none.tsv
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
class era seed attempt program_id min_site_ratio min_site top0.1_share control_share ratio_to_control max_item_reads control_max reads
|
||||
mx8+sh256x27 none 0 0 d11a312a6f1860d2 0.99743 0 0.002383 0.002382 1.0002 27 27 134217728
|
||||
mx8+sh256x27 none 7 3 545f5f5cdca1adaf 0.99872 3 0.002383 0.002379 1.0016 27 27 134217728
|
||||
mx8+sh256x27 none 3 0 63be26aa9e0323a7 0.99904 1 0.002382 0.002380 1.0010 27 28 134217728
|
||||
mx8+sh256x27 none 2 2 3a68098aed6c7f40 1.00011 2 0.002381 0.002380 1.0004 27 29 134217728
|
||||
mx8+sh256x27 none 1 2 6826db4d7d33105a 0.99794 4 0.002380 0.002381 0.9993 27 27 134217728
|
||||
mx8+sh256x27 none 5 0 bf6bf3b3bd2da848 1.00011 14 0.002381 0.002379 1.0008 27 28 134217728
|
||||
mx8+sh256x27 none 4 0 858b8d2dc6543f25 0.97224 2 0.002382 0.002377 1.0020 27 29 134217728
|
||||
mx8+sh256x27 none 6 2 46b2809292569495 0.99192 6 0.002380 0.002380 1.0000 28 28 134217728
|
||||
mx8+sh256x27 none 8 0 6c27b57978ee3352 0.99266 1 0.002381 0.002379 1.0010 27 27 134217728
|
||||
mx8+sh256x27 none 9 10 7eae27998eb8665a 1.00002 11 0.002380 0.002380 1.0001 27 28 134217728
|
||||
mx8+sh256x27 none 13 3 3d8b2bc39168cb7f 0.99793 4 0.002381 0.002383 0.9994 27 27 134217728
|
||||
mx8+sh256x27 none 11 2 ae67b4dd5251fb6a 0.99692 14 0.002380 0.002382 0.9992 27 27 134217728
|
||||
mx8+sh256x27 none 15 0 7164644d33b0a84e 0.99244 6 0.002402 0.002380 1.0094 28 27 134217728
|
||||
mx8+sh256x27 none 14 0 243233af6588fdfc 1.00008 10 0.002381 0.002382 0.9996 26 27 134217728
|
||||
mx8+sh256x27 none 12 0 54e35300fdd79c5b 0.99465 0 0.002381 0.002381 0.9999 27 27 134217728
|
||||
mx8+sh256x27 none 10 2 5236758d791df4cd 1.00006 1 0.002420 0.002381 1.0163 29 27 134217728
|
||||
|
17
docs/analysis/class-v6/logs/census/uniform-w16-none.tsv
Normal file
17
docs/analysis/class-v6/logs/census/uniform-w16-none.tsv
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
class era seed attempt program_id min_site_ratio min_site top0.1_share control_share ratio_to_control max_item_reads control_max reads
|
||||
w16m8g+sh256x27 none 5 1 78bcd6b9f7a6ad43 1.00262 10 0.002382 0.002379 1.0013 27 28 134217728
|
||||
w16m8g+sh256x27 none 1 1 0bc1d6adf0f97c8d 1.00095 5 0.002383 0.002381 1.0005 30 27 134217728
|
||||
w16m8g+sh256x27 none 2 4 41a382862fa1488d 1.00110 2 0.002383 0.002380 1.0014 27 29 134217728
|
||||
w16m8g+sh256x27 none 6 1 3acc6704472bbd7d 1.00096 2 0.002384 0.002380 1.0016 27 28 134217728
|
||||
w16m8g+sh256x27 none 4 1 f555de19a14e4b82 1.00251 11 0.002380 0.002377 1.0011 27 29 134217728
|
||||
w16m8g+sh256x27 none 0 2 54596ced0a2b0f05 1.00250 1 0.002386 0.002382 1.0014 28 27 134217728
|
||||
w16m8g+sh256x27 none 7 0 e2e1dec77412f084 0.99668 11 0.002381 0.002379 1.0009 27 27 134217728
|
||||
w16m8g+sh256x27 none 3 6 7fed1428f93b1a65 1.00255 7 0.002380 0.002380 0.9998 27 28 134217728
|
||||
w16m8g+sh256x27 none 8 1 2ff06eec9e814c1c 1.00249 11 0.002380 0.002379 1.0005 29 27 134217728
|
||||
w16m8g+sh256x27 none 11 0 3f0276dd1258a6bc 1.00244 3 0.002382 0.002382 1.0000 26 27 134217728
|
||||
w16m8g+sh256x27 none 10 6 b55755978e54358d 1.00258 14 0.002381 0.002381 1.0000 26 27 134217728
|
||||
w16m8g+sh256x27 none 12 0 e3dee32001ff9ac3 1.00251 13 0.002382 0.002381 1.0001 27 27 134217728
|
||||
w16m8g+sh256x27 none 14 8 9342c807b4b01b42 1.00198 12 0.002394 0.002382 1.0053 27 27 134217728
|
||||
w16m8g+sh256x27 none 13 1 2e19dbf83daa8bae 1.00258 14 0.002381 0.002383 0.9993 28 27 134217728
|
||||
w16m8g+sh256x27 none 9 6 36de958c90f91238 1.00249 14 0.002378 0.002380 0.9994 27 28 134217728
|
||||
w16m8g+sh256x27 none 15 2 87d31ccbad786389 1.00245 7 0.002380 0.002380 0.9999 28 27 134217728
|
||||
|
259
docs/analysis/class-v6/logs/census/w16-era.tsv
Normal file
259
docs/analysis/class-v6/logs/census/w16-era.tsv
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
# w16-era class w16m8g+sh256x27 eras [0,1,2,3,4,5,6,7] seeds 32 era-widths 16 weights base bittest 1 bin d601098699840534 start 2026-10-08T13:02:33Z host igneum-build-4 threads 16
|
||||
cand era seed candidates accepted attempt bit_z bit_site bit_bit reasons
|
||||
w16-era 0 9 5 1 4 2.9 6 22 a'=3;c,=1;
|
||||
w16-era 0 8 1 1 0 2.8 0 4
|
||||
w16-era 0 10 1 1 0 3.3 11 5
|
||||
w16-era 0 12 3 1 2 2.9 15 11 a'=2;
|
||||
w16-era 0 14 13 1 12 3.1 6 24 a'=9;a=2;c,=1;
|
||||
w16-era 0 0 2 1 1 3.2 2 11 a'=1;
|
||||
w16-era 0 7 8 1 7 2.9 11 27 a'=5;c,=2;
|
||||
w16-era 0 15 6 1 5 3.1 1 5 a'=3;c,=1;a=1;
|
||||
w16-era 0 6 2 1 1 2.9 9 18 a'=1;
|
||||
w16-era 0 11 9 1 8 2.8 0 7 c,=5;a'=3;
|
||||
w16-era 0 3 2 1 1 3.1 13 9 a'=1;
|
||||
w16-era 0 2 9 1 8 3.7 9 13 a'=7;a=1;
|
||||
w16-era 0 4 9 1 8 3.0 10 7 a'=4;c,=3;a=1;
|
||||
w16-era 0 13 1 1 0 2.7 8 17
|
||||
w16-era 0 5 14 1 13 3.1 3 13 a'=12;a=1;
|
||||
w16-era 0 1 6 1 5 3.3 13 21 a'=4;c,=1;
|
||||
w16-era 0 16 5 1 4 3.0 0 5 a'=4;
|
||||
w16-era 0 17 3 1 2 3.5 15 12 a'=2;
|
||||
w16-era 0 21 4 1 3 3.0 1 15 a'=3;
|
||||
w16-era 0 20 8 1 7 2.6 13 27 a'=5;c,=1;a=1;
|
||||
w16-era 0 25 6 1 5 3.0 12 2 c,=2;a=2;a'=1;
|
||||
w16-era 0 23 7 1 6 3.7 1 22 a'=2;a=2;c,=1;c=1;
|
||||
w16-era 0 19 5 1 4 3.1 3 2 a'=3;a=1;
|
||||
w16-era 0 18 3 1 2 4.0 2 7 c,=2;
|
||||
w16-era 0 22 6 1 5 3.0 9 4 a'=4;a=1;
|
||||
w16-era 0 24 3 1 2 3.3 8 8 a'=2;
|
||||
w16-era 0 28 15 1 14 3.5 13 25 a'=10;c,=2;a=2;
|
||||
w16-era 0 26 2 1 1 3.1 11 12 a'=1;
|
||||
w16-era 0 27 7 1 6 2.9 15 17 a'=6;
|
||||
w16-era 0 29 4 1 3 2.6 15 10 c,=2;a'=1;
|
||||
w16-era 0 30 8 1 7 3.5 13 18 a'=6;a=1;
|
||||
w16-era 0 31 17 1 16 2.8 12 13 a'=7;c,=5;a=3;b=1;
|
||||
w16-era 1 2 9 1 8 3.5 5 6 a'=7;a=1;
|
||||
w16-era 1 0 2 1 1 4.0 14 13 a'=1;
|
||||
w16-era 1 3 2 1 1 3.1 1 4 a'=1;
|
||||
w16-era 1 1 6 1 5 2.9 15 5 a'=4;c,=1;
|
||||
w16-era 1 5 14 1 13 5.5 2 7 a'=12;a=1;
|
||||
w16-era 1 8 1 1 0 3.3 10 8
|
||||
w16-era 1 7 8 1 7 2.9 2 3 a'=5;c,=2;
|
||||
w16-era 1 6 2 1 1 3.1 3 21 a'=1;
|
||||
w16-era 1 4 12 1 11 3.4 3 22 a'=6;c,=4;a=1;
|
||||
w16-era 1 9 5 1 4 2.8 5 15 a'=3;c,=1;
|
||||
w16-era 1 10 1 1 0 3.2 12 16
|
||||
w16-era 1 12 3 1 2 3.0 7 20 a'=2;
|
||||
w16-era 1 11 9 1 8 3.0 0 14 c,=5;a'=3;
|
||||
w16-era 1 13 1 1 0 3.0 14 7
|
||||
w16-era 1 14 13 1 12 3.0 2 5 a'=9;a=2;c,=1;
|
||||
w16-era 1 15 6 1 5 3.7 6 8 a'=3;c,=1;a=1;
|
||||
w16-era 1 16 5 1 4 3.1 9 22 a'=4;
|
||||
w16-era 1 17 3 1 2 2.9 14 6 a'=2;
|
||||
w16-era 1 19 5 1 4 3.6 3 27 a'=3;a=1;
|
||||
w16-era 1 18 3 1 2 2.8 8 11 c,=2;
|
||||
w16-era 1 20 8 1 7 2.9 6 21 a'=5;c,=1;a=1;
|
||||
w16-era 1 21 4 1 3 3.3 10 4 a'=3;
|
||||
w16-era 1 24 3 1 2 2.8 13 21 a'=2;
|
||||
w16-era 1 22 6 1 5 3.3 7 5 a'=4;a=1;
|
||||
w16-era 1 23 4 1 3 3.6 0 8 c,=1;a'=1;a=1;
|
||||
w16-era 1 25 6 1 5 3.1 4 8 c,=2;a=2;a'=1;
|
||||
w16-era 1 27 7 1 6 2.7 10 7 a'=6;
|
||||
w16-era 1 26 2 1 1 3.3 12 22 a'=1;
|
||||
w16-era 1 28 15 1 14 3.0 12 6 a'=10;c,=2;a=2;
|
||||
w16-era 1 29 4 1 3 3.2 2 11 c,=2;a'=1;
|
||||
w16-era 1 30 41 1 40 3.3 0 6 a'=29;c,=8;a=2;b=1;
|
||||
w16-era 1 31 17 1 16 3.2 10 3 a'=7;c,=5;a=3;b=1;
|
||||
w16-era 2 0 2 1 1 3.5 6 26 a'=1;
|
||||
w16-era 2 1 4 1 3 4.1 3 18 a'=3;
|
||||
w16-era 2 2 9 1 8 3.7 8 10 a'=7;a=1;
|
||||
w16-era 2 4 2 1 1 3.2 8 22 a'=1;
|
||||
w16-era 2 3 2 1 1 2.9 0 18 a'=1;
|
||||
w16-era 2 5 14 1 13 3.1 7 10 a'=12;a=1;
|
||||
w16-era 2 8 1 1 0 3.8 5 19
|
||||
w16-era 2 6 2 1 1 3.0 12 6 a'=1;
|
||||
w16-era 2 7 4 1 3 2.9 13 4 a'=3;
|
||||
w16-era 2 9 3 1 2 3.8 12 2 a'=2;
|
||||
w16-era 2 10 1 1 0 2.8 3 19
|
||||
w16-era 2 11 2 1 1 3.2 8 9 a'=1;
|
||||
w16-era 2 12 3 1 2 3.7 7 17 a'=2;
|
||||
w16-era 2 13 1 1 0 3.1 1 19
|
||||
w16-era 2 14 5 1 4 4.4 15 2 a'=3;a=1;
|
||||
w16-era 2 15 3 1 2 3.3 0 25 a'=2;
|
||||
w16-era 2 16 5 1 4 2.9 13 26 a'=4;
|
||||
w16-era 2 18 1 1 0 2.9 3 26
|
||||
w16-era 2 17 3 1 2 3.0 9 20 a'=2;
|
||||
w16-era 2 20 7 1 6 3.0 12 2 a'=5;a=1;
|
||||
w16-era 2 19 5 1 4 3.1 12 17 a'=3;a=1;
|
||||
w16-era 2 21 4 1 3 3.2 11 17 a'=3;
|
||||
w16-era 2 23 2 1 1 3.0 12 12 a=1;
|
||||
w16-era 2 24 3 1 2 3.1 10 7 a'=2;
|
||||
w16-era 2 22 6 1 5 2.8 0 4 a'=4;a=1;
|
||||
w16-era 2 25 4 1 3 3.4 12 16 a=2;a'=1;
|
||||
w16-era 2 26 2 1 1 3.2 14 22 a'=1;
|
||||
w16-era 2 28 8 1 7 3.2 9 19 a'=5;a=2;
|
||||
w16-era 2 27 7 1 6 3.1 5 11 a'=6;
|
||||
w16-era 2 29 2 1 1 3.3 4 19 a'=1;
|
||||
w16-era 2 30 8 1 7 3.3 8 7 a'=6;a=1;
|
||||
w16-era 2 31 4 1 3 2.9 11 22 a'=2;b=1;
|
||||
w16-era 3 0 2 1 1 3.5 9 11 a'=1;
|
||||
w16-era 3 2 9 1 8 3.2 0 23 a'=7;a=1;
|
||||
w16-era 3 1 4 1 3 2.6 10 26 a'=3;
|
||||
w16-era 3 4 2 1 1 3.2 10 20 a'=1;
|
||||
w16-era 3 3 2 1 1 3.9 4 4 a'=1;
|
||||
w16-era 3 5 14 1 13 3.2 3 21 a'=12;a=1;
|
||||
w16-era 3 6 2 1 1 3.2 6 19 a'=1;
|
||||
w16-era 3 8 1 1 0 3.1 7 16
|
||||
w16-era 3 7 4 1 3 2.8 9 26 a'=3;
|
||||
w16-era 3 9 3 1 2 3.2 3 11 a'=2;
|
||||
w16-era 3 10 1 1 0 3.2 0 23
|
||||
w16-era 3 12 3 1 2 3.3 8 8 a'=2;
|
||||
w16-era 3 11 2 1 1 3.2 13 22 a'=1;
|
||||
w16-era 3 13 1 1 0 3.4 0 4
|
||||
w16-era 3 14 5 1 4 5.5 15 3 a'=3;a=1;
|
||||
w16-era 3 15 3 1 2 3.5 11 17 a'=2;
|
||||
w16-era 3 16 5 1 4 3.2 6 4 a'=4;
|
||||
w16-era 3 17 3 1 2 3.1 3 14 a'=2;
|
||||
w16-era 3 20 7 1 6 3.7 8 3 a'=5;a=1;
|
||||
w16-era 3 18 1 1 0 3.0 11 7
|
||||
w16-era 3 19 5 1 4 3.1 10 11 a'=3;a=1;
|
||||
w16-era 3 21 4 1 3 3.0 15 5 a'=3;
|
||||
w16-era 3 23 2 1 1 3.0 11 8 a=1;
|
||||
w16-era 3 24 3 1 2 3.7 0 19 a'=2;
|
||||
w16-era 3 22 6 1 5 3.8 5 13 a'=4;a=1;
|
||||
w16-era 3 28 8 1 7 4.9 9 2 a'=5;a=2;
|
||||
w16-era 3 25 4 1 3 3.9 0 2 a=2;a'=1;
|
||||
w16-era 3 27 7 1 6 3.2 6 5 a'=6;
|
||||
w16-era 3 26 2 1 1 3.2 3 6 a'=1;
|
||||
w16-era 3 29 2 1 1 3.1 7 2 a'=1;
|
||||
w16-era 3 30 8 1 7 2.7 2 5 a'=6;a=1;
|
||||
w16-era 3 31 10 1 9 5.0 10 2 a'=4;c,=2;a=2;b=1;
|
||||
w16-era 4 0 2 1 1 3.0 6 11 a'=1;
|
||||
w16-era 4 2 9 1 8 3.8 5 10 a'=7;a=1;
|
||||
w16-era 4 1 6 1 5 3.6 0 10 a'=4;c,=1;
|
||||
w16-era 4 3 2 1 1 3.1 10 15 a'=1;
|
||||
w16-era 4 4 9 1 8 4.0 1 14 a'=4;c,=3;a=1;
|
||||
w16-era 4 5 21 1 20 2.8 5 13 a'=15;c,=3;a=2;
|
||||
w16-era 4 6 2 1 1 3.4 3 16 a'=1;
|
||||
w16-era 4 8 1 1 0 3.4 1 5
|
||||
w16-era 4 11 9 1 8 3.8 8 23 c,=5;a'=3;
|
||||
w16-era 4 7 8 1 7 2.7 7 16 a'=5;c,=2;
|
||||
w16-era 4 9 5 1 4 3.0 2 3 a'=3;c,=1;
|
||||
w16-era 4 10 1 1 0 3.6 0 15
|
||||
w16-era 4 12 3 1 2 2.8 11 14 a'=2;
|
||||
w16-era 4 13 1 1 0 3.2 12 3
|
||||
w16-era 4 14 13 1 12 3.2 15 13 a'=9;a=2;c,=1;
|
||||
w16-era 4 15 6 1 5 2.9 15 12 a'=3;c,=1;a=1;
|
||||
w16-era 4 21 4 1 3 3.5 8 12 a'=3;
|
||||
w16-era 4 24 3 1 2 2.9 2 17 a'=2;
|
||||
w16-era 4 16 5 1 4 2.9 14 4 a'=4;
|
||||
w16-era 4 27 7 1 6 2.9 2 13 a'=6;
|
||||
w16-era 4 17 3 1 2 3.0 13 7 a'=2;
|
||||
w16-era 4 29 4 1 3 2.7 5 18 c,=2;a'=1;
|
||||
w16-era 4 22 6 1 5 3.2 2 18 a'=4;a=1;
|
||||
w16-era 4 20 8 1 7 3.0 13 8 a'=5;c,=1;a=1;
|
||||
w16-era 4 19 5 1 4 3.0 14 12 a'=3;a=1;
|
||||
w16-era 4 18 3 1 2 3.0 8 13 c,=2;
|
||||
w16-era 4 26 2 1 1 3.4 4 14 a'=1;
|
||||
w16-era 4 28 15 1 14 3.5 12 10 a'=10;c,=2;a=2;
|
||||
w16-era 4 25 6 1 5 3.2 12 20 c,=2;a=2;a'=1;
|
||||
w16-era 4 23 36 1 35 3.9 1 2 a'=21;c,=9;a=4;c=1;
|
||||
w16-era 4 30 41 1 40 3.4 8 12 a'=29;c,=8;a=2;b=1;
|
||||
w16-era 4 31 17 1 16 2.8 15 4 a'=7;c,=5;a=3;b=1;
|
||||
w16-era 5 1 6 1 5 3.6 11 15 a'=4;c,=1;
|
||||
w16-era 5 7 8 1 7 2.5 15 25 a'=5;c,=2;
|
||||
w16-era 5 5 21 1 20 5.2 9 18 a'=15;c,=3;a=2;
|
||||
w16-era 5 2 9 1 8 3.0 5 22 a'=7;a=1;
|
||||
w16-era 5 8 1 1 0 3.6 6 22
|
||||
w16-era 5 9 5 1 4 3.6 6 9 a'=3;c,=1;
|
||||
w16-era 5 0 2 1 1 3.0 1 20 a'=1;
|
||||
w16-era 5 4 9 1 8 3.2 0 11 a'=4;c,=3;a=1;
|
||||
w16-era 5 10 1 1 0 3.2 1 4
|
||||
w16-era 5 3 2 1 1 2.7 14 13 a'=1;
|
||||
w16-era 5 6 2 1 1 3.2 0 8 a'=1;
|
||||
w16-era 5 15 6 1 5 3.1 0 18 a'=3;c,=1;a=1;
|
||||
w16-era 5 12 3 1 2 3.3 10 23 a'=2;
|
||||
w16-era 5 17 3 1 2 2.8 7 23 a'=2;
|
||||
w16-era 5 11 9 1 8 3.1 13 25 c,=5;a'=3;
|
||||
w16-era 5 13 1 1 0 3.0 4 23
|
||||
w16-era 5 16 5 1 4 3.3 15 12 a'=4;
|
||||
w16-era 5 19 5 1 4 3.2 6 25 a'=3;a=1;
|
||||
w16-era 5 14 13 1 12 3.3 6 23 a'=9;a=2;c,=1;
|
||||
w16-era 5 24 3 1 2 2.7 8 2 a'=2;
|
||||
w16-era 5 22 6 1 5 3.0 1 8 a'=4;a=1;
|
||||
w16-era 5 21 4 1 3 3.1 13 9 a'=3;
|
||||
w16-era 5 18 3 1 2 3.0 13 4 c,=2;
|
||||
w16-era 5 26 2 1 1 3.1 3 20 a'=1;
|
||||
w16-era 5 27 7 1 6 3.4 13 26 a'=6;
|
||||
w16-era 5 20 8 1 7 3.2 1 4 a'=5;c,=1;a=1;
|
||||
w16-era 5 28 15 1 14 4.3 12 22 a'=10;c,=2;a=2;
|
||||
w16-era 5 29 4 1 3 3.4 3 23 c,=2;a'=1;
|
||||
w16-era 5 23 4 1 3 3.4 7 27 c,=1;a'=1;a=1;
|
||||
w16-era 5 25 6 1 5 3.4 9 24 c,=2;a=2;a'=1;
|
||||
w16-era 6 1 6 1 5 3.8 0 3 a'=4;c,=1;
|
||||
w16-era 6 0 2 1 1 3.5 10 6 a'=1;
|
||||
w16-era 5 30 41 1 40 2.9 15 18 a'=29;c,=8;a=2;b=1;
|
||||
w16-era 6 4 9 1 8 2.8 5 20 a'=4;c,=3;a=1;
|
||||
w16-era 6 3 2 1 1 3.0 9 2 a'=1;
|
||||
w16-era 6 8 1 1 0 3.2 14 18
|
||||
w16-era 5 31 17 1 16 3.5 2 5 a'=7;c,=5;a=3;b=1;
|
||||
w16-era 6 6 2 1 1 3.8 11 11 a'=1;
|
||||
w16-era 6 7 8 1 7 2.9 6 19 a'=5;c,=2;
|
||||
w16-era 6 2 9 1 8 3.2 5 5 a'=7;a=1;
|
||||
w16-era 6 5 21 1 20 3.0 2 23 a'=15;c,=3;a=2;
|
||||
w16-era 6 9 5 1 4 3.1 12 10 a'=3;c,=1;
|
||||
w16-era 6 14 13 1 12 3.5 6 5 a'=9;a=2;c,=1;
|
||||
w16-era 6 13 1 1 0 3.0 12 12
|
||||
w16-era 6 16 5 1 4 3.2 1 9 a'=4;
|
||||
w16-era 6 10 1 1 0 2.8 7 22
|
||||
w16-era 6 11 9 1 8 2.5 6 24 c,=5;a'=3;
|
||||
w16-era 6 12 3 1 2 3.0 7 5 a'=2;
|
||||
w16-era 6 19 5 1 4 3.1 9 3 a'=3;a=1;
|
||||
w16-era 6 22 6 1 5 3.7 3 25 a'=4;a=1;
|
||||
w16-era 6 21 4 1 3 3.0 15 4 a'=3;
|
||||
w16-era 6 24 3 1 2 3.5 15 14 a'=2;
|
||||
w16-era 6 15 6 1 5 2.9 11 13 a'=3;c,=1;a=1;
|
||||
w16-era 6 20 8 1 7 3.1 10 13 a'=5;c,=1;a=1;
|
||||
w16-era 6 27 7 1 6 3.1 2 18 a'=6;
|
||||
w16-era 6 18 3 1 2 3.1 4 11 c,=2;
|
||||
w16-era 6 17 3 1 2 2.7 4 7 a'=2;
|
||||
w16-era 6 28 15 1 14 3.6 12 6 a'=10;c,=2;a=2;
|
||||
w16-era 6 23 4 1 3 3.0 6 12 c,=1;a'=1;a=1;
|
||||
w16-era 6 30 41 1 40 2.9 4 7 a'=29;c,=8;a=2;b=1;
|
||||
w16-era 6 26 2 1 1 2.9 8 26 a'=1;
|
||||
w16-era 7 1 4 1 3 3.8 3 20 a'=3;
|
||||
w16-era 6 29 4 1 3 3.4 1 6 c,=2;a'=1;
|
||||
w16-era 7 3 2 1 1 3.1 13 5 a'=1;
|
||||
w16-era 7 6 2 1 1 3.0 6 17 a'=1;
|
||||
w16-era 7 2 9 1 8 4.5 7 9 a'=7;a=1;
|
||||
w16-era 6 25 7 1 6 3.3 10 17 c,=2;a=2;c'''=1;a'=1;
|
||||
w16-era 7 0 2 1 1 3.5 3 21 a'=1;
|
||||
w16-era 7 5 14 1 13 3.2 7 7 a'=12;a=1;
|
||||
w16-era 6 31 17 1 16 3.1 14 24 a'=7;c,=5;a=3;b=1;
|
||||
w16-era 7 9 3 1 2 2.8 13 23 a'=2;
|
||||
w16-era 7 4 2 1 1 3.0 6 22 a'=1;
|
||||
w16-era 7 11 2 1 1 3.2 14 5 a'=1;
|
||||
w16-era 7 7 6 1 5 3.0 12 4 a'=4;c,=1;
|
||||
w16-era 7 8 1 1 0 3.2 14 24
|
||||
w16-era 7 13 1 1 0 3.2 13 18
|
||||
w16-era 7 10 1 1 0 3.2 8 4
|
||||
w16-era 7 15 6 1 5 3.4 12 9 a'=3;c,=1;a=1;
|
||||
w16-era 7 16 5 1 4 2.9 14 11 a'=4;
|
||||
w16-era 7 14 5 1 4 3.0 4 11 a'=3;a=1;
|
||||
w16-era 7 12 3 1 2 3.4 15 5 a'=2;
|
||||
w16-era 7 22 6 1 5 3.5 7 13 a'=4;a=1;
|
||||
w16-era 7 23 2 1 1 2.9 14 27 a=1;
|
||||
w16-era 7 24 3 1 2 3.0 14 19 a'=2;
|
||||
w16-era 7 17 3 1 2 3.5 11 19 a'=2;
|
||||
w16-era 7 18 3 1 2 3.2 2 5 c,=2;
|
||||
w16-era 7 19 5 1 4 3.3 9 11 a'=3;a=1;
|
||||
w16-era 7 20 8 1 7 3.0 12 19 a'=5;c,=1;a=1;
|
||||
w16-era 7 21 4 1 3 3.1 2 3 a'=3;
|
||||
w16-era 7 28 14 1 13 3.4 5 6 a'=10;a=2;c,=1;
|
||||
w16-era 7 30 8 1 7 3.1 1 7 a'=6;a=1;
|
||||
w16-era 7 29 2 1 1 3.0 10 16 a'=1;
|
||||
w16-era 7 31 6 1 5 3.2 10 5 a'=3;c,=1;b=1;
|
||||
w16-era 7 25 4 1 3 3.1 1 20 a=2;a'=1;
|
||||
w16-era 7 26 2 1 1 2.8 2 2 a'=1;
|
||||
w16-era 7 27 7 1 6 2.9 0 9 a'=6;
|
||||
# end 2026-10-08T13:04:55Z rows=257
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
259
docs/analysis/class-v6/logs/census/w16-none.tsv
Normal file
259
docs/analysis/class-v6/logs/census/w16-none.tsv
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
# w16-none class w16m8g+sh256x27 eras [none] seeds 256 era-widths 16 weights base bittest off bin d601098699840534 start 2026-10-08T12:51:04Z host igneum-build-4 threads 16
|
||||
cand era seed candidates accepted attempt bit_z bit_site bit_bit reasons
|
||||
w16-none none 14 9 1 8 10.4 12 27 a'=7;a=1;
|
||||
w16-none none 12 1 1 0 3.2 1 21
|
||||
w16-none none 13 2 1 1 3.2 3 14 a'=1;
|
||||
w16-none none 9 7 1 6 3.0 3 14 a'=5;a=1;
|
||||
w16-none none 7 1 1 0 9.0 11 3
|
||||
w16-none none 8 2 1 1 3.7 8 2 a'=1;
|
||||
w16-none none 10 7 1 6 3.4 0 21 a'=6;
|
||||
w16-none none 1 2 1 1 14.8 5 2 a'=1;
|
||||
w16-none none 5 2 1 1 3.5 1 14 a'=1;
|
||||
w16-none none 15 3 1 2 3.3 8 5 a'=2;
|
||||
w16-none none 16 10 1 9 2.8 3 15 a'=9;
|
||||
w16-none none 11 1 1 0 3.0 15 7
|
||||
w16-none none 20 3 1 2 3.0 5 6 a'=1;a=1;
|
||||
w16-none none 6 2 1 1 16.2 2 2 a'=1;
|
||||
w16-none none 19 3 1 2 3.9 10 20 a'=2;
|
||||
w16-none none 4 2 1 1 3.8 13 10 a'=1;
|
||||
w16-none none 21 5 1 4 2.7 14 10 a'=4;
|
||||
w16-none none 0 3 1 2 3.3 12 7 a'=2;
|
||||
w16-none none 2 5 1 4 13.9 2 2 a'=3;a=1;
|
||||
w16-none none 18 3 1 2 3.0 10 2 a'=2;
|
||||
w16-none none 22 1 1 0 3.2 12 14
|
||||
w16-none none 24 2 1 1 7.1 10 2 a'=1;
|
||||
w16-none none 33 2 1 1 3.9 3 26 a=1;
|
||||
w16-none none 17 2 1 1 3.0 10 22 a'=1;
|
||||
w16-none none 27 3 1 2 3.3 8 13 b=1;a'=1;
|
||||
w16-none none 29 3 1 2 15.0 9 2 a'=2;
|
||||
w16-none none 30 3 1 2 9.9 0 6 a'=1;a=1;
|
||||
w16-none none 3 7 1 6 2.9 0 23 a'=3;a=2;c'''=1;
|
||||
w16-none none 28 2 1 1 32.3 4 2 a'=1;
|
||||
w16-none none 32 4 1 3 3.0 2 4 a'=3;
|
||||
w16-none none 25 13 1 12 7.2 6 2 a'=10;a=2;
|
||||
w16-none none 23 4 1 3 4.1 0 16 a'=3;
|
||||
w16-none none 34 2 1 1 3.1 0 25 a=1;
|
||||
w16-none none 31 12 1 11 3.6 13 25 a'=7;a=2;c''=1;b=1;
|
||||
w16-none none 37 4 1 3 29.2 11 2 a'=3;
|
||||
w16-none none 35 12 1 11 38.7 6 2 a'=9;a=2;
|
||||
w16-none none 40 1 1 0 3.0 11 2
|
||||
w16-none none 39 1 1 0 3.4 0 14
|
||||
w16-none none 42 1 1 0 14.7 7 2
|
||||
w16-none none 46 5 1 4 16.8 5 2 a'=4;
|
||||
w16-none none 36 1 1 0 3.5 10 23
|
||||
w16-none none 26 7 1 6 17.4 2 2 a'=4;c''=1;a=1;
|
||||
w16-none none 48 2 1 1 3.0 6 5 a'=1;
|
||||
w16-none none 38 4 1 3 3.5 3 18 c'''=1;b=1;a'=1;
|
||||
w16-none none 43 2 1 1 3.3 10 15 a'=1;
|
||||
w16-none none 44 5 1 4 15.9 8 2 a'=4;
|
||||
w16-none none 45 9 1 8 2.8 15 13 a'=7;a=1;
|
||||
w16-none none 47 1 1 0 3.4 7 7
|
||||
w16-none none 49 2 1 1 3.4 0 15 a=1;
|
||||
w16-none none 50 2 1 1 16.2 12 2 a'=1;
|
||||
w16-none none 52 1 1 0 16.8 8 2
|
||||
w16-none none 54 5 1 4 18.7 3 2 a'=3;a=1;
|
||||
w16-none none 41 5 1 4 16.4 9 2 c''=2;a'=2;
|
||||
w16-none none 56 4 1 3 3.7 8 6 a'=3;
|
||||
w16-none none 58 6 1 5 7.3 9 3 a'=5;
|
||||
w16-none none 60 2 1 1 2.8 13 16 a'=1;
|
||||
w16-none none 55 1 1 0 3.4 1 4
|
||||
w16-none none 51 2 1 1 3.0 5 14 a'=1;
|
||||
w16-none none 53 2 1 1 15.5 1 2 c''=1;
|
||||
w16-none none 59 4 1 3 2.9 10 27 a'=3;
|
||||
w16-none none 57 1 1 0 3.0 3 15
|
||||
w16-none none 64 4 1 3 3.3 1 19 a'=2;a=1;
|
||||
w16-none none 61 3 1 2 17.4 14 2 a'=2;
|
||||
w16-none none 67 4 1 3 15.8 15 2 a'=2;a=1;
|
||||
w16-none none 63 15 1 14 2.5 3 17 a'=13;c'''=1;
|
||||
w16-none none 65 4 1 3 16.7 4 2 a'=2;b=1;
|
||||
w16-none none 62 4 1 3 14.4 8 2 a'=2;c''=1;
|
||||
w16-none none 75 1 1 0 3.1 11 8
|
||||
w16-none none 76 4 1 3 3.4 6 22 a'=2;a=1;
|
||||
w16-none none 66 6 1 5 15.9 10 2 a'=4;a=1;
|
||||
w16-none none 68 2 1 1 16.5 1 2 a'=1;
|
||||
w16-none none 71 3 1 2 3.4 10 23 c=1;a'=1;
|
||||
w16-none none 69 4 1 3 43.3 0 15 a'=2;a=1;
|
||||
w16-none none 70 2 1 1 2.8 2 2 a'=1;
|
||||
w16-none none 72 7 1 6 2.8 2 11 a'=5;c'''=1;
|
||||
w16-none none 73 4 1 3 7.2 0 2 a'=3;
|
||||
w16-none none 74 1 1 0 5.2 12 2
|
||||
w16-none none 77 14 1 13 3.1 7 24 a'=9;b=2;c''=1;a=1;
|
||||
w16-none none 78 3 1 2 3.4 10 4 a'=2;
|
||||
w16-none none 80 5 1 4 3.3 11 18 a'=4;
|
||||
w16-none none 79 1 1 0 3.3 9 18
|
||||
w16-none none 82 3 1 2 3.1 10 10 a'=2;
|
||||
w16-none none 81 3 1 2 3.1 5 19 a'=2;
|
||||
w16-none none 87 1 1 0 32.4 9 10
|
||||
w16-none none 86 2 1 1 3.4 2 2 a'=1;
|
||||
w16-none none 83 1 1 0 3.5 6 24
|
||||
w16-none none 89 4 1 3 8.6 0 2 a'=2;b=1;
|
||||
w16-none none 90 2 1 1 3.6 6 8 a'=1;
|
||||
w16-none none 93 1 1 0 3.1 7 27
|
||||
w16-none none 94 1 1 0 14.4 5 2
|
||||
w16-none none 85 1 1 0 3.0 1 11
|
||||
w16-none none 88 6 1 5 3.0 13 19 a'=4;b=1;
|
||||
w16-none none 92 1 1 0 4.2 2 21
|
||||
w16-none none 84 1 1 0 56.3 2 10
|
||||
w16-none none 96 3 1 2 3.1 4 17 a'=2;
|
||||
w16-none none 95 2 1 1 3.3 4 2 a'=1;
|
||||
w16-none none 91 1 1 0 2.9 15 16
|
||||
w16-none none 97 1 1 0 2.8 1 10
|
||||
w16-none none 100 2 1 1 2.8 13 3 a'=1;
|
||||
w16-none none 98 1 1 0 55.7 1 5
|
||||
w16-none none 101 1 1 0 4.1 5 17
|
||||
w16-none none 99 1 1 0 3.3 8 27
|
||||
w16-none none 106 4 1 3 4.9 2 15 a'=2;a=1;
|
||||
w16-none none 102 1 1 0 2.8 11 12
|
||||
w16-none none 105 3 1 2 21.5 4 6 a'=2;
|
||||
w16-none none 103 1 1 0 15.8 8 2
|
||||
w16-none none 108 2 1 1 17.2 10 2 a'=1;
|
||||
w16-none none 107 3 1 2 10.7 0 27 a'=2;
|
||||
w16-none none 109 1 1 0 3.1 3 3
|
||||
w16-none none 110 2 1 1 2.8 11 5 a'=1;
|
||||
w16-none none 111 1 1 0 8.7 8 2
|
||||
w16-none none 104 1 1 0 2.9 1 26
|
||||
w16-none none 112 3 1 2 16.1 3 2 b=1;a'=1;
|
||||
w16-none none 115 1 1 0 15.9 10 2
|
||||
w16-none none 114 10 1 9 3.6 0 5 a'=8;a=1;
|
||||
w16-none none 113 1 1 0 17.3 12 2
|
||||
w16-none none 116 4 1 3 2.8 9 27 a'=3;
|
||||
w16-none none 117 4 1 3 7.2 1 2 a'=3;
|
||||
w16-none none 123 1 1 0 17.3 13 17
|
||||
w16-none none 125 1 1 0 2.5 10 2
|
||||
w16-none none 121 1 1 0 3.1 15 16
|
||||
w16-none none 124 1 1 0 3.0 1 19
|
||||
w16-none none 119 2 1 1 8.2 3 21 a'=1;
|
||||
w16-none none 120 5 1 4 15.4 5 2 a'=3;c''=1;
|
||||
w16-none none 118 3 1 2 15.2 10 2 c=1;a=1;
|
||||
w16-none none 126 2 1 1 2.9 2 21 a'=1;
|
||||
w16-none none 122 4 1 3 3.0 4 2 c'''=1;a'=1;a=1;
|
||||
w16-none none 129 1 1 0 3.9 13 25
|
||||
w16-none none 131 2 1 1 16.4 5 2 a'=1;
|
||||
w16-none none 133 1 1 0 28.1 0 10
|
||||
w16-none none 130 7 1 6 3.2 15 25 a'=5;b=1;
|
||||
w16-none none 135 2 1 1 2.8 13 18 a'=1;
|
||||
w16-none none 132 1 1 0 3.1 13 27
|
||||
w16-none none 134 2 1 1 7.9 15 2 a'=1;
|
||||
w16-none none 128 2 1 1 15.8 2 2 a'=1;
|
||||
w16-none none 138 9 1 8 3.3 1 17 a'=6;b=1;a=1;
|
||||
w16-none none 137 2 1 1 3.0 3 4 a'=1;
|
||||
w16-none none 127 7 1 6 3.4 7 18 a'=4;c'''=1;a=1;
|
||||
w16-none none 141 4 1 3 13.2 1 2 a'=2;c=1;
|
||||
w16-none none 139 5 1 4 2.4 0 21 a'=4;
|
||||
w16-none none 142 2 1 1 11.6 3 2 a=1;
|
||||
w16-none none 140 4 1 3 9.9 14 11 b=1;a'=1;a=1;
|
||||
w16-none none 144 3 1 2 3.0 7 7 a'=2;
|
||||
w16-none none 145 1 1 0 2.7 5 26
|
||||
w16-none none 147 2 1 1 3.0 2 5 a'=1;
|
||||
w16-none none 149 2 1 1 8.8 11 2 a'=1;
|
||||
w16-none none 148 2 1 1 3.2 14 2 a=1;
|
||||
w16-none none 153 5 1 4 3.5 2 11 a'=4;
|
||||
w16-none none 151 2 1 1 2.8 4 13 a'=1;
|
||||
w16-none none 146 1 1 0 8.2 8 2
|
||||
w16-none none 136 5 1 4 2.9 8 25 a'=2;c'''=1;b=1;
|
||||
w16-none none 152 2 1 1 13.2 13 25 a'=1;
|
||||
w16-none none 155 7 1 6 3.6 4 6 a'=3;a=3;
|
||||
w16-none none 143 6 1 5 2.8 2 16 a'=2;a=2;c=1;
|
||||
w16-none none 150 1 1 0 28.9 3 9
|
||||
w16-none none 154 3 1 2 3.1 9 27 a'=2;
|
||||
w16-none none 156 2 1 1 16.2 13 2 a=1;
|
||||
w16-none none 157 2 1 1 3.1 8 25 a'=1;
|
||||
w16-none none 158 8 1 7 3.0 9 20 a'=6;a=1;
|
||||
w16-none none 159 1 1 0 3.4 12 9
|
||||
w16-none none 160 2 1 1 2.8 2 25 a'=1;
|
||||
w16-none none 170 3 1 2 7.1 12 2 c=1;a'=1;
|
||||
w16-none none 162 3 1 2 9.0 11 2 a'=2;
|
||||
w16-none none 163 5 1 4 3.2 11 16 a'=4;
|
||||
w16-none none 164 3 1 2 3.0 8 2 a'=1;a=1;
|
||||
w16-none none 165 4 1 3 3.6 10 12 a'=3;
|
||||
w16-none none 168 1 1 0 5.7 1 2
|
||||
w16-none none 169 1 1 0 8.0 5 3
|
||||
w16-none none 166 8 1 7 2.8 0 19 a'=7;
|
||||
w16-none none 173 4 1 3 3.6 0 20 a'=3;
|
||||
w16-none none 161 6 1 5 18.7 4 2 a'=5;
|
||||
w16-none none 167 5 1 4 7.8 14 2 a'=3;a=1;
|
||||
w16-none none 172 5 1 4 4.1 8 8 a'=3;a=1;
|
||||
w16-none none 183 3 1 2 3.3 5 2 a'=2;
|
||||
w16-none none 177 1 1 0 3.3 15 10
|
||||
w16-none none 181 3 1 2 7.5 6 2 a'=2;
|
||||
w16-none none 171 1 1 0 3.2 10 14
|
||||
w16-none none 174 2 1 1 3.1 3 26 a=1;
|
||||
w16-none none 185 2 1 1 10.5 5 2 a'=1;
|
||||
w16-none none 184 6 1 5 2.9 13 18 a'=4;b=1;
|
||||
w16-none none 186 1 1 0 15.3 5 2
|
||||
w16-none none 178 4 1 3 3.5 14 2 a'=2;c''=1;
|
||||
w16-none none 187 6 1 5 3.2 7 20 a'=4;a=1;
|
||||
w16-none none 176 6 1 5 6.0 14 2 a'=4;a=1;
|
||||
w16-none none 175 1 1 0 2.8 0 8
|
||||
w16-none none 182 1 1 0 3.5 8 15
|
||||
w16-none none 193 1 1 0 5.0 15 2
|
||||
w16-none none 192 2 1 1 15.7 15 2 a'=1;
|
||||
w16-none none 180 1 1 0 3.0 15 14
|
||||
w16-none none 179 1 1 0 3.0 15 4
|
||||
w16-none none 188 4 1 3 55.6 1 18 a'=2;c''=1;
|
||||
w16-none none 198 7 1 6 15.0 6 2 a'=5;b=1;
|
||||
w16-none none 190 4 1 3 3.0 2 11 c'''=1;b=1;a'=1;
|
||||
w16-none none 200 10 1 9 3.9 3 19 a'=6;a=3;
|
||||
w16-none none 191 2 1 1 6.8 15 2 a'=1;
|
||||
w16-none none 189 3 1 2 2.9 14 19 a'=2;
|
||||
w16-none none 196 3 1 2 15.2 10 2 a'=2;
|
||||
w16-none none 202 3 1 2 3.9 0 22 a'=2;
|
||||
w16-none none 197 2 1 1 16.5 15 2 a'=1;
|
||||
w16-none none 203 4 1 3 4.4 10 16 a'=3;
|
||||
w16-none none 205 1 1 0 3.1 14 21
|
||||
w16-none none 194 17 1 16 3.5 11 10 a'=10;b=3;c=2;a=1;
|
||||
w16-none none 195 2 1 1 2.7 15 19 a'=1;
|
||||
w16-none none 204 2 1 1 3.1 11 14 a=1;
|
||||
w16-none none 199 1 1 0 3.5 14 14
|
||||
w16-none none 206 11 1 10 7.8 0 2 a'=9;a=1;
|
||||
w16-none none 201 4 1 3 3.2 14 26 a'=3;
|
||||
w16-none none 210 3 1 2 16.4 11 2 a'=2;
|
||||
w16-none none 207 2 1 1 2.9 1 8 a'=1;
|
||||
w16-none none 208 4 1 3 6.3 9 2 a'=2;a=1;
|
||||
w16-none none 211 3 1 2 3.0 3 23 a'=1;a=1;
|
||||
w16-none none 215 2 1 1 6.3 8 2 a'=1;
|
||||
w16-none none 209 12 1 11 3.5 12 25 a'=9;c=1;a=1;
|
||||
w16-none none 212 4 1 3 3.3 14 10 a'=3;
|
||||
w16-none none 213 3 1 2 9.0 2 20 a'=2;
|
||||
w16-none none 214 6 1 5 36.2 2 2 a'=5;
|
||||
w16-none none 216 2 1 1 3.1 0 4 a'=1;
|
||||
w16-none none 221 3 1 2 3.0 6 17 a'=2;
|
||||
w16-none none 222 1 1 0 10.7 6 20
|
||||
w16-none none 223 1 1 0 3.1 5 20
|
||||
w16-none none 225 2 1 1 3.2 13 3 a'=1;
|
||||
w16-none none 220 1 1 0 3.7 2 13
|
||||
w16-none none 227 5 1 4 19.7 3 2 a'=4;
|
||||
w16-none none 219 7 1 6 2.6 11 23 a'=6;
|
||||
w16-none none 217 1 1 0 2.9 0 12
|
||||
w16-none none 231 4 1 3 2.6 15 9 a'=3;
|
||||
w16-none none 218 3 1 2 2.9 11 23 c=1;a'=1;
|
||||
w16-none none 229 1 1 0 3.2 0 19
|
||||
w16-none none 224 3 1 2 3.1 5 8 a'=2;
|
||||
w16-none none 228 1 1 0 9.4 11 2
|
||||
w16-none none 226 1 1 0 30.3 14 16
|
||||
w16-none none 232 3 1 2 2.8 0 11 a'=2;
|
||||
w16-none none 237 4 1 3 3.7 13 7 a'=3;
|
||||
w16-none none 240 1 1 0 14.9 6 2
|
||||
w16-none none 238 7 1 6 7.2 12 3 a'=5;a=1;
|
||||
w16-none none 233 1 1 0 17.0 13 2
|
||||
w16-none none 243 1 1 0 6.3 0 2
|
||||
w16-none none 241 11 1 10 12.8 2 2 a'=10;
|
||||
w16-none none 244 3 1 2 3.6 13 10 a'=2;
|
||||
w16-none none 235 1 1 0 5.2 5 2
|
||||
w16-none none 242 6 1 5 2.7 2 4 a'=5;
|
||||
w16-none none 234 1 1 0 7.6 1 2
|
||||
w16-none none 230 9 1 8 15.8 3 2 a'=7;c'''=1;
|
||||
w16-none none 239 1 1 0 3.1 13 9
|
||||
w16-none none 245 6 1 5 3.6 1 10 a'=5;
|
||||
w16-none none 236 1 1 0 3.6 7 15
|
||||
w16-none none 247 2 1 1 15.4 6 2 a'=1;
|
||||
w16-none none 249 1 1 0 6.6 2 16
|
||||
w16-none none 250 2 1 1 2.8 5 5 a'=1;
|
||||
w16-none none 252 4 1 3 2.9 7 4 a'=2;a=1;
|
||||
w16-none none 246 4 1 3 16.3 11 2 a'=2;c''=1;
|
||||
w16-none none 255 1 1 0 3.0 6 7
|
||||
w16-none none 248 3 1 2 38.0 1 2 a'=2;
|
||||
w16-none none 251 3 1 2 3.6 9 10 b=1;a'=1;
|
||||
w16-none none 253 3 1 2 6.2 0 2 a'=2;
|
||||
w16-none none 254 5 1 4 7.4 2 2 a'=4;
|
||||
# end 2026-10-08T12:54:55Z rows=257
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
46
docs/analysis/class-v6/logs/connected/bench-4090.log
Normal file
46
docs/analysis/class-v6/logs/connected/bench-4090.log
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
RESULT start 2026-10-08T15:14:09Z host=dbd4e396219f arch=sm_89 card=NVIDIA GeForce RTX 4090, 580.159.04, 450.00 W
|
||||
RESULT pack packs/cs64-v6c0
|
||||
RESULT ptxas packs/cs64-v6c0 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads
|
||||
RESULT ptxas packs/cs64-v6c0 ptxas info : Used 87 registers, used 0 barriers, 376 bytes cmem[0], 8 bytes cmem[2]
|
||||
RESULT ptxas packs/cs64-v6c0 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads
|
||||
RESULT ptxas packs/cs64-v6c0 ptxas info : Used 40 registers, used 0 barriers, 372 bytes cmem[0]
|
||||
RESULT ptxas packs/cs64-v6c0 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads
|
||||
RESULT ptxas packs/cs64-v6c0 ptxas info : Used 43 registers, used 0 barriers, 364 bytes cmem[0]
|
||||
RESULT bench packs/cs64-v6c0 class-v5 bench pack "igneum-v6c/0" class control generator 2 (test harness: no pool, no network, no wallet)
|
||||
RESULT bench packs/cs64-v6c0 GPU: NVIDIA GeForce RTX 4090 (128 SMs, cc 8.9, 24081 MiB), CUDA driver 13.0 runtime 12.8
|
||||
RESULT bench packs/cs64-v6c0 hash kernel: 87 registers/thread, 20 resident blocks/SM at 1 warp/block
|
||||
RESULT bench packs/cs64-v6c0 device memory at start: 395 MiB used of 24081 MiB
|
||||
RESULT bench packs/cs64-v6c0 cache fill (GPU): 1.84 ms first, 1.82 ms second
|
||||
RESULT bench packs/cs64-v6c0 cache head and last 16 words against the pack: PASS
|
||||
RESULT bench packs/cs64-v6c0 dataset build (GPU): 30.57 ms first, 30.54 ms second (16777216 items, 1024 MiB)
|
||||
RESULT bench packs/cs64-v6c0 device memory after the build: 1675 MiB used
|
||||
RESULT bench packs/cs64-v6c0 dataset self-test: head PASS, last PASS, samples 64 of 64
|
||||
RESULT bench packs/cs64-v6c0 vector warps against the pack: 3 of 3 PASS
|
||||
RESULT bench packs/cs64-v6c0 fingerprint of 2^24 outputs at base 0: ad0cec2a42c84aff (lane 0 c2b2466c00d3f12b)
|
||||
RESULT bench packs/cs64-v6c0 hash rate: 62.436 MH/s over 250 batches of 2^24 (GPU event time 67177.7 ms)
|
||||
RESULT bench packs/cs64-v6c0 RESULT pack=igneum-v6c/0 class=control build_ms=30.54 rate_mhs=62.436 vectors=3/3
|
||||
RESULT smi packs/cs64-v6c0 samples 64 mean_power_w 268.8 mean_sm_mhz 2811 mean_mem_mhz 10251
|
||||
RESULT pack v5-genesis
|
||||
RESULT ptxas v5-genesis cc1plus: fatal error: ../../bench.cu: No such file or directory
|
||||
RESULT ptxas v5-genesis 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads
|
||||
RESULT ptxas v5-genesis ptxas info : Used 32 registers, used 0 barriers, 376 bytes cmem[0]
|
||||
RESULT ptxas v5-genesis 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads
|
||||
RESULT ptxas v5-genesis ptxas info : Used 40 registers, used 0 barriers, 384 bytes cmem[0]
|
||||
RESULT ptxas v5-genesis 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads
|
||||
RESULT ptxas v5-genesis ptxas info : Used 43 registers, used 0 barriers, 364 bytes cmem[0]
|
||||
RESULT bench v5-genesis class-v5 bench pack "igneum-genesis" class v5 generator 5 (test harness: no pool, no network, no wallet)
|
||||
RESULT bench v5-genesis GPU: NVIDIA GeForce RTX 4090 (128 SMs, cc 8.9, 24081 MiB), CUDA driver 13.0 runtime 12.8
|
||||
RESULT bench v5-genesis hash kernel: 32 registers/thread, 24 resident blocks/SM at 1 warp/block
|
||||
RESULT bench v5-genesis device memory at start: 395 MiB used of 24081 MiB
|
||||
RESULT bench v5-genesis cache fill (GPU): 1.83 ms first, 1.81 ms second
|
||||
RESULT bench v5-genesis cache head and last 16 words against the pack: PASS
|
||||
RESULT bench v5-genesis leaves: 93 x 64 B from leaves.bin (5952 bytes), FNV-1a 64 850ad094a937a5c5 against the pack's 850ad094a937a5c5: PASS; state root 1c583d352bb9c75a06dadb8d82d42836ebe8afa82d0b413be87bf921741f1526, chain block af89be5ddbadb6f6b4aee28ac8f249713be5d4c12621e3cea7f83ceada3c66b3 (159357)
|
||||
RESULT bench v5-genesis dataset build (GPU): 30.80 ms first, 30.75 ms second (16777216 items, 1024 MiB)
|
||||
RESULT bench v5-genesis device memory after the build: 1677 MiB used
|
||||
RESULT bench v5-genesis dataset self-test: head PASS, last PASS, samples 64 of 64
|
||||
RESULT bench v5-genesis vector warps against the pack: 3 of 3 PASS
|
||||
RESULT bench v5-genesis fingerprint of 2^24 outputs at base 0: ae74193ddad19e19 (lane 0 61b73fdc4b19aa6e)
|
||||
RESULT bench v5-genesis hash rate: 62.437 MH/s over 250 batches of 2^24 (GPU event time 67176.1 ms)
|
||||
RESULT bench v5-genesis RESULT pack=igneum-genesis class=v5 build_ms=30.75 rate_mhs=62.437 vectors=3/3
|
||||
RESULT smi v5-genesis samples 64 mean_power_w 271.3 mean_sm_mhz 2805 mean_mem_mhz 10251
|
||||
RESULT end 2026-10-08T15:16:34Z
|
||||
40
docs/analysis/class-v6/logs/connected/bench-5090c.log
Normal file
40
docs/analysis/class-v6/logs/connected/bench-5090c.log
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
RESULT start 2026-10-08T15:09:57Z host=f271665b754f arch=sm_120 card=NVIDIA GeForce RTX 5090, 580.159.03, 575.00 W
|
||||
RESULT pack packs/cs64-v6c0
|
||||
RESULT ptxas packs/cs64-v6c0 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads
|
||||
RESULT ptxas packs/cs64-v6c0 ptxas info : Used 80 registers, used 0 barriers
|
||||
RESULT ptxas packs/cs64-v6c0 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads
|
||||
RESULT ptxas packs/cs64-v6c0 ptxas info : Used 38 registers, used 0 barriers
|
||||
RESULT ptxas packs/cs64-v6c0 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads
|
||||
RESULT ptxas packs/cs64-v6c0 ptxas info : Used 41 registers, used 0 barriers
|
||||
RESULT bench packs/cs64-v6c0 class-v5 bench pack "igneum-v6c/0" class control generator 2 (test harness: no pool, no network, no wallet)
|
||||
RESULT bench packs/cs64-v6c0 GPU: NVIDIA GeForce RTX 5090 (170 SMs, cc 12.0, 32109 MiB), CUDA driver 13.0 runtime 12.8
|
||||
RESULT bench packs/cs64-v6c0 hash kernel: 80 registers/thread, 24 resident blocks/SM at 1 warp/block
|
||||
RESULT bench packs/cs64-v6c0 device memory at start: 6188 MiB used of 32109 MiB
|
||||
RESULT bench packs/cs64-v6c0 cache fill (GPU): 0.65 ms first, 0.65 ms second
|
||||
RESULT bench packs/cs64-v6c0 cache head and last 16 words against the pack: PASS
|
||||
RESULT bench packs/cs64-v6c0 dataset build (GPU): 29.75 ms first, 27.68 ms second (16777216 items, 1024 MiB)
|
||||
RESULT bench packs/cs64-v6c0 device memory after the build: 7468 MiB used
|
||||
RESULT bench packs/cs64-v6c0 dataset self-test: head PASS, last PASS, samples 64 of 64
|
||||
RESULT bench packs/cs64-v6c0 vector warps against the pack: 3 of 3 PASS
|
||||
RESULT bench packs/cs64-v6c0 fingerprint of 2^24 outputs at base 0: ad0cec2a42c84aff (lane 0 c2b2466c00d3f12b)
|
||||
RESULT bench packs/cs64-v6c0 hash rate: 64.929 MH/s over 250 batches of 2^24 (GPU event time 64598.6 ms)
|
||||
RESULT bench packs/cs64-v6c0 RESULT pack=igneum-v6c/0 class=control build_ms=27.68 rate_mhs=64.929 vectors=3/3
|
||||
RESULT smi packs/cs64-v6c0 samples 128 mean_power_w 574.8 mean_sm_mhz 2778 mean_mem_mhz 13801
|
||||
RESULT pack v5-genesis
|
||||
RESULT ptxas v5-genesis cc1plus: fatal error: ../../bench.cu: No such file or directory
|
||||
RESULT bench v5-genesis class-v5 bench pack "igneum-genesis" class v5 generator 5 (test harness: no pool, no network, no wallet)
|
||||
RESULT bench v5-genesis GPU: NVIDIA GeForce RTX 5090 (170 SMs, cc 12.0, 32109 MiB), CUDA driver 13.0 runtime 12.8
|
||||
RESULT bench v5-genesis hash kernel: 48 registers/thread, 24 resident blocks/SM at 1 warp/block
|
||||
RESULT bench v5-genesis device memory at start: 6188 MiB used of 32109 MiB
|
||||
RESULT bench v5-genesis cache fill (GPU): 0.65 ms first, 0.65 ms second
|
||||
RESULT bench v5-genesis cache head and last 16 words against the pack: PASS
|
||||
RESULT bench v5-genesis leaves: 93 x 64 B from leaves.bin (5952 bytes), FNV-1a 64 850ad094a937a5c5 against the pack's 850ad094a937a5c5: PASS; state root 1c583d352bb9c75a06dadb8d82d42836ebe8afa82d0b413be87bf921741f1526, chain block af89be5ddbadb6f6b4aee28ac8f249713be5d4c12621e3cea7f83ceada3c66b3 (159357)
|
||||
RESULT bench v5-genesis dataset build (GPU): 27.57 ms first, 27.53 ms second (16777216 items, 1024 MiB)
|
||||
RESULT bench v5-genesis device memory after the build: 7470 MiB used
|
||||
RESULT bench v5-genesis dataset self-test: head PASS, last PASS, samples 64 of 64
|
||||
RESULT bench v5-genesis vector warps against the pack: 3 of 3 PASS
|
||||
RESULT bench v5-genesis fingerprint of 2^24 outputs at base 0: ae74193ddad19e19 (lane 0 61b73fdc4b19aa6e)
|
||||
RESULT bench v5-genesis hash rate: 65.303 MH/s over 250 batches of 2^24 (GPU event time 64228.4 ms)
|
||||
RESULT bench v5-genesis RESULT pack=igneum-genesis class=v5 build_ms=27.53 rate_mhs=65.303 vectors=3/3
|
||||
RESULT smi v5-genesis samples 126 mean_power_w 574.8 mean_sm_mhz 2788 mean_mem_mhz 13801
|
||||
RESULT end 2026-10-08T15:12:13Z
|
||||
259
docs/analysis/class-v6/logs/connected/cs64-era-bt.tsv
Normal file
259
docs/analysis/class-v6/logs/connected/cs64-era-bt.tsv
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
# cs64-era-bt class cs64s27x16 eras [0,1,2,3,4,5,6,7] seeds 32 era-widths 4 weights base bittest 1 bin 54b905920b91becc start 2026-10-08T15:01:08Z host f271665b754f threads 40
|
||||
cand era seed candidates accepted attempt bit_z bit_site bit_bit reasons
|
||||
cs64-era-bt 0 16 9 1 8 3.2 7 8 c,=7;c=1;
|
||||
cs64-era-bt 0 21 2 1 1 3.2 14 21 c=1;
|
||||
cs64-era-bt 0 20 1 1 0 3.8 10 1
|
||||
cs64-era-bt 0 30 2 1 1 3.0 3 18 c,=1;
|
||||
cs64-era-bt 0 19 3 1 2 3.6 1 19 c,=2;
|
||||
cs64-era-bt 1 1 2 1 1 2.8 6 20 c=1;
|
||||
cs64-era-bt 0 12 5 1 4 3.5 10 25 c,=3;c=1;
|
||||
cs64-era-bt 0 18 4 1 3 3.2 11 13 c,=3;
|
||||
cs64-era-bt 0 17 7 1 6 4.2 4 19 c,=4;c=2;
|
||||
cs64-era-bt 0 6 1 1 0 5.1 3 19
|
||||
cs64-era-bt 0 28 5 1 4 5.9 12 19 c,=2;c=2;
|
||||
cs64-era-bt 0 0 9 1 8 4.2 9 19 c,=5;c=3;
|
||||
cs64-era-bt 0 5 2 1 1 3.9 5 1 c=1;
|
||||
cs64-era-bt 0 29 1 1 0 4.2 8 22
|
||||
cs64-era-bt 1 0 9 1 8 3.2 14 26 c,=5;c=3;
|
||||
cs64-era-bt 0 11 2 1 1 3.1 5 0 c,=1;
|
||||
cs64-era-bt 0 7 4 1 3 2.9 11 20 c,=3;
|
||||
cs64-era-bt 0 15 1 1 0 2.8 14 16
|
||||
cs64-era-bt 1 7 4 1 3 3.3 12 8 c,=3;
|
||||
cs64-era-bt 0 25 2 1 1 3.3 2 14 c=1;
|
||||
cs64-era-bt 0 13 5 1 4 3.2 13 11 c,=4;
|
||||
cs64-era-bt 0 4 1 1 0 3.2 12 20
|
||||
cs64-era-bt 0 31 3 1 2 3.0 1 6 c,=1;c=1;
|
||||
cs64-era-bt 0 22 1 1 0 3.0 1 19
|
||||
cs64-era-bt 0 26 3 1 2 3.4 3 13 c,=2;
|
||||
cs64-era-bt 0 14 6 1 5 3.5 1 19 c,=4;c=1;
|
||||
cs64-era-bt 0 10 4 1 3 3.0 0 18 c=2;c,=1;
|
||||
cs64-era-bt 1 2 1 1 0 2.9 15 20
|
||||
cs64-era-bt 1 4 3 1 2 3.2 7 22 c,=2;
|
||||
cs64-era-bt 0 27 2 1 1 3.9 15 19 c,=1;
|
||||
cs64-era-bt 0 9 1 1 0 3.1 1 17
|
||||
cs64-era-bt 1 3 1 1 0 3.3 15 7
|
||||
cs64-era-bt 0 3 1 1 0 2.7 12 10
|
||||
cs64-era-bt 1 5 2 1 1 3.6 4 16 c=1;
|
||||
cs64-era-bt 0 23 3 1 2 3.7 3 20 c,=2;
|
||||
cs64-era-bt 0 8 7 1 6 5.3 8 19 c,=5;c=1;
|
||||
cs64-era-bt 1 6 9 1 8 2.8 14 19 c,=6;c=2;
|
||||
cs64-era-bt 0 1 6 1 5 3.3 0 8 c,=3;c=2;
|
||||
cs64-era-bt 0 2 1 1 0 2.8 9 21
|
||||
cs64-era-bt 0 24 6 1 5 3.0 6 9 c,=5;
|
||||
cs64-era-bt 1 8 7 1 6 5.3 8 6 c,=5;c=1;
|
||||
cs64-era-bt 1 14 6 1 5 2.9 1 6 c,=4;c=1;
|
||||
cs64-era-bt 1 15 1 1 0 3.2 6 1
|
||||
cs64-era-bt 1 22 1 1 0 3.3 4 25
|
||||
cs64-era-bt 1 11 2 1 1 3.1 3 6 c,=1;
|
||||
cs64-era-bt 1 9 1 1 0 4.0 11 0
|
||||
cs64-era-bt 1 19 3 1 2 3.2 5 13 c,=2;
|
||||
cs64-era-bt 1 10 4 1 3 4.0 10 17 c=2;c,=1;
|
||||
cs64-era-bt 1 30 2 1 1 3.1 15 15 c,=1;
|
||||
cs64-era-bt 1 13 5 1 4 3.3 7 3 c,=4;
|
||||
cs64-era-bt 2 1 2 1 1 3.3 0 4 c=1;
|
||||
cs64-era-bt 1 25 2 1 1 3.3 14 25 c,=1;
|
||||
cs64-era-bt 1 12 5 1 4 3.1 2 9 c,=3;c=1;
|
||||
cs64-era-bt 2 15 1 1 0 3.0 13 7
|
||||
cs64-era-bt 1 28 3 1 2 3.6 4 18 c,=1;c=1;
|
||||
cs64-era-bt 1 16 9 1 8 3.3 13 18 c,=7;c=1;
|
||||
cs64-era-bt 1 17 7 1 6 2.9 7 25 c,=4;c=2;
|
||||
cs64-era-bt 1 20 1 1 0 3.0 15 9
|
||||
cs64-era-bt 1 21 2 1 1 2.6 11 19 c=1;
|
||||
cs64-era-bt 2 5 2 1 1 3.0 10 12 c=1;
|
||||
cs64-era-bt 2 13 2 1 1 3.1 2 26 c=1;
|
||||
cs64-era-bt 1 27 2 1 1 5.8 15 6 c,=1;
|
||||
cs64-era-bt 2 12 2 1 1 2.8 13 23 c=1;
|
||||
cs64-era-bt 1 18 4 1 3 3.4 0 5 c,=3;
|
||||
cs64-era-bt 1 29 1 1 0 3.9 10 6
|
||||
cs64-era-bt 1 31 4 1 3 3.5 1 23 c=2;c,=1;
|
||||
cs64-era-bt 2 11 2 1 1 3.0 12 15 c=1;
|
||||
cs64-era-bt 1 23 1 1 0 4.8 12 6
|
||||
cs64-era-bt 1 26 3 1 2 3.5 5 8 c,=2;
|
||||
cs64-era-bt 2 3 1 1 0 2.6 11 1
|
||||
cs64-era-bt 1 24 6 1 5 2.9 0 19 c,=5;
|
||||
cs64-era-bt 2 16 1 1 0 3.0 7 12
|
||||
cs64-era-bt 2 4 1 1 0 2.7 0 10
|
||||
cs64-era-bt 2 8 2 1 1 3.5 13 24 c=1;
|
||||
cs64-era-bt 2 0 2 1 1 3.1 5 25 c=1;
|
||||
cs64-era-bt 2 9 1 1 0 2.6 6 1
|
||||
cs64-era-bt 2 6 1 1 0 3.1 0 1
|
||||
cs64-era-bt 2 10 1 1 0 3.1 2 20
|
||||
cs64-era-bt 2 14 1 1 0 3.2 3 15
|
||||
cs64-era-bt 2 2 1 1 0 3.3 0 0
|
||||
cs64-era-bt 2 7 2 1 1 3.2 9 24 c=1;
|
||||
cs64-era-bt 2 17 1 1 0 2.7 10 2
|
||||
cs64-era-bt 2 18 1 1 0 3.2 3 11
|
||||
cs64-era-bt 2 26 2 1 1 4.6 1 26 c=1;
|
||||
cs64-era-bt 2 21 2 1 1 2.8 15 4 c=1;
|
||||
cs64-era-bt 2 29 1 1 0 3.5 11 7
|
||||
cs64-era-bt 2 23 1 1 0 3.7 15 20
|
||||
cs64-era-bt 2 24 1 1 0 3.5 3 11
|
||||
cs64-era-bt 2 19 3 1 2 3.1 8 20 c=2;
|
||||
cs64-era-bt 2 20 1 1 0 2.8 10 3
|
||||
cs64-era-bt 3 5 2 1 1 3.1 3 20 c=1;
|
||||
cs64-era-bt 3 4 1 1 0 3.7 9 10
|
||||
cs64-era-bt 2 31 1 1 0 3.3 15 18
|
||||
cs64-era-bt 2 22 1 1 0 2.9 5 1
|
||||
cs64-era-bt 3 11 2 1 1 3.2 6 13 c,=1;
|
||||
cs64-era-bt 3 1 2 1 1 3.3 9 13 c=1;
|
||||
cs64-era-bt 2 30 1 1 0 3.1 8 20
|
||||
cs64-era-bt 3 16 1 1 0 3.2 13 0
|
||||
cs64-era-bt 3 2 1 1 0 3.1 3 25
|
||||
cs64-era-bt 3 8 2 1 1 3.3 0 14 c=1;
|
||||
cs64-era-bt 3 0 2 1 1 3.2 0 7 c=1;
|
||||
cs64-era-bt 2 28 2 1 1 3.0 2 23 c=1;
|
||||
cs64-era-bt 2 27 1 1 0 3.3 9 18
|
||||
cs64-era-bt 3 14 1 1 0 2.8 2 18
|
||||
cs64-era-bt 2 25 2 1 1 3.1 2 7 c=1;
|
||||
cs64-era-bt 3 20 1 1 0 3.4 2 2
|
||||
cs64-era-bt 3 6 1 1 0 3.1 2 9
|
||||
cs64-era-bt 3 18 1 1 0 2.8 9 2
|
||||
cs64-era-bt 3 22 1 1 0 3.0 11 11
|
||||
cs64-era-bt 3 9 1 1 0 3.4 3 9
|
||||
cs64-era-bt 3 3 1 1 0 3.3 2 14
|
||||
cs64-era-bt 3 10 1 1 0 3.8 5 4
|
||||
cs64-era-bt 3 15 1 1 0 2.7 7 3
|
||||
cs64-era-bt 3 13 2 1 1 3.1 0 25 c=1;
|
||||
cs64-era-bt 3 12 2 1 1 3.4 14 17 c=1;
|
||||
cs64-era-bt 3 7 2 1 1 3.2 8 17 c=1;
|
||||
cs64-era-bt 3 19 3 1 2 3.3 13 17 c=2;
|
||||
cs64-era-bt 3 26 2 1 1 3.3 5 16 c=1;
|
||||
cs64-era-bt 3 21 2 1 1 3.0 0 15 c=1;
|
||||
cs64-era-bt 3 29 1 1 0 3.0 5 7
|
||||
cs64-era-bt 3 31 1 1 0 3.4 15 8
|
||||
cs64-era-bt 3 17 1 1 0 3.0 2 12
|
||||
cs64-era-bt 3 23 1 1 0 2.9 3 25
|
||||
cs64-era-bt 3 25 2 1 1 3.2 13 6 c=1;
|
||||
cs64-era-bt 4 1 2 1 1 3.3 12 22 c=1;
|
||||
cs64-era-bt 3 24 1 1 0 3.5 13 5
|
||||
cs64-era-bt 3 27 1 1 0 2.9 15 13
|
||||
cs64-era-bt 3 28 2 1 1 4.4 15 0 c=1;
|
||||
cs64-era-bt 4 12 5 1 4 3.6 7 23 c,=3;c=1;
|
||||
cs64-era-bt 3 30 1 1 0 2.8 8 20
|
||||
cs64-era-bt 4 10 4 1 3 3.1 11 16 c=2;c,=1;
|
||||
cs64-era-bt 4 6 1 1 0 5.0 3 10
|
||||
cs64-era-bt 4 2 1 1 0 2.9 10 22
|
||||
cs64-era-bt 4 0 9 1 8 3.7 9 10 c,=5;c=3;
|
||||
cs64-era-bt 4 4 3 1 2 2.8 4 24 c,=2;
|
||||
cs64-era-bt 4 5 2 1 1 3.3 15 27 c=1;
|
||||
cs64-era-bt 4 11 2 1 1 3.7 8 16 c,=1;
|
||||
cs64-era-bt 4 3 1 1 0 2.7 10 11
|
||||
cs64-era-bt 4 7 4 1 3 4.4 11 10 c,=3;
|
||||
cs64-era-bt 4 19 3 1 2 3.4 2 0 c,=2;
|
||||
cs64-era-bt 4 13 5 1 4 3.1 15 14 c,=3;c=1;
|
||||
cs64-era-bt 4 8 7 1 6 5.1 12 10 c,=5;c=1;
|
||||
cs64-era-bt 4 16 9 1 8 2.7 5 8 c,=7;c=1;
|
||||
cs64-era-bt 4 15 1 1 0 3.2 14 25
|
||||
cs64-era-bt 4 9 1 1 0 3.7 6 8
|
||||
cs64-era-bt 4 24 6 1 5 3.7 7 11 c,=5;
|
||||
cs64-era-bt 4 14 6 1 5 3.3 3 20 c,=4;c=1;
|
||||
cs64-era-bt 4 21 2 1 1 2.8 8 10 c=1;
|
||||
cs64-era-bt 4 31 4 1 3 3.4 3 20 c=2;c,=1;
|
||||
cs64-era-bt 4 29 1 1 0 3.7 10 9
|
||||
cs64-era-bt 4 17 7 1 6 3.6 8 12 c,=4;c=2;
|
||||
cs64-era-bt 4 28 3 1 2 3.3 8 6 c,=1;c=1;
|
||||
cs64-era-bt 4 25 2 1 1 3.3 0 22 c=1;
|
||||
cs64-era-bt 4 18 4 1 3 3.4 3 16 c,=3;
|
||||
cs64-era-bt 4 20 1 1 0 3.2 9 11
|
||||
cs64-era-bt 5 7 4 1 3 4.0 11 22 c,=3;
|
||||
cs64-era-bt 4 22 1 1 0 3.2 8 24
|
||||
cs64-era-bt 4 30 2 1 1 2.7 7 14 c,=1;
|
||||
cs64-era-bt 5 1 2 1 1 3.2 9 2 c=1;
|
||||
cs64-era-bt 4 26 3 1 2 3.6 12 24 c,=2;
|
||||
cs64-era-bt 4 23 1 1 0 5.8 12 10
|
||||
cs64-era-bt 4 27 3 1 2 4.0 15 21 c,=2;
|
||||
cs64-era-bt 5 3 1 1 0 3.1 5 8
|
||||
cs64-era-bt 5 2 1 1 0 3.0 2 6
|
||||
cs64-era-bt 5 5 2 1 1 3.5 14 23 c=1;
|
||||
cs64-era-bt 5 6 1 1 0 4.6 3 22
|
||||
cs64-era-bt 5 8 7 1 6 5.2 8 22 c,=5;c=1;
|
||||
cs64-era-bt 5 0 9 1 8 2.7 8 23 c,=5;c=3;
|
||||
cs64-era-bt 5 4 3 1 2 3.0 12 20 c,=2;
|
||||
cs64-era-bt 5 9 1 1 0 4.1 8 7
|
||||
cs64-era-bt 5 12 5 1 4 3.2 12 13 c,=3;c=1;
|
||||
cs64-era-bt 5 14 6 1 5 3.7 1 22 c,=4;c=1;
|
||||
cs64-era-bt 5 11 2 1 1 3.1 4 11 c,=1;
|
||||
cs64-era-bt 5 10 4 1 3 3.2 6 5 c=2;c,=1;
|
||||
cs64-era-bt 5 15 1 1 0 3.6 13 2
|
||||
cs64-era-bt 5 16 9 1 8 2.8 15 2 c,=7;c=1;
|
||||
cs64-era-bt 5 20 1 1 0 3.3 2 16
|
||||
cs64-era-bt 5 21 2 1 1 3.1 10 16 c=1;
|
||||
cs64-era-bt 5 17 7 1 6 3.0 5 19 c,=4;c=2;
|
||||
cs64-era-bt 5 19 3 1 2 2.7 14 24 c,=2;
|
||||
cs64-era-bt 5 13 5 1 4 3.2 10 13 c,=4;
|
||||
cs64-era-bt 5 29 1 1 0 3.6 12 26
|
||||
cs64-era-bt 5 22 1 1 0 3.4 0 16
|
||||
cs64-era-bt 5 25 2 1 1 3.0 5 1 c=1;
|
||||
cs64-era-bt 5 18 4 1 3 2.8 11 4 c,=3;
|
||||
cs64-era-bt 5 30 2 1 1 3.0 15 21 c,=1;
|
||||
cs64-era-bt 5 24 6 1 5 2.9 14 1 c,=5;
|
||||
cs64-era-bt 5 23 1 1 0 5.7 12 22
|
||||
cs64-era-bt 5 27 2 1 1 5.0 15 22 c,=1;
|
||||
cs64-era-bt 5 28 5 1 4 3.2 12 22 c,=2;c=2;
|
||||
cs64-era-bt 6 5 2 1 1 2.8 5 11 c=1;
|
||||
cs64-era-bt 5 26 3 1 2 3.2 5 21 c,=2;
|
||||
cs64-era-bt 6 4 3 1 2 3.1 12 10 c,=2;
|
||||
cs64-era-bt 6 2 1 1 0 3.1 0 9
|
||||
cs64-era-bt 6 0 9 1 8 3.0 1 26 c,=5;c=3;
|
||||
cs64-era-bt 6 7 4 1 3 3.6 9 16 c,=3;
|
||||
cs64-era-bt 6 6 9 1 8 3.1 10 5 c,=6;c=2;
|
||||
cs64-era-bt 6 1 2 1 1 3.4 7 27 c=1;
|
||||
cs64-era-bt 5 31 4 1 3 3.7 5 7 c=2;c,=1;
|
||||
cs64-era-bt 6 11 2 1 1 3.0 6 22 c,=1;
|
||||
cs64-era-bt 6 10 4 1 3 3.3 10 1 c=2;c,=1;
|
||||
cs64-era-bt 6 3 1 1 0 4.0 10 5
|
||||
cs64-era-bt 6 18 4 1 3 3.2 13 25 c,=3;
|
||||
cs64-era-bt 6 8 7 1 6 5.7 8 3 c,=5;c=1;
|
||||
cs64-era-bt 6 9 1 1 0 2.5 9 4
|
||||
cs64-era-bt 6 14 6 1 5 4.0 12 12 c,=4;c=1;
|
||||
cs64-era-bt 6 13 5 1 4 3.7 2 25 c,=4;
|
||||
cs64-era-bt 6 12 5 1 4 2.7 2 3 c,=3;c=1;
|
||||
cs64-era-bt 6 20 1 1 0 3.0 4 26
|
||||
cs64-era-bt 6 15 1 1 0 3.0 11 24
|
||||
cs64-era-bt 6 21 2 1 1 2.7 0 13 c=1;
|
||||
cs64-era-bt 6 23 3 1 2 2.6 8 13 c,=2;
|
||||
cs64-era-bt 6 27 2 1 1 3.0 6 17 c,=1;
|
||||
cs64-era-bt 6 24 6 1 5 3.0 9 22 c,=5;
|
||||
cs64-era-bt 6 31 4 1 3 3.6 1 16 c=2;c,=1;
|
||||
cs64-era-bt 6 16 9 1 8 2.9 8 0 c,=7;c=1;
|
||||
cs64-era-bt 6 19 3 1 2 3.0 15 2 c,=2;
|
||||
cs64-era-bt 6 17 7 1 6 2.8 11 0 c,=4;c=2;
|
||||
cs64-era-bt 6 29 1 1 0 3.2 14 17
|
||||
cs64-era-bt 6 26 3 1 2 2.7 7 21 c,=2;
|
||||
cs64-era-bt 7 6 1 1 0 4.3 3 7
|
||||
cs64-era-bt 6 22 1 1 0 3.9 9 14
|
||||
cs64-era-bt 7 11 2 1 1 3.8 14 24 c,=1;
|
||||
cs64-era-bt 6 30 2 1 1 2.7 7 7 c,=1;
|
||||
cs64-era-bt 6 28 5 1 4 4.3 10 20 c,=2;c=2;
|
||||
cs64-era-bt 7 1 2 1 1 2.9 8 11 c=1;
|
||||
cs64-era-bt 6 25 2 1 1 3.0 2 1 c=1;
|
||||
cs64-era-bt 7 0 2 1 1 3.3 1 12 c=1;
|
||||
cs64-era-bt 7 5 2 1 1 3.3 4 23 c=1;
|
||||
cs64-era-bt 7 4 1 1 0 3.1 13 14
|
||||
cs64-era-bt 7 12 5 1 4 3.3 4 5 c=3;c,=1;
|
||||
cs64-era-bt 7 13 4 1 3 2.7 8 11 c,=2;c=1;
|
||||
cs64-era-bt 7 9 1 1 0 3.4 12 24
|
||||
cs64-era-bt 7 2 1 1 0 2.6 5 8
|
||||
cs64-era-bt 7 3 1 1 0 3.8 14 22
|
||||
cs64-era-bt 7 10 4 1 3 4.0 13 25 c=2;c,=1;
|
||||
cs64-era-bt 7 8 2 1 1 3.0 12 17 c=1;
|
||||
cs64-era-bt 7 7 2 1 1 2.9 13 6 c,=1;
|
||||
cs64-era-bt 7 15 1 1 0 2.7 14 22
|
||||
cs64-era-bt 7 14 1 1 0 3.1 11 1
|
||||
cs64-era-bt 7 21 2 1 1 2.8 7 21 c=1;
|
||||
cs64-era-bt 7 17 1 1 0 3.3 5 11
|
||||
cs64-era-bt 7 25 2 1 1 2.9 14 21 c=1;
|
||||
cs64-era-bt 7 24 1 1 0 3.2 3 20
|
||||
cs64-era-bt 7 26 2 1 1 2.3 3 20 c,=1;
|
||||
cs64-era-bt 7 23 1 1 0 3.8 12 27
|
||||
cs64-era-bt 7 18 1 1 0 2.9 14 23
|
||||
cs64-era-bt 7 16 2 1 1 3.7 5 11 c,=1;
|
||||
cs64-era-bt 7 27 2 1 1 3.5 13 8 c,=1;
|
||||
cs64-era-bt 7 20 1 1 0 3.0 3 14
|
||||
cs64-era-bt 7 22 1 1 0 2.8 5 25
|
||||
cs64-era-bt 7 30 1 1 0 5.9 2 27
|
||||
cs64-era-bt 7 29 1 1 0 3.2 13 4
|
||||
cs64-era-bt 7 31 1 1 0 3.8 5 27
|
||||
cs64-era-bt 7 19 3 1 2 3.0 13 1 c=2;
|
||||
cs64-era-bt 7 28 2 1 1 3.0 14 11 c=1;
|
||||
# end 2026-10-08T15:07:36Z rows=257
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
259
docs/analysis/class-v6/logs/connected/cs64-era-nobt.tsv
Normal file
259
docs/analysis/class-v6/logs/connected/cs64-era-nobt.tsv
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
# cs64-era-nobt class cs64s27x16 eras [0,1,2,3,4,5,6,7] seeds 32 era-widths 4 weights base bittest off bin 54b905920b91becc start 2026-10-08T15:01:08Z host f271665b754f threads 24
|
||||
cand era seed candidates accepted attempt bit_z bit_site bit_bit reasons
|
||||
cs64-era-nobt 0 21 2 1 1 3.2 14 21 c=1;
|
||||
cs64-era-nobt 0 6 1 1 0 5.1 3 19
|
||||
cs64-era-nobt 0 2 1 1 0 2.8 9 21
|
||||
cs64-era-nobt 0 4 1 1 0 3.2 12 20
|
||||
cs64-era-nobt 0 16 1 1 0 24.0 13 19
|
||||
cs64-era-nobt 0 12 2 1 1 12.5 15 19 c=1;
|
||||
cs64-era-nobt 0 17 1 1 0 24.9 13 19
|
||||
cs64-era-nobt 0 8 2 1 1 32.0 3 19 c=1;
|
||||
cs64-era-nobt 0 3 1 1 0 2.7 12 10
|
||||
cs64-era-nobt 0 15 1 1 0 2.8 14 16
|
||||
cs64-era-nobt 0 1 2 1 1 7.0 1 20 c=1;
|
||||
cs64-era-nobt 0 19 3 1 2 3.6 1 19 c=2;
|
||||
cs64-era-nobt 0 11 2 1 1 3.1 5 0 c=1;
|
||||
cs64-era-nobt 0 7 2 1 1 46.9 13 19 c=1;
|
||||
cs64-era-nobt 0 22 1 1 0 3.0 1 19
|
||||
cs64-era-nobt 0 10 1 1 0 16.2 6 19
|
||||
cs64-era-nobt 0 18 1 1 0 14.6 2 19
|
||||
cs64-era-nobt 0 0 2 1 1 10.8 0 19 c=1;
|
||||
cs64-era-nobt 0 9 1 1 0 3.1 1 17
|
||||
cs64-era-nobt 0 14 1 1 0 65.0 8 19
|
||||
cs64-era-nobt 0 5 2 1 1 3.9 5 1 c=1;
|
||||
cs64-era-nobt 0 23 1 1 0 7.4 12 19
|
||||
cs64-era-nobt 0 13 2 1 1 31.2 12 19 c=1;
|
||||
cs64-era-nobt 0 20 1 1 0 3.8 10 1
|
||||
cs64-era-nobt 0 24 1 1 0 31.8 2 19
|
||||
cs64-era-nobt 0 30 1 1 0 46.5 14 19
|
||||
cs64-era-nobt 0 26 2 1 1 29.4 3 19 c=1;
|
||||
cs64-era-nobt 0 31 1 1 0 6.9 0 19
|
||||
cs64-era-nobt 0 29 1 1 0 4.2 8 22
|
||||
cs64-era-nobt 0 28 2 1 1 70.2 15 19 c=1;
|
||||
cs64-era-nobt 0 27 1 1 0 8.7 9 19
|
||||
cs64-era-nobt 1 3 1 1 0 3.3 15 7
|
||||
cs64-era-nobt 0 25 2 1 1 3.3 2 14 c=1;
|
||||
cs64-era-nobt 1 0 2 1 1 11.4 0 6 c=1;
|
||||
cs64-era-nobt 1 12 2 1 1 13.1 15 6 c=1;
|
||||
cs64-era-nobt 1 5 2 1 1 3.6 4 16 c=1;
|
||||
cs64-era-nobt 1 9 1 1 0 4.0 11 0
|
||||
cs64-era-nobt 1 4 1 1 0 10.9 6 7
|
||||
cs64-era-nobt 1 6 1 1 0 6.7 3 6
|
||||
cs64-era-nobt 1 1 2 1 1 2.8 6 20 c=1;
|
||||
cs64-era-nobt 1 7 2 1 1 46.8 13 6 c=1;
|
||||
cs64-era-nobt 1 10 1 1 0 14.5 6 6
|
||||
cs64-era-nobt 1 2 1 1 0 2.9 15 20
|
||||
cs64-era-nobt 1 11 2 1 1 3.1 3 6 c=1;
|
||||
cs64-era-nobt 1 13 2 1 1 32.6 11 6 c=1;
|
||||
cs64-era-nobt 1 15 1 1 0 3.2 6 1
|
||||
cs64-era-nobt 1 8 2 1 1 31.6 3 6 c=1;
|
||||
cs64-era-nobt 1 14 1 1 0 63.5 8 6
|
||||
cs64-era-nobt 1 16 1 1 0 24.5 13 6
|
||||
cs64-era-nobt 1 17 1 1 0 24.0 13 6
|
||||
cs64-era-nobt 1 19 3 1 2 3.2 5 13 c=2;
|
||||
cs64-era-nobt 1 21 2 1 1 2.6 11 19 c=1;
|
||||
cs64-era-nobt 1 18 1 1 0 15.9 2 6
|
||||
cs64-era-nobt 1 20 1 1 0 3.0 15 9
|
||||
cs64-era-nobt 1 30 1 1 0 47.8 14 6
|
||||
cs64-era-nobt 1 23 1 1 0 4.8 12 6
|
||||
cs64-era-nobt 1 27 1 1 0 10.1 9 6
|
||||
cs64-era-nobt 2 1 2 1 1 3.3 0 4 c=1;
|
||||
cs64-era-nobt 1 28 2 1 1 70.7 15 6 c=1;
|
||||
cs64-era-nobt 1 24 1 1 0 30.4 2 6
|
||||
cs64-era-nobt 1 26 2 1 1 29.1 3 6 c=1;
|
||||
cs64-era-nobt 1 22 1 1 0 3.3 4 25
|
||||
cs64-era-nobt 1 25 2 1 1 3.3 14 25 c=1;
|
||||
cs64-era-nobt 1 29 1 1 0 3.9 10 6
|
||||
cs64-era-nobt 2 0 2 1 1 3.1 5 25 c=1;
|
||||
cs64-era-nobt 2 3 1 1 0 2.6 11 1
|
||||
cs64-era-nobt 2 11 2 1 1 3.0 12 15 c=1;
|
||||
cs64-era-nobt 2 7 2 1 1 3.2 9 24 c=1;
|
||||
cs64-era-nobt 1 31 1 1 0 7.8 0 6
|
||||
cs64-era-nobt 2 2 1 1 0 3.3 0 0
|
||||
cs64-era-nobt 2 9 1 1 0 2.6 6 1
|
||||
cs64-era-nobt 2 10 1 1 0 3.1 2 20
|
||||
cs64-era-nobt 2 8 2 1 1 3.5 13 24 c=1;
|
||||
cs64-era-nobt 2 6 1 1 0 3.1 0 1
|
||||
cs64-era-nobt 2 4 1 1 0 2.7 0 10
|
||||
cs64-era-nobt 2 5 2 1 1 3.0 10 12 c=1;
|
||||
cs64-era-nobt 2 12 2 1 1 2.8 13 23 c=1;
|
||||
cs64-era-nobt 2 16 1 1 0 3.0 7 12
|
||||
cs64-era-nobt 2 13 2 1 1 3.1 2 26 c=1;
|
||||
cs64-era-nobt 2 14 1 1 0 3.2 3 15
|
||||
cs64-era-nobt 2 24 1 1 0 3.5 3 11
|
||||
cs64-era-nobt 2 15 1 1 0 3.0 13 7
|
||||
cs64-era-nobt 2 25 2 1 1 3.1 2 7 c=1;
|
||||
cs64-era-nobt 2 20 1 1 0 2.8 10 3
|
||||
cs64-era-nobt 2 18 1 1 0 3.2 3 11
|
||||
cs64-era-nobt 2 19 3 1 2 3.1 8 20 c=2;
|
||||
cs64-era-nobt 2 17 1 1 0 2.7 10 2
|
||||
cs64-era-nobt 2 22 1 1 0 2.9 5 1
|
||||
cs64-era-nobt 2 21 2 1 1 2.8 15 4 c=1;
|
||||
cs64-era-nobt 2 23 1 1 0 3.7 15 20
|
||||
cs64-era-nobt 2 26 2 1 1 4.6 1 26 c=1;
|
||||
cs64-era-nobt 2 29 1 1 0 3.5 11 7
|
||||
cs64-era-nobt 2 28 2 1 1 3.0 2 23 c=1;
|
||||
cs64-era-nobt 2 27 1 1 0 3.3 9 18
|
||||
cs64-era-nobt 2 30 1 1 0 3.1 8 20
|
||||
cs64-era-nobt 3 1 2 1 1 3.3 9 13 c=1;
|
||||
cs64-era-nobt 2 31 1 1 0 3.3 15 18
|
||||
cs64-era-nobt 3 0 2 1 1 3.2 0 7 c=1;
|
||||
cs64-era-nobt 3 4 1 1 0 3.7 9 10
|
||||
cs64-era-nobt 3 6 1 1 0 3.1 2 9
|
||||
cs64-era-nobt 3 5 2 1 1 3.1 3 20 c=1;
|
||||
cs64-era-nobt 3 3 1 1 0 3.3 2 14
|
||||
cs64-era-nobt 3 2 1 1 0 3.1 3 25
|
||||
cs64-era-nobt 3 7 2 1 1 3.2 8 17 c=1;
|
||||
cs64-era-nobt 3 8 2 1 1 3.3 0 14 c=1;
|
||||
cs64-era-nobt 3 17 1 1 0 3.0 2 12
|
||||
cs64-era-nobt 3 11 2 1 1 3.2 6 13 c=1;
|
||||
cs64-era-nobt 3 9 1 1 0 3.4 3 9
|
||||
cs64-era-nobt 3 15 1 1 0 2.7 7 3
|
||||
cs64-era-nobt 3 18 1 1 0 2.8 9 2
|
||||
cs64-era-nobt 3 10 1 1 0 3.8 5 4
|
||||
cs64-era-nobt 3 19 3 1 2 3.3 13 17 c=2;
|
||||
cs64-era-nobt 3 14 1 1 0 2.8 2 18
|
||||
cs64-era-nobt 3 21 2 1 1 3.0 0 15 c=1;
|
||||
cs64-era-nobt 3 12 2 1 1 3.4 14 17 c=1;
|
||||
cs64-era-nobt 3 13 2 1 1 3.1 0 25 c=1;
|
||||
cs64-era-nobt 3 16 1 1 0 3.2 13 0
|
||||
cs64-era-nobt 3 24 1 1 0 3.5 13 5
|
||||
cs64-era-nobt 3 20 1 1 0 3.4 2 2
|
||||
cs64-era-nobt 3 22 1 1 0 3.0 11 11
|
||||
cs64-era-nobt 3 26 2 1 1 3.3 5 16 c=1;
|
||||
cs64-era-nobt 3 29 1 1 0 3.0 5 7
|
||||
cs64-era-nobt 3 23 1 1 0 2.9 3 25
|
||||
cs64-era-nobt 3 31 1 1 0 3.4 15 8
|
||||
cs64-era-nobt 3 25 2 1 1 3.2 13 6 c=1;
|
||||
cs64-era-nobt 4 1 2 1 1 3.3 12 22 c=1;
|
||||
cs64-era-nobt 4 0 2 1 1 11.8 0 10 c=1;
|
||||
cs64-era-nobt 3 28 2 1 1 4.4 15 0 c=1;
|
||||
cs64-era-nobt 4 4 1 1 0 11.1 6 11
|
||||
cs64-era-nobt 3 30 1 1 0 2.8 8 20
|
||||
cs64-era-nobt 3 27 1 1 0 2.9 15 13
|
||||
cs64-era-nobt 4 3 1 1 0 2.7 10 11
|
||||
cs64-era-nobt 4 2 1 1 0 2.9 10 22
|
||||
cs64-era-nobt 4 6 1 1 0 5.0 3 10
|
||||
cs64-era-nobt 4 5 2 1 1 3.3 15 27 c=1;
|
||||
cs64-era-nobt 4 7 2 1 1 47.7 13 10 c=1;
|
||||
cs64-era-nobt 4 10 1 1 0 15.7 6 10
|
||||
cs64-era-nobt 4 9 1 1 0 3.7 6 8
|
||||
cs64-era-nobt 4 11 2 1 1 3.7 8 16 c=1;
|
||||
cs64-era-nobt 4 12 2 1 1 15.9 15 10 c=1;
|
||||
cs64-era-nobt 4 13 2 1 1 32.3 11 10 c=1;
|
||||
cs64-era-nobt 4 8 2 1 1 33.1 3 10 c=1;
|
||||
cs64-era-nobt 4 14 1 1 0 63.9 8 10
|
||||
cs64-era-nobt 4 15 1 1 0 3.2 14 25
|
||||
cs64-era-nobt 4 17 1 1 0 22.9 13 10
|
||||
cs64-era-nobt 4 16 1 1 0 22.5 13 10
|
||||
cs64-era-nobt 4 18 1 1 0 16.0 2 10
|
||||
cs64-era-nobt 4 20 1 1 0 3.2 9 11
|
||||
cs64-era-nobt 4 22 1 1 0 3.2 8 24
|
||||
cs64-era-nobt 4 28 2 1 1 70.4 15 10 c=1;
|
||||
cs64-era-nobt 4 19 3 1 2 3.4 2 0 c=2;
|
||||
cs64-era-nobt 4 29 1 1 0 3.7 10 9
|
||||
cs64-era-nobt 4 31 1 1 0 7.3 0 10
|
||||
cs64-era-nobt 4 25 2 1 1 3.3 0 22 c=1;
|
||||
cs64-era-nobt 4 21 2 1 1 2.8 8 10 c=1;
|
||||
cs64-era-nobt 4 26 2 1 1 30.5 3 10 c=1;
|
||||
cs64-era-nobt 4 24 1 1 0 30.3 2 10
|
||||
cs64-era-nobt 4 27 1 1 0 9.1 9 10
|
||||
cs64-era-nobt 4 30 1 1 0 48.5 14 10
|
||||
cs64-era-nobt 4 23 1 1 0 5.8 12 10
|
||||
cs64-era-nobt 5 0 2 1 1 10.7 0 22 c=1;
|
||||
cs64-era-nobt 5 1 2 1 1 3.2 9 2 c=1;
|
||||
cs64-era-nobt 5 4 1 1 0 9.9 6 23
|
||||
cs64-era-nobt 5 3 1 1 0 3.1 5 8
|
||||
cs64-era-nobt 5 5 2 1 1 3.5 14 23 c=1;
|
||||
cs64-era-nobt 5 2 1 1 0 3.0 2 6
|
||||
cs64-era-nobt 5 7 2 1 1 47.0 13 22 c=1;
|
||||
cs64-era-nobt 5 6 1 1 0 4.6 3 22
|
||||
cs64-era-nobt 5 9 1 1 0 4.1 8 7
|
||||
cs64-era-nobt 5 10 1 1 0 14.9 12 22
|
||||
cs64-era-nobt 5 8 2 1 1 31.2 3 22 c=1;
|
||||
cs64-era-nobt 5 11 2 1 1 3.1 4 11 c=1;
|
||||
cs64-era-nobt 5 14 1 1 0 63.4 8 22
|
||||
cs64-era-nobt 5 12 2 1 1 13.0 15 22 c=1;
|
||||
cs64-era-nobt 5 13 2 1 1 30.8 11 22 c=1;
|
||||
cs64-era-nobt 5 15 1 1 0 3.6 13 2
|
||||
cs64-era-nobt 5 16 1 1 0 24.0 13 22
|
||||
cs64-era-nobt 5 18 1 1 0 13.4 2 22
|
||||
cs64-era-nobt 5 17 1 1 0 23.8 13 22
|
||||
cs64-era-nobt 5 19 3 1 2 2.7 14 24 c=2;
|
||||
cs64-era-nobt 5 21 2 1 1 3.1 10 16 c=1;
|
||||
cs64-era-nobt 5 20 1 1 0 3.3 2 16
|
||||
cs64-era-nobt 5 26 2 1 1 29.3 3 22 c=1;
|
||||
cs64-era-nobt 5 24 1 1 0 30.5 2 22
|
||||
cs64-era-nobt 5 23 1 1 0 5.7 12 22
|
||||
cs64-era-nobt 5 22 1 1 0 3.4 0 16
|
||||
cs64-era-nobt 5 25 2 1 1 3.0 5 1 c=1;
|
||||
cs64-era-nobt 5 27 1 1 0 10.1 9 22
|
||||
cs64-era-nobt 5 28 2 1 1 71.5 15 22 c=1;
|
||||
cs64-era-nobt 5 29 1 1 0 3.6 12 26
|
||||
cs64-era-nobt 5 31 1 1 0 6.4 0 22
|
||||
cs64-era-nobt 5 30 1 1 0 46.0 14 22
|
||||
cs64-era-nobt 6 1 2 1 1 3.4 7 27 c=1;
|
||||
cs64-era-nobt 6 0 2 1 1 11.7 0 3 c=1;
|
||||
cs64-era-nobt 6 2 1 1 0 3.1 0 9
|
||||
cs64-era-nobt 6 3 1 1 0 4.0 10 5
|
||||
cs64-era-nobt 6 4 1 1 0 8.4 6 4
|
||||
cs64-era-nobt 6 6 1 1 0 6.1 3 3
|
||||
cs64-era-nobt 6 5 2 1 1 2.8 5 11 c=1;
|
||||
cs64-era-nobt 6 7 2 1 1 47.9 13 3 c=1;
|
||||
cs64-era-nobt 6 10 1 1 0 16.1 6 3
|
||||
cs64-era-nobt 6 8 2 1 1 32.0 3 3 c=1;
|
||||
cs64-era-nobt 6 11 2 1 1 3.0 6 22 c=1;
|
||||
cs64-era-nobt 6 9 1 1 0 2.5 9 4
|
||||
cs64-era-nobt 6 12 2 1 1 14.8 15 3 c=1;
|
||||
cs64-era-nobt 6 13 2 1 1 31.9 11 3 c=1;
|
||||
cs64-era-nobt 6 14 1 1 0 63.6 8 3
|
||||
cs64-era-nobt 6 15 1 1 0 3.0 11 24
|
||||
cs64-era-nobt 6 16 1 1 0 24.1 13 3
|
||||
cs64-era-nobt 6 17 1 1 0 22.9 13 3
|
||||
cs64-era-nobt 6 18 1 1 0 18.8 2 3
|
||||
cs64-era-nobt 6 19 3 1 2 3.0 15 2 c=2;
|
||||
cs64-era-nobt 6 21 2 1 1 2.7 0 13 c=1;
|
||||
cs64-era-nobt 6 20 1 1 0 3.0 4 26
|
||||
cs64-era-nobt 6 23 1 1 0 8.2 12 3
|
||||
cs64-era-nobt 6 22 1 1 0 3.9 9 14
|
||||
cs64-era-nobt 6 24 1 1 0 30.1 2 3
|
||||
cs64-era-nobt 6 26 2 1 1 29.5 3 3 c=1;
|
||||
cs64-era-nobt 6 28 2 1 1 71.3 15 3 c=1;
|
||||
cs64-era-nobt 6 25 2 1 1 3.0 2 1 c=1;
|
||||
cs64-era-nobt 6 27 1 1 0 8.6 9 3
|
||||
cs64-era-nobt 6 29 1 1 0 3.2 14 17
|
||||
cs64-era-nobt 6 30 1 1 0 47.4 14 3
|
||||
cs64-era-nobt 6 31 1 1 0 6.2 0 3
|
||||
cs64-era-nobt 7 1 2 1 1 2.9 8 11 c=1;
|
||||
cs64-era-nobt 7 0 2 1 1 3.3 1 12 c=1;
|
||||
cs64-era-nobt 7 4 1 1 0 3.1 13 14
|
||||
cs64-era-nobt 7 6 1 1 0 4.3 3 7
|
||||
cs64-era-nobt 7 3 1 1 0 3.8 14 22
|
||||
cs64-era-nobt 7 2 1 1 0 2.6 5 8
|
||||
cs64-era-nobt 7 5 2 1 1 3.3 4 23 c=1;
|
||||
cs64-era-nobt 7 7 2 1 1 2.9 13 6 c=1;
|
||||
cs64-era-nobt 7 8 2 1 1 3.0 12 17 c=1;
|
||||
cs64-era-nobt 7 10 1 1 0 15.2 6 27
|
||||
cs64-era-nobt 7 9 1 1 0 3.4 12 24
|
||||
cs64-era-nobt 7 11 2 1 1 3.8 14 24 c=1;
|
||||
cs64-era-nobt 7 12 2 1 1 12.5 15 27 c=1;
|
||||
cs64-era-nobt 7 14 1 1 0 3.1 11 1
|
||||
cs64-era-nobt 7 13 2 1 1 27.9 14 27 c=1;
|
||||
cs64-era-nobt 7 15 1 1 0 2.7 14 22
|
||||
cs64-era-nobt 7 16 1 1 0 24.0 13 27
|
||||
cs64-era-nobt 7 17 1 1 0 3.3 5 11
|
||||
cs64-era-nobt 7 18 1 1 0 2.9 14 23
|
||||
cs64-era-nobt 7 19 3 1 2 3.0 13 1 c=2;
|
||||
cs64-era-nobt 7 21 2 1 1 2.8 7 21 c=1;
|
||||
cs64-era-nobt 7 20 1 1 0 3.0 3 14
|
||||
cs64-era-nobt 7 23 1 1 0 3.8 12 27
|
||||
cs64-era-nobt 7 22 1 1 0 2.8 5 25
|
||||
cs64-era-nobt 7 26 2 1 1 2.3 3 20 c=1;
|
||||
cs64-era-nobt 7 24 1 1 0 3.2 3 20
|
||||
cs64-era-nobt 7 25 2 1 1 2.9 14 21 c=1;
|
||||
cs64-era-nobt 7 28 2 1 1 3.0 14 11 c=1;
|
||||
cs64-era-nobt 7 29 1 1 0 3.2 13 4
|
||||
cs64-era-nobt 7 27 1 1 0 10.0 9 27
|
||||
cs64-era-nobt 7 30 1 1 0 5.9 2 27
|
||||
cs64-era-nobt 7 31 1 1 0 3.8 5 27
|
||||
# end 2026-10-08T15:07:58Z rows=257
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
259
docs/analysis/class-v6/logs/connected/cs64-none.tsv
Normal file
259
docs/analysis/class-v6/logs/connected/cs64-none.tsv
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
# cs64-none class cs64s27x16 eras [none] seeds 256 era-widths 4 weights base bittest off bin 54b905920b91becc start 2026-10-08T15:01:08Z host f271665b754f threads 40
|
||||
cand era seed candidates accepted attempt bit_z bit_site bit_bit reasons
|
||||
cs64-none none 12 1 1 0 3.8 4 20
|
||||
cs64-none none 8 1 1 0 55.1 1 0
|
||||
cs64-none none 36 1 1 0 3.2 15 24
|
||||
cs64-none none 2 1 1 0 2.7 12 3
|
||||
cs64-none none 14 1 1 0 2.7 0 16
|
||||
cs64-none none 10 1 1 0 3.1 1 22
|
||||
cs64-none none 37 1 1 0 42.9 6 0
|
||||
cs64-none none 38 2 1 1 20.3 3 0 c=1;
|
||||
cs64-none none 29 2 1 1 3.4 7 24 c=1;
|
||||
cs64-none none 7 1 1 0 35.3 11 0
|
||||
cs64-none none 31 1 1 0 17.2 3 0
|
||||
cs64-none none 18 1 1 0 49.8 13 0
|
||||
cs64-none none 11 1 1 0 3.3 4 26
|
||||
cs64-none none 0 1 1 0 3.6 9 17
|
||||
cs64-none none 35 1 1 0 42.4 13 0
|
||||
cs64-none none 23 3 1 2 29.2 11 0 c=2;
|
||||
cs64-none none 4 1 1 0 38.8 11 0
|
||||
cs64-none none 30 1 1 0 3.1 13 17
|
||||
cs64-none none 20 1 1 0 3.0 3 11
|
||||
cs64-none none 17 1 1 0 3.8 7 1
|
||||
cs64-none none 32 1 1 0 5.5 8 0
|
||||
cs64-none none 28 2 1 1 2.8 6 11 c=1;
|
||||
cs64-none none 39 1 1 0 3.7 9 0
|
||||
cs64-none none 21 1 1 0 31.7 9 0
|
||||
cs64-none none 9 1 1 0 59.0 9 0
|
||||
cs64-none none 33 2 1 1 30.3 11 0 c=1;
|
||||
cs64-none none 24 1 1 0 21.1 10 0
|
||||
cs64-none none 5 2 1 1 23.5 6 0 c=1;
|
||||
cs64-none none 25 1 1 0 2.8 9 27
|
||||
cs64-none none 6 1 1 0 17.3 0 0
|
||||
cs64-none none 1 1 1 0 16.6 3 0
|
||||
cs64-none none 16 1 1 0 22.5 2 0
|
||||
cs64-none none 3 1 1 0 26.8 7 0
|
||||
cs64-none none 26 1 1 0 24.3 14 0
|
||||
cs64-none none 27 1 1 0 3.4 14 7
|
||||
cs64-none none 34 2 1 1 3.1 2 26 c=1;
|
||||
cs64-none none 22 1 1 0 22.3 3 0
|
||||
cs64-none none 19 1 1 0 16.5 14 1
|
||||
cs64-none none 13 1 1 0 4.7 12 1
|
||||
cs64-none none 15 3 1 2 4.0 0 1 c=2;
|
||||
cs64-none none 46 1 1 0 2.6 3 4
|
||||
cs64-none none 42 2 1 1 4.0 10 21 c=1;
|
||||
cs64-none none 50 1 1 0 64.1 10 1
|
||||
cs64-none none 51 2 1 1 30.9 11 0 c=1;
|
||||
cs64-none none 40 1 1 0 65.4 6 0
|
||||
cs64-none none 63 1 1 0 6.3 10 0
|
||||
cs64-none none 66 1 1 0 2.6 14 24
|
||||
cs64-none none 58 3 1 2 17.7 4 0 c=2;
|
||||
cs64-none none 43 1 1 0 64.4 9 0
|
||||
cs64-none none 59 1 1 0 127.7 4 0
|
||||
cs64-none none 45 1 1 0 13.3 13 0
|
||||
cs64-none none 44 1 1 0 3.0 14 3
|
||||
cs64-none none 41 1 1 0 12.4 8 0
|
||||
cs64-none none 53 1 1 0 3.2 8 7
|
||||
cs64-none none 47 1 1 0 33.8 7 0
|
||||
cs64-none none 56 1 1 0 3.0 12 22
|
||||
cs64-none none 52 1 1 0 47.9 12 0
|
||||
cs64-none none 48 2 1 1 50.0 4 1 c=1;
|
||||
cs64-none none 49 1 1 0 3.3 7 2
|
||||
cs64-none none 60 2 1 1 9.4 6 0 c=1;
|
||||
cs64-none none 55 1 1 0 13.0 13 0
|
||||
cs64-none none 75 3 1 2 3.2 10 3 c=2;
|
||||
cs64-none none 54 3 1 2 13.5 8 0 c=2;
|
||||
cs64-none none 57 1 1 0 32.5 6 0
|
||||
cs64-none none 76 1 1 0 9.3 12 0
|
||||
cs64-none none 61 2 1 1 15.1 4 0 c=1;
|
||||
cs64-none none 64 1 1 0 3.0 2 8
|
||||
cs64-none none 70 2 1 1 20.6 2 0 c=1;
|
||||
cs64-none none 71 1 1 0 14.4 2 0
|
||||
cs64-none none 67 4 1 3 9.5 6 0 c=3;
|
||||
cs64-none none 62 1 1 0 3.0 7 20
|
||||
cs64-none none 65 3 1 2 12.0 4 0 c=2;
|
||||
cs64-none none 79 1 1 0 84.3 9 0
|
||||
cs64-none none 69 2 1 1 2.9 3 22 c=1;
|
||||
cs64-none none 77 1 1 0 19.8 15 0
|
||||
cs64-none none 68 2 1 1 31.4 3 0 c=1;
|
||||
cs64-none none 85 2 1 1 3.1 9 15 c=1;
|
||||
cs64-none none 73 1 1 0 3.5 1 2
|
||||
cs64-none none 78 3 1 2 3.1 7 13 c=2;
|
||||
cs64-none none 72 1 1 0 3.8 7 1
|
||||
cs64-none none 74 1 1 0 5.5 0 0
|
||||
cs64-none none 88 1 1 0 64.0 13 0
|
||||
cs64-none none 83 2 1 1 4.0 8 15 c=1;
|
||||
cs64-none none 101 1 1 0 29.9 11 0
|
||||
cs64-none none 80 3 1 2 4.1 1 0 c=2;
|
||||
cs64-none none 81 2 1 1 11.0 7 0 c=1;
|
||||
cs64-none none 91 1 1 0 3.4 8 22
|
||||
cs64-none none 84 1 1 0 26.1 1 0
|
||||
cs64-none none 86 1 1 0 2.9 9 24
|
||||
cs64-none none 82 1 1 0 2.9 9 19
|
||||
cs64-none none 87 1 1 0 3.3 10 21
|
||||
cs64-none none 93 1 1 0 3.1 7 26
|
||||
cs64-none none 103 1 1 0 4.4 0 0
|
||||
cs64-none none 99 3 1 2 50.9 13 0 c=2;
|
||||
cs64-none none 96 1 1 0 3.0 7 26
|
||||
cs64-none none 89 1 1 0 93.8 8 0
|
||||
cs64-none none 102 1 1 0 14.1 3 0
|
||||
cs64-none none 94 1 1 0 3.6 1 19
|
||||
cs64-none none 92 1 1 0 3.9 0 13
|
||||
cs64-none none 114 1 1 0 3.9 7 17
|
||||
cs64-none none 95 6 1 5 32.2 11 0 c=5;
|
||||
cs64-none none 97 1 1 0 6.7 6 0
|
||||
cs64-none none 90 2 1 1 2.9 8 27 c=1;
|
||||
cs64-none none 104 1 1 0 12.8 2 0
|
||||
cs64-none none 108 3 1 2 21.0 12 0 c=2;
|
||||
cs64-none none 111 1 1 0 18.4 4 0
|
||||
cs64-none none 106 1 1 0 4.8 6 0
|
||||
cs64-none none 100 1 1 0 13.0 1 0
|
||||
cs64-none none 98 1 1 0 3.2 14 25
|
||||
cs64-none none 105 1 1 0 5.4 1 0
|
||||
cs64-none none 113 1 1 0 3.7 0 26
|
||||
cs64-none none 121 2 1 1 3.4 10 6 c=1;
|
||||
cs64-none none 107 2 1 1 3.4 1 8 c=1;
|
||||
cs64-none none 118 1 1 0 27.1 15 0
|
||||
cs64-none none 110 1 1 0 2.8 15 4
|
||||
cs64-none none 115 1 1 0 67.1 0 0
|
||||
cs64-none none 122 1 1 0 128.0 4 0
|
||||
cs64-none none 117 2 1 1 25.7 6 0 c=1;
|
||||
cs64-none none 109 1 1 0 46.1 3 0
|
||||
cs64-none none 112 1 1 0 60.5 11 0
|
||||
cs64-none none 119 1 1 0 3.3 3 3
|
||||
cs64-none none 116 1 1 0 6.9 13 0
|
||||
cs64-none none 124 1 1 0 8.1 15 0
|
||||
cs64-none none 126 1 1 0 2.6 10 18
|
||||
cs64-none none 125 1 1 0 35.3 3 0
|
||||
cs64-none none 140 1 1 0 26.0 1 10
|
||||
cs64-none none 123 1 1 0 3.2 8 6
|
||||
cs64-none none 130 1 1 0 53.1 11 0
|
||||
cs64-none none 120 1 1 0 2.9 5 6
|
||||
cs64-none none 145 3 1 2 51.8 4 0 c=2;
|
||||
cs64-none none 128 1 1 0 3.3 4 1
|
||||
cs64-none none 127 4 1 3 41.6 4 0 c=3;
|
||||
cs64-none none 143 1 1 0 23.8 8 0
|
||||
cs64-none none 129 1 1 0 14.0 11 0
|
||||
cs64-none none 139 2 1 1 8.2 13 0 c=1;
|
||||
cs64-none none 144 1 1 0 3.8 13 13
|
||||
cs64-none none 134 1 1 0 54.0 5 0
|
||||
cs64-none none 132 1 1 0 3.1 12 16
|
||||
cs64-none none 131 1 1 0 20.3 0 0
|
||||
cs64-none none 138 1 1 0 3.1 3 25
|
||||
cs64-none none 133 2 1 1 30.1 4 0 c=1;
|
||||
cs64-none none 152 1 1 0 127.9 13 0
|
||||
cs64-none none 141 1 1 0 4.3 11 0
|
||||
cs64-none none 137 2 1 1 21.0 1 0 c=1;
|
||||
cs64-none none 146 1 1 0 3.5 4 22
|
||||
cs64-none none 136 1 1 0 3.0 7 16
|
||||
cs64-none none 150 1 1 0 6.4 1 0
|
||||
cs64-none none 153 1 1 0 15.0 8 0
|
||||
cs64-none none 135 6 1 5 10.2 11 0 c=5;
|
||||
cs64-none none 142 2 1 1 11.2 7 0 c=1;
|
||||
cs64-none none 149 1 1 0 24.9 11 0
|
||||
cs64-none none 148 2 1 1 43.3 9 0 c=1;
|
||||
cs64-none none 163 1 1 0 17.9 15 0
|
||||
cs64-none none 147 1 1 0 30.1 4 0
|
||||
cs64-none none 155 2 1 1 4.8 7 0 c=1;
|
||||
cs64-none none 151 4 1 3 55.9 0 0 c=3;
|
||||
cs64-none none 166 1 1 0 3.2 8 0
|
||||
cs64-none none 157 1 1 0 3.6 1 17
|
||||
cs64-none none 160 1 1 0 4.1 6 26
|
||||
cs64-none none 154 1 1 0 16.5 13 0
|
||||
cs64-none none 156 1 1 0 83.9 2 0
|
||||
cs64-none none 165 1 1 0 9.2 4 0
|
||||
cs64-none none 162 1 1 0 19.9 10 0
|
||||
cs64-none none 158 1 1 0 3.1 8 3
|
||||
cs64-none none 169 2 1 1 35.4 8 0 c=1;
|
||||
cs64-none none 159 1 1 0 5.0 7 0
|
||||
cs64-none none 161 1 1 0 3.2 13 7
|
||||
cs64-none none 164 1 1 0 59.8 8 0
|
||||
cs64-none none 180 1 1 0 4.0 11 2
|
||||
cs64-none none 177 1 1 0 21.3 0 0
|
||||
cs64-none none 175 1 1 0 3.1 12 21
|
||||
cs64-none none 168 1 1 0 3.5 0 8
|
||||
cs64-none none 171 1 1 0 18.7 2 0
|
||||
cs64-none none 172 1 1 0 18.7 3 0
|
||||
cs64-none none 170 1 1 0 22.3 0 0
|
||||
cs64-none none 176 3 1 2 3.2 1 2 c=2;
|
||||
cs64-none none 173 2 1 1 49.9 4 0 c=1;
|
||||
cs64-none none 181 1 1 0 3.3 4 8
|
||||
cs64-none none 179 1 1 0 17.5 2 0
|
||||
cs64-none none 167 1 1 0 19.3 9 0
|
||||
cs64-none none 186 1 1 0 38.4 5 0
|
||||
cs64-none none 174 1 1 0 24.1 0 0
|
||||
cs64-none none 185 2 1 1 3.0 4 14 c=1;
|
||||
cs64-none none 178 2 1 1 43.0 3 0 c=1;
|
||||
cs64-none none 182 1 1 0 3.3 0 26
|
||||
cs64-none none 187 2 1 1 82.5 0 0 c=1;
|
||||
cs64-none none 189 3 1 2 8.2 15 0 c=2;
|
||||
cs64-none none 188 1 1 0 31.2 6 0
|
||||
cs64-none none 190 1 1 0 3.5 12 11
|
||||
cs64-none none 193 1 1 0 19.7 5 0
|
||||
cs64-none none 183 1 1 0 3.0 15 21
|
||||
cs64-none none 192 1 1 0 6.8 3 0
|
||||
cs64-none none 196 1 1 0 3.6 9 0
|
||||
cs64-none none 198 1 1 0 8.6 13 0
|
||||
cs64-none none 184 1 1 0 3.5 7 0
|
||||
cs64-none none 195 1 1 0 14.5 6 0
|
||||
cs64-none none 191 2 1 1 29.9 6 0 c=1;
|
||||
cs64-none none 194 1 1 0 14.1 6 0
|
||||
cs64-none none 197 1 1 0 31.6 5 0
|
||||
cs64-none none 204 1 1 0 3.0 15 15
|
||||
cs64-none none 210 1 1 0 46.5 15 0
|
||||
cs64-none none 207 1 1 0 3.5 8 11
|
||||
cs64-none none 202 1 1 0 3.8 11 12
|
||||
cs64-none none 199 1 1 0 18.5 3 1
|
||||
cs64-none none 200 2 1 1 10.9 12 0 c=1;
|
||||
cs64-none none 209 1 1 0 3.1 10 17
|
||||
cs64-none none 214 1 1 0 3.3 5 16
|
||||
cs64-none none 201 2 1 1 3.3 8 17 c=1;
|
||||
cs64-none none 208 1 1 0 34.3 1 0
|
||||
cs64-none none 206 1 1 0 15.5 11 0
|
||||
cs64-none none 205 1 1 0 28.4 11 0
|
||||
cs64-none none 216 1 1 0 45.3 13 0
|
||||
cs64-none none 203 1 1 0 3.3 1 24
|
||||
cs64-none none 221 1 1 0 3.2 12 16
|
||||
cs64-none none 220 1 1 0 3.5 14 14
|
||||
cs64-none none 218 4 1 3 4.0 6 22 c=3;
|
||||
cs64-none none 223 3 1 2 42.6 10 0 c=2;
|
||||
cs64-none none 224 1 1 0 3.2 2 11
|
||||
cs64-none none 212 1 1 0 28.8 1 0
|
||||
cs64-none none 217 1 1 0 3.0 15 7
|
||||
cs64-none none 215 4 1 3 53.8 9 0 c=3;
|
||||
cs64-none none 225 3 1 2 3.8 4 1 c=2;
|
||||
cs64-none none 228 1 1 0 3.0 8 3
|
||||
cs64-none none 211 1 1 0 3.7 8 1
|
||||
cs64-none none 213 1 1 0 13.4 4 0
|
||||
cs64-none none 229 1 1 0 3.9 12 1
|
||||
cs64-none none 219 2 1 1 33.2 6 1 c=1;
|
||||
cs64-none none 232 1 1 0 3.2 8 13
|
||||
cs64-none none 231 1 1 0 9.3 15 0
|
||||
cs64-none none 222 1 1 0 16.5 10 0
|
||||
cs64-none none 235 1 1 0 32.7 3 0
|
||||
cs64-none none 234 1 1 0 3.6 11 23
|
||||
cs64-none none 233 1 1 0 3.1 13 21
|
||||
cs64-none none 226 3 1 2 9.0 15 0 c=2;
|
||||
cs64-none none 227 2 1 1 3.4 1 4 c=1;
|
||||
cs64-none none 238 1 1 0 15.6 13 0
|
||||
cs64-none none 230 1 1 0 20.9 0 0
|
||||
cs64-none none 237 3 1 2 3.9 5 19 c=2;
|
||||
cs64-none none 236 1 1 0 3.2 7 23
|
||||
cs64-none none 239 1 1 0 19.7 4 0
|
||||
cs64-none none 251 1 1 0 26.7 14 0
|
||||
cs64-none none 240 1 1 0 31.3 12 0
|
||||
cs64-none none 252 2 1 1 61.1 9 0 c=1;
|
||||
cs64-none none 242 1 1 0 14.0 12 0
|
||||
cs64-none none 244 1 1 0 2.7 11 8
|
||||
cs64-none none 241 1 1 0 50.7 13 0
|
||||
cs64-none none 248 1 1 0 3.1 1 17
|
||||
cs64-none none 246 2 1 1 11.2 5 0 c=1;
|
||||
cs64-none none 253 2 1 1 8.2 9 0 c=1;
|
||||
cs64-none none 247 1 1 0 34.2 5 0
|
||||
cs64-none none 249 1 1 0 3.3 7 3
|
||||
cs64-none none 250 2 1 1 25.8 9 0 c=1;
|
||||
cs64-none none 243 2 1 1 52.5 8 1 c=1;
|
||||
cs64-none none 254 1 1 0 3.1 12 10
|
||||
cs64-none none 245 1 1 0 11.4 8 0
|
||||
cs64-none none 255 1 1 0 12.2 8 0
|
||||
# end 2026-10-08T15:07:34Z rows=257
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
17
docs/analysis/class-v6/logs/connected/uniform-cs64-none.tsv
Normal file
17
docs/analysis/class-v6/logs/connected/uniform-cs64-none.tsv
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
class era seed attempt program_id min_site_ratio min_site top0.1_share control_share ratio_to_control max_item_reads control_max reads
|
||||
cs64s27x16 none 0 0 9ad55de91485542b 1.00006 15 0.002383 0.002382 1.0002 27 27 134217728
|
||||
cs64s27x16 none 4 0 ef05f7b4f74f8b34 0.99876 11 0.002381 0.002377 1.0016 26 29 134217728
|
||||
cs64s27x16 none 9 0 4f7dfb188ea079f8 0.99407 9 0.002382 0.002380 1.0010 27 28 134217728
|
||||
cs64s27x16 none 3 0 423f3fcb11ffd16c 0.99937 7 0.002380 0.002380 1.0001 27 28 134217728
|
||||
cs64s27x16 none 12 0 f2ee7ca7642ce409 1.00010 1 0.002378 0.002381 0.9987 27 27 134217728
|
||||
cs64s27x16 none 7 0 a05222bdb885f832 0.99795 11 0.002381 0.002379 1.0010 28 27 134217728
|
||||
cs64s27x16 none 10 0 d6229a4ebc3da203 1.00013 1 0.002381 0.002381 0.9999 28 27 134217728
|
||||
cs64s27x16 none 8 0 5d3c176e39a2d7bd 0.99565 1 0.002377 0.002379 0.9991 27 27 134217728
|
||||
cs64s27x16 none 1 0 118f3f6ee409eb00 1.00000 3 0.002379 0.002381 0.9990 26 27 134217728
|
||||
cs64s27x16 none 5 1 66da80dabf645c59 0.99958 6 0.002382 0.002379 1.0014 27 28 134217728
|
||||
cs64s27x16 none 15 2 7261a08393ef6b71 1.00000 2 0.002379 0.002380 0.9996 26 27 134217728
|
||||
cs64s27x16 none 6 0 0f25af70edd287cc 0.99985 0 0.002381 0.002380 1.0003 26 28 134217728
|
||||
cs64s27x16 none 11 0 b1ccae46f9288cc2 1.00008 14 0.002381 0.002382 0.9997 27 27 134217728
|
||||
cs64s27x16 none 2 0 53c13e9f37bad71f 1.00010 3 0.002380 0.002380 0.9999 26 29 134217728
|
||||
cs64s27x16 none 14 0 e84c88374beb684f 1.00010 6 0.002382 0.002382 1.0002 27 27 134217728
|
||||
cs64s27x16 none 13 0 87d88cf6b4db69a2 1.00011 10 0.002383 0.002383 1.0003 29 27 134217728
|
||||
|
17
docs/analysis/class-v6/logs/connected/worker-4090.log
Normal file
17
docs/analysis/class-v6/logs/connected/worker-4090.log
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
RESULT worker pack packs/cs64-bound 15:17:37
|
||||
RESULT check packs/cs64-bound info igneum-worker-cuda 1.0 (4 October 2026): device 0 NVIDIA_GeForce_RTX_4090 (sm_89, 128 SMs), driver 13.0 from libcuda.so.1, NVRTC 12.8 from libnvrtc.so.12, target sm_89 (the device's architecture, listed by NVRTC)
|
||||
RESULT check packs/cs64-bound check PASS packs/cs64-bound in 2953 ms: nvrtc 1813 cache 2 dataset 31 hot 0 check 1106 race 0 ms variant base class v2; self-test PASS (cache head, last line and FNV-1a 64 48c4f5bf24166b2e; dataset head, word [268435455] and 64 samples; 96 of 96 vector lanes)
|
||||
RESULT check packs/cs64-bound epoch 69676e65756d2d7636632f30 day 6461792f323032362d31302d3033, dataset 2^28 words, cache 2^26 words in 65536 segments, 87 registers, 20 blocks/SM at 1 warp(s)/block, target sm_89
|
||||
RESULT bench packs/cs64-bound pack packs/cs64-bound on NVIDIA_GeForce_RTX_4090: nvrtc 1749 cache 2 dataset 31 hot 0 check 1101 race 0 ms variant base class v2; self-test PASS (cache head, last line and FNV-1a 64 48c4f5bf24166b2e; dataset head, word [268435455] and 64 samples; 96 of 96 vector lanes)
|
||||
RESULT bench packs/cs64-bound warm-up dispatch (base 0): 268.83 ms; 250 timed dispatches of 16777216 nonces: mean 268.83 ms
|
||||
RESULT bench packs/cs64-bound RESULT pack=packs/cs64-bound class=mx8+sh256x27 device=NVIDIA_GeForce_RTX_4090 arch=sm_89 regs=87 blocks_per_sm=20 warps=0 resident=2560 arena_mib=0 hot_mib=0 hot_slots=0 hot_fill_ms=0.00 nonces=16777216 batches=250 check=PASS fingerprint=ad0cec2a42c84aff mhs=62.408 loads=128 bytes=512 scratch_ops=0 time=wall
|
||||
RESULT smi packs/cs64-bound samples 66 mean_power_w 269.4 mean_sm_mhz 2809
|
||||
RESULT worker pack v5-genesis 15:18:52
|
||||
RESULT check v5-genesis info igneum-worker-cuda 1.0 (4 October 2026): device 0 NVIDIA_GeForce_RTX_4090 (sm_89, 128 SMs), driver 13.0 from libcuda.so.1, NVRTC 12.8 from libnvrtc.so.12, target sm_89 (the device's architecture, listed by NVRTC)
|
||||
RESULT check v5-genesis check PASS v5-genesis in 1722 ms: nvrtc 586 cache 2 dataset 31 hot 0 check 1102 race 0 ms variant base class v5 (state leaves 93, uploaded for the build and freed); self-test PASS (cache head, last line and FNV-1a 64 48c4f5bf24166b2e; dataset head, word [268435455] and 64 samples; 96 of 96 vector lanes)
|
||||
RESULT check v5-genesis epoch 69676e65756d2d67656e65736973 day 6461792f323032362d31302d3033, dataset 2^28 words, cache 2^26 words in 65536 segments, 32 registers, 24 blocks/SM at 1 warp(s)/block, target sm_89
|
||||
RESULT bench v5-genesis pack v5-genesis on NVIDIA_GeForce_RTX_4090: nvrtc 559 cache 2 dataset 32 hot 0 check 1100 race 0 ms variant base class v5 (state leaves 93, uploaded for the build and freed); self-test PASS (cache head, last line and FNV-1a 64 48c4f5bf24166b2e; dataset head, word [268435455] and 64 samples; 96 of 96 vector lanes)
|
||||
RESULT bench v5-genesis warm-up dispatch (base 0): 268.89 ms; 250 timed dispatches of 16777216 nonces: mean 268.89 ms
|
||||
RESULT bench v5-genesis RESULT pack=v5-genesis class=mx8+sh256x27+state device=NVIDIA_GeForce_RTX_4090 arch=sm_89 regs=32 blocks_per_sm=24 warps=0 resident=3072 arena_mib=0 hot_mib=0 hot_slots=0 hot_fill_ms=0.00 nonces=16777216 batches=250 check=PASS fingerprint=ae74193ddad19e19 mhs=62.394 loads=128 bytes=512 scratch_ops=0 time=wall
|
||||
RESULT smi v5-genesis samples 65 mean_power_w 271.9 mean_sm_mhz 2805
|
||||
RESULT worker end 15:20:03
|
||||
17
docs/analysis/class-v6/logs/connected/worker-5090.log
Normal file
17
docs/analysis/class-v6/logs/connected/worker-5090.log
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
RESULT worker pack packs/cs64-bound 15:16:17
|
||||
RESULT check packs/cs64-bound info igneum-worker-cuda 1.0 (4 October 2026): device 0 NVIDIA_GeForce_RTX_5090 (sm_120, 170 SMs), driver 13.0 from libcuda.so.1, NVRTC 12.8 from libnvrtc.so.12, target sm_120 (the device's architecture, listed by NVRTC)
|
||||
RESULT check packs/cs64-bound check PASS packs/cs64-bound in 2256 ms: nvrtc 1089 cache 3 dataset 30 hot 0 check 1130 race 0 ms variant base class v2; self-test PASS (cache head, last line and FNV-1a 64 48c4f5bf24166b2e; dataset head, word [268435455] and 64 samples; 96 of 96 vector lanes)
|
||||
RESULT check packs/cs64-bound epoch 69676e65756d2d7636632f30 day 6461792f323032362d31302d3033, dataset 2^28 words, cache 2^26 words in 65536 segments, 80 registers, 24 blocks/SM at 1 warp(s)/block, target sm_120
|
||||
RESULT bench packs/cs64-bound pack packs/cs64-bound on NVIDIA_GeForce_RTX_5090: nvrtc 1086 cache 3 dataset 30 hot 0 check 1122 race 0 ms variant base class v2; self-test PASS (cache head, last line and FNV-1a 64 48c4f5bf24166b2e; dataset head, word [268435455] and 64 samples; 96 of 96 vector lanes)
|
||||
RESULT bench packs/cs64-bound warm-up dispatch (base 0): 268.09 ms; 250 timed dispatches of 16777216 nonces: mean 266.80 ms
|
||||
RESULT bench packs/cs64-bound RESULT pack=packs/cs64-bound class=mx8+sh256x27 device=NVIDIA_GeForce_RTX_5090 arch=sm_120 regs=80 blocks_per_sm=24 warps=0 resident=4080 arena_mib=0 hot_mib=0 hot_slots=0 hot_fill_ms=0.00 nonces=16777216 batches=250 check=PASS fingerprint=ad0cec2a42c84aff mhs=62.882 loads=128 bytes=512 scratch_ops=0 time=wall
|
||||
RESULT smi packs/cs64-bound samples 65 mean_power_w 574.2 mean_sm_mhz 2797
|
||||
RESULT worker pack v5-genesis 15:17:29
|
||||
RESULT check v5-genesis info igneum-worker-cuda 1.0 (4 October 2026): device 0 NVIDIA_GeForce_RTX_5090 (sm_120, 170 SMs), driver 13.0 from libcuda.so.1, NVRTC 12.8 from libnvrtc.so.12, target sm_120 (the device's architecture, listed by NVRTC)
|
||||
RESULT check v5-genesis check PASS v5-genesis in 1709 ms: nvrtc 505 cache 5 dataset 32 hot 0 check 1162 race 0 ms variant base class v5 (state leaves 93, uploaded for the build and freed); self-test PASS (cache head, last line and FNV-1a 64 48c4f5bf24166b2e; dataset head, word [268435455] and 64 samples; 96 of 96 vector lanes)
|
||||
RESULT check v5-genesis epoch 69676e65756d2d67656e65736973 day 6461792f323032362d31302d3033, dataset 2^28 words, cache 2^26 words in 65536 segments, 48 registers, 24 blocks/SM at 1 warp(s)/block, target sm_120
|
||||
RESULT bench v5-genesis pack v5-genesis on NVIDIA_GeForce_RTX_5090: nvrtc 500 cache 3 dataset 30 hot 0 check 1156 race 0 ms variant base class v5 (state leaves 93, uploaded for the build and freed); self-test PASS (cache head, last line and FNV-1a 64 48c4f5bf24166b2e; dataset head, word [268435455] and 64 samples; 96 of 96 vector lanes)
|
||||
RESULT bench v5-genesis warm-up dispatch (base 0): 258.26 ms; 250 timed dispatches of 16777216 nonces: mean 266.46 ms
|
||||
RESULT bench v5-genesis RESULT pack=v5-genesis class=mx8+sh256x27+state device=NVIDIA_GeForce_RTX_5090 arch=sm_120 regs=48 blocks_per_sm=24 warps=0 resident=4080 arena_mib=0 hot_mib=0 hot_slots=0 hot_fill_ms=0.00 nonces=16777216 batches=250 check=PASS fingerprint=ae74193ddad19e19 mhs=62.963 loads=128 bytes=512 scratch_ops=0 time=wall
|
||||
RESULT smi v5-genesis samples 65 mean_power_w 571.4 mean_sm_mhz 2798
|
||||
RESULT worker end 15:18:41
|
||||
3539
docs/analysis/class-v6/logs/f8-A.log
Normal file
3539
docs/analysis/class-v6/logs/f8-A.log
Normal file
File diff suppressed because it is too large
Load diff
3538
docs/analysis/class-v6/logs/f8-B.log
Normal file
3538
docs/analysis/class-v6/logs/f8-B.log
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,12 @@
|
|||
binary bc21542a5b421170 commit fg7 test family_gate_era_census from 2 seeds 64 threads 8 start 2026-10-08T13:05:16Z
|
||||
lease: 0 of 8 pool cores free (88 leased); waiting
|
||||
lease: holding 8 pool cores (14,15,16,17,24,25,26,27, waited 1217 s, class measure): family gate: family_gate_era_census f8space-p2-65 seeds 2+64
|
||||
|
||||
running 1 test
|
||||
test accept::tests::family_gate_era_census ... family_gate_era_census: 64 eras 2..66 in 49 s on 8 threads -> /srv/builds/_adv-family-gate/logs/f8space-p2-65-family_gate_era_census-2-64.tsv
|
||||
ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 80 filtered out; finished in 49.18s
|
||||
|
||||
lease: released 8 pool cores after 1267 s, exit 0
|
||||
end 2026-10-08T13:26:23Z rc 0
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
k shape reps mixer width R M pos w_add w_xor w_mul w_mad w_shfl w_rotl w_sub w_mulhi w_rotr w_or attempt candidates a_prime a b c_const c_lane c_sat c_bias c_distinct c1_sat_source c2_low_entropy c3_hot_item min_ratio min_site top_count_max bucket_ratio_max bucket_z_max bucket_site bucket_win bit_z_max bit_site bit_win bit last_resort_k c3_ratios secs
|
||||
8 256 27 8 1 22 573e71b3 [1, 4, 7, 12] 12 10 8 8 8 7 6 6 6 4 2 3 1 1 0 0 0 0 0 0 0 0 0 0.99628 14 4 3.719 21.75 14 2 -512.76 6 1 22 4.6
|
||||
2 256 27 8 1 4 bf0fab63 [2, 3, 7, 15] 12 10 8 8 8 7 6 6 6 4 5 6 4 0 1 0 0 0 0 0 0 0 0 0.99994 10 3 1.688 5.50 6 2 -2.83 6 2 16 4.7
|
||||
4 256 27 8 1 13 2477779f [5, 10, 11, 15] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99613 1 4 7.172 49.38 1 2 -510.71 1 2 13 4.7
|
||||
6 256 27 8 1 21 05ba4603 [0, 2, 7, 11] 12 10 8 8 8 7 6 6 6 4 6 7 5 0 1 0 0 0 0 0 0 0 0 0.99987 14 4 2.062 6.01 1 1 -3.95 10 2 24 4.8
|
||||
3 256 27 8 1 1 a80102a7 [0, 5, 8, 15] 12 10 8 8 8 7 6 6 6 4 1 2 0 1 0 0 0 0 0 0 0 0 0 0.99968 8 4 2.375 5.50 0 0 257.84 8 1 1 5.0
|
||||
5 256 27 8 1 4 435fd6c5 [0, 3, 11, 12] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99990 1 3 1.938 5.30 4 1 -2.60 2 0 17 5.0
|
||||
7 256 27 8 1 25 ff347267 [2, 6, 7, 14] 12 10 8 8 8 7 6 6 6 4 1 2 1 0 0 0 0 0 0 0 0 0 0 0.99946 13 4 2.938 7.75 13 0 -511.12 13 0 25 5.1
|
||||
9 256 27 8 1 18 07114293 [0, 3, 11, 14] 12 10 8 8 8 7 6 6 6 4 8 9 7 1 0 0 0 0 0 0 0 0 0 0.99990 6 3 2.438 5.75 10 0 168.61 12 0 18 8.6
|
||||
12 256 27 8 1 16 2fda8d19 [0, 4, 6, 10] 12 10 8 8 8 7 6 6 6 4 1 2 1 0 0 0 0 0 0 0 0 0 0 0.99999 14 4 2.375 5.50 0 0 35.93 0 0 17 4.6
|
||||
11 256 27 8 1 29 b889f389 [3, 5, 9, 12] 12 10 8 8 8 7 6 6 6 4 3 4 2 1 0 0 0 0 0 0 0 0 0 0.99993 12 4 2.312 5.25 3 0 -5.30 1 1 0 4.7
|
||||
13 256 27 8 1 26 3340424b [1, 7, 14, 15] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99994 3 4 2.562 6.25 8 0 144.94 8 0 26 4.7
|
||||
10 256 27 8 1 17 5732bb5d [3, 5, 8, 13] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99626 8 3 6.266 42.12 8 2 -512.88 8 2 17 4.9
|
||||
16 256 27 8 1 1 a6ea3eb1 [1, 4, 7, 10] 12 10 8 8 8 7 6 6 6 4 3 4 3 0 0 0 0 0 0 0 0 0 0 0.99991 8 4 2.375 5.50 9 0 -128.13 4 0 1 4.7
|
||||
14 256 27 8 1 23 55fb3ce5 [2, 5, 8, 11] 12 10 8 8 8 7 6 6 6 4 4 5 4 0 0 0 0 0 0 0 0 0 0 0.99993 1 4 2.375 5.50 9 0 3.25 8 1 0 4.8
|
||||
15 256 27 8 1 26 26f35c81 [0, 2, 14, 15] 12 10 8 8 8 7 6 6 6 4 4 5 2 0 1 0 0 0 0 0 0 1 0 0.99997 10 3 2.500 6.00 1 0 -58.21 0 1 0 c2:943 7.3
|
||||
21 256 27 8 1 22 48825377 [3, 5, 6, 8] 12 10 8 8 8 7 6 6 6 4 3 4 3 0 0 0 0 0 0 0 0 0 0 0.99995 11 4 2.438 5.75 0 0 2.63 0 0 4 4.4
|
||||
20 256 27 8 1 28 aa378839 [0, 3, 7, 10] 12 10 8 8 8 7 6 6 6 4 2 3 2 0 0 0 0 0 0 0 0 0 0 0.99988 14 4 1.906 5.13 7 1 3.34 12 2 24 4.5
|
||||
22 256 27 8 1 25 1d32e3af [7, 9, 11, 15] 12 10 8 8 8 7 6 6 6 4 1 2 1 0 0 0 0 0 0 0 0 0 0 0.99991 7 3 1.938 5.30 1 1 -129.05 6 2 25 4.9
|
||||
23 256 27 8 1 26 d88595c9 [0, 1, 4, 14] 12 10 8 8 8 7 6 6 6 4 1 2 1 0 0 0 0 0 0 0 0 0 0 0.99996 5 4 2.312 5.25 0 0 2.97 5 0 13 5.1
|
||||
19 256 27 8 1 9 69944467 [6, 9, 11, 12] 12 10 8 8 8 7 6 6 6 4 2 3 0 1 0 0 0 0 0 0 0 1 0 0.99995 13 3 1.906 5.13 6 1 4.40 2 0 25 c2:933 6.9
|
||||
18 256 27 8 1 22 83e41bc3 [1, 8, 10, 13] 12 10 8 8 8 7 6 6 6 4 9 10 7 1 0 0 0 0 0 0 0 1 0 0.99992 10 4 2.375 5.50 1 0 3.38 6 1 23 c2:927 7.0
|
||||
17 256 27 8 1 7 fb71a153 [2, 5, 8, 10] 12 10 8 8 8 7 6 6 6 4 3 4 3 0 0 0 0 0 0 0 0 0 0 0.99992 15 4 2.375 5.50 11 0 2.93 14 0 4 8.4
|
||||
24 256 27 8 1 3 44fd49d7 [0, 4, 7, 9] 12 10 8 8 8 7 6 6 6 4 1 2 1 0 0 0 0 0 0 0 0 0 0 0.99985 14 4 1.906 5.13 3 1 -9.35 15 0 6 4.9
|
||||
26 256 27 8 1 19 9f5f0bdb [2, 7, 11, 12] 12 10 8 8 8 7 6 6 6 4 1 2 1 0 0 0 0 0 0 0 0 0 0 0.99910 2 3 6.938 23.75 2 0 -511.00 2 0 19 4.7
|
||||
25 256 27 8 1 14 c619dd6b [2, 6, 8, 15] 12 10 8 8 8 7 6 6 6 4 2 3 2 0 0 0 0 0 0 0 0 0 0 0.99984 2 3 2.250 5.00 11 0 -2.72 13 0 3 4.8
|
||||
27 256 27 8 1 15 ab4deb19 [4, 5, 11, 13] 12 10 8 8 8 7 6 6 6 4 6 7 6 0 0 0 0 0 0 0 0 0 0 0.99899 1 3 8.562 30.25 1 0 -512.72 1 0 15 4.5
|
||||
28 256 27 8 1 26 347b218b [0, 6, 12, 13] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99995 0 3 2.312 5.25 3 0 3.66 1 0 10 4.7
|
||||
29 256 27 8 1 25 e597ba17 [4, 5, 9, 14] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99991 2 4 2.500 6.00 10 0 84.65 12 1 25 4.6
|
||||
30 256 27 8 1 18 ac8f1c1b [3, 4, 11, 12] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99990 4 3 2.375 5.50 4 0 -2.95 4 0 13 4.6
|
||||
32 256 27 8 1 17 dc6791fd [6, 11, 14, 15] 12 10 8 8 8 7 6 6 6 4 6 7 3 1 0 0 0 2 0 0 0 0 0 0.99878 12 4 8.188 28.75 12 0 -513.54 12 0 17 4.7
|
||||
33 256 27 8 1 6 3e376d09 [0, 3, 4, 9] 12 10 8 8 8 7 6 6 6 4 4 5 4 0 0 0 0 0 0 0 0 0 0 0.99711 0 4 2.312 5.25 10 0 -512.62 7 1 6 4.6
|
||||
35 256 27 8 1 24 96fc94e3 [0, 7, 12, 15] 12 10 8 8 8 7 6 6 6 4 1 2 1 0 0 0 0 0 0 0 0 0 0 0.99986 11 4 1.656 5.25 14 2 -4.81 1 0 25 4.8
|
||||
36 256 27 8 1 27 4c7a6647 [9, 10, 12, 15] 12 10 8 8 8 7 6 6 6 4 12 13 11 1 0 0 0 0 0 0 0 0 0 0.99993 11 4 2.375 5.50 14 0 2.74 6 0 3 4.9
|
||||
34 256 27 8 1 7 2e362f07 [3, 6, 11, 14] 12 10 8 8 8 7 6 6 6 4 1 2 0 0 0 0 0 0 0 0 0 0 1 0.99992 3 4 2.375 5.50 14 0 10.78 4 1 7 c3:992 6.7
|
||||
38 256 27 8 1 19 06d774fb [3, 4, 12, 15] 12 10 8 8 8 7 6 6 6 4 1 2 1 0 0 0 0 0 0 0 0 0 0 0.99993 6 4 2.312 5.25 1 0 -63.05 5 2 19 4.5
|
||||
31 256 27 8 1 14 d4aea2c7 [2, 5, 12, 14] 12 10 8 8 8 7 6 6 6 4 4 5 4 0 0 0 0 0 0 0 0 0 0 0.99994 15 3 2.375 5.50 1 0 7.27 12 0 15 8.4
|
||||
37 256 27 8 1 18 d1f25c1f [1, 7, 8, 13] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99994 13 4 1.938 5.30 8 1 3.82 3 0 18 4.7
|
||||
39 256 27 8 1 3 a51e7c6d [3, 6, 12, 13] 12 10 8 8 8 7 6 6 6 4 1 2 1 0 0 0 0 0 0 0 0 0 0 0.99905 1 4 2.031 5.83 7 1 -513.32 1 0 3 4.9
|
||||
40 256 27 8 1 16 4bc62819 [1, 9, 10, 13] 12 10 8 8 8 7 6 6 6 4 3 4 2 0 1 0 0 0 0 0 0 0 0 0.99995 4 4 2.375 5.50 3 0 -3.37 15 0 2 4.6
|
||||
41 256 27 8 1 16 7a5295a9 [0, 3, 7, 11] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99982 11 4 2.000 5.66 0 1 -3.16 14 1 4 4.8
|
||||
42 256 27 8 1 16 e3baafe9 [2, 7, 10, 14] 12 10 8 8 8 7 6 6 6 4 1 2 1 0 0 0 0 0 0 0 0 0 0 0.99989 9 4 2.375 5.50 13 0 3.63 9 2 0 4.9
|
||||
44 256 27 8 1 14 2d1bd537 [2, 7, 13, 15] 12 10 8 8 8 7 6 6 6 4 1 2 0 1 0 0 0 0 0 0 0 0 0 0.99616 8 4 7.281 50.25 8 2 -512.09 8 2 14 4.7
|
||||
43 256 27 8 1 19 cd014ac5 [11, 12, 13, 14] 12 10 8 8 8 7 6 6 6 4 5 6 4 1 0 0 0 0 0 0 0 0 0 0.99919 7 4 2.594 9.02 7 1 -127.25 7 1 20 5.1
|
||||
46 256 27 8 1 24 6472aa9d [3, 9, 10, 13] 12 10 8 8 8 7 6 6 6 4 5 6 4 1 0 0 0 0 0 0 0 0 0 0.99722 4 4 2.703 13.62 4 2 -512.03 4 2 24 5.0
|
||||
47 256 27 8 1 25 00a713dd [3, 7, 8, 9] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99989 11 4 2.375 5.50 6 0 3.36 7 2 18 5.1
|
||||
49 256 27 8 1 30 b1cbeac1 [6, 8, 9, 14] 12 10 8 8 8 7 6 6 6 4 6 7 5 1 0 0 0 0 0 0 0 0 0 0.99991 0 4 2.438 5.75 5 0 -2.86 5 0 21 5.1
|
||||
45 256 27 8 1 18 d9c46e27 [3, 4, 6, 13] 12 10 8 8 8 7 6 6 6 4 3 4 2 1 0 0 0 0 0 0 0 0 0 0.99963 13 3 2.812 10.25 13 1 255.79 13 1 18 8.7
|
||||
50 256 27 8 1 10 2b3dbe09 [7, 13, 14, 15] 12 10 8 8 8 7 6 6 6 4 1 2 1 0 0 0 0 0 0 0 0 0 0 0.99807 5 4 3.500 14.14 5 1 -512.40 5 1 10 6.1
|
||||
48 256 27 8 1 13 5e051e51 [9, 10, 12, 13] 12 10 8 8 8 7 6 6 6 4 6 7 5 1 0 0 0 0 0 0 0 0 0 0.99986 14 3 2.500 6.00 11 0 -2.94 5 0 21 7.6
|
||||
54 256 27 8 1 31 24b4a1e5 [5, 10, 13, 15] 12 10 8 8 8 7 6 6 6 4 1 2 1 0 0 0 0 0 0 0 0 0 0 0.99994 12 4 2.375 5.50 10 0 2.77 11 2 14 5.3
|
||||
51 256 27 8 1 3 d94b2ac7 [0, 2, 4, 9] 12 10 8 8 8 7 6 6 6 4 4 5 4 0 0 0 0 0 0 0 0 0 0 0.99993 12 4 2.312 5.25 3 0 -3.88 6 1 15 8.8
|
||||
55 256 27 8 1 7 0e0c23b9 [1, 5, 13, 14] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99625 7 4 2.312 5.25 8 0 -511.26 7 2 7 5.3
|
||||
53 256 27 8 1 9 7ec0e095 [1, 8, 9, 14] 12 10 8 8 8 7 6 6 6 4 1 2 1 0 0 0 0 0 0 0 0 0 0 0.99949 15 3 2.500 6.00 13 0 159.62 15 2 9 8.8
|
||||
52 256 27 8 1 12 dd60e49d [2, 4, 12, 14] 12 10 8 8 8 7 6 6 6 4 1 2 1 0 0 0 0 0 0 0 0 0 0 0.99618 14 4 8.078 56.62 14 2 -512.06 14 2 12 9.4
|
||||
57 256 27 8 1 25 2bfb510f [0, 3, 5, 9] 12 10 8 8 8 7 6 6 6 4 4 5 4 0 0 0 0 0 0 0 0 0 0 0.99997 11 4 1.938 5.30 10 1 -3.40 2 0 19 6.1
|
||||
59 256 27 8 1 7 ab70a8c7 [2, 3, 8, 10] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99993 14 3 2.094 6.19 15 1 -11.71 13 0 7 5.1
|
||||
61 256 27 8 1 4 99ec1a13 [3, 8, 13, 14] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99909 1 4 2.375 5.50 9 0 -512.35 1 0 4 5.0
|
||||
58 256 27 8 1 30 2faae5d7 [1, 5, 14, 15] 12 10 8 8 8 7 6 6 6 4 1 2 1 0 0 0 0 0 0 0 0 0 0 0.99987 7 3 1.969 5.48 14 1 3.57 8 1 3 9.0
|
||||
56 256 27 8 1 24 dc5e362b [6, 7, 9, 10] 12 10 8 8 8 7 6 6 6 4 4 5 3 0 0 0 0 0 0 0 0 1 0 0.99995 2 4 2.312 5.25 3 0 3.51 8 1 6 c2:965 11.7
|
||||
65 256 27 8 1 23 8d7fa8fb [1, 10, 11, 13] 12 10 8 8 8 7 6 6 6 4 3 4 2 0 1 0 0 0 0 0 0 0 0 0.99811 4 4 2.562 12.50 4 2 -449.07 4 2 23 5.0
|
||||
64 256 27 8 1 8 f7401b9f [4, 7, 10, 15] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99990 1 4 2.562 6.25 0 0 -68.29 0 0 8 6.1
|
||||
60 256 27 8 1 4 cf5e9ceb [0, 2, 5, 8] 12 10 8 8 8 7 6 6 6 4 1 2 1 0 0 0 0 0 0 0 0 0 0 0.99989 8 3 2.250 5.00 15 0 -108.06 2 2 5 9.0
|
||||
62 256 27 8 1 11 ae89e4ab [3, 5, 8, 13] 12 10 8 8 8 7 6 6 6 4 1 2 1 0 0 0 0 0 0 0 0 0 0 0.99815 9 3 2.203 9.62 9 2 -343.68 9 2 11 8.9
|
||||
63 256 27 8 1 20 eda85b67 [0, 3, 9, 13] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99988 2 4 2.375 5.50 4 0 -64.53 14 0 21 9.3
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
binary bc21542a5b421170 commit fg7 test family_gate_era_census from 212 seeds 14 threads 4 start 2026-10-08T13:05:16Z
|
||||
lease: 0 of 4 pool cores free (88 leased); waiting
|
||||
lease: holding 4 pool cores (28,29,30,31, waited 1217 s, class measure): family gate: family_gate_era_census f8space-p212-225 seeds 212+14
|
||||
|
||||
running 1 test
|
||||
test accept::tests::family_gate_era_census ... family_gate_era_census: 14 eras 212..226 in 35 s on 4 threads -> /srv/builds/_adv-family-gate/logs/f8space-p212-225-family_gate_era_census-212-14.tsv
|
||||
ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 80 filtered out; finished in 34.69s
|
||||
|
||||
Terminated
|
||||
lease: released 4 pool cores after 1252 s, exit 0
|
||||
end 2026-10-08T13:26:08Z rc 0
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
k shape reps mixer width R M pos w_add w_xor w_mul w_mad w_shfl w_rotl w_sub w_mulhi w_rotr w_or attempt candidates a_prime a b c_const c_lane c_sat c_bias c_distinct c1_sat_source c2_low_entropy c3_hot_item min_ratio min_site top_count_max bucket_ratio_max bucket_z_max bucket_site bucket_win bit_z_max bit_site bit_win bit last_resort_k c3_ratios secs
|
||||
215 256 27 8 1 7 9f95aef9 [3, 4, 7, 14] 12 10 8 8 8 7 6 6 6 4 5 6 4 1 0 0 0 0 0 0 0 0 0 0.99969 6 3 2.000 5.66 10 1 -256.22 6 1 7 8.7
|
||||
213 256 27 8 1 31 04aeeee3 [0, 1, 3, 9] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99995 13 3 1.938 5.30 5 1 3.02 3 1 0 8.8
|
||||
214 256 27 8 1 1 04870f7f [1, 2, 6, 15] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99990 13 3 2.438 5.75 14 0 -10.66 13 0 2 8.9
|
||||
212 256 27 8 1 8 d56f9b19 [5, 7, 13, 15] 12 10 8 8 8 7 6 6 6 4 4 5 3 0 0 0 0 0 0 0 0 0 1 0.99577 9 4 1.719 5.75 9 2 -512.14 9 2 8 c3:991 13.0
|
||||
216 256 27 8 1 30 ddd46ce1 [0, 1, 7, 9] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99987 8 3 2.312 5.25 3 0 -3.32 12 0 20 8.5
|
||||
217 256 27 8 1 15 2bb44f89 [10, 11, 14, 15] 12 10 8 8 8 7 6 6 6 4 1 2 0 1 0 0 0 0 0 0 0 0 0 0.99987 8 3 2.312 5.25 3 0 -64.88 0 1 15 8.7
|
||||
218 256 27 8 1 10 210c1f81 [3, 6, 12, 13] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99934 5 4 2.000 5.66 5 1 -353.27 5 1 11 9.2
|
||||
219 256 27 8 1 2 fbc1458b [2, 8, 13, 14] 12 10 8 8 8 7 6 6 6 4 2 3 1 1 0 0 0 0 0 0 0 0 0 0.99993 12 3 2.312 5.25 3 0 2.83 9 2 4 9.1
|
||||
221 256 27 8 1 16 147a60c9 [2, 6, 9, 11] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99900 14 3 7.688 26.75 14 0 -512.53 14 0 16 8.0
|
||||
220 256 27 8 1 16 3b974b5b [0, 1, 9, 10] 12 10 8 8 8 7 6 6 6 4 2 3 2 0 0 0 0 0 0 0 0 0 0 0.99872 1 4 6.812 32.88 1 1 -438.52 1 1 16 8.6
|
||||
222 256 27 8 1 27 38ebb1bd [0, 5, 8, 10] 12 10 8 8 8 7 6 6 6 4 1 2 1 0 0 0 0 0 0 0 0 0 0 0.99987 0 4 2.375 5.50 0 0 -122.37 1 0 27 8.9
|
||||
223 256 27 8 1 17 195d676f [0, 6, 8, 13] 12 10 8 8 8 7 6 6 6 4 4 5 3 1 0 0 0 0 0 0 0 0 0 0.99993 6 3 2.031 5.83 1 1 3.29 15 1 4 8.5
|
||||
224 256 27 8 1 18 8a4c7517 [1, 2, 3, 6] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99987 12 4 1.656 5.25 13 2 3.29 2 0 17 8.7
|
||||
225 256 27 8 1 2 2ef225c7 [0, 12, 14, 15] 12 10 8 8 8 7 6 6 6 4 0 1 0 0 0 0 0 0 0 0 0 0 0 0.99740 4 3 2.375 5.50 5 0 -567.19 4 1 2 8.9
|
||||
|
27
docs/analysis/class-v6/logs/fg-axes.py
Normal file
27
docs/analysis/class-v6/logs/fg-axes.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#!/usr/bin/env python3
|
||||
"""family-gate lane: the per-axis and per-rotation-class readings of one stratum's TSV (markdown tables)."""
|
||||
import sys, csv, collections, statistics
|
||||
rows = [r for p in sys.argv[1:] for r in csv.DictReader(open(p), delimiter='\t')]
|
||||
acc = [r for r in rows if r['attempt'] not in ('', '-1')]
|
||||
print(f"eras {len(rows)}, accepted {len(acc)}, exhausted {len(rows)-len(acc)}")
|
||||
def z(r): return abs(float(r['bit_z_max']))
|
||||
print("\n| Axis | Value | Eras | Mean attempt | Max attempt | r per candidate | (c''') refusals per candidate | Biased site over 6 sigma | Over 100 sigma | Bucket sigma p99 | min ratio p1 |")
|
||||
print("|---|---|---|---|---|---|---|---|---|---|---|")
|
||||
def rcls(r): R = int(r['R']); return 'R 28..31 (bit 0 cut at D = 28)' if R >= 28 else 'R 1..27'
|
||||
for axis, keyf in [('width (words)', lambda r: r['width']), ('shape', lambda r: r['shape']), ('mixer m', lambda r: r['mixer']), ('rotation class', rcls)]:
|
||||
d = collections.defaultdict(list)
|
||||
for r in rows: d[keyf(r)].append(r)
|
||||
for k in sorted(d, key=lambda x: (len(x), x)):
|
||||
g = d[k]; ga = [r for r in g if r['attempt'] not in ('', '-1')]
|
||||
cands = sum(int(r['candidates']) for r in g); rej = cands - len(ga); c3 = sum(int(r['c3_hot_item']) for r in g)
|
||||
bz = sorted(float(r['bucket_z_max']) for r in ga); mr = sorted(float(r['min_ratio']) for r in ga)
|
||||
print(f"| {axis} | {k} | {len(g)} | {statistics.mean(int(r['attempt']) for r in ga):.2f} | {max(int(r['attempt']) for r in ga)} | {rej/cands:.3f} | {100*c3/cands:.2f} percent | {100*sum(z(r)>6 for r in ga)/len(ga):.1f} percent | {100*sum(z(r)>100 for r in ga)/len(ga):.1f} percent | +{bz[int(len(bz)*.99)]:.1f} | {mr[int(len(mr)*.01)]:.4f} |")
|
||||
rel = collections.Counter((int(r['bit']) - int(r['R'])) % 32 for r in acc if z(r) > 6)
|
||||
tot = sum(rel.values())
|
||||
print("\nBiased bit minus R, mod 32, among the biased eras:", ', '.join(f"{k}: {v} ({100*v/tot:.0f} percent)" for k, v in rel.most_common(5)))
|
||||
neg = sum(1 for r in acc if float(r['bit_z_max']) < -6); pos = sum(1 for r in acc if float(r['bit_z_max']) > 6)
|
||||
print(f"Sign: {neg} with P(bit) under 1/2 (the product law), {pos} over 1/2 (an or-shaped source)")
|
||||
clean = sorted(float(r['bucket_z_max']) for r in acc if z(r) <= 6); n = len(clean)
|
||||
print(f"Bucket excess on the {n} bit-clean eras: median +{clean[n//2]:.1f} sigma, p95 +{clean[int(n*.95)]:.1f}, p99 +{clean[int(n*.99)]:.1f}, max +{clean[-1]:.1f}")
|
||||
cz = sorted(z(r) for r in acc if z(r) <= 6); print(f"Bit |z| on the eras under 6 sigma: median {cz[len(cz)//2]:.2f}, p99 {cz[int(len(cz)*.99)]:.2f} (the maximum of about 26 free bits over 16 sites of a clean program)")
|
||||
mr = sorted(float(r['min_ratio']) for r in acc); print(f"Minimum (c'') ratio of the accepted draw: min {mr[0]:.5f}, p1 {mr[int(len(mr)*.01)]:.5f}, p5 {mr[int(len(mr)*.05)]:.5f}, median {mr[len(mr)//2]:.5f}")
|
||||
9
docs/analysis/class-v6/logs/fg-build.sh
Executable file
9
docs/analysis/class-v6/logs/fg-build.sh
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env bash
|
||||
# family-gate lane: build the test binary of a box-side source dir under the lease pool and pin a copy. usage: fg-build.sh <tagdir> [threads]
|
||||
set -euo pipefail
|
||||
TAG="$1"; T="${2:-32}"; ROOT=/srv/builds/_adv-family-gate; W="$ROOT/src-$TAG"
|
||||
cd "$W/igneum-pow"; export PATH="$HOME/.cargo/bin:$PATH"; CARGO=$(command -v cargo)
|
||||
export RUSTC_WRAPPER=${RUSTC_WRAPPER:-$(command -v sccache || true)}
|
||||
/srv/builds/_bin/lease pool "$T" --min 8 --class measure --label "family gate: build the harness test binary ($TAG)" --owner class-v6-family-gate -- "$CARGO" test --release --lib --no-run -j "{cores}" > "$ROOT/logs/build-$TAG.log" 2>&1
|
||||
BIN=$(ls -t target/release/deps/igneum_pow-* | grep -v '\.d$' | head -1)
|
||||
mkdir -p "$ROOT/bin/$TAG"; cp "$BIN" "$ROOT/bin/$TAG/igneum_pow_test"; sha256sum "$ROOT/bin/$TAG/igneum_pow_test" | tee "$ROOT/bin/$TAG/sha256"
|
||||
12
docs/analysis/class-v6/logs/fg-census.sh
Executable file
12
docs/analysis/class-v6/logs/fg-census.sh
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
#!/usr/bin/env bash
|
||||
# family-gate lane: one chunk of the class v5 attempts census (every candidate of each seed's chain draw through the real rule, the
|
||||
# era of each seed drawn from its own label) from a pinned copy of the test binary, under the lease pool.
|
||||
# usage: fg-census.sh <commit> <test-name> <from> <seeds> <threads> <tag>
|
||||
set -euo pipefail
|
||||
C="$1"; TEST="$2"; FROM="$3"; N="$4"; T="$5"; TAG="$6"
|
||||
ROOT=/srv/builds/_adv-family-gate; BIN="$ROOT/bin/$C/igneum_pow_test"; LOG="$ROOT/logs/$TAG-$TEST-$FROM-$N.log"
|
||||
echo "$$" > "$ROOT/pids/$TAG-$TEST-$FROM-$N.pid"
|
||||
echo "binary $(sha256sum "$BIN" | cut -c1-16) commit $C test $TEST from $FROM seeds $N threads $T start $(date -u +%FT%TZ)" > "$LOG"
|
||||
IGNEUM_V5_CENSUS_FROM="$FROM" IGNEUM_V5_CENSUS_SEEDS="$N" IGNEUM_V5_CENSUS_THREADS="$T" IGNEUM_FG_FROM="$FROM" IGNEUM_FG_SEEDS="$N" IGNEUM_FG_THREADS="$T" IGNEUM_FG_OUT="$ROOT/logs/$TAG-$TEST-$FROM-$N.tsv" \
|
||||
/srv/builds/_bin/lease pool "$T" --min 8 --class measure --label "family gate: $TEST $TAG seeds $FROM+$N" --owner class-v6-family-gate -- "$BIN" --ignored "$TEST" --nocapture --test-threads 1 >> "$LOG" 2>&1
|
||||
echo "end $(date -u +%FT%TZ) rc $?" >> "$LOG"
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue