Skip to main content

hashiverse_lib/transport/
mem_transport.rs

1//! # In-memory transport for tests
2//!
3//! A fully synchronous in-process implementation of
4//! [`crate::transport::transport::TransportFactory`] and
5//! [`crate::transport::transport::TransportServer`]: every "server" registers itself in
6//! a shared registry keyed by id, and every "client" request is just a channel send into
7//! the matching server's request queue.
8//!
9//! This is what makes the integration-test harness fast and deterministic. A virtual
10//! network of dozens of servers + clients runs inside a single test binary, with no
11//! sockets, no TLS negotiation, no PoW relaxation fudge, and no flaky wall-clock
12//! ordering. Swap `MemTransportFactory` for the HTTPS factory and the same protocol code
13//! runs on the real network.
14
15use crate::tools::types::Id;
16use crate::transport::ddos::ddos::{DdosConnectionGuard, DdosProtection};
17use crate::transport::transport::{IncomingRequest, ServerState, TransportFactory, TransportServer};
18use crate::transport::transport_ownership_proof::{EmptyMarkerOwnershipProof, TransportOwnershipProof};
19use anyhow::{Result, anyhow};
20use bytes::Bytes;
21use log::info;
22use parking_lot::RwLock;
23use std::collections::HashMap;
24use crate::tools::time::DurationMillis;
25use crate::tools::time_provider::time_provider::TimeProvider;
26use std::sync::Arc;
27use tokio::sync::{mpsc, oneshot};
28use tokio_util::sync::CancellationToken;
29use tokio_util::task::TaskTracker;
30use crate::transport::bootstrap_provider::bootstrap_provider::BootstrapProvider;
31use crate::transport::bootstrap_provider::manual_bootstrap_provider::ManualBootstrapProvider;
32use crate::transport::ddos::noop_ddos::NoopDdosProtection;
33
34#[derive(Debug)]
35struct RpcMessage {
36    caller_address: String,
37    bytes: Bytes,
38    response_tx: oneshot::Sender<Result<Bytes>>,
39}
40
41struct ServerEntry {
42    command_tx: mpsc::Sender<RpcMessage>,
43}
44
45struct ServerManager {
46    servers: Arc<RwLock<HashMap<u16, Arc<ServerEntry>>>>,
47}
48
49impl ServerManager {
50    pub fn new() -> Self {
51        ServerManager {
52            servers: Arc::new(RwLock::new(HashMap::new())),
53        }
54    }
55    pub async fn remove_server(&self, port: u16) {
56        let mut servers_locked = self.servers.write();
57        servers_locked.remove(&port);
58    }
59}
60
61/// An entirely in-process [`TransportServer`] used by the integration test harness.
62///
63/// Servers created by `MemTransportFactory` share a process-wide registry keyed by port;
64/// "sending a request" from one client to one server becomes a channel send on the registry.
65/// There is no serialization to sockets, no DNS, no kernel — which makes this both
66/// dramatically faster than a real network and fully deterministic when paired with a virtual
67/// [`crate::tools::time_provider::time_provider::TimeProvider`]. Port `0` is translated to a
68/// freshly-allocated port number, mirroring the semantics of a real OS bind.
69///
70/// Not for production use: there is nothing here that crosses a process or host boundary.
71pub struct MemTransportServer {
72    port: u16,
73    address: String,
74    server_manager: Arc<ServerManager>,
75    command_rx: Arc<RwLock<Option<mpsc::Receiver<RpcMessage>>>>,
76    state: Arc<RwLock<ServerState>>,
77    ddos_protection: Arc<dyn DdosProtection>,
78}
79
80#[async_trait::async_trait]
81impl TransportServer for MemTransportServer {
82    fn get_address(&self) -> &String {
83        &self.address
84    }
85
86    fn get_transport_ownership_proof(&self) -> Arc<dyn TransportOwnershipProof> {
87        Arc::new(EmptyMarkerOwnershipProof)
88    }
89
90    async fn listen(&self, cancellation_token: CancellationToken, handler: mpsc::Sender<IncomingRequest>) -> Result<()> {
91        async fn process_connection(_cancellation_token: CancellationToken, handler: mpsc::Sender<IncomingRequest>, message: RpcMessage, ddos_protection: Arc<dyn DdosProtection>) -> anyhow::Result<()> {
92            // trace!("accepted connection");
93            // scopeguard::defer! { trace!("dropped connection"); }
94            // trace!("received packet={:?}", message.bytes);
95            let ddos_connection_guard = match DdosConnectionGuard::try_new(ddos_protection, message.caller_address.as_str()) {
96                Some(guard) => Arc::new(guard),
97                None => return Ok(()),
98            };
99            let caller_address = ddos_connection_guard.ip().to_string();
100            let (reply_tx, reply_rx) = oneshot::channel();
101            handler.send(IncomingRequest::new(caller_address, message.bytes, reply_tx, ddos_connection_guard)).await?;
102            let response = reply_rx.await?;
103            let _ = message.response_tx.send(Ok(response.to_bytes()));
104
105            Ok(())
106        }
107
108        // Check that we can transition to listening
109        {
110            let mut state = self.state.write();
111            match *state {
112                ServerState::Listening => {
113                    anyhow::bail!("server is already listening");
114                }
115                ServerState::Shutdown => {
116                    anyhow::bail!("server has been shut down");
117                }
118                ServerState::Created => {
119                    *state = ServerState::Listening;
120                }
121            }
122        }
123
124        let task_tracker = TaskTracker::new();
125
126        info!("listening on address {}", self.address);
127
128        // Take ownership of the receiver.  If there's no receiver, we can't listen.  Should never happen!
129        let mut receiver = match self.command_rx.write().take() {
130            Some(r) => r,
131            None => {
132                return Err(anyhow!("no receiver available on address {}", self.address));
133            }
134        };
135
136        loop {
137            tokio::select! {
138                _ = cancellation_token.cancelled() => {
139                    break;
140                }
141
142                Some(msg) = receiver.recv() => {
143                    task_tracker.spawn(
144                        process_connection(cancellation_token.clone(), handler.clone(), msg, self.ddos_protection.clone())
145                    );
146                }
147            }
148        }
149
150        info!("stopped listening on port {}", self.address);
151        self.server_manager.remove_server(self.port).await;
152
153        // Wait for existing connections to complete
154        info!("waiting for open connections to complete");
155        task_tracker.close();
156        task_tracker.wait().await;
157
158        // Notify the "shutdown" coroutine that we have successfully shutdown
159        info!("all open connections complete");
160        *self.state.write() = ServerState::Shutdown;
161
162        Ok(())
163    }
164}
165
166#[derive(Clone)]
167pub struct MemTransportFactory {
168    server_manager: Arc<ServerManager>,
169    ddos_protection: Arc<dyn DdosProtection>,
170    bootstrap_provider: Arc<dyn BootstrapProvider>,
171    /// Simulated latency added to every `rpc` call, slept on the given clock.  Shared across clones of this factory.
172    rpc_delay: Arc<RwLock<Option<(Arc<dyn TimeProvider>, DurationMillis)>>>,
173}
174
175impl MemTransportFactory {
176    pub fn new(ddos_protection: Arc<dyn DdosProtection>, bootstrap_provider: Arc<dyn BootstrapProvider>) -> Self {
177        Self {
178            server_manager: Arc::new(ServerManager::new()),
179            ddos_protection,
180            bootstrap_provider,
181            rpc_delay: Arc::new(RwLock::new(None)),
182        }
183    }
184
185    /// Simulates network latency: every subsequent `rpc` call sleeps `rpc_delay` on `time_provider` before reaching the server,
186    /// so it scales with the test's clock.  Lets tests tell RPCs that overlap from RPCs that run one after another.
187    pub fn set_rpc_delay(&self, time_provider: Arc<dyn TimeProvider>, rpc_delay: DurationMillis) {
188        *self.rpc_delay.write() = Some((time_provider, rpc_delay));
189    }
190
191    pub fn clear_rpc_delay(&self) {
192        *self.rpc_delay.write() = None;
193    }
194
195    #[allow(clippy::should_implement_trait)] // wraps Arc<Self>, can't satisfy the Default trait
196    pub fn default() -> Arc<Self> {
197        Arc::new(Self::new(NoopDdosProtection::default(), ManualBootstrapProvider::new_mem_multiple()))
198    }
199}
200
201#[async_trait::async_trait]
202impl TransportFactory for MemTransportFactory {
203    async fn get_bootstrap_addresses(&self) -> Vec<String> {
204        self.bootstrap_provider.get_bootstrap_addresses().await
205    }
206
207    async fn create_server(&self, _base_path: &str, port: u16, force_local_network: bool) -> anyhow::Result<Arc<dyn TransportServer>> {
208        if !force_local_network {
209            return Err(anyhow!("only local network is supported"));
210        }
211
212        let mut servers_locked = self.server_manager.servers.write();
213
214        if servers_locked.contains_key(&port) {
215            return Err(anyhow!("server already exists on port {}", port));
216        }
217
218        // If they have requested port 0, pick the first available empty slot
219        let bound_port = match port {
220            0 => {
221                servers_locked.keys().max().unwrap_or(&0u16) + 1
222            }
223            _ => port
224        };
225
226        let address = format!("{}", bound_port);
227
228        // Create channels for communication.  Buffer sized generously so bursts of
229        // concurrent in-memory RPCs don't trip capacity limits; backpressure is still
230        // applied via awaited `send` below, which is closer to the behaviour of a real
231        // TCP socket than `try_send`'s fail-fast.
232        let (tx, rx) = mpsc::channel::<RpcMessage>(256);
233
234        // Create the server
235        let mem_transport_server = Arc::new(MemTransportServer {
236            port: bound_port,
237            address,
238            server_manager: self.server_manager.clone(),
239            command_rx: Arc::new(RwLock::new(Some(rx))),
240            state: Arc::new(RwLock::new(ServerState::Created)),
241            ddos_protection: self.ddos_protection.clone(),
242        });
243
244        // Store the server and its sender in the map
245        servers_locked.insert(bound_port, Arc::new(ServerEntry { command_tx: tx }));
246
247        Ok(mem_transport_server)
248    }
249
250    async fn rpc(&self, address: &str, bytes: Bytes) -> Result<Bytes> {
251        let rpc_delay = self.rpc_delay.read().clone();
252        if let Some((time_provider, rpc_delay)) = rpc_delay {
253            time_provider.sleep_millis(rpc_delay).await;
254        }
255
256        let port: u16 = address.parse()?;
257
258        let server_entry = {
259            let servers = self.server_manager.servers.read();
260            let server_entry = servers.get(&port).ok_or_else(|| anyhow::anyhow!("no server found with port {}", port))?;
261            server_entry.clone()
262        };
263
264        // trace!("connected to: {:?}", address);
265        // defer! { trace!("disconnected from: {:?}", &address); }
266
267        // Create a oneshot channel for the response
268        let (response_tx, response_rx) = oneshot::channel();
269
270        // Create the message
271        let message = RpcMessage { caller_address: format!("mem:{}", Id::random()), bytes, response_tx };
272
273        // Send the message to the server using the sender from the server entry.
274        // Awaited `send` applies backpressure if the receiver is saturated, rather than
275        // dropping the request — mirrors how a real TCP transport would behave.
276        server_entry.command_tx.send(message).await.map_err(|e| anyhow::anyhow!("failed to send request: {}", e))?;
277
278        // Wait for the response
279        response_rx.await.map_err(|_| anyhow::anyhow!("server disconnected before responding"))?
280    }
281}
282
283
284#[cfg(test)]
285mod tests {
286    use crate::transport::mem_transport::MemTransportFactory;
287    use crate::transport::bootstrap_provider::manual_bootstrap_provider::ManualBootstrapProvider;
288    use crate::transport::ddos::noop_ddos::NoopDdosProtection;
289    use std::sync::Arc;
290
291    #[tokio::test]
292    async fn rpc_test() -> anyhow::Result<()> {
293        let factory: Arc<dyn crate::transport::transport::TransportFactory> = Arc::new(MemTransportFactory::new(NoopDdosProtection::default(), ManualBootstrapProvider::default()));
294        crate::transport::transport::tests::rpc_test(factory).await
295    }
296
297    #[tokio::test]
298    async fn bind_port_zero_test() -> anyhow::Result<()> {
299        let factory: Arc<dyn crate::transport::transport::TransportFactory> = Arc::new(MemTransportFactory::new(NoopDdosProtection::default(), ManualBootstrapProvider::default()));
300        crate::transport::transport::tests::bind_port_zero_test(factory).await
301    }
302
303    /// The delay is slept on the supplied (here scaled) clock, applies to every call, is shared by clones of the factory,
304    /// and delays concurrent calls independently.  (An RPC to a port with no server still pays the delay first, so no server is needed.)
305    #[tokio::test]
306    async fn rpc_delay_is_per_call_concurrent_and_on_the_given_clock() -> anyhow::Result<()> {
307        use crate::tools::time::MILLIS_IN_SECOND;
308        use crate::tools::time_provider::time_provider::{ScaledTimeProvider, TimeProvider};
309        use crate::transport::transport::TransportFactory;
310        use bytes::Bytes;
311
312        // 5 scaled seconds per rpc = 500ms of real time at 10x: each assertion's pass/fail boundary is a whole delay
313        // (500ms of real time) away from the expected timing, which leaves headroom on a loaded CI runner
314        let time_provider = Arc::new(ScaledTimeProvider::new(10.0));
315        let rpc_delay = MILLIS_IN_SECOND.const_mul(5);
316        const CONCURRENT_RPCS: usize = 5;
317
318        let factory = MemTransportFactory::new(NoopDdosProtection::default(), ManualBootstrapProvider::default());
319        let factory_clone = factory.clone();
320
321        // No delay by default
322        let started = time_provider.current_time_millis();
323        assert!(factory.rpc("65000", Bytes::new()).await.is_err());
324        assert!(time_provider.current_time_millis() - started < rpc_delay);
325
326        // Setting it on a clone affects the original, and the delay elapses on the scaled clock
327        factory_clone.set_rpc_delay(time_provider.clone(), rpc_delay);
328        let started = time_provider.current_time_millis();
329        assert!(factory.rpc("65000", Bytes::new()).await.is_err());
330        assert!(time_provider.current_time_millis() - started >= rpc_delay);
331
332        // Concurrent calls overlap: the batch takes about one delay, not CONCURRENT_RPCS of them
333        let started = time_provider.current_time_millis();
334        let results = futures::future::join_all((0..CONCURRENT_RPCS).map(|_| factory.rpc("65000", Bytes::new()))).await;
335        assert!(results.iter().all(|result| result.is_err()));
336        let elapsed = time_provider.current_time_millis() - started;
337        assert!(elapsed >= rpc_delay);
338        assert!(elapsed < rpc_delay.const_mul(2), "concurrent delayed rpcs must overlap, took {}", elapsed);
339
340        // And it can be switched off again
341        factory.clear_rpc_delay();
342        let started = time_provider.current_time_millis();
343        assert!(factory.rpc("65000", Bytes::new()).await.is_err());
344        assert!(time_provider.current_time_millis() - started < rpc_delay);
345        Ok(())
346    }
347}