diff --git a/core/archipelago/src/fips/anchors.rs b/core/archipelago/src/fips/anchors.rs index 9362d15b..899fd689 100644 --- a/core/archipelago/src/fips/anchors.rs +++ b/core/archipelago/src/fips/anchors.rs @@ -49,9 +49,7 @@ pub const DEFAULT_PUBLIC_ANCHOR_NPUB: &str = pub const DEFAULT_PUBLIC_ANCHOR_ADDR: &str = "185.18.221.160:8443"; pub const DEFAULT_PUBLIC_ANCHOR_TRANSPORT: &str = "tcp"; -/// The default public anchor as a ready-to-apply `SeedAnchor`. Carried -/// implicitly by `load()` on nodes that have never edited their anchor -/// list, so every node dials it without operator action. +/// The upstream public anchor as a ready-to-apply `SeedAnchor`. pub fn default_public_anchor() -> SeedAnchor { SeedAnchor { npub: DEFAULT_PUBLIC_ANCHOR_NPUB.to_string(), @@ -61,6 +59,38 @@ pub fn default_public_anchor() -> SeedAnchor { } } +// Archipelago-operated anchor on vps2 (the OTA/registry host, 146.59.87.168). +// Every node already reaches this host for updates, so it is reachable from +// networks that the upstream anchor is not — which is most of them (the +// upstream anchor answers on one IPv4 that many home/office networks can't +// reach, and its DNS resolves IPv6-first while the daemon is IPv4-only). +// TCP because that traverses NAT/firewalls best; 8444 because 8443 on that +// host is already taken by a container. +pub const ARCHY_ANCHOR_NPUB: &str = + "npub1dptaktwxv0mm245g2lqjykwm5ll0jpc6m3r4242ydfa9z7qe6urs3jvrak"; +pub const ARCHY_ANCHOR_ADDR: &str = "146.59.87.168:8444"; +pub const ARCHY_ANCHOR_TRANSPORT: &str = "tcp"; + +/// The Archipelago-operated anchor as a ready-to-apply `SeedAnchor`. +pub fn archy_anchor() -> SeedAnchor { + SeedAnchor { + npub: ARCHY_ANCHOR_NPUB.to_string(), + address: ARCHY_ANCHOR_ADDR.to_string(), + transport: ARCHY_ANCHOR_TRANSPORT.to_string(), + label: "Archipelago anchor (vps2)".to_string(), + } +} + +/// The default anchor set carried implicitly by `load()` on nodes that have +/// never edited their anchor list, so every node dials them without operator +/// action. Multiple anchors so one unreachable rendezvous host can't strand a +/// node: `fipsctl connect` is attempted for each, and whichever the node's +/// network can reach wins. The Archipelago-operated anchor is listed first +/// because it is reachable from the widest set of networks. +pub fn default_public_anchors() -> Vec { + vec![archy_anchor(), default_public_anchor()] +} + /// One seed-anchor entry. `address` must be directly dialable (IP or /// resolvable hostname + UDP port); `transport` is one of "udp", "tcp", /// "tor", "ethernet" (the values upstream `fipsctl connect` accepts). @@ -94,7 +124,7 @@ fn anchors_path(data_dir: &Path) -> PathBuf { pub async fn load(data_dir: &Path) -> Result> { let path = anchors_path(data_dir); if !path.exists() { - return Ok(vec![default_public_anchor()]); + return Ok(default_public_anchors()); } let bytes = tokio::fs::read(&path) .await @@ -268,28 +298,46 @@ mod tests { } #[tokio::test] - async fn load_missing_seeds_default_public_anchor() { - // A node that has never edited its anchor list should still get - // the public anchor so it can bootstrap the mesh out of the box. + async fn load_missing_seeds_default_public_anchors() { + // A node that has never edited its anchor list should still get the + // full default anchor set so it can bootstrap the mesh out of the box. let dir = tempfile::tempdir().unwrap(); let got = load(dir.path()).await.unwrap(); - assert_eq!(got, vec![default_public_anchor()]); - // ...and the default must be the TCP/8443 form, not the dead udp:8668. - assert_eq!(got[0].transport, "tcp"); - assert!(got[0].address.ends_with(":8443")); + assert_eq!(got, default_public_anchors()); + // The Archipelago-operated anchor must come first (widest reachability) + // and the upstream anchor must remain present as a fallback. + assert_eq!(got[0], archy_anchor()); + assert!(got.contains(&default_public_anchor())); + // Every default must be a TCP form (traverses NAT/firewalls), never the + // dead udp:8668 the upstream anchor never answers on. + assert!(got.iter().all(|a| a.transport == "tcp")); } #[tokio::test] - async fn removing_default_persists_as_empty() { - // Once the operator removes the default, a file exists and is - // authoritative — we must not silently re-seed it on next load. + async fn removing_one_default_persists_and_keeps_the_other() { + // Editing the anchor list (here removing one default) makes the file + // authoritative: the removed anchor must not be silently re-seeded on + // next load, and the remaining default must stay. let dir = tempfile::tempdir().unwrap(); + let list = remove(dir.path(), ARCHY_ANCHOR_NPUB).await.unwrap(); + assert!(!list.iter().any(|a| a.npub == ARCHY_ANCHOR_NPUB)); + assert!(list.contains(&default_public_anchor())); + let got = load(dir.path()).await.unwrap(); + assert_eq!(got, list, "edited list is authoritative; no re-seed"); + } + + #[tokio::test] + async fn removing_all_defaults_persists_as_empty() { + // Removing every default leaves an empty authoritative list that must + // not be re-seeded on next load. + let dir = tempfile::tempdir().unwrap(); + remove(dir.path(), ARCHY_ANCHOR_NPUB).await.unwrap(); let list = remove(dir.path(), DEFAULT_PUBLIC_ANCHOR_NPUB) .await .unwrap(); assert!(list.is_empty()); let got = load(dir.path()).await.unwrap(); - assert!(got.is_empty(), "default must stay removed once edited"); + assert!(got.is_empty(), "defaults must stay removed once edited"); } #[tokio::test]