subcoin_primitives/
consensus.rs

1use crate::MAX_BLOCK_WEIGHT;
2use bitcoin::blockdata::weight::WITNESS_SCALE_FACTOR;
3use bitcoin::{Amount, Transaction, Weight};
4use std::collections::HashSet;
5
6// MinCoinbaseScriptLen is the minimum length a coinbase script can be.
7const MIN_COINBASE_SCRIPT_LEN: usize = 2;
8
9// MaxCoinbaseScriptLen is the maximum length a coinbase script can be.
10const MAX_COINBASE_SCRIPT_LEN: usize = 100;
11
12/// Transaction validation error.
13#[derive(Debug, thiserror::Error)]
14pub enum TxError {
15    #[error("Transaction has no inputs")]
16    EmptyInput,
17    #[error("Transaction has no outputs")]
18    EmptyOutput,
19    #[error("Transaction is too large")]
20    TransactionOversize,
21    #[error("Transaction contains duplicate inputs at index {0}")]
22    DuplicateTxInput(usize),
23    #[error("Output value (0) is too large")]
24    OutputValueTooLarge(Amount),
25    #[error("Total output value (0) is too large")]
26    TotalOutputValueTooLarge(Amount),
27    #[error(
28        "Coinbase transaction script length of {0} is out of range \
29        (min: {MIN_COINBASE_SCRIPT_LEN}, max: {MAX_COINBASE_SCRIPT_LEN})"
30    )]
31    BadCoinbaseLength(usize),
32    #[error("Transaction input refers to a previous output that is null")]
33    PreviousOutputNull,
34}
35
36/// Basic checks that don't depend on any context.
37// <https://github.com/bitcoin/bitcoin/blob/6f9db1ebcab4064065ccd787161bf2b87e03cc1f/src/consensus/tx_check.cpp#L11>
38pub fn check_transaction_sanity(tx: &Transaction) -> Result<(), TxError> {
39    if tx.input.is_empty() {
40        return Err(TxError::EmptyInput);
41    }
42
43    if tx.output.is_empty() {
44        return Err(TxError::EmptyOutput);
45    }
46
47    if Weight::from_wu((tx.base_size() * WITNESS_SCALE_FACTOR) as u64) > MAX_BLOCK_WEIGHT {
48        return Err(TxError::TransactionOversize);
49    }
50
51    let mut value_out = Amount::ZERO;
52    tx.output.iter().try_for_each(|txout| {
53        if txout.value > Amount::MAX_MONEY {
54            return Err(TxError::OutputValueTooLarge(txout.value));
55        }
56
57        value_out += txout.value;
58
59        if value_out > Amount::MAX_MONEY {
60            return Err(TxError::TotalOutputValueTooLarge(value_out));
61        }
62
63        Ok(())
64    })?;
65
66    // Check for duplicate inputs.
67    let mut seen_inputs = HashSet::with_capacity(tx.input.len());
68    for (index, txin) in tx.input.iter().enumerate() {
69        if !seen_inputs.insert(txin.previous_output) {
70            return Err(TxError::DuplicateTxInput(index));
71        }
72    }
73
74    // Coinbase script length must be between min and max length.
75    if tx.is_coinbase() {
76        let script_sig_len = tx.input[0].script_sig.len();
77
78        if !(MIN_COINBASE_SCRIPT_LEN..=MAX_COINBASE_SCRIPT_LEN).contains(&script_sig_len) {
79            return Err(TxError::BadCoinbaseLength(script_sig_len));
80        }
81    } else {
82        // Previous transaction outputs referenced by the inputs to this
83        // transaction must not be null.
84        if tx.input.iter().any(|txin| txin.previous_output.is_null()) {
85            return Err(TxError::PreviousOutputNull);
86        }
87    }
88
89    Ok(())
90}