diff --git a/proto-cuda/host.cu b/proto-cuda/host.cu index 29db7416e..903a9a5f1 100644 --- a/proto-cuda/host.cu +++ b/proto-cuda/host.cu @@ -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 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 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)); diff --git a/proto-metal/main.swift b/proto-metal/main.swift index 5f82fd19a..d76469362 100644 --- a/proto-metal/main.swift +++ b/proto-metal/main.swift @@ -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)") diff --git a/sim/README.md b/sim/README.md index d1b2f2651..4e2d92680 100644 --- a/sim/README.md +++ b/sim/README.md @@ -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. diff --git a/sim/__pycache__/finality_v2.cpython-310.pyc b/sim/__pycache__/finality_v2.cpython-310.pyc index 3df129fe0..ec0d5a41c 100644 Binary files a/sim/__pycache__/finality_v2.cpython-310.pyc and b/sim/__pycache__/finality_v2.cpython-310.pyc differ diff --git a/sim/finality_v2.py b/sim/finality_v2.py index 666ad0c1d..dc5d80efd 100644 --- a/sim/finality_v2.py +++ b/sim/finality_v2.py @@ -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)) diff --git a/site/index.html b/site/index.html index 7f9e9ec20..04b6e9ea7 100644 --- a/site/index.html +++ b/site/index.html @@ -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}

Watch the chain prove itself

Blocks arrive every second. Miners prove them in shards. A checkpoint locks every 30 seconds. All of it will be on this page, live.

-
-
+
+
+
simulated preview · how testnet will look
+
blocks 0proven 0locked 0
+
+
+ mined + shards being proven proven - being proven by miners - just mined - checkpoint locked by sustained miners + checkpoint locked by sustained miners
@@ -326,10 +335,10 @@ footer .fl{display:flex;flex-wrap:wrap;gap:20px}
03

Proving for others

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.

- Windows - macOS - Linux - HiveOS + Windows + macOS + Linux + HiveOS

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.

@@ -387,7 +396,7 @@ footer .fl{display:flex;flex-wrap:wrap;gap:20px}
IGNEUM· nothing on this page is an offer to sell anything
@@ -437,24 +446,55 @@ footer .fl{display:flex;flex-wrap:wrap;gap:20px} } if(!reduce){var sp=spacing();for(var i=0;i