Skip to main content

bsv/transaction/
beef.rs

1//! BEEF format (BRC-62/95/96) serialization and deserialization.
2//!
3//! Supports V1, V2, and Atomic BEEF variants for SPV proof packaging.
4
5use std::io::{Cursor, Read, Write};
6
7use crate::primitives::utils::{from_hex, to_hex};
8use crate::transaction::beef_tx::BeefTx;
9use crate::transaction::error::TransactionError;
10use crate::transaction::merkle_path::MerklePath;
11use crate::transaction::{read_u32_le, read_varint, write_u32_le, write_varint};
12
13/// BEEF V1 version marker (0x0100BEEF in LE = 4022206465).
14pub const BEEF_V1: u32 = 4022206465;
15/// BEEF V2 version marker (0x0200BEEF in LE = 4022206466).
16pub const BEEF_V2: u32 = 4022206466;
17/// Atomic BEEF prefix (0x01010101).
18pub const ATOMIC_BEEF: u32 = 0x01010101;
19
20/// A BEEF (Background Evaluation Extended Format) container.
21///
22/// Contains a set of BUMPs (Merkle paths) and transactions that together
23/// form a validity proof chain for SPV verification.
24#[derive(Debug, Clone)]
25pub struct Beef {
26    /// BEEF version (BEEF_V1 or BEEF_V2).
27    pub version: u32,
28    /// Merkle paths (BUMPs) proving transaction inclusion in blocks.
29    pub bumps: Vec<MerklePath>,
30    /// Transactions with BEEF metadata.
31    pub txs: Vec<BeefTx>,
32    /// For Atomic BEEF: the txid of the proven transaction.
33    pub atomic_txid: Option<String>,
34}
35
36impl Beef {
37    /// Create a new empty Beef with the given version.
38    pub fn new(version: u32) -> Self {
39        Beef {
40            version,
41            bumps: Vec::new(),
42            txs: Vec::new(),
43            atomic_txid: None,
44        }
45    }
46
47    /// Deserialize a Beef from binary format.
48    pub fn from_binary(reader: &mut impl Read) -> Result<Self, TransactionError> {
49        let mut version = read_u32_le(reader)?;
50        let mut atomic_txid = None;
51
52        if version == ATOMIC_BEEF {
53            // Read 32-byte txid (reversed/LE on wire -> BE display hex)
54            let mut txid_bytes = [0u8; 32];
55            reader.read_exact(&mut txid_bytes)?;
56            txid_bytes.reverse();
57            atomic_txid = Some(to_hex(&txid_bytes));
58            // Read inner BEEF version
59            version = read_u32_le(reader)?;
60        }
61
62        if version != BEEF_V1 && version != BEEF_V2 {
63            return Err(TransactionError::BeefError(format!(
64                "Serialized BEEF must start with {BEEF_V1} or {BEEF_V2} but starts with {version}"
65            )));
66        }
67
68        let mut beef = Beef::new(version);
69
70        // Read bumps
71        let bump_count = read_varint(reader)
72            .map_err(|e| TransactionError::InvalidFormat(e.to_string()))?
73            as usize;
74        for _ in 0..bump_count {
75            let bump = MerklePath::from_binary(reader)?;
76            beef.bumps.push(bump);
77        }
78
79        // Read transactions
80        let tx_count = read_varint(reader)
81            .map_err(|e| TransactionError::InvalidFormat(e.to_string()))?
82            as usize;
83        for _ in 0..tx_count {
84            let beef_tx = if version == BEEF_V2 {
85                BeefTx::from_binary_v2(reader)?
86            } else {
87                BeefTx::from_binary_v1(reader)?
88            };
89            beef.txs.push(beef_tx);
90        }
91
92        beef.atomic_txid = atomic_txid;
93
94        // Link source transactions: for each input of each tx, if the input's
95        // source_txid matches another tx in the BEEF, set source_transaction.
96        beef.link_source_transactions();
97
98        Ok(beef)
99    }
100
101    /// Serialize this Beef to binary format.
102    pub fn to_binary(&self, writer: &mut impl Write) -> Result<(), TransactionError> {
103        // Write Atomic BEEF prefix if applicable
104        if let Some(ref txid) = self.atomic_txid {
105            write_u32_le(writer, ATOMIC_BEEF)?;
106            let mut txid_bytes =
107                from_hex(txid).map_err(|e| TransactionError::InvalidFormat(e.to_string()))?;
108            txid_bytes.reverse(); // BE display -> LE wire
109            writer.write_all(&txid_bytes)?;
110        }
111
112        write_u32_le(writer, self.version)?;
113
114        // Write bumps
115        write_varint(writer, self.bumps.len() as u64)?;
116        for bump in &self.bumps {
117            bump.to_binary(writer)?;
118        }
119
120        // Write transactions
121        write_varint(writer, self.txs.len() as u64)?;
122        for tx in &self.txs {
123            if self.version == BEEF_V2 {
124                tx.to_binary_v2(writer)?;
125            } else {
126                tx.to_binary_v1(writer)?;
127            }
128        }
129
130        Ok(())
131    }
132
133    /// Deserialize a Beef from a hex string.
134    pub fn from_hex(hex: &str) -> Result<Self, TransactionError> {
135        let bytes = from_hex(hex).map_err(|e| TransactionError::InvalidFormat(e.to_string()))?;
136        let mut cursor = Cursor::new(bytes);
137        Self::from_binary(&mut cursor)
138    }
139
140    /// Serialize this Beef to a hex string.
141    pub fn to_hex(&self) -> Result<String, TransactionError> {
142        let mut buf = Vec::new();
143        self.to_binary(&mut buf)?;
144        Ok(to_hex(&buf))
145    }
146
147    /// Extract the subject transaction from this BEEF, consuming it.
148    ///
149    /// If `atomic_txid` is set, returns the transaction matching that txid.
150    /// Otherwise, returns the last transaction (the subject).
151    /// Before returning, links source transactions from the BEEF for each input.
152    pub fn into_transaction(
153        self,
154    ) -> Result<crate::transaction::transaction::Transaction, TransactionError> {
155        let subject_idx = if let Some(ref atomic_txid) = self.atomic_txid {
156            self.txs
157                .iter()
158                .position(|btx| btx.txid == *atomic_txid)
159                .ok_or_else(|| {
160                    TransactionError::BeefError(format!(
161                        "atomic txid {atomic_txid} not found in BEEF"
162                    ))
163                })?
164        } else {
165            if self.txs.is_empty() {
166                return Err(TransactionError::BeefError(
167                    "BEEF contains no transactions".into(),
168                ));
169            }
170            self.txs.len() - 1
171        };
172
173        let mut tx = self.txs[subject_idx]
174            .tx
175            .clone()
176            .ok_or_else(|| TransactionError::BeefError("subject tx is txid-only".into()))?;
177
178        // Set merkle_path on subject tx from its bump_index.
179        if let Some(bi) = self.txs[subject_idx].bump_index {
180            if bi >= self.bumps.len() {
181                return Err(TransactionError::BeefError(format!(
182                    "bump_index {} out of bounds (only {} bumps)",
183                    bi,
184                    self.bumps.len()
185                )));
186            }
187            if tx.merkle_path.is_none() {
188                tx.merkle_path = Some(self.bumps[bi].clone());
189            }
190        }
191
192        // Link source transactions: for each input, find source tx in BEEF
193        // and set merkle_path from bump_index.
194        for input in &mut tx.inputs {
195            if let Some(ref source_txid) = input.source_txid {
196                if input.source_transaction.is_none() {
197                    for btx in &self.txs {
198                        if btx.txid == *source_txid {
199                            if let Some(ref source_tx) = btx.tx {
200                                let mut linked = source_tx.clone();
201                                if let Some(bi) = btx.bump_index {
202                                    if bi >= self.bumps.len() {
203                                        return Err(TransactionError::BeefError(format!(
204                                            "bump_index {} out of bounds (only {} bumps) for source tx {}",
205                                            bi, self.bumps.len(), btx.txid
206                                        )));
207                                    }
208                                    if linked.merkle_path.is_none() {
209                                        linked.merkle_path = Some(self.bumps[bi].clone());
210                                    }
211                                }
212                                input.source_transaction = Some(Box::new(linked));
213                            }
214                            break;
215                        }
216                    }
217                }
218            }
219        }
220
221        Ok(tx)
222    }
223
224    /// Topologically sort transactions by dependency order.
225    ///
226    /// Uses Kahn's algorithm. Proven transactions (with bump_index) and those
227    /// with no in-BEEF dependencies come first; dependent transactions follow.
228    pub fn sort_txs(&mut self) {
229        use std::collections::{HashMap, VecDeque};
230
231        let n = self.txs.len();
232        if n <= 1 {
233            return;
234        }
235
236        // Build txid -> index map
237        let txid_to_idx: HashMap<&str, usize> = self
238            .txs
239            .iter()
240            .enumerate()
241            .map(|(i, btx)| (btx.txid.as_str(), i))
242            .collect();
243
244        // Compute in-degree for each tx (how many of its input txids are in this BEEF)
245        let mut in_degree = vec![0usize; n];
246        // adjacency: txid_idx -> list of dependent tx indices
247        let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); n];
248
249        for (i, btx) in self.txs.iter().enumerate() {
250            for input_txid in &btx.input_txids {
251                if let Some(&dep_idx) = txid_to_idx.get(input_txid.as_str()) {
252                    if dep_idx != i {
253                        in_degree[i] += 1;
254                        dependents[dep_idx].push(i);
255                    }
256                }
257            }
258        }
259
260        // Start with nodes having in-degree 0
261        let mut queue: VecDeque<usize> = VecDeque::new();
262        for (i, &deg) in in_degree.iter().enumerate() {
263            if deg == 0 {
264                queue.push_back(i);
265            }
266        }
267
268        let mut sorted_indices: Vec<usize> = Vec::with_capacity(n);
269        while let Some(idx) = queue.pop_front() {
270            sorted_indices.push(idx);
271            for &dep in &dependents[idx] {
272                in_degree[dep] -= 1;
273                if in_degree[dep] == 0 {
274                    queue.push_back(dep);
275                }
276            }
277        }
278
279        // If there are remaining nodes (cycle), append them
280        if sorted_indices.len() < n {
281            for i in 0..n {
282                if !sorted_indices.contains(&i) {
283                    sorted_indices.push(i);
284                }
285            }
286        }
287
288        // Reorder self.txs according to sorted_indices
289        let old_txs = std::mem::take(&mut self.txs);
290        self.txs = sorted_indices
291            .into_iter()
292            .map(|i| old_txs[i].clone())
293            .collect();
294    }
295
296    /// Find a `BeefTx` by txid.
297    pub fn find_txid(&self, txid: &str) -> Option<&BeefTx> {
298        self.txs.iter().find(|btx| btx.txid == txid)
299    }
300
301    /// Merge a MerklePath (BUMP) that is assumed to be fully valid.
302    ///
303    /// If an identical bump (same block height, same computed root) already exists,
304    /// combines them. Otherwise appends a new bump.
305    ///
306    /// After merging, scans transactions to assign bump indices to any that match
307    /// a leaf in the merged bump.
308    ///
309    /// Returns the index of the merged bump.
310    pub fn merge_bump(&mut self, bump: &MerklePath) -> Result<usize, TransactionError> {
311        let mut bump_index: Option<usize> = None;
312
313        for (i, existing) in self.bumps.iter_mut().enumerate() {
314            if existing.block_height == bump.block_height {
315                let root_a = existing.compute_root(None)?;
316                let root_b = bump.compute_root(None)?;
317                if root_a == root_b {
318                    existing.combine(bump)?;
319                    bump_index = Some(i);
320                    break;
321                }
322            }
323        }
324
325        if bump_index.is_none() {
326            bump_index = Some(self.bumps.len());
327            self.bumps.push(bump.clone());
328        }
329
330        let bi = bump_index.expect("bump_index was just set");
331
332        // Check if any existing transactions are proven by this bump
333        let bump_ref = &self.bumps[bi];
334        let leaf_txids: Vec<String> = bump_ref.path[0]
335            .iter()
336            .filter_map(|leaf| leaf.hash.clone())
337            .collect();
338
339        for btx in &mut self.txs {
340            if btx.bump_index.is_none() && leaf_txids.contains(&btx.txid) {
341                btx.bump_index = Some(bi);
342            }
343        }
344
345        Ok(bi)
346    }
347
348    /// Remove an existing transaction with the given txid.
349    pub fn remove_existing_txid(&mut self, txid: &str) {
350        if let Some(pos) = self.txs.iter().position(|btx| btx.txid == txid) {
351            self.txs.remove(pos);
352        }
353    }
354
355    /// Merge a raw serialized transaction into this BEEF.
356    ///
357    /// Replaces any existing transaction with the same txid.
358    ///
359    /// If `bump_index` is provided, it must be a valid index into `self.bumps`.
360    pub fn merge_raw_tx(
361        &mut self,
362        raw_tx: &[u8],
363        bump_index: Option<usize>,
364    ) -> Result<BeefTx, TransactionError> {
365        let mut cursor = std::io::Cursor::new(raw_tx);
366        let tx = crate::transaction::transaction::Transaction::from_binary(&mut cursor)?;
367        let new_tx = BeefTx::from_tx(tx, bump_index)?;
368        self.remove_existing_txid(&new_tx.txid);
369        let txid = new_tx.txid.clone();
370        self.txs.push(new_tx);
371
372        // Try to find a bump for this transaction if none provided
373        if bump_index.is_none() {
374            self.try_to_validate_bump_index(&txid);
375        }
376
377        Ok(self.txs.last().cloned().expect("just pushed"))
378    }
379
380    /// Merge another Beef into this one.
381    ///
382    /// All BUMPs from `other` are merged first (deduplicating by block height + root),
383    /// then all transactions are merged (replacing any with matching txids).
384    pub fn merge_beef(&mut self, other: &Beef) -> Result<(), TransactionError> {
385        for bump in &other.bumps {
386            self.merge_bump(bump)?;
387        }
388
389        for btx in &other.txs {
390            if btx.is_txid_only() {
391                // Merge txid-only if we don't already have this txid
392                if self.find_txid(&btx.txid).is_none() {
393                    self.txs.push(BeefTx::from_txid(btx.txid.clone()));
394                }
395            } else if let Some(ref tx) = btx.tx {
396                // Re-derive the bump index in the context of our bumps
397                let new_bump_index = self.find_bump_index_for_txid(&btx.txid);
398                let new_btx = BeefTx::from_tx(tx.clone(), new_bump_index)?;
399                self.remove_existing_txid(&btx.txid);
400                let txid = new_btx.txid.clone();
401                self.txs.push(new_btx);
402                if new_bump_index.is_none() {
403                    self.try_to_validate_bump_index(&txid);
404                }
405            }
406        }
407
408        Ok(())
409    }
410
411    /// Merge a Beef from binary data into this one.
412    pub fn merge_beef_from_binary(&mut self, data: &[u8]) -> Result<(), TransactionError> {
413        let mut cursor = std::io::Cursor::new(data);
414        let other = Beef::from_binary(&mut cursor)?;
415        self.merge_beef(&other)
416    }
417
418    /// Serialize this Beef as Atomic BEEF (BRC-95) for a specific transaction.
419    ///
420    /// The target `txid` must exist in this Beef. After sorting by dependency order,
421    /// if the target transaction is not the last one, transactions after it are excluded.
422    ///
423    /// The output format is: `ATOMIC_BEEF(4 bytes) + txid(32 bytes LE) + BEEF binary`.
424    pub fn to_binary_atomic(&self, txid: &str) -> Result<Vec<u8>, TransactionError> {
425        // Verify the txid exists
426        if self.find_txid(txid).is_none() {
427            return Err(TransactionError::BeefError(format!(
428                "{txid} does not exist in this Beef"
429            )));
430        }
431
432        // Clone and set up atomic txid
433        let mut atomic_beef = self.clone();
434        atomic_beef.atomic_txid = Some(txid.to_string());
435
436        // If the target tx is not the last one, remove transactions after it
437        if let Some(pos) = atomic_beef.txs.iter().position(|btx| btx.txid == txid) {
438            atomic_beef.txs.truncate(pos + 1);
439        }
440
441        let mut buf = Vec::new();
442        atomic_beef.to_binary(&mut buf)?;
443        Ok(buf)
444    }
445
446    /// Try to find a bump index for a txid by scanning all bumps.
447    fn try_to_validate_bump_index(&mut self, txid: &str) {
448        for (i, bump) in self.bumps.iter().enumerate() {
449            let found = bump.path[0]
450                .iter()
451                .any(|leaf| leaf.hash.as_deref() == Some(txid));
452            if found {
453                if let Some(btx) = self.txs.iter_mut().find(|btx| btx.txid == txid) {
454                    btx.bump_index = Some(i);
455                }
456                return;
457            }
458        }
459    }
460
461    /// Find the bump index for a txid, if any bump contains it.
462    fn find_bump_index_for_txid(&self, txid: &str) -> Option<usize> {
463        for (i, bump) in self.bumps.iter().enumerate() {
464            let found = bump.path[0]
465                .iter()
466                .any(|leaf| leaf.hash.as_deref() == Some(txid));
467            if found {
468                return Some(i);
469            }
470        }
471        None
472    }
473
474    /// Link source transactions within this BEEF.
475    ///
476    /// For each transaction input, if its source_txid matches another transaction
477    /// in this BEEF, set source_transaction to point to it.
478    fn link_source_transactions(&mut self) {
479        // Collect txid -> index mapping
480        let txid_map: Vec<(String, usize)> = self
481            .txs
482            .iter()
483            .enumerate()
484            .map(|(i, btx)| (btx.txid.clone(), i))
485            .collect();
486
487        // We need to clone transactions to set source_transaction references
488        // because Rust ownership rules prevent borrowing self.txs mutably
489        // while also reading from it. We clone the source txs.
490        let tx_clones: Vec<Option<crate::transaction::transaction::Transaction>> =
491            self.txs.iter().map(|btx| btx.tx.clone()).collect();
492
493        for btx in &mut self.txs {
494            if let Some(ref mut tx) = btx.tx {
495                for input in &mut tx.inputs {
496                    if let Some(ref source_txid) = input.source_txid {
497                        if input.source_transaction.is_none() {
498                            // Find matching tx in BEEF
499                            if let Some((_, idx)) =
500                                txid_map.iter().find(|(tid, _)| tid == source_txid)
501                            {
502                                if let Some(ref source_tx) = tx_clones[*idx] {
503                                    input.source_transaction = Some(Box::new(source_tx.clone()));
504                                }
505                            }
506                        }
507                    }
508                }
509            }
510        }
511    }
512}
513
514#[cfg(test)]
515mod tests {
516    use super::*;
517    use serde::Deserialize;
518
519    #[derive(Deserialize)]
520    struct BeefVector {
521        name: String,
522        hex: String,
523        version: u32,
524        bump_count: usize,
525        tx_count: usize,
526        #[serde(default)]
527        txid: Option<String>,
528    }
529
530    fn load_test_vectors() -> Vec<BeefVector> {
531        let json = include_str!("../../test-vectors/beef_valid.json");
532        serde_json::from_str(json).expect("failed to parse beef_valid.json")
533    }
534
535    #[test]
536    fn test_beef_v1_round_trip() {
537        let vectors = load_test_vectors();
538        for v in vectors.iter().filter(|v| v.version == 1) {
539            let beef = Beef::from_hex(&v.hex)
540                .unwrap_or_else(|e| panic!("failed to parse '{}': {}", v.name, e));
541            assert_eq!(
542                beef.bumps.len(),
543                v.bump_count,
544                "bump count mismatch for '{}'",
545                v.name
546            );
547            assert_eq!(
548                beef.txs.len(),
549                v.tx_count,
550                "tx count mismatch for '{}'",
551                v.name
552            );
553
554            let result_hex = beef
555                .to_hex()
556                .unwrap_or_else(|e| panic!("failed to serialize '{}': {}", v.name, e));
557            assert_eq!(result_hex, v.hex, "round-trip failed for '{}'", v.name);
558        }
559    }
560
561    #[test]
562    fn test_beef_tx_count() {
563        let vectors = load_test_vectors();
564        for v in &vectors {
565            let beef = Beef::from_hex(&v.hex)
566                .unwrap_or_else(|e| panic!("failed to parse '{}': {}", v.name, e));
567            assert_eq!(
568                beef.bumps.len(),
569                v.bump_count,
570                "bump count mismatch for '{}'",
571                v.name
572            );
573            assert_eq!(
574                beef.txs.len(),
575                v.tx_count,
576                "tx count mismatch for '{}'",
577                v.name
578            );
579
580            // Verify txid if provided
581            if let Some(ref expected_txid) = v.txid {
582                let last_tx = &beef.txs[beef.txs.len() - 1];
583                assert_eq!(
584                    &last_tx.txid, expected_txid,
585                    "txid mismatch for '{}'",
586                    v.name
587                );
588            }
589        }
590    }
591
592    #[test]
593    fn test_merge_beef_combines_bumps_and_txs() {
594        let vectors = load_test_vectors();
595        // Parse two separate BEEFs and merge them
596        let beef_a = Beef::from_hex(&vectors[0].hex).expect("parse beef_a");
597        let beef_b = Beef::from_hex(&vectors[1].hex).expect("parse beef_b");
598
599        let mut merged = Beef::new(BEEF_V2);
600        merged.merge_beef(&beef_a).expect("merge beef_a");
601        merged.merge_beef(&beef_b).expect("merge beef_b");
602
603        // Merged should contain txs from both
604        assert!(
605            merged.txs.len() >= beef_a.txs.len(),
606            "merged should have at least as many txs as beef_a"
607        );
608        assert!(
609            !merged.bumps.is_empty(),
610            "merged should have at least one bump"
611        );
612
613        // All txids from both should be present
614        for btx in &beef_a.txs {
615            assert!(
616                merged.find_txid(&btx.txid).is_some(),
617                "merged should contain txid {} from beef_a",
618                btx.txid
619            );
620        }
621        for btx in &beef_b.txs {
622            assert!(
623                merged.find_txid(&btx.txid).is_some(),
624                "merged should contain txid {} from beef_b",
625                btx.txid
626            );
627        }
628    }
629
630    #[test]
631    fn test_merge_beef_deduplicates_same_txid() {
632        let vectors = load_test_vectors();
633        let beef_a = Beef::from_hex(&vectors[0].hex).expect("parse beef");
634
635        let mut merged = Beef::new(BEEF_V2);
636        merged.merge_beef(&beef_a).expect("merge first");
637        let count_after_first = merged.txs.len();
638
639        // Merge the same beef again
640        merged.merge_beef(&beef_a).expect("merge second");
641        assert_eq!(
642            merged.txs.len(),
643            count_after_first,
644            "merging same beef twice should not duplicate txs"
645        );
646    }
647
648    #[test]
649    fn test_merge_beef_from_binary() {
650        let vectors = load_test_vectors();
651        let beef_a = Beef::from_hex(&vectors[0].hex).expect("parse beef");
652        let binary = crate::primitives::utils::from_hex(&vectors[0].hex).expect("hex decode");
653
654        let mut merged = Beef::new(BEEF_V2);
655        merged
656            .merge_beef_from_binary(&binary)
657            .expect("merge from binary");
658
659        assert_eq!(merged.txs.len(), beef_a.txs.len());
660        assert_eq!(merged.bumps.len(), beef_a.bumps.len());
661    }
662
663    #[test]
664    fn test_merge_raw_tx() {
665        let vectors = load_test_vectors();
666        let beef = Beef::from_hex(&vectors[0].hex).expect("parse beef");
667
668        // Extract the raw tx bytes from the first transaction
669        if let Some(ref tx) = beef.txs[0].tx {
670            let mut raw_tx_buf = Vec::new();
671            tx.to_binary(&mut raw_tx_buf).expect("serialize tx");
672
673            let mut new_beef = Beef::new(BEEF_V2);
674            let result = new_beef
675                .merge_raw_tx(&raw_tx_buf, None)
676                .expect("merge raw tx");
677            assert_eq!(result.txid, beef.txs[0].txid);
678            assert_eq!(new_beef.txs.len(), 1);
679        }
680    }
681
682    #[test]
683    fn test_merge_raw_tx_replaces_existing() {
684        let vectors = load_test_vectors();
685        let beef = Beef::from_hex(&vectors[0].hex).expect("parse beef");
686
687        if let Some(ref tx) = beef.txs[0].tx {
688            let mut raw_tx_buf = Vec::new();
689            tx.to_binary(&mut raw_tx_buf).expect("serialize tx");
690
691            let mut new_beef = Beef::new(BEEF_V2);
692            new_beef
693                .merge_raw_tx(&raw_tx_buf, None)
694                .expect("merge first");
695            new_beef
696                .merge_raw_tx(&raw_tx_buf, None)
697                .expect("merge second");
698
699            assert_eq!(
700                new_beef.txs.len(),
701                1,
702                "merging same raw tx twice should replace, not duplicate"
703            );
704        }
705    }
706
707    #[test]
708    fn test_to_binary_atomic() {
709        let vectors = load_test_vectors();
710        let beef = Beef::from_hex(&vectors[0].hex).expect("parse beef");
711
712        if let Some(ref expected_txid) = vectors[0].txid {
713            let atomic = beef
714                .to_binary_atomic(expected_txid)
715                .expect("to_binary_atomic");
716
717            // Should start with ATOMIC_BEEF prefix
718            assert!(atomic.len() > 36, "atomic output too short");
719            let prefix = u32::from_le_bytes([atomic[0], atomic[1], atomic[2], atomic[3]]);
720            assert_eq!(prefix, ATOMIC_BEEF, "should start with ATOMIC_BEEF prefix");
721
722            // Should contain the txid (reversed) at bytes 4..36
723            let mut txid_bytes =
724                crate::primitives::utils::from_hex(expected_txid).expect("hex decode txid");
725            txid_bytes.reverse(); // to LE wire format
726            assert_eq!(
727                &atomic[4..36],
728                &txid_bytes[..],
729                "atomic should contain txid in LE"
730            );
731
732            // Round-trip: parse the atomic BEEF back
733            let mut cursor = Cursor::new(&atomic);
734            let parsed = Beef::from_binary(&mut cursor).expect("parse atomic beef");
735            assert_eq!(
736                parsed.atomic_txid.as_deref(),
737                Some(expected_txid.as_str()),
738                "parsed atomic txid should match"
739            );
740            assert_eq!(
741                parsed.txs.len(),
742                beef.txs.len(),
743                "parsed atomic should have same tx count"
744            );
745        }
746    }
747
748    #[test]
749    fn test_to_binary_atomic_nonexistent_txid() {
750        let vectors = load_test_vectors();
751        let beef = Beef::from_hex(&vectors[0].hex).expect("parse beef");
752
753        let result = beef
754            .to_binary_atomic("0000000000000000000000000000000000000000000000000000000000000000");
755        assert!(result.is_err(), "should error for nonexistent txid");
756    }
757
758    #[test]
759    fn test_find_txid() {
760        let vectors = load_test_vectors();
761        let beef = Beef::from_hex(&vectors[0].hex).expect("parse beef");
762
763        if let Some(ref expected_txid) = vectors[0].txid {
764            assert!(
765                beef.find_txid(expected_txid).is_some(),
766                "should find existing txid"
767            );
768        }
769
770        assert!(
771            beef.find_txid("0000000000000000000000000000000000000000000000000000000000000000")
772                .is_none(),
773            "should not find nonexistent txid"
774        );
775    }
776
777    #[test]
778    fn test_into_transaction_returns_last_tx() {
779        let vectors = load_test_vectors();
780        let beef = Beef::from_hex(&vectors[0].hex).expect("parse beef");
781        let expected_txid = beef.txs.last().unwrap().txid.clone();
782        let tx = beef.into_transaction().expect("into_transaction");
783        assert_eq!(
784            tx.id().unwrap(),
785            expected_txid,
786            "should return last (subject) tx"
787        );
788    }
789
790    #[test]
791    fn test_from_beef_hex() {
792        let vectors = load_test_vectors();
793        let beef = Beef::from_hex(&vectors[0].hex).expect("parse beef");
794        let expected_txid = beef.txs.last().unwrap().txid.clone();
795        let tx = crate::transaction::transaction::Transaction::from_beef(&vectors[0].hex)
796            .expect("from_beef");
797        assert_eq!(
798            tx.id().unwrap(),
799            expected_txid,
800            "from_beef should return subject tx"
801        );
802    }
803
804    #[test]
805    fn test_sort_txs_proven_before_unproven() {
806        let vectors = load_test_vectors();
807        let mut beef = Beef::from_hex(&vectors[0].hex).expect("parse beef");
808        beef.sort_txs();
809        // After sorting, proven txs (with bump_index) should come before unproven
810        let mut seen_unproven = false;
811        for btx in &beef.txs {
812            if btx.bump_index.is_some() {
813                assert!(!seen_unproven, "proven tx should not come after unproven");
814            } else {
815                seen_unproven = true;
816            }
817        }
818    }
819
820    #[test]
821    fn test_sort_txs_idempotent() {
822        let vectors = load_test_vectors();
823        let mut beef = Beef::from_hex(&vectors[0].hex).expect("parse beef");
824        beef.sort_txs();
825        let first_order: Vec<String> = beef.txs.iter().map(|t| t.txid.clone()).collect();
826        beef.sort_txs();
827        let second_order: Vec<String> = beef.txs.iter().map(|t| t.txid.clone()).collect();
828        assert_eq!(first_order, second_order, "sort_txs should be idempotent");
829    }
830
831    #[test]
832    fn test_merge_bump() {
833        let vectors = load_test_vectors();
834        let beef = Beef::from_hex(&vectors[0].hex).expect("parse beef");
835
836        let mut new_beef = Beef::new(BEEF_V2);
837        // Merge the first bump
838        let idx = new_beef.merge_bump(&beef.bumps[0]).expect("merge bump");
839        assert_eq!(idx, 0, "first bump should be at index 0");
840        assert_eq!(new_beef.bumps.len(), 1);
841
842        // Merging same bump again should combine, not add
843        let idx2 = new_beef
844            .merge_bump(&beef.bumps[0])
845            .expect("merge bump again");
846        assert_eq!(idx2, 0, "same bump should merge to index 0");
847        assert_eq!(
848            new_beef.bumps.len(),
849            1,
850            "should still be 1 bump after re-merge"
851        );
852    }
853
854    #[test]
855    fn test_into_transaction_sets_merkle_path_from_bumps() {
856        // Vector 1 has 2 txs: a proven source tx and an unproven subject tx.
857        // into_transaction should set merkle_path on the linked source tx.
858        let vectors = load_test_vectors();
859        let beef = Beef::from_hex(&vectors[1].hex).expect("parse vector 1");
860        assert_eq!(beef.txs.len(), 2, "vector 1 should have 2 txs");
861
862        // Find which tx has a bump (the proven one)
863        let proven_count = beef.txs.iter().filter(|t| t.bump_index.is_some()).count();
864        assert!(proven_count >= 1, "at least one tx should have a bump");
865
866        let tx = beef.into_transaction().expect("into_transaction");
867
868        // The subject tx (last in BEEF) is the unproven one — check if it
869        // has a merkle_path set when appropriate.
870        // Check source transactions have merkle_path set from bumps.
871        for input in &tx.inputs {
872            if let Some(ref source_txid) = input.source_txid {
873                if let Some(ref source_tx) = input.source_transaction {
874                    // Source tx was in the BEEF with a bump — merkle_path should be set
875                    assert!(
876                        source_tx.merkle_path.is_some(),
877                        "source tx {source_txid} should have merkle_path set from BEEF bump"
878                    );
879                }
880            }
881        }
882    }
883
884    #[test]
885    fn test_into_transaction_sets_merkle_path_on_subject() {
886        // Vector 0 has 1 tx with a bump. into_transaction should set
887        // merkle_path on the subject tx itself.
888        let vectors = load_test_vectors();
889        let beef = Beef::from_hex(&vectors[0].hex).expect("parse vector 0");
890        assert!(
891            beef.txs[0].bump_index.is_some(),
892            "vector 0 tx should have a bump"
893        );
894
895        let tx = beef.into_transaction().expect("into_transaction");
896        assert!(
897            tx.merkle_path.is_some(),
898            "subject tx with bump should have merkle_path set"
899        );
900    }
901}