Skip to main content

bsv/transaction/
transaction.rs

1//! Bitcoin transaction type with wire format and EF format serialization.
2
3use std::io::{Cursor, Read, Write};
4
5use crate::primitives::hash::hash256;
6use crate::primitives::transaction_signature::{
7    SIGHASH_ANYONECANPAY, SIGHASH_FORKID, SIGHASH_NONE, SIGHASH_SINGLE,
8};
9use crate::script::locking_script::LockingScript;
10use crate::script::templates::ScriptTemplateUnlock;
11use crate::transaction::error::TransactionError;
12use crate::transaction::merkle_path::MerklePath;
13use crate::transaction::transaction_input::TransactionInput;
14use crate::transaction::transaction_output::TransactionOutput;
15use crate::transaction::{
16    read_u32_le, read_u64_le, read_varint, write_u32_le, write_u64_le, write_varint,
17};
18
19/// EF format marker bytes: [0x00, 0x00, 0x00, 0x00, 0x00, 0xEF]
20const EF_MARKER: [u8; 6] = [0x00, 0x00, 0x00, 0x00, 0x00, 0xEF];
21
22/// A Bitcoin transaction with inputs, outputs, and optional merkle proof.
23///
24/// Supports standard binary and Extended Format (EF) serialization,
25/// BEEF/Atomic BEEF packaging, and BIP-143 sighash preimage computation
26/// for signing. Translates the TS SDK Transaction.ts.
27#[derive(Debug, Clone)]
28pub struct Transaction {
29    /// Transaction version number.
30    pub version: u32,
31    /// Transaction inputs.
32    pub inputs: Vec<TransactionInput>,
33    /// Transaction outputs.
34    pub outputs: Vec<TransactionOutput>,
35    /// Lock time.
36    pub lock_time: u32,
37    /// Merkle path for SPV verification (populated from BEEF).
38    pub merkle_path: Option<MerklePath>,
39}
40
41impl Transaction {
42    /// Create a new empty transaction with default values.
43    pub fn new() -> Self {
44        Self {
45            version: 1,
46            inputs: Vec::new(),
47            outputs: Vec::new(),
48            lock_time: 0,
49            merkle_path: None,
50        }
51    }
52
53    /// Deserialize a transaction from binary wire format.
54    pub fn from_binary(reader: &mut impl Read) -> Result<Self, TransactionError> {
55        let version = read_u32_le(reader)?;
56
57        let input_count = read_varint(reader)? as usize;
58        let mut inputs = Vec::with_capacity(input_count);
59        for _ in 0..input_count {
60            inputs.push(TransactionInput::from_binary(reader)?);
61        }
62
63        let output_count = read_varint(reader)? as usize;
64        let mut outputs = Vec::with_capacity(output_count);
65        for _ in 0..output_count {
66            outputs.push(TransactionOutput::from_binary(reader)?);
67        }
68
69        let lock_time = read_u32_le(reader)?;
70
71        Ok(Transaction {
72            version,
73            inputs,
74            outputs,
75            lock_time,
76            merkle_path: None,
77        })
78    }
79
80    /// Deserialize a transaction from a hex string.
81    pub fn from_hex(hex: &str) -> Result<Self, TransactionError> {
82        let bytes = hex_to_bytes(hex)
83            .map_err(|e| TransactionError::InvalidFormat(format!("invalid hex: {}", e)))?;
84        let mut cursor = Cursor::new(bytes);
85        Self::from_binary(&mut cursor)
86    }
87
88    /// Parse a transaction from a BEEF hex string, returning the subject transaction.
89    ///
90    /// Decodes the hex to bytes, parses the BEEF structure, and extracts the
91    /// subject transaction (the last tx, or the atomic txid target).
92    pub fn from_beef(beef_hex: &str) -> Result<Self, TransactionError> {
93        let beef = crate::transaction::beef::Beef::from_hex(beef_hex)?;
94        beef.into_transaction()
95    }
96
97    /// Serialize a transaction to binary wire format.
98    pub fn to_binary(&self, writer: &mut impl Write) -> Result<(), TransactionError> {
99        write_u32_le(writer, self.version)?;
100
101        write_varint(writer, self.inputs.len() as u64)?;
102        for input in &self.inputs {
103            input.to_binary(writer)?;
104        }
105
106        write_varint(writer, self.outputs.len() as u64)?;
107        for output in &self.outputs {
108            output.to_binary(writer)?;
109        }
110
111        write_u32_le(writer, self.lock_time)?;
112        Ok(())
113    }
114
115    /// Serialize a transaction to a hex string.
116    pub fn to_hex(&self) -> Result<String, TransactionError> {
117        let bytes = self.to_bytes()?;
118        Ok(bytes_to_hex(&bytes))
119    }
120
121    /// Serialize a transaction to a byte vector.
122    pub fn to_bytes(&self) -> Result<Vec<u8>, TransactionError> {
123        let mut buf = Vec::new();
124        self.to_binary(&mut buf)?;
125        Ok(buf)
126    }
127
128    /// Serialize this transaction and its input chain to BEEF (BRC-62) format.
129    ///
130    /// Walks the `source_transaction` tree recursively to collect all ancestor
131    /// transactions and their merkle paths, then delegates to `Beef` for
132    /// deduplication, topological sorting, and serialization.
133    /// Matches the TS SDK's `Transaction.toBEEF()`.
134    pub fn to_beef(&self) -> Result<Vec<u8>, TransactionError> {
135        use crate::transaction::beef::{Beef, BEEF_V1};
136        use crate::transaction::beef_tx::BeefTx;
137        use std::collections::HashSet;
138
139        let mut beef = Beef::new(BEEF_V1);
140        let mut seen = HashSet::new();
141
142        fn collect(
143            tx: &Transaction,
144            beef: &mut Beef,
145            seen: &mut HashSet<String>,
146        ) -> Result<(), TransactionError> {
147            let txid = tx.id()?;
148            if seen.contains(&txid) {
149                return Ok(());
150            }
151            seen.insert(txid.clone());
152
153            if let Some(ref mp) = tx.merkle_path {
154                // Proven tx: merge its bump and add with the resulting index.
155                let bump_idx = beef.merge_bump(mp)?;
156                let btx = BeefTx::from_tx(tx.clone(), Some(bump_idx))?;
157                beef.remove_existing_txid(&btx.txid);
158                beef.txs.push(btx);
159            } else {
160                // Unproven tx: recurse into source transactions first.
161                for input in &tx.inputs {
162                    if let Some(ref source_tx) = input.source_transaction {
163                        collect(source_tx, beef, seen)?;
164                    } else {
165                        return Err(TransactionError::BeefError(format!(
166                            "input spending {}:{} has no source transaction and tx {} has no merkle proof — cannot build BEEF",
167                            input.source_txid.as_deref().unwrap_or("unknown"),
168                            input.source_output_index,
169                            txid,
170                        )));
171                    }
172                }
173                let btx = BeefTx::from_tx(tx.clone(), None)?;
174                beef.remove_existing_txid(&btx.txid);
175                beef.txs.push(btx);
176            }
177
178            Ok(())
179        }
180
181        collect(self, &mut beef, &mut seen)?;
182
183        if beef.bumps.is_empty() {
184            return Err(TransactionError::BeefError(
185                "cannot produce BEEF: no merkle proofs found in the transaction chain".to_string(),
186            ));
187        }
188
189        beef.sort_txs();
190
191        let mut buf = Vec::new();
192        beef.to_binary(&mut buf)?;
193        Ok(buf)
194    }
195
196    /// Compute the transaction hash (double SHA-256).
197    ///
198    /// Returns the hash in internal byte order (LE).
199    pub fn hash(&self) -> Result<[u8; 32], TransactionError> {
200        let bytes = self.to_bytes()?;
201        Ok(hash256(&bytes))
202    }
203
204    /// Compute the transaction ID (hash reversed, hex-encoded).
205    ///
206    /// Returns the txid in display format (BE hex).
207    pub fn id(&self) -> Result<String, TransactionError> {
208        let mut h = self.hash()?;
209        h.reverse();
210        Ok(bytes_to_hex(&h))
211    }
212
213    /// Add an input to the transaction.
214    pub fn add_input(&mut self, input: TransactionInput) {
215        self.inputs.push(input);
216    }
217
218    /// Add an output to the transaction.
219    pub fn add_output(&mut self, output: TransactionOutput) {
220        self.outputs.push(output);
221    }
222
223    /// Deserialize a transaction from EF format (BRC-30).
224    ///
225    /// EF format: version(4) + EF_MARKER(6) + inputs_with_source_info + outputs + locktime(4)
226    /// Each input additionally includes source satoshis (u64 LE) and source locking script.
227    pub fn from_ef(reader: &mut impl Read) -> Result<Self, TransactionError> {
228        let version = read_u32_le(reader)?;
229
230        // Read and verify the 6-byte EF marker
231        let mut marker = [0u8; 6];
232        reader.read_exact(&mut marker)?;
233        if marker != EF_MARKER {
234            return Err(TransactionError::InvalidFormat(
235                "invalid EF marker".to_string(),
236            ));
237        }
238
239        let input_count = read_varint(reader)? as usize;
240        let mut inputs = Vec::with_capacity(input_count);
241        for _ in 0..input_count {
242            // Read standard input fields
243            let mut input = TransactionInput::from_binary(reader)?;
244
245            // Read source satoshis (u64 LE)
246            let source_satoshis = read_u64_le(reader)?;
247
248            // Read source locking script (varint + bytes)
249            let script_len = read_varint(reader)? as usize;
250            let mut script_bytes = vec![0u8; script_len];
251            if script_len > 0 {
252                reader.read_exact(&mut script_bytes)?;
253            }
254            let source_locking_script = LockingScript::from_binary(&script_bytes);
255
256            // Create a minimal source transaction with one output at the referenced index
257            let mut source_tx = Transaction::new();
258            // Pad outputs up to the referenced index
259            for _ in 0..input.source_output_index {
260                source_tx.outputs.push(TransactionOutput::default());
261            }
262            source_tx.outputs.push(TransactionOutput {
263                satoshis: Some(source_satoshis),
264                locking_script: source_locking_script,
265                change: false,
266            });
267            input.source_transaction = Some(Box::new(source_tx));
268
269            inputs.push(input);
270        }
271
272        let output_count = read_varint(reader)? as usize;
273        let mut outputs = Vec::with_capacity(output_count);
274        for _ in 0..output_count {
275            outputs.push(TransactionOutput::from_binary(reader)?);
276        }
277
278        let lock_time = read_u32_le(reader)?;
279
280        Ok(Transaction {
281            version,
282            inputs,
283            outputs,
284            lock_time,
285            merkle_path: None,
286        })
287    }
288
289    /// Deserialize a transaction from an EF format hex string.
290    pub fn from_hex_ef(hex: &str) -> Result<Self, TransactionError> {
291        let bytes = hex_to_bytes(hex)
292            .map_err(|e| TransactionError::InvalidFormat(format!("invalid hex: {}", e)))?;
293        let mut cursor = Cursor::new(bytes);
294        Self::from_ef(&mut cursor)
295    }
296
297    /// Serialize a transaction to EF format (BRC-30).
298    pub fn to_ef(&self, writer: &mut impl Write) -> Result<(), TransactionError> {
299        write_u32_le(writer, self.version)?;
300
301        // Write EF marker
302        writer.write_all(&EF_MARKER)?;
303
304        write_varint(writer, self.inputs.len() as u64)?;
305        for input in &self.inputs {
306            // Write standard input fields
307            input.to_binary(writer)?;
308
309            // Write source satoshis and locking script from source transaction
310            if let Some(ref source_tx) = input.source_transaction {
311                let idx = input.source_output_index as usize;
312                if idx < source_tx.outputs.len() {
313                    let source_output = &source_tx.outputs[idx];
314                    write_u64_le(writer, source_output.satoshis.unwrap_or(0))?;
315                    let script_bin = source_output.locking_script.to_binary();
316                    write_varint(writer, script_bin.len() as u64)?;
317                    writer.write_all(&script_bin)?;
318                } else {
319                    return Err(TransactionError::MissingSourceTransaction);
320                }
321            } else {
322                return Err(TransactionError::MissingSourceTransaction);
323            }
324        }
325
326        write_varint(writer, self.outputs.len() as u64)?;
327        for output in &self.outputs {
328            output.to_binary(writer)?;
329        }
330
331        write_u32_le(writer, self.lock_time)?;
332        Ok(())
333    }
334
335    /// Serialize a transaction to an EF format hex string.
336    pub fn to_hex_ef(&self) -> Result<String, TransactionError> {
337        let mut buf = Vec::new();
338        self.to_ef(&mut buf)?;
339        Ok(bytes_to_hex(&buf))
340    }
341
342    /// Serialize a transaction to EF format raw bytes. Equivalent to
343    /// `hex_to_bytes(self.to_hex_ef()?)` but avoids the hex round-trip.
344    pub fn to_bytes_ef(&self) -> Result<Vec<u8>, TransactionError> {
345        let mut buf = Vec::new();
346        self.to_ef(&mut buf)?;
347        Ok(buf)
348    }
349
350    // -- Sighash preimage computation -----------------------------------------
351
352    /// Resolve the txid bytes (internal/LE byte order) for the input at `input_index`.
353    fn resolve_input_txid_bytes(&self, input_index: usize) -> Result<[u8; 32], TransactionError> {
354        let input = &self.inputs[input_index];
355        if let Some(ref txid) = input.source_txid {
356            let mut bytes = hex_to_bytes(txid)
357                .map_err(|e| TransactionError::InvalidFormat(format!("invalid txid hex: {}", e)))?;
358            bytes.reverse(); // display (BE) -> internal (LE)
359            let mut arr = [0u8; 32];
360            if bytes.len() == 32 {
361                arr.copy_from_slice(&bytes);
362            }
363            Ok(arr)
364        } else if let Some(ref source_tx) = input.source_transaction {
365            source_tx.hash()
366        } else {
367            Err(TransactionError::InvalidFormat(
368                "input has neither source_txid nor source_transaction".to_string(),
369            ))
370        }
371    }
372
373    /// Compute the BIP143/ForkID sighash preimage for the input at `input_index`.
374    ///
375    /// This is the standard BSV post-fork sighash format. The `scope` flags
376    /// should include SIGHASH_FORKID for normal BSV transactions.
377    ///
378    /// Parameters:
379    /// - `input_index`: index of the input being signed
380    /// - `scope`: sighash flags (e.g., SIGHASH_ALL | SIGHASH_FORKID)
381    /// - `source_satoshis`: value of the UTXO being spent
382    /// - `source_locking_script`: locking script of the UTXO being spent
383    pub fn sighash_preimage(
384        &self,
385        input_index: usize,
386        scope: u32,
387        source_satoshis: u64,
388        source_locking_script: &LockingScript,
389    ) -> Result<Vec<u8>, TransactionError> {
390        if input_index >= self.inputs.len() {
391            return Err(TransactionError::InvalidSighash(format!(
392                "input_index {} out of range (tx has {} inputs)",
393                input_index,
394                self.inputs.len()
395            )));
396        }
397
398        let base_type = scope & 0x1f;
399        let anyone_can_pay = (scope & SIGHASH_ANYONECANPAY) != 0;
400
401        let mut preimage = Vec::with_capacity(256);
402
403        // 1. nVersion (4 bytes LE)
404        preimage.extend_from_slice(&self.version.to_le_bytes());
405
406        // 2. hashPrevouts
407        if !anyone_can_pay {
408            let mut prevouts = Vec::new();
409            for (i, input) in self.inputs.iter().enumerate() {
410                let txid_bytes = self.resolve_input_txid_bytes(i)?;
411                prevouts.extend_from_slice(&txid_bytes);
412                prevouts.extend_from_slice(&input.source_output_index.to_le_bytes());
413            }
414            preimage.extend_from_slice(&hash256(&prevouts));
415        } else {
416            preimage.extend_from_slice(&[0u8; 32]);
417        }
418
419        // 3. hashSequence
420        if !anyone_can_pay && base_type != SIGHASH_NONE && base_type != SIGHASH_SINGLE {
421            let mut sequences = Vec::new();
422            for input in &self.inputs {
423                sequences.extend_from_slice(&input.sequence.to_le_bytes());
424            }
425            preimage.extend_from_slice(&hash256(&sequences));
426        } else {
427            preimage.extend_from_slice(&[0u8; 32]);
428        }
429
430        // 4. outpoint: this input's txid (LE) + output_index (4 bytes LE)
431        let this_txid = self.resolve_input_txid_bytes(input_index)?;
432        preimage.extend_from_slice(&this_txid);
433        preimage.extend_from_slice(&self.inputs[input_index].source_output_index.to_le_bytes());
434
435        // 5. scriptCode: varint-prefixed source_locking_script bytes
436        let script_bytes = source_locking_script.to_binary();
437        write_varint_to_vec(&mut preimage, script_bytes.len() as u64);
438        preimage.extend_from_slice(&script_bytes);
439
440        // 6. value: source_satoshis (8 bytes LE)
441        preimage.extend_from_slice(&source_satoshis.to_le_bytes());
442
443        // 7. nSequence: this input's sequence (4 bytes LE)
444        preimage.extend_from_slice(&self.inputs[input_index].sequence.to_le_bytes());
445
446        // 8. hashOutputs
447        if base_type != SIGHASH_NONE && base_type != SIGHASH_SINGLE {
448            // ALL: hash of all outputs serialized
449            let mut outputs_data = Vec::new();
450            for output in &self.outputs {
451                outputs_data.extend_from_slice(&output.satoshis.unwrap_or(0).to_le_bytes());
452                let script_bytes = output.locking_script.to_binary();
453                write_varint_to_vec(&mut outputs_data, script_bytes.len() as u64);
454                outputs_data.extend_from_slice(&script_bytes);
455            }
456            preimage.extend_from_slice(&hash256(&outputs_data));
457        } else if base_type == SIGHASH_SINGLE && input_index < self.outputs.len() {
458            // SINGLE: hash of the output at input_index
459            let output = &self.outputs[input_index];
460            let mut out_data = Vec::new();
461            out_data.extend_from_slice(&output.satoshis.unwrap_or(0).to_le_bytes());
462            let script_bytes = output.locking_script.to_binary();
463            write_varint_to_vec(&mut out_data, script_bytes.len() as u64);
464            out_data.extend_from_slice(&script_bytes);
465            preimage.extend_from_slice(&hash256(&out_data));
466        } else {
467            // NONE or SINGLE out-of-range: 32 zero bytes
468            preimage.extend_from_slice(&[0u8; 32]);
469        }
470
471        // 9. nLockTime (4 bytes LE)
472        preimage.extend_from_slice(&self.lock_time.to_le_bytes());
473
474        // 10. sighash type (4 bytes LE) -- scope with FORKID bit
475        preimage.extend_from_slice(&(scope | SIGHASH_FORKID).to_le_bytes());
476
477        Ok(preimage)
478    }
479
480    /// Compute the legacy OTDA sighash preimage for the input at `input_index`.
481    ///
482    /// Used when SIGHASH_FORKID is NOT set (pre-fork transactions or Chronicle mode).
483    /// This is the original Bitcoin sighash algorithm.
484    ///
485    /// The `sub_script` is the scriptCode bytes. OP_CODESEPARATOR opcodes will be
486    /// stripped automatically before inclusion in the preimage.
487    pub fn sighash_preimage_legacy(
488        &self,
489        input_index: usize,
490        scope: u32,
491        sub_script: &[u8],
492    ) -> Result<Vec<u8>, TransactionError> {
493        if input_index >= self.inputs.len() {
494            return Err(TransactionError::InvalidSighash(format!(
495                "input_index {} out of range (tx has {} inputs)",
496                input_index,
497                self.inputs.len()
498            )));
499        }
500
501        // Strip OP_CODESEPARATOR (0xab) opcodes from the script.
502        // Must parse properly to avoid removing 0xab bytes that appear as push data.
503        let sub_script = strip_codeseparator(sub_script);
504
505        let base_type = scope & 0x1f;
506        let anyone_can_pay = (scope & SIGHASH_ANYONECANPAY) != 0;
507        let is_none = base_type == SIGHASH_NONE;
508        let is_single = base_type == SIGHASH_SINGLE;
509
510        // SIGHASH_SINGLE bug: if input_index >= outputs, return [1, 0, 0, ..., 0]
511        if is_single && input_index >= self.outputs.len() {
512            let mut result = vec![0u8; 32];
513            result[0] = 1;
514            return Ok(result);
515        }
516
517        let empty_script: Vec<u8> = Vec::new();
518
519        let mut preimage = Vec::with_capacity(512);
520
521        // Version
522        preimage.extend_from_slice(&self.version.to_le_bytes());
523
524        // Inputs
525        if anyone_can_pay {
526            // Only the current input
527            write_varint_to_vec(&mut preimage, 1);
528            let txid_bytes = self.resolve_input_txid_bytes(input_index)?;
529            preimage.extend_from_slice(&txid_bytes);
530            preimage.extend_from_slice(&self.inputs[input_index].source_output_index.to_le_bytes());
531            write_varint_to_vec(&mut preimage, sub_script.len() as u64);
532            preimage.extend_from_slice(&sub_script);
533            preimage.extend_from_slice(&self.inputs[input_index].sequence.to_le_bytes());
534        } else {
535            write_varint_to_vec(&mut preimage, self.inputs.len() as u64);
536            for (i, input) in self.inputs.iter().enumerate() {
537                let txid_bytes = self.resolve_input_txid_bytes(i)?;
538                preimage.extend_from_slice(&txid_bytes);
539                preimage.extend_from_slice(&input.source_output_index.to_le_bytes());
540
541                // Script: only include sub_script for the input being signed
542                if i == input_index {
543                    write_varint_to_vec(&mut preimage, sub_script.len() as u64);
544                    preimage.extend_from_slice(&sub_script);
545                } else {
546                    write_varint_to_vec(&mut preimage, empty_script.len() as u64);
547                }
548
549                // Sequence: for SINGLE and NONE, zero out other inputs' sequences
550                if i == input_index || (!is_single && !is_none) {
551                    preimage.extend_from_slice(&input.sequence.to_le_bytes());
552                } else {
553                    preimage.extend_from_slice(&0u32.to_le_bytes());
554                }
555            }
556        }
557
558        // Outputs
559        if is_none {
560            write_varint_to_vec(&mut preimage, 0);
561        } else if is_single {
562            write_varint_to_vec(&mut preimage, (input_index + 1) as u64);
563            for i in 0..input_index {
564                // Blank outputs before the matching one: satoshis = -1 (0xFFFFFFFFFFFFFFFF), empty script
565                preimage.extend_from_slice(&u64::MAX.to_le_bytes());
566                write_varint_to_vec(&mut preimage, 0);
567                let _ = i;
568            }
569            // The output at input_index
570            let output = &self.outputs[input_index];
571            preimage.extend_from_slice(&output.satoshis.unwrap_or(0).to_le_bytes());
572            let script_bytes = output.locking_script.to_binary();
573            write_varint_to_vec(&mut preimage, script_bytes.len() as u64);
574            preimage.extend_from_slice(&script_bytes);
575        } else {
576            // ALL: serialize all outputs
577            write_varint_to_vec(&mut preimage, self.outputs.len() as u64);
578            for output in &self.outputs {
579                preimage.extend_from_slice(&output.satoshis.unwrap_or(0).to_le_bytes());
580                let script_bytes = output.locking_script.to_binary();
581                write_varint_to_vec(&mut preimage, script_bytes.len() as u64);
582                preimage.extend_from_slice(&script_bytes);
583            }
584        }
585
586        // Locktime
587        preimage.extend_from_slice(&self.lock_time.to_le_bytes());
588
589        // Sighash type (4 bytes LE)
590        preimage.extend_from_slice(&scope.to_le_bytes());
591
592        Ok(preimage)
593    }
594
595    // -- Transaction signing --------------------------------------------------
596
597    /// Sign the input at `input_index` using a ScriptTemplateUnlock implementation.
598    ///
599    /// Computes the sighash preimage (BIP143/ForkID format) and passes it to the
600    /// template's sign() method, then sets the resulting unlocking script on the input.
601    pub fn sign(
602        &mut self,
603        input_index: usize,
604        template: &dyn ScriptTemplateUnlock,
605        scope: u32,
606        source_satoshis: u64,
607        source_locking_script: &LockingScript,
608    ) -> Result<(), TransactionError> {
609        let preimage =
610            self.sighash_preimage(input_index, scope, source_satoshis, source_locking_script)?;
611        let unlocking_script = template
612            .sign(&preimage)
613            .map_err(|e| TransactionError::SigningFailed(format!("{}", e)))?;
614        self.inputs[input_index].unlocking_script = Some(unlocking_script);
615        Ok(())
616    }
617
618    /// Sign all unsigned inputs using the same template.
619    ///
620    /// A convenience method that reduces the per-input signing loop. For each
621    /// input that has no `unlocking_script` yet, this resolves `source_satoshis`
622    /// and `source_locking_script` from the input's `source_transaction` and
623    /// signs with the given template and sighash scope.
624    ///
625    /// Inputs that already have an unlocking script are skipped.
626    ///
627    /// Each input must have its `source_transaction` set so that the source
628    /// output's satoshis and locking script can be resolved. If you need
629    /// different templates or scopes per input, use the single-input `sign()`.
630    pub fn sign_all_inputs(
631        &mut self,
632        template: &dyn ScriptTemplateUnlock,
633        scope: u32,
634    ) -> Result<(), TransactionError> {
635        let num_inputs = self.inputs.len();
636
637        for i in 0..num_inputs {
638            // Skip inputs that already have an unlocking script
639            if self.inputs[i].unlocking_script.is_some() {
640                continue;
641            }
642
643            // Resolve source satoshis and locking script from source_transaction
644            let (source_satoshis, source_locking_script) = {
645                let source_tx = self.inputs[i].source_transaction.as_ref().ok_or_else(|| {
646                    TransactionError::SigningFailed(format!(
647                        "input {}: source_transaction required for sign_all_inputs()",
648                        i
649                    ))
650                })?;
651                let out_idx = self.inputs[i].source_output_index as usize;
652                let output = source_tx.outputs.get(out_idx).ok_or_else(|| {
653                    TransactionError::SigningFailed(format!(
654                        "input {}: source transaction has no output at index {}",
655                        i, out_idx
656                    ))
657                })?;
658                let satoshis = output.satoshis.ok_or_else(|| {
659                    TransactionError::SigningFailed(format!(
660                        "input {}: source output {} has no satoshis",
661                        i, out_idx
662                    ))
663                })?;
664                (satoshis, output.locking_script.clone())
665            };
666
667            let preimage =
668                self.sighash_preimage(i, scope, source_satoshis, &source_locking_script)?;
669            let unlocking_script = template
670                .sign(&preimage)
671                .map_err(|e| TransactionError::SigningFailed(format!("input {}: {}", i, e)))?;
672            self.inputs[i].unlocking_script = Some(unlocking_script);
673        }
674
675        Ok(())
676    }
677}
678
679impl Default for Transaction {
680    fn default() -> Self {
681        Self::new()
682    }
683}
684
685/// Convert a byte slice to a lowercase hex string.
686fn bytes_to_hex(bytes: &[u8]) -> String {
687    let mut s = String::with_capacity(bytes.len() * 2);
688    for b in bytes {
689        s.push_str(&format!("{:02x}", b));
690    }
691    s
692}
693
694/// Strip OP_CODESEPARATOR (0xab) opcodes from a raw script byte array.
695///
696/// Properly parses the script to avoid removing 0xab bytes that appear
697/// as data within push operations.
698fn strip_codeseparator(script: &[u8]) -> Vec<u8> {
699    const OP_CODESEPARATOR: u8 = 0xab;
700
701    let mut result = Vec::with_capacity(script.len());
702    let mut i = 0;
703    while i < script.len() {
704        let opcode = script[i];
705        if opcode == OP_CODESEPARATOR {
706            // Skip this opcode
707            i += 1;
708            continue;
709        }
710
711        if opcode > 0 && opcode < 76 {
712            // Direct push: opcode is the number of bytes to push
713            let push_len = opcode as usize;
714            let end = std::cmp::min(i + 1 + push_len, script.len());
715            result.extend_from_slice(&script[i..end]);
716            i = end;
717        } else if opcode == 76 {
718            // OP_PUSHDATA1: next byte is length
719            if i + 1 < script.len() {
720                let push_len = script[i + 1] as usize;
721                let end = std::cmp::min(i + 2 + push_len, script.len());
722                result.extend_from_slice(&script[i..end]);
723                i = end;
724            } else {
725                result.push(opcode);
726                i += 1;
727            }
728        } else if opcode == 77 {
729            // OP_PUSHDATA2: next 2 bytes are length (LE)
730            if i + 2 < script.len() {
731                let push_len = u16::from_le_bytes([script[i + 1], script[i + 2]]) as usize;
732                let end = std::cmp::min(i + 3 + push_len, script.len());
733                result.extend_from_slice(&script[i..end]);
734                i = end;
735            } else {
736                result.extend_from_slice(&script[i..]);
737                break;
738            }
739        } else if opcode == 78 {
740            // OP_PUSHDATA4: next 4 bytes are length (LE)
741            if i + 4 < script.len() {
742                let push_len = u32::from_le_bytes([
743                    script[i + 1],
744                    script[i + 2],
745                    script[i + 3],
746                    script[i + 4],
747                ]) as usize;
748                let end = std::cmp::min(i + 5 + push_len, script.len());
749                result.extend_from_slice(&script[i..end]);
750                i = end;
751            } else {
752                result.extend_from_slice(&script[i..]);
753                break;
754            }
755        } else {
756            // Regular opcode (0x00, or 0x4f..0xff except 0xab)
757            result.push(opcode);
758            i += 1;
759        }
760    }
761    result
762}
763
764/// Write a Bitcoin-style varint directly to a Vec<u8> (no io::Write needed).
765pub(crate) fn write_varint_to_vec(buf: &mut Vec<u8>, val: u64) {
766    if val < 0xfd {
767        buf.push(val as u8);
768    } else if val <= 0xffff {
769        buf.push(0xfd);
770        buf.extend_from_slice(&(val as u16).to_le_bytes());
771    } else if val <= 0xffff_ffff {
772        buf.push(0xfe);
773        buf.extend_from_slice(&(val as u32).to_le_bytes());
774    } else {
775        buf.push(0xff);
776        buf.extend_from_slice(&val.to_le_bytes());
777    }
778}
779
780/// Convert a hex string to bytes.
781fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, String> {
782    if !hex.len().is_multiple_of(2) {
783        return Err("odd length hex string".to_string());
784    }
785    let mut bytes = Vec::with_capacity(hex.len() / 2);
786    for i in (0..hex.len()).step_by(2) {
787        let byte = u8::from_str_radix(&hex[i..i + 2], 16)
788            .map_err(|e| format!("invalid hex at position {}: {}", i, e))?;
789        bytes.push(byte);
790    }
791    Ok(bytes)
792}
793
794#[cfg(test)]
795mod tests {
796    use super::*;
797    use crate::primitives::private_key::PrivateKey;
798    use crate::primitives::transaction_signature::{SIGHASH_ALL, SIGHASH_FORKID};
799    use crate::script::templates::p2pkh::P2PKH;
800    use crate::script::templates::ScriptTemplateLock;
801    use serde::Deserialize;
802
803    #[derive(Deserialize)]
804    struct TestVector {
805        description: String,
806        hex: String,
807        txid: String,
808        version: u32,
809        inputs: usize,
810        outputs: usize,
811        locktime: u32,
812    }
813
814    fn load_test_vectors() -> Vec<TestVector> {
815        let json = include_str!("../../test-vectors/transaction_valid.json");
816        serde_json::from_str(json).expect("failed to parse transaction_valid.json")
817    }
818
819    #[test]
820    fn test_from_binary_round_trip() {
821        let vectors = load_test_vectors();
822        for v in &vectors {
823            let tx = Transaction::from_hex(&v.hex)
824                .unwrap_or_else(|e| panic!("failed to parse '{}': {}", v.description, e));
825            let result_hex = tx
826                .to_hex()
827                .unwrap_or_else(|e| panic!("failed to serialize '{}': {}", v.description, e));
828            assert_eq!(
829                result_hex, v.hex,
830                "round-trip failed for '{}'",
831                v.description
832            );
833        }
834    }
835
836    #[test]
837    fn test_txid() {
838        let vectors = load_test_vectors();
839        for v in &vectors {
840            let tx = Transaction::from_hex(&v.hex)
841                .unwrap_or_else(|e| panic!("failed to parse '{}': {}", v.description, e));
842            let txid = tx
843                .id()
844                .unwrap_or_else(|e| panic!("failed to compute id for '{}': {}", v.description, e));
845            assert_eq!(txid, v.txid, "txid mismatch for '{}'", v.description);
846        }
847    }
848
849    #[test]
850    fn test_input_output_counts() {
851        let vectors = load_test_vectors();
852        for v in &vectors {
853            let tx = Transaction::from_hex(&v.hex)
854                .unwrap_or_else(|e| panic!("failed to parse '{}': {}", v.description, e));
855            assert_eq!(
856                tx.inputs.len(),
857                v.inputs,
858                "input count mismatch for '{}'",
859                v.description
860            );
861            assert_eq!(
862                tx.outputs.len(),
863                v.outputs,
864                "output count mismatch for '{}'",
865                v.description
866            );
867            assert_eq!(
868                tx.version, v.version,
869                "version mismatch for '{}'",
870                v.description
871            );
872            assert_eq!(
873                tx.lock_time, v.locktime,
874                "locktime mismatch for '{}'",
875                v.description
876            );
877        }
878    }
879
880    #[test]
881    fn test_empty_transaction() {
882        let tx = Transaction::new();
883        assert_eq!(tx.version, 1);
884        assert!(tx.inputs.is_empty());
885        assert!(tx.outputs.is_empty());
886        assert_eq!(tx.lock_time, 0);
887        assert!(tx.merkle_path.is_none());
888    }
889
890    #[test]
891    fn test_add_input_output() {
892        let mut tx = Transaction::new();
893        assert_eq!(tx.inputs.len(), 0);
894        assert_eq!(tx.outputs.len(), 0);
895
896        tx.add_input(TransactionInput::default());
897        assert_eq!(tx.inputs.len(), 1);
898
899        tx.add_output(TransactionOutput::default());
900        assert_eq!(tx.outputs.len(), 1);
901    }
902
903    fn make_test_tx_with_source_for_ef() -> Transaction {
904        // Re-use the same EF vector as test_ef_round_trip to build a tx with
905        // source_transaction populated (required for to_hex_ef / to_bytes_ef).
906        let ef_hex = "010000000000000000ef01ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e000000006a47304402203a61a2e931612b4bda08d541cfb980885173b8dcf64a3471238ae7abcd368d6402204cbf24f04b9aa2256d8901f0ed97866603d2be8324c2bfb7a37bf8fc90edd5b441210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff3e660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac013c660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac00000000";
907        Transaction::from_hex_ef(ef_hex).expect("make_test_tx_with_source_for_ef: parse failed")
908    }
909
910    #[test]
911    fn test_to_bytes_ef_round_trips_with_to_hex_ef() {
912        let tx = make_test_tx_with_source_for_ef();
913        let bytes = tx.to_bytes_ef().expect("to_bytes_ef");
914        let hex_form = tx.to_hex_ef().expect("to_hex_ef");
915        assert_eq!(hex_form, hex::encode(&bytes));
916    }
917
918    #[test]
919    fn test_ef_round_trip() {
920        // EF format vector from TS SDK test
921        let ef_hex = "010000000000000000ef01ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e000000006a47304402203a61a2e931612b4bda08d541cfb980885173b8dcf64a3471238ae7abcd368d6402204cbf24f04b9aa2256d8901f0ed97866603d2be8324c2bfb7a37bf8fc90edd5b441210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff3e660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac013c660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac00000000";
922
923        let tx = Transaction::from_hex_ef(ef_hex).expect("failed to parse EF hex");
924        assert_eq!(tx.inputs.len(), 1);
925        assert_eq!(tx.outputs.len(), 1);
926
927        // Verify source transaction info was captured
928        let input = &tx.inputs[0];
929        assert!(input.source_transaction.is_some());
930        let source_tx = input.source_transaction.as_ref().unwrap();
931        let source_output = &source_tx.outputs[input.source_output_index as usize];
932        assert_eq!(source_output.satoshis, Some(0x663e)); // 26174 satoshis
933
934        // Round-trip: serialize back to EF hex
935        let result_hex = tx.to_hex_ef().expect("failed to serialize to EF");
936        assert_eq!(result_hex, ef_hex);
937    }
938
939    #[test]
940    fn test_hash_and_id_consistency() {
941        // tx2 from the TS SDK test
942        let tx2hex = "01000000029e8d016a7b0dc49a325922d05da1f916d1e4d4f0cb840c9727f3d22ce8d1363f000000008c493046022100e9318720bee5425378b4763b0427158b1051eec8b08442ce3fbfbf7b30202a44022100d4172239ebd701dae2fbaaccd9f038e7ca166707333427e3fb2a2865b19a7f27014104510c67f46d2cbb29476d1f0b794be4cb549ea59ab9cc1e731969a7bf5be95f7ad5e7f904e5ccf50a9dc1714df00fbeb794aa27aaff33260c1032d931a75c56f2ffffffffa3195e7a1ab665473ff717814f6881485dc8759bebe97e31c301ffe7933a656f020000008b48304502201c282f35f3e02a1f32d2089265ad4b561f07ea3c288169dedcf2f785e6065efa022100e8db18aadacb382eed13ee04708f00ba0a9c40e3b21cf91da8859d0f7d99e0c50141042b409e1ebbb43875be5edde9c452c82c01e3903d38fa4fd89f3887a52cb8aea9dc8aec7e2c9d5b3609c03eb16259a2537135a1bf0f9c5fbbcbdbaf83ba402442ffffffff02206b1000000000001976a91420bb5c3bfaef0231dc05190e7f1c8e22e098991e88acf0ca0100000000001976a9149e3e2d23973a04ec1b02be97c30ab9f2f27c3b2c88ac00000000";
943        let tx2idhex = "8c9aa966d35bfeaf031409e0001b90ccdafd8d859799eb945a3c515b8260bcf2";
944
945        let tx = Transaction::from_hex(tx2hex).unwrap();
946        let id = tx.id().unwrap();
947        assert_eq!(id, tx2idhex);
948
949        // Verify hash is the reverse of id
950        let hash = tx.hash().unwrap();
951        let mut reversed_hash = hash;
952        reversed_hash.reverse();
953        let reversed_hex = bytes_to_hex(&reversed_hash);
954        assert_eq!(reversed_hex, tx2idhex);
955    }
956
957    // -- Sighash preimage tests -----------------------------------------------
958
959    /// OTDA sighash test vectors from TS SDK sighashTestData.ts
960    /// Format: (raw_tx_hex, script_hex, input_index, hash_type_unsigned, expected_otda_hash_display)
961    fn otda_test_vectors() -> Vec<(&'static str, &'static str, usize, u32, &'static str)> {
962        vec![
963            ("0122769903cfc6fedb9c63fe76930fed0c87b44be46f5032a534fa05861548616a5b99034701000000040063656ac864c70228b3f6ebaf97be065b2180ece52fae4f9039c7bf932e567625d7d89582abe1340100000008ac6363650063ab65ffffffff587125b913706705dc799454ab0343ee8aa59a2f2d538f75e12c0deeb40aa741030000000027b3a02003d21f34040000000000fa7f2c0300000000016387213b0300000000056aabacacab00000000", "", 1, 902085315, "49fdc84c5f88a590c5c65e17de58da7d1028133b74ad2d56a8b15ba317ac98f6"),
964            ("7e8c3f7902634018b6e1db2ca591816dff64a6cff74643de7455323ebfc560500aad9eee8c0100000001519c2d146d06fdca2c2e5fa3a2559812df66b6b5e40a3c57b2f7071ae6fe3863c74ab0952d0100000001000bf6638c013c5f6503000000000351ac63cbbf66be", "acab", 0, 2433331782, "f6261bacaed3a70d504cd70d3c0623e3593f6d197cd47316e56cea79ceabe095"),
965            ("459499bb032fdcc39d3c6cf819dcaa0a0165d97578446aa87ab745fb9fdcd3e6177b4cba3d0000000005006a6a5265ffffffff10e5929ebe065273c112cab15f6a1f6d9a8a517c288311b048b16663b3d406dc030000000700535263655151ffffffff981d73a7f3d477ab055398bcf9a7d349db1a8e6362055e20f4207ad1b775bac301000000066a6363ac6552ffffffff0403342603000000000165c4390004000000000965ac52006565006365373ce8010000000005520000516aba5a9404000000000351655300000000", "6a5352", 0, 3544391288, "738b7dcb86260e6fe3fad331ff342429c157730bbcb90c205b9e08568557cd94"),
966            ("cb3b8d30043ccd81c3bda7f594cca60e2eef170c67ffe8a1eb1f1a994dc40a0a5cf89fa9690100000009ab536a52acab5300653bea9324983da711ccb6eaff060930e6f55cf6df75e5abdda91a8d5fc25c3b9b28d0e7370200000003ab51522f86cdbd8aa19b6b8536efb6ca8cc23ebccef585ad00a78b5956d803908482bb44b25c550000000007abab65530051ac8177a2acebc517db1d5b5be14f91ab40e811ec0316cf029ce657a4b06f04f30698f0a0e50000000007516aac636a6351ffffffff02ccbefa02000000000252ab3e297f0100000000060052525163521acc3e2b", "6aac636a63535153", 0, 3406487088, "3568dfad7e968afd3492bd146c8b0e3255f90e5b642a4ec10105693e8b029132"),
967        ]
968    }
969
970    #[test]
971    fn test_sighash_preimage_legacy_vectors() {
972        let vectors = otda_test_vectors();
973        let mut passed = 0;
974
975        for (i, (raw_tx_hex, script_hex, input_index, hash_type, expected_hash)) in
976            vectors.iter().enumerate()
977        {
978            let tx = Transaction::from_hex(raw_tx_hex)
979                .unwrap_or_else(|e| panic!("vector {}: failed to parse tx: {}", i, e));
980
981            let sub_script = if script_hex.is_empty() {
982                vec![]
983            } else {
984                hex_to_bytes(script_hex).unwrap()
985            };
986
987            let preimage = tx
988                .sighash_preimage_legacy(*input_index, *hash_type, &sub_script)
989                .unwrap_or_else(|e| panic!("vector {}: sighash error: {}", i, e));
990
991            // The expected hash is in display (BE/reversed) format
992            let mut hash_bytes = hash256(&preimage);
993            hash_bytes.reverse();
994            let computed_hash = bytes_to_hex(&hash_bytes);
995
996            if computed_hash == *expected_hash {
997                passed += 1;
998            } else {
999                println!(
1000                    "MISMATCH vector {}: expected={}, got={}",
1001                    i, expected_hash, computed_hash
1002                );
1003            }
1004        }
1005
1006        println!(
1007            "sighash legacy OTDA vectors: {}/{} passed",
1008            passed,
1009            vectors.len()
1010        );
1011        assert_eq!(
1012            passed,
1013            vectors.len(),
1014            "all sighash OTDA vectors should pass"
1015        );
1016    }
1017
1018    #[test]
1019    fn test_sighash_preimage_bip143() {
1020        // Test vector from Go SDK: "1 Input 2 Outputs - SIGHASH_ALL (FORKID)"
1021        let unsigned_tx_hex = "010000000193a35408b6068499e0d5abd799d3e827d9bfe70c9b75ebe209c91d25072326510000000000ffffffff02404b4c00000000001976a91404ff367be719efa79d76e4416ffb072cd53b208888acde94a905000000001976a91404d03f746652cfcb6cb55119ab473a045137d26588ac00000000";
1022        let source_script_hex = "76a914c0a3c167a28cabb9fbb495affa0761e6e74ac60d88ac";
1023        let source_satoshis: u64 = 100_000_000;
1024        let expected_preimage_hex = "010000007ced5b2e5cf3ea407b005d8b18c393b6256ea2429b6ff409983e10adc61d0ae83bb13029ce7b1f559ef5e747fcac439f1455a2ec7c5f09b72290795e7066504493a35408b6068499e0d5abd799d3e827d9bfe70c9b75ebe209c91d2507232651000000001976a914c0a3c167a28cabb9fbb495affa0761e6e74ac60d88ac00e1f50500000000ffffffff87841ab2b7a4133af2c58256edb7c3c9edca765a852ebe2d0dc962604a30f1030000000041000000";
1025
1026        let tx = Transaction::from_hex(unsigned_tx_hex).unwrap();
1027        let source_script_bytes = hex_to_bytes(source_script_hex).unwrap();
1028        let source_locking_script = LockingScript::from_binary(&source_script_bytes);
1029
1030        let scope = SIGHASH_ALL | SIGHASH_FORKID;
1031        let preimage = tx
1032            .sighash_preimage(0, scope, source_satoshis, &source_locking_script)
1033            .unwrap();
1034        let preimage_hex = bytes_to_hex(&preimage);
1035
1036        assert_eq!(
1037            preimage_hex, expected_preimage_hex,
1038            "BIP143 preimage should match Go SDK test vector"
1039        );
1040    }
1041
1042    // -- Transaction signing tests --------------------------------------------
1043
1044    #[test]
1045    fn test_sign_p2pkh() {
1046        let key = PrivateKey::from_hex("1").unwrap();
1047        let p2pkh_lock = P2PKH::from_private_key(key.clone());
1048        let p2pkh_unlock = P2PKH::from_private_key(key.clone());
1049
1050        let lock_script = p2pkh_lock.lock().unwrap();
1051
1052        // Build a transaction with one input and one output
1053        let mut tx = Transaction::new();
1054        tx.add_input(TransactionInput {
1055            source_transaction: None,
1056            source_txid: Some("00".repeat(32)),
1057            source_output_index: 0,
1058            unlocking_script: None,
1059            sequence: 0xffffffff,
1060        });
1061        tx.add_output(TransactionOutput {
1062            satoshis: Some(50000),
1063            locking_script: lock_script.clone(),
1064            change: false,
1065        });
1066
1067        // Sign the input
1068        let scope = SIGHASH_ALL | SIGHASH_FORKID;
1069        tx.sign(0, &p2pkh_unlock, scope, 100000, &lock_script)
1070            .expect("signing should succeed");
1071
1072        // Verify unlocking script is set
1073        let unlock = tx.inputs[0].unlocking_script.as_ref().unwrap();
1074        let chunks = unlock.chunks();
1075        assert_eq!(
1076            chunks.len(),
1077            2,
1078            "P2PKH unlock should have 2 chunks (sig + pubkey)"
1079        );
1080
1081        // First chunk: signature (DER + sighash byte)
1082        let sig_data = chunks[0].data.as_ref().unwrap();
1083        assert!(
1084            sig_data.len() >= 70 && sig_data.len() <= 74,
1085            "signature length {} should be 70-74",
1086            sig_data.len()
1087        );
1088        assert_eq!(
1089            *sig_data.last().unwrap(),
1090            (SIGHASH_ALL | SIGHASH_FORKID) as u8,
1091            "last byte should be sighash type"
1092        );
1093
1094        // Second chunk: compressed public key (33 bytes)
1095        let pubkey_data = chunks[1].data.as_ref().unwrap();
1096        assert_eq!(pubkey_data.len(), 33);
1097    }
1098
1099    #[test]
1100    fn test_sign_and_verify_round_trip() {
1101        use crate::script::spend::{Spend, SpendParams};
1102
1103        let key = PrivateKey::from_hex("abcdef01").unwrap();
1104        let p2pkh = P2PKH::from_private_key(key.clone());
1105        let lock_script = p2pkh.lock().unwrap();
1106
1107        // Build and sign a transaction
1108        let mut tx = Transaction::new();
1109        let source_satoshis = 100_000u64;
1110
1111        tx.add_input(TransactionInput {
1112            source_transaction: None,
1113            source_txid: Some("aa".repeat(32)),
1114            source_output_index: 0,
1115            unlocking_script: None,
1116            sequence: 0xffffffff,
1117        });
1118        tx.add_output(TransactionOutput {
1119            satoshis: Some(90_000),
1120            locking_script: lock_script.clone(),
1121            change: false,
1122        });
1123
1124        let scope = SIGHASH_ALL | SIGHASH_FORKID;
1125        tx.sign(0, &p2pkh, scope, source_satoshis, &lock_script)
1126            .expect("signing should succeed");
1127
1128        // Now verify with Spend
1129        let unlock_script = tx.inputs[0].unlocking_script.clone().unwrap();
1130
1131        let mut spend = Spend::new(SpendParams {
1132            locking_script: lock_script.clone(),
1133            unlocking_script: unlock_script,
1134            source_txid: "aa".repeat(32),
1135            source_output_index: 0,
1136            source_satoshis,
1137            transaction_version: tx.version,
1138            transaction_lock_time: tx.lock_time,
1139            transaction_sequence: tx.inputs[0].sequence,
1140            other_inputs: vec![],
1141            other_outputs: tx.outputs.clone(),
1142            input_index: 0,
1143        });
1144
1145        let valid = spend.validate().expect("spend validation should not error");
1146        assert!(valid, "signed transaction should verify successfully");
1147    }
1148
1149    #[test]
1150    fn test_to_beef_round_trip_all_vectors() {
1151        // Parse known valid BEEFs via from_beef, re-serialize with to_beef(),
1152        // and verify the round-trip produces valid BEEF with the same subject tx.
1153        let vectors_json = std::fs::read_to_string("test-vectors/beef_valid.json").unwrap();
1154        let vectors: Vec<serde_json::Value> = serde_json::from_str(&vectors_json).unwrap();
1155
1156        for (i, v) in vectors.iter().enumerate() {
1157            let hex = v["hex"].as_str().unwrap();
1158            let version = v["version"].as_u64().unwrap();
1159            if version != 1 {
1160                continue; // to_beef() produces V1 only
1161            }
1162
1163            // Parse BEEF at the Beef level to get subject tx with full source chain
1164            let beef = crate::transaction::beef::Beef::from_hex(hex)
1165                .unwrap_or_else(|e| panic!("vector {}: from_hex failed: {}", i, e));
1166            let tx = beef
1167                .into_transaction()
1168                .unwrap_or_else(|e| panic!("vector {}: into_transaction failed: {}", i, e));
1169
1170            // Re-serialize to BEEF
1171            let beef_bytes = tx
1172                .to_beef()
1173                .unwrap_or_else(|e| panic!("vector {}: to_beef failed: {}", i, e));
1174
1175            // Verify BEEF V1 header
1176            assert_eq!(
1177                &beef_bytes[0..4],
1178                &[0x01, 0x00, 0xBE, 0xEF],
1179                "vector {}: wrong BEEF header",
1180                i
1181            );
1182
1183            // Parse back and verify same subject txid
1184            let re_hex: String = beef_bytes.iter().map(|b| format!("{:02x}", b)).collect();
1185            let re_parsed = Transaction::from_beef(&re_hex)
1186                .unwrap_or_else(|e| panic!("vector {}: round-trip from_beef failed: {}", i, e));
1187            assert_eq!(
1188                re_parsed.id().unwrap(),
1189                tx.id().unwrap(),
1190                "vector {}: txid mismatch after round-trip",
1191                i
1192            );
1193            assert_eq!(
1194                re_parsed.outputs.len(),
1195                tx.outputs.len(),
1196                "vector {}: output count mismatch",
1197                i
1198            );
1199        }
1200    }
1201
1202    #[test]
1203    fn test_to_beef_byte_equality_simple() {
1204        // For the simple single-tx vector, check that to_beef produces
1205        // byte-identical output to the original.
1206        let vectors_json = std::fs::read_to_string("test-vectors/beef_valid.json").unwrap();
1207        let vectors: Vec<serde_json::Value> = serde_json::from_str(&vectors_json).unwrap();
1208        let original_hex = vectors[0]["hex"].as_str().unwrap();
1209
1210        let tx = Transaction::from_beef(original_hex).expect("from_beef");
1211        let beef_bytes = tx.to_beef().expect("to_beef");
1212        let result_hex: String = beef_bytes.iter().map(|b| format!("{:02x}", b)).collect();
1213
1214        assert_eq!(
1215            result_hex, original_hex,
1216            "round-trip should produce identical bytes"
1217        );
1218    }
1219
1220    #[test]
1221    fn test_to_beef_multi_tx_with_source_chain() {
1222        // Build a transaction with a source transaction that has a merkle path.
1223        // This exercises the recursive collect into Beef.
1224        use crate::script::locking_script::LockingScript;
1225        use crate::transaction::merkle_path::{MerklePath, MerklePathLeaf};
1226        use crate::transaction::transaction_input::TransactionInput;
1227        use crate::transaction::transaction_output::TransactionOutput;
1228
1229        // Create a proven parent transaction.
1230        let mut parent = Transaction::new();
1231        parent.add_output(TransactionOutput {
1232            satoshis: Some(50_000),
1233            locking_script: LockingScript::from_binary(&vec![
1234                0x76, 0xa9, 0x14, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
1235                0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x88, 0xac,
1236            ]),
1237            change: false,
1238        });
1239        let parent_txid = parent.id().unwrap();
1240
1241        // Give parent a merkle path (proven in block).
1242        parent.merkle_path = Some(MerklePath {
1243            block_height: 100,
1244            path: vec![vec![
1245                MerklePathLeaf {
1246                    offset: 0,
1247                    hash: Some(parent_txid.clone()),
1248                    txid: true,
1249                    duplicate: false,
1250                },
1251                MerklePathLeaf {
1252                    offset: 1,
1253                    hash: Some("bb".repeat(32)),
1254                    txid: false,
1255                    duplicate: false,
1256                },
1257            ]],
1258        });
1259
1260        // Create child transaction spending parent.
1261        let mut child = Transaction::new();
1262        child.add_input(TransactionInput {
1263            source_transaction: Some(Box::new(parent.clone())),
1264            source_txid: Some(parent_txid.clone()),
1265            source_output_index: 0,
1266            unlocking_script: None,
1267            sequence: 0xffffffff,
1268        });
1269        child.add_output(TransactionOutput {
1270            satoshis: Some(40_000),
1271            locking_script: LockingScript::from_binary(&vec![0x6a, 0x04, 0xde, 0xad]),
1272            change: false,
1273        });
1274
1275        // Serialize to BEEF.
1276        let beef_bytes = child.to_beef().expect("to_beef should succeed");
1277
1278        // Verify header.
1279        assert_eq!(&beef_bytes[0..4], &[0x01, 0x00, 0xBE, 0xEF]);
1280
1281        // Parse back via Beef and verify structure.
1282        let re_beef = crate::transaction::beef::Beef::from_hex(
1283            &beef_bytes
1284                .iter()
1285                .map(|b| format!("{:02x}", b))
1286                .collect::<String>(),
1287        )
1288        .expect("re-parse BEEF");
1289
1290        assert_eq!(re_beef.bumps.len(), 1, "should have 1 bump");
1291        assert_eq!(re_beef.txs.len(), 2, "should have 2 txs (parent + child)");
1292
1293        // Proven parent should come before unproven child (topological order).
1294        assert!(
1295            re_beef.txs[0].bump_index.is_some(),
1296            "first tx should be proven parent"
1297        );
1298        assert!(
1299            re_beef.txs[1].bump_index.is_none(),
1300            "second tx should be unproven child"
1301        );
1302    }
1303
1304    #[test]
1305    fn test_to_beef_errors_no_merkle_proofs() {
1306        // A bare transaction with no source chain and no merkle path should error.
1307        let mut tx = Transaction::new();
1308        tx.add_output(TransactionOutput {
1309            satoshis: Some(1000),
1310            locking_script: crate::script::locking_script::LockingScript::from_binary(&vec![
1311                0x6a, 0x02, 0xab, 0xcd,
1312            ]),
1313            change: false,
1314        });
1315
1316        let result = tx.to_beef();
1317        assert!(result.is_err(), "to_beef should fail with no proofs");
1318        let err = result.unwrap_err().to_string();
1319        assert!(
1320            err.contains("merkle proof") || err.contains("source transaction"),
1321            "error should mention missing proofs, got: {}",
1322            err
1323        );
1324    }
1325
1326    #[test]
1327    fn test_to_beef_errors_missing_source_transaction() {
1328        // A transaction with an input that has no source_transaction and no
1329        // merkle_path should produce a clear error.
1330        use crate::transaction::transaction_input::TransactionInput;
1331        use crate::transaction::transaction_output::TransactionOutput;
1332
1333        let mut tx = Transaction::new();
1334        tx.add_input(TransactionInput {
1335            source_transaction: None,
1336            source_txid: Some("aa".repeat(32)),
1337            source_output_index: 0,
1338            unlocking_script: None,
1339            sequence: 0xffffffff,
1340        });
1341        tx.add_output(TransactionOutput {
1342            satoshis: Some(1000),
1343            locking_script: crate::script::locking_script::LockingScript::from_binary(&vec![
1344                0x6a, 0x02, 0xab, 0xcd,
1345            ]),
1346            change: false,
1347        });
1348
1349        let result = tx.to_beef();
1350        assert!(
1351            result.is_err(),
1352            "to_beef should fail with missing source tx"
1353        );
1354        let err = result.unwrap_err().to_string();
1355        assert!(
1356            err.contains("no source transaction"),
1357            "error should mention missing source transaction, got: {}",
1358            err
1359        );
1360    }
1361}