Skip to main content

bsv/transaction/
beef_tx.rs

1//! BeefTx: a single transaction within a BEEF validity proof set.
2//!
3//! Wraps a Transaction with BEEF metadata (bump index, txid-only support).
4//! Supports V1 and V2 binary serialization formats.
5
6use std::io::{Read, Write};
7
8use crate::primitives::utils::{from_hex, to_hex};
9use crate::transaction::error::TransactionError;
10use crate::transaction::transaction::Transaction;
11use crate::transaction::{read_varint, write_varint};
12
13/// Format marker for transactions in BEEF V2 format (BRC-96).
14#[repr(u8)]
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum TxDataFormat {
17    /// Raw transaction without BUMP proof.
18    RawTx = 0,
19    /// Raw transaction with a BUMP index.
20    RawTxAndBumpIndex = 1,
21    /// Transaction represented by txid only (no raw data).
22    TxidOnly = 2,
23}
24
25impl TxDataFormat {
26    /// Convert a u8 byte to a TxDataFormat variant.
27    pub fn from_byte(b: u8) -> Result<Self, TransactionError> {
28        match b {
29            0 => Ok(TxDataFormat::RawTx),
30            1 => Ok(TxDataFormat::RawTxAndBumpIndex),
31            2 => Ok(TxDataFormat::TxidOnly),
32            _ => Err(TransactionError::InvalidFormat(format!(
33                "unknown TxDataFormat byte: {b}"
34            ))),
35        }
36    }
37}
38
39/// A single bitcoin transaction associated with a BEEF validity proof set.
40///
41/// Simple case: transaction data included directly as a full Transaction.
42/// Supports "known" transactions represented by just their txid.
43#[derive(Debug, Clone)]
44pub struct BeefTx {
45    /// The transaction (None if txid-only format).
46    pub tx: Option<Transaction>,
47    /// TXID hex (big-endian display format).
48    pub txid: String,
49    /// BUMP index into the Beef.bumps array (None if no proof).
50    pub bump_index: Option<usize>,
51    /// List of input txids this transaction depends on.
52    pub input_txids: Vec<String>,
53}
54
55impl BeefTx {
56    /// Create a BeefTx from a full Transaction.
57    pub fn from_tx(tx: Transaction, bump_index: Option<usize>) -> Result<Self, TransactionError> {
58        let txid = tx.id()?;
59        let input_txids = if bump_index.is_some() {
60            Vec::new()
61        } else {
62            Self::collect_input_txids(&tx)
63        };
64        Ok(BeefTx {
65            tx: Some(tx),
66            txid,
67            bump_index,
68            input_txids,
69        })
70    }
71
72    /// Create a BeefTx from a txid only (no raw transaction data).
73    pub fn from_txid(txid: String) -> Self {
74        BeefTx {
75            tx: None,
76            txid,
77            bump_index: None,
78            input_txids: Vec::new(),
79        }
80    }
81
82    /// Whether this transaction is represented by txid only (no raw data).
83    pub fn is_txid_only(&self) -> bool {
84        self.tx.is_none()
85    }
86
87    /// Whether this transaction has a BUMP proof.
88    pub fn has_proof(&self) -> bool {
89        self.bump_index.is_some()
90    }
91
92    /// Collect unique input txids from a transaction.
93    fn collect_input_txids(tx: &Transaction) -> Vec<String> {
94        let mut txids = Vec::new();
95        for input in &tx.inputs {
96            if let Some(ref stxid) = input.source_txid {
97                if !txids.contains(stxid) {
98                    txids.push(stxid.clone());
99                }
100            }
101        }
102        txids
103    }
104
105    /// Deserialize a BeefTx from BEEF V1 binary format.
106    ///
107    /// V1 format: raw_transaction + has_bump(u8) + [bump_index(varint)]
108    pub fn from_binary_v1(reader: &mut impl Read) -> Result<Self, TransactionError> {
109        let tx = Transaction::from_binary(reader)?;
110        let mut has_bump_buf = [0u8; 1];
111        reader.read_exact(&mut has_bump_buf)?;
112        let bump_index = if has_bump_buf[0] != 0 {
113            Some(
114                read_varint(reader).map_err(|e| TransactionError::InvalidFormat(e.to_string()))?
115                    as usize,
116            )
117        } else {
118            None
119        };
120        Self::from_tx(tx, bump_index)
121    }
122
123    /// Deserialize a BeefTx from BEEF V2 binary format (BRC-96).
124    ///
125    /// V2 format: tx_data_format(u8) + format-specific data
126    pub fn from_binary_v2(reader: &mut impl Read) -> Result<Self, TransactionError> {
127        let mut format_buf = [0u8; 1];
128        reader.read_exact(&mut format_buf)?;
129        let format = TxDataFormat::from_byte(format_buf[0])?;
130
131        match format {
132            TxDataFormat::TxidOnly => {
133                // Read 32-byte txid (stored reversed/LE on wire)
134                let mut txid_bytes = [0u8; 32];
135                reader.read_exact(&mut txid_bytes)?;
136                txid_bytes.reverse();
137                let txid = to_hex(&txid_bytes);
138                Ok(BeefTx::from_txid(txid))
139            }
140            TxDataFormat::RawTxAndBumpIndex => {
141                let bump_index = read_varint(reader)
142                    .map_err(|e| TransactionError::InvalidFormat(e.to_string()))?
143                    as usize;
144                let tx = Transaction::from_binary(reader)?;
145                Self::from_tx(tx, Some(bump_index))
146            }
147            TxDataFormat::RawTx => {
148                let tx = Transaction::from_binary(reader)?;
149                Self::from_tx(tx, None)
150            }
151        }
152    }
153
154    /// Serialize a BeefTx to BEEF V1 binary format.
155    pub fn to_binary_v1(&self, writer: &mut impl Write) -> Result<(), TransactionError> {
156        if let Some(ref tx) = self.tx {
157            tx.to_binary(writer)?;
158        } else {
159            return Err(TransactionError::BeefError(
160                "cannot serialize txid-only BeefTx in V1 format".to_string(),
161            ));
162        }
163
164        if let Some(bump_index) = self.bump_index {
165            writer.write_all(&[1u8])?; // has_bump = true
166            write_varint(writer, bump_index as u64)?;
167        } else {
168            writer.write_all(&[0u8])?; // has_bump = false
169        }
170        Ok(())
171    }
172
173    /// Serialize a BeefTx to BEEF V2 binary format (BRC-96).
174    pub fn to_binary_v2(&self, writer: &mut impl Write) -> Result<(), TransactionError> {
175        if self.is_txid_only() {
176            writer.write_all(&[TxDataFormat::TxidOnly as u8])?;
177            let mut txid_bytes =
178                from_hex(&self.txid).map_err(|e| TransactionError::InvalidFormat(e.to_string()))?;
179            txid_bytes.reverse(); // Display BE -> wire LE
180            writer.write_all(&txid_bytes)?;
181        } else if let Some(bump_index) = self.bump_index {
182            writer.write_all(&[TxDataFormat::RawTxAndBumpIndex as u8])?;
183            write_varint(writer, bump_index as u64)?;
184            self.tx
185                .as_ref()
186                .ok_or_else(|| {
187                    TransactionError::InvalidFormat("BeefTx has bump_index but no tx".to_string())
188                })?
189                .to_binary(writer)?;
190        } else {
191            writer.write_all(&[TxDataFormat::RawTx as u8])?;
192            self.tx
193                .as_ref()
194                .ok_or_else(|| {
195                    TransactionError::InvalidFormat("BeefTx has no tx data".to_string())
196                })?
197                .to_binary(writer)?;
198        }
199        Ok(())
200    }
201}