1#[cfg(feature = "memory-storage")]
10use crate::beef_limits;
11
12use async_trait::async_trait;
13
14use crate::types::{AppliedTransaction, Outpoint, Output};
15
16#[derive(Debug, Clone)]
21pub struct TransactionBeef {
22 pub txid: String,
24 pub beef: Vec<u8>,
26}
27
28#[async_trait(?Send)]
33pub trait Storage {
34 async fn insert_output(&self, output: &Output) -> Result<(), StorageError>;
44
45 async fn delete_output(
50 &self,
51 txid: &str,
52 output_index: u32,
53 topic: &str,
54 ) -> Result<(), StorageError>;
55
56 async fn mark_utxo_as_spent(
58 &self,
59 txid: &str,
60 output_index: u32,
61 topic: &str,
62 ) -> Result<(), StorageError>;
63
64 async fn update_consumed_by(
66 &self,
67 txid: &str,
68 output_index: u32,
69 topic: &str,
70 consumed_by: &[Outpoint],
71 ) -> Result<(), StorageError>;
72
73 async fn update_transaction_beef(&self, txid: &str, beef: &[u8]) -> Result<(), StorageError>;
75
76 async fn mark_transaction_proven(&self, txid: &str) -> Result<(), StorageError> {
94 let _ = txid;
95 Ok(())
96 }
97
98 async fn mark_transaction_proven_at(
105 &self,
106 txid: &str,
107 height: Option<u64>,
108 ) -> Result<(), StorageError> {
109 let _ = height;
110 self.mark_transaction_proven(txid).await
111 }
112
113 async fn update_output_block_height(
117 &self,
118 txid: &str,
119 output_index: u32,
120 topic: &str,
121 block_height: u32,
122 ) -> Result<(), StorageError> {
123 let _ = (txid, output_index, topic, block_height);
124 Ok(())
125 }
126
127 async fn insert_applied_transaction(&self, tx: &AppliedTransaction)
129 -> Result<(), StorageError>;
130
131 async fn does_applied_transaction_exist(
133 &self,
134 tx: &AppliedTransaction,
135 ) -> Result<bool, StorageError>;
136
137 async fn delete_applied_transaction(&self, tx: &AppliedTransaction)
143 -> Result<(), StorageError>;
144
145 async fn find_output(
154 &self,
155 txid: &str,
156 output_index: u32,
157 topic: Option<&str>,
158 spent: Option<bool>,
159 include_beef: bool,
160 ) -> Result<Option<Output>, StorageError>;
161
162 async fn find_outputs_by_outpoints(
166 &self,
167 outpoints: &[Outpoint],
168 include_beef: bool,
169 ) -> Result<Vec<Output>, StorageError> {
170 let mut results = Vec::with_capacity(outpoints.len());
171 for op in outpoints {
172 if let Some(output) = self
173 .find_output(&op.txid, op.output_index, None, None, include_beef)
174 .await?
175 {
176 results.push(output);
177 }
178 }
179 Ok(results)
180 }
181
182 async fn find_served_outputs(
185 &self,
186 outpoints: &[Outpoint],
187 ) -> Result<Vec<Output>, StorageError> {
188 self.find_outputs_by_outpoints(outpoints, true).await
189 }
190
191 async fn find_outputs_for_transaction(
193 &self,
194 txid: &str,
195 include_beef: bool,
196 ) -> Result<Vec<Output>, StorageError>;
197
198 async fn find_utxos_for_topic(
203 &self,
204 topic: &str,
205 since: Option<f64>,
206 limit: Option<u64>,
207 include_beef: bool,
208 ) -> Result<Vec<Output>, StorageError>;
209
210 async fn find_transactions_for_proof_check(
242 &self,
243 limit: u64,
244 min_age_secs: u64,
245 ) -> Result<Vec<TransactionBeef>, StorageError> {
246 let _ = (limit, min_age_secs);
247 Ok(Vec::new())
248 }
249
250 async fn update_last_interaction(
256 &self,
257 host: &str,
258 topic: &str,
259 since: u64,
260 ) -> Result<(), StorageError>;
261
262 async fn get_last_interaction(&self, host: &str, topic: &str) -> Result<u64, StorageError>;
264
265 async fn record_peer_sync_outcome(
288 &self,
289 host: &str,
290 topic: &str,
291 success: bool,
292 ) -> Result<(), StorageError>;
293
294 async fn get_peer_sync_health(
299 &self,
300 host: &str,
301 topic: &str,
302 ) -> Result<PeerSyncHealth, StorageError>;
303
304 async fn record_peer_sync_yield(
322 &self,
323 host: &str,
324 topic: &str,
325 yielded: bool,
326 ) -> Result<PeerYieldStreak, StorageError>;
327
328 async fn put_deferred_graph(
361 &self,
362 record: &crate::gasp::DeferredGraph,
363 ) -> Result<crate::gasp::DeferredGraphSave, StorageError>;
364
365 async fn find_deferred_graphs(
369 &self,
370 host: &str,
371 topic: &str,
372 ) -> Result<Vec<crate::gasp::DeferredGraphKey>, StorageError>;
373
374 async fn get_deferred_graph(
376 &self,
377 host: &str,
378 topic: &str,
379 outpoint: &str,
380 ) -> Result<Option<crate::gasp::DeferredGraph>, StorageError>;
381
382 async fn delete_deferred_graph(
384 &self,
385 host: &str,
386 topic: &str,
387 outpoint: &str,
388 ) -> Result<(), StorageError>;
389
390 async fn remember_expired_outpoint(
407 &self,
408 _topic: &str,
409 _outpoint: &str,
410 _expiry: u64,
411 ) -> Result<(), StorageError> {
412 Ok(())
413 }
414
415 async fn find_expired_outpoints(
418 &self,
419 _topic: &str,
420 _outpoints: &[String],
421 ) -> Result<Vec<String>, StorageError> {
422 Ok(Vec::new())
423 }
424}
425
426#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
429pub struct PeerYieldStreak {
430 pub yieldless_syncs: u64,
432 pub secs_since_first: Option<u64>,
435}
436
437#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
440pub struct PeerSyncHealth {
441 pub consecutive_failures: u64,
444 pub secs_since_last_attempt: Option<u64>,
447}
448
449#[async_trait(?Send)]
458impl<T: Storage + ?Sized> Storage for std::rc::Rc<T> {
459 async fn insert_output(&self, output: &Output) -> Result<(), StorageError> {
460 (**self).insert_output(output).await
461 }
462 async fn delete_output(
463 &self,
464 txid: &str,
465 output_index: u32,
466 topic: &str,
467 ) -> Result<(), StorageError> {
468 (**self).delete_output(txid, output_index, topic).await
469 }
470 async fn mark_utxo_as_spent(
471 &self,
472 txid: &str,
473 output_index: u32,
474 topic: &str,
475 ) -> Result<(), StorageError> {
476 (**self).mark_utxo_as_spent(txid, output_index, topic).await
477 }
478 async fn update_consumed_by(
479 &self,
480 txid: &str,
481 output_index: u32,
482 topic: &str,
483 consumed_by: &[Outpoint],
484 ) -> Result<(), StorageError> {
485 (**self)
486 .update_consumed_by(txid, output_index, topic, consumed_by)
487 .await
488 }
489 async fn update_transaction_beef(&self, txid: &str, beef: &[u8]) -> Result<(), StorageError> {
490 (**self).update_transaction_beef(txid, beef).await
491 }
492 async fn mark_transaction_proven(&self, txid: &str) -> Result<(), StorageError> {
493 (**self).mark_transaction_proven(txid).await
494 }
495 async fn mark_transaction_proven_at(
496 &self,
497 txid: &str,
498 height: Option<u64>,
499 ) -> Result<(), StorageError> {
500 (**self).mark_transaction_proven_at(txid, height).await
504 }
505 async fn update_output_block_height(
506 &self,
507 txid: &str,
508 output_index: u32,
509 topic: &str,
510 block_height: u32,
511 ) -> Result<(), StorageError> {
512 (**self)
513 .update_output_block_height(txid, output_index, topic, block_height)
514 .await
515 }
516 async fn insert_applied_transaction(
517 &self,
518 tx: &AppliedTransaction,
519 ) -> Result<(), StorageError> {
520 (**self).insert_applied_transaction(tx).await
521 }
522 async fn does_applied_transaction_exist(
523 &self,
524 tx: &AppliedTransaction,
525 ) -> Result<bool, StorageError> {
526 (**self).does_applied_transaction_exist(tx).await
527 }
528 async fn delete_applied_transaction(
529 &self,
530 tx: &AppliedTransaction,
531 ) -> Result<(), StorageError> {
532 (**self).delete_applied_transaction(tx).await
533 }
534 async fn find_output(
535 &self,
536 txid: &str,
537 output_index: u32,
538 topic: Option<&str>,
539 spent: Option<bool>,
540 include_beef: bool,
541 ) -> Result<Option<Output>, StorageError> {
542 (**self)
543 .find_output(txid, output_index, topic, spent, include_beef)
544 .await
545 }
546 async fn find_served_outputs(
547 &self,
548 outpoints: &[Outpoint],
549 ) -> Result<Vec<Output>, StorageError> {
550 (**self).find_served_outputs(outpoints).await
551 }
552
553 async fn find_outputs_for_transaction(
554 &self,
555 txid: &str,
556 include_beef: bool,
557 ) -> Result<Vec<Output>, StorageError> {
558 (**self)
559 .find_outputs_for_transaction(txid, include_beef)
560 .await
561 }
562 async fn find_utxos_for_topic(
563 &self,
564 topic: &str,
565 since: Option<f64>,
566 limit: Option<u64>,
567 include_beef: bool,
568 ) -> Result<Vec<Output>, StorageError> {
569 (**self)
570 .find_utxos_for_topic(topic, since, limit, include_beef)
571 .await
572 }
573 async fn find_transactions_for_proof_check(
574 &self,
575 limit: u64,
576 min_age_secs: u64,
577 ) -> Result<Vec<TransactionBeef>, StorageError> {
578 (**self)
579 .find_transactions_for_proof_check(limit, min_age_secs)
580 .await
581 }
582 async fn update_last_interaction(
583 &self,
584 host: &str,
585 topic: &str,
586 since: u64,
587 ) -> Result<(), StorageError> {
588 (**self).update_last_interaction(host, topic, since).await
589 }
590 async fn get_last_interaction(&self, host: &str, topic: &str) -> Result<u64, StorageError> {
591 (**self).get_last_interaction(host, topic).await
592 }
593 async fn record_peer_sync_outcome(
594 &self,
595 host: &str,
596 topic: &str,
597 success: bool,
598 ) -> Result<(), StorageError> {
599 (**self)
600 .record_peer_sync_outcome(host, topic, success)
601 .await
602 }
603 async fn get_peer_sync_health(
604 &self,
605 host: &str,
606 topic: &str,
607 ) -> Result<PeerSyncHealth, StorageError> {
608 (**self).get_peer_sync_health(host, topic).await
609 }
610 async fn record_peer_sync_yield(
611 &self,
612 host: &str,
613 topic: &str,
614 yielded: bool,
615 ) -> Result<PeerYieldStreak, StorageError> {
616 (**self).record_peer_sync_yield(host, topic, yielded).await
617 }
618 async fn put_deferred_graph(
619 &self,
620 record: &crate::gasp::DeferredGraph,
621 ) -> Result<crate::gasp::DeferredGraphSave, StorageError> {
622 (**self).put_deferred_graph(record).await
623 }
624 async fn find_deferred_graphs(
625 &self,
626 host: &str,
627 topic: &str,
628 ) -> Result<Vec<crate::gasp::DeferredGraphKey>, StorageError> {
629 (**self).find_deferred_graphs(host, topic).await
630 }
631 async fn get_deferred_graph(
632 &self,
633 host: &str,
634 topic: &str,
635 outpoint: &str,
636 ) -> Result<Option<crate::gasp::DeferredGraph>, StorageError> {
637 (**self).get_deferred_graph(host, topic, outpoint).await
638 }
639 async fn delete_deferred_graph(
640 &self,
641 host: &str,
642 topic: &str,
643 outpoint: &str,
644 ) -> Result<(), StorageError> {
645 (**self).delete_deferred_graph(host, topic, outpoint).await
646 }
647 async fn remember_expired_outpoint(
648 &self,
649 topic: &str,
650 outpoint: &str,
651 expiry: u64,
652 ) -> Result<(), StorageError> {
653 (**self)
654 .remember_expired_outpoint(topic, outpoint, expiry)
655 .await
656 }
657 async fn find_expired_outpoints(
658 &self,
659 topic: &str,
660 outpoints: &[String],
661 ) -> Result<Vec<String>, StorageError> {
662 (**self).find_expired_outpoints(topic, outpoints).await
663 }
664}
665
666#[derive(Debug, thiserror::Error)]
672pub enum StorageError {
673 #[error("not found: {0}")]
674 NotFound(String),
675
676 #[error("duplicate: {0}")]
677 Duplicate(String),
678
679 #[error("database error: {0}")]
680 Database(String),
681
682 #[error("serialization error: {0}")]
683 Serialization(String),
684
685 #[error("{0}")]
686 Other(String),
687}
688
689#[cfg(any(test, feature = "memory-storage"))]
694pub mod memory {
695 use super::*;
696 use std::collections::HashMap;
697 use std::sync::Mutex;
698
699 #[derive(Debug, Default)]
703 pub struct MemoryStorage {
704 outputs: Mutex<HashMap<(String, u32, String), Output>>,
706 transactions: Mutex<HashMap<String, Vec<u8>>>,
708 proven: Mutex<std::collections::HashSet<String>>,
716 applied: Mutex<HashMap<(String, String), bool>>,
718 sync_state: Mutex<HashMap<(String, String), u64>>,
720 clock_secs: Mutex<u64>,
725 created_at: Mutex<HashMap<String, u64>>,
729 peer_health: Mutex<HashMap<(String, String), (u64, u64)>>,
734 proven_at: Mutex<HashMap<String, Option<u64>>>,
738 deferred_graphs: Mutex<HashMap<(String, String, String), crate::gasp::DeferredGraph>>,
741 deferred_graph_writes: Mutex<u64>,
744 deferred_graph_reads: Mutex<u64>,
747 deferred_graph_keys_fault: Mutex<bool>,
749 deferred_graph_ceiling: Mutex<Option<usize>>,
752 deferred_graph_byte_ceiling: Mutex<Option<usize>>,
756 deferred_graph_put_fault: Mutex<bool>,
758 expired_outpoints: Mutex<HashMap<(String, String), (u64, u64)>>,
762 deferred_graph_put_attempts: Mutex<u64>,
765 peer_yieldless: Mutex<HashMap<(String, String), YieldStreakRow>>,
771 }
772
773 type YieldStreakRow = (u64, u64, u64);
776
777 impl MemoryStorage {
778 pub fn new() -> Self {
779 Self::default()
780 }
781
782 pub fn proven_at_height(&self, txid: &str) -> Option<Option<u64>> {
786 self.proven_at.lock().unwrap().get(txid).copied()
787 }
788
789 pub fn expired_outpoints(&self, topic: &str) -> Vec<String> {
794 let now = *self.clock_secs.lock().unwrap();
795 let mut held: Vec<String> = self
796 .expired_outpoints
797 .lock()
798 .unwrap()
799 .iter()
800 .filter(|((t, _), (keep, _))| t == topic && *keep > now)
801 .map(|((_, o), _)| o.clone())
802 .collect();
803 held.sort();
804 held
805 }
806
807 pub fn advance_clock(&self, secs: u64) {
808 *self.clock_secs.lock().unwrap() += secs;
809 }
810
811 fn stamp_created(&self, txid: &str) {
814 let now = *self.clock_secs.lock().unwrap();
815 self.created_at
816 .lock()
817 .unwrap()
818 .entry(txid.to_string())
819 .or_insert(now);
820 }
821
822 fn age_gate_open(&self, txid: &str, min_age_secs: u64) -> bool {
825 if min_age_secs == 0 {
826 return true;
827 }
828 let now = *self.clock_secs.lock().unwrap();
829 match self.created_at.lock().unwrap().get(txid) {
830 None => true, Some(created) => now.saturating_sub(*created) >= min_age_secs,
832 }
833 }
834
835 pub fn deferred_graphs(&self) -> Vec<crate::gasp::DeferredGraph> {
837 let mut all: Vec<_> = self
838 .deferred_graphs
839 .lock()
840 .unwrap()
841 .values()
842 .cloned()
843 .collect();
844 all.sort_by(|a, b| (a.score, &a.outpoint).cmp(&(b.score, &b.outpoint)));
845 all
846 }
847
848 pub fn deferred_graph_writes(&self) -> u64 {
850 *self.deferred_graph_writes.lock().unwrap()
851 }
852
853 pub fn deferred_graph_reads(&self) -> u64 {
855 *self.deferred_graph_reads.lock().unwrap()
856 }
857
858 pub fn set_deferred_graph_keys_fault(&self, fault: bool) {
861 *self.deferred_graph_keys_fault.lock().unwrap() = fault;
862 }
863
864 pub fn set_deferred_graph_ceiling(&self, ceiling: Option<usize>) {
867 *self.deferred_graph_ceiling.lock().unwrap() = ceiling;
868 }
869
870 pub fn set_deferred_graph_byte_ceiling(&self, ceiling: Option<usize>) {
874 *self.deferred_graph_byte_ceiling.lock().unwrap() = ceiling;
875 }
876
877 pub fn set_deferred_graph_put_fault(&self, fault: bool) {
880 *self.deferred_graph_put_fault.lock().unwrap() = fault;
881 }
882
883 pub fn deferred_graph_put_attempts(&self) -> u64 {
886 *self.deferred_graph_put_attempts.lock().unwrap()
887 }
888
889 pub fn peer_yieldless_syncs(&self, host: &str, topic: &str) -> u64 {
894 self.peer_yieldless
895 .lock()
896 .unwrap()
897 .get(&(crate::gasp::peer_origin(host), topic.to_string()))
898 .map_or(0, |(count, _, _)| *count)
899 }
900
901 pub fn output_count(&self) -> usize {
903 self.outputs.lock().unwrap().len()
904 }
905
906 pub fn transaction_count(&self) -> usize {
908 self.transactions.lock().unwrap().len()
909 }
910 }
911
912 #[async_trait(?Send)]
913 impl Storage for MemoryStorage {
914 async fn insert_output(&self, output: &Output) -> Result<(), StorageError> {
915 let key = (
916 output.txid.clone(),
917 output.output_index,
918 output.topic.clone(),
919 );
920 let mut outputs = self.outputs.lock().unwrap();
921
922 if outputs.contains_key(&key) {
924 if let Some(ref beef) = output.beef {
926 self.transactions
927 .lock()
928 .unwrap()
929 .entry(output.txid.clone())
930 .or_insert_with(|| beef.clone());
931 }
932 return Ok(());
933 }
934
935 let mut stored = output.clone();
937 stored.beef = None;
938 outputs.insert(key, stored);
939
940 if let Some(ref beef) = output.beef {
942 self.transactions
943 .lock()
944 .unwrap()
945 .entry(output.txid.clone())
946 .or_insert_with(|| beef.clone());
947 self.stamp_created(&output.txid);
948 }
949
950 Ok(())
951 }
952
953 async fn delete_output(
954 &self,
955 txid: &str,
956 output_index: u32,
957 topic: &str,
958 ) -> Result<(), StorageError> {
959 let key = (txid.to_string(), output_index, topic.to_string());
960 let mut outputs = self.outputs.lock().unwrap();
961 outputs.remove(&key);
962
963 let has_remaining = outputs.keys().any(|(t, _, _)| t == txid);
965 if !has_remaining {
966 self.transactions.lock().unwrap().remove(txid);
967 self.proven.lock().unwrap().remove(txid);
968 }
969
970 Ok(())
971 }
972
973 async fn mark_utxo_as_spent(
974 &self,
975 txid: &str,
976 output_index: u32,
977 topic: &str,
978 ) -> Result<(), StorageError> {
979 let key = (txid.to_string(), output_index, topic.to_string());
980 if let Some(output) = self.outputs.lock().unwrap().get_mut(&key) {
981 output.spent = true;
982 }
983 Ok(())
984 }
985
986 async fn update_consumed_by(
987 &self,
988 txid: &str,
989 output_index: u32,
990 topic: &str,
991 consumed_by: &[Outpoint],
992 ) -> Result<(), StorageError> {
993 let key = (txid.to_string(), output_index, topic.to_string());
994 if let Some(output) = self.outputs.lock().unwrap().get_mut(&key) {
995 output.consumed_by = consumed_by.to_vec();
996 }
997 Ok(())
998 }
999
1000 async fn update_transaction_beef(
1001 &self,
1002 txid: &str,
1003 beef: &[u8],
1004 ) -> Result<(), StorageError> {
1005 let has_proof = beef_limits::has_proof(beef, txid);
1011 if has_proof {
1012 self.proven.lock().unwrap().insert(txid.to_string());
1013 }
1014 self.transactions
1015 .lock()
1016 .unwrap()
1017 .insert(txid.to_string(), beef.to_vec());
1018 self.stamp_created(txid);
1021 Ok(())
1022 }
1023
1024 async fn mark_transaction_proven(&self, txid: &str) -> Result<(), StorageError> {
1025 self.proven.lock().unwrap().insert(txid.to_string());
1028 Ok(())
1029 }
1030
1031 async fn mark_transaction_proven_at(
1032 &self,
1033 txid: &str,
1034 height: Option<u64>,
1035 ) -> Result<(), StorageError> {
1036 self.proven.lock().unwrap().insert(txid.to_string());
1040 self.proven_at
1041 .lock()
1042 .unwrap()
1043 .insert(txid.to_string(), height);
1044 Ok(())
1045 }
1046
1047 async fn update_output_block_height(
1048 &self,
1049 txid: &str,
1050 output_index: u32,
1051 topic: &str,
1052 block_height: u32,
1053 ) -> Result<(), StorageError> {
1054 let key = (txid.to_string(), output_index, topic.to_string());
1055 if let Some(output) = self.outputs.lock().unwrap().get_mut(&key) {
1056 output.block_height = Some(block_height);
1057 }
1058 Ok(())
1059 }
1060
1061 async fn insert_applied_transaction(
1062 &self,
1063 tx: &AppliedTransaction,
1064 ) -> Result<(), StorageError> {
1065 self.applied
1066 .lock()
1067 .unwrap()
1068 .insert((tx.txid.clone(), tx.topic.clone()), true);
1069 Ok(())
1070 }
1071
1072 async fn does_applied_transaction_exist(
1073 &self,
1074 tx: &AppliedTransaction,
1075 ) -> Result<bool, StorageError> {
1076 Ok(self
1077 .applied
1078 .lock()
1079 .unwrap()
1080 .contains_key(&(tx.txid.clone(), tx.topic.clone())))
1081 }
1082
1083 async fn delete_applied_transaction(
1084 &self,
1085 tx: &AppliedTransaction,
1086 ) -> Result<(), StorageError> {
1087 self.applied
1088 .lock()
1089 .unwrap()
1090 .remove(&(tx.txid.clone(), tx.topic.clone()));
1091 Ok(())
1092 }
1093
1094 async fn find_output(
1095 &self,
1096 txid: &str,
1097 output_index: u32,
1098 topic: Option<&str>,
1099 spent: Option<bool>,
1100 include_beef: bool,
1101 ) -> Result<Option<Output>, StorageError> {
1102 let outputs = self.outputs.lock().unwrap();
1103
1104 if let Some(topic) = topic {
1106 let key = (txid.to_string(), output_index, topic.to_string());
1107 if let Some(output) = outputs.get(&key) {
1108 if let Some(s) = spent {
1109 if output.spent != s {
1110 return Ok(None);
1111 }
1112 }
1113 let mut result = output.clone();
1114 if include_beef {
1115 result.beef = self.transactions.lock().unwrap().get(txid).cloned();
1116 }
1117 return Ok(Some(result));
1118 }
1119 return Ok(None);
1120 }
1121
1122 for ((t, oi, _), output) in outputs.iter() {
1124 if t == txid && *oi == output_index {
1125 if let Some(s) = spent {
1126 if output.spent != s {
1127 continue;
1128 }
1129 }
1130 let mut result = output.clone();
1131 if include_beef {
1132 result.beef = self.transactions.lock().unwrap().get(txid).cloned();
1133 }
1134 return Ok(Some(result));
1135 }
1136 }
1137 Ok(None)
1138 }
1139
1140 async fn find_served_outputs(
1141 &self,
1142 outpoints: &[Outpoint],
1143 ) -> Result<Vec<Output>, StorageError> {
1144 let outputs = self.find_outputs_by_outpoints(outpoints, true).await?;
1145 let now = i64::try_from(*self.clock_secs.lock().unwrap())
1146 .unwrap_or(i64::MAX)
1147 .saturating_mul(1000);
1148 let proven = self.proven.lock().unwrap();
1149 let stamps = self.created_at.lock().unwrap();
1150 Ok(outputs
1151 .into_iter()
1152 .filter(|output| {
1153 let seen = stamps
1154 .get(&output.txid)
1155 .and_then(|s| i64::try_from(*s).ok())
1156 .map(|s| s.saturating_mul(1000));
1157 crate::proof_window::verdict(proven.contains(&output.txid), seen, now)
1158 == crate::proof_window::Verdict::Served
1159 })
1160 .collect())
1161 }
1162
1163 async fn find_outputs_for_transaction(
1164 &self,
1165 txid: &str,
1166 include_beef: bool,
1167 ) -> Result<Vec<Output>, StorageError> {
1168 let outputs = self.outputs.lock().unwrap();
1169 let beef = if include_beef {
1170 self.transactions.lock().unwrap().get(txid).cloned()
1171 } else {
1172 None
1173 };
1174
1175 let results: Vec<Output> = outputs
1176 .iter()
1177 .filter(|((t, _, _), _)| t == txid)
1178 .map(|(_, output)| {
1179 let mut o = output.clone();
1180 if include_beef {
1181 o.beef.clone_from(&beef);
1182 }
1183 o
1184 })
1185 .collect();
1186
1187 Ok(results)
1188 }
1189
1190 async fn find_utxos_for_topic(
1191 &self,
1192 topic: &str,
1193 since: Option<f64>,
1194 limit: Option<u64>,
1195 include_beef: bool,
1196 ) -> Result<Vec<Output>, StorageError> {
1197 let outputs = self.outputs.lock().unwrap();
1198 let transactions = self.transactions.lock().unwrap();
1199
1200 let mut results: Vec<Output> = outputs
1201 .iter()
1202 .filter(|((_, _, t), o)| {
1203 t == topic && !o.spent && since.is_none_or(|s| o.score.unwrap_or(0.0) >= s)
1204 })
1205 .map(|(_, output)| {
1206 let mut o = output.clone();
1207 if include_beef {
1208 o.beef = transactions.get(&o.txid).cloned();
1209 }
1210 o
1211 })
1212 .collect();
1213
1214 results.sort_by(|a, b| {
1216 a.score
1217 .unwrap_or(0.0)
1218 .partial_cmp(&b.score.unwrap_or(0.0))
1219 .unwrap_or(std::cmp::Ordering::Equal)
1220 });
1221
1222 if let Some(limit) = limit {
1223 results.truncate(limit as usize);
1224 }
1225
1226 Ok(results)
1227 }
1228
1229 async fn find_transactions_for_proof_check(
1230 &self,
1231 limit: u64,
1232 min_age_secs: u64,
1233 ) -> Result<Vec<TransactionBeef>, StorageError> {
1234 let candidates: Vec<TransactionBeef> = {
1247 let transactions = self.transactions.lock().unwrap();
1248 let proven = self.proven.lock().unwrap();
1249 transactions
1250 .iter()
1251 .filter(|(txid, _)| !proven.contains(*txid))
1252 .map(|(txid, beef)| TransactionBeef {
1253 txid: txid.clone(),
1254 beef: beef.clone(),
1255 })
1256 .collect()
1257 };
1258 Ok(candidates
1259 .into_iter()
1260 .filter(|c| self.age_gate_open(&c.txid, min_age_secs))
1261 .take(limit as usize)
1262 .collect())
1263 }
1264
1265 async fn update_last_interaction(
1266 &self,
1267 host: &str,
1268 topic: &str,
1269 since: u64,
1270 ) -> Result<(), StorageError> {
1271 self.sync_state
1272 .lock()
1273 .unwrap()
1274 .insert((host.to_string(), topic.to_string()), since);
1275 Ok(())
1276 }
1277
1278 async fn get_last_interaction(&self, host: &str, topic: &str) -> Result<u64, StorageError> {
1279 Ok(*self
1280 .sync_state
1281 .lock()
1282 .unwrap()
1283 .get(&(host.to_string(), topic.to_string()))
1284 .unwrap_or(&0))
1285 }
1286
1287 async fn record_peer_sync_outcome(
1288 &self,
1289 host: &str,
1290 topic: &str,
1291 success: bool,
1292 ) -> Result<(), StorageError> {
1293 let now = *self.clock_secs.lock().unwrap();
1294 let mut health = self.peer_health.lock().unwrap();
1295 let entry = health
1296 .entry((host.to_string(), topic.to_string()))
1297 .or_insert((0, now));
1298 entry.0 = if success { 0 } else { entry.0 + 1 };
1299 entry.1 = now;
1300 Ok(())
1301 }
1302
1303 async fn get_peer_sync_health(
1304 &self,
1305 host: &str,
1306 topic: &str,
1307 ) -> Result<PeerSyncHealth, StorageError> {
1308 let now = *self.clock_secs.lock().unwrap();
1309 Ok(self
1310 .peer_health
1311 .lock()
1312 .unwrap()
1313 .get(&(host.to_string(), topic.to_string()))
1314 .map(|(fails, attempt_at)| PeerSyncHealth {
1315 consecutive_failures: *fails,
1316 secs_since_last_attempt: Some(now.saturating_sub(*attempt_at)),
1317 })
1318 .unwrap_or_default())
1319 }
1320
1321 async fn record_peer_sync_yield(
1322 &self,
1323 host: &str,
1324 topic: &str,
1325 yielded: bool,
1326 ) -> Result<PeerYieldStreak, StorageError> {
1327 let now = *self.clock_secs.lock().unwrap();
1328 let key = (host.to_string(), topic.to_string());
1329 let not_failed = self
1332 .peer_health
1333 .lock()
1334 .unwrap()
1335 .get(&key)
1336 .is_none_or(|(fails, _)| *fails == 0);
1337 let mut held = self.peer_yieldless.lock().unwrap();
1338 if yielded {
1339 held.remove(&key);
1340 return Ok(PeerYieldStreak::default());
1341 }
1342 let streak = held.entry(key).or_insert((0, now, now));
1343 if streak.0 == 0
1344 || (not_failed
1345 && now.saturating_sub(streak.2) > crate::gasp::PEER_YIELDLESS_DECAY_SECS)
1346 {
1347 *streak = (0, now, now);
1348 }
1349 streak.0 += 1;
1350 streak.2 = now;
1351 Ok(PeerYieldStreak {
1352 yieldless_syncs: streak.0,
1353 secs_since_first: Some(now.saturating_sub(streak.1)),
1354 })
1355 }
1356
1357 async fn put_deferred_graph(
1358 &self,
1359 record: &crate::gasp::DeferredGraph,
1360 ) -> Result<crate::gasp::DeferredGraphSave, StorageError> {
1361 let key = (
1362 record.peer.clone(),
1363 record.topic.clone(),
1364 record.outpoint.clone(),
1365 );
1366 *self.deferred_graph_put_attempts.lock().unwrap() += 1;
1367 if *self.deferred_graph_put_fault.lock().unwrap() {
1368 return Err(StorageError::Database("deferred graph save faulted".into()));
1369 }
1370 if let Some(ceiling) = *self.deferred_graph_ceiling.lock().unwrap() {
1371 let held = self.deferred_graphs.lock().unwrap();
1372 if !held.contains_key(&key) && held.len() >= ceiling {
1373 return Ok(crate::gasp::DeferredGraphSave::AtCeiling);
1374 }
1375 }
1376 if let Some(ceiling) = *self.deferred_graph_byte_ceiling.lock().unwrap() {
1377 let held = self.deferred_graphs.lock().unwrap();
1378 let others: usize = held
1379 .iter()
1380 .filter(|(k, _)| **k != key)
1381 .map(|(_, r)| r.byte_size())
1382 .sum();
1383 if others + record.byte_size() > ceiling {
1384 return Ok(crate::gasp::DeferredGraphSave::AtCeiling);
1385 }
1386 }
1387 *self.deferred_graph_writes.lock().unwrap() += 1;
1388 self.deferred_graphs.lock().unwrap().insert(
1389 (
1390 record.peer.clone(),
1391 record.topic.clone(),
1392 record.outpoint.clone(),
1393 ),
1394 record.clone(),
1395 );
1396 Ok(crate::gasp::DeferredGraphSave::Saved)
1397 }
1398
1399 async fn find_deferred_graphs(
1400 &self,
1401 host: &str,
1402 topic: &str,
1403 ) -> Result<Vec<crate::gasp::DeferredGraphKey>, StorageError> {
1404 if *self.deferred_graph_keys_fault.lock().unwrap() {
1405 return Err(StorageError::Database("deferred graph keys: fault".into()));
1406 }
1407 Ok(self
1408 .deferred_graphs()
1409 .into_iter()
1410 .filter(|r| r.peer == host && r.topic == topic)
1411 .map(|r| crate::gasp::DeferredGraphKey {
1412 outpoint: r.outpoint,
1413 score: r.score,
1414 })
1415 .collect())
1416 }
1417
1418 async fn get_deferred_graph(
1419 &self,
1420 host: &str,
1421 topic: &str,
1422 outpoint: &str,
1423 ) -> Result<Option<crate::gasp::DeferredGraph>, StorageError> {
1424 *self.deferred_graph_reads.lock().unwrap() += 1;
1425 Ok(self
1426 .deferred_graphs
1427 .lock()
1428 .unwrap()
1429 .get(&(host.to_string(), topic.to_string(), outpoint.to_string()))
1430 .cloned())
1431 }
1432
1433 async fn delete_deferred_graph(
1434 &self,
1435 host: &str,
1436 topic: &str,
1437 outpoint: &str,
1438 ) -> Result<(), StorageError> {
1439 self.deferred_graphs.lock().unwrap().remove(&(
1440 host.to_string(),
1441 topic.to_string(),
1442 outpoint.to_string(),
1443 ));
1444 Ok(())
1445 }
1446
1447 async fn remember_expired_outpoint(
1448 &self,
1449 topic: &str,
1450 outpoint: &str,
1451 expiry: u64,
1452 ) -> Result<(), StorageError> {
1453 let now = *self.clock_secs.lock().unwrap();
1454 let mut held = self.expired_outpoints.lock().unwrap();
1455 held.retain(|_, (keep, _)| *keep > now);
1456 held.insert(
1457 (topic.to_string(), outpoint.to_string()),
1458 (
1459 expiry.max(now) + crate::gasp::EXPIRED_OUTPOINT_MEMO_SECS,
1460 now,
1461 ),
1462 );
1463 let mut of_topic: Vec<(u64, String)> = held
1465 .iter()
1466 .filter(|((t, _), _)| t == topic)
1467 .map(|((_, o), (_, at))| (*at, o.clone()))
1468 .collect();
1469 let cap = crate::gasp::EXPIRED_OUTPOINT_MEMO_MAX_ROWS;
1470 if of_topic.len() > cap {
1471 of_topic.sort();
1472 for (_, o) in &of_topic[..of_topic.len() - cap] {
1473 held.remove(&(topic.to_string(), o.clone()));
1474 }
1475 }
1476 Ok(())
1477 }
1478
1479 async fn find_expired_outpoints(
1480 &self,
1481 topic: &str,
1482 outpoints: &[String],
1483 ) -> Result<Vec<String>, StorageError> {
1484 let now = *self.clock_secs.lock().unwrap();
1485 let held = self.expired_outpoints.lock().unwrap();
1486 Ok(outpoints
1487 .iter()
1488 .filter(|o| {
1489 held.get(&(topic.to_string(), (*o).clone()))
1490 .is_some_and(|(keep, _)| *keep > now)
1491 })
1492 .cloned()
1493 .collect())
1494 }
1495 }
1496}
1497
1498#[cfg(test)]
1503mod tests {
1504 use super::memory::MemoryStorage;
1505 use super::*;
1506 use crate::types::AppliedTransaction;
1507
1508 #[tokio::test]
1513 async fn the_rc_blanket_forwards_the_anchored_proven_latch() {
1514 let store = std::rc::Rc::new(MemoryStorage::new());
1515 let via_dyn: std::rc::Rc<dyn Storage> = store.clone();
1516 via_dyn
1517 .mark_transaction_proven_at("tx", Some(965_772))
1518 .await
1519 .unwrap();
1520 assert_eq!(
1521 store.proven_at_height("tx"),
1522 Some(Some(965_772)),
1523 "the Rc blanket forwarded the height to the inner store"
1524 );
1525 via_dyn
1527 .mark_transaction_proven_at("tx2", None)
1528 .await
1529 .unwrap();
1530 assert_eq!(store.proven_at_height("tx2"), Some(None));
1531 assert_eq!(
1532 store.proven_at_height("never"),
1533 None,
1534 "a txid never latched is absent"
1535 );
1536 }
1537
1538 fn make_output(txid: &str, index: u32, topic: &str, score: f64) -> Output {
1539 Output {
1540 txid: txid.to_string(),
1541 output_index: index,
1542 output_script: vec![0x76, 0xa9],
1543 satoshis: 1000,
1544 topic: topic.to_string(),
1545 spent: false,
1546 outputs_consumed: vec![],
1547 consumed_by: vec![],
1548 beef: Some(vec![0xBE, 0xEF]),
1549 block_height: None,
1550 score: Some(score),
1551 }
1552 }
1553
1554 #[tokio::test]
1555 async fn test_insert_and_find_output() {
1556 let store = MemoryStorage::new();
1557 let output = make_output("abc", 0, "tm_test", 1.0);
1558 store.insert_output(&output).await.unwrap();
1559
1560 let found = store
1562 .find_output("abc", 0, Some("tm_test"), None, false)
1563 .await
1564 .unwrap()
1565 .unwrap();
1566 assert_eq!(found.txid, "abc");
1567 assert_eq!(found.output_index, 0);
1568 assert!(found.beef.is_none());
1569
1570 let found = store
1572 .find_output("abc", 0, Some("tm_test"), None, true)
1573 .await
1574 .unwrap()
1575 .unwrap();
1576 assert_eq!(found.beef.unwrap(), vec![0xBE, 0xEF]);
1577 }
1578
1579 #[tokio::test]
1580 async fn test_insert_duplicate_is_noop() {
1581 let store = MemoryStorage::new();
1582 let output = make_output("abc", 0, "tm_test", 1.0);
1583 store.insert_output(&output).await.unwrap();
1584 store.insert_output(&output).await.unwrap();
1585 assert_eq!(store.output_count(), 1);
1586 }
1587
1588 #[tokio::test]
1589 async fn test_delete_output_cleans_up_beef() {
1590 let store = MemoryStorage::new();
1591 let output = make_output("abc", 0, "tm_test", 1.0);
1592 store.insert_output(&output).await.unwrap();
1593 assert_eq!(store.transaction_count(), 1);
1594
1595 store.delete_output("abc", 0, "tm_test").await.unwrap();
1596 assert_eq!(store.output_count(), 0);
1597 assert_eq!(store.transaction_count(), 0); }
1599
1600 #[tokio::test]
1601 async fn test_delete_output_keeps_beef_if_other_outputs_exist() {
1602 let store = MemoryStorage::new();
1603 store
1604 .insert_output(&make_output("abc", 0, "tm_test", 1.0))
1605 .await
1606 .unwrap();
1607 store
1608 .insert_output(&make_output("abc", 1, "tm_test", 2.0))
1609 .await
1610 .unwrap();
1611 assert_eq!(store.output_count(), 2);
1612 assert_eq!(store.transaction_count(), 1);
1613
1614 store.delete_output("abc", 0, "tm_test").await.unwrap();
1615 assert_eq!(store.output_count(), 1);
1616 assert_eq!(store.transaction_count(), 1); }
1618
1619 #[tokio::test]
1620 async fn test_mark_utxo_as_spent() {
1621 let store = MemoryStorage::new();
1622 store
1623 .insert_output(&make_output("abc", 0, "tm_test", 1.0))
1624 .await
1625 .unwrap();
1626
1627 store.mark_utxo_as_spent("abc", 0, "tm_test").await.unwrap();
1628
1629 let found = store
1630 .find_output("abc", 0, Some("tm_test"), None, false)
1631 .await
1632 .unwrap()
1633 .unwrap();
1634 assert!(found.spent);
1635 }
1636
1637 #[tokio::test]
1638 async fn test_find_output_with_spent_filter() {
1639 let store = MemoryStorage::new();
1640 store
1641 .insert_output(&make_output("abc", 0, "tm_test", 1.0))
1642 .await
1643 .unwrap();
1644
1645 assert!(store
1647 .find_output("abc", 0, Some("tm_test"), Some(false), false)
1648 .await
1649 .unwrap()
1650 .is_some());
1651 assert!(store
1652 .find_output("abc", 0, Some("tm_test"), Some(true), false)
1653 .await
1654 .unwrap()
1655 .is_none());
1656
1657 store.mark_utxo_as_spent("abc", 0, "tm_test").await.unwrap();
1658
1659 assert!(store
1661 .find_output("abc", 0, Some("tm_test"), Some(true), false)
1662 .await
1663 .unwrap()
1664 .is_some());
1665 assert!(store
1666 .find_output("abc", 0, Some("tm_test"), Some(false), false)
1667 .await
1668 .unwrap()
1669 .is_none());
1670 }
1671
1672 #[tokio::test]
1673 async fn test_update_consumed_by() {
1674 let store = MemoryStorage::new();
1675 store
1676 .insert_output(&make_output("abc", 0, "tm_test", 1.0))
1677 .await
1678 .unwrap();
1679
1680 let consumed = vec![Outpoint::new("def", 0), Outpoint::new("ghi", 1)];
1681 store
1682 .update_consumed_by("abc", 0, "tm_test", &consumed)
1683 .await
1684 .unwrap();
1685
1686 let found = store
1687 .find_output("abc", 0, Some("tm_test"), None, false)
1688 .await
1689 .unwrap()
1690 .unwrap();
1691 assert_eq!(found.consumed_by.len(), 2);
1692 assert_eq!(found.consumed_by[0].txid, "def");
1693 assert_eq!(found.consumed_by[1].output_index, 1);
1694 }
1695
1696 #[tokio::test]
1697 async fn test_update_transaction_beef() {
1698 let store = MemoryStorage::new();
1699 store
1700 .insert_output(&make_output("abc", 0, "tm_test", 1.0))
1701 .await
1702 .unwrap();
1703
1704 store
1705 .update_transaction_beef("abc", &[0xDE, 0xAD])
1706 .await
1707 .unwrap();
1708
1709 let found = store
1710 .find_output("abc", 0, Some("tm_test"), None, true)
1711 .await
1712 .unwrap()
1713 .unwrap();
1714 assert_eq!(found.beef.unwrap(), vec![0xDE, 0xAD]);
1715 }
1716
1717 #[tokio::test]
1718 async fn test_update_output_block_height() {
1719 let store = MemoryStorage::new();
1720 store
1721 .insert_output(&make_output("abc", 0, "tm_test", 1.0))
1722 .await
1723 .unwrap();
1724
1725 store
1726 .update_output_block_height("abc", 0, "tm_test", 850_000)
1727 .await
1728 .unwrap();
1729
1730 let found = store
1731 .find_output("abc", 0, Some("tm_test"), None, false)
1732 .await
1733 .unwrap()
1734 .unwrap();
1735 assert_eq!(found.block_height, Some(850_000));
1736 }
1737
1738 #[tokio::test]
1739 async fn test_find_outputs_for_transaction() {
1740 let store = MemoryStorage::new();
1741 store
1742 .insert_output(&make_output("abc", 0, "tm_test", 1.0))
1743 .await
1744 .unwrap();
1745 store
1746 .insert_output(&make_output("abc", 1, "tm_test", 2.0))
1747 .await
1748 .unwrap();
1749 store
1750 .insert_output(&make_output("other", 0, "tm_test", 3.0))
1751 .await
1752 .unwrap();
1753
1754 let results = store
1755 .find_outputs_for_transaction("abc", false)
1756 .await
1757 .unwrap();
1758 assert_eq!(results.len(), 2);
1759 }
1760
1761 #[tokio::test]
1762 async fn test_find_utxos_for_topic() {
1763 let store = MemoryStorage::new();
1764 store
1765 .insert_output(&make_output("a", 0, "tm_test", 1.0))
1766 .await
1767 .unwrap();
1768 store
1769 .insert_output(&make_output("b", 0, "tm_test", 2.0))
1770 .await
1771 .unwrap();
1772 store
1773 .insert_output(&make_output("c", 0, "tm_test", 3.0))
1774 .await
1775 .unwrap();
1776 store
1777 .insert_output(&make_output("d", 0, "tm_other", 4.0))
1778 .await
1779 .unwrap();
1780
1781 store.mark_utxo_as_spent("b", 0, "tm_test").await.unwrap();
1783
1784 let results = store
1786 .find_utxos_for_topic("tm_test", None, None, false)
1787 .await
1788 .unwrap();
1789 assert_eq!(results.len(), 2); let results = store
1793 .find_utxos_for_topic("tm_test", Some(2.0), None, false)
1794 .await
1795 .unwrap();
1796 assert_eq!(results.len(), 1); let results = store
1800 .find_utxos_for_topic("tm_test", None, Some(1), false)
1801 .await
1802 .unwrap();
1803 assert_eq!(results.len(), 1);
1804 assert_eq!(results[0].txid, "a"); }
1806
1807 #[tokio::test]
1808 async fn test_find_utxos_for_topic_sorted_by_score() {
1809 let store = MemoryStorage::new();
1810 store
1811 .insert_output(&make_output("c", 0, "tm_test", 30.0))
1812 .await
1813 .unwrap();
1814 store
1815 .insert_output(&make_output("a", 0, "tm_test", 10.0))
1816 .await
1817 .unwrap();
1818 store
1819 .insert_output(&make_output("b", 0, "tm_test", 20.0))
1820 .await
1821 .unwrap();
1822
1823 let results = store
1824 .find_utxos_for_topic("tm_test", None, None, false)
1825 .await
1826 .unwrap();
1827 assert_eq!(results[0].txid, "a");
1828 assert_eq!(results[1].txid, "b");
1829 assert_eq!(results[2].txid, "c");
1830 }
1831
1832 #[tokio::test]
1833 async fn test_applied_transaction() {
1834 let store = MemoryStorage::new();
1835 let tx = AppliedTransaction {
1836 txid: "abc".to_string(),
1837 topic: "tm_test".to_string(),
1838 };
1839
1840 assert!(!store.does_applied_transaction_exist(&tx).await.unwrap());
1841 store.insert_applied_transaction(&tx).await.unwrap();
1842 assert!(store.does_applied_transaction_exist(&tx).await.unwrap());
1843
1844 let tx2 = AppliedTransaction {
1846 txid: "abc".to_string(),
1847 topic: "tm_other".to_string(),
1848 };
1849 assert!(!store.does_applied_transaction_exist(&tx2).await.unwrap());
1850 }
1851
1852 #[tokio::test]
1853 async fn test_sync_state() {
1854 let store = MemoryStorage::new();
1855
1856 assert_eq!(
1857 store
1858 .get_last_interaction("host1", "tm_test")
1859 .await
1860 .unwrap(),
1861 0
1862 );
1863
1864 store
1865 .update_last_interaction("host1", "tm_test", 100)
1866 .await
1867 .unwrap();
1868 assert_eq!(
1869 store
1870 .get_last_interaction("host1", "tm_test")
1871 .await
1872 .unwrap(),
1873 100
1874 );
1875
1876 store
1878 .update_last_interaction("host1", "tm_test", 200)
1879 .await
1880 .unwrap();
1881 assert_eq!(
1882 store
1883 .get_last_interaction("host1", "tm_test")
1884 .await
1885 .unwrap(),
1886 200
1887 );
1888
1889 assert_eq!(
1891 store
1892 .get_last_interaction("host2", "tm_test")
1893 .await
1894 .unwrap(),
1895 0
1896 );
1897 }
1898
1899 #[tokio::test]
1900 async fn test_find_outputs_by_outpoints_batch() {
1901 let store = MemoryStorage::new();
1902 store
1903 .insert_output(&make_output("a", 0, "tm_test", 1.0))
1904 .await
1905 .unwrap();
1906 store
1907 .insert_output(&make_output("b", 0, "tm_test", 2.0))
1908 .await
1909 .unwrap();
1910 store
1911 .insert_output(&make_output("c", 0, "tm_test", 3.0))
1912 .await
1913 .unwrap();
1914
1915 let outpoints = vec![
1916 Outpoint::new("a", 0),
1917 Outpoint::new("c", 0),
1918 Outpoint::new("missing", 0),
1919 ];
1920 let results = store
1921 .find_outputs_by_outpoints(&outpoints, false)
1922 .await
1923 .unwrap();
1924 assert_eq!(results.len(), 2); }
1926
1927 #[tokio::test]
1928 async fn test_find_output_without_topic() {
1929 let store = MemoryStorage::new();
1930 store
1931 .insert_output(&make_output("abc", 0, "tm_test", 1.0))
1932 .await
1933 .unwrap();
1934
1935 let found = store
1937 .find_output("abc", 0, None, None, false)
1938 .await
1939 .unwrap();
1940 assert!(found.is_some());
1941 assert_eq!(found.unwrap().topic, "tm_test");
1942 }
1943}