ergo_lib/chain/transaction/
ergo_transaction.rs

1//! Exposes common properties for signed and unsigned transactions
2use ergotree_interpreter::sigma_protocol::{
3    prover::ContextExtension,
4    verifier::{VerificationResult, VerifierError},
5};
6use ergotree_ir::{
7    chain::{
8        ergo_box::{box_value::BoxValue, BoxId, ErgoBox},
9        token::{TokenAmountError, TokenId},
10    },
11    serialization::SigmaSerializationError,
12};
13use itertools::Itertools;
14use thiserror::Error;
15
16use crate::wallet::tx_context::TransactionContextError;
17
18use super::{unsigned::UnsignedTransaction, DataInput, Transaction};
19
20/// Errors when validating transaction
21#[derive(Error, Debug)]
22pub enum TxValidationError {
23    /// Transaction has more than [`i16::MAX`] inputs
24    #[error("Sum of ERG in outputs overflowed")]
25    /// Sum of ERG in outputs has overflowed
26    OutputSumOverflow,
27    /// Sum of ERG in inputs has overflowed
28    #[error("Sum of ERG in inputs has overflowed")]
29    InputSumOverflow,
30    /// Token Amount Error
31    #[error("Token amount is not valid, {0}")]
32    TokenAmountError(#[from] TokenAmountError),
33    #[error("Unique inputs: {0}, actual inputs: {1}")]
34    /// The transaction is attempting to spend the same [`BoxId`] twice
35    DoubleSpend(usize, usize),
36    #[error("ERG value not preserved, input amount: {0}, output amount: {1}")]
37    /// The amount of Ergo in inputs must be equal to the amount of ergo in output (cannot be burned)
38    ErgPreservationError(u64, u64),
39    #[error("Token preservation error for {token_id:?}, in amount: {in_amount:?}, out_amount: {out_amount:?}, allowed new token id: {new_token_id:?}")]
40    /// Transaction is creating more tokens than exists in inputs. This is only allowed when minting a new token
41    TokenPreservationError {
42        /// If the transaction is minting a new token, then it must have this token id
43        new_token_id: TokenId,
44        /// The token id whose amount was not preserved
45        token_id: TokenId,
46        /// Total amount of token in inputs
47        in_amount: u64,
48        /// Total amount of token in outputs
49        out_amount: u64,
50    },
51    #[error("Output {0} is dust, amount {1:?} < minimum {2}")]
52    /// Transaction was creating a dust output. The value of a box should be >= than box size * [Parameters::min_value_per_byte](crate::chain::parameters::Parameters::min_value_per_byte())
53    DustOutput(BoxId, BoxValue, u64),
54    #[error("Creation height {0} > preheader height")]
55    /// The output's height is greater than the current block height
56    InvalidHeightError(u32),
57    #[error("Creation height {0} <= input box max height{1}")]
58    /// After Block V3, all output boxes height must be >= max(inputs.height). See <https://github.com/ergoplatform/eips/blob/master/eip-0039.md> for more information
59    MonotonicHeightError(u32, u32),
60    #[error("Output box's creation height is negative (not allowed after block version 1)")]
61    /// Negative heights are not allowed after block v1.
62    /// When using sigma-rust where heights are always unsigned, this error may be because creation height was set to be >= 2147483648
63    NegativeHeight,
64    #[error("Output box size {0} > maximum {}", ErgoBox::MAX_BOX_SIZE)]
65    /// Box size is > [ErgoBox::MAX_BOX_SIZE]
66    BoxSizeExceeded(usize),
67    #[error("Output box size {0} > maximum {}", ErgoBox::MAX_SCRIPT_SIZE)]
68    /// Script size is > [ErgoBox::MAX_SCRIPT_SIZE]
69    ScriptSizeExceeded(usize),
70    #[error("TX context error: {0}")]
71    /// Transaction Context Error
72    TransactionContextError(#[from] TransactionContextError),
73    /// Input's proposition reduced to false. This means the proof provided for the input was most likely invalid
74    #[error("Input {0} reduced to false during verification: {1:?}")]
75    ReducedToFalse(usize, VerificationResult),
76    /// Serialization error
77    #[error("Sigma serialization error: {0}")]
78    SigmaSerializationError(#[from] SigmaSerializationError),
79    /// Verifying input script failed
80    #[error("Verifier error on input {0}: {1}")]
81    VerifierError(usize, VerifierError),
82}
83
84/// Exposes common properties for signed and unsigned transactions
85pub trait ErgoTransaction {
86    /// input boxes ids
87    fn inputs_ids(&self) -> impl ExactSizeIterator<Item = BoxId>;
88    /// data input boxes
89    fn data_inputs(&self) -> Option<&[DataInput]>;
90    /// output boxes
91    fn outputs(&self) -> &[ErgoBox];
92    /// ContextExtension for the given input index
93    fn context_extension(&self, input_index: usize) -> Option<ContextExtension>;
94
95    /// Stateless transaction validation (no blockchain context) for a transaction
96    /// Returns [`Ok(())`] if validation has succeeded or returns [`TxValidationError`]
97    fn validate_stateless(&self) -> Result<(), TxValidationError> {
98        // Note that we don't need to check if inputs/data inputs/outputs are >= 1 <= 32767 here since BoundedVec takes care of that
99        let inputs = self.inputs_ids();
100        let outputs = self.outputs();
101
102        outputs
103            .iter()
104            .try_fold(0i64, |a, b| a.checked_add(b.value.as_i64()))
105            .ok_or(TxValidationError::OutputSumOverflow)?;
106
107        // Check if there are no double-spends in input (one BoxId being spent more than once)
108        let len = inputs.len();
109        let unique_count = inputs.unique().count();
110        if unique_count != len {
111            return Err(TxValidationError::DoubleSpend(unique_count, len));
112        }
113        Ok(())
114    }
115}
116
117impl ErgoTransaction for UnsignedTransaction {
118    fn inputs_ids(&self) -> impl ExactSizeIterator<Item = BoxId> {
119        self.inputs.iter().map(|input| input.box_id)
120    }
121
122    fn data_inputs(&self) -> Option<&[DataInput]> {
123        self.data_inputs.as_ref().map(|di| di.as_slice())
124    }
125
126    fn outputs(&self) -> &[ErgoBox] {
127        self.outputs.as_slice()
128    }
129
130    fn context_extension(&self, input_index: usize) -> Option<ContextExtension> {
131        self.inputs
132            .get(input_index)
133            .map(|input| input.extension.clone())
134    }
135}
136
137impl ErgoTransaction for Transaction {
138    fn inputs_ids(&self) -> impl ExactSizeIterator<Item = BoxId> {
139        self.inputs.iter().map(|input| input.box_id)
140    }
141
142    fn data_inputs(&self) -> Option<&[DataInput]> {
143        self.data_inputs.as_ref().map(|di| di.as_slice())
144    }
145
146    fn outputs(&self) -> &[ErgoBox] {
147        self.outputs.as_slice()
148    }
149
150    fn context_extension(&self, input_index: usize) -> Option<ContextExtension> {
151        self.inputs
152            .get(input_index)
153            .map(|input| input.spending_proof.extension.clone())
154    }
155}