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_outputs_for_transaction(
184 &self,
185 txid: &str,
186 include_beef: bool,
187 ) -> Result<Vec<Output>, StorageError>;
188
189 async fn find_utxos_for_topic(
194 &self,
195 topic: &str,
196 since: Option<f64>,
197 limit: Option<u64>,
198 include_beef: bool,
199 ) -> Result<Vec<Output>, StorageError>;
200
201 async fn find_transactions_for_proof_check(
233 &self,
234 limit: u64,
235 min_age_secs: u64,
236 ) -> Result<Vec<TransactionBeef>, StorageError> {
237 let _ = (limit, min_age_secs);
238 Ok(Vec::new())
239 }
240
241 async fn update_last_interaction(
247 &self,
248 host: &str,
249 topic: &str,
250 since: u64,
251 ) -> Result<(), StorageError>;
252
253 async fn get_last_interaction(&self, host: &str, topic: &str) -> Result<u64, StorageError>;
255
256 async fn record_peer_sync_outcome(
279 &self,
280 host: &str,
281 topic: &str,
282 success: bool,
283 ) -> Result<(), StorageError>;
284
285 async fn get_peer_sync_health(
290 &self,
291 host: &str,
292 topic: &str,
293 ) -> Result<PeerSyncHealth, StorageError>;
294
295 async fn record_peer_sync_yield(
313 &self,
314 host: &str,
315 topic: &str,
316 yielded: bool,
317 ) -> Result<PeerYieldStreak, StorageError>;
318
319 async fn put_deferred_graph(
352 &self,
353 record: &crate::gasp::DeferredGraph,
354 ) -> Result<crate::gasp::DeferredGraphSave, StorageError>;
355
356 async fn find_deferred_graphs(
360 &self,
361 host: &str,
362 topic: &str,
363 ) -> Result<Vec<crate::gasp::DeferredGraphKey>, StorageError>;
364
365 async fn get_deferred_graph(
367 &self,
368 host: &str,
369 topic: &str,
370 outpoint: &str,
371 ) -> Result<Option<crate::gasp::DeferredGraph>, StorageError>;
372
373 async fn delete_deferred_graph(
375 &self,
376 host: &str,
377 topic: &str,
378 outpoint: &str,
379 ) -> Result<(), StorageError>;
380}
381
382#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
385pub struct PeerYieldStreak {
386 pub yieldless_syncs: u64,
388 pub secs_since_first: Option<u64>,
391}
392
393#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
396pub struct PeerSyncHealth {
397 pub consecutive_failures: u64,
400 pub secs_since_last_attempt: Option<u64>,
403}
404
405#[async_trait(?Send)]
414impl<T: Storage + ?Sized> Storage for std::rc::Rc<T> {
415 async fn insert_output(&self, output: &Output) -> Result<(), StorageError> {
416 (**self).insert_output(output).await
417 }
418 async fn delete_output(
419 &self,
420 txid: &str,
421 output_index: u32,
422 topic: &str,
423 ) -> Result<(), StorageError> {
424 (**self).delete_output(txid, output_index, topic).await
425 }
426 async fn mark_utxo_as_spent(
427 &self,
428 txid: &str,
429 output_index: u32,
430 topic: &str,
431 ) -> Result<(), StorageError> {
432 (**self).mark_utxo_as_spent(txid, output_index, topic).await
433 }
434 async fn update_consumed_by(
435 &self,
436 txid: &str,
437 output_index: u32,
438 topic: &str,
439 consumed_by: &[Outpoint],
440 ) -> Result<(), StorageError> {
441 (**self)
442 .update_consumed_by(txid, output_index, topic, consumed_by)
443 .await
444 }
445 async fn update_transaction_beef(&self, txid: &str, beef: &[u8]) -> Result<(), StorageError> {
446 (**self).update_transaction_beef(txid, beef).await
447 }
448 async fn mark_transaction_proven(&self, txid: &str) -> Result<(), StorageError> {
449 (**self).mark_transaction_proven(txid).await
450 }
451 async fn mark_transaction_proven_at(
452 &self,
453 txid: &str,
454 height: Option<u64>,
455 ) -> Result<(), StorageError> {
456 (**self).mark_transaction_proven_at(txid, height).await
460 }
461 async fn update_output_block_height(
462 &self,
463 txid: &str,
464 output_index: u32,
465 topic: &str,
466 block_height: u32,
467 ) -> Result<(), StorageError> {
468 (**self)
469 .update_output_block_height(txid, output_index, topic, block_height)
470 .await
471 }
472 async fn insert_applied_transaction(
473 &self,
474 tx: &AppliedTransaction,
475 ) -> Result<(), StorageError> {
476 (**self).insert_applied_transaction(tx).await
477 }
478 async fn does_applied_transaction_exist(
479 &self,
480 tx: &AppliedTransaction,
481 ) -> Result<bool, StorageError> {
482 (**self).does_applied_transaction_exist(tx).await
483 }
484 async fn delete_applied_transaction(
485 &self,
486 tx: &AppliedTransaction,
487 ) -> Result<(), StorageError> {
488 (**self).delete_applied_transaction(tx).await
489 }
490 async fn find_output(
491 &self,
492 txid: &str,
493 output_index: u32,
494 topic: Option<&str>,
495 spent: Option<bool>,
496 include_beef: bool,
497 ) -> Result<Option<Output>, StorageError> {
498 (**self)
499 .find_output(txid, output_index, topic, spent, include_beef)
500 .await
501 }
502 async fn find_outputs_for_transaction(
503 &self,
504 txid: &str,
505 include_beef: bool,
506 ) -> Result<Vec<Output>, StorageError> {
507 (**self)
508 .find_outputs_for_transaction(txid, include_beef)
509 .await
510 }
511 async fn find_utxos_for_topic(
512 &self,
513 topic: &str,
514 since: Option<f64>,
515 limit: Option<u64>,
516 include_beef: bool,
517 ) -> Result<Vec<Output>, StorageError> {
518 (**self)
519 .find_utxos_for_topic(topic, since, limit, include_beef)
520 .await
521 }
522 async fn find_transactions_for_proof_check(
523 &self,
524 limit: u64,
525 min_age_secs: u64,
526 ) -> Result<Vec<TransactionBeef>, StorageError> {
527 (**self)
528 .find_transactions_for_proof_check(limit, min_age_secs)
529 .await
530 }
531 async fn update_last_interaction(
532 &self,
533 host: &str,
534 topic: &str,
535 since: u64,
536 ) -> Result<(), StorageError> {
537 (**self).update_last_interaction(host, topic, since).await
538 }
539 async fn get_last_interaction(&self, host: &str, topic: &str) -> Result<u64, StorageError> {
540 (**self).get_last_interaction(host, topic).await
541 }
542 async fn record_peer_sync_outcome(
543 &self,
544 host: &str,
545 topic: &str,
546 success: bool,
547 ) -> Result<(), StorageError> {
548 (**self)
549 .record_peer_sync_outcome(host, topic, success)
550 .await
551 }
552 async fn get_peer_sync_health(
553 &self,
554 host: &str,
555 topic: &str,
556 ) -> Result<PeerSyncHealth, StorageError> {
557 (**self).get_peer_sync_health(host, topic).await
558 }
559 async fn record_peer_sync_yield(
560 &self,
561 host: &str,
562 topic: &str,
563 yielded: bool,
564 ) -> Result<PeerYieldStreak, StorageError> {
565 (**self).record_peer_sync_yield(host, topic, yielded).await
566 }
567 async fn put_deferred_graph(
568 &self,
569 record: &crate::gasp::DeferredGraph,
570 ) -> Result<crate::gasp::DeferredGraphSave, StorageError> {
571 (**self).put_deferred_graph(record).await
572 }
573 async fn find_deferred_graphs(
574 &self,
575 host: &str,
576 topic: &str,
577 ) -> Result<Vec<crate::gasp::DeferredGraphKey>, StorageError> {
578 (**self).find_deferred_graphs(host, topic).await
579 }
580 async fn get_deferred_graph(
581 &self,
582 host: &str,
583 topic: &str,
584 outpoint: &str,
585 ) -> Result<Option<crate::gasp::DeferredGraph>, StorageError> {
586 (**self).get_deferred_graph(host, topic, outpoint).await
587 }
588 async fn delete_deferred_graph(
589 &self,
590 host: &str,
591 topic: &str,
592 outpoint: &str,
593 ) -> Result<(), StorageError> {
594 (**self).delete_deferred_graph(host, topic, outpoint).await
595 }
596}
597
598#[derive(Debug, thiserror::Error)]
604pub enum StorageError {
605 #[error("not found: {0}")]
606 NotFound(String),
607
608 #[error("duplicate: {0}")]
609 Duplicate(String),
610
611 #[error("database error: {0}")]
612 Database(String),
613
614 #[error("serialization error: {0}")]
615 Serialization(String),
616
617 #[error("{0}")]
618 Other(String),
619}
620
621#[cfg(any(test, feature = "memory-storage"))]
626pub mod memory {
627 use super::*;
628 use std::collections::HashMap;
629 use std::sync::Mutex;
630
631 #[derive(Debug, Default)]
635 pub struct MemoryStorage {
636 outputs: Mutex<HashMap<(String, u32, String), Output>>,
638 transactions: Mutex<HashMap<String, Vec<u8>>>,
640 proven: Mutex<std::collections::HashSet<String>>,
648 applied: Mutex<HashMap<(String, String), bool>>,
650 sync_state: Mutex<HashMap<(String, String), u64>>,
652 clock_secs: Mutex<u64>,
657 created_at: Mutex<HashMap<String, u64>>,
661 peer_health: Mutex<HashMap<(String, String), (u64, u64)>>,
666 proven_at: Mutex<HashMap<String, Option<u64>>>,
670 deferred_graphs: Mutex<HashMap<(String, String, String), crate::gasp::DeferredGraph>>,
673 deferred_graph_writes: Mutex<u64>,
676 deferred_graph_reads: Mutex<u64>,
679 deferred_graph_keys_fault: Mutex<bool>,
681 deferred_graph_ceiling: Mutex<Option<usize>>,
684 deferred_graph_byte_ceiling: Mutex<Option<usize>>,
688 deferred_graph_put_fault: Mutex<bool>,
690 deferred_graph_put_attempts: Mutex<u64>,
693 peer_yieldless: Mutex<HashMap<(String, String), YieldStreakRow>>,
699 }
700
701 type YieldStreakRow = (u64, u64, u64);
704
705 impl MemoryStorage {
706 pub fn new() -> Self {
707 Self::default()
708 }
709
710 pub fn proven_at_height(&self, txid: &str) -> Option<Option<u64>> {
714 self.proven_at.lock().unwrap().get(txid).copied()
715 }
716
717 pub fn advance_clock(&self, secs: u64) {
721 *self.clock_secs.lock().unwrap() += secs;
722 }
723
724 fn stamp_created(&self, txid: &str) {
727 let now = *self.clock_secs.lock().unwrap();
728 self.created_at
729 .lock()
730 .unwrap()
731 .entry(txid.to_string())
732 .or_insert(now);
733 }
734
735 fn age_gate_open(&self, txid: &str, min_age_secs: u64) -> bool {
738 if min_age_secs == 0 {
739 return true;
740 }
741 let now = *self.clock_secs.lock().unwrap();
742 match self.created_at.lock().unwrap().get(txid) {
743 None => true, Some(created) => now.saturating_sub(*created) >= min_age_secs,
745 }
746 }
747
748 pub fn deferred_graphs(&self) -> Vec<crate::gasp::DeferredGraph> {
750 let mut all: Vec<_> = self
751 .deferred_graphs
752 .lock()
753 .unwrap()
754 .values()
755 .cloned()
756 .collect();
757 all.sort_by(|a, b| (a.score, &a.outpoint).cmp(&(b.score, &b.outpoint)));
758 all
759 }
760
761 pub fn deferred_graph_writes(&self) -> u64 {
763 *self.deferred_graph_writes.lock().unwrap()
764 }
765
766 pub fn deferred_graph_reads(&self) -> u64 {
768 *self.deferred_graph_reads.lock().unwrap()
769 }
770
771 pub fn set_deferred_graph_keys_fault(&self, fault: bool) {
774 *self.deferred_graph_keys_fault.lock().unwrap() = fault;
775 }
776
777 pub fn set_deferred_graph_ceiling(&self, ceiling: Option<usize>) {
780 *self.deferred_graph_ceiling.lock().unwrap() = ceiling;
781 }
782
783 pub fn set_deferred_graph_byte_ceiling(&self, ceiling: Option<usize>) {
787 *self.deferred_graph_byte_ceiling.lock().unwrap() = ceiling;
788 }
789
790 pub fn set_deferred_graph_put_fault(&self, fault: bool) {
793 *self.deferred_graph_put_fault.lock().unwrap() = fault;
794 }
795
796 pub fn deferred_graph_put_attempts(&self) -> u64 {
799 *self.deferred_graph_put_attempts.lock().unwrap()
800 }
801
802 pub fn peer_yieldless_syncs(&self, host: &str, topic: &str) -> u64 {
807 self.peer_yieldless
808 .lock()
809 .unwrap()
810 .get(&(crate::gasp::peer_origin(host), topic.to_string()))
811 .map_or(0, |(count, _, _)| *count)
812 }
813
814 pub fn output_count(&self) -> usize {
816 self.outputs.lock().unwrap().len()
817 }
818
819 pub fn transaction_count(&self) -> usize {
821 self.transactions.lock().unwrap().len()
822 }
823 }
824
825 #[async_trait(?Send)]
826 impl Storage for MemoryStorage {
827 async fn insert_output(&self, output: &Output) -> Result<(), StorageError> {
828 let key = (
829 output.txid.clone(),
830 output.output_index,
831 output.topic.clone(),
832 );
833 let mut outputs = self.outputs.lock().unwrap();
834
835 if outputs.contains_key(&key) {
837 if let Some(ref beef) = output.beef {
839 self.transactions
840 .lock()
841 .unwrap()
842 .entry(output.txid.clone())
843 .or_insert_with(|| beef.clone());
844 }
845 return Ok(());
846 }
847
848 let mut stored = output.clone();
850 stored.beef = None;
851 outputs.insert(key, stored);
852
853 if let Some(ref beef) = output.beef {
855 self.transactions
856 .lock()
857 .unwrap()
858 .entry(output.txid.clone())
859 .or_insert_with(|| beef.clone());
860 self.stamp_created(&output.txid);
861 }
862
863 Ok(())
864 }
865
866 async fn delete_output(
867 &self,
868 txid: &str,
869 output_index: u32,
870 topic: &str,
871 ) -> Result<(), StorageError> {
872 let key = (txid.to_string(), output_index, topic.to_string());
873 let mut outputs = self.outputs.lock().unwrap();
874 outputs.remove(&key);
875
876 let has_remaining = outputs.keys().any(|(t, _, _)| t == txid);
878 if !has_remaining {
879 self.transactions.lock().unwrap().remove(txid);
880 self.proven.lock().unwrap().remove(txid);
881 }
882
883 Ok(())
884 }
885
886 async fn mark_utxo_as_spent(
887 &self,
888 txid: &str,
889 output_index: u32,
890 topic: &str,
891 ) -> Result<(), StorageError> {
892 let key = (txid.to_string(), output_index, topic.to_string());
893 if let Some(output) = self.outputs.lock().unwrap().get_mut(&key) {
894 output.spent = true;
895 }
896 Ok(())
897 }
898
899 async fn update_consumed_by(
900 &self,
901 txid: &str,
902 output_index: u32,
903 topic: &str,
904 consumed_by: &[Outpoint],
905 ) -> Result<(), StorageError> {
906 let key = (txid.to_string(), output_index, topic.to_string());
907 if let Some(output) = self.outputs.lock().unwrap().get_mut(&key) {
908 output.consumed_by = consumed_by.to_vec();
909 }
910 Ok(())
911 }
912
913 async fn update_transaction_beef(
914 &self,
915 txid: &str,
916 beef: &[u8],
917 ) -> Result<(), StorageError> {
918 let has_proof = beef_limits::has_proof(beef, txid);
924 if has_proof {
925 self.proven.lock().unwrap().insert(txid.to_string());
926 }
927 self.transactions
928 .lock()
929 .unwrap()
930 .insert(txid.to_string(), beef.to_vec());
931 self.stamp_created(txid);
934 Ok(())
935 }
936
937 async fn mark_transaction_proven(&self, txid: &str) -> Result<(), StorageError> {
938 self.proven.lock().unwrap().insert(txid.to_string());
941 Ok(())
942 }
943
944 async fn mark_transaction_proven_at(
945 &self,
946 txid: &str,
947 height: Option<u64>,
948 ) -> Result<(), StorageError> {
949 self.proven.lock().unwrap().insert(txid.to_string());
953 self.proven_at
954 .lock()
955 .unwrap()
956 .insert(txid.to_string(), height);
957 Ok(())
958 }
959
960 async fn update_output_block_height(
961 &self,
962 txid: &str,
963 output_index: u32,
964 topic: &str,
965 block_height: u32,
966 ) -> Result<(), StorageError> {
967 let key = (txid.to_string(), output_index, topic.to_string());
968 if let Some(output) = self.outputs.lock().unwrap().get_mut(&key) {
969 output.block_height = Some(block_height);
970 }
971 Ok(())
972 }
973
974 async fn insert_applied_transaction(
975 &self,
976 tx: &AppliedTransaction,
977 ) -> Result<(), StorageError> {
978 self.applied
979 .lock()
980 .unwrap()
981 .insert((tx.txid.clone(), tx.topic.clone()), true);
982 Ok(())
983 }
984
985 async fn does_applied_transaction_exist(
986 &self,
987 tx: &AppliedTransaction,
988 ) -> Result<bool, StorageError> {
989 Ok(self
990 .applied
991 .lock()
992 .unwrap()
993 .contains_key(&(tx.txid.clone(), tx.topic.clone())))
994 }
995
996 async fn delete_applied_transaction(
997 &self,
998 tx: &AppliedTransaction,
999 ) -> Result<(), StorageError> {
1000 self.applied
1001 .lock()
1002 .unwrap()
1003 .remove(&(tx.txid.clone(), tx.topic.clone()));
1004 Ok(())
1005 }
1006
1007 async fn find_output(
1008 &self,
1009 txid: &str,
1010 output_index: u32,
1011 topic: Option<&str>,
1012 spent: Option<bool>,
1013 include_beef: bool,
1014 ) -> Result<Option<Output>, StorageError> {
1015 let outputs = self.outputs.lock().unwrap();
1016
1017 if let Some(topic) = topic {
1019 let key = (txid.to_string(), output_index, topic.to_string());
1020 if let Some(output) = outputs.get(&key) {
1021 if let Some(s) = spent {
1022 if output.spent != s {
1023 return Ok(None);
1024 }
1025 }
1026 let mut result = output.clone();
1027 if include_beef {
1028 result.beef = self.transactions.lock().unwrap().get(txid).cloned();
1029 }
1030 return Ok(Some(result));
1031 }
1032 return Ok(None);
1033 }
1034
1035 for ((t, oi, _), output) in outputs.iter() {
1037 if t == txid && *oi == output_index {
1038 if let Some(s) = spent {
1039 if output.spent != s {
1040 continue;
1041 }
1042 }
1043 let mut result = output.clone();
1044 if include_beef {
1045 result.beef = self.transactions.lock().unwrap().get(txid).cloned();
1046 }
1047 return Ok(Some(result));
1048 }
1049 }
1050 Ok(None)
1051 }
1052
1053 async fn find_outputs_for_transaction(
1054 &self,
1055 txid: &str,
1056 include_beef: bool,
1057 ) -> Result<Vec<Output>, StorageError> {
1058 let outputs = self.outputs.lock().unwrap();
1059 let beef = if include_beef {
1060 self.transactions.lock().unwrap().get(txid).cloned()
1061 } else {
1062 None
1063 };
1064
1065 let results: Vec<Output> = outputs
1066 .iter()
1067 .filter(|((t, _, _), _)| t == txid)
1068 .map(|(_, output)| {
1069 let mut o = output.clone();
1070 if include_beef {
1071 o.beef.clone_from(&beef);
1072 }
1073 o
1074 })
1075 .collect();
1076
1077 Ok(results)
1078 }
1079
1080 async fn find_utxos_for_topic(
1081 &self,
1082 topic: &str,
1083 since: Option<f64>,
1084 limit: Option<u64>,
1085 include_beef: bool,
1086 ) -> Result<Vec<Output>, StorageError> {
1087 let outputs = self.outputs.lock().unwrap();
1088 let transactions = self.transactions.lock().unwrap();
1089
1090 let mut results: Vec<Output> = outputs
1091 .iter()
1092 .filter(|((_, _, t), o)| {
1093 t == topic && !o.spent && since.is_none_or(|s| o.score.unwrap_or(0.0) >= s)
1094 })
1095 .map(|(_, output)| {
1096 let mut o = output.clone();
1097 if include_beef {
1098 o.beef = transactions.get(&o.txid).cloned();
1099 }
1100 o
1101 })
1102 .collect();
1103
1104 results.sort_by(|a, b| {
1106 a.score
1107 .unwrap_or(0.0)
1108 .partial_cmp(&b.score.unwrap_or(0.0))
1109 .unwrap_or(std::cmp::Ordering::Equal)
1110 });
1111
1112 if let Some(limit) = limit {
1113 results.truncate(limit as usize);
1114 }
1115
1116 Ok(results)
1117 }
1118
1119 async fn find_transactions_for_proof_check(
1120 &self,
1121 limit: u64,
1122 min_age_secs: u64,
1123 ) -> Result<Vec<TransactionBeef>, StorageError> {
1124 let candidates: Vec<TransactionBeef> = {
1137 let transactions = self.transactions.lock().unwrap();
1138 let proven = self.proven.lock().unwrap();
1139 transactions
1140 .iter()
1141 .filter(|(txid, _)| !proven.contains(*txid))
1142 .map(|(txid, beef)| TransactionBeef {
1143 txid: txid.clone(),
1144 beef: beef.clone(),
1145 })
1146 .collect()
1147 };
1148 Ok(candidates
1149 .into_iter()
1150 .filter(|c| self.age_gate_open(&c.txid, min_age_secs))
1151 .take(limit as usize)
1152 .collect())
1153 }
1154
1155 async fn update_last_interaction(
1156 &self,
1157 host: &str,
1158 topic: &str,
1159 since: u64,
1160 ) -> Result<(), StorageError> {
1161 self.sync_state
1162 .lock()
1163 .unwrap()
1164 .insert((host.to_string(), topic.to_string()), since);
1165 Ok(())
1166 }
1167
1168 async fn get_last_interaction(&self, host: &str, topic: &str) -> Result<u64, StorageError> {
1169 Ok(*self
1170 .sync_state
1171 .lock()
1172 .unwrap()
1173 .get(&(host.to_string(), topic.to_string()))
1174 .unwrap_or(&0))
1175 }
1176
1177 async fn record_peer_sync_outcome(
1178 &self,
1179 host: &str,
1180 topic: &str,
1181 success: bool,
1182 ) -> Result<(), StorageError> {
1183 let now = *self.clock_secs.lock().unwrap();
1184 let mut health = self.peer_health.lock().unwrap();
1185 let entry = health
1186 .entry((host.to_string(), topic.to_string()))
1187 .or_insert((0, now));
1188 entry.0 = if success { 0 } else { entry.0 + 1 };
1189 entry.1 = now;
1190 Ok(())
1191 }
1192
1193 async fn get_peer_sync_health(
1194 &self,
1195 host: &str,
1196 topic: &str,
1197 ) -> Result<PeerSyncHealth, StorageError> {
1198 let now = *self.clock_secs.lock().unwrap();
1199 Ok(self
1200 .peer_health
1201 .lock()
1202 .unwrap()
1203 .get(&(host.to_string(), topic.to_string()))
1204 .map(|(fails, attempt_at)| PeerSyncHealth {
1205 consecutive_failures: *fails,
1206 secs_since_last_attempt: Some(now.saturating_sub(*attempt_at)),
1207 })
1208 .unwrap_or_default())
1209 }
1210
1211 async fn record_peer_sync_yield(
1212 &self,
1213 host: &str,
1214 topic: &str,
1215 yielded: bool,
1216 ) -> Result<PeerYieldStreak, StorageError> {
1217 let now = *self.clock_secs.lock().unwrap();
1218 let key = (host.to_string(), topic.to_string());
1219 let not_failed = self
1222 .peer_health
1223 .lock()
1224 .unwrap()
1225 .get(&key)
1226 .is_none_or(|(fails, _)| *fails == 0);
1227 let mut held = self.peer_yieldless.lock().unwrap();
1228 if yielded {
1229 held.remove(&key);
1230 return Ok(PeerYieldStreak::default());
1231 }
1232 let streak = held.entry(key).or_insert((0, now, now));
1233 if streak.0 == 0
1234 || (not_failed
1235 && now.saturating_sub(streak.2) > crate::gasp::PEER_YIELDLESS_DECAY_SECS)
1236 {
1237 *streak = (0, now, now);
1238 }
1239 streak.0 += 1;
1240 streak.2 = now;
1241 Ok(PeerYieldStreak {
1242 yieldless_syncs: streak.0,
1243 secs_since_first: Some(now.saturating_sub(streak.1)),
1244 })
1245 }
1246
1247 async fn put_deferred_graph(
1248 &self,
1249 record: &crate::gasp::DeferredGraph,
1250 ) -> Result<crate::gasp::DeferredGraphSave, StorageError> {
1251 let key = (
1252 record.peer.clone(),
1253 record.topic.clone(),
1254 record.outpoint.clone(),
1255 );
1256 *self.deferred_graph_put_attempts.lock().unwrap() += 1;
1257 if *self.deferred_graph_put_fault.lock().unwrap() {
1258 return Err(StorageError::Database("deferred graph save faulted".into()));
1259 }
1260 if let Some(ceiling) = *self.deferred_graph_ceiling.lock().unwrap() {
1261 let held = self.deferred_graphs.lock().unwrap();
1262 if !held.contains_key(&key) && held.len() >= ceiling {
1263 return Ok(crate::gasp::DeferredGraphSave::AtCeiling);
1264 }
1265 }
1266 if let Some(ceiling) = *self.deferred_graph_byte_ceiling.lock().unwrap() {
1267 let held = self.deferred_graphs.lock().unwrap();
1268 let others: usize = held
1269 .iter()
1270 .filter(|(k, _)| **k != key)
1271 .map(|(_, r)| r.byte_size())
1272 .sum();
1273 if others + record.byte_size() > ceiling {
1274 return Ok(crate::gasp::DeferredGraphSave::AtCeiling);
1275 }
1276 }
1277 *self.deferred_graph_writes.lock().unwrap() += 1;
1278 self.deferred_graphs.lock().unwrap().insert(
1279 (
1280 record.peer.clone(),
1281 record.topic.clone(),
1282 record.outpoint.clone(),
1283 ),
1284 record.clone(),
1285 );
1286 Ok(crate::gasp::DeferredGraphSave::Saved)
1287 }
1288
1289 async fn find_deferred_graphs(
1290 &self,
1291 host: &str,
1292 topic: &str,
1293 ) -> Result<Vec<crate::gasp::DeferredGraphKey>, StorageError> {
1294 if *self.deferred_graph_keys_fault.lock().unwrap() {
1295 return Err(StorageError::Database("deferred graph keys: fault".into()));
1296 }
1297 Ok(self
1298 .deferred_graphs()
1299 .into_iter()
1300 .filter(|r| r.peer == host && r.topic == topic)
1301 .map(|r| crate::gasp::DeferredGraphKey {
1302 outpoint: r.outpoint,
1303 score: r.score,
1304 })
1305 .collect())
1306 }
1307
1308 async fn get_deferred_graph(
1309 &self,
1310 host: &str,
1311 topic: &str,
1312 outpoint: &str,
1313 ) -> Result<Option<crate::gasp::DeferredGraph>, StorageError> {
1314 *self.deferred_graph_reads.lock().unwrap() += 1;
1315 Ok(self
1316 .deferred_graphs
1317 .lock()
1318 .unwrap()
1319 .get(&(host.to_string(), topic.to_string(), outpoint.to_string()))
1320 .cloned())
1321 }
1322
1323 async fn delete_deferred_graph(
1324 &self,
1325 host: &str,
1326 topic: &str,
1327 outpoint: &str,
1328 ) -> Result<(), StorageError> {
1329 self.deferred_graphs.lock().unwrap().remove(&(
1330 host.to_string(),
1331 topic.to_string(),
1332 outpoint.to_string(),
1333 ));
1334 Ok(())
1335 }
1336 }
1337}
1338
1339#[cfg(test)]
1344mod tests {
1345 use super::memory::MemoryStorage;
1346 use super::*;
1347 use crate::types::AppliedTransaction;
1348
1349 #[tokio::test]
1354 async fn the_rc_blanket_forwards_the_anchored_proven_latch() {
1355 let store = std::rc::Rc::new(MemoryStorage::new());
1356 let via_dyn: std::rc::Rc<dyn Storage> = store.clone();
1357 via_dyn
1358 .mark_transaction_proven_at("tx", Some(965_772))
1359 .await
1360 .unwrap();
1361 assert_eq!(
1362 store.proven_at_height("tx"),
1363 Some(Some(965_772)),
1364 "the Rc blanket forwarded the height to the inner store"
1365 );
1366 via_dyn
1368 .mark_transaction_proven_at("tx2", None)
1369 .await
1370 .unwrap();
1371 assert_eq!(store.proven_at_height("tx2"), Some(None));
1372 assert_eq!(
1373 store.proven_at_height("never"),
1374 None,
1375 "a txid never latched is absent"
1376 );
1377 }
1378
1379 fn make_output(txid: &str, index: u32, topic: &str, score: f64) -> Output {
1380 Output {
1381 txid: txid.to_string(),
1382 output_index: index,
1383 output_script: vec![0x76, 0xa9],
1384 satoshis: 1000,
1385 topic: topic.to_string(),
1386 spent: false,
1387 outputs_consumed: vec![],
1388 consumed_by: vec![],
1389 beef: Some(vec![0xBE, 0xEF]),
1390 block_height: None,
1391 score: Some(score),
1392 }
1393 }
1394
1395 #[tokio::test]
1396 async fn test_insert_and_find_output() {
1397 let store = MemoryStorage::new();
1398 let output = make_output("abc", 0, "tm_test", 1.0);
1399 store.insert_output(&output).await.unwrap();
1400
1401 let found = store
1403 .find_output("abc", 0, Some("tm_test"), None, false)
1404 .await
1405 .unwrap()
1406 .unwrap();
1407 assert_eq!(found.txid, "abc");
1408 assert_eq!(found.output_index, 0);
1409 assert!(found.beef.is_none());
1410
1411 let found = store
1413 .find_output("abc", 0, Some("tm_test"), None, true)
1414 .await
1415 .unwrap()
1416 .unwrap();
1417 assert_eq!(found.beef.unwrap(), vec![0xBE, 0xEF]);
1418 }
1419
1420 #[tokio::test]
1421 async fn test_insert_duplicate_is_noop() {
1422 let store = MemoryStorage::new();
1423 let output = make_output("abc", 0, "tm_test", 1.0);
1424 store.insert_output(&output).await.unwrap();
1425 store.insert_output(&output).await.unwrap();
1426 assert_eq!(store.output_count(), 1);
1427 }
1428
1429 #[tokio::test]
1430 async fn test_delete_output_cleans_up_beef() {
1431 let store = MemoryStorage::new();
1432 let output = make_output("abc", 0, "tm_test", 1.0);
1433 store.insert_output(&output).await.unwrap();
1434 assert_eq!(store.transaction_count(), 1);
1435
1436 store.delete_output("abc", 0, "tm_test").await.unwrap();
1437 assert_eq!(store.output_count(), 0);
1438 assert_eq!(store.transaction_count(), 0); }
1440
1441 #[tokio::test]
1442 async fn test_delete_output_keeps_beef_if_other_outputs_exist() {
1443 let store = MemoryStorage::new();
1444 store
1445 .insert_output(&make_output("abc", 0, "tm_test", 1.0))
1446 .await
1447 .unwrap();
1448 store
1449 .insert_output(&make_output("abc", 1, "tm_test", 2.0))
1450 .await
1451 .unwrap();
1452 assert_eq!(store.output_count(), 2);
1453 assert_eq!(store.transaction_count(), 1);
1454
1455 store.delete_output("abc", 0, "tm_test").await.unwrap();
1456 assert_eq!(store.output_count(), 1);
1457 assert_eq!(store.transaction_count(), 1); }
1459
1460 #[tokio::test]
1461 async fn test_mark_utxo_as_spent() {
1462 let store = MemoryStorage::new();
1463 store
1464 .insert_output(&make_output("abc", 0, "tm_test", 1.0))
1465 .await
1466 .unwrap();
1467
1468 store.mark_utxo_as_spent("abc", 0, "tm_test").await.unwrap();
1469
1470 let found = store
1471 .find_output("abc", 0, Some("tm_test"), None, false)
1472 .await
1473 .unwrap()
1474 .unwrap();
1475 assert!(found.spent);
1476 }
1477
1478 #[tokio::test]
1479 async fn test_find_output_with_spent_filter() {
1480 let store = MemoryStorage::new();
1481 store
1482 .insert_output(&make_output("abc", 0, "tm_test", 1.0))
1483 .await
1484 .unwrap();
1485
1486 assert!(store
1488 .find_output("abc", 0, Some("tm_test"), Some(false), false)
1489 .await
1490 .unwrap()
1491 .is_some());
1492 assert!(store
1493 .find_output("abc", 0, Some("tm_test"), Some(true), false)
1494 .await
1495 .unwrap()
1496 .is_none());
1497
1498 store.mark_utxo_as_spent("abc", 0, "tm_test").await.unwrap();
1499
1500 assert!(store
1502 .find_output("abc", 0, Some("tm_test"), Some(true), false)
1503 .await
1504 .unwrap()
1505 .is_some());
1506 assert!(store
1507 .find_output("abc", 0, Some("tm_test"), Some(false), false)
1508 .await
1509 .unwrap()
1510 .is_none());
1511 }
1512
1513 #[tokio::test]
1514 async fn test_update_consumed_by() {
1515 let store = MemoryStorage::new();
1516 store
1517 .insert_output(&make_output("abc", 0, "tm_test", 1.0))
1518 .await
1519 .unwrap();
1520
1521 let consumed = vec![Outpoint::new("def", 0), Outpoint::new("ghi", 1)];
1522 store
1523 .update_consumed_by("abc", 0, "tm_test", &consumed)
1524 .await
1525 .unwrap();
1526
1527 let found = store
1528 .find_output("abc", 0, Some("tm_test"), None, false)
1529 .await
1530 .unwrap()
1531 .unwrap();
1532 assert_eq!(found.consumed_by.len(), 2);
1533 assert_eq!(found.consumed_by[0].txid, "def");
1534 assert_eq!(found.consumed_by[1].output_index, 1);
1535 }
1536
1537 #[tokio::test]
1538 async fn test_update_transaction_beef() {
1539 let store = MemoryStorage::new();
1540 store
1541 .insert_output(&make_output("abc", 0, "tm_test", 1.0))
1542 .await
1543 .unwrap();
1544
1545 store
1546 .update_transaction_beef("abc", &[0xDE, 0xAD])
1547 .await
1548 .unwrap();
1549
1550 let found = store
1551 .find_output("abc", 0, Some("tm_test"), None, true)
1552 .await
1553 .unwrap()
1554 .unwrap();
1555 assert_eq!(found.beef.unwrap(), vec![0xDE, 0xAD]);
1556 }
1557
1558 #[tokio::test]
1559 async fn test_update_output_block_height() {
1560 let store = MemoryStorage::new();
1561 store
1562 .insert_output(&make_output("abc", 0, "tm_test", 1.0))
1563 .await
1564 .unwrap();
1565
1566 store
1567 .update_output_block_height("abc", 0, "tm_test", 850_000)
1568 .await
1569 .unwrap();
1570
1571 let found = store
1572 .find_output("abc", 0, Some("tm_test"), None, false)
1573 .await
1574 .unwrap()
1575 .unwrap();
1576 assert_eq!(found.block_height, Some(850_000));
1577 }
1578
1579 #[tokio::test]
1580 async fn test_find_outputs_for_transaction() {
1581 let store = MemoryStorage::new();
1582 store
1583 .insert_output(&make_output("abc", 0, "tm_test", 1.0))
1584 .await
1585 .unwrap();
1586 store
1587 .insert_output(&make_output("abc", 1, "tm_test", 2.0))
1588 .await
1589 .unwrap();
1590 store
1591 .insert_output(&make_output("other", 0, "tm_test", 3.0))
1592 .await
1593 .unwrap();
1594
1595 let results = store
1596 .find_outputs_for_transaction("abc", false)
1597 .await
1598 .unwrap();
1599 assert_eq!(results.len(), 2);
1600 }
1601
1602 #[tokio::test]
1603 async fn test_find_utxos_for_topic() {
1604 let store = MemoryStorage::new();
1605 store
1606 .insert_output(&make_output("a", 0, "tm_test", 1.0))
1607 .await
1608 .unwrap();
1609 store
1610 .insert_output(&make_output("b", 0, "tm_test", 2.0))
1611 .await
1612 .unwrap();
1613 store
1614 .insert_output(&make_output("c", 0, "tm_test", 3.0))
1615 .await
1616 .unwrap();
1617 store
1618 .insert_output(&make_output("d", 0, "tm_other", 4.0))
1619 .await
1620 .unwrap();
1621
1622 store.mark_utxo_as_spent("b", 0, "tm_test").await.unwrap();
1624
1625 let results = store
1627 .find_utxos_for_topic("tm_test", None, None, false)
1628 .await
1629 .unwrap();
1630 assert_eq!(results.len(), 2); let results = store
1634 .find_utxos_for_topic("tm_test", Some(2.0), None, false)
1635 .await
1636 .unwrap();
1637 assert_eq!(results.len(), 1); let results = store
1641 .find_utxos_for_topic("tm_test", None, Some(1), false)
1642 .await
1643 .unwrap();
1644 assert_eq!(results.len(), 1);
1645 assert_eq!(results[0].txid, "a"); }
1647
1648 #[tokio::test]
1649 async fn test_find_utxos_for_topic_sorted_by_score() {
1650 let store = MemoryStorage::new();
1651 store
1652 .insert_output(&make_output("c", 0, "tm_test", 30.0))
1653 .await
1654 .unwrap();
1655 store
1656 .insert_output(&make_output("a", 0, "tm_test", 10.0))
1657 .await
1658 .unwrap();
1659 store
1660 .insert_output(&make_output("b", 0, "tm_test", 20.0))
1661 .await
1662 .unwrap();
1663
1664 let results = store
1665 .find_utxos_for_topic("tm_test", None, None, false)
1666 .await
1667 .unwrap();
1668 assert_eq!(results[0].txid, "a");
1669 assert_eq!(results[1].txid, "b");
1670 assert_eq!(results[2].txid, "c");
1671 }
1672
1673 #[tokio::test]
1674 async fn test_applied_transaction() {
1675 let store = MemoryStorage::new();
1676 let tx = AppliedTransaction {
1677 txid: "abc".to_string(),
1678 topic: "tm_test".to_string(),
1679 };
1680
1681 assert!(!store.does_applied_transaction_exist(&tx).await.unwrap());
1682 store.insert_applied_transaction(&tx).await.unwrap();
1683 assert!(store.does_applied_transaction_exist(&tx).await.unwrap());
1684
1685 let tx2 = AppliedTransaction {
1687 txid: "abc".to_string(),
1688 topic: "tm_other".to_string(),
1689 };
1690 assert!(!store.does_applied_transaction_exist(&tx2).await.unwrap());
1691 }
1692
1693 #[tokio::test]
1694 async fn test_sync_state() {
1695 let store = MemoryStorage::new();
1696
1697 assert_eq!(
1698 store
1699 .get_last_interaction("host1", "tm_test")
1700 .await
1701 .unwrap(),
1702 0
1703 );
1704
1705 store
1706 .update_last_interaction("host1", "tm_test", 100)
1707 .await
1708 .unwrap();
1709 assert_eq!(
1710 store
1711 .get_last_interaction("host1", "tm_test")
1712 .await
1713 .unwrap(),
1714 100
1715 );
1716
1717 store
1719 .update_last_interaction("host1", "tm_test", 200)
1720 .await
1721 .unwrap();
1722 assert_eq!(
1723 store
1724 .get_last_interaction("host1", "tm_test")
1725 .await
1726 .unwrap(),
1727 200
1728 );
1729
1730 assert_eq!(
1732 store
1733 .get_last_interaction("host2", "tm_test")
1734 .await
1735 .unwrap(),
1736 0
1737 );
1738 }
1739
1740 #[tokio::test]
1741 async fn test_find_outputs_by_outpoints_batch() {
1742 let store = MemoryStorage::new();
1743 store
1744 .insert_output(&make_output("a", 0, "tm_test", 1.0))
1745 .await
1746 .unwrap();
1747 store
1748 .insert_output(&make_output("b", 0, "tm_test", 2.0))
1749 .await
1750 .unwrap();
1751 store
1752 .insert_output(&make_output("c", 0, "tm_test", 3.0))
1753 .await
1754 .unwrap();
1755
1756 let outpoints = vec![
1757 Outpoint::new("a", 0),
1758 Outpoint::new("c", 0),
1759 Outpoint::new("missing", 0),
1760 ];
1761 let results = store
1762 .find_outputs_by_outpoints(&outpoints, false)
1763 .await
1764 .unwrap();
1765 assert_eq!(results.len(), 2); }
1767
1768 #[tokio::test]
1769 async fn test_find_output_without_topic() {
1770 let store = MemoryStorage::new();
1771 store
1772 .insert_output(&make_output("abc", 0, "tm_test", 1.0))
1773 .await
1774 .unwrap();
1775
1776 let found = store
1778 .find_output("abc", 0, None, None, false)
1779 .await
1780 .unwrap();
1781 assert!(found.is_some());
1782 assert_eq!(found.unwrap().topic, "tm_test");
1783 }
1784}