From 31cec57eb64fe5ba5704f05a67081c1d0c5d1aa1 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:48:24 +0000 Subject: [PATCH] Open pool on the renamed coinbase tags IGNH and IGNW (the fork's pool-tags-node 7455b8d5): the read tool, the chain's tests, README, pool.md and spec 09 follow Co-Authored-By: Claude Fable 5.1 --- docs/plans/pool.md | 8 ++++---- docs/spec/09-pool-protocol.md | 6 +++--- pool/README.md | 2 +- pool/src/sidechain.rs | 4 ++-- pool/tools/dn3-split-read.mjs | 16 ++++++++-------- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/plans/pool.md b/docs/plans/pool.md index 2fd58eef7..efae3b7f4 100644 --- a/docs/plans/pool.md +++ b/docs/plans/pool.md @@ -375,8 +375,8 @@ refused on a second, a wrong pin never handshakes). Testnet and mainnet refuse t The design is spec 09 section 9.12 (written with the code). In one paragraph: the member runs its own node and `igneum-pool --open` beside it; the daemon holds no key, builds the member's templates from the member's node with the -member's key in the header and the member's own address in the coinbase, stamps the share chain's parent (`IGNS`) and -the window's split (`IGNP`) into every coinbase, re-stamps without a node call when the chain tip moves +member's key in the header and the member's own address in the coinbase, stamps the share chain's parent (`IGNH`) and +the window's split (`IGNW`) into every coinbase, re-stamps without a node call when the chain tip moves (`restamp_extra_data`: the coinbase payload rewritten and the merkle root re-derived as the body check derives it), and gossips shares with the other members' daemons. A share is a template at the chain's target; a block is a share at the block target; the executor pays a blue block's producer share by its split from `pool_split_activation_daa` on @@ -392,7 +392,7 @@ dozen hashes of work rounded the dev entry to a third of its share: the unit tes | File | What | |---|---| -| `consensus/core/src/evm.rs` (fork) | `IGNS` and `IGNP` encode and parse, `split_producer`, the switch `install_pool_split_activation` | +| `consensus/core/src/evm.rs` (fork) | `IGNH` and `IGNW` encode and parse, `split_producer`, the switch `install_pool_split_activation` | | `consensus/core/src/config/params.rs` (fork) | `pool_split_activation_daa`: never by default, in the digest once set, the fees' pattern; the override test | | `igneum/exec/src/executor.rs`, `service.rs` (fork) | `SegmentBlock.split`; the reward loop pays by the split at or above the switch; the test with an odd subsidy (the dust) | | `kaspad/src/daemon.rs` (fork) | installs the switch from the params beside the fees | @@ -478,7 +478,7 @@ So the window holds no pool-0 share numbers; its rerun waits for both fixes on t ### 10.5b The Devnet 3 pair (7 October 2026, evening): two rules from the first live open pool the project lead's ruling through main (17:3xZ): the pool split goes on on Devnet 3 by activation height once one open pool of two -or more members has mined Devnet 3 for an hour with the switch at never (IGNS and IGNP carried, exec roots agreeing); +or more members has mined Devnet 3 for an hour with the switch at never (IGNH and IGNW carried, exec roots agreeing); the height is the hour's last block's DAA + 7,200, compiled by the node lane. The fleet lane's pair (dn3-pool-a, dn3-pool-b: 3070s on separate hosts, 0.3.22 nodes 69d1b56e, the paired miner, `igneum-pool --open` beside each) found two faults in the daemon before the first share, both of this lane: diff --git a/docs/spec/09-pool-protocol.md b/docs/spec/09-pool-protocol.md index cb151c7f6..e03b7a2e7 100644 --- a/docs/spec/09-pool-protocol.md +++ b/docs/spec/09-pool-protocol.md @@ -159,10 +159,10 @@ The program changes every epoch and the dataset every day (section 1.12), and an Added 7 October 2026 (mission item 11; docs/plans/pool.md section 10). Lineage: Monero P2Pool (SChernykh, 2021: a 10-second share sidechain merge-mined with the main chain, PPLNS over the window, payouts as coinbase outputs, no operator; from its README, approximate), which this section rebuilds on three objects Igneum already had: the vote key in the header (9.6), the coinbase extra data (section 3.10's key reveal and the devnet's `IGNA` address), and an execution layer that pays by rule from consensus data (the 20 percent proving share). Nothing here changes block validity; the one consensus-visible rule is the executor's split (item 5). 1. A member of the open pool runs its own node and the open daemon beside it (`igneum-pool --open`). The daemon serves this section's member protocol to the member's own miners, builds every template from the member's node with the member's key in the header and the member's own payout address in the coinbase (`IGNA`), and holds no key and no balance. -2. A share is a template at the share chain's target: a lane hash at or below `share_target64` of the chain, with the coinbase extra data carrying, beside the reveal and `IGNA`, the share it extends (`IGNS` \|\| 64 hex: the parent's block hash, the zero hash for a genesis share) and the window's payout split (`IGNP` \|\| 4 hex count \|\| count x (40 hex address \|\| 8 hex weight), `kaspa_consensus_core::evm`). A share's identity is its block hash. A share whose lane hash is also at or below the block target is a block, submitted to the member's node as any block. -3. The chain. One parent per share; the heaviest cumulative work (the sum of `2^64 / target` over the ancestry) is the tip; a share off the tip's ancestry is stale and pays nothing; a share extending more than 256 heights below the tip is refused. Target: `chain_share_s` seconds per share (10 by default), held by the Kaspa DAA's shape over the last 30 shares (the window's mean target times its actual span over its expected span, header timestamps), clamped to a factor of 4 against the parent's target, never above 2^62 nor ten doublings above the chain's first target; the first target is a chain constant (`--chain-genesis-target64`, by default the network's genesis block target eight times easier; never a node's current template, which differs between members). Members gossip shares (`p2p.rs`: `hello`, `share`, `get_shares`, newline JSON); every share is checked by every member: the parent known, the height, the target the chain fixes after the parent, the block hash and the body's merkle root, the reveal's key in the header, `IGNA` the share's address, `IGNS` the parent, `IGNP` byte-equal to the split the chain computes for that parent and that address, the seeds either those the member's own node reported for the share's epoch or (another node's view of the same epoch, which a DAG allows while the chain below the lead settles) an epoch seed that is a block the member's node holds at that epoch's seed depth with the epoch's class, era and rung, and the PoW under those seeds. Nodes never see a share. +2. A share is a template at the share chain's target: a lane hash at or below `share_target64` of the chain, with the coinbase extra data carrying, beside the reveal and `IGNA`, the share it extends (`IGNH` \|\| 64 hex: the parent's block hash, the zero hash for a genesis share) and the window's payout split (`IGNW` \|\| 4 hex count \|\| count x (40 hex address \|\| 8 hex weight), `kaspa_consensus_core::evm`). A share's identity is its block hash. A share whose lane hash is also at or below the block target is a block, submitted to the member's node as any block. +3. The chain. One parent per share; the heaviest cumulative work (the sum of `2^64 / target` over the ancestry) is the tip; a share off the tip's ancestry is stale and pays nothing; a share extending more than 256 heights below the tip is refused. Target: `chain_share_s` seconds per share (10 by default), held by the Kaspa DAA's shape over the last 30 shares (the window's mean target times its actual span over its expected span, header timestamps), clamped to a factor of 4 against the parent's target, never above 2^62 nor ten doublings above the chain's first target; the first target is a chain constant (`--chain-genesis-target64`, by default the network's genesis block target eight times easier; never a node's current template, which differs between members). Members gossip shares (`p2p.rs`: `hello`, `share`, `get_shares`, newline JSON); every share is checked by every member: the parent known, the height, the target the chain fixes after the parent, the block hash and the body's merkle root, the reveal's key in the header, `IGNA` the share's address, `IGNH` the parent, `IGNW` byte-equal to the split the chain computes for that parent and that address, the seeds either those the member's own node reported for the share's epoch or (another node's view of the same epoch, which a DAG allows while the chain below the lead settles) an epoch seed that is a block the member's node holds at that epoch's seed depth with the epoch's class, era and rung, and the PoW under those seeds. Nodes never see a share. 4. The split. For a share by address A at target T extending parent P: the last `window_shares - 1` shares ending at P plus the share itself, weighed by `2^64 / target`, summed per address; the software dev fee as one more entry at `open_dev_fee_percent` of the whole (the same 1 percent the solo miner pays, so solo, pool-0 and the open pool are fee-neutral); scaled to u32 weights of 2^32; descending by weight then ascending by address; at most 256 entries (the smallest dropped past it). Every member of a chain holds the same `window_shares`, `chain_share_s`, fee and dev address, or its shares mismatch on the split and are refused: these four are the chain's constants, named by its `--open-chain` string. -5. Payment (the consensus-visible rule, `igneum/exec/src/executor.rs`): from `pool_split_activation_daa` on (a params field, in the digest once set, never by default; the fast-time gate ran it at 0), a blue block whose coinbase carries a well-formed `IGNP` has its producer share paid by the split: each entry `floor(share x weight / sum)`, the rounding dust to the block's `IGNA` address; a block without a split, below the switch, or with a malformed split pays its `IGNA` address alone, as every block does today. The 20 percent proving share is untouched. So a block found on the share chain pays the window from its own coinbase, every node computes the same credits from the block alone, and nobody holds anything for anybody. +5. Payment (the consensus-visible rule, `igneum/exec/src/executor.rs`): from `pool_split_activation_daa` on (a params field, in the digest once set, never by default; the fast-time gate ran it at 0), a blue block whose coinbase carries a well-formed `IGNW` has its producer share paid by the split: each entry `floor(share x weight / sum)`, the rounding dust to the block's `IGNA` address; a block without a split, below the switch, or with a malformed split pays its `IGNA` address alone, as every block does today. The 20 percent proving share is untouched. So a block found on the share chain pays the window from its own coinbase, every node computes the same credits from the block alone, and nobody holds anything for anybody. 6. What a withheld share earns: nothing. A share kept from the gossip is in no other member's chain, so no block but the withholder's own names it in a split, and the withholder's own blocks pay the others' shares by the same rule. Dropping another member's shares is the same act and costs the dropper the same. 7. The drop proof. Every share a member finds is written to its own `shares.log` as one self-certifying line (the seeds, the header with its nonce, the coinbase): `igneum-pool verify-share [--block ]` re-hashes it, and against a block says whether that block's split pays the share's address (exit 3, `BLOCK DROPS`, when it does not). 8. What is not here yet (pool.md section 10.6): uncles (a fork's loser is stale; the gate measures the stale rate), share bodies as transaction ids instead of the full template, a late joiner's verification of shares from epochs its node no longer reports, and the chain's share rate against the network's: at 1 block a second a 10-second share chain is sub-block work only for a pool under a tenth of the network, so the gate ran the chain at 0.1 s per share and a public open chain will set its rate from its hashrate (the mini and nano chains of Monero P2Pool are the same answer). diff --git a/pool/README.md b/pool/README.md index ecee8872e..6585adf42 100644 --- a/pool/README.md +++ b/pool/README.md @@ -84,7 +84,7 @@ start in the clear unless `--allow-plain` says so. The same binary beside a member's own node, with no payout key: the member's miners connect to it on loopback (or a rig's LAN), it builds their templates from the member's node with the member's key in the header and the member's own -address in the coinbase, stamps the share chain's parent (`IGNS`) and the window's split (`IGNP`) into every coinbase, +address in the coinbase, stamps the share chain's parent (`IGNH`) and the window's split (`IGNW`) into every coinbase, and gossips shares with the other members' daemons (`--peer`). A block found on the chain pays the window from its own coinbase by the execution layer's split rule (`pool_split_activation_daa` in the node's override file; blocks below it pay the finder alone). Nobody holds a balance; the only fee is the software dev fee as one entry of the split (the solo diff --git a/pool/src/sidechain.rs b/pool/src/sidechain.rs index ab0c668fc..ba24d449b 100644 --- a/pool/src/sidechain.rs +++ b/pool/src/sidechain.rs @@ -2,11 +2,11 @@ //! //! A share is a real block template at the share chain's target: a header whose `vote_key_hash` is the member's own //! key, whose coinbase extra data carries the member's key reveal, its own `IGNA` payout address, the share it -//! extends (`IGNS`) and the window's payout split (`IGNP`), with a nonce whose lane hash is at or below the chain's +//! extends (`IGNH`) and the window's payout split (`IGNW`), with a nonce whose lane hash is at or below the chain's //! target. Shares form a chain (one parent each; the heaviest cumulative work wins; a share off the winning chain is //! stale and pays nothing). When a share's lane hash is also at or below the block target it is a block, and its //! coinbase, which every member checked before hashing, pays the window: the execution layer splits the producer -//! share by `IGNP` from `pool_split_activation_daa` on (`kaspa_consensus_core::evm::split_producer`). Nobody holds a +//! share by `IGNW` from `pool_split_activation_daa` on (`kaspa_consensus_core::evm::split_producer`). Nobody holds a //! balance and nobody holds an operator key: the chain is computed by every member from the shares alone. //! //! Shape (Monero P2Pool's, SChernykh 2021, approximate from its README; the lineage is in pool.md section 10): diff --git a/pool/tools/dn3-split-read.mjs b/pool/tools/dn3-split-read.mjs index 220680f58..bbfa66ad4 100644 --- a/pool/tools/dn3-split-read.mjs +++ b/pool/tools/dn3-split-read.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node // The Devnet 3 read for the pool split's activation (the node lane's 6.11 condition, 7 October 2026): over a window of // chain blocks on the hub node, the first and last chain block (number, hash, UTC time), the blocks whose coinbase -// carries the share chain's commitment (IGNS) and the window's split (IGNP), and the executed state root of the window's +// carries the share chain's commitment (IGNH) and the window's split (IGNW), and the executed state root of the window's // last block on every node named, so the node lane can set `pool_split_activation_daa` at the hour's last block + 7,200. // Reads chain-side facts only (getVirtualChainFromBlock, getBlock with transactions, igneum_getExecStatus and // eth_getBlockByNumber on the exec RPC); never a process name, never a reported rate. @@ -41,9 +41,9 @@ async function execCall(url, method, params = []) { } const hexToBytes = (h) => Buffer.from(h.replace(/^0x/, ''), 'hex'); const hasTag = (payloadHex, tag) => hexToBytes(payloadHex).includes(Buffer.from(tag, 'ascii')); -// the split's entries, from IGNP || 4 hex count || count x (40 hex address || 8 hex weight) (consensus/core/src/evm.rs) +// the split's entries, from IGNW || 4 hex count || count x (40 hex address || 8 hex weight) (consensus/core/src/evm.rs) function splitOf(payloadHex) { - const b = hexToBytes(payloadHex); const i = b.indexOf(Buffer.from('IGNP', 'ascii')); if (i < 0) return null; + const b = hexToBytes(payloadHex); const i = b.indexOf(Buffer.from('IGNW', 'ascii')); if (i < 0) return null; const body = b.subarray(i + 4).toString('latin1'); const n = parseInt(body.slice(0, 4), 16); if (!(n > 0)) return null; const out = []; for (let k = 0; k < n; k++) { const e = body.slice(4 + k * 48, 4 + k * 48 + 48); if (e.length < 48) return null; out.push({ address: '0x' + e.slice(0, 40), weight: parseInt(e.slice(40, 48), 16) }); } return out; @@ -80,15 +80,15 @@ for (const cb of chainBlocks) { const cbTx = (b.transactions || [])[0]; const payload = cbTx?.payload || ''; const split = splitOf(payload); - blocks.push({ hash: h, daa: Number(b.header.daaScore), ts: Number(b.header.timestamp), chain: b === cb, igns: hasTag(payload, 'IGNS'), ignp: !!split, igna: hasTag(payload, 'IGNA'), split_entries: split ? split.length : 0, vote_key_hash: b.header.voteKeyHash || b.header.vote_key_hash }); + blocks.push({ hash: h, daa: Number(b.header.daaScore), ts: Number(b.header.timestamp), chain: b === cb, ignh: hasTag(payload, 'IGNH'), ignw: !!split, igna: hasTag(payload, 'IGNA'), split_entries: split ? split.length : 0, vote_key_hash: b.header.voteKeyHash || b.header.vote_key_hash }); } } const first = chainBlocks[0], last = chainBlocks[chainBlocks.length - 1]; const iso = (ms) => new Date(Number(ms)).toISOString(); -const withBoth = blocks.filter(b => b.igns && b.ignp); -const onlyOne = blocks.filter(b => b.igns !== b.ignp); +const withBoth = blocks.filter(b => b.ignh && b.ignw); +const onlyOne = blocks.filter(b => b.ignh !== b.ignw); log(`window: ${chainBlocks.length} chain blocks, ${blocks.length} blue blocks; first chain block DAA ${first.header.daaScore} ${String(first.verboseData?.hash || first.header.hash).slice(0, 16)} at ${iso(first.header.timestamp)}; last DAA ${last.header.daaScore} ${String(last.verboseData?.hash || last.header.hash).slice(0, 16)} at ${iso(last.header.timestamp)}`); -log(`blocks carrying IGNS and IGNP: ${withBoth.length} (${withBoth.filter(b => b.chain).length} chain blocks); one tag without the other: ${onlyOne.length}; distinct vote keys among the split blocks: ${new Set(withBoth.map(b => b.vote_key_hash)).size}; split entries min/max: ${withBoth.length ? Math.min(...withBoth.map(b => b.split_entries)) : 0}/${withBoth.length ? Math.max(...withBoth.map(b => b.split_entries)) : 0}`); +log(`blocks carrying IGNH and IGNW: ${withBoth.length} (${withBoth.filter(b => b.chain).length} chain blocks); one tag without the other: ${onlyOne.length}; distinct vote keys among the split blocks: ${new Set(withBoth.map(b => b.vote_key_hash)).size}; split entries min/max: ${withBoth.length ? Math.min(...withBoth.map(b => b.split_entries)) : 0}/${withBoth.length ? Math.max(...withBoth.map(b => b.split_entries)) : 0}`); // exec roots across the nodes at one executed height const nodes = [{ name: 'hub', ws: HUB, exec: HUB_EXEC }, ...NODES.map((n, i) => ({ name: `node${i + 1}`, ...n }))]; const statuses = []; @@ -106,7 +106,7 @@ const result = { first_chain_block: { number_daa: Number(first.header.daaScore), blue_score: Number(first.header.blueScore), hash: first.verboseData?.hash || first.header.hash, time: iso(first.header.timestamp) }, last_chain_block: { number_daa: Number(last.header.daaScore), blue_score: Number(last.header.blueScore), hash: last.verboseData?.hash || last.header.hash, time: iso(last.header.timestamp) }, activation_daa_if_set_now: Number(last.header.daaScore) + 7200, - chain_blocks: chainBlocks.length, blue_blocks: blocks.length, blocks_with_igns_and_ignp: withBoth.length, chain_blocks_with_split: withBoth.filter(b => b.chain).length, blocks_with_one_tag_only: onlyOne.length, + chain_blocks: chainBlocks.length, blue_blocks: blocks.length, blocks_with_ignh_and_ignw: withBoth.length, chain_blocks_with_split: withBoth.filter(b => b.chain).length, blocks_with_one_tag_only: onlyOne.length, distinct_vote_keys_in_split_blocks: new Set(withBoth.map(b => b.vote_key_hash)).size, exec: { statuses, roots_at: lowest, roots, agree: distinctRoots.size === 1 }, split_blocks: withBoth.map(b => ({ hash: b.hash, daa: b.daa, time: iso(b.ts), chain: b.chain, entries: b.split_entries })),