igneum/tools/dev-fee/run.mjs
igneum-labs e80e8f870e Dev fee: the test-network measurement in the bench log; run.mjs waits for the nodes and edits the override as text
9 fee blocks in the 785 blocks of the two fee-paying miners (1.15%, expected 1 in 100 templates), the miners' fee
counters equal the chain's count on both nodes, the control miner at --dev-fee 0 paid nothing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 23:15:27 +00:00

119 lines
8 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`;
// edited as text: JSON.parse would turn the u64 "never" sentinels into floats the node refuses
const overrideText = readFileSync(FILE, 'utf8')
.replace(/"skip_proof_of_work":\s*(true|false)/, '"skip_proof_of_work": true') // the node accepts any nonce; the stub miner still checks its own kHeavyHash
.replace(/"genesis_bits":\s*\d+/, `"genesis_bits": ${0x207fffff}`) // target at the floor: the first nonce of every template passes that check
.replace(/,\s*"proving_v0_activation_daa":\s*\d+/, ''); // a proving-branch field the devnet-v4 node does not know
if (!/"skip_proof_of_work": true/.test(overrideText) || !/"genesis_bits": 545259519/.test(overrideText)) throw new Error('override edit failed');
writeFileSync(override, overrideText);
// 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);
this.proc.on('exit', (c) => { if (!stopping) { console.error(`node ${this.i} exited with ${c}; see ${this.logFile}`); stopAll().then(() => process.exit(1)); } });
for (let k = 0; k < 60; k++) { // until the node answers RPC
if (spawnSync(MINER, ['watch', '1', this.grpc], { encoding: 'utf8', timeout: 15000 }).status === 0) break;
await sleep(1000);
}
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;
}
let stopping = false;
async function stopAll() {
stopping = true;
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', (c) => { if (c !== 0 && !stopping) { console.error(`a miner exited with ${c} after ${((Date.now() - t0) / 1000).toFixed(0)} s; see ${TMP}/miner-*.log`); } if (--left === 0) r(); }); });
if ((Date.now() - t0) / 1000 < SECS * 0.9) { console.error('the miners stopped early; no measurement'); await stopAll(); process.exit(1); }
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();