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