Site: live chain scene with miner sparks, shard fills and lock ripples; OS and GitHub logos

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-josh 2026-10-03 16:59:18 +01:00
parent 945b329dd2
commit 8bb80a7e80
6 changed files with 282 additions and 67 deletions

View file

@ -22,6 +22,14 @@
#include "program.h"
#include "vectors.h"
// Packs written before 3 October 2026 have no dataset mode: they are closed-form (mode 0).
#ifndef IGNEUM_DATASET_MODE
#define IGNEUM_DATASET_MODE 0
#endif
#if IGNEUM_DATASET_MODE == 1
#include "memhard.h" // the memory-hard core, compiled here for the host reference (see proto-metal/MEMHARD.md)
#endif
#define CUDA_CHECK(call) do { cudaError_t err_ = (call); if (err_ != cudaSuccess) { \
std::fprintf(stderr, "CUDA error: %s (%d)\n at %s:%d\n in %s\n", cudaGetErrorString(err_), (int)err_, __FILE__, __LINE__, #call); \
std::exit(2); } } while (0)
@ -38,6 +46,80 @@ static uint32_t host_ds_elem(uint32_t i, uint32_t d0, uint32_t d1) {
return x;
}
static double wallMs();
static uint64_t fnv1a64(const void* p, size_t n) {
const uint8_t* b = (const uint8_t*)p;
uint64_t h = 0xcbf29ce484222325ull;
for (size_t i = 0; i < n; ++i) { h ^= b[i]; h *= 0x100000001b3ull; }
return h;
}
#if IGNEUM_DATASET_MODE == 1
// Memory-hard mode: the 256 MiB cache on the device (filled by the pack's kernel) and on the host (filled by the
// same mh_cache_segment text on one thread). Both are built once per process in setupCache(), compared word for
// word, and checked against the head, last line and FNV-1a 64 the Mac recorded in vectors.h.
static const uint32_t CACHE_WORDS_HOST = 1u << IGNEUM_CACHE_LOG2_WORDS;
static uint32_t* gCache = nullptr; // device
static std::vector<uint32_t> hCache; // host
static double gCacheFillFirstMs = 0, gCacheFillSecondMs = 0, gCacheHostMs = 0;
static bool gCachePass = false;
static bool setupCache() {
size_t bytes = (size_t)CACHE_WORDS_HOST * 4u;
CUDA_CHECK(cudaMalloc((void**)&gCache, bytes));
cudaEvent_t e0, e1;
CUDA_CHECK(cudaEventCreate(&e0));
CUDA_CHECK(cudaEventCreate(&e1));
for (int pass = 0; pass < 2; ++pass) {
CUDA_CHECK(cudaEventRecord(e0));
CUDA_CHECK(igneum_launch_cache_fill(gCache, IGNEUM_CACHE_SEGMENTS));
CUDA_CHECK(cudaEventRecord(e1));
CUDA_CHECK(cudaEventSynchronize(e1));
float msf = 0.f;
CUDA_CHECK(cudaEventElapsedTime(&msf, e0, e1));
if (pass == 0) gCacheFillFirstMs = msf; else gCacheFillSecondMs = msf;
}
CUDA_CHECK(cudaEventDestroy(e0));
CUDA_CHECK(cudaEventDestroy(e1));
std::printf("cache fill (GPU): %.2f ms first, %.2f ms second (%u chains x %u ChaCha blocks, %u MiB)\n",
gCacheFillFirstMs, gCacheFillSecondMs, (unsigned)IGNEUM_CACHE_SEGMENTS,
1u << IGNEUM_CACHE_SEGMENT_LOG2_LINES, (unsigned)(bytes >> 20));
hCache.assign(CACHE_WORDS_HOST, 0u);
double h0 = wallMs();
for (uint32_t seg = 0; seg < IGNEUM_CACHE_SEGMENTS; ++seg) mh_cache_segment(hCache.data(), seg);
gCacheHostMs = wallMs() - h0;
std::printf("cache fill (host, one thread): %.1f ms\n", gCacheHostMs);
std::vector<uint32_t> dev(CACHE_WORDS_HOST);
CUDA_CHECK(cudaMemcpy(dev.data(), gCache, bytes, cudaMemcpyDeviceToHost));
bool same = std::memcmp(dev.data(), hCache.data(), bytes) == 0;
uint64_t fnv = fnv1a64(hCache.data(), bytes);
bool fnvOk = (fnv == IGNEUM_CACHE_FNV64);
bool headOk = std::memcmp(hCache.data(), IGNEUM_CACHE_HEAD, 64) == 0;
bool lastOk = std::memcmp(hCache.data() + CACHE_WORDS_HOST - 16u, IGNEUM_CACHE_LAST, 64) == 0;
if (!same) {
for (uint32_t i = 0; i < CACHE_WORDS_HOST; ++i) if (dev[i] != hCache[i]) {
std::printf(" cache[%u]: gpu 0x%08x host 0x%08x (first difference)\n", i, dev[i], hCache[i]); break;
}
}
gCachePass = same && fnvOk && headOk && lastOk;
std::printf("cache check: %s (GPU == host all %u words %s, host FNV-1a 64 %016llx vs Mac %016llx %s, head 16 vs Mac %s, last line vs Mac %s)\n",
gCachePass ? "PASS" : "FAIL", CACHE_WORDS_HOST, same ? "PASS" : "FAIL",
(unsigned long long)fnv, (unsigned long long)IGNEUM_CACHE_FNV64, fnvOk ? "PASS" : "FAIL",
headOk ? "PASS" : "FAIL", lastOk ? "PASS" : "FAIL");
return gCachePass;
}
// dataset[w] derived on the host from the host cache, exactly as proto-metal's verifier does it.
static uint32_t host_ds_word(uint32_t w) {
uint32_t s[16];
mh_item(hCache.data(), w >> 4u, s);
return s[w & 15u];
}
#endif
// ---------------------------------------------------------------------------------------------
// Options
@ -150,20 +232,31 @@ static SizeResult runSize(const Options& o, int mib, uint64_t* dOut, uint32_t no
CUDA_CHECK(cudaEventCreate(&e0));
CUDA_CHECK(cudaEventCreate(&e1));
// Fill twice: the Mac showed a first-touch cost on the first fill of a process.
// Fill (or build) twice: the Mac showed a first-touch cost on the first fill of a process.
for (int pass = 0; pass < 2; ++pass) {
CUDA_CHECK(cudaEventRecord(e0));
#if IGNEUM_DATASET_MODE == 1
CUDA_CHECK(igneum_launch_build(dDs, gCache, r.words / 16u));
#else
CUDA_CHECK(igneum_launch_fill(dDs, r.words, IGNEUM_DAY0, IGNEUM_DAY1));
#endif
CUDA_CHECK(cudaEventRecord(e1));
CUDA_CHECK(cudaEventSynchronize(e1));
float msf = 0.f;
CUDA_CHECK(cudaEventElapsedTime(&msf, e0, e1));
if (pass == 0) r.fillFirstMs = msf; else r.fillSecondMs = msf;
}
#if IGNEUM_DATASET_MODE == 1
std::printf("dataset build (memory-hard, from the cache): %.2f ms first, %.2f ms second -> %.1f M items/s, %.2f G cache-line reads/s (second, GPU time)\n",
r.fillFirstMs, r.fillSecondMs, (double)(r.words / 16u) / 1e6 / (r.fillSecondMs / 1000.0),
(double)(r.words / 16u) * (double)IGNEUM_ITEM_ROUNDS / 1e9 / (r.fillSecondMs / 1000.0));
#else
std::printf("dataset fill: %.2f ms first, %.2f ms second -> %.0f GB/s write (second, GPU time)\n",
r.fillFirstMs, r.fillSecondMs, (double)bytes / 1e9 / (r.fillSecondMs / 1000.0));
#endif
// Dataset self-test: head 16 (any size), element [MASK] (pack size only), 64 pseudo-random points vs host formula.
// Dataset self-test: head 16 (any size), element [MASK] (pack size only), 64 pseudo-random points vs the host
// formula (closed form) or the host derivation from the host cache (memory-hard), and the Mac's 64 sampled words.
{
uint32_t head[16];
CUDA_CHECK(cudaMemcpy(head, dDs, sizeof(head), cudaMemcpyDeviceToHost));

View file

@ -1635,21 +1635,27 @@ func regexCount(_ pattern: String, in text: String) -> Int {
// Static check: every dataset access in the generated MSL is `dataset[rN & MASK]`, and the identifier
// `dataset` appears nowhere else except the kernel parameter.
// A wide load (lever b) is `dataset[(simd_broadcast(rN, 0) & WMASK) + lane]` with WMASK = MASK & ~31 and lane < 32,
// so its index is at most MASK as well.
func maskCheckMSL(_ msl: String) -> (ok: Bool, detail: String) {
let total = regexCount("dataset\\[", in: msl)
let masked = regexCount("dataset\\[r[0-7] & MASK\\]", in: msl)
let wide = regexCount("dataset\\[\\(simd_broadcast\\(r[0-7], 0\\) & WMASK\\) \\+ lane\\]", in: msl)
let words = regexCount("\\bdataset\\b", in: msl)
let ok = total == masked && words == total + 1
return (ok, "MSL: \(total) dataset[ accesses, \(masked) of the form dataset[rN & MASK], identifier appears \(words) times (expected \(total + 1))")
let ok = total == masked + wide && words == total + 1
return (ok, "MSL: \(total) dataset[ accesses, \(masked) of the form dataset[rN & MASK], \(wide) wide loads dataset[(simd_broadcast(rN, 0) & WMASK) + lane], identifier appears \(words) times (expected \(total + 1))")
}
// Same for the CUDA twin: hash accesses are `ds[rN & mask]`; the fill kernel's one write is guarded by `if (i < n)`.
func maskCheckCUDA(_ cu: String) -> (ok: Bool, detail: String) {
// The closed-form pack has one fill write guarded by `if (i < n)`; the memory-hard pack writes the dataset through a
// `d` pointer in igneum_build and has no `ds[` write at all.
func maskCheckCUDA(_ cu: String, memhard: Bool) -> (ok: Bool, detail: String) {
let total = regexCount("\\bds\\[", in: cu)
let masked = regexCount("\\bds\\[r[0-7] & mask\\]", in: cu)
let wide = regexCount("\\bds\\[\\(__shfl_sync\\(0xffffffffu, r[0-7], 0\\) & wmask\\) \\+ lane\\]", in: cu)
let fill = regexCount("if \\(i < n\\) ds\\[i\\] = ds_elem", in: cu)
let ok = total == masked + fill && fill == 1
return (ok, "CUDA: \(total) ds[ accesses, \(masked) of the form ds[rN & mask], \(fill) guarded fill write")
let ok = total == masked + wide + fill && fill == (memhard ? 0 : 1)
return (ok, "CUDA: \(total) ds[ accesses, \(masked) of the form ds[rN & mask], \(wide) wide, \(fill) guarded fill write (expected \(memhard ? 0 : 1))")
}
// MARK: - --fuzz
@ -1705,7 +1711,6 @@ func runFuzz(_ opts: Options, ctx: DatasetContext) -> Bool {
fail += 1; print("GPU RUN FAIL seed \"\(seedString)\" dataset 2^\(log2)"); continue
}
let g1 = nowNs()
let mask = UInt32((1 << log2) - 1)
let ds = ctx.source(log2: log2)
var ok = true
for (w, base) in bases.enumerated() {
@ -1925,7 +1930,6 @@ func popcount64(_ v: UInt64) -> Int { v.nonzeroBitCount }
func runStats(_ opts: Options, ctx: DatasetContext) -> Bool {
let gpu = ctx.gpu
let log2 = opts.datasetLog2
let mask = UInt32((1 << log2) - 1)
let ds = ctx.source(log2: log2)
let n = 1 << 20
print("\n=== output statistics, 2^20 consecutive nonces per seed, dataset 2^\(log2) words ===")
@ -2120,10 +2124,10 @@ func runDeterminism(_ opts: Options, ctx: DatasetContext) -> Bool {
var fillFps = [UInt64]()
var sampleBad = 0
if let shared = gpu.device.makeBuffer(length: words * 4, options: .storageModeShared) {
for (idx, ds) in [dataset, dataset2].enumerated() {
for (idx, dsBuf) in [dataset, dataset2].enumerated() {
let cb = gpu.queue.makeCommandBuffer()!
let blit = cb.makeBlitCommandEncoder()!
blit.copy(from: ds, sourceOffset: 0, to: shared, destinationOffset: 0, size: words * 4)
blit.copy(from: dsBuf, sourceOffset: 0, to: shared, destinationOffset: 0, size: words * 4)
blit.endEncoding()
cb.commit(); cb.waitUntilCompleted()
fillFps.append(fnv64(shared.contents(), words * 4))
@ -2160,7 +2164,7 @@ func runMemcheck(_ opts: Options, ctx: DatasetContext) -> Bool {
if !r.ok { ok = false }
print("static 2^\(log2): \(r.ok ? "PASS" : "FAIL") \(r.detail)")
}
let cu = maskCheckCUDA(generateCUDA(program))
let cu = maskCheckCUDA(generateCUDA(program, memhard: ctx.mp), memhard: !ctx.closed)
if !cu.ok { ok = false }
print("static CUDA twin: \(cu.ok ? "PASS" : "FAIL") \(cu.detail)")
print("program has \(program.instrs.filter { $0.op == .load }.count) load instructions (\(program.loadsPerHash) loads/hash)")

View file

@ -39,3 +39,40 @@ the first full day after the burst, the doubling or the churn.
Model limits are listed at the end of `results.md`: no latency, no DAG, no VRF sampling noise,
instant difficulty retarget, free keys.
## finality_v2.py
Simulates FINALITY RULE V2 (CLAUDE.md, review round 2): flat 30-day weight window, no damping,
dust threshold 100 blocks, a checkpoint every 30 blocks, every voter signs every checkpoint,
lock at 2/3 of the ACTIVE denominator (weight x participation over a 240-checkpoint presence
window) compared against the TOTAL denominator. Unlike `finality_sim.py` it steps one 30-s slot
at a time and models regions with message delay, uptime, partitions (each side forms its own
checkpoints and certificates, merged at the heal with conflicting locks counted), an eclipsed
pool, and equivocation stripping. The DAG stays abstract: every block is blue, a side's
checkpoint block at index i is the block at blue score 30i in that side's view.
Three readings of "participation" are selectable with `--pmode` inside the script (the scenarios
run all three where it matters): `cert` (the brief, an uncertified index credits nobody),
`seen` (an uncertified index credits the keys whose votes were observed), `frozen` (the window
is over certified indices only). Scenarios A to G and every assumption are in `results_v2.md`.
Requirements: Python 3, numpy (3.10.10, numpy 2.2.6 on 3 October 2026).
Run everything (about four minutes):
python3 finality_v2.py > out_v2.md
Options:
--seed N random seed, default 7
--scenarios A,E subset of A,B,C,D,E,F,G
--delay 2.0 one-way inter-region delay in seconds for the main runs (A also sweeps 0.5, 2, 5)
--grace 15 seconds after a checkpoint during which late votes still enter the certificate
--quick shortened runs for development
The runs behind `results_v2.md`:
python3 finality_v2.py
python3 finality_v2.py --seed 11 --scenarios B,E
Timing output goes to stderr, tables to stdout. Minutes in C to F are wall minutes after the event.

View file

@ -89,10 +89,19 @@ class P:
self.presence = 240 # checkpoints in the participation window
self.dust = 100 # blocks
self.quorum = TWO_THIRDS
self.floor = 0.0 # hybrid: active denominator never below floor x total weight (0 = off)
self.daa = "none" # "none": a partition side mines at its hashrate share; "full": each side retargets to 30 blocks/slot at once
self.__dict__.update(kw)
def label(self):
return self.denom if self.denom == "total" else "active/" + self.pmode
if self.denom == "total":
return "total"
s = "active/" + self.pmode
if self.floor > 0:
s += "+floor%.2f" % self.floor
if self.daa != "none":
s += "+daa"
return s
class View:
@ -257,7 +266,14 @@ class Sim:
p = self.p
slot = self.slot
tot = self.hash.sum()
blocks = self.rng.poisson(BLOCKS_PER_CP * self.hash / tot) if tot > 0 else np.zeros(self.N)
if p.daa == "full" and len(self.views) > 1:
blocks = np.zeros(self.N)
for v in self.views:
side_tot = self.hash[v.mine_mask].sum()
if side_tot > 0:
blocks[v.mine_mask] = self.rng.poisson(BLOCKS_PER_CP * self.hash[v.mine_mask] / side_tot)
else:
blocks = self.rng.poisson(BLOCKS_PER_CP * self.hash / tot) if tot > 0 else np.zeros(self.N)
hp = (slot // SLOTS_PER_HOUR) % WINDOW_HOURS
if slot % SLOTS_PER_HOUR == 0:
self.weight -= self.buckets[hp]
@ -306,6 +322,8 @@ class Sim:
else:
denom = float(self.weight[elig].sum())
total = float(self.weight[elig].sum())
if p.denom == "active" and p.floor > 0:
denom = max(denom, p.floor * total)
need = p.quorum * denom
vi = np.flatnonzero(voters)
signed_w = float(self.weight[vi].sum())
@ -633,23 +651,35 @@ def scenario_b(args):
def scenario_c(args):
out = ["### C. Silent set: a random set holding x of weight stops signing at day 60 and keeps mining", ""]
out = ["### C. Silent set: a random set holding x of weight stops signing at day 60 (hour 3 of the run) and keeps mining", ""]
fracs = (0.34, 0.40, 0.45, 0.50, 0.55)
configs = [("active", "cert", args.hours_c), ("active", "seen", args.hours_c), ("active", "frozen", args.hours_c),
("total", "cert", args.hours_c_total)]
configs = [("active", "cert", args.hours_c, 0.0, 240), ("active", "seen", args.hours_c, 0.0, 240),
("active", "frozen", args.hours_c, 0.0, 240), ("active", "cert", args.hours_c_total, 0.0, 2880),
("active", "cert", args.hours_c_total, 0.8, 240), ("total", "cert", args.hours_c_total, 0.0, 240)]
labels = []
for denom, pmode, hours, floor, presence in configs:
lab = P(denom=denom, pmode=pmode, floor=floor, presence=presence).label()
if presence != 240:
lab += ", presence %d" % presence
labels.append(lab)
rows = []
detail = []
for frac in fracs:
row = [pct(frac, 0)]
for denom, pmode, hours in configs:
p = P(denom=denom, pmode=pmode, delay=args.delay)
for denom, pmode, hours, floor, presence in configs:
p = P(denom=denom, pmode=pmode, delay=args.delay, floor=floor, presence=presence)
sim, rng, _ = build_honest(p, args.seed)
sim.warm_start()
sim.init_views(warm=True)
sim.run(SLOTS_PER_HOUR)
sim.run(3 * SLOTS_PER_HOUR)
silent, got = pick_weight_subset(rng, sim.weight, frac)
sim.signs[silent] = False
t_event = sim.slot
v0 = sim.views[0]
e0 = sim.elig_mask()
s_on = float(sim.weight[e0 & sim.online & ~silent].sum() / sim.weight[e0].sum())
p0 = float((sim.weight * sim.participation(v0, v0.next_idx))[e0].sum() / sim.weight[e0].sum())
predicted = max(0, int(round(p.presence * (p0 - 1.5 * s_on))))
sim.run(int(hours * SLOTS_PER_HOUR))
recs = sim.recs()
fl = first_lock_after(recs, t_event)
@ -661,37 +691,39 @@ def scenario_c(args):
later = stalls_between(recs, fl, sim.slot) if fl is not None else st
fl_txt = "never (%s)" % fmt_min(sim.slot - t_event) if fl is None else fmt_min(fl - t_event)
row.append("%s, %d stalled" % (fl_txt, st))
if denom == "active" and pmode == "cert":
if denom == "active" and pmode == "cert" and floor == 0 and presence == 240:
tail = recs[(recs[:, 0] >= sim.slot - SLOTS_PER_HOUR) & (recs[:, 3] >= 0)]
detail.append([pct(frac, 0), pct(got), int(silent.sum()), fl_txt, st, later, "%.3f" % sil_part,
detail.append([pct(frac, 0), pct(got), int(silent.sum()), pct(s_on), "%.3f" % p0, predicted, fl_txt, st, later, "%.3f" % sil_part,
"%.3f" % (np.median(tail[:, 4]) if tail.shape[0] else float("nan")),
"%.3f" % (np.median(tail[:, 5]) if tail.shape[0] else float("nan"))])
rows.append(row)
out.append("Time from the event to the first lock, and checkpoints stalled in the run (%d h active runs, %d h total run):" % (
out.append("Time from the event to the first lock, and checkpoints stalled in the run (%d h for the first three columns, %d h for the rest):" % (
args.hours_c, args.hours_c_total))
out.append("")
out.append(md_table(["silent weight"] + ["%s/%s" % (d, m) if d == "active" else d for d, m, _ in configs], rows))
out.append(md_table(["silent weight"] + labels, rows))
out.append("")
out.append("Active/cert detail:")
out.append("Active/cert detail. Predicted stalls = presence x (p0 - 1.5 x online signing share), where p0 is the weight-averaged participation at the event:")
out.append("")
out.append(md_table(["silent weight", "picked", "keys", "first lock", "stalled", "stalled after first lock",
out.append(md_table(["silent weight", "picked", "keys", "online signing share at event", "p0", "predicted stalls", "first lock", "stalled", "stalled after first lock",
"silent participation at end", "signed/denominator at end (median, last hour)",
"active/total at end"], detail))
return "\n".join(out)
def scenario_d(args):
out = ["### D. Churn: a random set holding x of weight stops mining and signing at day 60", ""]
out = ["### D. Churn: a random set holding x of weight stops mining and signing at day 60 (hour 3 of the run)", ""]
fracs = (0.35, 0.50)
rows = []
dconfigs = [P(denom="active", delay=args.delay), P(denom="active", presence=2880, delay=args.delay),
P(denom="active", floor=0.8, delay=args.delay), P(denom="total", delay=args.delay)]
for frac in fracs:
for denom in ("active", "total"):
for p in dconfigs:
days = args.days_d35 if frac < 0.4 else args.days_d50
p = P(denom=denom, delay=args.delay)
denom = p.label() + (", presence 2880" if p.presence == 2880 else "")
sim, rng, _ = build_honest(p, args.seed)
sim.warm_start()
sim.init_views(warm=True)
sim.run(SLOTS_PER_HOUR)
sim.run(3 * SLOTS_PER_HOUR)
gone, got = pick_weight_subset(rng, sim.weight, frac)
sim.signs[gone] = False
sim.online[gone] = False
@ -780,7 +812,7 @@ def scenario_e(args):
rows_conf.append(rc)
rows_first.append(rf)
labels = [p.label() for p in configs]
out.append("Conflicting locks (two certificates at one index, different blocks). Attacker = equivocating key holding the stated share of total weight, honest weight split as stated. Pass needs 0 at 0%% attacker for 30, 90 and 150 min.")
out.append("Conflicting locks (two certificates at one index, different blocks). Attacker = equivocating key holding the stated share of total weight, honest weight split as stated. Pass needs 0 at 0% attacker for 30, 90 and 150 min.")
out.append("")
out.append(md_table(["honest split", "attacker", "partition min"] + labels, rows_conf))
out.append("")
@ -788,18 +820,25 @@ def scenario_e(args):
out.append("")
out.append(md_table(["honest split", "attacker", "partition min"] + labels, rows_first))
out.append("")
# supplementary lopsided splits at 0% attacker, 150 min
# supplementary: more splits, 0% attacker, plus the DAA-retarget and floor variants
configs2 = configs + [P(denom="active", pmode="cert", daa="full", delay=args.delay),
P(denom="active", pmode="seen", daa="full", delay=args.delay),
P(denom="active", pmode="cert", floor=0.8, daa="full", delay=args.delay)]
labels2 = [p.label() for p in configs2]
rows = []
for name, fr in (("60/40", [0.6, 0.4]), ("67/33", [0.67, 0.33]), ("80/20", [0.8, 0.2])):
for name, fr in (("50/50", [0.5, 0.5]), ("60/40", [0.6, 0.4]), ("67/33", [0.67, 0.33]), ("80/20", [0.8, 0.2]),
("33/33/34", [0.33, 0.33, 0.34])):
for dur in (150, 360):
rc = [name, dur]
for p in configs:
for p in configs2:
r = run_partition(args.seed, fr, 0.0, dur, p, post_min=120)
rc.append("%d conflicts; first locks %s min" % (r["conflicts"], " / ".join(fm(x) for x in r["side_first_lock"])))
rc.append("%d; %s" % (r["conflicts"], " / ".join(fm(x) for x in r["side_first_lock"])))
rows.append(rc)
out.append("Supplementary, lopsided honest splits, 0% attacker:")
out.append("Supplementary, 0% attacker, 150 and 360 min. Cell = conflicting locks; minutes to each side's first lock. "
"'+daa' = each side retargets to 1 block/s at once (worst case for the presence clock); "
"'+floor0.80' = active denominator never below 80% of total weight (a lock needs at least 53.3% of total).")
out.append("")
out.append(md_table(["honest split", "partition min"] + labels, rows))
out.append(md_table(["honest split", "partition min"] + labels2, rows))
out.append("")
# presence window sweep, active/cert, 0% attacker
rows = []
@ -937,8 +976,10 @@ def scenario_g(args):
rows_lat.append(lat_row("%s, day before" % denom, lat_stats(recs, 0, t_event)))
rows_lat.append(lat_row("%s, days 1 to 5 after" % denom, lat_stats(recs, t_event, t_event + 5 * SLOTS_PER_DAY)))
rows_lat.append(lat_row("%s, days 5 to %d after" % (denom, args.days_g), lat_stats(recs, t_event + 5 * SLOTS_PER_DAY, None)))
ev.append([denom, last_23, next((d for d, so, sn, _ in series if sn >= 0.45), None),
next((d for d, so, sn, _ in series if sn >= 0.49), None), stalls_between(recs, t_event, sim.slot)])
r45 = next((d for d, so, sn, _ in series if sn >= 0.45), None)
r49 = next((d for d, so, sn, _ in series if sn >= 0.49), None)
ev.append([denom, last_23, "not in run" if r45 is None else r45, "not in run" if r49 is None else r49,
stalls_between(recs, t_event, sim.slot)])
out.append("Weight shares (active run):")
out.append("")
out.append(md_table(["day after doubling", "old cohort", "new cohort", "new cohort formula t/60", "new keys under dust"], rows_share))

View file

@ -94,6 +94,11 @@ section{padding-block:clamp(56px,8vw,96px)}
.sec-head p{font-size:16px;color:var(--ash);max-width:46ch}
/* dag */
.viz{padding:clamp(16px,3vw,28px);overflow:hidden}
.viz-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:10px 20px;margin-bottom:12px}
.viz-stats{display:flex;gap:18px;font-size:12px;color:var(--ash)}
.viz-stats b{color:var(--bone);font-weight:500;font-variant-numeric:tabular-nums}
#chain{display:block;width:100%;height:clamp(220px,36vw,340px);border-radius:12px;background:var(--obsidian)}
.dag{padding:clamp(18px,3vw,32px);overflow:hidden}
.dag-rows{display:flex;flex-direction:column;gap:var(--dg,14px);--bs:34px;--dg:14px}
@media (max-width:640px){.dag-rows{--bs:26px;--dg:9px}}
@ -234,13 +239,17 @@ footer .fl{display:flex;flex-wrap:wrap;gap:20px}
<h2>Watch the chain prove itself</h2>
<p>Blocks arrive every second. Miners prove them in shards. A checkpoint locks every 30 seconds. All of it will be on this page, live.</p>
</div>
<div class="card dag reveal" aria-label="Animated block DAG">
<div class="dag-rows" id="dag"></div>
<div class="card viz reveal" aria-label="Animated block DAG, simulated preview">
<div class="viz-head">
<div class="eyebrow"><span class="dot" style="background:var(--ember);margin-right:8px"></span>simulated preview · how testnet will look</div>
<div class="viz-stats mono"><span>blocks <b id="c-blocks">0</b></span><span>proven <b id="c-proven">0</b></span><span>locked <b id="c-locked">0</b></span></div>
</div>
<canvas id="chain" aria-hidden="true"></canvas>
<div class="legend">
<span><i class="sw" style="border:2px solid var(--line-2)"></i>mined</span>
<span><i class="sw" style="background:var(--molten)"></i>shards being proven</span>
<span><i class="sw" style="background:var(--ember)"></i>proven</span>
<span><i class="sw" style="background:var(--molten)"></i>being proven by miners</span>
<span><i class="sw" style="border:2px solid var(--line-2)"></i>just mined</span>
<span><i class="sw" style="border:2px solid var(--ember)"></i>checkpoint locked by sustained miners</span>
<span><i class="sw" style="border:2px solid var(--ember);border-radius:50%"></i>checkpoint locked by sustained miners</span>
</div>
</div>
<div class="grid" style="margin-top:24px">
@ -326,10 +335,10 @@ footer .fl{display:flex;flex-wrap:wrap;gap:20px}
<div class="card reveal" style="display:flex;flex-direction:column;gap:10px"><div class="eyebrow">03</div><h3 style="font-size:20px">Proving for others</h3><p style="font-size:15px;color:#3D3B37">Rollups and bridges pay for proofs in their own money. Small market today, growing with every chain that moves to proofs. Upside, not a promise.</p></div>
</div>
<div class="cta reveal" style="margin-top:32px;align-items:center">
<a href="#journey" class="btn dark">Windows</a>
<a href="#journey" class="btn dark">macOS</a>
<a href="#journey" class="btn dark">Linux</a>
<a href="#journey" class="btn" style="color:#0C0C0E;border-color:#0C0C0E">HiveOS</a>
<a href="#journey" class="btn dark"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M3 5.5l7.5-1v7H3zM11.5 4.3L21 3v8.5h-9.5zM3 12.5h7.5v7L3 18.5zM11.5 12.5H21V21l-9.5-1.3z"/></svg>Windows</a>
<a href="#journey" class="btn dark"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M16.4 12.6c0-2.3 1.9-3.4 2-3.5-1.1-1.6-2.8-1.8-3.4-1.8-1.4-.1-2.8.8-3.5.8-.7 0-1.8-.8-3-.8-1.5 0-3 .9-3.8 2.3-1.6 2.8-.4 7 1.2 9.3.8 1.1 1.7 2.4 2.9 2.3 1.2 0 1.6-.7 3-.7s1.8.7 3 .7c1.3 0 2-1.1 2.8-2.3.9-1.3 1.2-2.6 1.3-2.6-.1 0-2.5-.9-2.5-3.7zM14.1 5.8c.6-.8 1.1-1.9.9-3-.9 0-2 .6-2.7 1.4-.6.7-1.1 1.8-1 2.9 1.1.1 2.1-.5 2.8-1.3z"/></svg>macOS</a>
<a href="#journey" class="btn dark"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M12 2c-2.4 0-4 1.9-4 4.6 0 1.2.2 2 0 2.8-.6 1.4-2.1 2.9-2.6 4.9-.3 1.1-.1 2 .3 2.6-.6.4-1.3 1-1.1 1.7.3 1 2.1 1.2 3.2 1.8.7.4 1.5.6 2.1.1.6.2 1.3.3 2.1.3s1.5-.1 2.1-.3c.6.5 1.4.3 2.1-.1 1.1-.6 2.9-.8 3.2-1.8.2-.7-.5-1.3-1.1-1.7.4-.6.6-1.5.3-2.6-.5-2-2-3.5-2.6-4.9-.2-.8 0-1.6 0-2.8C16 3.9 14.4 2 12 2zm-1.4 4.2c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zm2.8 0c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zM12 9.3c.9 0 1.9.5 1.9 1s-1 1.2-1.9 1.2-1.9-.7-1.9-1.2 1-1 1.9-1zm0 3.4c2.2 0 3.6 2.6 3.6 4.4 0 1.5-1.6 2.3-3.6 2.3s-3.6-.8-3.6-2.3c0-1.8 1.4-4.4 3.6-4.4z"/></svg>Linux</a>
<a href="#journey" class="btn" style="color:#0C0C0E;border-color:#0C0C0E"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" aria-hidden="true"><path d="M12 2.5l8.2 4.75v9.5L12 21.5l-8.2-4.75v-9.5z"/><path d="M12 7.5l4.3 2.5v5L12 17.5l-4.3-2.5v-5z"/></svg>HiveOS</a>
<p style="font-size:14px;color:#55534F">One click: install, press start, the card mines and proves to a wallet the app makes for you. Available at public testnet. Open source, 1% dev fee like every miner you already run.</p>
</div>
</div>
@ -387,7 +396,7 @@ footer .fl{display:flex;flex-wrap:wrap;gap:20px}
<div class="brand"><svg viewBox="0 0 100 100" width="22" height="22" aria-hidden="true"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"></polygon><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"></polygon></svg><span class="word" style="font-size:14px;color:var(--bone)">IGNEUM</span><span>· nothing on this page is an offer to sell anything</span></div>
<div class="fl">
<a href="/litepaper">Litepaper</a>
<a href="https://github.com/joshmalone117/igneum" rel="noopener">GitHub</a>
<a href="https://github.com/joshmalone117/igneum" rel="noopener" style="display:inline-flex;align-items:center;gap:6px"><svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M12 .5C5.7.5.5 5.7.5 12c0 5.1 3.3 9.4 7.9 10.9.6.1.8-.3.8-.6v-2.1c-3.2.7-3.9-1.4-3.9-1.4-.5-1.3-1.3-1.7-1.3-1.7-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.7 1.3 3.4 1 .1-.8.4-1.3.7-1.6-2.6-.3-5.3-1.3-5.3-5.7 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.2 1.2.9-.3 1.9-.4 2.9-.4s2 .1 2.9.4c2.2-1.5 3.2-1.2 3.2-1.2.6 1.6.2 2.8.1 3.1.8.8 1.2 1.8 1.2 3.1 0 4.4-2.7 5.4-5.3 5.7.4.4.8 1.1.8 2.2v3.2c0 .3.2.7.8.6 4.6-1.5 7.9-5.8 7.9-10.9C23.5 5.7 18.3.5 12 .5z"/></svg>GitHub</a>
<a href="#journey">Journey</a>
</div>
</div>
@ -437,24 +446,55 @@ footer .fl{display:flex;flex-wrap:wrap;gap:20px}
}
if(!reduce){var sp=spacing();for(var i=0;i<Math.ceil(W/sp)+2;i++){spawn(W-i*sp);nodes[nodes.length-1].born=performance.now()-i*1000;}requestAnimationFrame(frame);}
// DAG animation in the prove section
var dag=document.getElementById('dag');
var rows=[[],[],[]];
function cap(){var cs=getComputedStyle(dag);var bs=parseFloat(cs.getPropertyValue('--bs'))||34,gp=parseFloat(cs.getPropertyValue('--dg'))||14;var off=bs*0.7;return Math.max(4,Math.floor((dag.clientWidth-2*off+gp)/(bs+gp)));}
function render(){dag.innerHTML='';var cs=getComputedStyle(dag);var bs=parseFloat(cs.getPropertyValue('--bs'))||34;rows.forEach(function(r,i){var d=document.createElement('div');d.className='dag-row';d.style.paddingLeft=Math.round(i*bs*0.7)+'px';r.forEach(function(b){var e=document.createElement('div');e.className='blk '+b.state+(b.fresh?' new':'');if(b.state==='lock'){e.innerHTML='<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="#F4F1EC" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="11" width="14" height="9" rx="2"></rect><path d="M8 11V7a4 4 0 0 1 8 0v4"></path></svg>';}b.fresh=false;d.appendChild(e);});dag.appendChild(d);});}
var tick=0;
function step(){
tick++;
rows.forEach(function(r){r.forEach(function(b){b.age++;if(b.state==='mined'&&b.age>3)b.state='proving';else if(b.state==='proving'&&b.age>7)b.state='proven';});});
var ri=tick%3;rows[ri].push({state:'mined',age:0,fresh:true});
if(tick%10===0){var r=rows[1];for(var i=r.length-1;i>=0;i--){if(r[i].state==='proven'){r[i].state='lock';break;}}}
var c=cap();rows.forEach(function(r){while(r.length>c)r.shift();});
render();
}
for(var k=0;k<24;k++){step();}
rows.forEach(function(r){r.forEach(function(b){b.fresh=false;});});render();
if(!reduce)setInterval(step,1000);
window.addEventListener('resize',function(){var c=cap();rows.forEach(function(r){while(r.length>c)r.shift();});render();});
// chain scene: blocks stream in, miners fly in to prove shards, locks ripple
(function(){
var c=document.getElementById('chain'),x=c.getContext('2d'),W,H,blocks=[],sparks=[],ripples=[],n=0,proven=0,locked=0,lanes=5,S,GAP,speed;
var cb=document.getElementById('c-blocks'),cp=document.getElementById('c-proven'),cl=document.getElementById('c-locked');
function size(){W=c.clientWidth;H=c.clientHeight;c.width=W*dpr;c.height=H*dpr;x.setTransform(dpr,0,0,dpr,0,0);S=W<600?18:26;GAP=S*3.1;speed=GAP/1000;}
size();window.addEventListener('resize',size);
function laneY(l){return H*0.14+l*(H*0.72/(lanes-1));}
function spawn(px){n++;var lane=Math.floor(Math.random()*lanes);var b={id:n,x:px,y:laneY(lane),born:performance.now(),shards:[0,0,0,0],claimed:[false,false,false,false],state:'mined',cp:n%10===0,parents:[],glow:0};
var cands=blocks.slice(-8).filter(function(q){return q.x<px-S;}).sort(function(a2,b2){return b2.x-a2.x;});
if(cands.length)b.parents.push(cands[0]);cands.slice(1).forEach(function(q){if(Math.random()<0.4&&b.parents.length<3)b.parents.push(q);});
blocks.push(b);if(blocks.length>80)blocks.shift();cb.textContent=n;}
function rr(px,py,w,h,r){x.beginPath();x.moveTo(px+r,py);x.arcTo(px+w,py,px+w,py+h,r);x.arcTo(px+w,py+h,px,py+h,r);x.arcTo(px,py+h,px,py,r);x.arcTo(px,py,px+w,py,r);x.closePath();}
var last=0,lastShard=0;
function frame(t){
if(t-last>1000){spawn(W+S);last=t;}
var dt=16;
blocks.forEach(function(b){b.x-=speed*dt;var age=t-b.born;
if(b.state==='mined'&&age>1200)b.state='proving';
if(b.state==='proving'){for(var i=0;i<4;i++){if(!b.claimed[i]&&Math.random()<0.012){b.claimed[i]=true;var fromTop=Math.random()<0.5;sparks.push({x:Math.random()*W,y:fromTop?-6:H+6,tx:b.x,ty:b.y,b:b,i:i,p:0});}}
if(b.shards.every(function(v){return v>=1;})){b.state='proven';b.glow=1;proven++;cp.textContent=proven;if(b.cp)setTimeout(function(){b.locked=true;locked++;cl.textContent=locked;ripples.push({x:b.x,y:b.y,r:S,a:1});},1800);}}
if(b.glow>0)b.glow-=0.02;});
sparks.forEach(function(sp){sp.p+=0.035;sp.tx=sp.b.x;sp.ty=sp.b.y;if(sp.p>=1){sp.done=true;sp.b.shards[sp.i]=1;}});
sparks=sparks.filter(function(sp){return !sp.done;});
ripples.forEach(function(r){r.r+=1.6;r.a-=0.018;r.x-=speed*dt;});ripples=ripples.filter(function(r){return r.a>0;});
x.clearRect(0,0,W,H);
// faint lane rails
x.strokeStyle='rgba(42,42,48,0.6)';x.lineWidth=1;for(var l=0;l<lanes;l++){x.beginPath();x.moveTo(0,laneY(l));x.lineTo(W,laneY(l));x.stroke();}
// edges
blocks.forEach(function(b){b.parents.forEach(function(q){if(blocks.indexOf(q)<0)return;var both=b.state==='proven'&&q.state==='proven';x.strokeStyle=both?'rgba(242,84,27,0.45)':'rgba(154,154,158,0.22)';x.lineWidth=both?1.5:1;x.beginPath();x.moveTo(b.x,b.y);var mx=(b.x+q.x)/2;x.bezierCurveTo(mx,b.y,mx,q.y,q.x,q.y);x.stroke();});});
// lock line: everything left of the newest locked block is final
var lk=null;blocks.forEach(function(b){if(b.locked&&(!lk||b.x>lk.x))lk=b;});
if(lk){x.strokeStyle='rgba(242,84,27,0.5)';x.setLineDash([4,6]);x.beginPath();x.moveTo(lk.x,8);x.lineTo(lk.x,H-8);x.stroke();x.setLineDash([]);}
// blocks
blocks.forEach(function(b){var h=S/2;
if(b.glow>0){var g=x.createRadialGradient(b.x,b.y,0,b.x,b.y,S*1.6);g.addColorStop(0,'rgba(242,84,27,'+(0.45*b.glow)+')');g.addColorStop(1,'rgba(242,84,27,0)');x.fillStyle=g;x.beginPath();x.arc(b.x,b.y,S*1.6,0,Math.PI*2);x.fill();}
if(b.state==='proven'){x.fillStyle='#F2541B';rr(b.x-h,b.y-h,S,S,S*0.24);x.fill();}
else{x.fillStyle='#0C0C0E';rr(b.x-h,b.y-h,S,S,S*0.24);x.fill();x.strokeStyle=b.state==='proving'?'rgba(255,179,92,0.9)':'rgba(58,58,66,1)';x.lineWidth=2;rr(b.x-h,b.y-h,S,S,S*0.24);x.stroke();
if(b.state==='proving'){var q=S/2-3;for(var i=0;i<4;i++){if(b.shards[i]>=1){x.fillStyle='#FFB35C';var qx=b.x-h+3+(i%2)*q,qy=b.y-h+3+Math.floor(i/2)*q;rr(qx,qy,q-1,q-1,2);x.fill();}}}}
if(b.locked){x.strokeStyle='rgba(242,84,27,0.95)';x.lineWidth=2;x.beginPath();x.arc(b.x,b.y,S*0.95,0,Math.PI*2);x.stroke();x.fillStyle='#F4F1EC';x.font='bold '+Math.round(S*0.45)+'px IBM Plex Mono, monospace';x.textAlign='center';x.fillText('L',b.x,b.y+S*0.16);}
});
// sparks (miners claiming shards)
sparks.forEach(function(sp){var e=1-Math.pow(1-sp.p,3);var sx=sp.x+(sp.tx-sp.x)*e,sy=sp.y+(sp.ty-sp.y)*e;x.strokeStyle='rgba(255,179,92,'+(0.5*(1-sp.p))+')';x.lineWidth=1;x.beginPath();x.moveTo(sp.x+(sp.tx-sp.x)*Math.max(0,e-0.15),sp.y+(sp.ty-sp.y)*Math.max(0,e-0.15));x.lineTo(sx,sy);x.stroke();x.fillStyle='#FFB35C';x.beginPath();x.arc(sx,sy,2.2,0,Math.PI*2);x.fill();});
ripples.forEach(function(r){x.strokeStyle='rgba(242,84,27,'+r.a+')';x.lineWidth=1.5;x.beginPath();x.arc(r.x,r.y,r.r,0,Math.PI*2);x.stroke();});
if(!reduce)requestAnimationFrame(frame);
}
for(var i=0;i<Math.ceil(W/(S*3.1))+1;i++){spawn(W-i*S*3.1);var b=blocks[blocks.length-1];b.born=performance.now()-i*1000;if(i>5){b.state='proven';b.shards=[1,1,1,1];b.claimed=[true,true,true,true];proven++;if(b.cp&&i>8){b.locked=true;locked++;}}else if(i>1){b.state='proving';b.claimed=[true,true,false,false];b.shards=[1,1,0,0];}}
cb.textContent=n;cp.textContent=proven;cl.textContent=locked;
requestAnimationFrame(frame);
})();
// hourly program ticker
var ops=['rotl','rotr','xor','add','sub','mul','mulhi','mad','or'];