diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d495af3d9..9d37d7dc5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,6 +65,10 @@ jobs: run: bash tools/ci/copied-sources-check.sh - name: pinned guest programs match their manifest and are built only by pin-guests.sh run: bash tools/ci/pinned-guests-check.sh + - name: faucet unit tests (validation, the daily limits, the signed transaction; keccak, RLP and secp256k1 vectors) + run: node --test site/api/faucet.test.mjs + - name: ship tool self-test (version bump, the dl-both and public manifest helpers) + run: node tools/ship-app.mjs --self-test - name: relay unit tests (parsers, secret compare, the wake endpoint) run: node --test relay/test/parse.test.mjs relay/test/auth.test.mjs relay/test/wake.test.mjs - name: miner app notice strip and update card (ordering, keys, wording, timers, when the card shows) diff --git a/packaging/README-ship.md b/packaging/README-ship.md index 45119ffea..ec3893c9a 100644 --- a/packaging/README-ship.md +++ b/packaging/README-ship.md @@ -42,6 +42,28 @@ The six version files: `app/igneum-app/Cargo.toml`, `app/igneum-app/Cargo.lock`, | `--min-supported`, `--activation-height`, `--deadline-note`, `--channel` | passed to `publish-manifest.sh` | | `--rebuild` | build the DMG again even when a fresh one exists | | `--branch ` | accept another branch than master (a dry run from a worktree; the Windows build still runs on pushes to master) | +| `--public` | the manifest step also publishes the version into `dl/public/` (no token in any URL; its own signed manifest; the `/public/` aliases rewritten), the same deploy carries it, verify checks every public file and alias. The token folders are untouched | + +## The public downloads path (5 October 2026, testnet launch) + +`dl/public/` in the downloads folder holds only the current installers, the HiveOS package and the two signed manifests +(app and wallet) with URLs under `https://dl.igneum.network/dl/public/`, plus an unsigned index `igneum-downloads.json` +that the site build reads. Four stable aliases are rewrites in the downloads folder's `vercel.json`, written from what the +folder holds, so they follow every release by themselves: + +| Alias | Rewrites to | +|---|---| +| `https://dl.igneum.network/public/igneum-miner-windows.exe` | `dl/public/Igneum-Miner-Setup-.exe` | +| `https://dl.igneum.network/public/igneum-miner-mac.dmg` | `dl/public/Igneum-Miner-.dmg` | +| `https://dl.igneum.network/public/igneum-miner-hive.tar.gz` | `dl/public/igneum-hive-.tar.gz` | +| `https://dl.igneum.network/public/igneum-wallet-mac.dmg` | `dl/public/Igneum-Wallet-.dmg` | + +`packaging/ota/publish-public.sh --app | --wallet | --hive [--deploy] [--verify] [--dry-run]` does the work; +`publish-manifest.sh --public` and `ship-app.mjs --public` call it, so every future release lands there too. The +HiveOS package comes from `packaging/hive/make-hive-package.sh` (Linux node and miner from a PC `build` job, the two GPU +workers from `infra/cross/build-workers-linux.sh`) and is published with `--hive`. The site (`site/build.mjs`) reads the +index at build time and stamps every download button with the version and size; `TESTNET_OPEN` there removes the +"Public testnet: not yet open" line on the go. ## When a step fails diff --git a/packaging/ota/publish-manifest.sh b/packaging/ota/publish-manifest.sh index ce5ed9d79..33e3786cd 100755 --- a/packaging/ota/publish-manifest.sh +++ b/packaging/ota/publish-manifest.sh @@ -11,6 +11,9 @@ # consensus.override: the exact object every app writes to its override.json (the node's # --override-params-file), so it carries EVERY height switch, not just the new one; # carried over from the current manifest when not given +# [--public] also publish into dl/public/ (no token: the site's download +# links, packaging/ota/publish-public.sh --app, plus --wallet +# when the wallet manifest is in the folder); the same deploy # [--tuning tuning.json | --no-tuning] the fleet's per-card kernel tuning (tools/tuning.mjs writes it; # docs/design/miner-tuning.md); carried over from the current # manifest when not given, as is consensus.override @@ -36,7 +39,7 @@ TOKEN_FILE="$HOME/.config/igneum/dl-token" SIGNER="$ROOT/app/igneum-app/target/release/igneum-ota-sign" VERSION="" MAC="" WIN="" NOTES="" ACTIVATION="" DEADLINE="" MIN_SUPPORTED="" CHANNEL="devnet" BASE="" DEST="" DEPLOY=0 VERIFY_ONLY=0 TRIES=12 -OVERRIDE="" TUNING_FILE="" NO_TUNING=0 +OVERRIDE="" TUNING_FILE="" NO_TUNING=0 PUBLIC=0 while [ $# -gt 0 ]; do case "$1" in --version) VERSION="$2"; shift 2 ;; @@ -54,6 +57,7 @@ while [ $# -gt 0 ]; do --dest) DEST="$2"; shift 2 ;; --deploy) DEPLOY=1; shift ;; --no-deploy) DEPLOY=0; shift ;; + --public) PUBLIC=1; shift ;; # dl/public/ too (publish-public.sh --app [--wallet]); needs the real downloads folder --verify-only) VERIFY_ONLY=1; shift ;; # no write, no deploy: check the live manifest against the one in the folder --tries) TRIES="$2"; shift 2 ;; *) echo "unknown argument: $1" >&2; exit 2 ;; @@ -200,12 +204,20 @@ cat "$DEST/igneum-app-latest.json"; echo echo "signature: $(cat "$DEST/igneum-app-latest.json.sig")" echo "key fingerprint: $("$SIGNER" fingerprint "$PUB" | tail -1)" +# --public: the same version into dl/public/ with public URLs (its own signed manifest), the aliases rewritten +if [ "$PUBLIC" = 1 ]; then + [ -n "$DLSITE" ] || { echo "--public needs the real downloads folder (no --dest)" >&2; exit 1; } + pub_args=(--app); [ -f "$DEST/igneum-wallet-latest.json" ] && pub_args+=(--wallet) + "$HERE/publish-public.sh" "${pub_args[@]}" || { echo "publish-public.sh failed; dl/public/ not updated" >&2; exit 1; } +fi + if [ "$DEPLOY" = 1 ]; then [ -n "$DLSITE" ] || { echo "--deploy needs the real downloads folder (no --dest)" >&2; exit 1; } echo "deploying $DLSITE" (cd "$DLSITE" && npx --yes vercel@latest --global-config "$HOME/.config/igneum/vercel" deploy --prod --yes 2>&1 | sed "s#$TOKEN##g"; exit "${PIPESTATUS[0]}") \ || { echo "the deploy failed (the Vercel CLI's exit status above); nothing verified" >&2; exit 1; } verify_live_manifest || exit 1 + if [ "$PUBLIC" = 1 ]; then "$HERE/publish-public.sh" --verify --no-prune || exit 1; fi # the console's Builds tab (relay/): one build event and a fresh downloads listing; never fatal node "$ROOT/tools/console.mjs" post --kind build --title "OTA manifest $VERSION ($CHANNEL) live" --body "$NOTES" >/dev/null 2>&1 || true node "$ROOT/tools/console.mjs" sync-dl >/dev/null 2>&1 || true diff --git a/packaging/ota/publish-public.sh b/packaging/ota/publish-public.sh new file mode 100755 index 000000000..0f8b67b00 --- /dev/null +++ b/packaging/ota/publish-public.sh @@ -0,0 +1,253 @@ +#!/usr/bin/env bash +# The PUBLIC downloads path (5 October 2026, testnet launch): dl/public/ in the downloads folder holds only the current +# installers, the HiveOS package and the two signed manifests, with no token in any URL, plus one stable alias per +# platform that the site links: +# +# https://dl.igneum.network/public/igneum-miner-windows.exe -> dl/public/Igneum-Miner-Setup-.exe +# https://dl.igneum.network/public/igneum-miner-mac.dmg -> dl/public/Igneum-Miner-.dmg +# https://dl.igneum.network/public/igneum-miner-hive.tar.gz -> dl/public/igneum-hive-.tar.gz +# https://dl.igneum.network/public/igneum-wallet-mac.dmg -> dl/public/Igneum-Wallet-.dmg +# +# The aliases are rewrites in /vercel.json, written by this script from what dl/public/ holds, so they can +# never point at a file that is not there and they follow every release by themselves (the bytes exist once). +# Nothing is ever removed from the token folders; the public folder is pruned to the current files only. +# +# packaging/ota/publish-public.sh --app copy the files named by dl//igneum-app-latest.json, write +# dl/public/igneum-app-latest.json(.sig) with public URLs, signed +# packaging/ota/publish-public.sh --wallet the same for igneum-wallet-latest.json +# packaging/ota/publish-public.sh --hive .tar.gz> copy the HiveOS package (plus its .sha256) +# packaging/ota/publish-public.sh --aliases only rewrite vercel.json from the folder (runs after every step above) +# --dry-run read everything, print the plan, write nothing --deploy deploy the downloads folder afterwards +# --verify HEAD every public file and alias, GET the manifests (also after --deploy) --no-prune keep old files +# --dest --base-url a scratch folder instead of the downloads folder (tests) +# +# publish-manifest.sh --public calls this with --app (and --wallet when that manifest exists), so every future +# release lands here too. Reads ~/.config/igneum/dlsite-dir, dl-token (fresh on every run; it is being rotated), +# ota-signing-key(.pub); never prints a token. +set -euo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$HERE/../.." && pwd)" +CFG="$HOME/.config/igneum" +KEY="$CFG/ota-signing-key"; PUB_KEY="$CFG/ota-signing-key.pub" +SIGNER="${IGNEUM_OTA_SIGN:-$ROOT/app/igneum-app/target/release/igneum-ota-sign}" # a built signer elsewhere (another worktree) +HOST="https://dl.igneum.network" + +DO_APP=0 DO_WALLET=0 HIVE="" DO_ALIASES=0 DRY=0 DEPLOY=0 VERIFY=0 PRUNE=1 DEST="" BASE="" TRIES=12 +while [ $# -gt 0 ]; do + case "$1" in + --app) DO_APP=1; shift ;; + --wallet) DO_WALLET=1; shift ;; + --hive) HIVE="$2"; shift 2 ;; + --aliases) DO_ALIASES=1; shift ;; + --dry-run) DRY=1; shift ;; + --deploy) DEPLOY=1; shift ;; + --verify) VERIFY=1; shift ;; + --no-prune) PRUNE=0; shift ;; + --dest) DEST="$2"; shift 2 ;; + --base-url) BASE="$2"; shift 2 ;; + --tries) TRIES="$2"; shift 2 ;; + *) echo "unknown argument: $1" >&2; exit 2 ;; + esac +done +[ "$DO_APP$DO_WALLET$DO_ALIASES$VERIFY" != 0000 ] || [ -n "$HIVE" ] || { echo "nothing to do: --app, --wallet, --hive , --aliases or --verify" >&2; exit 2; } +command -v python3 >/dev/null || { echo "python3 is needed" >&2; exit 1; } +[ -x "$SIGNER" ] || { echo "no $SIGNER (cargo build --release --bin igneum-ota-sign in app/igneum-app)" >&2; exit 1; } + +TOKEN_FILE="$CFG/dl-token" +[ -f "$TOKEN_FILE" ] || { echo "no $TOKEN_FILE" >&2; exit 1; } +TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")" +if [ -z "$DEST" ]; then + DLSITE="${IGNEUM_DLSITE:-}" + [ -n "$DLSITE" ] || { [ -f "$CFG/dlsite-dir" ] && DLSITE="$(tr -d '[:space:]' < "$CFG/dlsite-dir")"; } || true + [ -n "$DLSITE" ] && [ -d "$DLSITE/dl/$TOKEN" ] || { echo "no downloads folder: ~/.config/igneum/dlsite-dir must hold dl//" >&2; exit 1; } +else + DLSITE="$DEST"; mkdir -p "$DLSITE/dl/$TOKEN" +fi +SRC="$DLSITE/dl/$TOKEN" +PUB="$DLSITE/dl/public" +[ -n "$BASE" ] || BASE="$HOST" +BASE="${BASE%/}" +BASE_PUB="$BASE/dl/public" +scrub() { sed "s#$TOKEN##g"; } +log() { printf '%s\n' "$*" | scrub; } +sha() { "$SIGNER" sha256 "$1" | cut -d' ' -f1; } +[ "$DRY" = 1 ] || mkdir -p "$PUB" + +# one manifest: copy the files it names from the token folder, rewrite every URL to the public base, sign, verify +publish_manifest() { # + local name="$1" src="$SRC/$1" out="$PUB/$1" files f sum + [ -f "$src" ] || { log "no $src: nothing to publish for $name"; return 1; } + files="$(python3 -c 'import json,sys; m=json.load(open(sys.argv[1])); print("\n".join(e["url"].rsplit("/",1)[1] for e in m.get("platforms",{}).values()))' "$src")" + for f in $files; do + [ -f "$SRC/$f" ] || { log "ERROR: $name names $f but dl//$f is not there" >&2; return 1; } + sum="$(python3 -c 'import json,sys; m=json.load(open(sys.argv[1])); print([e["sha256"] for e in m["platforms"].values() if e["url"].endswith("/"+sys.argv[2])][0])' "$src" "$f")" + [ "$(sha "$SRC/$f")" = "$sum" ] || { log "ERROR: dl//$f does not match the sha256 in $name" >&2; return 1; } + if [ -f "$PUB/$f" ] && [ "$(sha "$PUB/$f")" = "$sum" ]; then log " $f: already in dl/public/ (same sha256)" + elif [ "$DRY" = 1 ]; then log " $f: would copy into dl/public/ ($(stat -f %z "$SRC/$f") B)" + else cp "$SRC/$f" "$PUB/$f.part" && mv "$PUB/$f.part" "$PUB/$f"; log " $f: copied into dl/public/ ($(stat -f %z "$PUB/$f") B)"; fi + done + # the public manifest: the same fields, URLs under the public base, canonical bytes, our signature + local tmp; tmp="$(mktemp)" + python3 - "$src" "$tmp" "$BASE_PUB" <<'PY' +import json, sys +src, out, base = sys.argv[1:4] +m = json.load(open(src)) +for e in m.get("platforms", {}).values(): + e["url"] = base + "/" + e["url"].rsplit("/", 1)[1] +open(out, "w").write(json.dumps(m, sort_keys=True, separators=(",", ":"), ensure_ascii=False)) +PY + if grep -q "$TOKEN" "$tmp"; then rm -f "$tmp"; log "ERROR: the public $name would still carry the token" >&2; return 1; fi + if [ "$DRY" = 1 ]; then + log " $name: would write $(python3 -c 'import json,sys; m=json.load(open(sys.argv[1])); print(m.get("version"), "+".join(m.get("platforms",{})))' "$tmp") with URLs under $BASE_PUB, signed" + rm -f "$tmp"; return 0 + fi + "$SIGNER" sign "$KEY" "$tmp" > "$tmp.sig" + "$SIGNER" verify "$PUB_KEY" "$tmp" "$tmp.sig" >/dev/null + chmod 644 "$tmp" "$tmp.sig"; mv "$tmp" "$out"; mv "$tmp.sig" "$out.sig" + log " $name: $(python3 -c 'import json,sys; m=json.load(open(sys.argv[1])); print(m.get("version"), "+".join(m.get("platforms",{})))' "$out") written, signed, verified; URLs under $BASE_PUB" +} + +if [ "$DO_APP" = 1 ]; then log "app manifest -> dl/public/"; publish_manifest igneum-app-latest.json; fi +if [ "$DO_WALLET" = 1 ]; then log "wallet manifest -> dl/public/"; publish_manifest igneum-wallet-latest.json; fi +if [ -n "$HIVE" ]; then + [ -f "$HIVE" ] || { echo "missing: $HIVE" >&2; exit 1; } + hn="$(basename "$HIVE")" + case "$hn" in igneum-hive-*.tar.gz) ;; *) echo "$HIVE: the HiveOS package is igneum-hive-.tar.gz (packaging/hive/make-hive-package.sh)" >&2; exit 1 ;; esac + log "HiveOS package -> dl/public/" + if [ -f "$PUB/$hn" ] && [ "$(sha "$PUB/$hn")" = "$(sha "$HIVE")" ]; then log " $hn: already in dl/public/ (same sha256)" + elif [ "$DRY" = 1 ]; then log " $hn: would copy into dl/public/ ($(stat -f %z "$HIVE") B, sha256 $(sha "$HIVE"))" + else cp "$HIVE" "$PUB/$hn.part" && mv "$PUB/$hn.part" "$PUB/$hn"; "$SIGNER" sha256 "$PUB/$hn" | awk -v n="$hn" '{print $1 " " n}' > "$PUB/$hn.sha256"; log " $hn: copied into dl/public/ ($(stat -f %z "$PUB/$hn") B, sha256 $(sha "$PUB/$hn"))"; fi +fi + +# the aliases: what the two manifests and the newest HiveOS package in dl/public/ name, nothing else +ALIASES_JSON="$(python3 - "$PUB" "$DRY" <<'PY' +import json, os, re, sys +pub, dry = sys.argv[1], sys.argv[2] == "1" +def entry(name, plat): + p = os.path.join(pub, name) + if not os.path.exists(p): return None + e = json.load(open(p)).get("platforms", {}).get(plat) + if not e: return None + f = e["url"].rsplit("/", 1)[1] + return f if os.path.exists(os.path.join(pub, f)) or dry else None +hive = sorted([f for f in os.listdir(pub) if re.fullmatch(r"igneum-hive-\d+\.\d+\.\d+\.tar\.gz", f)], + key=lambda f: tuple(int(x) for x in re.findall(r"\d+", f)[:3])) if os.path.isdir(pub) else [] +aliases = { + "igneum-miner-windows.exe": entry("igneum-app-latest.json", "windows"), + "igneum-miner-mac.dmg": entry("igneum-app-latest.json", "mac"), + "igneum-miner-hive.tar.gz": hive[-1] if hive else None, + "igneum-wallet-mac.dmg": entry("igneum-wallet-latest.json", "mac"), +} +print(json.dumps(aliases)) +PY +)" +KEEP="$(python3 -c 'import json,sys; a=json.loads(sys.argv[1]); print(" ".join(v for v in a.values() if v))' "$ALIASES_JSON")" +log "aliases (vercel.json rewrites under /public/):" +python3 -c 'import json,sys; a=json.loads(sys.argv[1]); [print(" /public/%s -> %s" % (k, ("/dl/public/" + v) if v else "(no file yet; alias left out)")) for k,v in a.items()]' "$ALIASES_JSON" | scrub +VERCEL_JSON="$(python3 - "$ALIASES_JSON" <<'PY' +import json, sys +a = json.loads(sys.argv[1]) +rewrites = [{"source": "/public/" + k, "destination": "/dl/public/" + v} for k, v in a.items() if v] +cfg = { + "cleanUrls": False, + "trailingSlash": False, + "rewrites": rewrites, + "headers": [ + {"source": "/dl/public/(.*)\\.json", "headers": [{"key": "Cache-Control", "value": "public, max-age=60"}, {"key": "Access-Control-Allow-Origin", "value": "*"}]}, + {"source": "/dl/public/(.*)\\.sig", "headers": [{"key": "Cache-Control", "value": "public, max-age=60"}, {"key": "Access-Control-Allow-Origin", "value": "*"}]}, + {"source": "/public/(.*)", "headers": [{"key": "Cache-Control", "value": "public, max-age=300"}, {"key": "X-Content-Type-Options", "value": "nosniff"}]}, + ], +} +print(json.dumps(cfg, indent=2)) +PY +)" +if [ "$DRY" = 1 ]; then log " would write $DLSITE/vercel.json ($(printf '%s' "$VERCEL_JSON" | grep -c '"source"') rules)" +else printf '%s\n' "$VERCEL_JSON" > "$DLSITE/vercel.json.new" && mv "$DLSITE/vercel.json.new" "$DLSITE/vercel.json"; log " wrote $DLSITE/vercel.json"; fi + +# the index the site build reads (unsigned, a convenience: alias, file, version, size, sha256 per platform; the signed +# manifests stay the source of truth for the apps) +if [ "$DRY" = 0 ]; then + python3 - "$PUB" "$ALIASES_JSON" "$BASE" <<'PYIDX' +import json, os, re, sys, hashlib, datetime +pub, aliases, base = sys.argv[1], json.loads(sys.argv[2]), sys.argv[3] +def sha(p): + h = hashlib.sha256() + with open(p, "rb") as f: + for chunk in iter(lambda: f.read(1 << 20), b""): h.update(chunk) + return h.hexdigest() +def version_of(name, f): + if name.startswith("igneum-miner-hive"): return ".".join(re.findall(r"\d+", f)[:3]) + m = json.load(open(os.path.join(pub, "igneum-wallet-latest.json" if name.startswith("igneum-wallet") else "igneum-app-latest.json"))) + return m.get("version") +keys = {"igneum-miner-windows.exe": "miner-windows", "igneum-miner-mac.dmg": "miner-mac", "igneum-miner-hive.tar.gz": "miner-hive", "igneum-wallet-mac.dmg": "wallet-mac"} +files = {} +for alias, f in aliases.items(): + if not f: continue + p = os.path.join(pub, f) + files[keys[alias]] = {"alias": "/public/" + alias, "file": f, "path": "/dl/public/" + f, "version": version_of(alias, f), "size": os.path.getsize(p), "sha256": sha(p)} +out = {"updated": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "base": base, "files": files} +tmp = os.path.join(pub, "igneum-downloads.json.new") +open(tmp, "w").write(json.dumps(out, indent=2, sort_keys=True) + "\n"); os.chmod(tmp, 0o644); os.replace(tmp, os.path.join(pub, "igneum-downloads.json")) +print(" wrote dl/public/igneum-downloads.json (%d platform(s))" % len(files)) +PYIDX +fi + +# prune: dl/public/ holds only the current files (the manifests, what they name, the newest HiveOS package and its sha256) +if [ "$PRUNE" = 1 ] && [ -d "$PUB" ]; then + for f in "$PUB"/*; do + [ -f "$f" ] || continue + n="$(basename "$f")" + case "$n" in igneum-app-latest.json|igneum-app-latest.json.sig|igneum-wallet-latest.json|igneum-wallet-latest.json.sig|igneum-downloads.json) continue ;; esac + keep=0; for k in $KEEP; do [ "$n" = "$k" ] || [ "$n" = "$k.sha256" ] && keep=1; done + if [ "$keep" = 0 ]; then + if [ "$DRY" = 1 ]; then log " would remove $n from dl/public/ (not current)"; else rm -f "$f"; log " removed $n from dl/public/ (not current)"; fi + fi + done +fi +if [ "$DRY" = 1 ]; then log "dry run: nothing written"; fi + +if [ "$DEPLOY" = 1 ]; then + [ "$DRY" = 0 ] || { echo "--deploy and --dry-run together make no sense" >&2; exit 2; } + [ -z "$DEST" ] || { echo "--deploy needs the real downloads folder (no --dest)" >&2; exit 1; } + log "deploying $DLSITE" + (cd "$DLSITE" && npx --yes vercel@latest --global-config "$CFG/vercel" deploy --prod --yes 2>&1 | scrub; exit "${PIPESTATUS[0]}") \ + || { echo "the deploy failed (the Vercel CLI's exit status above); nothing verified" >&2; exit 1; } + VERIFY=1 +fi + +if [ "$VERIFY" = 1 ]; then + fail=0; t=0 + check_one() { # -> 0 when HEAD is 200 with the local size + local url="$1" f="$2" hdr code len size + size="$(stat -f %z "$f")" + hdr="$(curl -sI -L -H 'Cache-Control: no-cache' "$url" 2>/dev/null | tr -d '\r')" + code="$(printf '%s\n' "$hdr" | awk 'toupper($1) ~ /^HTTP\// {c=$2} END{print c+0}')" + len="$(printf '%s\n' "$hdr" | awk 'tolower($1)=="content-length:"{l=$2} END{print l+0}')" + printf ' %s %s B %s (local %s B)\n' "$code" "$len" "$url" "$size" | scrub + [ "$code" = 200 ] && [ "$len" = "$size" ] + } + while [ "$t" -lt "$TRIES" ]; do + t=$((t + 1)); fail=0 + for f in "$PUB"/*; do + [ -f "$f" ] || continue + n="$(basename "$f")" + check_one "$BASE_PUB/$n" "$f" || fail=1 + case "$n" in igneum-app-latest.json|igneum-wallet-latest.json) + tmp="$(mktemp)"; curl -fsSL -H 'Cache-Control: no-cache' -o "$tmp" "$BASE_PUB/$n" 2>/dev/null || true + if cmp -s "$tmp" "$f" && curl -fsSL -H 'Cache-Control: no-cache' -o "$tmp.sig" "$BASE_PUB/$n.sig" 2>/dev/null && "$SIGNER" verify "$PUB_KEY" "$tmp" "$tmp.sig" >/dev/null 2>&1; then echo " $n: byte-identical, signature OK"; else echo " $n: NOT the local bytes yet (or the signature fails)"; fail=1; fi + rm -f "$tmp" "$tmp.sig" ;; + igneum-downloads.json) + tmp="$(mktemp)"; curl -fsSL -H 'Cache-Control: no-cache' -o "$tmp" "$BASE_PUB/$n" 2>/dev/null || true + if cmp -s "$tmp" "$f"; then echo " $n: byte-identical"; else echo " $n: NOT the local bytes yet"; fail=1; fi + rm -f "$tmp" ;; + esac + done + for pair in $(python3 -c 'import json,sys; a=json.loads(sys.argv[1]); print(" ".join(k+"="+v for k,v in a.items() if v))' "$ALIASES_JSON"); do + check_one "$BASE/public/${pair%%=*}" "$PUB/${pair#*=}" || fail=1 + done + [ "$fail" = 0 ] && break + [ "$t" -lt "$TRIES" ] && { echo " not all served yet (try $t of $TRIES); again in 10 s"; sleep 10; } + done + if [ "$fail" = 1 ]; then echo "public folder: NOT fully served after $t tries" >&2; exit 1; fi + echo "public folder verified: every file and alias answers 200 with the local size (try $t of $TRIES)" +fi diff --git a/site/404.html b/site/404.html index c2084f155..79b85452b 100644 --- a/site/404.html +++ b/site/404.html @@ -166,6 +166,7 @@ p{margin:0;color:var(--ink-2);max-width:52ch} The wallet GPU bench table The dev fee + Testnet faucet