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 4. Remote bridges forward to their local radicle-node
""" """
import json
import os import os
import socket import socket
import select import select
@ -23,6 +24,7 @@ import subprocess
import threading import threading
import time import time
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable, Dict, List, Optional, Set, Tuple from typing import Callable, Dict, List, Optional, Set, Tuple
import RNS import RNS
@ -92,6 +94,7 @@ class RadicleBridge:
auto_seed: bool = True, auto_seed: bool = True,
announce_retry_delays: Tuple[int, ...] = (5, 15, 30), announce_retry_delays: Tuple[int, ...] = (5, 15, 30),
rad_home: Optional[str] = None, rad_home: Optional[str] = None,
state_path: Optional[Path] = None,
): ):
"""Initialize the bridge. """Initialize the bridge.
@ -108,6 +111,8 @@ class RadicleBridge:
intervals on LoRa to respect duty cycle limits, e.g. (60, 300, 900). 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. rad_home: RAD_HOME for rad CLI calls. None = use system default.
Set to seed home when bridging a dedicated seed node. 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.listen_port = listen_port
self.radicle_host = radicle_host self.radicle_host = radicle_host
@ -116,6 +121,7 @@ class RadicleBridge:
self.auto_seed = auto_seed self.auto_seed = auto_seed
self.announce_retry_delays = announce_retry_delays self.announce_retry_delays = announce_retry_delays
self.rad_home = rad_home self.rad_home = rad_home
self.state_path = state_path
# Initialize Reticulum # Initialize Reticulum
self.reticulum = RNS.Reticulum(config_path) self.reticulum = RNS.Reticulum(config_path)
@ -172,6 +178,9 @@ class RadicleBridge:
RNS.Transport.register_announce_handler(self._handle_announce) RNS.Transport.register_announce_handler(self._handle_announce)
RNS.log("Registered announce handler for bridge discovery", RNS.LOG_INFO) 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 # Announce presence; repeat a few times so peers that come up shortly
# after us don't miss it due to interface initialisation timing # after us don't miss it due to interface initialisation timing
self.announce() self.announce()
@ -215,6 +224,48 @@ class RadicleBridge:
) )
RNS.Transport.request_path(bridge_hash) 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( def _reconnect_link(
self, bridge_hash: bytes, timeout: float = 20.0 self, bridge_hash: bytes, timeout: float = 20.0
) -> Optional[RNS.Link]: ) -> Optional[RNS.Link]:
@ -660,6 +711,9 @@ class RadicleBridge:
daemon=True, daemon=True,
).start() ).start()
if nid_is_new:
self._save_state()
if self.auto_seed and nid_is_new: if self.auto_seed and nid_is_new:
threading.Thread( threading.Thread(
target=self._auto_register_seed, target=self._auto_register_seed,

View File

@ -7,7 +7,7 @@ import sys
import time import time
import signal import signal
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional, Tuple
DEFAULT_IDENTITY_PATH = Path.home() / ".radicle-rns" / "identity" DEFAULT_IDENTITY_PATH = Path.home() / ".radicle-rns" / "identity"
@ -362,6 +362,7 @@ def cmd_seed(args):
auto_seed=True, auto_seed=True,
announce_retry_delays=announce_retry_delays, announce_retry_delays=announce_retry_delays,
rad_home=str(seed_home), rad_home=str(seed_home),
state_path=seed_home / "bridge_state.json",
) )
bridge.set_local_radicle_nid(nid) bridge.set_local_radicle_nid(nid)
@ -457,6 +458,7 @@ def cmd_bridge(args):
auto_connect=auto_connect, auto_connect=auto_connect,
auto_seed=auto_seed, auto_seed=auto_seed,
announce_retry_delays=announce_retry_delays, 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' # Resolve local radicle NID: explicit flag > auto-detect from 'rad self'