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