Skip to main content

rings_core/message/handlers/
connection.rs

1use async_trait::async_trait;
2
3use crate::dht::types::Chord;
4use crate::dht::PeerRingAction;
5use crate::dht::TopoInfo;
6use crate::error::Error;
7use crate::error::Result;
8use crate::message::effects::CoreEffect;
9use crate::message::types::ConnectNodeReport;
10use crate::message::types::ConnectNodeSend;
11use crate::message::types::FindSuccessorReport;
12use crate::message::types::FindSuccessorSend;
13use crate::message::types::Message;
14use crate::message::types::PeerLivenessProbe;
15use crate::message::types::PeerLivenessReport;
16use crate::message::types::QueryForTopoInfoReport;
17use crate::message::types::QueryForTopoInfoSend;
18use crate::message::types::Then;
19use crate::message::FindSuccessorReportHandler;
20use crate::message::FindSuccessorThen;
21use crate::message::HandleMsg;
22use crate::message::MessageHandler;
23use crate::message::MessagePayload;
24
25mod topology_view;
26
27#[cfg(all(test, not(target_family = "wasm")))]
28use topology_view::confirmed_topology;
29use topology_view::connect_successor_hint;
30#[cfg(all(test, not(target_family = "wasm")))]
31use topology_view::topology_has_confirmed_peer;
32
33/// PeerLivenessProbe is a direct overlay liveness probe.
34#[cfg_attr(all(feature = "wasm", target_family = "wasm"), async_trait(?Send))]
35#[cfg_attr(not(all(feature = "wasm", target_family = "wasm")), async_trait)]
36impl HandleMsg<PeerLivenessProbe> for MessageHandler {
37    async fn handle(&self, ctx: &MessagePayload, msg: &PeerLivenessProbe) -> Result<()> {
38        if ctx.should_forward_from(self.dht.did) {
39            return self
40                .run_effects([CoreEffect::forward_payload(ctx, None)])
41                .await;
42        }
43
44        self.run_effects([CoreEffect::send_report_message(
45            ctx,
46            Message::PeerLivenessReport(msg.resp()),
47        )])
48        .await
49    }
50}
51
52/// PeerLivenessReport is handled by the callback's verified-inbound liveness update.
53#[cfg_attr(all(feature = "wasm", target_family = "wasm"), async_trait(?Send))]
54#[cfg_attr(not(all(feature = "wasm", target_family = "wasm")), async_trait)]
55impl HandleMsg<PeerLivenessReport> for MessageHandler {
56    async fn handle(&self, _ctx: &MessagePayload, _msg: &PeerLivenessReport) -> Result<()> {
57        Ok(())
58    }
59}
60
61/// QueryForTopoInfoSend is direct message
62#[cfg_attr(all(feature = "wasm", target_family = "wasm"), async_trait(?Send))]
63#[cfg_attr(not(all(feature = "wasm", target_family = "wasm")), async_trait)]
64impl HandleMsg<QueryForTopoInfoSend> for MessageHandler {
65    async fn handle(&self, ctx: &MessagePayload, msg: &QueryForTopoInfoSend) -> Result<()> {
66        let info: TopoInfo = TopoInfo::try_from(self.dht.as_ref())?;
67        if msg.targets(self.dht.did) {
68            self.run_effects([CoreEffect::send_report_message(
69                ctx,
70                Message::QueryForTopoInfoReport(msg.resp(info)),
71            )])
72            .await?
73        }
74        Ok(())
75    }
76}
77
78/// Try join received node into DHT after received from TopoInfo.
79#[cfg_attr(all(feature = "wasm", target_family = "wasm"), async_trait(?Send))]
80#[cfg_attr(not(all(feature = "wasm", target_family = "wasm")), async_trait)]
81impl HandleMsg<QueryForTopoInfoReport> for MessageHandler {
82    async fn handle(&self, _ctx: &MessagePayload, msg: &QueryForTopoInfoReport) -> Result<()> {
83        match msg.then {
84            <QueryForTopoInfoReport as Then>::Then::SyncSuccessor => {
85                let successors = msg.info.successors.clone();
86                self.connect_dht_peers(successors.iter().copied()).await?;
87                for peer in successors {
88                    if self.transport.get_connection(peer).is_some() {
89                        self.join_dht(peer).await?;
90                    }
91                }
92            }
93            <QueryForTopoInfoReport as Then>::Then::Stabilization => {
94                // Candidates begin as non-routable pending handshakes. Only
95                // peers whose data channel has opened may enter the DHT view.
96                let candidates = msg
97                    .info
98                    .predecessor
99                    .into_iter()
100                    .chain(msg.info.successors.iter().copied());
101                self.connect_dht_peers(candidates).await?;
102
103                if let Some(ev) = self.transport.stabilize_routable_topology(&msg.info)? {
104                    self.handle_dht_events(&ev).await?;
105                }
106            }
107        }
108        Ok(())
109    }
110}
111
112#[cfg_attr(all(feature = "wasm", target_family = "wasm"), async_trait(?Send))]
113#[cfg_attr(not(all(feature = "wasm", target_family = "wasm")), async_trait)]
114impl HandleMsg<ConnectNodeSend> for MessageHandler {
115    async fn handle(&self, ctx: &MessagePayload, msg: &ConnectNodeSend) -> Result<()> {
116        if !self.transport.accepts_connection_offer(msg) {
117            tracing::warn!(
118                local = %self.dht.did,
119                tx_id = %ctx.transaction.tx_id,
120                origin = ?ctx.relay.try_origin_sender().ok(),
121                relay_destination = %ctx.relay.destination,
122                transaction_destination = %ctx.transaction.destination,
123                mode = ?msg.dht_protocol_mode(),
124                "CONNECT_NODE offer rejected by DHT protocol mismatch"
125            );
126            return Ok(());
127        }
128
129        if ctx.should_forward_from(self.dht.did) {
130            tracing::trace!(
131                local = %self.dht.did,
132                tx_id = %ctx.transaction.tx_id,
133                origin = ?ctx.relay.try_origin_sender().ok(),
134                next_hop = %ctx.relay.next_hop,
135                relay_destination = %ctx.relay.destination,
136                transaction_destination = %ctx.transaction.destination,
137                sdp_bytes = msg.sdp.len(),
138                "CONNECT_NODE offer forward"
139            );
140            self.run_effects([CoreEffect::forward_payload(ctx, None)])
141                .await
142        } else {
143            let peer = ctx.relay.try_origin_sender()?;
144            tracing::trace!(
145                local = %self.dht.did,
146                peer = %peer,
147                tx_id = %ctx.transaction.tx_id,
148                sdp_bytes = msg.sdp.len(),
149                "CONNECT_NODE offer answer start"
150            );
151            let answer = match self
152                .transport
153                .answer_remote_connection(peer, self.inner_callback(), msg)
154                .await
155            {
156                Ok(answer) => {
157                    tracing::trace!(
158                        local = %self.dht.did,
159                        peer = %peer,
160                        tx_id = %ctx.transaction.tx_id,
161                        sdp_bytes = answer.sdp.len(),
162                        "CONNECT_NODE offer answer complete"
163                    );
164                    answer
165                }
166                Err(error) => {
167                    tracing::warn!(
168                        local = %self.dht.did,
169                        peer = %peer,
170                        tx_id = %ctx.transaction.tx_id,
171                        error = ?error,
172                        "CONNECT_NODE offer answer failed"
173                    );
174                    return Err(error);
175                }
176            };
177            self.run_effects([CoreEffect::send_report_message(
178                ctx,
179                Message::ConnectNodeReport(answer),
180            )])
181            .await
182        }
183    }
184}
185
186#[cfg_attr(all(feature = "wasm", target_family = "wasm"), async_trait(?Send))]
187#[cfg_attr(not(all(feature = "wasm", target_family = "wasm")), async_trait)]
188impl HandleMsg<ConnectNodeReport> for MessageHandler {
189    async fn handle(&self, ctx: &MessagePayload, msg: &ConnectNodeReport) -> Result<()> {
190        if ctx.should_forward_from(self.dht.did) {
191            tracing::trace!(
192                local = %self.dht.did,
193                tx_id = %ctx.transaction.tx_id,
194                origin = ?ctx.relay.try_origin_sender().ok(),
195                next_hop = %ctx.relay.next_hop,
196                relay_destination = %ctx.relay.destination,
197                transaction_destination = %ctx.transaction.destination,
198                sdp_bytes = msg.sdp.len(),
199                "CONNECT_NODE answer forward"
200            );
201            self.run_effects([CoreEffect::forward_payload(ctx, None)])
202                .await
203        } else {
204            let peer = ctx.relay.try_origin_sender()?;
205            tracing::trace!(
206                local = %self.dht.did,
207                peer = %peer,
208                tx_id = %ctx.transaction.tx_id,
209                sdp_bytes = msg.sdp.len(),
210                "CONNECT_NODE answer accept start"
211            );
212            match self.transport.accept_remote_connection(peer, msg).await {
213                Ok(()) => {
214                    tracing::trace!(
215                        local = %self.dht.did,
216                        peer = %peer,
217                        tx_id = %ctx.transaction.tx_id,
218                        "CONNECT_NODE answer accept complete"
219                    );
220                    Ok(())
221                }
222                Err(error) => {
223                    tracing::warn!(
224                        local = %self.dht.did,
225                        peer = %peer,
226                        tx_id = %ctx.transaction.tx_id,
227                        error = ?error,
228                        "CONNECT_NODE answer accept failed"
229                    );
230                    Err(error)
231                }
232            }
233        }
234    }
235}
236
237#[cfg_attr(all(feature = "wasm", target_family = "wasm"), async_trait(?Send))]
238#[cfg_attr(not(all(feature = "wasm", target_family = "wasm")), async_trait)]
239impl HandleMsg<FindSuccessorSend> for MessageHandler {
240    async fn handle(&self, ctx: &MessagePayload, msg: &FindSuccessorSend) -> Result<()> {
241        match self.dht.find_successor(msg.did)? {
242            PeerRingAction::Some(did) => {
243                if msg.accepts_local_successor(self.dht.did) {
244                    match &msg.then {
245                        FindSuccessorThen::Report(handler) => {
246                            let did = match handler {
247                                FindSuccessorReportHandler::Connect => connect_successor_hint(
248                                    self.dht.as_ref(),
249                                    ctx.relay.try_origin_sender()?,
250                                    did,
251                                )?,
252                                _ => did,
253                            };
254                            self.run_effects([CoreEffect::send_report_message(
255                                ctx,
256                                Message::FindSuccessorReport(FindSuccessorReport {
257                                    did,
258                                    handler: handler.clone(),
259                                }),
260                            )])
261                            .await
262                        }
263                    }
264                } else {
265                    self.run_effects([CoreEffect::forward_payload(ctx, Some(did))])
266                        .await
267                }
268            }
269            PeerRingAction::RemoteAction(next, _) => {
270                self.run_effects([CoreEffect::reset_destination(ctx, next)])
271                    .await
272            }
273            act => Err(Error::unexpected_peer_ring_action(act)),
274        }
275    }
276}
277
278#[cfg_attr(all(feature = "wasm", target_family = "wasm"), async_trait(?Send))]
279#[cfg_attr(not(all(feature = "wasm", target_family = "wasm")), async_trait)]
280impl HandleMsg<FindSuccessorReport> for MessageHandler {
281    async fn handle(&self, ctx: &MessagePayload, msg: &FindSuccessorReport) -> Result<()> {
282        if ctx.should_forward_from(self.dht.did) {
283            return self
284                .run_effects([CoreEffect::forward_payload(ctx, None)])
285                .await;
286        }
287
288        match &msg.handler {
289            FindSuccessorReportHandler::FixFingerTable { index } => {
290                let disposition = self.transport.record_finger_candidate(msg.did, *index)?;
291                if disposition.needs_connection() && msg.reports_remote_successor(self.dht.did) {
292                    self.connect_dht_peer(msg.did).await?;
293                    let _ = self.transport.record_finger_candidate(msg.did, *index)?;
294                }
295            }
296            FindSuccessorReportHandler::Connect if msg.reports_remote_successor(self.dht.did) => {
297                self.connect_dht_peer(msg.did).await?;
298            }
299            _ => {}
300        }
301
302        Ok(())
303    }
304}
305
306#[cfg(not(all(feature = "wasm", target_family = "wasm")))]
307#[cfg(test)]
308pub mod tests {
309    //! tests
310    use rings_transport::core::transport::WebrtcConnectionState;
311    use tokio::time::sleep;
312    use tokio::time::Duration;
313
314    use super::*;
315    use crate::dht::successor::SuccessorReader;
316    use crate::ecc::tests::gen_ordered_keys;
317    use crate::ecc::SecretKey;
318    use crate::tests::default::assert_no_more_msg;
319    use crate::tests::default::gen_pure_dht;
320    use crate::tests::default::prepare_node;
321    use crate::tests::default::wait_for_connection_state;
322    use crate::tests::default::wait_for_msgs;
323    use crate::tests::default::wait_for_successor;
324    use crate::tests::default::Node;
325    use crate::tests::manually_establish_connection;
326
327    #[test]
328    fn test_topology_report_keeps_only_confirmed_peers() {
329        let active = SecretKey::random().address().into();
330        let pending_successor = SecretKey::random().address().into();
331        let pending_predecessor = SecretKey::random().address().into();
332        let confirmed = confirmed_topology(
333            &TopoInfo {
334                successors: vec![active, pending_successor],
335                predecessor: Some(pending_predecessor),
336            },
337            |peer| peer == active,
338        );
339
340        assert_eq!(confirmed.successors, vec![active]);
341        assert_eq!(confirmed.predecessor, None);
342        assert!(topology_has_confirmed_peer(&confirmed));
343    }
344
345    #[test]
346    fn test_connect_successor_hint_skips_requester_self_report() -> Result<()> {
347        let keys = gen_ordered_keys(4);
348        let local = keys[0].address().into();
349        let requester = keys[1].address().into();
350        let next = keys[2].address().into();
351        let tail = keys[3].address().into();
352        let dht = gen_pure_dht(local);
353
354        dht.join(next)?;
355        dht.join(tail)?;
356        dht.join(requester)?;
357
358        assert_eq!(dht.successors().list()?, vec![requester, next, tail]);
359        assert_eq!(connect_successor_hint(&dht, requester, requester)?, next);
360        Ok(())
361    }
362
363    #[tokio::test]
364    async fn test_sync_successor_report_connects_advertised_successor() -> Result<()> {
365        let keys = gen_ordered_keys(3);
366        let node1 = prepare_node(keys[0]).await;
367        let node2 = prepare_node(keys[1]).await;
368        let node3 = prepare_node(keys[2]).await;
369
370        manually_establish_connection(&node1.swarm, &node2.swarm).await;
371        wait_for_msgs([&node1, &node2, &node3]).await;
372        manually_establish_connection(&node2.swarm, &node3.swarm).await;
373        wait_for_msgs([&node1, &node2, &node3]).await;
374
375        if node1.swarm.transport.get_connection(node3.did()).is_some() {
376            node1.swarm.disconnect(node3.did()).await?;
377            wait_for_msgs([&node1, &node2, &node3]).await;
378        }
379        assert!(node1.swarm.transport.get_connection(node3.did()).is_none());
380        assert!(!node1.dht().successors().contains(&node3.did())?);
381
382        node2
383            .swarm
384            .send_direct_message(
385                Message::QueryForTopoInfoReport(QueryForTopoInfoReport {
386                    info: TopoInfo {
387                        successors: vec![node3.did()],
388                        predecessor: None,
389                    },
390                    then: <QueryForTopoInfoReport as Then>::Then::SyncSuccessor,
391                }),
392                node1.did(),
393            )
394            .await?;
395
396        wait_for_connection_state(&node1, node3.did(), WebrtcConnectionState::Connected).await?;
397        wait_for_successor(&node1, node3.did()).await?;
398        wait_for_msgs([&node1, &node2, &node3]).await;
399        assert_no_more_msg([&node1, &node2, &node3]).await;
400        Ok(())
401    }
402
403    // node1.key < node2.key < node3.key
404    //
405    // Firstly, we connect node1 to node2, node2 to node3.
406    // Then, we connect node1 to node3 via DHT.
407    //
408    // After full connected, the topological structure should be:
409    //
410    // Node1 ------------ Node2 ------------ Node3
411    //   |-------------------------------------|
412    //
413    // --------- Connect node1 and node2
414    // 0. Node1 and node2 will set each other as their successor in DHTJoin handler.
415    //
416    // 1. Node1 send FindSuccessorSend(node1) to node2.
417    //    Meanwhile, node2 send FindSuccessorSend(node2) to node1.
418    //
419    // 2. Node1 respond by sending FindSuccessorReport(node2) to node2.
420    //    Meanwhile, node2 respond by sending FindSuccessorReport(node1) to node1.
421    //    But no node should update local successor by those reports.
422    //
423    // --------- Join node3 to node2
424    // 0. Node2 and node3 will set each other as their successor in DHTJoin handler.
425    //
426    // 1. Node3 send FindSuccessorSend(node3) to node2.
427    //    Meanwhile, node2 send FindSuccessorSend(node2) to node3.
428    //
429    // 2. Node3 respond by sending FindSuccessorReport(node2) to node2.
430    //    Meanwhile, node2 respond by sending FindSuccessorReport(node3) to node3.
431    //    But no node should update local successor by those reports.
432    //
433    // --------- Connect node1 to node3 via DHT
434    // 0. After checking finger table locally, node1 pick node2 to send ConnectNodeSend(node3).
435    //
436    // 1. Node2 relay ConnectNodeSend(node3) to node3.
437    //
438    // 2. Node3 respond by sending ConnectNodeReport(node1) to node2.
439    //
440    // 3. Node2 relay ConnectNodeReport(node1) to node1.
441    //
442    // --------- Communications after successful connection
443    //
444    #[tokio::test]
445    async fn test_triple_nodes_connection_1_2_3() -> Result<()> {
446        let keys = gen_ordered_keys(3);
447        let (key1, key2, key3) = (keys[0], keys[1], keys[2]);
448        test_triple_ordered_nodes_connection(key1, key2, key3).await?;
449        Ok(())
450    }
451
452    // The 2_3_1 should have same behavior as 1_2_3 since they are all clockwise.
453    #[tokio::test]
454    async fn test_triple_nodes_connection_2_3_1() -> Result<()> {
455        let keys = gen_ordered_keys(3);
456        let (key1, key2, key3) = (keys[0], keys[1], keys[2]);
457        test_triple_ordered_nodes_connection(key2, key3, key1).await?;
458        Ok(())
459    }
460
461    // The 3_1_2 should have same behavior as 1_2_3 since they are all clockwise.
462    #[tokio::test]
463    async fn test_triple_nodes_connection_3_1_2() -> Result<()> {
464        let keys = gen_ordered_keys(3);
465        let (key1, key2, key3) = (keys[0], keys[1], keys[2]);
466        test_triple_ordered_nodes_connection(key3, key1, key2).await?;
467        Ok(())
468    }
469
470    // node1.key > node2.key > node3.key
471    //
472    // All the processes are the same as test_triple_nodes_1_2_3. Except the following:
473    //
474    // --------- Join node3 to node2
475    // 0. Node3 will set node2 as successor in DHTJoin handler.
476    //
477    //    Node2 will not set node3 as successor in DHTJoin handler.
478    //    Because node2.processor.max() is node1, and node1.bias(node1) < node1.bias(node3).
479    //    That means node1 is closer to node2 than node3 on the clock circle.
480    //
481    // 1. Node3 send FindSuccessorSend(node3) to node2. Node2 relay it to Node1.
482    //    Meanwhile, node2 send FindSuccessorSend(node2) to node3.
483    //
484    // 2. Node3 respond by sending FindSuccessorReport(node2) to node2.
485    //    Meanwhile, node1 respond by sending FindSuccessorReport(node2) to node3 through node2.
486    //
487    // --------- Communications after successful connection
488    //
489    #[tokio::test]
490    async fn test_triple_nodes_connection_3_2_1() -> Result<()> {
491        let keys = gen_ordered_keys(3);
492        let (key1, key2, key3) = (keys[0], keys[1], keys[2]);
493        test_triple_desc_ordered_nodes_connection(key3, key2, key1).await?;
494        Ok(())
495    }
496
497    // The 2_1_3 should have same behavior as 3_2_1 since they are all anti-clockwise.
498    #[tokio::test]
499    async fn test_triple_nodes_connection_2_1_3() -> Result<()> {
500        let keys = gen_ordered_keys(3);
501        let (key1, key2, key3) = (keys[0], keys[1], keys[2]);
502        test_triple_desc_ordered_nodes_connection(key2, key1, key3).await?;
503        Ok(())
504    }
505
506    // The 1_3_2 should have same behavior as 3_2_1 since they are all anti-clockwise.
507    #[tokio::test]
508    async fn test_triple_nodes_connection_1_3_2() -> Result<()> {
509        let keys = gen_ordered_keys(3);
510        let (key1, key2, key3) = (keys[0], keys[1], keys[2]);
511        test_triple_desc_ordered_nodes_connection(key1, key3, key2).await?;
512        Ok(())
513    }
514
515    async fn test_triple_ordered_nodes_connection(
516        key1: SecretKey,
517        key2: SecretKey,
518        key3: SecretKey,
519    ) -> Result<(Node, Node, Node)> {
520        let node1 = prepare_node(key1).await;
521        let node2 = prepare_node(key2).await;
522        let node3 = prepare_node(key3).await;
523
524        println!("========================================");
525        println!("||  now we connect node1 and node2    ||");
526        println!("========================================");
527
528        manually_establish_connection(&node1.swarm, &node2.swarm).await;
529        wait_for_msgs([&node1, &node2, &node3]).await;
530        assert_no_more_msg([&node1, &node2, &node3]).await;
531
532        node1.assert_transports(vec![node2.did()]);
533        node2.assert_transports(vec![node1.did()]);
534        node3.assert_transports(vec![]);
535        assert_eq!(node1.dht().successors().list()?, vec![node2.did()]);
536        assert_eq!(node2.dht().successors().list()?, vec![node1.did()]);
537        assert_eq!(node3.dht().successors().list()?, vec![]);
538
539        println!("========================================");
540        println!("||  now we start join node3 to node2  ||");
541        println!("========================================");
542
543        manually_establish_connection(&node3.swarm, &node2.swarm).await;
544        wait_for_msgs([&node1, &node2, &node3]).await;
545        assert_no_more_msg([&node1, &node2, &node3]).await;
546
547        println!("=== Check state before connect via DHT ===");
548        if node1.swarm.transport.get_connection(node3.did()).is_some() {
549            node1.assert_transports(vec![node2.did(), node3.did()]);
550            node2.assert_transports(vec![node1.did(), node3.did()]);
551            node3.assert_transports(vec![node1.did(), node2.did()]);
552            assert_eq!(node1.dht().successors().list()?, vec![
553                node2.did(),
554                node3.did()
555            ]);
556            assert_eq!(node2.dht().successors().list()?, vec![
557                node3.did(),
558                node1.did()
559            ]);
560            assert_eq!(node3.dht().successors().list()?, vec![
561                node1.did(),
562                node2.did()
563            ]);
564        } else {
565            node1.assert_transports(vec![node2.did()]);
566            node2.assert_transports(vec![node1.did(), node3.did()]);
567            node3.assert_transports(vec![node2.did()]);
568            assert_eq!(node1.dht().successors().list()?, vec![node2.did(),]);
569            assert_eq!(node2.dht().successors().list()?, vec![
570                node3.did(),
571                node1.did()
572            ]);
573            assert_eq!(node3.dht().successors().list()?, vec![node2.did()]);
574        }
575
576        println!("=============================================");
577        println!("||  now we connect node1 to node3 via DHT  ||");
578        println!("=============================================");
579
580        if node1.swarm.transport.get_connection(node3.did()).is_none() {
581            node1.swarm.connect(node3.did()).await?;
582        }
583        wait_for_msgs([&node1, &node2, &node3]).await;
584        assert_no_more_msg([&node1, &node2, &node3]).await;
585
586        println!("=== Check state after connect via DHT ===");
587        node1.assert_transports(vec![node2.did(), node3.did()]);
588        node2.assert_transports(vec![node1.did(), node3.did()]);
589        node3.assert_transports(vec![node1.did(), node2.did()]);
590        assert_eq!(node1.dht().successors().list()?, vec![
591            node2.did(),
592            node3.did()
593        ]);
594        assert_eq!(node2.dht().successors().list()?, vec![
595            node3.did(),
596            node1.did()
597        ]);
598        assert_eq!(node3.dht().successors().list()?, vec![
599            node1.did(),
600            node2.did()
601        ]);
602
603        Ok((node1, node2, node3))
604    }
605
606    async fn test_triple_desc_ordered_nodes_connection(
607        key1: SecretKey,
608        key2: SecretKey,
609        key3: SecretKey,
610    ) -> Result<(Node, Node, Node)> {
611        let node1 = prepare_node(key1).await;
612        let node2 = prepare_node(key2).await;
613        let node3 = prepare_node(key3).await;
614
615        println!("========================================");
616        println!("||  now we connect node1 and node2    ||");
617        println!("========================================");
618
619        manually_establish_connection(&node1.swarm, &node2.swarm).await;
620        wait_for_msgs([&node1, &node2, &node3]).await;
621        assert_no_more_msg([&node1, &node2, &node3]).await;
622
623        assert_eq!(node1.dht().successors().list()?, vec![node2.did()]);
624        assert_eq!(node2.dht().successors().list()?, vec![node1.did()]);
625        assert_eq!(node3.dht().successors().list()?, vec![]);
626
627        println!("========================================");
628        println!("||  now we start join node3 to node2  ||");
629        println!("========================================");
630
631        manually_establish_connection(&node3.swarm, &node2.swarm).await;
632        wait_for_msgs([&node1, &node2, &node3]).await;
633        assert_no_more_msg([&node1, &node2, &node3]).await;
634
635        println!("=== Check state before connect via DHT ===");
636        if node1.swarm.transport.get_connection(node3.did()).is_some() {
637            node1.assert_transports(vec![node2.did(), node3.did()]);
638            node2.assert_transports(vec![node1.did(), node3.did()]);
639            node3.assert_transports(vec![node1.did(), node2.did()]);
640            assert_eq!(node1.dht().successors().list()?, vec![
641                node3.did(),
642                node2.did()
643            ]);
644            assert_eq!(node2.dht().successors().list()?, vec![
645                node1.did(),
646                node3.did()
647            ]);
648            assert_eq!(node3.dht().successors().list()?, vec![
649                node2.did(),
650                node1.did()
651            ]);
652        } else {
653            node1.assert_transports(vec![node2.did()]);
654            node2.assert_transports(vec![node1.did(), node3.did()]);
655            node3.assert_transports(vec![node2.did()]);
656            assert_eq!(node1.dht().successors().list()?, vec![node2.did()]);
657            assert_eq!(node2.dht().successors().list()?, vec![
658                node1.did(),
659                node3.did()
660            ]);
661            assert_eq!(node3.dht().successors().list()?, vec![node2.did()]);
662        }
663
664        println!("=============================================");
665        println!("||  now we connect node1 to node3 via DHT  ||");
666        println!("=============================================");
667
668        if node1.swarm.transport.get_connection(node3.did()).is_none() {
669            node1.swarm.connect(node3.did()).await?;
670        }
671        wait_for_msgs([&node1, &node2, &node3]).await;
672        assert_no_more_msg([&node1, &node2, &node3]).await;
673
674        println!("=== Check state after connect via DHT ===");
675        node1.assert_transports(vec![node2.did(), node3.did()]);
676        node2.assert_transports(vec![node1.did(), node3.did()]);
677        node3.assert_transports(vec![node1.did(), node2.did()]);
678        assert_eq!(node1.dht().successors().list()?, vec![
679            node3.did(),
680            node2.did()
681        ]);
682        assert_eq!(node2.dht().successors().list()?, vec![
683            node1.did(),
684            node3.did()
685        ]);
686        assert_eq!(node3.dht().successors().list()?, vec![
687            node2.did(),
688            node1.did()
689        ]);
690
691        Ok((node1, node2, node3))
692    }
693
694    #[tokio::test]
695    async fn test_fourth_node_connection() -> Result<()> {
696        let keys = gen_ordered_keys(4);
697        let (key1, key2, key3, key4) = (keys[0], keys[1], keys[2], keys[3]);
698        let (node1, node2, node3) = test_triple_ordered_nodes_connection(key1, key2, key3).await?;
699        // we now have three connected nodes
700        // node1 -> node2 -> node3
701        //  |-<-----<---------<--|
702
703        let node4 = prepare_node(key4).await;
704
705        // Unless we use a fixed did value, we cannot fully predict the communication order between node4 and the nodes,
706        // because we do not know the distance between node4 and each node.
707        //
708        // Therefore, here we only guarantee that messages can be processed correctly without checking the specific message order.
709        //
710        // In addition, we check the final state to ensure the entire process meets expectations.
711
712        // connect node4 to node2
713        manually_establish_connection(&node4.swarm, &node2.swarm).await;
714        // Poll for convergence rather than sleeping a fixed amount: under the
715        // release-LTO CI run with native WebRTC, 6s is not always enough and the
716        // assertions below would flake. The expected final state is unchanged.
717        wait_until_with_state(
718            "node4 joined: DHT successors converged",
719            || {
720                Ok(
721                    node1.dht().successors().list()? == vec![node2.did(), node3.did(), node4.did()]
722                        && node2.dht().successors().list()?
723                            == vec![node3.did(), node4.did(), node1.did()]
724                        && node3.dht().successors().list()?
725                            == vec![node4.did(), node1.did(), node2.did()]
726                        && node4.dht().successors().list()?
727                            == vec![node1.did(), node2.did(), node3.did()],
728                )
729            },
730            || describe_nodes([&node1, &node2, &node3, &node4]),
731        )
732        .await?;
733
734        println!("=== Check state before connect via DHT ===");
735        node1.assert_transports(vec![node2.did(), node3.did(), node4.did()]);
736        node2.assert_transports(vec![node3.did(), node4.did(), node1.did()]);
737        node3.assert_transports(vec![node4.did(), node1.did(), node2.did()]);
738        node4.assert_transports(vec![node1.did(), node2.did(), node3.did()]);
739        assert_eq!(node1.dht().successors().list()?, vec![
740            node2.did(),
741            node3.did(),
742            node4.did(),
743        ]);
744        assert_eq!(node2.dht().successors().list()?, vec![
745            node3.did(),
746            node4.did(),
747            node1.did(),
748        ]);
749        assert_eq!(node3.dht().successors().list()?, vec![
750            node4.did(),
751            node1.did(),
752            node2.did(),
753        ]);
754        assert_eq!(node4.dht().successors().list()?, vec![
755            node1.did(),
756            node2.did(),
757            node3.did(),
758        ]);
759
760        println!("========================================");
761        println!("| test node4 connect node3 via dht     |");
762        println!("========================================");
763        println!(
764            "node1.did(): {:?}, node2.did(): {:?}, node3.did(): {:?}, node4.did(): {:?}",
765            node1.did(),
766            node2.did(),
767            node3.did(),
768            node4.did(),
769        );
770        println!("==================================================");
771
772        if node4.swarm.transport.get_connection(node3.did()).is_none() {
773            node4.swarm.connect(node3.did()).await?;
774        }
775        // Same as above: poll for the post-connect converged state instead of a
776        // fixed 6s sleep so the test is robust under CI contention.
777        wait_until_with_state(
778            "node4 connected node3: DHT successors converged",
779            || {
780                Ok(
781                    node1.dht().successors().list()? == vec![node2.did(), node3.did(), node4.did()]
782                        && node2.dht().successors().list()?
783                            == vec![node3.did(), node4.did(), node1.did()]
784                        && node3.dht().successors().list()?
785                            == vec![node4.did(), node1.did(), node2.did()]
786                        && node4.dht().successors().list()?
787                            == vec![node1.did(), node2.did(), node3.did()],
788                )
789            },
790            || describe_nodes([&node1, &node2, &node3, &node4]),
791        )
792        .await?;
793
794        println!("=== Check state after connect via DHT ===");
795        node1.assert_transports(vec![node2.did(), node3.did(), node4.did()]);
796        node2.assert_transports(vec![node3.did(), node4.did(), node1.did()]);
797        node3.assert_transports(vec![node4.did(), node1.did(), node2.did()]);
798        node4.assert_transports(vec![node1.did(), node2.did(), node3.did()]);
799        assert_eq!(node1.dht().successors().list()?, vec![
800            node2.did(),
801            node3.did(),
802            node4.did()
803        ]);
804        assert_eq!(node2.dht().successors().list()?, vec![
805            node3.did(),
806            node4.did(),
807            node1.did(),
808        ]);
809        assert_eq!(node3.dht().successors().list()?, vec![
810            node4.did(),
811            node1.did(),
812            node2.did(),
813        ]);
814        assert_eq!(node4.dht().successors().list()?, vec![
815            node1.did(),
816            node2.did(),
817            node3.did(),
818        ]);
819
820        Ok(())
821    }
822
823    #[cfg(feature = "dummy")]
824    #[tokio::test]
825    async fn test_joining_between_bootstrap_and_successor_connects_successor_hint() -> Result<()> {
826        let keys = gen_ordered_keys(4);
827        let (node1, node2, node3) =
828            test_triple_ordered_nodes_connection(keys[0], keys[2], keys[3]).await?;
829        let joining = prepare_node(keys[1]).await;
830
831        manually_establish_connection(&joining.swarm, &node1.swarm).await;
832        wait_until(
833            "joining peer connects past bootstrap successor self-report",
834            || {
835                Ok(joining
836                    .swarm
837                    .transport
838                    .get_connection(node2.did())
839                    .is_some())
840            },
841        )
842        .await?;
843
844        wait_for_msgs([&node1, &node2, &node3, &joining]).await;
845        assert_no_more_msg([&node1, &node2, &node3, &joining]).await;
846
847        joining.assert_transports(vec![node1.did(), node2.did(), node3.did()]);
848        assert_eq!(node1.dht().successors().list()?, vec![
849            joining.did(),
850            node2.did(),
851            node3.did(),
852        ]);
853        assert_eq!(joining.dht().successors().list()?, vec![
854            node2.did(),
855            node3.did(),
856            node1.did(),
857        ]);
858
859        Ok(())
860    }
861
862    /// Poll `cond` every 200ms until it returns true, failing after ~60s.
863    /// Used instead of fixed sleeps so the test is deterministic regardless of
864    /// how long the WebRTC handshake/teardown takes on a given machine.
865    ///
866    /// The window is generous on purpose: ICE paces connectivity checks at
867    /// ~200ms each, so on a host with many network interfaces (lots of
868    /// candidate pairs) establishing the connection can legitimately take ~20s.
869    async fn wait_until(msg: &str, mut cond: impl FnMut() -> Result<bool>) -> Result<()> {
870        wait_until_with_state(msg, &mut cond, String::new).await
871    }
872
873    async fn wait_until_with_state(
874        msg: &str,
875        mut cond: impl FnMut() -> Result<bool>,
876        state: impl Fn() -> String,
877    ) -> Result<()> {
878        for _ in 0..300 {
879            if cond()? {
880                return Ok(());
881            }
882            sleep(Duration::from_millis(200)).await;
883        }
884        let state = state();
885        if state.is_empty() {
886            Err(Error::InvalidMessage(format!("timeout waiting for: {msg}")))
887        } else {
888            Err(Error::InvalidMessage(format!(
889                "timeout waiting for: {msg}\n{state}"
890            )))
891        }
892    }
893
894    fn describe_nodes<'a>(nodes: impl IntoIterator<Item = &'a Node>) -> String {
895        nodes
896            .into_iter()
897            .map(|node| {
898                format!(
899                    "{:?}: successors={:?}, transports={:?}",
900                    node.did(),
901                    node.dht().successors().list().unwrap_or_default(),
902                    node.swarm.transport.get_connection_ids(),
903                )
904            })
905            .collect::<Vec<_>>()
906            .join("\n")
907    }
908
909    #[tokio::test]
910    async fn test_finger_when_disconnect() -> Result<()> {
911        let key1 = SecretKey::random();
912        let key2 = SecretKey::random();
913
914        let node1 = prepare_node(key1).await;
915        let node2 = prepare_node(key2).await;
916
917        {
918            assert!(node1.dht().lock_finger()?.is_empty());
919            assert!(node1.dht().lock_finger()?.is_empty());
920        }
921
922        manually_establish_connection(&node1.swarm, &node2.swarm).await;
923
924        // The data channels open and `on_data_channel_open -> join_dht` runs
925        // asynchronously, so poll until both sides have joined each other rather
926        // than asserting after a fixed wait.
927        wait_until("node1 and node2 to join each other's DHT", || {
928            let finger1 = node1.dht().lock_finger()?.clone().clone_finger();
929            let finger2 = node2.dht().lock_finger()?.clone().clone_finger();
930            Ok(finger1.into_iter().any(|x| x == Some(node2.did()))
931                && finger2.into_iter().any(|x| x == Some(node1.did())))
932        })
933        .await?;
934
935        node1.assert_transports(vec![node2.did()]);
936        node2.assert_transports(vec![node1.did()]);
937
938        println!("===================================");
939        println!("| test disconnect node1 and node2 |");
940        println!("===================================");
941        node1.swarm.disconnect(node2.did()).await?;
942
943        // node1 closes locally; node2 learns via the data channel closing and
944        // tears its side down promptly (without waiting for the ICE `Failed`
945        // timeout). Poll until both sides have removed the connection.
946        wait_until("both sides to drop the connection", || {
947            Ok(node1.swarm.transport.get_connection(node2.did()).is_none()
948                && node2.swarm.transport.get_connection(node1.did()).is_none())
949        })
950        .await?;
951
952        node1.assert_transports(vec![]);
953        node2.assert_transports(vec![]);
954
955        wait_until("both sides to remove each other from DHT fingers", || {
956            let finger1 = node1.dht().lock_finger()?.clone().clone_finger();
957            let finger2 = node2.dht().lock_finger()?.clone().clone_finger();
958            Ok(
959                finger1.into_iter().all(|x| x.is_none())
960                    && finger2.into_iter().all(|x| x.is_none()),
961            )
962        })
963        .await?;
964
965        Ok(())
966    }
967}