bp_core/network/
bootstrap.rs1use libp2p::{multiaddr::Protocol, Multiaddr, PeerId, Swarm};
34use serde::{Deserialize, Serialize};
35use std::path::Path;
36
37use crate::network::behaviour::BillPouchBehaviour;
38
39#[derive(Debug, Default, Serialize, Deserialize)]
43pub struct BootstrapList(pub Vec<String>);
44
45impl BootstrapList {
46 pub fn load(path: &Path) -> Self {
49 std::fs::read_to_string(path)
50 .ok()
51 .and_then(|s| serde_json::from_str(&s).ok())
52 .unwrap_or_default()
53 }
54
55 pub fn save(&self, path: &Path) {
57 match serde_json::to_string_pretty(self) {
58 Ok(s) => {
59 if let Err(e) = std::fs::write(path, s) {
60 tracing::warn!("bootstrap: failed to save list: {e}");
61 }
62 }
63 Err(e) => tracing::warn!("bootstrap: serialization error: {e}"),
64 }
65 }
66
67 pub fn add(&mut self, addr: String) {
69 if !self.0.contains(&addr) {
70 self.0.push(addr);
71 }
72 }
73
74 pub fn remove(&mut self, addr: &str) {
76 self.0.retain(|a| a != addr);
77 }
78
79 pub fn apply(&self, swarm: &mut Swarm<BillPouchBehaviour>) {
84 if self.0.is_empty() {
85 return;
86 }
87 tracing::info!(n = self.0.len(), "bootstrap: applying bootstrap peer list");
88 for addr_str in &self.0 {
89 match addr_str.parse::<Multiaddr>() {
90 Ok(addr) => match peer_id_from_multiaddr(&addr) {
91 Some(peer_id) => {
92 swarm
93 .behaviour_mut()
94 .kad
95 .add_address(&peer_id, addr.clone());
96 swarm.behaviour_mut().gossipsub.add_explicit_peer(&peer_id);
97 if let Err(e) = swarm.dial(addr.clone()) {
98 tracing::warn!(addr = %addr_str, "bootstrap: dial failed: {e}");
99 } else {
100 tracing::debug!(addr = %addr_str, peer = %peer_id, "bootstrap: dialing");
101 }
102 }
103 None => {
104 tracing::warn!(
105 addr = %addr_str,
106 "bootstrap: no /p2p/<PeerId> in address, skipping"
107 );
108 }
109 },
110 Err(e) => {
111 tracing::warn!(addr = %addr_str, "bootstrap: invalid multiaddr: {e}");
112 }
113 }
114 }
115 }
116}
117
118fn peer_id_from_multiaddr(addr: &Multiaddr) -> Option<PeerId> {
123 addr.iter().find_map(|proto| {
124 if let Protocol::P2p(peer_id) = proto {
125 Some(peer_id)
126 } else {
127 None
128 }
129 })
130}
131
132#[cfg(test)]
135mod tests {
136 use super::*;
137
138 fn tmp_path(name: &str) -> std::path::PathBuf {
139 std::env::temp_dir().join(format!("bp_bootstrap_test_{name}.json"))
140 }
141
142 #[test]
143 fn load_missing_returns_default() {
144 let list = BootstrapList::load(Path::new("/tmp/no_such_bootstrap_xyz.json"));
145 assert!(list.0.is_empty());
146 }
147
148 #[test]
149 fn load_corrupt_returns_default() {
150 let path = tmp_path("corrupt");
151 std::fs::write(&path, b"NOT JSON{{").unwrap();
152 let list = BootstrapList::load(&path);
153 let _ = std::fs::remove_file(&path);
154 assert!(list.0.is_empty());
155 }
156
157 #[test]
158 fn round_trip() {
159 let path = tmp_path("roundtrip");
160 let mut list = BootstrapList::default();
161 list.add("/ip4/1.2.3.4/tcp/4001/p2p/12D3KooWFakeAAA".into());
162 list.add("/ip4/5.6.7.8/tcp/4001/p2p/12D3KooWFakeBBB".into());
163 list.save(&path);
164
165 let loaded = BootstrapList::load(&path);
166 let _ = std::fs::remove_file(&path);
167 assert_eq!(loaded.0.len(), 2);
168 }
169
170 #[test]
171 fn add_deduplicates() {
172 let mut list = BootstrapList::default();
173 list.add("addr1".into());
174 list.add("addr1".into());
175 assert_eq!(list.0.len(), 1);
176 }
177
178 #[test]
179 fn remove_works() {
180 let mut list = BootstrapList::default();
181 list.add("addr1".into());
182 list.add("addr2".into());
183 list.remove("addr1");
184 assert_eq!(list.0, vec!["addr2".to_string()]);
185 }
186
187 #[test]
188 fn peer_id_from_multiaddr_with_p2p() {
189 let addr_no_p2p: Multiaddr = "/ip4/1.2.3.4/tcp/4001".parse().unwrap();
193 assert!(peer_id_from_multiaddr(&addr_no_p2p).is_none());
194 }
195}