cuprate_address_book/
store.rs

1#![expect(
2    single_use_lifetimes,
3    reason = "false positive on generated derive code on `SerPeerDataV1`"
4)]
5
6use std::fs;
7
8use borsh::{from_slice, to_vec, BorshDeserialize, BorshSerialize};
9use tokio::task::{spawn_blocking, JoinHandle};
10
11use cuprate_p2p_core::{services::ZoneSpecificPeerListEntryBase, NetZoneAddress};
12
13use crate::{peer_list::PeerList, AddressBookConfig, BorshNetworkZone};
14
15// TODO: store anchor and ban list.
16
17#[derive(BorshSerialize)]
18struct SerPeerDataV1<'a, A: NetZoneAddress> {
19    white_list: Vec<&'a ZoneSpecificPeerListEntryBase<A>>,
20    gray_list: Vec<&'a ZoneSpecificPeerListEntryBase<A>>,
21}
22
23#[derive(BorshDeserialize)]
24struct DeserPeerDataV1<A: NetZoneAddress> {
25    white_list: Vec<ZoneSpecificPeerListEntryBase<A>>,
26    gray_list: Vec<ZoneSpecificPeerListEntryBase<A>>,
27}
28
29pub(crate) fn save_peers_to_disk<Z: BorshNetworkZone>(
30    cfg: &AddressBookConfig,
31    white_list: &PeerList<Z>,
32    gray_list: &PeerList<Z>,
33) -> JoinHandle<std::io::Result<()>> {
34    // maybe move this to another thread but that would require cloning the data ... this
35    // happens so infrequently that it's probably not worth it.
36    let data = to_vec(&SerPeerDataV1 {
37        white_list: white_list.peers.values().collect::<Vec<_>>(),
38        gray_list: gray_list.peers.values().collect::<Vec<_>>(),
39    })
40    .unwrap();
41
42    let dir = cfg.peer_store_directory.clone();
43    let file = dir.join(Z::NAME);
44    let mut tmp_file = file.clone();
45    tmp_file.set_extension("tmp");
46
47    spawn_blocking(move || {
48        fs::create_dir_all(dir)?;
49        fs::write(&tmp_file, &data).and_then(|()| fs::rename(tmp_file, file))
50    })
51}
52
53pub(crate) async fn read_peers_from_disk<Z: BorshNetworkZone>(
54    cfg: &AddressBookConfig,
55) -> Result<
56    (
57        Vec<ZoneSpecificPeerListEntryBase<Z::Addr>>,
58        Vec<ZoneSpecificPeerListEntryBase<Z::Addr>>,
59    ),
60    std::io::Error,
61> {
62    let file = cfg.peer_store_directory.join(Z::NAME);
63
64    tracing::info!("Loading peers from file: {} ", file.display());
65
66    let data = spawn_blocking(move || fs::read(file)).await.unwrap()?;
67
68    let de_ser: DeserPeerDataV1<Z::Addr> = from_slice(&data)?;
69    Ok((de_ser.white_list, de_ser.gray_list))
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use crate::peer_list::{tests::make_fake_peer_list, PeerList};
76
77    use cuprate_test_utils::test_netzone::{TestNetZone, TestNetZoneAddr};
78
79    #[test]
80    fn ser_deser_peer_list() {
81        let white_list = make_fake_peer_list(0, 50);
82        let gray_list = make_fake_peer_list(50, 100);
83
84        let data = to_vec(&SerPeerDataV1 {
85            white_list: white_list.peers.values().collect::<Vec<_>>(),
86            gray_list: gray_list.peers.values().collect::<Vec<_>>(),
87        })
88        .unwrap();
89
90        let de_ser: DeserPeerDataV1<TestNetZoneAddr> = from_slice(&data).unwrap();
91
92        let white_list_2: PeerList<TestNetZone<true>> = PeerList::new(de_ser.white_list);
93        let gray_list_2: PeerList<TestNetZone<true>> = PeerList::new(de_ser.gray_list);
94
95        assert_eq!(white_list.peers.len(), white_list_2.peers.len());
96        assert_eq!(gray_list.peers.len(), gray_list_2.peers.len());
97
98        for addr in white_list.peers.keys() {
99            assert!(white_list_2.contains_peer(addr));
100        }
101
102        for addr in gray_list.peers.keys() {
103            assert!(gray_list_2.contains_peer(addr));
104        }
105    }
106}