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