From 39d986586a92e2b9ee50f359cb4bac6f6251c2f7 Mon Sep 17 00:00:00 2001 From: epicexcelsior Date: Sun, 31 May 2026 03:01:54 -0800 Subject: [PATCH 001/107] test(cli): repair reproducible TCP bridge harness --- .gitignore | 8 +++++--- config/tcp_bridge/exit/config | 13 +++++++++++++ config/tcp_bridge/relay/config | 13 +++++++++++++ package.json | 2 +- requirements-dev.txt | 4 ++++ scripts/test.sh | 13 +++++++++++++ tests/test_tcp_bridge.py | 15 ++++++++------- 7 files changed, 57 insertions(+), 11 deletions(-) create mode 100644 config/tcp_bridge/exit/config create mode 100644 config/tcp_bridge/relay/config create mode 100644 requirements-dev.txt create mode 100755 scripts/test.sh diff --git a/.gitignore b/.gitignore index e12306c..5f4a697 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ venv/ .venv/ +.venv-test/ config/storage/ __pycache__/ node_modules/ @@ -10,6 +11,7 @@ run_client.sh wallet.json wallet_*.json nonce_*.json -config/*/storage/ -config/*/interfaces/ -*.identity \ No newline at end of file +config/**/storage/ +config/**/interfaces/ +config/**/anonmesh_exit_identity +*.identity diff --git a/config/tcp_bridge/exit/config b/config/tcp_bridge/exit/config new file mode 100644 index 0000000..df2d6e5 --- /dev/null +++ b/config/tcp_bridge/exit/config @@ -0,0 +1,13 @@ +[reticulum] + enable_transport = True + share_instance = Yes + shared_instance_port = 37432 + instance_control_port = 37433 + +[interfaces] + + [[TCP Bridge Client]] + type = TCPClientInterface + enabled = yes + target_host = 127.0.0.1 + target_port = 4243 diff --git a/config/tcp_bridge/relay/config b/config/tcp_bridge/relay/config new file mode 100644 index 0000000..d39c602 --- /dev/null +++ b/config/tcp_bridge/relay/config @@ -0,0 +1,13 @@ +[reticulum] + enable_transport = True + share_instance = Yes + shared_instance_port = 37430 + instance_control_port = 37431 + +[interfaces] + + [[TCP Bridge Server]] + type = TCPServerInterface + enabled = yes + listen_ip = 127.0.0.1 + listen_port = 4243 diff --git a/package.json b/package.json index 1984da3..c7b555a 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "type": "commonjs", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "./scripts/test.sh" }, "dependencies": { "@arcium-hq/client": "^0.9.2", diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..ddf59a2 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +-r requirements.txt + +pytest>=8.0,<9 +solders>=0.20 diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..1fea584 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +VENV_DIR="${ANONMESH_TEST_VENV:-$PROJECT_ROOT/.venv-test}" + +if [[ ! -x "$VENV_DIR/bin/python" ]]; then + python3 -m venv "$VENV_DIR" +fi + +"$VENV_DIR/bin/python" -m pip install --quiet -r "$PROJECT_ROOT/requirements-dev.txt" +exec "$VENV_DIR/bin/python" -m pytest "$@" diff --git a/tests/test_tcp_bridge.py b/tests/test_tcp_bridge.py index 88e3b17..d1e1efc 100644 --- a/tests/test_tcp_bridge.py +++ b/tests/test_tcp_bridge.py @@ -20,14 +20,15 @@ import signal import subprocess -RELAY_CONFIG = os.path.join(os.path.dirname(__file__), "..", "..", "config", "relay") -EXIT_CONFIG = os.path.join(os.path.dirname(__file__), "..", "..", "config", "exit") -EXIT_NODE_SCRIPT = os.path.join(os.path.dirname(__file__), "exit_node.py") PROJECT_ROOT = os.path.join(os.path.dirname(__file__), "..") +RELAY_CONFIG = os.path.join(PROJECT_ROOT, "config", "tcp_bridge", "relay") +EXIT_CONFIG = os.path.join(PROJECT_ROOT, "config", "tcp_bridge", "exit") +EXIT_NODE_SCRIPT = os.path.join(PROJECT_ROOT, "scripts", "exit_node.py") +RNSD_EXECUTABLE = os.path.join(os.path.dirname(sys.executable), "rnsd") # Validate paths for path, label in [(RELAY_CONFIG, "relay config"), (EXIT_CONFIG, "exit config"), - (EXIT_NODE_SCRIPT, "exit_node.py")]: + (EXIT_NODE_SCRIPT, "exit_node.py"), (RNSD_EXECUTABLE, "rnsd")]: resolved = os.path.realpath(path) if not os.path.exists(resolved): print(f"FATAL: {label} not found at {resolved}") @@ -84,7 +85,7 @@ def sighandler(sig, frame): # ── Step 1: Start relay rnsd ────────────────────────────────────────────── log_info(f"Starting relay rnsd (config: {RELAY_CONFIG})") relay_proc = subprocess.Popen( - ["rnsd", "--config", RELAY_CONFIG], + [RNSD_EXECUTABLE, "--config", RELAY_CONFIG], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) procs.append(relay_proc) @@ -102,7 +103,7 @@ def sighandler(sig, frame): # ── Step 2: Start exit rnsd ─────────────────────────────────────────────── log_info(f"Starting exit rnsd (config: {EXIT_CONFIG})") exit_rnsd = subprocess.Popen( - ["rnsd", "--config", EXIT_CONFIG], + [RNSD_EXECUTABLE, "--config", EXIT_CONFIG], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) procs.append(exit_rnsd) @@ -265,7 +266,7 @@ def received_announce(self, destination_hash, announced_identity, app_data): prefix = "CLIENT" if text.startswith("CLIENT:") else " " if "SUCCESS" in text: log_ok(text.replace("CLIENT: ", "")) - elif "FAIL" in text: + elif text.startswith("CLIENT: FAIL"): log_err(text.replace("CLIENT: ", "")) else: log_info(text.replace("CLIENT: ", "")) From effe727f7d0d66da9048f4a3ecc4082258a119d4 Mon Sep 17 00:00:00 2001 From: epicexcelsior Date: Sun, 31 May 2026 03:02:05 -0800 Subject: [PATCH 002/107] fix(wallet): validate execute-payment addresses --- tests/test_wallet.py | 59 ++++++++++++++++++++++++++++++++++---------- wallet.py | 20 +++++++++------ 2 files changed, 58 insertions(+), 21 deletions(-) diff --git a/tests/test_wallet.py b/tests/test_wallet.py index f305613..fa31a29 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -6,6 +6,7 @@ import json import base64 from pathlib import Path +from unittest.mock import patch, MagicMock import pytest import state @@ -19,6 +20,7 @@ # A known valid base58 Hash string (all 1s — the default/zero hash) ZERO_HASH = "11111111111111111111111111111111" +MXE_PUBKEY_HEX = "00" * 32 def _write_keypair(path: Path) -> Keypair: @@ -27,6 +29,22 @@ def _write_keypair(path: Path) -> Keypair: return kp +def _arcium_accounts() -> dict[str, str]: + return { + name: str(Keypair().pubkey()) + for name in ( + "mxeAccount", + "compDefAccount", + "mempoolAccount", + "executingPool", + "computationAccount", + "clusterAccount", + "poolAccount", + "clockAccount", + ) + } + + # ── generate_wallet ─────────────────────────────────────────────────────────── def test_generate_wallet_creates_file(tmp_path): @@ -282,11 +300,21 @@ def test_offline_sign_nonce_transfer_zero_lamports_still_signs(tmp_path): # ── partial_sign_execute_payment ────────────────────────────────────────────── -def test_partial_sign_execute_payment_returns_base64(tmp_path): +@patch("wallet._account_exists", return_value=True) +@patch("arcium_client._run_shim") +@patch("arcium_client.rescue_encrypt") +def test_partial_sign_execute_payment_returns_base64(mock_encrypt, mock_shim, _mock_exists, tmp_path): payer = _write_keypair(tmp_path / "payer.json") beacon = Keypair() to = Keypair() nonce = Keypair() + mint = Keypair() + mock_encrypt.return_value = { + "ciphertexts": [[0] * 32], + "pubkey_hex": "00" * 32, + "nonce_bn": "0", + } + mock_shim.return_value = _arcium_accounts() tx = wallet.partial_sign_execute_payment( str(tmp_path / "payer.json"), @@ -294,7 +322,9 @@ def test_partial_sign_execute_payment_returns_base64(tmp_path): str(nonce.pubkey()), str(to.pubkey()), 500_000, - ZERO_HASH, + MXE_PUBKEY_HEX, + str(mint.pubkey()), + nonce_value=ZERO_HASH, ) assert tx is not None decoded = base64.b64decode(tx) @@ -308,7 +338,8 @@ def test_partial_sign_execute_payment_missing_keypair(tmp_path, capsys): str(Keypair().pubkey()), str(Keypair().pubkey()), 1_000, - ZERO_HASH, + MXE_PUBKEY_HEX, + str(Keypair().pubkey()), ) assert result is None assert "Failed to load" in capsys.readouterr().out @@ -322,7 +353,8 @@ def test_partial_sign_execute_payment_invalid_address(tmp_path, capsys): "also-invalid", "still-invalid", 1_000, - ZERO_HASH, + MXE_PUBKEY_HEX, + "invalid-mint", ) assert result is None assert "Invalid address" in capsys.readouterr().out @@ -330,24 +362,25 @@ def test_partial_sign_execute_payment_invalid_address(tmp_path, capsys): # ── create_nonce_account (instruction build) ────────────────────────────────── -def test_create_nonce_account_authority_param_name(tmp_path, monkeypatch): +def test_create_nonce_account_authority_param_name(tmp_path): """ Regression: InitializeNonceAccountParams uses 'authority', not 'authorized_pubkey'. Mock out RPC calls and verify the instruction builds without ValueError. """ - from unittest.mock import patch, MagicMock _write_keypair(tmp_path / "payer.json") + _write_keypair(tmp_path / "nonce.json") - mock_rpc = MagicMock() - mock_rpc.return_value = {"result": 1_447_680} # rent mock_blockhash = MagicMock(return_value="4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi") - mock_send = MagicMock(return_value={"result": "SIG"}) - with patch("wallet.rpc_call", mock_rpc), \ - patch("wallet.get_recent_blockhash", mock_blockhash), \ - patch("wallet.rpc_call", mock_send): + with patch("rpc.rpc_call", side_effect=[{"result": 1_447_680}, {"result": "SIG"}]), \ + patch("rpc.get_recent_blockhash", mock_blockhash): # The call must not raise ValueError: Missing required key: authority try: - wallet.create_nonce_account(str(tmp_path / "payer.json"), None, None) + result = wallet.create_nonce_account( + str(tmp_path / "payer.json"), + str(tmp_path / "nonce.json"), + None, + ) except ValueError as e: pytest.fail(f"InitializeNonceAccountParams raised ValueError: {e}") + assert result is not None diff --git a/wallet.py b/wallet.py index eb949a2..3658065 100644 --- a/wallet.py +++ b/wallet.py @@ -337,14 +337,18 @@ def partial_sign_execute_payment( log_err(f"Failed to load keypair: {exc}") return None - payer_pubkey = payer.pubkey() - beacon_pubkey = Pubkey.from_string(beacon_pubkey_str) - nonce_pubkey = Pubkey.from_string(nonce_account_str) - prog_pubkey = Pubkey.from_string(program_id_str) - mint_pubkey = Pubkey.from_string(mint_str) - recipient_pk = Pubkey.from_string(recipient_str) - # Treasury defaults to beacon/operator if not explicitly provided - treasury_pk = Pubkey.from_string(treasury_str) if treasury_str else beacon_pubkey + payer_pubkey = payer.pubkey() + try: + beacon_pubkey = Pubkey.from_string(beacon_pubkey_str) + nonce_pubkey = Pubkey.from_string(nonce_account_str) + prog_pubkey = Pubkey.from_string(program_id_str) + mint_pubkey = Pubkey.from_string(mint_str) + recipient_pk = Pubkey.from_string(recipient_str) + # Treasury defaults to beacon/operator if not explicitly provided + treasury_pk = Pubkey.from_string(treasury_str) if treasury_str else beacon_pubkey + except ValueError as exc: + log_err(f"Invalid address: {exc}") + return None # Random u64 computation offset — uniquely identifies this Arcium computation comp_offset = int.from_bytes(_secrets.token_bytes(8), "little") From 7dff4883aad0493e76cdc00a43151fe086f005d8 Mon Sep 17 00:00:00 2001 From: epicexcelsior Date: Sun, 31 May 2026 03:04:03 -0800 Subject: [PATCH 003/107] feat(cli): add headless node preflight launcher --- scripts/headless-node.sh | 114 +++++++++++++++++++++++++++++ scripts/preflight.py | 154 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 268 insertions(+) create mode 100755 scripts/headless-node.sh create mode 100755 scripts/preflight.py diff --git a/scripts/headless-node.sh b/scripts/headless-node.sh new file mode 100755 index 0000000..adc7614 --- /dev/null +++ b/scripts/headless-node.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +PYTHON_BIN="${ANONMESH_PYTHON:-$PROJECT_ROOT/venv/bin/python}" +STATE_DIR="${ANONMESH_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/anonmesh}" +PID_FILE="$STATE_DIR/headless-node.pid" +LOG_FILE="$STATE_DIR/headless-node.log" +CONFIG_DIR="${ANONMESH_CONFIG_DIR:-$HOME/.reticulum}" +NETWORK="${ANONMESH_NETWORK:-devnet}" +RPC_URL="${ANONMESH_RPC_URL:-}" + +if [[ ! -x "$PYTHON_BIN" ]]; then + PYTHON_BIN="${ANONMESH_PYTHON:-python3}" +fi + +node_args=(--config "$CONFIG_DIR" --network "$NETWORK") +preflight_args=(--config "$CONFIG_DIR" --network "$NETWORK") +if [[ -n "$RPC_URL" ]]; then + node_args+=(--rpc "$RPC_URL") + preflight_args+=(--rpc "$RPC_URL") +fi + +is_running() { + [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null +} + +start() { + if is_running; then + echo "headless node already running (pid $(cat "$PID_FILE"))" + return 0 + fi + + "$PYTHON_BIN" "$SCRIPT_DIR/preflight.py" "${preflight_args[@]}" + mkdir -p "$STATE_DIR" + nohup "$PYTHON_BIN" "$SCRIPT_DIR/exit_node.py" "${node_args[@]}" "$@" >> "$LOG_FILE" 2>&1 & + echo "$!" > "$PID_FILE" + sleep 1 + + if ! is_running; then + echo "headless node failed to start; inspect $LOG_FILE" >&2 + return 1 + fi + + echo "headless node started (pid $(cat "$PID_FILE"))" + echo "logs: $0 logs" +} + +status() { + if is_running; then + echo "headless node running (pid $(cat "$PID_FILE"))" + echo "logs: $LOG_FILE" + return 0 + fi + + echo "headless node stopped" + return 1 +} + +stop() { + if ! is_running; then + echo "headless node already stopped" + return 0 + fi + + pid="$(cat "$PID_FILE")" + kill "$pid" + for _ in {1..20}; do + if ! kill -0 "$pid" 2>/dev/null; then + rm -f "$PID_FILE" + echo "headless node stopped" + return 0 + fi + sleep 0.1 + done + + echo "headless node did not stop within 2s (pid $pid)" >&2 + return 1 +} + +logs() { + mkdir -p "$STATE_DIR" + touch "$LOG_FILE" + tail -n 100 -f "$LOG_FILE" +} + +run() { + "$PYTHON_BIN" "$SCRIPT_DIR/preflight.py" "${preflight_args[@]}" + exec "$PYTHON_BIN" "$SCRIPT_DIR/exit_node.py" "${node_args[@]}" "$@" +} + +usage() { + cat < None: + self.failures = 0 + + def ok(self, message: str) -> None: + print(f"[ok] {message}") + + def warn(self, message: str) -> None: + print(f"[warn] {message}") + + def fail(self, message: str) -> None: + self.failures += 1 + print(f"[fail] {message}") + + +def config_file(config_dir: str | None) -> Path: + return Path(config_dir or "~/.reticulum").expanduser() / "config" + + +def check_python(checks: Checks) -> None: + version = sys.version_info + label = f"Python {version.major}.{version.minor}.{version.micro}" + if version >= (3, 10): + checks.ok(label) + else: + checks.fail(f"{label}; Python 3.10+ is required") + + +def check_module(checks: Checks, module: str, label: str) -> None: + try: + importlib.import_module(module) + except ImportError as exc: + checks.fail(f"{label} import failed: {exc}") + else: + checks.ok(f"{label} import") + + +def read_config(checks: Checks, path: Path) -> str | None: + if not path.is_file(): + checks.fail(f"Reticulum config not found: {path}") + return None + + text = path.read_text() + missing = [ + section for section in ("[reticulum]", "[interfaces]") + if section not in text + ] + if missing: + checks.fail(f"Reticulum config missing sections: {', '.join(missing)}") + return None + + checks.ok(f"Reticulum config: {path}") + return text + + +def check_optional_transports(checks: Checks, text: str | None, args: argparse.Namespace) -> None: + text = text or "" + has_ble = "BLEInterface" in text + has_rnode = "RNodeInterface" in text + has_meshtastic = "Meshtastic" in text + + if args.ble or has_ble: + checks.fail( + "desktop BLE is experimental: installing bleak does not provide " + "a supported Reticulum BLEInterface" + ) + + if args.rnode and not has_rnode: + checks.fail("RNode requested but no RNodeInterface exists in the Reticulum config") + + if has_rnode: + ports = re.findall(r"^\s*port\s*=\s*(\S+)\s*$", text, flags=re.MULTILINE) + if not ports: + checks.fail("RNodeInterface exists but no serial port is configured") + for port in ports: + if Path(port).exists(): + checks.ok(f"RNode serial port: {port}") + else: + checks.fail(f"RNode serial port not found: {port}") + + if args.meshtastic or has_meshtastic: + check_module(checks, "meshtastic", "Meshtastic") + + +def check_rpc(checks: Checks, rpc_url: str) -> None: + try: + import requests + + response = requests.post( + rpc_url, + json={"jsonrpc": "2.0", "id": 1, "method": "getHealth"}, + timeout=8, + ) + response.raise_for_status() + body = response.json() + if body.get("result") != "ok": + checks.fail(f"Solana RPC returned unexpected getHealth response: {body}") + return + except Exception as exc: + checks.fail(f"Solana RPC unreachable: {rpc_url} ({exc})") + else: + checks.ok(f"Solana RPC reachable: {rpc_url}") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--config", "-c", default=None, help="Reticulum config directory") + parser.add_argument("--network", "-n", choices=sorted(SOLANA_ENDPOINTS), default="devnet") + parser.add_argument("--rpc", default=None, help="Custom Solana RPC URL") + parser.add_argument("--skip-rpc", action="store_true", help="Skip the Solana RPC reachability check") + parser.add_argument("--ble", action="store_true", help="Check the experimental desktop BLE path") + parser.add_argument("--rnode", action="store_true", help="Require an RNodeInterface in the config") + parser.add_argument("--meshtastic", action="store_true", help="Require the Meshtastic Python package") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + checks = Checks() + + check_python(checks) + check_module(checks, "RNS", "Reticulum") + check_module(checks, "requests", "requests") + text = read_config(checks, config_file(args.config)) + check_optional_transports(checks, text, args) + if not args.skip_rpc: + check_rpc(checks, args.rpc or SOLANA_ENDPOINTS[args.network]) + + if checks.failures: + print(f"\npreflight failed: {checks.failures} check(s)") + return 1 + print("\npreflight passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 9a0c2664288996881dff01018d5ae471d42411aa Mon Sep 17 00:00:00 2001 From: epicexcelsior Date: Sun, 31 May 2026 03:06:02 -0800 Subject: [PATCH 004/107] docs(cli): mark desktop BLE path experimental --- README.md | 51 +++++++++++++++++++++++++++---------- config/reticulum_rnode.conf | 10 +++----- scripts/exit_node.py | 1 - setup.sh | 12 ++++----- 4 files changed, 45 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 56ba6a2..db6233e 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ **Mesh First, Chain When It Matters.** -anonmesh is a Python MVP for tunneling Solana JSON-RPC requests over [Reticulum's](https://reticulum.network/) end-to-end encrypted mesh network. Off-grid devices interact with the Solana blockchain through connected gateway nodes ("Beacons") over virtually any transport medium — LoRa, BLE, WiFi, Packet Radio, TCP hubs, and more. +anonmesh is a Python MVP for tunneling Solana JSON-RPC requests over [Reticulum's](https://reticulum.network/) end-to-end encrypted mesh network. Off-grid devices interact with the Solana blockchain through connected gateway nodes ("Beacons") over supported Reticulum transports such as LoRa, WiFi, Packet Radio, and TCP hubs. Desktop BLE relay remains experimental. -After relaying a transaction, the Beacon co-signs and submits an `execute_payment` instruction to the **ble_revshare** Anchor program, logging encrypted payment statistics via [Arcium MPC](https://arcium.com/) — so revenue-share accounting happens on-chain without leaking raw amounts. +When configured, a Beacon can co-sign and submit an `execute_payment` instruction to the **ble_revshare** Anchor program after relaying a transaction with the required Arcium metadata. This logs encrypted payment statistics via [Arcium MPC](https://arcium.com/) without leaking raw amounts. ## Architecture @@ -46,7 +46,7 @@ chmod +x setup.sh ./setup.sh --client # client only (adds solders, qrcode) ./setup.sh --both # both ./setup.sh --systemd # also install beacon as a systemd service -./setup.sh --ble # add Bluetooth Low Energy transport +./setup.sh --ble # install experimental BLE research deps only ./setup.sh --meshtastic # add Meshtastic / LoRa transport ./setup.sh --wallet-setup # generate signing keypair + durable nonce account ./setup.sh --mainnet # target Solana mainnet-beta instead of devnet @@ -54,25 +54,24 @@ chmod +x setup.sh ## Configuration -Copy `.env.example` to `.env` and edit: +Create `.env` only if you need optional Arcium or wallet overrides: ```bash -cp .env.example .env +touch .env ``` Key variables: | Variable | Default | Description | |---|---|---| -| `SOLANA_NETWORK` | `devnet` | `devnet` or `mainnet` | -| `ARCIUM_ENABLED` | `1` | Set to `0` to disable Arcium MPC | -| `ARCIUM_PAYER_KEYPAIR` | `~/.config/solana/id.json` | Keypair that pays Arcium computation fees | +| `SOLANA_NETWORK` | `devnet` | Launcher network: `devnet` or `mainnet` | +| `ARCIUM_ENABLED` | `0` | Set to `1` to enable Arcium MPC | +| `ARCIUM_PAYER_KEYPAIR` | *(unset)* | Keypair that pays Arcium computation fees | | `ARCIUM_RPC_URL` | devnet public endpoint | RPC for Arcium transactions | -| `ARCIUM_MXE_PUBKEY_HEX` | *(pre-filled for devnet)* | MXE x25519 public key | +| `ARCIUM_MXE_PUBKEY_HEX` | *(unset)* | MXE x25519 public key | | `ARCIUM_CLUSTER_OFFSET` | `456` | `456` = devnet, `2026` = mainnet-alpha | | `ARCIUM_BROADCASTER_TOKEN_ACCOUNT` | *(derived)* | Beacon's SPL token account for rev-share | | `ARCIUM_TREASURY_TOKEN_ACCOUNT` | *(derived from broadcaster)* | Treasury token account | -| `ANNOUNCE_INTERVAL` | `300` | Seconds between Reticulum re-announces | ## Usage @@ -97,6 +96,30 @@ The beacon prints its **DESTINATION HASH** on startup — share this with client ./run_client.sh --balance ``` +### 3. Run a Headless Exit Node + +Use the headless launcher for a laptop or Linux server that only forwards RPC: + +```bash +./scripts/headless-node.sh preflight +./scripts/headless-node.sh start +./scripts/headless-node.sh status +./scripts/headless-node.sh logs +./scripts/headless-node.sh stop +``` + +Override `ANONMESH_CONFIG_DIR`, `ANONMESH_NETWORK`, or `ANONMESH_RPC_URL` when the defaults do not match your deployment. + +## Testing + +```bash +# Fresh local test environment + unit tests +npm test -- -q + +# Localhost Reticulum relay → headless exit node → Solana devnet +.venv-test/bin/python tests/test_tcp_bridge.py +``` + ## Arcium MPC — execute_payment flow After the beacon relays a `sendTransaction` containing Arcium metadata, it: @@ -141,20 +164,20 @@ node scripts/