bsv-rs 0.4.3

BSV SDK for Rust: primitives, the script interpreter, transactions and BEEF/SPV, BRC-42 wallets, BRC-103 auth, the overlay network. Reference parity with the TypeScript SDK, native and wasm32.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
//! 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
    }

    /// True for a raw transaction whose input count is 0. A txid-only entry
    /// has no bytes to read and is never this.
    ///
    /// Such bytes are no transaction: the node refuses one before any script
    /// runs, so no block holds it and nothing spends it. Since 0.4.1
    /// [`Beef::verify_valid`](super::beef::Beef::verify_valid) refuses a BEEF
    /// that carries one, with or without a BUMP index, as the streaming
    /// reader does (`Kind::NoInputs`).
    pub fn has_no_inputs(&self) -> bool {
        if let Some(ref tx) = self.tx {
            return tx.inputs.is_empty();
        }
        match self.raw_tx {
            // The input count is the varint after the four bytes of version.
            Some(ref raw_tx) => {
                let mut reader = Reader::new(raw_tx);
                reader.read_bytes(4).is_ok() && matches!(reader.read_var_int_num(), Ok(0))
            }
            None => false,
        }
    }

    /// True for a raw transaction with at least one input and an output
    /// count of 0. A txid-only entry has no bytes to read and is never this;
    /// raw bytes that do not parse are not this either (the parse refuses
    /// them first).
    ///
    /// The node refuses such bytes beside a transaction with no input, after
    /// it (bsv-script-lean@87f0461 `lean/BsvScript/TxRules.lean:86`,
    /// `checkTransactionCommon_voutEmpty`). Since 0.4.3
    /// [`Beef::verify_valid`](super::beef::Beef::verify_valid) refuses a BEEF
    /// that carries one, with or without a BUMP index, as the streaming
    /// reader does (`Kind::NoOutputs`).
    pub fn has_no_outputs(&self) -> bool {
        if self.has_no_inputs() {
            return false;
        }
        if let Some(ref tx) = self.tx {
            return tx.outputs.is_empty();
        }
        match self.raw_tx {
            Some(ref raw_tx) => {
                matches!(Transaction::from_binary(raw_tx), Ok(tx) if tx.outputs.is_empty())
            }
            None => false,
        }
    }

    /// 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);
    }
}