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 <noreply@anthropic.com>
This commit is contained in:
Maciek "mab122" Bator 2026-04-23 13:35:58 +02:00
parent 4d3fdcf5f9
commit 3ee1fa8c58
2 changed files with 57 additions and 1 deletions

View File

@ -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,

View File

@ -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'