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    /// Compute the transaction hash (double SHA-256).
129    ///
130    /// Returns the hash in internal byte order (LE).
131    pub fn hash(&self) -> Result<[u8; 32], TransactionError> {
132        let bytes = self.to_bytes()?;
133        Ok(hash256(&bytes))
134    }
135
136    /// Compute the transaction ID (hash reversed, hex-encoded).
137    ///
138    /// Returns the txid in display format (BE hex).
139    pub fn id(&self) -> Result<String, TransactionError> {
140        let mut h = self.hash()?;
141        h.reverse();
142        Ok(bytes_to_hex(&h))
143    }
144
145    /// Add an input to the transaction.
146    pub fn add_input(&mut self, input: TransactionInput) {
147        self.inputs.push(input);
148    }
149
150    /// Add an output to the transaction.
151    pub fn add_output(&mut self, output: TransactionOutput) {
152        self.outputs.push(output);
153    }
154
155    /// Deserialize a transaction from EF format (BRC-30).
156    ///
157    /// EF format: version(4) + EF_MARKER(6) + inputs_with_source_info + outputs + locktime(4)
158    /// Each input additionally includes source satoshis (u64 LE) and source locking script.
159    pub fn from_ef(reader: &mut impl Read) -> Result<Self, TransactionError> {
160        let version = read_u32_le(reader)?;
161
162        // Read and verify the 6-byte EF marker
163        let mut marker = [0u8; 6];
164        reader.read_exact(&mut marker)?;
165        if marker != EF_MARKER {
166            return Err(TransactionError::InvalidFormat(
167                "invalid EF marker".to_string(),
168            ));
169        }
170
171        let input_count = read_varint(reader)? as usize;
172        let mut inputs = Vec::with_capacity(input_count);
173        for _ in 0..input_count {
174            // Read standard input fields
175            let mut input = TransactionInput::from_binary(reader)?;
176
177            // Read source satoshis (u64 LE)
178            let source_satoshis = read_u64_le(reader)?;
179
180            // Read source locking script (varint + bytes)
181            let script_len = read_varint(reader)? as usize;
182            let mut script_bytes = vec![0u8; script_len];
183            if script_len > 0 {
184                reader.read_exact(&mut script_bytes)?;
185            }
186            let source_locking_script = LockingScript::from_binary(&script_bytes);
187
188            // Create a minimal source transaction with one output at the referenced index
189            let mut source_tx = Transaction::new();
190            // Pad outputs up to the referenced index
191            for _ in 0..input.source_output_index {
192                source_tx.outputs.push(TransactionOutput::default());
193            }
194            source_tx.outputs.push(TransactionOutput {
195                satoshis: Some(source_satoshis),
196                locking_script: source_locking_script,
197                change: false,
198            });
199            input.source_transaction = Some(Box::new(source_tx));
200
201            inputs.push(input);
202        }
203
204        let output_count = read_varint(reader)? as usize;
205        let mut outputs = Vec::with_capacity(output_count);
206        for _ in 0..output_count {
207            outputs.push(TransactionOutput::from_binary(reader)?);
208        }
209
210        let lock_time = read_u32_le(reader)?;
211
212        Ok(Transaction {
213            version,
214            inputs,
215            outputs,
216            lock_time,
217            merkle_path: None,
218        })
219    }
220
221    /// Deserialize a transaction from an EF format hex string.
222    pub fn from_hex_ef(hex: &str) -> Result<Self, TransactionError> {
223        let bytes = hex_to_bytes(hex)
224            .map_err(|e| TransactionError::InvalidFormat(format!("invalid hex: {}", e)))?;
225        let mut cursor = Cursor::new(bytes);
226        Self::from_ef(&mut cursor)
227    }
228
229    /// Serialize a transaction to EF format (BRC-30).
230    pub fn to_ef(&self, writer: &mut impl Write) -> Result<(), TransactionError> {
231        write_u32_le(writer, self.version)?;
232
233        // Write EF marker
234        writer.write_all(&EF_MARKER)?;
235
236        write_varint(writer, self.inputs.len() as u64)?;
237        for input in &self.inputs {
238            // Write standard input fields
239            input.to_binary(writer)?;
240
241            // Write source satoshis and locking script from source transaction
242            if let Some(ref source_tx) = input.source_transaction {
243                let idx = input.source_output_index as usize;
244                if idx < source_tx.outputs.len() {
245                    let source_output = &source_tx.outputs[idx];
246                    write_u64_le(writer, source_output.satoshis.unwrap_or(0))?;
247                    let script_bin = source_output.locking_script.to_binary();
248                    write_varint(writer, script_bin.len() as u64)?;
249                    writer.write_all(&script_bin)?;
250                } else {
251                    return Err(TransactionError::MissingSourceTransaction);
252                }
253            } else {
254                return Err(TransactionError::MissingSourceTransaction);
255            }
256        }
257
258        write_varint(writer, self.outputs.len() as u64)?;
259        for output in &self.outputs {
260            output.to_binary(writer)?;
261        }
262
263        write_u32_le(writer, self.lock_time)?;
264        Ok(())
265    }
266
267    /// Serialize a transaction to an EF format hex string.
268    pub fn to_hex_ef(&self) -> Result<String, TransactionError> {
269        let mut buf = Vec::new();
270        self.to_ef(&mut buf)?;
271        Ok(bytes_to_hex(&buf))
272    }
273
274    // -- Sighash preimage computation -----------------------------------------
275
276    /// Resolve the txid bytes (internal/LE byte order) for the input at `input_index`.
277    fn resolve_input_txid_bytes(&self, input_index: usize) -> Result<[u8; 32], TransactionError> {
278        let input = &self.inputs[input_index];
279        if let Some(ref txid) = input.source_txid {
280            let mut bytes = hex_to_bytes(txid)
281                .map_err(|e| TransactionError::InvalidFormat(format!("invalid txid hex: {}", e)))?;
282            bytes.reverse(); // display (BE) -> internal (LE)
283            let mut arr = [0u8; 32];
284            if bytes.len() == 32 {
285                arr.copy_from_slice(&bytes);
286            }
287            Ok(arr)
288        } else if let Some(ref source_tx) = input.source_transaction {
289            source_tx.hash()
290        } else {
291            Err(TransactionError::InvalidFormat(
292                "input has neither source_txid nor source_transaction".to_string(),
293            ))
294        }
295    }
296
297    /// Compute the BIP143/ForkID sighash preimage for the input at `input_index`.
298    ///
299    /// This is the standard BSV post-fork sighash format. The `scope` flags
300    /// should include SIGHASH_FORKID for normal BSV transactions.
301    ///
302    /// Parameters:
303    /// - `input_index`: index of the input being signed
304    /// - `scope`: sighash flags (e.g., SIGHASH_ALL | SIGHASH_FORKID)
305    /// - `source_satoshis`: value of the UTXO being spent
306    /// - `source_locking_script`: locking script of the UTXO being spent
307    pub fn sighash_preimage(
308        &self,
309        input_index: usize,
310        scope: u32,
311        source_satoshis: u64,
312        source_locking_script: &LockingScript,
313    ) -> Result<Vec<u8>, TransactionError> {
314        if input_index >= self.inputs.len() {
315            return Err(TransactionError::InvalidSighash(format!(
316                "input_index {} out of range (tx has {} inputs)",
317                input_index,
318                self.inputs.len()
319            )));
320        }
321
322        let base_type = scope & 0x1f;
323        let anyone_can_pay = (scope & SIGHASH_ANYONECANPAY) != 0;
324
325        let mut preimage = Vec::with_capacity(256);
326
327        // 1. nVersion (4 bytes LE)
328        preimage.extend_from_slice(&self.version.to_le_bytes());
329
330        // 2. hashPrevouts
331        if !anyone_can_pay {
332            let mut prevouts = Vec::new();
333            for (i, input) in self.inputs.iter().enumerate() {
334                let txid_bytes = self.resolve_input_txid_bytes(i)?;
335                prevouts.extend_from_slice(&txid_bytes);
336                prevouts.extend_from_slice(&input.source_output_index.to_le_bytes());
337            }
338            preimage.extend_from_slice(&hash256(&prevouts));
339        } else {
340            preimage.extend_from_slice(&[0u8; 32]);
341        }
342
343        // 3. hashSequence
344        if !anyone_can_pay && base_type != SIGHASH_NONE && base_type != SIGHASH_SINGLE {
345            let mut sequences = Vec::new();
346            for input in &self.inputs {
347                sequences.extend_from_slice(&input.sequence.to_le_bytes());
348            }
349            preimage.extend_from_slice(&hash256(&sequences));
350        } else {
351            preimage.extend_from_slice(&[0u8; 32]);
352        }
353
354        // 4. outpoint: this input's txid (LE) + output_index (4 bytes LE)
355        let this_txid = self.resolve_input_txid_bytes(input_index)?;
356        preimage.extend_from_slice(&this_txid);
357        preimage.extend_from_slice(&self.inputs[input_index].source_output_index.to_le_bytes());
358
359        // 5. scriptCode: varint-prefixed source_locking_script bytes
360        let script_bytes = source_locking_script.to_binary();
361        write_varint_to_vec(&mut preimage, script_bytes.len() as u64);
362        preimage.extend_from_slice(&script_bytes);
363
364        // 6. value: source_satoshis (8 bytes LE)
365        preimage.extend_from_slice(&source_satoshis.to_le_bytes());
366
367        // 7. nSequence: this input's sequence (4 bytes LE)
368        preimage.extend_from_slice(&self.inputs[input_index].sequence.to_le_bytes());
369
370        // 8. hashOutputs
371        if base_type != SIGHASH_NONE && base_type != SIGHASH_SINGLE {
372            // ALL: hash of all outputs serialized
373            let mut outputs_data = Vec::new();
374            for output in &self.outputs {
375                outputs_data.extend_from_slice(&output.satoshis.unwrap_or(0).to_le_bytes());
376                let script_bytes = output.locking_script.to_binary();
377                write_varint_to_vec(&mut outputs_data, script_bytes.len() as u64);
378                outputs_data.extend_from_slice(&script_bytes);
379            }
380            preimage.extend_from_slice(&hash256(&outputs_data));
381        } else if base_type == SIGHASH_SINGLE && input_index < self.outputs.len() {
382            // SINGLE: hash of the output at input_index
383            let output = &self.outputs[input_index];
384            let mut out_data = Vec::new();
385            out_data.extend_from_slice(&output.satoshis.unwrap_or(0).to_le_bytes());
386            let script_bytes = output.locking_script.to_binary();
387            write_varint_to_vec(&mut out_data, script_bytes.len() as u64);
388            out_data.extend_from_slice(&script_bytes);
389            preimage.extend_from_slice(&hash256(&out_data));
390        } else {
391            // NONE or SINGLE out-of-range: 32 zero bytes
392            preimage.extend_from_slice(&[0u8; 32]);
393        }
394
395        // 9. nLockTime (4 bytes LE)
396        preimage.extend_from_slice(&self.lock_time.to_le_bytes());
397
398        // 10. sighash type (4 bytes LE) -- scope with FORKID bit
399        preimage.extend_from_slice(&(scope | SIGHASH_FORKID).to_le_bytes());
400
401        Ok(preimage)
402    }
403
404    /// Compute the legacy OTDA sighash preimage for the input at `input_index`.
405    ///
406    /// Used when SIGHASH_FORKID is NOT set (pre-fork transactions or Chronicle mode).
407    /// This is the original Bitcoin sighash algorithm.
408    ///
409    /// The `sub_script` is the scriptCode bytes. OP_CODESEPARATOR opcodes will be
410    /// stripped automatically before inclusion in the preimage.
411    pub fn sighash_preimage_legacy(
412        &self,
413        input_index: usize,
414        scope: u32,
415        sub_script: &[u8],
416    ) -> Result<Vec<u8>, TransactionError> {
417        if input_index >= self.inputs.len() {
418            return Err(TransactionError::InvalidSighash(format!(
419                "input_index {} out of range (tx has {} inputs)",
420                input_index,
421                self.inputs.len()
422            )));
423        }
424
425        // Strip OP_CODESEPARATOR (0xab) opcodes from the script.
426        // Must parse properly to avoid removing 0xab bytes that appear as push data.
427        let sub_script = strip_codeseparator(sub_script);
428
429        let base_type = scope & 0x1f;
430        let anyone_can_pay = (scope & SIGHASH_ANYONECANPAY) != 0;
431        let is_none = base_type == SIGHASH_NONE;
432        let is_single = base_type == SIGHASH_SINGLE;
433
434        // SIGHASH_SINGLE bug: if input_index >= outputs, return [1, 0, 0, ..., 0]
435        if is_single && input_index >= self.outputs.len() {
436            let mut result = vec![0u8; 32];
437            result[0] = 1;
438            return Ok(result);
439        }
440
441        let empty_script: Vec<u8> = Vec::new();
442
443        let mut preimage = Vec::with_capacity(512);
444
445        // Version
446        preimage.extend_from_slice(&self.version.to_le_bytes());
447
448        // Inputs
449        if anyone_can_pay {
450            // Only the current input
451            write_varint_to_vec(&mut preimage, 1);
452            let txid_bytes = self.resolve_input_txid_bytes(input_index)?;
453            preimage.extend_from_slice(&txid_bytes);
454            preimage.extend_from_slice(&self.inputs[input_index].source_output_index.to_le_bytes());
455            write_varint_to_vec(&mut preimage, sub_script.len() as u64);
456            preimage.extend_from_slice(&sub_script);
457            preimage.extend_from_slice(&self.inputs[input_index].sequence.to_le_bytes());
458        } else {
459            write_varint_to_vec(&mut preimage, self.inputs.len() as u64);
460            for (i, input) in self.inputs.iter().enumerate() {
461                let txid_bytes = self.resolve_input_txid_bytes(i)?;
462                preimage.extend_from_slice(&txid_bytes);
463                preimage.extend_from_slice(&input.source_output_index.to_le_bytes());
464
465                // Script: only include sub_script for the input being signed
466                if i == input_index {
467                    write_varint_to_vec(&mut preimage, sub_script.len() as u64);
468                    preimage.extend_from_slice(&sub_script);
469                } else {
470                    write_varint_to_vec(&mut preimage, empty_script.len() as u64);
471                }
472
473                // Sequence: for SINGLE and NONE, zero out other inputs' sequences
474                if i == input_index || (!is_single && !is_none) {
475                    preimage.extend_from_slice(&input.sequence.to_le_bytes());
476                } else {
477                    preimage.extend_from_slice(&0u32.to_le_bytes());
478                }
479            }
480        }
481
482        // Outputs
483        if is_none {
484            write_varint_to_vec(&mut preimage, 0);
485        } else if is_single {
486            write_varint_to_vec(&mut preimage, (input_index + 1) as u64);
487            for i in 0..input_index {
488                // Blank outputs before the matching one: satoshis = -1 (0xFFFFFFFFFFFFFFFF), empty script
489                preimage.extend_from_slice(&u64::MAX.to_le_bytes());
490                write_varint_to_vec(&mut preimage, 0);
491                let _ = i;
492            }
493            // The output at input_index
494            let output = &self.outputs[input_index];
495            preimage.extend_from_slice(&output.satoshis.unwrap_or(0).to_le_bytes());
496            let script_bytes = output.locking_script.to_binary();
497            write_varint_to_vec(&mut preimage, script_bytes.len() as u64);
498            preimage.extend_from_slice(&script_bytes);
499        } else {
500            // ALL: serialize all outputs
501            write_varint_to_vec(&mut preimage, self.outputs.len() as u64);
502            for output in &self.outputs {
503                preimage.extend_from_slice(&output.satoshis.unwrap_or(0).to_le_bytes());
504                let script_bytes = output.locking_script.to_binary();
505                write_varint_to_vec(&mut preimage, script_bytes.len() as u64);
506                preimage.extend_from_slice(&script_bytes);
507            }
508        }
509
510        // Locktime
511        preimage.extend_from_slice(&self.lock_time.to_le_bytes());
512
513        // Sighash type (4 bytes LE)
514        preimage.extend_from_slice(&scope.to_le_bytes());
515
516        Ok(preimage)
517    }
518
519    // -- Transaction signing --------------------------------------------------
520
521    /// Sign the input at `input_index` using a ScriptTemplateUnlock implementation.
522    ///
523    /// Computes the sighash preimage (BIP143/ForkID format) and passes it to the
524    /// template's sign() method, then sets the resulting unlocking script on the input.
525    pub fn sign(
526        &mut self,
527        input_index: usize,
528        template: &dyn ScriptTemplateUnlock,
529        scope: u32,
530        source_satoshis: u64,
531        source_locking_script: &LockingScript,
532    ) -> Result<(), TransactionError> {
533        let preimage =
534            self.sighash_preimage(input_index, scope, source_satoshis, source_locking_script)?;
535        let unlocking_script = template
536            .sign(&preimage)
537            .map_err(|e| TransactionError::SigningFailed(format!("{}", e)))?;
538        self.inputs[input_index].unlocking_script = Some(unlocking_script);
539        Ok(())
540    }
541
542    /// Sign all unsigned inputs using the same template.
543    ///
544    /// A convenience method that reduces the per-input signing loop. For each
545    /// input that has no `unlocking_script` yet, this resolves `source_satoshis`
546    /// and `source_locking_script` from the input's `source_transaction` and
547    /// signs with the given template and sighash scope.
548    ///
549    /// Inputs that already have an unlocking script are skipped.
550    ///
551    /// Each input must have its `source_transaction` set so that the source
552    /// output's satoshis and locking script can be resolved. If you need
553    /// different templates or scopes per input, use the single-input `sign()`.
554    pub fn sign_all_inputs(
555        &mut self,
556        template: &dyn ScriptTemplateUnlock,
557        scope: u32,
558    ) -> Result<(), TransactionError> {
559        let num_inputs = self.inputs.len();
560
561        for i in 0..num_inputs {
562            // Skip inputs that already have an unlocking script
563            if self.inputs[i].unlocking_script.is_some() {
564                continue;
565            }
566
567            // Resolve source satoshis and locking script from source_transaction
568            let (source_satoshis, source_locking_script) = {
569                let source_tx = self.inputs[i].source_transaction.as_ref().ok_or_else(|| {
570                    TransactionError::SigningFailed(format!(
571                        "input {}: source_transaction required for sign_all_inputs()",
572                        i
573                    ))
574                })?;
575                let out_idx = self.inputs[i].source_output_index as usize;
576                let output = source_tx.outputs.get(out_idx).ok_or_else(|| {
577                    TransactionError::SigningFailed(format!(
578                        "input {}: source transaction has no output at index {}",
579                        i, out_idx
580                    ))
581                })?;
582                let satoshis = output.satoshis.ok_or_else(|| {
583                    TransactionError::SigningFailed(format!(
584                        "input {}: source output {} has no satoshis",
585                        i, out_idx
586                    ))
587                })?;
588                (satoshis, output.locking_script.clone())
589            };
590
591            let preimage =
592                self.sighash_preimage(i, scope, source_satoshis, &source_locking_script)?;
593            let unlocking_script = template
594                .sign(&preimage)
595                .map_err(|e| TransactionError::SigningFailed(format!("input {}: {}", i, e)))?;
596            self.inputs[i].unlocking_script = Some(unlocking_script);
597        }
598
599        Ok(())
600    }
601}
602
603impl Default for Transaction {
604    fn default() -> Self {
605        Self::new()
606    }
607}
608
609/// Convert a byte slice to a lowercase hex string.
610fn bytes_to_hex(bytes: &[u8]) -> String {
611    let mut s = String::with_capacity(bytes.len() * 2);
612    for b in bytes {
613        s.push_str(&format!("{:02x}", b));
614    }
615    s
616}
617
618/// Strip OP_CODESEPARATOR (0xab) opcodes from a raw script byte array.
619///
620/// Properly parses the script to avoid removing 0xab bytes that appear
621/// as data within push operations.
622fn strip_codeseparator(script: &[u8]) -> Vec<u8> {
623    const OP_CODESEPARATOR: u8 = 0xab;
624
625    let mut result = Vec::with_capacity(script.len());
626    let mut i = 0;
627    while i < script.len() {
628        let opcode = script[i];
629        if opcode == OP_CODESEPARATOR {
630            // Skip this opcode
631            i += 1;
632            continue;
633        }
634
635        if opcode > 0 && opcode < 76 {
636            // Direct push: opcode is the number of bytes to push
637            let push_len = opcode as usize;
638            let end = std::cmp::min(i + 1 + push_len, script.len());
639            result.extend_from_slice(&script[i..end]);
640            i = end;
641        } else if opcode == 76 {
642            // OP_PUSHDATA1: next byte is length
643            if i + 1 < script.len() {
644                let push_len = script[i + 1] as usize;
645                let end = std::cmp::min(i + 2 + push_len, script.len());
646                result.extend_from_slice(&script[i..end]);
647                i = end;
648            } else {
649                result.push(opcode);
650                i += 1;
651            }
652        } else if opcode == 77 {
653            // OP_PUSHDATA2: next 2 bytes are length (LE)
654            if i + 2 < script.len() {
655                let push_len = u16::from_le_bytes([script[i + 1], script[i + 2]]) as usize;
656                let end = std::cmp::min(i + 3 + push_len, script.len());
657                result.extend_from_slice(&script[i..end]);
658                i = end;
659            } else {
660                result.extend_from_slice(&script[i..]);
661                break;
662            }
663        } else if opcode == 78 {
664            // OP_PUSHDATA4: next 4 bytes are length (LE)
665            if i + 4 < script.len() {
666                let push_len = u32::from_le_bytes([
667                    script[i + 1],
668                    script[i + 2],
669                    script[i + 3],
670                    script[i + 4],
671                ]) as usize;
672                let end = std::cmp::min(i + 5 + push_len, script.len());
673                result.extend_from_slice(&script[i..end]);
674                i = end;
675            } else {
676                result.extend_from_slice(&script[i..]);
677                break;
678            }
679        } else {
680            // Regular opcode (0x00, or 0x4f..0xff except 0xab)
681            result.push(opcode);
682            i += 1;
683        }
684    }
685    result
686}
687
688/// Write a Bitcoin-style varint directly to a Vec<u8> (no io::Write needed).
689fn write_varint_to_vec(buf: &mut Vec<u8>, val: u64) {
690    if val < 0xfd {
691        buf.push(val as u8);
692    } else if val <= 0xffff {
693        buf.push(0xfd);
694        buf.extend_from_slice(&(val as u16).to_le_bytes());
695    } else if val <= 0xffff_ffff {
696        buf.push(0xfe);
697        buf.extend_from_slice(&(val as u32).to_le_bytes());
698    } else {
699        buf.push(0xff);
700        buf.extend_from_slice(&val.to_le_bytes());
701    }
702}
703
704/// Convert a hex string to bytes.
705fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, String> {
706    if !hex.len().is_multiple_of(2) {
707        return Err("odd length hex string".to_string());
708    }
709    let mut bytes = Vec::with_capacity(hex.len() / 2);
710    for i in (0..hex.len()).step_by(2) {
711        let byte = u8::from_str_radix(&hex[i..i + 2], 16)
712            .map_err(|e| format!("invalid hex at position {}: {}", i, e))?;
713        bytes.push(byte);
714    }
715    Ok(bytes)
716}
717
718#[cfg(test)]
719mod tests {
720    use super::*;
721    use crate::primitives::private_key::PrivateKey;
722    use crate::primitives::transaction_signature::{SIGHASH_ALL, SIGHASH_FORKID};
723    use crate::script::templates::p2pkh::P2PKH;
724    use crate::script::templates::ScriptTemplateLock;
725    use serde::Deserialize;
726
727    #[derive(Deserialize)]
728    struct TestVector {
729        description: String,
730        hex: String,
731        txid: String,
732        version: u32,
733        inputs: usize,
734        outputs: usize,
735        locktime: u32,
736    }
737
738    fn load_test_vectors() -> Vec<TestVector> {
739        let json = include_str!("../../test-vectors/transaction_valid.json");
740        serde_json::from_str(json).expect("failed to parse transaction_valid.json")
741    }
742
743    #[test]
744    fn test_from_binary_round_trip() {
745        let vectors = load_test_vectors();
746        for v in &vectors {
747            let tx = Transaction::from_hex(&v.hex)
748                .unwrap_or_else(|e| panic!("failed to parse '{}': {}", v.description, e));
749            let result_hex = tx
750                .to_hex()
751                .unwrap_or_else(|e| panic!("failed to serialize '{}': {}", v.description, e));
752            assert_eq!(
753                result_hex, v.hex,
754                "round-trip failed for '{}'",
755                v.description
756            );
757        }
758    }
759
760    #[test]
761    fn test_txid() {
762        let vectors = load_test_vectors();
763        for v in &vectors {
764            let tx = Transaction::from_hex(&v.hex)
765                .unwrap_or_else(|e| panic!("failed to parse '{}': {}", v.description, e));
766            let txid = tx
767                .id()
768                .unwrap_or_else(|e| panic!("failed to compute id for '{}': {}", v.description, e));
769            assert_eq!(txid, v.txid, "txid mismatch for '{}'", v.description);
770        }
771    }
772
773    #[test]
774    fn test_input_output_counts() {
775        let vectors = load_test_vectors();
776        for v in &vectors {
777            let tx = Transaction::from_hex(&v.hex)
778                .unwrap_or_else(|e| panic!("failed to parse '{}': {}", v.description, e));
779            assert_eq!(
780                tx.inputs.len(),
781                v.inputs,
782                "input count mismatch for '{}'",
783                v.description
784            );
785            assert_eq!(
786                tx.outputs.len(),
787                v.outputs,
788                "output count mismatch for '{}'",
789                v.description
790            );
791            assert_eq!(
792                tx.version, v.version,
793                "version mismatch for '{}'",
794                v.description
795            );
796            assert_eq!(
797                tx.lock_time, v.locktime,
798                "locktime mismatch for '{}'",
799                v.description
800            );
801        }
802    }
803
804    #[test]
805    fn test_empty_transaction() {
806        let tx = Transaction::new();
807        assert_eq!(tx.version, 1);
808        assert!(tx.inputs.is_empty());
809        assert!(tx.outputs.is_empty());
810        assert_eq!(tx.lock_time, 0);
811        assert!(tx.merkle_path.is_none());
812    }
813
814    #[test]
815    fn test_add_input_output() {
816        let mut tx = Transaction::new();
817        assert_eq!(tx.inputs.len(), 0);
818        assert_eq!(tx.outputs.len(), 0);
819
820        tx.add_input(TransactionInput::default());
821        assert_eq!(tx.inputs.len(), 1);
822
823        tx.add_output(TransactionOutput::default());
824        assert_eq!(tx.outputs.len(), 1);
825    }
826
827    #[test]
828    fn test_ef_round_trip() {
829        // EF format vector from TS SDK test
830        let ef_hex = "010000000000000000ef01ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e000000006a47304402203a61a2e931612b4bda08d541cfb980885173b8dcf64a3471238ae7abcd368d6402204cbf24f04b9aa2256d8901f0ed97866603d2be8324c2bfb7a37bf8fc90edd5b441210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff3e660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac013c660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac00000000";
831
832        let tx = Transaction::from_hex_ef(ef_hex).expect("failed to parse EF hex");
833        assert_eq!(tx.inputs.len(), 1);
834        assert_eq!(tx.outputs.len(), 1);
835
836        // Verify source transaction info was captured
837        let input = &tx.inputs[0];
838        assert!(input.source_transaction.is_some());
839        let source_tx = input.source_transaction.as_ref().unwrap();
840        let source_output = &source_tx.outputs[input.source_output_index as usize];
841        assert_eq!(source_output.satoshis, Some(0x663e)); // 26174 satoshis
842
843        // Round-trip: serialize back to EF hex
844        let result_hex = tx.to_hex_ef().expect("failed to serialize to EF");
845        assert_eq!(result_hex, ef_hex);
846    }
847
848    #[test]
849    fn test_hash_and_id_consistency() {
850        // tx2 from the TS SDK test
851        let tx2hex = "01000000029e8d016a7b0dc49a325922d05da1f916d1e4d4f0cb840c9727f3d22ce8d1363f000000008c493046022100e9318720bee5425378b4763b0427158b1051eec8b08442ce3fbfbf7b30202a44022100d4172239ebd701dae2fbaaccd9f038e7ca166707333427e3fb2a2865b19a7f27014104510c67f46d2cbb29476d1f0b794be4cb549ea59ab9cc1e731969a7bf5be95f7ad5e7f904e5ccf50a9dc1714df00fbeb794aa27aaff33260c1032d931a75c56f2ffffffffa3195e7a1ab665473ff717814f6881485dc8759bebe97e31c301ffe7933a656f020000008b48304502201c282f35f3e02a1f32d2089265ad4b561f07ea3c288169dedcf2f785e6065efa022100e8db18aadacb382eed13ee04708f00ba0a9c40e3b21cf91da8859d0f7d99e0c50141042b409e1ebbb43875be5edde9c452c82c01e3903d38fa4fd89f3887a52cb8aea9dc8aec7e2c9d5b3609c03eb16259a2537135a1bf0f9c5fbbcbdbaf83ba402442ffffffff02206b1000000000001976a91420bb5c3bfaef0231dc05190e7f1c8e22e098991e88acf0ca0100000000001976a9149e3e2d23973a04ec1b02be97c30ab9f2f27c3b2c88ac00000000";
852        let tx2idhex = "8c9aa966d35bfeaf031409e0001b90ccdafd8d859799eb945a3c515b8260bcf2";
853
854        let tx = Transaction::from_hex(tx2hex).unwrap();
855        let id = tx.id().unwrap();
856        assert_eq!(id, tx2idhex);
857
858        // Verify hash is the reverse of id
859        let hash = tx.hash().unwrap();
860        let mut reversed_hash = hash;
861        reversed_hash.reverse();
862        let reversed_hex = bytes_to_hex(&reversed_hash);
863        assert_eq!(reversed_hex, tx2idhex);
864    }
865
866    // -- Sighash preimage tests -----------------------------------------------
867
868    /// OTDA sighash test vectors from TS SDK sighashTestData.ts
869    /// Format: (raw_tx_hex, script_hex, input_index, hash_type_unsigned, expected_otda_hash_display)
870    fn otda_test_vectors() -> Vec<(&'static str, &'static str, usize, u32, &'static str)> {
871        vec![
872            ("0122769903cfc6fedb9c63fe76930fed0c87b44be46f5032a534fa05861548616a5b99034701000000040063656ac864c70228b3f6ebaf97be065b2180ece52fae4f9039c7bf932e567625d7d89582abe1340100000008ac6363650063ab65ffffffff587125b913706705dc799454ab0343ee8aa59a2f2d538f75e12c0deeb40aa741030000000027b3a02003d21f34040000000000fa7f2c0300000000016387213b0300000000056aabacacab00000000", "", 1, 902085315, "49fdc84c5f88a590c5c65e17de58da7d1028133b74ad2d56a8b15ba317ac98f6"),
873            ("7e8c3f7902634018b6e1db2ca591816dff64a6cff74643de7455323ebfc560500aad9eee8c0100000001519c2d146d06fdca2c2e5fa3a2559812df66b6b5e40a3c57b2f7071ae6fe3863c74ab0952d0100000001000bf6638c013c5f6503000000000351ac63cbbf66be", "acab", 0, 2433331782, "f6261bacaed3a70d504cd70d3c0623e3593f6d197cd47316e56cea79ceabe095"),
874            ("459499bb032fdcc39d3c6cf819dcaa0a0165d97578446aa87ab745fb9fdcd3e6177b4cba3d0000000005006a6a5265ffffffff10e5929ebe065273c112cab15f6a1f6d9a8a517c288311b048b16663b3d406dc030000000700535263655151ffffffff981d73a7f3d477ab055398bcf9a7d349db1a8e6362055e20f4207ad1b775bac301000000066a6363ac6552ffffffff0403342603000000000165c4390004000000000965ac52006565006365373ce8010000000005520000516aba5a9404000000000351655300000000", "6a5352", 0, 3544391288, "738b7dcb86260e6fe3fad331ff342429c157730bbcb90c205b9e08568557cd94"),
875            ("cb3b8d30043ccd81c3bda7f594cca60e2eef170c67ffe8a1eb1f1a994dc40a0a5cf89fa9690100000009ab536a52acab5300653bea9324983da711ccb6eaff060930e6f55cf6df75e5abdda91a8d5fc25c3b9b28d0e7370200000003ab51522f86cdbd8aa19b6b8536efb6ca8cc23ebccef585ad00a78b5956d803908482bb44b25c550000000007abab65530051ac8177a2acebc517db1d5b5be14f91ab40e811ec0316cf029ce657a4b06f04f30698f0a0e50000000007516aac636a6351ffffffff02ccbefa02000000000252ab3e297f0100000000060052525163521acc3e2b", "6aac636a63535153", 0, 3406487088, "3568dfad7e968afd3492bd146c8b0e3255f90e5b642a4ec10105693e8b029132"),
876        ]
877    }
878
879    #[test]
880    fn test_sighash_preimage_legacy_vectors() {
881        let vectors = otda_test_vectors();
882        let mut passed = 0;
883
884        for (i, (raw_tx_hex, script_hex, input_index, hash_type, expected_hash)) in
885            vectors.iter().enumerate()
886        {
887            let tx = Transaction::from_hex(raw_tx_hex)
888                .unwrap_or_else(|e| panic!("vector {}: failed to parse tx: {}", i, e));
889
890            let sub_script = if script_hex.is_empty() {
891                vec![]
892            } else {
893                hex_to_bytes(script_hex).unwrap()
894            };
895
896            let preimage = tx
897                .sighash_preimage_legacy(*input_index, *hash_type, &sub_script)
898                .unwrap_or_else(|e| panic!("vector {}: sighash error: {}", i, e));
899
900            // The expected hash is in display (BE/reversed) format
901            let mut hash_bytes = hash256(&preimage);
902            hash_bytes.reverse();
903            let computed_hash = bytes_to_hex(&hash_bytes);
904
905            if computed_hash == *expected_hash {
906                passed += 1;
907            } else {
908                println!(
909                    "MISMATCH vector {}: expected={}, got={}",
910                    i, expected_hash, computed_hash
911                );
912            }
913        }
914
915        println!(
916            "sighash legacy OTDA vectors: {}/{} passed",
917            passed,
918            vectors.len()
919        );
920        assert_eq!(
921            passed,
922            vectors.len(),
923            "all sighash OTDA vectors should pass"
924        );
925    }
926
927    #[test]
928    fn test_sighash_preimage_bip143() {
929        // Test vector from Go SDK: "1 Input 2 Outputs - SIGHASH_ALL (FORKID)"
930        let unsigned_tx_hex = "010000000193a35408b6068499e0d5abd799d3e827d9bfe70c9b75ebe209c91d25072326510000000000ffffffff02404b4c00000000001976a91404ff367be719efa79d76e4416ffb072cd53b208888acde94a905000000001976a91404d03f746652cfcb6cb55119ab473a045137d26588ac00000000";
931        let source_script_hex = "76a914c0a3c167a28cabb9fbb495affa0761e6e74ac60d88ac";
932        let source_satoshis: u64 = 100_000_000;
933        let expected_preimage_hex = "010000007ced5b2e5cf3ea407b005d8b18c393b6256ea2429b6ff409983e10adc61d0ae83bb13029ce7b1f559ef5e747fcac439f1455a2ec7c5f09b72290795e7066504493a35408b6068499e0d5abd799d3e827d9bfe70c9b75ebe209c91d2507232651000000001976a914c0a3c167a28cabb9fbb495affa0761e6e74ac60d88ac00e1f50500000000ffffffff87841ab2b7a4133af2c58256edb7c3c9edca765a852ebe2d0dc962604a30f1030000000041000000";
934
935        let tx = Transaction::from_hex(unsigned_tx_hex).unwrap();
936        let source_script_bytes = hex_to_bytes(source_script_hex).unwrap();
937        let source_locking_script = LockingScript::from_binary(&source_script_bytes);
938
939        let scope = SIGHASH_ALL | SIGHASH_FORKID;
940        let preimage = tx
941            .sighash_preimage(0, scope, source_satoshis, &source_locking_script)
942            .unwrap();
943        let preimage_hex = bytes_to_hex(&preimage);
944
945        assert_eq!(
946            preimage_hex, expected_preimage_hex,
947            "BIP143 preimage should match Go SDK test vector"
948        );
949    }
950
951    // -- Transaction signing tests --------------------------------------------
952
953    #[test]
954    fn test_sign_p2pkh() {
955        let key = PrivateKey::from_hex("1").unwrap();
956        let p2pkh_lock = P2PKH::from_private_key(key.clone());
957        let p2pkh_unlock = P2PKH::from_private_key(key.clone());
958
959        let lock_script = p2pkh_lock.lock().unwrap();
960
961        // Build a transaction with one input and one output
962        let mut tx = Transaction::new();
963        tx.add_input(TransactionInput {
964            source_transaction: None,
965            source_txid: Some("00".repeat(32)),
966            source_output_index: 0,
967            unlocking_script: None,
968            sequence: 0xffffffff,
969        });
970        tx.add_output(TransactionOutput {
971            satoshis: Some(50000),
972            locking_script: lock_script.clone(),
973            change: false,
974        });
975
976        // Sign the input
977        let scope = SIGHASH_ALL | SIGHASH_FORKID;
978        tx.sign(0, &p2pkh_unlock, scope, 100000, &lock_script)
979            .expect("signing should succeed");
980
981        // Verify unlocking script is set
982        let unlock = tx.inputs[0].unlocking_script.as_ref().unwrap();
983        let chunks = unlock.chunks();
984        assert_eq!(
985            chunks.len(),
986            2,
987            "P2PKH unlock should have 2 chunks (sig + pubkey)"
988        );
989
990        // First chunk: signature (DER + sighash byte)
991        let sig_data = chunks[0].data.as_ref().unwrap();
992        assert!(
993            sig_data.len() >= 70 && sig_data.len() <= 74,
994            "signature length {} should be 70-74",
995            sig_data.len()
996        );
997        assert_eq!(
998            *sig_data.last().unwrap(),
999            (SIGHASH_ALL | SIGHASH_FORKID) as u8,
1000            "last byte should be sighash type"
1001        );
1002
1003        // Second chunk: compressed public key (33 bytes)
1004        let pubkey_data = chunks[1].data.as_ref().unwrap();
1005        assert_eq!(pubkey_data.len(), 33);
1006    }
1007
1008    #[test]
1009    fn test_sign_and_verify_round_trip() {
1010        use crate::script::spend::{Spend, SpendParams};
1011
1012        let key = PrivateKey::from_hex("abcdef01").unwrap();
1013        let p2pkh = P2PKH::from_private_key(key.clone());
1014        let lock_script = p2pkh.lock().unwrap();
1015
1016        // Build and sign a transaction
1017        let mut tx = Transaction::new();
1018        let source_satoshis = 100_000u64;
1019
1020        tx.add_input(TransactionInput {
1021            source_transaction: None,
1022            source_txid: Some("aa".repeat(32)),
1023            source_output_index: 0,
1024            unlocking_script: None,
1025            sequence: 0xffffffff,
1026        });
1027        tx.add_output(TransactionOutput {
1028            satoshis: Some(90_000),
1029            locking_script: lock_script.clone(),
1030            change: false,
1031        });
1032
1033        let scope = SIGHASH_ALL | SIGHASH_FORKID;
1034        tx.sign(0, &p2pkh, scope, source_satoshis, &lock_script)
1035            .expect("signing should succeed");
1036
1037        // Now verify with Spend
1038        let unlock_script = tx.inputs[0].unlocking_script.clone().unwrap();
1039
1040        let mut spend = Spend::new(SpendParams {
1041            locking_script: lock_script.clone(),
1042            unlocking_script: unlock_script,
1043            source_txid: "aa".repeat(32),
1044            source_output_index: 0,
1045            source_satoshis,
1046            transaction_version: tx.version,
1047            transaction_lock_time: tx.lock_time,
1048            transaction_sequence: tx.inputs[0].sequence,
1049            other_inputs: vec![],
1050            other_outputs: tx.outputs.clone(),
1051            input_index: 0,
1052        });
1053
1054        let valid = spend.validate().expect("spend validation should not error");
1055        assert!(valid, "signed transaction should verify successfully");
1056    }
1057}