From 3ee1fa8c58b1d293e438ad7cd1561248e08f47e3 Mon Sep 17 00:00:00 2001 From: "Maciek \"mab122\" Bator" Date: Thu, 23 Apr 2026 13:35:58 +0200 Subject: [PATCH] fix: persist bridge NIDs across restarts; fix missing Tuple import bridge.py: - state_path param: optional JSON file for persisting discovered bridge NIDs - _load_state: on startup, re-registers each saved NID at its allocated port so radicle-node reconnects fast without waiting for a re-announce cycle - _save_state: called whenever a new NID is first discovered - Added json/Path imports cli.py: - cmd_seed: passes state_path={seed_home}/bridge_state.json - cmd_bridge: passes state_path=~/.radicle-rns/bridge_state.json - Fixed missing Tuple import (used by _parse_delays return type annotation) Co-Authored-By: Claude Sonnet 4.6 --- src/radicle_reticulum/bridge.py | 54 +++++++++++++++++++++++++++++++++ src/radicle_reticulum/cli.py | 4 ++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/radicle_reticulum/bridge.py b/src/radicle_reticulum/bridge.py index a3156d7..2a2f9d9 100644 --- a/src/radicle_reticulum/bridge.py +++ b/src/radicle_reticulum/bridge.py @@ -15,6 +15,7 @@ The bridge: 4. Remote bridges forward to their local radicle-node """ +import json import os import socket import select @@ -23,6 +24,7 @@ import subprocess import threading import time from dataclasses import dataclass, field +from pathlib import Path from typing import Callable, Dict, List, Optional, Set, Tuple import RNS @@ -92,6 +94,7 @@ class RadicleBridge: auto_seed: bool = True, announce_retry_delays: Tuple[int, ...] = (5, 15, 30), rad_home: Optional[str] = None, + state_path: Optional[Path] = None, ): """Initialize the bridge. @@ -108,6 +111,8 @@ class RadicleBridge: intervals on LoRa to respect duty cycle limits, e.g. (60, 300, 900). rad_home: RAD_HOME for rad CLI calls. None = use system default. Set to seed home when bridging a dedicated seed node. + state_path: JSON file to persist discovered bridge NIDs across restarts. + None = no persistence. """ self.listen_port = listen_port self.radicle_host = radicle_host @@ -116,6 +121,7 @@ class RadicleBridge: self.auto_seed = auto_seed self.announce_retry_delays = announce_retry_delays self.rad_home = rad_home + self.state_path = state_path # Initialize Reticulum self.reticulum = RNS.Reticulum(config_path) @@ -172,6 +178,9 @@ class RadicleBridge: RNS.Transport.register_announce_handler(self._handle_announce) RNS.log("Registered announce handler for bridge discovery", RNS.LOG_INFO) + # Load persisted NIDs first so radicle-node is ready for reconnects + self._load_state() + # Announce presence; repeat a few times so peers that come up shortly # after us don't miss it due to interface initialisation timing self.announce() @@ -215,6 +224,48 @@ class RadicleBridge: ) RNS.Transport.request_path(bridge_hash) + def _load_state(self): + """Load persisted bridge NIDs and re-register them with radicle-node.""" + if not self.state_path or not self.state_path.exists(): + return + try: + data = json.loads(self.state_path.read_text()) + nids: Dict[str, str] = data.get("bridge_nids", {}) + except Exception as e: + RNS.log(f"Could not load bridge state: {e}", RNS.LOG_WARNING) + return + + for hex_hash, nid in nids.items(): + try: + bridge_hash = bytes.fromhex(hex_hash) + except ValueError: + continue + with self._remote_bridges_lock: + self._remote_bridges[bridge_hash] = 0.0 # unknown age; path maintenance will refresh + self._bridge_nids[bridge_hash] = nid + # Allocate port and re-register with radicle-node so it can connect + threading.Thread( + target=self._auto_register_seed, + args=(nid, bridge_hash), + daemon=True, + ).start() + RNS.log( + f"Restored bridge state: {hex_hash[:16]} → {nid[:32]}", + RNS.LOG_INFO, + ) + + def _save_state(self): + """Persist current bridge NIDs to disk for next startup.""" + if not self.state_path: + return + try: + self.state_path.parent.mkdir(parents=True, exist_ok=True) + with self._remote_bridges_lock: + nids = {h.hex(): n for h, n in self._bridge_nids.items()} + self.state_path.write_text(json.dumps({"bridge_nids": nids}, indent=2)) + except Exception as e: + RNS.log(f"Could not save bridge state: {e}", RNS.LOG_WARNING) + def _reconnect_link( self, bridge_hash: bytes, timeout: float = 20.0 ) -> Optional[RNS.Link]: @@ -660,6 +711,9 @@ class RadicleBridge: daemon=True, ).start() + if nid_is_new: + self._save_state() + if self.auto_seed and nid_is_new: threading.Thread( target=self._auto_register_seed, diff --git a/src/radicle_reticulum/cli.py b/src/radicle_reticulum/cli.py index 2781085..354f061 100644 --- a/src/radicle_reticulum/cli.py +++ b/src/radicle_reticulum/cli.py @@ -7,7 +7,7 @@ import sys import time import signal from pathlib import Path -from typing import Optional +from typing import Optional, Tuple DEFAULT_IDENTITY_PATH = Path.home() / ".radicle-rns" / "identity" @@ -362,6 +362,7 @@ def cmd_seed(args): auto_seed=True, announce_retry_delays=announce_retry_delays, rad_home=str(seed_home), + state_path=seed_home / "bridge_state.json", ) bridge.set_local_radicle_nid(nid) @@ -457,6 +458,7 @@ def cmd_bridge(args): auto_connect=auto_connect, auto_seed=auto_seed, announce_retry_delays=announce_retry_delays, + state_path=Path.home() / ".radicle-rns" / "bridge_state.json", ) # Resolve local radicle NID: explicit flag > auto-detect from 'rad self'