Mac app: dial the public seed node first, the home node second
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
a64fde76d6
commit
e5dc196f96
7 changed files with 1761 additions and 1 deletions
498
docs/analysis/weak-program-census-2026-10-03.md
Normal file
498
docs/analysis/weak-program-census-2026-10-03.md
Normal file
|
|
@ -0,0 +1,498 @@
|
|||
# Weak-program census and the load-count rule
|
||||
|
||||
Date: 3 October 2026. Ledger items M6 (weak programs) and M5 (load count varies 6x), `docs/fud-ledger.md`.
|
||||
Status: Measured. Every number below was produced on this machine on this date by the commands in section 10.
|
||||
|
||||
Machine: Apple M5 Max (12 performance + 6 efficiency cores, 64 GB), macOS Darwin 25.6.0, rustc 1.99.0 (rustup),
|
||||
release build with LTO. Tool: `igneum-census/` (new crate, depends on `igneum-pow` as a path dependency and changes
|
||||
nothing in it). The census ran at `nice -n 15` on 8 threads while a devnet build shared the machine.
|
||||
|
||||
## 1. Summary
|
||||
|
||||
| Question | Answer |
|
||||
|---|---|
|
||||
| Programs examined | 100,000 under the current generator (seeds `igneum-census-2026-10-03/0` to `/99999`), each on 4,096 nonces with the memory-hard 1 GiB dataset of day 2026-10-03; 100,000 more under the proposed generator on 2,048 nonces each; 100,000 again under the current generator with the closed-form dataset to check that the dynamic test does not depend on the day |
|
||||
| Weak programs under the current generator | 1.23 percent fail the register and bias tests (a dead or stuck register, a lane-constant load site, more than 1 percent of final registers saturated, or an output bit biased past 6 sigma); 2.43 percent have a register with no injecting write (a static property that produces most of the stuck registers) |
|
||||
| The bigger finding | 94.8 percent of programs re-read at least one dataset address inside every hash. 19.9 percent of all loads in the population are repeats of an address the same hash already read. The GPU does not pay for a repeat, so the static load count overstates the memory work by 20 percent on average and the hash rate tracks the distinct count, not the static count (section 5). This is the real cause of ledger M5 |
|
||||
| Rejection rule (M6) | Static: no cyclically redundant load, every register has at least one injecting write. Dynamic: 64 fixed warps on a seed-derived closed-form dataset, no constant register bit, no lane-constant load site, saturation under 1 percent, no output bit past 6 sigma, more than 120 distinct addresses per hash on average. Section 6 |
|
||||
| Rejection rate | Under the current generator the rule rejects 95.0 percent (the redundancy alone rejects 94.8 percent), which is why the fix is a generator rule, not a filter. Under the proposed generator it rejects 5.14 percent (3.93 static, 2.05 dynamic, overlapping), so 1.054 candidates per epoch on average. Section 7 |
|
||||
| Load-count rule (M5) | Exactly 16 load slots per program, drawn first from slots 1..63, and a load may only read a register that an earlier instruction of the program wrote and that no load has read since (the fresh-source rule). Every hash then does 128 distinct random reads; a 32-lane unit derives 4,096 items, the design bound of spec section 1.11. Sections 5 and 6 |
|
||||
| Hash-rate spread left by the rule | From the load count: none. Residual from program shape: 1.10x across the eight programs with measured Mac rates (3.51 to 3.85 G distinct loads/s), approximate; on the RTX 5090 the rule puts every hour at about 141 Mhash/s (18.0 G distinct loads/s over 128), against 118 to 321 Mhash/s for the 1st to 99th percentile program today. Section 8 |
|
||||
|
||||
## 2. Method
|
||||
|
||||
Seeds. Program `i` is `generate(seed_words("igneum-census-2026-10-03/" + i))`, the production generator of
|
||||
`igneum-pow/src/generator.rs`, so any program here is reproducible with `igneum-pow hash --seed
|
||||
igneum-census-2026-10-03/i` or `igneum-census show --seed ...`.
|
||||
|
||||
Interpreter. An instrumented copy of `verify.rs::interpret_warp` (same register-major loops, same op semantics,
|
||||
same `MemhardCpu::fetch` for the dataset words). On every program the first warp is also run through
|
||||
`igneum_pow::hash_warp` and the 32 outputs compared; the run aborts on any disagreement (none occurred). At start
|
||||
the tool checks `igneum-genesis` lanes 0 and 31 against the spec vectors (`1fb0b3bbc1ac8279`, `fa052263a854f3de`).
|
||||
|
||||
Nonce sample. 128 warps per program (4,096 nonces) drawn as 64 pairs: base `b` uniform over the aligned 32-bit
|
||||
range, partner `b XOR (1 << k)` with `k` uniform in 5..31, from a SplitMix64 stream seeded by the program's seed
|
||||
string. Within a warp, lanes `l` and `l XOR m` for `m` in {1, 2, 4, 8, 16} differ in exactly one nonce bit, so a
|
||||
warp gives 80 single-bit-flip pairs for bits 0..4 and a pair of warps gives 32 for a bit in 5..31. Avalanche is
|
||||
measured over all of them (11,264 pairs per program).
|
||||
|
||||
Metrics per program (one TSV line, 36 columns):
|
||||
|
||||
| Metric | Definition |
|
||||
|---|---|
|
||||
| `loads`, `lph` | load instructions, loads per hash (x8) |
|
||||
| `dist_mean`, `dist_min` | distinct masked dataset addresses per hash, mean and minimum over the 4,096 hashes |
|
||||
| `redundant_frac` | `1 - dist_mean / lph`, the share of loads that repeat an address the same hash already read |
|
||||
| `dist_total_frac` | distinct addresses over the whole sample divided by total loads (birthday collisions in 2^28 words are under 0.2 percent of this) |
|
||||
| `s_redundant` | static: loads per iteration whose source register was not written since the previous load from it, counted cyclically over the 64 instructions |
|
||||
| `s_cancel` | static: of those, loads that also share the destination, unwritten in between; the pair is the identity |
|
||||
| `sites_lane_const` | load sites (iteration x instruction) whose 32 lanes read the same address in every warp |
|
||||
| `sites_nonce_const` | load sites whose address is the same in every lane of every warp |
|
||||
| `end_sat_frac`, `end_zero`, `end_ones` | share and counts of final register values (8 x 4,096) equal to 0 or 0xffffffff |
|
||||
| `or_sat_frac` | share of `or` executions whose result is 0xffffffff |
|
||||
| `const_bits_max`, `const_bits_sum`, `nonce_indep_regs` | per register, bits that are the same in every final value over the sample (AND of all values OR NOT the OR of all values): the maximum over registers, the total, and the registers at 32 (nonce-independent) |
|
||||
| `bias_max`, `bias_z_max` | largest deviation of an output bit's ones frequency from 0.5, and in units of `0.5 / sqrt(4096)` |
|
||||
| `aval_mean`, `aval_std`, `flip_min`, `flip_max`, `aval_hi_mean` | output bits flipped per single-bit nonce flip: mean and standard deviation, the least and most flipped output bit, and the mean over the high-bit (cross-warp) pairs alone |
|
||||
| `never_written`, `never_read`, `rotl_only`, `inj_missing`, `last_contract` | static register facts: never a destination; never a source; only `rotl` writes it; no write by an injecting op (`add sub xor mad shfl load`, the ops that are bijective in `dst` and bring another register in); last write of the program is a contraction (`or mul mulhi`) |
|
||||
| `depth`, `mlp` | longest chain of loads in series over the 8 iterations (a load's depth is its source's depth plus one, carried through every op), and `lph / depth` |
|
||||
| `dups` | duplicate 64-bit outputs in the sample; see the note in section 3 |
|
||||
|
||||
Runtime: 2,797 s for the 100,000-program memory-hard census (8 threads, 128 warps each; 12.8 million warps,
|
||||
about 1.7 ms per warp per thread under load).
|
||||
|
||||
## 3. The population under the current generator
|
||||
|
||||
Distributions (100,000 programs):
|
||||
|
||||
| Metric | mean | min | p1 | p10 | p50 | p90 | p99 | p99.9 | max |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| lph (static loads per hash) | 127.93 | 24 | 64 | 96 | 128 | 160 | 192 | 216 | 256 |
|
||||
| dist_mean (distinct addresses per hash) | 102.44 | 23.96 | 56 | 73 | 104.00 | 128.74 | 152 | 169 | 200 |
|
||||
| redundant_frac | 0.191 | 0 | 0 | 0.069 | 0.189 | 0.316 | 0.421 | 0.500 | 0.632 |
|
||||
| s_redundant (per iteration) | 3.20 | 0 | 0 | 1 | 3 | 6 | 8 | 11 | 15 |
|
||||
| s_cancel (per iteration) | 0.26 | 0 | 0 | 0 | 0 | 1 | 2 | 3 | 4 |
|
||||
| ors | 2.57 | 0 | 0 | 1 | 2 | 5 | 7 | 8 | 12 |
|
||||
| depth (loads in series) | 41.3 | 9 | 24 | 32 | 40 | 56 | 66 | 80 | 104 |
|
||||
| mlp | 3.20 | 1.14 | 1.83 | 2.33 | 3.12 | 4.19 | 5.33 | 6.50 | 9.41 |
|
||||
| sites_lane_const | 0.002 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 29 |
|
||||
| end_sat_frac | 0.0005 | 0 | 0 | 0 | 0 | 0.0000 | 0.0021 | 0.122 | 0.500 |
|
||||
| or_sat_frac | 0.0020 | 0 | 0 | 0.0000 | 0.0001 | 0.0035 | 0.0305 | 0.251 | 0.808 |
|
||||
| const_bits_max | 0.038 | 0 | 0 | 0 | 0 | 0 | 0 | 9 | 32 |
|
||||
| nonce_indep_regs | 0.0004 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 4 |
|
||||
| bias_max | 0.0203 | 0.0107 | 0.0142 | 0.0164 | 0.0200 | 0.0244 | 0.0295 | 0.0342 | 0.111 |
|
||||
| bias_z_max | 2.59 | 1.38 | 1.81 | 2.09 | 2.56 | 3.13 | 3.78 | 4.38 | 14.25 |
|
||||
| aval_mean | 32.000 | 31.854 | 31.916 | 31.954 | 32.000 | 32.046 | 32.084 | 32.111 | 32.148 |
|
||||
| aval_std | 3.9996 | 3.890 | 3.940 | 3.967 | 4.000 | 4.032 | 4.058 | 4.078 | 4.104 |
|
||||
| flip_min | 0.4894 | 0.4727 | 0.4837 | 0.4867 | 0.4896 | 0.4919 | 0.4933 | 0.4943 | 0.4959 |
|
||||
| flip_max | 0.5106 | 0.5046 | 0.5067 | 0.5081 | 0.5104 | 0.5132 | 0.5163 | 0.5189 | 0.5233 |
|
||||
| aval_hi_mean | 32.000 | 31.636 | 31.795 | 31.887 | 32.000 | 32.114 | 32.206 | 32.269 | 32.385 |
|
||||
|
||||
Load count (static), 100,000 programs. The count is binomial(64, 0.25) times 8: mean 127.9, standard deviation
|
||||
27.7. Observed range 24 to 256 (ledger M5 quoted 40 to 232 over 10,000).
|
||||
|
||||
| loads/hash | 24 to 56 | 64 | 72 | 80 | 88 | 96 | 104 | 112 | 120 | 128 | 136 | 144 | 152 | 160 | 168 | 176 | 184 | 192 | 200 to 256 |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| programs | 485 | 643 | 1461 | 2503 | 4372 | 6196 | 8353 | 9937 | 11334 | 11402 | 10903 | 9421 | 7460 | 5642 | 4054 | 2406 | 1550 | 939 | 939 |
|
||||
| cumulative % | 0.49 | 1.13 | 2.59 | 5.09 | 9.46 | 15.66 | 24.01 | 33.95 | 45.28 | 56.69 | 67.59 | 77.01 | 84.47 | 90.11 | 94.17 | 96.57 | 98.12 | 99.06 | 100 |
|
||||
|
||||
Distinct addresses per hash (mean per program, rounded to the nearest 8): 24 to 48 in 0.6 percent, 56 to 72 in
|
||||
9.8 percent, 80 to 120 in 74.6 percent, 128 to 144 in 13.3 percent, 152 to 200 in 1.7 percent. The population
|
||||
Summed over the 100,000 programs, one hash of each does 12,792,736 static loads and 10,243,731 distinct ones:
|
||||
19.93 percent of all loads are repeats.
|
||||
|
||||
Flags:
|
||||
|
||||
| Flag | programs | share |
|
||||
|---|---|---|
|
||||
| s_redundant > 0 (a load re-reads an address inside the hash, static) | 94,774 | 94.77% |
|
||||
| dist_mean < lph (the same, measured) | 96,771 | 96.77% |
|
||||
| s_cancel > 0 (two loads cancel to the identity) | 22,530 | 22.53% |
|
||||
| inj_missing > 0 (a register with no injecting write) | 2,429 | 2.43% |
|
||||
| never_written > 0 | 143 | 0.14% |
|
||||
| rotl_only > 0 | 142 | 0.14% |
|
||||
| never_read > 0 | 160 | 0.16% |
|
||||
| last_contract > 0 (a register whose last write is or, mul or mulhi) | 79,833 | 79.83% |
|
||||
| end_sat_frac > 0.001 | 3,367 | 3.37% |
|
||||
| end_sat_frac > 0.01 | 680 | 0.68% |
|
||||
| or_sat_frac > 0.1 | 294 | 0.29% |
|
||||
| const_bits_max > 0 (a register with a nonce-independent bit) | 726 | 0.73% |
|
||||
| const_bits_max >= 4 | 172 | 0.17% |
|
||||
| nonce_indep_regs > 0 (a whole register nonce-independent) | 31 | 0.03% |
|
||||
| sites_lane_const > 0 (a load site all 32 lanes read at one address) | 30 | 0.03% |
|
||||
| sites_nonce_const > 0 | 30 | 0.03% |
|
||||
| bias_z_max > 4 | 409 | 0.41% (405 expected by chance over 64 bits x 100,000 programs) |
|
||||
| bias_z_max > 5 | 12 | 0.012% (3.7 expected by chance) |
|
||||
| bias_z_max > 6 | 6 | 0.006% (0.0 expected by chance) |
|
||||
| aval_mean outside 31..33, or any output bit flipping outside 0.45..0.55 | 0 | 0 |
|
||||
| dups > 0 | 6 | 0.006% (see below) |
|
||||
|
||||
Avalanche is clean across the whole population: no program has a mean outside 31.85 to 32.15 or an output bit
|
||||
that flips outside 0.473 to 0.523, and the cross-warp (high nonce bit) means stay in 31.64 to 32.39. Bias and
|
||||
avalanche are not where this generator is weak. The six `dups` programs are sampler collisions (two of the 128
|
||||
base nonces coincided; about 6 expected in 100,000 at 128 draws from 2^27), confirmed by `igneum-census show`
|
||||
("sampler collision: base nonce ... drawn twice"), so `dups` is not used by any rule.
|
||||
|
||||
Saturation rises with the number of `or` instructions, as expected, and is not confined to programs with many:
|
||||
|
||||
| or instructions | programs | end_sat_frac > 0.01 | or_sat_frac > 0.1 | const_bits_max > 0 |
|
||||
|---|---|---|---|---|
|
||||
| 0 | 7,202 | 9 (0.12%) | 0 | 59 (0.82%) |
|
||||
| 1 | 19,449 | 38 (0.20%) | 14 (0.07%) | 120 (0.62%) |
|
||||
| 2 | 25,608 | 68 (0.27%) | 42 (0.16%) | 157 (0.61%) |
|
||||
| 3 | 22,341 | 130 (0.58%) | 79 (0.35%) | 150 (0.67%) |
|
||||
| 4 | 14,076 | 127 (0.90%) | 65 (0.46%) | 112 (0.80%) |
|
||||
| 5 | 7,040 | 133 (1.89%) | 34 (0.48%) | 73 (1.04%) |
|
||||
| 6 | 2,882 | 90 (3.12%) | 26 (0.90%) | 29 (1.01%) |
|
||||
| 7 | 1,009 | 45 (4.46%) | 19 (1.88%) | 14 (1.39%) |
|
||||
| 8 or more | 393 | 40 (10.18%) | 15 (3.82%) | 12 (3.05%) |
|
||||
|
||||
Programs with no `or` at all still produce stuck registers (59 of 7,202), which is the `mulhi` mechanism below.
|
||||
|
||||
## 4. What the tails are
|
||||
|
||||
Three mechanisms account for the dynamic tails. Each was read off `igneum-census show`, which prints the
|
||||
instruction list and, per register, the share of final values at 0 and at all-ones, the constant bits, and the
|
||||
list of ops that write it.
|
||||
|
||||
1. A register with no injecting write (`inj_missing`, 2.43 percent of programs). Program 5110: r3 is written only
|
||||
by `rotl@6 or@20 rotr@57`. `or` with a random word each iteration clears a zero bit with probability 1/2, so
|
||||
after 8 iterations r3 is 0xffffffff in every hash (`ones 1.0000, const bits 32`). r5, written by `mul@8` and
|
||||
three `xor`s whose sources are r3 and r7 (both stuck), ends at 0 in every hash. 29 of the program's load sites
|
||||
read from a stuck register, so all 32 lanes read one address (`sites_lane_const 29`): a GPU serves those loads
|
||||
from one cache line. 18 of the 30 lane-constant programs and 23 of the 31 nonce-independent-register programs
|
||||
have `inj_missing > 0`.
|
||||
|
||||
2. A zero-absorbing set of registers (no static signature). Program 52079: r2, r4, r6 and r7 end at 0 in every
|
||||
hash although each has loads and xors among its writes. `mulhi(a, b)` is bits 32..63 of the product, so for
|
||||
uniform inputs it loses about a bit of magnitude per application; a register fed mostly by `mulhi` and `mul`
|
||||
of its neighbours contracts to 0 within a few iterations, and once a set of registers is at 0 it stays there:
|
||||
`mulhi` and `mul` by a zero register give 0, `mad` with a zero factor leaves the destination alone, and a load
|
||||
through a zero register reads `dataset[0]`, a constant that the xor then cancels against the next read of it.
|
||||
The last write of each of the four registers is a `mulhi`, `mul` or `mad` whose other operand is in the set.
|
||||
Half the final state of this program is a constant, and `end_sat_frac 0.5000`.
|
||||
|
||||
3. Contraction as the last write (`last_contract`, 79.8 percent of programs, mostly harmless). Program 99109 has no
|
||||
stuck register and no redundant load, but r5, r6 and r7 are finished by `or@53 or@60 or@62` and `mul@59`, so
|
||||
the final r7 is all-ones 12 percent of the time and one output bit has ones frequency 0.611 (`bias_z_max
|
||||
14.25`). Only 6 programs in 100,000 exceed 6 sigma on any output bit at 4,096 nonces, against 0.03 expected by
|
||||
chance, and all six are of this kind. A bias common to every miner is a difficulty distortion for one hour,
|
||||
not an unfairness, but a rule that costs nothing should still exclude it (section 6).
|
||||
|
||||
## 5. Redundant loads: the finding behind M5
|
||||
|
||||
A load is `dst ^= dataset[src AND MASK]`. If the next load from the same `src` comes before anything writes `src`,
|
||||
it reads the same address. The generator draws `src` uniformly from the seven registers other than `dst` and
|
||||
writes a uniformly drawn `dst` on every instruction, so after a load from register `s` the next instruction is a
|
||||
load from `s` with probability 1/28 and a write to `s` with probability 1/8; the next load from `s` is a repeat
|
||||
with probability about 0.22, and with 16 loads per program the expected number of repeats is about 3.2 per
|
||||
iteration, which is what the census measures (`s_redundant` mean 3.20, 94.8 percent of programs above 0). Of
|
||||
those, a repeat into the same destination with nothing in between cancels to the identity (`s_cancel`, 22.5
|
||||
percent of programs): two loads that do nothing at all.
|
||||
|
||||
The static count matches the dynamic one. `dist_mean` equals `lph - 8 x s_redundant` in nearly every program;
|
||||
the small shortfalls (for example `igneum-genesis/epoch1`: 79.47 against 80) are `rotr` by a lane whose amount
|
||||
is 0 mod 32, `or` into a saturated word and similar value-level coincidences.
|
||||
|
||||
The GPU does not pay for a repeat. The kernel has no stores to global memory inside the loop, so a compiler is
|
||||
free to reuse the loaded value, and an L1 hit costs nothing against a DRAM miss even if it does not. The Mac rates
|
||||
measured in `proto-metal/README.md` and `MEMHARD.md` section 2.4 bear this out. "G loads/s" is Mhash/s times loads
|
||||
per hash; the distinct count is from this census (`igneum-census probe`, 64 warps per seed):
|
||||
|
||||
| Seed | Static loads/hash | Distinct per hash | Repeats per iteration | Load critical path | M5 Max Mhash/s | G static loads/s | G distinct loads/s | RTX 5090 Mhash/s | G static | G distinct |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| igneum-genesis | 104 | 80.00 | 3 | 49 | 45.2 | 4.70 | 3.62 | 228.1 | 23.72 | 18.25 |
|
||||
| igneum-genesis/epoch1 | 104 | 79.47 | 3 | 32 | 48.4 | 5.03 | 3.85 | | | |
|
||||
| igneum-genesis/epoch2 | 112 | 88.00 | 3 | 40 | 40.0 | 4.48 | 3.52 | | | |
|
||||
| igneum-second-seed | 104 | 104.00 | 0 | 41 | 35.5 | 3.69 | 3.69 | | | |
|
||||
| igneum-second-seed/epoch1 | 144 | 104.00 | 5 | 48 | 35.4 | 5.10 | 3.68 | | | |
|
||||
| igneum-hourly | 128 | 96.00 | 4 | 40 | 36.6 | 4.68 | 3.51 | 185.3 | 23.72 | 17.79 |
|
||||
| igneum-hourly/epoch1 | 128 | 95.76 | 4 | 40 | 37.5 | 4.80 | 3.59 | | | |
|
||||
| igneum-hourly/epoch2 | 120 | 96.00 | 3 | 48 | 36.6 | 4.39 | 3.51 | | | |
|
||||
| spread over the 8 Mac rows | | | | | | 3.69 to 5.10, 1.38x | 3.51 to 3.85, 1.10x | | | |
|
||||
|
||||
Two readings. `igneum-second-seed` (104 static, 104 distinct) and `igneum-second-seed/epoch1` (144 static, 104
|
||||
distinct) hash at the same rate on the Mac, 35.5 and 35.4 Mhash/s: a 144-load program and a 104-load program do
|
||||
the same memory work because 40 of the 144 are repeats. And README observation 3, which called
|
||||
`igneum-second-seed` an outlier at the same 13 loads as `igneum-genesis`, is resolved: `igneum-genesis` does 80
|
||||
real reads per hash, `igneum-second-seed` 104. Distinct loads per second vary 1.10x across the eight programs
|
||||
(coefficient of variation 3 percent); static loads per second vary 1.38x (9 percent). The load critical path
|
||||
(32 to 49 here) does not order the residual; the GPU hides that latency with occupancy.
|
||||
|
||||
The two 5090 programs both carry about 24 percent repeats, so they cannot tell the two readings apart (static
|
||||
and distinct are each constant across them). One run decides it: `igneum-second-seed` on the 5090 is predicted at
|
||||
about 173 Mhash/s if the card is bound by distinct loads (18.0 G/s over 104) and 228 if by static loads
|
||||
(23.7 G/s over 104). That run is the next bench-log item for the Windows machine.
|
||||
|
||||
Consequence for M5. Fixing the static load count does not fix the memory work per hash: a program with 16 load
|
||||
instructions does anywhere from 6 to 16 distinct reads per iteration today. The rule must fix the distinct
|
||||
count, which the fresh-source rule below does by construction.
|
||||
|
||||
## 6. The rules
|
||||
|
||||
Two generator rules and one rejection rule. The generator rules change every program (the test vectors are
|
||||
re-cut when they are adopted, spec section 1.16 already schedules that); the rejection rule is what makes a node
|
||||
skip a seed.
|
||||
|
||||
G1, exact load count. Draw the 16 load slots first (a uniform 16-subset of the 64 slots by a partial
|
||||
Fisher-Yates over the program stream), then draw the other 48 ops from the ten non-load weights. Every program
|
||||
has 16 load instructions and 128 loads per hash.
|
||||
|
||||
G2, fresh source. On a load slot the source is drawn from the registers (other than `dst`) that an earlier
|
||||
instruction of this program has written and that no load has read since that write. Such a register holds a
|
||||
value produced in this iteration, so the load's address cannot repeat any earlier load's address in the hash,
|
||||
including across the iteration boundary. Nothing is eligible at instruction 0, so the 16 load slots are drawn
|
||||
from slots 1..63. If the eligible list is empty (a load early in the list whose few written registers are all
|
||||
`dst` or already read) the source is drawn as on an ALU slot and the program fails R-a below.
|
||||
|
||||
A first form of G2 was measured too (section 7): eligible meant "not read by a load since its last write", with
|
||||
all eight registers eligible at instruction 0. It stops repeats inside the linear program but not across the
|
||||
wrap, and the wrap alone made R-a reject 36 percent of programs, which is why the definition above is the one
|
||||
proposed.
|
||||
|
||||
R, rejection (deterministic, evaluated by every node on the candidate program before it is used):
|
||||
|
||||
| Part | Test | What it catches |
|
||||
|---|---|---|
|
||||
| R-a (static) | no load whose source register is unwritten since the previous load from it, in cyclic order over the 64 instructions | the empty-list case of G2; under the current generator, 94.8 percent of programs |
|
||||
| R-b (static) | every register has at least one write by `add`, `sub`, `xor`, `mad`, `shfl` or `load` | the saturating registers of mechanism 1 (2.43 percent of programs today) |
|
||||
| R-c (dynamic) | over 64 fixed warps (base nonces from SplitMix64 seeded by the program seed, as the census draws them) on the closed-form dataset keyed by the program's own seed words: no register has a bit that is constant over all 2,048 final values; no load site reads one address in all 32 lanes of any warp; final values equal to 0 or 0xffffffff are under 1 percent; no output bit's ones frequency deviates from 0.5 by more than 6 sigma (0.066 at 2,048 nonces); the mean number of distinct addresses per hash exceeds 120 (fewer than one repeat per iteration) | mechanism 2 (zero-absorbing sets), the rest of mechanism 1, mechanism 3, and the value-level repeats of section 7.1 |
|
||||
|
||||
R-c uses the closed-form dataset (`verify.rs::dataset_elem`, six integer ops) rather than the day's memory-hard
|
||||
dataset so that the test is a pure function of the program, costs under 2 ms on one core (the closed-form
|
||||
interpreter runs at 0.002 ms per warp, `igneum-pow` README), and does not have to wait for the day's cache. The
|
||||
closed-form words are as random-looking as the memory-hard ones for every property R-c measures; section 7.3
|
||||
checks this on the whole population. A candidate that fails R is skipped and the next candidate is generated from
|
||||
`seed_words_from_bytes(program_seed || k_le32)` for attempt `k = 1, 2, ...` (attempt 0 is the bare seed, so every
|
||||
existing vector stands). Section 7 measures how often that happens.
|
||||
|
||||
Not in the rule, and why: `last_contract` (80 percent of programs) is too common and R-c already catches the
|
||||
cases where it matters; `or` count caps the same; a cap on the load critical path (`depth`) is a hash-rate
|
||||
question, not a weakness, and section 5 shows the GPU does not care at this depth.
|
||||
|
||||
Cross-check of the static half against the dynamic flags under the current generator (R-a or R-b, 94,889
|
||||
programs rejected): of the 726 programs with a constant register bit it rejects 697, of the 30 with a
|
||||
lane-constant load site all 30, of the 31 with a nonce-independent register all 31, of the 680 with more than 1
|
||||
percent saturation 663, of the 6 past 6 sigma 5. The register and bias conditions of R-c alone (1,232 programs)
|
||||
catch all of those by definition; the static half exists so that the common structural causes are excluded
|
||||
without running anything, and so that a reviewer can read the rule.
|
||||
|
||||
## 7. The proposed generator, measured
|
||||
|
||||
The same 100,000 seeds were run through G1 + G2 (`--gen fixed16-fresh2`, the proposed form) and through G1 with
|
||||
the first form of G2 (`--gen fixed16-fresh`), 64 warps (2,048 nonces) per program, memory-hard dataset, same
|
||||
nonce sampler. The generator is implemented in `igneum-census/src/main.rs` (`generate_fixed16`), not in
|
||||
`igneum-pow`.
|
||||
|
||||
### 7.1 Three generators side by side
|
||||
|
||||
| | Current generator | G1 + G2 first form (all registers eligible at instruction 0) | G1 + G2 proposed (eligible = written earlier in the program and not read by a load since) |
|
||||
|---|---|---|---|
|
||||
| Loads per hash | 24 to 256, mean 127.9 | 128 | 128 |
|
||||
| Distinct addresses per hash, mean (p1 / p50 / p99) | 102.4 (56 / 104 / 152) | 124.9 (113.7 / 127.8 / 128) | 127.7 (120 / 127.9990 / 128) |
|
||||
| Programs with a repeated load (static, cyclic) | 94.77% | 36.19% | 1.5710% |
|
||||
| Programs with a cancelling pair | 22.53% | 1.56% | 0.2390% |
|
||||
| inj_missing > 0 | 2.43% | 2.41% | 2.4050% |
|
||||
| Load critical path, mean (p1 / p99) | 41.3 (24 / 66) | 47.6 (32 / 72) | 51.6 (32 / 80) |
|
||||
| const_bits_max > 0 | 0.73% | 0.73% | 0.6920% |
|
||||
| sites_lane_const > 0 | 0.03% | 0.03% | 0.0390% |
|
||||
| end_sat_frac > 0.01 | 0.68% | 0.61% | 0.6000% |
|
||||
| bias_z_max > 6 | 6 programs | 2 programs | 2 programs |
|
||||
| R-a or R-b (static) rejects | 94.89% | 37.44% | 3.93% |
|
||||
| R-c register and bias conditions reject | 1.23% | 1.15% | 1.08% |
|
||||
| R-c distinct-count condition (mean distinct per hash above 120) rejects | 93.36% | 6.28% | 1.06% |
|
||||
| R-c (all) rejects | 93.44% | 7.19% | 2.05% |
|
||||
| R (all) rejects | 94.95% | 38.25% | 5.14% |
|
||||
| Expected candidates per epoch | 19.8 | 1.62 | 1.054 |
|
||||
|
||||
Reading. The register-level weaknesses (stuck registers, saturation, lane-constant sites) are properties of the
|
||||
ALU mix and come out the same under every generator, at about 1.2 percent of programs; the generator rules do
|
||||
not touch them and R-c is what removes them. The first form of G2 removes repeats inside the linear program but
|
||||
leaves the wrap (a load from a register that was read by a load late in the previous iteration and not written
|
||||
since), which by itself fails 36 percent of programs. The proposed form is cyclically fresh by construction and
|
||||
fails R-a only when a load early in the list finds no written-and-unread register other than its destination.
|
||||
Under the proposed form a load's address can still coincide with an earlier one by value. Mostly this is noise
|
||||
at the 1-in-32 level (`rotr` by a lane amount that is 0 mod 32, `mad` with a zero product, `or` into a saturated
|
||||
word), different per lane, and the accepted population's `dist_mean` sits within a fraction of a load of 128
|
||||
(section 7.2). In 1.06 percent of programs it is structural: `igneum-census show` on program 83504 lists 16
|
||||
pairs of load sites that read the same address in every lane (two registers holding the same word when the
|
||||
second load executes; the cause was not isolated), so the hash does 112 reads where the kernel text says 128.
|
||||
That is why R-c carries the distinct-count condition: a program must average more than 120 distinct addresses
|
||||
per hash over the test warps, and the accepted population then has no program below 120.05.
|
||||
|
||||
### 7.2 The accepted population under the proposed generator
|
||||
|
||||
Metrics over the 94,858 programs of the proposed generator that R accepts:
|
||||
|
||||
| Metric | mean | min | p1 | p50 | p99 | max |
|
||||
|---|---|---|---|---|---|---|
|
||||
| lph | 128 | 128 | 128 | 128 | 128 | 128 |
|
||||
| dist_mean | 127.89 | 120.05 | 126.91 | 128.00 | 128 | 128 |
|
||||
| redundant_frac | 0.0009 | 0 | 0 | 0.0000 | 0.0085 | 0.0621 |
|
||||
| depth (loads in series) | 51.7 | 24 | 32 | 49 | 80 | 104 |
|
||||
| mlp | 2.58 | 1.23 | 1.60 | 2.61 | 4.00 | 5.33 |
|
||||
| end_sat_frac | 0.0000 | 0 | 0 | 0 | 0.0018 | 0.0094 |
|
||||
| or_sat_frac | 0.0012 | 0 | 0 | 0.0001 | 0.0243 | 0.2304 |
|
||||
| const_bits_max | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||
| bias_z_max | 2.60 | 1.37 | 1.81 | 2.56 | 3.76 | 5.88 |
|
||||
| aval_mean | 32.000 | 31.787 | 31.882 | 32.000 | 32.119 | 32.223 |
|
||||
| flip_min | 0.4851 | 0.4671 | 0.4769 | 0.4854 | 0.4906 | 0.4941 |
|
||||
| flip_max | 0.5150 | 0.5062 | 0.5094 | 0.5146 | 0.5231 | 0.5326 |
|
||||
|
||||
Every accepted program does 128 loads per hash of which at least 120 and typically 128 are distinct, has no
|
||||
constant register bit, keeps saturation under 1 percent, and shows the avalanche and bit-flip figures of an
|
||||
ideal 64-bit function to within sampling noise at 2,048 nonces. The load critical path is longer than under the
|
||||
current generator (median 49 against 40 loads in series) because a fresh source is often the register the
|
||||
previous load just wrote; section 5 found no rate effect of the critical path at this depth on the Mac, and the
|
||||
5090 run of ten programs from this generator (section 10) is what settles it on NVIDIA.
|
||||
|
||||
### 7.3 The dynamic test does not depend on the dataset
|
||||
|
||||
R-c is specified on the closed-form dataset so that it is a pure function of the program. To check that this
|
||||
sees the same programs as the memory-hard dataset, both censuses were repeated with `--closed-form` (same seeds,
|
||||
same warps, `dataset_elem` words instead of the cache-derived ones; 125.6 s and 59.4 s on 8 threads once the
|
||||
machine was quiet) and the R-c conditions compared program by program.
|
||||
|
||||
Current generator, 100,000 programs, 4,096 nonces each, same nonce sample on both datasets:
|
||||
|
||||
| Condition | memory-hard | closed-form | both | memory-hard only | closed-form only |
|
||||
|---|---|---|---|---|---|
|
||||
| const_bits_max > 0 | 726 | 729 | 708 | 18 | 21 |
|
||||
| const_bits_max >= 4 | 172 | 168 | 164 | 8 | 4 |
|
||||
| nonce_indep_regs > 0 | 31 | 36 | 22 | 9 | 14 |
|
||||
| sites_lane_const > 0 | 30 | 33 | 22 | 8 | 11 |
|
||||
| end_sat_frac > 0.01 | 680 | 679 | 679 | 1 | 0 |
|
||||
| bias_z_max > 6 | 6 | 8 | 5 | 1 | 3 |
|
||||
| dist_mean <= lph - 8 | 93362 | 93362 | 93362 | 0 | 0 |
|
||||
| R-c, register and bias conditions | 1232 | 1232 | 1216 | 16 | 16 |
|
||||
| R-c, all five conditions | 93437 | 93438 | 93437 | 0 | 1 |
|
||||
|
||||
Per-program differences between the two datasets: `dist_mean` max 0.327 (mean 0.0021), `end_sat_frac` max 0.0110 (mean 0.000018), `or_sat_frac` max 0.0102, `bias_z_max` max 3.50 (mean 0.464, the sampling noise of two independent draws), `aval_mean` max 0.220.
|
||||
|
||||
Proposed generator (G1 + G2), 100,000 programs, 2,048 nonces each, same nonce sample on both datasets:
|
||||
|
||||
| Condition | memory-hard | closed-form | both | memory-hard only | closed-form only |
|
||||
|---|---|---|---|---|---|
|
||||
| const_bits_max > 0 | 692 | 681 | 635 | 57 | 46 |
|
||||
| const_bits_max >= 4 | 147 | 145 | 137 | 10 | 8 |
|
||||
| nonce_indep_regs > 0 | 31 | 34 | 24 | 7 | 10 |
|
||||
| sites_lane_const > 0 | 39 | 40 | 33 | 6 | 7 |
|
||||
| end_sat_frac > 0.01 | 600 | 600 | 599 | 1 | 1 |
|
||||
| bias_z_max > 6 | 2 | 2 | 2 | 0 | 0 |
|
||||
| dist_mean <= lph - 8 | 1057 | 1056 | 1056 | 1 | 0 |
|
||||
| R-c, register and bias conditions | 1078 | 1079 | 1059 | 19 | 20 |
|
||||
| R-c, all five conditions | 2054 | 2055 | 2035 | 19 | 20 |
|
||||
|
||||
Per-program differences between the two datasets: `dist_mean` max 0.416 (mean 0.0042), `end_sat_frac` max 0.0123 (mean 0.000023), `or_sat_frac` max 0.0159, `bias_z_max` max 3.31 (mean 0.464, the sampling noise of two independent draws), `aval_mean` max 0.317.
|
||||
|
||||
Reading. The per-program metrics are the same to three decimals on both datasets; saturation, the distinct
|
||||
count and the lane-constant sites are properties of the program, and the dataset words only need to look random
|
||||
for them to show. The verdicts that differ are at the edges of the thresholds: every `const_bits_max > 0`
|
||||
disagreement is a program with exactly one constant bit on one dataset and a nearly constant bit on the other
|
||||
(the strong cases, four or more constant bits, agree in 164 of 172 and 168 under the current generator and
|
||||
137 of 147 and 145 under the proposed one), the `bias_z_max > 6` disagreements
|
||||
are programs between 5 and 7 sigma, and the single `end_sat_frac` disagreement sits at 1.0 percent. Under the
|
||||
proposed generator the full R-c verdict agrees on all but 39 of 100,000 programs (2054 rejected on the memory-hard
|
||||
dataset, 2055 on the closed-form one), all threshold-edge cases of the kinds just listed. A borderline program
|
||||
that one dataset accepts and the other rejects is a program with a nearly constant bit or a bias near 6 sigma;
|
||||
whichever side of the line it falls, R-c on the closed-form dataset is the definition, every node evaluates
|
||||
the same one, and the cost of accepting a borderline program is a near-constant bit in one register, not a
|
||||
shortcut.
|
||||
|
||||
## 8. Hash-rate spread, before and after
|
||||
|
||||
The RTX 5090 is bound at about 23.7 G random loads per second past its L2 (`docs/bench-log.md`, RTX 5090 sweep),
|
||||
which is 18.0 G distinct loads per second once repeats are discounted (section 5, mean of the two programs).
|
||||
Rate per hour is that figure over the hour's loads per hash.
|
||||
|
||||
| Population | Distinct loads per hash (p1 / p50 / p99, min / max) | RTX 5090 Mhash/s at p1 / p50 / p99 | Spread p1 to p99 | Spread min to max |
|
||||
|---|---|---|---|---|
|
||||
| Current generator, by static count (the ledger's reading) | 64 / 128 / 192, 24 / 256 | 370 / 185 / 123 (at 23.7 G static loads/s) | 3.0x | 10.7x |
|
||||
| Current generator, by distinct count (what the card does) | 56 / 104 / 152, 24 / 200 | 321 / 173 / 118 | 2.7x | 8.3x |
|
||||
| G1 + G2 + R | 128 / 128 / 128 | 141 / 141 / 141 | 1.0x from the load count | 1.0x |
|
||||
|
||||
What remains after the rule is the program-shape residual: 1.10x across the eight programs with measured Mac
|
||||
rates (section 5), approximate, cause not isolated, to be re-measured on the 5090 with ten programs from the
|
||||
new generator. The Mac projection is 3.62 G distinct loads/s over 128, about 28 Mhash/s per hour on the M5 Max.
|
||||
Both projections assume the card stays random-access bound at 128 distinct loads, which the 5090 sweep showed for
|
||||
104 and 128 static loads.
|
||||
|
||||
Note the level: 141 Mhash/s on the 5090 is below today's 185 and 228 because today's measured programs do 80
|
||||
and 96 real reads per hash, not 104 and 128. An alternative that keeps today's median memory work is 13 load
|
||||
slots (104 distinct, 173 Mhash/s, 3,328 items per unit). The recommendation is 16: it is the spec's own
|
||||
candidate, it matches the 25 percent load weight, it puts a unit at exactly the 4,096-item design bound of spec
|
||||
section 1.11, and the Rust verifier at 4,608 items measured 0.579 ms per unit, so 4,096 items is about 0.52 ms
|
||||
against the 10 ms gate. Hash rate is a number the difficulty absorbs; memory work per hash is the defence.
|
||||
|
||||
## 9. Proposed spec text
|
||||
|
||||
For `docs/spec/01-lottery-hash.md`, replacing the "What fixes them" paragraph of section 1.4.2 and the op draw of
|
||||
section 1.4.3, and adding a section 1.4.6. The spec's owner applies it; nothing below has been written into the
|
||||
spec.
|
||||
|
||||
> **1.4.2 Op weights and the load count.** Every program contains exactly 16 `load` instructions (`LOAD_SLOTS`,
|
||||
> Definition), so every hash performs 128 loads and a 32-lane unit derives at most 4,096 dataset items, the
|
||||
> bound of section 1.11. The other 48 instructions are drawn from the ten non-load families with the weights of
|
||||
> the table (12 10 8 8 8 7 6 6 6 4, sum 75). The load weight 25 of the earlier table is retired; it survives only
|
||||
> as the ratio 16 of 64.
|
||||
>
|
||||
> **1.4.3 Draw order.** From the program stream of 1.3.3, in this order. (1) Load slots: let `p[0..62] = 1..63`
|
||||
> (instruction 0 is never a load: nothing is fresh before it); for `i` in 0..15 draw `j = i + below(63 - i)` and
|
||||
> swap `p[i]` and `p[j]`; the load slots are `p[0..15]`.
|
||||
> (2) For each instruction `k` in 0..63, nine draws: `roll = below(75)` (on a load slot the roll is drawn and
|
||||
> ignored; otherwise the op is the first entry of the ten-family table whose cumulative weight exceeds `roll`);
|
||||
> `dst = below(8)`; on an ALU slot `a = below(7)` and `src = a + (a >= dst)`, on a load slot `a = below(|E|)`
|
||||
> and `src = E[a]` where `E` is the list, in register order, of registers other than `dst` that an earlier
|
||||
> instruction of this program has written and that no later `load` has used as its source (`E` is empty before
|
||||
> instruction 0), and if `E` is empty then `a = below(7)` and `src = a + (a >= dst)` as on an ALU slot;
|
||||
> `b = below(8)`; `imm = low32(next())`; `imm2 = low32(next())`; `rot = 1 + below(31)`; `bit = below(32)`;
|
||||
> `mask = 1 << below(5)`. 592 draws per program. A load's source holds a value written in the same iteration
|
||||
> that no earlier load has read, so no load repeats the address of an earlier load of the same hash; 1.4.6 (a)
|
||||
> excludes the programs where `E` was empty.
|
||||
>
|
||||
> **1.4.6 Program acceptance.** A candidate program is accepted only if all of the following hold, and every
|
||||
> conforming implementation MUST evaluate them identically. (a) For every `load`, some instruction between the
|
||||
> previous `load` from the same source register and this one, in cyclic order over the 64 instructions, writes
|
||||
> that register. (b) Every register `r0..r7` is the destination of at least one `add`, `sub`, `xor`, `mad`,
|
||||
> `shfl` or `load`. (c) The program is interpreted (section 1.7) for 64 units at base nonces
|
||||
> `low32(next()) AND NOT 31` from a SplitMix64 stream seeded with `FNV-1a-64("igneum-accept/" || seed words as
|
||||
> little-endian bytes)`, with init words `I` equal to the seed words and dataset words
|
||||
> `dataset_elem(idx, S[0], S[1])` of `verify.rs` in place of the memory-hard dataset, and over those 2,048
|
||||
> evaluations: no register has a bit equal in every final value; no `load` site (iteration, instruction) reads
|
||||
> one address in all 32 lanes of any unit; the number of final register values equal to 0 or 2^32 - 1 is below
|
||||
> 164 (1 percent of 16,384); every output bit's ones count is within 6 x sqrt(2048) / 2 = 136 of 1,024; and the
|
||||
> number of distinct masked dataset addresses read by one lane in one evaluation, summed over the 2,048
|
||||
> evaluations, exceeds 245,760 (a mean above 120 of the 128 loads). If
|
||||
> the candidate fails, attempt `k + 1` is generated from `seed_words_from_bytes(program_seed || k_le32)` for
|
||||
> `k = 1, 2, ...`, attempt 0 being `seed_words_from_bytes(program_seed)`; the first accepted candidate is the
|
||||
> program of the epoch. Measured rejection rate under this generator: see `docs/analysis/weak-program-census-2026-10-03.md`
|
||||
> section 7: 5.14 percent, so the probability that 15 consecutive candidates fail is below 2^-64, and an
|
||||
> implementation MAY treat 32 consecutive failures as a consensus fault.
|
||||
|
||||
The 592-draw count: 16 slot draws plus 64 x 9. The test-vector consequence: every pack and every vector in
|
||||
sections 1.4.3, 1.15 and 1.17 is re-cut on adoption; `igneum-census show --gen fixed16-fresh --seed
|
||||
igneum-genesis` prints the first program of the new generator today (`--gen fixed16-fresh2`, op mix `load=16
|
||||
add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1`, load critical path 48).
|
||||
|
||||
## 10. What this does not show, and reproduction
|
||||
|
||||
Not shown: any GPU number for the new generator (the 141 Mhash/s figure is a projection from the 5090's measured
|
||||
random-load rate; the next 5090 session should run ten programs from `--gen fixed16-fresh2` and `igneum-second-seed`
|
||||
through the existing pack path); the distinct-versus-static reading on NVIDIA (one run decides it, section 5); an adversary who
|
||||
grinds the epoch seed (the VDF of section 4 of the spec is the answer, and the rejection rule removes the
|
||||
programs a grinder would want); cryptographic strength of anything (ledger M7); whether 6 sigma at 2,048 nonces
|
||||
is the right bias threshold for the lottery (it is the loosest threshold that catches every bias the census
|
||||
found and triggers by chance about once in 10^6 programs).
|
||||
|
||||
Commands (from `igneum-census/`, `~/.cargo/bin/cargo build --release` first; `S` is a scratch directory):
|
||||
|
||||
```
|
||||
nice -n 15 ./target/release/igneum-census run --root igneum-census-2026-10-03 --count 100000 --warps 128 --threads 8 --gen default --out $S/census-default-100k.tsv
|
||||
nice -n 15 ./target/release/igneum-census run --root igneum-census-2026-10-03 --count 100000 --warps 64 --threads 8 --gen fixed16-fresh2 --out $S/census-fixed16-fresh2-100k.tsv
|
||||
nice -n 15 ./target/release/igneum-census run --root igneum-census-2026-10-03 --count 100000 --warps 64 --threads 8 --gen fixed16-fresh --out $S/census-fixed16-fresh-100k.tsv
|
||||
nice -n 15 ./target/release/igneum-census run --root igneum-census-2026-10-03 --count 100000 --warps 128 --threads 8 --gen default --closed-form --out $S/census-default-cf-100k.tsv
|
||||
./target/release/igneum-census summarise --in $S/census-default-100k.tsv
|
||||
./target/release/igneum-census probe --warps 64 --seed igneum-genesis --seed igneum-genesis/epoch1 ... (section 5 table)
|
||||
./target/release/igneum-census show --warps 16 --seed igneum-census-2026-10-03/52079 (section 4 diagnostics)
|
||||
```
|
||||
|
||||
The TSVs (about 25 MB each) are not checked in; every table is reproducible from the commands above.
|
||||
|
|
@ -325,3 +325,14 @@ Machine: Apple M5 Max, 64 GB, load 61.19 55.26 50.53. Private test network of ig
|
|||
Full JSON per scenario under /tmp/igneum-harness/results and /tmp/igneum-harness/sim. The simulator (igneum/harness-sim in the fork worktree) runs real consensus code in virtual time with PoW skipped, as rusty-kaspa simpa does; the live scenarios (5, 6, 7 Part B) drive real igneumd processes over wRPC and the fork's own p2p (igneum/p2p-probe).
|
||||
|
||||
Finality and difficulty-controller scenarios are stubs here: their criteria are written and they run against those branches once merged into the harness worktree (see tools/harness/scenarios/stubs.mjs).
|
||||
|
||||
## 3 October 2026, weak-program census: 400,000 program runs through the CPU reference, the redundant-load finding, and the rules for M5 and M6 (cryptographer)
|
||||
|
||||
Machine: Apple M5 Max, 8 threads at `nice -n 15` while a devnet build and its simulations shared the box (load average 25 to 107), rustc 1.99.0, release build with LTO. New crate `igneum-census/` (path dependency on `igneum-pow`, nothing in `igneum-pow` changed); the instrumented interpreter is checked against `igneum_pow::hash_warp` on the first warp of every program and against the `igneum-genesis` spec vectors at start.
|
||||
Commands: `igneum-census run --root igneum-census-2026-10-03 --count 100000 --warps 128 --threads 8 --gen default` (memory-hard, 1 GiB, day 2026-10-03; 2,797 s), the same with `--gen fixed16-fresh --warps 64` (1,049 s), `--gen fixed16-fresh2 --warps 64` (6,650 s, starved to under a core for most of it), and `--gen default --closed-form --warps 128` (125.6 s once the machine was quiet); `summarise`, `probe`, `show`. Full tables and the rules in `docs/analysis/weak-program-census-2026-10-03.md`.
|
||||
Current generator, 100,000 programs x 4,096 nonces: loads per hash 24 to 256 (mean 127.9); distinct addresses per hash 24 to 200 (mean 102.4): 19.9 percent of all loads re-read an address the same hash already read, 94.8 percent of programs have at least one such load, 22.5 percent have a pair that cancels to the identity. The Mac rates of the eight bench seeds vary 1.38x by static loads/s and 1.10x by distinct loads/s (3.51 to 3.85 G/s), so the GPU is bound by the distinct count; `igneum-second-seed/epoch1` (144 static, 104 distinct) hashes at the rate of `igneum-second-seed` (104 and 104).
|
||||
Weak programs, current generator: 2.43 percent have a register with no injecting write (saturates to all ones); 0.73 percent have a register with a nonce-independent bit, 0.03 percent a whole nonce-independent register, 0.03 percent a load site read at one address by all 32 lanes, 0.68 percent more than 1 percent of final registers at 0 or all ones, 6 programs an output bit past 6 sigma (0.03 expected by chance). Avalanche clean on every program (mean 31.85 to 32.15, every output bit flips 0.473 to 0.523). Mechanisms: no injecting write; zero-absorbing register sets closed under mulhi/mul; or or mul as the last write.
|
||||
Rules: G1 exactly 16 load slots drawn first from slots 1..63; G2 a load reads only a register written earlier in the program and not read by a load since; R-a no cyclically redundant load; R-b every register has an injecting write; R-c 64 fixed warps on the seed-keyed closed-form dataset with no constant register bit, no lane-constant site, saturation under 1 percent, no output bit past 6 sigma, more than 120 distinct addresses per hash on average. Rejection: 95.0 percent under the current generator (the redundancy alone), 38.3 percent under the first form of G2 (the iteration wrap), 5.14 percent under the proposed form (R-a or R-b 3.93 percent, R-c 2.05 percent), so 1.054 candidates per epoch on average; the accepted population does 120.05 to 128 distinct loads per hash, median 128.00.
|
||||
Closed-form check: with the same seeds and nonces on the closed-form dataset instead of the memory-hard one, R-c agrees on 99,961 of the 100,000 proposed-generator programs (2,054 rejected memory-hard, 2,055 closed-form; the 39 that differ sit at a threshold edge, one nearly constant bit or a bias near 6 sigma) and the per-program metrics agree to three decimals, so the acceptance test can be a pure function of the program.
|
||||
Hash-rate spread: today 2.7x between the 1st and 99th percentile program by distinct loads (56 to 152 per hash; 321 to 118 Mhash/s projected on the RTX 5090 at 18.0 G distinct loads/s), 8.3x min to max; under G1 + G2 every program does 128 distinct loads, projected 141 Mhash/s on the 5090 and 28 on the M5 Max, with the 1.10x program-shape residual the only spread left, approximate. One 5090 run of `igneum-second-seed` (predicted 173 Mhash/s if distinct-bound, 228 if static-bound) settles the reading on NVIDIA.
|
||||
Not done: no GPU run of the new generator; the spec text is proposed in the analysis doc, section 9, not written into `docs/spec/01-lottery-hash.md`; test vectors are re-cut when the generator rule is adopted.
|
||||
|
|
|
|||
2
igneum-census/.gitignore
vendored
Normal file
2
igneum-census/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
target/
|
||||
out/
|
||||
14
igneum-census/Cargo.lock
generated
Normal file
14
igneum-census/Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "igneum-census"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"igneum-pow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "igneum-pow"
|
||||
version = "0.1.0"
|
||||
23
igneum-census/Cargo.toml
Normal file
23
igneum-census/Cargo.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
name = "igneum-census"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Weak-program census for the Igneum lottery hash (ledger items M5 and M6): generates programs from a fixed root, runs them through the igneum-pow CPU reference with instrumentation, and summarises the distributions"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "igneum-census"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
igneum-pow = { path = "../igneum-pow" }
|
||||
|
||||
# The census derives millions of dataset items; an unoptimised build would take hours.
|
||||
[profile.dev]
|
||||
opt-level = 3
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
1212
igneum-census/src/main.rs
Normal file
1212
igneum-census/src/main.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -26,7 +26,7 @@ VERSION="0.1.0"
|
|||
NETWORK="devnet"
|
||||
# The seed node(s) to peer with. For now Josh's Mac on the house LAN; a public seed node address
|
||||
# (host:port) replaces it when one exists. A comma list is allowed: "1.2.3.4:26611,5.6.7.8:26611".
|
||||
SEED_PEERS="${SEED_PEERS:-192.168.68.64:26611}"
|
||||
SEED_PEERS="${SEED_PEERS:-188.245.5.161:26611,192.168.68.64:26611}"
|
||||
# Miner identities on this Mac. 1 is right for one GPU; 0 runs the node alone.
|
||||
MINERS="${MINERS:-1}"
|
||||
STATUS_SECS="${STATUS_SECS:-30}"
|
||||
|
|
|
|||
Loading…
Reference in a new issue