From c502a59791531dab36bc416cbe2a3fa3ae700dd2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 4 Sep 2026 01:32:55 +0000 Subject: [PATCH] Make bind addresses and P2P ports survive git pull Keep localhost-only wallet RPC and custom P2P ports in .env instead of compose edits. Drop unused FEE_RECIPIENT, require Arch before Omarchy heuristics, and confirm before replacing distro Docker packages. Co-authored-by: David Feder --- .env.example | 17 +++++++---- README.md | 51 ++++++++++++++++++-------------- common.sh | 24 +++++++++++++--- docker-compose.yml | 27 +++++++++++++---- install.sh | 58 +++++++++++++++++++++++++++++-------- status.sh | 29 +++++++++++++++++-- tests/test_beacon_flags.sh | 57 ++++++++++++++++++++++++++++++++++-- tests/test_common.sh | 59 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 267 insertions(+), 55 deletions(-) diff --git a/.env.example b/.env.example index 0889fb4..933aaed 100644 --- a/.env.example +++ b/.env.example @@ -7,10 +7,13 @@ # Host data directory (JWT + execution + consensus). Default matches official docs. # DATA_DIR=/blockchain -# HTTP JSON-RPC (wallets). Bound to 0.0.0.0 (LAN). Default 8545 +# HTTP JSON-RPC (wallets). Default bind 0.0.0.0 (LAN), port 8545. +# Set HTTP_ADDR=127.0.0.1 for this machine only (survives git pull; do not edit compose). +# HTTP_ADDR=0.0.0.0 # HTTP_PORT=8545 -# WebSocket RPC. Bound to 0.0.0.0 (LAN). Default 8546 +# WebSocket RPC. Default bind 0.0.0.0 (LAN), port 8546. +# WS_ADDR=0.0.0.0 # WS_PORT=8546 # Beacon REST/JSON HTTP API. Default host 127.0.0.1 (this machine only), port 3500. @@ -26,13 +29,15 @@ # Lower this on 16 GB hosts if the machine swaps; raise it on 32 GB+ SSDs. # GETH_CACHE=1024 +# P2P listen ports (host networking). Change only if you already forwarded +# different ports or run a second node. Keep TCP/UDP pairs in sync on the router. +# GETH_P2P_PORT=30303 +# BEACON_P2P_TCP_PORT=13000 +# BEACON_P2P_UDP_PORT=12000 + # Optional: pin or float client images. # Defaults in docker-compose.yml are digest-pinned version tags. # GETH_IMAGE=registry.gitlab.com/pulsechaincom/go-pulse:v3.3.0 # BEACON_IMAGE=registry.gitlab.com/pulsechaincom/prysm-pulse/beacon-chain:v2.3.0 # To float on upstream latest instead: ./update.sh --latest # (that command writes GETH_IMAGE / BEACON_IMAGE into this file so it sticks) - -# Suggested validator fee recipient (when you later run a validator). -# Not used by this RPC stack. Leave blank if you only need a private RPC. -# FEE_RECIPIENT=0xYourPulseChainAddressHere diff --git a/README.md b/README.md index 6186aec..a205eeb 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This project packages the official PulseChain clients in Docker Compose with a s | [Go-Pulse](https://gitlab.com/pulsechaincom/go-pulse) | Execution layer (JSON-RPC / WebSocket) | `registry.gitlab.com/pulsechaincom/go-pulse:v3.3.0` | | [Prysm-Pulse](https://gitlab.com/pulsechaincom/prysm-pulse) | Consensus layer (beacon chain) | `registry.gitlab.com/pulsechaincom/prysm-pulse/beacon-chain:v2.3.0` | -**Defaults:** mainnet · checkpoint sync · data under `/blockchain` · wallet RPC on the LAN (`0.0.0.0`) · Engine API and beacon HTTP/gRPC localhost-only · digest-pinned client tags (`./update.sh --latest` writes `:latest` into `.env`) +**Defaults:** mainnet · checkpoint sync · data under `/blockchain` · wallet RPC on the LAN (`0.0.0.0`) · Engine API and beacon HTTP/gRPC localhost-only · digest-pinned client tags (`./update.sh --latest` writes `:latest` into `.env`) · bind addresses and P2P ports via `.env` (so `git pull` does not wipe them) --- @@ -82,7 +82,7 @@ CORS / vhosts default to `*` so LAN web wallets can reach the node. A page you v The installer warns if the host itself has a public IP and UFW is not active. That is the typical VPS misconfiguration (an unauthenticated public RPC). Home machines behind NAT are fine. -LAN binding is intentional so phones and other machines on the same network can use `http://YOUR_LAN_IP:8545`. To restrict access to the host only, see [Localhost-only mode](#localhost-only-mode). +LAN binding is intentional so phones and other machines on the same network can use `http://YOUR_LAN_IP:8545`. To restrict access to the host only, set `HTTP_ADDR` / `WS_ADDR` in `.env` — see [Localhost-only mode](#localhost-only-mode). --- @@ -208,13 +208,14 @@ Chain data is stored under **`/blockchain`** and is retained when containers are By default, **wallet** RPC binds to `0.0.0.0` (all interfaces). The Engine API (`--authrpc.addr=127.0.0.1`, port 8551) and beacon HTTP/gRPC (`BEACON_HTTP_HOST` / `BEACON_GRPC_HOST`, default `127.0.0.1`) are already host-only. -To accept wallet RPC connections **only on the host**: +To accept wallet RPC connections **only on the host**, set these in `.env` (do not edit `docker-compose.yml` — a `git pull` would overwrite compose edits): -1. Edit `docker-compose.yml`. -2. Under the **geth** service, change: - - `--http.addr=0.0.0.0` → `--http.addr=127.0.0.1` - - `--ws.addr=0.0.0.0` → `--ws.addr=127.0.0.1` -3. Apply the change (`./restart.sh` recreates containers from compose; it does not keep stale flags): +```bash +HTTP_ADDR=127.0.0.1 +WS_ADDR=127.0.0.1 +``` + +Then apply: ```bash ./restart.sh @@ -239,14 +240,14 @@ Then run `./restart.sh`. Keep those ports firewalled to your LAN; never port-for | Port | Protocol | Purpose | Default bind | |------|----------|---------|--------------| -| 8545 | TCP | HTTP JSON-RPC (wallets) | `0.0.0.0` (LAN) | -| 8546 | TCP | WebSocket RPC | `0.0.0.0` (LAN) | +| 8545 | TCP | HTTP JSON-RPC (wallets) | `0.0.0.0` (LAN; `HTTP_ADDR` / `HTTP_PORT`) | +| 8546 | TCP | WebSocket RPC | `0.0.0.0` (LAN; `WS_ADDR` / `WS_PORT`) | | 3500 | TCP | Beacon REST API | `127.0.0.1` (localhost; override with `BEACON_HTTP_HOST`) | | 4000 | TCP | Beacon gRPC | `127.0.0.1` (localhost; override with `BEACON_GRPC_HOST`) | | 8551 | TCP | Engine API (JWT; geth ↔ beacon) | `127.0.0.1` (localhost only) | -| 30303 | TCP/UDP | Execution P2P | Host | -| 13000 | TCP | Beacon P2P | Host | -| 12000 | UDP | Beacon P2P | Host | +| 30303 | TCP/UDP | Execution P2P | Host (`GETH_P2P_PORT`) | +| 13000 | TCP | Beacon P2P | Host (`BEACON_P2P_TCP_PORT`) | +| 12000 | UDP | Beacon P2P | Host (`BEACON_P2P_UDP_PORT`) | **Do not** forward RPC/API ports 8545, 8546, 3500, or 4000 to the public internet. @@ -258,13 +259,13 @@ Your node can already make **outbound** connections. That is not enough if you w To properly participate in the network you should also accept **inbound** peers. Nodes that only make outbound connections put more load on the network and usually have worse peer counts and slower sync. -**Open these ports for inbound traffic:** +**Open these ports for inbound traffic** (defaults; keep the router in sync if you change `.env`): -| Port | Protocol | Purpose | -|------|----------|---------| -| 30303 | TCP + UDP | Go-Pulse (execution) | -| 13000 | TCP | Beacon P2P | -| 12000 | UDP | Beacon P2P | +| Port | Protocol | Purpose | `.env` | +|------|----------|---------|--------| +| 30303 | TCP + UDP | Go-Pulse (execution) | `GETH_P2P_PORT` | +| 13000 | TCP | Beacon P2P | `BEACON_P2P_TCP_PORT` | +| 12000 | UDP | Beacon P2P | `BEACON_P2P_UDP_PORT` | ### 1. Firewall on the node (UFW) @@ -277,6 +278,8 @@ sudo ufw allow 13000/tcp sudo ufw allow 12000/udp ``` +`./install.sh` uses the P2P ports from `.env` when it adds these rules. + ### 2. Port forwarding on your router (required for inbound peers) This is the part most people skip — and it’s the most important. @@ -338,17 +341,20 @@ On a VPS or cloud VM, `10.0.0.0/8` (and often `172.16.0.0/12`) is the **VPC**, n | JWT secret | `$DATA_DIR/jwt.hex` (mode `600`) | | Execution image | `go-pulse:v3.3.0` digest-pinned (override with `GETH_IMAGE` or `./update.sh --latest`) | | Beacon image | `beacon-chain:v2.3.0` digest-pinned (override with `BEACON_IMAGE` or `./update.sh --latest`) | +| Wallet RPC bind | `0.0.0.0` (`HTTP_ADDR` / `WS_ADDR`; set `127.0.0.1` for localhost-only) | | Beacon HTTP / gRPC | `127.0.0.1` (`BEACON_HTTP_HOST` / `BEACON_GRPC_HOST`) | +| P2P ports | `30303` / `13000` / `12000` (`GETH_P2P_PORT`, `BEACON_P2P_TCP_PORT`, `BEACON_P2P_UDP_PORT`) | | Geth IPC | disabled (`--ipcdisable`; admin APIs are not on the host filesystem) | | Geth cache | `1024` MB (`GETH_CACHE`) | | File descriptors | `nofile` 65535 (container ulimit) | | Checkpoint sync | `https://checkpoint.pulsechain.com` (trusted third party; same as official docs) | | Restart policy | `unless-stopped` | | Stop grace period | `5m` | +| Geth healthcheck | `geth attach --exec eth.chainId` against local HTTP RPC (beacon does not wait on it) | | Container logs | json-file, 50 MB × 5 files | | Networking | `host` (aligned with official examples; simplifies P2P) | -Optional variables (`DATA_DIR`, ports, image pins) are documented in `.env.example` and are read by both Compose and the helper scripts. +Optional variables (`DATA_DIR`, bind addresses, ports, image pins) are documented in `.env.example` and are read by both Compose and the helper scripts. Changing `DATA_DIR` after the first sync starts a new empty node at the new path; the old chain data stays where it was. --- @@ -357,10 +363,11 @@ Optional variables (`DATA_DIR`, ports, image pins) are documented in `.env.examp | Issue | Suggested action | |-------|------------------| | Docker permission denied | Log out and back in after install (docker group membership), or prefix commands with `sudo`. On Omarchy this is common until the session picks up the `docker` group. | -| `address already in use` / crash loop | Another node is using ports 8545, 8546, 3500, 4000, or 8551. Stop the other process or change ports in `docker-compose.yml` | +| Installer asks about Docker CE | On Ubuntu/Debian, replacing `docker.io` / distro Compose can stop existing containers. Confirm only if you want Docker CE, or install Docker yourself and re-run. Unattended: `PULSE_ALLOW_DOCKER_CE=1`. | +| `address already in use` / crash loop | Another node is using ports 8545, 8546, 3500, 4000, or 8551. Stop the other process or change ports in `.env` | | Beacon cannot find execution client | Confirm both containers are running and that `/blockchain/jwt.hex` exists and is shared by both | | JWT / `401 Unauthorized` to execution | Ensure only one execution client is on port 8551 and both services use the same `/blockchain/jwt.hex`. The file must be 64 hex characters with **no newline**. Keep the host clock in sync (NTP / `timedatectl`); JWT `iat` skew also returns 401. | -| Wallet cannot connect | Verify LAN IP from `./status.sh` (not `hostname -I` / docker0), same network, host firewall rules; test `curl` against `127.0.0.1:8545` on the node. `./restart.sh` after compose edits (it recreates containers). | +| Wallet cannot connect | Verify LAN IP from `./status.sh` (not `hostname -I` / docker0), same network, host firewall rules; test `curl` against `127.0.0.1:8545` on the node. If you set `HTTP_ADDR=127.0.0.1`, only wallets on this machine work. `./restart.sh` after `.env` or compose edits (it recreates containers). | | `./restart.sh` did nothing (old versions) | Current `restart.sh` uses `docker compose up -d --force-recreate`. Upgrade the scripts if an older copy only ran `up -d`. | | Disk space pressure | Full nodes grow over time — monitor free space and use a large SSD | | Slow sync | Prefer NVMe storage, adequate RAM, and open P2P ports where practical. On 16 GB hosts, try `GETH_CACHE=512` in `.env` if the machine swaps. | diff --git a/common.sh b/common.sh index a74b63c..3d2cf74 100755 --- a/common.sh +++ b/common.sh @@ -13,7 +13,7 @@ GETH_CONTAINER="pulse-geth" BEACON_CONTAINER="pulse-beacon" # Env keys interpolated by docker-compose.yml / passed through sudo. -COMPOSE_ENV_KEYS="DATA_DIR,HTTP_PORT,WS_PORT,BEACON_HTTP_PORT,BEACON_GRPC_PORT,BEACON_HTTP_HOST,BEACON_GRPC_HOST,GETH_IMAGE,BEACON_IMAGE,GETH_CACHE" +COMPOSE_ENV_KEYS="DATA_DIR,HTTP_PORT,WS_PORT,HTTP_ADDR,WS_ADDR,BEACON_HTTP_PORT,BEACON_GRPC_PORT,BEACON_HTTP_HOST,BEACON_GRPC_HOST,GETH_IMAGE,BEACON_IMAGE,GETH_CACHE,GETH_P2P_PORT,BEACON_P2P_TCP_PORT,BEACON_P2P_UDP_PORT" _trim() { local s="$1" @@ -100,15 +100,28 @@ load_dotenv "${_COMMON_DIR}/.env" [[ -n "${DATA_DIR:-}" ]] || DATA_DIR=/blockchain [[ -n "${HTTP_PORT:-}" ]] || HTTP_PORT=8545 [[ -n "${WS_PORT:-}" ]] || WS_PORT=8546 +[[ -n "${HTTP_ADDR:-}" ]] || HTTP_ADDR=0.0.0.0 +[[ -n "${WS_ADDR:-}" ]] || WS_ADDR=0.0.0.0 [[ -n "${BEACON_HTTP_PORT:-}" ]] || BEACON_HTTP_PORT=3500 [[ -n "${BEACON_GRPC_PORT:-}" ]] || BEACON_GRPC_PORT=4000 [[ -n "${BEACON_HTTP_HOST:-}" ]] || BEACON_HTTP_HOST=127.0.0.1 [[ -n "${BEACON_GRPC_HOST:-}" ]] || BEACON_GRPC_HOST=127.0.0.1 [[ -n "${GETH_CACHE:-}" ]] || GETH_CACHE=1024 +[[ -n "${GETH_P2P_PORT:-}" ]] || GETH_P2P_PORT=30303 +[[ -n "${BEACON_P2P_TCP_PORT:-}" ]] || BEACON_P2P_TCP_PORT=13000 +[[ -n "${BEACON_P2P_UDP_PORT:-}" ]] || BEACON_P2P_UDP_PORT=12000 [[ -n "${GETH_IMAGE:-}" ]] || GETH_IMAGE="${GETH_IMAGE_PINNED}" [[ -n "${BEACON_IMAGE:-}" ]] || BEACON_IMAGE="${BEACON_IMAGE_PINNED}" -export DATA_DIR HTTP_PORT WS_PORT BEACON_HTTP_PORT BEACON_GRPC_PORT -export BEACON_HTTP_HOST BEACON_GRPC_HOST GETH_CACHE GETH_IMAGE BEACON_IMAGE +export DATA_DIR HTTP_PORT WS_PORT HTTP_ADDR WS_ADDR BEACON_HTTP_PORT BEACON_GRPC_PORT +export BEACON_HTTP_HOST BEACON_GRPC_HOST GETH_CACHE GETH_P2P_PORT +export BEACON_P2P_TCP_PORT BEACON_P2P_UDP_PORT GETH_IMAGE BEACON_IMAGE + +wallet_rpc_is_localhost() { + case "${HTTP_ADDR}" in + 127.0.0.1|localhost) return 0 ;; + *) return 1 ;; + esac +} # Cache how we talk to Docker so we do not run `docker info` on every call. _DOCKER_MODE="" @@ -276,10 +289,13 @@ os_is_debian_family() { } # Omarchy (https://omarchy.org) is Arch-based. Stock images still report ID=arch, -# so also look for Omarchy tools and install paths. +# so also look for Omarchy tools and install paths — but only on Arch-family hosts. +# Leftover ~/.local/share/omarchy on Ubuntu must not steal the Debian Docker CE path. os_is_omarchy() { local os_id="${1:-}" + local id_like="${2:-}" [[ "${os_id}" == "omarchy" ]] && return 0 + os_is_arch_family "${os_id}" "${id_like}" || return 1 command -v omarchy-pkg-add >/dev/null 2>&1 && return 0 command -v omarchy >/dev/null 2>&1 && return 0 [[ -f /etc/profile.d/omarchy.sh ]] && return 0 diff --git a/docker-compose.yml b/docker-compose.yml index bc28335..e5aca6b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -48,19 +48,34 @@ services: - --authrpc.port=8551 - --authrpc.vhosts=localhost - --http - - --http.addr=0.0.0.0 + - --http.addr=${HTTP_ADDR:-0.0.0.0} - --http.port=${HTTP_PORT:-8545} - --http.api=eth,net,web3,txpool - --http.corsdomain=* - --http.vhosts=* - --ws - - --ws.addr=0.0.0.0 + - --ws.addr=${WS_ADDR:-0.0.0.0} - --ws.port=${WS_PORT:-8546} - --ws.api=eth,net,web3,txpool - --ws.origins=* - --maxpeers=150 - - --discovery.port=30303 - - --port=30303 + - --discovery.port=${GETH_P2P_PORT:-30303} + - --port=${GETH_P2P_PORT:-30303} + # Image is alpine + geth only. attach-to-HTTP is the available readiness probe. + # Beacon still starts as soon as the container exists (depends_on without + # service_healthy) so a probe mismatch cannot dead-lock first boot. + healthcheck: + test: + - CMD + - geth + - attach + - --exec + - eth.chainId + - http://127.0.0.1:${HTTP_PORT:-8545} + interval: 20s + timeout: 10s + retries: 15 + start_period: 45s # --------------------------------------------------------------------------- # Consensus client — Prysm-Pulse Beacon Chain @@ -93,6 +108,6 @@ services: # gRPC API (localhost; not needed for MetaMask) - --rpc-host=${BEACON_GRPC_HOST:-127.0.0.1} - --rpc-port=${BEACON_GRPC_PORT:-4000} - - --p2p-tcp-port=13000 - - --p2p-udp-port=12000 + - --p2p-tcp-port=${BEACON_P2P_TCP_PORT:-13000} + - --p2p-udp-port=${BEACON_P2P_UDP_PORT:-12000} - --p2p-max-peers=150 diff --git a/install.sh b/install.sh index 13a155e..94d563b 100755 --- a/install.sh +++ b/install.sh @@ -68,7 +68,7 @@ else OS_ID_LIKE="" fi -if os_is_omarchy "${OS_ID}"; then +if os_is_omarchy "${OS_ID}" "${OS_ID_LIKE}"; then ok "Detected Omarchy Linux (Arch-based)" elif os_is_debian_family "${OS_ID}"; then ok "Detected Debian-family OS: ${OS_ID}" @@ -105,9 +105,35 @@ elif ! docker compose version >/dev/null 2>&1; then fi fi +debian_docker_conflict_installed() { + dpkg-query -W -f='${Status} ${Package}\n' docker.io docker-doc docker-compose docker-compose-v2 podman-docker 2>/dev/null \ + | grep -q 'install ok installed' +} + if [[ "${need_docker_install}" == true ]]; then info "Installing Docker Engine + Compose plugin..." if os_is_debian_family "${OS_ID}"; then + echo "" + warn "Docker CE setup removes conflicting distro packages if they are present:" + warn " docker.io, docker-doc, docker-compose, docker-compose-v2, podman-docker, containerd, runc" + warn "Other containers using those packages may stop. Named volumes and bind mounts are not deleted." + echo "" + if debian_docker_conflict_installed; then + warn "This machine already has a distro Docker package (docker.io / docker-compose / podman-docker)." + warn "Replacing it with Docker CE is the usual fix, but it can disrupt existing containers." + warn "To skip this prompt, re-run with PULSE_ALLOW_DOCKER_CE=1" + echo "" + if [[ "${PULSE_ALLOW_DOCKER_CE:-}" == "1" ]]; then + warn "Continuing Docker CE replacement because PULSE_ALLOW_DOCKER_CE=1" + elif confirm_yes "Remove conflicting distro Docker packages and install Docker CE? [y/N] "; then + warn "Proceeding with Docker CE install..." + else + if [[ ! -t 0 ]]; then + die "Aborted (non-interactive). Install Docker CE yourself, or re-run with PULSE_ALLOW_DOCKER_CE=1" + fi + die "Aborted. Install Docker manually, or re-run and confirm, or set PULSE_ALLOW_DOCKER_CE=1" + fi + fi $SUDO apt-get update -y # Distro docker.io / containerd packages conflict with Docker CE. $SUDO apt-get remove -y docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc || true @@ -140,7 +166,7 @@ if [[ "${need_docker_install}" == true ]]; then else die "Docker was installed but the daemon is not responding. Try: sudo systemctl status docker" fi - elif os_is_omarchy "${OS_ID}" || os_is_arch_family "${OS_ID}" "${OS_ID_LIKE}"; then + elif os_is_omarchy "${OS_ID}" "${OS_ID_LIKE}" || os_is_arch_family "${OS_ID}" "${OS_ID_LIKE}"; then # Arch packages: docker-compose is the v2 CLI plugin (`docker compose`). # Do not use pacman -Syu — Omarchy's ALPM guard aborts unattended sysupgrades. install_arch_packages docker docker-compose docker-buildx openssl @@ -176,7 +202,7 @@ if ! command -v openssl >/dev/null 2>&1; then if os_is_debian_family "${OS_ID}"; then $SUDO apt-get update -y $SUDO apt-get install -y openssl || die "Please install openssl and re-run." - elif os_is_omarchy "${OS_ID}" || os_is_arch_family "${OS_ID}" "${OS_ID_LIKE}"; then + elif os_is_omarchy "${OS_ID}" "${OS_ID_LIKE}" || os_is_arch_family "${OS_ID}" "${OS_ID_LIKE}"; then install_arch_packages openssl else die "openssl is required. Please install it and re-run." @@ -305,7 +331,7 @@ fi # --------------------------------------------------------------------------- # 7. Port conflict pre-check (host networking shares the host's ports) # --------------------------------------------------------------------------- -PORTS_TO_CHECK=("${HTTP_PORT}" "${WS_PORT}" "${BEACON_HTTP_PORT}" "${BEACON_GRPC_PORT}" 8551 30303 13000 12000) +PORTS_TO_CHECK=("${HTTP_PORT}" "${WS_PORT}" "${BEACON_HTTP_PORT}" "${BEACON_GRPC_PORT}" 8551 "${GETH_P2P_PORT}" "${BEACON_P2P_TCP_PORT}" "${BEACON_P2P_UDP_PORT}") if our_stack_running; then ok "Existing ${GETH_CONTAINER}/${BEACON_CONTAINER} detected — re-run will refresh this stack (not a foreign port conflict)." else @@ -387,10 +413,10 @@ if command -v ufw >/dev/null 2>&1; then } # Allow P2P for better connectivity - ufw_try 30303/tcp comment 'PulseChain Geth P2P' - ufw_try 30303/udp comment 'PulseChain Geth P2P' - ufw_try 13000/tcp comment 'PulseChain Beacon P2P TCP' - ufw_try 12000/udp comment 'PulseChain Beacon P2P UDP' + ufw_try "${GETH_P2P_PORT}/tcp" comment 'PulseChain Geth P2P' + ufw_try "${GETH_P2P_PORT}/udp" comment 'PulseChain Geth P2P' + ufw_try "${BEACON_P2P_TCP_PORT}/tcp" comment 'PulseChain Beacon P2P TCP' + ufw_try "${BEACON_P2P_UDP_PORT}/udp" comment 'PulseChain Beacon P2P UDP' # Restrict wallet RPC to common private LAN ranges (safe default). # Beacon HTTP/gRPC default to localhost; rules still help if you later bind them to the LAN. @@ -412,8 +438,8 @@ if command -v ufw >/dev/null 2>&1; then echo "" if ufw_is_active; then ok "UFW is already active — new PulseChain rules apply immediately." - if os_is_omarchy "${OS_ID}"; then - info "Omarchy defaults to deny-incoming; inbound P2P (30303/13000/12000) is now allowed." + if os_is_omarchy "${OS_ID}" "${OS_ID_LIKE}"; then + info "Omarchy defaults to deny-incoming; inbound P2P (${GETH_P2P_PORT}/${BEACON_P2P_TCP_PORT}/${BEACON_P2P_UDP_PORT}) is now allowed." fi warn "If your LAN uses a different subnet than 10/8, 172.16/12, or 192.168/16, edit the RPC rules." warn "IPv4 rules do not cover IPv6 — if the host has global IPv6, add matching rules or disable it." @@ -475,14 +501,22 @@ echo -e " Images: ${BOLD}${GETH_IMAGE}${NC}" echo -e " ${BOLD}${BEACON_IMAGE}${NC}" echo "" echo -e "${YELLOW}${BOLD}SECURITY REMINDER${NC}" -echo -e " Wallet RPC ports ${BOLD}${HTTP_PORT}${NC} and ${BOLD}${WS_PORT}${NC} are open on your LAN." +if wallet_rpc_is_localhost; then + echo -e " Wallet RPC binds ${BOLD}${HTTP_ADDR}${NC}:${HTTP_PORT} (this machine only)." +else + echo -e " Wallet RPC ports ${BOLD}${HTTP_PORT}${NC} and ${BOLD}${WS_PORT}${NC} are open on your LAN (${HTTP_ADDR} / ${WS_ADDR})." +fi echo -e " Beacon HTTP (${BEACON_HTTP_PORT}) and gRPC (${BEACON_GRPC_PORT}) bind ${BEACON_HTTP_HOST} / ${BEACON_GRPC_HOST}." echo -e " Engine API (8551) is localhost-only. Use only on a trusted home network." echo -e " ${BOLD}Do not${NC} port-forward RPC/API ports to the internet." echo "" echo -e "${BOLD}Connect MetaMask / Internet Money:${NC}" echo -e " Network Name: PulseChain" -echo -e " RPC URL: ${CYAN}http://${LAN_IP}:${HTTP_PORT}${NC}" +if wallet_rpc_is_localhost; then + echo -e " RPC URL: ${CYAN}http://127.0.0.1:${HTTP_PORT}${NC}" +else + echo -e " RPC URL: ${CYAN}http://${LAN_IP}:${HTTP_PORT}${NC}" +fi echo -e " Chain ID: 369" echo -e " Symbol: PLS" echo -e " Explorer: https://scan.pulsechain.com" diff --git a/status.sh b/status.sh index 468096a..e78a79c 100755 --- a/status.sh +++ b/status.sh @@ -53,8 +53,10 @@ elif isinstance(sync_result, dict): current = hex_int(sync_result.get("currentBlock", "?")) highest = hex_int(sync_result.get("highestBlock", "?")) print(f"eth_syncing: yes current={current} highest={highest}") + print("Do not send transactions yet (execution still syncing).") else: print(f"eth_syncing: {sync_result}") + print("Do not send transactions yet (execution not reporting synced).") print(f"eth_blockNumber: {block_n}") print(f"net_peerCount: {peer_n}") @@ -101,8 +103,10 @@ if is_syncing is False: print("syncing: false (beacon reports synced)") elif is_syncing is True: print(f"syncing: yes head_slot={head} distance={distance}") + print("Do not send transactions yet (beacon still syncing).") else: print(f"syncing: {is_syncing}") + print("Do not send transactions yet (beacon not reporting synced).") if el_offline is not None: print(f"el_offline: {el_offline}") @@ -124,10 +128,29 @@ fi echo "" echo "=== Wallet RPC ===" LAN_IP="$(detect_lan_ip)" -echo " LAN: http://${LAN_IP}:${HTTP_PORT}" -echo " This host: http://127.0.0.1:${HTTP_PORT}" +if wallet_rpc_is_localhost; then + echo " Bind: ${HTTP_ADDR}:${HTTP_PORT} (localhost-only; phones on the LAN cannot connect)" + echo " This host: http://127.0.0.1:${HTTP_PORT}" +else + echo " Bind: ${HTTP_ADDR}:${HTTP_PORT}" + echo " LAN: http://${LAN_IP}:${HTTP_PORT}" + echo " This host: http://127.0.0.1:${HTTP_PORT}" + echo " (hostname -I can print docker0 — prefer the LAN line above or this script)" +fi echo " Chain ID: 369 (PulseChain)" -echo " (hostname -I can print docker0 — prefer the LAN line above or this script)" + +echo "" +echo "=== Images ===" +if container_running "${GETH_CONTAINER}"; then + echo " ${GETH_CONTAINER}: $(run_docker inspect -f '{{.Config.Image}}' "${GETH_CONTAINER}" 2>/dev/null || echo unknown)" +else + echo " ${GETH_CONTAINER}: not running" +fi +if container_running "${BEACON_CONTAINER}"; then + echo " ${BEACON_CONTAINER}: $(run_docker inspect -f '{{.Config.Image}}' "${BEACON_CONTAINER}" 2>/dev/null || echo unknown)" +else + echo " ${BEACON_CONTAINER}: not running" +fi echo "" echo "=== Disk (${DATA_DIR}) ===" diff --git a/tests/test_beacon_flags.sh b/tests/test_beacon_flags.sh index f4c2436..fe491bc 100755 --- a/tests/test_beacon_flags.sh +++ b/tests/test_beacon_flags.sh @@ -75,6 +75,37 @@ else fail "missing --cache=\${GETH_CACHE:-1024} in docker-compose.yml" fi +if grep -E '^\s+-\s+--http\.addr=\$\{HTTP_ADDR:-0\.0\.0\.0\}' "$COMPOSE" >/dev/null; then + pass "geth --http.addr is tunable via HTTP_ADDR" +else + fail "missing --http.addr=\${HTTP_ADDR:-0.0.0.0} in docker-compose.yml" +fi + +if grep -E '^\s+-\s+--ws\.addr=\$\{WS_ADDR:-0\.0\.0\.0\}' "$COMPOSE" >/dev/null; then + pass "geth --ws.addr is tunable via WS_ADDR" +else + fail "missing --ws.addr=\${WS_ADDR:-0.0.0.0} in docker-compose.yml" +fi + +if grep -E '^\s+-\s+--port=\$\{GETH_P2P_PORT:-30303\}' "$COMPOSE" >/dev/null \ + && grep -E '^\s+-\s+--p2p-tcp-port=\$\{BEACON_P2P_TCP_PORT:-13000\}' "$COMPOSE" >/dev/null; then + pass "P2P ports are tunable via .env" +else + fail "missing interpolated GETH_P2P_PORT / BEACON_P2P_TCP_PORT in docker-compose.yml" +fi + +if grep -q 'healthcheck:' "$COMPOSE" && grep -q 'eth.chainId' "$COMPOSE"; then + pass "geth healthcheck uses geth attach eth.chainId" +else + fail "docker-compose.yml should healthcheck geth via attach eth.chainId" +fi + +if grep -q 'condition: service_healthy' "$COMPOSE"; then + fail "beacon must not wait on service_healthy (probe mismatch would dead-lock first boot)" +else + pass "beacon depends_on does not require service_healthy" +fi + if grep -E '^\s+-\s+--subscribe-all-subnets' "$COMPOSE" >/dev/null; then fail "subscribe-all-subnets should not be set for a private RPC node" else @@ -123,8 +154,10 @@ run_compose_config() { return 1 fi cfg="$( - env -u DATA_DIR -u HTTP_PORT -u WS_PORT -u BEACON_HTTP_PORT -u BEACON_GRPC_PORT \ + env -u DATA_DIR -u HTTP_PORT -u WS_PORT -u HTTP_ADDR -u WS_ADDR \ + -u BEACON_HTTP_PORT -u BEACON_GRPC_PORT \ -u BEACON_HTTP_HOST -u BEACON_GRPC_HOST -u GETH_CACHE \ + -u GETH_P2P_PORT -u BEACON_P2P_TCP_PORT -u BEACON_P2P_UDP_PORT \ -u GETH_IMAGE -u BEACON_IMAGE \ docker compose --env-file /dev/null -f "$COMPOSE" config 2>&1 )" || { @@ -192,6 +225,15 @@ if CFG="$(run_compose_config)"; then assert_contains "$GETH_CMD" '--http.port=8545' \ "compose config: --http.port=8545" \ "compose config missing default --http.port=8545" + assert_contains "$GETH_CMD" '--http.addr=0.0.0.0' \ + "compose config: --http.addr=0.0.0.0" \ + "compose config missing default --http.addr=0.0.0.0" + assert_contains "$GETH_CMD" '--port=30303' \ + "compose config: --port=30303" \ + "compose config missing default geth P2P port" + assert_contains "$BEACON_CMD" '--p2p-tcp-port=13000' \ + "compose config: --p2p-tcp-port=13000" \ + "compose config missing default beacon P2P TCP port" assert_contains "$CFG" 'go-pulse:v3.3.0' \ "compose config pins go-pulse:v3.3.0" \ "compose config did not pin go-pulse:v3.3.0" @@ -207,7 +249,7 @@ if CFG="$(run_compose_config)"; then # .env / environment interpolation still works OVERRIDE="$(DATA_DIR=/mnt/pulse-data HTTP_PORT=18545 BEACON_HTTP_PORT=13500 \ - BEACON_HTTP_HOST=0.0.0.0 GETH_CACHE=512 \ + BEACON_HTTP_HOST=0.0.0.0 GETH_CACHE=512 HTTP_ADDR=127.0.0.1 GETH_P2P_PORT=40303 \ docker compose --env-file /dev/null -f "$COMPOSE" config 2>&1)" || { fail "docker compose config with overrides failed: $OVERRIDE" OVERRIDE="" @@ -230,6 +272,12 @@ if CFG="$(run_compose_config)"; then assert_contains "$OVERRIDE" '--cache=512' \ "GETH_CACHE override interpolates" \ "GETH_CACHE override did not interpolate" + assert_contains "$OVERRIDE" '--http.addr=127.0.0.1' \ + "HTTP_ADDR override interpolates" \ + "HTTP_ADDR override did not interpolate" + assert_contains "$OVERRIDE" '--port=40303' \ + "GETH_P2P_PORT override interpolates" \ + "GETH_P2P_PORT override did not interpolate" fi fi @@ -257,6 +305,11 @@ if [[ -f "$README" ]]; then else fail "README should mention --authrpc.addr=127.0.0.1" fi + if grep -q 'HTTP_ADDR=127.0.0.1' "$README" && ! grep -q '\-\-http.addr=0.0.0.0.*127.0.0.1' "$README"; then + pass "README documents localhost wallet RPC via HTTP_ADDR in .env" + else + fail "README should tell users to set HTTP_ADDR in .env (not edit compose)" + fi if grep -q './status.sh' "$README" && ! grep -Fq 'hostname -I | awk' "$README"; then pass "README does not recommend hostname -I as the LAN IP method" else diff --git a/tests/test_common.sh b/tests/test_common.sh index 2909285..1c94e48 100755 --- a/tests/test_common.sh +++ b/tests/test_common.sh @@ -84,6 +84,26 @@ else fail "empty-value defaults got '${empty_port}'" fi +if [[ "${HTTP_ADDR}" == "0.0.0.0" && "${WS_ADDR}" == "0.0.0.0" && "${GETH_P2P_PORT}" == "30303" ]]; then + pass "HTTP_ADDR / WS_ADDR / GETH_P2P_PORT defaults" +else + fail "bind/P2P defaults got HTTP_ADDR='${HTTP_ADDR:-}' WS_ADDR='${WS_ADDR:-}' GETH_P2P_PORT='${GETH_P2P_PORT:-}'" +fi + +empty_bind="$(HTTP_ADDR='' WS_ADDR='' GETH_P2P_PORT='' BEACON_P2P_TCP_PORT='' BEACON_P2P_UDP_PORT='' bash -c \ + "source '${ROOT}/common.sh'; printf '%s %s %s %s %s' \"\${HTTP_ADDR}\" \"\${WS_ADDR}\" \"\${GETH_P2P_PORT}\" \"\${BEACON_P2P_TCP_PORT}\" \"\${BEACON_P2P_UDP_PORT}\"")" +if [[ "${empty_bind}" == "0.0.0.0 0.0.0.0 30303 13000 12000" ]]; then + pass "empty HTTP_ADDR / P2P ports fall back to defaults" +else + fail "empty bind/P2P defaults got '${empty_bind}'" +fi + +if HTTP_ADDR=127.0.0.1 wallet_rpc_is_localhost && ! HTTP_ADDR=0.0.0.0 wallet_rpc_is_localhost; then + pass "wallet_rpc_is_localhost treats 127.0.0.1 as localhost-only" +else + fail "wallet_rpc_is_localhost misclassified HTTP_ADDR" +fi + # confirm_yes is non-interactive safe if confirm_yes "should not prompt" &2