igneum/tools/dev-fee/run.mjs
igneum-josh 6b4ec59648 Miner dev fee: app switch and UI line, HiveOS package, Linux worker cross-build, docs and public text
Decision of 4 October 2026 (evening): the Igneum Miner software takes a visible, switchable 1% dev fee, the norm
for GPU miners; the protocol stays fee-free. The miner side (--dev-fee, the 1-in-100 template counter, the audit
command) is on branch dev-fee of the node fork.

- app: settings.dev_fee (default on) passes --dev-fee 0 to igneum-miner when off; Settings shows the miner's own
  "dev fee 1% (1 block in 100) to 0x..." line next to the rewards address with a switch; the engine parses the
  miner's start line and its dev-fee block lines (fee_session, lifetime fee_total, an event per fee block)
- packaging/hive: h-manifest.conf, h-config.sh, h-run.sh, h-stats.sh, make-hive-package.sh (igneum-hive-<v>.tar.gz
  with the Linux igneumd, igneum-miner and both GPU workers), README with the Flight Sheet, selftest.sh (bash -n,
  stub binaries, the three hooks the way Hive runs them, the stats JSON parsed). Hive itself is untested
- infra/cross/build-workers-linux.sh: the NVRTC and OpenCL workers cross-compiled for Linux with zig;
  proto-opencl/cl_dynamic.h gains the Linux dlopen branch (libOpenCL.so.1)
- tools/dev-fee/run.mjs: the fee-block test network (two nodes on 29900+, three CPU miners, payouts audit)
- docs/design/miner-dev-fee.md (mechanism, flag, lines, the DEV_FEE_ADDRESS placeholder and the devnet address),
  docs/fud-ledger.md E18 and the E5/L9 status line, litepaper "What a miner's hour looks like" paragraph, homepage
  miner section note

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 22:46:41 +01:00

109 lines
6.9 KiB
JavaScript
Executable file

#!/usr/bin/env node
// The software dev fee measured on a private test network (4 October 2026): two nodes on 127.0.0.1 ports 29900 and
// up (network id igneum-devnet-957, data under /tmp/igneum-dev-fee), the fast-time profile with proof of work skipped
// and the genesis difficulty at the floor, so a CPU stub miner solves every template it holds. Three CPU miners pace
// themselves with --hold-ms: two at the default --dev-fee 1 and one control at --dev-fee 0. At the end the chain is
// read back with `igneum-miner payouts` on the SECOND node (what propagated, not what the miner's node accepted) and
// the dev address's block count is set against the miners' own fee= counters and the 1-in-100 expectation.
// Never touches the live devnet (26610/26611), the fast-time simnet (29500+) or the proving-v0 network (29800+).
//
// node tools/dev-fee/run.mjs [--secs 600] [--hold-ms 500]
// IGNEUMD=<igneumd of the devnet-v4 line> IGNEUM_MINER=<igneum-miner with --dev-fee> (defaults below)
import { spawn, spawnSync } from 'node:child_process';
import { mkdirSync, rmSync, writeFileSync, readFileSync, openSync, existsSync } from 'node:fs';
const ROOT = new URL('../../', import.meta.url).pathname;
const FILE = `${ROOT}infra/fast-time/override-60x.json`;
const IGNEUMD = process.env.IGNEUMD || `${ROOT}vendor/igneum-node-v4/target/release/igneumd`;
const MINER = process.env.IGNEUM_MINER || `${ROOT}vendor/igneum-node-devfee/target/release/igneum-miner`;
const TMP = '/tmp/igneum-dev-fee';
const BASE = 29900, SUFFIX = 957;
const args = process.argv.slice(2);
const flag = (name, dflt) => { const i = args.indexOf(name); return i >= 0 && args[i + 1] !== undefined ? +args[i + 1] : dflt; };
const SECS = flag('--secs', 600);
const HOLD = flag('--hold-ms', 500);
const started = [];
const log = (...a) => console.log(new Date().toISOString().slice(11, 23), ...a);
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
for (const b of [IGNEUMD, MINER]) if (!existsSync(b)) { console.error(`missing ${b}`); process.exit(2); }
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
const override = `${TMP}/override.json`;
const params = JSON.parse(readFileSync(FILE, 'utf8'));
params.skip_proof_of_work = true; // the node accepts any nonce; the stub miner still checks its own kHeavyHash
params.genesis_bits = 0x207fffff; // target at the floor: the first nonce of every template passes the miner's check
writeFileSync(override, JSON.stringify(params));
// the user addresses: fixed test values, no keys behind them (the fee address is the miner's DEV_FEE_ADDRESS_DEVNET)
const USER_A = '0x' + 'a1'.repeat(20), USER_B = '0x' + 'b2'.repeat(20), USER_C = '0x' + 'c3'.repeat(20);
class Node {
constructor(i, connect = []) {
this.i = i; this.grpcPort = BASE + i * 10; this.p2pPort = BASE + i * 10 + 1; this.evmPort = BASE + i * 10 + 2;
this.connect = connect; this.dir = `${TMP}/n${i}`; this.logFile = `${this.dir}/node.log`;
}
get grpc() { return `grpc://127.0.0.1:${this.grpcPort}`; }
async start() {
mkdirSync(this.dir, { recursive: true });
const a = ['--devnet', `--devnet-suffix=${SUFFIX}`, '--nodnsseed', '--disable-upnp', '--nologfiles', '--enable-unsynced-mining', '--utxoindex', '--unsaferpc',
`--appdir=${this.dir}`, `--rpclisten=127.0.0.1:${this.grpcPort}`, `--evm-rpclisten=127.0.0.1:${this.evmPort}`,
`--listen=127.0.0.1:${this.p2pPort}`, `--override-params-file=${override}`, '--loglevel=info', '--yes'];
if (this.connect.length) a.push(`--connect=${this.connect.join(',')}`); else a.push('--outpeers=0');
const out = openSync(this.logFile, 'a');
this.proc = spawn(IGNEUMD, a, { stdio: ['ignore', out, out] });
started.push(this.proc);
await sleep(1500);
log(`n${this.i} up pid ${this.proc.pid} grpc ${this.grpcPort} p2p ${this.p2pPort}`);
return this;
}
}
function miner(argv, name) {
const out = openSync(`${TMP}/${name}.log`, 'a');
const p = spawn(MINER, argv, { stdio: ['ignore', out, out] });
started.push(p);
return p;
}
async function stopAll() {
for (const p of started.slice().reverse()) { try { p.kill('SIGINT'); } catch { } }
await sleep(1500);
for (const p of started) { try { p.kill('SIGKILL'); } catch { } }
}
process.on('SIGINT', async () => { await stopAll(); process.exit(130); });
const tailOf = (f, re) => { try { return readFileSync(f, 'utf8').split('\n').filter(l => re.test(l)); } catch { return []; } };
const t0 = Date.now();
const n0 = await new Node(0).start();
const n1 = await new Node(1, [`127.0.0.1:${n0.p2pPort}`]).start();
await sleep(2000);
log(`miners: a and b at --dev-fee 1 (default), c at --dev-fee 0, --engine stub 1 thread --hold-ms ${HOLD}, ${SECS} s`);
const common = (label, evm) => ['mine', n0.grpc, '1', String(SECS), label, '--engine', 'stub', '--hold-ms', String(HOLD), '--no-vote', '--status-secs', '30', '--payout-label', label, '--evm-address', evm];
const pa = miner([...common('devfee-a', USER_A)], 'miner-a');
const pb = miner([...common('devfee-b', USER_B), '--dev-fee', '1'], 'miner-b');
const pc = miner([...common('devfee-c', USER_C), '--dev-fee', '0'], 'miner-c');
await sleep(3000);
for (const n of ['a', 'b', 'c']) log(`miner-${n}: ${tailOf(`${TMP}/miner-${n}.log`, / dev fee /).map(l => l.replace(/^\S+ /, ''))[0] || '(no dev fee line yet)'}`);
await new Promise(r => { let left = 3; for (const p of [pa, pb, pc]) p.on('exit', () => { if (--left === 0) r(); }); });
log(`miners done after ${((Date.now() - t0) / 1000).toFixed(0)} s`);
await sleep(3000);
const summary = {};
for (const n of ['a', 'b', 'c']) {
const s = tailOf(`${TMP}/miner-${n}.log`, /^MINER SUMMARY/)[0] || '';
const kv = (k) => +(s.match(new RegExp(` ${k}=(\\d+)`)) || [0, 0])[1];
const feeLines = tailOf(`${TMP}/miner-${n}.log`, / dev-fee block /).length;
summary[n] = { found: kv('found'), rejected: kv('rejected'), fee: kv('fee'), fee_lines: feeLines };
log(`miner-${n}: found=${summary[n].found} rejected=${summary[n].rejected} fee=${summary[n].fee} (dev-fee block lines: ${feeLines})`);
}
for (const [name, n] of [['node 0 (the miners\' node)', n0], ['node 1 (peer)', n1]]) {
const r = spawnSync(MINER, ['payouts', n.grpc], { encoding: 'utf8', timeout: 120000 });
log(`payouts on ${name}:\n${(r.stdout || '').trim()}${r.stderr ? '\n' + r.stderr.trim() : ''}`);
writeFileSync(`${TMP}/payouts-n${n.i}.txt`, r.stdout || '');
}
const payouts = readFileSync(`${TMP}/payouts-n1.txt`, 'utf8');
const total = +(payouts.match(/blocks=(\d+)/) || [0, 0])[1];
const dev = payouts.split('\n').find(l => /\(dev fee\)/.test(l)) || '';
const devBlocks = +(dev.match(/blocks=(\d+)/) || [0, 0])[1];
const feePaying = summary.a.found + summary.b.found;
log(`RESULT total_blocks_on_node1=${total} dev_fee_blocks=${devBlocks} miners_fee_counters=${summary.a.fee + summary.b.fee} (control c: ${summary.c.fee}) share_of_fee_paying_miners_blocks=${feePaying ? (100 * devBlocks / feePaying).toFixed(3) : 'n/a'}% expected=1.000%`);
await stopAll();