diff --git a/docs/bench-log.md b/docs/bench-log.md index 7a70cb43..81775f25 100644 --- a/docs/bench-log.md +++ b/docs/bench-log.md @@ -1051,3 +1051,43 @@ moving (150.8M at the height, then stepping down as PC 2's card paused for a pro the chain; a consensus rule changed under a running network with miners on three platforms. The first measurement of v2 on the devnet's own regime (two large miners, bursty parallel blocks) needs PC 2 back from its job; the cloud numbers stand meanwhile (settle 157 to 272 s, no swing). + +## 4 October 2026 (evening), the software dev fee measured on a test network: 9 fee blocks in 785, the chain and the miners' counters agree + +Decision of the evening: the Igneum Miner software takes a visible, switchable 1% dev fee (one block template in 100 +requested with the dev payout address, chosen by a template counter, never at random); the protocol carries no fee. +Branch `dev-fee` of both repositories; `docs/design/miner-dev-fee.md`; FUD ledger E18. + +Machine: Apple M5 Max, macOS 26.6.2, load 140 to 170 (many other agents' builds and runs alongside; only counts were +taken, no rates). Network: `node tools/dev-fee/run.mjs --secs 600 --hold-ms 500`, two devnet-v4 nodes (`igneumd` +3bfe346f) on 127.0.0.1 ports 29900 and 29910, network id igneum-devnet-957, the fast-time profile with proof of work +skipped and `genesis_bits` at the floor so a CPU stub miner solves every template it holds. Three `igneum-miner` +processes (dev-fee branch a2c7ba83, `--engine stub`, 1 thread, exponential hold with mean 500 ms, no voting), each +paying its own test address: a and b at the default `--dev-fee 1`, c the control at `--dev-fee 0`. 600 s. Then +`igneum-miner payouts` on both nodes: every block the node holds, counted by the `IGNA` payout address in its coinbase. + +What the miners printed at start, verbatim: + + dev fee 1% (1 block in 100) to 0xdfaea67368f3e3753397d878f97efe6aa8020c2e; --dev-fee 0 turns it off (a, b) + dev fee off (--dev-fee 0); the default is 1% (1 block in 100) to 0xdfaea67368f3e3753397d878f97efe6aa8020c2e (c) + +| Miner | `--dev-fee` | Blocks found (its summary) | Rejected | `fee=` in its summary | `dev-fee block` lines | Blocks to its own address on the chain | +|---|---|---|---|---|---|---| +| a | 1 (default) | 385 | 0 | 5 | 5 | 380 | +| b | 1 | 400 | 0 | 4 | 4 | 396 | +| c | 0 (control) | 397 | 0 | 0 | 0 | 397 | + +The chain (`payouts`, identical on node 0, the miners' node, and node 1, its peer): 1,183 blocks; 380 + 396 + 397 to +the three user addresses, 9 to the dev-fee address `0xdfaea6...0c2e` (0.761% of all blocks, 1.146% of the 785 blocks +of the two fee-paying miners), and 1 block with no `IGNA` field, the genesis block. Found per miner = own blocks + fee +blocks exactly (380 + 5, 396 + 4, 397 + 0), so every fee block the miners logged is on the chain and no other block +went to the dev address. The control miner paid nothing. + +Reading. The expectation is 1 in 100 templates; the stub miner found a block on most but not every template once the +difficulty had risen on three miners at about 2 blocks/s against the 1 block/s target (a and b held about 500 templates +each, 5 and 4 of them fee templates at positions 99, 199, ..., and converted 77 to 80% of templates into blocks), so +the block share lands near 1% with the sampling noise of 9 events (1.15% here). The exactness claim is on the template +side and is the unit test (`dev_fee_tests`: 100 of 10,000 at positions 99, 199, ...; 0 at `--dev-fee 0`; exact at 2 to +100%); the chain test shows the fee blocks reach the chain and are counted the same by the miner and by both nodes. +Not measured: worker (GPU) mode, where the fee template is one of the templates the background fetcher rotates through +once a second per identity, so the share is 1 in 100 templates by time, not by job; and nothing on Windows. diff --git a/tools/dev-fee/run.mjs b/tools/dev-fee/run.mjs index 10c51db7..fba14a6f 100755 --- a/tools/dev-fee/run.mjs +++ b/tools/dev-fee/run.mjs @@ -30,10 +30,13 @@ for (const b of [IGNEUMD, MINER]) if (!existsSync(b)) { console.error(`missing $ 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)); +// 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); @@ -53,7 +56,11 @@ class Node { const out = openSync(this.logFile, 'a'); this.proc = spawn(IGNEUMD, a, { stdio: ['ignore', out, out] }); started.push(this.proc); - await sleep(1500); + 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; } @@ -64,7 +71,9 @@ function miner(argv, name) { 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 { } } @@ -83,7 +92,8 @@ 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(); }); }); +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);