1use 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
13pub const BEEF_V1: u32 = 4022206465;
15pub const BEEF_V2: u32 = 4022206466;
17pub const ATOMIC_BEEF: u32 = 0x01010101;
19
20#[derive(Debug, Clone)]
25pub struct Beef {
26 pub version: u32,
28 pub bumps: Vec<MerklePath>,
30 pub txs: Vec<BeefTx>,
32 pub atomic_txid: Option<String>,
34}
35
36impl Beef {
37 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 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 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 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 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 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 beef.link_source_transactions();
97
98 Ok(beef)
99 }
100
101 pub fn to_binary(&self, writer: &mut impl Write) -> Result<(), TransactionError> {
103 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(); writer.write_all(&txid_bytes)?;
110 }
111
112 write_u32_le(writer, self.version)?;
113
114 write_varint(writer, self.bumps.len() as u64)?;
116 for bump in &self.bumps {
117 bump.to_binary(writer)?;
118 }
119
120 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 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 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 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 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 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 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 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 let mut in_degree = vec![0usize; n];
246 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 let mut queue: VecDeque<usize> = VecDeque::new();
262 for (i, °) 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 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 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 pub fn find_txid(&self, txid: &str) -> Option<&BeefTx> {
298 self.txs.iter().find(|btx| btx.txid == txid)
299 }
300
301 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 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 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 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 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 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 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 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 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 pub fn to_binary_atomic(&self, txid: &str) -> Result<Vec<u8>, TransactionError> {
425 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 let mut atomic_beef = self.clone();
434 atomic_beef.atomic_txid = Some(txid.to_string());
435
436 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 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 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 fn link_source_transactions(&mut self) {
479 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 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 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 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 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 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 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 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 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 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 let mut txid_bytes =
724 crate::primitives::utils::from_hex(expected_txid).expect("hex decode txid");
725 txid_bytes.reverse(); assert_eq!(
727 &atomic[4..36],
728 &txid_bytes[..],
729 "atomic should contain txid in LE"
730 );
731
732 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 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 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 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 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 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 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 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 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}