Merge build/class-v5 a2d333d6 into v5-fasttime (igneum-pow at the (c''') acceptance commit, the pair for the rerun on fork b1680b57)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
b04026ed8a
6 changed files with 538 additions and 43 deletions
|
|
@ -28,6 +28,7 @@
|
|||
| 12:00 to 12:11 | PASS on every check (section 10) |
|
||||
| 12:12 to 12:23 | The no-flip case PASS (section 10); both branches pushed; master merged for the build-2 test route (d1285cef) |
|
||||
| 18:5x to 19:07 | Resumed on the project lead's first-priority order (build class v5 now, on the 0.3.23 line, byte 6): the frozen sub-version 3 (ca3-v4-amend 017e7037) merged into class-v5 and release-0.3.23-node into class-v5-node; the draw's and the acceptance rules' class keys set the state flag aside (the acceptance key had judged a v5 candidate under the pre-amendment rule and the draw diverged at instruction 0; fixed, 70 unit tests green on build-2 at 18:04Z); class v5 pinned to object byte 6 counted exactly (byte 7 is v4 sub-version 3); the gap list as section 13; the FIRST CLASS V5 PACK `proto-cuda/packs-ca3-v5/v5-dn3-epoch0` (Devnet 3's genesis 4020cb43... as epoch and era seed, day 20,733, the genesis state's 11 leaves under root 7e37a9fb...), exported on build-1 at 18:06:39 to 18:06:41Z, program id e5a4ac5978462156, its Metal fingerprint on the M5 Max under the measure lock at 18:07:04 to 18:07:09Z: 82b19cbde8557ea5, the three vector warps bit-exact, the build 29.8 ms GPU; the Metal pack bench takes `leaves.bin` |
|
||||
| 19:2x to 19:32 | The AP-F4-1 mixer-draw rule and the AP-F1-1 shadow rule written behind the v5 class with their known-failed cases (2f9bf135; the suite running on build-2); the M5 Max rows measured (section 7: +0.23 ms per warp); the node side handed to the node lane, the kits to the v5-kits lane, the families to the attack-pass lane, a 5090 or 4090 fingerprint run of the first pack asked of the fleet lane |
|
||||
| 13:2x | The coordinator's correction: the hot-set bound is NOT satisfied by sub-version 1 (F8's re-gate: nine of the first 30 seeds over 1.2x, p31 at 29.3x, 0.19 percent hot-set programs; saturated values pass saturation-preserving writers); v5 inherits the gap by merge and takes sub-version 2 by merge when it lands; the AP-F4-1 and AP-F1-1 rules come in the next round; the lane stops here |
|
||||
| 12:5x to 13:21 | The amended class v4 taken in: ca3-v4-amend 8c728ca3 merged into class-v5 (4e737543) and release-0.3.20-node into class-v5-node (699db5a2); the source rule's class key sets the state flag aside so v5 and its rungs draw under it (a unit test pins the v5 chain draw equal to the amended v4's with no lossy-sourced load, the unamended v3 stream the known-failed case); generator 5 keeps `program_id(5, seed, attempt)` with no sub-version (the hash lane's reading: the suffix separates two streams inside generator 4); class v5 is object byte 6 above the amended v4's 5 (the fork, the harness, the default byte); igneum-pow 67 unit and 39 integration tests on igneum-build-2, the packs test with the re-exported control pack, the flip case PASS again with bytes 6,6,6 (the flip at epoch 8 on 3 of 3, 183 v5 blocks, the stale miner off on 59 of 59, the stateless node stopped at 480, equal roots) |
|
||||
| 10:4x | The Counter ASIC coordinator relays the project lead's ruling of option A on AP-F8-1: the injecting-source load draw lands in class v4 itself on the hash lane's branch ca3-v4-amend (0.3.19); class v5 takes it by merging that branch once its commit is up, not by a second implementation; the page's section 11 row stands as the record of the bound and its gate |
|
||||
|
|
@ -191,6 +192,7 @@ The same machinery as class v4 and the ladder (counter-asic-3-node.md section 6;
|
|||
|---|---|---|
|
||||
| The devnet's state (node 1's exec snapshot, tip 159,357) | 93 records, 8,619 bytes; stream built in 0.1 ms; rebuilt and root-checked in 0.1 ms | `igneum-day-stream`, 08:33 UK, core 40 at nice 19 |
|
||||
| Verifier per unit, class v4 against class v5 (one 32-lane warp, core 40 at 3.72 GHz, nice 19, the ladder's method; the box at load 70 from other lanes' builds, so every figure is a loaded-box figure: the ladder's rung-0 row read 5.14 alone and 8.77 loaded at load 25) | v4: cold 8.11 ms alone, 8.74 ms with the sibling loaded; average of 50: 7.83 and 8.63. v5 over the devnet's 93 leaves: cold 8.71 alone, 9.03 loaded; average of 50: 8.54 and 8.83. So v5 adds 0.28 ms cold and 0.20 ms on average with the sibling loaded (+3.2 and +2.3 percent), inside the prototype's +0.11 to 0.21 ms row, and both classes stay under the 10 ms gate on the loaded core at load 70 | `tools/class-v5/verify-bench-remote.sh`, 09:37 UK, raw lines under `docs/design/class-v5-bench/20261007T083716Z/` |
|
||||
| One M5 Max core (the Mac under the measure lock, nice 19, the box rule's own order: class v5 against class v4 interleaved three times, 50 warps each, 19:31 UK, Mac load 8 to 9 from other agents) | class v5 2.535, 2.649, 2.398 ms per warp (average of 50; cold 2.50, 2.99, 2.26); class v4 2.263, 2.224, 2.393 (cold 2.29, 2.25, 2.31): v5 adds 0.23 ms per warp on the average (+10 percent), the order's +0.2 ms; both classes at a quarter of the 10 ms gate on this core, and the 2019-class core by the 2.5x rule reads about 6.3 ms for v5 (O-1.14 still owed). Per tier: a node on any core from a 2019 laptop up keeps its margin; a miner's CPU engine (the harness's) pays the same 10 percent per verified warp and nothing per hash | `igneum-pow bench --program-class v5 --state <stream> --warps 50` on the Mac's own build (rustup 1.99.0; the sub-version 3 igneum-pow at 2f9bf135), raw lines kept in the lane's scratch |
|
||||
| RTX 4090 (the fleet's pod cv5-2, driver 595.91.07, nvcc 12.8, 450 W limit, the card alone; `proto-newpow/class-v5/bench.cu` on the two pinned packs, 10:23 to 10:30 UK): hash rate, v4 against v5 | v4 63.067, 63.066, 63.064, 63.065 MH/s; v5 63.058, 63.058, 63.052, 63.059 MH/s over 10 batches of 2^24 each (GPU event time): v5 is 0.01 percent under v4, equal within the batch noise; the hash kernel is the same text | `docs/design/class-v5-bench/pod-4090-20261007/run.log` and `run60.log` |
|
||||
| 4090: watts | the four 20-s windows read 269.4 (v4), 275.8 (v5), 275.9 (v4), 284.2 (v5) W and the four 60-s windows in the order v5, v4, v5, v4 read 284.5, 294.3, 295.8, 297.1 W: a monotone warm-up drift over the eight minutes of the run with the classes interleaved inside it (v4's last window is the highest), so the class moves nothing a window can read; the prototype's control-against-sd1 rows on 6 October read 205 to 208 W on another 4090 at the same 63.08 MH/s (this pod's card draws 4.3 to 4.7 microjoules per hash against that box's 3.27: another host, another power state, the same rate) | the same logs |
|
||||
| 4090: dataset build with the devnet's leaves (93 x 64 B) | v4 30.56 to 30.58 ms, v5 30.79 to 30.81 ms: +0.24 ms (+0.8 percent) for the cache-resident leaf read; the prototype's +1.43 ms was with a 1 GiB leaf array | the same logs |
|
||||
|
|
@ -234,7 +236,7 @@ Per tier, what the 4090 rows mean: every NVIDIA card from 8 to 32 GB keeps its h
|
|||
| Case | Run (UK) | SUMMARY |
|
||||
|---|---|---|
|
||||
| The known-failed case first (`--signal 5,5,4 --expect flip`) | 09:37 to 09:48 | FAIL as it must: no flip at 7,519 bps of byte 5 (two signalling nodes of three plus the stateless node), 665 blocks, 0 rejected, four sinks equal, eleven flip checks named `FAILED CHECK`, exit 1 (`failed-case.json`) |
|
||||
| The flip with the stale miner and the stateless node (`--signal 5,5,5 --expect flip --stale 2`) | 12:00 to 12:11 (the sixth run; the five before it found the harness's own faults and two of the fork's, section 0) | PASS on every check: `Program class v5 by miner signal: epoch 8 (... weakest 10000 bps, 420 of 420 blue blocks)` on 3 of 3 nodes, the first epoch with seven full windows after v4; the template class v5 from epoch 8 (DAA 480) at 486 s wall; 481 / 185 blocks across the boundary; the three signalling nodes at one sink, 665/665/665; the miners' v5 program ids equal to the CLI's `--program-class v5 --state` id on epochs 9, 10 and 11 and unequal to the same seeds' v4 ids; a different state root per v5 epoch (828e6d26..., 2ae82ead..., 3a94e581...: the refresh per window), the same root on every executing node; the stale miner (its first stream kept for every later epoch) accepted 0 after the first refresh and rejected on 66 of 66 by its own node; the stateless node (`--evm-disable`) accepted 0 after the flip, 14 refusal lines, its chain stopped at 480 (`flip-stale.json`) |
|
||||
| The flip with the stale miner and the stateless node (`--signal 5,5,5 --expect flip --stale 2`) | 12:00 to 12:11 (the sixth run; the five before it found the harness's own faults and two of the fork's, section 0). CORRECTION 20:3x UK (the v5-fasttime lane): the two "accepted 0 after" counts of this row and the 13:21 row were parse misses (the harness read the miner's line clock as HH:MM:SS where it is epoch seconds), so they proved nothing; the stale miner's 66 rejections by its own node and the stateless node's chain stopped at 480 with 14 refusal lines are real and are what those two checks rest on until the re-run with the fixed parser (section 0) | PASS on every check: `Program class v5 by miner signal: epoch 8 (... weakest 10000 bps, 420 of 420 blue blocks)` on 3 of 3 nodes, the first epoch with seven full windows after v4; the template class v5 from epoch 8 (DAA 480) at 486 s wall; 481 / 185 blocks across the boundary; the three signalling nodes at one sink, 665/665/665; the miners' v5 program ids equal to the CLI's `--program-class v5 --state` id on epochs 9, 10 and 11 and unequal to the same seeds' v4 ids; a different state root per v5 epoch (828e6d26..., 2ae82ead..., 3a94e581...: the refresh per window), the same root on every executing node; the stale miner (its first stream kept for every later epoch) accepted 0 after the first refresh and rejected on 66 of 66 by its own node; the stateless node (`--evm-disable`) accepted 0 after the flip, 14 refusal lines, its chain stopped at 480 (`flip-stale.json`) |
|
||||
| Two of three signal v5 (`--signal 5,5,4 --expect no-flip --epochs 11`) | 12:12 to 12:23 (the first start refused at once: port 30140 was a stranger's, the new bind check fired) | PASS: no v5 epoch over epochs 0 to 11, the byte-5 share at the sink 6,274 to 9,000 bps epoch by epoch (two signalling nodes of three plus the stateless node at byte 5, the third at byte 4), on the chain 505 blocks at byte 5 and 158 at byte 4 (7,605 bps), no signal line on any node, 664 blocks, 0 rejected, four sinks equal at 663 (`no-flip.json`) |
|
||||
|
||||
## 11. What is implemented, tested, owed
|
||||
|
|
@ -270,9 +272,9 @@ Every item of this page that has no code, no test or no measurement yet, with th
|
|||
| Fingerprints equal across platforms, G1 on the 5090 and the Mac, AMD and Intel after | the 4090 pack fingerprints are in (section 7); the Mac's Metal fingerprint is the clock reading below; the 5090 (PC 2) and AMD rows owed | this lane (Mac), the fleet and PC lanes (5090, 9070 XT) | 2 |
|
||||
| The crossing on Devnet 3 by height after its gate | NOT started: the floor in the Devnet 3 override, the Devnet 2 style gate (zero rejected across the flip, exec roots agreeing, every node holding the epoch's stream); the harness's flip case is the rehearsal (PASS twice) | node lane (a283f5f0d364ceef0) owns the fork, the shipper (ae892a8b0f78fe31c) the cut; this lane the gate's v5 checks | 3 |
|
||||
| Miner cost rows per card (5090, M5 Max, 9070 XT, 4070) | the 4090 row only (rate equal within 0.01 percent, build +0.24 ms); the four named cards owed, each labelled measured with the date | fleet (5090, 4070), this lane (M5 Max under the lock), PC 1 (9070 XT) | 3 |
|
||||
| The exec snapshot wire carrying the day streams (a proof-synced node's trusted-data path) | NO code: the executor persists nothing of the captures across a restart and the p2p snapshot carries none | node lane | 3 |
|
||||
| The exec snapshot wire carrying the day streams (a proof-synced node's trusted-data path) | DONE by the node lane (19:3x UK): fork branch class-v5-node-wire 7737ebd9 on both box mirrors, igneum/exec only, SNAPSHOT_VERSION kept at 1 with `epoch_streams` as the last field and a legacy fallback (a 0.3.23 snapshot loads on a pre-field node and the other way round through the sweep); the writer carries the two newest captures, each checked to rebuild to its root; the loader rebuilds each, compares its root with the loaded state's record at that block, publishes the final one to `ExecState.stream_cache` at once and holds the next until its cut, refuses a tampered or foreign stream and installs the rest; tests `snapshot::tests::the_legacy_shape_is_the_struct_without_the_streams` and `service::class_v5_tests::a_snapshot_carries_the_epoch_streams_and_a_loader_serves_them`, known-failed first; exec suite 43 of 43 on build-2 at 18:33:09Z | node lane | 0 |
|
||||
| The pool protocol's per-epoch state fetch | NO code | pool lane | 2 |
|
||||
| The day-state witness in the pruning-proof format | NO code (the class-signal witness is owed in the same shape) | node lane | 4 |
|
||||
| The day-state witness in the pruning-proof format | IN PROGRESS (the node lane, from 19:3x UK, on class-v5-node-wire): the reference block's hash and state root per epoch beside the epoch seed witnesses (`ProofSeeds::from_witnesses`), checked against the syncing node's snapshot record once it holds one; nothing in igneum-pow changes | node lane | 4 |
|
||||
| The spec text (01 1.8.5 the leaf line, 1.12 the cut, 10 the witness) | NO text | this lane | 2 |
|
||||
| The litepaper paragraph and ledger M35 | done (the litepaper's measured numbers are the 4090's) | this lane | 0 |
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
//! | (a) static | 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) static | every register `r0..r7` is the destination of at least one `add`, `sub`, `xor`, `mad`, `shfl` or `load` |
|
||||
//! | (c) dynamic | the program is interpreted for [`ACCEPT_UNITS`] (64) units of 32 lanes at base nonces drawn from SplitMix64 seeded with `FNV-1a-64("igneum-accept/" \|\| seed words as little-endian bytes)`, each `low32(next()) AND NOT 31`, with init words equal to the seed words and the closed-form dataset `dataset_elem(idx, S[0], S[1])` at [`ACCEPT_DATASET_LOG2`] (2^28 words) in place of the memory-hard dataset. Over the 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; fewer than [`MAX_SATURATED`] (164, 1 percent of 16,384) final register values are 0 or 2^32 - 1; every output bit's ones count is within [`BIAS_TOLERANCE`] (136, 6 sigma) of 1,024; the distinct masked addresses read by one lane in one evaluation, summed over the 2,048 evaluations, exceed [`MIN_DISTINCT_SUM`] (245,760, a mean above 120 of the 128 loads) |
|
||||
//! | (c''') class v5 | the per-site distinct-index floor of (c'') raised to [`MIN_DISTINCT_RATIO_V5`] (0.995) on the same 2^20-evaluation run, keyed on the state flag; the ratio at the 0.98 floor is still named (c'') first (`docs/design/class-v5-stored-state.md` section 14) |
|
||||
//!
|
||||
//! The dynamic test uses the closed form so that it is a pure function of the program (no cache, no day) and
|
||||
//! costs about a millisecond on one core. A hot-table load (`docs/plans/hot-table.md`) reads the closed form keyed by
|
||||
|
|
@ -40,6 +41,18 @@ pub const MIN_DISTINCT_RATIO_V4: f64 = 0.98;
|
|||
/// Kept for the record and the driver, not wired: the most repeated source value per site over the (c) units'
|
||||
/// 16,384 evaluations (a uniform site repeats a value 2 or 3 times; the finding's bands sit under the ratio instead).
|
||||
pub const MAX_SOURCE_REPEAT_V4: u32 = 8;
|
||||
/// (c'''), class v5 (`docs/design/class-v5-stored-state.md` section 14): the per-site distinct-index floor of the
|
||||
/// ratio pass raised from sub-version 3's 0.98 to the bottom of the clean seeds' spread. On the 2^20-evaluation
|
||||
/// sample the model's spread is about 0.0001 (the collision count is near Poisson with mean n^2 / 2W = 8,192 on
|
||||
/// the quarter window); the sub-version 3 clean seeds' site rows read 0.9960 at the minimum, 0.9990 at p1 and
|
||||
/// 1.0000 at the median (adv-accept's census), and the residual class the in-house pass attributed (seed 100767,
|
||||
/// program 9d68e6286fc817d4: site 6 reads the multiples of 2^19 through `rotl(x * stride, rot)` of a `mad` value
|
||||
/// that is near zero in a value class, 3.35 percent of its live reads on the top 0.1 percent of items, 1,677 reads
|
||||
/// of index 0 in 10^6 nonces) reads 0.9919 closed and 0.9920 on the live day at the same sample: 0.012 above the
|
||||
/// 0.98 floor, 0.003 under this one. The floor is the cheapest fix that reaches the class (main's order of 7 October
|
||||
/// 2026): no new sample, the verdict at attempt 2 with no 2^24 run; the census test `v5_hot_census` reads its clean
|
||||
/// rejection rate and the attempts histogram beside the per-site hot-item test.
|
||||
pub const MIN_DISTINCT_RATIO_V5: f64 = 0.995;
|
||||
/// Hashes the dynamic test evaluates: 2,048.
|
||||
pub const ACCEPT_HASHES: usize = ACCEPT_UNITS * LANES;
|
||||
/// Domain tag of the base-nonce stream.
|
||||
|
|
@ -88,6 +101,11 @@ pub enum Reject {
|
|||
/// `evaluations`, `ratio_milli` / 1000 of a uniform source on its window, under the floor: a low-entropy index band
|
||||
/// (F8's p23, p18, p19, p15, p56).
|
||||
LowEntropySite { site: u8, distinct: u32, evaluations: u32, ratio_milli: u32 },
|
||||
/// (c'''), class v5: the load at `site` read `distinct` distinct dataset word indices over `evaluations`,
|
||||
/// `ratio_milli` / 1000 of the window model, under [`MIN_DISTINCT_RATIO_V5`] (and at or above sub-version 3's
|
||||
/// floor, else (c'') names it); `top_index` was its most read index, `top_count` times: a hot set from a
|
||||
/// value-level constant upstream of the site that neither the lineage rule nor the 0.98 floor reaches (seed 100767).
|
||||
HotItemSite { site: u8, distinct: u32, evaluations: u32, ratio_milli: u32, top_index: u32, top_count: u32 },
|
||||
/// (c): output bit `bit` was set in `ones` of 2,048 hashes.
|
||||
OutputBias { bit: u8, ones: u32 },
|
||||
/// (c): the distinct-address sum was `sum`.
|
||||
|
|
@ -108,6 +126,7 @@ impl std::fmt::Display for Reject {
|
|||
Reject::Saturated { count } => write!(f, "(c) {count} of 16384 final register values saturated (limit 163)"),
|
||||
Reject::UnfreshLoadSource { instr, reg } => write!(f, "(a') load at {instr} reads r{reg}, not fresh by dataflow in the loop's steady state (class v4 sub-version 2)"),
|
||||
Reject::RepeatedSource { site, value, count } => write!(f, "(c'') load site {site} read the value {value:#010x} in {count} of 16384 evaluations (limit {})", MAX_SOURCE_REPEAT_V4 - 1),
|
||||
Reject::HotItemSite { site, distinct, evaluations, ratio_milli, top_index, top_count } => write!(f, "(c''') load site {site} read {distinct} distinct word indices over {evaluations} evaluations, {ratio_milli}/1000 of the window model, under the class v5 floor (most read: index {top_index:#x}, {top_count} times): a hot set from a value-level constant upstream"),
|
||||
Reject::LowEntropySite { site, distinct, evaluations, ratio_milli } => write!(f, "(c'') load site {site} read {distinct} distinct word indices over {evaluations} evaluations, {}.{:03} of a uniform source on its window (floor {MIN_DISTINCT_RATIO_V4} at 2^20)", ratio_milli / 1000, ratio_milli % 1000),
|
||||
Reject::SaturatedSource { site, count } => write!(f, "(c') load site {site} read a saturated source value in {count} of 16384 evaluations (limit 163)"),
|
||||
Reject::OutputBias { bit, ones } => write!(f, "(c) output bit {bit} set in {ones} of 2048 hashes"),
|
||||
|
|
@ -195,31 +214,37 @@ pub fn check_distinct_indices_v4(p: &Program) -> Result<(), Reject> {
|
|||
/// window (`N - N^2 / 2W`, the window `2^28 >> min(win, 2)` words of the closed-form dataset), `Err` at the first
|
||||
/// site under `floor`, else the minimum ratio and its site.
|
||||
pub fn distinct_ratio_pass(p: &Program, units: usize, floor: f64) -> Result<(f64, usize), Reject> {
|
||||
let n = (units * LANES * ITERATIONS) as f64;
|
||||
let d = distinct_indices_v4(p, units)?;
|
||||
let mut min = (f64::MAX, 0usize);
|
||||
let mut site = 0usize;
|
||||
for i in &p.instrs {
|
||||
if !i.op.is_load() {
|
||||
continue;
|
||||
}
|
||||
let wsize = ((1u64 << ACCEPT_DATASET_LOG2) >> (i.win as u64).min(2)) as f64;
|
||||
let ratio = d[site] as f64 / (n - n * n / (2.0 * wsize));
|
||||
if ratio < floor {
|
||||
return Err(Reject::LowEntropySite { site: site as u8, distinct: d[site], evaluations: n as u32, ratio_milli: (ratio * 1000.0) as u32 });
|
||||
}
|
||||
if ratio < min.0 {
|
||||
min = (ratio, site);
|
||||
}
|
||||
site += 1;
|
||||
}
|
||||
Ok(min)
|
||||
let stats = site_index_stats(p, units)?;
|
||||
distinct_ratio_on(p, &stats, units, floor)
|
||||
}
|
||||
|
||||
/// The distinct dataset word indices every load site reads over `units` units of the seed's acceptance stream on
|
||||
/// the closed-form words (the sample caps the count near `units x 32 x 8`, so a site's index entropy is read only
|
||||
/// below about log2 of that).
|
||||
pub fn distinct_indices_v4(p: &Program, units: usize) -> Result<Vec<u32>, Reject> {
|
||||
Ok(site_index_stats(p, units)?.into_iter().map(|s| s.distinct).collect())
|
||||
}
|
||||
|
||||
/// What one load site's word indices over the ratio pass look like: the distinct count (c'' and c'''), the sum of
|
||||
/// `C(run, 2)` over the indices (the colliding pairs), and the most read index with its count (the diagnostic the
|
||||
/// reject names).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct SiteIndexStats {
|
||||
pub distinct: u32,
|
||||
pub pairs: u64,
|
||||
pub top_index: u32,
|
||||
pub top_count: u32,
|
||||
}
|
||||
|
||||
/// The window of a load site in words at the rule's dataset: `2^28 >> min(win, 2)`.
|
||||
pub fn site_window_words(ins: &Instr) -> u64 {
|
||||
(1u64 << ACCEPT_DATASET_LOG2) >> (ins.win as u64).min(2)
|
||||
}
|
||||
|
||||
/// One interpreter run over `units` units with every load site's word indices kept, then per site the sorted
|
||||
/// run lengths: [`SiteIndexStats`] per site in load order. The ratio pass (c'') and the hot-item rule (c''') read
|
||||
/// the same run, so class v5 pays the sort once.
|
||||
pub fn site_index_stats(p: &Program, units: usize) -> Result<Vec<SiteIndexStats>, Reject> {
|
||||
let loads = p.loads_per_hash();
|
||||
let sites = loads / ITERATIONS;
|
||||
let mut acc = Acc {
|
||||
|
|
@ -239,12 +264,89 @@ pub fn distinct_indices_v4(p: &Program, units: usize) -> Result<Vec<u32>, Reject
|
|||
let mut out = Vec::with_capacity(sites);
|
||||
for ix in acc.indices.take().unwrap().iter_mut() {
|
||||
ix.sort_unstable();
|
||||
ix.dedup();
|
||||
out.push(ix.len() as u32);
|
||||
let mut st = SiteIndexStats { distinct: 0, pairs: 0, top_index: 0, top_count: 0 };
|
||||
let mut i = 0;
|
||||
while i < ix.len() {
|
||||
let mut j = i + 1;
|
||||
while j < ix.len() && ix[j] == ix[i] {
|
||||
j += 1;
|
||||
}
|
||||
let run = (j - i) as u32;
|
||||
st.distinct += 1;
|
||||
st.pairs += (run as u64) * (run as u64 - 1) / 2;
|
||||
if run > st.top_count {
|
||||
st.top_count = run;
|
||||
st.top_index = ix[i];
|
||||
}
|
||||
i = j;
|
||||
}
|
||||
out.push(st);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// (c'''), class v5: the raised floor on the ratio pass's sample, `Err` at the first load site under `floor`, else
|
||||
/// the minimum ratio and its site. Pure in the program as (c'') is (the closed form, the seed's acceptance stream);
|
||||
/// the state leaves move the dataset's words, not the indices a site reads (adv-accept read the exemplar's site 6
|
||||
/// at 0.9919 closed and 0.9920 on the live day).
|
||||
pub fn hot_item_pass(p: &Program, stats: &[SiteIndexStats], units: usize, floor: f64) -> Result<(f64, usize), Reject> {
|
||||
let n = (units * LANES * ITERATIONS) as f64;
|
||||
let mut min = (f64::MAX, 0usize);
|
||||
let mut site = 0usize;
|
||||
for i in &p.instrs {
|
||||
if !i.op.is_load() {
|
||||
continue;
|
||||
}
|
||||
let st = &stats[site];
|
||||
let ratio = site_ratio(st.distinct, n, site_window_words(i));
|
||||
if ratio < floor {
|
||||
return Err(Reject::HotItemSite { site: site as u8, distinct: st.distinct, evaluations: n as u32, ratio_milli: (ratio * 1000.0) as u32, top_index: st.top_index, top_count: st.top_count });
|
||||
}
|
||||
if ratio < min.0 {
|
||||
min = (ratio, site);
|
||||
}
|
||||
site += 1;
|
||||
}
|
||||
Ok(min)
|
||||
}
|
||||
|
||||
/// A site's distinct-index ratio against the window model `N - N^2 / 2W` (the (c'') form, kept so the census
|
||||
/// numbers of sub-version 3 read on the same scale).
|
||||
pub fn site_ratio(distinct: u32, n: f64, window: u64) -> f64 {
|
||||
distinct as f64 / (n - n * n / (2.0 * window as f64))
|
||||
}
|
||||
|
||||
/// The ratio floor (c'') on precomputed site stats: `Err` at the first site under `floor`, else the minimum ratio
|
||||
/// and its site (the pass [`distinct_ratio_pass`] runs when it has no stats yet).
|
||||
pub fn distinct_ratio_on(p: &Program, stats: &[SiteIndexStats], units: usize, floor: f64) -> Result<(f64, usize), Reject> {
|
||||
let n = (units * LANES * ITERATIONS) as f64;
|
||||
let mut min = (f64::MAX, 0usize);
|
||||
let mut site = 0usize;
|
||||
for i in &p.instrs {
|
||||
if !i.op.is_load() {
|
||||
continue;
|
||||
}
|
||||
let d = stats[site].distinct;
|
||||
let ratio = site_ratio(d, n, site_window_words(i));
|
||||
if ratio < floor {
|
||||
return Err(Reject::LowEntropySite { site: site as u8, distinct: d, evaluations: n as u32, ratio_milli: (ratio * 1000.0) as u32 });
|
||||
}
|
||||
if ratio < min.0 {
|
||||
min = (ratio, site);
|
||||
}
|
||||
site += 1;
|
||||
}
|
||||
Ok(min)
|
||||
}
|
||||
|
||||
/// (c'') then (c'''), class v5: one 2^20-evaluation run, the ratio floor of sub-version 3 and the hot-item rule on
|
||||
/// it. The order is the module table's: a low-entropy band is named before a hot item on the same site.
|
||||
pub fn check_indices_v5(p: &Program) -> Result<(), Reject> {
|
||||
let stats = site_index_stats(p, ACCEPT_UNITS_DISTINCT_V4)?;
|
||||
distinct_ratio_on(p, &stats, ACCEPT_UNITS_DISTINCT_V4, MIN_DISTINCT_RATIO_V4)?;
|
||||
hot_item_pass(p, &stats, ACCEPT_UNITS_DISTINCT_V4, MIN_DISTINCT_RATIO_V5).map(|_| ())
|
||||
}
|
||||
|
||||
/// Whether `class` is the class v4 shape (the 256-instruction shadow block over the class v3 base, the pass count and
|
||||
/// the era set aside): the shape the sub-version 2 rules (a') and (c') apply to, on every draw path.
|
||||
pub fn is_class_v4_shape(class: &LoadClass) -> bool {
|
||||
|
|
@ -601,8 +703,14 @@ pub fn check_dynamic(p: &Program) -> Result<AcceptReport, Reject> {
|
|||
if let Some((site, &count)) = acc.sat_source.iter().enumerate().find(|(_, &c)| c >= MAX_SATURATED) {
|
||||
return Err(Reject::SaturatedSource { site: site as u8, count });
|
||||
}
|
||||
// (c''), the ratio on the candidate that passed everything else (the draw's last and dearest test)
|
||||
check_distinct_indices_v4(p)?;
|
||||
// (c''), the ratio on the candidate that passed everything else (the draw's last and dearest test); class v5
|
||||
// reads (c''') the hot-item rule on the same run (`docs/design/class-v5-stored-state.md` section 14), keyed
|
||||
// on the state flag so no class v4 verdict moves
|
||||
if p.class.state {
|
||||
check_indices_v5(p)?;
|
||||
} else {
|
||||
check_distinct_indices_v4(p)?;
|
||||
}
|
||||
}
|
||||
let half = (ACCEPT_HASHES / 2) as u32;
|
||||
let mut bias_max = 0u32;
|
||||
|
|
@ -631,6 +739,147 @@ mod tests {
|
|||
use crate::generator::{candidate, candidate_class, generate, generate_class, GeneratorConfig, generate_v1, LoadClass};
|
||||
use crate::verify::{DatasetMode, DatasetSource};
|
||||
|
||||
/// The f8 label space's chain draw (adv-accept's census space): epoch and era seed bytes for seed `k`, and the
|
||||
/// candidate at `attempt` under `base` (the chain's class v4 sub-version 3 draw for `V4_CLASS`, the class v5 draw
|
||||
/// for `V5_CLASS`), stamped as `generate_era` stamps it.
|
||||
fn f8_seed(k: u32) -> (Vec<u8>, Vec<u8>) {
|
||||
use crate::seed::seed_words_from_bytes;
|
||||
let w = |s: String| -> Vec<u8> { seed_words_from_bytes(s.as_bytes()).iter().flat_map(|x| x.to_le_bytes()).collect() };
|
||||
(w(format!("igneum-attack-f8/program/{k}")), w(format!("igneum-attack-f8/era/{k}")))
|
||||
}
|
||||
fn f8_label(epoch: &[u8]) -> String {
|
||||
format!("igneum-epoch/{}", epoch.iter().map(|b| format!("{b:02x}")).collect::<String>())
|
||||
}
|
||||
fn f8_candidate(epoch: &[u8], era: &[u8], attempt: u32, base: LoadClass) -> Program {
|
||||
use crate::generator::{era_generator_of, V3_ALLOWED};
|
||||
let mut p = candidate_class(&f8_label(epoch), epoch, attempt, LoadClass::era(base, era, &V3_ALLOWED));
|
||||
p.generator = era_generator_of(&base);
|
||||
p.era_bytes = Some(era.to_vec());
|
||||
p
|
||||
}
|
||||
fn f8_draw(epoch: &[u8], era: &[u8], base: LoadClass) -> Program {
|
||||
use crate::generator::{generate_era, V3_ALLOWED};
|
||||
generate_era(&f8_label(epoch), epoch, base, era, &V3_ALLOWED)
|
||||
}
|
||||
|
||||
/// (c''') known-failed first (`docs/design/class-v5-stored-state.md` section 14; main's order of 7 October
|
||||
/// 2026): adv-accept's seed 100767 is the chain's class v4 sub-version 3 draw at attempt 2 (program
|
||||
/// 9d68e6286fc817d4), passes every part of the sub-version 3 rule with its site 6 at 0.9919, and reads a hot set
|
||||
/// live (3.35 percent of site 6's reads on the top 0.1 percent of items, the multiples of 2^19). The same shape
|
||||
/// under class v5 is refused at attempt 2 by the raised floor, naming site 6 under 0.995, and the class v5 draw
|
||||
/// moves past it; the class v4 verdict does not move. A clean seed (the genesis draw of both classes) clears
|
||||
/// the floor with room.
|
||||
#[test]
|
||||
fn class_v5_hot_set_rule_known_failed_seed_100767() {
|
||||
use crate::generator::V5_CLASS;
|
||||
let (epoch, era) = f8_seed(100767);
|
||||
let v4 = f8_candidate(&epoch, &era, 2, V4_CLASS);
|
||||
assert_eq!(format!("{:016x}", v4.program_id()), "9d68e6286fc817d4", "the exemplar is the chain's sub-version 3 program");
|
||||
assert_eq!(v4.seed, [0xcc5466bf, 0x375211d6, 0xf22b6e63, 0x638d9f8b, 0x85903c90, 0x03605228, 0x5f97734b, 0x910fb493]);
|
||||
assert!(check(&v4).is_ok(), "class v4 sub-version 3 accepts it: {:?}", check(&v4).err());
|
||||
let v4_stats = site_index_stats(&v4, ACCEPT_UNITS_DISTINCT_V4).unwrap();
|
||||
let (min, site) = distinct_ratio_on(&v4, &v4_stats, ACCEPT_UNITS_DISTINCT_V4, 0.0).unwrap();
|
||||
println!("seed 100767 class v4: min site {site} ratio {min:.4} (distinct {}, most read index {:#x} {} times)", v4_stats[site].distinct, v4_stats[site].top_index, v4_stats[site].top_count);
|
||||
assert_eq!(site, 6);
|
||||
assert!(min > 0.98 && min < 0.995, "the exemplar sits between the floors: {min:.4}");
|
||||
|
||||
let v5 = f8_candidate(&epoch, &era, 2, V5_CLASS);
|
||||
assert_eq!(v5.instrs, v4.instrs, "the same base program under class v5");
|
||||
match check(&v5) {
|
||||
Err(Reject::HotItemSite { site, ratio_milli, distinct, evaluations, top_index, top_count }) => {
|
||||
println!("seed 100767 class v5 attempt 2: (c''') site {site} {distinct} of {evaluations}, ratio {}.{:03}, most read {top_index:#x} x{top_count}", ratio_milli / 1000, ratio_milli % 1000);
|
||||
assert_eq!(site, 6);
|
||||
assert!(ratio_milli >= 980 && ratio_milli < 995);
|
||||
}
|
||||
other => panic!("class v5 must refuse the exemplar at attempt 2 by (c'''), got {other:?}"),
|
||||
}
|
||||
let drawn = f8_draw(&epoch, &era, V5_CLASS);
|
||||
println!("seed 100767 class v5 draw: attempt {} id {:016x}", drawn.attempt, drawn.program_id());
|
||||
assert!(drawn.attempt > 2, "the class v5 draw moves past the exemplar");
|
||||
assert!(check(&drawn).is_ok());
|
||||
let drawn_v4 = f8_draw(&epoch, &era, V4_CLASS);
|
||||
assert_eq!(drawn_v4.attempt, 2, "the class v4 draw still lands on it");
|
||||
|
||||
for (name, p) in [("genesis v4", generate_class("igneum-genesis", V4_CLASS)), ("genesis v5", generate_class("igneum-genesis", V5_CLASS))] {
|
||||
let st = site_index_stats(&p, ACCEPT_UNITS_DISTINCT_V4).unwrap();
|
||||
let (min, site) = distinct_ratio_on(&p, &st, ACCEPT_UNITS_DISTINCT_V4, 0.0).unwrap();
|
||||
println!("{name}: min site {site} ratio {min:.4}");
|
||||
assert!(min >= MIN_DISTINCT_RATIO_V5, "{name}: a clean seed clears the class v5 floor: {min:.4}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The census behind the floor (run by hand on a box: `cargo test --release --lib -- --ignored v5_hot_census
|
||||
/// --nocapture`, `IGNEUM_V5_CENSUS_SEEDS` seeds of the f8 label space from `IGNEUM_V5_CENSUS_FROM`, default
|
||||
/// 4,600 from 0, `IGNEUM_V5_CENSUS_THREADS` threads, default 88): per seed the class v4 sub-version 3 draw's
|
||||
/// attempt and its accepted program's minimum site ratio, the class v5 draw's attempt, and the counts under
|
||||
/// candidate floors, so the clean rejection rate and the attempts histogram of the chosen floor are on record.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn v5_hot_census() {
|
||||
use crate::generator::V5_CLASS;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
let seeds: u32 = std::env::var("IGNEUM_V5_CENSUS_SEEDS").ok().and_then(|v| v.parse().ok()).unwrap_or(4600);
|
||||
let from: u32 = std::env::var("IGNEUM_V5_CENSUS_FROM").ok().and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
let threads: usize = std::env::var("IGNEUM_V5_CENSUS_THREADS").ok().and_then(|v| v.parse().ok()).unwrap_or(88);
|
||||
let next = AtomicU32::new(from);
|
||||
let t0 = std::time::Instant::now();
|
||||
// per seed: (k, v4 attempt, v4 min ratio, v4 min site, v5 attempt, v5 min ratio)
|
||||
let rows: Vec<(u32, u32, f64, usize, u32, f64)> = std::thread::scope(|sc| {
|
||||
let hs: Vec<_> = (0..threads).map(|_| sc.spawn(|| {
|
||||
let mut out = Vec::new();
|
||||
loop {
|
||||
let k = next.fetch_add(1, Ordering::Relaxed);
|
||||
if k >= from + seeds {
|
||||
break out;
|
||||
}
|
||||
let (epoch, era) = f8_seed(k);
|
||||
let v4 = f8_draw(&epoch, &era, V4_CLASS);
|
||||
let st = site_index_stats(&v4, ACCEPT_UNITS_DISTINCT_V4).unwrap();
|
||||
let (min4, site4) = distinct_ratio_on(&v4, &st, ACCEPT_UNITS_DISTINCT_V4, 0.0).unwrap();
|
||||
let v5 = f8_draw(&epoch, &era, V5_CLASS);
|
||||
let st5 = site_index_stats(&v5, ACCEPT_UNITS_DISTINCT_V4).unwrap();
|
||||
let (min5, _) = distinct_ratio_on(&v5, &st5, ACCEPT_UNITS_DISTINCT_V4, 0.0).unwrap();
|
||||
out.push((k, v4.attempt, min4, site4, v5.attempt, min5));
|
||||
}
|
||||
})).collect();
|
||||
let mut rows: Vec<_> = hs.into_iter().flat_map(|h| h.join().unwrap()).collect();
|
||||
rows.sort_by_key(|r| r.0);
|
||||
rows
|
||||
});
|
||||
let n = rows.len() as f64;
|
||||
let mut sorted: Vec<f64> = rows.iter().map(|r| r.2).collect();
|
||||
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let q = |p: f64| sorted[((p * (sorted.len() - 1) as f64).round() as usize).min(sorted.len() - 1)];
|
||||
println!("v5_hot_census: {} seeds {from}..{} in {:.0} s on {threads} threads", rows.len(), from + seeds, t0.elapsed().as_secs_f64());
|
||||
println!("class v4 sub-version 3 accepted programs' minimum site ratio: min {:.4} p0.1 {:.4} p1 {:.4} p5 {:.4} median {:.4} max {:.4}", sorted[0], q(0.001), q(0.01), q(0.05), q(0.5), sorted[sorted.len() - 1]);
|
||||
for floor in [0.98, 0.99, 0.995, 0.998, 0.999] {
|
||||
let under = rows.iter().filter(|r| r.2 < floor).count();
|
||||
println!("floor {floor:.3}: {under} of {} class v4 accepted programs under it ({:.3} percent)", rows.len(), under as f64 * 100.0 / n);
|
||||
}
|
||||
let mut lowest: Vec<_> = rows.iter().collect();
|
||||
lowest.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap());
|
||||
for r in lowest.iter().take(25) {
|
||||
println!("low: seed {} v4 attempt {} min site {} ratio {:.4}; v5 attempt {} min ratio {:.4}", r.0, r.1, r.3, r.2, r.4, r.5);
|
||||
}
|
||||
let hist = |pick: fn(&(u32, u32, f64, usize, u32, f64)) -> u32| {
|
||||
let mut h = std::collections::BTreeMap::new();
|
||||
for r in &rows {
|
||||
*h.entry(pick(r)).or_insert(0u32) += 1;
|
||||
}
|
||||
let mean = rows.iter().map(|r| pick(r) as f64).sum::<f64>() / n;
|
||||
(h, mean)
|
||||
};
|
||||
let (h4, m4) = hist(|r| r.1);
|
||||
let (h5, m5) = hist(|r| r.4);
|
||||
println!("attempts histogram class v4 (mean {m4:.3}): {:?}", h4);
|
||||
println!("attempts histogram class v5 (mean {m5:.3}): {:?}", h5);
|
||||
let moved = rows.iter().filter(|r| r.1 != r.4).count();
|
||||
println!("seeds whose class v5 draw lands on another attempt than the class v4 draw: {moved} of {} ({:.3} percent)", rows.len(), moved as f64 * 100.0 / n);
|
||||
let v5_under = rows.iter().filter(|r| r.5 < MIN_DISTINCT_RATIO_V5).count();
|
||||
println!("class v5 accepted programs under the class v5 floor {MIN_DISTINCT_RATIO_V5}: {v5_under} (must be 0)");
|
||||
assert_eq!(v5_under, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distinct_bound_scales_with_the_load_count() {
|
||||
assert_eq!(min_distinct_sum(128), MIN_DISTINCT_SUM);
|
||||
|
|
|
|||
|
|
@ -1293,6 +1293,46 @@ pub fn candidate_from_words(seed_string: &str, seed_bytes: &[u8], seed: [u32; 8]
|
|||
/// [`candidate_from_words`] for a load class. For [`LoadClass::V2`] this is the version 2 draw stream exactly;
|
||||
/// for any other class the slot count is the class's and every instruction takes a tenth draw, `below(100)`,
|
||||
/// the width roll (used only on a load slot, drawn on every slot so the stream stays uniform).
|
||||
/// Class v5's shadow rule (AP-F1-1): the peephole-removable share a block may have, per mille of its instructions.
|
||||
pub const SHADOW_REMOVABLE_MAX_PERMILLE: usize = 30;
|
||||
/// Class v5's shadow rule: redraws before the last block stands as drawn (never reached at 4e-3 per try).
|
||||
pub const SHADOW_REDRAW_CAP: u32 = 64;
|
||||
|
||||
/// The instructions of a shadow block an honest compiler removes (the AP-F1-1 census's classes): an instruction whose
|
||||
/// destination's last writer, with no write to the destination or to the source in between, is the same op on the
|
||||
/// same source and immediates and the pair cancels (xor: `x ^= s` twice) or is idempotent (or: `x |= s` twice), or a
|
||||
/// rotate of a register last written by a rotate (the two merge into one), or an add followed by a sub (or a sub by an
|
||||
/// add) of the same source and immediate (sum-cancel). Counted per removable instruction, never across a pass.
|
||||
pub fn shadow_removable_count(block: &[Instr]) -> usize {
|
||||
let mut last: [Option<usize>; 8] = [None; 8];
|
||||
let mut removable = 0;
|
||||
for (k, ins) in block.iter().enumerate() {
|
||||
let d = ins.dst as usize;
|
||||
if let Some(j) = last[d] {
|
||||
let prev = &block[j];
|
||||
// the source must not have been written between j and k (its value is the same)
|
||||
let src_untouched = !block[j + 1..k].iter().any(|i| i.dst == ins.src);
|
||||
let same_operands = prev.src == ins.src && prev.imm == ins.imm && prev.imm2 == ins.imm2 && prev.src2 == ins.src2 && prev.rot == ins.rot && prev.bit == ins.bit && prev.mask == ins.mask;
|
||||
let hit = match (prev.op, ins.op) {
|
||||
(Op::Xor, Op::Xor) | (Op::Or, Op::Or) => same_operands && src_untouched,
|
||||
// every op reads its own destination (`d = d op a`), so two rotates on one register always fold on
|
||||
// paper; the census counts "written twice from one source": the same rotate with the same amount
|
||||
// (rotl, an immediate) or the same amount register unwritten between (rotr), which keeps the
|
||||
// metric at the census's 0.62 percent average instead of every rotate pair
|
||||
(Op::Rotl, Op::Rotl) => prev.rot == ins.rot,
|
||||
(Op::Rotr, Op::Rotr) => prev.src == ins.src && src_untouched,
|
||||
(Op::Add, Op::Sub) | (Op::Sub, Op::Add) => same_operands && src_untouched,
|
||||
_ => false,
|
||||
};
|
||||
if hit {
|
||||
removable += 1;
|
||||
}
|
||||
}
|
||||
last[d] = Some(k);
|
||||
}
|
||||
removable
|
||||
}
|
||||
|
||||
pub fn candidate_from_words_class(
|
||||
seed_string: &str,
|
||||
seed_bytes: &[u8],
|
||||
|
|
@ -1447,7 +1487,14 @@ pub fn candidate_from_words_class(
|
|||
// op from the non-load table, the source as on an ALU slot, the same per-instruction draws (the width roll and
|
||||
// the era windows included when the class takes them, drawn and ignored) so the stream shape is the program's.
|
||||
let mut shadow = Vec::new();
|
||||
let mut shadow_redraws = 0u32;
|
||||
if let Some(sh) = class.shadow {
|
||||
// Class v5 (docs/design/class-v5-stored-state.md section 11, AP-F1-1): a shadow block whose peephole-removable
|
||||
// share exceeds SHADOW_REMOVABLE_MAX_PERMILLE (3.0 percent of the block; the census's histogram puts 384 of 100,000
|
||||
// draws there) is redrawn from the continuing stream, so an honest compiler's simplification cannot take more
|
||||
// than 3 percent of the shadow's useful work. Every other class keeps its first draw.
|
||||
loop {
|
||||
shadow.clear();
|
||||
for _ in 0..sh.instrs {
|
||||
let mut roll = rng.below(75);
|
||||
let mut op = Op::Add;
|
||||
|
|
@ -1476,7 +1523,13 @@ pub fn candidate_from_words_class(
|
|||
}
|
||||
shadow.push(Instr { op, dst: dst as u8, src: src as u8, src2: b as u8, imm, imm2, rot, bit: bit as u8, mask, width: 1, win: 0, off: 0 });
|
||||
}
|
||||
if !class.state || shadow_removable_count(&shadow) * 1000 <= sh.instrs as usize * SHADOW_REMOVABLE_MAX_PERMILLE || shadow_redraws >= SHADOW_REDRAW_CAP {
|
||||
break;
|
||||
}
|
||||
shadow_redraws += 1;
|
||||
}
|
||||
}
|
||||
let _ = shadow_redraws;
|
||||
Program {
|
||||
seed_string: seed_string.to_string(),
|
||||
seed_bytes: seed_bytes.to_vec(),
|
||||
|
|
@ -1954,6 +2007,56 @@ mod tests {
|
|||
assert!(!ProgramClass::V4.has_state() && ProgramClass::V5.has_state());
|
||||
}
|
||||
|
||||
/// Class v5's shadow rule (AP-F1-1), the known-failed case first: a synthetic block of xor-cancel pairs is counted and
|
||||
/// is over the bound; a scan of seeds finds a v5 draw whose first block was redrawn (the census's 4e-3), every v5
|
||||
/// block is under 3.0 percent removable, and the same seed's class v4 block (never redrawn) is the first draw.
|
||||
#[test]
|
||||
fn class_v5_shadow_redundancy_rule() {
|
||||
let mk = |op: Op, dst: u8, src: u8| Instr { op, dst, src, src2: 0, imm: 7, imm2: 9, rot: 3, bit: 0, mask: 1, width: 1, win: 0, off: 0 };
|
||||
let pair = vec![mk(Op::Xor, 1, 2), mk(Op::Xor, 1, 2)];
|
||||
assert_eq!(shadow_removable_count(&pair), 1, "the known-failed case: an xor-cancel pair");
|
||||
let broken = vec![mk(Op::Xor, 1, 2), mk(Op::Add, 2, 3), mk(Op::Xor, 1, 2)];
|
||||
assert_eq!(shadow_removable_count(&broken), 0, "the source moved between the two");
|
||||
let rot = vec![mk(Op::Rotl, 4, 0), mk(Op::Rotl, 4, 0)];
|
||||
assert_eq!(shadow_removable_count(&rot), 1, "two rotl by one amount merge");
|
||||
let mixed = vec![mk(Op::Rotl, 4, 0), mk(Op::Rotr, 4, 5)];
|
||||
assert_eq!(shadow_removable_count(&mixed), 0, "a rotl and a rotr are two sources: not the census's merge");
|
||||
let rotr = vec![mk(Op::Rotr, 4, 5), mk(Op::Rotr, 4, 5)];
|
||||
assert_eq!(shadow_removable_count(&rotr), 1, "two rotr by one register merge");
|
||||
let sum = vec![mk(Op::Add, 6, 7), mk(Op::Sub, 6, 7)];
|
||||
assert_eq!(shadow_removable_count(&sum), 1, "sum-cancel");
|
||||
let mut bad = Vec::new();
|
||||
for _ in 0..128 {
|
||||
bad.push(mk(Op::Or, 3, 5));
|
||||
bad.push(mk(Op::Or, 3, 5));
|
||||
}
|
||||
assert!(shadow_removable_count(&bad) * 1000 > bad.len() * SHADOW_REMOVABLE_MAX_PERMILLE, "a block of or-idempotent pairs is over the bound");
|
||||
let era = [7u8; 32];
|
||||
let mut redrawn = 0;
|
||||
let mut scanned = 0;
|
||||
for i in 0..6_000u32 {
|
||||
let seed = format!("igneum-shadow-scan/{i}");
|
||||
let v5 = generate_from_seed_bytes_program_class(&seed, seed.as_bytes(), ProgramClass::V5, Some(&era));
|
||||
assert!(shadow_removable_count(&v5.shadow) * 1000 <= v5.shadow.len() * SHADOW_REMOVABLE_MAX_PERMILLE, "{seed}: a v5 block over the bound");
|
||||
let v4 = generate_from_seed_bytes_program_class(&seed, seed.as_bytes(), ProgramClass::V4, Some(&era));
|
||||
if v5.shadow == v4.shadow {
|
||||
assert_eq!(v5.instrs, v4.instrs, "{seed}: an unredrawn seed is the amended v4 draw for draw");
|
||||
} else {
|
||||
// the acceptance rules read the shadow (the freshness fixpoint), so a redrawn block can move the accepted
|
||||
// attempt and with it the base program; what must hold is that the first v4 block was over the bound
|
||||
redrawn += 1;
|
||||
let first = candidate_class(&seed, seed.as_bytes(), v4.attempt, v4.class);
|
||||
assert!(shadow_removable_count(&first.shadow) * 1000 > first.shadow.len() * SHADOW_REMOVABLE_MAX_PERMILLE || v5.attempt != v4.attempt, "{seed}: v5 redrew a block the rule admits");
|
||||
}
|
||||
scanned += 1;
|
||||
if redrawn >= 2 && scanned >= 1_000 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(redrawn >= 1, "no redraw in {scanned} seeds (the census says about 4e-3 per draw)");
|
||||
assert!(redrawn * 50 <= scanned, "{redrawn} redraws in {scanned} seeds: the metric is far above the census's 4e-3");
|
||||
}
|
||||
|
||||
/// Class v5 (docs/design/class-v5-stored-state.md) takes the amended class v4 draw (AP-F8-1) as the chain draws it:
|
||||
/// with an era present every load's source was last written by an injecting op or a rotate, the base program and
|
||||
/// the shadow block equal the amended v4's of the same seed, and the same holds at a ladder rung; the state flag
|
||||
|
|
@ -1976,9 +2079,21 @@ mod tests {
|
|||
for seed in ["igneum-genesis", "igneum-epoch-7", "igneum-epoch-99"] {
|
||||
let v4 = generate_from_seed_bytes_program_class(seed, seed.as_bytes(), ProgramClass::V4, Some(&era));
|
||||
let v5 = generate_from_seed_bytes_program_class(seed, seed.as_bytes(), ProgramClass::V5, Some(&era));
|
||||
assert_eq!(v5.instrs, v4.instrs, "{seed}: the base program is the amended v4's");
|
||||
assert_eq!(v5.shadow, v4.shadow, "{seed}: and the shadow block");
|
||||
assert_eq!((v5.seed, v5.attempt), (v4.seed, v4.attempt));
|
||||
if v5.shadow == v4.shadow {
|
||||
assert_eq!(v5.instrs, v4.instrs, "{seed}: the base program is the amended v4's");
|
||||
assert_eq!((v5.seed, v5.attempt), (v4.seed, v4.attempt));
|
||||
} else {
|
||||
// the AP-F1-1 shadow rule redrew this seed's block (and the acceptance, which reads the shadow, may have
|
||||
// moved the attempt): the first v4 block must have been over the bound
|
||||
let first = candidate_class(seed, seed.as_bytes(), v4.attempt, v4.class);
|
||||
assert!(shadow_removable_count(&first.shadow) * 1000 > first.shadow.len() * SHADOW_REMOVABLE_MAX_PERMILLE || v5.attempt != v4.attempt, "{seed}: v5 differs from v4 without a redraw");
|
||||
}
|
||||
if v5.attempt != v4.attempt {
|
||||
// the attempt moved: the v4 draw's program under the v5 shape is refused by a class v5 rule ((c''') the
|
||||
// raised floor, or the acceptance reading a redrawn shadow), never silently
|
||||
let same = candidate_class(seed, seed.as_bytes(), v4.attempt, v5.class);
|
||||
assert!(crate::accept::check(&same).is_err(), "{seed}: the v5 draw skipped attempt {} without a reason", v4.attempt);
|
||||
}
|
||||
// the draw equality above is the claim; sub-version 3's own freshness rule (dataflow, with the last resort after
|
||||
// the 256 cap) is what the chain applies, so the sub-version 1 scan below is informational for v5
|
||||
let _ = scan(&v5);
|
||||
|
|
@ -1987,7 +2102,9 @@ mod tests {
|
|||
assert_ne!(v5.program_id(), v4.program_id());
|
||||
let r1 = generate_from_seed_bytes_program_class_shadow(seed, seed.as_bytes(), ProgramClass::V5, Some(&era), 35);
|
||||
let r1v4 = generate_from_seed_bytes_program_class_shadow(seed, seed.as_bytes(), ProgramClass::V4, Some(&era), 35);
|
||||
assert_eq!(r1.instrs, r1v4.instrs, "{seed}: rung 1 too");
|
||||
if r1.shadow == r1v4.shadow {
|
||||
assert_eq!(r1.instrs, r1v4.instrs, "{seed}: rung 1 too");
|
||||
}
|
||||
let _ = scan(&r1);
|
||||
assert_eq!(r1.class, v5_class_at(35).with_era_of(&r1v4.class));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -206,6 +206,46 @@ pub struct MixParams {
|
|||
pub shape: Shape,
|
||||
/// The per-day derivation program when `shape.derive_len != 0`, else `None`.
|
||||
pub derive: Option<DeriveProgram>,
|
||||
/// Class v5: how many mixer blocks the AP-F4-1 rule redrew before this one (0 on every other class, and on most days).
|
||||
pub redraws: u32,
|
||||
}
|
||||
|
||||
/// Class v5's mixer-draw rule (AP-F4-1): the NAF sum of the 16 multipliers at least this.
|
||||
pub const MIXER_NAF_SUM_MIN: u32 = 163;
|
||||
/// Class v5's mixer-draw rule: every multiplier's NAF weight at least this.
|
||||
pub const MIXER_NAF_WORD_MIN: u32 = 4;
|
||||
/// Class v5's mixer-draw rule: at least this many distinct rotation amounts among the eight.
|
||||
pub const MIXER_DISTINCT_ROT_MIN: usize = 4;
|
||||
/// Class v5's mixer-draw rule: redraws before the last block stands as drawn (never reached at 6.1e-4 per try).
|
||||
pub const MIXER_REDRAW_CAP: u32 = 64;
|
||||
|
||||
/// The non-adjacent-form weight of a 32-bit word: the number of non-zero digits of its NAF, the adders a
|
||||
/// shift-and-add multiplier by that constant needs (the M1 metric of the weak-day census).
|
||||
pub fn naf_weight(mut x: u64) -> u32 {
|
||||
let mut w = 0;
|
||||
while x != 0 {
|
||||
if x & 1 == 1 {
|
||||
w += 1;
|
||||
// the digit is +1 or -1: take x to the nearest multiple of 4
|
||||
if x & 3 == 3 {
|
||||
x += 1;
|
||||
} else {
|
||||
x -= 1;
|
||||
}
|
||||
}
|
||||
x >>= 1;
|
||||
}
|
||||
w
|
||||
}
|
||||
|
||||
/// Whether a mixer block passes class v5's draw rule (AP-F4-1).
|
||||
pub fn mixer_block_admissible(rot: &[u32; 8], mul: &[u32; 16]) -> bool {
|
||||
let sum: u32 = mul.iter().map(|&m| naf_weight(m as u64)).sum();
|
||||
let words = mul.iter().all(|&m| naf_weight(m as u64) >= MIXER_NAF_WORD_MIN);
|
||||
let mut distinct = rot.to_vec();
|
||||
distinct.sort_unstable();
|
||||
distinct.dedup();
|
||||
sum >= MIXER_NAF_SUM_MIN && words && distinct.len() >= MIXER_DISTINCT_ROT_MIN
|
||||
}
|
||||
|
||||
impl MixParams {
|
||||
|
|
@ -227,8 +267,28 @@ impl MixParams {
|
|||
for c in rc.iter_mut() {
|
||||
*c = rng.next() as u32;
|
||||
}
|
||||
let mut redraws = 0u32;
|
||||
if shape.state {
|
||||
// Class v5 (docs/design/class-v5-stored-state.md section 11, AP-F4-1, the attack-pass lane's weak-day census):
|
||||
// a mixer block whose multipliers are cheap on an adder datapath (NAF sum under 163, a word under NAF weight 4)
|
||||
// or whose rotations repeat (under 4 distinct amounts) is redrawn from the next stream values, so no day is a
|
||||
// weak day for a per-day LUT-recompute FPGA (the worst calendar day of the census, chain day 29,337, was 1.121x).
|
||||
// About 6.1e-4 of days redraw. The derive program's draws (none under v5) come after, as before.
|
||||
while !mixer_block_admissible(&rot, &mul) && redraws < MIXER_REDRAW_CAP {
|
||||
for r in rot.iter_mut() {
|
||||
*r = 1 + rng.below(31) as u32;
|
||||
}
|
||||
for m in mul.iter_mut() {
|
||||
*m = (rng.next() as u32) | 1;
|
||||
}
|
||||
for c in rc.iter_mut() {
|
||||
*c = rng.next() as u32;
|
||||
}
|
||||
redraws += 1;
|
||||
}
|
||||
}
|
||||
let derive = if shape.is_derived() { Some(DeriveProgram::draw(&mut rng, shape.derive_len)) } else { None };
|
||||
Self { key, rot, mul, rc, shape, derive }
|
||||
Self { key, rot, mul, rc, shape, derive, redraws }
|
||||
}
|
||||
/// Parameters for a day string: the key is `seed_words("day/" + day)`.
|
||||
pub fn for_day(day: &str) -> Self {
|
||||
|
|
@ -758,6 +818,50 @@ impl MemhardCpu {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Class v5's mixer-draw rule (AP-F4-1), the known-failed case first: a block of cheap multipliers (NAF sum under
|
||||
/// 163) or repeated rotations is inadmissible; a scan of day keys finds days the rule redraws (the census's 6.1e-4),
|
||||
/// every v5 block passes after the draw, and the v4 constants of the same keys never move.
|
||||
#[test]
|
||||
fn class_v5_mixer_draw_rule() {
|
||||
assert_eq!(naf_weight(0), 0);
|
||||
assert_eq!(naf_weight(1), 1);
|
||||
assert_eq!(naf_weight(3), 2, "11 = 100 - 1");
|
||||
assert_eq!(naf_weight(7), 2, "111 = 1000 - 1");
|
||||
assert_eq!(naf_weight(0xffff_ffff), 2);
|
||||
assert_eq!(naf_weight(0b1010_1010), 4);
|
||||
let good_rot = [1u32, 5, 9, 13, 17, 21, 25, 29];
|
||||
let cheap = [0x8000_0001u32; 16];
|
||||
assert!(!mixer_block_admissible(&good_rot, &cheap), "the known-failed case: 16 two-adder multipliers");
|
||||
let dense = [0xaaaa_aaabu32; 16];
|
||||
assert!(mixer_block_admissible(&good_rot, &dense));
|
||||
assert!(!mixer_block_admissible(&[7u32; 8], &dense), "one rotation amount");
|
||||
assert!(!mixer_block_admissible(&[1u32, 2, 3, 3, 3, 3, 3, 3], &dense), "three distinct amounts");
|
||||
let v5 = Shape { mixer_mult: 8, cache_log2_words: 26, derive_len: 0, state: true };
|
||||
let v4 = Shape { mixer_mult: 8, cache_log2_words: 26, derive_len: 0, state: false };
|
||||
let mut redrawn = 0;
|
||||
let mut scanned = 0;
|
||||
for d in 0..60_000u64 {
|
||||
let key = crate::seed::seed_words_from_bytes(&crate::bind::day_bytes(20_000 + d));
|
||||
let a = MixParams::with_shape(key, v5);
|
||||
assert!(mixer_block_admissible(&a.rot, &a.mul), "day {d}: a v5 block fails the rule after the draw");
|
||||
if a.redraws > 0 {
|
||||
redrawn += 1;
|
||||
let b = MixParams::with_shape(key, v4);
|
||||
assert_eq!(b.redraws, 0, "v4 never redraws");
|
||||
assert_ne!((a.rot, a.mul), (b.rot, b.mul), "day {d}: v5 redrew, v4 kept the block");
|
||||
assert!(!mixer_block_admissible(&b.rot, &b.mul), "day {d}: the v4 block was the inadmissible one");
|
||||
} else {
|
||||
let b = MixParams::with_shape(key, v4);
|
||||
assert_eq!((a.rot, a.mul, a.rc), (b.rot, b.mul, b.rc), "day {d}: an admissible day is byte for byte v4's");
|
||||
}
|
||||
scanned += 1;
|
||||
if redrawn >= 3 && scanned >= 2_000 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(redrawn >= 1, "no redraw in {scanned} days (the census says about 6.1e-4 per day)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mix_params_for_day() {
|
||||
// MEMHARD.md section 1.4 and the igneum-genesis-mh pack.
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ const PIDS = `${TMP}/pids`;
|
|||
try { for (const pid of readFileSync(PIDS, 'utf8').split('\n').map(Number).filter(Boolean)) { try { process.kill(pid, 'SIGKILL'); console.log(`killed survivor pid ${pid} of the previous run`); } catch { } } } catch { }
|
||||
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
|
||||
const recordPid = (p) => { try { writeFileSync(PIDS, `${readFileSync(PIDS, 'utf8')}${p.pid}\n`); } catch { writeFileSync(PIDS, `${p.pid}\n`); } };
|
||||
const baseText = readFileSync(FILE, 'utf8');
|
||||
let baseText = readFileSync(FILE, 'utf8');
|
||||
const field = (name) => { const m = new RegExp(`"${name}":\\s*([0-9]+)`).exec(baseText); return m ? +m[1] : undefined; };
|
||||
const EPOCH = field('pow_epoch_blocks');
|
||||
const LEAD = field('pow_epoch_lead');
|
||||
|
|
@ -92,6 +92,29 @@ export function mergeOverrideText(text, fields) {
|
|||
return out.replace(/,?\s*}\s*$/, `,\n ${extra}\n}\n`);
|
||||
}
|
||||
const asText = (v) => v == null ? NEVER : String(v);
|
||||
// The 60x file is master's and carries every lane's override fields; the fork binary under BIN was built from the
|
||||
// fork source beside it and refuses a field it does not know (OverrideParams is deny_unknown_fields). The fields the
|
||||
// binary knows are the fields its OverrideParams names: read them from the source, drop the rest, and say so. A
|
||||
// missing source file means no filtering (the binary then speaks for itself).
|
||||
export function knownOverrideFields(paramsSource) {
|
||||
const m = /pub struct OverrideParams \{([\s\S]*?)\n\}/.exec(paramsSource);
|
||||
if (!m) return null;
|
||||
return new Set([...m[1].matchAll(/^\s*pub (\w+):/gm)].map((x) => x[1]));
|
||||
}
|
||||
export function dropUnknownOverrideText(text, known) {
|
||||
if (!known) return { text, dropped: [] };
|
||||
const dropped = [];
|
||||
let out = text;
|
||||
// text, not JSON.parse round-trip: the never value 18446744073709551615 does not survive a JS number. A value is a
|
||||
// one-level object, a one-level array, or a scalar (the 60x file's shapes).
|
||||
for (const k of Object.keys(JSON.parse(text))) if (!known.has(k)) { dropped.push(k); out = out.replace(new RegExp(`\\s*"${k}":\\s*(\\{[^{}]*\\}|\\[[^\\[\\]]*\\]|[^,}\\n]+),?`), ''); }
|
||||
out = out.replace(/,(\s*)}\s*$/, '$1}\n');
|
||||
return { text: out, dropped };
|
||||
}
|
||||
const paramsSource = (() => { try { return readFileSync(`${BIN}/../../consensus/core/src/config/params.rs`, 'utf8'); } catch { return null; } })();
|
||||
const filtered = dropUnknownOverrideText(baseText, paramsSource ? knownOverrideFields(paramsSource) : null);
|
||||
if (filtered.dropped.length) log(`override: dropped ${filtered.dropped.length} field(s) the fork binary does not know (another lane's, riding in master's 60x file): ${filtered.dropped.join(', ')}`);
|
||||
baseText = filtered.text;
|
||||
const override = `${TMP}/override.json`;
|
||||
writeFileSync(override, mergeOverrideText(baseText, { genesis_bits: GENESIS_BITS, skip_proof_of_work: false, program_class_v3_activation_daa: asText(V3_ACTIVATION), program_class_v4_activation_daa: asText(V4_FLOOR), program_class_v4_signal_window_daa: String(WINDOW), program_class_v5_activation_daa: asText(FLOOR) }));
|
||||
log(`signals ${SIGNAL.join('/')}, expect ${EXPECT}; v3 from ${V3_ACTIVATION ?? 'never'} (epoch ${FIRST_V3_EPOCH ?? 'none'}), v4 floor ${V4_FLOOR ?? 'never'} (epoch ${V4_FLOOR_EPOCH ?? 'none'}), v5 floor ${FLOOR ?? 'never'} (epoch ${FLOOR_EPOCH ?? 'none'}), window ${WINDOW} DAA x ${WINDOWS} (the first epoch with seven full windows after v4 is ${FIRST_FULL_EPOCH}); stale miner ${STALE ?? 'none'}; stateless node ${STATELESS_NODE ? 'n3' : 'none'}; ${EPOCH} DAA per epoch, lead ${LEAD}; run ${SECS} s or ${EPOCHS} epochs`);
|
||||
|
|
@ -287,16 +310,21 @@ const floorLines = SIGNALLING.map(n => n.grepLog(FLOOR_LINE).map(l => l.replace(
|
|||
// the stateless node: its miner's blocks accepted after the flip (must be 0) and its refusal lines
|
||||
const statelessNode = nodes.find(n => n.stateless) || null;
|
||||
const flipAtMs = flipWall;
|
||||
// accepted-after-the-flip counts read the node-side acceptance instead: each miner's ACCEPTED lines after the flip's
|
||||
// wall time, by the line's own clock (the miner prints `HH:MM:SS` UTC at the start of every line)
|
||||
// accepted-after counts: each miner's ACCEPTED lines after a wall time, by the line's own clock. igneum-miner stamps
|
||||
// every line with epoch SECONDS (`1791374997.353 ACCEPTED block ...`); the first form of this function matched
|
||||
// `HH:MM:SS` and counted nothing (found by the v5-fasttime lane, 7 October 2026, 20:3x UK: two PASS rows of the
|
||||
// 12:11 and 13:21 runs read "accepted 0" as a parse miss). The parser is proved on the honest miners in the same
|
||||
// run: `accepted_parser_proved` below requires at least one honest miner to show accepted lines after the flip.
|
||||
function acceptedAfter(i, wallMs) {
|
||||
if (wallMs == null) return 0;
|
||||
const since0 = new Date(wallMs).toISOString().slice(11, 19);
|
||||
const since = wallMs / 1000;
|
||||
let count = 0;
|
||||
for (const l of minerLog(i)) { if (!/ACCEPTED block/.test(l)) continue; const m = /^(\d{2}:\d{2}:\d{2})/.exec(l); if (m && m[1] >= since0) count++; }
|
||||
for (const l of minerLog(i)) { if (!/ACCEPTED block/.test(l)) continue; const m = /^(\d{10}(?:\.\d+)?) /.exec(l); if (m && +m[1] >= since) count++; }
|
||||
return count;
|
||||
}
|
||||
const statelessAcceptedAfterFlip = statelessNode ? acceptedAfter(statelessNode.i, flipAtMs) : 0;
|
||||
// the parser's own known-finished case: the honest miners accepted blocks after the flip (else every "accepted 0" is a miss)
|
||||
const honestAcceptedAfterFlip = SIGNALLING.filter(n => n.i !== STALE).map(n => acceptedAfter(n.i, flipAtMs));
|
||||
// the refusal reaches the miner as a template error and the node's log as the engine's line; either counts
|
||||
const statelessRefusals = statelessNode ? statelessNode.grepLog(REFUSAL_LINE).length + minerLog(statelessNode.i).filter(l => REFUSAL_LINE.test(l)).length : 0;
|
||||
// the stale miner: accepted after the FIRST REFRESH after the flip (one epoch later), and its rejections
|
||||
|
|
@ -340,6 +368,7 @@ if (EXPECT === 'flip') {
|
|||
v5_ids_differ_from_the_same_seed_v4_id: idRows.length > 0 && idRows.every(r => r.cli_v4 != null && r.cli_v4 !== r.miners_id),
|
||||
// proof of following: every v5 epoch's leaves came from another state root (the refresh per window)
|
||||
a_v5_epoch_per_window_refresh: idRows.filter(r => r.state_root != null).length >= 2 && new Set(idRows.filter(r => r.state_root != null).map(r => r.state_root)).size === idRows.filter(r => r.state_root != null).length,
|
||||
accepted_parser_proved: firstV4 == null || honestAcceptedAfterFlip.some(c => c > 0),
|
||||
stateless_node_mines_nothing_after_the_flip: !statelessNode || (statelessAcceptedAfterFlip === 0 && statelessRefusals > 0),
|
||||
stateless_node_stops_at_the_flip: !statelessNode || (counts[statelessNode.i] <= before.length && counts[statelessNode.i] >= before.length - 5),
|
||||
stale_miner_falls_off_at_the_first_refresh: STALE == null || (staleAcceptedAfterRefresh === 0 && staleRejected > 0),
|
||||
|
|
@ -369,7 +398,7 @@ if (EXPECT === 'flip') {
|
|||
const pass = Object.values(checks).every(Boolean);
|
||||
const summary = {
|
||||
pass, expect: EXPECT, signals: SIGNAL, checks, window: WINDOW, windows: WINDOWS, floor: FLOOR ?? 'never', v4_floor: V4_FLOOR ?? 'never', v3_activation: V3_ACTIVATION ?? 'never', epoch_blocks: EPOCH, lead: LEAD, first_full_window_epoch: FIRST_FULL_EPOCH, floor_epoch: FLOOR_EPOCH,
|
||||
stale_miner: STALE, stale_accepted_after_first_refresh: staleAcceptedAfterRefresh, stale_rejected: staleRejected, stateless_node: statelessNode ? statelessNode.i : null, stateless_accepted_after_flip: statelessAcceptedAfterFlip, stateless_refusal_lines: statelessRefusals,
|
||||
stale_miner: STALE, stale_accepted_after_first_refresh: staleAcceptedAfterRefresh, honest_accepted_after_flip: honestAcceptedAfterFlip, stale_rejected: staleRejected, stateless_node: statelessNode ? statelessNode.i : null, stateless_accepted_after_flip: statelessAcceptedAfterFlip, stateless_refusal_lines: statelessRefusals,
|
||||
state_roots_at_flip: rootsAtFlip, streams, days_seen: daysSeen, day_boundary_crossed: daysSeen.length > 1,
|
||||
node: IGNEUMD, miner: CPU_MINER, template_switch: firstV4, run_ended_at_s: endAt, final_daa: lastDaa, max_epoch_seen: maxEpochSeen,
|
||||
epochs: Object.fromEntries([...epochs.entries()].map(([k, v]) => [k, v])),
|
||||
|
|
|
|||
|
|
@ -73,8 +73,6 @@
|
|||
"vdf_scheme": 0,
|
||||
"era_vdf_t": 108000000,
|
||||
"fees_v1_activation_daa": 0,
|
||||
"difficulty_v3_activation_daa": 18446744073709551615,
|
||||
"finality_daa_rule_activation_daa": 18446744073709551615,
|
||||
"fork_gate_activation_daa": 18446744073709551615,
|
||||
"fork_gate_window_daa": 600,
|
||||
"vote_or_burn_activation_daa": 18446744073709551615,
|
||||
|
|
@ -82,10 +80,6 @@
|
|||
"signing_bonus_activation_daa": 18446744073709551615,
|
||||
"signing_bonus_bps": 1000,
|
||||
"finality_leave_activation_daa": 18446744073709551615,
|
||||
"latency_ladder": [{"reps": 27, "admissible": true}, {"reps": 35, "admissible": true}, {"reps": 53, "admissible": true}, {"reps": 88, "admissible": false}, {"reps": 173, "admissible": false}, {"reps": 267, "admissible": false}],
|
||||
"latency_ladder_activation_daa": 18446744073709551615,
|
||||
"latency_ladder_window_daa": 120,
|
||||
"exec_restart_state_root": "",
|
||||
"emission": {"launch_rate": "3168808781", "ramp_seconds": 2592000, "ramp_start_percent": 10, "step_seconds": 63115200, "step_decay_q32": 2147483648, "tail": {"kind": "cap"}},
|
||||
"proving_consensus_verify_daa": 18446744073709551615,
|
||||
"proving_shard_program_id": "",
|
||||
|
|
|
|||
Loading…
Reference in a new issue