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:
parent
21c99f708e
commit
97136e9f53
6 changed files with 282 additions and 67 deletions
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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/[second-owner-login]/igneum" rel="noopener">GitHub</a>
|
||||
<a href="https://github.com/[second-owner-login]/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'];
|
||||
|
|
|
|||
Loading…
Reference in a new issue