Skip to main content

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, Hash)]
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, Hash)]
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, PartialEq, Hash)]
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                let scale = 1.0 - margin; // smaller target -> harder
136                let scale_int = (scale * 1_000_000.0) as u64;
137                target *= BigUint::from(scale_int);
138                target /= BigUint::from(1_000_000u64);
139            }
140        }
141        let mut rng = rand::rng();
142        loop {
143            self.nonce = rng.random();
144            let hashing_buf = self.get_tx_hashing_buf()?;
145            if BigUint::from_bytes_be(&*Hash::new(&hashing_buf)) <= target {
146                self.transaction_id = Some(Hash::new(&hashing_buf));
147                return Ok(());
148            }
149        }
150    }
151
152    /// Get the buffer that needs to be signed by each inputs private key
153    pub fn get_input_signing_buf(&self) -> Result<Vec<u8>, EncodeError> {
154        let mut signature_less_transaction: Transaction = self.clone();
155
156        signature_less_transaction.transaction_id = None;
157        signature_less_transaction.nonce = 0;
158
159        for input in &mut signature_less_transaction.inputs {
160            input.signature = None; // remove all signatures for signing
161        }
162
163        Ok(bincode::encode_to_vec(
164            signature_less_transaction,
165            bincode::config::standard(),
166        )?)
167    }
168
169    /// Get the buffer that needs to be hashed to compute the pow puzzle
170    pub fn get_tx_hashing_buf(&self) -> Result<Vec<u8>, EncodeError> {
171        let mut signature_less_transaction = self.clone();
172
173        signature_less_transaction.transaction_id = None;
174
175        Ok(bincode::encode_to_vec(
176            signature_less_transaction,
177            bincode::config::standard(),
178        )?)
179    }
180
181    pub fn check_completeness(&self) -> Result<(), TransactionError> {
182        self.transaction_id.ok_or(TransactionError::MissingId)?;
183        for input in &self.inputs {
184            input.signature.ok_or(TransactionError::MissingSignatures)?;
185        }
186
187        Ok(())
188    }
189
190    pub fn address_count(&self) -> usize {
191        self.inputs.len() + self.outputs.len()
192    }
193
194    /// check if this tx contains a address
195    pub fn contains_address(&self, address: Public) -> bool {
196        for input in &self.inputs {
197            if input.output_owner == address {
198                return true;
199            }
200        }
201        for output in &self.outputs {
202            if output.receiver == address {
203                return true;
204            }
205        }
206        false
207    }
208}