diff --git a/igneum-pow/src/verify.rs b/igneum-pow/src/verify.rs index 2758d58fe..db324e7b6 100644 --- a/igneum-pow/src/verify.rs +++ b/igneum-pow/src/verify.rs @@ -454,9 +454,7 @@ impl Epoch { /// exactly (the era bytes are ignored and not recorded); class v3 draws from [`crate::generator::V3_CLASS`] /// with generator version 3 and records the era seed bytes (`E_n`) in the program for the pack. pub fn from_chain_seeds(epoch_seed: &[u8], day_bytes: &[u8], era_bytes: Option<&[u8]>, class: ProgramClass, label: &str) -> Self { - let mut epoch = Self::from_seed_bytes_class(epoch_seed, day_bytes, label, class.load_class()); - epoch.program = Self::chain_program(epoch_seed, era_bytes, class, label); - epoch + Self { program: Self::chain_program(epoch_seed, era_bytes, class, label), dataset: Self::chain_dataset(day_bytes, class) } } /// The program alone of [`Epoch::from_chain_seeds`] (no cache fill): for an engine that shares the day's cache. @@ -464,6 +462,18 @@ impl Epoch { crate::generator::generate_from_seed_bytes_program_class(label, epoch_seed, class, era_bytes) } + /// The day's cache and dataset of [`Epoch::from_chain_seeds`], the one entry the node's engine builds a day + /// cache through. The class is an argument because the Counter ASIC 2.0 integration gives class v3 its own item + /// construction (the mixer multiplier) and cache size schedule (ca2-mixer); today both classes build the day of + /// [`Epoch::from_seed_bytes`], and the engine keys its day caches on `(day, class)` so the two never share one. + pub fn chain_dataset(day_bytes: &[u8], class: ProgramClass) -> DatasetSource { + let _ = class; + let key = crate::seed::seed_words_from_bytes(day_bytes); + let mut dataset = DatasetSource::from_key(key, DatasetMode::MemoryHard, DEFAULT_DATASET_LOG2); + dataset.key_bytes = day_bytes.to_vec(); + dataset + } + /// The 32 hashes of the warp starting at `base_nonce`. pub fn hash_warp(&self, base_nonce: u32) -> [u64; LANES] { hash_warp(&self.program, base_nonce, &self.dataset) diff --git a/proto-cuda/nvrtc/emu/packfile-test.c b/proto-cuda/nvrtc/emu/packfile-test.c index c2a4ffb11..f9cf9dfbe 100644 --- a/proto-cuda/nvrtc/emu/packfile-test.c +++ b/proto-cuda/nvrtc/emu/packfile-test.c @@ -103,11 +103,41 @@ int main(int argc, char** argv) { { char text[2000], sw[200], kw[200]; words_hex(bare, sw); words_hex(keyw, kw); - snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, sw, kw); + snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_GENERATOR 2\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, sw, kw); write_file(dir, "program.h", text); write_file(dir, "seeds.txt", "epoch_seed_hex " EPOCH_34 "\nday_seed_hex " DAY_20731 "\n"); err[0] = 0; CHECK(pf_load(dir, &pk, err, sizeof(err)) == 1 && pk.attempt == 0, "no attempt line reads as attempt 0 and the bare words load"); + CHECK(strcmp(pk.programClass, "v2") == 0 && pk.eraHex[0] == 0, "a generator 2 pack without a class line is class v2 with no era"); + } + // 4. Program classes (Counter ASIC 2.0, 5 October 2026, spec 01 section 1.4.5): a generator this worker does not + // run is refused; a generator 3 pack is class v3 and carries its era seed; a class line that contradicts the + // generator is refused; a pack with no generator line at all (generator 1, the retired lever generator) is refused. + { + char text[2400], sw[200], kw[200], why[256]; + words_hex(bare, sw); words_hex(keyw, kw); + snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_GENERATOR 9\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, sw, kw); + write_file(dir, "program.h", text); + err[0] = 0; + CHECK(pf_load(dir, &pk, err, sizeof(err)) == 0 && strstr(err, "generator 9 is not a generator version this worker runs") != NULL, "generator 9 is refused in plain words"); + snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_GENERATOR 3\n#define IGNEUM_PROGRAM_CLASS \"v3\"\n#define IGNEUM_ERA_SEED_HEX \"%s\"\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, EPOCH_33, sw, kw); + write_file(dir, "program.h", text); + err[0] = 0; + CHECK(pf_load(dir, &pk, err, sizeof(err)) == 1 && strcmp(pk.programClass, "v3") == 0 && strcmp(pk.eraHex, EPOCH_33) == 0, "a generator 3 pack loads as class v3 with its era seed"); + CHECK(pf_pack_class_ok(pk.programClass, pk.eraHex, "v3", EPOCH_33, why, sizeof(why)) == 1, "the v3 pack matches a job naming class v3 and its era"); + CHECK(pf_pack_class_ok(pk.programClass, pk.eraHex, "", "", why, sizeof(why)) == 1, "a job naming no class accepts the pack"); + CHECK(pf_pack_class_ok(pk.programClass, pk.eraHex, "v2", "", why, sizeof(why)) == 0 && strstr(why, "program class mismatch") == why, "a job naming class v2 refuses the v3 pack"); + CHECK(pf_pack_class_ok(pk.programClass, pk.eraHex, "v3", EPOCH_34, why, sizeof(why)) == 0 && strstr(why, "era seed mismatch") == why, "a job naming another era refuses the v3 pack"); + CHECK(pf_pack_class_ok("v2", "", "v3", EPOCH_33, why, sizeof(why)) == 0, "a job naming class v3 refuses a v2 pack"); + CHECK(pf_pack_class_ok("v2", "", "v2", EPOCH_33, why, sizeof(why)) == 1, "an era named on a v2 job is ignored"); + snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_GENERATOR 3\n#define IGNEUM_PROGRAM_CLASS \"v2\"\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, sw, kw); + write_file(dir, "program.h", text); + err[0] = 0; + CHECK(pf_load(dir, &pk, err, sizeof(err)) == 0 && strstr(err, "does not match IGNEUM_GENERATOR 3") != NULL, "a class line that contradicts the generator is refused"); + snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, sw, kw); + write_file(dir, "program.h", text); + err[0] = 0; + CHECK(pf_load(dir, &pk, err, sizeof(err)) == 0 && strstr(err, "generator 1 is not") != NULL, "a pack with no generator line (generator 1) is refused"); } printf("%s: %d failure(s)\n", argv[0], failures); diff --git a/proto-cuda/nvrtc/packfile.h b/proto-cuda/nvrtc/packfile.h index 285888bae..ba5587a99 100644 --- a/proto-cuda/nvrtc/packfile.h +++ b/proto-cuda/nvrtc/packfile.h @@ -29,6 +29,8 @@ typedef struct { // read-width experiment (5 October 2026): the load class (0 when absent), bytes per hash, variant 5's scratch uint32_t loadsPerHash, bytesPerHash, scratchOps, persistent, scratchWordsPerLane; char loadClass[64]; + char programClass[8]; /* IGNEUM_PROGRAM_CLASS: "v2" or "v3" (Counter ASIC 2.0); absent = the generator's class */ + char eraHex[65]; /* IGNEUM_ERA_SEED_HEX of a class v3 chain pack; empty otherwise */ // seeds.txt (or program.h): the seeds as the worker protocol carries them char epochHex[65]; char dayHex[PF_HEX_CAP]; @@ -271,6 +273,21 @@ static int pf_load(const char* dir, PfPack* pk, char* err, size_t cap) { if (!pf_define_u32(prog, "IGNEUM_CACHE_LOG2_WORDS", &pk->cacheLog2Words)) { free(prog); return pf_fail(err, cap, "program.h has no IGNEUM_CACHE_LOG2_WORDS"); } if (!pf_define_u32(prog, "IGNEUM_CACHE_SEGMENTS", &pk->cacheSegments)) { free(prog); return pf_fail(err, cap, "program.h has no IGNEUM_CACHE_SEGMENTS"); } if (!pf_define_u32(prog, "IGNEUM_GENERATOR", &pk->generator)) pk->generator = 1; + /* Spec 01 section 1.4.5: a pack whose generator version is not one this worker runs is refused. Generator 2 is + * program class v2 (the lottery hash of 4 October 2026), generator 3 is class v3 (Counter ASIC 2.0). */ + if (pk->generator != 2 && pk->generator != 3) { + char m[200]; snprintf(m, sizeof(m), "program pack generator %u is not a generator version this worker runs (2 or 3)", (unsigned)pk->generator); + free(prog); return pf_fail(err, cap, m); + } + strcpy(pk->programClass, pk->generator == 3 ? "v3" : "v2"); + { + char named[8] = {0}; + if (pf_define_str(prog, "IGNEUM_PROGRAM_CLASS", named, sizeof(named)) && strcmp(named, pk->programClass) != 0) { + char m[200]; snprintf(m, sizeof(m), "program pack IGNEUM_PROGRAM_CLASS \"%.7s\" does not match IGNEUM_GENERATOR %u", named, (unsigned)pk->generator); + free(prog); return pf_fail(err, cap, m); + } + } + pk->eraHex[0] = 0; pf_define_str(prog, "IGNEUM_ERA_SEED_HEX", pk->eraHex, sizeof(pk->eraHex)); if (!pf_define_u32(prog, "IGNEUM_PROGRAM_ATTEMPT", &pk->attempt)) pk->attempt = 0; if (pf_define_words(prog, "IGNEUM_SEEDW_INIT", pk->seedw, 8) != 8) { free(prog); return pf_fail(err, cap, "program.h has no IGNEUM_SEEDW_INIT with 8 words"); } pk->loadsPerHash = 128; pf_define_u32(prog, "IGNEUM_LOADS_PER_HASH", &pk->loadsPerHash); @@ -382,4 +399,29 @@ static int pf_selftest(const PfPack* pk, const uint32_t* cacheHead, const uint32 return 0; } + +/* Counter ASIC 2.0 (5 October 2026): a job or prepare line may end with `class=` and `era=` tokens (sent + * only when the chain is on class v3, so every v2 line is the line of before). A pack matches the line when its class + * is the named class and, when an era is named, its era seed is that era. Empty wanted strings accept any pack. + * Returns 1 on a match, else 0 with the reason in `why`. */ +static int pf_pack_class_ok(const char* packClass, const char* packEra, const char* wantClass, const char* wantEra, char* why, size_t cap) { + if (wantClass && wantClass[0] && strcmp(wantClass, packClass) != 0) { + snprintf(why, cap, "program class mismatch: this pack is class %s, the job names class %s (export the pack again)", packClass, wantClass); + return 0; + } + if (wantEra && wantEra[0] && strcmp(packClass, "v3") == 0) { + size_t i; int eq = strlen(packEra) == strlen(wantEra); + for (i = 0; eq && packEra[i]; ++i) if (tolower((unsigned char)packEra[i]) != tolower((unsigned char)wantEra[i])) eq = 0; + if (!eq) { snprintf(why, cap, "era seed mismatch: this pack was drawn under era %.16s, the job names era %.16s (export the pack again)", packEra[0] ? packEra : "(none)", wantEra); return 0; } + } + return 1; +} + +/* Reads a `class=` or `era=` token into `cls` / `era` (small fixed buffers). Returns 1 when the token was one of them. */ +static int pf_class_token(const char* tok, char* cls, size_t clsCap, char* era, size_t eraCap) { + if (strncmp(tok, "class=", 6) == 0) { strncpy(cls, tok + 6, clsCap - 1); cls[clsCap - 1] = 0; return 1; } + if (strncmp(tok, "era=", 4) == 0) { strncpy(era, tok + 4, eraCap - 1); era[eraCap - 1] = 0; return 1; } + return 0; +} + #endif diff --git a/proto-cuda/nvrtc/worker.cpp b/proto-cuda/nvrtc/worker.cpp index 800617e17..175b2bc0b 100644 --- a/proto-cuda/nvrtc/worker.cpp +++ b/proto-cuda/nvrtc/worker.cpp @@ -427,6 +427,7 @@ struct Pair { double raceMs = 0; // read-width experiment (5 October 2026): the pack's load class and, for variant 5, the persistent-warp scratch std::string loadClass = "v2"; + std::string programClass = "v2", eraHex; // Counter ASIC 2.0: the pack's class and era seed (packfile.h) uint32_t loadsPerHash = 128, bytesPerHash = 512, scratchOps = 0; bool persistent = false; CUdeviceptr scratch = 0; @@ -439,6 +440,24 @@ static bool pairIs(const Pair* p, const std::string& epochHex, const std::string return p && hexEq(p->epochHex, epochHex) && hexEq(p->dayHex, dayHex); } +// Counter ASIC 2.0: a job that names a class (and an era) belongs to a pair of that class (and era) only, so a pack +// of the old class for the same seeds is not this job's pair and the prepared pack of the right class wins. +static bool pairIsClass(const Pair* p, const std::string& epochHex, const std::string& dayHex, const std::string& cls, const std::string& era) { + char why[256]; + return pairIs(p, epochHex, dayHex) && pf_pack_class_ok(p->programClass.c_str(), p->eraHex.c_str(), cls.c_str(), era.c_str(), why, sizeof(why)); +} + +// The trailing `class=` and `era=` tokens of a job or prepare line (absent on every class v2 line), removed from `f`. +static void takeClassTokens(std::vector& f, std::string& cls, std::string& era) { + while (!f.empty()) { + char c[8] = {0}, e[65] = {0}; + if (!pf_class_token(f.back().c_str(), c, sizeof(c), e, sizeof(e))) break; + if (c[0]) cls = c; + if (e[0]) era = e; + f.pop_back(); + } +} + // The job loop and a race take turns on the card: a variant is timed with no job running (exclusive numbers), and // mining resumes between variants. Held per chunk by the job loop, per variant by the race. static std::mutex gpuMutex; @@ -809,6 +828,7 @@ static Pair* buildPair(Ctx& c, const std::string& dir, CUstream s, std::string& c.drv.occupancy(&p->blocksPerSM, p->fHashBound, 32 * c.blockWarps, 0); } p->loadClass = pk.loadClass; p->loadsPerHash = pk.loadsPerHash; p->bytesPerHash = pk.bytesPerHash; p->scratchOps = pk.scratchOps; + p->programClass = pk.programClass; p->eraHex = pk.eraHex; p->persistent = pk.persistent != 0; p->residentWarps = p->blocksPerSM * c.blockWarps * c.sms; size_t scratchBytes = 0; @@ -912,6 +932,7 @@ static std::string pairSummary(const Pair* p) { struct PrepareTask { std::string epochHex, dayHex, dir, error; + std::string wantClass, wantEra; // the class and era the prepare line named (empty: any) std::atomic done{false}; Pair* result = nullptr; double t0 = 0; @@ -929,6 +950,11 @@ static void prepareRun(Ctx* c, PrepareTask* t) { err = "the pack in " + t->dir + " is for epoch " + p->epochHex.substr(0, 16) + " day " + p->dayHex + ", not the prepared seeds"; releasePair(*c, p); p = nullptr; } + char why[256]; + if (p && !pf_pack_class_ok(p->programClass.c_str(), p->eraHex.c_str(), t->wantClass.c_str(), t->wantEra.c_str(), why, sizeof(why))) { + err = "pack " + t->dir + ": " + why; + releasePair(*c, p); p = nullptr; + } t->result = p; t->error = err; t->done = true; @@ -1083,6 +1109,8 @@ static int runServe(Ctx& c, const Options& o, Pair* cur) { } delete task; task = nullptr; } + std::string wantClass, wantEra; + takeClassTokens(f, wantClass, wantEra); if (f[0] == "prepare") { if (f.size() < 4) { emit(fmt("prepare-failed %s %s a pack directory is needed as the third field (igneum-miner --prepare-packs )", f.size() > 1 ? f[1].c_str() : "0", f.size() > 2 ? f[2].c_str() : "0")); continue; } if (f[1].size() != 64) { emit(fmt("prepare-failed %s %s bad field (epoch_seed 64 hex, day_seed hex)", f[1].c_str(), f[2].c_str())); continue; } @@ -1094,6 +1122,7 @@ static int runServe(Ctx& c, const Options& o, Pair* cur) { prepareRoot = parentDir(dir); task = new PrepareTask(); task->epochHex = f[1]; task->dayHex = f[2]; task->dir = dir; task->t0 = wallMs(); + task->wantClass = wantClass; task->wantEra = wantEra; task->thread = std::thread(prepareRun, &c, task); info(fmt("prepare started for epoch %.16s day %s from %s (NVRTC %s in the background)", f[1].c_str(), f[2].c_str(), dir.c_str(), c.archOpt.c_str())); continue; @@ -1115,7 +1144,7 @@ static int runServe(Ctx& c, const Options& o, Pair* cur) { pf_seed_words_from_bytes(daySeed, dl, kw); double t0 = wallMs(); bool switched = false; - if (!pairIs(cur, f[6], f[7]) && !task && !pairIs(prepared, f[6], f[7])) { + if (!pairIsClass(cur, f[6], f[7], wantClass, wantEra) && !task && !pairIsClass(prepared, f[6], f[7], wantClass, wantEra)) { // Self-heal: a job on seeds this worker has no pair for and no prepare in flight (a prepare failed, or // the miner never sent one). The miner writes a pack per pair under its --prepare-packs root; find it by // seeds.txt and build it now, in the foreground. The miner only re-sends prepare for the pair after this one. @@ -1131,11 +1160,18 @@ static int runServe(Ctx& c, const Options& o, Pair* cur) { else emit("error " + jobId + " could not build " + dir + ": " + berr); } } - if (!pairIs(cur, f[6], f[7])) { - if (pairIs(prepared, f[6], f[7])) { + if (!pairIsClass(cur, f[6], f[7], wantClass, wantEra)) { + char why[256]; + if (pairIsClass(prepared, f[6], f[7], wantClass, wantEra)) { if (old) releasePair(c, old); old = cur; cur = prepared; prepared = nullptr; switched = true; - info(fmt("switched to the prepared pair epoch %.16s day %s in %.2f ms", cur->epochHex.c_str(), cur->dayHex.c_str(), wallMs() - t0)); + info(fmt("switched to the prepared pair epoch %.16s day %s (class %s) in %.2f ms", cur->epochHex.c_str(), cur->dayHex.c_str(), cur->programClass.c_str(), wallMs() - t0)); + } else if (pairIs(cur, f[6], f[7]) && !pf_pack_class_ok(cur->programClass.c_str(), cur->eraHex.c_str(), wantClass.c_str(), wantEra.c_str(), why, sizeof(why))) { + // Counter ASIC 2.0: right seeds, wrong class or era. The miner prepares the pair again from a pack of + // the class the chain is on (the `need` line), and the pack of the other class is never mined. + emit(fmt("need %s %s", f[6].c_str(), f[7].c_str())); + emit(fmt("error %s pack %s: %s", jobId.c_str(), cur->dir.c_str(), why)); + continue; } else if (!hexEq(cur->epochHex, f[6])) { emit(fmt("need %s %s", f[6].c_str(), f[7].c_str())); // the miner prepares this pair (4 October 2026) emit(fmt("error %s epoch seed mismatch: this worker holds epoch %.16s (program words %08x %08x ...)%s, the job is for epoch %.16s (bare seed words %08x %08x ...); send prepare with a pack directory", diff --git a/proto-metal/main.swift b/proto-metal/main.swift index 5073f00f3..ef1ad029a 100644 --- a/proto-metal/main.swift +++ b/proto-metal/main.swift @@ -3033,9 +3033,24 @@ func runServe(_ opts: Options) -> Never { emit("ready metal \(gpu.device.name.replacingOccurrences(of: " ", with: "_")) dataset-log2 \(datasetLog2) batch \(batch) prepare \(opts.noPrepare ? 0 : 1) race \(opts.race)") var lastPair: (String, String)? = nil while let line = readLine(strippingNewline: true) { - let f = line.split(separator: " ").map(String.init) + var f = line.split(separator: " ").map(String.init) if f.isEmpty { continue } if f[0] == "quit" { break } + // Counter ASIC 2.0 (5 October 2026): a job or prepare line of a class v3 epoch ends with `class=v3 era=`. + // This worker regenerates programs from the seed with the Swift version 2 generator, so it refuses every + // class v3 line until the integration branch adds generator 3 here (the one-click workers build from packs + // and take v3 packs). + var wantClass = "" + while let last = f.last, last.hasPrefix("class=") || last.hasPrefix("era=") { + if last.hasPrefix("class=") { wantClass = String(last.dropFirst(6)) } + f.removeLast() + } + if wantClass != "" && wantClass != "v2" { + let id = f.count > 1 ? f[1] : "0" + if f[0] == "prepare" { emit("prepare-failed \(id) \(f.count > 2 ? f[2] : "0") program class \(wantClass) is not implemented by this worker (Swift generator version 2 only)") } + else { emit("error \(id) program class \(wantClass) is not implemented by this worker (Swift generator version 2 only); use the CUDA or OpenCL worker with a class \(wantClass) pack") } + continue + } if f[0] == "prepare" { if opts.noPrepare { emit("info ignored (started with --no-prepare): \(line)"); continue } if f.count < 3 { emit("prepare-failed 0 0 malformed prepare line (need epoch_seed_hex and day_seed_hex)"); continue } diff --git a/proto-opencl/host.c b/proto-opencl/host.c index bda66488a..fd313253f 100644 --- a/proto-opencl/host.c +++ b/proto-opencl/host.c @@ -1052,6 +1052,8 @@ typedef struct { double buildMs, cacheMs, datasetMs, checkMs; char check[1024]; /* the self-test verdict (packfile.h), one line */ int checked; + char programClass[8]; /* Counter ASIC 2.0: the pack's class ("v2" or "v3") and era seed, from packfile.h */ + char eraHex[65]; } ServePair; static int hexEq(const char* a, const char* b) { @@ -1071,6 +1073,18 @@ static int pairIs(const ServePair* p, const char* epochHex, const char* dayHex, return memcmp(sw, p->sw, 32) == 0 && memcmp(kw, p->kw, 32) == 0; } +/* Counter ASIC 2.0: a job that names a class (and an era) belongs to a pair of that class (and era) only. */ +static int pairIsClass(const ServePair* p, const char* epochHex, const char* dayHex, const uint32_t sw[8], const uint32_t kw[8], const char* cls, const char* era) { + char why[256]; + return pairIs(p, epochHex, dayHex, sw, kw) && pf_pack_class_ok(p->programClass[0] ? p->programClass : "v2", p->eraHex, cls, era, why, sizeof(why)); +} + +/* The trailing `class=` and `era=` tokens of a job or prepare line (absent on every class v2 line), removed from f. */ +static void takeClassTokens(char** f, int* nf, char* cls, size_t clsCap, char* era, size_t eraCap) { + cls[0] = 0; era[0] = 0; + while (*nf > 0 && pf_class_token(f[*nf - 1], cls, clsCap, era, eraCap)) --*nf; +} + static void releasePair(ServePair* p) { if (!p) return; if (p->ds) { clReleaseMemObject(p->ds); ++gMemReleased; } @@ -1089,6 +1103,8 @@ typedef struct { char epochHex[65]; char dayHex[512]; char packDir[1024]; + char wantClass[8]; /* the class and era the prepare line named (empty: any) */ + char wantEra[65]; uint32_t words, cacheWords, segments; char error[512]; ServePair* result; /* set by the thread on success */ @@ -1209,6 +1225,14 @@ static void prepareRun(PrepareTask* t) { double tb; strncpy(p->epochHex, t->epochHex, 64); p->epochHex[64] = 0; strncpy(p->dayHex, t->dayHex, sizeof(p->dayHex) - 1); + { + /* Counter ASIC 2.0: the pack's class and era are read before anything is built; a pack of another class + * than the prepare line names is refused here, so the miner exports one of the right class */ + PfPack pk; char perr[512]; char why[256]; + if (!pf_load(t->packDir, &pk, perr, sizeof(perr))) { snprintf(t->error, sizeof(t->error), "pack %s: %s", t->packDir, perr); releasePair(p); t->doneAt = wallMs(); t->done = 1; return; } + strncpy(p->programClass, pk.programClass, sizeof(p->programClass) - 1); strncpy(p->eraHex, pk.eraHex, sizeof(p->eraHex) - 1); + if (!pf_pack_class_ok(p->programClass, p->eraHex, t->wantClass, t->wantEra, why, sizeof(why))) { snprintf(t->error, sizeof(t->error), "pack %s: %s", t->packDir, why); releasePair(p); t->doneAt = wallMs(); t->done = 1; return; } + } snprintf(path, sizeof(path), "%s/kernel_bound.cl", t->packDir); src = readFile(path, &srcLen); if (!src) { snprintf(t->error, sizeof(t->error), "cannot read %s", path); releasePair(p); t->doneAt = wallMs(); t->done = 1; return; } @@ -1370,6 +1394,7 @@ static int runServe(Device* dv, const DeviceInfo* di, const Options* o) { memcpy(cur->sw, gPack.seedw, 32); memcpy(cur->kw, gPack.keyw, 32); strncpy(cur->epochHex, gPack.epochHex, 64); cur->epochHex[64] = 0; strncpy(cur->dayHex, gPack.dayHex, sizeof(cur->dayHex) - 1); + strncpy(cur->programClass, gPack.programClass, sizeof(cur->programClass) - 1); strncpy(cur->eraHex, gPack.eraHex, sizeof(cur->eraHex) - 1); if (!pairBuffers(dv, di, dv->q, cur, words, gServeCacheWords, gServeSegments, o->packDir, perr, sizeof(perr))) { printf("error 0 pack %s: %s\n", o->packDir, perr); fflush(stdout); return 1; } printf("info first pack %s: cache %.0f dataset %.0f check %.0f ms (%.0f ms in all); %s\n", o->packDir, cur->cacheMs, cur->datasetMs, cur->checkMs, wallMs() - t0, cur->check); } else { @@ -1462,11 +1487,12 @@ static int runServe(Device* dv, const DeviceInfo* di, const Options* o) { #define SERVE_FATAL(jobIdStr, fmt, ...) do { printf("error %s worker fault: " fmt "; exiting 3 so the miner restarts the worker\n", jobIdStr, __VA_ARGS__); fflush(stdout); exit(3); } while (0) #define SERVE_CHECK(jobIdStr, call) do { cl_int e_ = (call); if (e_ != CL_SUCCESS) SERVE_FATAL(jobIdStr, "%s failed with %s (%d)", #call, clErrName(e_), (int)e_); } while (0) while (fgets(line, sizeof(line), stdin)) { - char* f[9]; + char* f[12]; /* job + 7 fields, plus the class= and era= tokens of a class v3 line */ int nf = 0; char* tok; char* save = NULL; char jobId[64]; + char wantClass[8], wantEra[65]; uint8_t prehash[32], epochSeed[32], daySeed[256]; size_t prehashLen = 0, epochLen = 0, dayLen = 0; unsigned long long target = 0, nonceStart = 0, nonceCount = 0; @@ -1494,9 +1520,10 @@ static int runServe(Device* dv, const DeviceInfo* di, const Options* o) { fflush(stdout); free(task); task = NULL; } - for (tok = strtok_r(line, " ", &save); tok && nf < 9; tok = strtok_r(NULL, " ", &save)) f[nf++] = tok; + for (tok = strtok_r(line, " ", &save); tok && nf < 12; tok = strtok_r(NULL, " ", &save)) f[nf++] = tok; if (nf == 0) continue; if (strcmp(f[0], "quit") == 0) break; + takeClassTokens(f, &nf, wantClass, sizeof(wantClass), wantEra, sizeof(wantEra)); if (strcmp(f[0], "prepare") == 0) { if (o->noPrepare) { printf("info ignored (started with --no-prepare): prepare\n"); fflush(stdout); continue; } if (nf < 4) { printf("prepare-failed %s %s this ahead-of-time worker needs a pack directory as the third field (igneum-miner --prepare-packs )\n", nf > 1 ? f[1] : "0", nf > 2 ? f[2] : "0"); fflush(stdout); continue; } @@ -1506,6 +1533,7 @@ static int runServe(Device* dv, const DeviceInfo* di, const Options* o) { task = (PrepareTask*)calloc(1, sizeof(PrepareTask)); task->dv = dv; task->di = di; task->words = words; task->cacheWords = gServeCacheWords; task->segments = gServeSegments; task->t0 = wallMs(); strncpy(task->epochHex, f[1], 64); strncpy(task->dayHex, f[2], sizeof(task->dayHex) - 1); strncpy(task->packDir, f[3], sizeof(task->packDir) - 1); + strncpy(task->wantClass, wantClass, sizeof(task->wantClass) - 1); strncpy(task->wantEra, wantEra, sizeof(task->wantEra) - 1); if (!startPrepareThread(task)) { printf("prepare-failed %s %s cannot start the prepare thread\n", f[1], f[2]); fflush(stdout); free(task); task = NULL; continue; } printf("info prepare started for epoch %.16s day %s from %s (builds in the background)\n", f[1], f[2], f[3]); fflush(stdout); continue; @@ -1522,12 +1550,19 @@ static int runServe(Device* dv, const DeviceInfo* di, const Options* o) { seedWordsFromBytes(epochSeed, 32, sw); seedWordsFromBytes(daySeed, dayLen, kw); t0 = wallMs(); - if (!pairIs(cur, f[6], f[7], sw, kw)) { - if (pairIs(prepared, f[6], f[7], sw, kw)) { + if (!pairIsClass(cur, f[6], f[7], sw, kw, wantClass, wantEra)) { + char why[256]; + if (pairIsClass(prepared, f[6], f[7], sw, kw, wantClass, wantEra)) { /* The prepared pair: switch now, release the old one after this job */ if (old) releasePair(old); old = cur; cur = prepared; prepared = NULL; switched = 1; - printf("info switched to the prepared pair epoch %.16s day %s in %.2f ms\n", cur->epochHex, cur->dayHex, wallMs() - t0); fflush(stdout); + printf("info switched to the prepared pair epoch %.16s day %s (class %s) in %.2f ms\n", cur->epochHex, cur->dayHex, cur->programClass[0] ? cur->programClass : "v2", wallMs() - t0); fflush(stdout); + } else if (pairIs(cur, f[6], f[7], sw, kw) && !pf_pack_class_ok(cur->programClass[0] ? cur->programClass : "v2", cur->eraHex, wantClass, wantEra, why, sizeof(why))) { + /* Counter ASIC 2.0: right seeds, wrong class or era; the miner prepares the pair from a pack of the + * class the chain is on (the need line), and this pack is never mined */ + printf("need %s %s\n", f[6], f[7]); + printf("error %s pack %s: %s\n", jobId, o->packDir ? o->packDir : "(compiled-in)", why); + fflush(stdout); continue; } else if (cur->epochHex[0] ? !hexEq(cur->epochHex, f[6]) : memcmp(sw, cur->sw, 32) != 0) { printf("need %s %s\n", f[6], f[7]); /* the miner prepares this pair (4 October 2026) */ printf("error %s epoch seed mismatch: this worker holds %s%s (program words %08x %08x ...)%s, the job is for epoch %.16s (bare seed words %08x %08x ...); send prepare with a pack directory, or run igneum-miner export-pack and rebuild\n",