From 1028c2579fff366100d1626615ff7c96cc8f8331 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Sun, 4 Oct 2026 21:46:41 +0000 Subject: [PATCH] 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-.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 --- app/igneum-app/src/config.rs | 9 ++- app/igneum-app/src/engine.rs | 42 ++++++++++- app/igneum-app/src/server.rs | 3 +- app/igneum-app/src/state.rs | 15 ++++ app/igneum-app/ui/app.js | 13 ++++ app/igneum-app/ui/index.html | 6 +- docs/design/miner-dev-fee.md | 71 ++++++++++++++++++ docs/fud-ledger.md | 10 +++ infra/cross/.gitignore | 2 + infra/cross/build-workers-linux.sh | 39 ++++++++++ packaging/hive/.gitignore | 2 + packaging/hive/README.md | 69 ++++++++++++++++++ packaging/hive/h-config.sh | 61 ++++++++++++++++ packaging/hive/h-manifest.conf | 6 ++ packaging/hive/h-run.sh | 86 ++++++++++++++++++++++ packaging/hive/h-stats.sh | 39 ++++++++++ packaging/hive/make-hive-package.sh | 54 ++++++++++++++ packaging/hive/selftest.sh | 64 ++++++++++++++++ proto-opencl/cl_dynamic.h | 16 +++- site/index.html | 2 +- site/litepaper.html | 3 +- tools/dev-fee/run.mjs | 109 ++++++++++++++++++++++++++++ 22 files changed, 710 insertions(+), 11 deletions(-) create mode 100644 docs/design/miner-dev-fee.md create mode 100755 infra/cross/build-workers-linux.sh create mode 100644 packaging/hive/.gitignore create mode 100644 packaging/hive/README.md create mode 100755 packaging/hive/h-config.sh create mode 100644 packaging/hive/h-manifest.conf create mode 100755 packaging/hive/h-run.sh create mode 100755 packaging/hive/h-stats.sh create mode 100755 packaging/hive/make-hive-package.sh create mode 100755 packaging/hive/selftest.sh create mode 100755 tools/dev-fee/run.mjs diff --git a/app/igneum-app/src/config.rs b/app/igneum-app/src/config.rs index 96631586..cc0dc0c0 100644 --- a/app/igneum-app/src/config.rs +++ b/app/igneum-app/src/config.rs @@ -55,6 +55,13 @@ pub struct Settings { /// Prove the shards assigned to this machine's keys (src/prover.rs). Default off until the GPU numbers exist. #[serde(default)] pub prove: bool, + /// The miner software's dev fee (1 block template in 100 to the dev address; decision of 4 October 2026). Default + /// on; off passes `--dev-fee 0` to igneum-miner. The protocol itself carries no fee. + #[serde(default = "yes")] + pub dev_fee: bool, + /// Lifetime dev-fee blocks this machine found (the miner's `dev-fee block` lines), carried across runs. + #[serde(default)] + pub fee_total: u64, } fn one() -> u32 { @@ -66,7 +73,7 @@ fn yes() -> bool { impl Default for Settings { fn default() -> Settings { - Settings { setup_done: false, address: String::new(), address_source: String::new(), key_saved: false, identities: 1, cards: HashMap::new(), display_name: String::new(), vote: true, paused: false, accepted_total: 0, auto_update: true, remote_jobs: true, prove: false } + Settings { setup_done: false, address: String::new(), address_source: String::new(), key_saved: false, identities: 1, cards: HashMap::new(), display_name: String::new(), vote: true, paused: false, accepted_total: 0, auto_update: true, remote_jobs: true, prove: false, dev_fee: true, fee_total: 0 } } } diff --git a/app/igneum-app/src/engine.rs b/app/igneum-app/src/engine.rs index fe4b8e20..9f54e48b 100644 --- a/app/igneum-app/src/engine.rs +++ b/app/igneum-app/src/engine.rs @@ -96,8 +96,10 @@ impl Shared { st.mining.state = "idle".into(); st.mining.paused = settings.paused; st.mining.accepted_total = settings.accepted_total; + st.mining.fee_total = settings.fee_total; st.address = address_state(&settings, &wallet_path); - st.settings = crate::state::SettingsState { identities: settings.identities, vote: settings.vote, start_at_login: crate::platform::start_at_login_is_on(), auto_update: settings.auto_update, remote_jobs: settings.remote_jobs, prove: settings.prove }; + st.settings = crate::state::SettingsState { identities: settings.identities, vote: settings.vote, start_at_login: crate::platform::start_at_login_is_on(), auto_update: settings.auto_update, remote_jobs: settings.remote_jobs, prove: settings.prove, dev_fee: settings.dev_fee }; + st.dev_fee = crate::state::DevFeeState { on: settings.dev_fee, percent: if settings.dev_fee { 1 } else { 0 }, address: String::new(), line: String::new() }; st.live_page = packaged.live_page.clone(); st.finality.message = "waiting for the miner".into(); st.clock.hint = crate::platform::clock_hint().into(); @@ -251,7 +253,7 @@ impl Shared { Ok(json!({ "ok": true })) } - pub fn apply_settings(&self, identities: Option, vote: Option, login: Option, address: Option<&str>, display_name: Option<&str>) -> Result { + pub fn apply_settings(&self, identities: Option, vote: Option, login: Option, address: Option<&str>, display_name: Option<&str>, dev_fee: Option) -> Result { let mut restart = Vec::new(); { let mut s = self.settings.lock().unwrap(); @@ -272,6 +274,12 @@ impl Shared { restart.push(if v { "voting on".into() } else { "voting off".into() }); } } + if let Some(v) = dev_fee { + if v != s.dev_fee { + s.dev_fee = v; + restart.push(if v { "dev fee on (1 block in 100)".into() } else { "dev fee off".into() }); + } + } if let Some(a) = address { let a = a.trim().to_ascii_lowercase(); if !a.is_empty() && a != s.address { @@ -289,6 +297,9 @@ impl Shared { let mut st = self.state.lock().unwrap(); st.settings.identities = s.identities; st.settings.vote = s.vote; + st.settings.dev_fee = s.dev_fee; + st.dev_fee.on = s.dev_fee; + st.dev_fee.percent = if s.dev_fee { 1 } else { 0 }; st.address = address_state(&s, &self.wallet_path); } if let Some(on) = login { @@ -992,6 +1003,11 @@ impl Engine { if !s.vote { a.push("--no-vote".into()); } + if !s.dev_fee { + // the miner software's dev fee (default 1% of templates); the switch in Settings + a.push("--dev-fee".into()); + a.push("0".into()); + } a.push("--payout-label".into()); a.push(slot.label.clone()); if r.network != "devnet" { @@ -2009,6 +2025,26 @@ impl Engine { } drop(st); self.shared.event("block", &format!("block accepted by the node ({card_name})")); + } else if text.contains(" dev-fee block ") { + // the software dev fee: one template in 100 paid the dev address (igneum-miner, "Software dev fee") + let total = { + let mut s = self.shared.settings.lock().unwrap(); + s.fee_total += 1; + s.fee_total + }; + let mut st = self.st(); + st.mining.fee_session += 1; + st.mining.fee_total = total; + drop(st); + self.shared.event("block", &format!("that block was the dev fee's (1 in 100, {card_name})")); + } else if let Some(rest) = text.split(" dev fee ").nth(1) { + // the miner's start line: "dev fee 1% (1 block in 100) to 0x...; --dev-fee 0 turns it off", or + // "dev fee off (--dev-fee 0); the default is 1% (1 block in 100) to 0x...", or "dev fee off: " + let on = !rest.starts_with("off"); + let percent = if on { rest.split('%').next().and_then(|p| p.trim().parse::().ok()).unwrap_or(1) } else { 0 }; + let address = rest.find("0x").map(|k| rest[k..].chars().take(42).collect::()).filter(|a| a.len() == 42 && keys::valid_address(a)).unwrap_or_default(); + let mut st = self.st(); + st.dev_fee = crate::state::DevFeeState { on, percent, address, line: rest.to_string() }; } else if text.contains(" STATUS '") { self.miners[i].last_status = Some(Instant::now()); let avg = kv_f64(text, "hash").unwrap_or(0.0); @@ -2107,7 +2143,7 @@ impl Engine { let miner_txt = if st.mining.cards.is_empty() { "no GPU miner".to_string() } else { - format!("accepted {} blocks ({} this run), {:.2} MH/s, {}", st.mining.accepted_total, st.mining.accepted_session, st.mining.hash_total, st.mining.state) + format!("accepted {} blocks ({} this run, dev fee {}), {:.2} MH/s, {}", st.mining.accepted_total, st.mining.accepted_session, st.mining.fee_session, st.mining.hash_total, st.mining.state) }; let up = fmt_uptime(self.shared.started.elapsed().as_secs()); drop(st); diff --git a/app/igneum-app/src/server.rs b/app/igneum-app/src/server.rs index ddd271e9..ab32b693 100644 --- a/app/igneum-app/src/server.rs +++ b/app/igneum-app/src/server.rs @@ -257,7 +257,8 @@ fn api_post(shared: &Arc, path: &str, body: Value) -> Result shared.set_prove(body.get("on").and_then(|v| v.as_bool()).unwrap_or(false)), "/api/prove/setup" => crate::prover::setup(shared), diff --git a/app/igneum-app/src/state.rs b/app/igneum-app/src/state.rs index b5b25b55..d3739d17 100644 --- a/app/igneum-app/src/state.rs +++ b/app/igneum-app/src/state.rs @@ -82,6 +82,18 @@ pub struct MiningState { pub accepted_session: u64, pub rejected_session: u64, pub found: Vec, // unix times of the accepted blocks, last hour + /// dev-fee blocks (the miner's `dev-fee block` lines): this run, and lifetime + pub fee_session: u64, + pub fee_total: u64, +} + +/// The miner software's dev fee as the miner reports it at start (`dev fee 1% (1 block in 100) to 0x...`). +#[derive(Clone, Serialize, Default)] +pub struct DevFeeState { + pub on: bool, + pub percent: u32, + pub address: String, // 0x + 40 hex once a miner has printed it + pub line: String, // the miner's own words } #[derive(Clone, Serialize, Default)] @@ -158,6 +170,8 @@ pub struct SettingsState { pub remote_jobs: bool, /// the prover service (src/prover.rs) pub prove: bool, + /// the miner software's dev fee switch (settings; `--dev-fee 0` when off) + pub dev_fee: bool, } /// One remote job this machine ran (the ledger entry), for the Settings history and the last-job strip. @@ -267,6 +281,7 @@ pub struct State { pub clock: ClockState, pub address: AddressState, pub settings: SettingsState, + pub dev_fee: DevFeeState, pub update: UpdateState, pub jobs: JobsState, pub events: Vec, diff --git a/app/igneum-app/ui/app.js b/app/igneum-app/ui/app.js index 9c512dc4..b42da44e 100644 --- a/app/igneum-app/ui/app.js +++ b/app/igneum-app/ui/app.js @@ -110,6 +110,8 @@ $('s-name').value = state.display_name || ''; $('s-mid').textContent = (state.machine_id || '').slice(0, 8); $('s-vote').checked = state.settings.vote; + $('s-devfee').checked = state.settings.dev_fee; + $('s-devfee-text').textContent = devFeeText(state); $('s-login').checked = state.settings.start_at_login; $('s-version').textContent = 'Igneum Miner ' + state.version; $('s-node-dir').textContent = 'chain data: ' + state.node_dir; @@ -138,6 +140,15 @@ api('api/cards', { cards: choices }).then(function (r) { if (r.ok) toast('Applied; only the changed workers restart'); }); }); $('s-vote').addEventListener('change', function () { api('api/settings', { vote: this.checked }).then(function (r) { if (r.ok) toast(r.restart ? 'Applied; the miner restarts' : 'Applied'); }); }); + $('s-devfee').addEventListener('change', function () { api('api/settings', { dev_fee: this.checked }).then(function (r) { if (r.ok) toast(r.restart ? 'Applied; the miner restarts' : 'Applied'); }); }); + // the miner's own start line, next to the rewards address: "dev fee 1% (1 block in 100) to 0x...; --dev-fee 0 turns it off" + function devFeeText(s) { + var d = s.dev_fee || {}; + var addr = d.address ? ' to ' + d.address.slice(0, 8) + '\u2026' + d.address.slice(-4) : ''; + if (!s.settings.dev_fee) return 'Dev fee off (--dev-fee 0); the default is 1% (1 block in 100)' + addr; + if (d.line && !d.on && !d.address) return 'Dev fee: ' + d.line; + return 'Dev fee ' + (d.percent || 1) + '% (1 block in 100)' + addr + (d.address ? '' : '; the miner names the address when it starts'); + } $('s-login').addEventListener('change', function () { var on = this.checked; api('api/settings', { start_at_login: on }).then(function (r) { if (!r.ok) { toast(r.error || 'could not change'); $('s-login').checked = !on; } }); }); $('s-update').addEventListener('click', function () { api('api/update/check', {}); $('s-update-note').textContent = 'Checking.'; setTimeout(fillSettings, 4000); }); $('s-install').addEventListener('click', function () { api('api/update/install', {}); toast('Installing at once'); }); @@ -333,6 +344,8 @@ var nowU = s.now; $('d-found-10').textContent = m.found.filter(function (t) { return nowU - t <= 600; }).length; $('d-found-60').textContent = m.found.length; + $('d-fee').textContent = withCommas(m.fee_total || 0); + $('d-fee').title = (m.fee_session || 0) + ' this run; 1 block in 100 pays the miner software\'s dev fee'; $('d-chain-blocks').textContent = compact(n.blocks); // per-card tiles (enabled cards), the rest in one muted line var on = m.cards.filter(function (c) { return c.enabled; }), off = m.cards.filter(function (c) { return !c.enabled; }); diff --git a/app/igneum-app/ui/index.html b/app/igneum-app/ui/index.html index f68a688c..e6520a38 100644 --- a/app/igneum-app/ui/index.html +++ b/app/igneum-app/ui/index.html @@ -104,7 +104,7 @@
step 2 of 2

Where should rewards go?

-

Every block this machine finds pays one EVM address. Pick one way.

+

Every block this machine finds pays one EVM address. Pick one way. One block in 100 pays the miner software's dev fee; Settings turns it off.

diff --git a/site/litepaper.html b/site/litepaper.html index f92145b5..a2bbb785 100644 --- a/site/litepaper.html +++ b/site/litepaper.html @@ -419,7 +419,8 @@ body.all .pager{display:none}

Hardware

The dataset starts at 2 GB and grows by half a gigabyte a year, so a 4 GB card mines for about four years and an 8 GB card for more than a decade, approximate. 12 GB or more proves full shards. NVIDIA and AMD both work, because the mining program is generated for the architecture both share and the proof system is hash-based. Apple's chips are GPUs with unified memory, so Macs mine too. There is no CPU mining lane, on purpose, because CPU mining is what botnets farm. Nodes, wallets and exchanges need no GPU at all.

What a miner's hour looks like

-

The card hashes the lottery continuously. When the client sees a shard or an external job it can win, it switches the card to proving for a few seconds, posts the proof, and goes back to hashing. The client does the switching and the miner sees one balance. The official client is open source and charges a 1% dev fee, like every miner you already run, and any other client is welcome.

+

The card hashes the lottery continuously. When the client sees a shard or an external job it can win, it switches the card to proving for a few seconds, posts the proof, and goes back to hashing. The client does the switching and the miner sees one balance.

+

The protocol carries no fee: no dev fund, no cut to any team. The Igneum Miner software takes an optional 1% dev fee, the way other GPU miners do. One block template in 100 is requested with the dev address instead of yours, by a counter, not a random draw, so it is exactly 1 in 100 and anyone can check it from the source or from the chain. One flag turns it off (--dev-fee 0, a switch in the app, a line in the HiveOS config). The miner prints the fee and the address when it starts. Any other client is welcome.

One click, for everyone else

Farm operators get HiveOS support on day one. Everyone else gets the Igneum app: install it on Windows, macOS or Linux, press one button, and the card is mining and proving to a wallet the app made for you, with earnings shown in IGN and in your currency, and mining paused while you game. It is the same client with a face on it. The app shows you the seed phrase and has you confirm it before mining starts, offers a hardware wallet for your earnings, updates only when you accept a release signed by the key published in the genesis block, and is downloaded only from this domain or the repository with its hash shown beside the button. Nobody from Igneum will ever ask for your seed. Mining never runs in a browser, because browser compute is slow and browser mining has meant malware since Coinhive. The browser is for the dashboard, and for verifying the chain.

Fair launch, announced

diff --git a/tools/dev-fee/run.mjs b/tools/dev-fee/run.mjs new file mode 100755 index 00000000..10c51db7 --- /dev/null +++ b/tools/dev-fee/run.mjs @@ -0,0 +1,109 @@ +#!/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();