igneum/tools/observer/hash-origin.test.mjs
igneum-labs 00a8d244b7 Launch pack tools: per-tier income table from the bench rows and TESTNET_1, the daily hash-origin report, the launch-gates check (mission item 10)
tools/launch/income-tiers.mjs renders docs/analysis/income-tiers.md (public) from tools/launch/income-tiers.json: eleven measured cards (the 6 October rented-card rows, the 9070 XT telemetry run, the M5 Max Metal bench, each with its source), IGN a day at 1, 10 and 100 GH/s after the ramp on EmissionSchedule::TESTNET_1 (100 IGN a block, 90-day ramp from 10 percent, monthly 2^(-1/24), the 80 percent producer share), electricity a day and per mined IGN at USD 0.005, 0.02 and 0.10 a kWh, a rig and a pool-user line, the consequences per tier, the owed rows; --check fails CI when the page and its inputs disagree; the test pins the arithmetic (6,912 IGN a day for 100 MH/s on 100 GH/s at the launch rate, day 1 at 10 percent, one step at 2^(-1/24)).

tools/observer/hash-origin.mjs: once a day from the observer's tables, who found the blocks: keys with a block and above dust, attested pools against shared payout addresses (a multi-key machine is not a pool until its operator attests), the project fleet's share from the intake identity lines plus the fleet registry's key file, the ten largest keys, the 2x step since yesterday, the community gates (first 100 keys, first outside block, first attested outside pool at 10 percent for 7 days; the X5 count stays an upper bound until the two observer columns exist). --dry reads only; --write keeps hash_origin_days and hash_origin_reports and live_state.hash_origin; --post goes through the Discord poster's guard. Fixture, a known-finished and a known-failed day in the test. Ran read-only on the live devnet today: 113 keys, 0.76 GH/s, the fleet share wrong until the key file exists.

tools/ci/launch-gates-check.mjs (in pre-push.sh with the two test lines): every row of the Launch gates table in testnet-go.md has a check and every backticked path exists; the site-lane handoff and the income table carry no served-page or export pattern and no em dash; the eight regulatory sentences are present in order under the label. Self-test fires on each.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 08:53:12 +00:00

72 lines
5 KiB
JavaScript

// node --test tools/observer/hash-origin.test.mjs
// One known-finished day (the fixture: every share adds up, the outside block and the two pools are found, the step
// fires at 2.5x) and one known-failed day (an empty window passes no gate; a key with no blue block is not a key; a
// day without yesterday has no ratio), per the watcher rule of CLAUDE.md (4 October 2026).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { compute, renderText, DUST_BLOCKS_30D, OUTSIDE_POOL_DAYS } from './hash-origin.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const fixture = () => JSON.parse(readFileSync(join(HERE, 'fixtures', 'hash-origin-day.json'), 'utf8'));
const close = (a, b, eps = 1e-9) => assert.ok(Math.abs(a - b) < eps, `${a} ~ ${b}`);
test('the known-finished day: counts, shares, pools, fleet, the step and the gates', () => {
const r = compute(fixture());
assert.equal(r.blue_blocks, 80960);
assert.equal(r.keys.with_block, 10, 'the 0-blue key is not a key with a block');
assert.equal(r.keys.new_today, 7, 'three keys mined yesterday; seven are new');
// above dust over 30 days: a (58,000), b (23,000), c, d, e, f, 1, 2, 3 (900) = 9; key 4 has 90 < 100; key 5 none
assert.equal(r.keys.above_dust_30d, 9);
assert.equal(r.pools.attested, 2); assert.equal(r.pools.shared_payouts, 2);
const project = r.pools.rows.find(p => p.kind === 'project'), outside = r.pools.rows.find(p => p.kind === 'attested');
assert.equal(project.keys, 2); assert.equal(project.blue_blocks, 15000);
assert.equal(outside.keys, 2); assert.equal(outside.blue_blocks, 15000);
close(r.pools.attested_share, 30000 / 80960); close(r.pools.shared_share, 30000 / 80960);
assert.equal(r.fleet.keys, 3); assert.equal(r.fleet.blue_blocks, 51000); close(r.fleet.share, 51000 / 80960);
assert.equal(r.outside.keys, 7); assert.equal(r.outside.blue_blocks, 29960);
close(r.fleet.share + r.outside.share, 1);
close(r.keys.top10_share, 1, 1e-9);
close(r.network.ratio, 2.5); assert.equal(r.network.step_flag, true);
assert.equal(r.gates.first_100_keys, false);
assert.equal(r.gates.first_block_outside_fleet, true);
assert.equal(r.gates.outside_pool_today, true, 'the outside pool holds 18.5 percent today');
assert.equal(r.gates.outside_pool_days, 2, 'today plus yesterday; the day before breaks the streak');
assert.equal(r.gates.first_outside_pool, false);
assert.equal(r.gates.first_1000_independent, false);
assert.equal(r.day_of_go, 2);
const t = renderText(r);
assert.ok(t.includes('A STEP'), 'the step is said');
assert.ok(t.includes(`2 of ${OUTSIDE_POOL_DAYS} days`));
assert.ok(!/[0-9a-f]{32}/i.test(t), 'no 32-hex token in the posted text (discord-hooks guard)');
assert.ok(!/\b\d{1,3}(\.\d{1,3}){3}\b/.test(t), 'no IP address in the posted text');
});
test('the known-failed day: an empty window passes nothing and claims nothing', () => {
const r = compute({ date: '2026-10-21', blocks: [{ vote_key_hash: 'a'.repeat(64), evm_miner: 'x', blue: 0, total: 5 }], fleetKeys: ['a'.repeat(64)] });
assert.equal(r.blue_blocks, 0); assert.equal(r.keys.with_block, 0); assert.equal(r.keys.above_dust_30d, 0);
assert.equal(r.pools.attested, 0); assert.equal(r.pools.shared_payouts, 0); assert.equal(r.fleet.share, null); assert.equal(r.outside.share, null);
assert.equal(r.network.hps, null); assert.equal(r.network.ratio, null); assert.equal(r.network.step_flag, false);
for (const g of ['first_100_keys', 'first_block_outside_fleet', 'outside_pool_today', 'first_outside_pool', 'first_1000_independent']) assert.equal(r.gates[g], false, g);
assert.equal(r.day_of_go, null);
assert.ok(renderText(r).includes('not yet'));
});
test('the first-100 gate and the 7-day outside pool pass only on their numbers', () => {
const blocks = []; for (let i = 0; i < 100; i++) blocks.push({ vote_key_hash: i.toString(16).padStart(64, '0'), evm_miner: 'igna-outside-pool', blue: 10, total: 10 });
const prev = []; for (let i = 0; i < OUTSIDE_POOL_DAYS - 1; i++) prev.push({ gates: { outside_pool_today: true }, network: { hps: 1e9 } });
const unattested = compute({ date: '2026-11-01', blocks, networkHps: 1e9, prevReports: prev });
assert.equal(unattested.gates.first_100_keys, true);
assert.equal(unattested.gates.outside_pool_today, false, 'a shared payout nobody attested is not a pool');
assert.equal(unattested.pools.shared_payouts, 1); assert.equal(unattested.pools.attested, 0);
const r = compute({ date: '2026-11-01', blocks, networkHps: 1e9, prevReports: prev, knownPools: ['igna-outside-pool'] });
assert.equal(r.gates.first_100_keys, true);
assert.equal(r.gates.first_outside_pool, true);
assert.equal(r.network.step_flag, false, '1.0x is not a step');
const r99 = compute({ date: '2026-11-01', blocks: blocks.slice(1), prevReports: prev.slice(1), knownPools: ['igna-outside-pool'] });
assert.equal(r99.gates.first_100_keys, false);
assert.equal(r99.gates.first_outside_pool, false);
assert.ok(DUST_BLOCKS_30D === 100);
});