1#![cfg(not(target_arch = "wasm32"))]
13
14use crate::p2p::sync_ops::{
15 SyncOpCodec, SyncOpRelay, SyncOpRequest, SyncOpResponse, SYNC_OP_PROTOCOL,
16};
17use libp2p::futures::StreamExt;
18use libp2p::request_response::{self, OutboundRequestId, ProtocolSupport};
19use libp2p::swarm::{NetworkBehaviour, SwarmEvent};
20use libp2p::{Multiaddr, PeerId, StreamProtocol, Swarm};
21use std::collections::HashMap;
22use tokio::sync::{mpsc, oneshot};
23
24#[derive(NetworkBehaviour)]
25struct SyncBehaviour {
26 rr: request_response::Behaviour<SyncOpCodec>,
27}
28
29enum Cmd {
30 Listen(Multiaddr, oneshot::Sender<Result<Multiaddr, String>>),
31 AddPeer(PeerId, Multiaddr),
32 Publish(PeerId, Vec<Vec<u8>>, oneshot::Sender<Result<u64, String>>),
33 Pull(
34 PeerId,
35 u64,
36 oneshot::Sender<Result<(Vec<Vec<u8>>, u64), String>>,
37 ),
38}
39
40enum Pending {
41 Publish(oneshot::Sender<Result<u64, String>>),
42 Pull(oneshot::Sender<Result<(Vec<Vec<u8>>, u64), String>>),
43}
44
45pub struct Libp2pSyncNode {
47 pub peer_id: PeerId,
48 cmd_tx: mpsc::UnboundedSender<Cmd>,
49}
50
51impl Libp2pSyncNode {
52 pub fn spawn(relay: SyncOpRelay) -> Self {
54 let key = libp2p::identity::Keypair::generate_ed25519();
55 let peer_id = PeerId::from(key.public());
56 let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
57
58 let swarm = libp2p::SwarmBuilder::with_existing_identity(key)
59 .with_tokio()
60 .with_tcp(
61 libp2p::tcp::Config::default(),
62 libp2p::noise::Config::new,
63 libp2p::yamux::Config::default,
64 )
65 .expect("tcp transport")
66 .with_behaviour(|_| SyncBehaviour {
67 rr: request_response::Behaviour::new(
68 [(StreamProtocol::new(SYNC_OP_PROTOCOL), ProtocolSupport::Full)],
69 request_response::Config::default(),
70 ),
71 })
72 .expect("behaviour")
73 .with_swarm_config(|c| {
74 c.with_idle_connection_timeout(std::time::Duration::from_secs(60))
75 })
76 .build();
77
78 tokio::spawn(event_loop(swarm, relay, cmd_rx));
79 Self { peer_id, cmd_tx }
80 }
81
82 async fn call<R>(&self, make: impl FnOnce(oneshot::Sender<R>) -> Cmd) -> Result<R, String> {
83 let (tx, rx) = oneshot::channel();
84 self.cmd_tx
85 .send(make(tx))
86 .map_err(|_| "sync node stopped".to_string())?;
87 rx.await
88 .map_err(|_| "sync node dropped the response".to_string())
89 }
90
91 pub async fn listen(&self, addr: &str) -> Result<Multiaddr, String> {
93 let a: Multiaddr = addr.parse().map_err(|e| format!("bad multiaddr: {e}"))?;
94 self.call(|tx| Cmd::Listen(a, tx)).await.and_then(|r| r)
95 }
96
97 pub fn add_peer(&self, peer: PeerId, addr: Multiaddr) {
99 let _ = self.cmd_tx.send(Cmd::AddPeer(peer, addr));
100 }
101
102 pub async fn publish(&self, peer: PeerId, frames: Vec<Vec<u8>>) -> Result<u64, String> {
104 self.call(|tx| Cmd::Publish(peer, frames, tx))
105 .await
106 .and_then(|r| r)
107 }
108
109 pub async fn pull(&self, peer: PeerId, since: u64) -> Result<(Vec<Vec<u8>>, u64), String> {
111 self.call(|tx| Cmd::Pull(peer, since, tx))
112 .await
113 .and_then(|r| r)
114 }
115}
116
117async fn event_loop(
118 mut swarm: Swarm<SyncBehaviour>,
119 relay: SyncOpRelay,
120 mut cmd_rx: mpsc::UnboundedReceiver<Cmd>,
121) {
122 let mut pending: HashMap<OutboundRequestId, Pending> = HashMap::new();
123 let mut pending_listen: Option<oneshot::Sender<Result<Multiaddr, String>>> = None;
124
125 loop {
126 tokio::select! {
127 cmd = cmd_rx.recv() => match cmd {
128 None => break, Some(Cmd::Listen(addr, tx)) => match swarm.listen_on(addr) {
130 Ok(_) => pending_listen = Some(tx),
131 Err(e) => { let _ = tx.send(Err(e.to_string())); }
132 },
133 Some(Cmd::AddPeer(peer, addr)) => {
134 swarm.add_peer_address(peer, addr);
138 }
139 Some(Cmd::Publish(peer, frames, tx)) => {
140 let id = swarm.behaviour_mut().rr.send_request(&peer, SyncOpRequest::Publish { op_frames: frames });
141 pending.insert(id, Pending::Publish(tx));
142 }
143 Some(Cmd::Pull(peer, since, tx)) => {
144 let id = swarm.behaviour_mut().rr.send_request(&peer, SyncOpRequest::PullSince { cursor: since });
145 pending.insert(id, Pending::Pull(tx));
146 }
147 },
148 event = swarm.select_next_some() => match event {
149 SwarmEvent::NewListenAddr { address, .. } => {
150 if let Some(tx) = pending_listen.take() { let _ = tx.send(Ok(address)); }
151 }
152 SwarmEvent::Behaviour(SyncBehaviourEvent::Rr(request_response::Event::Message { message, .. })) => match message {
153 request_response::Message::Request { request, channel, .. } => {
155 let resp = relay.handle(request);
156 let _ = swarm.behaviour_mut().rr.send_response(channel, resp);
157 }
158 request_response::Message::Response { request_id, response } => {
160 match (pending.remove(&request_id), response) {
161 (Some(Pending::Publish(tx)), SyncOpResponse::Published { accepted }) => { let _ = tx.send(Ok(accepted)); }
162 (Some(Pending::Pull(tx)), SyncOpResponse::Pulled { op_frames, next_cursor }) => { let _ = tx.send(Ok((op_frames, next_cursor))); }
163 (Some(Pending::Publish(tx)), _) => { let _ = tx.send(Err("unexpected response to publish".into())); }
164 (Some(Pending::Pull(tx)), _) => { let _ = tx.send(Err("unexpected response to pull".into())); }
165 (None, _) => {}
166 }
167 }
168 },
169 SwarmEvent::Behaviour(SyncBehaviourEvent::Rr(request_response::Event::OutboundFailure { request_id, error, .. })) => {
170 if let Some(p) = pending.remove(&request_id) {
171 let msg = format!("libp2p outbound failure: {error}");
172 match p {
173 Pending::Publish(tx) => { let _ = tx.send(Err(msg)); }
174 Pending::Pull(tx) => { let _ = tx.send(Err(msg)); }
175 }
176 }
177 }
178 _ => {}
179 }
180 }
181 }
182}
183
184pub struct BlockingSyncClient {
191 node: Libp2pSyncNode,
194 peer: PeerId,
195 rt: tokio::runtime::Runtime,
196}
197
198impl BlockingSyncClient {
199 pub fn connect(relay: SyncOpRelay, peer_id: &str, peer_addr: &str) -> Result<Self, String> {
204 let peer: PeerId = peer_id.parse().map_err(|e| format!("bad peer id: {e}"))?;
205 let addr: Multiaddr = peer_addr
206 .parse()
207 .map_err(|e| format!("bad multiaddr: {e}"))?;
208 let rt = tokio::runtime::Builder::new_multi_thread()
209 .worker_threads(1)
210 .enable_all()
211 .build()
212 .map_err(|e| format!("sync runtime: {e}"))?;
213 let node = {
216 let _guard = rt.enter();
217 Libp2pSyncNode::spawn(relay)
218 };
219 node.add_peer(peer, addr);
220 Ok(Self { node, peer, rt })
221 }
222
223 pub fn local_peer_id(&self) -> PeerId {
225 self.node.peer_id
226 }
227
228 pub fn publish_frames(&self, frames: Vec<Vec<u8>>) -> Result<u64, String> {
230 self.rt.block_on(self.node.publish(self.peer, frames))
231 }
232
233 pub fn pull_frames(&self, since: u64) -> Result<(Vec<Vec<u8>>, u64), String> {
235 self.rt.block_on(self.node.pull(self.peer, since))
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
246 fn two_nodes_exchange_ops_over_libp2p() {
247 let rt = tokio::runtime::Builder::new_current_thread()
248 .enable_all()
249 .build()
250 .unwrap();
251 rt.block_on(async {
252 let relay_a = SyncOpRelay::new();
254 let a = Libp2pSyncNode::spawn(relay_a.clone());
255 let a_addr = a.listen("/ip4/127.0.0.1/tcp/0").await.expect("A listen");
256
257 let b = Libp2pSyncNode::spawn(SyncOpRelay::new());
259 b.add_peer(a.peer_id, a_addr);
260
261 let accepted = b
262 .publish(a.peer_id, vec![b"op-1".to_vec(), b"op-2".to_vec()])
263 .await
264 .expect("publish");
265 assert_eq!(accepted, 2, "both ops newly accepted by A");
266 assert_eq!(relay_a.len(), 2, "A's relay holds the published ops");
267
268 let (frames, cursor) = b.pull(a.peer_id, 0).await.expect("pull");
269 assert_eq!(frames, vec![b"op-1".to_vec(), b"op-2".to_vec()]);
270 assert_eq!(cursor, 2);
271 });
272 }
273
274 #[test]
277 fn blocking_client_round_trips_against_a_listening_node() {
278 let rt_a = tokio::runtime::Builder::new_multi_thread()
281 .worker_threads(1)
282 .enable_all()
283 .build()
284 .unwrap();
285 let relay_a = SyncOpRelay::new();
286 let a = {
287 let _guard = rt_a.enter();
288 Libp2pSyncNode::spawn(relay_a.clone())
289 };
290 let a_addr = rt_a
291 .block_on(a.listen("/ip4/127.0.0.1/tcp/0"))
292 .expect("A listen");
293
294 let b = BlockingSyncClient::connect(
296 SyncOpRelay::new(),
297 &a.peer_id.to_string(),
298 &a_addr.to_string(),
299 )
300 .expect("connect");
301
302 let accepted = b
303 .publish_frames(vec![b"op-1".to_vec(), b"op-2".to_vec()])
304 .expect("publish");
305 assert_eq!(accepted, 2, "both frames newly accepted by A");
306 assert_eq!(relay_a.len(), 2, "A's relay holds the published frames");
307
308 let (frames, cursor) = b.pull_frames(0).expect("pull");
309 assert_eq!(frames, vec![b"op-1".to_vec(), b"op-2".to_vec()]);
310 assert_eq!(cursor, 2);
311 }
312}