Skip to main content

bsv_overlay_engine/
gasp_overlay.rs

1//! OverlayGASPStorage — bridges the Engine's storage to the GASP protocol.
2//!
3//! Wraps a reference to the Engine's `Storage` trait plus a topic name, providing
4//! the `GASPStorage` interface that `GASPSync` needs to synchronize UTXOs with
5//! remote peers.
6//!
7//! ## finalize_graph
8//!
9//! When a graph is finalized, the nodes are converted into ordered BEEF byte
10//! arrays (ancestors first, root last) and stored in a shared
11//! `FinalizedGraphSink`. The caller (Engine) drains the sink and submits each
12//! BEEF to `Engine::submit()` with `HistoricalTxNoSpv` mode: after
13//! `GASPSync::sync()` returns, or, under a per-peer sync budget, after every
14//! completed UTXO (`gasp::FinalizedGraphHook`, bsv-low #552).
15//!
16//! ## find_needed_inputs
17//!
18//! Parses the node's raw transaction hex to determine what inputs are needed:
19//! - A proven node ends the walk unless its topic manager names inputs needed
20//!   for overlay history: when its output is not yet admissible, those not
21//!   held; when it is admissible, those neither held nor landed (lane E1D).
22//! - If no proof, all transaction inputs are requested (minus any already
23//!   known in local storage).
24//!
25//! ## validate_graph_anchor (bsv-low #551)
26//!
27//! Before a graph is finalized it is checked the way the reference checks it
28//! (`OverlayGASPStorage.ts` `validateGraphAnchor`): the ROOT node's BEEF is
29//! verified by the engine's own SPV check, then the ordered BEEFs are replayed
30//! through the topic manager over a set of coins, and the graph is discarded
31//! WHOLE unless its root is a coin at the end. That is what makes the
32//! `HistoricalTxNoSpv` submits of `finalize_graph` rest on a true premise.
33//!
34//! Ported from `~/bsv/overlay-services/src/GASP/OverlayGASPStorage.ts` (388 lines).
35
36use 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
54/// The most store reads [`OverlayGASPStorage::find_needed_inputs`] spends
55/// on asking whether an admitted node's named inputs have LANDED (E1D's
56/// re-ask; the E1D lens fold, L3): two per transaction at most (its applied
57/// row, then its held outputs). A transaction past it is requested from the
58/// peer. The figure is the door's own per-topic bound (`PREDECESSOR_READS`).
59const LANDED_READS_PER_NODE: usize = 16;
60
61/// A GASP node stored during in-progress graph construction.
62#[derive(Debug, Clone)]
63struct PendingNode {
64    node: GASPNode,
65    #[allow(dead_code)]
66    spent_by: Option<String>,
67    /// Children of this node (nodes whose transactions are inputs to this one).
68    children: Vec<String>, // keys into the pending_graphs map
69}
70
71/// A finalized graph ready for Engine submission.
72///
73/// Contains ordered BEEF byte arrays (ancestors first, root last) and
74/// the topic they belong to.
75#[derive(Debug, Clone)]
76pub struct FinalizedGraph {
77    /// Topic this graph belongs to.
78    pub topic: String,
79    /// Ordered BEEF byte arrays — ancestors first, root transaction last.
80    pub beefs: Vec<Vec<u8>>,
81}
82
83/// Shared container for finalized graph data.
84///
85/// The Engine creates this before sync and passes it to `OverlayGASPStorage`.
86/// After sync completes, the Engine drains the list and submits each BEEF.
87pub type FinalizedGraphSink = Rc<Mutex<Vec<FinalizedGraph>>>;
88
89/// Create a new empty finalized graph sink.
90pub fn new_finalized_graph_sink() -> FinalizedGraphSink {
91    Rc::new(Mutex::new(Vec::new()))
92}
93
94/// `GASPStorage` implementation that delegates to the Engine's `Storage` trait.
95///
96/// Tracks one topic at a time — create a new instance per (topic, peer) sync.
97/// During sync, incoming graph nodes are accumulated in memory. On `finalize_graph`,
98/// the accumulated nodes are converted to BEEF format and pushed to the shared
99/// `FinalizedGraphSink` for later Engine submission.
100pub struct OverlayGASPStorage<'a> {
101    /// The Engine's storage backend.
102    storage: &'a dyn Storage,
103    /// Topic being synchronized.
104    topic: String,
105    /// Optional topic manager for overlay-specific history behind proven nodes.
106    topic_manager: Option<&'a dyn TopicManager>,
107    /// Temporary graphs being constructed during sync.
108    /// Key: node identifier (graph_id for root, "txid.outputIndex" for children).
109    /// Value: the pending node with its relationship data.
110    pending_graphs: Mutex<HashMap<String, PendingNode>>,
111    /// Shared sink for completed graphs ready for Engine submission.
112    finalized_sink: FinalizedGraphSink,
113    /// If true, finalize uses strict `to_beef(false)` so a missing ancestor
114    /// fails loud rather than silently emitting a partial BEEF. Defaults to
115    /// `false` (tolerant `to_beef(true)`) — byte-identical to today. Only set
116    /// `true` when ancestor hydration is enabled, so post-hydration
117    /// completeness is enforced.
118    strict_beef: bool,
119    /// The engine's chain tracker, for the anchor's Bitcoin check. `None` is
120    /// the reference's `'scripts only'`: a merkle path is accepted unchecked,
121    /// every unproven input's script still runs.
122    chain_tracker: Option<&'a dyn ChainTracker>,
123    /// [`crate::engine::Engine::set_script_verification`], handed over so the
124    /// anchor check obeys the same switch as `Engine::submit`. Default `true`.
125    verify_scripts: bool,
126    /// [`crate::engine::Engine::set_walk_budget`], handed over so the anchor
127    /// check's script walk runs under the same two limbs as `Engine::submit`
128    /// (bsv-low #592). Default [`crate::engine::DoorBudget::DEFAULT`].
129    walk_budget: crate::engine::DoorBudget,
130    /// The peer this instance syncs from: the key of its deferred graphs
131    /// (bsv-low #555, [`Self::with_peer`]).
132    peer: String,
133    /// Whether that peer is CONFIGURED (`SyncTarget::Peers`), written into
134    /// each record saved (the delta-2 fold's D2-M2, [`Self::with_configured_peer`]).
135    configured_peer: bool,
136    /// The graph the anchor check last PASSED, parsed and checked, kept for
137    /// its `finalize_graph` so a graph is parsed once (bsv-low #586). Dropped
138    /// by anything that changes or discards a pending graph.
139    anchored: Mutex<Option<AnchoredGraph>>,
140}
141
142/// A graph the anchor check passed: what `finalize_graph` assembles from.
143struct AnchoredGraph {
144    graph_id: String,
145    strict_beef: bool,
146    order: Vec<String>,
147    assembly: GraphAssembly,
148}
149
150/// Lends the engine's tracker to the anchor check and notes whether it
151/// FAULTED: a tracker that could not answer is a fault of the moment, not a
152/// verdict on the graph (see `validate_graph_anchor`). Noted here, on the
153/// call, so no error text is matched.
154struct 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
178/// What the anchor check needs of a pending graph, assembled under the lock.
179struct AnchorInput {
180    /// The root node's transaction id.
181    root_txid: String,
182    /// The root node's BEEF (tolerant: a source the graph does not carry is
183    /// absent, and merged from local storage or refused by the verifier).
184    root_beef: Vec<u8>,
185    /// Inputs of the root's UNPROVEN ancestry that the graph does not carry.
186    absent_sources: Vec<(String, u32)>,
187    /// The keys of the graph's nodes in finalize order: ancestors first,
188    /// root last. Each one's BEEF is assembled when the replay reaches it
189    /// and dropped after it (bsv-low #586): the check never holds the
190    /// graph's BEEFs together.
191    order: Vec<String>,
192    /// The graph's transactions and proofs, parsed once and CHECKED: every
193    /// BEEF of `order` can be assembled from it.
194    assembly: GraphAssembly,
195}
196
197/// One transaction of a pending graph, parsed once.
198struct ParsedTx {
199    tx: Transaction,
200    txid: String,
201}
202
203/// One node of a pending graph as its BEEFs need it.
204struct AssemblyNode {
205    /// Shared by every node of one transaction (one node per output index
206    /// an unproven spender asked for).
207    parsed: Rc<ParsedTx>,
208    /// The node's own proof. A proven node is a leaf of every BEEF.
209    proof: Option<MerklePath>,
210    /// Per input of an UNPROVEN node: the key of the node that holds its
211    /// source, when the graph has one. Empty for a proven node.
212    sources: Vec<Option<String>>,
213}
214
215/// A pending graph's BEEFs, assembled from its raw transactions and proofs
216/// parsed ONCE each (bsv-low #586, zanaadu-v2 #377).
217///
218/// Before it, `get_beef_for_node` hydrated a `source_transaction` tree by
219/// recursion: every reference to a shared ancestor rebuilt that ancestor's
220/// whole subtree (parse, raw bytes and hex of each transaction, cloned per
221/// parent), and serialized it at every level, so an unmined graph with
222/// fan-in cost its PATHS, not its transactions: Zanaadu measured 36x the
223/// fetched bytes on 13 nodes, each further unproven layer multiplying by its
224/// fan-in. The reference hydrates the same way (`OverlayGASPStorage.ts`
225/// `getBEEFForNode`, f999e0c1a: `hydrator` parses a node again for every
226/// input that reaches it; JavaScript shares nothing between the copies
227/// either), so it has the same multiplier.
228///
229/// Here a transaction is parsed once per graph (keyed by its bytes), a node
230/// names its sources by KEY, and a node's BEEF is the walk
231/// `Transaction::to_beef` made over the hydrated tree (`bsv-rs`
232/// `collect_ancestors`: depth first, inputs from the last to the first, a
233/// proven node a leaf, a txid collected once, a transaction after its
234/// sources; proofs deduplicated by height and root), made over the keys
235/// instead, then the SDK's own `Beef` (`merge_bump`, `merge_transaction`,
236/// `to_binary`, whose sort is the SDK's). The bytes are the ones the tree
237/// gave, for every graph: the frozen digests of `tests/gasp_topic_manager.rs`
238/// and of the witness hold.
239///
240/// Both walks keep their work on the heap (the recursion they replace used
241/// the stack, once per unproven link), and neither follows a link back into
242/// a node it is still inside.
243#[derive(Default)]
244struct GraphAssembly {
245    /// By the SHA-256 of a transaction's raw bytes.
246    txs: HashMap<[u8; 32], Rc<ParsedTx>>,
247    /// The transaction of each node key asked for.
248    parsed: HashMap<String, Rc<ParsedTx>>,
249    /// By node key.
250    nodes: HashMap<String, Rc<AssemblyNode>>,
251    /// (node key, strict) whose BEEF is known to assemble.
252    checked: HashSet<(String, bool)>,
253}
254
255impl GraphAssembly {
256    /// The transaction of the node `key`, parsed once per graph.
257    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    /// The node `key`: its transaction, its proof, and where the graph
284    /// holds each input's source.
285    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        // A proven node is a leaf. An unproven one takes each input's source
305        // from the node filed under that outpoint, when the graph has it.
306        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    /// Check that the BEEF of `key` can be assembled: every node it reaches
333    /// parses (its transaction, its proof), and under `strict` no unproven
334    /// node lacks a source (`to_beef(false)`'s rule; tolerant, a missing
335    /// source is left out silently, `to_beef(true)`). The errors are the ones
336    /// the hydrating recursion gave, in its order: a node on entry, its
337    /// sources in input order, its own missing source on the way out. A node
338    /// is checked once per graph.
339    fn check(
340        &mut self,
341        key: &str,
342        refs: &HashMap<String, PendingNode>,
343        strict: bool,
344    ) -> Result<(), GASPError> {
345        // (node key, the node, the next input to follow)
346        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                    // The SDK's own refusal, word for word (`to_beef(false)`).
372                    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    /// The txid of a checked node.
387    fn txid_of(&self, key: &str) -> Option<&str> {
388        self.nodes.get(key).map(|node| node.parsed.txid.as_str())
389    }
390
391    /// The BEEF of a CHECKED node: its transaction and its unproven
392    /// ancestry down to the first proven node of every branch, each
393    /// transaction once. Nothing is hydrated: the only copies made are the
394    /// ones the `Beef` being written holds, dropped with it.
395    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                    // Already collected (or being collected): skip it.
412                } else if let Some(merkle_path) = &node.proof {
413                    // Proven: its proof and itself, and no further. Proofs
414                    // are deduplicated by block height and computed root.
415                    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        // The writer grows by doubling: a graph's BEEFs are held together
458        // by the finalize, so each gives its slack back.
459        let mut bytes = beef.to_binary();
460        bytes.shrink_to_fit();
461        bytes
462    }
463}
464
465impl<'a> OverlayGASPStorage<'a> {
466    /// Create a new `OverlayGASPStorage` for the given topic.
467    ///
468    /// The `sink` parameter is a shared container where finalized graphs are
469    /// pushed. The Engine holds a clone and drains it after sync.
470    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    /// Keep a graph the anchor check passed for its finalize.
492    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    /// Take the graph kept by the anchor check (leaving none).
505    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    /// Name the peer this instance syncs from, the key under which its
513    /// deferred graphs are kept (bsv-low #555). `Engine::start_gasp_sync`
514    /// wires it.
515    #[must_use]
516    pub fn with_peer(mut self, peer: impl Into<String>) -> Self {
517        self.peer = peer.into();
518        self
519    }
520
521    /// Say whether the peer is a CONFIGURED one (`SyncTarget::Peers`) or
522    /// one `ls_ship` discovered (bsv-low #555, the delta-2 fold's D2-M2):
523    /// each record saved carries it (`DeferredGraph::configured`), so a
524    /// storage can reserve part of its ceiling for configured peers.
525    /// Default `false`. `Engine::start_gasp_sync` wires it.
526    #[must_use]
527    pub fn with_configured_peer(mut self, configured: bool) -> Self {
528        self.configured_peer = configured;
529        self
530    }
531
532    /// Check merkle roots of the anchor against this chain tracker (the
533    /// engine's: `Engine::start_gasp_sync` wires it). Without one the anchor
534    /// check is the reference's `'scripts only'` walk.
535    #[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    /// Carry [`crate::engine::Engine::set_script_verification`] into the
542    /// anchor check. `false` is that switch's escape hatch, not a mode: the
543    /// pre-2026-09-08 structural check (roots only, and nothing without a
544    /// tracker). Default `true`.
545    #[must_use]
546    pub fn with_script_verification(mut self, enabled: bool) -> Self {
547        self.verify_scripts = enabled;
548        self
549    }
550
551    /// Carry [`crate::engine::Engine::set_walk_budget`] into the anchor
552    /// check (bsv-low #592).
553    #[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    /// Consult the topic manager when a proven node needs overlay history.
560    /// Without a manager, proven nodes retain the legacy stopping rule.
561    #[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    /// Enable strict-BEEF finalize (`to_beef(false)`), failing loud on a
568    /// missing ancestor instead of silently emitting a partial BEEF.
569    ///
570    /// Defaults to OFF (tolerant). Only enable this together with ancestor
571    /// hydration so that, after the chain fallback has spliced in ancestors,
572    /// an incomplete graph is discarded + retried rather than stored partial.
573    #[must_use]
574    pub fn with_strict_beef(mut self, strict: bool) -> Self {
575        self.strict_beef = strict;
576        self
577    }
578
579    /// Take all finalized graphs from the shared sink, leaving it empty.
580    ///
581    /// Convenience method for callers who hold a reference to this storage
582    /// rather than the raw sink.
583    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    /// Get the number of pending graphs (for testing).
592    #[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    /// Get the number of finalized graphs (for testing).
601    #[cfg(test)]
602    pub fn finalized_graph_count(&self) -> usize {
603        self.finalized_sink.lock().unwrap().len()
604    }
605
606    /// The keys of a graph's nodes in the order their BEEFs are finalized
607    /// (ancestors first, root last): depth first from the root over each
608    /// node's `children`, a node after its children, each key once. An
609    /// explicit stack (bsv-low #586; it was a recursion, in this order).
610    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        // (node key, its children, the next child to visit)
614        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    /// Compute ordered BEEFs for a graph (ancestors first, root last), each
638    /// node's BEEF assembled from the graph's transactions parsed ONCE
639    /// ([`GraphAssembly`]).
640    ///
641    /// `anchored`: the graph as the anchor check left it, already parsed and
642    /// checked; without one (a finalize with no anchor check before it) it is
643    /// parsed and checked here.
644    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    /// Everything `validate_graph_anchor` needs of the pending graph, read in
667    /// one pass under the lock (nothing awaits while the guard is held).
668    ///
669    /// It also enforces the one structural rule the rest relies on: every
670    /// node hangs off its parent by an input the parent REALLY spends. The
671    /// walk asks the peer for an outpoint and files the answer under the
672    /// answer's own txid, so a peer that answers with some other transaction
673    /// would otherwise ride into the ordered BEEFs unlinked to anything the
674    /// Bitcoin check covers. With the rule, every node of the graph is tied
675    /// to the verified root by a chain of txids. (A divergence by addition
676    /// from the reference at f999e0c1a, which has no such rule; upstream added
677    /// it later in `appendToGraph`: "The GASP child node is not an input of
678    /// its declared parent".)
679    ///
680    /// A graph that cannot be ASSEMBLED (a raw transaction or a proof that
681    /// does not parse, or under `strict_beef` a missing ancestor) is
682    /// `GASPError::AnchorUnavailable`, not a refusal: before #551 that error
683    /// surfaced from `finalize_graph` and failed the UTXO, so the cursor gap
684    /// guard asked for it again (#43), and it still does.
685    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        // (node key, whether a path of UNPROVEN nodes leads from the root to it)
703        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        // The root's CHECKED copy is assembled tolerant whatever `strict_beef`
744        // says: it is never stored, sources the storage holds are merged into
745        // it, and the verifier refuses any source still missing. The ordered
746        // BEEFs are assembled exactly as `finalize_graph` will assemble them.
747        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    /// Returns UTXOs known for this topic since the given score/timestamp.
787    ///
788    /// Delegates to `Storage::find_utxos_for_topic()`, converting `Output`
789    /// records to `GASPOutput` with txid, output_index, and score.
790    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, // don't need BEEF for listing
802            )
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    /// Hydrate a GASP node with transaction data from storage.
817    ///
818    /// Looks up the root output (from graph_id) and searches its BEEF tree
819    /// for the requested txid. This is a simplified version that returns
820    /// the raw transaction data and proof if available.
821    ///
822    /// For a full implementation, this would delegate to
823    /// `Engine::provide_foreign_gasp_node()`, but since we only hold a
824    /// `Storage` reference (not the full Engine), we do a simpler lookup.
825    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        // Try to find the output with BEEF data
833        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                // Parse the BEEF to extract raw transaction hex
842                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        // Fallback: look up without topic filter
870        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    /// Determine which inputs are needed to validate this node.
902    ///
903    /// - A proven node ends the walk if its topic manager names no inputs it
904    ///   still needs: for a node whose output is NOT admissible, every named
905    ///   input not held (D13); for one whose output IS admissible, every named
906    ///   input neither held nor landed (lane E1D, an addition: the reference
907    ///   stops at an admissible node). Without a manager, proven nodes always
908    ///   stop.
909    /// - If no proof, parses the raw transaction and requests all inputs,
910    ///   filtering out any inputs already known in local storage.
911    /// - Admission errors propagate to GASP's incoming UTXO handler. Errors
912    ///   identifying needed inputs are logged and cut off this node, like TS.
913    /// - Unproven raw_tx parse failures retain the legacy graceful fallback.
914    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        // A proven node whose own output the dry run admits (lane E1D).
920        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            // TS passes { dryRun: true }: the node is the peer's and nothing
934            // is admitted here, so a manager that writes on admission must not.
935            // Utils.toArray(string) defaults to UTF-8, even for hex-like text.
936            // This call is outside the needed-input catch in the reference.
937            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            // The reference stops here (D13). Lane E1D (bsv-low #575,
948            // zanaadu-v2 #365) asks the named inputs of an ADMITTED node too:
949            // a manager that admits a successor with no coin (the pf head
950            // manager over its own head state) ended the walk at the
951            // successor while its predecessor had not landed, the graph was
952            // the successor alone, and its finalize submit recorded it with
953            // no coin; the predecessor, landed on a later tick, stood unspent
954            // beside the tip for good. So the predecessor is asked FIRST: a
955            // named input that is neither held nor landed (below) is
956            // requested and lands before the node in the graph's order, a
957            // finalize submit of it that does not land stops the graph, and
958            // a named input the peer cannot serve prunes (D14). When every
959            // named input is known the walk stops here, as the reference's.
960            admitted = admittance.outputs_to_admit.contains(&node.output_index);
961            // A node with no input to name stops here, the manager not
962            // asked (the reference's call count for such a node).
963            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            // A serialization RED is unreachable: bsv-rs stops collecting ancestors at a merkle path.
973            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            // Unproven nodes always need all inputs, independently of the manager.
1000            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        // Strip inputs that are already known in local storage.
1025        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        // An admitted node's named inputs are requested only while their
1048        // landing is unknown (E1D): one whose transaction has an applied row
1049        // in the topic, or an output held there, has landed, and the walk
1050        // stops as the reference's. A read that faults keeps the input: one
1051        // round trip to the peer is the cheaper error. So does a transaction
1052        // past [`LANDED_READS_PER_NODE`] (the E1D lens fold, L3: a manager
1053        // names as many inputs as it likes, D14's decoys included, and each
1054        // costs up to two reads): it is requested, and a landed one the peer
1055        // serves is a dupe at its finalize submit.
1056        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    /// Append a node to a temporary graph being constructed during sync.
1108    ///
1109    /// If `spent_by` is `None`, this is the root node and is keyed by `graph_id`.
1110    /// Otherwise, the node is keyed by its computed txid.outputIndex, and is
1111    /// registered as a child of the parent node identified by `spent_by`.
1112    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        // Compute the key for this node.
1126        let node_key = if spent_by.is_none() {
1127            // Root node — keyed by graph_id
1128            node.graph_id.clone()
1129        } else {
1130            // Child node — keyed by txid.outputIndex
1131            match Transaction::from_hex(&node.raw_tx) {
1132                Ok(tx) => format!("{}.{}", tx.id(), node.output_index),
1133                Err(_) => {
1134                    // Fallback: use raw_tx prefix as key
1135                    format!(
1136                        "{}.{}",
1137                        &node.raw_tx[..node.raw_tx.len().min(64)],
1138                        node.output_index
1139                    )
1140                }
1141            }
1142        };
1143
1144        // The pending graphs change: nothing kept of an anchor check holds.
1145        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        // Insert this node
1152        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 spent_by is set, register this node as a child of the parent.
1162        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    /// Check a completed graph before anything of it is finalized (bsv-low
1172    /// #551). The reference's `validateGraphAnchor`
1173    /// (`OverlayGASPStorage.ts` at f999e0c1a, lines 261 to 297), in its two
1174    /// steps. An `Err` is a discard: `GASPSync::complete_graph` drops the
1175    /// graph and NOTHING of it reaches the sink.
1176    ///
1177    /// **1. Bitcoin.** The ROOT node's BEEF (`GraphAssembly::beef_of`) is verified
1178    /// by the engine's own check, `verify_spv_like_the_reference`: the
1179    /// reference's `spvTx.verify(this.engine.chainTracker)`. With a tracker
1180    /// every merkle root in that BEEF is checked against it and every unproven
1181    /// input's script is executed; with none it is `'scripts only'` (roots
1182    /// accepted unchecked, scripts still run). One verifier, the same switch
1183    /// (`with_script_verification`) as `Engine::submit`.
1184    ///
1185    /// **2. Overlay, all or nothing.** The ordered BEEFs (ancestors first, root
1186    /// last) are replayed through the topic manager as the reference's
1187    /// `admitHistoricalBEEF` does: mode `historical-tx`, no off-chain values,
1188    /// `previous_coins` the inputs whose source outpoint is a coin (the
1189    /// engine's little-endian u32 input indices), and every output the manager
1190    /// admits joins the coins. If the ROOT's outpoint is not a coin at the
1191    /// end, the graph is refused. The set only GROWS, as in the reference at
1192    /// f999e0c1a: it reads `outputsToAdmit` alone, never `coinsToRetain`, and
1193    /// removes nothing ("a Set of all historical coins to retain (no need to
1194    /// remove them)"). The reference passes `{ dryRun: true }` and so does
1195    /// this replay (`TopicAdmittanceContext::DRY_RUN`): a manager that writes
1196    /// on admission writes nothing here.
1197    ///
1198    /// WITHOUT a topic manager step 2 does not run: there is nothing to
1199    /// replay, and `Engine::submit` refuses such a topic (`UnsupportedTopic`),
1200    /// so nothing can be admitted. `Engine::start_gasp_sync` always wires the
1201    /// manager of a topic it can admit.
1202    ///
1203    /// **Divergences by addition** (each with its why):
1204    ///
1205    /// - *Coins the storage already holds count.* `find_needed_inputs` strips
1206    ///   an input the local storage already holds (as the reference does), so
1207    ///   its node is not in the graph and the replay's own set never sees it.
1208    ///   The reference then refuses a root that extends a coin it already
1209    ///   holds: the next head of a head chain, synced one tick after the
1210    ///   last, would be discarded forever. A previous coin is therefore an
1211    ///   input in the set OR in the storage for this topic, the exact lookup
1212    ///   `Engine::submit` makes when finalize submits the same BEEF.
1213    /// - *Sources the storage already holds are merged for step 1.* For the
1214    ///   same reason an UNPROVEN node may lack an input's source transaction
1215    ///   (the reference throws in `getBEEFForNode` and discards). The stored
1216    ///   BEEF of that output is merged into the checked copy, so the spend is
1217    ///   executed against its real source and that source's own ancestry is
1218    ///   verified too. If the stored BEEF is itself incomplete the verifier
1219    ///   refuses, and the graph is discarded as in the reference. Only the
1220    ///   CHECKED copy is merged; the finalized bytes are untouched.
1221    /// - *Every node is an input of its parent* (`read_anchor_input`).
1222    /// - *A conflicting spend refuses the graph.* Two transactions of one
1223    ///   graph that spend the same outpoint cannot both be admitted by the
1224    ///   finalize submits (the first one consumes the coin), so a replay that
1225    ///   showed the coin to both would promise an admission finalize cannot
1226    ///   keep. Upstream added the same rule after f999e0c1a (`spentOutpoints`,
1227    ///   "Historical GASP graph contains a conflicting spend").
1228    /// - *A fault of the moment is not a verdict.* A chain tracker that could
1229    ///   not answer, or a storage read that failed, says nothing about the
1230    ///   graph: the error is `GASPError::AnchorUnavailable`, the graph is
1231    ///   still discarded, but `complete_graph` FAILS the UTXO so the cursor
1232    ///   gap guard asks for it again. The reference discards and moves on,
1233    ///   which here would lose the UTXO for good behind an advanced cursor.
1234    /// - *A manager error in the replay is "not now", not a refusal.* The
1235    ///   replay is where a manager is shown the coins, so it is where one
1236    ///   whose own state lags can say it cannot place a transaction YET. An
1237    ///   `Err` from it is `GASPError::AnchorUnavailable` too: the UTXO fails
1238    ///   and is asked for again. `Ok` with nothing admitted stays the final
1239    ///   refusal, and there the cursor moves, as in the reference (which
1240    ///   moves it past a refused and a failed graph alike, `GASP.ts` 389 to
1241    ///   392, 419 to 423, 559 to 572, `Engine.ts` 1600 to 1604). The cost: a
1242    ///   manager that errors FOREVER on one UTXO holds that peer's cursor
1243    ///   below it, and what lies above and is not yet held is walked again
1244    ///   on every tick.
1245    ///
1246    /// **Cost.** Step 1 runs the script of every input of every unproven
1247    /// transaction in the root's BEEF and asks the tracker once per merkle
1248    /// path in it (a proven root: one question, no script). The ancestors a
1249    /// manager named BEHIND a proven node are not in that BEEF: their own
1250    /// merkle paths are not checked, here or in the reference; they are bound
1251    /// to the verified root by txid instead. Step 2 is one manager call per
1252    /// transaction and one storage read per input that is not already a coin
1253    /// of the set, which is what the finalize submits cost again.
1254    ///
1255    /// Since bsv-low #592 the walk runs under the engine's walk budget
1256    /// (`DoorBudget`, the door's two limbs, estimated from the bytes before
1257    /// anything runs). A breach is no verdict on the graph, and nothing
1258    /// stands behind this check (no network accepted these bytes, and the
1259    /// finalize submits them `historical-tx-no-spv`), so a breach is never
1260    /// the replay's to judge (the E592 lens fold, H1: as built at `4060ffd`
1261    /// it went on, and a peer's root that spent a held coin with no
1262    /// signature was finalized). It is HELD: `GASPError::AnchorWalkHeld`,
1263    /// the graph not finalized, the UTXO failed (or, under a per-graph
1264    /// budget, its record kept with nothing pending), the cursor below it,
1265    /// counted (`GASPSync::anchor_walk_held`). The roots the walk reached are
1266    /// checked first, so a bad proof is still the refusal it is. The hold
1267    /// ends when the peer serves the root PROVEN: #555's resume re-asks an
1268    /// unproven root, a proven one restarts the walk (`root_proven`), and a
1269    /// proven root is trusted without a script run. So an honest 0-conf graph
1270    /// too big to walk lands after its block; a forgery never lands. What a
1271    /// forging peer costs: its own slot (the UTXO held below the cursor, its
1272    /// record re-asked each pass up to `DEFERRED_GRAPH_MAX_PASSES`), inside
1273    /// its per-peer budget, and, as it never yields a finalized graph or a
1274    /// cursor move on that UTXO, D16's yieldless bound quarantines it if it
1275    /// serves nothing else, and ONLY with a per-graph budget
1276    /// (`Engine::set_graph_budget`): the yieldless bound exists only there
1277    /// (the E592 delta lens, D1-L2). With NO per-graph budget (a library
1278    /// consumer that sets none) there is no record and no yieldless count:
1279    /// the held UTXO fails on every sync, the sync runs to its end and is a
1280    /// success under #302, the cursor stays below that UTXO for good, and
1281    /// the graph is fetched and its walk estimated again on every tick;
1282    /// nothing is admitted, and the cost is ours, the residual D15 states for
1283    /// `AnchorUnavailable`. The reference serialises this method behind
1284    /// `acquireAnchorValidationSlot` (at most 4 at once); here graphs are
1285    /// completed one at a time by one sync on one thread, so there is
1286    /// nothing to serialise.
1287    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        // ── 1. Bitcoin ──────────────────────────────────────────────────
1300        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        // The checked copy has done its work (bsv-low #586: not held through
1333        // the replay).
1334        drop(root_beef);
1335        // A walk that could not run within the budget (bsv-low #592) is not
1336        // a verdict on the graph, and nothing else judges it: HELD until the
1337        // root is served proven (the E592 lens fold, H1; the doc above).
1338        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        // ── 2. Overlay ──────────────────────────────────────────────────
1370        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            // One node's BEEF at a time, the bytes `finalize_graph` will
1379            // assemble, dropped when its turn is over (bsv-low #586).
1380            let beef = &anchor.assembly.beef_of(key);
1381            // The parse `Engine::submit` makes of the same bytes at finalize.
1382            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            // Two outputs of one transaction are two nodes with one BEEF.
1393            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            // TS passes { dryRun: true }: the replay only asks, the finalize
1426            // submit is the admission.
1427            // An `Err` is the manager's "not now" (a fault of the moment, see
1428            // the divergences above); `Ok` with nothing is its refusal.
1429            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    /// Finalize a graph — convert accumulated nodes to ordered BEEFs and push
1454    /// them to the shared sink for later Engine submission.
1455    ///
1456    /// Computes ordered BEEF byte arrays (ancestors first, root last) from the
1457    /// temporary graph nodes and stores them in the `FinalizedGraphSink`. The
1458    /// Engine drains it (after the sync, or per completed UTXO under a
1459    /// per-peer budget) and submits each one with `HistoricalTxNoSpv` mode, which skips SPV because
1460    /// `validate_graph_anchor` has already done it (the reference's
1461    /// `finalizeGraph`, lines 317 to 333).
1462    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        // Compute ordered BEEFs for the graph.
1480        let beefs =
1481            Self::compute_ordered_beefs(graph_id, &refs, self.strict_beef, self.take_anchored())?;
1482
1483        // Push to shared sink for later Engine submission.
1484        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        // Remove all nodes belonging to this graph from pending refs.
1494        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    /// Discard a temporary graph that failed validation.
1511    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    // Keyed by topic, not peer: the expiry is read from the outpoint's own
1582    // bytes, so any peer that serves it serves the same dead advert.
1583    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// ============================================================================
1599// Tests
1600// ============================================================================
1601
1602#[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    /// Build a minimal valid transaction hex with one input and one output.
1631    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    /// Build a coinbase-like tx (no meaningful inputs).
1646    /// A raw transaction with no input, read by `Transaction::from_hex`
1647    /// alone (no BEEF door, which refuses it since bsv-rs 0.4.1).
1648    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        // A coinbase's null outpoint: a transaction with no input is refused
1663        // since bsv-rs 0.4.1 (NL-8 W6).
1664        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    // ── find_known_utxos tests ────────────────────────────────────────
1679
1680    #[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    // ── find_needed_inputs tests ──────────────────────────────────────
1780
1781    #[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(), // not valid tx hex
1789            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        // A node with a proof (merkle path) should not need inputs.
1806        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        // Insert the source output so it's already known
1859        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    // ── append / finalize / discard tests ─────────────────────────────
1908
1909    #[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        // Check that finalized data was produced
1933        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        // Each BEEF should be parseable
1963        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        // The reference throws `Graph node with ID ... not found`.
2036        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        // Verify it can be used as dyn GASPStorage
2058        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    /// Integration test: OverlayGASPStorage works with GASPSync.
2064    #[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        // Set up local storage with one UTXO
2070        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        // Mock remote with two UTXOs — provides unparsable raw_tx so
2080        // find_needed_inputs returns None (graceful fallback).
2081        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, // unidirectional
2144        );
2145
2146        sync.sync(None).await.unwrap();
2147        // The synthetic graph cannot assemble a BEEF. Nothing completed,
2148        // so the cursor remains at entry and both outputs are retried.
2149        assert_eq!(sync.last_interaction, 0);
2150    }
2151
2152    /// Integration test: full sync with valid transactions produces submittable BEEFs.
2153    #[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        // Build a valid coinbase-style tx for the remote to provide.
2163        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                // Served PROVEN: since bsv-low #551 the anchor check refuses
2200                // an unproven transaction that creates satoshis (this one has
2201                // no inputs). With no chain tracker a merkle path is accepted
2202                // unchecked, the reference's 'scripts only'.
2203                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        // Verify the BEEF is parseable
2240        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        // Should be empty after draining
2270        let taken2 = gasp_storage.take_finalized_graphs();
2271        assert!(taken2.is_empty());
2272    }
2273
2274    // ── bsv-low #586: the assembly gives the bytes the hydrated tree gave ──
2275
2276    /// `get_beef_for_node` as it was on `fbb7fa8`, verbatim: the hydrating
2277    /// recursion [`GraphAssembly`] replaced. Kept as the reference the
2278    /// assembly is compared with, byte for byte and error for error.
2279    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    /// `compute_ordered_beefs` as it was on `fbb7fa8`, verbatim.
2329    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    /// A small deterministic generator (no dependency, the same graphs on
2367    /// every run).
2368    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    /// A random pending graph: up to 9 transactions, each spending up to
2397    /// four outputs of earlier ones (two outputs of one transaction too, the
2398    /// diamond), some proven (two of a pair sharing one block, so their
2399    /// proofs COMBINE), some referenced outpoints absent from the graph (a
2400    /// partial BEEF, a strict refusal), and now and then a proof or a raw
2401    /// transaction that does not parse. Returns the graph and its root key.
2402    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        // Proofs: a transaction alone in its block, or a pair in one block.
2428        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        // Every referenced outpoint is a node, most of the time; a node is
2462        // a child of the node(s) of its spender.
2463        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    // The assembly against the recursion it replaced, over 600 random
2501    // graphs, tolerant and strict: the BEEF of every node, the ordered BEEFs
2502    // of the graph, and every refusal, are the same bytes and the same words.
2503    #[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                    // A fresh assembly per node, and one shared by the graph
2517                    // below (`compute_ordered_beefs`): both orders of use.
2518                    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        // The generator reached every class.
2548        assert!(beefs > 2000 && proven > 300 && partial > 100 && refusals > 300);
2549    }
2550
2551    // A node unknown to the graph, and the SDK's own words for a missing
2552    // source under `strict_beef`.
2553    #[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        // Tolerant, the same node assembles: a partial BEEF of one transaction.
2580        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    // The walks keep their work on the heap. A chain of 20,000 unproven
2588    // links is assembled; the recursion it replaced used the stack once per
2589    // link. And a link back into a node the walk is still inside (the root
2590    // is filed under the graph id the PEER names, so a peer can file a root
2591    // under an outpoint its own ancestry spends) ends there: the recursion
2592    // never returned.
2593    #[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        // root (filed under `decoy.0`) spends y.0; y spends `decoy.0`.
2624        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}