Counter ASIC 3.0 gates (node): class v4 = mx8+sh256x27 through the stack (igneum-pow ProgramClass::V4, V4_CLASS, generator 4, program_id(4, seed, attempt), the era composed as v3's; program.h and program.json class v4; packcheck, packfile.h, the CUDA and OpenCL identity rule and the Metal worker accept generator 4 and the class=v4 token, the Metal worker takes v4 from the prepared pack only; v2 and v3 byte-identical, the pinned packs diffed); the G4 harness class-v4.mjs (two switches, the never case); override-60x.json: the v4 field at never, the duplicated proving v1 block removed, the four 0.3.12/0.3.13 fields added; CI check override-json-check.sh (no duplicate key in any override file)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
651b0315e4
commit
f3e8702e71
13 changed files with 562 additions and 69 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -69,6 +69,8 @@ jobs:
|
|||
run: bash tools/ci/no-conflict-markers.sh
|
||||
- name: copied sources are re-stamped before a build
|
||||
run: bash tools/ci/copied-sources-check.sh
|
||||
- name: override params files parse with no duplicate key (the duplicate-field class, 6 October 2026)
|
||||
run: bash tools/ci/override-json-check.sh
|
||||
- name: the signer is never piped into head
|
||||
run: bash tools/ci/signer-pipe-check.sh
|
||||
- name: bash bodies in PowerShell job scripts pass bash -n, the lost-quote class (self-test first, then the tree)
|
||||
|
|
|
|||
|
|
@ -157,14 +157,21 @@ fn wide_load_stmt(dialect: CoreDialect, d: &str, idx: &str, width: u8, src: Wide
|
|||
|
||||
/// The program class lines of program.h (Counter ASIC 2.0): `IGNEUM_PROGRAM_CLASS` and, when the program was drawn
|
||||
/// on the chain, `IGNEUM_ERA_SEED_HEX`. Empty for every version 2 program, so the pinned packs do not change; a
|
||||
/// worker reads an absent line as class v2. The generator version is `IGNEUM_GENERATOR` as before (3 for class v3).
|
||||
/// worker reads an absent line as class v2. The generator version is `IGNEUM_GENERATOR` as before (3 for class v3,
|
||||
/// 4 for class v4; the v3 comment lines are byte for byte the 0.3.11 ones, so the pinned v3 packs do not change).
|
||||
fn program_class_header_lines(p: &Program) -> String {
|
||||
if p.program_class() == ProgramClass::V2 {
|
||||
return String::new();
|
||||
}
|
||||
let mut s = String::new();
|
||||
s.push_str("// Program class v3 (Counter ASIC 2.0, docs/plans/counter-asic-2-rollout.md): generator version 3; a worker that\n");
|
||||
s.push_str("// runs another class refuses this pack, and a job line names the class it wants (class=v3 era=<hex>).\n");
|
||||
if p.program_class() == ProgramClass::V4 {
|
||||
s.push_str("// Program class v4 (Counter ASIC 3.0, docs/plans/counter-asic-3-node.md): generator version 4, class v3 plus the\n");
|
||||
s.push_str("// latency-shadow block (IGNEUM_SHADOW_INSTRS x IGNEUM_SHADOW_REPS per iteration); a worker that runs another class\n");
|
||||
s.push_str("// refuses this pack, and a job line names the class it wants (class=v4 era=<hex>).\n");
|
||||
} else {
|
||||
s.push_str("// Program class v3 (Counter ASIC 2.0, docs/plans/counter-asic-2-rollout.md): generator version 3; a worker that\n");
|
||||
s.push_str("// runs another class refuses this pack, and a job line names the class it wants (class=v3 era=<hex>).\n");
|
||||
}
|
||||
s.push_str(&format!("#define IGNEUM_PROGRAM_CLASS {}\n", jstr(p.program_class().name())));
|
||||
if let Some(era) = &p.era_bytes {
|
||||
s.push_str(&format!("#define IGNEUM_ERA_SEED_HEX {}\n", jstr(&hex_bytes(era))));
|
||||
|
|
|
|||
|
|
@ -774,15 +774,21 @@ impl Default for LoadClass {
|
|||
/// Generator version of a class v3 program (spec 01 section 1.4.6: `program_id(3, seed, attempt)`).
|
||||
pub const GENERATOR_VERSION_V3: u32 = 3;
|
||||
|
||||
/// Generator version of a class v4 program (Counter ASIC 3.0, 6 October 2026, PROPOSED: `program_id(4, seed, attempt)`).
|
||||
pub const GENERATOR_VERSION_V4: u32 = 4;
|
||||
|
||||
/// The program class of an epoch (Counter ASIC 2.0, 5 October 2026, `docs/plans/counter-asic-2-rollout.md`): one
|
||||
/// height switch in the node, `program_class_v3_activation_daa`, rounded up to an epoch boundary, decides which
|
||||
/// class an epoch's program is drawn from. V2 is the lottery hash as adopted on 4 October 2026, byte for byte.
|
||||
/// V3 is generator version 3: its program id carries `generator = 3` and its load class is [`V3_CLASS`].
|
||||
/// V4 (Counter ASIC 3.0, 6 October 2026, the candidate `mx8+sh256x27` behind `program_class_v4_activation_daa`) is
|
||||
/// generator version 4: its program id carries `generator = 4` and its load class is [`V4_CLASS`].
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
|
||||
pub enum ProgramClass {
|
||||
#[default]
|
||||
V2,
|
||||
V3,
|
||||
V4,
|
||||
}
|
||||
|
||||
/// The load class of program class v3, decided 5 October 2026 (Counter ASIC 2.0, `docs/plans/counter-asic-2-status.md`
|
||||
|
|
@ -793,6 +799,13 @@ pub enum ProgramClass {
|
|||
/// [`generate_from_seed_bytes_program_class`] (`LoadClass::era(V3_CLASS, era, &V3_ALLOWED)`); here `era` is `None`.
|
||||
pub const V3_CLASS: LoadClass = LoadClass { era: None, hot: None, ..LoadClass::MX8 };
|
||||
|
||||
/// The load class of program class v4 (Counter ASIC 3.0 item 8, `docs/analysis/latency-shadow-2026-10-06.md`, the
|
||||
/// candidate of 6 October 2026, gated by `docs/plans/counter-asic-3-node.md`): class v3 plus the latency-shadow block
|
||||
/// of 256 ALU instructions run 27 times per iteration ("mx8+sh256x27", 55,296 shadow instructions per hash). The
|
||||
/// base program, the 16 loads, the item construction, the cache growth rule and the era draw are class v3's, draw
|
||||
/// for draw, so a v4 epoch's day cache and dataset are the v3 day's. Composed with the era exactly as V3 is.
|
||||
pub const V4_CLASS: LoadClass = LoadClass { shadow: Some(ShadowClass { instrs: 256, reps: 27 }), ..V3_CLASS };
|
||||
|
||||
/// The width set class v3's era draw chooses from: 4 bytes only (the read-width decision of 5 October 2026; the
|
||||
/// draw is consumed, so widening the set at genesis keeps the derivation).
|
||||
pub const V3_ALLOWED: [u8; 1] = [1];
|
||||
|
|
@ -801,8 +814,14 @@ pub const V3_ALLOWED: [u8; 1] = [1];
|
|||
/// over the width set `allowed`, generator 3 stamped and the era bytes recorded (`docs/plans/era-layout.md`).
|
||||
/// The chain's path is this with `base = V3_CLASS` and `allowed = V3_ALLOWED`.
|
||||
pub fn generate_era(seed_string: &str, seed_bytes: &[u8], base: LoadClass, era_bytes: &[u8], allowed: &[u8]) -> Program {
|
||||
generate_era_generator(seed_string, seed_bytes, base, era_bytes, allowed, GENERATOR_VERSION_V3)
|
||||
}
|
||||
|
||||
/// [`generate_era`] with the generator version stamped by the caller: 3 for class v3 over [`V3_CLASS`], 4 for
|
||||
/// class v4 over [`V4_CLASS`] (Counter ASIC 3.0; the shadow block of the base rides through `LoadClass::era`).
|
||||
pub fn generate_era_generator(seed_string: &str, seed_bytes: &[u8], base: LoadClass, era_bytes: &[u8], allowed: &[u8], generator: u32) -> Program {
|
||||
let mut p = generate_from_seed_bytes_class(seed_string, seed_bytes, LoadClass::era(base, era_bytes, allowed));
|
||||
p.generator = GENERATOR_VERSION_V3;
|
||||
p.generator = generator;
|
||||
p.era_bytes = Some(era_bytes.to_vec());
|
||||
p
|
||||
}
|
||||
|
|
@ -813,6 +832,7 @@ impl ProgramClass {
|
|||
match self {
|
||||
ProgramClass::V2 => LoadClass::V2,
|
||||
ProgramClass::V3 => V3_CLASS,
|
||||
ProgramClass::V4 => V4_CLASS,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -821,23 +841,26 @@ impl ProgramClass {
|
|||
match self {
|
||||
ProgramClass::V2 => GENERATOR_VERSION,
|
||||
ProgramClass::V3 => GENERATOR_VERSION_V3,
|
||||
ProgramClass::V4 => GENERATOR_VERSION_V4,
|
||||
}
|
||||
}
|
||||
|
||||
/// The class of a generator version: 2 and 3 are the two classes, anything else is no class this crate runs.
|
||||
/// The class of a generator version: 2, 3 and 4 are the three classes, anything else is no class this crate runs.
|
||||
pub fn from_generator(generator: u32) -> Option<ProgramClass> {
|
||||
match generator {
|
||||
GENERATOR_VERSION => Some(ProgramClass::V2),
|
||||
GENERATOR_VERSION_V3 => Some(ProgramClass::V3),
|
||||
GENERATOR_VERSION_V4 => Some(ProgramClass::V4),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// "v2" or "v3": the `IGNEUM_PROGRAM_CLASS` string of a pack and the `class=` token of a job line.
|
||||
/// "v2", "v3" or "v4": the `IGNEUM_PROGRAM_CLASS` string of a pack and the `class=` token of a job line.
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
ProgramClass::V2 => "v2",
|
||||
ProgramClass::V3 => "v3",
|
||||
ProgramClass::V4 => "v4",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -845,10 +868,16 @@ impl ProgramClass {
|
|||
match s.trim() {
|
||||
"v2" => Some(ProgramClass::V2),
|
||||
"v3" => Some(ProgramClass::V3),
|
||||
"v4" => Some(ProgramClass::V4),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the class reads the era seed (every class after v2: the era layout is drawn inside the class).
|
||||
pub fn has_era(&self) -> bool {
|
||||
*self != ProgramClass::V2
|
||||
}
|
||||
|
||||
/// The generator version as a byte, for wire formats that carry the class as a number.
|
||||
pub fn as_u8(&self) -> u8 {
|
||||
self.generator_version() as u8
|
||||
|
|
@ -957,18 +986,23 @@ impl Program {
|
|||
/// || attempt_le32`. Written into every pack so a version 1 program, or another attempt of the same seed,
|
||||
/// can never be mistaken for this one.
|
||||
pub fn program_id(&self) -> u64 {
|
||||
if self.class.is_v2() || self.generator == GENERATOR_VERSION_V3 {
|
||||
// Spec 01 section 1.4.6: a class v3 program's id is `program_id(3, seed, attempt)`; the generator version
|
||||
// in the preimage separates it from every version 2 program of the same seed
|
||||
if self.class.is_v2() || self.generator == GENERATOR_VERSION_V3 || self.generator == GENERATOR_VERSION_V4 {
|
||||
// Spec 01 section 1.4.6: a class v3 program's id is `program_id(3, seed, attempt)`, a class v4 program's
|
||||
// `program_id(4, seed, attempt)` (Counter ASIC 3.0); the generator version in the preimage separates
|
||||
// them from every version 2 program of the same seed
|
||||
program_id(self.generator, &self.seed, self.attempt)
|
||||
} else {
|
||||
program_id_class(self.generator, &self.seed, self.attempt, &self.class)
|
||||
}
|
||||
}
|
||||
|
||||
/// The program class of this program, from its generator version (3 = v3, everything else v2).
|
||||
/// The program class of this program, from its generator version (3 = v3, 4 = v4, everything else v2).
|
||||
pub fn program_class(&self) -> ProgramClass {
|
||||
if self.generator == GENERATOR_VERSION_V3 { ProgramClass::V3 } else { ProgramClass::V2 }
|
||||
match self.generator {
|
||||
GENERATOR_VERSION_V3 => ProgramClass::V3,
|
||||
GENERATOR_VERSION_V4 => ProgramClass::V4,
|
||||
_ => ProgramClass::V2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1306,15 +1340,18 @@ pub fn generate_from_seed_bytes_class(seed_string: &str, seed_bytes: &[u8], clas
|
|||
/// program, so its packs and its id say generator 3 (spec 01 sections 1.4.5 and 1.4.6).
|
||||
pub fn generate_from_seed_bytes_program_class(seed_string: &str, seed_bytes: &[u8], class: ProgramClass, era_bytes: Option<&[u8]>) -> Program {
|
||||
// Class v3 with an era seed: the era layout (docs/plans/era-layout.md) drawn from the era bytes inside V3_CLASS.
|
||||
// Without era bytes (a template before the era is known) the bare V3_CLASS stands.
|
||||
if let (ProgramClass::V3, Some(era)) = (class, era_bytes) {
|
||||
return generate_era(seed_string, seed_bytes, V3_CLASS, era, &V3_ALLOWED);
|
||||
// Class v4 (Counter ASIC 3.0): the same draw inside V4_CLASS (V3_CLASS plus the shadow block), generator 4.
|
||||
// Without era bytes (a template before the era is known) the bare class stands.
|
||||
match (class, era_bytes) {
|
||||
(ProgramClass::V3, Some(era)) => return generate_era(seed_string, seed_bytes, V3_CLASS, era, &V3_ALLOWED),
|
||||
(ProgramClass::V4, Some(era)) => return generate_era_generator(seed_string, seed_bytes, V4_CLASS, era, &V3_ALLOWED, GENERATOR_VERSION_V4),
|
||||
_ => {}
|
||||
}
|
||||
let mut p = generate_from_seed_bytes_class(seed_string, seed_bytes, class.load_class());
|
||||
p.generator = class.generator_version();
|
||||
// The era is a property of class v3 programs; a version 2 program never records one, so the pinned v2 packs
|
||||
// and every v2 export stay byte-identical whatever the chain reports for the era
|
||||
p.era_bytes = if class == ProgramClass::V3 { era_bytes.map(|b| b.to_vec()) } else { None };
|
||||
// The era is a property of class v3 and v4 programs; a version 2 program never records one, so the pinned v2
|
||||
// packs and every v2 export stay byte-identical whatever the chain reports for the era
|
||||
p.era_bytes = if class.has_era() { era_bytes.map(|b| b.to_vec()) } else { None };
|
||||
p
|
||||
}
|
||||
|
||||
|
|
@ -1644,16 +1681,68 @@ mod tests {
|
|||
assert_eq!(v3.program_id(), program_id(GENERATOR_VERSION_V3, &v3.seed, v3.attempt));
|
||||
assert_ne!(v3.program_id(), program_id(GENERATOR_VERSION, &v3.seed, v3.attempt));
|
||||
assert_ne!(v3.program_id(), v2.program_id());
|
||||
for c in [ProgramClass::V2, ProgramClass::V3] {
|
||||
for c in [ProgramClass::V2, ProgramClass::V3, ProgramClass::V4] {
|
||||
assert_eq!(ProgramClass::parse(c.name()), Some(c));
|
||||
assert_eq!(ProgramClass::from_generator(c.generator_version()), Some(c));
|
||||
assert_eq!(ProgramClass::from_u8(c.as_u8()), Some(c));
|
||||
}
|
||||
assert_eq!(ProgramClass::from_generator(1), None);
|
||||
assert_eq!(ProgramClass::from_generator(4), None);
|
||||
assert_eq!(ProgramClass::parse("v4"), None);
|
||||
assert_eq!(ProgramClass::from_generator(5), None);
|
||||
assert_eq!(ProgramClass::parse("v5"), None);
|
||||
assert_eq!(ProgramClass::default(), ProgramClass::V2);
|
||||
assert_eq!(ProgramClass::V2.load_class(), LoadClass::V2);
|
||||
assert!(!ProgramClass::V2.has_era() && ProgramClass::V3.has_era() && ProgramClass::V4.has_era());
|
||||
}
|
||||
|
||||
/// Counter ASIC 3.0 (6 October 2026): class v4 is class v3 with the latency-shadow block `sh256x27`, drawn after
|
||||
/// the base program, so a v4 program's base instructions, attempt and era draw are the v3 program's of the same
|
||||
/// seed and era, draw for draw; its generator is 4, its id `program_id(4, seed, attempt)`, its era recorded;
|
||||
/// v2 and v3 are untouched.
|
||||
#[test]
|
||||
fn program_class_v4_is_class_v3_with_the_shadow_block() {
|
||||
assert_eq!(V4_CLASS, V3_CLASS.with_shadow(256, 27));
|
||||
assert_eq!(V4_CLASS.name(), "mx8+sh256x27");
|
||||
assert_eq!(LoadClass::parse("mx8+sh256x27"), Some(V4_CLASS));
|
||||
assert_eq!(LoadClass { shadow: None, ..V4_CLASS }, V3_CLASS, "v4 differs from v3 in the shadow alone");
|
||||
assert_eq!(V4_CLASS.shadow_instrs_per_hash(), 8 * 256 * 27);
|
||||
assert_eq!(ProgramClass::V4.load_class(), V4_CLASS);
|
||||
assert_eq!(ProgramClass::V4.generator_version(), GENERATOR_VERSION_V4);
|
||||
let era = [7u8; 32];
|
||||
let v3 = generate_from_seed_bytes_program_class("igneum-genesis", b"igneum-genesis", ProgramClass::V3, Some(&era));
|
||||
let v4 = generate_from_seed_bytes_program_class("igneum-genesis", b"igneum-genesis", ProgramClass::V4, Some(&era));
|
||||
assert_eq!(v4.generator, GENERATOR_VERSION_V4);
|
||||
assert_eq!(v4.program_class(), ProgramClass::V4);
|
||||
assert_eq!(v4.era_bytes.as_deref(), Some(&era[..]));
|
||||
assert_eq!(v4.instrs, v3.instrs, "the base program is class v3's, draw for draw");
|
||||
assert_eq!(v4.attempt, v3.attempt);
|
||||
assert_eq!(v4.seed, v3.seed);
|
||||
assert!(v3.shadow.is_empty() && !v3.has_shadow());
|
||||
assert_eq!(v4.shadow.len(), 256);
|
||||
assert_eq!(v4.shadow_reps(), 27);
|
||||
assert_eq!(v4.shadow_instrs_per_hash(), 55_296);
|
||||
assert_eq!(LoadClass { era: None, ..v4.class }, V4_CLASS, "the era rides inside V4_CLASS");
|
||||
assert_eq!(v4.class.era, v3.class.era, "the same era draw as class v3");
|
||||
assert_eq!(v4.class, LoadClass::era(V4_CLASS, &era, &V3_ALLOWED));
|
||||
assert!(check(&v4).is_ok());
|
||||
assert_eq!(v4.program_id(), program_id(GENERATOR_VERSION_V4, &v4.seed, v4.attempt));
|
||||
assert_ne!(v4.program_id(), v3.program_id());
|
||||
assert_ne!(v4.program_id(), program_id(GENERATOR_VERSION, &v4.seed, v4.attempt));
|
||||
// without an era (a template before the era is known) the bare V4_CLASS stands, generator 4
|
||||
let bare = generate_from_seed_bytes_program_class("igneum-genesis", b"igneum-genesis", ProgramClass::V4, None);
|
||||
assert_eq!(bare.class, V4_CLASS);
|
||||
assert_eq!(bare.generator, GENERATOR_VERSION_V4);
|
||||
assert_eq!(bare.era_bytes, None);
|
||||
// the block changes the hash over the same dataset; the same seed and era derive the same block again
|
||||
let ds = crate::verify::DatasetSource::new("2026-10-06", crate::verify::DatasetMode::ClosedForm, 20);
|
||||
let h3 = crate::verify::hash_warp(&v3, 0, &ds);
|
||||
let h4 = crate::verify::hash_warp(&v4, 0, &ds);
|
||||
assert_ne!(h3, h4);
|
||||
let again = generate_from_seed_bytes_program_class("igneum-genesis", b"igneum-genesis", ProgramClass::V4, Some(&era));
|
||||
assert_eq!(again.shadow, v4.shadow);
|
||||
assert_eq!(crate::verify::hash_warp(&again, 0, &ds), h4);
|
||||
// v2 and v3 are byte for byte what they were (the pinned packs are diffed in tests/packs.rs)
|
||||
assert_eq!(V3_CLASS.shadow, None);
|
||||
assert_eq!(generate("igneum-genesis").program_id(), 0xbcc1248b10cc90f2);
|
||||
}
|
||||
|
||||
/// Era layout: the draw is deterministic, within bounds, and the six test eras are pinned; an era program has
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ pub mod verify;
|
|||
|
||||
pub use bind::{block_init_words, day_bytes, pow256_from_lane, target64_from_le256};
|
||||
pub use accept::{check as accept_program, AcceptReport, Reject};
|
||||
pub use generator::{generate, generate_from_seed_bytes, generate_from_seed_bytes_program_class, Instr, LoadClass, Op, Program, ProgramClass, GENERATOR_VERSION, GENERATOR_VERSION_V3, V3_CLASS};
|
||||
pub use generator::{generate, generate_from_seed_bytes, generate_from_seed_bytes_program_class, Instr, LoadClass, Op, Program, ProgramClass, GENERATOR_VERSION, GENERATOR_VERSION_V3, GENERATOR_VERSION_V4, V3_CLASS, V4_CLASS};
|
||||
pub use memhard::{cache_log2_words, dataset_log2_words, days_since_genesis, growth_doublings, Cache, MemhardCpu, MixParams, Shape};
|
||||
pub use seed::{fnv1a64, seed_words, SplitMix64};
|
||||
pub use verify::{hash_warp, interpret_warp_init, verify_block, DatasetMode, DatasetSource, Epoch};
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ struct Args {
|
|||
class: LoadClass,
|
||||
/// Days since genesis for the cache growth rule of a class with `growth` (0: the genesis cache).
|
||||
days: u64,
|
||||
/// The program class (Counter ASIC 2.0 seam): v2 (default) or v3, which draws from V3_CLASS with generator 3.
|
||||
/// The program class (Counter ASIC 2.0 seam): v2 (default), v3 (V3_CLASS, generator 3) or v4 (V4_CLASS, generator 4).
|
||||
program_class: Option<ProgramClass>,
|
||||
/// The era seed bytes a class v3 chain program records (`--era-hex`).
|
||||
era_hex: Option<String>,
|
||||
|
|
@ -96,7 +96,7 @@ fn usage() -> ! {
|
|||
\x20 --class C load class: v2 (default), mx4, mx8 (class v3: mixer x8, cache growth), dr<len> (Counter ASIC 3.0 item 2: the per-day derivation program, dr736 = the x8-equivalent), w4, w16, w64, w64x4, p4,p16,p64[xN], <class>m<mult>[g]\n\
|
||||
\x20 also: w4, w16, w64, w64x4, p4,p16,p64[xN], <class>m<mult>[g], <class>+sh<S>x<R> (latency-shadow block of S ALU instructions x R passes per iteration, Counter ASIC 3.0 item 8)\n\
|
||||
\x20 --days N days since genesis for the cache growth rule of a class with it (default 0: the 2^26-word cache)\n\
|
||||
\x20 --program-class v2|v3 the program class of the seam (v3 = generator 3 on V3_CLASS, the chain's own derivation; --era-hex records the era seed)\n\
|
||||
\x20 --program-class v2|v3|v4 the program class of the seam (v3 = generator 3 on V3_CLASS, v4 = generator 4 on V4_CLASS = mx8+sh256x27, the chain's own derivation; --era-hex records the era seed)\n\
|
||||
\x20 --era E era layout over --class: igneum-era-test/<n> or <n>:<64 hex> (the 32-byte era seed E_n)\n\
|
||||
\x20 --era-widths 4[,16,64] the width set the era draws from, in bytes (default 4: pinned; more lets the era draw it)"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ pub struct PackIdentity {
|
|||
pub attempt: u32,
|
||||
pub seedw: [u32; 8],
|
||||
pub keyw: [u32; 8],
|
||||
/// `IGNEUM_GENERATOR` (2 or 3; a pack without the line is generator 1, which no worker runs).
|
||||
/// `IGNEUM_GENERATOR` (2, 3 or 4; a pack without the line is generator 1, which no worker runs).
|
||||
pub generator: u32,
|
||||
/// The program class the generator version names (Counter ASIC 2.0).
|
||||
pub class: ProgramClass,
|
||||
|
|
@ -164,9 +164,9 @@ pub fn verify_pack_texts(program_h: &str, seeds_txt: Option<&str>, want_epoch: &
|
|||
verify_pack_texts_chain(program_h, seeds_txt, want_epoch, want_day, None, None)
|
||||
}
|
||||
|
||||
/// [`verify_pack_texts`] that also demands a program class and, for class v3, the era seed the chain is on
|
||||
/// (Counter ASIC 2.0, 5 October 2026). `want_class` `None` accepts either class; `want_era` `None` skips the era.
|
||||
/// A pack whose `IGNEUM_GENERATOR` is neither 2 nor 3 is refused whatever is wanted.
|
||||
/// [`verify_pack_texts`] that also demands a program class and, for class v3 and v4, the era seed the chain is on
|
||||
/// (Counter ASIC 2.0, 5 October 2026; class v4 Counter ASIC 3.0, 6 October 2026). `want_class` `None` accepts any
|
||||
/// class; `want_era` `None` skips the era. A pack whose `IGNEUM_GENERATOR` is not 2, 3 or 4 is refused whatever is wanted.
|
||||
pub fn verify_pack_texts_chain(
|
||||
program_h: &str,
|
||||
seeds_txt: Option<&str>,
|
||||
|
|
@ -177,7 +177,7 @@ pub fn verify_pack_texts_chain(
|
|||
) -> Result<PackIdentity, PackFault> {
|
||||
let generator = define_u32(program_h, "IGNEUM_GENERATOR").unwrap_or(1);
|
||||
let Some(class) = ProgramClass::from_generator(generator) else {
|
||||
return Err(PackFault::WrongClass(format!("IGNEUM_GENERATOR {generator} is not a generator version this software runs (2 or 3)")));
|
||||
return Err(PackFault::WrongClass(format!("IGNEUM_GENERATOR {generator} is not a generator version this software runs (2, 3 or 4)")));
|
||||
};
|
||||
// IGNEUM_PROGRAM_CLASS, when present, must name the class the generator version names
|
||||
if let Some(named) = define_str(program_h, "IGNEUM_PROGRAM_CLASS") {
|
||||
|
|
@ -191,12 +191,12 @@ pub fn verify_pack_texts_chain(
|
|||
return Err(PackFault::WrongClass(format!("the pack is program class {} (generator {generator}), the chain is on class {}", class.name(), want.name())));
|
||||
}
|
||||
}
|
||||
if let (Some(want), ProgramClass::V3) = (want_era, class) {
|
||||
if let (Some(want), true) = (want_era, class.has_era()) {
|
||||
let want_hex = hex(want);
|
||||
match &era_hex {
|
||||
Some(h) if *h == want_hex => {}
|
||||
Some(h) => return Err(PackFault::WrongClass(format!("the pack's era seed {} is not the era seed {} the job names", short(h), short(&want_hex)))),
|
||||
None => return Err(PackFault::WrongClass("a class v3 pack without IGNEUM_ERA_SEED_HEX; the job names an era seed".into())),
|
||||
None => return Err(PackFault::WrongClass(format!("a class {} pack without IGNEUM_ERA_SEED_HEX; the job names an era seed", class.name()))),
|
||||
}
|
||||
}
|
||||
let seedw = define_words(program_h, "IGNEUM_SEEDW_INIT").ok_or_else(|| PackFault::Unreadable("program.h has no IGNEUM_SEEDW_INIT with 8 words".into()))?;
|
||||
|
|
@ -393,5 +393,28 @@ mod tests {
|
|||
// a class line that contradicts the generator
|
||||
let bad = h3.replace("#define IGNEUM_PROGRAM_CLASS \"v3\"\n", "#define IGNEUM_PROGRAM_CLASS \"v2\"\n");
|
||||
assert!(matches!(verify_pack_texts(&bad, None, &e, &d), Err(PackFault::Disagree(_))));
|
||||
// Counter ASIC 3.0: a class v4 chain pack carries generator 4, the class line and the era seed; it is refused
|
||||
// when v3 (or v2) is wanted, and a v3 pack is refused when v4 is wanted; the era rule applies to v4 as to v3
|
||||
let v4 = Epoch::from_chain_seeds(&e, &d, Some(&era), ProgramClass::V4, "class test");
|
||||
let h4 = program_header(&v4.program, "test day", &v4.dataset);
|
||||
assert!(h4.contains("#define IGNEUM_GENERATOR 4\n"));
|
||||
assert!(h4.contains("#define IGNEUM_PROGRAM_CLASS \"v4\"\n"));
|
||||
assert!(h4.contains(&format!("#define IGNEUM_ERA_SEED_HEX \"{}\"\n", hex(&era))));
|
||||
assert!(h4.contains("#define IGNEUM_SHADOW_INSTRS 256\n") && h4.contains("#define IGNEUM_SHADOW_REPS 27\n"), "{h4}");
|
||||
let id = verify_pack_texts_chain(&h4, None, &e, &d, Some(ProgramClass::V4), Some(&era)).unwrap();
|
||||
assert_eq!((id.generator, id.class, id.era_hex.as_deref()), (4, ProgramClass::V4, Some(hex(&era).as_str())));
|
||||
assert_eq!(id.attempt, v3.program.attempt, "the v4 attempt is the v3 attempt of the same seed");
|
||||
assert!(verify_pack_texts(&h4, None, &e, &d).is_ok(), "no class wanted: any class passes");
|
||||
assert!(matches!(verify_pack_texts_chain(&h4, None, &e, &d, Some(ProgramClass::V3), None), Err(PackFault::WrongClass(_))));
|
||||
assert!(matches!(verify_pack_texts_chain(&h4, None, &e, &d, Some(ProgramClass::V2), None), Err(PackFault::WrongClass(_))));
|
||||
assert!(matches!(verify_pack_texts_chain(&h3, None, &e, &d, Some(ProgramClass::V4), None), Err(PackFault::WrongClass(_))));
|
||||
assert!(matches!(verify_pack_texts_chain(&h2, None, &e, &d, Some(ProgramClass::V4), None), Err(PackFault::WrongClass(_))));
|
||||
let err = verify_pack_texts_chain(&h4, None, &e, &d, Some(ProgramClass::V4), Some(&[1u8; 32])).unwrap_err();
|
||||
assert!(err.to_string().contains("era seed"), "{err}");
|
||||
let no_era = h4.replace(&format!("#define IGNEUM_ERA_SEED_HEX \"{}\"\n", hex(&era)), "");
|
||||
let err = verify_pack_texts_chain(&no_era, None, &e, &d, Some(ProgramClass::V4), Some(&era)).unwrap_err();
|
||||
assert!(err.to_string().contains("class v4 pack without IGNEUM_ERA_SEED_HEX"), "{err}");
|
||||
// the v3 pack of the same seeds is unchanged by the v4 class existing
|
||||
assert_eq!(program_header(&v3.program, "test day", &v3.dataset), h3);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,12 @@ every node, `program_class_v3_activation_daa` a few epochs ahead; reports blocks
|
|||
program id and class before and after, rejected blocks, forks, and every node's switch line; see
|
||||
`docs/plans/counter-asic-2-node.md`).
|
||||
|
||||
Class v4 switch gate (Counter ASIC 3.0 G4): `node infra/fast-time/class-v4.mjs` (the same shape on ports 29660 and up,
|
||||
`igneum-devnet-966`, data `/tmp/igneum-fast-time-v4`, `program_class_v3_activation_daa` at 60 and
|
||||
`program_class_v4_activation_daa` at 150 by default, so one run crosses v2, v3 and v4; `--activation never` is the
|
||||
known-failed case, which must report FAIL with no v4 epoch; `--metal <igneum-bench>` puts a real Metal miner on node 0
|
||||
for G4b; see `docs/plans/counter-asic-3-node.md`).
|
||||
|
||||
Miners: `igneum-miner` follows the epoch length and lead its node reports in every template (`pow_epoch`), no flag.
|
||||
The dataset day is not in the template, so a real-hash miner on a fast-time network takes `IGNEUM_POW_DAY_MS=1440000`
|
||||
in its environment (the node reads it from the file). The environment variables `IGNEUM_POW_EPOCH_BLOCKS`,
|
||||
|
|
@ -62,6 +68,8 @@ Time parameters, divided by 60 (devnet value, 60x value):
|
|||
| `proving_v1_segment_blocks`, `proving_v1_aggregator_share_bps` | 8, 1,000 | 8, 1,000 | a block count and a share |
|
||||
| `proving_v1_unproven_daa` | 600 | 10 | a DAA clock (the unproven allowance), divided by 60; the proving agent confirms the value |
|
||||
| `program_class_v3_activation_daa` | never | never | a height, not a clock: the lottery hash draws programs from class v3 (Counter ASIC 2.0, 5 Oct 2026) from the first EPOCH whose start is at or above this DAA score (rounded up to an epoch boundary: at 60 DAA per epoch, 150 means epoch 3 at DAA 180); `infra/fast-time/class-v3.mjs` sets it a few epochs ahead in its merged file |
|
||||
| `program_class_v4_activation_daa` | never | never | a height, not a clock: the lottery hash draws programs from class v4 (Counter ASIC 3.0, 6 Oct 2026: class v3 plus the latency-shadow block) from the first EPOCH whose start is at or above this DAA score, rounded up like the v3 switch; `infra/fast-time/class-v4.mjs` sets it a few epochs ahead in its merged file |
|
||||
| `proving_v1_fresh_rule_daa`, `exec_restart_number`, `exec_restart_trust_daa`, `exec_restart_hash` | never, never, never, "" | the same | heights and a hash, not clocks (the 0.3.12 and 0.3.13 switches); present so the fork's every-field test (`fast_time_60x_file_is_the_devnet_at_60x`) holds; added 6 Oct 2026 with the class v4 field |
|
||||
|
||||
Unchanged, and why:
|
||||
|
||||
|
|
|
|||
305
infra/fast-time/class-v4.mjs
Normal file
305
infra/fast-time/class-v4.mjs
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
#!/usr/bin/env node
|
||||
// Counter ASIC 3.0 gate G4 (docs/plans/counter-asic-3-node.md; the 2.0 shape: docs/plans/counter-asic-2-rollout.md
|
||||
// section 7): the fast-time 3-node network mining across a program class v4 activation. A private network on
|
||||
// 127.0.0.1 ports 29660 and up, data under /tmp/igneum-fast-time-v4, network id igneum-devnet-966, every node on
|
||||
// infra/fast-time/override-60x.json merged with a CPU genesis difficulty (genesis_bits 0x1f010000, 2^16 hashes per
|
||||
// block, as sim/difficulty/testnet_v2.py), `program_class_v3_activation_daa` at --v3-activation (default 60: epoch 1,
|
||||
// so the run crosses v2 -> v3 -> v4 and the two switches are both exercised) and `program_class_v4_activation_daa`
|
||||
// at --activation (default 150: inside epoch 2 at 60 DAA per epoch, so the switch rounds UP to epoch 3 at DAA 180,
|
||||
// the boundary rule under test). One real CPU miner per node (igneum-miner --engine igneum-pow, 1 thread) follows
|
||||
// its node's templates, so every block of the run is a real lottery-hash solution and every node verifies every
|
||||
// block of the other two under the class of its epoch.
|
||||
//
|
||||
// Reports, from the nodes' RPC and the logs: blocks on each side of the v4 boundary, the program class and id of
|
||||
// every epoch (before and after), rejected blocks (the miners' submit answers and the nodes' "PoW rejected" lines),
|
||||
// forks (every node's sink, selected tip and block count at the end), and every node's two switch lines. Exit 0 when
|
||||
// every check passes. Never touches the live devnet (26610/26611, 26640/26641, 28640), the class-v3 harness (29600)
|
||||
// or the simnet.mjs ports.
|
||||
//
|
||||
// The known-failed case (the gate rule: a gate is trusted only after it fires on one finished and one failed case):
|
||||
// `--activation never` leaves the v4 switch at never; the harness must then report FAIL with no v4 epoch seen.
|
||||
//
|
||||
// node infra/fast-time/class-v4.mjs [--secs 420] [--activation 150|never] [--v3-activation 60|never] [--epochs-after 2]
|
||||
// [--threads 1] [--metal <igneum-bench>] [--genesis-bits 0x1f010000]
|
||||
// IGNEUMD and IGNEUM_MINER name the binaries (default: the ca3 fork worktree's target-ca3v4/release).
|
||||
// --metal: node 0's miner is a real Metal GPU worker (igneum-miner --worker <igneum-bench> --prepare-packs <dir>
|
||||
// --exit-on-seed-change, the app's own shape), so the Metal worker's class v4 path (the prepare line with the pack
|
||||
// directory and the class and era tokens, the pack-built program and day) mines across the boundary (gate G4b);
|
||||
// the report then adds the miner's PREPARE lines, the worker's prepared lines, any need / mismatch / refusal line,
|
||||
// the accepted blocks on each side and the swap time. --genesis-bits raises the CPU difficulty for a GPU miner
|
||||
// (0x1e010000 = 2^24 hashes per block: under a second on an M5 Max; the CPU miners on nodes 1 and 2 then verify
|
||||
// and rarely find).
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdirSync, rmSync, writeFileSync, readFileSync, openSync, existsSync } from 'node:fs';
|
||||
import { connectRpc } from '../../tools/finality-attacks/lib/rpc.mjs';
|
||||
import { devAddress } from '../../tools/harness/lib/address.mjs';
|
||||
|
||||
const ROOT = new URL('../../', import.meta.url).pathname;
|
||||
const FILE = `${ROOT}infra/fast-time/override-60x.json`;
|
||||
const BIN = process.env.IGNEUM_CA3_BIN || `${ROOT}vendor/igneum-node-ca3v4/target-ca3v4/release`;
|
||||
const IGNEUMD = process.env.IGNEUMD || `${BIN}/igneumd`;
|
||||
const CPU_MINER = process.env.IGNEUM_MINER || `${BIN}/igneum-miner`;
|
||||
const TMP = '/tmp/igneum-fast-time-v4';
|
||||
const BASE = 29660, SUFFIX = 966;
|
||||
const NEVER = '18446744073709551615';
|
||||
const args = process.argv.slice(2);
|
||||
const flag = (name, dflt) => { const i = args.indexOf(`--${name}`); return i >= 0 ? +args[i + 1] : dflt; };
|
||||
const sflag = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : null; };
|
||||
// an activation flag: a DAA score, or "never" (the known-failed case for the v4 switch; a v2-only chain for the v3 one)
|
||||
const actflag = (name, dflt) => { const v = sflag(name); if (v == null) return dflt; return v === 'never' ? null : +v; };
|
||||
const GENESIS_BITS = flag('genesis-bits', 0x1f010000);
|
||||
const METAL = sflag('metal');
|
||||
const SECS = flag('secs', 420);
|
||||
const ACTIVATION = actflag('activation', 150); // class v4 (N5); null = never
|
||||
const V3_ACTIVATION = actflag('v3-activation', 60); // class v3 (N4); null = never
|
||||
const EPOCHS_AFTER = flag('epochs-after', 2);
|
||||
const THREADS = flag('threads', 1);
|
||||
const started = [];
|
||||
const log = (...a) => console.log(new Date().toISOString().slice(11, 23), ...a);
|
||||
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
||||
for (const b of [IGNEUMD, CPU_MINER, ...(METAL ? [METAL] : [])]) if (!existsSync(b)) { console.error(`missing ${b}`); process.exit(2); }
|
||||
|
||||
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
|
||||
// The file is merged as TEXT, never through JSON.parse: a `never` height is 18446744073709551615, which JavaScript
|
||||
// rounds to 1.8446744073709552e+19 and the node refuses ("expected u64"; the first gate run, 5 October 2026 21:30Z).
|
||||
// The merged fields are appended after the file's last field; a field already in the file is removed first.
|
||||
const 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 DAY_MS = field('pow_day_ms');
|
||||
const FIRST_V4_EPOCH = ACTIVATION == null ? null : Math.ceil(ACTIVATION / EPOCH);
|
||||
const FIRST_V3_EPOCH = V3_ACTIVATION == null ? null : Math.ceil(V3_ACTIVATION / EPOCH);
|
||||
const BOUNDARY = FIRST_V4_EPOCH == null ? Infinity : FIRST_V4_EPOCH * EPOCH;
|
||||
const override = `${TMP}/override.json`;
|
||||
export function mergeOverrideText(text, fields) {
|
||||
let out = text;
|
||||
for (const k of Object.keys(fields)) out = out.replace(new RegExp(`\\s*"${k}":\\s*[^,}\\n]+,?`), '');
|
||||
// a string of digits is written raw (a never height is the u64 max, beyond a JavaScript number); other strings quoted
|
||||
const extra = Object.entries(fields).map(([k, v]) => `"${k}": ${typeof v === 'string' && !/^\d+$/.test(v) ? JSON.stringify(v) : v}`).join(', ');
|
||||
return out.replace(/,?\s*}\s*$/, `,\n ${extra}\n}\n`);
|
||||
}
|
||||
// the two heights as digit text (the file's own two fields, both at never, are removed first by the merge)
|
||||
const asText = (v) => v == null ? NEVER : String(v);
|
||||
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(ACTIVATION) }));
|
||||
log(`v3 activation ${V3_ACTIVATION ?? 'never'} (first v3 epoch ${FIRST_V3_EPOCH ?? 'none'}), v4 activation ${ACTIVATION ?? 'never'} at ${EPOCH} DAA per epoch: the first v4 epoch is ${FIRST_V4_EPOCH ?? 'none'} (DAA ${Number.isFinite(BOUNDARY) ? BOUNDARY : 'never'}); run ${SECS} s, CPU genesis bits 0x${GENESIS_BITS.toString(16)}`);
|
||||
|
||||
class Node {
|
||||
constructor(i, connect = []) {
|
||||
this.i = i; this.grpcPort = BASE + i * 10; this.p2pPort = BASE + i * 10 + 1; this.jsonPort = BASE + i * 10 + 2;
|
||||
this.connect = connect; this.dir = `${TMP}/n${i}`; this.logFile = `${this.dir}/node.log`;
|
||||
}
|
||||
get grpc() { return `grpc://127.0.0.1:${this.grpcPort}`; }
|
||||
async start() {
|
||||
mkdirSync(this.dir, { recursive: true });
|
||||
const a = ['--devnet', `--devnet-suffix=${SUFFIX}`, '--nodnsseed', '--disable-upnp', '--nologfiles', '--enable-unsynced-mining', '--utxoindex',
|
||||
`--appdir=${this.dir}`, `--rpclisten=127.0.0.1:${this.grpcPort}`, `--rpclisten-json=127.0.0.1:${this.jsonPort}`,
|
||||
`--listen=127.0.0.1:${this.p2pPort}`, `--override-params-file=${override}`, '--loglevel=info', '--yes'];
|
||||
if (this.connect.length) a.push(`--connect=${this.connect.join(',')}`); else a.push('--outpeers=0');
|
||||
const out = openSync(this.logFile, 'a');
|
||||
this.proc = spawn(IGNEUMD, a, { stdio: ['ignore', out, out] });
|
||||
started.push(this.proc);
|
||||
await sleep(1200);
|
||||
this.rpc = await connectRpc(`ws://127.0.0.1:${this.jsonPort}`);
|
||||
log(`n${this.i} up pid ${this.proc.pid} json ${this.jsonPort} p2p ${this.p2pPort}`);
|
||||
return this;
|
||||
}
|
||||
grepLog(re) { try { return readFileSync(this.logFile, 'utf8').split('\n').filter(l => re.test(l)); } catch { return []; } }
|
||||
}
|
||||
function miner(bin, argv, name, env = {}) {
|
||||
const out = openSync(`${TMP}/${name}.log`, 'a');
|
||||
const p = spawn(bin, argv, { stdio: ['ignore', out, out], env: { ...process.env, ...env } });
|
||||
started.push(p);
|
||||
return p;
|
||||
}
|
||||
async function stopAll() {
|
||||
for (const p of started.reverse()) { try { p.kill('SIGINT'); } catch { } }
|
||||
await sleep(1500);
|
||||
for (const p of started) { try { p.kill('SIGKILL'); } catch { } }
|
||||
}
|
||||
process.on('SIGINT', async () => { await stopAll(); process.exit(130); });
|
||||
process.on('unhandledRejection', async (e) => { log(`FAILED: ${e?.stack || e}`); await stopAll(); process.exit(3); }); // a thrown start leaves no node behind
|
||||
const minerName = (i) => (METAL && i === 0) ? 'metal0' : `cpu${i}`;
|
||||
const minerLog = (i) => { try { return readFileSync(`${TMP}/${minerName(i)}.log`, 'utf8').split('\n'); } catch { return []; } };
|
||||
const V4_LINE = /Program class v4 from the override file/, V3_LINE = /Program class v3 from the override file/;
|
||||
const tail = (l, what) => l.replace(new RegExp(`^.*?(Program class ${what})`), '$1');
|
||||
|
||||
const t0 = Date.now();
|
||||
const since = () => ((Date.now() - t0) / 1000).toFixed(1);
|
||||
const n0 = await new Node(0).start();
|
||||
const n1 = await new Node(1, [`127.0.0.1:${n0.p2pPort}`]).start();
|
||||
const n2 = await new Node(2, [`127.0.0.1:${n0.p2pPort}`]).start(); // --connect takes one address
|
||||
const nodes = [n0, n1, n2];
|
||||
for (const n of nodes) {
|
||||
const v3 = n.grepLog(V3_LINE)[0], v4 = n.grepLog(V4_LINE)[0];
|
||||
log(`n${n.i} switch lines: ${v3 ? tail(v3, 'v3') : '(no v3 line)'} | ${v4 ? tail(v4, 'v4') : '(no v4 line)'}`);
|
||||
}
|
||||
log(`n0 PoW schedule: ${n0.grepLog(/PoW schedule from the override file/).map(l => l.replace(/^.*?(PoW schedule)/, '$1')).join(' | ') || '(no line)'}`);
|
||||
log(`n0 digest: ${n0.grepLog(/Consensus params digest/).map(l => l.replace(/^.*?digest: /, '').slice(0, 16)).join(' ')}`);
|
||||
|
||||
// one real CPU miner per node, 1 thread, 2^16 expected hashes per block at genesis; with --metal, node 0's miner drives
|
||||
// the Metal worker the way the app does (the pack for every prepared pair under packs/prepare, exit 42/44 on a refusal)
|
||||
const PACKS = `${TMP}/packs/prepare`;
|
||||
mkdirSync(PACKS, { recursive: true });
|
||||
nodes.forEach((n, i) => {
|
||||
if (METAL && i === 0) {
|
||||
miner(CPU_MINER, ['mine', n.grpc, '1', String(SECS), 'metal0', '--engine', 'igneum-pow', '--worker', METAL, '--prepare-packs', PACKS, '--exit-on-seed-change', '--payout-label', 'metal0', '--status-secs', '30', '--no-vote'], 'metal0', { IGNEUM_POW_DAY_MS: String(DAY_MS) });
|
||||
log(`n0 miner: Metal worker ${METAL}, packs under ${PACKS}`);
|
||||
} else {
|
||||
miner(CPU_MINER, ['mine', n.grpc, String(THREADS), String(SECS), `cpu${i}`, '--engine', 'igneum-pow', '--payout-label', `cpu${i}`, '--status-secs', '30', '--no-vote'], `cpu${i}`, { IGNEUM_POW_DAY_MS: String(DAY_MS) });
|
||||
}
|
||||
});
|
||||
const pay = devAddress('fast-time-v4');
|
||||
|
||||
const epochs = new Map(); // epoch index -> { class, firstSeenDaa, at }
|
||||
let firstV4 = null, firstV3 = null, lastEpoch = -1, lastReport = 0, lastDaa = 0, switchEnd = null;
|
||||
const samples = [];
|
||||
while (Date.now() - t0 < SECS * 1000) {
|
||||
await sleep(1000);
|
||||
let daa = null, epoch = null, cls = null, nextCls = null, era = null, eraSeed = null, act3 = null, act4 = null;
|
||||
try {
|
||||
const t = await n0.rpc.call('getBlockTemplate', { payAddress: pay, extraData: [] });
|
||||
const pe = t.powEpoch || t.pow_epoch || {};
|
||||
daa = pe.virtualDaaScore ?? t.block?.header?.daaScore; epoch = pe.epochIndex; cls = pe.programClass; nextCls = pe.nextProgramClass;
|
||||
era = pe.eraIndex; eraSeed = pe.eraSeed; act3 = pe.programClassV3ActivationDaa; act4 = pe.programClassV4ActivationDaa;
|
||||
} catch (e) { log(`template: ${e.message}`); }
|
||||
if (epoch != null && epoch !== lastEpoch) {
|
||||
epochs.set(epoch, { class: cls, firstSeenDaa: daa, at: +since() });
|
||||
log(`epoch ${lastEpoch} -> ${epoch} at daa ${daa}, ${since()} s: template class ${cls} (generator), next epoch class ${nextCls}, era ${era} seed ${String(eraSeed).slice(0, 16)}, activation v3 ${act3} v4 ${act4}`);
|
||||
if (firstV3 == null && cls === 3) { firstV3 = { epoch, daa, at: +since() }; log(`CLASS SWITCH: the template is class v3 from epoch ${epoch} (daa ${daa}) at ${since()} s wall`); }
|
||||
if (firstV4 == null && cls === 4) { firstV4 = { epoch, daa, at: +since() }; log(`CLASS SWITCH: the template is class v4 from epoch ${epoch} (daa ${daa}) at ${since()} s wall`); }
|
||||
lastEpoch = epoch;
|
||||
}
|
||||
lastDaa = daa ?? lastDaa;
|
||||
if (Date.now() - lastReport > 15000) {
|
||||
lastReport = Date.now();
|
||||
const counts = await Promise.all(nodes.map(async n => { try { const d = await n.rpc.call('getBlockDagInfo'); return `${d.blockCount}/${String(d.sink).slice(0, 8)}`; } catch { return '?'; } }));
|
||||
log(`t=${since()} s daa ${daa} epoch ${epoch} class ${cls} blocks/sink per node ${counts.join(' ')}`);
|
||||
samples.push({ t: +since(), daa, epoch, class: cls, nodes: counts });
|
||||
}
|
||||
if (firstV4 != null && daa != null && daa >= BOUNDARY + EPOCHS_AFTER * EPOCH) { switchEnd = +since(); break; }
|
||||
}
|
||||
await sleep(3000); // let the last blocks relay before the end-of-run reads
|
||||
|
||||
// ---- the end-of-run reads -----------------------------------------------------------------------------------------
|
||||
const dag = await Promise.all(nodes.map(async n => { try { return await n.rpc.call('getBlockDagInfo'); } catch (e) { return { error: e.message }; } }));
|
||||
const genesis = dag[0].pruningPointHash;
|
||||
// every block the first node holds, by DAA score, from genesis
|
||||
async function allBlocks(n) {
|
||||
const out = []; let low = genesis; const seen = new Set();
|
||||
for (let round = 0; round < 500; round++) {
|
||||
const r = await n.rpc.call('getBlocks', { lowHash: low, includeBlocks: true, includeTransactions: false });
|
||||
const blocks = r.blocks || [];
|
||||
let added = 0;
|
||||
for (const b of blocks) { const h = b.verboseData?.hash || b.header?.hash; if (seen.has(h)) continue; seen.add(h); out.push({ hash: h, daa: +b.header.daaScore, chain: !!b.verboseData?.isChainBlock, blue: +(b.verboseData?.blueScore ?? 0) }); added++; }
|
||||
if (!blocks.length || added === 0) break;
|
||||
low = (r.blockHashes || []).at(-1) || blocks.at(-1).verboseData?.hash; if (!low) break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
let blocks = [];
|
||||
try { blocks = await allBlocks(n0); } catch (e) { log(`getBlocks: ${e.message}`); }
|
||||
const before = blocks.filter(b => b.daa < BOUNDARY), after = blocks.filter(b => b.daa >= BOUNDARY);
|
||||
const chainBefore = before.filter(b => b.chain).length, chainAfter = after.filter(b => b.chain).length;
|
||||
|
||||
// program ids and classes per epoch from the miners' "program and 256 MiB cache ready" lines
|
||||
const programs = new Map(); // epoch seed -> { class, id, miners: Set }
|
||||
for (const i of [0, 1, 2]) for (const l of minerLog(i)) {
|
||||
const m = /epoch seed ([0-9a-f]{64}) day (\d+) \(daa (\d+)\): program and 256 MiB cache ready in ([\d.]+) ms; class (v\d) program id ([0-9a-f]{16})/.exec(l);
|
||||
if (!m) continue;
|
||||
const k = m[1]; const e = programs.get(k) || { seed: k.slice(0, 16), epoch: Math.floor(+m[3] / EPOCH), class: m[5], id: m[6], miners: new Set(), ready_ms: [] };
|
||||
if (e.id !== m[6] || e.class !== m[5]) e.disagree = true;
|
||||
e.miners.add(i); e.ready_ms.push(+m[4]); programs.set(k, e);
|
||||
}
|
||||
// ready_ms: the program generation plus the day's cache and dataset build on one CPU core (the first epoch of a day
|
||||
// pays the cache; a v4 epoch after a v3 one reuses the v3 day cache, so its number is the program alone)
|
||||
const programRows = [...programs.values()].sort((a, b) => a.epoch - b.epoch).map(p => ({ epoch: p.epoch, class: p.class, program_id: p.id, seed: p.seed, miners: p.miners.size, disagree: !!p.disagree, ready_ms: p.ready_ms }));
|
||||
const idsOf = (c) => programRows.filter(p => p.class === c).map(p => p.program_id);
|
||||
const v2Ids = idsOf('v2'), v3Ids = idsOf('v3'), v4Ids = idsOf('v4');
|
||||
const olderIds = [...v2Ids, ...v3Ids];
|
||||
|
||||
// rejections: the miners' submit answers and the nodes' PoW lines
|
||||
const accepted = [0, 1, 2].map(i => minerLog(i).filter(l => /ACCEPTED block/.test(l)).length);
|
||||
const rejectedMiner = [0, 1, 2].map(i => minerLog(i).filter(l => /rejected nonce=|submit error/.test(l)));
|
||||
const rejectedNode = nodes.map(n => n.grepLog(/PoW rejected|Rejected block|rejected block/i));
|
||||
const switchLines = nodes.map(n => n.grepLog(V4_LINE).map(l => tail(l, 'v4'))[0] || null);
|
||||
const v3SwitchLines = nodes.map(n => n.grepLog(V3_LINE).map(l => tail(l, 'v3'))[0] || null);
|
||||
const sinks = dag.map(d => String(d.sink || '?').slice(0, 16));
|
||||
const counts = dag.map(d => d.blockCount ?? '?');
|
||||
const tips = dag.map(d => (d.tipHashes || []).length);
|
||||
|
||||
// the Metal miner's protocol lines (gate G4b): PREPARE sent (the miner), prepared / need / error (the worker), the swap
|
||||
let metal = null;
|
||||
if (METAL) {
|
||||
const L = minerLog(0);
|
||||
const t = (l) => { const m = /^(\d+\.\d+) /.exec(l); return m ? +m[1] * 1000 : null; };
|
||||
const switchAt = firstV4 ? t0 + firstV4.at * 1000 : null;
|
||||
const prepares = L.filter(l => /PREPARE sent for epoch seed/.test(l)).map(l => l.replace(/^\S+ /, ''));
|
||||
const prepared = L.filter(l => /worker: prepared /.test(l)).map(l => l.replace(/^\S+ /, ''));
|
||||
const preparedV4 = prepared.filter(l => / class v4 /.test(l));
|
||||
const need = L.filter(l => /worker: need |^\S+ worker error:.*need /.test(l) || /\bneed [0-9a-f]{64}/.test(l));
|
||||
// a PACK OUT OF DATE before the first class v4 prepare is the v2/v3 rebuild path at work (a seed that flipped inside
|
||||
// the quarter-lead confirm window, 3 DAA on the 60x profile); the gate judges the v4 path from its first prepare on
|
||||
const firstV4Prepare = L.findIndex(l => /PREPARE sent for epoch seed .* class v4/.test(l));
|
||||
const sinceV4 = (l, i) => firstV4Prepare < 0 || i >= firstV4Prepare;
|
||||
const mismatch = L.filter((l, i) => sinceV4(l, i) && /program class mismatch|era seed mismatch|PACK OUT OF DATE|pack .*: program pack/.test(l));
|
||||
const mismatchBefore = L.filter((l, i) => !sinceV4(l, i) && /program class mismatch|era seed mismatch|PACK OUT OF DATE/.test(l));
|
||||
const refused = L.filter(l => /exiting with code 4[24]|refused the program pack|prepare-failed/.test(l));
|
||||
const swaps = L.filter(l => /swapped with no pause|compiles inline|worker without prepare support/.test(l)).map(l => l.replace(/^\S+ /, ''));
|
||||
const accepted = L.filter(l => /ACCEPTED block/.test(l));
|
||||
const acceptedAfter = switchAt ? accepted.filter(l => (t(l) || 0) >= switchAt).length : 0;
|
||||
const found = L.filter(l => /worker: found |^\S+ found /.test(l)).length;
|
||||
const status = L.filter(l => /miner 'metal0' \[igneum-pow\]/.test(l)).map(l => l.replace(/^\S+ /, ''));
|
||||
const lastStatus = status.at(-1) || '';
|
||||
const mismatched = +(/mismatched=(\d+)/.exec(lastStatus)?.[1] ?? 0);
|
||||
const rate = /hash=([\d.]+) MH\/s/.exec(lastStatus)?.[1];
|
||||
metal = { worker: METAL, prepares, prepared, prepared_v4: preparedV4, prepared_v3: prepared.filter(l => / class v3 /.test(l)), need: need.length, mismatch_lines: mismatch, rebuild_lines_before_v4: mismatchBefore, refused, swaps, accepted_total: accepted.length, accepted_after_switch: acceptedAfter, found_lines: found, cpu_recheck_mismatched: mismatched, rate_mh_s: rate, last_status: lastStatus };
|
||||
}
|
||||
|
||||
const checks = {
|
||||
v4_switch_line_on_every_node: switchLines.every(Boolean),
|
||||
v4_switch_line_names_the_rounded_epoch: switchLines.every(l => l && FIRST_V4_EPOCH != null && l.includes(`active from epoch ${FIRST_V4_EPOCH} `)),
|
||||
v3_switch_line_on_every_node: v3SwitchLines.every(Boolean),
|
||||
template_switched_at_the_first_v4_epoch: firstV4 != null && firstV4.epoch === FIRST_V4_EPOCH,
|
||||
template_switched_at_the_first_v3_epoch: FIRST_V3_EPOCH == null ? firstV3 == null : (firstV3 != null && firstV3.epoch === FIRST_V3_EPOCH),
|
||||
blocks_before_the_boundary: before.length > 0,
|
||||
blocks_after_the_boundary: after.length > 0,
|
||||
v3_and_v4_programs_seen: v3Ids.length > 0 && v4Ids.length > 0,
|
||||
v2_programs_seen_when_v3_starts_later: FIRST_V3_EPOCH == null || FIRST_V3_EPOCH === 0 || v2Ids.length > 0,
|
||||
program_ids_differ_across_the_switch: v4Ids.length > 0 && !olderIds.some(id => v4Ids.includes(id)),
|
||||
miners_agree_on_every_program: programRows.every(p => !p.disagree),
|
||||
zero_rejected_by_miners: rejectedMiner.every(r => r.length === 0),
|
||||
zero_rejected_by_nodes: rejectedNode.every(r => r.length === 0),
|
||||
sinks_agree: new Set(sinks).size === 1,
|
||||
block_counts_agree: new Set(counts.map(String)).size === 1,
|
||||
...(METAL ? {
|
||||
metal_prepare_sent_for_v4: metal.prepares.some(l => / class v4 /.test(l)),
|
||||
metal_worker_prepared_v4_pack: metal.prepared_v4.length > 0,
|
||||
metal_no_need_or_mismatch: metal.need === 0 && metal.mismatch_lines.length === 0 && metal.refused.length === 0,
|
||||
metal_accepted_blocks_after_switch: metal.accepted_after_switch > 0,
|
||||
metal_cpu_recheck_clean: metal.cpu_recheck_mismatched === 0,
|
||||
metal_swapped_without_pause: metal.swaps.some(l => /swapped with no pause/.test(l)),
|
||||
} : {}),
|
||||
};
|
||||
const pass = Object.values(checks).every(Boolean);
|
||||
const summary = {
|
||||
pass, checks, activation: ACTIVATION ?? 'never', v3_activation: V3_ACTIVATION ?? 'never', epoch_blocks: EPOCH, first_v3_epoch: FIRST_V3_EPOCH, first_v4_epoch: FIRST_V4_EPOCH, boundary_daa: Number.isFinite(BOUNDARY) ? BOUNDARY : 'never', secs: SECS, threads: THREADS,
|
||||
node: IGNEUMD, miner: CPU_MINER, metal_worker: METAL, genesis_bits: `0x${GENESIS_BITS.toString(16)}`,
|
||||
template_switch_v3: firstV3, template_switch: firstV4, run_ended_at_s: switchEnd, final_daa: lastDaa,
|
||||
blocks: { total: blocks.length, before_boundary: before.length, after_boundary: after.length, chain_before: chainBefore, chain_after: chainAfter },
|
||||
programs: programRows, accepted_per_miner: accepted,
|
||||
rejected_by_miners: rejectedMiner.map(r => r.length), rejected_by_nodes: rejectedNode.map(r => r.length),
|
||||
rejected_lines: [...rejectedMiner.flat(), ...rejectedNode.flat()].slice(0, 20),
|
||||
sinks, block_counts: counts, tips_per_node: tips, switch_lines: switchLines, v3_switch_lines: v3SwitchLines, samples, metal,
|
||||
};
|
||||
writeFileSync(`${TMP}/summary.json`, JSON.stringify(summary, null, 2));
|
||||
log(`SUMMARY ${pass ? 'PASS' : 'FAIL'}: ${firstV4 ? `v4 from epoch ${firstV4.epoch} at DAA ${firstV4.daa}` : 'NO v4 epoch seen'}; blocks ${before.length} before / ${after.length} after the boundary at DAA ${Number.isFinite(BOUNDARY) ? BOUNDARY : 'never'} (chain ${chainBefore} / ${chainAfter}); programs ${programRows.map(p => `e${p.epoch}:${p.class}:${p.program_id}:${Math.max(...p.ready_ms)}ms`).join(' ')}; rejected miners ${rejectedMiner.map(r => r.length).join('/')} nodes ${rejectedNode.map(r => r.length).join('/')}; sinks ${sinks.join(' ')} (${checks.sinks_agree ? 'agree' : 'DIFFER'}); block counts ${counts.join('/')}; v4 switch lines ${switchLines.filter(Boolean).length}/3, v3 switch lines ${v3SwitchLines.filter(Boolean).length}/3`);
|
||||
if (metal) {
|
||||
log(`METAL: ${metal.prepares.length} PREPARE lines (${metal.prepares.filter(l => / class v4 /.test(l)).length} class v4, ${metal.prepares.filter(l => / class v3 /.test(l)).length} class v3), ${metal.prepared.length} prepared (${metal.prepared_v4.length} v4, ${metal.prepared_v3.length} v3), need ${metal.need}, mismatch/refusal lines ${metal.mismatch_lines.length + metal.refused.length}, accepted ${metal.accepted_total} (${metal.accepted_after_switch} after the switch), cpu re-check mismatched ${metal.cpu_recheck_mismatched}, rate ${metal.rate_mh_s} MH/s`);
|
||||
for (const l of [...metal.prepares, ...metal.prepared, ...metal.swaps]) log(` ${l.slice(0, 260)}`);
|
||||
for (const l of [...metal.mismatch_lines, ...metal.refused].slice(0, 10)) log(` BAD ${l.slice(0, 260)}`);
|
||||
}
|
||||
for (const [k, v] of Object.entries(checks)) if (!v) log(`FAILED CHECK ${k}`);
|
||||
log(`summary: ${TMP}/summary.json`);
|
||||
await stopAll();
|
||||
process.exit(pass ? 0 : 1);
|
||||
|
|
@ -55,15 +55,16 @@
|
|||
"proving_v0_activation_daa": 18446744073709551615,
|
||||
"finality_v3_activation_daa": 18446744073709551615,
|
||||
"program_class_v3_activation_daa": 18446744073709551615,
|
||||
"program_class_v4_activation_daa": 18446744073709551615,
|
||||
"proving_v1_fresh_rule_daa": 18446744073709551615,
|
||||
"exec_restart_number": 18446744073709551615,
|
||||
"exec_restart_hash": "",
|
||||
"exec_restart_trust_daa": 18446744073709551615,
|
||||
"pow_genesis_dataset_log2": 28,
|
||||
"proving_v1_activation_daa": 18446744073709551615,
|
||||
"proving_v1_segment_blocks": 8,
|
||||
"proving_v1_unproven_daa": 10,
|
||||
"proving_v1_aggregator_share_bps": 1000,
|
||||
"fees_v1_activation_daa": 0,
|
||||
"proving_v1_activation_daa": 18446744073709551615,
|
||||
"proving_v1_segment_blocks": 8,
|
||||
"proving_v1_unproven_daa": 10,
|
||||
"proving_v1_aggregator_share_bps": 1000,
|
||||
"fees": {"pgas": {"version": 1, "cycles_per_pgas": 1000, "intrinsic_pgas_per_tx": 300, "modexp_base": 10, "modexp_per_byte_numer": 1, "modexp_per_byte_denom": 10}, "block_proving_gas_limit": 120000, "shard_proving_gas_budget": 30000, "min_execution_base_fee_wei": 100000000000, "min_proving_base_fee_wei": 10000000000000, "initial_execution_base_fee_wei": 100000000000, "initial_proving_base_fee_wei": 10000000000000, "base_fee_change_denominator": 8}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,6 +130,24 @@ int main(int argc, char** argv) {
|
|||
CHECK(pf_pack_class_ok(pk.programClass, pk.eraHex, "v3", EPOCH_34, why, sizeof(why)) == 0 && strstr(why, "era seed mismatch") == why, "a job naming another era refuses the v3 pack");
|
||||
CHECK(pf_pack_class_ok("v2", "", "v3", EPOCH_33, why, sizeof(why)) == 0, "a job naming class v3 refuses a v2 pack");
|
||||
CHECK(pf_pack_class_ok("v2", "", "v2", EPOCH_33, why, sizeof(why)) == 1, "an era named on a v2 job is ignored");
|
||||
// Counter ASIC 3.0 (6 October 2026): a generator 4 pack is class v4 with its era seed; a job naming class v3
|
||||
// refuses it and a job naming class v4 refuses a v3 pack; the era rule applies to v4 as to v3
|
||||
snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_GENERATOR 4\n#define IGNEUM_PROGRAM_CLASS \"v4\"\n#define IGNEUM_ERA_SEED_HEX \"%s\"\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, EPOCH_33, sw, kw);
|
||||
write_file(dir, "program.h", text);
|
||||
err[0] = 0;
|
||||
CHECK(pf_load(dir, &pk, err, sizeof(err)) == 1 && strcmp(pk.programClass, "v4") == 0 && strcmp(pk.eraHex, EPOCH_33) == 0, "a generator 4 pack loads as class v4 with its era seed");
|
||||
CHECK(pf_pack_class_ok(pk.programClass, pk.eraHex, "v4", EPOCH_33, why, sizeof(why)) == 1, "the v4 pack matches a job naming class v4 and its era");
|
||||
CHECK(pf_pack_class_ok(pk.programClass, pk.eraHex, "v3", EPOCH_33, why, sizeof(why)) == 0 && strstr(why, "program class mismatch") == why, "a job naming class v3 refuses the v4 pack");
|
||||
CHECK(pf_pack_class_ok(pk.programClass, pk.eraHex, "v4", EPOCH_34, why, sizeof(why)) == 0 && strstr(why, "era seed mismatch") == why, "a job naming another era refuses the v4 pack");
|
||||
CHECK(pf_pack_class_ok("v3", EPOCH_33, "v4", EPOCH_33, why, sizeof(why)) == 0, "a job naming class v4 refuses a v3 pack");
|
||||
{
|
||||
char c[8] = {0}, e[65] = {0};
|
||||
CHECK(pf_class_token("class=v4", c, sizeof(c), e, sizeof(e)) == 1 && strcmp(c, "v4") == 0, "the class=v4 token is read");
|
||||
}
|
||||
snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_GENERATOR 4\n#define IGNEUM_PROGRAM_CLASS \"v3\"\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, sw, kw);
|
||||
write_file(dir, "program.h", text);
|
||||
err[0] = 0;
|
||||
CHECK(pf_load(dir, &pk, err, sizeof(err)) == 0 && strstr(err, "does not match IGNEUM_GENERATOR 4") != NULL, "a v3 class line on a generator 4 pack is refused");
|
||||
snprintf(text, sizeof(text), "#define IGNEUM_SEED_BYTES_HEX \"%s\"\n#define IGNEUM_DAY_BYTES_HEX \"%s\"\n#define IGNEUM_GENERATOR 3\n#define IGNEUM_PROGRAM_CLASS \"v2\"\n#define IGNEUM_DATASET_LOG2 28\n#define IGNEUM_DATASET_MODE 1\n#define IGNEUM_SEEDW_INIT { %s }\n#define IGNEUM_KEY_INIT { %s }\n#define IGNEUM_CACHE_LOG2_WORDS 26\n#define IGNEUM_CACHE_SEGMENTS 4096u\n", EPOCH_34, DAY_20731, sw, kw);
|
||||
write_file(dir, "program.h", text);
|
||||
err[0] = 0;
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ typedef struct {
|
|||
// read-width experiment (5 October 2026): the load class (0 when absent), bytes per hash, variant 5's scratch
|
||||
uint32_t loadsPerHash, bytesPerHash, scratchOps, persistent, scratchWordsPerLane;
|
||||
char loadClass[64];
|
||||
char programClass[8]; /* IGNEUM_PROGRAM_CLASS: "v2" or "v3" (Counter ASIC 2.0); absent = the generator's class */
|
||||
char eraHex[65]; /* IGNEUM_ERA_SEED_HEX of a class v3 chain pack; empty otherwise */
|
||||
char programClass[8]; /* IGNEUM_PROGRAM_CLASS: "v2", "v3" (Counter ASIC 2.0) or "v4" (Counter ASIC 3.0); absent = the generator's class */
|
||||
char eraHex[65]; /* IGNEUM_ERA_SEED_HEX of a class v3 or v4 chain pack; empty otherwise */
|
||||
// Counter ASIC 2.0 (5 October 2026): the mixer multiplier of the item derivation (IGNEUM_MIXER_MULT, 1 when absent:
|
||||
// version 2; 4 under class v3). The emitted memhard.h / kernel.cl carry it in their text; this is for the log lines.
|
||||
uint32_t mixerMult;
|
||||
|
|
@ -284,12 +284,14 @@ static int pf_load(const char* dir, PfPack* pk, char* err, size_t cap) {
|
|||
if (!pf_define_u32(prog, "IGNEUM_CACHE_SEGMENTS", &pk->cacheSegments)) { free(prog); return pf_fail(err, cap, "program.h has no IGNEUM_CACHE_SEGMENTS"); }
|
||||
if (!pf_define_u32(prog, "IGNEUM_GENERATOR", &pk->generator)) pk->generator = 1;
|
||||
/* Spec 01 section 1.4.5: a pack whose generator version is not one this worker runs is refused. Generator 2 is
|
||||
* program class v2 (the lottery hash of 4 October 2026), generator 3 is class v3 (Counter ASIC 2.0). */
|
||||
if (pk->generator != 2 && pk->generator != 3) {
|
||||
char m[200]; snprintf(m, sizeof(m), "program pack generator %u is not a generator version this worker runs (2 or 3)", (unsigned)pk->generator);
|
||||
* program class v2 (the lottery hash of 4 October 2026), generator 3 is class v3 (Counter ASIC 2.0), generator 4
|
||||
* is class v4 (Counter ASIC 3.0, 6 October 2026: class v3 plus the latency-shadow block, emitted in the pack's own
|
||||
* kernel text, so this loader needs nothing new beyond the number and the class token). */
|
||||
if (pk->generator != 2 && pk->generator != 3 && pk->generator != 4) {
|
||||
char m[200]; snprintf(m, sizeof(m), "program pack generator %u is not a generator version this worker runs (2, 3 or 4)", (unsigned)pk->generator);
|
||||
free(prog); return pf_fail(err, cap, m);
|
||||
}
|
||||
strcpy(pk->programClass, pk->generator == 3 ? "v3" : "v2");
|
||||
strcpy(pk->programClass, pk->generator == 4 ? "v4" : pk->generator == 3 ? "v3" : "v2");
|
||||
{
|
||||
char named[8] = {0};
|
||||
if (pf_define_str(prog, "IGNEUM_PROGRAM_CLASS", named, sizeof(named)) && strcmp(named, pk->programClass) != 0) {
|
||||
|
|
@ -429,16 +431,16 @@ static int pf_selftest(const PfPack* pk, const uint32_t* cacheHead, const uint32
|
|||
}
|
||||
|
||||
|
||||
/* Counter ASIC 2.0 (5 October 2026): a job or prepare line may end with `class=<v2|v3>` and `era=<hex>` tokens (sent
|
||||
* only when the chain is on class v3, so every v2 line is the line of before). A pack matches the line when its class
|
||||
* is the named class and, when an era is named, its era seed is that era. Empty wanted strings accept any pack.
|
||||
* Returns 1 on a match, else 0 with the reason in `why`. */
|
||||
/* Counter ASIC 2.0 (5 October 2026): a job or prepare line may end with `class=<v2|v3|v4>` and `era=<hex>` tokens (sent
|
||||
* only when the chain is on class v3 or v4, so every v2 line is the line of before). A pack matches the line when its
|
||||
* class is the named class and, when an era is named, its era seed is that era (every class after v2 carries one).
|
||||
* Empty wanted strings accept any pack. Returns 1 on a match, else 0 with the reason in `why`. */
|
||||
static int pf_pack_class_ok(const char* packClass, const char* packEra, const char* wantClass, const char* wantEra, char* why, size_t cap) {
|
||||
if (wantClass && wantClass[0] && strcmp(wantClass, packClass) != 0) {
|
||||
snprintf(why, cap, "program class mismatch: this pack is class %s, the job names class %s (export the pack again)", packClass, wantClass);
|
||||
return 0;
|
||||
}
|
||||
if (wantEra && wantEra[0] && strcmp(packClass, "v3") == 0) {
|
||||
if (wantEra && wantEra[0] && strcmp(packClass, "v2") != 0) {
|
||||
size_t i; int eq = strlen(packEra) == strlen(wantEra);
|
||||
for (i = 0; eq && packEra[i]; ++i) if (tolower((unsigned char)packEra[i]) != tolower((unsigned char)wantEra[i])) eq = 0;
|
||||
if (!eq) { snprintf(why, cap, "era seed mismatch: this pack was drawn under era %.16s, the job names era %.16s (export the pack again)", packEra[0] ? packEra : "(none)", wantEra); return 0; }
|
||||
|
|
|
|||
|
|
@ -2784,7 +2784,7 @@ final class ServeProgram {
|
|||
/// The Swift-generated program (class v2); nil for a program compiled from a pack (class v3, Counter ASIC 2.0),
|
||||
/// which has no Swift instruction list and so never races variants
|
||||
let program: Program?
|
||||
/// Counter ASIC 2.0: the class ("v2" or "v3") and, for a pack program, its era seed hex and loads per hash
|
||||
/// Counter ASIC 2.0: the class ("v2", "v3" or, Counter ASIC 3.0, "v4") and, for a pack program, its era seed hex and loads per hash
|
||||
let programClass: String
|
||||
let eraHex: String
|
||||
let loadsPerHash: Int
|
||||
|
|
@ -2804,7 +2804,7 @@ final class ServeProgram {
|
|||
/// Whether this program is the one a line naming `cls` (and `era`) wants; empty names accept any (class v2 lines).
|
||||
func matches(cls: String, era: String) -> Bool {
|
||||
if cls != "" && cls != programClass { return false }
|
||||
if era != "" && programClass == "v3" && era.lowercased() != eraHex.lowercased() { return false }
|
||||
if era != "" && isPackClass(programClass) && era.lowercased() != eraHex.lowercased() { return false }
|
||||
return true
|
||||
}
|
||||
var compiled: CompiledHash { lock.lock(); defer { lock.unlock() }; return slot }
|
||||
|
|
@ -2984,11 +2984,18 @@ final class ServeDataset {
|
|||
}
|
||||
func matches(cls: String, era: String) -> Bool {
|
||||
if cls != "" && cls != programClass { return false }
|
||||
if era != "" && programClass == "v3" && era.lowercased() != eraHex.lowercased() { return false }
|
||||
if era != "" && isPackClass(programClass) && era.lowercased() != eraHex.lowercased() { return false }
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/// Counter ASIC 2.0 and 3.0: the classes this worker runs from a prepared pack only (never from the Swift version 2
|
||||
/// generator): class v3 (generator 3) and class v4 (generator 4, class v3 plus the latency-shadow block, which is in
|
||||
/// the pack's own program_bound.metal). Each carries an era seed.
|
||||
func isPackClass(_ cls: String) -> Bool { cls == "v3" || cls == "v4" }
|
||||
/// The class name of a pack's IGNEUM_GENERATOR (packfile.h's rule).
|
||||
func packClassOf(generator: UInt32) -> String { generator == 4 ? "v4" : generator == 3 ? "v3" : "v2" }
|
||||
|
||||
// The resident programs and datasets, shared by the job loop (main thread) and the prepare queue (background).
|
||||
final class ServeStore {
|
||||
private let lock = NSLock()
|
||||
|
|
@ -3053,7 +3060,8 @@ func serveProgram(_ gpu: GPU, _ store: ServeStore, seedHex: String, seed: [UInt8
|
|||
|
||||
// Counter ASIC 2.0 (5 October 2026): a class v3 program comes from the pack igneum-miner export-pack / --prepare-packs
|
||||
// wrote (program_bound.metal, the Rust emitter's text; program.h for the identity), never from the Swift generator.
|
||||
// The pack is checked the way the one-click workers check it (proto-cuda/nvrtc/packfile.h): IGNEUM_GENERATOR 2 or 3,
|
||||
// Counter ASIC 3.0 (6 October 2026): a class v4 program (generator 4) the same way; its shadow block is in that text.
|
||||
// The pack is checked the way the one-click workers check it (proto-cuda/nvrtc/packfile.h): IGNEUM_GENERATOR 2, 3 or 4,
|
||||
// the class line against the generator, the seed bytes against the line's epoch seed, IGNEUM_SEEDW_INIT against the
|
||||
// words of IGNEUM_PROGRAM_ATTEMPT of that seed, and the class and era against what the line names.
|
||||
func servePackProgram(_ gpu: GPU, _ store: ServeStore, seedHex: String, seed: [UInt8], dir: String, wantClass: String, wantEra: String) throws -> (ServeProgram, Double) {
|
||||
|
|
@ -3077,12 +3085,12 @@ func servePackProgram(_ gpu: GPU, _ store: ServeStore, seedHex: String, seed: [U
|
|||
}
|
||||
func refuse(_ why: String) -> NSError { NSError(domain: "pack", code: 2, userInfo: [NSLocalizedDescriptionKey: "pack \(dir): \(why)"]) }
|
||||
let generator = defineU32("IGNEUM_GENERATOR") ?? 1
|
||||
guard generator == 2 || generator == 3 else { throw refuse("program pack generator \(generator) is not a generator version this worker runs (2 or 3)") }
|
||||
let packClass = generator == 3 ? "v3" : "v2"
|
||||
guard generator == 2 || generator == 3 || generator == 4 else { throw refuse("program pack generator \(generator) is not a generator version this worker runs (2, 3 or 4)") }
|
||||
let packClass = packClassOf(generator: generator)
|
||||
if let named = defineStr("IGNEUM_PROGRAM_CLASS"), named != packClass { throw refuse("program pack IGNEUM_PROGRAM_CLASS \"\(named)\" does not match IGNEUM_GENERATOR \(generator)") }
|
||||
let eraHex = defineStr("IGNEUM_ERA_SEED_HEX") ?? ""
|
||||
if wantClass != "" && wantClass != packClass { throw refuse("program class mismatch: this pack is class \(packClass), the line names class \(wantClass) (export the pack again)") }
|
||||
if wantEra != "" && packClass == "v3" && wantEra.lowercased() != eraHex.lowercased() { throw refuse("era seed mismatch: this pack was drawn under era \(eraHex.isEmpty ? "(none)" : String(eraHex.prefix(16))), the line names era \(wantEra.prefix(16)) (export the pack again)") }
|
||||
if wantEra != "" && isPackClass(packClass) && wantEra.lowercased() != eraHex.lowercased() { throw refuse("era seed mismatch: this pack was drawn under era \(eraHex.isEmpty ? "(none)" : String(eraHex.prefix(16))), the line names era \(wantEra.prefix(16)) (export the pack again)") }
|
||||
if let packSeed = defineStr("IGNEUM_SEED_BYTES_HEX"), packSeed.lowercased() != seedHex.lowercased() { throw refuse("the pack is for epoch \(packSeed.prefix(16)), not \(seedHex.prefix(16))") }
|
||||
let attempt = defineU32("IGNEUM_PROGRAM_ATTEMPT") ?? 0
|
||||
guard let seedw = defineWords("IGNEUM_SEEDW_INIT") else { throw refuse("program.h has no IGNEUM_SEEDW_INIT with 8 words") }
|
||||
|
|
@ -3128,11 +3136,11 @@ func servePackDataset(_ gpu: GPU, _ store: ServeStore, dayHex: String, dir: Stri
|
|||
return String(programH[Range(m.range(at: 1), in: programH)!])
|
||||
}
|
||||
let generator = defineU32("IGNEUM_GENERATOR") ?? 1
|
||||
let packClass = generator == 3 ? "v3" : "v2"
|
||||
let packClass = packClassOf(generator: generator)
|
||||
let eraHex = defineStr("IGNEUM_ERA_SEED_HEX") ?? ""
|
||||
if let packDay = defineStr("IGNEUM_DAY_BYTES_HEX"), packDay.lowercased() != dayHex.lowercased() { throw refuse("the pack's day is \(packDay), not \(dayHex)") }
|
||||
if wantClass != "" && wantClass != packClass { throw refuse("program class mismatch: this pack is class \(packClass), the line names class \(wantClass)") }
|
||||
if wantEra != "" && packClass == "v3" && wantEra.lowercased() != eraHex.lowercased() { throw refuse("era seed mismatch: the pack's era is \(eraHex.prefix(16)), the line names \(wantEra.prefix(16))") }
|
||||
if wantEra != "" && isPackClass(packClass) && wantEra.lowercased() != eraHex.lowercased() { throw refuse("era seed mismatch: the pack's era is \(eraHex.prefix(16)), the line names \(wantEra.prefix(16))") }
|
||||
if (defineU32("IGNEUM_HOT_MB") ?? 0) > 0 { throw refuse("a hot-table pack (IGNEUM_HOT_MB) is not served by this worker") }
|
||||
guard (defineU32("IGNEUM_DATASET_MODE") ?? 0) == 1 else { throw refuse("not a memory-hard pack (IGNEUM_DATASET_MODE 1)") }
|
||||
let datasetLog2 = Int(defineU32("IGNEUM_DATASET_LOG2") ?? 28)
|
||||
|
|
@ -3182,18 +3190,19 @@ func runServe(_ opts: Options) -> Never {
|
|||
if f.isEmpty { continue }
|
||||
if f[0] == "quit" { break }
|
||||
// Counter ASIC 2.0 (5 October 2026): a job or prepare line of a class v3 epoch ends with `class=v3 era=<hex>`
|
||||
// (a class v2 line is the line of before). A class v2 program is generated here from the seed (the Swift
|
||||
// version 2 generator); a class v3 program is compiled from the pack the prepare line names (servePackProgram),
|
||||
// so a v3 job on seeds with no prepared v3 pack asks for one (`need`) instead of mining the wrong program.
|
||||
// (a class v2 line is the line of before); Counter ASIC 3.0 (6 October 2026): a class v4 epoch's with
|
||||
// `class=v4 era=<hex>`. A class v2 program is generated here from the seed (the Swift version 2 generator); a
|
||||
// class v3 or v4 program is compiled from the pack the prepare line names (servePackProgram), so a v3 or v4
|
||||
// job on seeds with no prepared pack of that class asks for one (`need`) instead of mining the wrong program.
|
||||
var wantClass = "", wantEra = ""
|
||||
while let last = f.last, last.hasPrefix("class=") || last.hasPrefix("era=") {
|
||||
if last.hasPrefix("class=") { wantClass = String(last.dropFirst(6)) } else { wantEra = String(last.dropFirst(4)) }
|
||||
f.removeLast()
|
||||
}
|
||||
if wantClass != "" && wantClass != "v2" && wantClass != "v3" {
|
||||
if wantClass != "" && wantClass != "v2" && !isPackClass(wantClass) {
|
||||
let id = f.count > 1 ? f[1] : "0"
|
||||
if f[0] == "prepare" { emit("prepare-failed \(id) \(f.count > 2 ? f[2] : "0") program class \(wantClass) is not one this worker runs (v2 or v3)") }
|
||||
else { emit("error \(id) program class \(wantClass) is not one this worker runs (v2 or v3)") }
|
||||
if f[0] == "prepare" { emit("prepare-failed \(id) \(f.count > 2 ? f[2] : "0") program class \(wantClass) is not one this worker runs (v2, v3 or v4)") }
|
||||
else { emit("error \(id) program class \(wantClass) is not one this worker runs (v2, v3 or v4)") }
|
||||
continue
|
||||
}
|
||||
if f[0] == "prepare" {
|
||||
|
|
@ -3206,15 +3215,15 @@ func runServe(_ opts: Options) -> Never {
|
|||
let have = (store.program(epochHex, cls: wantClass, era: wantEra) != nil, store.dataset(dayHex, cls: wantClass, era: wantEra) != nil)
|
||||
if have.0 && have.1 { emit("prepared \(epochHex) \(dayHex) 0 program 0 dataset 0 (already resident)"); continue }
|
||||
let packDir = f.count > 3 ? f[3...].joined(separator: " ") : ""
|
||||
if wantClass == "v3" && packDir.isEmpty { emit("prepare-failed \(epochHex) \(dayHex) a class v3 program is compiled from a pack: send prepare with a pack directory (igneum-miner --prepare-packs <dir>)"); continue }
|
||||
if isPackClass(wantClass) && packDir.isEmpty { emit("prepare-failed \(epochHex) \(dayHex) a class \(wantClass) program is compiled from a pack: send prepare with a pack directory (igneum-miner --prepare-packs <dir>)"); continue }
|
||||
prepareQueue.async {
|
||||
let t0 = nowNs()
|
||||
do {
|
||||
let (sp, progMs) = wantClass == "v3"
|
||||
let (sp, progMs) = isPackClass(wantClass)
|
||||
? try servePackProgram(gpu, store, seedHex: epochHex, seed: epochSeed, dir: packDir, wantClass: wantClass, wantEra: wantEra)
|
||||
: try serveProgram(gpu, store, seedHex: epochHex, seed: epochSeed, datasetLog2: datasetLog2)
|
||||
// a class v3 day is built from the pack (its construction and layout), a v2 day by the Swift context
|
||||
let (sd, dsMs) = wantClass == "v3"
|
||||
// a class v3 or v4 day is built from the pack (its construction and layout), a v2 day by the Swift context
|
||||
let (sd, dsMs) = isPackClass(wantClass)
|
||||
? try servePackDataset(gpu, store, dayHex: dayHex, dir: packDir, wantClass: wantClass, wantEra: wantEra)
|
||||
: serveDataset(gpu, store, dayHex: dayHex, day: daySeed, datasetLog2: datasetLog2)
|
||||
// The race: the prepared program against its own dataset, exclusive windows between jobs
|
||||
|
|
@ -3226,7 +3235,7 @@ func runServe(_ opts: Options) -> Never {
|
|||
}
|
||||
let (np, nd) = store.counts()
|
||||
emit("prepared \(epochHex) \(dayHex) \(fmt(ms(t0, nowNs()), 1)) program \(fmt(progMs, 1)) dataset \(fmt(dsMs, 1)) race \(fmt(raceMs, 1)) variant \(sp.compiled.variant) class \(sp.programClass) loads/hash \(sp.loadsPerHash) cache-fill \(fmt(sd.cacheFillGPUms, 1)) build \(fmt(sd.buildGPUms, 1)) resident \(np) programs \(nd) datasets")
|
||||
} catch { emit("prepare-failed \(epochHex) \(dayHex) \(wantClass == "v3" ? "" : "Metal compile failed: ")\(error.localizedDescription)") }
|
||||
} catch { emit("prepare-failed \(epochHex) \(dayHex) \(isPackClass(wantClass) ? "" : "Metal compile failed: ")\(error.localizedDescription)") }
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
|
@ -3251,12 +3260,12 @@ func runServe(_ opts: Options) -> Never {
|
|||
if store.program(pair.0, cls: wantClass, era: wantEra) == nil || store.dataset(pair.1, cls: wantClass, era: wantEra) == nil { prepareQueue.sync {} }
|
||||
let sp: ServeProgram
|
||||
let sd: ServeDataset
|
||||
if wantClass == "v3" {
|
||||
// a v3 job runs on the pack program and the pack-built day of its class and era, both prepared; nothing
|
||||
// is compiled inline (the Swift generator and context are the version 2 construction)
|
||||
if isPackClass(wantClass) {
|
||||
// a v3 or v4 job runs on the pack program and the pack-built day of its class and era, both prepared;
|
||||
// nothing is compiled inline (the Swift generator and context are the version 2 construction)
|
||||
guard let resident = store.program(pair.0, cls: wantClass, era: wantEra), let day = store.dataset(pair.1, cls: wantClass, era: wantEra) else {
|
||||
emit("need \(pair.0) \(pair.1)")
|
||||
emit("error \(jobId) pack (metal): program class mismatch: no class v3 program and day for epoch \(pair.0.prefix(16)) day \(pair.1) is resident (this worker builds v3 from a pack); send prepare with a pack directory")
|
||||
emit("error \(jobId) pack (metal): program class mismatch: no class \(wantClass) program and day for epoch \(pair.0.prefix(16)) day \(pair.1) is resident (this worker builds \(wantClass) from a pack); send prepare with a pack directory")
|
||||
continue
|
||||
}
|
||||
sp = resident; sd = day
|
||||
|
|
|
|||
29
tools/ci/override-json-check.sh
Executable file
29
tools/ci/override-json-check.sh
Executable file
|
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/env bash
|
||||
# The duplicate-field class (6 October 2026): two merges each appended the proving v1 block to
|
||||
# infra/fast-time/override-60x.json, so the file carried `proving_v1_activation_daa` twice; the node refuses such a file
|
||||
# ("duplicate field") and the class-v3 and class-v4 fast-time gates could not start a node from it, while the fork's
|
||||
# every-field test skips when the file is not beside it (it is not on the PCs). Rule: every override params file the
|
||||
# harnesses and the node read parses as JSON with no duplicate key. This check fails CI when one does not.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/../.."
|
||||
fail=0
|
||||
while IFS= read -r f; do
|
||||
if ! python3 - "$f" <<'PY'
|
||||
import json, sys, collections
|
||||
def pairs(p):
|
||||
c = collections.Counter(k for k, _ in p)
|
||||
dups = sorted(k for k, n in c.items() if n > 1)
|
||||
if dups:
|
||||
raise SystemExit(f"duplicate key(s) {dups}")
|
||||
return dict(p)
|
||||
try:
|
||||
json.load(open(sys.argv[1]), object_pairs_hook=pairs)
|
||||
except SystemExit as e:
|
||||
print(f"override-json: {sys.argv[1]}: {e}"); sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"override-json: {sys.argv[1]}: does not parse: {e}"); sys.exit(1)
|
||||
PY
|
||||
then fail=1; fi
|
||||
done < <(git ls-files 'infra/fast-time/*.json' 'tools/finality-attacks/redteam/*.json' 'infra/**/override*.json' | sort -u)
|
||||
[ "$fail" = 0 ] && echo "override-json: every override params file parses with no duplicate key"
|
||||
exit $fail
|
||||
Loading…
Reference in a new issue