ant_bootstrap/cache_store/
mod.rs

1// Copyright 2024 MaidSafe.net limited.
2//
3// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
4// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
5// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
6// KIND, either express or implied. Please review the Licences for the specific language governing
7// permissions and limitations relating to use of the SAFE Network Software.
8
9pub mod cache_data_v0;
10pub mod cache_data_v1;
11
12use crate::{craft_valid_multiaddr, BootstrapCacheConfig, Error, InitialPeersConfig, Result};
13use libp2p::{multiaddr::Protocol, Multiaddr, PeerId};
14use std::fs;
15
16pub type CacheDataLatest = cache_data_v1::CacheData;
17pub const CACHE_DATA_VERSION_LATEST: u32 = cache_data_v1::CacheData::CACHE_DATA_VERSION;
18
19#[derive(Clone, Debug)]
20pub struct BootstrapCacheStore {
21    pub(crate) config: BootstrapCacheConfig,
22    pub(crate) data: CacheDataLatest,
23}
24
25impl BootstrapCacheStore {
26    pub fn config(&self) -> &BootstrapCacheConfig {
27        &self.config
28    }
29
30    /// Create an empty CacheStore with the given configuration
31    pub fn new(config: BootstrapCacheConfig) -> Result<Self> {
32        info!("Creating new CacheStore with config: {:?}", config);
33
34        // Create cache directory if it doesn't exist
35        if !config.cache_dir.exists() {
36            info!(
37                "Attempting to create cache directory at {:?}",
38                config.cache_dir
39            );
40            fs::create_dir_all(&config.cache_dir).inspect_err(|err| {
41                warn!(
42                    "Failed to create cache directory at {:?}: {err}",
43                    config.cache_dir
44                );
45            })?;
46        }
47
48        let store = Self {
49            config,
50            data: CacheDataLatest::default(),
51        };
52
53        Ok(store)
54    }
55
56    /// Create an empty CacheStore from the given Initial Peers Configuration.
57    /// This also modifies the `BootstrapCacheConfig` if provided based on the `InitialPeersConfig`.
58    /// And also performs some actions based on the `InitialPeersConfig`.
59    ///
60    /// `InitialPeersConfig::bootstrap_cache_dir` will take precedence over the path provided inside `config`.
61    pub fn new_from_initial_peers_config(
62        init_peers_config: &InitialPeersConfig,
63        config: Option<BootstrapCacheConfig>,
64    ) -> Result<Self> {
65        let mut config = if let Some(cfg) = config {
66            cfg
67        } else {
68            BootstrapCacheConfig::new(init_peers_config.local)?
69        };
70
71        if let Some(cache_dir) = &init_peers_config.bootstrap_cache_dir {
72            config.cache_dir = cache_dir.clone();
73        }
74
75        let mut store = Self::new(config)?;
76
77        // If it is the first node, clear the cache.
78        if init_peers_config.first {
79            info!("First node in network, writing empty cache to disk");
80            store.write()?;
81        } else {
82            info!("Flushing cache to disk on init.");
83            store.sync_and_flush_to_disk()?;
84        }
85
86        Ok(store)
87    }
88
89    pub fn peer_count(&self) -> usize {
90        self.data.peers.len()
91    }
92
93    pub fn get_all_addrs(&self) -> impl Iterator<Item = &Multiaddr> {
94        self.data.get_all_addrs()
95    }
96
97    /// Remove a peer from the cache. This does not update the cache on disk.
98    pub fn remove_peer(&mut self, peer_id: &PeerId) {
99        self.data.peers.retain(|(id, _)| id != peer_id);
100    }
101
102    /// Add an address to the cache
103    pub fn add_addr(&mut self, addr: Multiaddr) {
104        let Some(addr) = craft_valid_multiaddr(&addr, false) else {
105            return;
106        };
107        let peer_id = match addr.iter().find(|p| matches!(p, Protocol::P2p(_))) {
108            Some(Protocol::P2p(id)) => id,
109            _ => return,
110        };
111        if addr.iter().any(|p| matches!(p, Protocol::P2pCircuit)) {
112            return;
113        }
114
115        debug!("Adding addr to bootstrap cache: {addr}");
116
117        self.data.add_peer(
118            peer_id,
119            [addr].iter(),
120            self.config.max_addrs_per_peer,
121            self.config.max_peers,
122        );
123    }
124
125    /// Load cache data from disk
126    /// Make sure to have clean addrs inside the cache as we don't call craft_valid_multiaddr
127    pub fn load_cache_data(cfg: &BootstrapCacheConfig) -> Result<CacheDataLatest> {
128        // try loading latest first
129        match cache_data_v1::CacheData::read_from_file(
130            &cfg.cache_dir,
131            &Self::cache_file_name(cfg.local),
132        ) {
133            Ok(mut data) => {
134                while data.peers.len() > cfg.max_peers {
135                    data.peers.pop_front();
136                }
137                return Ok(data);
138            }
139            Err(err) => {
140                warn!("Failed to load cache data from latest version: {err}");
141            }
142        }
143
144        // Try loading older version
145        match cache_data_v0::CacheData::read_from_file(
146            &cfg.cache_dir,
147            &Self::cache_file_name(cfg.local),
148        ) {
149            Ok(data) => {
150                warn!("Loaded cache data from older version, upgrading to latest version");
151                let mut data: CacheDataLatest = data.into();
152                while data.peers.len() > cfg.max_peers {
153                    data.peers.pop_front();
154                }
155
156                Ok(data)
157            }
158            Err(err) => {
159                warn!("Failed to load cache data from older version: {err}");
160                Err(Error::FailedToParseCacheData)
161            }
162        }
163    }
164
165    /// Flush the cache to disk after syncing with the CacheData from the file.
166    /// Do not perform cleanup when `data` is fetched from the network. The SystemTime might not be accurate.
167    pub fn sync_and_flush_to_disk(&mut self) -> Result<()> {
168        if self.config.disable_cache_writing {
169            info!("Cache writing is disabled, skipping sync to disk");
170            return Ok(());
171        }
172
173        info!(
174            "Flushing cache to disk, with data containing: {} peers",
175            self.data.peers.len(),
176        );
177
178        if let Ok(data_from_file) = Self::load_cache_data(&self.config) {
179            self.data.sync(
180                &data_from_file,
181                self.config.max_addrs_per_peer,
182                self.config.max_peers,
183            );
184        } else {
185            warn!("Failed to load cache data from file, overwriting with new data");
186        }
187
188        self.write().inspect_err(|e| {
189            error!("Failed to save cache to disk: {e}");
190        })?;
191
192        // Flush after writing
193        self.data.peers.clear();
194
195        Ok(())
196    }
197
198    /// Write the cache to disk atomically. This will overwrite the existing cache file, use sync_and_flush_to_disk to
199    /// sync with the file first.
200    pub fn write(&self) -> Result<()> {
201        if self.config.disable_cache_writing {
202            info!("Cache writing is disabled, skipping sync to disk");
203            return Ok(());
204        }
205
206        let filename = Self::cache_file_name(self.config.local);
207
208        self.data.write_to_file(&self.config.cache_dir, &filename)?;
209
210        if self.config.backwards_compatible_writes {
211            cache_data_v0::CacheData::from(&self.data)
212                .write_to_file(&self.config.cache_dir, &filename)?;
213        }
214
215        Ok(())
216    }
217
218    /// Returns the name of the cache filename based on the local flag
219    pub fn cache_file_name(local: bool) -> String {
220        if local {
221            format!(
222                "bootstrap_cache_local_{}.json",
223                crate::get_network_version()
224            )
225        } else {
226            format!("bootstrap_cache_{}.json", crate::get_network_version())
227        }
228    }
229}