bsv-rs 0.3.5

BSV blockchain SDK for Rust - primitives, script, transactions, and more
Documentation
//! BeefTx - Transaction wrapper for BEEF format.
//!
//! A `BeefTx` represents a single Bitcoin transaction associated with a BEEF validity proof set.
//! It can contain:
//! - A full transaction (parsed or raw bytes)
//! - Just a transaction ID (for "known" transactions)
//!
//! The BEEF format supports "known" transactions which are represented solely by their txid.
//! This allows parties with shared history to exchange smaller proofs.

use crate::primitives::{from_hex, sha256d, to_hex, Reader, Writer};
use crate::Result;

use super::transaction::Transaction;

/// BEEF V1 format marker - when written as little-endian u32, produces bytes: 01 00 BE EF
pub const BEEF_V1: u32 = 0xEFBE0001;

/// BEEF V2 format marker - when written as little-endian u32, produces bytes: 02 00 BE EF
pub const BEEF_V2: u32 = 0xEFBE0002;

/// Atomic BEEF format marker.
pub const ATOMIC_BEEF: u32 = 0x01010101;

/// Transaction data format in BEEF.
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TxDataFormat {
    /// Raw transaction without BUMP index.
    RawTx = 0,
    /// Raw transaction with BUMP index.
    RawTxAndBumpIndex = 1,
    /// Transaction ID only (BRC-96).
    TxidOnly = 2,
}

impl TxDataFormat {
    /// Converts a u8 to TxDataFormat.
    pub fn from_u8(value: u8) -> Option<Self> {
        match value {
            0 => Some(Self::RawTx),
            1 => Some(Self::RawTxAndBumpIndex),
            2 => Some(Self::TxidOnly),
            _ => None,
        }
    }
}

/// A single Bitcoin transaction associated with a BEEF validity proof set.
///
/// Simple case is transaction data included directly, either as raw bytes or fully parsed data.
/// Supports "known" transactions which are represented by just their txid.
#[derive(Debug, Clone)]
pub struct BeefTx {
    /// Index into the BEEF's bumps array, if this transaction has a proof.
    bump_index: Option<usize>,
    /// Parsed transaction (lazily populated from raw_tx).
    tx: Option<Transaction>,
    /// Raw transaction bytes.
    raw_tx: Option<Vec<u8>>,
    /// Transaction ID (cached or for txid-only entries).
    txid: Option<String>,
    /// TXIDs of inputs (populated when transaction data is available and no proof).
    pub input_txids: Vec<String>,
    /// Whether this transaction is valid (has proof or chains to proofs).
    pub is_valid: Option<bool>,
}

impl BeefTx {
    /// Creates a BeefTx from a parsed Transaction.
    pub fn from_tx(tx: Transaction, bump_index: Option<usize>) -> Self {
        let mut btx = Self {
            bump_index,
            tx: Some(tx),
            raw_tx: None,
            txid: None,
            input_txids: Vec::new(),
            is_valid: None,
        };
        btx.update_input_txids();
        btx
    }

    /// Creates a BeefTx from raw transaction bytes.
    pub fn from_raw_tx(raw_tx: Vec<u8>, bump_index: Option<usize>) -> Self {
        let mut btx = Self {
            bump_index,
            tx: None,
            raw_tx: Some(raw_tx),
            txid: None,
            input_txids: Vec::new(),
            is_valid: None,
        };
        btx.update_input_txids();
        btx
    }

    /// Creates a BeefTx from just a transaction ID.
    pub fn from_txid(txid: String) -> Self {
        Self {
            bump_index: None,
            tx: None,
            raw_tx: None,
            txid: Some(txid),
            input_txids: Vec::new(),
            is_valid: None,
        }
    }

    /// Returns the bump index if this transaction has a proof.
    pub fn bump_index(&self) -> Option<usize> {
        self.bump_index
    }

    /// Sets the bump index.
    pub fn set_bump_index(&mut self, index: Option<usize>) {
        self.bump_index = index;
        self.update_input_txids();
    }

    /// Returns true if this transaction has a proof (bump).
    pub fn has_proof(&self) -> bool {
        self.bump_index.is_some()
    }

    /// Returns true if this is a txid-only entry (no transaction data).
    pub fn is_txid_only(&self) -> bool {
        self.txid.is_some() && self.raw_tx.is_none() && self.tx.is_none()
    }

    /// Returns the transaction ID.
    ///
    /// Computes and caches the txid if not already known.
    pub fn txid(&self) -> String {
        if let Some(ref txid) = self.txid {
            return txid.clone();
        }

        if let Some(ref tx) = self.tx {
            return tx.id();
        }

        if let Some(ref raw_tx) = self.raw_tx {
            // Compute txid from raw transaction bytes
            let hash = sha256d(raw_tx);
            let mut reversed = hash;
            reversed.reverse();
            return to_hex(&reversed);
        }

        panic!("BeefTx has no transaction data or txid");
    }

    /// Returns a reference to the parsed transaction, parsing from raw bytes if needed.
    pub fn tx(&self) -> Option<&Transaction> {
        self.tx.as_ref()
    }

    /// Returns a mutable reference to the parsed transaction.
    pub fn tx_mut(&mut self) -> Option<&mut Transaction> {
        // Ensure we have a parsed transaction
        if self.tx.is_none() && self.raw_tx.is_some() {
            if let Some(ref raw_tx) = self.raw_tx {
                if let Ok(tx) = Transaction::from_binary(raw_tx) {
                    self.tx = Some(tx);
                }
            }
        }
        self.tx.as_mut()
    }

    /// Returns the raw transaction bytes.
    pub fn raw_tx(&self) -> Option<&[u8]> {
        if self.raw_tx.is_some() {
            return self.raw_tx.as_deref();
        }

        None
    }

    /// Returns raw transaction bytes, computing from parsed tx if needed.
    pub fn raw_tx_or_compute(&mut self) -> Option<Vec<u8>> {
        if let Some(ref raw_tx) = self.raw_tx {
            return Some(raw_tx.clone());
        }

        if let Some(ref tx) = self.tx {
            let bytes = tx.to_binary();
            self.raw_tx = Some(bytes.clone());
            return Some(bytes);
        }

        None
    }

    /// Updates the input_txids list based on transaction data.
    fn update_input_txids(&mut self) {
        if self.has_proof() {
            // If we have a proof, we don't need to track input txids
            self.input_txids = Vec::new();
            return;
        }

        // Parse transaction if we have raw bytes but not parsed
        if self.tx.is_none() && self.raw_tx.is_some() {
            if let Some(ref raw_tx) = self.raw_tx {
                if let Ok(tx) = Transaction::from_binary(raw_tx) {
                    self.tx = Some(tx);
                }
            }
        }

        if let Some(ref tx) = self.tx {
            let mut seen = std::collections::HashSet::new();
            for input in &tx.inputs {
                // Use get_source_txid() which falls back to source_transaction
                // when source_txid is None (e.g., when input was created via
                // TransactionInput::with_source_transaction())
                if let Ok(txid) = input.get_source_txid() {
                    if !txid.is_empty() && seen.insert(txid.clone()) {
                        self.input_txids.push(txid);
                    }
                }
            }
        }
    }

    /// Writes this BeefTx to a Writer.
    pub fn to_writer(&self, writer: &mut Writer, version: u32) {
        if version == BEEF_V2 {
            if self.is_txid_only() {
                // TXID only format
                writer.write_u8(TxDataFormat::TxidOnly as u8);
                let txid_bytes = from_hex(&self.txid()).unwrap_or_default();
                let mut reversed = txid_bytes;
                reversed.reverse();
                writer.write_bytes(&reversed);
            } else if let Some(bump_idx) = self.bump_index {
                // Raw tx with bump index
                writer.write_u8(TxDataFormat::RawTxAndBumpIndex as u8);
                writer.write_var_int(bump_idx as u64);
                self.write_tx_data(writer);
            } else {
                // Raw tx without bump index
                writer.write_u8(TxDataFormat::RawTx as u8);
                self.write_tx_data(writer);
            }
        } else {
            // V1 format
            self.write_tx_data(writer);
            if let Some(bump_idx) = self.bump_index {
                writer.write_u8(1);
                writer.write_var_int(bump_idx as u64);
            } else {
                writer.write_u8(0);
            }
        }
    }

    /// Writes the transaction data (raw bytes or serialized).
    fn write_tx_data(&self, writer: &mut Writer) {
        if let Some(ref raw_tx) = self.raw_tx {
            writer.write_bytes(raw_tx);
        } else if let Some(ref tx) = self.tx {
            writer.write_bytes(&tx.to_binary());
        } else {
            panic!("BeefTx has no transaction data to write");
        }
    }

    /// Reads a BeefTx from a Reader.
    pub fn from_reader(reader: &mut Reader, version: u32) -> Result<Self> {
        if version == BEEF_V2 {
            let format = reader.read_u8()?;

            match TxDataFormat::from_u8(format) {
                Some(TxDataFormat::TxidOnly) => {
                    // Read txid (reversed)
                    let txid_slice = reader.read_bytes(32)?;
                    let mut txid_bytes = txid_slice.to_vec();
                    txid_bytes.reverse();
                    let txid = to_hex(&txid_bytes);
                    Ok(Self::from_txid(txid))
                }
                Some(TxDataFormat::RawTxAndBumpIndex) => {
                    let bump_index = reader.read_var_int_num()?;
                    let tx_start = reader.position();
                    let tx = Transaction::from_reader_internal(reader)?;
                    let raw_tx = reader.consumed_since(tx_start).to_vec();
                    let mut btx = Self::from_raw_tx(raw_tx, Some(bump_index));
                    btx.tx = Some(tx);
                    btx.update_input_txids();
                    Ok(btx)
                }
                Some(TxDataFormat::RawTx) | None => {
                    let tx_start = reader.position();
                    let tx = Transaction::from_reader_internal(reader)?;
                    let raw_tx = reader.consumed_since(tx_start).to_vec();
                    let mut btx = Self::from_raw_tx(raw_tx, None);
                    btx.tx = Some(tx);
                    btx.update_input_txids();
                    Ok(btx)
                }
            }
        } else {
            // V1 format
            let tx_start = reader.position();
            let tx = Transaction::from_reader_internal(reader)?;
            let raw_tx = reader.consumed_since(tx_start).to_vec();
            let has_bump = reader.read_u8()? != 0;
            let bump_index = if has_bump {
                Some(reader.read_var_int_num()?)
            } else {
                None
            };
            let mut btx = Self::from_raw_tx(raw_tx, bump_index);
            btx.tx = Some(tx);
            btx.update_input_txids();
            Ok(btx)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // Simple P2PKH transaction hex
    const TEST_TX_HEX: &str = "0100000001c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd3704000000004847304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901ffffffff0200ca9a3b00000000434104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84cac00286bee0000000043410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac00000000";

    #[test]
    fn test_from_tx() {
        let tx = Transaction::from_hex(TEST_TX_HEX).unwrap();
        let beef_tx = BeefTx::from_tx(tx, None);
        assert!(!beef_tx.is_txid_only());
        assert!(!beef_tx.has_proof());
        assert_eq!(beef_tx.txid().len(), 64);
    }

    #[test]
    fn test_from_raw_tx() {
        let raw_tx = from_hex(TEST_TX_HEX).unwrap();
        let beef_tx = BeefTx::from_raw_tx(raw_tx, Some(0));
        assert!(!beef_tx.is_txid_only());
        assert!(beef_tx.has_proof());
        assert_eq!(beef_tx.bump_index(), Some(0));
    }

    #[test]
    fn test_from_txid() {
        let txid = "abc123".repeat(11)[..64].to_string();
        let beef_tx = BeefTx::from_txid(txid.clone());
        assert!(beef_tx.is_txid_only());
        assert!(!beef_tx.has_proof());
        assert_eq!(beef_tx.txid(), txid);
    }

    #[test]
    fn test_input_txids() {
        let tx = Transaction::from_hex(TEST_TX_HEX).unwrap();
        let beef_tx = BeefTx::from_tx(tx, None);
        // This transaction has one input, so should have one input txid
        assert_eq!(beef_tx.input_txids.len(), 1);
    }

    #[test]
    fn test_input_txids_cleared_with_proof() {
        let tx = Transaction::from_hex(TEST_TX_HEX).unwrap();
        let beef_tx = BeefTx::from_tx(tx, Some(0));
        // With a proof, input_txids should be empty
        assert!(beef_tx.input_txids.is_empty());
    }

    #[test]
    fn test_tx_data_format() {
        assert_eq!(TxDataFormat::from_u8(0), Some(TxDataFormat::RawTx));
        assert_eq!(
            TxDataFormat::from_u8(1),
            Some(TxDataFormat::RawTxAndBumpIndex)
        );
        assert_eq!(TxDataFormat::from_u8(2), Some(TxDataFormat::TxidOnly));
        assert_eq!(TxDataFormat::from_u8(3), None);
    }
}