Skip to main content

hashiverse_server_lib/server/
hashiverse_server.rs

1//! # Top-level server orchestrator
2//!
3//! [`HashiverseServer`] is the server-binary analogue of
4//! [`hashiverse_lib::client::hashiverse_client::HashiverseClient`]: the single struct
5//! that wires together every subsystem the node needs to operate and hands them to
6//! the inbound-request handler.
7//!
8//! What it owns:
9//!
10//! - **Identity** — a persisted [`hashiverse_lib::tools::server_id::ServerId`] is
11//!   loaded from the [`crate::environment::environment::Environment`] or minted
12//!   fresh with proof-of-work on first start.
13//! - **Transport** — built via a `TransportFactory` (TLS in production, plain TCP
14//!   or in-memory in tests) and bound to the port from
15//!   [`crate::server::args::Args`].
16//! - **DHT** — a [`crate::server::kademlia::kademlia::Kademlia`] populated from the
17//!   persisted peer buckets at startup and kept up to date by the handler dispatch.
18//! - **Caches** — [`crate::server::post_bundle_caching`] and
19//!   [`crate::server::post_bundle_feedback_caching`] plus per-connection reply-salt
20//!   and heal caches, all backed by `moka` with TTL/TTI eviction.
21//! - **Replay protection** — a short-window set of observed request salts so an
22//!   attacker can't replay a valid signed request back at us.
23
24use crate::environment::environment::{Environment, EnvironmentDimensions, EnvironmentFactory, CONFIG_KADEMLIA_PEER_BUCKETS, CONFIG_SERVER_ID};
25use crate::server::kademlia::kademlia;
26use crate::server::kademlia::kademlia::Kademlia;
27use crate::server::post_bundle_caching::PostBundleCache;
28use crate::server::post_bundle_feedback_caching::PostBundleFeedbackCache;
29use crate::server::stats::RequestRateWindows;
30use hashiverse_lib::anyhow_assert_eq;
31use hashiverse_lib::protocol::payload::payload::{AnnounceResponseV2, AnnounceV2, BootstrapResponseV1, BootstrapV1, PayloadRequestKind, PayloadResponseKind, PeerStatsResponseV1, PAYLOAD_REQUEST_KIND_COUNT};
32use hashiverse_lib::protocol::peer::Peer;
33use hashiverse_lib::protocol::rpc;
34use hashiverse_lib::tools::runtime_services::RuntimeServices;
35use hashiverse_lib::tools::server_id::ServerId;
36use hashiverse_lib::tools::time::{TimeMillis, MILLIS_IN_MINUTE, MILLIS_IN_SECOND};
37use hashiverse_lib::tools::time_provider::moka_clock::TimeProviderMokaClock;
38use hashiverse_lib::tools::time_provider::time_provider::TimeProvider;
39use hashiverse_lib::tools::types::{Id, Salt};
40use hashiverse_lib::tools::{config, tools};
41use hashiverse_lib::tools::json;
42use hashiverse_lib::transport::transport::{IncomingRequest, TransportServer};
43use log::{error, info, trace, warn};
44use moka::sync::Cache;
45use parking_lot::{Mutex, RwLock};
46use std::sync::Arc;
47use std::sync::atomic::AtomicU64;
48use std::time::Duration;
49use bytes::Bytes;
50use tokio::sync::mpsc;
51use tokio_util::sync::CancellationToken;
52use hashiverse_lib::protocol::rpc::rpc_response::RpcResponsePacketRx;
53use hashiverse_lib::tools::hyper_log_log::HyperLogLog;
54use hashiverse_lib::protocol::payload::payload::TrendingHashtagsFetchResponseV1;
55use crate::server::args::Args;
56
57pub struct HashiverseServer {
58    pub runtime_services: Arc<RuntimeServices>,
59    pub environment: Arc<Environment>,
60    pub server_id: ServerId,
61    pub kademlia: Arc<RwLock<Kademlia<Id, Peer>>>,
62    pub transport_server: Arc<dyn TransportServer>,
63    pub peer_self: Arc<RwLock<Peer>>,
64    pub heal_in_progress: Cache<Id, ()>,
65    pub seen_salts: Cache<Salt, ()>,
66    pub post_bundle_cache: PostBundleCache,
67    pub post_bundle_feedback_cache: PostBundleFeedbackCache,
68    pub trending_hashtags: Cache<String, HyperLogLog>,
69    pub trending_hashtags_response_cache: Mutex<Option<(TimeMillis, TrendingHashtagsFetchResponseV1)>>,
70    /// Per-`PayloadRequestKind` running counter of inbound dispatches. Incremented
71    /// after packet decode + replay-guard but before per-handler PoW gates, so
72    /// adversarial load shows up too.
73    pub request_counters: Arc<[AtomicU64; PAYLOAD_REQUEST_KIND_COUNT]>,
74    /// Per-`PayloadRequestKind` decaying call-rate estimates (hour/day/month).
75    /// Recorded alongside `request_counters` on the same hot path; rendered next
76    /// to the all-time total so idle servers no longer look busy. Each kind has
77    /// its own `Mutex` so a single-kind flood contends only with itself.
78    pub request_rate_windows: Arc<[Mutex<RequestRateWindows>; PAYLOAD_REQUEST_KIND_COUNT]>,
79    /// Cached signed stats blob. The `TimeMillis` is the timestamp recorded in
80    /// the cached `PeerStatsResponseV1` itself — the cache hands the response
81    /// back verbatim so clients re-sharing it get a single canonical byte sequence.
82    pub peer_stats_response_cache: Mutex<Option<(TimeMillis, PeerStatsResponseV1)>>,
83}
84
85impl HashiverseServer {
86    pub async fn new(runtime_services: Arc<RuntimeServices>, environment_factory: Arc<dyn EnvironmentFactory>, args: Args) -> anyhow::Result<Arc<Self>> {
87        let environment_dimensions = EnvironmentDimensions::default().with_max_size_bytes(args.max_post_database_size_megabytes * 1024 * 1024); 
88        let environment = environment_factory.open_next_available(environment_dimensions).await?;
89
90        // let passphrase = passphrase::get_passphrase(args.passphrase_path);
91        let config_server_id = environment.config_get_bytes(CONFIG_SERVER_ID)?;
92        let server_id = match config_server_id {
93            None => {
94                let server_id = ServerId::new("own_pow", runtime_services.time_provider.as_ref(), config::SERVER_KEY_POW_MIN, args.skip_pq_commitment_bytes, runtime_services.pow_generator.as_ref()).await?;
95                environment.config_put_bytes(CONFIG_SERVER_ID, server_id.encode()?)?;
96                info!("starting new server with server_id={}", server_id);
97                server_id
98            }
99            Some(config_server_id) => {
100                let server_id = ServerId::decode(config_server_id.as_ref())?;
101                server_id.verify()?;
102                info!("restarting existing server with server_id={}", server_id);
103                server_id
104            }
105        };
106
107        let transport_server = runtime_services.transport_factory.create_server(&args.base_path, args.port, args.force_local_network).await?;
108
109        // Update the address in our Peer record
110        let mut peer_self = server_id.to_peer(runtime_services.time_provider.as_ref())?;
111        peer_self.address = transport_server.get_address().to_string();
112        peer_self.sign(runtime_services.time_provider.as_ref(), &server_id.keys.signature_key)?;
113
114        // Boto our kademlia
115        let mut kademlia = Kademlia::<Id, Peer>::new(server_id.id, config::SERVER_KADEMLIA_MAX_PEERS_PER_BUCKET);
116        {
117            let now = runtime_services.time_provider.current_time_millis();
118
119            // We always belong in our own kademlia
120            kademlia.add_peer(peer_self.clone(), now)?;
121
122            // Have we previously persisted our peer buckets?
123            {
124                let try_result = try {
125                    let peer_buckets = environment.config_get_struct::<Vec<Vec<Peer>>>(CONFIG_KADEMLIA_PEER_BUCKETS)?;
126                    if let Some(peer_buckets) = peer_buckets {
127                        for peer_bucket in peer_buckets {
128                            for peer in peer_bucket {
129                                // Re-check on load: records persisted before timestamp checks existed may be poisoned.
130                                if let Err(e) = peer.verify().and_then(|_| peer.verify_timestamps(now)) {
131                                    warn!("dropping persisted peer {} that failed verification: {}", peer, e);
132                                    continue;
133                                }
134                                kademlia.add_peer(peer, now)?;
135                            }
136                        }
137                    }
138                };
139
140                if let Err(e) = try_result {
141                    warn!("problem depersisting peer_buckets: {}", e);
142                }
143            }
144        }
145
146        info!("server_id={}", server_id);
147        info!("peer_self={}", peer_self);
148
149        // All time-based moka caches below run on our TimeProvider (scaled in tests), not wall time.
150        let time_provider = runtime_services.time_provider.clone();
151
152        let hashiverse_server = HashiverseServer {
153            runtime_services,
154            environment: Arc::new(environment),
155            server_id,
156            kademlia: Arc::new(RwLock::new(kademlia)),
157            transport_server,
158            peer_self: Arc::new(RwLock::new(peer_self)),
159            heal_in_progress: Cache::builder().time_to_live(Duration::from_secs(60)).external_clock(Arc::new(TimeProviderMokaClock::new(time_provider.clone()))).build(),
160            seen_salts: Cache::builder().time_to_live(Duration::from_mins(5)).max_capacity(100_000).external_clock(Arc::new(TimeProviderMokaClock::new(time_provider.clone()))).build(),
161            post_bundle_cache: PostBundleCache::new(config::SERVER_POST_BUNDLE_CACHE_MAX_ORIGINATORS_PER_LOCATION, config::SERVER_POST_BUNDLE_CACHE_MAX_BYTES, time_provider.clone()),
162            post_bundle_feedback_cache: PostBundleFeedbackCache::new(config::SERVER_POST_BUNDLE_FEEDBACK_CACHE_MAX_BYTES, time_provider.clone()),
163            trending_hashtags: Cache::builder().max_capacity(256).build(),
164            trending_hashtags_response_cache: Mutex::new(None),
165            request_counters: Arc::new(std::array::from_fn(|_| AtomicU64::new(0))),
166            request_rate_windows: Arc::new(std::array::from_fn(|_| Mutex::new(RequestRateWindows::new()))),
167            peer_stats_response_cache: Mutex::new(None),
168        };
169
170        Ok(Arc::new(hashiverse_server))
171    }
172
173    pub async fn run(&self, cancellation_token: CancellationToken) {
174        info!("server started");
175
176        let (tx, rx) = mpsc::channel::<IncomingRequest>(32);
177
178        let res = tokio::try_join!(
179            self.wrap_and_dispatch_network_envelopes(cancellation_token.clone(), rx),
180            self.maintain_environment(cancellation_token.clone(), self.runtime_services.time_provider.clone()),
181            self.maintain_kademlia(cancellation_token.clone(), self.runtime_services.time_provider.clone()),
182            self.transport_server.listen(cancellation_token.clone(), tx),
183        );
184
185        match res {
186            Ok(_) => info!("server stopped"),
187            Err(e) => error!("server stopped with error: {}", e),
188        }
189    }
190
191    pub async fn add_potential_peer_to_kademlia(&self, peer: Peer, time_millis: TimeMillis) {
192        // Verify this peer
193        let result = peer.verify().and_then(|_| peer.verify_timestamps(time_millis));
194        if let Err(e) = result {
195            warn!("peer {} failed verification: {}", peer, e);
196            return;
197        }
198
199        // Has this peer done enough work?
200        if peer.pow_initial.pow < config::SERVER_KEY_POW_MIN {
201            warn!("peer {} failed pow so not adding to our kademlia", peer);
202            return;
203        }
204
205        let result = self.kademlia.write().add_peer(peer, time_millis);
206        if let Err(e) = result {
207            warn!("problem adding peer: {}", e);
208        }
209    }
210
211    async fn rpc_server_unknown(&self, address: &str, payload_request_kind: PayloadRequestKind, payload: Bytes) -> anyhow::Result<RpcResponsePacketRx> {
212        rpc::rpc::rpc_server_unknown(&self.runtime_services, &self.server_id.id, address, payload_request_kind, payload).await
213    }
214
215    async fn rpc_server_known(&self, destination_peer: &Peer, payload_request_kind: PayloadRequestKind, payload: Bytes) -> anyhow::Result<RpcResponsePacketRx> {
216        rpc::rpc::rpc_server_known(&self.runtime_services, &self.server_id.id, destination_peer, payload_request_kind, payload).await
217    }
218
219    async fn maintain_environment(&self, cancellation_token: CancellationToken, time_provider: Arc<dyn TimeProvider>) -> Result<(), anyhow::Error> {
220        loop {
221            if cancellation_token.is_cancelled() {
222                break;
223            }
224
225            let time_millis = time_provider.current_time_millis();
226
227            self.environment.do_maintenance(&cancellation_token, time_millis).await?;
228
229            tools::cancellable_sleep_millis(self.runtime_services.time_provider.as_ref(), MILLIS_IN_MINUTE, &cancellation_token).await;
230        }
231
232        Ok(())
233    }
234
235    async fn maintain_kademlia(&self, cancellation_token: CancellationToken, time_provider: Arc<dyn TimeProvider>) -> Result<(), anyhow::Error> {
236        let mut last_bootstrap = TimeMillis::zero();
237        let mut last_announce = TimeMillis::zero();
238        let mut last_peers_dump_to_storage = self.runtime_services.time_provider.current_time_millis();
239
240        loop {
241            if cancellation_token.is_cancelled() {
242                break;
243            }
244
245            let now = time_provider.current_time_millis();
246
247            // Do we need to bootstrap?
248            if now - last_bootstrap > config::MILLIS_TO_WAIT_BETWEEN_BOOTSTRAPS {
249                last_bootstrap = now;
250
251                let needs_bootstrapping = { self.kademlia.read().len() < config::MINIMUM_PEERS_TO_STOP_BOOTSTRAPPING };
252
253                if needs_bootstrapping {
254
255                    // Lets randomize these addresses so that the first one is not snowed
256                    let mut bootstrap_addresses = self.runtime_services.transport_factory.get_bootstrap_addresses().await;
257                    tools::shuffle(&mut bootstrap_addresses);
258                    trace!("bootstrap addresses: {:?}", bootstrap_addresses);
259
260                    for bootstrap_address in bootstrap_addresses {
261                        if cancellation_token.is_cancelled() {
262                            break;
263                        }
264
265                        let try_result: anyhow::Result<()> = try {
266                            {
267                                trace!("bootstrapping {}", bootstrap_address);
268                                let rpc_response_packet_rx = self.rpc_server_unknown(&bootstrap_address, PayloadRequestKind::BootstrapV1, json::struct_to_bytes(&BootstrapV1 {})?).await?;
269                                anyhow_assert_eq!(&PayloadResponseKind::BootstrapResponseV1, &rpc_response_packet_rx.response_request_kind);
270                                let response = json::bytes_to_struct::<BootstrapResponseV1>(&rpc_response_packet_rx.bytes)?;
271                                for peer in response.peers_random {
272                                    self.add_potential_peer_to_kademlia(peer, now).await;
273                                }
274                            }
275                        };
276
277                        if let Err(e) = try_result {
278                            warn!("problem bootstrapping {}: {}", bootstrap_address, e);
279                        }
280
281                        let needs_bootstrapping = { self.kademlia.read().len() < config::MINIMUM_PEERS_TO_STOP_BOOTSTRAPPING };
282                        if !needs_bootstrapping {
283                            break;
284                        }
285                        
286                    }
287
288                    trace!("We now have {} peers", self.kademlia.read().len());
289                }
290            }
291
292            // Do we need to announce?
293            if now - last_announce > config::MILLIS_TO_WAIT_BETWEEN_ANNOUNCES {
294                last_announce = now;
295
296                let peer_self = self.peer_self.read().clone();
297
298
299                // Pick some candidates who we will poke
300                let mut announce_peers = Vec::<Peer>::new();
301                {
302                    let kademlia = self.kademlia.read();
303
304                    // One that is potentially dead
305                    let peer_with_lowest_score = kademlia.get_peer_with_lowest_score();
306                    if let Some(peer_with_lowest_score) = peer_with_lowest_score {
307                        announce_peers.push(peer_with_lowest_score.clone());
308                    }
309
310                    // Someone in our vicinity so our vicinity becomes more tightly knit
311                    let (peers_nearest, _) = kademlia.get_peers_for_key(&peer_self.id, 8);
312                    if !peers_nearest.is_empty() {
313                        announce_peers.push(tools::random_element(&peers_nearest).clone());
314                    }
315                }
316
317                // Pick the peer that we havent heard from for the longest
318                for announce_peer in announce_peers {
319                    if cancellation_token.is_cancelled() {
320                        break;
321                    }
322
323                    // If we have to announce to ourself, then simply push our own freshest peer
324                    if announce_peer == peer_self {
325                        // trace!("We are own own oldest peer!");
326                        self.add_potential_peer_to_kademlia(peer_self.clone(), now).await;
327                        continue;
328                    }
329
330                    // V2-first outbound. The transport ownership proof carries our current
331                    // ACME chain (HTTPS) or an empty marker (mem / TCP). If we don't have a
332                    // proof to send yet — e.g. ACME hasn't completed first issuance — we skip
333                    // this announce tick; the next tick will pick up the chain as soon as the
334                    // cert refresher loads it (see HttpsTransportOwnershipProof's cache + the
335                    // RwLock pull pattern).
336                    let proof_payload = match self.transport_server.get_transport_ownership_proof().make_ownership_proof_payload() {
337                        Some(p) => p,
338                        None => {
339                            trace!("skipping announce to {}: no transport ownership proof available yet", announce_peer);
340                            continue;
341                        }
342                    };
343
344                    let try_result: anyhow::Result<()> = try {
345                        // Any failure — peer unreachable, our ownership proof didn't validate at their end,
346                        // a malformed response — prunes the peer below.
347                        let rpc_response_packet_rx = self.rpc_server_known(
348                            &announce_peer,
349                            PayloadRequestKind::AnnounceV2,
350                            json::struct_to_bytes(&AnnounceV2 { peer_self: peer_self.clone(), proof_payload: proof_payload.to_vec() })?,
351                        ).await?;
352                        anyhow_assert_eq!(&PayloadResponseKind::AnnounceResponseV2, &rpc_response_packet_rx.response_request_kind);
353                        let response = json::bytes_to_struct::<AnnounceResponseV2>(&rpc_response_packet_rx.bytes)?;
354                        self.add_potential_peer_to_kademlia(response.peer_self, now).await;
355                        for peer in response.peers_nearest {
356                            self.add_potential_peer_to_kademlia(peer, now).await;
357                        }
358                    };
359
360                    if let Err(e) = try_result {
361                        warn!("problem announcing {}: {}", announce_peer, e);
362                        self.kademlia.write().remove_peer(&announce_peer.id, now);
363                    }
364                }
365            }
366
367            // Do we need to persist our peer buckets?
368            {
369                let try_result = try {
370                    let kademlia = self.kademlia.read();
371                    if last_peers_dump_to_storage < kademlia.peers_last_changed() && now - last_peers_dump_to_storage > config::MILLIS_TO_WAIT_BETWEEN_PEER_DUMPS {
372                        last_peers_dump_to_storage = kademlia.peers_last_changed();
373                        let peer_buckets = kademlia.get_peer_buckets();
374                        //let total_peers: usize = peer_buckets.iter().map(|peers| peers.len()).sum();
375                        // trace!("persisting peer_buckets of length={}", total_peers);
376                        self.environment.config_put_struct(CONFIG_KADEMLIA_PEER_BUCKETS, &peer_buckets)?;
377                    }
378                };
379
380                if let Err(e) = try_result {
381                    warn!("problem persisting peer_buckets: {}", e);
382                }
383            }
384
385            tools::cancellable_sleep_millis(self.runtime_services.time_provider.as_ref(), MILLIS_IN_SECOND.const_mul(30), &cancellation_token).await;
386        }
387
388        Ok(())
389    }
390}
391
392impl kademlia::Peer<Id> for Peer {
393    fn id(&self) -> &Id {
394        &self.id
395    }
396    fn score(&self, time_millis: TimeMillis) -> f64 {
397        Peer::score(self, time_millis)
398    }
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404    use crate::environment::mem_environment_store::MemEnvironmentFactory;
405    use hashiverse_lib::protocol::peer::PeerPow;
406    use hashiverse_lib::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
407    use hashiverse_lib::tools::time::MILLIS_IN_DAY;
408    use hashiverse_lib::tools::time_provider::time_provider::RealTimeProvider;
409    use hashiverse_lib::tools::types::{Hash, Pow};
410    use hashiverse_lib::transport::mem_transport::MemTransportFactory;
411
412    /// 2100-01-01T00:00:00Z
413    const YEAR_2100_MILLIS: TimeMillis = TimeMillis(4_102_444_800_000);
414
415    async fn make_server() -> anyhow::Result<Arc<HashiverseServer>> {
416        let runtime_services = Arc::new(RuntimeServices { time_provider: Arc::new(RealTimeProvider), transport_factory: MemTransportFactory::default(), pow_generator: Arc::new(SingleThreadedPowGenerator::new()) });
417        HashiverseServer::new(runtime_services, Arc::new(MemEnvironmentFactory::new("")), Args::default_for_testing()).await
418    }
419
420    async fn make_other_server_id() -> anyhow::Result<ServerId> {
421        ServerId::new("other_server", &RealTimeProvider, config::SERVER_KEY_POW_MIN, true, &SingleThreadedPowGenerator::new()).await
422    }
423
424    fn kademlia_contains(server: &HashiverseServer, id: &Id) -> bool {
425        server.kademlia.read().get_all_peers().iter().any(|peer| peer.id == *id)
426    }
427
428    #[tokio::test]
429    async fn honest_peer_is_added_to_kademlia() -> anyhow::Result<()> {
430        let server = make_server().await?;
431        let other_server_id = make_other_server_id().await?;
432        let honest_peer = other_server_id.to_peer(&RealTimeProvider)?;
433
434        server.add_potential_peer_to_kademlia(honest_peer, RealTimeProvider.current_time_millis()).await;
435        assert!(kademlia_contains(&server, &other_server_id.id));
436        Ok(())
437    }
438
439    // Regression for C4: a correctly signed record whose pow_current_day is self-dated to 2100 passes
440    // Peer::verify (the pow genuinely matches its inputs) but must not get into the routing table.
441    #[tokio::test]
442    async fn future_dated_peer_is_not_added_to_kademlia() -> anyhow::Result<()> {
443        let server = make_server().await?;
444        let other_server_id = make_other_server_id().await?;
445        let mut future_dated_peer = other_server_id.to_peer(&RealTimeProvider)?;
446        future_dated_peer.pow_current_day = PeerPow::new(other_server_id.id, &future_dated_peer.verification_key_bytes, &future_dated_peer.pq_commitment_bytes, YEAR_2100_MILLIS, Hash::random(), Salt::random())?;
447        future_dated_peer.sign(&RealTimeProvider, &other_server_id.keys.signature_key)?;
448        future_dated_peer.verify()?; // The signature/pow checks alone are satisfied — this is the gap
449
450        server.add_potential_peer_to_kademlia(future_dated_peer, RealTimeProvider.current_time_millis()).await;
451        assert!(!kademlia_contains(&server, &other_server_id.id), "future-dated peer must be rejected");
452        Ok(())
453    }
454
455    #[test]
456    fn future_dated_peer_cannot_outscore_honest_peer() {
457        let now = RealTimeProvider.current_time_millis();
458        let mut future_dated_peer = Peer::zero();
459        future_dated_peer.pow_current_day = PeerPow { timestamp: YEAR_2100_MILLIS, pow: Pow(1), ..PeerPow::zero() };
460        future_dated_peer.pow_current_month = PeerPow { timestamp: YEAR_2100_MILLIS, pow: Pow(0), ..PeerPow::zero() };
461
462        let mut honest_peer = Peer::zero();
463        honest_peer.pow_current_day = PeerPow { timestamp: now - MILLIS_IN_DAY, pow: Pow(20), ..PeerPow::zero() };
464        honest_peer.pow_current_month = PeerPow { timestamp: now - MILLIS_IN_DAY, pow: Pow(20), ..PeerPow::zero() };
465
466        let future_dated_score = future_dated_peer.score(now);
467        assert!(future_dated_score.is_finite(), "score={}", future_dated_score);
468        assert!(future_dated_score < honest_peer.score(now), "future_dated_score={} honest_score={}", future_dated_score, honest_peer.score(now));
469    }
470}