snap_coin/core/
transaction.rs

1use bincode::{Decode, Encode, error::EncodeError};
2use num_bigint::BigUint;
3use rand::Rng;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7use crate::crypto::{
8    Hash, Signature,
9    keys::{Private, Public},
10};
11
12/// A way of finding this transaction. Alias for Hash
13pub type TransactionId = Hash;
14
15pub const MAX_TRANSACTION_IO: usize = 150;
16
17#[derive(Error, Debug)]
18pub enum TransactionError {
19    #[error("{0}")]
20    EncodeError(#[from] EncodeError),
21
22    #[error("Transaction missing ID")]
23    MissingId,
24
25    #[error("Transaction missing signature/s")]
26    MissingSignatures,
27
28    #[error("Transaction hash is invalid: {0}")]
29    InvalidHash(String),
30
31    #[error("Transaction hash does not meet required difficulty: {0}")]
32    InsufficientDifficulty(String),
33
34    #[error("Transaction has no inputs")]
35    NoInputs,
36
37    #[error("Transaction input not found in UTXOs: {0}")]
38    InputNotFound(String),
39
40    #[error("Transaction input index invalid for transaction {tx_id}: {input_tx_id}")]
41    InvalidInputIndex { tx_id: String, input_tx_id: String },
42
43    #[error("Referenced transaction input is already spent")]
44    SpentInputIndex,
45
46    #[error("Transaction input signature is invalid for transaction {0}")]
47    InvalidSignature(String),
48
49    #[error("Transaction input output owner is invalid for transaction {0}")]
50    IncorrectOutputOwner(String),
51
52    #[error("Double spending detected in the same transaction {0}")]
53    DoubleSpend(String),
54
55    #[error("Transaction output amount cannot be zero for transaction {0}")]
56    ZeroOutput(String),
57
58    #[error("Transaction inputs and outputs don't sum up to same amount for transaction {0}")]
59    SumMismatch(String),
60
61    #[error("Transaction has too many inputs or outputs")]
62    TooMuchIO,
63
64    #[error("{0}")]
65    Other(String),
66}
67
68/// A transaction input, that are funding a set transaction output, that must exist in the current utxo set
69#[derive(Encode, Decode, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
70pub struct TransactionInput {
71    pub transaction_id: TransactionId,
72    pub output_index: usize,
73    pub signature: Option<Signature>,
74    pub output_owner: Public,
75}
76
77/// A transaction output, specifying the transactions set receiver
78#[derive(Encode, Decode, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
79pub struct TransactionOutput {
80    pub amount: u64,
81    pub receiver: Public,
82}
83
84/// A transaction containing transaction inputs (funding this transaction) and outputs (spending this transactions funds)
85/// The transaction id is a way of finding this transaction once it becomes part of this blockchain. It is also the transaction id that is the actual Hash of the transaction buffer obtained by get_tx_hashing_buf
86/// The timestamp is set by the sender, and only loosely validated
87/// The nonce is also set by the sender to allow the transaction to be mined (POW)
88#[derive(Encode, Decode, Debug, Clone, Serialize, Deserialize)]
89pub struct Transaction {
90    pub inputs: Vec<TransactionInput>,
91    pub outputs: Vec<TransactionOutput>,
92    pub transaction_id: Option<TransactionId>,
93    pub nonce: u64,
94    pub timestamp: u64,
95}
96
97impl Transaction {
98    /// Create a new transaction timestamped now with a set of inputs and outputs. Signed automatically with the signing_keys (in same order as transaction inputs)
99    /// WARNING: The transaction still needs to be mined (POW)! compute_pow() must be called after
100    pub fn new_transaction_now(
101        inputs: Vec<TransactionInput>,
102        outputs: Vec<TransactionOutput>,
103        signing_keys: &mut Vec<Private>,
104    ) -> Result<Self, EncodeError> {
105        let mut transaction = Self {
106            inputs,
107            outputs,
108            transaction_id: None,
109            nonce: 0,
110            timestamp: chrono::Utc::now().timestamp() as u64,
111        };
112        let signing_buf = transaction.get_input_signing_buf()?;
113
114        for (input, key) in transaction.inputs.iter_mut().zip(signing_keys.iter_mut()) {
115            input.signature = Some(Signature::new_signature(key, &signing_buf));
116        }
117
118        Ok(transaction)
119    }
120
121    /// Mine this transaction (aka. compute POW to allow it to be placed in the network)
122    /// WARNING: You most likely want to supply this function with the LIVE transaction difficulty, which is adjusted for mempool difficulty pressure
123    /// WARNING: This is single threaded and needs to complete before a new block is mined on the network as otherwise tx_difficulty becomes invalid, and so the transaction too. This effect can also be amplified, by mempool pressure, which might tell nodes to reject this transaction if the live transaction difficulty criteria is not met.
124    /// Difficulty margin can be used to prevent the behavior mentioned above, however it will lead to a longer PoW compute time.
125    /// Difficulty margin is a percentage [0 - 1), where 0 means no change to target difficulty, and 1 means difficulty is 0 (incomputable, ever). It is recommended, that users (if displayed) should be presented with a 0 - 50% logarithmically scaled slider.
126    pub fn compute_pow(
127        &mut self,
128        live_tx_difficulty: &[u8; 32],
129        difficulty_margin: Option<f64>,
130    ) -> Result<(), EncodeError> {
131        let mut target = BigUint::from_bytes_be(live_tx_difficulty);
132
133        if let Some(margin) = difficulty_margin {
134            if margin > 0.0 {
135                // Larger margin -> target becomes smaller → more difficult
136                target /= BigUint::from((margin * 1000.0) as u64);
137            }
138        }
139        let mut rng = rand::rng();
140        loop {
141            self.nonce = rng.random();
142            let hashing_buf = self.get_tx_hashing_buf()?;
143            if BigUint::from_bytes_be(&*Hash::new(&hashing_buf)) <= target {
144                self.transaction_id = Some(Hash::new(&hashing_buf));
145                return Ok(());
146            }
147        }
148    }
149
150    /// Get the buffer that needs to be signed by each inputs private key
151    pub fn get_input_signing_buf(&self) -> Result<Vec<u8>, EncodeError> {
152        let mut signature_less_transaction: Transaction = self.clone();
153
154        signature_less_transaction.transaction_id = None;
155        signature_less_transaction.nonce = 0;
156
157        for input in &mut signature_less_transaction.inputs {
158            input.signature = None; // remove all signatures for signing
159        }
160
161        Ok(bincode::encode_to_vec(
162            signature_less_transaction,
163            bincode::config::standard(),
164        )?)
165    }
166
167    /// Get the buffer that needs to be hashed to compute the pow puzzle
168    pub fn get_tx_hashing_buf(&self) -> Result<Vec<u8>, EncodeError> {
169        let mut signature_less_transaction = self.clone();
170
171        signature_less_transaction.transaction_id = None;
172
173        Ok(bincode::encode_to_vec(
174            signature_less_transaction,
175            bincode::config::standard(),
176        )?)
177    }
178
179    pub fn check_completeness(&self) -> Result<(), TransactionError> {
180        self.transaction_id.ok_or(TransactionError::MissingId)?;
181        for input in &self.inputs {
182            input.signature.ok_or(TransactionError::MissingSignatures)?;
183        }
184
185        Ok(())
186    }
187
188    pub fn address_count(&self) -> usize {
189        self.inputs.len() + self.outputs.len()
190    }
191
192    /// check if this tx contains a address
193    pub fn contains_address(&self, address: Public) -> bool {
194        for input in &self.inputs {
195            if input.output_owner == address {
196                return true;
197            }
198        }
199        for output in &self.outputs {
200            if output.receiver == address {
201                return true;
202            }
203        }
204        false
205    }
206}