Skip to main content

blvm_primitives/serialization/
transaction.rs

1//! Transaction wire format serialization/deserialization
2//!
3//! Bitcoin transaction wire format specification.
4//! Must match Bitcoin protocol serialization exactly for consensus compatibility.
5
6use super::varint::{decode_varint, encode_varint};
7use crate::error::{ConsensusError, Result};
8use crate::types::*;
9use std::borrow::Cow;
10
11#[cfg(feature = "production")]
12use smallvec::SmallVec;
13
14/// Error type for transaction parsing failures
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum TransactionParseError {
17    InsufficientBytes,
18    InvalidVersion,
19    InvalidInputCount,
20    InvalidOutputCount,
21    InvalidScriptLength,
22    InvalidLockTime,
23}
24
25impl std::fmt::Display for TransactionParseError {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        match self {
28            TransactionParseError::InsufficientBytes => {
29                write!(f, "Insufficient bytes to parse transaction")
30            }
31            TransactionParseError::InvalidVersion => write!(f, "Invalid transaction version"),
32            TransactionParseError::InvalidInputCount => write!(f, "Invalid input count"),
33            TransactionParseError::InvalidOutputCount => write!(f, "Invalid output count"),
34            TransactionParseError::InvalidScriptLength => write!(f, "Invalid script length"),
35            TransactionParseError::InvalidLockTime => write!(f, "Invalid lock time"),
36        }
37    }
38}
39
40impl std::error::Error for TransactionParseError {}
41
42/// Serialize a transaction to Bitcoin wire format
43#[inline(always)]
44pub fn serialize_transaction(tx: &Transaction) -> Vec<u8> {
45    let mut result = Vec::new();
46    serialize_transaction_append(&mut result, tx);
47    result
48}
49
50/// Append serialized transaction to buffer (shared logic for into/inner).
51#[inline(always)]
52fn serialize_transaction_append(result: &mut Vec<u8>, tx: &Transaction) {
53    result.extend_from_slice(&(tx.version as i32).to_le_bytes());
54    result.extend_from_slice(&encode_varint(tx.inputs.len() as u64));
55
56    for input in &tx.inputs {
57        result.extend_from_slice(&input.prevout.hash);
58        result.extend_from_slice(&input.prevout.index.to_le_bytes());
59        result.extend_from_slice(&encode_varint(input.script_sig.len() as u64));
60        result.extend_from_slice(&input.script_sig);
61        result.extend_from_slice(&(input.sequence as u32).to_le_bytes());
62    }
63
64    result.extend_from_slice(&encode_varint(tx.outputs.len() as u64));
65
66    for output in &tx.outputs {
67        result.extend_from_slice(&(output.value as u64).to_le_bytes());
68        result.extend_from_slice(&encode_varint(output.script_pubkey.len() as u64));
69        result.extend_from_slice(&output.script_pubkey);
70    }
71
72    result.extend_from_slice(&(tx.lock_time as u32).to_le_bytes());
73}
74
75/// Serialize transaction into an existing buffer
76#[inline(always)]
77pub fn serialize_transaction_into(dst: &mut Vec<u8>, tx: &Transaction) -> usize {
78    dst.clear();
79    serialize_transaction_append(dst, tx);
80    dst.len()
81}
82
83/// Serialize a transaction in SegWit wire format
84pub fn serialize_transaction_with_witness(tx: &Transaction, witnesses: &[Witness]) -> Vec<u8> {
85    assert_eq!(
86        witnesses.len(),
87        tx.inputs.len(),
88        "witness count must match input count"
89    );
90    let mut result = Vec::new();
91    result.extend_from_slice(&(tx.version as i32).to_le_bytes());
92    result.push(0x00);
93    result.push(0x01);
94    result.extend_from_slice(&encode_varint(tx.inputs.len() as u64));
95    for input in &tx.inputs {
96        result.extend_from_slice(&input.prevout.hash);
97        result.extend_from_slice(&input.prevout.index.to_le_bytes());
98        result.extend_from_slice(&encode_varint(input.script_sig.len() as u64));
99        result.extend_from_slice(&input.script_sig);
100        result.extend_from_slice(&(input.sequence as u32).to_le_bytes());
101    }
102    result.extend_from_slice(&encode_varint(tx.outputs.len() as u64));
103    for output in &tx.outputs {
104        result.extend_from_slice(&(output.value as u64).to_le_bytes());
105        result.extend_from_slice(&encode_varint(output.script_pubkey.len() as u64));
106        result.extend_from_slice(&output.script_pubkey);
107    }
108    for witness in witnesses {
109        result.extend_from_slice(&encode_varint(witness.len() as u64));
110        for element in witness {
111            result.extend_from_slice(&encode_varint(element.len() as u64));
112            result.extend_from_slice(element);
113        }
114    }
115    result.extend_from_slice(&(tx.lock_time as u32).to_le_bytes());
116    result
117}
118
119/// Deserialize a transaction from Bitcoin wire format
120pub fn deserialize_transaction(data: &[u8]) -> Result<Transaction> {
121    let mut offset = 0;
122
123    if data.len() < offset + 4 {
124        return Err(ConsensusError::Serialization(Cow::Owned(
125            TransactionParseError::InsufficientBytes.to_string(),
126        )));
127    }
128    let version = i32::from_le_bytes([
129        data[offset],
130        data[offset + 1],
131        data[offset + 2],
132        data[offset + 3],
133    ]) as u64;
134    offset += 4;
135
136    let is_segwit = data.len() >= offset + 2 && data[offset] == 0x00 && data[offset + 1] == 0x01;
137
138    if is_segwit {
139        offset += 2;
140    }
141
142    let (input_count, varint_len) = decode_varint(&data[offset..])?;
143    offset += varint_len;
144
145    if input_count > 1000000 {
146        return Err(ConsensusError::Serialization(Cow::Owned(
147            TransactionParseError::InvalidInputCount.to_string(),
148        )));
149    }
150
151    #[cfg(feature = "production")]
152    let mut inputs = SmallVec::<[TransactionInput; 2]>::new();
153    #[cfg(not(feature = "production"))]
154    let mut inputs = Vec::new();
155
156    for _ in 0..input_count {
157        if data.len() < offset + 36 {
158            return Err(ConsensusError::Serialization(Cow::Owned(
159                TransactionParseError::InsufficientBytes.to_string(),
160            )));
161        }
162        let mut hash = [0u8; 32];
163        hash.copy_from_slice(&data[offset..offset + 32]);
164        offset += 32;
165
166        let index = u32::from_le_bytes([
167            data[offset],
168            data[offset + 1],
169            data[offset + 2],
170            data[offset + 3],
171        ]);
172        offset += 4;
173
174        let (script_len, varint_len) = decode_varint(&data[offset..])?;
175        offset += varint_len;
176
177        if data.len() < offset + script_len as usize {
178            return Err(ConsensusError::Serialization(Cow::Owned(
179                TransactionParseError::InsufficientBytes.to_string(),
180            )));
181        }
182        let script_sig = data[offset..offset + script_len as usize].to_vec();
183        offset += script_len as usize;
184
185        let sequence = u32::from_le_bytes([
186            data[offset],
187            data[offset + 1],
188            data[offset + 2],
189            data[offset + 3],
190        ]) as u64;
191        offset += 4;
192
193        inputs.push(TransactionInput {
194            prevout: OutPoint { hash, index },
195            script_sig,
196            sequence,
197        });
198    }
199
200    let (output_count, varint_len) = decode_varint(&data[offset..])?;
201    offset += varint_len;
202
203    if output_count > 1000000 {
204        return Err(ConsensusError::Serialization(Cow::Owned(
205            TransactionParseError::InvalidOutputCount.to_string(),
206        )));
207    }
208
209    #[cfg(feature = "production")]
210    let mut outputs = SmallVec::<[TransactionOutput; 2]>::new();
211    #[cfg(not(feature = "production"))]
212    let mut outputs = Vec::new();
213
214    for _ in 0..output_count {
215        if data.len() < offset + 8 {
216            return Err(ConsensusError::Serialization(Cow::Owned(
217                TransactionParseError::InsufficientBytes.to_string(),
218            )));
219        }
220        let value = i64::from_le_bytes([
221            data[offset],
222            data[offset + 1],
223            data[offset + 2],
224            data[offset + 3],
225            data[offset + 4],
226            data[offset + 5],
227            data[offset + 6],
228            data[offset + 7],
229        ]);
230        offset += 8;
231
232        let (script_len, varint_len) = decode_varint(&data[offset..])?;
233        offset += varint_len;
234
235        if data.len() < offset + script_len as usize {
236            return Err(ConsensusError::Serialization(Cow::Owned(
237                TransactionParseError::InsufficientBytes.to_string(),
238            )));
239        }
240        let script_pubkey = data[offset..offset + script_len as usize].to_vec();
241        offset += script_len as usize;
242
243        outputs.push(TransactionOutput {
244            value,
245            script_pubkey,
246        });
247    }
248
249    if is_segwit {
250        for _ in 0..input_count {
251            let (stack_count, varint_len) = decode_varint(&data[offset..])?;
252            offset += varint_len;
253            for _ in 0..stack_count {
254                let (item_len, varint_len) = decode_varint(&data[offset..])?;
255                offset += varint_len;
256                if data.len() < offset + item_len as usize {
257                    return Err(ConsensusError::Serialization(Cow::Owned(
258                        TransactionParseError::InsufficientBytes.to_string(),
259                    )));
260                }
261                offset += item_len as usize;
262            }
263        }
264    }
265
266    if data.len() < offset + 4 {
267        return Err(ConsensusError::Serialization(Cow::Owned(
268            TransactionParseError::InsufficientBytes.to_string(),
269        )));
270    }
271    let lock_time = u32::from_le_bytes([
272        data[offset],
273        data[offset + 1],
274        data[offset + 2],
275        data[offset + 3],
276    ]) as u64;
277
278    Ok(Transaction {
279        version,
280        inputs,
281        outputs,
282        lock_time,
283    })
284}
285
286/// Deserialize a transaction, returning (tx, bytes_consumed). Convenience wrapper that discards witness data.
287pub fn deserialize_transaction_with_offset(data: &[u8]) -> Result<(Transaction, usize)> {
288    let (tx, _witnesses, bytes_consumed) = deserialize_transaction_with_witness(data)?;
289    Ok((tx, bytes_consumed))
290}
291
292/// Deserialize a transaction from Bitcoin wire format, returning transaction, witness, and bytes consumed
293pub fn deserialize_transaction_with_witness(
294    data: &[u8],
295) -> Result<(Transaction, Vec<Witness>, usize)> {
296    let mut offset = 0;
297
298    if data.len() < offset + 4 {
299        return Err(ConsensusError::Serialization(Cow::Owned(
300            TransactionParseError::InsufficientBytes.to_string(),
301        )));
302    }
303    let version = i32::from_le_bytes([
304        data[offset],
305        data[offset + 1],
306        data[offset + 2],
307        data[offset + 3],
308    ]) as u64;
309    offset += 4;
310
311    let is_segwit = data.len() >= offset + 2 && data[offset] == 0x00 && data[offset + 1] == 0x01;
312
313    if is_segwit {
314        offset += 2;
315    }
316
317    let (input_count, varint_len) = decode_varint(&data[offset..])?;
318    offset += varint_len;
319
320    if input_count > 1000000 {
321        return Err(ConsensusError::Serialization(Cow::Owned(
322            TransactionParseError::InvalidInputCount.to_string(),
323        )));
324    }
325
326    #[cfg(feature = "production")]
327    let mut inputs = SmallVec::<[TransactionInput; 2]>::new();
328    #[cfg(not(feature = "production"))]
329    let mut inputs = Vec::new();
330
331    for _ in 0..input_count {
332        if data.len() < offset + 36 {
333            return Err(ConsensusError::Serialization(Cow::Owned(
334                TransactionParseError::InsufficientBytes.to_string(),
335            )));
336        }
337        let mut hash = [0u8; 32];
338        hash.copy_from_slice(&data[offset..offset + 32]);
339        offset += 32;
340
341        let index = u32::from_le_bytes([
342            data[offset],
343            data[offset + 1],
344            data[offset + 2],
345            data[offset + 3],
346        ]);
347        offset += 4;
348
349        let (script_len, varint_len) = decode_varint(&data[offset..])?;
350        offset += varint_len;
351
352        if data.len() < offset + script_len as usize {
353            return Err(ConsensusError::Serialization(Cow::Owned(
354                TransactionParseError::InsufficientBytes.to_string(),
355            )));
356        }
357        let script_sig = data[offset..offset + script_len as usize].to_vec();
358        offset += script_len as usize;
359
360        let sequence = u32::from_le_bytes([
361            data[offset],
362            data[offset + 1],
363            data[offset + 2],
364            data[offset + 3],
365        ]) as u64;
366        offset += 4;
367
368        inputs.push(TransactionInput {
369            prevout: OutPoint { hash, index },
370            script_sig,
371            sequence,
372        });
373    }
374
375    let (output_count, varint_len) = decode_varint(&data[offset..])?;
376    offset += varint_len;
377
378    if output_count > 1000000 {
379        return Err(ConsensusError::Serialization(Cow::Owned(
380            TransactionParseError::InvalidOutputCount.to_string(),
381        )));
382    }
383
384    #[cfg(feature = "production")]
385    let mut outputs = SmallVec::<[TransactionOutput; 2]>::new();
386    #[cfg(not(feature = "production"))]
387    let mut outputs = Vec::new();
388
389    for _ in 0..output_count {
390        if data.len() < offset + 8 {
391            return Err(ConsensusError::Serialization(Cow::Owned(
392                TransactionParseError::InsufficientBytes.to_string(),
393            )));
394        }
395        let value = i64::from_le_bytes([
396            data[offset],
397            data[offset + 1],
398            data[offset + 2],
399            data[offset + 3],
400            data[offset + 4],
401            data[offset + 5],
402            data[offset + 6],
403            data[offset + 7],
404        ]);
405        offset += 8;
406
407        let (script_len, varint_len) = decode_varint(&data[offset..])?;
408        offset += varint_len;
409
410        if data.len() < offset + script_len as usize {
411            return Err(ConsensusError::Serialization(Cow::Owned(
412                TransactionParseError::InsufficientBytes.to_string(),
413            )));
414        }
415        let script_pubkey = data[offset..offset + script_len as usize].to_vec();
416        offset += script_len as usize;
417
418        outputs.push(TransactionOutput {
419            value,
420            script_pubkey,
421        });
422    }
423
424    let mut all_witnesses: Vec<Witness> = Vec::new();
425    if is_segwit {
426        for _ in 0..input_count {
427            let (stack_count, varint_len) = decode_varint(&data[offset..])?;
428            offset += varint_len;
429
430            let mut witness_stack: Witness = Vec::new();
431            for _ in 0..stack_count {
432                let (item_len, varint_len) = decode_varint(&data[offset..])?;
433                offset += varint_len;
434
435                if data.len() < offset + item_len as usize {
436                    return Err(ConsensusError::Serialization(Cow::Owned(
437                        TransactionParseError::InsufficientBytes.to_string(),
438                    )));
439                }
440                witness_stack.push(data[offset..offset + item_len as usize].to_vec());
441                offset += item_len as usize;
442            }
443            all_witnesses.push(witness_stack);
444        }
445    } else {
446        for _ in 0..input_count {
447            all_witnesses.push(Vec::new());
448        }
449    }
450
451    if data.len() < offset + 4 {
452        return Err(ConsensusError::Serialization(Cow::Owned(
453            TransactionParseError::InsufficientBytes.to_string(),
454        )));
455    }
456    let lock_time = u32::from_le_bytes([
457        data[offset],
458        data[offset + 1],
459        data[offset + 2],
460        data[offset + 3],
461    ]) as u64;
462    offset += 4;
463
464    let tx = Transaction {
465        version,
466        inputs,
467        outputs,
468        lock_time,
469    };
470
471    Ok((tx, all_witnesses, offset))
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477
478    #[test]
479    fn test_serialize_deserialize_round_trip() {
480        let tx = Transaction {
481            version: 1,
482            inputs: crate::tx_inputs![TransactionInput {
483                prevout: OutPoint {
484                    hash: [1; 32],
485                    index: 0
486                },
487                script_sig: vec![0x51],
488                sequence: 0xffffffff,
489            }],
490            outputs: crate::tx_outputs![TransactionOutput {
491                value: 5000000000,
492                script_pubkey: vec![0x51],
493            }],
494            lock_time: 0,
495        };
496
497        let serialized = serialize_transaction(&tx);
498        let deserialized = deserialize_transaction(&serialized).unwrap();
499
500        assert_eq!(deserialized.version, tx.version);
501        assert_eq!(deserialized.inputs.len(), tx.inputs.len());
502        assert_eq!(deserialized.outputs.len(), tx.outputs.len());
503        assert_eq!(deserialized.lock_time, tx.lock_time);
504    }
505}