1use crate::beef_limits;
37
38use std::collections::{HashMap, HashSet};
39use std::rc::Rc;
40use std::sync::atomic::{AtomicBool, Ordering};
41use std::sync::Mutex;
42
43use async_trait::async_trait;
44use bsv_rs::transaction::{Beef, ChainTracker, ChainTrackerError, MerklePath, Transaction};
45use tracing::{debug, error, warn};
46
47use crate::gasp::{GASPError, GASPStorage};
48use crate::storage::Storage;
49use crate::topic_manager::TopicManager;
50use crate::types::{
51 GASPInputRequest, GASPNode, GASPNodeResponse, GASPOutput, SubmitMode, TopicAdmittanceContext,
52};
53
54const LANDED_READS_PER_NODE: usize = 16;
60
61#[derive(Debug, Clone)]
63struct PendingNode {
64 node: GASPNode,
65 #[allow(dead_code)]
66 spent_by: Option<String>,
67 children: Vec<String>, }
70
71#[derive(Debug, Clone)]
76pub struct FinalizedGraph {
77 pub topic: String,
79 pub beefs: Vec<Vec<u8>>,
81}
82
83pub type FinalizedGraphSink = Rc<Mutex<Vec<FinalizedGraph>>>;
88
89pub fn new_finalized_graph_sink() -> FinalizedGraphSink {
91 Rc::new(Mutex::new(Vec::new()))
92}
93
94pub struct OverlayGASPStorage<'a> {
101 storage: &'a dyn Storage,
103 topic: String,
105 topic_manager: Option<&'a dyn TopicManager>,
107 pending_graphs: Mutex<HashMap<String, PendingNode>>,
111 finalized_sink: FinalizedGraphSink,
113 strict_beef: bool,
119 chain_tracker: Option<&'a dyn ChainTracker>,
123 verify_scripts: bool,
126 walk_budget: crate::engine::DoorBudget,
130 peer: String,
133 configured_peer: bool,
136 anchored: Mutex<Option<AnchoredGraph>>,
140}
141
142struct AnchoredGraph {
144 graph_id: String,
145 strict_beef: bool,
146 order: Vec<String>,
147 assembly: GraphAssembly,
148}
149
150struct FaultNotingTracker<'a> {
155 inner: &'a dyn ChainTracker,
156 faulted: AtomicBool,
157}
158
159#[async_trait]
160impl ChainTracker for FaultNotingTracker<'_> {
161 async fn is_valid_root_for_height(
162 &self,
163 root: &str,
164 height: u32,
165 ) -> Result<bool, ChainTrackerError> {
166 let answer = self.inner.is_valid_root_for_height(root, height).await;
167 if answer.is_err() {
168 self.faulted.store(true, Ordering::Relaxed);
169 }
170 answer
171 }
172
173 async fn current_height(&self) -> Result<u32, ChainTrackerError> {
174 self.inner.current_height().await
175 }
176}
177
178struct AnchorInput {
180 root_txid: String,
182 root_beef: Vec<u8>,
185 absent_sources: Vec<(String, u32)>,
187 order: Vec<String>,
192 assembly: GraphAssembly,
195}
196
197struct ParsedTx {
199 tx: Transaction,
200 txid: String,
201}
202
203struct AssemblyNode {
205 parsed: Rc<ParsedTx>,
208 proof: Option<MerklePath>,
210 sources: Vec<Option<String>>,
213}
214
215#[derive(Default)]
244struct GraphAssembly {
245 txs: HashMap<[u8; 32], Rc<ParsedTx>>,
247 parsed: HashMap<String, Rc<ParsedTx>>,
249 nodes: HashMap<String, Rc<AssemblyNode>>,
251 checked: HashSet<(String, bool)>,
253}
254
255impl GraphAssembly {
256 fn parsed_tx(
258 &mut self,
259 key: &str,
260 refs: &HashMap<String, PendingNode>,
261 ) -> Result<Rc<ParsedTx>, GASPError> {
262 if let Some(parsed) = self.parsed.get(key) {
263 return Ok(parsed.clone());
264 }
265 let pending = refs
266 .get(key)
267 .ok_or_else(|| GASPError::Other(format!("Node {key} not found in graph refs")))?;
268 let unparsed =
269 |e: bsv_rs::Error| GASPError::Other(format!("Failed to parse raw_tx for {key}: {e}"));
270 let raw = bsv_rs::primitives::from_hex(&pending.node.raw_tx).map_err(unparsed)?;
271 let parsed = match self.txs.entry(bsv_rs::primitives::hash::sha256(&raw)) {
272 std::collections::hash_map::Entry::Occupied(known) => known.get().clone(),
273 std::collections::hash_map::Entry::Vacant(new) => {
274 let tx = Transaction::from_binary(&raw).map_err(unparsed)?;
275 let txid = tx.id();
276 new.insert(Rc::new(ParsedTx { tx, txid })).clone()
277 }
278 };
279 self.parsed.insert(key.to_string(), parsed.clone());
280 Ok(parsed)
281 }
282
283 fn node(
286 &mut self,
287 key: &str,
288 refs: &HashMap<String, PendingNode>,
289 ) -> Result<Rc<AssemblyNode>, GASPError> {
290 if let Some(node) = self.nodes.get(key) {
291 return Ok(node.clone());
292 }
293 let parsed = self.parsed_tx(key, refs)?;
294 let proof =
295 match refs
296 .get(key)
297 .and_then(|pending| pending.node.proof.as_ref())
298 {
299 Some(proof_hex) => Some(beef_limits::proof_from_hex(proof_hex).map_err(|e| {
300 GASPError::Other(format!("Failed to parse proof for {key}: {e}"))
301 })?),
302 None => None,
303 };
304 let sources = if proof.is_some() {
307 Vec::new()
308 } else {
309 parsed
310 .tx
311 .inputs
312 .iter()
313 .map(|input| {
314 let source_txid = input.get_source_txid().unwrap_or_default();
315 if source_txid.is_empty() {
316 return None;
317 }
318 let child_key = format!("{}.{}", source_txid, input.source_output_index);
319 refs.contains_key(&child_key).then_some(child_key)
320 })
321 .collect()
322 };
323 let node = Rc::new(AssemblyNode {
324 parsed,
325 proof,
326 sources,
327 });
328 self.nodes.insert(key.to_string(), node.clone());
329 Ok(node)
330 }
331
332 fn check(
340 &mut self,
341 key: &str,
342 refs: &HashMap<String, PendingNode>,
343 strict: bool,
344 ) -> Result<(), GASPError> {
345 let mut stack: Vec<(String, Rc<AssemblyNode>, usize)> = Vec::new();
347 let mut inside: HashSet<String> = HashSet::new();
348 let mut entering: Option<String> = Some(key.to_string());
349 loop {
350 if let Some(key) = entering.take() {
351 if !self.checked.contains(&(key.clone(), strict)) && inside.insert(key.clone()) {
352 let node = self.node(&key, refs)?;
353 stack.push((key, node, 0));
354 }
355 }
356 let Some((_, node, next)) = stack.last_mut() else {
357 return Ok(());
358 };
359 if let Some(source) = node.sources.get(*next) {
360 *next += 1;
361 entering.clone_from(source);
362 continue;
363 }
364 let (key, node, _) = stack.pop().expect("the stack is not empty");
365 if strict {
366 if let Some(missing) = node.sources.iter().rposition(Option::is_none) {
367 let source_txid = node.parsed.tx.inputs[missing]
368 .source_txid
369 .as_deref()
370 .unwrap_or("unknown");
371 let e = bsv_rs::Error::TransactionError(format!(
373 "Missing source transaction for input {missing} (txid: {source_txid}). \
374 Set allow_partial=true to skip missing source transactions."
375 ));
376 return Err(GASPError::Other(format!(
377 "Failed to serialize BEEF for {key}: {e}"
378 )));
379 }
380 }
381 inside.remove(&key);
382 self.checked.insert((key, strict));
383 }
384 }
385
386 fn txid_of(&self, key: &str) -> Option<&str> {
388 self.nodes.get(key).map(|node| node.parsed.txid.as_str())
389 }
390
391 fn beef_of(&self, key: &str) -> Vec<u8> {
396 struct Frame<'n> {
397 node: &'n AssemblyNode,
398 inputs_left: usize,
399 }
400 let mut seen: HashSet<&str> = HashSet::new();
401 let mut inside: HashSet<&str> = HashSet::new();
402 let mut ancestors: Vec<&AssemblyNode> = Vec::new();
403 let mut bumps: Vec<MerklePath> = Vec::new();
404 let mut bump_index_by_root: HashMap<String, usize> = HashMap::new();
405 let mut stack: Vec<Frame> = Vec::new();
406 let mut entering: Option<&AssemblyNode> = self.nodes.get(key).map(Rc::as_ref);
407 loop {
408 if let Some(node) = entering.take() {
409 let txid = node.parsed.txid.as_str();
410 if seen.contains(txid) || inside.contains(txid) {
411 } else if let Some(merkle_path) = &node.proof {
413 let root = merkle_path.compute_root(Some(txid)).unwrap_or_default();
416 let slot = format!("{}:{}", merkle_path.block_height, root);
417 match bump_index_by_root.get(&slot) {
418 Some(&existing) if bumps[existing].combine(merkle_path).is_ok() => {}
419 _ => {
420 bump_index_by_root.insert(slot, bumps.len());
421 bumps.push(merkle_path.clone());
422 }
423 }
424 seen.insert(txid);
425 ancestors.push(node);
426 } else {
427 inside.insert(txid);
428 stack.push(Frame {
429 node,
430 inputs_left: node.sources.len(),
431 });
432 }
433 }
434 let Some(top) = stack.last_mut() else {
435 break;
436 };
437 if top.inputs_left == 0 {
438 let node = stack.pop().expect("the stack is not empty").node;
439 inside.remove(node.parsed.txid.as_str());
440 seen.insert(node.parsed.txid.as_str());
441 ancestors.push(node);
442 continue;
443 }
444 top.inputs_left -= 1;
445 if let Some(source) = &top.node.sources[top.inputs_left] {
446 entering = self.nodes.get(source).map(Rc::as_ref);
447 }
448 }
449
450 let mut beef = Beef::new();
451 for bump in bumps {
452 beef.merge_bump(bump);
453 }
454 for node in ancestors {
455 beef.merge_transaction(node.parsed.tx.clone());
456 }
457 let mut bytes = beef.to_binary();
460 bytes.shrink_to_fit();
461 bytes
462 }
463}
464
465impl<'a> OverlayGASPStorage<'a> {
466 pub fn new(
471 storage: &'a dyn Storage,
472 topic: impl Into<String>,
473 sink: FinalizedGraphSink,
474 ) -> Self {
475 Self {
476 storage,
477 topic: topic.into(),
478 topic_manager: None,
479 pending_graphs: Mutex::new(HashMap::new()),
480 finalized_sink: sink,
481 strict_beef: false,
482 chain_tracker: None,
483 verify_scripts: true,
484 walk_budget: crate::engine::DoorBudget::DEFAULT,
485 peer: String::new(),
486 configured_peer: false,
487 anchored: Mutex::new(None),
488 }
489 }
490
491 fn keep_anchored(&self, graph_id: &str, order: Vec<String>, assembly: GraphAssembly) {
493 *self
494 .anchored
495 .lock()
496 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(AnchoredGraph {
497 graph_id: graph_id.to_string(),
498 strict_beef: self.strict_beef,
499 order,
500 assembly,
501 });
502 }
503
504 fn take_anchored(&self) -> Option<AnchoredGraph> {
506 self.anchored
507 .lock()
508 .unwrap_or_else(std::sync::PoisonError::into_inner)
509 .take()
510 }
511
512 #[must_use]
516 pub fn with_peer(mut self, peer: impl Into<String>) -> Self {
517 self.peer = peer.into();
518 self
519 }
520
521 #[must_use]
527 pub fn with_configured_peer(mut self, configured: bool) -> Self {
528 self.configured_peer = configured;
529 self
530 }
531
532 #[must_use]
536 pub fn with_chain_tracker(mut self, tracker: &'a dyn ChainTracker) -> Self {
537 self.chain_tracker = Some(tracker);
538 self
539 }
540
541 #[must_use]
546 pub fn with_script_verification(mut self, enabled: bool) -> Self {
547 self.verify_scripts = enabled;
548 self
549 }
550
551 #[must_use]
554 pub fn with_walk_budget(mut self, budget: crate::engine::DoorBudget) -> Self {
555 self.walk_budget = budget;
556 self
557 }
558
559 #[must_use]
562 pub fn with_topic_manager(mut self, manager: &'a dyn TopicManager) -> Self {
563 self.topic_manager = Some(manager);
564 self
565 }
566
567 #[must_use]
574 pub fn with_strict_beef(mut self, strict: bool) -> Self {
575 self.strict_beef = strict;
576 self
577 }
578
579 pub fn take_finalized_graphs(&self) -> Vec<FinalizedGraph> {
584 self.finalized_sink
585 .lock()
586 .unwrap_or_else(std::sync::PoisonError::into_inner)
587 .drain(..)
588 .collect()
589 }
590
591 #[cfg(test)]
593 pub fn pending_graph_count(&self) -> usize {
594 let refs = self.pending_graphs.lock().unwrap();
595 let graph_ids: std::collections::HashSet<&str> =
596 refs.values().map(|pn| pn.node.graph_id.as_str()).collect();
597 graph_ids.len()
598 }
599
600 #[cfg(test)]
602 pub fn finalized_graph_count(&self) -> usize {
603 self.finalized_sink.lock().unwrap().len()
604 }
605
606 fn ordered_keys(graph_id: &str, refs: &HashMap<String, PendingNode>) -> Vec<String> {
611 let mut order: Vec<String> = Vec::new();
612 let mut visited: HashSet<&str> = HashSet::new();
613 let mut stack: Vec<(&str, &[String], usize)> = Vec::new();
615 let mut entering: Option<&str> = Some(graph_id);
616 loop {
617 if let Some(key) = entering.take() {
618 if visited.insert(key) {
619 if let Some(pending) = refs.get(key) {
620 stack.push((key, &pending.children, 0));
621 }
622 }
623 }
624 let Some((key, children, next)) = stack.last_mut() else {
625 return order;
626 };
627 if let Some(child) = children.get(*next) {
628 *next += 1;
629 entering = Some(child.as_str());
630 } else {
631 order.push((*key).to_string());
632 stack.pop();
633 }
634 }
635 }
636
637 fn compute_ordered_beefs(
645 graph_id: &str,
646 refs: &HashMap<String, PendingNode>,
647 strict_beef: bool,
648 anchored: Option<AnchoredGraph>,
649 ) -> Result<Vec<Vec<u8>>, GASPError> {
650 let (order, assembly) = match anchored {
651 Some(kept) if kept.graph_id == graph_id && kept.strict_beef == strict_beef => {
652 (kept.order, kept.assembly)
653 }
654 _ => {
655 let mut assembly = GraphAssembly::default();
656 let order = Self::ordered_keys(graph_id, refs);
657 for key in &order {
658 assembly.check(key, refs, strict_beef)?;
659 }
660 (order, assembly)
661 }
662 };
663 Ok(order.iter().map(|key| assembly.beef_of(key)).collect())
664 }
665
666 fn read_anchor_input(
686 graph_id: &str,
687 refs: &HashMap<String, PendingNode>,
688 strict_beef: bool,
689 ) -> Result<AnchorInput, GASPError> {
690 let unassembled = |e: GASPError| {
691 GASPError::AnchorUnavailable(format!("graph {graph_id} cannot be assembled: {e}"))
692 };
693 if !refs.contains_key(graph_id) {
694 return Err(GASPError::ValidationFailed(format!(
695 "Graph node with ID {graph_id} not found"
696 )));
697 }
698
699 let mut assembly = GraphAssembly::default();
700 let mut absent_sources: Vec<(String, u32)> = Vec::new();
701 let mut visited: HashSet<&str> = HashSet::new();
702 let mut stack: Vec<(&str, bool)> = vec![(graph_id, true)];
704 while let Some((key, in_root_beef)) = stack.pop() {
705 if !visited.insert(key) {
706 continue;
707 }
708 let Some(pending) = refs.get(key) else {
709 continue;
710 };
711 let parsed = assembly.parsed_tx(key, refs).map_err(unassembled)?;
712 let spends: HashSet<String> = parsed
713 .tx
714 .inputs
715 .iter()
716 .filter_map(|input| {
717 let source_txid = input.get_source_txid().ok()?;
718 Some(format!("{}.{}", source_txid, input.source_output_index))
719 })
720 .collect();
721 for child in &pending.children {
722 if !spends.contains(child) {
723 return Err(GASPError::ValidationFailed(format!(
724 "node {child} is not an input of its declared parent {key}"
725 )));
726 }
727 }
728 let unproven = pending.node.proof.is_none();
729 if in_root_beef && unproven {
730 for outpoint in &spends {
731 if !refs.contains_key(outpoint) {
732 if let Some(source) = crate::gasp::parse_outpoint(outpoint) {
733 absent_sources.push(source);
734 }
735 }
736 }
737 }
738 for child in &pending.children {
739 stack.push((child.as_str(), in_root_beef && unproven));
740 }
741 }
742
743 assembly.check(graph_id, refs, false).map_err(unassembled)?;
748 let root_beef = assembly.beef_of(graph_id);
749 let root_txid = assembly.txid_of(graph_id).unwrap_or_default().to_string();
750 let order = Self::ordered_keys(graph_id, refs);
751 for key in &order {
752 assembly
753 .check(key, refs, strict_beef)
754 .map_err(unassembled)?;
755 }
756 Ok(AnchorInput {
757 root_txid,
758 root_beef,
759 absent_sources,
760 order,
761 assembly,
762 })
763 }
764}
765
766#[async_trait(?Send)]
767impl GASPStorage for OverlayGASPStorage<'_> {
768 fn sync_expiry(&self, node: &GASPNode, outpoint: &str) -> Result<Option<u64>, GASPError> {
769 let Some(manager) = self.topic_manager else {
770 return Ok(None);
771 };
772 let tx = Transaction::from_hex(&node.raw_tx)
773 .map_err(|e| GASPError::Other(format!("sync root parse: {e}")))?;
774 if format!("{}.{}", tx.id(), node.output_index) != outpoint {
775 return Err(GASPError::Other(
776 "sync root content-address mismatch".into(),
777 ));
778 }
779 let output = tx
780 .outputs
781 .get(node.output_index as usize)
782 .ok_or_else(|| GASPError::Other("sync root output missing".into()))?;
783 Ok(manager.sync_expiry(&output.locking_script))
784 }
785
786 async fn find_known_utxos(
791 &self,
792 since: u64,
793 limit: Option<u64>,
794 ) -> Result<Vec<GASPOutput>, GASPError> {
795 let outputs = self
796 .storage
797 .find_utxos_for_topic(
798 &self.topic,
799 Some(since as f64),
800 limit,
801 false, )
803 .await
804 .map_err(|e| GASPError::StorageError(e.to_string()))?;
805
806 Ok(outputs
807 .iter()
808 .map(|o| GASPOutput {
809 txid: o.txid.clone(),
810 output_index: o.output_index,
811 score: o.score.unwrap_or(0.0),
812 })
813 .collect())
814 }
815
816 async fn hydrate_gasp_node(
826 &self,
827 graph_id: &str,
828 txid: &str,
829 output_index: u32,
830 _metadata: bool,
831 ) -> Result<GASPNode, GASPError> {
832 let output = self
834 .storage
835 .find_output(txid, output_index, Some(&self.topic), None, true)
836 .await
837 .map_err(|e| GASPError::StorageError(e.to_string()))?;
838
839 if let Some(output) = output {
840 if let Some(ref beef) = output.beef {
841 match beef_limits::transaction_from_beef(
843 beef,
844 None,
845 &beef_limits::STORED_BEEF_LIMITS,
846 ) {
847 Ok(tx) => {
848 let mut node = GASPNode {
849 graph_id: graph_id.to_string(),
850 raw_tx: tx.to_hex(),
851 output_index,
852 proof: None,
853 tx_metadata: None,
854 output_metadata: None,
855 inputs: None,
856 };
857 if let Some(ref merkle_path) = tx.merkle_path {
858 node.proof = Some(merkle_path.to_hex());
859 }
860 return Ok(node);
861 }
862 Err(e) => {
863 warn!("Failed to parse BEEF for {txid}: {e}");
864 }
865 }
866 }
867 }
868
869 let output = self
871 .storage
872 .find_output(txid, output_index, None, None, true)
873 .await
874 .map_err(|e| GASPError::StorageError(e.to_string()))?
875 .ok_or_else(|| {
876 GASPError::NodeNotFound(format!("Output {txid}.{output_index} not found"))
877 })?;
878
879 let beef = output.beef.as_ref().ok_or_else(|| {
880 GASPError::NodeNotFound(format!("No BEEF data for {txid}.{output_index}"))
881 })?;
882
883 let tx = beef_limits::transaction_from_beef(beef, None, &beef_limits::STORED_BEEF_LIMITS)
884 .map_err(|e| GASPError::Other(format!("BEEF parse error: {e}")))?;
885
886 let mut node = GASPNode {
887 graph_id: graph_id.to_string(),
888 raw_tx: tx.to_hex(),
889 output_index,
890 proof: None,
891 tx_metadata: None,
892 output_metadata: None,
893 inputs: None,
894 };
895 if let Some(ref merkle_path) = tx.merkle_path {
896 node.proof = Some(merkle_path.to_hex());
897 }
898 Ok(node)
899 }
900
901 async fn find_needed_inputs(
915 &self,
916 node: &GASPNode,
917 ) -> Result<Option<GASPNodeResponse>, GASPError> {
918 let mut requested_inputs: HashMap<String, GASPInputRequest> = HashMap::new();
919 let mut admitted = false;
921
922 if let Some(proof_hex) = &node.proof {
923 let Some(manager) = self.topic_manager else {
924 return Ok(None);
925 };
926 let mut tx = Transaction::from_hex(&node.raw_tx)
927 .map_err(|e| GASPError::Other(format!("Failed to parse raw_tx: {e}")))?;
928 tx.merkle_path = Some(
929 beef_limits::proof_from_hex(proof_hex)
930 .map_err(|e| GASPError::Other(format!("Failed to parse proof: {e}")))?,
931 );
932
933 let admittance = manager
938 .identify_admissible_outputs(
939 &tx,
940 &[],
941 node.tx_metadata.as_deref().map(str::as_bytes),
942 SubmitMode::HistoricalTx,
943 &TopicAdmittanceContext::DRY_RUN,
944 )
945 .await
946 .map_err(|e| GASPError::Other(e.to_string()))?;
947 admitted = admittance.outputs_to_admit.contains(&node.output_index);
961 if admitted
964 && !tx
965 .inputs
966 .iter()
967 .any(|input| input.get_source_txid().is_ok_and(|s| !s.is_empty()))
968 {
969 return Ok(None);
970 }
971
972 let needed_inputs = async {
974 let beef = tx
975 .to_beef(false)
976 .map_err(|e| format!("Failed to serialize proven BEEF: {e}"))?;
977 manager
978 .identify_needed_inputs(&beef, None)
979 .await
980 .map_err(|e| e.to_string())
981 }
982 .await;
983 match needed_inputs {
984 Ok(inputs) => {
985 for input in inputs {
986 requested_inputs
987 .insert(input.to_graph_id(), GASPInputRequest { metadata: false });
988 }
989 }
990 Err(e) => {
991 error!(
992 "An error occurred when identifying needed inputs for transaction: {}.{}: {e}",
993 tx.id(), node.output_index
994 );
995 return Ok(None);
996 }
997 }
998 } else {
999 let tx = match Transaction::from_hex(&node.raw_tx) {
1001 Ok(tx) => tx,
1002 Err(e) => {
1003 warn!(
1004 "Cannot parse raw_tx for find_needed_inputs (graph_id={}): {e}",
1005 node.graph_id
1006 );
1007 return Ok(None);
1008 }
1009 };
1010 for input in &tx.inputs {
1011 let source_txid = input.get_source_txid().unwrap_or_default();
1012 if source_txid.is_empty() {
1013 continue;
1014 }
1015 let outpoint = format!("{}.{}", source_txid, input.source_output_index);
1016 requested_inputs.insert(outpoint, GASPInputRequest { metadata: false });
1017 }
1018 }
1019
1020 if requested_inputs.is_empty() {
1021 return Ok(None);
1022 }
1023
1024 let mut to_remove = Vec::new();
1026 for outpoint in requested_inputs.keys() {
1027 if let Some((txid, oi)) = crate::gasp::parse_outpoint(outpoint) {
1028 match self
1029 .storage
1030 .find_output(&txid, oi, Some(&self.topic), None, false)
1031 .await
1032 {
1033 Ok(Some(_)) => {
1034 to_remove.push(outpoint.clone());
1035 }
1036 Ok(None) => {}
1037 Err(e) => {
1038 debug!("Storage lookup failed for {outpoint}: {e}");
1039 }
1040 }
1041 }
1042 }
1043 for key in &to_remove {
1044 requested_inputs.remove(key);
1045 }
1046
1047 if admitted {
1057 let mut landed: HashSet<String> = HashSet::new();
1058 let mut unlanded: HashSet<String> = HashSet::new();
1059 let mut reads = 0usize;
1060 let mut outpoints: Vec<&String> = requested_inputs.keys().collect();
1061 outpoints.sort();
1062 for outpoint in outpoints {
1063 let Some((txid, _)) = crate::gasp::parse_outpoint(outpoint) else {
1064 continue;
1065 };
1066 if landed.contains(&txid) || unlanded.contains(&txid) {
1067 continue;
1068 }
1069 if reads + 2 > LANDED_READS_PER_NODE {
1070 break;
1071 }
1072 reads += 1;
1073 let record = crate::types::AppliedTransaction {
1074 txid: txid.clone(),
1075 topic: self.topic.clone(),
1076 };
1077 let mut holds = matches!(
1078 self.storage.does_applied_transaction_exist(&record).await,
1079 Ok(true)
1080 );
1081 if !holds {
1082 reads += 1;
1083 holds = matches!(
1084 self.storage.find_outputs_for_transaction(&txid, false).await,
1085 Ok(outputs) if outputs.iter().any(|o| o.topic == self.topic)
1086 );
1087 }
1088 if holds {
1089 landed.insert(txid);
1090 } else {
1091 unlanded.insert(txid);
1092 }
1093 }
1094 requested_inputs.retain(|outpoint, _| {
1095 crate::gasp::parse_outpoint(outpoint)
1096 .is_none_or(|(txid, _)| !landed.contains(&txid))
1097 });
1098 }
1099
1100 if requested_inputs.is_empty() {
1101 return Ok(None);
1102 }
1103
1104 Ok(Some(GASPNodeResponse { requested_inputs }))
1105 }
1106
1107 async fn append_to_graph(
1113 &self,
1114 node: &GASPNode,
1115 spent_by: Option<&str>,
1116 ) -> Result<(), GASPError> {
1117 debug!(
1118 "Appending node to graph {}: tx={}..., oi={}, spent_by={:?}",
1119 node.graph_id,
1120 &node.raw_tx[..node.raw_tx.len().min(16)],
1121 node.output_index,
1122 spent_by,
1123 );
1124
1125 let node_key = if spent_by.is_none() {
1127 node.graph_id.clone()
1129 } else {
1130 match Transaction::from_hex(&node.raw_tx) {
1132 Ok(tx) => format!("{}.{}", tx.id(), node.output_index),
1133 Err(_) => {
1134 format!(
1136 "{}.{}",
1137 &node.raw_tx[..node.raw_tx.len().min(64)],
1138 node.output_index
1139 )
1140 }
1141 }
1142 };
1143
1144 self.take_anchored();
1146 let mut refs = self
1147 .pending_graphs
1148 .lock()
1149 .map_err(|e| GASPError::Other(format!("Lock poisoned: {e}")))?;
1150
1151 refs.insert(
1153 node_key.clone(),
1154 PendingNode {
1155 node: node.clone(),
1156 spent_by: spent_by.map(std::string::ToString::to_string),
1157 children: Vec::new(),
1158 },
1159 );
1160
1161 if let Some(parent_key) = spent_by {
1163 if let Some(parent) = refs.get_mut(parent_key) {
1164 parent.children.push(node_key);
1165 }
1166 }
1167
1168 Ok(())
1169 }
1170
1171 async fn validate_graph_anchor(&self, graph_id: &str) -> Result<(), GASPError> {
1288 let anchor = {
1289 let refs = self
1290 .pending_graphs
1291 .lock()
1292 .map_err(|e| GASPError::Other(format!("Lock poisoned: {e}")))?;
1293 Self::read_anchor_input(graph_id, &refs, self.strict_beef)?
1294 };
1295 let unavailable =
1296 |what: String| GASPError::AnchorUnavailable(format!("graph {graph_id}: {what}"));
1297 let refused = |why: String| GASPError::ValidationFailed(format!("graph {graph_id}: {why}"));
1298
1299 let mut root_beef = anchor.root_beef;
1301 if !anchor.absent_sources.is_empty() {
1302 let mut merged = beef_limits::parse_beef(&root_beef, &beef_limits::PEER_BEEF_LIMITS)
1303 .map_err(|e| refused(format!("root BEEF does not parse: {e}")))?;
1304 for (txid, output_index) in &anchor.absent_sources {
1305 let held = self
1306 .storage
1307 .find_output(txid, *output_index, Some(&self.topic), None, true)
1308 .await
1309 .map_err(|e| unavailable(format!("reading {txid}.{output_index}: {e}")))?;
1310 if let Some(stored) = held.and_then(|output| output.beef) {
1311 if let Ok(stored) =
1312 beef_limits::parse_beef(&stored, &beef_limits::STORED_BEEF_LIMITS)
1313 {
1314 merged.merge_beef(&stored);
1315 }
1316 }
1317 }
1318 root_beef = merged.to_binary();
1319 }
1320 let tracker = self.chain_tracker.map(|inner| FaultNotingTracker {
1321 inner,
1322 faulted: AtomicBool::new(false),
1323 });
1324 let verdict = crate::engine::verify_spv_like_the_reference(
1325 tracker.as_ref().map(|t| t as &dyn ChainTracker),
1326 self.verify_scripts,
1327 &root_beef,
1328 &anchor.root_txid,
1329 self.walk_budget,
1330 )
1331 .await;
1332 drop(root_beef);
1335 let verdict = match verdict {
1339 Ok(Some(stop)) => {
1340 warn!(
1341 "GASP anchor: the script walk of {} could not run ({}: at {}; {}): held, the graph is not finalized until its root is served proven (bsv-low #592, E592 fold)",
1342 stop.subject_txid,
1343 stop.limb.as_str(),
1344 stop.at_txid,
1345 stop.what
1346 );
1347 return Err(GASPError::AnchorWalkHeld(format!(
1348 "graph {graph_id}: the script walk of {} could not run within the budget ({}: {})",
1349 stop.subject_txid,
1350 stop.limb.as_str(),
1351 stop.what
1352 )));
1353 }
1354 Ok(None) => Ok(()),
1355 Err(e) => Err(e),
1356 };
1357 if let Err(e) = verdict {
1358 if tracker
1359 .as_ref()
1360 .is_some_and(|t| t.faulted.load(Ordering::Relaxed))
1361 {
1362 return Err(unavailable(format!("the chain tracker faulted: {e}")));
1363 }
1364 return Err(refused(format!(
1365 "The graph is not well-anchored according to the rules of Bitcoin: {e}"
1366 )));
1367 }
1368
1369 let Some(manager) = self.topic_manager else {
1371 self.keep_anchored(graph_id, anchor.order, anchor.assembly);
1372 return Ok(());
1373 };
1374 let mut coins: HashSet<String> = HashSet::new();
1375 let mut spent: HashSet<String> = HashSet::new();
1376 let mut replayed: HashSet<String> = HashSet::new();
1377 for key in &anchor.order {
1378 let beef = &anchor.assembly.beef_of(key);
1381 let subject = beef_limits::parse_beef(beef, &beef_limits::PEER_BEEF_LIMITS)
1383 .map(|mut b| crate::subject::subject_txid_of(&mut b))
1384 .map_err(|e| refused(format!("a graph BEEF does not parse: {e}")))?;
1385 let tx = beef_limits::transaction_from_beef(
1386 beef,
1387 subject.as_deref(),
1388 &beef_limits::PEER_BEEF_LIMITS,
1389 )
1390 .map_err(|e| refused(format!("a graph BEEF does not parse: {e}")))?;
1391 let txid = tx.id();
1392 if !replayed.insert(txid.clone()) {
1394 continue;
1395 }
1396 let mut previous_coins: Vec<u8> = Vec::new();
1397 for (input_index, input) in tx.inputs.iter().enumerate() {
1398 let source_txid = input.get_source_txid().unwrap_or_default();
1399 if source_txid.is_empty() {
1400 continue;
1401 }
1402 let outpoint = format!("{}.{}", source_txid, input.source_output_index);
1403 if !spent.insert(outpoint.clone()) {
1404 return Err(refused(format!(
1405 "the graph contains a conflicting spend of {outpoint}"
1406 )));
1407 }
1408 let is_coin = coins.contains(&outpoint)
1409 || self
1410 .storage
1411 .find_output(
1412 &source_txid,
1413 input.source_output_index,
1414 Some(&self.topic),
1415 None,
1416 false,
1417 )
1418 .await
1419 .map_err(|e| unavailable(format!("reading {outpoint}: {e}")))?
1420 .is_some();
1421 if is_coin {
1422 previous_coins.extend_from_slice(&(input_index as u32).to_le_bytes());
1423 }
1424 }
1425 let admittance = manager
1430 .identify_admissible_outputs(
1431 &tx,
1432 &previous_coins,
1433 None,
1434 SubmitMode::HistoricalTx,
1435 &TopicAdmittanceContext::DRY_RUN,
1436 )
1437 .await
1438 .map_err(|e| unavailable(format!("the topic manager failed on {txid}: {e}")))?;
1439 for output_index in admittance.outputs_to_admit {
1440 coins.insert(format!("{txid}.{output_index}"));
1441 }
1442 }
1443 if !coins.contains(graph_id) {
1444 return Err(refused(
1445 "This graph did not result in topical admittance of the root node. Rejecting."
1446 .to_string(),
1447 ));
1448 }
1449 self.keep_anchored(graph_id, anchor.order, anchor.assembly);
1450 Ok(())
1451 }
1452
1453 async fn finalize_graph(&self, graph_id: &str) -> Result<(), GASPError> {
1463 let refs = self
1464 .pending_graphs
1465 .lock()
1466 .map_err(|e| GASPError::Other(format!("Lock poisoned: {e}")))?;
1467
1468 if !refs.contains_key(graph_id) {
1469 return Err(GASPError::Other(format!("No pending graph for {graph_id}")));
1470 }
1471
1472 let node_count = refs
1473 .values()
1474 .filter(|pn| pn.node.graph_id == graph_id)
1475 .count();
1476
1477 debug!("Finalizing graph {graph_id} ({node_count} nodes). Computing ordered BEEFs.");
1478
1479 let beefs =
1481 Self::compute_ordered_beefs(graph_id, &refs, self.strict_beef, self.take_anchored())?;
1482
1483 drop(refs);
1485 self.finalized_sink
1486 .lock()
1487 .map_err(|e| GASPError::Other(format!("Lock poisoned: {e}")))?
1488 .push(FinalizedGraph {
1489 topic: self.topic.clone(),
1490 beefs,
1491 });
1492
1493 let mut refs = self
1495 .pending_graphs
1496 .lock()
1497 .map_err(|e| GASPError::Other(format!("Lock poisoned: {e}")))?;
1498 let keys_to_remove: Vec<String> = refs
1499 .iter()
1500 .filter(|(_, pn)| pn.node.graph_id == graph_id)
1501 .map(|(k, _)| k.clone())
1502 .collect();
1503 for key in keys_to_remove {
1504 refs.remove(&key);
1505 }
1506
1507 Ok(())
1508 }
1509
1510 async fn discard_graph(&self, graph_id: &str) -> Result<(), GASPError> {
1512 debug!("Discarding graph: {graph_id}");
1513 self.take_anchored();
1514 let mut refs = self
1515 .pending_graphs
1516 .lock()
1517 .map_err(|e| GASPError::Other(format!("Lock poisoned: {e}")))?;
1518 let keys_to_remove: Vec<String> = refs
1519 .iter()
1520 .filter(|(_, pn)| pn.node.graph_id == graph_id)
1521 .map(|(k, _)| k.clone())
1522 .collect();
1523 for key in keys_to_remove {
1524 refs.remove(&key);
1525 }
1526 Ok(())
1527 }
1528
1529 async fn load_deferred_graphs(&self) -> Result<Vec<crate::gasp::DeferredGraphKey>, GASPError> {
1530 self.storage
1531 .find_deferred_graphs(&self.peer, &self.topic)
1532 .await
1533 .map_err(|e| GASPError::StorageError(e.to_string()))
1534 }
1535
1536 async fn get_deferred_graph(
1537 &self,
1538 outpoint: &str,
1539 ) -> Result<Option<crate::gasp::DeferredGraph>, GASPError> {
1540 self.storage
1541 .get_deferred_graph(&self.peer, &self.topic, outpoint)
1542 .await
1543 .map_err(|e| GASPError::StorageError(e.to_string()))
1544 }
1545
1546 async fn save_deferred_graph(
1547 &self,
1548 record: &crate::gasp::DeferredGraph,
1549 ) -> Result<crate::gasp::DeferredGraphSave, GASPError> {
1550 let mut record = record.clone();
1551 record.peer.clone_from(&self.peer);
1552 record.topic.clone_from(&self.topic);
1553 record.configured = self.configured_peer;
1554 self.storage
1555 .put_deferred_graph(&record)
1556 .await
1557 .map_err(|e| GASPError::StorageError(e.to_string()))
1558 }
1559
1560 async fn reserve_unanchorable_retry(
1561 &self,
1562 record: &crate::gasp::DeferredGraph,
1563 ) -> Result<bool, GASPError> {
1564 let mut record = record.clone();
1565 record.peer.clone_from(&self.peer);
1566 record.topic.clone_from(&self.topic);
1567 record.configured = self.configured_peer;
1568 self.storage
1569 .reserve_unanchorable_retry(&record)
1570 .await
1571 .map_err(|e| GASPError::StorageError(e.to_string()))
1572 }
1573
1574 async fn delete_deferred_graph(&self, outpoint: &str) -> Result<(), GASPError> {
1575 self.storage
1576 .delete_deferred_graph(&self.peer, &self.topic, outpoint)
1577 .await
1578 .map_err(|e| GASPError::StorageError(e.to_string()))
1579 }
1580
1581 async fn remember_expired(&self, outpoint: &str, expiry: u64) -> Result<(), GASPError> {
1584 self.storage
1585 .remember_expired_outpoint(&self.topic, outpoint, expiry)
1586 .await
1587 .map_err(|e| GASPError::StorageError(e.to_string()))
1588 }
1589
1590 async fn remembered_expired(&self, outpoints: &[String]) -> Result<Vec<String>, GASPError> {
1591 self.storage
1592 .find_expired_outpoints(&self.topic, outpoints)
1593 .await
1594 .map_err(|e| GASPError::StorageError(e.to_string()))
1595 }
1596}
1597
1598#[cfg(test)]
1603mod tests {
1604 use super::*;
1605 use crate::gasp::GASPStorage;
1606 use crate::storage::memory::MemoryStorage;
1607 use crate::storage::Storage;
1608 use crate::types::Output;
1609
1610 fn make_sink() -> FinalizedGraphSink {
1611 new_finalized_graph_sink()
1612 }
1613
1614 fn make_output(txid: &str, index: u32, topic: &str, score: f64) -> Output {
1615 Output {
1616 txid: txid.to_string(),
1617 output_index: index,
1618 output_script: vec![0x76, 0xa9],
1619 satoshis: 1000,
1620 topic: topic.to_string(),
1621 spent: false,
1622 outputs_consumed: vec![],
1623 consumed_by: vec![],
1624 beef: Some(vec![0xBE, 0xEF]),
1625 block_height: None,
1626 score: Some(score),
1627 }
1628 }
1629
1630 fn make_valid_tx_hex(source_txid: &str, source_oi: u32) -> String {
1632 let mut tx = Transaction::new();
1633 let input = bsv_rs::transaction::TransactionInput::new(source_txid.to_string(), source_oi);
1634 tx.inputs.push(input);
1635 tx.outputs.push(bsv_rs::transaction::TransactionOutput::new(
1636 100,
1637 bsv_rs::script::LockingScript::from_hex(
1638 "76a914000000000000000000000000000000000000000088ac",
1639 )
1640 .unwrap(),
1641 ));
1642 tx.to_hex()
1643 }
1644
1645 fn make_no_input_tx_hex() -> String {
1649 let mut tx = Transaction::new();
1650 tx.outputs.push(bsv_rs::transaction::TransactionOutput::new(
1651 5000,
1652 bsv_rs::script::LockingScript::from_hex(
1653 "76a914000000000000000000000000000000000000000088ac",
1654 )
1655 .unwrap(),
1656 ));
1657 tx.to_hex()
1658 }
1659
1660 fn make_coinbase_tx_hex() -> String {
1661 let mut tx = Transaction::new();
1662 let mut input = bsv_rs::transaction::TransactionInput::new("00".repeat(32), u32::MAX);
1665 input.unlocking_script =
1666 Some(bsv_rs::script::UnlockingScript::from_binary(&[0x51]).unwrap());
1667 tx.inputs.push(input);
1668 tx.outputs.push(bsv_rs::transaction::TransactionOutput::new(
1669 5000,
1670 bsv_rs::script::LockingScript::from_hex(
1671 "76a914000000000000000000000000000000000000000088ac",
1672 )
1673 .unwrap(),
1674 ));
1675 tx.to_hex()
1676 }
1677
1678 #[tokio::test]
1681 async fn find_known_utxos_returns_correct_outputs() {
1682 let store = MemoryStorage::new();
1683 store
1684 .insert_output(&make_output("tx1", 0, "tm_test", 100.0))
1685 .await
1686 .unwrap();
1687 store
1688 .insert_output(&make_output("tx2", 0, "tm_test", 200.0))
1689 .await
1690 .unwrap();
1691 store
1692 .insert_output(&make_output("tx3", 0, "tm_other", 300.0))
1693 .await
1694 .unwrap();
1695
1696 let gasp_storage = OverlayGASPStorage::new(&store, "tm_test", make_sink());
1697
1698 let utxos = gasp_storage.find_known_utxos(0, None).await.unwrap();
1699 assert_eq!(utxos.len(), 2, "Should return only tm_test UTXOs");
1700 assert_eq!(utxos[0].txid, "tx1");
1701 assert_eq!(utxos[1].txid, "tx2");
1702 }
1703
1704 #[tokio::test]
1705 async fn find_known_utxos_respects_since() {
1706 let store = MemoryStorage::new();
1707 store
1708 .insert_output(&make_output("tx1", 0, "tm_test", 100.0))
1709 .await
1710 .unwrap();
1711 store
1712 .insert_output(&make_output("tx2", 0, "tm_test", 200.0))
1713 .await
1714 .unwrap();
1715 store
1716 .insert_output(&make_output("tx3", 0, "tm_test", 300.0))
1717 .await
1718 .unwrap();
1719
1720 let gasp_storage = OverlayGASPStorage::new(&store, "tm_test", make_sink());
1721
1722 let utxos = gasp_storage.find_known_utxos(200, None).await.unwrap();
1723 assert_eq!(utxos.len(), 2, "Should return UTXOs with score >= 200");
1724 assert_eq!(utxos[0].txid, "tx2");
1725 assert_eq!(utxos[1].txid, "tx3");
1726 }
1727
1728 #[tokio::test]
1729 async fn find_known_utxos_respects_limit() {
1730 let store = MemoryStorage::new();
1731 store
1732 .insert_output(&make_output("tx1", 0, "tm_test", 100.0))
1733 .await
1734 .unwrap();
1735 store
1736 .insert_output(&make_output("tx2", 0, "tm_test", 200.0))
1737 .await
1738 .unwrap();
1739 store
1740 .insert_output(&make_output("tx3", 0, "tm_test", 300.0))
1741 .await
1742 .unwrap();
1743
1744 let gasp_storage = OverlayGASPStorage::new(&store, "tm_test", make_sink());
1745
1746 let utxos = gasp_storage.find_known_utxos(0, Some(2)).await.unwrap();
1747 assert_eq!(utxos.len(), 2, "Should limit to 2 results");
1748 }
1749
1750 #[tokio::test]
1751 async fn find_known_utxos_excludes_spent() {
1752 let store = MemoryStorage::new();
1753 store
1754 .insert_output(&make_output("tx1", 0, "tm_test", 100.0))
1755 .await
1756 .unwrap();
1757 store
1758 .insert_output(&make_output("tx2", 0, "tm_test", 200.0))
1759 .await
1760 .unwrap();
1761 store.mark_utxo_as_spent("tx1", 0, "tm_test").await.unwrap();
1762
1763 let gasp_storage = OverlayGASPStorage::new(&store, "tm_test", make_sink());
1764
1765 let utxos = gasp_storage.find_known_utxos(0, None).await.unwrap();
1766 assert_eq!(utxos.len(), 1, "Should exclude spent UTXOs");
1767 assert_eq!(utxos[0].txid, "tx2");
1768 }
1769
1770 #[tokio::test]
1771 async fn find_known_utxos_empty_topic() {
1772 let store = MemoryStorage::new();
1773 let gasp_storage = OverlayGASPStorage::new(&store, "tm_empty", make_sink());
1774
1775 let utxos = gasp_storage.find_known_utxos(0, None).await.unwrap();
1776 assert!(utxos.is_empty());
1777 }
1778
1779 #[tokio::test]
1782 async fn find_needed_inputs_returns_none_for_unparsable_tx() {
1783 let store = MemoryStorage::new();
1784 let gasp_storage = OverlayGASPStorage::new(&store, "tm_test", make_sink());
1785
1786 let node = GASPNode {
1787 graph_id: "abc.0".to_string(),
1788 raw_tx: "deadbeef".to_string(), output_index: 0,
1790 proof: None,
1791 tx_metadata: None,
1792 output_metadata: None,
1793 inputs: None,
1794 };
1795
1796 let result = gasp_storage.find_needed_inputs(&node).await.unwrap();
1797 assert!(result.is_none(), "Unparsable tx: graceful fallback to None");
1798 }
1799
1800 #[tokio::test]
1801 async fn find_needed_inputs_returns_none_for_proved_tx() {
1802 let store = MemoryStorage::new();
1803 let gasp_storage = OverlayGASPStorage::new(&store, "tm_test", make_sink());
1804
1805 let raw_tx = make_valid_tx_hex(
1807 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1808 0,
1809 );
1810 let node = GASPNode {
1811 graph_id: "abc.0".to_string(),
1812 raw_tx,
1813 output_index: 0,
1814 proof: Some("some_proof_hex".to_string()),
1815 tx_metadata: None,
1816 output_metadata: None,
1817 inputs: None,
1818 };
1819
1820 let result = gasp_storage.find_needed_inputs(&node).await.unwrap();
1821 assert!(result.is_none(), "Proved tx should not need inputs");
1822 }
1823
1824 #[tokio::test]
1825 async fn find_needed_inputs_requests_inputs_for_unproved_tx() {
1826 let store = MemoryStorage::new();
1827 let gasp_storage = OverlayGASPStorage::new(&store, "tm_test", make_sink());
1828
1829 let source_txid = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
1830 let raw_tx = make_valid_tx_hex(source_txid, 0);
1831
1832 let node = GASPNode {
1833 graph_id: "abc.0".to_string(),
1834 raw_tx,
1835 output_index: 0,
1836 proof: None,
1837 tx_metadata: None,
1838 output_metadata: None,
1839 inputs: None,
1840 };
1841
1842 let result = gasp_storage.find_needed_inputs(&node).await.unwrap();
1843 assert!(result.is_some(), "Unproved tx should request inputs");
1844 let response = result.unwrap();
1845 assert_eq!(response.requested_inputs.len(), 1);
1846 let key = format!("{source_txid}.0");
1847 assert!(
1848 response.requested_inputs.contains_key(&key),
1849 "Should request the source input"
1850 );
1851 }
1852
1853 #[tokio::test]
1854 async fn find_needed_inputs_strips_known_inputs() {
1855 let store = MemoryStorage::new();
1856 let source_txid = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
1857
1858 store
1860 .insert_output(&make_output(source_txid, 0, "tm_test", 50.0))
1861 .await
1862 .unwrap();
1863
1864 let gasp_storage = OverlayGASPStorage::new(&store, "tm_test", make_sink());
1865
1866 let raw_tx = make_valid_tx_hex(source_txid, 0);
1867 let node = GASPNode {
1868 graph_id: "abc.0".to_string(),
1869 raw_tx,
1870 output_index: 0,
1871 proof: None,
1872 tx_metadata: None,
1873 output_metadata: None,
1874 inputs: None,
1875 };
1876
1877 let result = gasp_storage.find_needed_inputs(&node).await.unwrap();
1878 assert!(
1879 result.is_none(),
1880 "Should strip already-known inputs, resulting in None"
1881 );
1882 }
1883
1884 #[tokio::test]
1885 async fn find_needed_inputs_returns_none_for_coinbase() {
1886 let store = MemoryStorage::new();
1887 let gasp_storage = OverlayGASPStorage::new(&store, "tm_test", make_sink());
1888
1889 let raw_tx = make_no_input_tx_hex();
1890 let node = GASPNode {
1891 graph_id: "abc.0".to_string(),
1892 raw_tx,
1893 output_index: 0,
1894 proof: None,
1895 tx_metadata: None,
1896 output_metadata: None,
1897 inputs: None,
1898 };
1899
1900 let result = gasp_storage.find_needed_inputs(&node).await.unwrap();
1901 assert!(
1902 result.is_none(),
1903 "Coinbase-like tx (no inputs) should need nothing"
1904 );
1905 }
1906
1907 #[tokio::test]
1910 async fn append_and_finalize_graph() {
1911 let store = MemoryStorage::new();
1912 let sink = make_sink();
1913 let gasp_storage = OverlayGASPStorage::new(&store, "tm_test", sink.clone());
1914
1915 let raw_tx = make_coinbase_tx_hex();
1916 let node = GASPNode {
1917 graph_id: "abc.0".to_string(),
1918 raw_tx,
1919 output_index: 0,
1920 proof: None,
1921 tx_metadata: None,
1922 output_metadata: None,
1923 inputs: None,
1924 };
1925
1926 gasp_storage.append_to_graph(&node, None).await.unwrap();
1927 assert_eq!(gasp_storage.pending_graph_count(), 1);
1928
1929 gasp_storage.finalize_graph("abc.0").await.unwrap();
1930 assert_eq!(gasp_storage.pending_graph_count(), 0);
1931
1932 let finalized = sink.lock().unwrap();
1934 assert_eq!(finalized.len(), 1, "Should have one finalized graph");
1935 assert_eq!(finalized[0].topic, "tm_test");
1936 assert!(!finalized[0].beefs.is_empty(), "Should have BEEF data");
1937 }
1938
1939 #[tokio::test]
1940 async fn finalize_graph_produces_submittable_beef() {
1941 let store = MemoryStorage::new();
1942 let sink = make_sink();
1943 let gasp_storage = OverlayGASPStorage::new(&store, "tm_test", sink.clone());
1944
1945 let raw_tx = make_coinbase_tx_hex();
1946 let node = GASPNode {
1947 graph_id: "root.0".to_string(),
1948 raw_tx,
1949 output_index: 0,
1950 proof: None,
1951 tx_metadata: None,
1952 output_metadata: None,
1953 inputs: None,
1954 };
1955
1956 gasp_storage.append_to_graph(&node, None).await.unwrap();
1957 gasp_storage.finalize_graph("root.0").await.unwrap();
1958
1959 let finalized = sink.lock().unwrap();
1960 assert_eq!(finalized.len(), 1);
1961
1962 for beef_bytes in &finalized[0].beefs {
1964 let tx = Transaction::from_beef(beef_bytes, None);
1965 assert!(
1966 tx.is_ok(),
1967 "Finalized BEEF should be parseable: {:?}",
1968 tx.err()
1969 );
1970 }
1971 }
1972
1973 #[tokio::test]
1974 async fn append_multiple_nodes_same_graph() {
1975 let store = MemoryStorage::new();
1976 let gasp_storage = OverlayGASPStorage::new(&store, "tm_test", make_sink());
1977
1978 let raw_tx1 = make_coinbase_tx_hex();
1979 let node1 = GASPNode {
1980 graph_id: "abc.0".to_string(),
1981 raw_tx: raw_tx1,
1982 output_index: 0,
1983 proof: None,
1984 tx_metadata: None,
1985 output_metadata: None,
1986 inputs: None,
1987 };
1988 let raw_tx2 = make_coinbase_tx_hex();
1989 let node2 = GASPNode {
1990 graph_id: "abc.0".to_string(),
1991 raw_tx: raw_tx2,
1992 output_index: 0,
1993 proof: None,
1994 tx_metadata: None,
1995 output_metadata: None,
1996 inputs: None,
1997 };
1998
1999 gasp_storage.append_to_graph(&node1, None).await.unwrap();
2000 gasp_storage
2001 .append_to_graph(&node2, Some("abc.0"))
2002 .await
2003 .unwrap();
2004 assert_eq!(gasp_storage.pending_graph_count(), 1);
2005 }
2006
2007 #[tokio::test]
2008 async fn discard_graph_removes_pending() {
2009 let store = MemoryStorage::new();
2010 let gasp_storage = OverlayGASPStorage::new(&store, "tm_test", make_sink());
2011
2012 let raw_tx = make_coinbase_tx_hex();
2013 let node = GASPNode {
2014 graph_id: "abc.0".to_string(),
2015 raw_tx,
2016 output_index: 0,
2017 proof: None,
2018 tx_metadata: None,
2019 output_metadata: None,
2020 inputs: None,
2021 };
2022
2023 gasp_storage.append_to_graph(&node, None).await.unwrap();
2024 assert_eq!(gasp_storage.pending_graph_count(), 1);
2025
2026 gasp_storage.discard_graph("abc.0").await.unwrap();
2027 assert_eq!(gasp_storage.pending_graph_count(), 0);
2028 }
2029
2030 #[tokio::test]
2031 async fn validate_graph_anchor_refuses_an_unknown_graph() {
2032 let store = MemoryStorage::new();
2033 let gasp_storage = OverlayGASPStorage::new(&store, "tm_test", make_sink());
2034
2035 let error = gasp_storage
2037 .validate_graph_anchor("nonexistent.0")
2038 .await
2039 .unwrap_err();
2040 assert!(matches!(error, GASPError::ValidationFailed(_)), "{error}");
2041 }
2042
2043 #[tokio::test]
2044 async fn finalize_empty_graph_errors() {
2045 let store = MemoryStorage::new();
2046 let gasp_storage = OverlayGASPStorage::new(&store, "tm_test", make_sink());
2047
2048 let result = gasp_storage.finalize_graph("nonexistent.0").await;
2049 assert!(result.is_err(), "Should error on unknown graph");
2050 }
2051
2052 #[tokio::test]
2053 async fn gasp_storage_is_object_safe() {
2054 let store = MemoryStorage::new();
2055 let gasp_storage = OverlayGASPStorage::new(&store, "tm_test", make_sink());
2056
2057 let boxed: Box<dyn GASPStorage> = Box::new(gasp_storage);
2059 let utxos = boxed.find_known_utxos(0, None).await.unwrap();
2060 assert!(utxos.is_empty());
2061 }
2062
2063 #[tokio::test]
2065 async fn overlay_gasp_storage_with_gasp_sync() {
2066 use crate::gasp::{GASPRemote, GASPSync};
2067 use crate::types::{GASPInitialReply, GASPInitialRequest, GASPInitialResponse};
2068
2069 let store = MemoryStorage::new();
2071 store
2072 .insert_output(&make_output("local_tx", 0, "tm_test", 50.0))
2073 .await
2074 .unwrap();
2075
2076 let sink = make_sink();
2077 let gasp_storage = OverlayGASPStorage::new(&store, "tm_test", sink.clone());
2078
2079 struct TestRemote;
2082
2083 #[async_trait(?Send)]
2084 impl GASPRemote for TestRemote {
2085 async fn get_initial_response(
2086 &self,
2087 request: &GASPInitialRequest,
2088 ) -> Result<GASPInitialResponse, GASPError> {
2089 Ok(GASPInitialResponse {
2090 utxo_list: vec![
2091 GASPOutput {
2092 txid: "remote_tx1".to_string(),
2093 output_index: 0,
2094 score: 100.0,
2095 },
2096 GASPOutput {
2097 txid: "remote_tx2".to_string(),
2098 output_index: 0,
2099 score: 200.0,
2100 },
2101 ],
2102 since: request.since,
2103 })
2104 }
2105 async fn get_initial_reply(
2106 &self,
2107 _: &GASPInitialResponse,
2108 ) -> Result<GASPInitialReply, GASPError> {
2109 Ok(GASPInitialReply {
2110 utxo_list: Vec::new(),
2111 })
2112 }
2113 async fn request_node(
2114 &self,
2115 graph_id: &str,
2116 txid: &str,
2117 output_index: u32,
2118 _: bool,
2119 ) -> Result<GASPNode, GASPError> {
2120 Ok(GASPNode {
2121 graph_id: graph_id.to_string(),
2122 raw_tx: format!("rawtx_{txid}"),
2123 output_index,
2124 proof: None,
2125 tx_metadata: None,
2126 output_metadata: None,
2127 inputs: None,
2128 })
2129 }
2130 async fn submit_node(
2131 &self,
2132 _: &GASPNode,
2133 ) -> Result<Option<GASPNodeResponse>, GASPError> {
2134 Ok(None)
2135 }
2136 }
2137
2138 let mut sync = GASPSync::new(
2139 Box::new(gasp_storage),
2140 Box::new(TestRemote),
2141 0,
2142 "[TEST]",
2143 true, );
2145
2146 sync.sync(None).await.unwrap();
2147 assert_eq!(sync.last_interaction, 0);
2150 }
2151
2152 #[tokio::test]
2154 async fn gasp_sync_with_valid_tx_produces_finalized_beefs() {
2155 use crate::gasp::{GASPRemote, GASPSync};
2156 use crate::types::{GASPInitialReply, GASPInitialRequest, GASPInitialResponse};
2157
2158 let store = MemoryStorage::new();
2159 let sink = make_sink();
2160 let gasp_storage = OverlayGASPStorage::new(&store, "tm_test", sink.clone());
2161
2162 let valid_tx_hex = make_coinbase_tx_hex();
2164
2165 struct ValidTxRemote {
2166 tx_hex: String,
2167 }
2168
2169 #[async_trait(?Send)]
2170 impl GASPRemote for ValidTxRemote {
2171 async fn get_initial_response(
2172 &self,
2173 request: &GASPInitialRequest,
2174 ) -> Result<GASPInitialResponse, GASPError> {
2175 Ok(GASPInitialResponse {
2176 utxo_list: vec![GASPOutput {
2177 txid: "valid_tx".to_string(),
2178 output_index: 0,
2179 score: 100.0,
2180 }],
2181 since: request.since,
2182 })
2183 }
2184 async fn get_initial_reply(
2185 &self,
2186 _: &GASPInitialResponse,
2187 ) -> Result<GASPInitialReply, GASPError> {
2188 Ok(GASPInitialReply {
2189 utxo_list: Vec::new(),
2190 })
2191 }
2192 async fn request_node(
2193 &self,
2194 graph_id: &str,
2195 _txid: &str,
2196 output_index: u32,
2197 _: bool,
2198 ) -> Result<GASPNode, GASPError> {
2199 let txid = Transaction::from_hex(&self.tx_hex).unwrap().id();
2204 let proof = MerklePath::new(
2205 100,
2206 vec![vec![bsv_rs::transaction::MerklePathLeaf::new_txid(0, txid)]],
2207 )
2208 .unwrap();
2209 Ok(GASPNode {
2210 graph_id: graph_id.to_string(),
2211 raw_tx: self.tx_hex.clone(),
2212 output_index,
2213 proof: Some(proof.to_hex()),
2214 tx_metadata: None,
2215 output_metadata: None,
2216 inputs: None,
2217 })
2218 }
2219 async fn submit_node(
2220 &self,
2221 _: &GASPNode,
2222 ) -> Result<Option<GASPNodeResponse>, GASPError> {
2223 Ok(None)
2224 }
2225 }
2226
2227 let remote = ValidTxRemote {
2228 tx_hex: valid_tx_hex,
2229 };
2230
2231 let mut sync = GASPSync::new(Box::new(gasp_storage), Box::new(remote), 0, "[TEST]", true);
2232
2233 sync.sync(None).await.unwrap();
2234
2235 let finalized = sink.lock().unwrap();
2236 assert_eq!(finalized.len(), 1, "Should have one finalized graph");
2237 assert!(!finalized[0].beefs.is_empty(), "Should have BEEF bytes");
2238
2239 for beef_bytes in &finalized[0].beefs {
2241 let parsed = Transaction::from_beef(beef_bytes, None);
2242 assert!(parsed.is_ok(), "BEEF should be parseable");
2243 }
2244 }
2245
2246 #[tokio::test]
2247 async fn take_finalized_graphs_drains_list() {
2248 let store = MemoryStorage::new();
2249 let sink = make_sink();
2250 let gasp_storage = OverlayGASPStorage::new(&store, "tm_test", sink.clone());
2251
2252 let raw_tx = make_coinbase_tx_hex();
2253 let node = GASPNode {
2254 graph_id: "abc.0".to_string(),
2255 raw_tx,
2256 output_index: 0,
2257 proof: None,
2258 tx_metadata: None,
2259 output_metadata: None,
2260 inputs: None,
2261 };
2262
2263 gasp_storage.append_to_graph(&node, None).await.unwrap();
2264 gasp_storage.finalize_graph("abc.0").await.unwrap();
2265
2266 let taken = gasp_storage.take_finalized_graphs();
2267 assert_eq!(taken.len(), 1);
2268
2269 let taken2 = gasp_storage.take_finalized_graphs();
2271 assert!(taken2.is_empty());
2272 }
2273
2274 fn legacy_get_beef_for_node(
2280 node_key: &str,
2281 refs: &HashMap<String, PendingNode>,
2282 strict_beef: bool,
2283 ) -> Result<(Transaction, Vec<u8>), GASPError> {
2284 let pending = refs
2285 .get(node_key)
2286 .ok_or_else(|| GASPError::Other(format!("Node {node_key} not found in graph refs")))?;
2287
2288 let mut tx = Transaction::from_hex(&pending.node.raw_tx)
2289 .map_err(|e| GASPError::Other(format!("Failed to parse raw_tx for {node_key}: {e}")))?;
2290
2291 if let Some(ref proof_hex) = pending.node.proof {
2292 tx.merkle_path = Some(beef_limits::proof_from_hex(proof_hex).map_err(|e| {
2293 GASPError::Other(format!("Failed to parse proof for {node_key}: {e}"))
2294 })?);
2295 } else {
2296 let child_info: Vec<(usize, String)> = tx
2297 .inputs
2298 .iter()
2299 .enumerate()
2300 .filter_map(|(idx, input)| {
2301 let source_txid = input.get_source_txid().unwrap_or_default();
2302 if source_txid.is_empty() {
2303 return None;
2304 }
2305 let child_key = format!("{}.{}", source_txid, input.source_output_index);
2306 if refs.contains_key(&child_key) {
2307 Some((idx, child_key))
2308 } else {
2309 None
2310 }
2311 })
2312 .collect();
2313
2314 for (input_idx, child_key) in child_info {
2315 let (child_tx, _) = legacy_get_beef_for_node(&child_key, refs, strict_beef)?;
2316 tx.inputs[input_idx].source_transaction = Some(Box::new(child_tx));
2317 }
2318 }
2319
2320 let allow_partial = !strict_beef;
2321 let beef = tx.to_beef(allow_partial).map_err(|e| {
2322 GASPError::Other(format!("Failed to serialize BEEF for {node_key}: {e}"))
2323 })?;
2324
2325 Ok((tx, beef))
2326 }
2327
2328 fn legacy_compute_ordered_beefs(
2330 graph_id: &str,
2331 refs: &HashMap<String, PendingNode>,
2332 strict_beef: bool,
2333 ) -> Result<Vec<Vec<u8>>, GASPError> {
2334 let mut beefs: Vec<Vec<u8>> = Vec::new();
2335 let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
2336
2337 fn hydrate(
2338 node_key: &str,
2339 refs: &HashMap<String, PendingNode>,
2340 beefs: &mut Vec<Vec<u8>>,
2341 visited: &mut std::collections::HashSet<String>,
2342 strict_beef: bool,
2343 ) -> Result<(), GASPError> {
2344 if visited.contains(node_key) {
2345 return Ok(());
2346 }
2347 visited.insert(node_key.to_string());
2348
2349 let Some(pending) = refs.get(node_key) else {
2350 return Ok(());
2351 };
2352
2353 for child_key in &pending.children {
2354 hydrate(child_key, refs, beefs, visited, strict_beef)?;
2355 }
2356
2357 let (_, beef) = legacy_get_beef_for_node(node_key, refs, strict_beef)?;
2358 beefs.push(beef);
2359 Ok(())
2360 }
2361
2362 hydrate(graph_id, refs, &mut beefs, &mut visited, strict_beef)?;
2363 Ok(beefs)
2364 }
2365
2366 struct Lcg(u64);
2369
2370 impl Lcg {
2371 fn below(&mut self, n: u64) -> u64 {
2372 self.0 = self
2373 .0
2374 .wrapping_mul(6_364_136_223_846_793_005)
2375 .wrapping_add(1_442_695_040_888_963_407);
2376 (self.0 >> 33) % n
2377 }
2378 }
2379
2380 fn pending(raw_tx: String, output_index: u32, proof: Option<String>) -> PendingNode {
2381 PendingNode {
2382 node: GASPNode {
2383 graph_id: "g".to_string(),
2384 raw_tx,
2385 output_index,
2386 proof,
2387 tx_metadata: None,
2388 output_metadata: None,
2389 inputs: None,
2390 },
2391 spent_by: None,
2392 children: Vec::new(),
2393 }
2394 }
2395
2396 fn random_graph(rng: &mut Lcg) -> (HashMap<String, PendingNode>, String) {
2403 use bsv_rs::transaction::{MerklePathLeaf, TransactionInput, TransactionOutput};
2404 let count = 2 + rng.below(8) as usize;
2405 let mut txs: Vec<Transaction> = Vec::new();
2406 for i in 0..count {
2407 let mut tx = Transaction::new();
2408 let mut spent: HashSet<(usize, u32)> = HashSet::new();
2409 if i > 0 {
2410 for _ in 0..rng.below(5) {
2411 let source = rng.below(i as u64) as usize;
2412 let vout = rng.below(3) as u32;
2413 if spent.insert((source, vout)) {
2414 tx.inputs
2415 .push(TransactionInput::new(txs[source].id(), vout));
2416 }
2417 }
2418 }
2419 for _ in 0..3 {
2420 tx.outputs.push(TransactionOutput::new(
2421 1000 + i as u64,
2422 bsv_rs::script::LockingScript::from_hex("51").unwrap(),
2423 ));
2424 }
2425 txs.push(tx);
2426 }
2427 let mut proofs: Vec<Option<String>> = vec![None; count];
2429 let mut i = 0;
2430 while i < count {
2431 match rng.below(5) {
2432 0 => {
2433 let path = vec![vec![MerklePathLeaf::new_txid(0, txs[i].id())]];
2434 proofs[i] = Some(MerklePath::new(100 + i as u32, path).unwrap().to_hex());
2435 }
2436 1 if i + 1 < count => {
2437 let (a, b) = (txs[i].id(), txs[i + 1].id());
2438 let height = 100 + i as u32;
2439 let of_a = vec![vec![
2440 MerklePathLeaf::new_txid(0, a.clone()),
2441 MerklePathLeaf::new(1, b.clone()),
2442 ]];
2443 let of_b = vec![vec![
2444 MerklePathLeaf::new(0, a),
2445 MerklePathLeaf::new_txid(1, b),
2446 ]];
2447 proofs[i] = Some(MerklePath::new(height, of_a).unwrap().to_hex());
2448 proofs[i + 1] = Some(MerklePath::new(height, of_b).unwrap().to_hex());
2449 i += 1;
2450 }
2451 _ => {}
2452 }
2453 i += 1;
2454 }
2455 let root_key = format!("{}.0", txs[count - 1].id());
2456 let mut refs: HashMap<String, PendingNode> = HashMap::new();
2457 refs.insert(
2458 root_key.clone(),
2459 pending(txs[count - 1].to_hex(), 0, proofs[count - 1].clone()),
2460 );
2461 for (i, tx) in txs.iter().enumerate().rev() {
2464 let own_keys: Vec<String> = (0..3)
2465 .map(|vout| format!("{}.{vout}", tx.id()))
2466 .filter(|key| refs.contains_key(key))
2467 .collect();
2468 if own_keys.is_empty() || proofs[i].is_some() && rng.below(3) > 0 {
2469 continue;
2470 }
2471 for input in &tx.inputs {
2472 let source_txid = input.get_source_txid().unwrap();
2473 let source = txs.iter().position(|t| t.id() == source_txid).unwrap();
2474 let key = format!("{source_txid}.{}", input.source_output_index);
2475 if rng.below(6) == 0 {
2476 continue;
2477 }
2478 let mut node = pending(
2479 txs[source].to_hex(),
2480 input.source_output_index,
2481 proofs[source].clone(),
2482 );
2483 match rng.below(40) {
2484 0 => node.node.proof = Some("zz".to_string()),
2485 1 => node.node.raw_tx = "00".to_string(),
2486 _ => {}
2487 }
2488 refs.insert(key.clone(), node);
2489 for own in &own_keys {
2490 let children = &mut refs.get_mut(own).unwrap().children;
2491 if !children.contains(&key) {
2492 children.push(key.clone());
2493 }
2494 }
2495 }
2496 }
2497 (refs, root_key)
2498 }
2499
2500 #[test]
2504 fn e586_the_assembly_gives_the_hydrated_trees_bytes_and_errors() {
2505 let mut rng = Lcg(0x586);
2506 let (mut beefs, mut refusals, mut proven, mut partial) = (0, 0, 0, 0);
2507 for _ in 0..600 {
2508 let (refs, root_key) = random_graph(&mut rng);
2509 for strict in [false, true] {
2510 let mut keys: Vec<&String> = refs.keys().collect();
2511 keys.sort();
2512 for key in keys {
2513 let legacy = legacy_get_beef_for_node(key, &refs, strict)
2514 .map(|(_, beef)| beef)
2515 .map_err(|e| e.to_string());
2516 let mut assembly = GraphAssembly::default();
2519 let ours = assembly
2520 .check(key, &refs, strict)
2521 .map(|()| assembly.beef_of(key))
2522 .map_err(|e| e.to_string());
2523 assert_eq!(ours, legacy, "node {key}, strict {strict}");
2524 match &ours {
2525 Ok(beef) => {
2526 beefs += 1;
2527 let parsed = Beef::from_binary(beef).unwrap();
2528 proven += usize::from(!parsed.bumps.is_empty());
2529 partial += usize::from(
2530 !strict && legacy_get_beef_for_node(key, &refs, true).is_err(),
2531 );
2532 }
2533 Err(_) => refusals += 1,
2534 }
2535 }
2536 let legacy = legacy_compute_ordered_beefs(&root_key, &refs, strict)
2537 .map_err(|e| e.to_string());
2538 let ours =
2539 OverlayGASPStorage::compute_ordered_beefs(&root_key, &refs, strict, None)
2540 .map_err(|e| e.to_string());
2541 assert_eq!(ours, legacy, "graph {root_key}, strict {strict}");
2542 }
2543 }
2544 println!(
2545 "#586: {beefs} BEEFs equal ({proven} with a proof, {partial} partial), {refusals} refusals equal"
2546 );
2547 assert!(beefs > 2000 && proven > 300 && partial > 100 && refusals > 300);
2549 }
2550
2551 #[test]
2554 fn e586_the_refusals_are_the_recursions_words() {
2555 let parent = "ab".repeat(32);
2556 let raw = make_valid_tx_hex(&parent, 3);
2557 let key = format!("{}.0", Transaction::from_hex(&raw).unwrap().id());
2558 let refs = HashMap::from([(key.clone(), pending(raw, 0, None))]);
2559 let mut assembly = GraphAssembly::default();
2560 assert_eq!(
2561 assembly
2562 .check("nope.0", &refs, false)
2563 .unwrap_err()
2564 .to_string(),
2565 legacy_get_beef_for_node("nope.0", &refs, false)
2566 .unwrap_err()
2567 .to_string()
2568 );
2569 let strict = assembly.check(&key, &refs, true).unwrap_err().to_string();
2570 assert_eq!(
2571 strict,
2572 legacy_get_beef_for_node(&key, &refs, true)
2573 .unwrap_err()
2574 .to_string()
2575 );
2576 assert!(strict.contains(&format!(
2577 "Missing source transaction for input 0 (txid: {parent})"
2578 )));
2579 assembly.check(&key, &refs, false).unwrap();
2581 assert_eq!(
2582 assembly.beef_of(&key),
2583 legacy_get_beef_for_node(&key, &refs, false).unwrap().1
2584 );
2585 }
2586
2587 #[test]
2594 fn e586_a_deep_chain_and_a_cycle_do_not_use_the_stack() {
2595 use bsv_rs::transaction::{TransactionInput, TransactionOutput};
2596 let mut refs: HashMap<String, PendingNode> = HashMap::new();
2597 let mut previous: Option<String> = None;
2598 let mut tip = String::new();
2599 for i in 0..20_000u64 {
2600 let mut tx = Transaction::new();
2601 if let Some(txid) = &previous {
2602 tx.inputs.push(TransactionInput::new(txid.clone(), 0));
2603 }
2604 tx.outputs.push(TransactionOutput::new(
2605 i,
2606 bsv_rs::script::LockingScript::from_hex("51").unwrap(),
2607 ));
2608 let txid = tx.id();
2609 tip = format!("{txid}.0");
2610 let mut node = pending(tx.to_hex(), 0, None);
2611 if let Some(below) = &previous {
2612 node.children.push(format!("{below}.0"));
2613 }
2614 refs.insert(tip.clone(), node);
2615 previous = Some(txid);
2616 }
2617 assert_eq!(OverlayGASPStorage::ordered_keys(&tip, &refs).len(), 20_000);
2618 let mut assembly = GraphAssembly::default();
2619 assembly.check(&tip, &refs, true).unwrap();
2620 let beef = Beef::from_binary(&assembly.beef_of(&tip)).unwrap();
2621 assert_eq!(beef.txs.len(), 20_000);
2622
2623 let decoy = "cd".repeat(32);
2625 let y_raw = make_valid_tx_hex(&decoy, 0);
2626 let y = Transaction::from_hex(&y_raw).unwrap().id();
2627 let root_raw = make_valid_tx_hex(&y, 0);
2628 let root_key = format!("{decoy}.0");
2629 let mut root = pending(root_raw, 0, None);
2630 root.children.push(format!("{y}.0"));
2631 let refs = HashMap::from([
2632 (root_key.clone(), root),
2633 (format!("{y}.0"), pending(y_raw, 0, None)),
2634 ]);
2635 let mut assembly = GraphAssembly::default();
2636 assembly.check(&root_key, &refs, true).unwrap();
2637 let beef = Beef::from_binary(&assembly.beef_of(&root_key)).unwrap();
2638 assert_eq!(beef.txs.len(), 2);
2639 let beefs =
2640 OverlayGASPStorage::compute_ordered_beefs(&root_key, &refs, true, None).unwrap();
2641 assert_eq!(beefs.len(), 2);
2642 }
2643}