1use std::collections::BTreeSet;
8use std::sync::Arc;
9use std::sync::Mutex;
10use std::time::Duration;
11
12use async_trait::async_trait;
13use futures::lock::Mutex as AsyncMutex;
14use rings_core::dht::entry;
15use rings_core::dht::Did;
16use rings_core::ecc::VerificationPublicKey;
17use rings_core::lifecycle::StopToken;
18use rings_core::message::Encoded;
19use rings_core::message::Encoder;
20use rings_core::session::SessionSk;
21use rings_core::utils::get_epoch_ms;
22
23use crate::error::Error;
24use crate::error::Result;
25use crate::extension::ext::MaybeSend;
26use crate::online::OnlineNodeDescriptor;
27use crate::online::OnlineNodeDescriptorBody;
28use crate::online::OnlineNodeType;
29use crate::online::ONLINE_NODES_TOPIC;
30use crate::online::ONLINE_NODE_CAPABILITY_STORAGE;
31use crate::processor::Processor;
32
33const DEFAULT_ONLINE_NODE_HEARTBEAT_INTERVAL_SECS: u64 = 30;
34const DEFAULT_ONLINE_NODE_TTL_SECS: u64 = 90;
35
36pub(crate) const fn default_online_node_heartbeat_interval_secs() -> u64 {
38 DEFAULT_ONLINE_NODE_HEARTBEAT_INTERVAL_SECS
39}
40
41pub(crate) const fn default_online_node_ttl_secs() -> u64 {
43 DEFAULT_ONLINE_NODE_TTL_SECS
44}
45
46pub(crate) fn default_online_node_type() -> OnlineNodeType {
48 #[cfg(feature = "ffi")]
49 {
50 OnlineNodeType::Ffi
51 }
52 #[cfg(all(not(feature = "ffi"), feature = "browser", target_family = "wasm"))]
53 {
54 OnlineNodeType::Browser
55 }
56 #[cfg(all(
57 not(feature = "ffi"),
58 not(all(feature = "browser", target_family = "wasm"))
59 ))]
60 {
61 OnlineNodeType::Native
62 }
63}
64
65pub(crate) const fn default_advertise_presence() -> bool {
67 true
68}
69
70pub(crate) fn validate_online_node_registration_timing(
72 advertise_presence: bool,
73 heartbeat_interval: Duration,
74 ttl: Duration,
75) -> Result<()> {
76 if advertise_presence && heartbeat_interval >= ttl {
77 return Err(Error::InvalidConfig(format!(
78 "online_node_heartbeat_interval ({heartbeat_interval:?}) must be less than online_node_ttl ({ttl:?}) when advertise_presence is enabled"
79 )));
80 }
81 Ok(())
82}
83
84#[cfg(not(all(feature = "browser", target_family = "wasm")))]
85pub(crate) async fn sleep_registration_interval(interval: Duration) -> Result<()> {
86 futures_timer::Delay::new(interval).await;
89 Ok(())
90}
91
92#[cfg(all(feature = "browser", target_family = "wasm"))]
93pub(crate) async fn sleep_registration_interval(interval: Duration) -> Result<()> {
94 let interval_ms = i32::try_from(interval.as_millis()).unwrap_or(i32::MAX);
95 rings_core::utils::js_utils::window_sleep(interval_ms)
96 .await
97 .map_err(|error| Error::JsError(format!("{error:?}")))?;
98 Ok(())
99}
100
101pub struct RegistrationContext<'a> {
106 processor: &'a Processor,
107 stop: StopToken,
108}
109
110impl<'a> RegistrationContext<'a> {
111 pub(crate) fn new(processor: &'a Processor) -> Self {
112 Self::new_with_stop(processor, StopToken::never())
113 }
114
115 pub(crate) const fn new_with_stop(processor: &'a Processor, stop: StopToken) -> Self {
116 Self { processor, stop }
117 }
118
119 pub fn should_stop(&self) -> bool {
121 self.stop.should_stop()
122 }
123
124 pub(crate) fn ensure_running(&self) -> Result<()> {
125 if self.should_stop() {
126 return Err(Error::RegistrationStopped);
127 }
128 Ok(())
129 }
130
131 pub fn did(&self) -> Did {
133 self.processor.did()
134 }
135
136 pub fn network_id(&self) -> u32 {
138 self.processor.swarm.network_id()
139 }
140
141 pub fn storage_redundancy(&self) -> u16 {
143 self.processor.swarm.storage_redundancy()
144 }
145
146 pub fn dht_virtual_nodes(&self) -> u16 {
148 self.processor.swarm.dht_virtual_nodes()
149 }
150
151 pub fn account_verification_pubkey(&self) -> Result<VerificationPublicKey> {
153 self.processor
154 .swarm
155 .account_verification_pubkey()
156 .map_err(Error::CoreError)
157 }
158
159 pub fn session_sk(&self) -> &SessionSk {
161 self.processor.session_sk()
162 }
163
164 pub(crate) async fn fetch_storage_entry(&self, entry_key: Did) -> Result<Option<entry::Entry>> {
165 self.processor
166 .fetch_storage_entry_with_stop(entry_key, &self.stop)
167 .await
168 }
169}
170
171#[derive(Clone, Debug)]
173pub struct DhtRegistrationPublisher {
174 topic: String,
175 publish_gate: Arc<AsyncMutex<()>>,
176 published_values: Arc<Mutex<BTreeSet<Encoded>>>,
177}
178
179impl DhtRegistrationPublisher {
180 pub fn new(topic: impl Into<String>) -> Self {
182 Self {
183 topic: topic.into(),
184 publish_gate: Arc::new(AsyncMutex::new(())),
185 published_values: Arc::new(Mutex::new(BTreeSet::new())),
186 }
187 }
188
189 pub fn topic(&self) -> &str {
191 &self.topic
192 }
193
194 pub async fn publish(&self, context: &RegistrationContext<'_>, value: Encoded) -> Result<()> {
196 self.publish_many(context, std::iter::once(value)).await
197 }
198
199 pub async fn publish_many(
205 &self,
206 context: &RegistrationContext<'_>,
207 values: impl IntoIterator<Item = Encoded>,
208 ) -> Result<()> {
209 self.publish_many_with_replacement(context, values, false, |_| false)
210 .await
211 }
212
213 pub async fn publish_many_replacing(
219 &self,
220 context: &RegistrationContext<'_>,
221 values: impl IntoIterator<Item = Encoded>,
222 replaces_observed_value: impl Fn(&Encoded) -> bool,
223 ) -> Result<()> {
224 self.publish_many_with_replacement(context, values, true, replaces_observed_value)
225 .await
226 }
227
228 pub async fn publish_many_replacing_and_compacting(
235 &self,
236 context: &RegistrationContext<'_>,
237 values: impl IntoIterator<Item = Encoded>,
238 replaces_observed_value: impl Fn(&Encoded) -> bool,
239 preserves_observed_value: impl Fn(&Encoded) -> bool,
240 ) -> Result<()> {
241 let current_values = values.into_iter().collect::<BTreeSet<_>>();
242 let _publish_turn = self.publish_gate.lock().await;
243 context.ensure_running()?;
244 let observed_entry = self.observed_registry_entry(context).await?;
245 let observed_values = observed_entry
246 .as_ref()
247 .map(|entry| entry.data.clone())
248 .unwrap_or_default();
249 let should_compact_metadata = observed_entry
250 .as_ref()
251 .is_some_and(registry_entry_has_compactable_metadata);
252 let stale_values = {
253 let mut published_values = self.published_values.lock().map_err(|_| Error::Lock)?;
254 begin_registration_publish(
255 &mut published_values,
256 ¤t_values,
257 observed_values,
258 |observed| {
259 should_prune_observed_registry_value(
260 observed,
261 &replaces_observed_value,
262 &preserves_observed_value,
263 )
264 },
265 )
266 };
267 let should_compact = should_compact_metadata || !stale_values.is_empty();
268 let removals = stale_values.clone();
269
270 for value in ¤t_values {
271 context.ensure_running()?;
272 context
273 .processor
274 .storage_touch_data(&self.topic, value.clone())
275 .await?;
276 }
277 for stale_value in stale_values {
278 context.ensure_running()?;
279 context
280 .processor
281 .storage_tombstone_data(&self.topic, stale_value.clone())
282 .await?;
283 self.published_values
284 .lock()
285 .map_err(|_| Error::Lock)?
286 .remove(&stale_value);
287 }
288 if should_compact {
289 context.ensure_running()?;
290 context
291 .processor
292 .storage_compact_data(&self.topic, removals)
293 .await?;
294 }
295 {
296 let mut published_values = self.published_values.lock().map_err(|_| Error::Lock)?;
297 finish_registration_publish(&mut published_values, current_values);
298 }
299 Ok(())
300 }
301
302 async fn publish_many_with_replacement(
303 &self,
304 context: &RegistrationContext<'_>,
305 values: impl IntoIterator<Item = Encoded>,
306 load_observed_values: bool,
307 replaces_observed_value: impl Fn(&Encoded) -> bool,
308 ) -> Result<()> {
309 let current_values = values.into_iter().collect::<BTreeSet<_>>();
310 let _publish_turn = self.publish_gate.lock().await;
313 context.ensure_running()?;
314 let observed_values = if load_observed_values {
315 self.observed_registry_values(context).await?
316 } else {
317 vec![]
318 };
319 let stale_values = {
320 let mut published_values = self.published_values.lock().map_err(|_| Error::Lock)?;
321 begin_registration_publish(
322 &mut published_values,
323 ¤t_values,
324 observed_values,
325 replaces_observed_value,
326 )
327 };
328
329 for value in ¤t_values {
330 context.ensure_running()?;
331 context
332 .processor
333 .storage_touch_data(&self.topic, value.clone())
334 .await?;
335 }
336 for stale_value in stale_values {
337 context.ensure_running()?;
338 context
339 .processor
340 .storage_tombstone_data(&self.topic, stale_value.clone())
341 .await?;
342 self.published_values
343 .lock()
344 .map_err(|_| Error::Lock)?
345 .remove(&stale_value);
346 }
347 {
348 let mut published_values = self.published_values.lock().map_err(|_| Error::Lock)?;
349 finish_registration_publish(&mut published_values, current_values);
350 }
351 Ok(())
352 }
353
354 async fn observed_registry_values(
355 &self,
356 context: &RegistrationContext<'_>,
357 ) -> Result<Vec<Encoded>> {
358 Ok(self
359 .observed_registry_entry(context)
360 .await?
361 .map(|entry| entry.data)
362 .unwrap_or_default())
363 }
364
365 async fn observed_registry_entry(
366 &self,
367 context: &RegistrationContext<'_>,
368 ) -> Result<Option<entry::Entry>> {
369 let entry_key = entry::Entry::gen_did(&self.topic)?;
370 context.fetch_storage_entry(entry_key).await
371 }
372}
373
374fn registry_entry_has_compactable_metadata(entry: &entry::Entry) -> bool {
375 !entry.crdt.tombstones.is_empty()
376}
377
378fn should_prune_observed_registry_value(
379 observed: &Encoded,
380 replaces_observed_value: &impl Fn(&Encoded) -> bool,
381 preserves_observed_value: &impl Fn(&Encoded) -> bool,
382) -> bool {
383 replaces_observed_value(observed) || !preserves_observed_value(observed)
384}
385
386fn begin_registration_publish(
387 published_values: &mut BTreeSet<Encoded>,
388 current_values: &BTreeSet<Encoded>,
389 observed_values: Vec<Encoded>,
390 replaces_observed_value: impl Fn(&Encoded) -> bool,
391) -> Vec<Encoded> {
392 let mut stale_values = published_values
393 .iter()
394 .filter(|published| !current_values.contains(*published))
395 .cloned()
396 .collect::<BTreeSet<_>>();
397 stale_values.extend(
398 observed_values
399 .into_iter()
400 .filter(|observed| !current_values.contains(observed))
401 .filter(replaces_observed_value),
402 );
403 published_values.extend(current_values.iter().cloned());
407 stale_values.into_iter().collect()
408}
409
410fn finish_registration_publish(
411 published_values: &mut BTreeSet<Encoded>,
412 current_values: BTreeSet<Encoded>,
413) {
414 *published_values = current_values;
415}
416
417#[cfg_attr(all(feature = "browser", target_family = "wasm"), async_trait(?Send))]
419#[cfg_attr(not(all(feature = "browser", target_family = "wasm")), async_trait)]
420pub trait RegistrationTask: MaybeSend {
421 fn name(&self) -> &'static str;
423
424 fn interval(&self) -> Duration;
426
427 async fn register_once(&self, context: &RegistrationContext<'_>) -> Result<()>;
429}
430
431#[derive(Clone, Debug)]
433pub struct OnlineNodeCapabilities {
434 labels: Arc<Mutex<Vec<String>>>,
435}
436
437impl OnlineNodeCapabilities {
438 fn new(additional_capabilities: Vec<String>) -> Self {
439 let mut labels = Self::default_labels();
440 Self::append_unique_many(&mut labels, additional_capabilities);
441 Self {
442 labels: Arc::new(Mutex::new(labels)),
443 }
444 }
445
446 fn default_labels() -> Vec<String> {
447 vec![ONLINE_NODE_CAPABILITY_STORAGE.to_string()]
448 }
449
450 fn append_unique_many<I, S>(labels: &mut Vec<String>, capabilities: I)
451 where
452 I: IntoIterator<Item = S>,
453 S: Into<String>,
454 {
455 for capability in capabilities {
456 let capability = capability.into();
457 if !labels.iter().any(|known| known == &capability) {
458 labels.push(capability);
459 }
460 }
461 }
462
463 pub fn add_many<I>(&self, capabilities: I) -> Result<()>
465 where I: IntoIterator<Item = &'static str> {
466 let mut labels = self.labels.lock().map_err(|_| Error::Lock)?;
467 Self::append_unique_many(&mut labels, capabilities);
468 Ok(())
469 }
470
471 pub fn labels(&self) -> Result<Vec<String>> {
473 self.labels
474 .lock()
475 .map(|labels| labels.clone())
476 .map_err(|_| Error::Lock)
477 }
478}
479
480#[derive(Clone, Debug)]
482pub struct OnlineNodeRegistration {
483 heartbeat_interval: Duration,
484 ttl: Duration,
485 node_type: OnlineNodeType,
486 started_at_ms: u128,
487 endpoint_hint: Option<String>,
488 capabilities: OnlineNodeCapabilities,
489 publisher: DhtRegistrationPublisher,
490}
491
492impl OnlineNodeRegistration {
493 pub fn new(
495 heartbeat_interval: Duration,
496 ttl: Duration,
497 node_type: OnlineNodeType,
498 endpoint_hint: Option<String>,
499 additional_capabilities: Vec<String>,
500 ) -> Self {
501 Self {
502 heartbeat_interval,
503 ttl,
504 node_type,
505 started_at_ms: get_epoch_ms(),
506 endpoint_hint,
507 capabilities: OnlineNodeCapabilities::new(additional_capabilities),
508 publisher: DhtRegistrationPublisher::new(ONLINE_NODES_TOPIC),
509 }
510 }
511
512 pub fn validate_enabled_schedule(&self) -> Result<()> {
514 validate_online_node_registration_timing(true, self.heartbeat_interval, self.ttl)
515 }
516
517 pub fn default_capabilities() -> Vec<String> {
519 OnlineNodeCapabilities::default_labels()
520 }
521
522 pub fn add_capabilities<I>(&self, capabilities: I) -> Result<()>
524 where I: IntoIterator<Item = &'static str> {
525 self.capabilities.add_many(capabilities)
526 }
527
528 pub fn capabilities(&self) -> Result<Vec<String>> {
530 self.capabilities.labels()
531 }
532
533 pub fn descriptor_at(
535 &self,
536 context: &RegistrationContext<'_>,
537 now_ms: u128,
538 ) -> Result<OnlineNodeDescriptor> {
539 OnlineNodeDescriptor::new_signed(
540 OnlineNodeDescriptorBody {
541 did: context.did(),
542 public_key: context.account_verification_pubkey()?,
543 session_public_key: context.session_sk().session_public_key(),
544 node_type: self.node_type.clone(),
545 network_id: context.network_id(),
546 storage_redundancy: context.storage_redundancy(),
547 dht_virtual_nodes: context.dht_virtual_nodes(),
548 capabilities: self.capabilities()?,
549 endpoint_hint: self.endpoint_hint.clone(),
550 started_at_ms: self.started_at_ms,
551 heartbeat_at_ms: now_ms,
552 expires_at_ms: now_ms + self.ttl.as_millis(),
553 version: crate::util::build_version(),
554 },
555 context.session_sk(),
556 )
557 .map_err(Error::CoreError)
558 }
559
560 pub async fn publish_descriptor(
562 &self,
563 context: &RegistrationContext<'_>,
564 ) -> Result<OnlineNodeDescriptor> {
565 let now_ms = get_epoch_ms();
566 let descriptor = self.descriptor_at(context, now_ms)?;
567 let encoded = descriptor.encode().map_err(Error::CoreError)?;
568 self.publisher
569 .publish_many_replacing_and_compacting(
570 context,
571 std::iter::once(encoded),
572 |observed| {
573 observed
574 .decode::<OnlineNodeDescriptor>()
575 .is_ok_and(|descriptor| {
576 descriptor.did == context.did()
577 || (descriptor.verify_signature()
578 && descriptor.is_expired_at(now_ms))
579 })
580 },
581 |observed| {
582 observed
583 .decode::<OnlineNodeDescriptor>()
584 .is_ok_and(|descriptor| {
585 descriptor.verify_signature() && !descriptor.is_expired_at(now_ms)
586 })
587 },
588 )
589 .await?;
590 Ok(descriptor)
591 }
592
593 pub fn descriptors_from_entry(
595 entry: &rings_core::dht::entry::Entry,
596 ) -> Vec<OnlineNodeDescriptor> {
597 entry
598 .data
599 .iter()
600 .filter_map(|value| value.decode::<OnlineNodeDescriptor>().ok())
601 .collect()
602 }
603}
604
605#[cfg_attr(all(feature = "browser", target_family = "wasm"), async_trait(?Send))]
606#[cfg_attr(not(all(feature = "browser", target_family = "wasm")), async_trait)]
607impl RegistrationTask for OnlineNodeRegistration {
608 fn name(&self) -> &'static str {
609 "online-node"
610 }
611
612 fn interval(&self) -> Duration {
613 self.heartbeat_interval
614 }
615
616 async fn register_once(&self, context: &RegistrationContext<'_>) -> Result<()> {
617 self.publish_descriptor(context).await.map(|_| ())
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use std::collections::BTreeSet;
624
625 use rings_core::message::Encoded;
626
627 use super::*;
628
629 fn encoded(value: &str) -> Encoded {
630 value.into()
631 }
632
633 fn encoded_subset(mask: u8) -> BTreeSet<Encoded> {
634 ["a", "b", "c"]
635 .into_iter()
636 .enumerate()
637 .filter(|(bit, _value)| mask & (1 << bit) != 0)
638 .map(|(_bit, value)| encoded(value))
639 .collect()
640 }
641
642 #[test]
643 fn test_registration_publish_remembers_attempted_values_before_effects() {
644 let old = encoded("old");
645 let attempted = encoded("attempted");
646 let current = BTreeSet::from([attempted.clone()]);
647 let mut known = BTreeSet::from([old.clone()]);
648
649 let stale = begin_registration_publish(&mut known, ¤t, vec![], |_| false);
650
651 assert_eq!(stale, vec![old.clone()]);
652 assert_eq!(known, BTreeSet::from([old, attempted]));
653 }
654
655 #[test]
656 fn test_registration_publish_retry_tombstones_values_from_cancelled_attempts() {
657 let old = encoded("old");
658 let cancelled = encoded("cancelled");
659 let replacement = encoded("replacement");
660 let mut known = BTreeSet::from([old.clone()]);
661
662 let cancelled_current = BTreeSet::from([cancelled.clone()]);
663 let _ = begin_registration_publish(&mut known, &cancelled_current, vec![], |_| false);
664 let replacement_current = BTreeSet::from([replacement.clone()]);
665 let stale = begin_registration_publish(&mut known, &replacement_current, vec![], |_| false);
666
667 assert_eq!(
668 stale.into_iter().collect::<BTreeSet<_>>(),
669 BTreeSet::from([old, cancelled])
670 );
671 assert!(known.contains(&replacement));
672 finish_registration_publish(&mut known, replacement_current.clone());
673 assert_eq!(known, replacement_current);
674 }
675
676 #[test]
677 fn test_registration_publish_begin_finish_preserve_known_set_law() {
678 for old_mask in 0..8 {
679 for current_mask in 0..8 {
680 for replacement_mask in 0..8 {
681 let old = encoded_subset(old_mask);
682 let current = encoded_subset(current_mask);
683 let replacement = encoded_subset(replacement_mask);
684 let mut known = old.clone();
685
686 let _ = begin_registration_publish(&mut known, ¤t, vec![], |_| false);
687 let attempted = old.union(¤t).cloned().collect::<BTreeSet<_>>();
688 assert_eq!(known, attempted);
689
690 let stale =
691 begin_registration_publish(&mut known, &replacement, vec![], |_| false)
692 .into_iter()
693 .collect::<BTreeSet<_>>();
694 let expected_stale = attempted
695 .difference(&replacement)
696 .cloned()
697 .collect::<BTreeSet<_>>();
698 assert_eq!(stale, expected_stale);
699
700 finish_registration_publish(&mut known, replacement.clone());
701 assert_eq!(known, replacement);
702 }
703 }
704 }
705 }
706
707 #[test]
708 fn test_registration_publish_tombstones_matching_observed_values() {
709 let current = BTreeSet::from([encoded("self-new")]);
710 let observed_self_old = encoded("self-old");
711 let observed_other = encoded("other");
712 let mut known = BTreeSet::new();
713
714 let stale = begin_registration_publish(
715 &mut known,
716 ¤t,
717 vec![observed_self_old.clone(), observed_other],
718 |observed| observed == &observed_self_old,
719 );
720
721 assert_eq!(stale, vec![observed_self_old]);
722 assert_eq!(known, current);
723 }
724
725 #[test]
726 fn test_registration_pruning_removes_replaced_or_unpreserved_observed_values() {
727 let observed_self_old = encoded("self-old");
728 let observed_live = encoded("other-live");
729 let observed_invalid = encoded("invalid");
730
731 let should_prune = |observed: &Encoded| {
732 should_prune_observed_registry_value(
733 observed,
734 &|value| value == &observed_self_old,
735 &|value| value == &observed_live,
736 )
737 };
738
739 assert!(should_prune(&observed_self_old));
740 assert!(!should_prune(&observed_live));
741 assert!(should_prune(&observed_invalid));
742 }
743
744 #[test]
745 fn test_registration_publish_tombstones_unpreserved_observed_values() {
746 let current = BTreeSet::from([encoded("self-new")]);
747 let observed_self_old = encoded("self-old");
748 let observed_live = encoded("other-live");
749 let observed_invalid = encoded("invalid");
750 let mut known = BTreeSet::new();
751
752 let stale = begin_registration_publish(
753 &mut known,
754 ¤t,
755 vec![
756 observed_self_old.clone(),
757 observed_live,
758 observed_invalid.clone(),
759 ],
760 |observed| observed == &observed_self_old || observed == &observed_invalid,
761 );
762
763 assert_eq!(
764 stale.into_iter().collect::<BTreeSet<_>>(),
765 BTreeSet::from([observed_invalid, observed_self_old])
766 );
767 assert_eq!(known, current);
768 }
769}