470 lines
26 KiB
C
470 lines
26 KiB
C
// packfile.h: read a program pack at run time (C99, header only). 4 October 2026.
|
|
//
|
|
// The one-click workers (proto-cuda/nvrtc/worker.cpp and proto-opencl/host.c with --pack) are built once and serve
|
|
// any pack, so the constants that host.cu and host.c take from program.h and vectors.h at compile time are read
|
|
// here from the same files at run time: the dataset and cache sizes, the seed words, the seeds as hex (seeds.txt,
|
|
// written by igneum-miner export-pack and --prepare-packs; program.h carries the same bytes), and the self-test
|
|
// values (cache head, last line and FNV-1a 64; dataset head, last word and 64 samples; the three vector warps).
|
|
// No JSON parser: the headers are scanned for "#define NAME value" and "NAME[..] = { numbers }". Comments are skipped.
|
|
//
|
|
// Included by C (proto-opencl/host.c) and C++ (worker.cpp). Everything is static.
|
|
#ifndef IGNEUM_PACKFILE_H
|
|
#define IGNEUM_PACKFILE_H
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
|
|
#define PF_MAX_WARPS 8
|
|
#define PF_MAX_SAMPLES 256
|
|
#define PF_HEX_CAP 520
|
|
|
|
typedef struct {
|
|
// program.h
|
|
uint32_t datasetLog2, cacheLog2Words, cacheSegments, datasetMode, generator;
|
|
uint32_t attempt; // IGNEUM_PROGRAM_ATTEMPT: seedw are the words of this attempt of the epoch seed (0 = bare seed)
|
|
uint32_t seedw[8], keyw[8];
|
|
char seedString[600];
|
|
// 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", "v3" (Counter ASIC 2.0) or "v4" (Counter ASIC 3.0); absent = the generator's class */
|
|
char eraHex[65]; /* IGNEUM_ERA_SEED_HEX of a class v3 or v4 chain pack; empty otherwise */
|
|
// Counter ASIC 2.0 (5 October 2026): the mixer multiplier of the item derivation (IGNEUM_MIXER_MULT, 1 when absent:
|
|
// version 2; 4 under class v3). The emitted memhard.h / kernel.cl carry it in their text; this is for the log lines.
|
|
uint32_t mixerMult;
|
|
// hot-table experiment (5 October 2026, docs/plans/hot-table.md): hotMb 0 when the pack has no hot table; the
|
|
// table is filled on the device from the pack's igneum_hot_fill (never shipped), its self-test values from vectors.h
|
|
uint32_t hotMb, hotWords, hotSegments, hotSlots;
|
|
uint32_t hotKey[8];
|
|
int haveHot;
|
|
uint32_t hotHead[16], hotLast[16];
|
|
uint64_t hotFnv;
|
|
// seeds.txt (or program.h): the seeds as the worker protocol carries them
|
|
char epochHex[65];
|
|
char dayHex[PF_HEX_CAP];
|
|
// vectors.h
|
|
int haveVectors;
|
|
int vecWarps;
|
|
uint32_t vecBase[PF_MAX_WARPS];
|
|
uint64_t vecOut[PF_MAX_WARPS][32];
|
|
uint32_t dsHead[16];
|
|
uint32_t dsLastIndex, dsLast;
|
|
int nSamples;
|
|
uint32_t sampleIdx[PF_MAX_SAMPLES], sampleVal[PF_MAX_SAMPLES];
|
|
uint32_t cacheHead[16], cacheLast[16];
|
|
uint64_t cacheFnv;
|
|
} PfPack;
|
|
|
|
static char* pf_read_file(const char* path, size_t* len) {
|
|
FILE* f = fopen(path, "rb");
|
|
char* buf;
|
|
long n;
|
|
if (!f) return NULL;
|
|
fseek(f, 0, SEEK_END); n = ftell(f); fseek(f, 0, SEEK_SET);
|
|
if (n < 0) { fclose(f); return NULL; }
|
|
buf = (char*)malloc((size_t)n + 1);
|
|
if (!buf) { fclose(f); return NULL; }
|
|
if (fread(buf, 1, (size_t)n, f) != (size_t)n) { fclose(f); free(buf); return NULL; }
|
|
buf[n] = 0;
|
|
fclose(f);
|
|
if (len) *len = (size_t)n;
|
|
return buf;
|
|
}
|
|
|
|
static char* pf_read_pack_file(const char* dir, const char* name, size_t* len) {
|
|
char path[2048];
|
|
snprintf(path, sizeof(path), "%s/%s", dir, name);
|
|
return pf_read_file(path, len);
|
|
}
|
|
|
|
// The text after "#define NAME" (NAME followed by a space or tab) at the start of a line, or NULL.
|
|
static const char* pf_find_define(const char* text, const char* name) {
|
|
size_t n = strlen(name);
|
|
const char* p = text;
|
|
while ((p = strstr(p, "#define ")) != NULL) {
|
|
const char* q = p + 8;
|
|
if (p == text || p[-1] == '\n' || p[-1] == '\r') {
|
|
if (strncmp(q, name, n) == 0 && (q[n] == ' ' || q[n] == '\t')) return q + n;
|
|
}
|
|
p = q;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// A number with an optional C suffix (u, U, ull, ULL, l, L). *end is left after the suffix.
|
|
static int pf_number(const char* s, uint64_t* out, const char** end) {
|
|
char* e = NULL;
|
|
unsigned long long v;
|
|
if (!(isdigit((unsigned char)s[0]))) return 0;
|
|
v = strtoull(s, &e, 0);
|
|
if (e == s) return 0;
|
|
while (*e == 'u' || *e == 'U' || *e == 'l' || *e == 'L') ++e;
|
|
*out = (uint64_t)v;
|
|
*end = e;
|
|
return 1;
|
|
}
|
|
|
|
static int pf_define_u32(const char* text, const char* name, uint32_t* out) {
|
|
const char* p = pf_find_define(text, name);
|
|
uint64_t v;
|
|
const char* e;
|
|
if (!p) return 0;
|
|
while (*p == ' ' || *p == '\t') ++p;
|
|
if (!pf_number(p, &v, &e)) return 0;
|
|
*out = (uint32_t)v;
|
|
return 1;
|
|
}
|
|
|
|
static int pf_define_str(const char* text, const char* name, char* out, size_t cap) {
|
|
const char* p = pf_find_define(text, name);
|
|
const char* q;
|
|
size_t n;
|
|
if (!p) return 0;
|
|
while (*p == ' ' || *p == '\t') ++p;
|
|
if (*p != '"') return 0;
|
|
++p;
|
|
q = strchr(p, '"');
|
|
if (!q) return 0;
|
|
n = (size_t)(q - p);
|
|
if (n + 1 > cap) n = cap - 1;
|
|
memcpy(out, p, n); out[n] = 0;
|
|
return 1;
|
|
}
|
|
|
|
// Numbers inside the braces that follow `start`, comments and commas skipped, nested braces flattened.
|
|
// Returns the count read (at most max). `start` points at or before the opening brace.
|
|
static int pf_brace_numbers(const char* start, uint64_t* out, int max) {
|
|
const char* p = start;
|
|
int depth = 0, n = 0;
|
|
while (*p && *p != '{') { if (*p == ';') return 0; ++p; }
|
|
if (*p != '{') return 0;
|
|
for (;;) {
|
|
uint64_t v;
|
|
const char* e;
|
|
if (!*p) break;
|
|
if (*p == '/' && p[1] == '/') { while (*p && *p != '\n') ++p; continue; }
|
|
if (*p == '/' && p[1] == '*') { const char* c = strstr(p + 2, "*/"); if (!c) break; p = c + 2; continue; }
|
|
if (*p == '{') { ++depth; ++p; continue; }
|
|
if (*p == '}') { --depth; ++p; if (depth == 0) break; continue; }
|
|
if (pf_number(p, &v, &e)) { if (n < max) out[n] = v; ++n; p = e; continue; }
|
|
++p;
|
|
}
|
|
return n < max ? n : max;
|
|
}
|
|
|
|
static int pf_define_words(const char* text, const char* name, uint32_t* out, int max) {
|
|
const char* p = pf_find_define(text, name);
|
|
uint64_t tmp[PF_MAX_SAMPLES];
|
|
int n, i;
|
|
if (!p) return 0;
|
|
if (max > PF_MAX_SAMPLES) max = PF_MAX_SAMPLES;
|
|
n = pf_brace_numbers(p, tmp, max);
|
|
for (i = 0; i < n; ++i) out[i] = (uint32_t)tmp[i];
|
|
return n;
|
|
}
|
|
|
|
// "NAME" as a C symbol: `... NAME[...] = { ... };` or `... NAME = value;`. Returns the numbers found (at most max).
|
|
static int pf_symbol_numbers(const char* text, const char* name, uint64_t* out, int max) {
|
|
size_t n = strlen(name);
|
|
const char* p = text;
|
|
while ((p = strstr(p, name)) != NULL) {
|
|
const char* q = p + n;
|
|
int before = (p == text) || !(isalnum((unsigned char)p[-1]) || p[-1] == '_');
|
|
p = q;
|
|
if (!before) continue;
|
|
if (isalnum((unsigned char)*q) || *q == '_') continue;
|
|
while (*q == '[' ) { const char* c = strchr(q, ']'); if (!c) return 0; q = c + 1; }
|
|
while (*q == ' ' || *q == '\t') ++q;
|
|
if (*q != '=') continue;
|
|
++q;
|
|
while (*q == ' ' || *q == '\t') ++q;
|
|
if (*q == '{') return pf_brace_numbers(q, out, max);
|
|
{ uint64_t v; const char* e; if (pf_number(q, &v, &e)) { if (max > 0) out[0] = v; return max > 0 ? 1 : 0; } }
|
|
return 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int pf_symbol_u32s(const char* text, const char* name, uint32_t* out, int max) {
|
|
uint64_t tmp[PF_MAX_SAMPLES];
|
|
int n, i;
|
|
if (max > PF_MAX_SAMPLES) max = PF_MAX_SAMPLES;
|
|
n = pf_symbol_numbers(text, name, tmp, max);
|
|
for (i = 0; i < n; ++i) out[i] = (uint32_t)tmp[i];
|
|
return n;
|
|
}
|
|
|
|
static int pf_unhex(const char* s, uint8_t* out, size_t cap, size_t* len) {
|
|
size_t n = strlen(s), i;
|
|
if (n % 2 || n / 2 > cap) return 0;
|
|
for (i = 0; i < n; i += 2) {
|
|
unsigned v = 0;
|
|
char two[3];
|
|
two[0] = s[i]; two[1] = s[i + 1]; two[2] = 0;
|
|
if (!isxdigit((unsigned char)two[0]) || !isxdigit((unsigned char)two[1])) return 0;
|
|
if (sscanf(two, "%2x", &v) != 1) return 0;
|
|
out[i / 2] = (uint8_t)v;
|
|
}
|
|
*len = n / 2;
|
|
return 1;
|
|
}
|
|
|
|
// seed_words_from_bytes of igneum-pow/src/seed.rs: FNV-1a 64 with four salts, each finalised.
|
|
static void pf_seed_words_from_bytes(const uint8_t* b, size_t n, uint32_t out[8]) {
|
|
uint64_t salt;
|
|
for (salt = 0; salt < 4; ++salt) {
|
|
uint64_t h = 0xcbf29ce484222325ull ^ (salt * 0x9E3779B97F4A7C15ull);
|
|
size_t i;
|
|
for (i = 0; i < n; ++i) { h ^= b[i]; h *= 0x100000001b3ull; }
|
|
h ^= h >> 33; h *= 0xff51afd7ed558ccdull; h ^= h >> 33;
|
|
out[2 * salt] = (uint32_t)h;
|
|
out[2 * salt + 1] = (uint32_t)(h >> 32);
|
|
}
|
|
}
|
|
|
|
// attempt_words of igneum-pow/src/generator.rs: the words of attempt k of a program seed are
|
|
// seed_words_from_bytes(seed || k_le32) for k > 0 and the bare seed's words for k = 0. The generator retries a
|
|
// rejected candidate with the next attempt, so a pack's IGNEUM_SEEDW_INIT is the words of IGNEUM_PROGRAM_ATTEMPT,
|
|
// not of the bare seed. 5 October 2026: pf_load derived the expected words from the bare seed and refused every
|
|
// pack of a retried program (epoch 34, both PCs, the miner restarting the worker for an hour); this is the rule.
|
|
static void pf_program_words(const uint8_t* b, size_t n, uint32_t attempt, uint32_t out[8]) {
|
|
uint8_t buf[256 + 4];
|
|
if (attempt == 0) { pf_seed_words_from_bytes(b, n, out); return; }
|
|
if (n > 256) n = 256;
|
|
memcpy(buf, b, n);
|
|
buf[n] = (uint8_t)attempt; buf[n + 1] = (uint8_t)(attempt >> 8); buf[n + 2] = (uint8_t)(attempt >> 16); buf[n + 3] = (uint8_t)(attempt >> 24);
|
|
pf_seed_words_from_bytes(buf, n + 4, out);
|
|
}
|
|
|
|
static uint64_t pf_fnv1a64(const void* p, size_t n) {
|
|
const uint8_t* b = (const uint8_t*)p;
|
|
uint64_t h = 0xcbf29ce484222325ull;
|
|
size_t i;
|
|
for (i = 0; i < n; ++i) { h ^= b[i]; h *= 0x100000001b3ull; }
|
|
return h;
|
|
}
|
|
|
|
// One "key value" line of seeds.txt.
|
|
static int pf_seeds_line(const char* text, const char* key, char* out, size_t cap) {
|
|
size_t n = strlen(key);
|
|
const char* p = text;
|
|
while ((p = strstr(p, key)) != NULL) {
|
|
if ((p == text || p[-1] == '\n') && p[n] == ' ') {
|
|
const char* q = p + n + 1;
|
|
size_t len = 0;
|
|
while (q[len] && q[len] != '\r' && q[len] != '\n') ++len;
|
|
if (len + 1 > cap) return 0;
|
|
memcpy(out, q, len); out[len] = 0;
|
|
return 1;
|
|
}
|
|
p += n;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int pf_fail(char* err, size_t cap, const char* msg) { if (err && cap) { strncpy(err, msg, cap - 1); err[cap - 1] = 0; } return 0; }
|
|
|
|
// Reads program.h, seeds.txt (optional) and vectors.h (optional) of a pack directory. Returns 1 on success.
|
|
static int pf_load(const char* dir, PfPack* pk, char* err, size_t cap) {
|
|
char* prog;
|
|
char* seeds;
|
|
char* vec;
|
|
char ehex[65] = {0}, dhex[PF_HEX_CAP] = {0};
|
|
uint8_t bytes[256];
|
|
size_t blen;
|
|
memset(pk, 0, sizeof(*pk));
|
|
prog = pf_read_pack_file(dir, "program.h", NULL);
|
|
if (!prog) { char m[480]; snprintf(m, sizeof(m), "cannot read %.400s/program.h", dir); return pf_fail(err, cap, m); }
|
|
if (!pf_define_u32(prog, "IGNEUM_DATASET_LOG2", &pk->datasetLog2)) { free(prog); return pf_fail(err, cap, "program.h has no IGNEUM_DATASET_LOG2"); }
|
|
if (!pf_define_u32(prog, "IGNEUM_DATASET_MODE", &pk->datasetMode)) pk->datasetMode = 0;
|
|
if (pk->datasetMode != 1) { free(prog); return pf_fail(err, cap, "the pack is not memory-hard (IGNEUM_DATASET_MODE 1); the one-click workers serve memory-hard packs only"); }
|
|
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), generator 4
|
|
* is class v4 (Counter ASIC 3.0, 6 October 2026: class v3 plus the latency-shadow block, emitted in the pack's own
|
|
* kernel text, so this loader needs nothing new beyond the number and the class token). */
|
|
if (pk->generator != 2 && pk->generator != 3 && pk->generator != 4) {
|
|
char m[200]; snprintf(m, sizeof(m), "program pack generator %u is not a generator version this worker runs (2, 3 or 4)", (unsigned)pk->generator);
|
|
free(prog); return pf_fail(err, cap, m);
|
|
}
|
|
strcpy(pk->programClass, pk->generator == 4 ? "v4" : pk->generator == 3 ? "v3" : "v2");
|
|
{
|
|
/* Counter ASIC 3.0 (6 October 2026): the shadow block marks class v4. A generator 3 pack with IGNEUM_SHADOW_INSTRS
|
|
* is a v4 program stamped as v3 (the old export path; it carried the v3 control's program id) and is refused;
|
|
* a generator 4 pack without the block is no v4 pack. */
|
|
uint32_t shadow = 0;
|
|
if (!pf_define_u32(prog, "IGNEUM_SHADOW_INSTRS", &shadow)) shadow = 0;
|
|
if (pk->generator == 4 && shadow == 0) { free(prog); return pf_fail(err, cap, "program pack generator 4 (class v4) without IGNEUM_SHADOW_INSTRS: not a class v4 pack"); }
|
|
if (pk->generator == 3 && shadow != 0) { /* a generator 2 pack with a shadow is the measurement ladder (a class-bearing id) and loads */
|
|
char m[220]; snprintf(m, sizeof(m), "program pack generator %u with a shadow block (IGNEUM_SHADOW_INSTRS %u): a class v4 program is generator 4 (export the pack as class v4)", (unsigned)pk->generator, (unsigned)shadow);
|
|
free(prog); return pf_fail(err, cap, m);
|
|
}
|
|
}
|
|
{
|
|
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);
|
|
pk->bytesPerHash = pk->loadsPerHash * 4u; pf_define_u32(prog, "IGNEUM_BYTES_PER_HASH", &pk->bytesPerHash);
|
|
pk->scratchOps = 0; pf_define_u32(prog, "IGNEUM_SCRATCH_OPS", &pk->scratchOps);
|
|
pk->persistent = 0; pf_define_u32(prog, "IGNEUM_PERSISTENT_WARPS", &pk->persistent);
|
|
pk->scratchWordsPerLane = 8192; pf_define_u32(prog, "IGNEUM_SCRATCH_WORDS_PER_LANE", &pk->scratchWordsPerLane);
|
|
strcpy(pk->loadClass, "v2"); pf_define_str(prog, "IGNEUM_LOAD_CLASS", pk->loadClass, sizeof(pk->loadClass));
|
|
pk->mixerMult = 1; pf_define_u32(prog, "IGNEUM_MIXER_MULT", &pk->mixerMult);
|
|
pk->hotMb = 0; pf_define_u32(prog, "IGNEUM_HOT_MB", &pk->hotMb);
|
|
if (pk->hotMb) {
|
|
if (!pf_define_u32(prog, "IGNEUM_HOT_WORDS", &pk->hotWords) || !pf_define_u32(prog, "IGNEUM_HOT_SEGMENTS", &pk->hotSegments) ||
|
|
!pf_define_u32(prog, "IGNEUM_HOT_SLOTS", &pk->hotSlots) || pf_define_words(prog, "IGNEUM_HOT_KEY_INIT", pk->hotKey, 8) != 8) { free(prog); return pf_fail(err, cap, "program.h has IGNEUM_HOT_MB but not IGNEUM_HOT_WORDS, IGNEUM_HOT_SEGMENTS, IGNEUM_HOT_SLOTS and IGNEUM_HOT_KEY_INIT"); }
|
|
if (pk->hotWords != pk->hotMb * 262144u || pk->hotSegments != pk->hotMb * 256u || pk->hotMb > 4096u) { free(prog); return pf_fail(err, cap, "program.h hot table sizes disagree (words must be MiB x 2^18, segments MiB x 256)"); }
|
|
}
|
|
if (pf_define_words(prog, "IGNEUM_KEY_INIT", pk->keyw, 8) != 8) { free(prog); return pf_fail(err, cap, "program.h has no IGNEUM_KEY_INIT with 8 words"); }
|
|
if (!pf_define_str(prog, "IGNEUM_SEED_STRING", pk->seedString, sizeof(pk->seedString))) strncpy(pk->seedString, "(no IGNEUM_SEED_STRING)", sizeof(pk->seedString) - 1);
|
|
pf_define_str(prog, "IGNEUM_SEED_BYTES_HEX", ehex, sizeof(ehex));
|
|
pf_define_str(prog, "IGNEUM_DAY_BYTES_HEX", dhex, sizeof(dhex));
|
|
if (pk->datasetLog2 < 20 || pk->datasetLog2 > 31 || pk->cacheLog2Words < 16 || pk->cacheLog2Words > 30 || pk->cacheSegments == 0) { free(prog); return pf_fail(err, cap, "program.h sizes out of range"); }
|
|
free(prog);
|
|
|
|
// seeds.txt wins when present; program.h's bytes must agree with it
|
|
seeds = pf_read_pack_file(dir, "seeds.txt", NULL);
|
|
if (seeds) {
|
|
char e2[65] = {0}, d2[PF_HEX_CAP] = {0};
|
|
int he = pf_seeds_line(seeds, "epoch_seed_hex", e2, sizeof(e2));
|
|
int hd = pf_seeds_line(seeds, "day_seed_hex", d2, sizeof(d2));
|
|
free(seeds);
|
|
if (!he || !hd) return pf_fail(err, cap, "seeds.txt has no epoch_seed_hex / day_seed_hex line");
|
|
if (ehex[0] && strcmp(ehex, e2) != 0) return pf_fail(err, cap, "seeds.txt epoch_seed_hex differs from program.h IGNEUM_SEED_BYTES_HEX");
|
|
if (dhex[0] && strcmp(dhex, d2) != 0) return pf_fail(err, cap, "seeds.txt day_seed_hex differs from program.h IGNEUM_DAY_BYTES_HEX");
|
|
strcpy(ehex, e2); strcpy(dhex, d2);
|
|
}
|
|
if (!ehex[0] || !dhex[0]) return pf_fail(err, cap, "no seeds: neither seeds.txt nor IGNEUM_SEED_BYTES_HEX / IGNEUM_DAY_BYTES_HEX in program.h (a pack from igneum-pow export --seed <name> has no byte seeds)");
|
|
// The chain's epoch seed is 32 bytes (64 hex characters). A pack exported from a seed STRING (igneum-pow export
|
|
// --seed <name>, the read-width experiment's packs of 5 October 2026) carries the string's bytes instead; the
|
|
// seed-word re-derivation below checks either form, so any even-length hex seed is accepted here. The serve
|
|
// protocol still carries 64-hex seeds; a string-seed pack can only be benched (--bench, --bench-pack, --check).
|
|
if (strlen(ehex) < 2 || strlen(ehex) % 2 != 0) return pf_fail(err, cap, "epoch seed is not an even-length hex string");
|
|
strcpy(pk->epochHex, ehex); strcpy(pk->dayHex, dhex);
|
|
// The seed words derived from the bytes AND the attempt must be the pack's own words: otherwise the pack and
|
|
// its seeds disagree (a half rewritten directory, or an exporter on another rule)
|
|
{
|
|
uint32_t w[8];
|
|
char m[400];
|
|
if (!pf_unhex(ehex, bytes, sizeof(bytes), &blen) || blen == 0) return pf_fail(err, cap, "epoch seed hex is malformed");
|
|
pf_program_words(bytes, blen, pk->attempt, w);
|
|
if (memcmp(w, pk->seedw, 32) != 0) {
|
|
snprintf(m, sizeof(m), "program pack and its seeds disagree: IGNEUM_SEEDW_INIT is not attempt %u of the epoch seed %.16s (attempt %u gives %08x %08x ..., the pack has %08x %08x ...); run igneum-miner export-pack again",
|
|
(unsigned)pk->attempt, ehex, (unsigned)pk->attempt, w[0], w[1], pk->seedw[0], pk->seedw[1]);
|
|
return pf_fail(err, cap, m);
|
|
}
|
|
if (!pf_unhex(dhex, bytes, sizeof(bytes), &blen)) return pf_fail(err, cap, "day seed hex is malformed");
|
|
pf_seed_words_from_bytes(bytes, blen, w);
|
|
if (memcmp(w, pk->keyw, 32) != 0) return pf_fail(err, cap, "the day seed bytes do not give the pack's IGNEUM_KEY_INIT (wrong seeds.txt for this pack?)");
|
|
}
|
|
|
|
vec = pf_read_pack_file(dir, "vectors.h", NULL);
|
|
if (vec) {
|
|
uint64_t tmp[PF_MAX_WARPS * 32];
|
|
uint32_t warps = 0;
|
|
int n, w, l;
|
|
if (pf_define_u32(vec, "IGNEUM_VEC_WARPS", &warps) && warps >= 1 && warps <= PF_MAX_WARPS) {
|
|
pk->vecWarps = (int)warps;
|
|
n = pf_symbol_u32s(vec, "IGNEUM_VEC_BASE", pk->vecBase, PF_MAX_WARPS);
|
|
if (n != pk->vecWarps) pk->vecWarps = 0;
|
|
n = pf_symbol_numbers(vec, "IGNEUM_VEC_OUT", tmp, PF_MAX_WARPS * 32);
|
|
if (n != pk->vecWarps * 32) pk->vecWarps = 0;
|
|
for (w = 0; w < pk->vecWarps; ++w) for (l = 0; l < 32; ++l) pk->vecOut[w][l] = tmp[w * 32 + l];
|
|
}
|
|
if (pf_symbol_u32s(vec, "IGNEUM_DS_HEAD", pk->dsHead, 16) == 16 &&
|
|
pf_symbol_u32s(vec, "IGNEUM_DS_LAST_INDEX", &pk->dsLastIndex, 1) == 1 &&
|
|
pf_symbol_u32s(vec, "IGNEUM_DS_LAST", &pk->dsLast, 1) == 1 &&
|
|
pf_symbol_u32s(vec, "IGNEUM_CACHE_HEAD", pk->cacheHead, 16) == 16 &&
|
|
pf_symbol_u32s(vec, "IGNEUM_CACHE_LAST", pk->cacheLast, 16) == 16 &&
|
|
pf_symbol_numbers(vec, "IGNEUM_CACHE_FNV64", &pk->cacheFnv, 1) == 1 && pk->vecWarps > 0) {
|
|
uint32_t ns = 0;
|
|
pk->haveVectors = 1;
|
|
if (pk->hotMb && pf_symbol_u32s(vec, "IGNEUM_HOT_HEAD", pk->hotHead, 16) == 16 &&
|
|
pf_symbol_u32s(vec, "IGNEUM_HOT_LAST", pk->hotLast, 16) == 16 &&
|
|
pf_symbol_numbers(vec, "IGNEUM_HOT_FNV64", &pk->hotFnv, 1) == 1) pk->haveHot = 1;
|
|
if (pf_define_u32(vec, "IGNEUM_DS_SAMPLES", &ns) && ns > 0 && ns <= PF_MAX_SAMPLES) {
|
|
int a = pf_symbol_u32s(vec, "IGNEUM_DS_SAMPLE_INDEX", pk->sampleIdx, (int)ns);
|
|
int b = pf_symbol_u32s(vec, "IGNEUM_DS_SAMPLE_VALUE", pk->sampleVal, (int)ns);
|
|
pk->nSamples = (a == (int)ns && b == (int)ns) ? (int)ns : 0;
|
|
}
|
|
}
|
|
free(vec);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
// The self-test verdict from values the host read back from the device. `vec` holds vecWarps x 32 outputs of the
|
|
// bound kernel run with the pack's own seed words as init words (that is igneum_hash of kernel.cu). Writes one line.
|
|
// A hot-table pack (pk->hotMb) also hands the hot table's head, last line and FNV-1a 64 (NULL and 0 otherwise); a
|
|
// hot pack whose vectors.h carries no hot values is not checked on the table (the vectors cover it) and says so.
|
|
static int pf_selftest(const PfPack* pk, const uint32_t* cacheHead, const uint32_t* cacheLast, uint64_t cacheFnv,
|
|
const uint32_t* dsHead, uint32_t dsLast, const uint32_t* sampleVals, const uint64_t* vec,
|
|
const uint32_t* hotHead, const uint32_t* hotLast, uint64_t hotFnv,
|
|
char* out, size_t cap) {
|
|
int okCH = memcmp(cacheHead, pk->cacheHead, 64) == 0, okCL = memcmp(cacheLast, pk->cacheLast, 64) == 0;
|
|
int okFnv = (cacheFnv == pk->cacheFnv);
|
|
int okDH = memcmp(dsHead, pk->dsHead, 64) == 0, okDL = (dsLast == pk->dsLast);
|
|
int hotChecked = (pk->hotMb && pk->haveHot && hotHead && hotLast);
|
|
int okHot = !hotChecked || (memcmp(hotHead, pk->hotHead, 64) == 0 && memcmp(hotLast, pk->hotLast, 64) == 0 && hotFnv == pk->hotFnv);
|
|
int badS = 0, badV = 0, i, w, l, firstBadWarp = -1, firstBadLane = -1;
|
|
for (i = 0; i < pk->nSamples; ++i) if (sampleVals[i] != pk->sampleVal[i]) ++badS;
|
|
for (w = 0; w < pk->vecWarps; ++w) for (l = 0; l < 32; ++l) if (vec[w * 32 + l] != pk->vecOut[w][l]) { if (firstBadWarp < 0) { firstBadWarp = w; firstBadLane = l; } ++badV; }
|
|
if (okCH && okCL && okFnv && okDH && okDL && okHot && badS == 0 && badV == 0) {
|
|
if (pk->hotMb)
|
|
snprintf(out, cap, "self-test PASS (cache head, last line and FNV-1a 64 %016llx; dataset head, word [%u] and %d samples; hot table %u MiB %s; %d of %d vector lanes)",
|
|
(unsigned long long)cacheFnv, pk->dsLastIndex, pk->nSamples, pk->hotMb, hotChecked ? "head, last line and FNV-1a 64 ok" : "not in vectors.h (the vectors cover it)", pk->vecWarps * 32, pk->vecWarps * 32);
|
|
else
|
|
snprintf(out, cap, "self-test PASS (cache head, last line and FNV-1a 64 %016llx; dataset head, word [%u] and %d samples; %d of %d vector lanes)",
|
|
(unsigned long long)cacheFnv, pk->dsLastIndex, pk->nSamples, pk->vecWarps * 32, pk->vecWarps * 32);
|
|
return 1;
|
|
}
|
|
snprintf(out, cap, "self-test FAIL (cache head %s, cache last %s, cache FNV %016llx vs pack %016llx %s, dataset head %s, dataset last %s, samples %d bad of %d, hot table %s, vector lanes %d bad of %d%s)",
|
|
okCH ? "ok" : "BAD", okCL ? "ok" : "BAD", (unsigned long long)cacheFnv, (unsigned long long)pk->cacheFnv, okFnv ? "ok" : "BAD",
|
|
okDH ? "ok" : "BAD", okDL ? "ok" : "BAD", badS, pk->nSamples, hotChecked ? (okHot ? "ok" : "BAD") : "none", badV, pk->vecWarps * 32,
|
|
firstBadWarp >= 0 ? " (first bad lane in the warp at base nonce" : "");
|
|
if (firstBadWarp >= 0) {
|
|
size_t n = strlen(out);
|
|
snprintf(out + n, cap > n ? cap - n : 0, " %u lane %d: device %016llx expected %016llx)", pk->vecBase[firstBadWarp], firstBadLane,
|
|
(unsigned long long)vec[firstBadWarp * 32 + firstBadLane], (unsigned long long)pk->vecOut[firstBadWarp][firstBadLane]);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
/* Counter ASIC 2.0 (5 October 2026): a job or prepare line may end with `class=<v2|v3|v4>` and `era=<hex>` tokens (sent
|
|
* only when the chain is on class v3 or v4, 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 (every class after v2 carries one).
|
|
* 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, "v2") != 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
|