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>
109 lines
6.9 KiB
JavaScript
Executable file
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();
|