Pool v0 rebased onto master and the 0.3.14 fork: the program class and era seed ride with every seeds and job line; the re-check test for class v3 and class v4
The fleet's 10-member load run (6 October 2026, 18:14Z to 18:24Z) accepted 0 shares: the pool fork's miner re-checked GPU shares with a fixed class v2 program while the 0.3.14 workers hashed class v3 (docs/plans/pool.md section 9). pool: node.rs builds the share verifier's seeds from the template's pow_epoch with the class and the era, installs the node's genesis day, dataset size and class v3 activation beside the schedule, and sends program_class, next_program_class, era_seed, era_index, genesis_day_index, genesis_dataset_log2 and program_class_v3_activation_daa in `seeds` and program_class and era_seed in `job` (protocol.rs, additive fields); verify.rs tests through EpochSeeds::v2. Protocol, vardiff, PPLNS, stats API and page otherwise unchanged. Suite 22 of 22 on igneum-build-1. igneum-pow tests/recheck.rs: for class v3 (packs-ca2-mixer/mx8-devnet-epoch0) and class v4 (packs-ca3-v4/v4-devnet-epoch0) the pack's program reproduces the pack's 96 vectors (the worker's reference), the chain seam the node engine calls (Epoch::chain_program, chain_dataset_day) builds the same program, one known nonce hashes equal through both paths, and the fixed-v2 program of the same seed disagrees; the pinned v3 and v4 program ids differ. 2 of 2 on igneum-build-1. packaging/hive: the pool:// mode merged with master's per-card IDENTITIES=auto and OVERRIDE handling (selftest passes). pool/README.md and pool.md: building on igneum-build-1 (the vendor/igneum-node symlink on the box). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
b53b76df33
commit
8088b55166
6 changed files with 234 additions and 10 deletions
|
|
@ -1,7 +1,9 @@
|
|||
# Pool v0: what it does, what it does not, what was measured, and the listing checklist
|
||||
|
||||
5 October 2026, branch `pool-v0` (main worktree `../igneum-wt-pool`, fork worktree `vendor/igneum-node-pool` on the
|
||||
fork's `pool-v0` from `release-0.3.6` a24ab01a). the project lead's ask: a public mining pool with the stats API WhatToMine and
|
||||
fork's `pool-v0` from `release-0.3.6` a24ab01a). REBASED 6 October 2026 (section 9): branch `pool-v0-rebase` on master, fork
|
||||
branch `pool-v0-rebase` on `release-0.3.14-node` 4c6b129d (worktree `../igneum-wt-pool-rebase`, fork worktree
|
||||
`vendor/igneum-node-pr`); the share re-check hashes the template's program class, never a fixed one. the project lead's ask: a public mining pool with the stats API WhatToMine and
|
||||
HiveOS read, "the biggest step toward being listable". Built against `docs/spec/09-pool-protocol.md`, the miner's
|
||||
template subscription and worker protocol (`igneum/miner/src/main.rs`), the Hive package's `local` mode
|
||||
(`packaging/hive`), ledger F10 and G6 (pools and votes), and `docs/plans/explorer.md` section 6. Nothing is deployed.
|
||||
|
|
@ -24,8 +26,8 @@ template and worker code it reuses lives there.
|
|||
| Transport | newline JSON over plain TCP on 4463 (devnet), 4462, 4461; one object per line, `"t"` names the type, unknown fields ignored, 4 MiB line cap | 9.3, 9.5 |
|
||||
| Session | `hello` / `welcome` (version, chain id, pool address, modes, `vote_mode: member`, `share_scheme` with fee, window and min payout), `authorize` with the member's BLS pubkey and proof of possession (verified with `verify_pop`), payout address and worker name / `authorized` | 9.3, 9.5 |
|
||||
| Templates | mode A, one template per member per tip (NewBlockTemplate subscription, 1 s refresh, on demand): the member's `vote_key_hash` in the header, its key reveal and the pool's `IGNA` payout address in the coinbase extra data; sent as the node's `RpcRawBlock` JSON so a member with a node can submit it there | 9.4, 9.4.1 items 1 and 2, 9.6 item 2 |
|
||||
| Seeds | `seeds` on connection and on every change, from the template's `pow_epoch` (current and next epoch seed, boundary DAA, day, the schedule) | 9.9 |
|
||||
| Jobs | `job` with prehash, `target64`, `share_target64`, a random 2^32 nonce space, the seed pair, `clean` on a tip change | 9.5 |
|
||||
| Seeds | `seeds` on connection and on every change, from the template's `pow_epoch` (current and next epoch seed, boundary DAA, day, the schedule; since 6 October 2026 also `program_class`, `next_program_class`, `era_seed`, `era_index`, `genesis_day_index`, `genesis_dataset_log2`, `program_class_v3_activation_daa`, so a member without a node builds the same program and day cache as the pool's node) | 9.9 |
|
||||
| Jobs | `job` with prehash, `target64`, `share_target64`, a random 2^32 nonce space, the seed pair, `program_class` and `era_seed` (6 October 2026: the member refuses a job without a class, code `seeds`, and never hashes a guessed one), `clean` on a tip change | 9.5 |
|
||||
| Shares | `share` = a lane hash at or below `share_target64`; the pool evaluates the lane on the CPU and answers `share_result` with `ok`, `stale` (superseded job past the 2,000 ms grace), `duplicate`, `above_target`, `wrong_hash`, `unknown_job`; a share at or below `target64` is a block, submitted to every node | 9.8 items 1, 5, 7 |
|
||||
| Vardiff | per member, one share per 10 s, shift `s` with `share_target = target64 << s` never saturated; first correction sized from the measured rate (up to 8 steps), then one step per 30 s at most; idle members eased one step per 3 intervals | 9.8 items 2 and 4 (one deviation: the sized first step, below) |
|
||||
| Weight | `2^-s` of a block per share | 9.8 item 3 |
|
||||
|
|
@ -33,7 +35,7 @@ template and worker code it reuses lives there.
|
|||
| Payouts | EIP-1559 transfers from the pool's coinbase address through the node's eth_ JSON-RPC, one per payee at or above the minimum, at most 16 per round, receipts checked, a failed receipt re-credits; `--dry-run` records and sends nothing | design 1.1 (execution layer rewards) |
|
||||
| The 20% | untouched: the execution layer credits the proving escrow by rule from consensus data (`executor.rs`); the pool's templates name only its own address and receive the 80% | 2.5, 5.3 |
|
||||
| Votes | the member votes through its own node with its own key, as in solo mode; the pool holds no key and names each member's key in its header | 9.6, 9.7 items 1 and 2 |
|
||||
| Member checks | own key in the header, the pool's address and own key reveal in the coinbase (refused with `vote_key`, `payout`, `reveal`); the pool's epoch seed against the member's node (`seeds`); a custodial pool (`vote_mode` not `member`) refused by default | 9.4.1 items 1 and 2, 9.9 item 2, 9.6 item 5 |
|
||||
| Member checks | own key in the header, the pool's address and own key reveal in the coinbase (refused with `vote_key`, `payout`, `reveal`); the pool's epoch seed, program class and era seed against the member's node (`seeds`; the class and era joined the check on 6 October 2026); a custodial pool (`vote_mode` not `member`) refused by default | 9.4.1 items 1 and 2, 9.9 item 2, 9.6 item 5 |
|
||||
| Found blocks | the member submits to its own node and sends `solution`; the pool submits to every node | 9.2 |
|
||||
| API | `/api/stats`, `/api/blocks`, `/api/miners/<address>`, `/api/payments`, `/api/pool-stats` (Hive-style flat object); field contracts in `pool/src/api.rs`, fixtures from the measured run | explorer.md section 6 |
|
||||
| Page | `pool/web/index.html` under the site's tokens: tiles, connect card with the exact miner flags, address lookup, blocks, payments | |
|
||||
|
|
@ -196,3 +198,41 @@ in pool mode. Not implemented tonight.
|
|||
`producer_share(block_subsidy(block daa))`; the executor uses the merging chain block's subsidy; equal on the
|
||||
private run (section 5), may differ by the ramp slope across a day on a live chain. The pool's EVM balance is the
|
||||
truth and the page shows both.
|
||||
|
||||
## 9. 6 October 2026: the rebase onto 0.3.14 and the program class
|
||||
|
||||
The fleet agent's load run (18:14Z to 18:24Z, ten GPU members, vardiff working): 0 shares accepted in ten minutes. Every
|
||||
member's log: `WORKER MISMATCH nonce=... gpu=... cpu=...: share not sent`. The pool fork's miner re-checked every GPU
|
||||
share with its own CPU hasher, and that hasher (`release-0.3.6`'s kaspa-pow) predates program class v3: it hashed a fixed
|
||||
class v2 program while the 0.3.13 and 0.3.14 CUDA workers hashed the epoch's class v3 program, so the two never agreed and
|
||||
the member refused every share before it left the machine. Everything else in the pool worked.
|
||||
|
||||
What changed (branch `pool-v0-rebase` on master, fork `pool-v0-rebase` on `release-0.3.14-node` 4c6b129d):
|
||||
|
||||
| Where | Change | Forced by the rebase? |
|
||||
|---|---|---|
|
||||
| Fork `igneum/miner/src/pool.rs` | `EpochSeeds` carries `class` and `era` on 0.3.14; the member builds them from the job's `program_class` and `era_seed` (`job_seeds`), refuses a job without a class or with one this fork does not know (`job_refused`, code `seeds`), checks class and era against its own node's template beside the epoch seed (`seeds_agree`, through `crate::template`, which installs the node's schedule, genesis day and class v3 activation), installs the same three from the pool's `seeds` line when it has no node (`install_from_pool_line`), and ends every worker `job` and `prepare` line with the class tokens (`class=v3 era=<hex>`); `prepare` writes a checked pack under `--prepare-packs` as the solo miner does; a worker `error ... program class mismatch` gets a fresh prepare | yes (the struct changed) and the fix itself |
|
||||
| Fork `igneum/miner/src/main.rs` | `class_tokens`, `write_pack_checked`, `seeds_from_info` are `pub(crate)` | yes |
|
||||
| Pool `node.rs` | seeds from the template with the class and the era; the node's genesis day, dataset size and class v3 activation installed beside the schedule; `seeds` and `job` carry the new fields | yes (the struct changed) |
|
||||
| Pool `protocol.rs` | `Seeds` + 7 fields, `Job` + 2 fields (additive: a member ignores unknown fields, so the line still parses on the old build; the old build then hashes class v2 as before and keeps mismatching, so every member moves to this build) | yes |
|
||||
| Pool `verify.rs` | the tests build seeds through `EpochSeeds::v2` | yes |
|
||||
| Protocol, vardiff, PPLNS, stats API, page | unchanged | |
|
||||
| `packaging/hive` | the pool:// mode merged with master's per-card `IDENTITIES=auto` and `OVERRIDE` handling; `selftest.sh` passes, the pool-mode config exercised by hand (NODE=local, OVERRIDE set) | merge only |
|
||||
|
||||
Tests (every suite on igneum-build-1, `IGNEUM_AGENT=pool`):
|
||||
|
||||
| Suite | Result | What it pins |
|
||||
|---|---|---|
|
||||
| `igneum-pow` `tests/recheck.rs` | 2 of 2 | class v3 (`packs-ca2-mixer/mx8-devnet-epoch0`) and class v4 (`packs-ca3-v4/v4-devnet-epoch0`): the pack's program (generator, id) reproduces the pack's 96 vectors (the worker's reference), the chain seam the engine calls (`Epoch::chain_program`, `chain_dataset_day`) builds the same program, and one known nonce (prehash 0x5a..., nonce 0x12345678_00000bb7) hashes equal through both paths for each class; v3 and v4 differ from each other and from the fixed-v2 program of the same seed (the fleet's mismatch); the v3 id 73bcbfe8ccf988f1 and v4 id c120d7963abdcd96 as pinned |
|
||||
| fork `igneum-miner` | 21 of 21 | `pool::tests::cpu_recheck_equals_the_worker_reference_for_class_v3`: a job line of the pool's shape through `job_seeds`, then the real `IgneumEngine::epoch_for(seeds).hash_bound` against the pack's reference on the known nonce, the v2 re-check shown to disagree, the worker tokens checked; `a_class_v4_job_is_refused_until_the_fork_knows_class_v4`: this fork's `ProgramClass` has no V4, so a class 4 job is refused rather than hashed on a guess, and the test FAILS the day V4 lands (ca3-v4-node), which is when it becomes the v3 test's shape on the v4 pack; `pool_seeds_are_checked_against_the_verifier_class_and_era` |
|
||||
| pool `igneum-pool` | 22 of 22 | unchanged suites on the new seeds |
|
||||
|
||||
Consequences per tier: a pool member on any card hashes the program its pool's node names, so the fix costs no hash rate
|
||||
and no memory on any tier (the day cache size is the node's, 1 GiB on the devnet); a member without a verifier node now
|
||||
gets the genesis day and dataset size from the pool's `seeds` line, which it had no source for before (it would have built
|
||||
a wrong-size cache and mismatched silently); a pool on a class v4 network needs a fork with `ProgramClass::V4`
|
||||
(0.3.15, ca3-v4-node); until then its members refuse class 4 jobs loudly (`JOB ... REFUSED (seeds)`), never hash on a guess.
|
||||
|
||||
Building the pool crate on igneum-build-1: the pool reads the fork through the `vendor/igneum-node` symlink; lib.sh syncs
|
||||
the fork worktree it points at (`vendor/igneum-node-pr`) as a whole repository, and the symlink itself is created once on
|
||||
the box by hand (`ln -s igneum-node-pr /srv/builds/<worktree>/vendor/igneum-node`), the one step the scripts do not do.
|
||||
|
|
|
|||
137
igneum-pow/tests/recheck.rs
Normal file
137
igneum-pow/tests/recheck.rs
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
//! The miner's CPU re-check against the worker's reference path, class v3 and class v4 (6 October 2026).
|
||||
//!
|
||||
//! The fleet's 10-member pool run (18:14Z to 18:24Z): 0 shares accepted in ten minutes, every GPU answer refused as
|
||||
//! `WORKER MISMATCH`, because the pool fork's miner re-hashed each share with a fixed class v2 program while the 0.3.14
|
||||
//! CUDA worker hashed the class v3 program of the epoch. The rule since: the re-check builds its program through the
|
||||
//! chain seam (`Epoch::chain_program` and `Epoch::chain_dataset_day`, what the node's `IgneumEngine` calls) from the
|
||||
//! template's `(epoch seed, day, program class, era seed)`, never from a fixed class.
|
||||
//!
|
||||
//! This test pins that seam against the worker's reference for both classes the packs carry: the program the pack's
|
||||
//! kernels were emitted from (program.json, generator 3 or 4, with its id) and the pack's 96 vectors; then one known
|
||||
//! nonce through the worker's path (`block_init_words` plus `interpret_warp_init` on the pack's program) and through
|
||||
//! the re-check's path (the same on the seam's program and dataset) must agree, and the other class's program and
|
||||
//! the fixed-v2 program of the same seed must disagree (the mismatch the fleet saw).
|
||||
|
||||
use igneum_pow::bind::{block_init_words, lane_nonce, unhex};
|
||||
use igneum_pow::verify::DatasetSource;
|
||||
use igneum_pow::{interpret_warp_init, DatasetMode, Epoch, ProgramClass, Shape};
|
||||
use serde_json::Value;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn pack_dir(rel: &str) -> PathBuf {
|
||||
let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../proto-cuda").join(rel);
|
||||
assert!(p.join("program.json").is_file(), "pack {} is missing; this test never skips", p.display());
|
||||
p
|
||||
}
|
||||
|
||||
fn json(dir: &std::path::Path, f: &str) -> Value {
|
||||
serde_json::from_str(&std::fs::read_to_string(dir.join(f)).unwrap_or_else(|e| panic!("{}: {e}", dir.join(f).display()))).unwrap()
|
||||
}
|
||||
|
||||
fn hex64(v: &Value) -> u64 {
|
||||
u64::from_str_radix(v.as_str().unwrap().trim_start_matches("0x"), 16).unwrap()
|
||||
}
|
||||
|
||||
struct PackRef {
|
||||
class: ProgramClass,
|
||||
seed_label: String,
|
||||
seed_bytes: Vec<u8>,
|
||||
era: Vec<u8>,
|
||||
day_bytes: Vec<u8>,
|
||||
log2: u32,
|
||||
/// The worker's reference: the pack's program and day dataset, checked against the pack's vectors
|
||||
reference: Epoch,
|
||||
}
|
||||
|
||||
/// The pack as the worker sees it: program.json's program (generator, id and class as recorded) over the pack's
|
||||
/// day dataset, trusted only after its 96 vectors reproduce.
|
||||
fn load(rel: &str, want: ProgramClass) -> PackRef {
|
||||
let dir = pack_dir(rel);
|
||||
let j = json(&dir, "program.json");
|
||||
let class = match j["program_class"].as_str().unwrap() {
|
||||
"v3" => ProgramClass::V3,
|
||||
"v4" => ProgramClass::V4,
|
||||
other => panic!("{rel}: class {other}"),
|
||||
};
|
||||
assert_eq!(class, want, "{rel}: the pack's class");
|
||||
let seed_label = j["seed"].as_str().unwrap().to_string();
|
||||
let seed_bytes = unhex(j["seed_bytes"].as_str().unwrap()).unwrap();
|
||||
let era = unhex(j["era_seed_bytes"].as_str().unwrap()).unwrap();
|
||||
let day_bytes = unhex(j["dataset"]["day_bytes"].as_str().unwrap()).unwrap();
|
||||
let log2 = j["dataset"]["log2_words"].as_u64().unwrap() as u32;
|
||||
let program = igneum_pow::generate_from_seed_bytes_program_class(&seed_label, &seed_bytes, class, Some(&era));
|
||||
assert_eq!(program.generator, j["generator"].as_u64().unwrap() as u32, "{rel}: generator");
|
||||
assert_eq!(program.generator, class.generator_version(), "{rel}: the generator is the class's");
|
||||
assert_eq!(program.program_id(), hex64(&j["program_id"]), "{rel}: program id");
|
||||
let shape = Shape::for_class(&program.class);
|
||||
let mut dataset = DatasetSource::from_key_shape(igneum_pow::seed::seed_words_from_bytes(&day_bytes), DatasetMode::MemoryHard, log2, shape);
|
||||
dataset.key_bytes = day_bytes.clone();
|
||||
let reference = Epoch { program, dataset };
|
||||
let v = json(&dir, "vectors.json");
|
||||
let warps = v["warps"].as_array().unwrap();
|
||||
assert_eq!(warps.len(), 3, "{rel}: three vector warps");
|
||||
for w in warps {
|
||||
let base = w["base_nonce"].as_u64().unwrap() as u32;
|
||||
let got = reference.hash_warp(base);
|
||||
for (lane, e) in w["expected"].as_array().unwrap().iter().enumerate() {
|
||||
assert_eq!(got[lane], hex64(e), "{rel}: vector base {base} lane {lane}");
|
||||
}
|
||||
}
|
||||
PackRef { class, seed_label, seed_bytes, era, day_bytes, log2, reference }
|
||||
}
|
||||
|
||||
/// The re-check's epoch: the chain seam the node engine and the miner call, from the template's four values.
|
||||
/// The packs are day-0 caches, so days since genesis is 0 and the genesis dataset size is the pack's.
|
||||
fn recheck_epoch(p: &PackRef) -> Epoch {
|
||||
Epoch { program: Epoch::chain_program(&p.seed_bytes, Some(&p.era), p.class, &p.seed_label), dataset: Epoch::chain_dataset_day(&p.day_bytes, p.class, 0, p.log2) }
|
||||
}
|
||||
|
||||
const PREHASH: [u8; 32] = [0x5a; 32];
|
||||
const NONCE: u64 = 0x1234_5678_0000_0bb7; // high word 0x12345678, lane 0x0bb7 (warp base 0x0ba0, lane 23)
|
||||
|
||||
/// The bound lane hash of one nonce on an epoch, the way a worker computes it (init words from the prehash and the
|
||||
/// nonce's high word, the warp at the lane's base) and the way `EpochRef::hash_bound` re-checks it.
|
||||
fn bound(e: &Epoch, prehash: &[u8; 32], nonce: u64) -> u64 {
|
||||
let init = block_init_words(prehash, nonce);
|
||||
let lane = lane_nonce(nonce);
|
||||
interpret_warp_init(&e.program, &init, lane & !31, &e.dataset).hashes[(lane & 31) as usize]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpu_recheck_equals_the_worker_reference_for_class_v3_and_class_v4() {
|
||||
let v3 = load("packs-ca2-mixer/mx8-devnet-epoch0", ProgramClass::V3);
|
||||
let v4 = load("packs-ca3-v4/v4-devnet-epoch0", ProgramClass::V4);
|
||||
assert_eq!(v3.seed_bytes, v4.seed_bytes, "the two packs share the epoch seed (devnet genesis), so only the class differs");
|
||||
assert_eq!(v3.era, v4.era);
|
||||
let mut hashes = Vec::new();
|
||||
for p in [&v3, &v4] {
|
||||
let re = recheck_epoch(p);
|
||||
assert_eq!(re.program.program_id(), p.reference.program.program_id(), "{:?}: the seam's program is the pack's", p.class);
|
||||
assert_eq!(re.program.generator, p.reference.program.generator);
|
||||
assert_eq!(re.program.instrs.len(), p.reference.program.instrs.len());
|
||||
let worker = bound(&p.reference, &PREHASH, NONCE);
|
||||
let cpu = bound(&re, &PREHASH, NONCE);
|
||||
assert_eq!(cpu, worker, "{:?}: CPU re-check {cpu:016x} against the worker's reference {worker:016x}", p.class);
|
||||
// the same nonce through a second prehash, so the agreement is not one lucky lane
|
||||
let (w2, c2) = (bound(&p.reference, &[0xa5; 32], NONCE ^ 0x1f), bound(&re, &[0xa5; 32], NONCE ^ 0x1f));
|
||||
assert_eq!(c2, w2, "{:?}: second nonce", p.class);
|
||||
hashes.push(cpu);
|
||||
}
|
||||
assert_ne!(hashes[0], hashes[1], "class v3 and class v4 programs of one seed hash a nonce differently");
|
||||
// the fleet's mismatch: a fixed class v2 program on the class v3 epoch's seed
|
||||
let v2 = Epoch { program: Epoch::chain_program(&v3.seed_bytes, None, ProgramClass::V2, &v3.seed_label), dataset: Epoch::chain_dataset_day(&v3.day_bytes, ProgramClass::V2, 0, v3.log2) };
|
||||
assert_ne!(bound(&v2, &PREHASH, NONCE), hashes[0], "a class v2 re-check refuses a class v3 share");
|
||||
assert_ne!(bound(&v2, &PREHASH, NONCE), hashes[1], "a class v2 re-check refuses a class v4 share");
|
||||
}
|
||||
|
||||
/// The generator-4 program id rule (counter-asic-3-status.md, the blocker closed 6 October 2026): the v4 program of
|
||||
/// a seed carries another id than the v3 program of the same seed, so a stale worker across the activation sees the
|
||||
/// mismatch, while v2 and v3 ids stay as the packs pinned them.
|
||||
#[test]
|
||||
fn program_ids_differ_between_class_v3_and_class_v4_of_one_seed() {
|
||||
let v3 = load("packs-ca2-mixer/mx8-devnet-epoch0", ProgramClass::V3);
|
||||
let v4 = load("packs-ca3-v4/v4-devnet-epoch0", ProgramClass::V4);
|
||||
assert_eq!(v3.reference.program.program_id(), 0x73bc_bfe8_ccf9_88f1, "the v3 control's id as pinned");
|
||||
assert_eq!(v4.reference.program.program_id(), 0xc120_d796_3abd_cd96, "the v4 candidate's id as pinned");
|
||||
assert_ne!(v3.reference.program.program_id(), v4.reference.program.program_id());
|
||||
}
|
||||
|
|
@ -163,3 +163,10 @@ Captured responses from the measured run are the fixtures.
|
|||
| `src/api.rs` | the HTTP server, the routes, the field contracts |
|
||||
| `web/index.html` | the page |
|
||||
| `tools/measure.mjs` | the private-network measurement |
|
||||
|
||||
## Building on igneum-build-1 (6 October 2026)
|
||||
|
||||
Every Linux build and suite runs on the box through `tools/build-remote.sh` from this directory (`IGNEUM_AGENT=pool`). The
|
||||
crate reads the fork through the `vendor/igneum-node` symlink; the build library syncs the fork worktree it points at as a
|
||||
whole repository, and the symlink is made once on the box by hand: `ln -s <fork worktree name> /srv/builds/<worktree>/vendor/igneum-node`.
|
||||
Artefacts land in `pool/target-remote/release/igneum-pool` (x86_64 Linux); the Mac builds no Linux binary.
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use crate::vardiff::{share_target, Vardiff};
|
|||
use crate::verify::JobKey;
|
||||
use kaspa_consensus_core::block::Block;
|
||||
use kaspa_consensus_core::header::Header;
|
||||
use kaspa_consensus_core::igneum::{block_subsidy, install_pow_schedule, pow_schedule, producer_share, PowSchedule};
|
||||
use kaspa_consensus_core::igneum::{block_subsidy, install_pow_genesis, install_pow_schedule, install_program_class_v3_activation, pow_genesis_dataset_log2, pow_genesis_day_index, pow_schedule, producer_share, program_class_v3_activation_daa, PowSchedule};
|
||||
use kaspa_grpc_client::GrpcClient;
|
||||
use kaspa_hashes::Hash;
|
||||
use kaspa_notify::{listener::ListenerId, scope::NewBlockTemplateScope};
|
||||
|
|
@ -48,12 +48,25 @@ pub async fn fetch_job(pool: &Arc<Pool>, member: &Arc<Member>) -> Result<(), Str
|
|||
let block: Block = raw.clone().try_into().map_err(|e| format!("block convert: {e}"))?;
|
||||
let header: Header = block.header.as_ref().clone();
|
||||
let info = tmpl.pow_epoch.as_ref().ok_or("the node reports no pow_epoch; a devnet-v4 line node is needed")?;
|
||||
// The node's PoW schedule, genesis day and dataset size, and class v3 activation are the pool's (what the solo
|
||||
// miner's `template()` installs): the share verifier's day cache and program must be the node's exactly
|
||||
let wanted = PowSchedule::clamped(info.epoch_blocks, info.epoch_lead, info.day_ms);
|
||||
if wanted != pow_schedule() {
|
||||
install_pow_schedule(wanted);
|
||||
eprintln!("{} node PoW schedule: {} DAA per epoch, lead {}, day {} ms", now(), wanted.epoch_blocks, wanted.epoch_lead, wanted.day_ms);
|
||||
}
|
||||
let seeds = EpochSeeds { epoch: info.epoch_seed, day: day_index(header.timestamp) };
|
||||
if (info.genesis_day_index, info.genesis_dataset_log2) != (pow_genesis_day_index(), pow_genesis_dataset_log2()) {
|
||||
install_pow_genesis(info.genesis_day_index, info.genesis_dataset_log2);
|
||||
eprintln!("{} node genesis day index {} and genesis dataset 2^{} words: the day cache follows them", now(), info.genesis_day_index, info.genesis_dataset_log2);
|
||||
}
|
||||
if info.program_class_v3_activation_daa != program_class_v3_activation_daa() {
|
||||
install_program_class_v3_activation(info.program_class_v3_activation_daa);
|
||||
eprintln!("{} node program class v3 activation: {} (epoch {} is class {}, the next epoch class {})", now(), info.program_class_v3_activation_daa, info.epoch_index, info.class().name(), info.next_class().name());
|
||||
}
|
||||
// Counter ASIC 2.0: the class and the era seed of the epoch ride with the template; the verifier hashes the
|
||||
// program they name, the same one the members' workers compile (6 October 2026: a fixed class here or on the
|
||||
// member refused every GPU share of the fleet run)
|
||||
let seeds = EpochSeeds { epoch: info.epoch_seed, day: day_index(header.timestamp), class: info.class(), era: info.era_seed.unwrap_or(kaspa_hashes::ZERO_HASH) };
|
||||
let engine = pool.engine.clone();
|
||||
let epoch = tokio::task::spawn_blocking(move || engine.epoch_for(&seeds)).await.map_err(|e| e.to_string())?;
|
||||
let prehash = header_prehash(&header);
|
||||
|
|
@ -119,9 +132,17 @@ pub async fn fetch_job(pool: &Arc<Pool>, member: &Arc<Member>) -> Result<(), Str
|
|||
epoch_blocks: info.epoch_blocks,
|
||||
epoch_lead: info.epoch_lead,
|
||||
day_ms: info.day_ms,
|
||||
program_class: info.program_class,
|
||||
next_program_class: info.next_program_class,
|
||||
era_seed: info.era_seed.map(|h| h.to_string()),
|
||||
era_index: info.era_index,
|
||||
genesis_day_index: info.genesis_day_index,
|
||||
genesis_dataset_log2: info.genesis_dataset_log2,
|
||||
program_class_v3_activation_daa: info.program_class_v3_activation_daa,
|
||||
}
|
||||
.line(),
|
||||
);
|
||||
println!("{} SEEDS for member {} ({}): epoch {} day {} class {} era {}", now(), member.id, member.worker, seeds.epoch, seeds.day, seeds.class.name(), seeds.era);
|
||||
}
|
||||
member.send(Msg::Template { template_id, mode: "A".into(), block: Box::new(raw), daa_score: header.daa_score, bits: header.bits, votes_carried: Vec::new() }.line());
|
||||
member.send(
|
||||
|
|
@ -135,6 +156,8 @@ pub async fn fetch_job(pool: &Arc<Pool>, member: &Arc<Member>) -> Result<(), Str
|
|||
nonce_count: hex_u64(1 << 32),
|
||||
epoch_seed: seeds.epoch.to_string(),
|
||||
day: seeds.day,
|
||||
program_class: seeds.class.generator_version(),
|
||||
era_seed: (seeds.class != kaspa_consensus_core::igneum::ProgramClass::V2).then(|| seeds.era.to_string()),
|
||||
clean,
|
||||
}
|
||||
.line(),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
//! v0 encoding notes (docs/plans/pool.md): a `template` carries the node's own `RpcRawBlock` JSON (mode A, the full
|
||||
//! block, so a member with a node can submit it there as well); `authorize` carries the member's payout address and
|
||||
//! worker name beside the key, because v0 pays by EVM address and the spec's message names the member by key only;
|
||||
//! `job` carries the seed pair the worker protocol wants (`epoch_seed`, `day`) so a member needs no `seeds` lookup
|
||||
//! `job` carries the seed pair the worker protocol wants (`epoch_seed`, `day`) and the program class and era seed
|
||||
//! (Counter ASIC 2.0) so a member needs no `seeds` lookup
|
||||
//! per job. Unknown fields are ignored, as the spec requires.
|
||||
|
||||
use kaspa_rpc_core::RpcRawBlock;
|
||||
|
|
@ -66,6 +67,18 @@ pub enum Msg {
|
|||
epoch_blocks: u64,
|
||||
epoch_lead: u64,
|
||||
day_ms: u64,
|
||||
/// Counter ASIC 2.0 (added 6 October 2026, after the fleet's 0-share run): the program class of the current
|
||||
/// epoch as the node's `pow_epoch.program_class` reports it (the generator number, 2 or 3), the next epoch's
|
||||
/// class, and the era seed class v3 programs are drawn under. A member hashes the program these name.
|
||||
program_class: u32,
|
||||
next_program_class: u32,
|
||||
era_seed: Option<String>,
|
||||
era_index: u64,
|
||||
/// The node's genesis day index, genesis dataset size (log2 words) and class v3 activation DAA: what a
|
||||
/// member without a verifier node installs so its day cache is built at the network's size
|
||||
genesis_day_index: u64,
|
||||
genesis_dataset_log2: u32,
|
||||
program_class_v3_activation_daa: u64,
|
||||
},
|
||||
SetTarget {
|
||||
shift: u32,
|
||||
|
|
@ -90,6 +103,10 @@ pub enum Msg {
|
|||
nonce_count: String,
|
||||
epoch_seed: String,
|
||||
day: u64,
|
||||
/// The program class (generator number) and era seed of the job's epoch: the member's CPU re-check and its
|
||||
/// worker lines use exactly these (never a fixed class)
|
||||
program_class: u32,
|
||||
era_seed: Option<String>,
|
||||
clean: bool,
|
||||
},
|
||||
JobRefused {
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ mod tests {
|
|||
#[test]
|
||||
fn known_good_and_known_bad_shares() {
|
||||
let engine = IgneumEngine::new();
|
||||
let seeds = EpochSeeds { epoch: Hash::from_bytes([5u8; 32]), day: 0 };
|
||||
let seeds = EpochSeeds::v2(Hash::from_bytes([5u8; 32]), 0);
|
||||
let epoch = engine.epoch_for(&seeds);
|
||||
let prehash = [9u8; 32];
|
||||
let nonce = 0x0123_4567_89ab_cdefu64;
|
||||
|
|
@ -111,7 +111,7 @@ mod tests {
|
|||
#[test]
|
||||
fn lanes_of_one_group_are_distinct_shares() {
|
||||
let engine = IgneumEngine::new();
|
||||
let seeds = EpochSeeds { epoch: Hash::from_bytes([6u8; 32]), day: 0 };
|
||||
let seeds = EpochSeeds::v2(Hash::from_bytes([6u8; 32]), 0);
|
||||
let epoch = engine.epoch_for(&seeds);
|
||||
let prehash = [1u8; 32];
|
||||
let base = 0x0000_0001_0000_0020u64;
|
||||
|
|
@ -133,7 +133,7 @@ mod cost {
|
|||
#[test]
|
||||
fn share_check_cost_ms() {
|
||||
let engine = IgneumEngine::new();
|
||||
let seeds = EpochSeeds { epoch: Hash::from_bytes([3u8; 32]), day: 0 };
|
||||
let seeds = EpochSeeds::v2(Hash::from_bytes([3u8; 32]), 0);
|
||||
let epoch = engine.epoch_for(&seeds);
|
||||
let key = JobKey { prehash: [2u8; 32], target64: 0, share_target64: u64::MAX, seeds };
|
||||
let mut v: Vec<f64> = (0..500u64).map(|i| check(&epoch, &key, i * 97, 0).cost_ms).collect();
|
||||
|
|
|
|||
Loading…
Reference in a new issue