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        let sequence = u32::from_le_bytes([
201            data[offset],
202            data[offset + 1],
203            data[offset + 2],
204            data[offset + 3],
205        ]) as u64;
206        offset += 4;
207
208        inputs.push(TransactionInput {
209            prevout: OutPoint { hash, index },
210            script_sig,
211            sequence,
212        });
213    }
214
215    let (output_count, varint_len) = decode_varint(&data[offset..])?;
216    offset += varint_len;
217
218    if output_count > 1000000 {
219        return Err(ConsensusError::Serialization(Cow::Owned(
220            TransactionParseError::InvalidOutputCount.to_string(),
221        )));
222    }
223
224    #[cfg(feature = "production")]
225    let mut outputs = SmallVec::<[TransactionOutput; 2]>::new();
226    #[cfg(not(feature = "production"))]
227    let mut outputs = Vec::new();
228
229    for _ in 0..output_count {
230        if data.len() < offset + 8 {
231            return Err(ConsensusError::Serialization(Cow::Owned(
232                TransactionParseError::InsufficientBytes.to_string(),
233            )));
234        }
235        let value = i64::from_le_bytes([
236            data[offset],
237            data[offset + 1],
238            data[offset + 2],
239            data[offset + 3],
240            data[offset + 4],
241            data[offset + 5],
242            data[offset + 6],
243            data[offset + 7],
244        ]);
245        offset += 8;
246
247        let (script_len, varint_len) = decode_varint(&data[offset..])?;
248        offset += varint_len;
249
250        let script_pubkey_end = checked_slice_end(offset, script_len)?;
251        if data.len() < script_pubkey_end {
252            return Err(ConsensusError::Serialization(Cow::Owned(
253                TransactionParseError::InsufficientBytes.to_string(),
254            )));
255        }
256        let script_pubkey = data[offset..script_pubkey_end].to_vec();
257        offset = script_pubkey_end;
258
259        outputs.push(TransactionOutput {
260            value,
261            script_pubkey,
262        });
263    }
264
265    if is_segwit {
266        for _ in 0..input_count {
267            let (stack_count, varint_len) = decode_varint(&data[offset..])?;
268            offset += varint_len;
269            for _ in 0..stack_count {
270                let (item_len, varint_len) = decode_varint(&data[offset..])?;
271                offset += varint_len;
272                let item_end = checked_slice_end(offset, item_len)?;
273                if data.len() < item_end {
274                    return Err(ConsensusError::Serialization(Cow::Owned(
275                        TransactionParseError::InsufficientBytes.to_string(),
276                    )));
277                }
278                offset = item_end;
279            }
280        }
281    }
282
283    if data.len() < offset + 4 {
284        return Err(ConsensusError::Serialization(Cow::Owned(
285            TransactionParseError::InsufficientBytes.to_string(),
286        )));
287    }
288    let lock_time = u32::from_le_bytes([
289        data[offset],
290        data[offset + 1],
291        data[offset + 2],
292        data[offset + 3],
293    ]) as u64;
294
295    Ok(Transaction {
296        version,
297        inputs,
298        outputs,
299        lock_time,
300    })
301}
302
303/// Deserialize a transaction, returning (tx, bytes_consumed). Convenience wrapper that discards witness data.
304pub fn deserialize_transaction_with_offset(data: &[u8]) -> Result<(Transaction, usize)> {
305    let (tx, _witnesses, bytes_consumed) = deserialize_transaction_with_witness(data)?;
306    Ok((tx, bytes_consumed))
307}
308
309/// Deserialize a transaction from Bitcoin wire format, returning transaction, witness, and bytes consumed
310pub fn deserialize_transaction_with_witness(
311    data: &[u8],
312) -> Result<(Transaction, Vec<Witness>, usize)> {
313    let mut offset = 0;
314
315    if data.len() < offset + 4 {
316        return Err(ConsensusError::Serialization(Cow::Owned(
317            TransactionParseError::InsufficientBytes.to_string(),
318        )));
319    }
320    let version = i32::from_le_bytes([
321        data[offset],
322        data[offset + 1],
323        data[offset + 2],
324        data[offset + 3],
325    ]) as u64;
326    offset += 4;
327
328    let is_segwit = data.len() >= offset + 2 && data[offset] == 0x00 && data[offset + 1] == 0x01;
329
330    if is_segwit {
331        offset += 2;
332    }
333
334    let (input_count, varint_len) = decode_varint(&data[offset..])?;
335    offset += varint_len;
336
337    if input_count > 1000000 {
338        return Err(ConsensusError::Serialization(Cow::Owned(
339            TransactionParseError::InvalidInputCount.to_string(),
340        )));
341    }
342
343    #[cfg(feature = "production")]
344    let mut inputs = SmallVec::<[TransactionInput; 2]>::new();
345    #[cfg(not(feature = "production"))]
346    let mut inputs = Vec::new();
347
348    for _ in 0..input_count {
349        if data.len() < offset + 36 {
350            return Err(ConsensusError::Serialization(Cow::Owned(
351                TransactionParseError::InsufficientBytes.to_string(),
352            )));
353        }
354        let mut hash = [0u8; 32];
355        hash.copy_from_slice(&data[offset..offset + 32]);
356        offset += 32;
357
358        let index = u32::from_le_bytes([
359            data[offset],
360            data[offset + 1],
361            data[offset + 2],
362            data[offset + 3],
363        ]);
364        offset += 4;
365
366        let (script_len, varint_len) = decode_varint(&data[offset..])?;
367        offset += varint_len;
368
369        let script_sig_end = checked_slice_end(offset, script_len)?;
370        if data.len() < script_sig_end {
371            return Err(ConsensusError::Serialization(Cow::Owned(
372                TransactionParseError::InsufficientBytes.to_string(),
373            )));
374        }
375        let script_sig = data[offset..script_sig_end].to_vec();
376        offset = script_sig_end;
377
378        let sequence = u32::from_le_bytes([
379            data[offset],
380            data[offset + 1],
381            data[offset + 2],
382            data[offset + 3],
383        ]) as u64;
384        offset += 4;
385
386        inputs.push(TransactionInput {
387            prevout: OutPoint { hash, index },
388            script_sig,
389            sequence,
390        });
391    }
392
393    let (output_count, varint_len) = decode_varint(&data[offset..])?;
394    offset += varint_len;
395
396    if output_count > 1000000 {
397        return Err(ConsensusError::Serialization(Cow::Owned(
398            TransactionParseError::InvalidOutputCount.to_string(),
399        )));
400    }
401
402    #[cfg(feature = "production")]
403    let mut outputs = SmallVec::<[TransactionOutput; 2]>::new();
404    #[cfg(not(feature = "production"))]
405    let mut outputs = Vec::new();
406
407    for _ in 0..output_count {
408        if data.len() < offset + 8 {
409            return Err(ConsensusError::Serialization(Cow::Owned(
410                TransactionParseError::InsufficientBytes.to_string(),
411            )));
412        }
413        let value = i64::from_le_bytes([
414            data[offset],
415            data[offset + 1],
416            data[offset + 2],
417            data[offset + 3],
418            data[offset + 4],
419            data[offset + 5],
420            data[offset + 6],
421            data[offset + 7],
422        ]);
423        offset += 8;
424
425        let (script_len, varint_len) = decode_varint(&data[offset..])?;
426        offset += varint_len;
427
428        let script_pubkey_end = checked_slice_end(offset, script_len)?;
429        if data.len() < script_pubkey_end {
430            return Err(ConsensusError::Serialization(Cow::Owned(
431                TransactionParseError::InsufficientBytes.to_string(),
432            )));
433        }
434        let script_pubkey = data[offset..script_pubkey_end].to_vec();
435        offset = script_pubkey_end;
436
437        outputs.push(TransactionOutput {
438            value,
439            script_pubkey,
440        });
441    }
442
443    let mut all_witnesses: Vec<Witness> = Vec::new();
444    if is_segwit {
445        for _ in 0..input_count {
446            let (stack_count, varint_len) = decode_varint(&data[offset..])?;
447            offset += varint_len;
448
449            let mut witness_stack: Witness = Vec::new();
450            for _ in 0..stack_count {
451                let (item_len, varint_len) = decode_varint(&data[offset..])?;
452                offset += varint_len;
453
454                let item_end = checked_slice_end(offset, item_len)?;
455                if data.len() < item_end {
456                    return Err(ConsensusError::Serialization(Cow::Owned(
457                        TransactionParseError::InsufficientBytes.to_string(),
458                    )));
459                }
460                witness_stack.push(data[offset..item_end].to_vec());
461                offset = item_end;
462            }
463            all_witnesses.push(witness_stack);
464        }
465    } else {
466        for _ in 0..input_count {
467            all_witnesses.push(Vec::new());
468        }
469    }
470
471    if data.len() < offset + 4 {
472        return Err(ConsensusError::Serialization(Cow::Owned(
473            TransactionParseError::InsufficientBytes.to_string(),
474        )));
475    }
476    let lock_time = u32::from_le_bytes([
477        data[offset],
478        data[offset + 1],
479        data[offset + 2],
480        data[offset + 3],
481    ]) as u64;
482    offset += 4;
483
484    let tx = Transaction {
485        version,
486        inputs,
487        outputs,
488        lock_time,
489    };
490
491    Ok((tx, all_witnesses, offset))
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497
498    #[test]
499    fn test_serialize_deserialize_round_trip() {
500        let tx = Transaction {
501            version: 1,
502            inputs: crate::tx_inputs![TransactionInput {
503                prevout: OutPoint {
504                    hash: [1; 32],
505                    index: 0
506                },
507                script_sig: vec![0x51],
508                sequence: 0xffffffff,
509            }],
510            outputs: crate::tx_outputs![TransactionOutput {
511                value: 5000000000,
512                script_pubkey: vec![0x51],
513            }],
514            lock_time: 0,
515        };
516
517        let serialized = serialize_transaction(&tx);
518        let deserialized = deserialize_transaction(&serialized).unwrap();
519
520        assert_eq!(deserialized.version, tx.version);
521        assert_eq!(deserialized.inputs.len(), tx.inputs.len());
522        assert_eq!(deserialized.outputs.len(), tx.outputs.len());
523        assert_eq!(deserialized.lock_time, tx.lock_time);
524    }
525}