Workers: the program class and era on the serve protocol; a pack of the wrong class or era is refused (Counter ASIC 2.0)

packfile.h: pf_load refuses a generator other than 2 or 3 (spec 01 section 1.4.5), reads IGNEUM_PROGRAM_CLASS
(must match the generator) and IGNEUM_ERA_SEED_HEX; pf_pack_class_ok and pf_class_token are the one rule for
the `class=<v2|v3> era=<hex>` tokens a job or prepare line of a class v3 epoch ends with (a v2 line is the
line of before, byte for byte). CUDA worker.cpp and OpenCL host.c: a pair's identity includes its class and era
when the line names them, so a prepared pack of the right class wins over the resident pack of the same seeds;
right seeds with the wrong class answer `need` plus `error <id> pack <dir>: program class mismatch ...`, and a
prepare on a pack of the wrong class fails in plain words. Metal main.swift: refuses every class v3 line until
the Swift generator carries version 3 (the integration branch). emu/packfile-test.c: the generator rule, a v3
pack with its era, the token matcher, the contradicting class line (13 checks pass).
igneum-pow verify.rs: Epoch::chain_dataset(day, class), the one entry the node's day cache goes through, so the
ca2-mixer class-specific item construction and cache schedule have a seam.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-05 20:33:59 +00:00
parent 1d69ba80c0
commit d29e21b1cd
6 changed files with 182 additions and 14 deletions

View file

@ -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)

View file

@ -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);

View file

@ -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=<v2|v3>` and `era=<hex>` 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

View file

@ -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<std::string>& 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<bool> 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 <dir>)", 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",

View file

@ -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=<hex>`.
// 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 }

View file

@ -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 <dir>)\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",