#!/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= IGNEUM_MINER= (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();