#!/bin/sh # be PyBLØCK — run your own DATUM gateway and get paid for the node you put up: # # CAROUSEL: curl -fsSL https://b.pyblock.xyz:8443/gateway | sh -s -- carousel # CHIRP: curl -fsSL https://b.pyblock.xyz:8443/gateway | sh -s -- chirp # WAVICLES: curl -fsSL https://b.pyblock.xyz:8443/gateway | sh -s -- wavicles # # What this does, and why it is different from the old /join: # Before, you published your node's template to PyBLØCK and we built the block. Now YOU build and # publish the block from YOUR node, and PyBLØCK only dictates the coinbase. That is about # sovereignty and about keeping more of the block (no node-runner cut when you ARE the node # runner), not about dodging anything: the 45-day coinbase lock Bitcoin Knots adds from block # 973440 applies to every freshly mined coin, whoever built it. # # What you get, always in the coinbase of the block, straight to your address — no custody, ever: # · CAROUSEL: the block your gateway builds pays whoever mines through it (99 %; 1 % PyBLØCK). # · CHIRP: your miners are syndicate members, paid by standing in every CHIRP block (0 % fee). # · WAVICLES: your miners are paid by work in the window, block after block (0 % fee). # # WHAT LEAVES YOUR MACHINE: the finished block, which you broadcast yourself, and your miners' # stratum usernames — which are their Bitcoin addresses — because the pool has to know who to pay. # Nothing about your node's transaction selection, your RPC credentials or your keys ever does. # · and whatever your own miners earn under the pool's rule, also in the same coinbase. # # REQUIREMENTS # · A BLAKE2b node: Bitcoin Knots >= 29.4.2 following the BLAKE2b fork, with RPC reachable. # · A build toolchain (the script tells you the exact package line for your distro). # · One open port for your miners' stratum (default 23334, change with PYBLOCK_STRATUM). # # ENVIRONMENT # PYBLOCK_CLI="bitcoin-cli -datadir=/data" your node, if it is not auto-detected # PYBLOCK_STRATUM=23334 port your miners will connect to # PYBLOCK_DIR=$HOME/pyblock-gateway where the gateway and its config live # PYBLOCK_GATEWAY_REPO= gateway source (default: the DATUM gateway build every PyBLØCK # node-runner runs today, pinned by commit; mirrored on b.pyblock) # PYBLOCK_TAG2="my name" your name in the coinbase of every block you build (the pool sets # the first tag; this is the second). Up to 32 of A-Z a-z 0-9 space . _ - # PYBLOCK_PATUM=1 set by PATUM: builds the PATUM-marked source (same code as the pin, # one extra commit that only adds a PATUM file) and defaults the tag to PATUM # PYBLOCK_SHIELDED=1 with PYBLOCK_PATUM=1: Shielded (alpha) — the gateway masks every job so # a hasher can't tell which share is a block (CONVOY pull request #53, # not merged yet). Solo work is always masked; pooled work only when the # pool hands out gateway keys. # PYBLOCK_INSTALL_DEPS=1 let the script install build dependencies # PYBLOCK_NO_SYSTEMD=1 run with nohup instead of a systemd unit set -eu PRODUCT="${1:-}" ADDR="${2:-${PYBLOCK_ADDR:-}}" DIR="${PYBLOCK_DIR:-$HOME/pyblock-gateway}" STRATUM="${PYBLOCK_STRATUM:-23334}" # 2026-09-24: the default source is PINNED to the exact commit PyBLØCK's node-runners run on all three Primes. # If GitHub is unreachable, the same source is taken from b.pyblock and checked against its SHA-256. # 2026-11-01: pin moved from 7491a50 to 69c61a9 = CONVOY DATUM Gateway v0.5.0beta (it includes 7491a50's # coinbase weight fix, merged as PR #24). Re-running this script now rebuilds when the installed gateway is # not the pinned commit (before, an existing binary was always kept). REPO="${PYBLOCK_GATEWAY_REPO:-https://github.com/CONVOYMining/datum_gateway.git}" GW_PIN="69c61a95cb941c35138c30b8d738e789ad8cdac1" GW_BUNDLE="https://b.pyblock.xyz:8443/dl/gateway/datum_gateway-69c61a9.bundle" # git bundle: builds with the real commit hash GW_BUNDLE_SHA="d20ea6d0af889a474b8beb30cca1696df4821231c29e6e16df8983679cf2aabf" GW_BUNDLE_REF="pyblock-gateway" # PATUM installs build the pin + one commit that only adds a PATUM file, so the pools can tell PATUM gateways # apart (the gateway announces its commit). Same code; taken from PyBLØCK's mirror (SHA-256). # 2026-10-02: a13eabd (on 7491a50). 2026-11-01: 43daf99 (on v0.5.0beta). if [ "${PYBLOCK_PATUM:-}" = "1" ] && [ -z "${PYBLOCK_GATEWAY_REPO:-}" ] && [ -z "${PYBLOCK_GATEWAY_REF:-}" ]; then GW_PIN="43daf99d7fbd717c3f8d4969ad3c3be9a1741d1f" GW_BUNDLE="https://b.pyblock.xyz:8443/dl/gateway/datum_gateway-43daf99-patum.bundle" GW_BUNDLE_SHA="f1c22416056af7a3b600c563911a800fabcc54535ea5d0647c1c3a3f960fa9d1" GW_BUNDLE_REF="pyblock-gateway-patum" # 2026-11-03: Shielded (alpha) = CONVOY pull request #53 (c89ce93, on v0.5.0beta) + the same PATUM marker commit. if [ "${PYBLOCK_SHIELDED:-}" = "1" ]; then GW_PIN="e44e0fd74d93ca7e0607451455569c3a3934884a" GW_BUNDLE="https://b.pyblock.xyz:8443/dl/gateway/datum_gateway-e44e0fd-patum-shielded.bundle" GW_BUNDLE_SHA="3c61897774435698f23ed8c77cde3960cc46657a995a66b440f9fb81a0180f0a" GW_BUNDLE_REF="pyblock-gateway-patum-shielded" fi fi # Second coinbase tag (your name in your blocks). Sanitized; PATUM installs default to "PATUM". TAG2="$(printf '%s' "${PYBLOCK_TAG2:-}" | tr -cd 'A-Za-z0-9 ._-' | cut -c1-32)" if [ -z "$TAG2" ] && [ "${PYBLOCK_PATUM:-}" = "1" ]; then TAG2="PATUM"; fi CLAIM="https://b.pyblock.xyz:8443/api/noderunner_claim.php" RULES='{"rules":["segwit","blake2b"]}' say() { printf '%s\n' "$*"; } die() { printf 'PyBLOCK: %s\n' "$*" >&2; exit 1; } case "$PRODUCT" in carousel) PRIME_PORT=28916; PRIME_PUBKEY="36f94e73ae4c9b9c479c544f035bc9e69565e9d882112031cb27032ecbac818e35a7976501d5cd0453f9fc3e9823127839225e89c45a2cc80902ba0a7e09577b"; SHARE="0 %"; TAG="PyBLOCK-CAROUSEL" ;; chirp) PRIME_PORT=28917; PRIME_PUBKEY="cf995bb5b975ef4b90932f0608c98a033679092e976982621243cd1226803c6417442870dff8372b11363d8f80665b1f298cc23132bf2dab23958a4bcc7d4f72"; SHARE="0 %"; TAG="PyBLOCK-CHIRP" ;; wavicles) PRIME_PORT=28915; PRIME_PUBKEY="b5e0b0326b964a92f3046c73a76b9e98173e0c3ea0e0ca771f4e6a3dd92e2935b2caad2a088fdd7bf0a48fc0c41776a9abb13c7d4c6964b53a9ceee4c775e036"; SHARE="0 %"; TAG="PyBLOCK-WAVICLES" ;; *) die "first argument must be 'carousel', 'chirp' or 'wavicles'. CAROUSEL: your gateway, your block — whoever mines through it keeps 99 %. CHIRP: a weighted draw that favours small, constant miners — 100 % to the syndicate, 0 % fee. WAVICLES: the work window paid by share, block after block — 100 % to the miners, 0 % fee. Usage: curl -fsSL https://b.pyblock.xyz:8443/gateway | sh -s -- carousel " ;; esac [ -n "$ADDR" ] || die "your BTC ADDRESS is required — it is where your $SHARE is paid. Usage: curl -fsSL https://b.pyblock.xyz:8443/gateway | sh -s -- $PRODUCT " # A glob is not a regex: `1[a-z]*` accepts anything after the second character, so a quote or a # newline used to get through and land inside the JSON config below, breaking it or adding keys. # Check the alphabet AND the length, and do it before building anything. addr_bad() { die "'$ADDR' is not a mainnet BTC address. Expected bc1q…/bc1p… (bech32) or 1…/3… (base58)."; } case "$ADDR" in bc1[qp]*) # bech32/bech32m: lowercase, and the charset has no 1, b, i or o. case "${ADDR#bc1}" in *[!qpzry9x8gf2tvdw0s3jn54khce6mua7l]*) addr_bad ;; esac [ "${#ADDR}" -ge 14 ] && [ "${#ADDR}" -le 90 ] || addr_bad # Knots caps every coinbase output at a 34-byte script from the fork height, so a long # witness program cannot be paid. Catch it here, not on the block that would have paid you. case "$ADDR" in bc1p*) [ "${#ADDR}" -le 62 ] || die "that taproot address is too long to be paid in a coinbase under the fork's 34-byte output rule. Use a bc1q… or 1… address." ;; esac ;; 1*|3*) case "$ADDR" in *[!123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]*) addr_bad ;; esac [ "${#ADDR}" -ge 26 ] && [ "${#ADDR}" -le 35 ] || addr_bad ;; *) addr_bad ;; esac # ---- 1. your node --------------------------------------------------------------------------- say "PyBLOCK: looking for a BLAKE2b node…" # 2026-10-02: un nodo SHA-256 TAMBIÉN acepta getblocktemplate con la regla "blake2b" (ignora reglas que no # conoce), así que en una máquina con los dos nodos se elegía el equivocado. La prueba real: getdeploymentinfo # trae {"blake2b":{…"active":true}} solo en la cadena BLAKE2b. is_b2b() { $1 getblocktemplate "$RULES" >/dev/null 2>&1 && $1 getdeploymentinfo 2>/dev/null | tr -d ' \n\t' | grep -q '"blake2b":{[^}]*"active":true'; } find_cli() { if [ -n "${PYBLOCK_CLI:-}" ]; then is_b2b "$PYBLOCK_CLI" && { printf '%s' "$PYBLOCK_CLI"; return; } printf ''; return fi for P in 8342 9332 8332; do is_b2b "bitcoin-cli -rpcport=$P" && { printf 'bitcoin-cli -rpcport=%s' "$P"; return; } done is_b2b "bitcoin-cli" && { printf 'bitcoin-cli'; return; } printf '' } CLI="$(find_cli)" [ -n "$CLI" ] || die "no BLAKE2b node found. Your node must answer getblocktemplate {\"rules\":[\"segwit\",\"blake2b\"]} — a node that is not following the BLAKE2b fork answers 'unknown rule blake2b'. You need Bitcoin Knots >= 29.4. If your node is in a container or a custom datadir, point the script at it: curl -fsSL … | PYBLOCK_CLI=\"bitcoin-cli -datadir=/data\" sh -s -- $PRODUCT $ADDR" HEIGHT="$($CLI getblockcount 2>/dev/null || echo '?')" say "PyBLOCK: node found via '$CLI', height $HEIGHT." # Is that node current? Read-only and best-effort — this script never touches a node it did not # install, but a node-runner deserves to be TOLD when his node is behind. (2026-09-21: the one-liner # shipped 29.4.1 for weeks after 29.4.2 was out, and the only way to find out was to read the # terminal scrollback yourself. Consensus rules ship with the node, not with this gateway.) gw_vkey() { printf '%s' "$1" | awk '{ t = $0; sub(/^v/, "", t); n = t; sub(/\.knots.*$/, "", n); d = t; sub(/^.*\.knots/, "", d); c = split(n, q, "."); printf "%05d%05d%05d%08d", q[1]+0, (c>1 ? q[2]+0 : 0), (c>2 ? q[3]+0 : 0), d+0; }' } NODEVER="$($CLI getnetworkinfo 2>/dev/null | sed -n 's/.*"subversion"[[:space:]]*:[[:space:]]*"\/Satoshi:\([0-9.]*\)[^/]*\/Knots:\([0-9]*\)\/".*/v\1.knots\2/p' | head -1 || true)" CURVER="$(curl -fsSL --max-time 10 https://b.pyblock.xyz:8443/dl/knots-version.json 2>/dev/null | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1 || true)" NODEKEY="$(gw_vkey "${NODEVER:-}" 2>/dev/null || true)"; CURKEY="$(gw_vkey "${CURVER:-}" 2>/dev/null || true)" if [ -n "$NODEVER" ] && [ -n "$CURVER" ] && [ "$NODEVER" != "$CURVER" ] && [ -n "$NODEKEY" ] && [ -n "$CURKEY" ]; then # solo avisamos si el nodo esta ATRAS; si esta adelante (probo un release nuevo) no lo molestamos. if [ "$(printf '%s\n%s\n' "$NODEKEY" "$CURKEY" | LC_ALL=C sort | tail -1)" = "$CURKEY" ]; then say "PyBLOCK: ---------------------------------------------------------------" say "PyBLOCK: HEADS UP — your node is $NODEVER; the current release is $CURVER." say "PyBLOCK: Consensus rules ship with the NODE, not with this gateway." say "PyBLOCK: https://github.com/bitcoinknots/bitcoin/releases/tag/$CURVER" say "PyBLOCK: ---------------------------------------------------------------" fi fi # Its RPC credentials and endpoint, which the gateway needs. Read from the node's own config or # a cookie. The endpoint is DERIVED, not assumed: defaulting to 8332 pointed the gateway at the # wrong node on any box that runs both chains, which is exactly the case here. # 2026-10-04: el -datadir= que viene en el comando (PATUM lo pasa) MANDA. `-getinfo` no trae la carpeta, así que # antes esto quedaba vacío y las credenciales salían de ~/.bitcoin — en una máquina con dos nodos, las del OTRO. DATADIR="$(printf '%s' "$CLI" | sed -n 's/.*-datadir=\([^ ]*\).*/\1/p')" [ -n "$DATADIR" ] || DATADIR="$($CLI -getinfo 2>/dev/null | sed -n 's/.*"datadir": *"\([^"]*\)".*/\1/p' | head -1 || true)" RPCPORT="$(printf '%s' "$CLI" | sed -n 's/.*-rpcport=\([0-9]*\).*/\1/p')" RPCHOST="$(printf '%s' "$CLI" | sed -n 's/.*-rpcconnect=\([^ ]*\).*/\1/p')" [ -n "$RPCHOST" ] || RPCHOST=127.0.0.1 if [ -z "$RPCPORT" ] && [ -n "$DATADIR" ] && [ -f "$DATADIR/bitcoin.conf" ]; then RPCPORT="$(sed -n 's/^[[:space:]]*rpcport[[:space:]]*=[[:space:]]*\([0-9]*\).*/\1/p' "$DATADIR/bitcoin.conf" | head -1)" fi if [ -z "$RPCPORT" ]; then # Ask the node which port answers, instead of guessing: try each and keep the one whose # getblocktemplate accepts the blake2b rule. for P in 8342 9332 8332; do # is_b2b, no solo getblocktemplate: un nodo SHA-256 TAMBIÉN acepta la regla "blake2b" (2026-10-04) if is_b2b "$CLI -rpcport=$P"; then RPCPORT=$P; break; fi done fi [ -n "$RPCPORT" ] || die "could not work out your node's RPC port. Pass it explicitly: curl -fsSL … | PYBLOCK_CLI=\"bitcoin-cli -rpcport=8342\" sh -s -- $PRODUCT $ADDR" say "PyBLOCK: node RPC at $RPCHOST:$RPCPORT." # ---- 2. the gateway ------------------------------------------------------------------------- # 700 y umask 077: en este directorio va a vivir el rpcpassword del nodo y la clave de identidad # del gateway, que es la llave de sus cobros. ( umask 077; mkdir -p "$DIR" ) chmod 700 "$DIR" 2>/dev/null || true umask 077 BIN="$DIR/datum_gateway" # The commit the installed binary was built from is kept next to it. Rebuild when it is not the one this run # asks for (a new pin, PATUM vs plain, or PYBLOCK_GATEWAY_REF), or when it is unknown (built before 2026-11-01). # A custom PYBLOCK_GATEWAY_REPO without a ref keeps an existing binary, as before. WANT='' if [ -z "${PYBLOCK_GATEWAY_REPO:-}" ] || [ -n "${PYBLOCK_GATEWAY_REF:-}" ]; then WANT="${PYBLOCK_GATEWAY_REF:-$GW_PIN}"; fi HAVE="$(cat "$DIR/datum_gateway.commit" 2>/dev/null || true)" if [ ! -x "$BIN" ] || { [ -n "$WANT" ] && [ "$HAVE" != "$WANT" ]; }; then if [ -x "$BIN" ]; then say "PyBLOCK: updating the gateway to ${WANT:-the requested source} (a few minutes)…" else say "PyBLOCK: building the gateway (a few minutes, once)…"; fi MISSING='' for c in git cmake cc; do command -v "$c" >/dev/null 2>&1 || MISSING="$MISSING $c"; done if [ -n "$MISSING" ]; then # 2026-10-01 (Umbrel 2.0, terminal de la app = contenedor sin sudo): root → sin sudo; sin root y sin sudo → decirlo claro. if [ "$(id -u)" = 0 ]; then SUDO=''; elif command -v sudo >/dev/null 2>&1; then SUDO='sudo '; else SUDO='__NOSUDO__'; fi if [ -f /.dockerenv ] || [ -f /run/.containerenv ]; then say "PyBLOCK: note — this shell is INSIDE a container (on Umbrel: the app's terminal). Packages installed here and a" say " gateway built here are wiped when the app restarts or updates. Prefer the host (Umbrel: SSH as umbrel)." fi if command -v apt-get >/dev/null 2>&1; then PKGS="git cmake build-essential libcurl4-openssl-dev libjansson-dev libmicrohttpd-dev libsodium-dev pkg-config" INSTALL="${SUDO}apt-get update && ${SUDO}apt-get install -y $PKGS" elif command -v dnf >/dev/null 2>&1; then PKGS="git cmake gcc gcc-c++ libcurl-devel jansson-devel libmicrohttpd-devel libsodium-devel pkgconf" INSTALL="${SUDO}dnf install -y $PKGS" elif command -v pacman >/dev/null 2>&1; then INSTALL="${SUDO}pacman -S --needed git cmake base-devel curl jansson libmicrohttpd libsodium" else INSTALL='' fi if [ "$SUDO" = '__NOSUDO__' ] && [ -n "$INSTALL" ]; then die "missing build tools:$MISSING — and this user is not root and has no sudo, so they can't be installed from here. Run the script as root (or on the host instead of inside a container)." fi if [ "${PYBLOCK_INSTALL_DEPS:-0}" = "1" ] && [ -n "$INSTALL" ]; then say "PyBLOCK: installing build dependencies…" sh -c "$INSTALL" || die "dependency install failed; install them yourself and re-run." else die "missing build tools:$MISSING Install them and re-run, or pass PYBLOCK_INSTALL_DEPS=1 to let this script do it: ${INSTALL:-install git, cmake, a C compiler, libcurl, jansson, libmicrohttpd and libsodium}" fi fi rm -rf "$DIR/src" # Source: PYBLOCK_GATEWAY_REF (tag/commit) if you pass one; otherwise the pinned commit. A custom # PYBLOCK_GATEWAY_REPO without a ref builds that repo's default branch as it is today. # Read the source before you run this if that matters to you — it is public and this script is # doing nothing you cannot check. GOT='' if [ "${GW_BUNDLE_REF#pyblock-gateway-patum}" != "$GW_BUNDLE_REF" ]; then : # PATUM source (plain or Shielded) lives only on PyBLØCK's mirror: straight to the bundle below elif [ -n "${PYBLOCK_GATEWAY_REPO:-}" ] && [ -z "${PYBLOCK_GATEWAY_REF:-}" ]; then GIT_TERMINAL_PROMPT=0 git clone --depth 1 "$REPO" "$DIR/src" >/dev/null 2>&1 && GOT=1 else REF="${PYBLOCK_GATEWAY_REF:-$GW_PIN}" mkdir -p "$DIR/src" && ( cd "$DIR/src" && git init -q && \ GIT_TERMINAL_PROMPT=0 git fetch -q --depth 1 "$REPO" "$REF" >/dev/null 2>&1 && git checkout -q FETCH_HEAD ) && GOT=1 fi if [ -z "$GOT" ] && [ -z "${PYBLOCK_GATEWAY_REPO:-}" ] && [ -z "${PYBLOCK_GATEWAY_REF:-}" ]; then if [ "${GW_BUNDLE_REF#pyblock-gateway-patum}" != "$GW_BUNDLE_REF" ]; then say "PyBLOCK: getting the PATUM gateway source from PyBLØCK's mirror (checked by SHA-256)…"; else say "PyBLOCK: GitHub unreachable — using PyBLØCK's mirror of the same source (checked by SHA-256)…"; fi rm -rf "$DIR/src"; mkdir -p "$DIR/src" rm -rf "$DIR/src" if curl -fsSL --max-time 120 "$GW_BUNDLE" -o "$DIR/src.bundle" && \ [ "$( (sha256sum "$DIR/src.bundle" 2>/dev/null || shasum -a 256 "$DIR/src.bundle") | cut -d' ' -f1)" = "$GW_BUNDLE_SHA" ]; then git clone -q -b "$GW_BUNDLE_REF" "$DIR/src.bundle" "$DIR/src" >/dev/null 2>&1 && GOT=1 fi rm -f "$DIR/src.bundle" fi [ -n "$GOT" ] || die "could not get the gateway source (tried $REPO${PYBLOCK_GATEWAY_REF:+ @ $PYBLOCK_GATEWAY_REF} and the PyBLØCK mirror)." ( cd "$DIR/src" && cmake -B build -DCMAKE_BUILD_TYPE=Release >/dev/null && cmake --build build -j"$(nproc 2>/dev/null || echo 2)" >/dev/null ) \ || die "the gateway did not build. Run it by hand to see why: cd $DIR/src && cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build" # New file + rename: a running gateway keeps its old binary until it restarts (cp over it fails "Text file busy"). cp "$DIR/src/build/datum_gateway" "$BIN.new" && mv -f "$BIN.new" "$BIN" if [ -n "$WANT" ]; then printf '%s\n' "$WANT" > "$DIR/datum_gateway.commit"; else rm -f "$DIR/datum_gateway.commit"; fi say "PyBLOCK: gateway built ($("$BIN" --version 2>/dev/null | head -1))." else say "PyBLOCK: gateway already present at $BIN." fi # ---- 3. its configuration ------------------------------------------------------------------- # RPC credentials: a cookie if the node uses one, otherwise rpcuser/rpcpassword from its config. # Each candidate is TRIED against the node (2026-10-04): on a machine with two nodes, the first conf found # can belong to the other one, and the gateway then gets 401 forever ("Could not fetch new template"). RPCUSER=''; RPCPASS='' rpc_ok() { curl -fsS --max-time 8 -u "$1:$2" -H 'content-type: text/plain' \ --data-binary '{"jsonrpc":"1.0","id":"pb","method":"getblockcount","params":[]}' "http://$RPCHOST:$RPCPORT/" >/dev/null 2>&1; } for D in "$DATADIR" "$HOME/.bitcoin" /var/lib/bitcoind /data; do [ -n "$D" ] || continue if [ -f "$D/.cookie" ]; then u="$(cut -d: -f1 "$D/.cookie")"; pw="$(cut -d: -f2- "$D/.cookie")" if rpc_ok "$u" "$pw"; then RPCUSER="$u"; RPCPASS="$pw"; break; fi fi if [ -f "$D/bitcoin.conf" ]; then u="$(sed -n 's/^ *rpcuser *= *//p' "$D/bitcoin.conf" | head -1)" pw="$(sed -n 's/^ *rpcpassword *= *//p' "$D/bitcoin.conf" | head -1)" if [ -n "$u" ] && rpc_ok "$u" "$pw"; then RPCUSER="$u"; RPCPASS="$pw"; break; fi fi done [ -n "$RPCUSER" ] || die "could not find RPC credentials that your node at $RPCHOST:$RPCPORT accepts. Put rpcuser= and rpcpassword= in your bitcoin.conf (or use a cookie) and re-run." # Sin contraseña, el /config del gateway responde 200 a cualquier usuario local y muestra el # usuario RPC del nodo. Se genera una y se guarda junto al config, que es 600. APIPASS="$(head -c 18 /dev/urandom | od -An -tx1 | tr -d ' \n')" APIPORT="${PYBLOCK_API_PORT:-7152}" # Puertos ocupados por OTRO proceso (p. ej. un gateway SHA-256 en 23334/7152 en la misma máquina): no pisarlos. # Si el que escucha es nuestro propio gateway de este producto (reinstalación), se reutiliza el puerto. port_busy() { { ss -ltnH 2>/dev/null || netstat -ltn 2>/dev/null; } | awk '{print $4}' | grep -qE "[:.]$1\$"; } own_gw() { pgrep -f "datum_gateway -c $DIR/$PRODUCT.json" >/dev/null 2>&1; } free_port() { _p=$1; _n=0; while port_busy "$_p" && [ $_n -lt 60 ]; do _p=$((_p+1)); _n=$((_n+1)); done; printf '%s' "$_p"; } # 2026-10-04: al ACTUALIZAR, conservar los puertos de la config existente (antes volvía a 23334/7152 y, si en esa # máquina los usaba otro gateway, el tuyo moría con "bind failed"). Solo se saltea el chequeo si esos puertos los # tiene TU gateway corriendo; en cualquier otro caso, puerto ocupado → el siguiente libre. OLDS=''; OLDA='' if [ -f "$DIR/$PRODUCT.json" ]; then OLDA="$(grep '"api"' "$DIR/$PRODUCT.json" | sed -n 's/.*"listen_port": *\([0-9]*\).*/\1/p' | head -1)" OLDS="$(grep -v '"api"' "$DIR/$PRODUCT.json" | sed -n 's/.*"listen_port": *\([0-9]*\).*/\1/p' | head -1)" [ -z "${PYBLOCK_STRATUM:-}" ] && [ -n "$OLDS" ] && STRATUM="$OLDS" [ -z "${PYBLOCK_API_PORT:-}" ] && [ -n "$OLDA" ] && APIPORT="$OLDA" fi mine() { own_gw && { [ "$1" = "$OLDS" ] || [ "$1" = "$OLDA" ]; }; } if port_busy "$STRATUM" && ! mine "$STRATUM"; then NS="$(free_port "$STRATUM")"; say "PyBLOCK: port $STRATUM is already in use on this machine — using $NS for your miners instead."; STRATUM="$NS"; fi if port_busy "$APIPORT" && ! mine "$APIPORT"; then NA="$(free_port "$APIPORT")"; say "PyBLOCK: port $APIPORT is already in use — the gateway's local status page will be on $NA."; APIPORT="$NA"; fi CONF="$DIR/$PRODUCT.json" # Se crea vacío y con permiso restringido ANTES de escribir nada: si se escribiera primero, el # archivo nace con la umask del usuario (644 con la habitual) y queda legible con la credencial # adentro — y si el heredoc corta a mitad, queda así para siempre. : > "$CONF" chmod 600 "$CONF" cat > "$CONF" <&1 | grep -q key_file; then say "PyBLOCK: persistent gateway identity ON. Keep $DIR/gateway.key — the pool identifies your" say " gateway by its signing key, so that file is what keeps your share arriving. It" say " never leaves your machine." else sed -i '/"key_file":/d' "$CONF" chmod 600 "$CONF" say "PyBLOCK: NOTE — this build has no persistent gateway identity: it generates a new keypair" say " on every start, so key_file was removed from the config instead of pretending." say " Mining to your own address is unaffected. On the Carousel/CHIRP ports, where the" say " pool credits the node-runner by signing key, re-register after a restart." fi say "PyBLOCK: config written to $CONF (miners connect to YOUR machine on port $STRATUM)." # `always_pay_self` and `pooled_mining_only: false` are deliberate: if PyBLOCK is ever # unreachable your gateway keeps mining to your own address instead of stopping. You never # lose a block because of us. # ---- 4. run it ------------------------------------------------------------------------------ UNIT="pyblock-gateway-$PRODUCT" if [ "${PYBLOCK_NO_SYSTEMD:-0}" != "1" ] && command -v systemctl >/dev/null 2>&1 && [ "$(id -u)" = 0 ]; then # It must NOT run as root. This is a C daemon holding a public stratum port open with # Restart=always; under sudo we know the real user, and the node's credentials are readable by # them because that is where we just read them from. RUNAS="${SUDO_USER:-}" if [ -z "$RUNAS" ]; then say "PyBLOCK: running this as root directly, so the service will too. Prefer 'sudo sh -s -- …'" say " from your own account, or set PYBLOCK_RUNAS=." fi RUNAS="${PYBLOCK_RUNAS:-$RUNAS}" [ -n "$RUNAS" ] && chown -R "$RUNAS" "$DIR" 2>/dev/null || true cat > "/etc/systemd/system/$UNIT.service" </dev/null 2>&1 && systemctl restart "$UNIT" >/dev/null 2>&1; }; then # No dejar una unit escrita y habilitada que arranque sola en el próximo boot con algo que # no funciona. systemctl disable "$UNIT" >/dev/null 2>&1 || true rm -f "/etc/systemd/system/$UNIT.service"; systemctl daemon-reload die "could not start $UNIT, so the unit was removed. Run it by hand to see why: $BIN -c $CONF" fi say "PyBLOCK: running as $UNIT${RUNAS:+ (user $RUNAS)}, starts on boot. Logs: journalctl -u $UNIT" elif command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet "$UNIT" 2>/dev/null; then # 2026-11-01: not root, but the gateway is already a systemd unit (installed earlier with sudo; the PATUM panel # runs this as the user). Killing it and starting a second copy with nohup left two gateways fighting for the # port; restart the unit instead (PATUM's sudoers rule allows exactly this restart). if sudo -n systemctl restart "$UNIT" 2>/dev/null; then say "PyBLOCK: restarted $UNIT with the new gateway. Logs: journalctl -u $UNIT" else say "PyBLOCK: $UNIT is running the old gateway. Restart it to use the new one: sudo systemctl restart $UNIT" fi else pkill -f "datum_gateway -c $CONF" 2>/dev/null || true nohup "$BIN" -c "$CONF" >"$DIR/$PRODUCT.out" 2>&1 & say "PyBLOCK: running (pid $!). To keep it across reboots, re-run this as root for a systemd unit." fi # ---- 5. claim your share -------------------------------------------------------------------- # WAVICLES pays no node-runner share (your miners are paid by work in the window), so there is # nothing to claim: the gateway only has to reach the pool. if [ "$PRODUCT" = "wavicles" ]; then say "" say "PyBLOCK: done. Your WAVICLES gateway is running and talks to b.pyblock.xyz:$PRIME_PORT." say " Point your miners at: stratum+tcp://:$STRATUM with your BTC address as the username." say " Every block your gateway publishes pays the work window in its own coinbase (0 % PyBLØCK fee)." say " See it live on https://b.pyblock.xyz:8443/wavicles.php" exit 0 fi say "PyBLOCK: waiting for your gateway to reach the pool…" GW='' i=0 while [ $i -lt 30 ]; do sleep 4 i=$((i + 1)) GW="$(curl -fsS --max-time 8 "$CLAIM?mode=whoami&product=$PRODUCT" 2>/dev/null \ | sed -n 's/.*"gateway":"\([0-9a-f]*\)".*/\1/p' | head -1 || true)" [ -n "$GW" ] && break done if [ -z "$GW" ]; then say "" say "PyBLOCK: the gateway is running but has not reached the pool yet." say " Check $DIR/$PRODUCT.log for the reason, then claim your $SHARE with:" say " curl -fsSL '$CLAIM?mode=whoami&product=$PRODUCT'" say " and once it shows your gateway key:" say " curl -fsS -X POST -d product=$PRODUCT -d gateway= -d address=$ADDR $CLAIM" exit 1 fi say "PyBLOCK: connected. Your gateway key is $GW." # Sin -f: con -f curl descarta el cuerpo, y el cuerpo es justamente el mensaje que explica qué # hacer (el caso del NAT es el más probable de todos). RESP="$(curl -sS --max-time 10 -X POST -d "product=$PRODUCT" -d "gateway=$GW" -d "address=$ADDR" "$CLAIM" 2>&1 || true)" case "$RESP" in *'"ok":true'*) say "" say "PyBLOCK: done. Your $SHARE goes to $ADDR in the coinbase of every block your gateway publishes." say " Point your miners at: stratum+tcp://:$STRATUM with your BTC address as the username." say " Your block, built and broadcast by your node. PyBLOCK only dictates the coinbase." ;; *) say "" say "PyBLOCK: the gateway is connected but the claim did not go through:" say " $RESP" say " Retry with: curl -fsS -X POST -d product=$PRODUCT -d gateway=$GW -d address=$ADDR $CLAIM" exit 1 ;; esac