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_with(reader, false)?;
80 beef.bumps.push(bump);
81 }
82
83 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 beef.link_source_transactions();
101
102 Ok(beef)
103 }
104
105 pub fn to_binary(&self, writer: &mut impl Write) -> Result<(), TransactionError> {
107 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(); writer.write_all(&txid_bytes)?;
114 }
115
116 write_u32_le(writer, self.version)?;
117
118 write_varint(writer, self.bumps.len() as u64)?;
120 for bump in &self.bumps {
121 bump.to_binary(writer)?;
122 }
123
124 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 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 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 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 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 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 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 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 let mut in_degree = vec![0usize; n];
250 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 let mut queue: VecDeque<usize> = VecDeque::new();
266 for (i, °) 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 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 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 pub fn find_txid(&self, txid: &str) -> Option<&BeefTx> {
302 self.txs.iter().find(|btx| btx.txid == txid)
303 }
304
305 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 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 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 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 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 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 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 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 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 pub fn to_binary_atomic(&self, txid: &str) -> Result<Vec<u8>, TransactionError> {
429 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 let mut atomic_beef = self.clone();
438 atomic_beef.atomic_txid = Some(txid.to_string());
439
440 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 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 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 fn link_source_transactions(&mut self) {
483 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 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 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 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 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 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 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 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 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 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 let mut txid_bytes =
728 crate::primitives::utils::from_hex(expected_txid).expect("hex decode txid");
729 txid_bytes.reverse(); assert_eq!(
731 &atomic[4..36],
732 &txid_bytes[..],
733 "atomic should contain txid in LE"
734 );
735
736 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 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 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 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 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 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 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 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 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}