igneum/proto-cuda/nvrtc/packfile.h
igneum-labs f535abb59d One-click CUDA worker: driver API + NVRTC loaded at run time, pack read at run time, Mac emulation check
proto-cuda/nvrtc/worker.cpp serves igneum-miner's worker protocol with nothing installed but the NVIDIA driver:
nvcuda.dll and the redistributable nvrtc64_120_0.dll are opened with LoadLibrary (cuda_api.h), the pack's kernel.cu
and kernel_bound.cu are handed to NVRTC byte for byte up to the host launch wrappers with program.h and memhard.h as
named headers, every pack is self-tested against its vectors.h (cache head, last line, FNV-1a 64, dataset head, last
word, 64 samples, 96 vector lanes) before it serves a job, prepare runs on a thread for the hourly swap, and a job on
seeds without a pair makes the worker find the miner's pack by seeds.txt and build it. packfile.h (C99) reads a pack
directory. fetch-redist.sh verifies and stages the NVIDIA 12.8.93 redistributables and the Khronos headers
(THIRD-PARTY.md records URLs, hashes and the EULA clause); build-windows.sh cross-compiles with mingw (static,
KERNEL32 + Universal CRT only). emu/: the two libraries as host functions, the pack kernels on host threads, the NVRTC
source compared with the pack files; test.sh PASS on the Mac (two real packs, swap, self-heal, 17 sampled hashes equal
igneum-pow hash-bound). Not run here: the real NVRTC compile and driver load (RTX 5090 PC).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 09:55:05 +00:00

350 lines
16 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 seedw[8], keyw[8];
char seedString[600];
// 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);
}
}
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;
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"); }
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)");
if (strlen(ehex) != 64) return pf_fail(err, cap, "epoch seed is not 64 hex characters");
strcpy(pk->epochHex, ehex); strcpy(pk->dayHex, dhex);
// The seed words derived from the bytes must be the pack's own words: otherwise the pack and the seeds disagree
{
uint32_t w[8];
if (!pf_unhex(ehex, bytes, 32, &blen) || blen != 32) return pf_fail(err, cap, "epoch seed hex is malformed");
pf_seed_words_from_bytes(bytes, 32, w);
if (memcmp(w, pk->seedw, 32) != 0) return pf_fail(err, cap, "the epoch seed bytes do not give the pack's IGNEUM_SEEDW_INIT (wrong seeds.txt for this pack?)");
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 (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.
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,
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 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 && badS == 0 && badV == 0) {
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, 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, 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;
}
#endif