diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..973a420 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,13 @@ +name: Test + +on: + push: + pull_request: + +jobs: + smoke: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run smoke tests + run: make test diff --git a/CHANGELOG.md b/CHANGELOG.md index b18837c..36872a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,27 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). --- +## [Unreleased] + +### Changed + +- Default Arc testnet version updated to **v0.7.1**. +- Consensus service now binds its P2P listener explicitly on TCP `31001`, matching the documented firewall rule. +- Consensus key handling now uses `priv_validator_key.json`, the key file generated by `arc-node-consensus init`. +- `status` now reports execution peer count, matching the README. +- Management commands now sanitize service state and tolerate missing local RPC cleanly before the node is installed. +- Follow-mode endpoints now use the v0.7.1 CLI format and let Arc derive matching WebSocket URLs. +- Systemd service flags now include upstream Arc `--full` pruning presets and consensus execution-persistence backpressure. +- Snapshot guidance now matches the upstream Arc node repo: fresh nodes need snapshots and should not be described as syncing from genesis. +- Keypair-only VPS sudo handling now fails with root/cloud-init instructions when sudo cannot already elevate, instead of attempting an impossible self-escalation. + +### Added + +- Lightweight local smoke tests for script syntax, help output, key path, ports, and README code fences. +- GitHub Actions workflow that runs the same smoke test target on pushes and pull requests. + +--- + ## [1.0.0] — 2026-04-15 Initial public release. @@ -21,7 +42,7 @@ Initial public release. - **Live monitor dashboard** — `monitor` command refreshes every 5 s with service status, sync lag, peer count, resource usage, and recent log lines - **Auto-detect update** — `update` queries the GitHub Releases API (falls back to tags) to find the latest `circlefin/arc-node` version; supports pinning with `update vX.Y.Z` - **Atomic update with rollback** — backs up current binaries before recompiling; automatically restores them and restarts services if any build step fails -- **Guided uninstall** — removes services, binaries, and optionally chain data (~120 GB) and source; each destructive step requires separate confirmation +- **Guided uninstall** — removes services, binaries, and optionally chain data (~139+ GB with current snapshots) and source; each destructive step requires separate confirmation - **Firewall configuration** — `--with-firewall` auto-configures `ufw` with SSH, EL P2P (30303 TCP+UDP), CL P2P (31001 TCP), and optionally JSON-RPC (8545 TCP) - **MetaMask / LAN RPC** — `--expose-rpc` binds JSON-RPC on `0.0.0.0:8545` and prints the server's public IP at setup completion - **Colour-safe output** — ANSI colour codes are stripped automatically when stdout is not a TTY (piped / CI logs) @@ -31,4 +52,4 @@ Initial public release. - Ubuntu 22.04 LTS and later - Debian 12 and later -- Arc Testnet **v0.6.0** (`circlefin/arc-node`) +- Arc Testnet **v0.7.1** (`circlefin/arc-node`) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3308761 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +SHELL := /usr/bin/env bash + +.PHONY: test check + +test: + bash -n setup.sh + bash tests/smoke.sh + +check: test diff --git a/README.md b/README.md index 08e1e5d..0b75160 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ > Arc is Circle's stablecoin-native Layer-1 blockchain — built for USDC and on-chain finance. [![Shell](https://img.shields.io/badge/shell-bash-89e051?logo=gnu-bash&logoColor=white)](https://www.gnu.org/software/bash/) -[![Testnet](https://img.shields.io/badge/Arc%20Testnet-v0.6.0-blue)](https://github.com/circlefin/arc-node) +[![Testnet](https://img.shields.io/badge/Arc%20Testnet-v0.7.1-blue)](https://github.com/circlefin/arc-node) [![Platform](https://img.shields.io/badge/platform-Ubuntu%2022.04%2B%20%7C%20Debian%2012%2B-orange)](https://ubuntu.com/) [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) @@ -58,7 +58,7 @@ |---|---| | Installs system packages, Rust, and Foundry | `apt-get` + `rustup` + `foundryup` | | Builds 3 Arc binaries from source | `cargo install` from `circlefin/arc-node` | -| Downloads blockchain snapshots (~60 GB) | `arc-snapshots download` | +| Downloads blockchain snapshots (~84 GB compressed) | `arc-snapshots download` | | Initialises your node's P2P identity key | `arc-node-consensus init` | | Registers auto-start + crash-restart services | `systemd` unit files | | Provides live monitoring, logging, and updates | Built-in subcommands | @@ -74,12 +74,12 @@ Setup takes **20–60 minutes** on a fast machine (dominated by Rust compilation | **OS** | Ubuntu 22.04 or Debian 12 | Ubuntu 22.04 LTS | | **CPU** | 8 cores | 16+ cores | | **RAM** | 64 GB | 128 GB | -| **Disk** | 150 GB free SSD | 500 GB+ NVMe SSD | +| **Disk** | 200 GB free SSD | 1 TB+ NVMe SSD | | **Network** | Stable broadband | 1 Gbps unmetered | | **User** | Non-root with `sudo` | — | | **Init** | systemd | — | -> **RAM note:** The script builds all three crates in parallel, limited to `nproc / 2` jobs to avoid OOM during the Reth link phase. If you have exactly 64 GB of RAM, consider passing `--swap 32G` to add headroom. +> **RAM note:** Each Rust build is limited to `nproc / 2` jobs to avoid OOM during the Reth link phase. If you have exactly 64 GB of RAM, consider passing `--swap 32G` to add headroom. --- @@ -87,12 +87,16 @@ Setup takes **20–60 minutes** on a fast machine (dominated by Rust compilation ```bash # One command to setup -curl -fsSL https://raw.githubusercontent.com/candyburst/arc-node-setup/main/setup.sh | bash -s -- --yes +curl -fsSL https://raw.githubusercontent.com/candyburst/arc-node-setup/main/setup.sh | bash -s -- setup --yes +``` Or equivalently: -curl -fsSL https://raw.githubusercontent.com/candyburst/arc-node-setup/main/setup.sh | bash -s -- -y +```bash +curl -fsSL https://raw.githubusercontent.com/candyburst/arc-node-setup/main/setup.sh | bash -s -- setup -y +``` +```bash # Download curl -O https://raw.githubusercontent.com/candyburst/arc-node-setup/main/setup.sh chmod +x setup.sh @@ -121,12 +125,12 @@ chmod +x setup.sh | `logs cl` | Tail consensus-layer logs | | `logs both` | Tail both layers simultaneously | | `update` | Auto-detect latest Arc version and rebuild | -| `update v0.7.0` | Upgrade to a specific version | +| `update v0.7.1` | Upgrade to a specific version | | `restart` | Restart both services (safe tear-down order) | | `stop` | Stop both services | | `start` | Start both services | | `uninstall` | Guided removal of services, binaries, and data | -| `rollback-sudo` | Remove the passwordless sudo drop-in written during setup | +| `rollback-sudo` | Remove a passwordless sudo drop-in for this user | | `help` | Show usage help | --- @@ -138,11 +142,11 @@ All options apply to the `setup` command: | Flag | Description | |---|---| | `-y`, `--yes` | Skip all yes/no prompts (non-interactive / CI mode) | -| `--skip-snap` | Skip snapshot download — syncs from genesis (very slow, not recommended) | +| `--skip-snap` | Skip snapshot download — only for existing compatible data | | `--expose-rpc` | Bind JSON-RPC on `0.0.0.0` — needed for MetaMask over LAN/WAN | | `--with-firewall` | Auto-configure `ufw` firewall rules | | `--swap SIZE` | Create a swap file, e.g. `--swap 16G` | -| `--version VER` | Install a specific Arc version, e.g. `--version v0.7.0` | +| `--version VER` | Install a specific Arc version, e.g. `--version v0.7.1` | | `-h`, `--help` | Show help | **Examples:** @@ -152,7 +156,7 @@ All options apply to the `setup` command: ./setup.sh setup --yes # Fully unattended / CI ./setup.sh setup --expose-rpc --with-firewall # Open to LAN + configure firewall ./setup.sh setup --swap 32G --yes # Add 32 GB swap, no prompts -./setup.sh setup --version v0.7.0 # Install a specific version +./setup.sh setup --version v0.7.1 # Install a specific version ``` --- @@ -164,7 +168,7 @@ All options apply to the `setup` command: Displays a banner, then checks: - **RAM** ≥ 64 GB (warns, does not block) -- **Disk** ≥ 150 GB free (warns, does not block) +- **Disk** ≥ 200 GB free (warns, does not block) - **OS** is Ubuntu 22.04+ or Debian 12+ - **User** is not root (root execution is blocked) - **systemd** is present @@ -208,16 +212,16 @@ Creates: /run/arc/ ← IPC socket directory ``` -Then offers to download the **testnet snapshot** (~60 GB download, ~120 GB on disk). This lets your node start near the chain tip in 1–2 hours rather than syncing from genesis (which would take many days). +Then offers to download the **testnet snapshots**. The upstream Arc repo currently lists the latest snapshots as roughly **68 GB EL + 16 GB CL compressed**, expanding to roughly **103 GB EL + 36 GB CL**. The script requires 200 GB free before starting so the compressed archives and extracted data both have room. -The free disk space is checked before downloading. Snapshot download is given a 4-hour timeout. +The Arc node docs say a fresh node needs a snapshot to bootstrap; syncing a new node from genesis is currently not supported. Snapshot download is given a 4-hour timeout. ### Phase 5 — Initialise Consensus Layer Runs `arc-node-consensus init` to generate your node's **P2P identity key** at: ``` -~/.arc/consensus/config/node_key.json +~/.arc/consensus/config/priv_validator_key.json ``` This is a one-time operation. The key is automatically backed up to `~/.arc-key-backup/` with a timestamp. **Keep this backup — losing it means a new P2P identity.** @@ -228,8 +232,8 @@ Writes two systemd unit files: | Service | Binary | Key Behaviour | |---|---|---| -| `arc-execution` | `arc-node-execution` | Runs Reth EL; exposes JSON-RPC on `localhost:8545` (or `0.0.0.0:8545` with `--expose-rpc`) | -| `arc-consensus` | `arc-node-consensus` | Runs the BFT consensus layer; connects to EL via IPC socket | +| `arc-execution` | `arc-node-execution` | Runs Reth EL with Arc's `--full` pruning preset; exposes JSON-RPC on `localhost:8545` (or `0.0.0.0:8545` with `--expose-rpc`) | +| `arc-consensus` | `arc-node-consensus` | Runs the BFT consensus layer in follow mode with `--full` and execution-persistence backpressure; connects to EL via IPC socket | Both services: - Start automatically on boot (`WantedBy=multi-user.target`) @@ -237,6 +241,8 @@ Both services: - Log to `journald` - Have `LimitNOFILE=1048576` +The service flags follow the upstream Arc node binary guide: both layers start with `--full`, and the consensus layer adds `--execution-persistence-backpressure --execution-persistence-backpressure-threshold=50`. + The script waits up to 120 seconds for the execution-layer IPC socket (`/run/arc/reth.ipc`) to appear before starting the consensus layer. If `--with-firewall` was passed, `ufw` is configured automatically (see [Firewall](#firewall-ufw)). @@ -305,7 +311,7 @@ Uses `journalctl -f` under the hood. Press `Ctrl+C` to stop. ```bash ./setup.sh update # Auto-detect latest version from GitHub -./setup.sh update v0.7.0 # Upgrade to a specific version +./setup.sh update v0.7.1 # Upgrade to a specific version ``` The update process: @@ -335,7 +341,7 @@ The ordering (consensus before execution on stop, execution before consensus on Guided removal. You are asked separately (with a danger prompt) about: 1. Services and binaries — removed automatically after confirmation -2. Chain data at `~/.arc` (~120+ GB) — separate `yes`-to-confirm prompt +2. Chain data at `~/.arc` (~139+ GB with current snapshots) — separate `yes`-to-confirm prompt 3. Source code at `~/arc-node-src` — optional 4. Passwordless sudo drop-in (if present) @@ -347,7 +353,7 @@ The key backup at `~/.arc-key-backup/` is intentionally kept. ./setup.sh rollback-sudo ``` -Removes `/etc/sudoers.d/-nopasswd` if it was written during setup on a keypair-only VPS. Safe to run even if the file does not exist. +Removes `/etc/sudoers.d/-nopasswd` if you created it for a keypair-only VPS. Safe to run even if the file does not exist. --- @@ -400,13 +406,13 @@ Configures `ufw` with these rules: ./setup.sh setup --skip-snap ``` -Skips the snapshot download entirely. The node will sync from genesis block 0, which can take **many days**. Not recommended unless you have a specific reason. +Skips only the script-managed snapshot download. Use this only if compatible execution and consensus data already exists, or if you will bootstrap it manually before expecting the services to sync. The current Arc node docs say a fresh node cannot bootstrap from genesis. ### Pin a Specific Arc Version ```bash -./setup.sh setup --version v0.6.0 -./setup.sh setup --version v0.7.0 --yes +./setup.sh setup --version v0.7.1 +./setup.sh setup --version v0.7.1 --yes ``` Version strings must match `v..` exactly. The tag must exist in `circlefin/arc-node`. @@ -423,9 +429,9 @@ Skips all yes/no prompts. Danger prompts (uninstall, irreversible data deletion) ## Keypair VPS — Passwordless Sudo -Many cloud VPS providers (AWS, GCP, Azure, DigitalOcean) use SSH key authentication with no password set on the account. In this case `sudo` cannot be used non-interactively, which would block automated setup steps. +Many cloud VPS providers (AWS, GCP, Azure, DigitalOcean) use SSH key authentication with no password set on the account. If the image already grants passwordless sudo, setup proceeds normally. If sudo prompts for a password but the account has no password, setup cannot elevate itself. -The script detects this by inspecting the shadow password field. If the account has no password (`!`, `!!`, or `*`), it offers to write: +The script detects this by inspecting the shadow password field. If the account has no password (`!`, `!!`, or `*`) and sudo is not already usable, it prints the sudoers drop-in to create from your provider console, cloud-init, or another root/admin session: ``` /etc/sudoers.d/-nopasswd @@ -438,7 +444,7 @@ Defaults: !use_pty Defaults: !authenticate ``` -This is validated with `visudo -c` before installation. **After setup is complete, remove it:** +Validate the temporary file with `visudo -c` before installing it. **After setup is complete, remove it:** ```bash ./setup.sh rollback-sudo @@ -467,12 +473,12 @@ After a successful install: ├── arc-setup.log ← Full setup log (all output) ├── arc-node-src/ ← Cloned + compiled source (circlefin/arc-node) ├── .arc/ -│ ├── execution/ ← Reth execution-layer data (~120 GB with snapshots) -│ └── consensus/ +│ ├── execution/ ← Reth execution-layer data (~103 GB with current snapshots) +│ └── consensus/ ← Consensus-layer data (~36 GB with current snapshots) │ └── config/ -│ └── node_key.json ← Your P2P identity key +│ └── priv_validator_key.json ← Your P2P identity key └── .arc-key-backup/ - └── node_key_.json ← Timestamped key backup (keep safe!) + └── priv_validator_key_.json ← Timestamped key backup (keep safe!) /usr/local/bin/ ├── arc-node-execution ← EL binary @@ -540,7 +546,7 @@ cat ~/arc-setup.log | Resource | URL | |---|---| -| Arc Docs | https://docs.arc.network | +| Arc Docs | https://docs.arc.io | | Block Explorer | https://testnet.arcscan.app | | Testnet Faucet | https://faucet.circle.com | | Arc Discord | https://discord.com/invite/buildonarc | @@ -567,7 +573,7 @@ Any chain, any token — every bit is appreciated! ☕ --- -> Arc Network is on public testnet. The network may experience instability, resets, or breaking changes. Always back up your `node_key.json`. +> Arc Network is on public testnet. The network may experience instability, resets, or breaking changes. Always back up your `priv_validator_key.json`. --- diff --git a/setup.sh b/setup.sh index 72960e5..be544a9 100644 --- a/setup.sh +++ b/setup.sh @@ -2,7 +2,7 @@ # ============================================================ # Arc Node Setup & Management Script # Supports : Ubuntu 22.04+ · Debian 12+ -# Arc Testnet v0.6.0 +# Arc Testnet v0.7.1 # https://github.com/candyburst/arc-node-setup # ============================================================ # @@ -14,21 +14,21 @@ # monitor Live dashboard — refreshes every 5s (Ctrl+C to exit) # status Quick one-shot status snapshot # logs Tail live logs: logs el | logs cl | logs both -# update Upgrade to a new version: update v0.7.0 +# update Upgrade to a new version: update v0.7.1 # restart Restart both services # stop Stop both services # start Start both services # uninstall Guided removal of services, binaries, and data -# rollback-sudo Remove the passwordless sudo drop-in written during setup +# rollback-sudo Remove a passwordless sudo drop-in for this user # help Show this help # # SETUP OPTIONS: # -y, --yes Skip all yes/no prompts (non-interactive / CI) -# --skip-snap Skip snapshot download (sync from genesis — very slow) +# --skip-snap Skip snapshot download (requires existing data) # --expose-rpc Bind JSON-RPC on 0.0.0.0 (needed for MetaMask over LAN) # --with-firewall Auto-configure ufw firewall rules # --swap SIZE Create swap file, e.g. --swap 16G -# --version VER Arc version to install (default: v0.6.0) +# --version VER Arc version to install (default: v0.7.1) # -h, --help Show this message # # EXAMPLES: @@ -39,7 +39,7 @@ # ./setup.sh monitor Open live monitoring dashboard # ./setup.sh logs el Tail execution-layer logs # ./setup.sh update Auto-detect latest version -# ./setup.sh update v0.7.0 Upgrade to specific version +# ./setup.sh update v0.7.1 Upgrade to specific version # ./setup.sh uninstall Guided removal # ============================================================ @@ -52,11 +52,12 @@ set -euo pipefail GITHUB_USER="candyburst" GITHUB_REPO="arc-node-setup" -ARC_VERSION_DEFAULT="v0.6.0" +ARC_VERSION_DEFAULT="v0.7.1" ARC_REPO="https://github.com/circlefin/arc-node.git" ARC_DATA_DIR="${HOME}/.arc" ARC_EXECUTION_DIR="${ARC_DATA_DIR}/execution" ARC_CONSENSUS_DIR="${ARC_DATA_DIR}/consensus" +CONSENSUS_KEY_BASENAME="priv_validator_key.json" IPC_DIR="/run/arc" BUILD_DIR="${HOME}/arc-node-src" LOG_FILE="${HOME}/arc-setup.log" @@ -64,8 +65,14 @@ STATE_FILE="${HOME}/.arc-setup-state" BACKUP_DIR="${HOME}/.arc-key-backup" MIN_RAM_GB=64 -MIN_DISK_GB=150 +MIN_DISK_GB=200 MONITOR_INTERVAL=5 +EL_RPC_PORT=8545 +EL_P2P_PORT=30303 +EL_METRICS_PORT=9001 +CL_RPC_PORT=31000 +CL_P2P_PORT=31001 +CL_METRICS_PORT=29000 NET_RPC_ENDPOINTS=( "https://rpc.quicknode.testnet.arc.network/" @@ -162,17 +169,17 @@ ${BOLD}COMMANDS${NC} ${CYAN}status${NC} Quick one-shot status snapshot ${CYAN}logs${NC} Tail live logs: logs el | logs cl | logs both ${CYAN}update${NC} Rebuild node — auto-detects latest version from GitHub - or pass a specific tag: update v0.7.0 + or pass a specific tag: update v0.7.1 ${CYAN}restart${NC} Restart both services ${CYAN}stop${NC} Stop both services ${CYAN}start${NC} Start both services ${CYAN}uninstall${NC} Guided removal of node - ${CYAN}rollback-sudo${NC} Remove the passwordless sudo drop-in written during setup + ${CYAN}rollback-sudo${NC} Remove a passwordless sudo drop-in for this user ${CYAN}help${NC} Show this message ${BOLD}SETUP OPTIONS${NC} ${YELLOW}-y, --yes${NC} Skip all yes/no prompts (non-interactive) - ${YELLOW}--skip-snap${NC} Skip snapshot download (sync from genesis — very slow) + ${YELLOW}--skip-snap${NC} Skip snapshot download (requires existing data) ${YELLOW}--expose-rpc${NC} Bind JSON-RPC on 0.0.0.0 (needed for MetaMask over LAN/WAN) ${YELLOW}--with-firewall${NC} Auto-configure ufw firewall rules ${YELLOW}--swap SIZE${NC} Create swap file, e.g. --swap 16G (use if RAM < ${MIN_RAM_GB} GB) @@ -187,12 +194,12 @@ ${BOLD}EXAMPLES${NC} ./setup.sh monitor Live monitoring dashboard ./setup.sh logs el Tail execution-layer logs ./setup.sh update Auto-detect latest version from GitHub releases API - ./setup.sh update v0.7.0 Upgrade to a specific version + ./setup.sh update v0.7.1 Upgrade to a specific version ./setup.sh uninstall Guided removal ./setup.sh rollback-sudo Remove the passwordless sudo drop-in ${BOLD}RESOURCES${NC} - Docs https://docs.arc.network + Docs https://docs.arc.io Explorer https://testnet.arcscan.app Faucet https://faucet.circle.com Discord https://discord.com/invite/buildonarc @@ -230,9 +237,9 @@ confirm_danger() { } # ════════════════════════════════════════════════════════════ -# SELF-HEALING SUDO BOOTSTRAP -# Detects keypair-based VPS (no password set) and auto- -# configures passwordless sudo so the script never prompts. +# SUDO BOOTSTRAP +# Validates that sudo works before privileged setup steps run. Keypair-only +# VPS accounts with no password need passwordless sudo configured ahead of time. # ════════════════════════════════════════════════════════════ _bootstrap_sudo() { # Already works non-interactively — nothing to do. @@ -240,13 +247,6 @@ _bootstrap_sudo() { local drop_in="/etc/sudoers.d/${USER}-nopasswd" - # Idempotency: drop-in already written from a previous partial run. - # /etc/sudoers.d/ is root-owned so we can't stat it without sudo. - # The || true is required — with set -euo pipefail a failed sudo -n would - # otherwise abort the script here before we've even tried to write the file. - local already_written=false - sudo -n test -f "$drop_in" 2>/dev/null && already_written=true || true - # Read the shadow password field without relying on sudo -n (which we know # has already failed at this point). Try getent shadow directly first (works # when the user belongs to the shadow group, e.g. on some Ubuntu images), @@ -270,8 +270,12 @@ _bootstrap_sudo() { echo "" echo -e " ${BOLD}${YELLOW}SUDO CONFIGURATION REQUIRED${NC}" echo -e " ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo -e " Setup needs passwordless sudo for the duration of the install." - echo -e " It will write the following drop-in file:" + echo -e " Setup needs sudo for package installs, systemd units, and /usr/local/bin." + echo -e " Because sudo currently requires authentication and this account has no" + echo -e " password, setup.sh cannot create the sudoers drop-in by itself." + echo "" + echo -e " Create this file as root from your provider console, cloud-init, or an" + echo -e " existing admin session, then re-run setup.sh:" echo "" echo -e " ${CYAN}/etc/sudoers.d/${USER}-nopasswd${NC}" echo "" @@ -284,47 +288,10 @@ _bootstrap_sudo() { echo -e " ${YELLOW} Run ./setup.sh rollback-sudo afterwards to remove it.${NC}" echo -e " ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" - - # In non-interactive / CI mode this is auto-accepted (user passed --yes or stdin is - # not a tty). In that case we still show the block above so the decision is auditable - # in any CI log. - if [[ -t 0 ]]; then - ask "Allow setup.sh to write this drop-in and continue? [Y/n] " - read -r _sudo_reply || true - _sudo_reply="${_sudo_reply:-y}" - if [[ ! "${_sudo_reply,,}" =~ ^(y|yes)$ ]]; then - echo -e "${RED}✖${NC} Aborted — not writing sudoers drop-in." - echo -e " Configure sudo yourself, then re-run setup.sh." - exit 1 - fi - else - echo -e "${YELLOW}⚠${NC} Non-interactive session — auto-accepting sudo drop-in (stdin is not a TTY)." - fi - - if ! $already_written; then - # Write a validated sudoers drop-in. Keep ALL Defaults overrides inside - # the drop-in (never append directly to /etc/sudoers — no visudo check). - if sudo -n bash -c " - printf '%s\n' \ - '${USER} ALL=(ALL) NOPASSWD:ALL' \ - 'Defaults:${USER} !use_pty' \ - 'Defaults:${USER} !authenticate' \ - > '${drop_in}.tmp' \ - && visudo -cf '${drop_in}.tmp' \ - && mv '${drop_in}.tmp' '${drop_in}' \ - && chmod 440 '${drop_in}' - " 2>/dev/null; then - echo -e "${GREEN}✔${NC} Passwordless sudo drop-in written. Continuing setup..." - echo -e "${DIM} Remember to run: ./setup.sh rollback-sudo when setup is done.${NC}" - echo "" - else - sudo -n rm -f "${drop_in}.tmp" 2>/dev/null || true - echo -e "${RED}✖${NC} Could not auto-configure sudo." - echo -e " Run this once manually, then re-run setup.sh:" - echo -e " ${BOLD}printf '%s\\n' '${USER} ALL=(ALL) NOPASSWD:ALL' 'Defaults:${USER} !use_pty' 'Defaults:${USER} !authenticate' | sudo tee /etc/sudoers.d/${USER}-nopasswd && sudo chmod 440 /etc/sudoers.d/${USER}-nopasswd${NC}" - exit 1 - fi - fi + echo -e " Root command:" + echo -e " ${BOLD}tmp='${drop_in}.tmp'; printf '%s\\n' '${USER} ALL=(ALL) NOPASSWD:ALL' 'Defaults:${USER} !use_pty' 'Defaults:${USER} !authenticate' > \"\$tmp\" && visudo -cf \"\$tmp\" && mv \"\$tmp\" '${drop_in}' && chmod 440 '${drop_in}'${NC}" + echo "" + fatal "Passwordless sudo must be configured before setup can continue." else # Password exists or shadow unreadable — standard VPS. # Fall through: sudo -v below will prompt once for the password. @@ -344,7 +311,7 @@ require_sudo() { _validate_version() { [[ "$1" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] \ - || fatal "Invalid version '${1}' — expected format: v.. (e.g. v0.7.0)" + || fatal "Invalid version '${1}' — expected format: v.. (e.g. v0.7.1)" } _validate_swap_size() { @@ -362,7 +329,7 @@ parse_setup_flags() { --swap) [[ -z "${2:-}" ]] && fatal "--swap requires a size argument (e.g. --swap 16G)" _validate_swap_size "$2" FLAG_SWAP="${2^^}"; shift ;; # normalise to uppercase: 16g → 16G - --version) [[ -z "${2:-}" ]] && fatal "--version requires a value (e.g. --version v0.7.0)" + --version) [[ -z "${2:-}" ]] && fatal "--version requires a value (e.g. --version v0.7.1)" _validate_version "$2" ARC_VERSION="$2"; shift ;; -h|--help) usage ;; @@ -410,8 +377,8 @@ phase_welcome() { echo -e "${YELLOW}⚠ Requirements at a glance:${NC}" echo " OS : Ubuntu 22.04+ or Debian 12+" echo " RAM : 64 GB+ (Reth spikes during initial sync)" - echo " Storage : 1 TB+ NVMe SSD (150 GB free minimum)" - echo " Network : Stable 24 Mbps+ (snapshots ≈ 60 GB)" + echo " Storage : 1 TB+ NVMe SSD (${MIN_DISK_GB} GB free minimum)" + echo " Network : Stable 24 Mbps+ (snapshots ≈ 84 GB compressed)" echo " Time : 1–3 hours total (compile + snapshot download)" echo "" [[ -n "$FLAG_SWAP" ]] && info "--swap ${FLAG_SWAP}: will create a swap file." @@ -807,15 +774,15 @@ phase_setup_data() { echo "" if $FLAG_SKIP_SNAP; then - warn "--skip-snap: Skipping snapshots. Node will sync from genesis (very slow)." + warn "--skip-snap: Skipping snapshot download. Arc docs do not support bootstrapping a fresh node from genesis." else - echo -e "${DIM} Snapshots let you start near the chain tip instead of syncing${NC}" - echo -e "${DIM} from block 0 (genesis), which would take many days.${NC}" + echo -e "${DIM} Arc docs require snapshots to bootstrap a fresh node.${NC}" + echo -e "${DIM} This downloads the latest EL and CL pruned snapshots.${NC}" echo "" - warn "~60 GB download → ~120 GB on disk. Typically 1–2 hours." + warn "~84 GB compressed → ~139 GB extracted. Download time depends on connection speed." echo "" if confirm "Download blockchain snapshots? (strongly recommended)"; then - local required_gb=130 + local required_gb=200 local free_gb; free_gb=$(df -BG "$ARC_DATA_DIR" | awk 'NR==2{gsub("G",""); print $4}') if [[ -z "$free_gb" ]]; then warn "Could not parse free disk space for ${ARC_DATA_DIR} — assuming 0 GB." @@ -831,7 +798,7 @@ phase_setup_data() { || fatal "Snapshot download failed or timed out after 4 hours. Check ${LOG_FILE}" success "Snapshots downloaded and extracted!" else - warn "Skipped — node will sync from genesis (very slow)." + warn "Skipped — setup will continue, but a fresh node needs compatible snapshot data before it can bootstrap." fi fi @@ -849,7 +816,8 @@ phase_init_consensus() { echo -e "${DIM} Generates a P2P identity key for your node (one-time operation).${NC}" echo "" - if [[ -f "${ARC_CONSENSUS_DIR}/config/node_key.json" ]]; then + local key_file="${ARC_CONSENSUS_DIR}/config/${CONSENSUS_KEY_BASENAME}" + if [[ -f "$key_file" ]]; then info "Identity key already exists — skipping init." else arc-node-consensus init --home "$ARC_CONSENSUS_DIR" 2>>"$LOG_FILE" \ @@ -862,7 +830,7 @@ phase_init_consensus() { } _backup_consensus_key() { - local key_file="${ARC_CONSENSUS_DIR}/config/node_key.json" + local key_file="${ARC_CONSENSUS_DIR}/config/${CONSENSUS_KEY_BASENAME}" [[ -f "$key_file" ]] || return 0 mkdir -p "$BACKUP_DIR"; chmod 700 "$BACKUP_DIR" @@ -874,10 +842,10 @@ _backup_consensus_key() { info "Matching key backup already exists (${existing}) — skipping." return 0 fi - done < <(find "$BACKUP_DIR" -maxdepth 1 -name 'node_key_*.json' -type f 2>/dev/null || true) + done < <(find "$BACKUP_DIR" -maxdepth 1 -name 'priv_validator_key_*.json' -type f 2>/dev/null || true) local dest - dest="${BACKUP_DIR}/node_key_$(date +%Y%m%d_%H%M%S).json" + dest="${BACKUP_DIR}/priv_validator_key_$(date +%Y%m%d_%H%M%S).json" cp "$key_file" "$dest" || fatal "Failed to back up consensus key to ${dest}. Check disk space." chmod 600 "$dest" success "Consensus key backed up → ${dest}" @@ -903,7 +871,7 @@ phase_install_services() { local rpc_addr="127.0.0.1" if $FLAG_EXPOSE_RPC; then rpc_addr="0.0.0.0" - warn "RPC exposed on 0.0.0.0:8545 — firewall recommended!" + warn "RPC exposed on 0.0.0.0:${EL_RPC_PORT} — firewall recommended!" fi # Pick the first reachable public RPC endpoint; fall back to the first entry. @@ -931,7 +899,7 @@ phase_install_services() { sudo tee /etc/systemd/system/arc-execution.service > /dev/null < /dev/null </dev/null || echo "unknown") - cl_status=$(sudo systemctl is-active arc-consensus 2>/dev/null || echo "unknown") + el_status=$(_service_state "arc-execution") + cl_status=$(_service_state "arc-consensus") if [[ "$el_status" == "active" ]]; then success "arc-execution : RUNNING" else error "arc-execution : ${el_status}"; warn "sudo journalctl -u arc-execution -n 50"; services_ok=false; fi @@ -1083,18 +1057,18 @@ phase_verify() { if command -v cast &>/dev/null; then info "Waiting for RPC to become available..." local rpc_deadline; rpc_deadline=$(( $(date +%s) + 30 )) - until cast block-number --rpc-url http://localhost:8545 &>/dev/null \ + until cast block-number --rpc-url "http://localhost:${EL_RPC_PORT}" &>/dev/null \ || [[ $(date +%s) -ge $rpc_deadline ]]; do sleep 1 done local b1 b2 - b1=$(cast block-number --rpc-url http://localhost:8545 2>/dev/null | tr -dc '0-9') + b1=$(cast block-number --rpc-url "http://localhost:${EL_RPC_PORT}" 2>/dev/null | tr -dc '0-9' || true) b1=${b1:-0} info "Waiting for block to advance (up to 30s)..." local adv_deadline; adv_deadline=$(( $(date +%s) + 30 )) while true; do - b2=$(cast block-number --rpc-url http://localhost:8545 2>/dev/null | tr -dc '0-9') + b2=$(cast block-number --rpc-url "http://localhost:${EL_RPC_PORT}" 2>/dev/null | tr -dc '0-9' || true) b2=${b2:-0} [[ "$b2" -gt "$b1" ]] && break [[ $(date +%s) -ge $adv_deadline ]] && break @@ -1103,7 +1077,7 @@ phase_verify() { if [[ "$b1" -eq 0 ]] && [[ "$b2" -eq 0 ]]; then warn "Node not yet responding to RPC — may still be initialising." - warn "Check again: cast block-number --rpc-url http://localhost:8545" + warn "Check again: cast block-number --rpc-url http://localhost:${EL_RPC_PORT}" elif [[ "$b2" -gt "$b1" ]]; then success "Node is syncing! Block advanced: ${b1} → ${b2}" else @@ -1111,7 +1085,7 @@ phase_verify() { fi else warn "cast not found — add ~/.foundry/bin to PATH, then run:" - warn " cast block-number --rpc-url http://localhost:8545" + warn " cast block-number --rpc-url http://localhost:${EL_RPC_PORT}" fi } @@ -1134,10 +1108,10 @@ print_summary() { || curl -sf --max-time 5 https://api4.my-ip.io/ip 2>/dev/null \ || echo "") fi - echo " JSON-RPC : http://${rpc_host}:8545" - echo " CL RPC : http://localhost:31000" - echo " EL Metrics : http://localhost:9001/metrics" - echo " CL Metrics : http://localhost:29000/metrics" + echo " JSON-RPC : http://${rpc_host}:${EL_RPC_PORT}" + echo " CL RPC : http://localhost:${CL_RPC_PORT}" + echo " EL Metrics : http://localhost:${EL_METRICS_PORT}/metrics" + echo " CL Metrics : http://localhost:${CL_METRICS_PORT}/metrics" echo "" echo -e "${BOLD}Quick commands:${NC}" @@ -1151,7 +1125,7 @@ print_summary() { echo "" echo -e "${BOLD}Resources:${NC}" - echo " Docs : https://docs.arc.network" + echo " Docs : https://docs.arc.io" echo " Explorer : https://testnet.arcscan.app" echo " Faucet : https://faucet.circle.com" echo " Discord : https://discord.com/invite/buildonarc" @@ -1204,14 +1178,25 @@ _net_head() { # Local block height via cast; returns decimal or "N/A". _local_block() { command -v cast &>/dev/null || { echo "N/A"; return; } - local raw; raw=$(cast block-number --rpc-url http://localhost:8545 2>/dev/null | tr -dc '0-9') + local raw; raw=$(cast block-number --rpc-url "http://localhost:${EL_RPC_PORT}" 2>/dev/null | tr -dc '0-9' || true) [[ -n "$raw" ]] && echo "$raw" || echo "N/A" } # One-line service status with uptime. +_service_state() { + local svc="$1" load_state active_state + load_state=$(systemctl show "$svc" --property=LoadState --value 2>/dev/null | head -1 || true) + active_state=$(systemctl show "$svc" --property=ActiveState --value 2>/dev/null | head -1 || true) + if [[ -z "$load_state" || "$load_state" == "not-found" ]]; then + echo "not-installed" + else + echo "${active_state:-unknown}" + fi +} + _svc_line() { local svc="$1" - local active; active=$(systemctl is-active "$svc" 2>/dev/null || echo "inactive") + local active; active=$(_service_state "$svc") if [[ "$active" == "active" ]]; then local ts; ts=$(systemctl show "$svc" --property=ActiveEnterTimestamp \ | cut -d= -f2 2>/dev/null || echo "") @@ -1227,6 +1212,8 @@ _svc_line() { fi fi echo -e "${GREEN}● RUNNING${NC}${DIM}${upstr}${NC}" + elif [[ "$active" == "not-installed" ]]; then + echo -e "${DIM}not installed${NC}" else echo -e "${RED}● ${active^^}${NC}" fi @@ -1267,7 +1254,7 @@ _disk_info() { # Connected peer count via net_peerCount RPC. _peers() { command -v cast &>/dev/null || { echo "N/A"; return; } - local raw; raw=$(cast rpc net_peerCount --rpc-url http://localhost:8545 2>/dev/null \ + local raw; raw=$(cast rpc net_peerCount --rpc-url "http://localhost:${EL_RPC_PORT}" 2>/dev/null \ | sed 's/\x1b\[[0-9;]*m//g' | tr -d '"' | tr -d '[:space:]' || echo "") if [[ "$raw" =~ ^0x[0-9a-fA-F]+$ ]]; then printf '%d' "$(( 16#${raw#0x} ))" @@ -1395,7 +1382,7 @@ cmd_status() { echo "" for svc in "arc-execution" "arc-consensus"; do - local active; active=$(systemctl is-active "$svc" 2>/dev/null || echo "not-installed") + local active; active=$(_service_state "$svc") printf " %-36s" "$svc" case "$active" in active) echo -e "${GREEN}● RUNNING${NC}" ;; @@ -1406,8 +1393,10 @@ cmd_status() { echo "" if command -v cast &>/dev/null; then - local lb; lb=$(cast block-number --rpc-url http://localhost:8545 2>/dev/null | tr -dc '0-9') + local lb; lb=$(cast block-number --rpc-url "http://localhost:${EL_RPC_PORT}" 2>/dev/null | tr -dc '0-9' || true) printf " %-36s${CYAN}%s${NC}\n" "Local block height" "${lb:-N/A}" + local peers; peers=$(_peers) + printf " %-36s${CYAN}%s${NC}\n" "Execution peers" "$peers" fi if [[ -d "$ARC_DATA_DIR" ]]; then @@ -1418,8 +1407,8 @@ cmd_status() { local free_disk; free_disk=$(df -h "$HOME" | awk 'NR==2{print $4}') printf " %-36s${CYAN}%s${NC}\n" "Free disk (home partition)" "$free_disk" echo "" - echo -e " ${DIM}RPC : http://localhost:8545 CL RPC : http://localhost:31000${NC}" - echo -e " ${DIM}Metrics : http://localhost:9001/metrics http://localhost:29000/metrics${NC}" + echo -e " ${DIM}RPC : http://localhost:${EL_RPC_PORT} CL RPC : http://localhost:${CL_RPC_PORT}${NC}" + echo -e " ${DIM}Metrics : http://localhost:${EL_METRICS_PORT}/metrics http://localhost:${CL_METRICS_PORT}/metrics${NC}" echo "" } @@ -1450,14 +1439,23 @@ cmd_logs() { # COMMAND: start / stop / restart # ════════════════════════════════════════════════════════════ +_stop_arc_services() { + sudo systemctl stop arc-consensus 2>/dev/null || true + sudo systemctl stop arc-execution 2>/dev/null || true +} + cmd_service() { local action="$1" require_sudo - echo -e "${CYAN}${action^}ing Arc services...${NC}" + case "$action" in + start) echo -e "${CYAN}Starting Arc services...${NC}" ;; + stop) echo -e "${CYAN}Stopping Arc services...${NC}" ;; + restart) echo -e "${CYAN}Restarting Arc services...${NC}" ;; + esac case "$action" in stop) # Always stop consensus before execution — safe tear-down order. - sudo systemctl stop arc-consensus arc-execution 2>/dev/null || true + _stop_arc_services success "Services stopped." ;; start) sudo systemctl start arc-execution @@ -1465,7 +1463,7 @@ cmd_service() { sudo systemctl start arc-consensus success "Services started." ;; restart) - sudo systemctl stop arc-consensus arc-execution 2>/dev/null || true + _stop_arc_services sudo systemctl is-active --quiet arc-consensus 2>/dev/null \ && warn "arc-consensus still active after stop — watch for state conflicts." sudo systemctl is-active --quiet arc-execution 2>/dev/null \ @@ -1531,8 +1529,8 @@ cmd_update() { new_ver="$detected" else warn "Could not reach GitHub API. Check your network." - ask "Which Arc version to update to? (e.g. v0.7.0): " - read -r new_ver || fatal "Unexpected EOF on stdin — pass the version explicitly: ./setup.sh update v0.7.0" + ask "Which Arc version to update to? (e.g. v0.7.1): " + read -r new_ver || fatal "Unexpected EOF on stdin — pass the version explicitly: ./setup.sh update v0.7.1" fi fi @@ -1550,7 +1548,7 @@ cmd_update() { confirm "Proceed with update to ${new_ver}?" || { echo "Cancelled."; exit 0; } info "Stopping services..." - sudo systemctl stop arc-consensus arc-execution 2>/dev/null || true + _stop_arc_services # Refuse to overwrite binaries if either service is still active. sudo systemctl is-active --quiet arc-consensus 2>/dev/null \ && fatal "arc-consensus is still active — refusing to overwrite binaries. Check: sudo journalctl -u arc-consensus -n 20" @@ -1655,8 +1653,8 @@ cmd_update() { # ════════════════════════════════════════════════════════════ # SUDO ROLLBACK -# Removes the passwordless sudo drop-in written by _bootstrap_sudo -# during setup on keypair-only VPS instances. Safe to call even +# Removes the passwordless sudo drop-in documented by _bootstrap_sudo +# for keypair-only VPS instances. Safe to call even # when the file does not exist (no-op with an info message). # Called by both cmd_uninstall and cmd_rollback_sudo. # ════════════════════════════════════════════════════════════ @@ -1671,7 +1669,7 @@ _rollback_sudo_dropin() { echo "" info "Sudoers drop-in found: ${drop_in}" - echo -e " ${DIM}This file was written by setup.sh to allow passwordless sudo on a keypair VPS.${NC}" + echo -e " ${DIM}This file grants passwordless sudo for keypair-only VPS setup.${NC}" echo -e " ${DIM}Removing it restores your original sudo configuration.${NC}" echo "" @@ -1690,8 +1688,8 @@ cmd_rollback_sudo() { echo -e "${BOLD}${CYAN}Rollback Passwordless Sudo${NC}" echo "════════════════════════════════════════════════════" echo "" - echo -e " This removes the sudoers drop-in written by setup.sh if your server" - echo -e " was detected as a keypair-only VPS during the initial install:" + echo -e " This removes the sudoers drop-in used when a keypair-only VPS" + echo -e " account has no password but setup needs sudo:" echo "" echo -e " ${DIM}/etc/sudoers.d/${USER}-nopasswd${NC}" echo "" @@ -1723,7 +1721,7 @@ cmd_uninstall() { echo " • Passwordless sudo drop-in (/etc/sudoers.d/${USER}-nopasswd)" echo "" echo "Optionally (asked separately):" - echo " • Chain data at ${ARC_DATA_DIR} (~120+ GB)" + echo " • Chain data at ${ARC_DATA_DIR} (~139+ GB with current snapshots)" echo " • Source code at ${BUILD_DIR}" echo "" @@ -1732,7 +1730,7 @@ cmd_uninstall() { # Services info "Stopping and removing services..." - sudo systemctl stop arc-consensus arc-execution 2>/dev/null || true + _stop_arc_services sudo systemctl disable arc-consensus arc-execution 2>/dev/null || true sudo rm -f /etc/systemd/system/arc-execution.service \ /etc/systemd/system/arc-consensus.service diff --git a/tests/smoke.sh b/tests/smoke.sh new file mode 100755 index 0000000..742a0ea --- /dev/null +++ b/tests/smoke.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +fail() { + printf 'smoke test failed: %s\n' "$*" >&2 + exit 1 +} + +assert_file_contains() { + local file="$1" pattern="$2" + grep -Eq -- "$pattern" "$file" || fail "${file} does not contain pattern: ${pattern}" +} + +assert_output_contains() { + local output="$1" pattern="$2" + grep -Eq -- "$pattern" <<<"$output" || fail "output does not contain pattern: ${pattern}" +} + +help_output="$(bash setup.sh help)" +assert_output_contains "$help_output" 'Arc Node Setup & Manager' +assert_output_contains "$help_output" 'Testnet v0\.7\.1' +assert_output_contains "$help_output" '\./setup\.sh update v0\.7\.1' +assert_output_contains "$help_output" '--version VER[[:space:]]+Arc version to install[[:space:]]+\(default: v0\.7\.1\)' +assert_output_contains "$help_output" 'https://docs\.arc\.io' +assert_file_contains README.md 'bash -s -- setup --yes' +assert_file_contains README.md 'bash -s -- setup -y' + +assert_file_contains setup.sh '^ARC_VERSION_DEFAULT="v0\.7\.1"$' +assert_file_contains setup.sh '^CONSENSUS_KEY_BASENAME="priv_validator_key\.json"$' +assert_file_contains setup.sh '^EL_RPC_PORT=8545$' +assert_file_contains setup.sh '^EL_P2P_PORT=30303$' +assert_file_contains setup.sh '^CL_RPC_PORT=31000$' +assert_file_contains setup.sh '^CL_P2P_PORT=31001$' +assert_file_contains setup.sh '^MIN_DISK_GB=200$' +assert_file_contains setup.sh 'systemctl show "\$svc" --property=LoadState --value' +assert_file_contains setup.sh '--private-key \$\{ARC_CONSENSUS_DIR\}/config/\$\{CONSENSUS_KEY_BASENAME\}' +assert_file_contains setup.sh '--full[[:space:]]*\\\\' +assert_file_contains setup.sh '--p2p\.addr /ip4/0\.0\.0\.0/tcp/\$\{CL_P2P_PORT\}' +assert_file_contains setup.sh '--follow\.endpoint https://rpc\.drpc\.testnet\.arc\.network,wss=rpc\.drpc\.testnet\.arc\.network' +assert_file_contains setup.sh '--follow\.endpoint https://rpc\.blockdaemon\.testnet\.arc\.network,wss=rpc\.blockdaemon\.testnet\.arc\.network/websocket' +assert_file_contains setup.sh '--execution-persistence-backpressure[[:space:]]*\\\\' +assert_file_contains setup.sh '--execution-persistence-backpressure-threshold=50' +assert_file_contains setup.sh 'sudo ufw allow "\$\{EL_P2P_PORT\}/tcp"' +assert_file_contains setup.sh 'sudo ufw allow "\$\{EL_P2P_PORT\}/udp"' +assert_file_contains setup.sh 'sudo ufw allow "\$\{CL_P2P_PORT\}/tcp"' +assert_file_contains setup.sh 'printf " %-36s\$\{CYAN\}%s\$\{NC\}\\n" "Execution peers" "\$peers"' +assert_file_contains README.md 'fresh node cannot bootstrap from genesis' + +if grep -Eq 'node_key\.json|node_key_' setup.sh README.md; then + fail 'stale node_key.json documentation or script reference found' +fi + +if grep -Eq -- '--follow\.endpoint [^\\]*,wss?://' setup.sh; then + fail 'follow endpoints use the old comma-plus-WebSocket-URL format' +fi + +if grep -Eq 'sudo -n bash -c|auto-accepting sudo drop-in|Allow setup\.sh to write|written during setup' setup.sh README.md; then + fail 'sudo bootstrap still claims it can self-write a sudoers drop-in' +fi + +if grep -Eq 'will sync from genesis|syncs from genesis|sync from genesis \(very slow\)' setup.sh README.md; then + fail 'snapshot skip guidance still claims a fresh genesis sync is supported' +fi + +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT +mkdir -p "$tmp_dir/bin" "$tmp_dir/home" + +cat > "$tmp_dir/bin/systemctl" <<'STUB' +#!/usr/bin/env bash +if [[ "${1:-}" == "show" ]]; then + svc="${2:-}" + prop="${3#--property=}" + case "${svc}:${prop}" in + arc-execution:LoadState) echo "loaded" ;; + arc-execution:ActiveState) echo "active" ;; + arc-consensus:LoadState) echo "loaded" ;; + arc-consensus:ActiveState) echo "inactive" ;; + *:LoadState) echo "not-found" ;; + *:ActiveState) echo "inactive" ;; + esac + exit 0 +fi +exit 1 +STUB + +cat > "$tmp_dir/bin/cast" <<'STUB' +#!/usr/bin/env bash +if [[ "${1:-}" == "block-number" ]]; then + echo "12345" +elif [[ "${1:-}" == "rpc" && "${2:-}" == "net_peerCount" ]]; then + echo '"0x2a"' +else + exit 1 +fi +STUB + +chmod +x "$tmp_dir/bin/systemctl" "$tmp_dir/bin/cast" +status_output="$(PATH="$tmp_dir/bin:$PATH" HOME="$tmp_dir/home" bash setup.sh status)" +assert_output_contains "$status_output" 'arc-execution[[:space:]]+.*RUNNING' +assert_output_contains "$status_output" 'arc-consensus[[:space:]]+.*INACTIVE' +assert_output_contains "$status_output" 'Local block height[[:space:]]+12345' +assert_output_contains "$status_output" 'Execution peers[[:space:]]+42' + +readme_fences="$(grep -c '^```' README.md)" +if (( readme_fences % 2 != 0 )); then + fail "README has an odd number of fenced code markers (${readme_fences})" +fi + +if awk ' + /^```bash$/ { in_bash=1; next } + /^```$/ { in_bash=0; next } + in_bash && /^Or equivalently:/ { found=1 } + END { exit found ? 0 : 1 } +' README.md; then + fail 'README Quick Start prose is inside a bash code fence' +fi + +printf 'smoke tests passed\n'