ca2-coord: verify.rs and tests/scratch.rs set to the release tree's versions (b9acc57 carries them through ca2-mixer), so the docs merge changes no shipped code
This commit is contained in:
parent
14b748fafa
commit
426252927e
2 changed files with 292 additions and 25 deletions
|
|
@ -1,10 +1,36 @@
|
|||
//! The CPU reference interpreter for one 32-lane warp (`cpuWarpTraced` in the Swift) and the API the node
|
||||
//! calls. Dataset words come from the memory-hard cache (default) or from the closed form (old packs).
|
||||
|
||||
use crate::generator::{generate, generate_class, Instr, LoadClass, Op, Program, ITERATIONS, LANES};
|
||||
use crate::memhard::MemhardCpu;
|
||||
use crate::generator::{generate, generate_class, EraParams, Instr, LoadClass, Op, Program, ProgramClass, ITERATIONS, LANES};
|
||||
use crate::memhard::{hot_index, HotTable, Layout, MemhardCpu, Shape};
|
||||
use crate::seed::day_key;
|
||||
|
||||
/// The load address of an era program (`docs/plans/era-layout.md` section 1.3): `y = rotl(x * M, R)`, then the
|
||||
/// window of the load site, `k = min(win, D - 26)` (0 when `D <= 26`), `idx = ((y & (MASK >> k)) | ((off &
|
||||
/// (2^k - 1)) << (D - k))) & MASK`. For every other class `idx = x & MASK`, the lottery hash's address. `mask` is
|
||||
/// `2^D - 1`. The acceptance mirror calls this at the rule's constant `D = 28`.
|
||||
#[inline(always)]
|
||||
pub fn load_index(era: Option<&EraParams>, ins: &Instr, x: u32, mask: u32, log2: u32) -> u32 {
|
||||
match era {
|
||||
None => x & mask,
|
||||
Some(e) => {
|
||||
let (wm, off) = window(ins, mask, log2);
|
||||
let y = x.wrapping_mul(e.stride_mul).rotate_left(e.stride_rot);
|
||||
((y & wm) | off) & mask
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The window of a load site at a dataset of `2^log2` words: `(window mask, offset)` such that
|
||||
/// `idx = (y & window mask) | offset` lies in the site's aligned window of `2^(log2 - k)` words.
|
||||
#[inline(always)]
|
||||
pub fn window(ins: &Instr, mask: u32, log2: u32) -> (u32, u32) {
|
||||
let k = (ins.win as u32).min(log2.saturating_sub(26));
|
||||
let wm = mask >> k;
|
||||
let off = ((ins.off as u32) & ((1u32 << k) - 1)) << (log2 - k);
|
||||
(wm, off)
|
||||
}
|
||||
|
||||
/// Read-width experiment (5 October 2026): a `load` of `W` words folds every word into `dst`:
|
||||
/// `x = dst XOR w[0]; for j in 1..W: x = (rotl(x, FOLD_ROT) * FOLD_MUL) XOR w[j]; dst = x`. For `W = 1` this is the
|
||||
/// lottery hash's `dst XOR dataset[...]`. The fold is state-dependent (the rotate-multiply sits between the words),
|
||||
|
|
@ -166,24 +192,55 @@ pub struct DatasetSource {
|
|||
/// in packs so any implementation can rebuild the key. Empty when the key was given directly.
|
||||
pub key_bytes: Vec<u8>,
|
||||
pub dataset: Dataset,
|
||||
/// The hot table of the epoch (hot-table experiment, `docs/plans/hot-table.md`): `Some` when the program's
|
||||
/// class has one; filled by [`Epoch::new_class`] and [`Epoch::from_seed_bytes_class`] from the program's seed
|
||||
/// bytes. A hot load reads `hot[hot_index(src, words)]`.
|
||||
pub hot: Option<HotTable>,
|
||||
}
|
||||
|
||||
impl DatasetSource {
|
||||
/// Build the source for a day. Memory-hard mode fills the 256 MiB cache on the calling thread.
|
||||
pub fn new(day: &str, mode: DatasetMode, log2_words: u32) -> Self {
|
||||
let mut ds = Self::from_key(day_key(day), mode, log2_words);
|
||||
Self::new_shape(day, mode, log2_words, Shape::V2)
|
||||
}
|
||||
|
||||
/// [`DatasetSource::new`] with the construction's shape (mixer multiplier, cache size; Counter ASIC 2.0).
|
||||
pub fn new_shape(day: &str, mode: DatasetMode, log2_words: u32, shape: Shape) -> Self {
|
||||
let mut ds = Self::from_key_shape(day_key(day), mode, log2_words, shape);
|
||||
ds.key_bytes = format!("day/{day}").into_bytes();
|
||||
ds
|
||||
}
|
||||
|
||||
pub fn from_key(key: [u32; 8], mode: DatasetMode, log2_words: u32) -> Self {
|
||||
Self::from_key_shape(key, mode, log2_words, Shape::V2)
|
||||
}
|
||||
|
||||
/// [`DatasetSource::from_key`] with the construction's shape. Memory-hard mode fills a cache of
|
||||
/// `2^shape.cache_log2_words` words on the calling thread.
|
||||
pub fn from_key_shape(key: [u32; 8], mode: DatasetMode, log2_words: u32, shape: Shape) -> Self {
|
||||
assert!((4..=32).contains(&log2_words), "dataset log2 must be in 4..=32");
|
||||
let mask = if log2_words == 32 { u32::MAX } else { (1u32 << log2_words) - 1 };
|
||||
let dataset = match mode {
|
||||
DatasetMode::ClosedForm => Dataset::ClosedForm { d0: key[0], d1: key[1] },
|
||||
DatasetMode::MemoryHard => Dataset::MemoryHard(MemhardCpu::new(key)),
|
||||
DatasetMode::MemoryHard => Dataset::MemoryHard(MemhardCpu::with_shape(key, shape)),
|
||||
};
|
||||
Self { log2_words, mask, key, key_bytes: Vec::new(), dataset }
|
||||
Self { log2_words, mask, key, key_bytes: Vec::new(), dataset, hot: None }
|
||||
}
|
||||
|
||||
/// This source with the hot table of the epoch whose program seed bytes are `seed_bytes` (`mb` MiB).
|
||||
pub fn with_hot(mut self, seed_bytes: &[u8], mb: u32) -> Self {
|
||||
self.hot = Some(HotTable::for_seed_bytes(seed_bytes, mb));
|
||||
self
|
||||
}
|
||||
|
||||
/// The hot table of a program's class, filled from its seed bytes (none for a class without one).
|
||||
pub fn attach_hot_for(&mut self, program: &Program) {
|
||||
self.hot = program.class.hot.map(|h| HotTable::for_seed_bytes(&program.seed_bytes, h.mb as u32));
|
||||
}
|
||||
|
||||
/// The shape of the memory-hard construction ([`Shape::V2`] for the closed form, which has none).
|
||||
pub fn shape(&self) -> Shape {
|
||||
self.memhard().map(|m| m.shape()).unwrap_or(Shape::V2)
|
||||
}
|
||||
|
||||
pub fn mode(&self) -> DatasetMode {
|
||||
|
|
@ -200,18 +257,23 @@ impl DatasetSource {
|
|||
}
|
||||
}
|
||||
|
||||
/// `dataset[w & mask]`.
|
||||
/// `dataset[w & mask]` under the linear layout (the lottery hash).
|
||||
pub fn word(&self, w: u32) -> u32 {
|
||||
self.word_at(Layout::LINEAR, w)
|
||||
}
|
||||
|
||||
/// `dataset[w & mask]` under a program's layout (era layout). The closed form has no items and ignores it.
|
||||
pub fn word_at(&self, layout: Layout, w: u32) -> u32 {
|
||||
let w = w & self.mask;
|
||||
match &self.dataset {
|
||||
Dataset::ClosedForm { d0, d1 } => dataset_elem(w, *d0, *d1),
|
||||
Dataset::MemoryHard(m) => m.word(w),
|
||||
Dataset::MemoryHard(m) => m.word_at(layout, w),
|
||||
}
|
||||
}
|
||||
|
||||
/// `out[k] = dataset[idx[k]]`; indices are already masked. Returns items derived (0 for the closed form).
|
||||
#[inline]
|
||||
fn fetch(&self, idx: &[u32; LANES], out: &mut [u32; LANES]) -> usize {
|
||||
fn fetch(&self, idx: &[u32; LANES], out: &mut [u32; LANES], layout: Layout) -> usize {
|
||||
match &self.dataset {
|
||||
Dataset::ClosedForm { d0, d1 } => {
|
||||
for k in 0..LANES {
|
||||
|
|
@ -219,14 +281,14 @@ impl DatasetSource {
|
|||
}
|
||||
0
|
||||
}
|
||||
Dataset::MemoryHard(m) => m.fetch(idx, out),
|
||||
Dataset::MemoryHard(m) => m.fetch(idx, out, layout),
|
||||
}
|
||||
}
|
||||
|
||||
/// `out[k][j] = dataset[base[k] + j]` for `j < width`; bases are masked and aligned to `width` words
|
||||
/// (`width` 4 or 16, so a lane's words lie in one item). Returns items derived (0 for the closed form).
|
||||
#[inline]
|
||||
fn fetch_wide(&self, base: &[u32; LANES], width: usize, out: &mut [[u32; 16]; LANES]) -> usize {
|
||||
fn fetch_wide(&self, base: &[u32; LANES], width: usize, out: &mut [[u32; 16]; LANES], layout: Layout) -> usize {
|
||||
match &self.dataset {
|
||||
Dataset::ClosedForm { d0, d1 } => {
|
||||
for k in 0..LANES {
|
||||
|
|
@ -236,7 +298,7 @@ impl DatasetSource {
|
|||
}
|
||||
0
|
||||
}
|
||||
Dataset::MemoryHard(m) => m.fetch_wide(base, width, out),
|
||||
Dataset::MemoryHard(m) => m.fetch_wide(base, width, out, layout),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -278,6 +340,9 @@ pub fn interpret_warp_scratch(
|
|||
trace: bool,
|
||||
) -> (WarpResult, Vec<ScratchEvent>) {
|
||||
let mask = ds.mask;
|
||||
let log2 = ds.log2_words;
|
||||
let era = program.class.era;
|
||||
let layout = program.class.layout();
|
||||
let mut r = [[0u32; LANES]; 8];
|
||||
for lane in 0..LANES {
|
||||
let nonce = base_nonce.wrapping_add(lane as u32);
|
||||
|
|
@ -298,10 +363,14 @@ pub fn interpret_warp_scratch(
|
|||
}
|
||||
}
|
||||
let slot_mask = program.class.scratch_slot_mask();
|
||||
if program.has_hot() {
|
||||
let h = ds.hot.as_ref().expect("a hot-table program needs the epoch's hot table on the dataset source");
|
||||
assert_eq!(h.n_words(), program.hot_words(), "the hot table's size is the class's");
|
||||
}
|
||||
for _ in 0..ITERATIONS {
|
||||
let sel = r[0];
|
||||
for ins in &program.instrs {
|
||||
step(ins, &mut r, &sel, mask, ds, &mut idx, &mut val, &mut items_derived);
|
||||
step(ins, &mut r, &sel, mask, log2, era.as_ref(), layout, ds, &mut idx, &mut val, &mut items_derived);
|
||||
if ins.op == Op::Scratch {
|
||||
let m = scratch.as_mut().expect("a scratch op needs a scratch class");
|
||||
let (d, a) = (ins.dst as usize, ins.src as usize);
|
||||
|
|
@ -329,6 +398,9 @@ fn step(
|
|||
r: &mut [[u32; LANES]; 8],
|
||||
sel: &[u32; LANES],
|
||||
mask: u32,
|
||||
log2: u32,
|
||||
era: Option<&EraParams>,
|
||||
layout: Layout,
|
||||
ds: &DatasetSource,
|
||||
idx: &mut [u32; LANES],
|
||||
val: &mut [u32; LANES],
|
||||
|
|
@ -404,9 +476,9 @@ fn step(
|
|||
}
|
||||
Op::Load if ins.width == 1 => {
|
||||
for lane in 0..LANES {
|
||||
idx[lane] = r[a][lane] & mask;
|
||||
idx[lane] = load_index(era, ins, r[a][lane], mask, log2);
|
||||
}
|
||||
*items_derived += ds.fetch(idx, val);
|
||||
*items_derived += ds.fetch(idx, val, layout);
|
||||
for lane in 0..LANES {
|
||||
r[d][lane] ^= val[lane];
|
||||
}
|
||||
|
|
@ -416,10 +488,10 @@ fn step(
|
|||
let width = ins.width as usize;
|
||||
let align = !(ins.width as u32 - 1);
|
||||
for lane in 0..LANES {
|
||||
idx[lane] = (r[a][lane] & mask) & align;
|
||||
idx[lane] = load_index(era, ins, r[a][lane], mask, log2) & align;
|
||||
}
|
||||
let mut vals = [[0u32; 16]; LANES];
|
||||
*items_derived += ds.fetch_wide(idx, width, &mut vals);
|
||||
*items_derived += ds.fetch_wide(idx, width, &mut vals, layout);
|
||||
for lane in 0..LANES {
|
||||
r[d][lane] = fold_words(r[d][lane], &vals[lane][..width]);
|
||||
}
|
||||
|
|
@ -427,13 +499,21 @@ fn step(
|
|||
Op::Scratch => {
|
||||
// handled by the caller (interpret_warp_init), which owns the unit's scratch model
|
||||
}
|
||||
Op::Hot => {
|
||||
// Hot-table experiment: one word of the epoch table at the multiply-shift index, plain xor fold.
|
||||
let h = ds.hot.as_ref().expect("a hot load needs the hot table");
|
||||
let n = h.n_words();
|
||||
for lane in 0..LANES {
|
||||
r[d][lane] ^= h.at(hot_index(r[a][lane], n));
|
||||
}
|
||||
}
|
||||
Op::WLoad => {
|
||||
// Lane 0's register, masked, aligned down to 32 words; lane l reads word base + l.
|
||||
let base = (r[a][0] & mask) & !31;
|
||||
for lane in 0..LANES {
|
||||
idx[lane] = base + lane as u32;
|
||||
}
|
||||
*items_derived += ds.fetch(idx, val);
|
||||
*items_derived += ds.fetch(idx, val, Layout::LINEAR);
|
||||
for lane in 0..LANES {
|
||||
r[d][lane] ^= val[lane];
|
||||
}
|
||||
|
|
@ -457,14 +537,37 @@ pub struct Epoch {
|
|||
/// Default dataset size: 2^28 words = 1 GiB.
|
||||
pub const DEFAULT_DATASET_LOG2: u32 = 28;
|
||||
|
||||
/// Days a day index lies after the network's genesis day (0 for the genesis day and any day before it). The node's
|
||||
/// entry; the same function as `memhard::days_since_genesis`.
|
||||
pub fn days_since_genesis(day_index: u64, genesis_day_index: u64) -> u64 {
|
||||
crate::memhard::days_since_genesis(day_index, genesis_day_index)
|
||||
}
|
||||
|
||||
impl Epoch {
|
||||
pub fn new(seed: &str, day: &str, mode: DatasetMode, dataset_log2: u32) -> Self {
|
||||
Self { program: generate(seed), dataset: DatasetSource::new(day, mode, dataset_log2) }
|
||||
}
|
||||
|
||||
/// [`Epoch::new`] with a load class (read-width experiment).
|
||||
/// [`Epoch::new`] with a load class (read-width experiment; Counter ASIC 2.0: the class's mixer multiplier
|
||||
/// shapes the dataset, the cache is the genesis size since a string day has no day index).
|
||||
pub fn new_class(seed: &str, day: &str, mode: DatasetMode, dataset_log2: u32, class: LoadClass) -> Self {
|
||||
Self { program: generate_class(seed, class), dataset: DatasetSource::new(day, mode, dataset_log2) }
|
||||
Self::new_class_day(seed, day, mode, dataset_log2, class, 0)
|
||||
}
|
||||
|
||||
/// [`Epoch::new_class`] on day `days_since_genesis` of the growth schedule (the cache of
|
||||
/// `memhard::cache_log2_words` for a class with the growth rule; the dataset size is the caller's).
|
||||
pub fn new_class_day(seed: &str, day: &str, mode: DatasetMode, dataset_log2: u32, class: LoadClass, days_since_genesis: u64) -> Self {
|
||||
let shape = Shape::for_class_day(&class, days_since_genesis);
|
||||
let program = generate_class(seed, class);
|
||||
let mut dataset = DatasetSource::new_shape(day, mode, dataset_log2, shape);
|
||||
// hot-table experiment: a hot class fills its table from the seed bytes
|
||||
dataset.attach_hot_for(&program);
|
||||
Self { program, dataset }
|
||||
}
|
||||
|
||||
/// `dataset[w]` as this epoch's program reads it: under the program's layout (era layout; linear for v2).
|
||||
pub fn dataset_word(&self, w: u32) -> u32 {
|
||||
self.dataset.word_at(self.program.class.layout(), w)
|
||||
}
|
||||
|
||||
/// The production shape: memory-hard, 1 GiB dataset.
|
||||
|
|
@ -480,15 +583,68 @@ impl Epoch {
|
|||
Self::from_seed_bytes_class(epoch_seed, day_bytes, label, LoadClass::V2)
|
||||
}
|
||||
|
||||
/// [`Epoch::from_seed_bytes`] with a load class (read-width experiment).
|
||||
/// [`Epoch::from_seed_bytes`] with a load class (read-width experiment; Counter ASIC 2.0: the class's mixer
|
||||
/// multiplier shapes the dataset). Day 0 of the growth schedule: the 2^26-word cache and the 2^28-word dataset,
|
||||
/// which is every devnet pack and vector. A node past the first doubling calls [`Epoch::from_seed_bytes_day`].
|
||||
pub fn from_seed_bytes_class(epoch_seed: &[u8], day_bytes: &[u8], label: &str, class: LoadClass) -> Self {
|
||||
Self::from_seed_bytes_day(epoch_seed, day_bytes, label, class, 0, DEFAULT_DATASET_LOG2)
|
||||
}
|
||||
|
||||
/// The chain's shape on day `days_since_genesis` (`memhard::days_since_genesis(day_index(header), day_index(genesis))`,
|
||||
/// the node's two day indices): the program of the class, and under the class's growth rule the cache of
|
||||
/// `memhard::cache_log2_words(d)` and the dataset of `memhard::dataset_log2_words(genesis_dataset_log2, d)`
|
||||
/// (the genesis size is 28 for the 1 GiB devnet, 29 for the designed 2 GiB). Without the growth rule the cache
|
||||
/// is 2^26 words and the dataset `2^genesis_dataset_log2` on every day.
|
||||
pub fn from_seed_bytes_day(epoch_seed: &[u8], day_bytes: &[u8], label: &str, class: LoadClass, days_since_genesis: u64, genesis_dataset_log2: u32) -> Self {
|
||||
let program = crate::generator::generate_from_seed_bytes_class(label, epoch_seed, class);
|
||||
let key = crate::seed::seed_words_from_bytes(day_bytes);
|
||||
let mut dataset = DatasetSource::from_key(key, DatasetMode::MemoryHard, DEFAULT_DATASET_LOG2);
|
||||
let shape = Shape::for_class_day(&class, days_since_genesis);
|
||||
let dataset_log2 = if class.growth { crate::memhard::dataset_log2_words(genesis_dataset_log2, days_since_genesis) } else { genesis_dataset_log2 };
|
||||
let mut dataset = DatasetSource::from_key_shape(key, DatasetMode::MemoryHard, dataset_log2, shape);
|
||||
dataset.key_bytes = day_bytes.to_vec();
|
||||
dataset.attach_hot_for(&program);
|
||||
Self { program, dataset }
|
||||
}
|
||||
|
||||
/// The chain's shape with the program class (Counter ASIC 2.0, 5 October 2026): what the node's engine and the
|
||||
/// miner's pack export build from the seeds a block template carries. Class v2 is [`Epoch::from_seed_bytes`]
|
||||
/// exactly (the era bytes are ignored and not recorded); class v3 draws from [`crate::generator::V3_CLASS`]
|
||||
/// with generator version 3 and records the era seed bytes (`E_n`) in the program for the pack.
|
||||
pub fn from_chain_seeds(epoch_seed: &[u8], day_bytes: &[u8], era_bytes: Option<&[u8]>, class: ProgramClass, label: &str) -> Self {
|
||||
Self { program: Self::chain_program(epoch_seed, era_bytes, class, label), dataset: Self::chain_dataset(day_bytes, class) }
|
||||
}
|
||||
|
||||
/// The program alone of [`Epoch::from_chain_seeds`] (no cache fill): for an engine that shares the day's cache.
|
||||
pub fn chain_program(epoch_seed: &[u8], era_bytes: Option<&[u8]>, class: ProgramClass, label: &str) -> Program {
|
||||
crate::generator::generate_from_seed_bytes_program_class(label, epoch_seed, class, era_bytes)
|
||||
}
|
||||
|
||||
/// The day's cache and dataset of [`Epoch::from_chain_seeds`], the one entry the node's engine builds a day
|
||||
/// cache through. The class is an argument because the Counter ASIC 2.0 integration gives class v3 its own item
|
||||
/// construction (the mixer multiplier) and cache size schedule (ca2-mixer); today both classes build the day of
|
||||
/// [`Epoch::from_seed_bytes`], and the engine keys its day caches on `(day, class)` so the two never share one.
|
||||
pub fn chain_dataset(day_bytes: &[u8], class: ProgramClass) -> DatasetSource {
|
||||
Self::chain_dataset_day(day_bytes, class, 0, DEFAULT_DATASET_LOG2)
|
||||
}
|
||||
|
||||
/// [`Epoch::chain_dataset`] with the day's position since genesis and the network's genesis dataset size: the
|
||||
/// entry the node's engine and the miner's export build every day cache through, so the cache growth schedule
|
||||
/// of spec 01 section 1.13.3 has one place to act (ca2-mixer, 5 October 2026, `docs/plans/mixer-x4.md`): the
|
||||
/// class's load class gives the mixer multiplier and whether the growth rule applies (`Shape::for_class_day`);
|
||||
/// under the rule the cache is `2^memhard::cache_log2_words(d)` words and the dataset
|
||||
/// `2^memhard::dataset_log2_words(genesis_dataset_log2, d)`; without it (class v2) the cache is 2^26 words and
|
||||
/// the dataset the genesis size on every day. `days_since_genesis` is [`days_since_genesis`] of the block's and
|
||||
/// the genesis header's day indices.
|
||||
pub fn chain_dataset_day(day_bytes: &[u8], class: ProgramClass, days_since_genesis: u64, genesis_dataset_log2: u32) -> DatasetSource {
|
||||
let lc = class.load_class();
|
||||
let shape = Shape::for_class_day(&lc, days_since_genesis);
|
||||
let dataset_log2 = if lc.growth { crate::memhard::dataset_log2_words(genesis_dataset_log2, days_since_genesis) } else { genesis_dataset_log2 };
|
||||
let key = crate::seed::seed_words_from_bytes(day_bytes);
|
||||
let mut dataset = DatasetSource::from_key_shape(key, DatasetMode::MemoryHard, dataset_log2, shape);
|
||||
dataset.key_bytes = day_bytes.to_vec();
|
||||
dataset
|
||||
}
|
||||
|
||||
/// The 32 hashes of the warp starting at `base_nonce`.
|
||||
pub fn hash_warp(&self, base_nonce: u32) -> [u64; LANES] {
|
||||
hash_warp(&self.program, base_nonce, &self.dataset)
|
||||
|
|
@ -548,7 +704,7 @@ mod tests {
|
|||
*b = ((k as u32).wrapping_mul(0x9E37_79B1) & ds.mask) & !15;
|
||||
}
|
||||
let mut out = [[0u32; 16]; LANES];
|
||||
let items = ds.fetch_wide(&base, 16, &mut out);
|
||||
let items = ds.fetch_wide(&base, 16, &mut out, Layout::LINEAR);
|
||||
assert!(items >= 1 && items <= LANES);
|
||||
for k in 0..LANES {
|
||||
for j in 0..16 {
|
||||
|
|
@ -559,7 +715,7 @@ mod tests {
|
|||
for b in base4.iter_mut() {
|
||||
*b += 8;
|
||||
}
|
||||
let items4 = ds.fetch_wide(&base4, 4, &mut out);
|
||||
let items4 = ds.fetch_wide(&base4, 4, &mut out, Layout::LINEAR);
|
||||
assert_eq!(items4, items);
|
||||
for k in 0..LANES {
|
||||
for j in 0..4 {
|
||||
|
|
@ -590,6 +746,116 @@ mod tests {
|
|||
assert_eq!(e.hash_warp(0), e.hash_warp(0));
|
||||
}
|
||||
|
||||
/// Era layout: the load address stays inside the site's window and below the mask at every dataset size (the
|
||||
/// window floor of 2^26 words clamps the shrink), the interleaved memory-hard dataset reads item(t(w))[j(w)] and
|
||||
/// is the same prefix at 2^20 and 2^22 words, the wide fetch agrees word for word, and an era epoch hashes
|
||||
/// deterministically through the interpreter and the single-nonce API.
|
||||
#[test]
|
||||
fn era_windows_layout_and_epochs() {
|
||||
let eb = EraParams::test_era_bytes("igneum-era-test/1");
|
||||
let c = LoadClass::era(LoadClass::V2, &eb, &[1]);
|
||||
let e = c.era.unwrap();
|
||||
let mut ins = Instr { op: Op::Load, dst: 0, src: 1, src2: 0, imm: 0, imm2: 0, rot: 1, bit: 0, mask: 1, width: 1, win: 2, off: 3 };
|
||||
let mut s = crate::seed::SplitMix64::new(7);
|
||||
for log2 in [20u32, 26, 27, 28, 29] {
|
||||
let mask = (1u64 << log2) as u32 - 1;
|
||||
let k = ins.win.min(log2.saturating_sub(26) as u8) as u32;
|
||||
for _ in 0..1000 {
|
||||
let x = s.next() as u32;
|
||||
let idx = load_index(Some(&e), &ins, x, mask, log2);
|
||||
assert!(idx <= mask);
|
||||
let (wm, off) = window(&ins, mask, log2);
|
||||
assert_eq!(idx & !wm, off, "log2 {log2}");
|
||||
assert_eq!(wm, mask >> k);
|
||||
assert_eq!(idx, ((x.wrapping_mul(e.stride_mul).rotate_left(e.stride_rot) & wm) | off) & mask);
|
||||
}
|
||||
}
|
||||
ins.win = 0;
|
||||
assert_eq!(load_index(None, &ins, 0xdead_beef, 0x0fff_ffff, 28), 0xdead_beef & 0x0fff_ffff);
|
||||
// the interleaved dataset: one day cache, the layout per program
|
||||
let l = e.layout();
|
||||
assert_eq!(l.pos, [1, 3, 8, 13]);
|
||||
let small = DatasetSource::new("2026-10-03", DatasetMode::MemoryHard, 20);
|
||||
let big = DatasetSource::new("2026-10-03", DatasetMode::MemoryHard, 22);
|
||||
let m = small.memhard().unwrap();
|
||||
for w in [0u32, 1, 4, 5, 255, 256, 4095, 8192, 0x0f_ffff] {
|
||||
let (t, j) = l.split(w);
|
||||
assert_eq!(small.word_at(l, w), crate::memhard::derive_item(t, &m.params, &m.cache)[j as usize], "w {w}");
|
||||
assert_eq!(small.word_at(l, w), big.word_at(l, w), "prefix at w {w}");
|
||||
assert_eq!(small.word(w), small.word_at(Layout::LINEAR, w));
|
||||
}
|
||||
let mut idx = [0u32; LANES];
|
||||
for (k, i) in idx.iter_mut().enumerate() {
|
||||
*i = (k as u32).wrapping_mul(0x9E37_79B1) & small.mask;
|
||||
}
|
||||
let mut out = [0u32; LANES];
|
||||
small.fetch(&idx, &mut out, l);
|
||||
for k in 0..LANES {
|
||||
assert_eq!(out[k], small.word_at(l, idx[k]));
|
||||
}
|
||||
// a 16-byte era: the wide fetch keeps a lane's four words in one item
|
||||
let eb3 = EraParams::test_era_bytes("igneum-era-test/3");
|
||||
let c3 = LoadClass::era(LoadClass::fixed(4, 16), &eb3, &[4]);
|
||||
let l3 = c3.layout();
|
||||
assert_eq!(l3.pos[..2], [0, 1]);
|
||||
let mut base = [0u32; LANES];
|
||||
for (k, b) in base.iter_mut().enumerate() {
|
||||
*b = ((k as u32).wrapping_mul(0x9E37_79B1) & small.mask) & !3;
|
||||
}
|
||||
let mut wide = [[0u32; 16]; LANES];
|
||||
small.fetch_wide(&base, 4, &mut wide, l3);
|
||||
for k in 0..LANES {
|
||||
for j in 0..4 {
|
||||
assert_eq!(wide[k][j], small.word_at(l3, base[k] + j as u32), "lane {k} word {j}");
|
||||
}
|
||||
}
|
||||
// era epochs hash deterministically, differ per era, and the single-nonce API agrees with the warp
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for (n, c) in [(1u64, c), (3, c3)] {
|
||||
let ep = Epoch::new_class("igneum-genesis", "2026-10-03", DatasetMode::MemoryHard, 20, c);
|
||||
assert_eq!(ep.dataset_word(5), ep.dataset.word_at(c.layout(), 5));
|
||||
let a = ep.hash_warp(64);
|
||||
assert_eq!(a, ep.hash_warp(64));
|
||||
assert_eq!(ep.hash(64 + 5), a[5]);
|
||||
assert!(seen.insert(a[0]), "era {n}");
|
||||
let closed = Epoch::new_class("igneum-genesis", "2026-10-03", DatasetMode::ClosedForm, 20, c);
|
||||
assert_ne!(closed.hash_warp(64), a);
|
||||
}
|
||||
}
|
||||
|
||||
/// Hot-table experiment: an epoch of a hot class carries the table, hashes deterministically and differs from
|
||||
/// version 2; the reference interpreter agrees with a hand-stepped hot load; a hot program without its table is
|
||||
/// refused.
|
||||
#[test]
|
||||
fn hot_epochs_hash() {
|
||||
let e = Epoch::new_class("igneum-genesis", "2026-10-03", DatasetMode::ClosedForm, 20, LoadClass::hot(32, 4));
|
||||
let h = e.dataset.hot.as_ref().expect("the epoch fills the hot table");
|
||||
assert_eq!(h.n_words(), 1 << 23);
|
||||
assert_eq!(h.key, crate::memhard::hot_key(b"igneum-genesis"));
|
||||
let v2 = Epoch::new_class("igneum-genesis", "2026-10-03", DatasetMode::ClosedForm, 20, LoadClass::V2);
|
||||
let a = e.hash_warp(0);
|
||||
assert_eq!(a, e.hash_warp(0));
|
||||
assert_ne!(a, v2.hash_warp(0));
|
||||
assert_ne!(a[0], a[1]);
|
||||
// the same program under a 64 MiB table reads other words
|
||||
let e64 = Epoch::new_class("igneum-genesis", "2026-10-03", DatasetMode::ClosedForm, 20, LoadClass::hot(64, 4));
|
||||
assert_eq!(e64.program.instrs, e.program.instrs);
|
||||
assert_ne!(e64.hash_warp(0), a);
|
||||
// from seed bytes, the chain's shape, with a hot class
|
||||
let genesis = crate::bind::unhex("edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07").unwrap();
|
||||
let ec = Epoch::from_seed_bytes_class(&genesis, &crate::bind::day_bytes(20_730), "devnet", LoadClass::hot(32, 2));
|
||||
assert_eq!(ec.dataset.hot.as_ref().unwrap().key, crate::memhard::hot_key(&genesis));
|
||||
assert_eq!(ec.hash_warp(0), ec.hash_warp(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "needs the epoch's hot table")]
|
||||
fn hot_program_without_a_table_is_refused() {
|
||||
let p = generate_class("igneum-genesis", LoadClass::hot(32, 4));
|
||||
let ds = DatasetSource::new("2026-10-03", DatasetMode::ClosedForm, 20);
|
||||
let _ = hash_warp(&p, 0, &ds);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_class_epochs_hash() {
|
||||
for name in ["w16", "w64x4", "50,35,15"] {
|
||||
|
|
|
|||
|
|
@ -240,10 +240,10 @@ fn written_words_unbiased_and_rehit_rates() {
|
|||
// ---------------------------------------------------------------------------------------------------------------
|
||||
|
||||
fn ins(op: Op, dst: u8, src: u8) -> Instr {
|
||||
Instr { op, dst, src, src2: 0, imm: 0, imm2: 0, rot: 1, bit: 0, mask: 1, width: 1 }
|
||||
Instr { op, dst, src, src2: 0, imm: 0, imm2: 0, rot: 1, bit: 0, mask: 1, width: 1, win: 0, off: 0 }
|
||||
}
|
||||
fn add_imm(dst: u8, src: u8, imm: u32) -> Instr {
|
||||
Instr { op: Op::Add, dst, src, src2: 0, imm, imm2: imm, rot: 1, bit: 0, mask: 1, width: 1 }
|
||||
Instr { op: Op::Add, dst, src, src2: 0, imm, imm2: imm, rot: 1, bit: 0, mask: 1, width: 1, win: 0, off: 0 }
|
||||
}
|
||||
|
||||
/// A hand-built program of class `c` named `name` (its seed is the name, so its fill words and init words are
|
||||
|
|
@ -261,6 +261,7 @@ fn edge(name: &str, c: LoadClass, instrs: Vec<Instr>) -> Program {
|
|||
generator: GENERATOR_VERSION,
|
||||
attempt: 0,
|
||||
class: c,
|
||||
era_bytes: None,
|
||||
instrs,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue