#!/usr/bin/env bash
# install-harlequin-node.sh — ONE installer for a Harlequin follower node. Any supported box, one line.
#
# Served at https://harlequinproject.org/install-harlequin-node.sh and run as:
#
#   servers / PCs, as root:    curl -fsSL https://harlequinproject.org/install-harlequin-node.sh | bash
#   servers / PCs, with sudo:  curl -fsSL https://harlequinproject.org/install-harlequin-node.sh | sudo bash
#   only wget on the box:      wget -qO- https://harlequinproject.org/install-harlequin-node.sh | bash
#   (a bare Debian ships with NEITHER curl nor sudo: apt update && apt install -y curl, as root)
#   Android tablet/phone:      (Termux)  pkg install -y proot-distro && proot-distro install debian \
#                                        && proot-distro login debian
#                              (inside Debian, with your VPN on)
#                                        curl -fsSL https://harlequinproject.org/install-harlequin-node.sh | bash
#
# It detects your CPU (x86_64 / aarch64) and your init system, then does ONLY this:
#   1. downloads the right node binary + the launch chain spec from the published distribution.
#   2. VERIFIES the sha256 of BOTH against the pinned values below — ABORTS on any mismatch.
#   3. systemd host  → installs an unprivileged system service (survives reboots).
#      no systemd (Android proot, containers) → PORTABLE mode: installs under ~/harlequin and runs the
#      node in the foreground (Ctrl+C stops it; re-run `~/harlequin/run-node.sh` to start again).
#   Force portable mode on a systemd host with:  HLQ_PORTABLE=1
#
# OPSEC / sovereignty: this script NEVER asks for, captures, or generates your ACCOUNT secret. Your
# account (your mask in the society) is created by YOU in the browser (/rito) or with
# `harlequin-node key generate`, and stored in YOUR password manager. A node needs no account to sync
# and serve. Run it behind your VPN: a node announces its IP to peers.
#
# Idempotent: safe to re-run. Re-running re-verifies and restarts; it never overwrites an existing node-key.
set -euo pipefail

# The WHOLE script is one brace group (27-sep-2026). With `curl … | bash`, bash runs what it has read so far:
# a download cut halfway would have executed half an installer as root. A group must be read to its closing
# brace before any of it runs, so a truncated copy is a syntax error and does NOTHING.
{

# ─────────────────────────────────────────────────────────────────────────────
# Pinned distribution (re-pinned on every release; sha256 is the security boundary of this script).
DIST_BASE="https://harlequinproject.org"
SPEC_URL="$DIST_BASE/dist/mainnet-raw.json"

# SECOND PLACE TO DOWNLOAD FROM (2026-08-27). Everything you are about to fetch comes from ONE machine.
# If it is down, the network keeps running and existing nodes keep talking — but nobody NEW can join,
# because there is nowhere to get the program. That is the single point of failure, and it is not the
# network: it is the download.
#
# Setting this to a base URL makes the installer try there when the first place does not answer. It does
# NOT weaken anything: the sha256 pins above are still checked against whatever arrives, so a hostile
# mirror cannot hand you a different binary — the script would stop. A mirror adds a PLACE, not a trust.
#
# Empty on purpose while no mirror is published. Empty means "there is no second place", and the
# installer SAYS SO when the first one fails, instead of pretending it tried.
MIRROR_BASE=""
SPEC_SHA256="d346ed19abb8592175d4b9e5f01958868e271bfd76b2a0fe1d956ee1b7806372"  # REHEARSAL chainspec 3 (genesis 0xe6f67e52, launch candidate 7):

BIN_URL_x86_64="$DIST_BASE/dist/harlequin-node"
BIN_SHA_x86_64="df4f2283dca937bd3ca83bc98285490b9f4d893037ad6ed4c385931d8d36a863" # launch candidate 7 (tag candidato-7-20261010: earnable reputation, committee and slot leaders only with a bound node)
BIN_URL_aarch64="$DIST_BASE/dist/harlequin-node-arm64"
# Phones, tablets and Raspberry Pi get the SAME release as ordinary PCs: built from the same tag,
# checked the same way.
BIN_SHA_aarch64="08bbdf422029138a671864aa81ffe53e51647a0a659a7b8997df011ffa033fbf" # launch candidate 7 aarch64, cross-built from the same tag

# Baked into the spec too; passed explicitly in portable mode for first-dial robustness.
# THREE DOORS, NOT ONE (2026-07-26). Until today the installer handed out a single address, so every
# newcomer entered through the same machine — and when that machine choked (as it did today: 15-20 s to
# serve 5 KB, one peer left, falling behind the chain) the door was the network. A newcomer whose first
# contact times out does not try again. These are tried in order; any one of them is enough to join.
#
# THE HOME NODE IS DELIBERATELY ABSENT AND MUST STAY ABSENT. It reaches the network outbound-only over
# WireGuard and its residential address is never published. Listing it here would print someone's home
# IP in a script that anyone can download — the project's second golden rule. If you are tempted to
# "complete the list" some day: that is the reason it is incomplete on purpose.
BOOTNODE="/ip4/148.116.86.24/tcp/30333/p2p/12D3KooWBiZXWDXuXKzKw8f6Wpmo3Mx81oAgFu2VG1fsZihs3BHC"
BOOTNODE2="/ip4/151.145.42.146/tcp/30333/p2p/12D3KooWLinJp4ZZnrcGsqXprk7snpdC64KnnTPenN2CP356z37X"
# bind-tool (28-sep-2026): the NODE side of linking this node to YOUR mask — prints two PUBLIC values you paste
# in the villa; no account secret ever passes through it. Reproducible build (node/build-bindtool.sh), same rules.
BIND_URL_x86_64="$DIST_BASE/dist/harlequin-bind-tool"
BIND_SHA_x86_64="320d2f3634cbfc2473eb4bc6ec84b8f81dadbd32b4bcf38f3bedef9d9fa8e0be"
BIND_URL_aarch64="$DIST_BASE/dist/harlequin-bind-tool-arm64"
BIND_SHA_aarch64="c93d07598cf8a06998203494665249f9f1bfeb36e4b829cd1d133534f02860bf"
# harlequin-heartbeat (2026-10-02, hosting reward E1): the daily beat that gets a node BOUND to a mask paid for hosting.
# Empty sha = this release does not ship it yet: the installer skips the beat (never downloads an unpinned file).
HB_URL_x86_64="$DIST_BASE/dist/harlequin-heartbeat"
HB_SHA_x86_64="e0be017e113277dfcca3fa9c3122bd3c1cfb36942e89a5a9a3058994f31bd6c9"  # launch candidate 6 tree, node/build-heartbeat.sh (2026-10-09)
HB_URL_aarch64="$DIST_BASE/dist/harlequin-heartbeat-arm64"
HB_SHA_aarch64="9ba3eadf843e43896c7730a75365ab9f2595f85aceda7819c36d781c05e52daa"
SVC="harlequin-node"

# Weak-subjectivity checkpoint (M1) — re-pinned on EVERY release, next to the sha256 pins above.
# The pinned block must be FINALIZED. After install the running node is checked against it: if the
# chain your peers serve does not contain exactly this block hash at this height, the node is STOPPED
# (fail-closed). A long-range attacker can grow a longer fork from old keys; they cannot forge this pin.
# Before trusting a fresh copy of this script, verify these values against AT LEAST TWO independent
# sources: (1) this script over HTTPS, (2) https://harlequinproject.org/network.html, (3) a node
# operator you already trust. If the sources disagree — STOP, do not join.
# F4 NOTE: values below pin the LAUNCH chain at genesis. At the relaunch ceremony (F4) they are
# re-pinned to a fresh finalized checkpoint, and on every release thereafter.
# 2026-10-10: REHEARSAL CHAIN 3 (rehearsal 2 was 2026-10-08, rehearsal 1 2026-09-28). A brand-new chain has one block nobody can dispute: its genesis. Until
# the chain has settled, the pin is the genesis itself (height 0); it moves to a settled finalized
# block at the next release. The history below is the previous chain's.
# Re-pinned 2026-08-14 to block #188805, finalized and then left to settle: it sits roughly a thousand
# blocks below the finalized head, so it is not a fresh block that a reorganisation could still drop.
# It is also well past the runtime upgrade at #79501, so this pin alone proves the chain you joined is
# the one running the current runtime. Cross-verified on three independent nodes before pinning — by
# height and by hash, agreeing exactly — and the previous pin (#79830, 2026-07-29) was verified the
# same way.
CHECKPOINT_HEIGHT="0"
CHECKPOINT_HASH="0xe6f67e52a1449fb46c7006457cbdf36a066b8abf5b2e741fb903dbf78029f138"
CHECKPOINT_CHAIN="Harlequin Launch (cold-start)"  # system_chain name (spec id: hlq_launch)
CHECKPOINT_PINNED_AT="2026-10-10"  # re-pin to a fresher finalized block before each public release
RPC_URL="http://127.0.0.1:9944"   # node RPC is local-only by default; the check runs on YOUR box
# ─────────────────────────────────────────────────────────────────────────────

die() { echo "  ✗ $*" >&2; exit 1; }
ok()  { echo "  ✓ $*"; }
info(){ echo "    $*"; }

echo
echo "  ──────────────── ♦ ♥ ♠ ♣ ────────────────"
echo
echo "         H  A  R  L  E  Q  U  I  N"
echo "            node installer · mainnet"
echo
echo "  ─────────────────────────────────────────"
echo
echo "  A node is NOT your mask. Running a node sustains the network;"
echo "  it does not create your identity in the society. Your mask is"
echo "  made by you alone, in your browser, at:"
echo "      https://harlequinproject.org/rito   (EN)"
echo "      https://proyectoharlequin.org/rito  (ES)"
echo "  This script never asks for it and never touches it."
echo

# 1. CPU architecture → binary + sha
ARCH="$(uname -m)"
case "$ARCH" in
  x86_64)  BIN_URL="$BIN_URL_x86_64";  BIN_SHA256="$BIN_SHA_x86_64";  BIND_URL="$BIND_URL_x86_64";  BIND_SHA256="$BIND_SHA_x86_64"; HB_URL="$HB_URL_x86_64"; HB_SHA256="$HB_SHA_x86_64" ;;
  aarch64) BIN_URL="$BIN_URL_aarch64"; BIN_SHA256="$BIN_SHA_aarch64"; BIND_URL="$BIND_URL_aarch64"; BIND_SHA256="$BIND_SHA_aarch64"; HB_URL="$HB_URL_aarch64"; HB_SHA256="$HB_SHA_aarch64" ;;
  *) die "unsupported CPU '$ARCH' (supported: x86_64, aarch64)." ;;
esac
ok "cpu: $ARCH"

# 1b. Memory and disk preflight. MEASURED on 2026-07-30, same box, same path, only the RAM changed:
#     1 GB → 0.0 blocks/s, stuck at block 47,152, ~10 days to finish, and NO error: the node crawls in
#     silence and the person concludes the project is broken. 2 GB → 840-1,066 blocks/s, whole chain in
#     ~2 minutes — but the first sync peaked at 2,031 MB and leaned on that box's swap to get there.
#     Overnight watch (8 h) settled the shape: that same 2 GB box took swap to 509/512 MB and ~1,800
#     allocation stalls DURING the sync, then RECOVERED — swap back to 404 MB, stall counter frozen,
#     still in step with the chain, zero errors. So the pressure is the SYNC, not steady state.
#     2 GB with NO swap is UNMEASURED and is not published as a fact. A roomy box settles at
#     230-320 MB and never touches swap (measured over 7 h 46 m) — hence 4 GB as the recommendation.
MEM_MB=0; SWAP_MB=0
if [ -r /proc/meminfo ]; then
  MEM_MB="$(awk '/^MemTotal:/{printf "%d", $2/1024}' /proc/meminfo 2>/dev/null || echo 0)"
  SWAP_MB="$(awk '/^SwapTotal:/{printf "%d", $2/1024}' /proc/meminfo 2>/dev/null || echo 0)"
fi
if [ "${MEM_MB:-0}" -gt 0 ]; then
  # 2026-09-26: with the cache caps below (64 MiB trie + 128 MiB db), a node installed from scratch on a
  # machine capped at 1 GB synced the whole chain in ~55 min, verified the anchor and kept up with finality,
  # 0 OOM kills. The old 4 GB floor measured the SDK's default caches (1 GiB + 1024 MiB), not the node.
  if [ "$MEM_MB" -lt 900 ]; then
    echo
    echo "  ⚠ THIS MACHINE HAS ${MEM_MB} MB OF MEMORY. The measured floor is 1 GB: below it, nobody has"
    echo "    tested a full sync. It may work slowly or stall. Adding swap is the cheap way to be safe:"
    echo "        fallocate -l 1G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile"
    echo "    Continuing anyway in 15s — Ctrl+C to stop."
    echo
    sleep 15
  else
    ok "memory: ${MEM_MB} MB (measured: 1 GB syncs the whole chain in about an hour; steady state near 230 MB)"
  fi
fi
DISK_MB="$(df -Pm "${HOME:-/}" 2>/dev/null | awk 'NR==2{print $4}' || echo 0)"
if [ "${DISK_MB:-0}" -gt 0 ] && [ "$DISK_MB" -lt 4000 ]; then
  echo "  ⚠ only ${DISK_MB} MB free on this filesystem. A full archive grows ~15 GB/year; the light profile (chosen below on small disks) stays near 300 MB."
fi

# 1c. Storage profile (2026-10-01). Archive keeps every block and state forever: ~6 KB/block ≈ 43 MB/day ≈ 15 GB/year,
#     measured on the rehearsal chain. A follower does not need that: measured on a 1 GB box for 21.6 h with pruning
#     at only 2,400 blocks (8 h), finality stayed in step (lag 12-27 blocks) and the disk FELL to 136 MB once pruning
#     bit. The light profile keeps 28,800 blocks (4 days) — three times the window proven safe for the finality
#     committee read (#270). Validators and bootnodes keep archive: they serve history to newcomers.
#     HLQ_PRUNING=archive|light forces it; default auto = light on small machines (< 2 GB RAM or < 20 GB free).
PRUNING="${HLQ_PRUNING:-auto}"
case "$PRUNING" in
  auto) if [ "${MEM_MB:-0}" -gt 0 ] && [ "$MEM_MB" -lt 2048 ] || { [ "${DISK_MB:-0}" -gt 0 ] && [ "$DISK_MB" -lt 20000 ]; }; then PRUNING=light; else PRUNING=archive; fi;;
  archive|light) ;;
  *) die "HLQ_PRUNING must be auto, archive or light (got: $PRUNING)";;
esac
if [ "$PRUNING" = light ]; then
  PRUNE_ARGS="--state-pruning 28800 --blocks-pruning 28800"
  ok "storage: light (keeps the last 4 days; stays near 150-300 MB). HLQ_PRUNING=archive keeps everything."
else
  PRUNE_ARGS="--state-pruning archive --blocks-pruning archive"
  ok "storage: archive (keeps everything, ~15 GB/year). HLQ_PRUNING=light keeps only the last 4 days."
fi

# 2. install mode: systemd service vs portable foreground
MODE="portable"
if [ -z "${HLQ_PORTABLE:-}" ] && [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then
  MODE="service"
fi
ok "mode: $MODE"

# Downloader: curl if present, wget if not. A freshly installed Debian ships with NEITHER, and the
# published one-liner starts with curl — measured on a bare container: the stranger's very first
# command answers "curl: command not found" and explains nothing. Bootstrap what we can, and when we
# cannot, say what is missing and the exact command that fixes it.
command -v curl >/dev/null 2>&1 || {
  # bootstrap curl where we can (fresh proot Debian has apt and we are root there). SAY IT OUT LOUD:
  # installing packages in silence on the machine of someone who has just arrived and does not trust
  # us yet is the wrong first impression. If wget is here, we do not touch their system at all.
  if command -v wget >/dev/null 2>&1; then
    info "no curl here — using wget instead (nothing will be installed on your system)."
  elif command -v apt-get >/dev/null 2>&1 && [ "$(id -u)" -eq 0 ]; then
    info "no curl and no wget here — installing 'curl' and 'ca-certificates' with apt (say no by pressing Ctrl+C now)."
    sleep 3
    apt-get update -y >/dev/null 2>&1 || true
    DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates >/dev/null 2>&1 || true
  fi
}
if command -v curl >/dev/null 2>&1; then
  HAVE_DL="curl"
elif command -v wget >/dev/null 2>&1; then
  HAVE_DL="wget"
else
  die "no downloader found: this needs curl (or wget). On Debian/Ubuntu as root:  apt update && apt install -y curl ca-certificates"
fi
ok "downloader: $HAVE_DL"
command -v sha256sum >/dev/null 2>&1 || \
  die "sha256sum is missing — it is what proves the binary is the right one, so we do not go on without it. On Debian/Ubuntu as root:  apt update && apt install -y coreutils"

if [ "$MODE" = "service" ]; then
  [ "$(id -u)" -eq 0 ] || die "service install needs root (pipe to 'sudo bash'). Or force HLQ_PORTABLE=1."
  PREFIX="/opt/harlequin"
else
  PREFIX="${HOME:-/root}/harlequin"
  # Runtime libs the binary links against (readelf NEEDED: libstdc++.so.6, libgcc_s.so.1, libm/libc)
  # PLUS the dynamic loader (ld-linux-aarch64) and zlib. A fresh Termux `proot-distro install debian`
  # ships pared to the bone — without these the binary fails to exec with "required file not found"
  # (missing loader) or "error while loading shared libraries". Install them BEFORE running the binary.
  # `apt-get update` MUST succeed first (a bare rootfs has empty package lists); if it can't, say so
  # instead of silently proceeding to a binary that then won't start.
  if command -v apt-get >/dev/null 2>&1 && [ "$(id -u)" -eq 0 ]; then
    ok "installing runtime libraries (a fresh proot Debian ships without them)…"
    apt-get update -y >/dev/null 2>&1 || apt-get update -y >/dev/null 2>&1 \
      || die "apt-get update failed — the node's libraries can't be installed. Check the network/VPN inside the proot and re-run."
    DEBIAN_FRONTEND=noninteractive apt-get install -y \
        libc6 libstdc++6 libgcc-s1 zlib1g ca-certificates >/dev/null 2>&1 \
      || die "could not install runtime libraries (apt). Try: apt-get install -y libc6 libstdc++6 libgcc-s1 zlib1g"
  fi
fi

NODE_NAME="${HLQ_NODE_NAME:-hlq-$(printf '%04x' $((RANDOM)))}"   # pseudonymous by default (no identity link)
NODE_NAME="$(printf '%s' "$NODE_NAME" | tr -cd 'A-Za-z0-9-' | cut -c1-32)"; [ -n "$NODE_NAME" ] || NODE_NAME="hlq-node"

# 3. download to a temp dir that is always cleaned.
#    HTTPS-only, TLS>=1.2, no redirects: the URLs are fixed and same-origin, so a redirect would mean
#    someone is steering us elsewhere. (sha256 below is the real backstop.)
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
dl() { # <url> <dest> — same guarantees on either tool: HTTPS-only, TLS>=1.2, no redirect following.
  if [ "$HAVE_DL" = "curl" ]; then
    curl -fsS --proto '=https' --tlsv1.2 --connect-timeout 15 --max-time 900 "$1" -o "$2"
  else
    wget -q --https-only --secure-protocol=TLSv1_2 --max-redirect=0 --timeout=15 -O "$2" "$1"
  fi
}
# dl_mirror <url> <dest> — the mirror is a file host (GitHub Releases answers 302 to its storage), so
# here redirects ARE followed, at most 3 and HTTPS-only on every hop. Nothing is trusted more for it:
# whatever arrives is checked against the same sha256 pins. (2026-10-07: with no redirects the planned
# Releases mirror would have failed on every download, and only the day the main place was down.)
dl_mirror() {
  if [ "$HAVE_DL" = "curl" ]; then
    curl -fsSL --proto '=https' --proto-redir '=https' --max-redirs 3 --tlsv1.2 --connect-timeout 15 --max-time 900 "$1" -o "$2"
  else
    # wget's --https-only binds only in recursive mode: a 302 to http:// would be followed. So the hops
    # are walked by hand, each Location must be https://, and the final GET still follows none.
    local u="$1" loc i
    for i in 1 2 3 4; do
      loc="$(wget -S --spider --https-only --secure-protocol=TLSv1_2 --max-redirect=0 --timeout=15 "$u" 2>&1 \
        | awk 'tolower($1)=="location:"{print $2}' | tr -d '\r' | tail -n 1)"
      [ -n "$loc" ] || break
      [ "$i" -lt 4 ] || return 1
      case "$loc" in https://*) u="$loc" ;; *) return 1 ;; esac
    done
    wget -q --https-only --secure-protocol=TLSv1_2 --max-redirect=0 --timeout=15 -O "$2" "$u"
  fi
}
[ "${BIN_URL#https://}"  != "$BIN_URL"  ] || die "binary URL is not https."
[ "${SPEC_URL#https://}" != "$SPEC_URL" ] || die "spec URL is not https."
[ -z "$MIRROR_BASE" ] || [ "${MIRROR_BASE#https://}" != "$MIRROR_BASE" ] || die "mirror URL is not https."
# dl_or_mirror <primary-url> <dest> <what>: try the published place; if it does not answer, try the
# mirror — and if there is no mirror, say exactly that. A download that fails in silence, or a mirror
# variable that is empty and looks configured, is how someone ends up with half an installation.
dl_or_mirror() {
  local url="$1" dest="$2" what="$3"
  dl "$url" "$dest" && return 0
  if [ -z "$MIRROR_BASE" ]; then
    die "could not download the $what from $url, and this installer has NO mirror configured.
     Nothing is broken on your side. The single place that serves the files is not answering.
     Check https://harlequinproject.org — and if you have the sha256 from a source you trust, any
     copy of the file works: the installer verifies it before using it."
  fi
  info "the main place did not answer — trying the mirror…"
  local path="${url#*/dist/}"
  dl_mirror "$MIRROR_BASE/$path" "$dest" \
    || die "could not download the $what from either place ($url and $MIRROR_BASE/$path)."
  ok "got the $what from the mirror. It will be sha256-verified exactly like the main one."
}

ok "downloading node binary ($ARCH)…"; dl_or_mirror "$BIN_URL"  "$TMP/harlequin-node"   "node binary"
ok "downloading launch chain spec…";   dl_or_mirror "$SPEC_URL" "$TMP/mainnet-raw.json" "chain spec"
ok "downloading bind-tool ($ARCH)…";   dl_or_mirror "$BIND_URL" "$TMP/harlequin-bind-tool" "bind-tool"

# 4. verify sha256 — ABORT on mismatch (the security boundary of this script)
verify() { # <file> <expected>
  local got; got="$(sha256sum "$1" | cut -d' ' -f1)"
  [ "$got" = "$2" ] || die "sha256 MISMATCH for $(basename "$1")
      expected: $2
      got:      $got
    Aborting — do NOT trust this file."
}
verify "$TMP/harlequin-node"   "$BIN_SHA256";  ok "binary sha256 verified."
verify "$TMP/mainnet-raw.json" "$SPEC_SHA256"; ok "chain spec sha256 verified."
verify "$TMP/harlequin-bind-tool" "$BIND_SHA256"; ok "bind-tool sha256 verified."
HAVE_HB=0
if [ -n "${HB_SHA256:-}" ]; then
  ok "downloading heartbeat ($ARCH)…"; dl_or_mirror "$HB_URL" "$TMP/harlequin-heartbeat" "heartbeat"
  verify "$TMP/harlequin-heartbeat" "$HB_SHA256"; ok "heartbeat sha256 verified."; HAVE_HB=1
fi

# 4b. weak-subjectivity verifier (M1) — installed next to the node, safe to re-run any time.
#     Exit codes: 0 = checkpoint verified · 1 = MISMATCH (wrong chain) · 2 = local RPC unreachable.
#     Both non-zero outcomes are treated FAIL-CLOSED by this installer: the node is stopped.
cat > "$TMP/verify-checkpoint.sh" <<VCEOF
#!/usr/bin/env bash
# verify-checkpoint.sh — Harlequin weak-subjectivity check (M1). Re-run any time.
# Pinned at install time ($CHECKPOINT_PINNED_AT); re-pinned on every release.
# Verify these values against >=2 independent sources (installer over HTTPS,
# https://harlequinproject.org/network.html, an operator you trust) before trusting them.
CHECKPOINT_HEIGHT="$CHECKPOINT_HEIGHT"
CHECKPOINT_HASH="$CHECKPOINT_HASH"
CHECKPOINT_CHAIN="$CHECKPOINT_CHAIN"
CHECKPOINT_PINNED_AT="$CHECKPOINT_PINNED_AT"
RPC_URL="$RPC_URL"
VCEOF
cat >> "$TMP/verify-checkpoint.sh" <<'VCEOF'
set -euo pipefail
rpc() { # curl if present, wget otherwise — a bare server may have only one of the two.
  if command -v curl >/dev/null 2>&1; then
    curl -fsS -m 10 -H 'Content-Type: application/json' -d "$1" "$RPC_URL" 2>/dev/null || true
  else
    wget -q -O- --timeout=10 --header='Content-Type: application/json' --post-data="$1" "$RPC_URL" 2>/dev/null || true
  fi
}
res() { printf '%s' "$1" | grep -o '"result":"[^"]*"' | head -1 | cut -d'"' -f4 || true; }

# 1. wait for the LOCAL node RPC (up to ~60s)
CH=""
for _ in $(seq 1 30); do
  CH="$(res "$(rpc '{"id":1,"jsonrpc":"2.0","method":"system_chain","params":[]}')")"
  [ -n "$CH" ] && break
  sleep 2
done
if [ -z "$CH" ]; then
  echo "  ✗ node RPC not reachable at $RPC_URL — checkpoint NOT verified." >&2
  exit 2
fi

# 2. chain identity
if [ "$CH" != "$CHECKPOINT_CHAIN" ]; then
  echo "  ✗ CHECKPOINT FAIL: node chain is '$CH', pinned '$CHECKPOINT_CHAIN'." >&2
  exit 1
fi

# 3. wait until the pinned height exists locally, then compare. Waiting is safe;
#    joining the wrong chain is not — so this loop never gives up on its own.
# Honesty of the waiting line (measured on a 1 GB box, 2026-07-30): a node short on memory does not
# die — it CRAWLS, and while it thrashes its RPC stops answering. The old line then printed
# "best block 0", which reads like "it started over" and sends people away thinking it is broken.
# So: say when the node is not answering, say when it IS answering but not advancing, and say the
# rate when it moves — a person who can see progress waits; one staring at a frozen 0 does not.
GOT=""; PREV=""; SAME=0
while :; do
  GOT="$(res "$(rpc "{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"chain_getBlockHash\",\"params\":[$CHECKPOINT_HEIGHT]}")")"
  [ -n "$GOT" ] && break
  HDR="$(rpc '{"id":1,"jsonrpc":"2.0","method":"chain_getHeader","params":[]}')"
  BESTHEX="$(printf '%s' "$HDR" | grep -o '"number":"0x[0-9a-fA-F]*"' | head -1 | cut -d'"' -f4 || true)"
  if [ -z "$BESTHEX" ]; then
    echo "    the node is not answering right now (it may be busy syncing, or short on memory) — waiting."
  else
    BEST=$(( BESTHEX ))
    if [ "$BEST" = "${PREV:-}" ]; then
      SAME=$((SAME+1))
      if [ "$SAME" -ge 4 ]; then
        echo "    stuck at block $BEST / $CHECKPOINT_HEIGHT for ~$((SAME*15))s — this usually means too little"
        echo "    memory (see the requirements on https://harlequinproject.org/join). The node is alive; it is crawling."
      else
        echo "    syncing… block $BEST / $CHECKPOINT_HEIGHT — no progress this round, waiting."
      fi
    else
      [ -n "${PREV:-}" ] && echo "    syncing… block $BEST / $CHECKPOINT_HEIGHT (+$((BEST-PREV)) in 15s)." \
                        || echo "    syncing… block $BEST / $CHECKPOINT_HEIGHT."
      SAME=0
    fi
    PREV="$BEST"
  fi
  sleep 15
done

if [ "$GOT" = "$CHECKPOINT_HASH" ]; then
  echo "  ✓ weak-subjectivity checkpoint verified: block $CHECKPOINT_HEIGHT = $CHECKPOINT_HASH (pinned $CHECKPOINT_PINNED_AT)."
  exit 0
fi
echo "  ✗ CHECKPOINT MISMATCH at block $CHECKPOINT_HEIGHT" >&2
echo "      pinned: $CHECKPOINT_HASH" >&2
echo "      got:    $GOT" >&2
echo "    Your peers are serving a DIFFERENT chain (possible long-range attack or wrong network)." >&2
echo "    Do NOT trust this node. Check https://harlequinproject.org/network.html and ask an operator you trust." >&2
exit 1
VCEOF
chmod +x "$TMP/verify-checkpoint.sh"

# A fresh node is a FOLLOWER (observer): it syncs, verifies, serves and relays, but does NOT validate —
# validating requires earned reputation (committee membership), by design. So no --validator anywhere here.
# --state/blocks-pruning archive: the finality committee for each block is read from on-chain state at that
# block's EPOCH-START; default pruning keeps only the last 256 states, so the read would return EMPTY and
# finality would stall (the #270 stall). Archive keeps all state so the read always resolves; the light profile
# (1c above) keeps 28,800 blocks, far more than the window the read needs (proven at 2,400 for 21 h on 1-oct).

if [ "$MODE" = "service" ]; then
  # ── service install (unprivileged system user + hardened unit) ─────────────
  id harlequin >/dev/null 2>&1 || { useradd --system --home-dir "$PREFIX" --shell /usr/sbin/nologin harlequin; ok "created system user 'harlequin'."; }
  install -d -o harlequin -g harlequin -m 0750 "$PREFIX" "$PREFIX/data"
  install -o root -g root -m 0755 "$TMP/harlequin-node"   "$PREFIX/harlequin-node"
  install -o root -g root -m 0755 "$TMP/harlequin-bind-tool" "$PREFIX/harlequin-bind-tool"
  install -o harlequin -g harlequin -m 0644 "$TMP/mainnet-raw.json" "$PREFIX/mainnet-raw.json"
  install -o root -g root -m 0755 "$TMP/verify-checkpoint.sh" "$PREFIX/verify-checkpoint.sh"
  ok "installed binary + spec under $PREFIX."

  # node-key (network identity, NOT an account; never overwritten)
  if [ ! -f "$PREFIX/node-key" ]; then
    runuser -u harlequin -- "$PREFIX/harlequin-node" key generate-node-key --file "$PREFIX/node-key" >/dev/null 2>&1 \
      || die "could not generate node-key."
    chown harlequin:harlequin "$PREFIX/node-key"; chmod 600 "$PREFIX/node-key"
    ok "generated network node-key (0600)."
  else
    ok "node-key already present (left untouched)."
  fi

  cat > "/etc/systemd/system/${SVC}.service" <<UNIT
[Unit]
Description=Harlequin node
After=network-online.target
Wants=network-online.target

[Service]
User=harlequin
Group=harlequin
# --trie-cache-size 64 MiB / --db-cache 128 MiB (2026-09-25): the SDK defaults are 1 GiB + 1024 MiB of
# cache, sized for large servers. They fill with uptime until they fight the node they serve — what this
# page's memory floor was really measuring. The live validators have run on 1 GB machines with exactly
# these caps since 2026-09-23.
# --pool-type single-state: root fix for the intermittent "Essential task txpool-background failed"
# (no backticks in this heredoc: it is unquoted, so the shell would try to RUN whatever they wrap —
#  measured on a clean box as 'bash: line 252: Essential: command not found' right before start)
# crash of the fork-aware pool. It bit twice on 2026-07-25, BOTH times while a node was catching up at
# ~1000 blocks/s — exactly what every newcomer does. A node that dies mid-sync is a person who does not
# try again, so the flag ships with the installer rather than waiting for a deployment round.
ExecStart=$PREFIX/harlequin-node \\
  --base-path $PREFIX/data \\
  --chain $PREFIX/mainnet-raw.json \\
  --node-key-file $PREFIX/node-key \\
  --name "$NODE_NAME" \\
  --port 30333 \\
  --consensus woven-trust-12000 \\
  --network-backend libp2p \\
  --no-mdns \\
  --bootnodes "${BOOTNODE}" "${BOOTNODE2}" \\
  $PRUNE_ARGS \\
  --pool-type single-state \\
  --no-telemetry \\
  --trie-cache-size 67108864 --db-cache 128 \\
  \$VOTE_ARGS
# VOTE_ARGS is empty until you link this node to your mask (harlequin-link-to-mask writes /etc/harlequin/vote.env).
EnvironmentFile=-/etc/harlequin/vote.env
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=$PREFIX/data
PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
RestrictNamespaces=true

[Install]
WantedBy=multi-user.target
UNIT
  # harlequin-link-to-mask: the NODE side of linking this node to your mask. It makes (or reuses) the node's
  # vote key inside data/ (the only place the service may write), restarts the node signing with it, and
  # prints the two PUBLIC values the villa asks for. Your mask's secret never comes near this machine.
  # It runs as root, so it lives in /usr/local/sbin and its config in /etc/harlequin — NOT in $PREFIX, which
  # the network-facing service user owns: a file root runs must not sit where that user can replace it.
  install -d -o root -g root -m 0755 /etc/harlequin
  LINKER=/usr/local/sbin/harlequin-link-to-mask
  cat > "$LINKER" <<LINK
#!/usr/bin/env bash
set -euo pipefail
[ "\$(id -u)" = 0 ] || { echo "  run it with sudo (it restarts the node service)." >&2; exit 1; }
MASK="\${1:-}"; [ -n "\$MASK" ] || { echo "  usage: sudo harlequin-link-to-mask <your mask address, r…> [--rotate]" >&2; exit 1; }
shift
KEY="$PREFIX/data/vote.key"
systemctl stop ${SVC}
runuser -u harlequin -- "$PREFIX/harlequin-bind-tool" --mask "\$MASK" --key-file "\$KEY" "\$@" || { systemctl start ${SVC}; exit 1; }
chown harlequin:harlequin "\$KEY"; chmod 600 "\$KEY"
umask 077; printf 'VOTE_ARGS=--vote-as-file %s\\n' "\$KEY" > /etc/harlequin/vote.env
systemctl start ${SVC}
echo "  ✓ node restarted, signing with its vote key. Now paste the two values above in $DIST_BASE/villa/entrar/ and sign."
LINK
  chown root:root "$LINKER"; chmod 0755 "$LINKER"

  # Daily hosting beat (E1): only does anything once this node is linked to a mask (the vote key exists). It signs a
  # recent block with that key and sends it to THIS node's RPC; no coins, no account secret. Randomised hour so a
  # thousand nodes do not beat in the same minute (and the hour says nothing about where you are).
  if [ "$HAVE_HB" = 1 ]; then
    install -o root -g root -m 0755 "$TMP/harlequin-heartbeat" "$PREFIX/harlequin-heartbeat"
    cat > /etc/systemd/system/harlequin-heartbeat.service <<HBS
[Unit]
Description=Harlequin hosting heartbeat (daily, once linked to a mask)
ConditionPathExists=$PREFIX/data/vote.key
After=${SVC}.service
[Service]
Type=oneshot
User=harlequin
ExecStart=$PREFIX/harlequin-heartbeat --key-file $PREFIX/data/vote.key
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
HBS
    cat > /etc/systemd/system/harlequin-heartbeat.timer <<HBT
[Unit]
Description=Harlequin hosting heartbeat, once a day
[Timer]
OnBootSec=30min
OnUnitActiveSec=1d
RandomizedDelaySec=4h
Persistent=true
[Install]
WantedBy=timers.target
HBT
    ok "daily hosting heartbeat installed (starts counting once you link the node to your mask)."
  fi

  systemctl daemon-reload
  [ "$HAVE_HB" = 1 ] && { systemctl enable --now harlequin-heartbeat.timer >/dev/null 2>&1 || true; }
  systemctl enable "$SVC" >/dev/null 2>&1 || true
  systemctl restart "$SVC"
  ok "service '${SVC}' enabled and started."

  # weak-subjectivity check (M1). Two distinct failures (review 2026-07-04, H3):
  #   exit 1 = checkpoint MISMATCH → hostile/wrong chain → stop AND disable (fail-closed).
  #   exit 2 = local RPC unreachable (transient/slow hardware) → NOT an attack signal: leave the
  #            node running UNVERIFIED and demand a manual re-run — disabling here would be a
  #            self-inflicted DoS on slow boxes.
  ok "verifying weak-subjectivity checkpoint (block $CHECKPOINT_HEIGHT)…"
  VC_RC=0; "$PREFIX/verify-checkpoint.sh" || VC_RC=$?
  if [ "$VC_RC" -eq 1 ]; then
    systemctl stop "$SVC" || true
    systemctl disable "$SVC" >/dev/null 2>&1 || true
    die "checkpoint MISMATCH — node stopped and disabled (fail-closed).
    Re-check your network and the pinned values against >=2 independent sources, then re-run this installer."
  elif [ "$VC_RC" -ne 0 ]; then
    echo "  ⚠ could not reach the node's local RPC — checkpoint NOT verified yet." >&2
    echo "    The node keeps running, but do NOT trust it until this passes:" >&2
    echo "        $PREFIX/verify-checkpoint.sh" >&2
  fi

  echo "  ────────────────────────────────────────────"
  ok "Node up. Open p2p port 30333/tcp in your firewall to accept peers."
  info "logs:    journalctl -u ${SVC} -f"
  info "status:  systemctl status ${SVC}"
  info "re-verify checkpoint any time: $PREFIX/verify-checkpoint.sh"
  info "account: $PREFIX/harlequin-node key generate   (store the phrase in YOUR password manager)"
  info "link this node to YOUR mask (so its service is credited to you):"
  info "    sudo harlequin-link-to-mask <your mask address, r…>"
  info "  then paste the two values it prints in $DIST_BASE/villa/entrar/ and sign there (no fee for a new mask)."
  echo "  Your node, your keys, no master."
  echo

else
  # ── portable install (no systemd: Android proot, containers) ────────────────
  mkdir -p "$PREFIX/data"
  install -m 0755 "$TMP/harlequin-node"   "$PREFIX/harlequin-node"
  install -m 0755 "$TMP/harlequin-bind-tool" "$PREFIX/harlequin-bind-tool"
  install -m 0644 "$TMP/mainnet-raw.json" "$PREFIX/mainnet-raw.json"
  install -m 0755 "$TMP/verify-checkpoint.sh" "$PREFIX/verify-checkpoint.sh"
  ok "installed binary + spec under $PREFIX."

  # node-key (network identity, NOT an account; never overwritten). WITHOUT this the node draws a
  # fresh identity on every start: to everyone else you are a different machine each time, so the
  # standing you build with your peers is thrown away at every restart. The systemd path has had
  # this since day one; the portable path — the one a phone or a tablet takes — did not, and nobody
  # noticed because nobody ever walked that path from scratch. Added 2026-08-27.
  if [ ! -f "$PREFIX/node-key" ]; then
    "$PREFIX/harlequin-node" key generate-node-key --file "$PREFIX/node-key" >/dev/null 2>&1 \
      || die "could not generate node-key."
    chmod 600 "$PREFIX/node-key"
    ok "generated network node-key (0600) — your node keeps the same identity across restarts."
  else
    ok "node-key already present (left untouched)."
  fi

  # --wasmtime-instantiation-strategy recreate-instance-copy-on-write: avoids wasmtime's pooling
  # allocator (reserves a huge mmap that dies inside proot/Android).
  # v4.1 UX: the node runs DETACHED from the terminal (nohup + logfile + pidfile). Closing the
  # terminal or locking the screen no longer kills it. The weak-subjectivity check (M1) still stops
  # it FAIL-CLOSED on mismatch before the script hands control back.
  cat > "$PREFIX/run-node.sh" <<RUN
#!/usr/bin/env bash
set -euo pipefail
cd "\$(dirname "\$0")"
# singleton: refuse a second copy (stale pidfile is fine — we check the process)
if [ -f node.pid ] && kill -0 "\$(cat node.pid)" 2>/dev/null; then
  echo "  ✓ node already running (pid \$(cat node.pid)). Status: ./node-status.sh" >&2
  exit 0
fi
# same ceiling the systemd unit sets with LimitNOFILE: a syncing node opens a lot of files at once.
ulimit -n 65536 2>/dev/null || true
VOTE=(); [ -f ./data/vote.key ] && VOTE=(--vote-as-file ./data/vote.key)   # set by ./link-to-mask.sh
nohup ./harlequin-node "\${VOTE[@]}" \\
  --base-path ./data \\
  --chain ./mainnet-raw.json \\
  --node-key-file ./node-key \\
  --name "${NODE_NAME}" \\
  --port 30333 \\
  --consensus woven-trust-12000 \\
  --network-backend libp2p \\
  --no-mdns \\
  --wasmtime-instantiation-strategy recreate-instance-copy-on-write \\
  --bootnodes "${BOOTNODE}" "${BOOTNODE2}" \\
  $PRUNE_ARGS \\
  --pool-type single-state \\
  --no-telemetry \\
  --trie-cache-size 67108864 --db-cache 128 > node.log 2>&1 &
NODE_PID=\$!
echo "\$NODE_PID" > node.pid
# exit 1 = checkpoint MISMATCH → kill the node (fail-closed). exit 2 = local RPC unreachable
# (transient/slow hardware, e.g. a tablet) → keep the node running, demand a manual re-check.
VC_RC=0; ./verify-checkpoint.sh || VC_RC=\$?
if [ "\$VC_RC" -eq 1 ]; then
  kill "\$NODE_PID" 2>/dev/null || true
  rm -f node.pid
  echo "  ✗ node stopped: checkpoint MISMATCH (fail-closed)." >&2
  echo "    Re-check the pinned values against >=2 independent sources, then run ./run-node.sh again." >&2
  exit 1
elif [ "\$VC_RC" -ne 0 ]; then
  echo "  ⚠ could not reach the node's local RPC — checkpoint NOT verified yet." >&2
  echo "    The node keeps running, but do NOT trust it until this passes:  ./verify-checkpoint.sh" >&2
fi
echo "  ✓ node running detached (pid \$NODE_PID). It survives closing this terminal."
echo "    peek at it:   ./node-status.sh      follow live:  tail -f node.log      stop: ./stop-node.sh"
RUN
  chmod +x "$PREFIX/run-node.sh"

  # One-line health view: process state + the node's own last sync line.
  cat > "$PREFIX/node-status.sh" <<'ST'
#!/usr/bin/env bash
cd "$(dirname "$0")"
if [ -f node.pid ] && kill -0 "$(cat node.pid)" 2>/dev/null; then
  echo "  ✓ node RUNNING (pid $(cat node.pid))"
else
  echo "  ✗ node NOT running. Start it: ./run-node.sh"
fi
grep -oE "(Syncing|Idle).*" node.log 2>/dev/null | tail -1
grep -oE "finalized #[0-9]+" node.log 2>/dev/null | tail -1
ST
  chmod +x "$PREFIX/node-status.sh"

  cat > "$PREFIX/stop-node.sh" <<'SP'
#!/usr/bin/env bash
cd "$(dirname "$0")"
if [ -f node.pid ] && kill -0 "$(cat node.pid)" 2>/dev/null; then
  kill "$(cat node.pid)" && rm -f node.pid && echo "  ✓ node stopped."
else
  rm -f node.pid; echo "  · node was not running."
fi
SP
  chmod +x "$PREFIX/stop-node.sh"

  # Foreground variant for OUTER supervisors. Android/proot gotcha (field-tested 2026-07-25):
  # proot is ptrace-based, so a nohup INSIDE the proot login dies when the login exits — detaching
  # must happen OUTSIDE. From Termux (not inside debian) run:
  #   setsid nohup proot-distro login debian -- bash ~/harlequin/run-node-fg.sh > ~/node.log 2>&1 &
  cat > "$PREFIX/run-node-fg.sh" <<RUNFG
#!/usr/bin/env bash
set -euo pipefail
cd "\$(dirname "\$0")"
ulimit -n 65536 2>/dev/null || true
VOTE=(); [ -f ./data/vote.key ] && VOTE=(--vote-as-file ./data/vote.key)   # set by ./link-to-mask.sh
exec ./harlequin-node "\${VOTE[@]}" \\
  --base-path ./data \\
  --chain ./mainnet-raw.json \\
  --node-key-file ./node-key \\
  --name "${NODE_NAME}" \\
  --port 30333 \\
  --consensus woven-trust-12000 \\
  --network-backend libp2p \\
  --no-mdns \\
  --wasmtime-instantiation-strategy recreate-instance-copy-on-write \\
  --bootnodes "${BOOTNODE}" "${BOOTNODE2}" \\
  $PRUNE_ARGS \\
  --pool-type single-state \\
  --no-telemetry \\
  --trie-cache-size 67108864 --db-cache 128
RUNFG
  chmod +x "$PREFIX/run-node-fg.sh"

  # Portable twin of link-to-mask.sh: vote key in ./data, node restarted signing with it.
  cat > "$PREFIX/link-to-mask.sh" <<PLINK
#!/usr/bin/env bash
set -euo pipefail
cd "\$(dirname "\$0")"
MASK="\${1:-}"; [ -n "\$MASK" ] || { echo "  usage: ./link-to-mask.sh <your mask address, r…> [--rotate]" >&2; exit 1; }
shift
mkdir -p ./data
./stop-node.sh >/dev/null 2>&1 || true
./harlequin-bind-tool --mask "\$MASK" --key-file ./data/vote.key "\$@" || { ./run-node.sh; exit 1; }
chmod 600 ./data/vote.key
./run-node.sh
echo "  ✓ now paste the two values above in $DIST_BASE/villa/entrar/ and sign there."
PLINK
  chmod +x "$PREFIX/link-to-mask.sh"

  echo "  ────────────────────────────────────────────"
  # 2026-10-02 (matrix 2.4): on a phone the first thing that kills a node is not the node, it is Android putting
  # Termux to sleep. Say it FIRST, before anything else, only where it applies.
  if [ -n "${ANDROID_ROOT:-}" ] || grep -qi android /proc/version 2>/dev/null; then
    echo "  ⚠ ANDROID: before anything else, or the phone will freeze your node within minutes:"
    echo "    1. In Termux (outside debian) run:  termux-wake-lock"
    echo "    2. Android Settings → Apps → Termux → Battery → Unrestricted (no battery optimisation)."
    echo "    3. Keep it on a charger and on Wi-Fi; a node syncs best when the phone is left alone."
    echo
  fi
  ok "All set. Starting your follower node '$NODE_NAME' (detached — it survives closing the terminal)."
  info "· Android/Termux (proot): to survive closing the APP, start it from Termux itself instead:"
  info "    setsid nohup proot-distro login debian -- bash ~/harlequin/run-node-fg.sh > ~/node.log 2>&1 &"
  info "· First start runs the weak-subjectivity check: your node must contain the pinned block or it stops."
  info "· Peek any time: $PREFIX/node-status.sh · live view: tail -f $PREFIX/node.log"
  info "· Your MASK (seed phrase) is created in the BROWSER: $DIST_BASE/rito"
  info "· Link this node to your mask: $PREFIX/link-to-mask.sh <your r… address>"
  info "  and paste the two values in $DIST_BASE/villa/entrar/ (the node's key never leaves this machine)."
  info "· Stop: $PREFIX/stop-node.sh · start again: $PREFIX/run-node.sh"
  info "· Keep your VPN on — a node announces its IP to peers."
  echo "  Your node, your keys, no master."
  echo "  ────────────────────────────────────────────"
  echo
  exec "$PREFIX/run-node.sh"
fi

}  # end of the brace group — nothing above runs unless the file arrived whole
