diff --git a/docs/plans/site-copy-pass.md b/docs/plans/site-copy-pass.md index a21591d29..77aff9d4c 100644 --- a/docs/plans/site-copy-pass.md +++ b/docs/plans/site-copy-pass.md @@ -15,6 +15,7 @@ tables, no orphan word on a line (tools/ci/site-orphan-check.mjs at 390 px). | 2 | Home, principles | (none; the home page was the scene and three facts) | Graphics cards only / The miners are the provers / No premine, no stake / Every claim has a status, one line each under the hero | | 3 | Home, live panel | (the scene with no heading) | Follow the work. See the chain grow. + "Every square is a real block, read from one node every 2 s. Nothing here is a replay." | | 4 | Home, reasons | (none) | Three reasons to power on: A program that never holds still / Work you can see / One click, and it mines, each in plain words with the measured limit named | +| 4a | Home, reasons (7 October 2026, the launch pack) | Three reasons to power on | Three things, in order: Your card stays a card / Income with no cliff / A fair supply, the three asks of twenty miner launch posts (docs/plans/launch-pack.md section 4 A), in the same three cards; the income card carries no emission number until the genesis re-cut, and the line under the grid points at the litepaper's finality section | | 5 | Home, economics | (on the litepaper only) | Made for the people running the hardware. Every block pays the card that found it and the cards that prove it. The protocol carves out nothing for a team, a foundation or a fund. | | 6 | Home, CTA band | (none) | Give your GPU something to do. Download Ember, watch the devnet, and tell us what breaks. The people on the Discord run cards like yours. | | 7 | Miner, hero | Install. Start. The card mines. (kept) | The miner built around your card. Your rate, your watts, what it costs a day at your price, and the chain drawn live with your own blocks ringed. | diff --git a/infra/build-server/README.md b/infra/build-server/README.md new file mode 100644 index 000000000..49f7ba106 --- /dev/null +++ b/infra/build-server/README.md @@ -0,0 +1,37 @@ +# The build boxes (infra/build-server) + +Three Hetzner dedicated servers in Falkenstein run everything the Mac must not: builds, test suites, benchmarks, CPU proving, the +devnet hands and the observer. The Mac keeps macOS binaries, the DMG and Metal tests (CLAUDE.md, "Running agents on this Mac"). + +## No mining on any Hetzner box, ever + +the project lead's rule through main, 7 October 2026: Hetzner's policies forbid crypto mining. The boxes run nodes, builds, tests, benchmarks and +CPU proving only. The pool's fast-time network runs its miners on rented GPU pods (tools/fleet), never on a box; a box may run the +network's nodes. The capacity layer refuses a job that would start `igneum-miner mine` or a GPU worker (capacity/run.sh), and no +hands unit carries a miner. A node started with `--enable-unsynced-mining` is a node flag, not a miner; nothing feeds it blocks here. + +## The boxes and the kind map + +| Box | Host file on the Mac | Takes | Never | +|---|---|---|---| +| igneum-build-1 (188.40.146.49, AX162-1-LTD) | `~/.config/igneum/build-server` | release gates (`--priority gate`), builds and cross-builds, checks, the GPU workers' host side, the devnet hands (node 1, the observer node, the observer), the Devnet 2 seed, the CI runner, the dashboard feed | suites and benches once box 2 exists | +| igneum-build-2 (AX162-1, on order) | `~/.config/igneum/build-server-2` | suites (`cargo test`), benches (`cargo bench`), the attack rows (`--box 2`) | gates, hands | +| igneum-build-3 (AX102-1, on order) | `~/.config/igneum/build-server-3` | proving and aggregation CPU work (proving/igneum-prove builds and suites), the second prover's shadow runner, the pool's fast-time NETWORK (nodes only, `--box 3`) | miners of any kind | + +`tools/build-remote.sh` routes by class (lib.sh `bs_route`): suite and bench to box 2, the proving crate to box 3, everything else +to box 1; `--box N` overrides; a class whose box has no host file yet falls back to box 1 and says so. `--priority gate` always runs +on box 1. Each box has its own mirrors, slots, locks and JSONL log under /srv; `run-from-mac.sh --box N ` provisions a box and +writes its host file; the dashboard collector reads every box it is told about. + +## Files + +| File | What | +|---|---| +| `provision.sh` | the box itself: install mode (rescue system, Ubuntu 24.04, RAID 1, no swap) and provision mode (user build, toolchains, the pin, sccache, zig, CUDA headers, docker, Caddy, mirrors, slots, sshd, ufw) | +| `run-from-mac.sh` | ships provision.sh, writes the host file, wires the `build` remotes and pushes every branch | +| `lib.sh`, `remote-run.sh` | the Mac and box halves of a remote run: sync, checkout, slots, scheduling classes, the JSONL line | +| `hands/` | the devnet hands' units, the mover and the restart read-backs | +| `capacity/` | the capacity layer (the box-work lane's): background jobs under the build slots, never a miner | +| `repro/`, `night/`, `prover/`, `runner/`, `workers/` | other lanes' pieces that live on the boxes | + +Plan, numbers and the gotchas: docs/plans/build-server.md; the hands: docs/plans/hands-on-build-1.md. diff --git a/infra/build-server/capacity/run.sh b/infra/build-server/capacity/run.sh index d522c59b6..95eb92f61 100755 --- a/infra/build-server/capacity/run.sh +++ b/infra/build-server/capacity/run.sh @@ -58,6 +58,11 @@ run_slice() { once() { for job in $SEQUENCE; do + # No mining on any Hetzner box, ever (the project lead through main, 7 October 2026; Hetzner's policies forbid it): a job that would start a + # miner or a GPU worker is refused here, whatever SEQUENCE says. Nodes, builds, tests, benchmarks and CPU proving only. + if grep -qE 'igneum-miner[[:space:]]+mine|igneum-worker-(cuda|opencl)|igneum-app.*--mine|cargo run.*-p[[:space:]]+igneum-miner' "$JOBS_DIR/$job.sh" 2>/dev/null; then + echo "capacity: REFUSED job $job: it would start a miner or a GPU worker; no mining on a Hetzner box (infra/build-server/README.md)" >&2; continue + fi [ -f "$JOBS_DIR/$job.sh" ] || { cap_say "no job $job"; continue; } # wait out a running build before starting a slice (do not even launch during a build) while cap_build_active; do diff --git a/infra/build-server/lib.sh b/infra/build-server/lib.sh index 04bf429c4..bb754b796 100755 --- a/infra/build-server/lib.sh +++ b/infra/build-server/lib.sh @@ -16,7 +16,19 @@ # shellcheck disable=SC2034 # shared with the scripts that source lib.sh BS_KEY="${IGNEUM_BUILD_KEY:-$HOME/.ssh/igneum_ed25519}" -BS_HOST_FILE="${IGNEUM_BUILD_HOST_FILE:-$HOME/.config/igneum/build-server}" # one line: build@ +BS_HOST_FILE="${IGNEUM_BUILD_HOST_FILE:-$HOME/.config/igneum/build-server}" # one line: build@ (box 1; box N is build-server-N) +# Several boxes (main, 7 October 2026: igneum-build-2 and igneum-build-3 on order). One host file per box: ~/.config/igneum/build-server +# is box 1, build-server-2 is box 2, build-server-3 is box 3 (run-from-mac.sh --box N writes it). The route by class, unless the +# caller passes --box: gates, builds, checks, cross-builds, the workers, the hands and the observer stay on box 1; suites, benches and +# the attack rows go to box 2; proving and aggregation CPU work, the second prover's shadow runner and the pool's fast-time NETWORK go +# to box 3. A class whose box has no host file yet falls back to box 1, and the log line says so. No box mines, ever (README.md). +bs_box_file() { case "${1:-1}" in 1) echo "$BS_HOST_FILE" ;; *) echo "${BS_HOST_FILE}-$1" ;; esac; } +bs_route() { # -> the box number, falling back to 1 + local want=1 + case "$1" in suite|bench|attack) want=2 ;; prove|shadow|fasttime) want=3 ;; esac + if [ "$want" != 1 ] && [ ! -s "$(bs_box_file "$want")" ]; then bs_log "class $1 routes to box $want, which has no host file yet ($(bs_box_file "$want")): box 1"; want=1; fi + echo "$want" +} BS_ROOT_REMOTE=/srv/builds BS_MIRROR_REPO=/srv/igneum.git BS_MIRROR_NODE=/srv/igneum-node.git @@ -24,11 +36,13 @@ BS_MIRROR_NODE=/srv/igneum-node.git bs_log() { printf '%s %s: %s\n' "$(date -u +%H:%M:%S)" "${BS_TOOL:-build-server}" "$*" >&2; } bs_die() { bs_log "ERROR: $*"; exit 1; } -bs_host() { +bs_host() { # [box number, default BS_BOX or 1] + BS_BOX="${1:-${BS_BOX:-1}}" BS_HOST="${BUILD_HOST:-}" if [ -z "$BS_HOST" ]; then - [ -s "$BS_HOST_FILE" ] || bs_die "no build server: write build@ to $BS_HOST_FILE (infra/build-server/run-from-mac.sh does) or set BUILD_HOST" - BS_HOST="$(head -1 "$BS_HOST_FILE" | tr -d '[:space:]')" + local f; f=$(bs_box_file "$BS_BOX") + [ -s "$f" ] || bs_die "no build server for box $BS_BOX: write build@ to $f (infra/build-server/run-from-mac.sh --box $BS_BOX does) or set BUILD_HOST" + BS_HOST="$(head -1 "$f" | tr -d '[:space:]')" fi case "$BS_HOST" in *@*) ;; *) bs_die "BUILD_HOST must be user@host, got '$BS_HOST'" ;; esac [ -r "$BS_KEY" ] || bs_die "no ssh key at $BS_KEY" diff --git a/infra/build-server/remote-run.sh b/infra/build-server/remote-run.sh index 0190a31a0..15facd9f4 100755 --- a/infra/build-server/remote-run.sh +++ b/infra/build-server/remote-run.sh @@ -110,6 +110,21 @@ checkout_tree() { set +e } +if [ "${1:-}" = --self-test-keeper ]; then + # the keeper restores a truncated holder line within its interval, and stops at release + t=$(mktemp -d); trap 'rm -rf "$t"' EXIT + BR_PID=$$; BR_LABEL="keeper self-test"; got=0; waited=3; SLOTS_DIR="$t"; BR_KEEP_S=1 + holder_line() { printf 'pid %s since %sZ waited %s s: %s\n' "$BR_PID" "$(date -u +%H:%M:%S)" "$1" "$BR_LABEL"; } + holder_line "$waited" > "$t/build-0" + keeper_pid=""; keep_line() { local f="$1" w="$2"; ( while kill -0 "$BR_PID" 2>/dev/null; do [ -s "$f" ] || holder_line "$w" > "$f" 2>/dev/null; sleep "${BR_KEEP_S:-20}"; done ) & keeper_pid=$!; } + keep_line "$t/build-0" "$waited" + : > "$t/build-0"; sleep 2.5 + grep -q "^pid $$ since .* waited 3 s: keeper self-test$" "$t/build-0" || { echo "keeper self-test: the truncated holder line was NOT restored"; kill "$keeper_pid" 2>/dev/null; exit 1; } + kill "$keeper_pid" 2>/dev/null; wait "$keeper_pid" 2>/dev/null; : > "$t/build-0"; sleep 2.5 + [ ! -s "$t/build-0" ] || { echo "keeper self-test: the keeper kept writing after release"; exit 1; } + echo "keeper self-test: a truncated holder line comes back within the interval; nothing is written after release"; exit 0 +fi + if [ "${1:-}" = --self-test ]; then t=$(mktemp -d); trap 'rm -rf "$t"' EXIT git init -q --bare -b master "$t/mirror.git" @@ -369,7 +384,17 @@ else echo "build-remote: holding build-$got on $BR_HOST (waited $waited s; $held of $slots slots held, CARGO_BUILD_JOBS=$BR_JOBS, kind ${BR_KIND:-other}, nice ${BR_NICE:-0}, cores $( [ "${BR_CORES:-0}" = 0 ] && nproc || echo "$BR_CORES"))" >&2 fi -release_slot() { if [ "$got" = measure ]; then : > "$SLOTS_DIR/measure"; else : > "$SLOTS_DIR/build-$got"; fi; } +# The holder keeps its own line (the watcher-trust rule, 7 October 2026 10:39Z: two held slots read EMPTY while two suites ran, +# because a run from a worktree without last night's append-mode fix still opens a busy sibling's slot file with `>` on every +# probe). A keeper re-writes the holder line whenever it finds the file empty, every BR_KEEP_S seconds, until release_slot. +keeper_pid="" +keep_line() { # + local f="$1" w="$2" + ( while kill -0 "$BR_PID" 2>/dev/null; do [ -s "$f" ] || holder_line "$w" > "$f" 2>/dev/null; sleep "${BR_KEEP_S:-20}"; done ) & + keeper_pid=$! +} +if [ "$got" = measure ]; then keep_line "$SLOTS_DIR/measure" "$waited"; else keep_line "$SLOTS_DIR/build-$got" "$waited"; fi +release_slot() { [ -n "$keeper_pid" ] && { kill "$keeper_pid" 2>/dev/null; wait "$keeper_pid" 2>/dev/null; keeper_pid=""; }; if [ "$got" = measure ]; then : > "$SLOTS_DIR/measure"; else : > "$SLOTS_DIR/build-$got"; fi; } cd "$BR_DIR" || { BR_CLASS=no-dir jsonlog 2 "$got" "$waited" "" "$(date +%s)" "" "" "" "" "" ""; redlog 2 0 no-dir; release_slot; exit 2; } # PRE-FLIGHT for a cargo command (the instant-death class, 6 October 2026): the manifest parses and every -p package exists, @@ -425,6 +450,9 @@ ncpu=$(nproc); cores_str="0-$((ncpu - 1))" if [ "${BR_CORES:-0}" -gt 0 ] && [ "${BR_CORES}" -lt "$ncpu" ]; then cores_str="$((ncpu - BR_CORES))-$((ncpu - 1))"; fi ( [ "${BR_NICE:-0}" -gt 0 ] && renice -n "$BR_NICE" -p $BASHPID >/dev/null 2>&1; [ "$cores_str" != "0-$((ncpu - 1))" ] && taskset -cp "$cores_str" $BASHPID >/dev/null 2>&1; eval "$BR_CMD" ) > >(tee -a "$BR_RUN_LOG") 2> >(tee -a "$BR_RUN_LOG" >&2) rc=$? +# the keeper stops BEFORE the bare `wait` (which flushes the two tees): a bare wait also waits for the keeper, and the keeper waits +# for this script, a deadlock that held build-2's first run 15 minutes after its test had passed (7 Oct 2026, 11:04 to 11:19Z) +if [ -n "$keeper_pid" ]; then kill "$keeper_pid" 2>/dev/null; wait "$keeper_pid" 2>/dev/null; keeper_pid=""; fi wait t2=$(date +%s); secs=$(( t2 - t1 )) exec_after=$(stat_field "Compile requests executed"); hits_after=$(stat_field "Cache hits "); misses_after=$(stat_field "Cache misses ") diff --git a/infra/build-server/run-from-mac.sh b/infra/build-server/run-from-mac.sh index eeb94ea0e..a5a275a8c 100755 --- a/infra/build-server/run-from-mac.sh +++ b/infra/build-server/run-from-mac.sh @@ -20,13 +20,20 @@ BS_TOOL=run-from-mac # shellcheck source=lib.sh . "$HERE/lib.sh" +BOX=1; while [ "${1:-}" = --box ]; do BOX="$2"; shift 2; done # --box N: igneum-build-N, host file build-server-N (7 Oct 2026) IP="${1:-}"; shift || true -[ -n "$IP" ] || bs_die "usage: run-from-mac.sh [--wire-only]" +[ -n "$IP" ] || bs_die "usage: run-from-mac.sh [--box N] [--wire-only]" WIRE_ONLY=0; [ "${1:-}" = --wire-only ] && WIRE_ONLY=1 +# box N is igneum-build-N with its own dashboard feed name build-N.igneum.network (the dashboard lane's collector reads one server +# section per box; main adds the A record with the IP) +[ "$BOX" = 1 ] || { BOX_HOSTNAME="${BOX_HOSTNAME:-igneum-build-$BOX}"; WORKERS_HOST="${WORKERS_HOST:-build-$BOX.igneum.network}"; export BOX_HOSTNAME WORKERS_HOST; } +export BS_BOX="$BOX" # lib.sh's bs_host derives the host file from BS_BOX (box 1: build-server; box N: build-server-N); never set BS_HOST_FILE here (7 Oct 2026: a second suffix, build-server-2-2) +HOST_FILE=$(bs_box_file "$BOX") +REMOTE="build"; [ "$BOX" = 1 ] || REMOTE="build-$BOX" # one git remote per box in the two repositories # the toolchain pin travels from rust-toolchain.toml unless RUST_TOOLCHAIN is set by hand (one file pins every side, 7 Oct 2026) [ -n "${RUST_TOOLCHAIN:-}" ] || RUST_TOOLCHAIN=$(sed -n 's/^channel *= *"\([^"]*\)".*/\1/p' "$REPO/rust-toolchain.toml" 2>/dev/null | head -1); export RUST_TOOLCHAIN PASS="" # a string, not an array: bash 3.2 (the Mac) treats an empty array as unbound under set -u -for v in MODE RUST_TOOLCHAIN SCCACHE_GB SCCACHE_VERSION NODE_MAJOR SLOTS P2P_PORTS BOX_HOSTNAME SSH_PUBKEY; do +for v in MODE RUST_TOOLCHAIN SCCACHE_GB SCCACHE_VERSION NODE_MAJOR SLOTS P2P_PORTS BOX_HOSTNAME WORKERS_HOST SSH_PUBKEY; do [ -n "${!v:-}" ] && PASS="$PASS $v=$(printf '%q' "${!v}")" done @@ -43,19 +50,19 @@ if [ "$WIRE_ONLY" = 0 ]; then if "${ROOT_SSH[@]}" 'hostname' 2>/dev/null | grep -q '^rescue'; then bs_die "still in the rescue system after provision.sh"; fi fi -mkdir -p "$(dirname "$BS_HOST_FILE")" -printf 'build@%s\n' "$IP" > "$BS_HOST_FILE" -bs_log "wrote $BS_HOST_FILE" +mkdir -p "$(dirname "$HOST_FILE")" +printf 'build@%s\n' "$IP" > "$HOST_FILE" +bs_log "wrote $HOST_FILE" bs_host bs_ssh 'hostname; nproc' >/dev/null || bs_die "build@$IP does not answer with $BS_KEY" wire_remote() { # repo-dir mirror label local dir="$1" mirror="$2" label="$3" url="$BS_HOST:$2" cur - cur=$(git -C "$dir" remote get-url build 2>/dev/null || true) - if [ -z "$cur" ]; then git -C "$dir" remote add build "$url"; bs_log "$label: remote build = $url" - elif [ "$cur" != "$url" ]; then git -C "$dir" remote set-url build "$url"; bs_log "$label: remote build -> $url" - else bs_log "$label: remote build ok"; fi - GIT_SSH_COMMAND="$BS_SSH_CMD" git -C "$dir" push -q --force build --all && bs_log "$label: every branch pushed to $mirror ($(git -C "$dir" branch --list | wc -l | tr -d ' ') branches)" || bs_die "$label: push to $mirror failed" + cur=$(git -C "$dir" remote get-url "$REMOTE" 2>/dev/null || true) + if [ -z "$cur" ]; then git -C "$dir" remote add "$REMOTE" "$url"; bs_log "$label: remote $REMOTE = $url" + elif [ "$cur" != "$url" ]; then git -C "$dir" remote set-url "$REMOTE" "$url"; bs_log "$label: remote $REMOTE -> $url" + else bs_log "$label: remote $REMOTE ok"; fi + GIT_SSH_COMMAND="$BS_SSH_CMD" git -C "$dir" push -q --force "$REMOTE" --all && bs_log "$label: every branch pushed to $mirror ($(git -C "$dir" branch --list | wc -l | tr -d ' ') branches)" || bs_die "$label: push to $mirror failed" } wire_remote "$MAIN_REPO" "$BS_MIRROR_REPO" "igneum" wire_remote "$MAIN_REPO/vendor/igneum-node" "$BS_MIRROR_NODE" "igneum-node (the fork)" diff --git a/infra/build-server/workers/install.sh b/infra/build-server/workers/install.sh index 6842091e3..91bce76d4 100755 --- a/infra/build-server/workers/install.sh +++ b/infra/build-server/workers/install.sh @@ -1,11 +1,18 @@ #!/usr/bin/env bash # Install or refresh the worker dashboard collector on igneum-build-1 from this Mac. -# infra/build-server/workers/install.sh copies tools/workers/{lib,collect}.mjs and the two units, enables the timer -# Needs root over ssh (root@ with ~/.ssh/igneum_ed25519); the host ip comes from ~/.config/igneum/build-server (build@). +# infra/build-server/workers/install.sh build-1: copies tools/workers/{lib,collect}.mjs and the two units, enables the timer +# infra/build-server/workers/install.sh 2 igneum-build-2 (host line in ~/.config/igneum/build-server-2), 3 for build-3 +# infra/build-server/workers/install.sh build@ any box by its host line +# Needs root over ssh (root@ with ~/.ssh/igneum_ed25519); the host ip comes from ~/.config/igneum/build-server[-N] (build@). set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; ROOT="$(cd "$HERE/../../.." && pwd)" KEY="${IGNEUM_BUILD_KEY:-$HOME/.ssh/igneum_ed25519}" -HOST_LINE="$(head -1 "${IGNEUM_BUILD_HOST_FILE:-$HOME/.config/igneum/build-server}" | tr -d '[:space:]')" +case "${1:-}" in + "") HOST_LINE="$(head -1 "${IGNEUM_BUILD_HOST_FILE:-$HOME/.config/igneum/build-server}" | tr -d '[:space:]')" ;; + [2-9]) HOST_LINE="$(head -1 "$HOME/.config/igneum/build-server-$1" | tr -d '[:space:]')" ;; + *@*) HOST_LINE="$1" ;; + *) echo "usage: install.sh [N | build@]" >&2; exit 2 ;; +esac IP="${HOST_LINE#*@}"; [ -n "$IP" ] || { echo "no build server in ~/.config/igneum/build-server" >&2; exit 1; } SSH=(ssh -i "$KEY" -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10) "${SSH[@]}" "root@$IP" 'install -d -o build -g build /srv/workers /srv/workers/bin /srv/workers/sources /srv/builds/_log; chown build:build /srv/builds/_log' diff --git a/site/404.html b/site/404.html index 89689ae5f..012e1065a 100644 --- a/site/404.html +++ b/site/404.html @@ -13,6 +13,7 @@ + @@ -157,71 +177,67 @@ main{padding-bottom:var(--sec)} - diff --git a/site/app.html b/site/app.html index b093527a5..737960a66 100644 --- a/site/app.html +++ b/site/app.html @@ -31,6 +31,7 @@ + @@ -158,71 +178,67 @@ main{padding-bottom:var(--sec)} - diff --git a/site/build.mjs b/site/build.mjs index 5846957c7..76b992e90 100644 --- a/site/build.mjs +++ b/site/build.mjs @@ -112,8 +112,7 @@ ${HEAD} @@ -179,71 +199,67 @@ main{padding-bottom:var(--sec)} - diff --git a/site/faucet.html b/site/faucet.html index 4cf1f984b..e4ef11f17 100644 --- a/site/faucet.html +++ b/site/faucet.html @@ -27,6 +27,7 @@ + - - +
- -
-
-
-
-
Built for GPU miners / devnet live
-

Your GPU has more to give.

-

You already own the card. Igneum gives it a job worth doing: mine the block, prove it, watch it lock. One click installs the node, the miner and the prover.

- -
Devnet build. Coins have no value. The chain may reset.
-
-
- -
IGNEUM / EMBERYour card becomes the network
-
01 / MiningIllustration
-
The card runs this hour’s program. A block comes out.Drawn, not live telemetry. The live graph is below.
-
GPU > BLOCK > PROOF > LOCK
-
- - - -
-
-
-
-
Graphics cards onlyA new mining program every hour
-
The miners are the proversSame card, two jobs, 80/20 of every block
-
No premine, no stakeNo fee to any team in the protocol
-
Every claim has a statusThe evidence page says how far each one is tested
-
-
-
- -
-
-
-
Live now
pending

pending vote keys mined a block in the last ten minutes; a card runs several.

-
Proven and paid
pending

shards proven and paid on the chain so far, pending of them in the last ten minutes.

-
The chip model
5x to 9x

Built for graphics cards. In our public model the strongest chip reaches 5x to 9x per joule against an RTX 5090 today; class v4, now on the vote, brings that to 2.1x to 3.9x, and class v5 makes the dataset the chain’s own state, so a chip that stores it or recomputes it is wrong on every item. The model and every measurement are public.

-
-

One node is read every 2 s. The chip figures are a cost model, not a measurement.

-
-
- -
-
+
Built for GPU miners / devnet live

Your GPU has
more to give.

You already own the card. Igneum gives it a job worth doing: mine the block, prove it, watch it lock. One click installs the node, the miner and the prover.

Devnet build. Coins have no value. The chain may reset.
IGNEUM / EMBERYour card becomes the network
01 / MiningIllustration
The card runs this hour’s program. A block comes out.Drawn, not live telemetry. The live graph is below.
GPU > BLOCK > PROOF > LOCK
Graphics cards onlyA new mining program every hour
The miners are the proversSame card, two jobs, 80/20 of every block
No premine, no stakeNo fee to any team in the protocol
Every claim has a statusThe evidence page says how far each one is tested
+
See what your card joins

Follow the work.
See the chain grow.

Open the observatory
BLOCKDAG/The live devnet, one lane per miner.
connecting
Live now
pending
pending vote keys mined a block in the last ten minutes; a card runs several
Proven and paid
pending
shards proven and paid on the chain so far, pending of them in the last ten minutes
The chip model
5x to 9x
Built for graphics cards. In our public model the strongest chip reaches 5x to 9x per joule against an RTX 5090 today; class v4, now on the vote, brings that to 2.1x to 3.9x, and class v5 makes the dataset the chain’s own state, so a chip that stores it or recomputes it is wrong on every item. The model and every measurement are public.
One node is read every 2 s. The chip figures are a cost model, not a measurement.Look a block up in the explorer
+
+
-
See what your card joins

Follow the work.
See the chain grow.

- Open the observatory -
-
-
BLOCKDAG / the live devnet, one lane per miner
connecting to the devnet observer
-
- -
-
Every square is a real block, read from one node every 2 s. Nothing here is a replay.Look a block up in the explorer
-
-
- -
-
-
-
Built around your card

Three reasons to power on.

-

From the first install to a block you can point at and say: mine.

+
What miners asked for, 2018 to 2026

Three things, in order.

+

Twenty launch posts and threads, and the same three asks each time: hardware that keeps its value, income that does not fall off a cliff, a fair supply. The front page answers those three. Finality is on page two.

-
+
01
-

A program that never holds still.

-
-

Every hour the chain draws a new mining program and your card compiles it. A chip wired for one program is out of date before it ships. The limits are measured and published, not promised.

+

Your card stays a card.

+

The hash is a new random program every hour over a dataset the chain draws from its own state. No scheduled fork, no release a team must ship. The chip model and its number are published with every era on /evidence. When you stop, the card still games.

How mining works
-
+
02
-

Work you can see.

-

Watch your block land. Open its proof shards. Follow a checkpoint from pending to locked. Each state is its own word and none of them is guessed from a block’s age.

- Follow a block +

Income with no cliff.

+

No halving day. The schedule glides month by month, then holds a steady share of supply for ever. A block pays its miner whether or not anyone buys a proof that day. What each card mines, and what its electricity costs, comes as one table when the testnet schedule is cut.

+ What your card does here
-
+
03
-

One click, and it mines.

-

Pick your platform, run the installer, press Start. Igneum Ember sets up the node, the miner and the prover, makes your wallet, and tunes each card for the most hashes per watt.

- Meet Ember +

A fair supply.

+

No premine. No fund, no foundation, no fee to any team. Nobody holds a coin before block one. The founders mine from genesis with disclosed addresses and the same software as everyone else. The launch weeks ramp up from a low start, so they are worth less to a private farm.

+ The economics, as published
+
Blocks are final by miners alone, with no stake and no other chain.How, on page two
- -
-
-
-
-
The economic design, as published
-

Made for the people running the hardware.

-

Every block pays the card that found it and the cards that prove it. The protocol carves out nothing for a team, a foundation or a fund. No premine, no stake.

- -
-
-
Each block, by the rule
-
100%to miners and provers
-
-
-
80% Miningto the card that found the block
-
20% Provingto the cards that prove it
-
0% Treasurynothing to any team
-
-

The protocol rule, not an income forecast. Cap 4,000,000,000 IGN, halving every two years. Devnet coins have no value.

-
-
-
-
- -
-
-
-
Find your place

A whole network to look round.

-
-
-

Keep what you earned.

A desktop wallet that checks finality itself. Block rewards and proving payouts in one history.

The wallet
-

See what your card does here.

Measured hash rates per card, with the watts, the date and the log entry behind each number.

The bench table
-

Understand the design.

Mining, proving, finality, the economics and the open questions, in one readable paper.

Read the litepaper
-
-
-
- -
-
-
-
Igneum Ember · one click

Install. Start. The card mines.

- Everything about the miner -
- -

Devnet. Coins have no value and the chain may be reset.

-

Public testnet: not yet open; the devnet build is here for people who want to look. The public testnet is weeks away: three seed nodes and the public RPC are up, and it opens when the go checklist closes.

-

Read more about the miner. The protocol carries no fee. The one payment to the project is the Ember software’s optional 1% dev fee, like other GPU miners, off with one flag. Download only from this domain. Nobody from Igneum will ask for your seed. Questions: the Discord.

-
-
- -
-
-
-
-
Your next block starts here
-

Give your GPU something to do.

-

Download Ember, watch the devnet, and tell us what breaks. The people on the Discord run cards like yours.

-
-
- -

Experimental software. No promise of earnings.

-
-
-
-
- -
-
-
-

Every criticism, answered or conceded, and what Igneum does not claim.

- The ledger -
-
-
+
The economic design, as published

Made for the people running the hardware.

Every block pays the card that found it and the cards that prove it. The protocol carves out nothing for a team, a foundation or a fund. No premine, no stake.

Each block, by the rule
100%to miners and provers
80% Miningto the card that found the block
20% Provingto the cards that prove it
0% Treasurynothing to any team

The protocol rule, not an income forecast. Cap 4,000,000,000 IGN, halving every two years. Devnet coins have no value.

+
Find your place

A whole network to look round.

Keep what you earned.

A desktop wallet that checks finality itself. Block rewards and proving payouts in one history.

The wallet

See what your card does here.

Measured hash rates per card, with the watts, the date and the log entry behind each number.

The bench table

Understand the design.

Mining, proving, finality, the economics and the open questions, in one readable paper.

Read the litepaper
+
Igneum Ember · one click

Install. Start. The card mines.

Everything about the miner

Devnet. Coins have no value and the chain may be reset.

Public testnet: not yet open; the devnet build is here for people who want to look. The public testnet is weeks away: three seed nodes and the public RPC are up, and it opens when the go checklist closes.

Read more about the miner. The protocol carries no fee. The one payment to the project is the Ember software’s optional 1% dev fee, like other GPU miners, off with one flag. Download only from this domain. Nobody from Igneum will ask for your seed. Questions: the Discord.

+
Your next block starts here

Give your GPU
something to do.

Download Ember, watch the devnet, and tell us what breaks. The people on the Discord run cards like yours.

Experimental software. No promise of earnings.

+

Every criticism, answered or conceded, and what Igneum does not claim.

The ledger
- - + @@ -400,7 +260,7 @@ function fmtHash(h){if(h===null||h===undefined||!(h>0))return 'pending';return h>=1e9?(h/1e9).toFixed(1)+' GH/s':(h/1e6).toFixed(1)+' MH/s';} var cap=document.getElementById('steps-caption'),st=document.getElementById('dag-state'); // the feed's state is the caption while the feed is not live; the scene writes the caption while it is - function onState(s,r){st.className='state '+(s==='live'?'live':s==='failed'?'bad':'');st.innerHTML=''+(s==='live'?'live devnet':s==='connecting'?'connecting to the devnet observer':r||s); + function onState(s,r){st.className='pill'+(s==='live'?' warm':'');st.textContent=(s==='live'?'live devnet':s==='connecting'?'connecting':r||s); if(s==='live')return;cap.classList.add('muted');cap.textContent=s==='connecting'?'Connecting to the devnet observer.':s==='stale'?(r.charAt(0).toUpperCase()+r.slice(1))+'. The scene holds the last stored blocks.':'Live feed unavailable. The scene holds its last blocks; the live page shows the last stored state.';} function onData(d){var s=d.state,pv=d.proving; document.getElementById('f-hash').textContent=fmtHash(s.hashes_per_second_estimate); diff --git a/site/journey.html b/site/journey.html index 7e03ba22c..e2896c285 100644 --- a/site/journey.html +++ b/site/journey.html @@ -31,6 +31,7 @@ + \ No newline at end of file diff --git a/site/partials/nav.html b/site/partials/nav.html index c24e6b4e8..5b195f3aa 100644 --- a/site/partials/nav.html +++ b/site/partials/nav.html @@ -1,9 +1,10 @@ diff --git a/site/provenance.html b/site/provenance.html index 9bf13d1c6..500f792bb 100644 --- a/site/provenance.html +++ b/site/provenance.html @@ -31,6 +31,7 @@ +

${r} at ${width}px: the package (left) and the served page (right)

package
served
`; + const tmp = join(out, `${name}-sheet.html`); writeFileSync(tmp, html); + await page.setViewportSize({ width: width * 2 + 72, height: 900 }); + await page.goto(pathToFileURL(tmp).href, { waitUntil: 'load' }); await page.waitForTimeout(400); + try { await page.screenshot({ path: sheet, fullPage: true, timeout: 30000 }); } catch (e) { console.error(`${r} sheet: ${e.message}`); } + await page.setViewportSize({ width, height: 900 }); + sheets.push(sheet); console.log(sheet); +} +await browser.close(); +console.log(`${sheets.length} sheets in ${out}`); diff --git a/tools/workers/collect.mjs b/tools/workers/collect.mjs index 42131a8c8..fad99beb4 100644 --- a/tools/workers/collect.mjs +++ b/tools/workers/collect.mjs @@ -18,7 +18,7 @@ import { spawnSync } from 'node:child_process'; import { hostname } from 'node:os'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { parseProcStat, cpuBusy, parseMeminfo, parseNetDev, parseLoadavg, parseLockDir, parseBuildsJsonl, parseSccacheStats, pickTemps, cargoProcess, deriveKind, BASELINES } from './lib.mjs'; +import { parseProcStat, cpuBusy, parseMeminfo, parseNetDev, parseLoadavg, parseLockDir, parseBuildsJsonl, parseSccacheStats, pickTemps, cargoProcess, deriveKind, parseSlotLine, BASELINES } from './lib.mjs'; const HERE = dirname(fileURLToPath(import.meta.url)); const argv = process.argv.slice(2); @@ -68,6 +68,7 @@ function slots(now) { } function procs(bootSec, clk, now) { const running = [], waiters = []; let compilers = 0; + const flockParents = new Map(); // ppid -> since, from the flock -w processes the waiters run for (const d of safeList('/proc')) { if (!/^\d+$/.test(d)) continue; const pid = Number(d); const comm = read(`/proc/${d}/comm`).trim(); @@ -82,17 +83,26 @@ function procs(bootSec, clk, now) { if (args[1] === '-w' && /^\d+$/.test(args[2] || '')) { const stat = read(`/proc/${d}/stat`); const rest = stat.slice(stat.lastIndexOf(')') + 2).split(' '); const ppid = Number(rest[1]); const startTicks = Number(rest[19]); - const since = Number.isFinite(startTicks) ? iso((bootSec + startTicks / clk) * 1000) : null; - const wait = read(join(LOCKS, `wait-${ppid}`)).trim(); // a waiter's own line, when the appender writes one - waiters.push({ pid: ppid, since, waiting_s: since ? Math.max(0, Math.round((now - Date.parse(since)) / 1000)) : null, label: wait || null, ...(wait ? { kind: deriveKind(parseLabelSafe(wait)) } : {}) }); + flockParents.set(ppid, Number.isFinite(startTicks) ? iso((bootSec + startTicks / clk) * 1000) : null); } } } - // the innermost cargo per worktree: a `cargo build` spawns no nested cargo, but a `cargo test` runs test binaries, not cargo + // the waiters' own lines: wait- (a queued build) and gate-pending- (a gate that takes the next free slot + // ahead of queued suites and benches), written by remote-run.sh in the slot-file format and removed on take or give-up; + // a file whose pid is gone is a leftover, not a waiter + const seen = new Set(); + for (const f of safeList(LOCKS)) { + const m = /^(wait|gate-pending)-(\d+)$/.exec(f); if (!m) continue; + const pid = Number(m[2]); if (!alive(pid) || seen.has(pid)) continue; seen.add(pid); + const line = parseSlotLine(read(join(LOCKS, f)).split('\n')[0], { now }); + const since = (line && line.since) || flockParents.get(pid) || null; + waiters.push({ pid, since, waiting_s: since ? Math.max(0, Math.round((now - Date.parse(since)) / 1000)) : null, label: line ? line.label : null, kind: line ? deriveKind(line) : null, agent: line ? line.agent : null, worktree: line ? line.worktree : null, crate: line ? line.crate : null, command: line ? line.command : null, nice: line ? line.nice : null, cores: line ? line.cores : null, priority: m[1] === 'gate-pending' ? 'gate' : 'normal' }); + } + for (const [ppid, since] of flockParents) if (!seen.has(ppid)) waiters.push({ pid: ppid, since, waiting_s: since ? Math.max(0, Math.round((now - Date.parse(since)) / 1000)) : null, label: null, priority: 'normal' }); + waiters.sort((a, b) => (a.priority === 'gate' ? 0 : 1) - (b.priority === 'gate' ? 0 : 1) || String(a.since || '').localeCompare(String(b.since || ''))); running.sort((a, b) => String(a.started_at).localeCompare(String(b.started_at))); return { running, waiters, compilers }; } -function parseLabelSafe(line) { const r = /: (.*)$/.exec(line); return { command: r ? r[1] : line }; } function sccache() { const bin = existsSync('/home/build/.cargo/bin/sccache') ? '/home/build/.cargo/bin/sccache' : 'sccache'; @@ -121,6 +131,7 @@ export function collect() { for (const r of p.running) { const h = s.held.find(h => h.worktree === r.worktree && (!h.crate || h.crate === r.crate || !r.crate)); r.slot = h ? h.slot : null; r.agent = h ? h.agent : null; r.queued_at = h ? h.since : null; r.waited_s = h ? h.waited_s : null; + if (h && h.class) r.kind = h.class; r.nice = h ? h.nice : null; r.cores = h ? h.cores : null; } const logText = read(LOG); const recent = parseBuildsJsonl(logText, { limit: 200 }); diff --git a/tools/workers/lib.mjs b/tools/workers/lib.mjs index 71f4ad550..52063e9fd 100644 --- a/tools/workers/lib.mjs +++ b/tools/workers/lib.mjs @@ -65,15 +65,28 @@ export function parseSlotLine(line, { day, now } = {}) { const d = day || new Date(nowMs).toISOString().slice(0, 10); let since = Date.parse(`${d}T${r[2]}:${r[3]}:${r[4]}Z`); if (since > nowMs + 60_000) since -= 86_400_000; - const label = r[6].trim(); - const agent = (/;\s*agent=([^;]+)\s*$/.exec(label) || [])[1] || null; - return { pid: Number(r[1]), since: new Date(since).toISOString().replace(/\.\d{3}Z$/, 'Z'), waited_s: Number(r[5]), label: agent ? label.replace(/;\s*agent=[^;]+\s*$/, '').trim() : label, agent, ...splitLabel(label) }; + const parts = splitLabelParts(r[6]); const sl = splitLabel(parts.label); + return { pid: Number(r[1]), since: new Date(since).toISOString().replace(/\.\d{3}Z$/, 'Z'), waited_s: Number(r[5]), label: parts.label, agent: parts.agent, class: parts.class, nice: parts.nice, cores: parts.cores, ...sl, kind: parts.class || deriveKind({ command: sl.command, crate: sl.crate, target: sl.target }) }; +} + +/// The label's trailing segments (7 October 2026, build box scheduling classes): "