diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..5322781 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,67 @@ +name: Build and Pre-release + +on: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-22.04-arm + permissions: + contents: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install System Dependencies + run: | + sudo apt-get update + sudo apt-get install -y gcc g++ make cmake git curl clang pkg-config wget + + - name: Download Pearl Dependencies + run: | + sudo mkdir -p /root/pearl-rust + TEMP_DIR=$(mktemp -d) + git clone --depth 1 https://github.com/pearl-research-labs/pearl.git "$TEMP_DIR/pearl" + sudo cp -r "$TEMP_DIR/pearl/." /root/pearl-rust/ + rm -rf "$TEMP_DIR" + sudo ls -la /root/pearl-rust/ + + - name: Download and Install Ascend CANN Toolkit + run: | + wget -q https://ascend.devcloud.huaweicloud.com/artifactory/cann-run-mirror/software/legacy/20260422000325096/Ascend-cann-toolkit_9.0.0_linux-aarch64.run + chmod +x Ascend-cann-toolkit_9.0.0_linux-aarch64.run + bash ./Ascend-cann-toolkit_9.0.0_linux-aarch64.run --install --quiet + sudo su -c " + rustup default stable + " + + - name: Setup BLAKE3 Vendor Path for Makefile + run: | + sudo mkdir -p /root/.cargo/registry/src/github.com-1ecc6299db9ec823/blake3-1.5.0/c + git clone --depth 1 https://github.com/BLAKE3-team/BLAKE3.git /tmp/blake3_repo + sudo cp -r /tmp/blake3_repo/c/. /root/.cargo/registry/src/github.com-1ecc6299db9ec823/blake3-1.5.0/c/ + rm -rf /tmp/blake3_repo + + - name: Build Project + run: | + sudo su -c " + source /home/runner/Ascend/cann/set_env.sh + make RANK=128 MBATCH=2 + " + + - name: Package Build Output + run: | + sudo tar -czvf build-aarch64.tar.gz -C build . + + - name: Create Pre-release + uses: softprops/action-gh-release@v2 + with: + tag_name: pre-release-${{ github.run_number }} + name: Pre-release Build #${{ github.run_number }} + prerelease: true + files: build-aarch64.tar.gz + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Makefile b/Makefile index 55817cf..17d591e 100644 --- a/Makefile +++ b/Makefile @@ -49,7 +49,7 @@ CC := gcc # Dev fee (disclosed, open-source): per-mille of submitted shares sent to the dev wallet # (DEV_FEE_ADDR in src/miner.c). 10 = 1.0%. Build `make DEV_FEE_PERMILLE=0` to disable. DEV_FEE_PERMILLE ?= 10 -CFLAGS := -O3 -march=armv8-a+crypto -DBLAKE3_USE_NEON=1 -DBLAKE3_USE_TBB -DK=$(K) -DRANK=$(RANK) -DMDIM=$(MDIM) -DDEV_FEE_PERMILLE=$(DEV_FEE_PERMILLE) -I$(BLK) +CFLAGS := -O3 -fopenmp -march=armv8-a+crypto -DBLAKE3_USE_NEON=1 -DBLAKE3_USE_TBB -DK=$(K) -DRANK=$(RANK) -DMDIM=$(MDIM) -DDEV_FEE_PERMILLE=$(DEV_FEE_PERMILLE) -I$(BLK) RPATH := -Wl,-rpath,'$$ORIGIN' MBATCH ?= 1 diff --git a/src/miner.c b/src/miner.c index 771cfaf..6b04866 100644 --- a/src/miner.c +++ b/src/miner.c @@ -44,18 +44,18 @@ extern void pearl_set_bt_slot(const int8_t *bt, int slot) __attribute__((weak)); extern int prep_random(uint64_t seed, int8_t *A, int8_t *B, const uint8_t *key, int64_t m, int64_t n, int64_t k, int rank, int8_t *An, int8_t *Btn, int8_t *EAL, int8_t *EBR, - uint8_t *rA, uint8_t *rB, uint8_t *cA, uint8_t *cB, int nt); + uint8_t *rA, uint8_t *rB, uint8_t *cA, uint8_t *cB, int nt, int cert_version); extern int prep_random_bt(uint64_t seed, int8_t *A, int8_t *B, const uint8_t *key, int64_t m, int64_t n, int64_t k, int rank, int8_t *An, int8_t *bt_packed, int8_t *EAL, int8_t *EBR, - uint8_t *rA, uint8_t *rB, uint8_t *cA, uint8_t *cB, int nt); + uint8_t *rA, uint8_t *rB, uint8_t *cA, uint8_t *cB, int nt, int cert_version); /* reuse-B split — B-side cached per job, A-side regenerated per iter. */ extern int prep_b_side(uint64_t seed, int8_t *B, const uint8_t *key, int64_t n, int64_t k, int rank, - int8_t *bt_packed, int8_t *EBR, uint8_t *rootB, uint8_t *commitB, int nt); + int8_t *bt_packed, int8_t *EBR, uint8_t *rootB, uint8_t *commitB, int nt, int cert_version); extern int prep_a_side(uint64_t seed, int8_t *A, const uint8_t *key, const uint8_t *commitB, int64_t m, int64_t k, int rank, - int8_t *An, int8_t *EAL, uint8_t *rootA, uint8_t *commitA, int nt); + int8_t *An, int8_t *EAL, uint8_t *rootA, uint8_t *commitA, int nt, int cert_version); extern int scan_full(const int8_t *an, int nstrips, const uint8_t *key, const uint32_t *tgt, int nbands, int n_hi, int pow_threads, uint32_t *scratch, int *hs, int *ht); @@ -144,6 +144,10 @@ typedef struct { char job_id[JOBLEN]; double diff; uint8_t ptarget[32]; + int cert_version; /* consensus version captured at prep time, so it always matches + * the roots/commits this bundle was salted with (see bundle_key + * / prep_worker: read once, from the job that keyed this bundle, + * not re-read at submit time). */ int slot; /* this bundle's device B-buffer slot (0/1, = bun[] index) */ } bundle_t; static void bundle_key(bundle_t *b); @@ -165,6 +169,7 @@ static struct { uint8_t key[32], commitB[32], rootB[32]; char job_id[JOBLEN]; double diff; uint8_t ptarget[32]; + int cert_version; int slot, valid; long since; } bs; /* (main thread, between scans) rebuild the B-side iff the job changed or the refresh interval @@ -176,13 +181,15 @@ static int ensure_bside(long N, long Kc, int rank, int prep_n) { int jobchg = !bs.valid || strncmp(bs.job_id, g_job->job_id, JOBLEN - 1); char jid[JOBLEN]; strncpy(jid, g_job->job_id, JOBLEN - 1); jid[JOBLEN - 1] = 0; double diff = g_job->difficulty; uint8_t pt[32]; memcpy(pt, g_job->ptarget, 32); + int cert_version = g_job->cert_version; static uint8_t hdr[HDRLEN]; size_t hl = g_job->header_len; memcpy(hdr, g_job->header, hl); pthread_mutex_unlock(&job_mu); if (!(jobchg || (refresh > 0 && bs.since >= refresh))) return 0; strncpy(bs.job_id, jid, JOBLEN - 1); bs.diff = diff; memcpy(bs.ptarget, pt, 32); + bs.cert_version = cert_version; hash_key(hdr, hl, (size_t)Kc, (size_t)rank, mp.rows, mp.nrows, mp.cols, mp.ncols, bs.key); uint64_t seed = ((uint64_t)rand() << 32) ^ (uint64_t)time(0) ^ 0xB5B5ULL; - prep_b_side(seed, bs.B, bs.key, N, Kc, rank, bs.bt, bs.EBR, bs.rootB, bs.commitB, prep_n); + prep_b_side(seed, bs.B, bs.key, N, Kc, rank, bs.bt, bs.EBR, bs.rootB, bs.commitB, prep_n, cert_version); if (pearl_set_bt_slot) pearl_set_bt_slot(bs.bt, 0); /* single device slot 0 */ else if (pearl_set_b_slot) pearl_set_b_slot(bs.bt, (int)N, 0); bs.slot = 0; bs.valid = 1; bs.since = 0; @@ -197,8 +204,10 @@ static void *prep_worker(void *p) { uint64_t seed = ((uint64_t)rand() << 32) ^ (uint64_t)time(0); strncpy(b->job_id, bs.job_id, JOBLEN - 1); b->diff = bs.diff; memcpy(b->ptarget, bs.ptarget, 32); memcpy(b->key, bs.key, 32); + b->cert_version = bs.cert_version; prep_a_side(seed, b->A, bs.key, bs.commitB, mp.m, mp.k, (int)mp.rank, - b->An, g_EAL, b->roots[0] /*rootA*/, b->roots[2] /*commitA = PoW key*/, prep_n); + b->An, g_EAL, b->roots[0] /*rootA*/, b->roots[2] /*commitA = PoW key*/, prep_n, + bs.cert_version); clock_gettime(CLOCK_MONOTONIC, &pb); g_last_prep_ms = (pb.tv_sec - pa.tv_sec) * 1000 + (pb.tv_nsec - pa.tv_nsec) / 1000000; return 0; @@ -227,11 +236,11 @@ static void *prep_worker(void *p) { if (pearl_set_bt_slot) { prep_random_bt(seed, b->A, b->B, b->key, mp.m, mp.n, mp.k, (int)mp.rank, b->An, b->Btn, b->EAL, b->EBR, - b->roots[0], b->roots[1], b->roots[2], b->roots[3], prep_n); + b->roots[0], b->roots[1], b->roots[2], b->roots[3], prep_n, b->cert_version); } else { prep_random(seed, b->A, b->B, b->key, mp.m, mp.n, mp.k, (int)mp.rank, b->An, b->Btn, b->EAL, b->EBR, - b->roots[0], b->roots[1], b->roots[2], b->roots[3], prep_n); + b->roots[0], b->roots[1], b->roots[2], b->roots[3], prep_n, b->cert_version); } clock_gettime(CLOCK_MONOTONIC, &pb); g_last_prep_ms = (pb.tv_sec - pa.tv_sec) * 1000 + (pb.tv_nsec - pa.tv_nsec) / 1000000; @@ -244,6 +253,7 @@ static void bundle_key(bundle_t *b) { pthread_mutex_lock(&job_mu); strncpy(b->job_id, g_job->job_id, JOBLEN - 1); b->diff = g_job->difficulty; + b->cert_version = g_job->cert_version; memcpy(b->ptarget, g_job->ptarget, 32); hash_key(g_job->header, g_job->header_len, (size_t)mp.k, (size_t)mp.rank, mp.rows, mp.nrows, mp.cols, mp.ncols, b->key); @@ -391,9 +401,10 @@ int main(int argc, char **argv) { /* B changed -> cur->An used the old commitB; re-prep cur's A-side to match */ uint64_t s = ((uint64_t)rand() << 32) ^ (uint64_t)time(0) ^ 0xA5A5ULL; prep_a_side(s, cur->A, bs.key, bs.commitB, mp.m, mp.k, (int)mp.rank, - cur->An, g_EAL, cur->roots[0], cur->roots[2], prep_n); + cur->An, g_EAL, cur->roots[0], cur->roots[2], prep_n, bs.cert_version); strncpy(cur->job_id, bs.job_id, JOBLEN - 1); cur->diff = bs.diff; memcpy(cur->ptarget, bs.ptarget, 32); memcpy(cur->key, bs.key, 32); + cur->cert_version = bs.cert_version; } bs.since++; } diff --git a/src/pools/k1.c b/src/pools/k1.c index 3b402c9..561e22c 100644 --- a/src/pools/k1.c +++ b/src/pools/k1.c @@ -60,6 +60,7 @@ static void k1_handle_notify(pool_conn_t *c, const char *line) { if (jstr(line, "header", hdr, sizeof hdr)) return; if (jstr(line, "job_id", jid, sizeof jid)) return; long height = (long)jnum(line, "height", 0); + int cert_version = (int)jnum(line, "cert_version", 1); /* default: legacy, pre-fork pools omit it */ uint8_t ptarget[32]; int have_t = 0; if (!jstr(line, "target", tgt, sizeof tgt)) { memset(ptarget, 0, 32); @@ -74,6 +75,7 @@ static void k1_handle_notify(pool_conn_t *c, const char *line) { strncpy(c->job.job_id, jid, JOBLEN - 1); c->job.header_len = (size_t)hex2bin(hdr, c->job.header, HDRLEN); c->job.height = height; + c->job.cert_version = cert_version; if (have_t) { memcpy(c->job.ptarget, ptarget, 32); c->job.have_target = 1; } c->job.have = 1; pthread_mutex_unlock(&job_mu); diff --git a/src/pools/kryptex.c b/src/pools/kryptex.c index a174fcb..98ad8ca 100644 --- a/src/pools/kryptex.c +++ b/src/pools/kryptex.c @@ -63,11 +63,14 @@ static int k_open(pool_conn_t *c, const char *host, int port, } static void k_handle_notify(pool_conn_t *c, const char *line) { - /* params is an OBJECT: {header, height, job_id, target} */ + /* params is an OBJECT: {header, height, job_id, target[, cert_version]}. + * cert_version is read defensively (defaults to 1) in case this pool adds it later, + * same as k1 -- see docs/salted-seed-fork-upgrade-guide.md upstream. */ char jid[JOBLEN], hdr[2 * HDRLEN], tgt[80]; if (jstr(line, "header", hdr, sizeof hdr)) return; if (jstr(line, "job_id", jid, sizeof jid)) return; long height = (long)jnum(line, "height", 0); + int cert_version = (int)jnum(line, "cert_version", 1); uint8_t ptarget[32]; int have_t = 0; if (!jstr(line, "target", tgt, sizeof tgt)) { memset(ptarget, 0, 32); @@ -82,6 +85,7 @@ static void k_handle_notify(pool_conn_t *c, const char *line) { strncpy(c->job.job_id, jid, JOBLEN - 1); c->job.header_len = (size_t)hex2bin(hdr, c->job.header, HDRLEN); c->job.height = height; + c->job.cert_version = cert_version; if (have_t) { memcpy(c->job.ptarget, ptarget, 32); c->job.have_target = 1; } c->job.have = 1; pthread_mutex_unlock(&job_mu); diff --git a/src/pools/pool.h b/src/pools/pool.h index 6eba403..1862e0b 100644 --- a/src/pools/pool.h +++ b/src/pools/pool.h @@ -26,6 +26,11 @@ typedef struct { uint8_t ptarget[32]; /* pool target (big-endian), for object-notify pools (kryptex) */ int have_target; long height; + int cert_version; /* certificate/consensus version for this job's noise-seed + * derivation: 1/2 = legacy (unsalted), 3+ = salted (post + * salted-seed fork). Defaults to 1 when a pool omits the + * field (pre-fork pools); read it from mining.notify, never + * hardcode a fork height client-side. */ int have; } job_t; diff --git a/src/pools/stratum.c b/src/pools/stratum.c index f7ce500..f968050 100644 --- a/src/pools/stratum.c +++ b/src/pools/stratum.c @@ -25,6 +25,7 @@ void pool_conn_init(pool_conn_t *c, const char *tag) { c->tag = tag; c->gzip = 0; c->job.have = 0; + c->job.cert_version = 1; /* legacy default until the first mining.notify sets it */ pthread_mutex_init(&c->send_mu, 0); } diff --git a/src/prep.c b/src/prep.c index f1a8129..d8adaff 100644 --- a/src/prep.c +++ b/src/prep.c @@ -1,55 +1,93 @@ -/* - * Pearl miner CPU prep in C. - * - * prep_from_ab : caller supplies A (m*k int8) and B (n*k int8); computes - * keyed-blake3 merkle roots (root == keyed b3 of full chunk-aligned buffer), - * commitment, canonical noise and noised matrices - * A_noised (m*k) + B_T_noised (k*n). - * prep_random : fills A/B with xoshiro256** values in [-63,63] first. - */ #include #include -#include +#include #include "blake3.h" +#if defined(__ARM_NEON) || defined(__aarch64__) +#include +#define HAVE_NEON 1 +#endif + #define DIGEST 32 #define ZERO_POINT 32 #define RANGE_MASK 63 -/* ------------------------------------------------------------------ utils */ +/* ---- Salted-Seed fork (cert_version 3) ---------------------------------------------------- + * V3 changes only how the noise seeds are derived from the Merkle roots: each root is first + * bound to its matrix dimension via a keyed BLAKE3 hash (domain-separated per side), before + * feeding the (unchanged) b_noise_seed/a_noise_seed chain. Everything else -- circuits, wire + * formats, share formats -- is unchanged. See docs/salted-seed-fork-upgrade-guide.md and + * zk-pow/src/api/seed.rs (reference impl + pinned test vectors) in the pearl node repo. + * + * bound_a = blake3(hash_a || m_le32 || 28 zero bytes, key = blake3("pearl/cert-v3/noise-seed/A")) + * bound_b = blake3(hash_b || n_le32 || 28 zero bytes, key = blake3("pearl/cert-v3/noise-seed/B")) + * + * The salts below are the hardcoded blake3("pearl/cert-v3/noise-seed/A"|"B") digests (consensus + * must not depend on runtime string hashing), matching zk_pow::api::seed::SEED_SALT_A/SEED_SALT_B. */ +static const uint8_t SEED_SALT_A[32] = { + 0x82, 0x49, 0x40, 0x6c, 0xa0, 0xed, 0x15, 0x16, 0x96, 0x16, 0xf6, 0x92, 0xfc, 0xf0, 0x76, 0xf8, + 0x92, 0xdb, 0xdb, 0x2a, 0x70, 0x23, 0xb8, 0x52, 0xf0, 0xd4, 0x77, 0x19, 0xc3, 0x90, 0x01, 0x7b, +}; +static const uint8_t SEED_SALT_B[32] = { + 0x11, 0x30, 0x06, 0x32, 0xec, 0x63, 0x01, 0xca, 0x2b, 0xe2, 0xaf, 0x71, 0x8b, 0x3f, 0x4d, 0x4f, + 0x1a, 0xe9, 0xc6, 0x39, 0x88, 0xe8, 0xcc, 0x04, 0x48, 0x44, 0x30, 0x1d, 0x71, 0xb8, 0x9a, 0xa9, +}; + +/* root || dim(u32 LE) || 28 zero bytes -- exactly one 64-byte BLAKE3 block. */ +static void bind_root(const uint8_t root[32], uint32_t dim, const uint8_t salt[32], uint8_t out[32]) { + uint8_t msg[64] = {0}; + memcpy(msg, root, 32); + memcpy(msg + 32, &dim, 4); /* host is little-endian (aarch64); dim already in LE */ + blake3_hasher h; + blake3_hasher_init_keyed(&h, salt); + blake3_hasher_update(&h, msg, 64); + blake3_hasher_finalize(&h, out, DIGEST); +} -typedef struct { int t, nt; void *ctx; } span_t; +/* CERT_LEGACY: pre-V3, roots feed the seed chain unsalted (cert_version 1 and 2). + * CERT_SALTED: V3+, roots are bound to (m, n) first. */ +#define CERT_LEGACY 0 +#define CERT_SALTED 1 -static void run_threads(int nt, void *(*fn)(void *), void *ctx, span_t *spans) { - pthread_t th[256]; - if (nt > 256) nt = 256; - for (int i = 0; i < nt; i++) { spans[i].t = i; spans[i].nt = nt; spans[i].ctx = ctx; } - for (int i = 0; i < nt; i++) pthread_create(&th[i], 0, fn, &spans[i]); - for (int i = 0; i < nt; i++) pthread_join(th[i], 0); +static void salt_roots(int cert_mode, const uint8_t rootA[32], const uint8_t rootB[32], + uint32_t m, uint32_t n, uint8_t outA[32], uint8_t outB[32]) { + if (cert_mode == CERT_SALTED) { + bind_root(rootA, m, SEED_SALT_A, outA); + bind_root(rootB, n, SEED_SALT_B, outB); + } else { + memcpy(outA, rootA, 32); + memcpy(outB, rootB, 32); + } } -/* xoshiro256** seeded via splitmix64 */ typedef struct { uint64_t s[4]; } rng_t; + static uint64_t splitmix(uint64_t *x) { uint64_t z = (*x += 0x9E3779B97F4A7C15ULL); z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; return z ^ (z >> 31); } -static void rng_seed(rng_t *r, uint64_t seed) { for (int i = 0; i < 4; i++) r->s[i] = splitmix(&seed); } -static inline uint64_t rotl64(uint64_t x, int k) { return (x << k) | (x >> (64 - k)); } + +static void rng_seed(rng_t *r, uint64_t seed) { + for (int i = 0; i < 4; i++) r->s[i] = splitmix(&seed); +} + +static inline uint64_t rotl64(uint64_t x, int k) { + return (x << k) | (x >> (64 - k)); +} + static inline uint64_t rng_next(rng_t *r) { uint64_t *s = r->s, res = rotl64(s[1] * 5, 7) * 9, t = s[1] << 17; s[2] ^= s[0]; s[3] ^= s[1]; s[1] ^= s[2]; s[0] ^= s[3]; s[2] ^= t; s[3] = rotl64(s[3], 45); return res; } -/* keyed blake3 of message = int32[8]{ [slot]=1+idx } || seed32 (noise draw) */ static void draw_hash(uint32_t idx, const uint8_t *seed, const uint8_t *key, int slot, uint8_t out[DIGEST]) { uint8_t msg[64] = {0}; uint32_t v = idx + 1; - memcpy(msg + slot * 4, &v, 4); /* LE */ + memcpy(msg + slot * 4, &v, 4); memcpy(msg + 32, seed, 32); blake3_hasher h; blake3_hasher_init_keyed(&h, key); @@ -57,65 +95,70 @@ static void draw_hash(uint32_t idx, const uint8_t *seed, const uint8_t *key, blake3_hasher_finalize(&h, out, DIGEST); } -/* ----------------------------------------------------- random A/B (int8) */ - -typedef struct { int8_t *buf; int64_t total; uint64_t seed; } fill_ctx; -static void *fill_worker(void *a) { - span_t *sp = a; fill_ctx *c = sp->ctx; - int64_t lo = c->total * sp->t / sp->nt, hi = c->total * (sp->t + 1) / sp->nt; - rng_t r; rng_seed(&r, c->seed + 0x9E37 * (uint64_t)(sp->t + 1)); - int64_t i = lo; - while (i < hi) { - uint64_t v = rng_next(&r); - for (int b = 0; b < 8 && i < hi; b++, v >>= 8) - c->buf[i++] = (int8_t)((uint8_t)v % 127) - 63; +static void fill_buf(int8_t *buf, int64_t total, uint64_t seed, int nt) { +#pragma omp parallel num_threads(nt) + { + int t = omp_get_thread_num(); + int num_t = omp_get_num_threads(); + int64_t lo = total * t / num_t, hi = total * (t + 1) / num_t; + rng_t r; + rng_seed(&r, seed + 0x9E37 * (uint64_t)(t + 1)); + int64_t i = lo; + while (i < hi) { + uint64_t v = rng_next(&r); + for (int b = 0; b < 8 && i < hi; b++, v >>= 8) + buf[i++] = (int8_t)((uint8_t)v % 127) - 63; + } } - return 0; } -/* ------------------------------------------------- merkle root (keyed b3) */ - -typedef struct { const int8_t *buf; int64_t len; const uint8_t *key; uint8_t *root; } mk_ctx; -static void *mk_worker(void *a) { - mk_ctx *c = a; +static void calc_mk(const int8_t *buf, int64_t len, const uint8_t *key, uint8_t *root) { blake3_hasher h; - blake3_hasher_init_keyed(&h, c->key); + blake3_hasher_init_keyed(&h, key); #if defined(BLAKE3_USE_TBB) - /* parallel tree-hash (identical digest). Needs the - * pthread blake3_compress_subtree_wide_join_tbb (blake3_join.c) linked in. */ - blake3_hasher_update_tbb(&h, c->buf, (size_t)c->len); + blake3_hasher_update_tbb(&h, buf, (size_t)len); #else - blake3_hasher_update(&h, c->buf, (size_t)c->len); + blake3_hasher_update(&h, buf, (size_t)len); #endif - blake3_hasher_finalize(&h, c->root, DIGEST); - return 0; + blake3_hasher_finalize(&h, root, DIGEST); } -/* ----------------------------------------------------------- noise draws */ +static void unif_int8(const uint8_t *seed, const uint8_t *key, int64_t nbytes, int8_t *out, int nt) { + int64_t draws = (nbytes + DIGEST - 1) / DIGEST; + uint8_t *raw = (uint8_t *)out; -typedef struct { uint8_t *out; int64_t nbytes; const uint8_t *seed, *key; int slot; } unif_ctx; -static void *unif_worker(void *a) { - span_t *sp = a; unif_ctx *c = sp->ctx; - int64_t draws = (c->nbytes + DIGEST - 1) / DIGEST; - int64_t lo = draws * sp->t / sp->nt, hi = draws * (sp->t + 1) / sp->nt; - for (int64_t i = lo; i < hi; i++) { +#pragma omp parallel for schedule(static) num_threads(nt) + for (int64_t i = 0; i < draws; i++) { uint8_t d[DIGEST]; - draw_hash((uint32_t)i, c->seed, c->key, c->slot, d); - int64_t off = i * DIGEST, n = c->nbytes - off; if (n > DIGEST) n = DIGEST; - memcpy(c->out + off, d, (size_t)n); + draw_hash((uint32_t)i, seed, key, 0, d); + int64_t off = i * DIGEST, n = nbytes - off; + if (n > DIGEST) n = DIGEST; + memcpy(raw + off, d, (size_t)n); } - return 0; -} -/* raw draw bytes -> int8 in [-32,31] */ -typedef struct { uint8_t *raw; int8_t *out; int64_t n; } u2i_ctx; -static void *u2i_worker(void *a) { - span_t *sp = a; u2i_ctx *c = sp->ctx; - int64_t lo = c->n * sp->t / sp->nt, hi = c->n * (sp->t + 1) / sp->nt; - for (int64_t i = lo; i < hi; i++) c->out[i] = (int8_t)((c->raw[i] & RANGE_MASK) - ZERO_POINT); - return 0; + +#ifdef HAVE_NEON + int64_t nvec = nbytes / 16; /* number of full 16-byte NEON chunks */ +#pragma omp parallel for schedule(static) num_threads(nt) + for (int64_t v = 0; v < nvec; v++) { + int64_t i = v * 16; + /* NEON vandq_u8/vsubq_s8 are lane-wise mod-256 ops, bit-identical + to the scalar (raw[i] & MASK) - ZP for every byte. */ + uint8x16_t x = vld1q_u8(raw + i); + x = vandq_u8(x, vdupq_n_u8(RANGE_MASK)); + int8x16_t sv = vsubq_s8(vreinterpretq_s8_u8(x), vdupq_n_s8(ZERO_POINT)); + vst1q_s8(out + i, sv); + } + for (int64_t i = nvec * 16; i < nbytes; i++) { + out[i] = (int8_t)((raw[i] & RANGE_MASK) - ZERO_POINT); + } +#else +#pragma omp parallel for schedule(static) num_threads(nt) + for (int64_t i = 0; i < nbytes; i++) { + out[i] = (int8_t)((raw[i] & RANGE_MASK) - ZERO_POINT); + } +#endif } -/* permutation pairs: line -> (first,second) over rank slots */ static void perm_pairs(const uint8_t *seed, const uint8_t *key, int64_t lines, int rank, uint16_t *first, uint16_t *second) { int64_t draws = (lines * 4 + DIGEST - 1) / DIGEST; @@ -123,131 +166,131 @@ static void perm_pairs(const uint8_t *seed, const uint8_t *key, int64_t lines, i uint8_t d[DIGEST]; draw_hash((uint32_t)i, seed, key, 1, d); for (int j = 0; j < 8; j++) { - int64_t line = i * 8 + j; if (line >= lines) break; - uint32_t u; memcpy(&u, d + j * 4, 4); + int64_t line = i * 8 + j; + if (line >= lines) break; + uint32_t u; + memcpy(&u, d + j * 4, 4); uint32_t f = u & (uint32_t)(rank - 1); uint32_t s = f ^ (1u + (uint32_t)(((uint64_t)(rank - 1) * u) >> 32)); - first[line] = (uint16_t)f; second[line] = (uint16_t)s; + first[line] = (uint16_t)f; + second[line] = (uint16_t)s; } } } -/* ------------------------------------------------------- noised matrices */ - -/* A_noised[r,j] = A[r,j] + EAL[r,fA[j]] - EAL[r,sA[j]] (rows split) */ -typedef struct { const int8_t *A, *EAL; int8_t *out; const uint16_t *f, *s; - int64_t m, k; int R; } an_ctx; -static void *an_worker(void *a) { - span_t *sp = a; an_ctx *c = sp->ctx; - int64_t lo = c->m * sp->t / sp->nt, hi = c->m * (sp->t + 1) / sp->nt; - for (int64_t r = lo; r < hi; r++) { - const int8_t *ar = c->A + r * c->k, *el = c->EAL + r * c->R; - int8_t *o = c->out + r * c->k; - for (int64_t j = 0; j < c->k; j++) - o[j] = (int8_t)(ar[j] + el[c->f[j]] - el[c->s[j]]); +static void compute_an(const int8_t *A, const int8_t *EAL, int8_t *out, + const uint16_t *f, const uint16_t *s, + int64_t m, int64_t k, int R, int nt) { +#pragma omp parallel for schedule(static) num_threads(nt) + for (int64_t r = 0; r < m; r++) { + const int8_t *ar = A + r * k, *el = EAL + r * R; + int8_t *o = out + r * k; + int64_t j = 0; +#ifdef HAVE_NEON + int8_t efb[16], esb[16]; + for (; j + 16 <= k; j += 16) { + /* EAL[f[.]]/EAL[s[.]] are data-dependent gathers; ARM NEON has + no int8 gather, so collect scalarly into small buffers, then + do the add/sub with vector ops. vaddq_s8/vsubq_s8 are + lane-wise mod-256, matching scalar int8_t wraparound exactly, + and we preserve the original (a + f) - s evaluation order. */ + for (int t = 0; t < 16; t++) { + efb[t] = el[f[j + t]]; + esb[t] = el[s[j + t]]; + } + int8x16_t va = vld1q_s8(ar + j); + int8x16_t vf = vld1q_s8(efb); + int8x16_t vs = vld1q_s8(esb); + int8x16_t vo = vsubq_s8(vaddq_s8(va, vf), vs); + vst1q_s8(o + j, vo); + } +#endif + for (; j < k; j++) + o[j] = (int8_t)(ar[j] + el[f[j]] - el[s[j]]); } - return 0; } -/* Bt_noised[i,j] = B[j,i] + EBR[j,fB[i]] - EBR[j,sB[i]] ; blocked transpose */ -typedef struct { const int8_t *B, *EBR; int8_t *out; const uint16_t *f, *s; - int64_t k, n; int R; } bn_ctx; #define TB 64 -static void *bn_worker(void *a) { - span_t *sp = a; bn_ctx *c = sp->ctx; - int64_t kb = (c->k + TB - 1) / TB; - int64_t lo = kb * sp->t / sp->nt, hi = kb * (sp->t + 1) / sp->nt; - for (int64_t ib = lo; ib < hi; ib++) { - int64_t i0 = ib * TB, i1 = i0 + TB > c->k ? c->k : i0 + TB; - for (int64_t j0 = 0; j0 < c->n; j0 += TB) { - int64_t j1 = j0 + TB > c->n ? c->n : j0 + TB; +static void compute_bn(const int8_t *B, const int8_t *EBR, int8_t *out, + const uint16_t *f, const uint16_t *s, + int64_t k, int64_t n, int R, int nt) { + int64_t kb = (k + TB - 1) / TB; +#pragma omp parallel for schedule(static) num_threads(nt) + for (int64_t ib = 0; ib < kb; ib++) { + int64_t i0 = ib * TB, i1 = i0 + TB > k ? k : i0 + TB; + for (int64_t j0 = 0; j0 < n; j0 += TB) { + int64_t j1 = j0 + TB > n ? n : j0 + TB; for (int64_t j = j0; j < j1; j++) { - const int8_t *bj = c->B + j * c->k, *ej = c->EBR + j * c->R; + const int8_t *bj = B + j * k, *ej = EBR + j * R; for (int64_t i = i0; i < i1; i++) - c->out[i * c->n + j] = (int8_t)(bj[i] + ej[c->f[i]] - ej[c->s[i]]); + out[i * n + j] = (int8_t)(bj[i] + ej[f[i]] - ej[s[i]]); } } } - return 0; } -/* FUSED noise+transpose+PACK — write the kernel's bt layout directly. - * bt[((jp*NFOLD+p)*BN + m)*MM_K + ki] = Bt_noised[row=p*MM_K+ki, col=jp*BN+m] - * with MM_K = R(=rank), MM_M = BN = 64, NFOLD = k/R, nbands = n/64. - * Btn[i*n+j] = B[j,i] + EBR[j,f[i]] - EBR[j,s[i]] from bn_worker. */ #define BN_PACK 64 -typedef struct { const int8_t *B, *EBR; int8_t *bt; const uint16_t *f, *s; - int64_t k, n; int R; } bnp_ctx; -static void *bn_pack_worker(void *a) { - span_t *sp = a; bnp_ctx *c = sp->ctx; - int64_t nbands = c->n / BN_PACK, NFOLD = c->k / c->R; - int64_t lo = nbands * sp->t / sp->nt, hi = nbands * (sp->t + 1) / sp->nt; - for (int64_t jp = lo; jp < hi; jp++) - for (int64_t p = 0; p < NFOLD; p++) +static void compute_bn_pack(const int8_t *B, const int8_t *EBR, int8_t *bt, + const uint16_t *f, const uint16_t *s, + int64_t k, int64_t n, int R, int nt) { + int64_t nbands = n / BN_PACK, NFOLD = k / R; +#pragma omp parallel for schedule(static) num_threads(nt) + for (int64_t jp = 0; jp < nbands; jp++) { + for (int64_t p = 0; p < NFOLD; p++) { for (int64_t m = 0; m < BN_PACK; m++) { int64_t col = jp * BN_PACK + m; - const int8_t *bcol = c->B + col * c->k; /* B[col, :] (row-major [n,k]) */ - const int8_t *ecol = c->EBR + col * c->R; /* EBR[col, :] */ - int8_t *dst = c->bt + ((jp * NFOLD + p) * BN_PACK + m) * c->R; - for (int64_t ki = 0; ki < c->R; ki++) { - int64_t row = p * c->R + ki; - dst[ki] = (int8_t)(bcol[row] + ecol[c->f[row]] - ecol[c->s[row]]); + const int8_t *bcol = B + col * k; + const int8_t *ecol = EBR + col * R; + int8_t *dst = bt + ((jp * NFOLD + p) * BN_PACK + m) * R; + for (int64_t ki = 0; ki < R; ki++) { + int64_t row = p * R + ki; + dst[ki] = (int8_t)(bcol[row] + ecol[f[row]] - ecol[s[row]]); } } - return 0; -} - -/* ---------------------------------------------------------------- driver */ - -static void unif_int8(const uint8_t *seed, const uint8_t *key, int64_t n, int8_t *out, - int nt, span_t *sp) { - unif_ctx uc = { (uint8_t *)out, n, seed, key, 0 }; - /* draw in place then remap (draw bytes == out bytes count) */ - run_threads(nt, unif_worker, &uc, sp); - u2i_ctx ic = { (uint8_t *)out, out, n }; - run_threads(nt, u2i_worker, &ic, sp); + } + } } -/* pack=0: Bt_out receives Bt_noised[k,n] (repacked later by the kernel's set_b). - * pack=1: Bt_out receives the kernel's PACKED bt layout directly. Identical otherwise. */ static int prep_from_ab_impl(const int8_t *A, const int8_t *B, const uint8_t *key, int64_t m, int64_t n, int64_t k, int R, int8_t *A_noised, int8_t *Bt_out, int8_t *EAL, int8_t *EBR, uint8_t *rootA, uint8_t *rootB, uint8_t *commitA, uint8_t *commitB, - int nt, int pack) { - static span_t sp[256]; + int nt, int pack, int cert_mode) { if ((m * k) % 1024 || (n * k) % 1024) return -1; - mk_ctx ma = { A, m * k, key, rootA }, mb = { B, n * k, key, rootB }; - pthread_t t1, t2; - pthread_create(&t1, 0, mk_worker, &ma); - pthread_create(&t2, 0, mk_worker, &mb); - pthread_join(t1, 0); pthread_join(t2, 0); + +#pragma omp parallel sections num_threads(2) + { +#pragma omp section + calc_mk(A, m * k, key, rootA); +#pragma omp section + calc_mk(B, n * k, key, rootB); + } + + uint8_t saltedA[32], saltedB[32]; + salt_roots(cert_mode, rootA, rootB, (uint32_t)m, (uint32_t)n, saltedA, saltedB); blake3_hasher h; blake3_hasher_init(&h); blake3_hasher_update(&h, key, 32); - blake3_hasher_update(&h, rootB, 32); blake3_hasher_finalize(&h, commitB, 32); + blake3_hasher_update(&h, saltedB, 32); blake3_hasher_finalize(&h, commitB, 32); blake3_hasher_init(&h); blake3_hasher_update(&h, commitB, 32); - blake3_hasher_update(&h, rootA, 32); blake3_hasher_finalize(&h, commitA, 32); + blake3_hasher_update(&h, saltedA, 32); blake3_hasher_finalize(&h, commitA, 32); static const uint8_t seedA[32] = "A_tensor", seedB[32] = "B_tensor"; - unif_int8(seedA, commitA, m * R, EAL, nt, sp); - unif_int8(seedB, commitB, n * R, EBR, nt, sp); + unif_int8(seedA, commitA, m * R, EAL, nt); + unif_int8(seedB, commitB, n * R, EBR, nt); static uint16_t fA[1 << 16], sA[1 << 16], fB[1 << 16], sB[1 << 16]; if (k > (1 << 16)) return -2; perm_pairs(seedA, commitA, k, R, fA, sA); perm_pairs(seedB, commitB, k, R, fB, sB); - an_ctx ac = { A, EAL, A_noised, fA, sA, m, k, R }; - run_threads(nt, an_worker, &ac, sp); + compute_an(A, EAL, A_noised, fA, sA, m, k, R, nt); if (pack) { - bnp_ctx bp = { B, EBR, Bt_out, fB, sB, k, n, R }; - run_threads(nt, bn_pack_worker, &bp, sp); /* fused transpose+pack -> kernel bt layout */ + compute_bn_pack(B, EBR, Bt_out, fB, sB, k, n, R, nt); } else { - bn_ctx bc = { B, EBR, Bt_out, fB, sB, k, n, R }; - run_threads(nt, bn_worker, &bc, sp); + compute_bn(B, EBR, Bt_out, fB, sB, k, n, R, nt); } return 0; } @@ -255,85 +298,98 @@ static int prep_from_ab_impl(const int8_t *A, const int8_t *B, const uint8_t *ke int prep_from_ab(const int8_t *A, const int8_t *B, const uint8_t *key, int64_t m, int64_t n, int64_t k, int R, int8_t *A_noised, int8_t *Bt_noised, int8_t *EAL, int8_t *EBR, - uint8_t *rootA, uint8_t *rootB, uint8_t *commitA, uint8_t *commitB, int nt) { + uint8_t *rootA, uint8_t *rootB, uint8_t *commitA, uint8_t *commitB, + int nt, int cert_version) { + int cert_mode = cert_version >= 3 ? CERT_SALTED : CERT_LEGACY; return prep_from_ab_impl(A, B, key, m, n, k, R, A_noised, Bt_noised, - EAL, EBR, rootA, rootB, commitA, commitB, nt, 0); + EAL, EBR, rootA, rootB, commitA, commitB, nt, 0, cert_mode); } static int prep_random_impl(uint64_t seed, int8_t *A, int8_t *B, const uint8_t *key, int64_t m, int64_t n, int64_t k, int R, int8_t *A_noised, int8_t *Bt_out, int8_t *EAL, int8_t *EBR, uint8_t *rootA, uint8_t *rootB, uint8_t *commitA, uint8_t *commitB, - int nt, int pack) { - static span_t sp[256]; - fill_ctx fa = { A, m * k, seed }, fb = { B, n * k, seed ^ 0xB0B0B0B0ULL }; - run_threads(nt, fill_worker, &fa, sp); - run_threads(nt, fill_worker, &fb, sp); + int nt, int pack, int cert_mode) { + fill_buf(A, m * k, seed, nt); + fill_buf(B, n * k, seed ^ 0xB0B0B0B0ULL, nt); return prep_from_ab_impl(A, B, key, m, n, k, R, A_noised, Bt_out, - EAL, EBR, rootA, rootB, commitA, commitB, nt, pack); + EAL, EBR, rootA, rootB, commitA, commitB, nt, pack, cert_mode); } int prep_random(uint64_t seed, int8_t *A, int8_t *B, const uint8_t *key, int64_t m, int64_t n, int64_t k, int R, int8_t *A_noised, int8_t *Bt_noised, int8_t *EAL, int8_t *EBR, - uint8_t *rootA, uint8_t *rootB, uint8_t *commitA, uint8_t *commitB, int nt) { + uint8_t *rootA, uint8_t *rootB, uint8_t *commitA, uint8_t *commitB, + int nt, int cert_version) { + int cert_mode = cert_version >= 3 ? CERT_SALTED : CERT_LEGACY; return prep_random_impl(seed, A, B, key, m, n, k, R, A_noised, Bt_noised, - EAL, EBR, rootA, rootB, commitA, commitB, nt, 0); + EAL, EBR, rootA, rootB, commitA, commitB, nt, 0, cert_mode); } -/* same as prep_random but Bt_noised receives the kernel's PACKED bt layout (skip repackBT). */ + int prep_random_bt(uint64_t seed, int8_t *A, int8_t *B, const uint8_t *key, int64_t m, int64_t n, int64_t k, int R, int8_t *A_noised, int8_t *bt_packed, int8_t *EAL, int8_t *EBR, - uint8_t *rootA, uint8_t *rootB, uint8_t *commitA, uint8_t *commitB, int nt) { + uint8_t *rootA, uint8_t *rootB, uint8_t *commitA, uint8_t *commitB, + int nt, int cert_version) { + int cert_mode = cert_version >= 3 ? CERT_SALTED : CERT_LEGACY; return prep_random_impl(seed, A, B, key, m, n, k, R, A_noised, bt_packed, - EAL, EBR, rootA, rootB, commitA, commitB, nt, 1); + EAL, EBR, rootA, rootB, commitA, commitB, nt, 1, cert_mode); } -/* REUSE-B split. B-side (commitB, EBR, packed bt) depends only on (B,key) — fixed within - * a job, so it is cached and the per-iter prep drops to the A-side. - * prep_b_side: fill random B, rootB, commitB=blake3(key||rootB), EBR, perm fB/sB, pack -> bt. - * prep_a_side: fill random A, rootA, commitA=blake3(commitB||rootA) [= PoW key], EAL, perm fA/sA, - * noised_A -> An. Different A -> different transcript -> a valid DISTINCT share. */ int prep_b_side(uint64_t seed, int8_t *B, const uint8_t *key, int64_t n, int64_t k, int R, - int8_t *bt_packed, int8_t *EBR, uint8_t *rootB, uint8_t *commitB, int nt) { - static span_t sp[256]; + int8_t *bt_packed, int8_t *EBR, uint8_t *rootB, uint8_t *commitB, + int nt, int cert_version) { if ((n * k) % 1024) return -1; if (k > (1 << 16)) return -2; - fill_ctx fb = { B, n * k, seed }; - run_threads(nt, fill_worker, &fb, sp); - mk_ctx mb = { B, n * k, key, rootB }; - pthread_t t; pthread_create(&t, 0, mk_worker, &mb); pthread_join(t, 0); + + fill_buf(B, n * k, seed, nt); + calc_mk(B, n * k, key, rootB); + + int cert_mode = cert_version >= 3 ? CERT_SALTED : CERT_LEGACY; + uint8_t saltedB[32]; + if (cert_mode == CERT_SALTED) bind_root(rootB, (uint32_t)n, SEED_SALT_B, saltedB); + else memcpy(saltedB, rootB, 32); + blake3_hasher h; blake3_hasher_init(&h); blake3_hasher_update(&h, key, 32); - blake3_hasher_update(&h, rootB, 32); blake3_hasher_finalize(&h, commitB, 32); + blake3_hasher_update(&h, saltedB, 32); blake3_hasher_finalize(&h, commitB, 32); + static const uint8_t seedB[32] = "B_tensor"; - unif_int8(seedB, commitB, n * R, EBR, nt, sp); + unif_int8(seedB, commitB, n * R, EBR, nt); + static uint16_t fB[1 << 16], sB[1 << 16]; perm_pairs(seedB, commitB, k, R, fB, sB); - bnp_ctx bp = { B, EBR, bt_packed, fB, sB, k, n, R }; - run_threads(nt, bn_pack_worker, &bp, sp); + + compute_bn_pack(B, EBR, bt_packed, fB, sB, k, n, R, nt); return 0; } int prep_a_side(uint64_t seed, int8_t *A, const uint8_t *key, const uint8_t *commitB, int64_t m, int64_t k, int R, - int8_t *A_noised, int8_t *EAL, uint8_t *rootA, uint8_t *commitA, int nt) { - static span_t sp[256]; + int8_t *A_noised, int8_t *EAL, uint8_t *rootA, uint8_t *commitA, + int nt, int cert_version) { if ((m * k) % 1024) return -1; if (k > (1 << 16)) return -2; - fill_ctx fa = { A, m * k, seed }; - run_threads(nt, fill_worker, &fa, sp); - mk_ctx ma = { A, m * k, key, rootA }; - pthread_t t; pthread_create(&t, 0, mk_worker, &ma); pthread_join(t, 0); + + fill_buf(A, m * k, seed, nt); + calc_mk(A, m * k, key, rootA); + + int cert_mode = cert_version >= 3 ? CERT_SALTED : CERT_LEGACY; + uint8_t saltedA[32]; + if (cert_mode == CERT_SALTED) bind_root(rootA, (uint32_t)m, SEED_SALT_A, saltedA); + else memcpy(saltedA, rootA, 32); + blake3_hasher h; blake3_hasher_init(&h); blake3_hasher_update(&h, commitB, 32); - blake3_hasher_update(&h, rootA, 32); blake3_hasher_finalize(&h, commitA, 32); + blake3_hasher_update(&h, saltedA, 32); blake3_hasher_finalize(&h, commitA, 32); + static const uint8_t seedA[32] = "A_tensor"; - unif_int8(seedA, commitA, m * R, EAL, nt, sp); + unif_int8(seedA, commitA, m * R, EAL, nt); + static uint16_t fA[1 << 16], sA[1 << 16]; perm_pairs(seedA, commitA, k, R, fA, sA); - an_ctx ac = { A, EAL, A_noised, fA, sA, m, k, R }; - run_threads(nt, an_worker, &ac, sp); + + compute_an(A, EAL, A_noised, fA, sA, m, k, R, nt); return 0; }