#!/usr/bin/env bash # Entrypoint for the MoonBite seed node on Railway. # # Data lives on a Railway Volume mounted at /data (set the Volume mount path # to /data in the service settings so the chain survives restarts/redeploys). # # RPC credentials come from Railway service Variables: # MOONBITE_RPC_USER, MOONBITE_RPC_PASSWORD (required) # MINE=1 (optional: mine blocks on this node) # MINE_ADDRESS= (optional: where mined coins go) set -euo pipefail DATADIR=/data mkdir -p "$DATADIR" # Credentials. DEPLOY.md told you to set BIGCOIN_RPC_USER/PASSWORD while this # script demanded MOONBITE_RPC_USER/PASSWORD and hard-failed without them, so a # service configured from the docs exited on the first line it reached. Accept # either spelling rather than making the name the thing that breaks the deploy. MOONBITE_RPC_USER="${MOONBITE_RPC_USER:-${BIGCOIN_RPC_USER:-}}" MOONBITE_RPC_PASSWORD="${MOONBITE_RPC_PASSWORD:-${BIGCOIN_RPC_PASSWORD:-}}" : "${MOONBITE_RPC_USER:?set MOONBITE_RPC_USER (or BIGCOIN_RPC_USER) in Railway Variables}" : "${MOONBITE_RPC_PASSWORD:?set MOONBITE_RPC_PASSWORD (or BIGCOIN_RPC_PASSWORD) in Railway Variables}" # The chain this binary considers valid. A /data Volume survives redeploys, so # after a consensus change (the 10 -> 50 MBITE subsidy, which moved genesis) the # volume still holds the OLD chain and the node sits on a fork nothing else # recognises: it handshakes with peers, then never answers getheaders. Stamp the # expected genesis alongside the data and wipe chain state when it disagrees. # Set RESET_CHAIN=1 to force the wipe. Wallets are left alone. EXPECTED_GENESIS=3d053c590c9dcaa972d12f20793cde15e060f6cbcd1ea7bd05f4c80724f39573 STAMP="$DATADIR/.genesis" if [[ "${RESET_CHAIN:-0}" == "1" ]] || { [[ -e "$DATADIR/blocks" ]] && [[ "$(cat "$STAMP" 2>/dev/null || echo none)" != "$EXPECTED_GENESIS" ]]; }; then echo "Chain data does not match genesis $EXPECTED_GENESIS - resetting to genesis." rm -rf "$DATADIR"/blocks "$DATADIR"/chainstate "$DATADIR"/indexes "$DATADIR"/mweb rm -f "$DATADIR"/leaf*.dat "$DATADIR"/mempool.dat "$DATADIR"/peers.dat "$DATADIR"/banlist.dat "$DATADIR"/anchors.dat "$DATADIR"/fee_estimates.dat rm -rf "$DATADIR"/probe fi echo "$EXPECTED_GENESIS" > "$STAMP" # P2P is public (via Railway TCP Proxy on 9444); RPC is NOT. # SECURITY: the RPC was previously bound to 0.0.0.0 + :: with rpcallowip # 0.0.0.0/0 + ::/0, which exposes a wallet-loaded node's spend/mining/admin RPC # to anything that can reach the port, behind only Basic auth — one weak/leaked # rpcpassword = full wallet drain. RPC is now loopback-only (fail closed). If # this (retired) Railway path is ever revived with an internal explorer that # needs the node RPC, bind it explicitly to the service's PRIVATE network # interface and set rpcallowip to that private subnet only — never 0.0.0.0/0, # and never attach a public domain/proxy to the RPC port. ARGS=( -datadir="$DATADIR" -server=1 -txindex=1 -listen=1 -bind=0.0.0.0:9444 -rpcbind=127.0.0.1 -rpcallowip=127.0.0.1 -rpcport=9445 -rpcuser="$MOONBITE_RPC_USER" -rpcpassword="$MOONBITE_RPC_PASSWORD" -printtoconsole # --- Memory caps so the container is not OOM-killed on small Railway plans. --- # dbcache defaults to ~450MB; on a 512MB container that alone triggers SIGKILL # ("Killed"). These keep the resident set well under ~256MB. Override any of # them via Railway Variables (DBCACHE / MAXMEMPOOL / PAR / MAXCONNECTIONS). -dbcache="${DBCACHE:-64}" -maxmempool="${MAXMEMPOOL:-50}" -par="${PAR:-1}" -maxconnections="${MAXCONNECTIONS:-40}" ) # Outbound peers. Without at least one, this node only ever sees INBOUND # connections (everything arriving through the TCP proxy is inbound), and a # node in initial block download refuses to fetch block bodies from inbound # peers - so it accepted headers from the network and sat at height 0 forever. # Dialling the seed makes that link outbound from our side, which block # download trusts. Comma-separated host:port list, overridable. IFS=',' read -ra _PEERS <<< "${ADDNODES:-67.205.154.64:9444}" for _peer in "${_PEERS[@]}"; do [[ -n "$_peer" ]] && ARGS+=( -addnode="$_peer" ) done # nMinimumChainWork is already zero in this binary's chainparams (verified in # source and in the shipped binary), so no override is needed. The variable # stays as an escape hatch only. if [[ -n "${MINIMUMCHAINWORK:-}" ]]; then ARGS+=( -minimumchainwork="$MINIMUMCHAINWORK" ) fi # Optional: advertise the public P2P address so peers can dial back in. if [[ -n "${EXTERNAL_IP:-}" ]]; then ARGS+=( -externalip="$EXTERNAL_IP" -discover=0 ) fi echo "Starting moonbited (MoonBite seed node) ..." moonbited "${ARGS[@]}" & NODE_PID=$! # Optional built-in miner so the fresh chain actually produces blocks. # A brand-new chain with zero miners stays at height 0 forever. if [[ "${MINE:-0}" == "1" ]]; then echo "MINE=1 -> will mine to ${MINE_ADDRESS:-}" ( # Wait for RPC to come up. until moonbite-cli -datadir="$DATADIR" -rpcport=9445 \ -rpcuser="$MOONBITE_RPC_USER" -rpcpassword="$MOONBITE_RPC_PASSWORD" \ getblockchaininfo >/dev/null 2>&1; do sleep 2 done mcli() { moonbite-cli -datadir="$DATADIR" -rpcport=9445 \ -rpcuser="$MOONBITE_RPC_USER" -rpcpassword="$MOONBITE_RPC_PASSWORD" "$@" } ADDR="${MINE_ADDRESS:-}" if [[ -z "$ADDR" ]]; then # First boot: create the "miner" wallet. On every later restart the wallet # already exists on the /data volume but Core does NOT auto-load it, so # createwallet fails and we must loadwallet instead. Try both (ignoring the # "already exists / already loaded" errors) so the wallet is loaded in all # three states: absent, present-unloaded, present-loaded. mcli createwallet miner >/dev/null 2>&1 || true mcli loadwallet miner >/dev/null 2>&1 || true # Do not start mining until we actually hold an address; a blank ADDR makes # every generatetoaddress call fail silently and the chain never advances. for _ in $(seq 1 15); do ADDR=$(mcli getnewaddress 2>/dev/null || true) [[ -n "$ADDR" ]] && break mcli loadwallet miner >/dev/null 2>&1 || true sleep 2 done echo "Mining to new wallet address: ${ADDR:-}" fi if [[ -z "$ADDR" ]]; then echo "Mining disabled: could not obtain a wallet address. Node stays up." exit 0 fi # Auto-stop after MINE_BLOCKS blocks (default 20) so a test run cannot # drain Railway credit. Set MINE_BLOCKS=0 to mine indefinitely. TARGET="${MINE_BLOCKS:-20}" HEIGHT=$(moonbite-cli -datadir="$DATADIR" -rpcport=9445 \ -rpcuser="$MOONBITE_RPC_USER" -rpcpassword="$MOONBITE_RPC_PASSWORD" \ getblockcount 2>/dev/null || echo 0) STOP_AT=$(( HEIGHT + TARGET )) echo "Mining test: current height $HEIGHT, will mine to $STOP_AT then stop." # Slow, steady mining loop (1 block attempt every few seconds) so we don't # peg the container CPU. RandomX is CPU-heavy; keep the batch small. while kill -0 "$NODE_PID" 2>/dev/null; do moonbite-cli -datadir="$DATADIR" -rpcport=9445 \ -rpcuser="$MOONBITE_RPC_USER" -rpcpassword="$MOONBITE_RPC_PASSWORD" \ generatetoaddress 1 "$ADDR" >/dev/null 2>&1 || true if [[ "$TARGET" != "0" ]]; then HEIGHT=$(moonbite-cli -datadir="$DATADIR" -rpcport=9445 \ -rpcuser="$MOONBITE_RPC_USER" -rpcpassword="$MOONBITE_RPC_PASSWORD" \ getblockcount 2>/dev/null || echo "$HEIGHT") if (( HEIGHT >= STOP_AT )); then echo "==========================================================" echo " TEST COMPLETE: mined $TARGET blocks, height is now $HEIGHT." echo " Mining stopped to save credit. Node still running (idle)." echo " Pause this Railway service now to stop all spending." echo "==========================================================" break fi fi sleep 5 done ) & fi wait "$NODE_PID"