snap_coin/core/
transaction.rs

1use bincode::{Decode, Encode, error::EncodeError};
2use num_bigint::BigUint;
3use rand::Rng;
4use serde::{Deserialize, Serialize};
5
6use crate::crypto::{
7    Hash, Signature,
8    keys::{Private, Public},
9};
10
11/// A way of finding this transaction. Alias for Hash
12pub type TransactionId = Hash;
13
14pub const MAX_TRANSACTION_IO: usize = 500;
15
16/// A transaction input, that are funding a set transaction output, that must exist in the current utxo set
17#[derive(Encode, Decode, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
18pub struct TransactionInput {
19    pub transaction_id: TransactionId,
20    pub output_index: usize,
21    pub signature: Option<Signature>,
22}
23
24/// A transaction output, specifying the transactions set receiver
25#[derive(Encode, Decode, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
26pub struct TransactionOutput {
27    pub amount: u64,
28    pub receiver: Public,
29}
30
31/// A transaction containing transaction inputs (funding this transaction) and outputs (spending this transactions funds)
32/// 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
33/// The timestamp is set by the sender, and only loosely validated
34/// The nonce is also set by the sender to allow the transaction to be mined (POW)
35#[derive(Encode, Decode, Debug, Clone, Serialize, Deserialize)]
36pub struct Transaction {
37    pub inputs: Vec<TransactionInput>,
38    pub outputs: Vec<TransactionOutput>,
39    pub transaction_id: Option<TransactionId>,
40    pub nonce: u64,
41    pub timestamp: u64,
42}
43
44impl Transaction {
45    /// 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)
46    /// WARNING: The transaction still needs to be mined (POW)! compute_pow() must be called after
47    pub fn new_transaction_now(
48        inputs: Vec<TransactionInput>,
49        outputs: Vec<TransactionOutput>,
50        signing_keys: &mut Vec<Private>,
51    ) -> Result<Self, EncodeError> {
52        let mut transaction = Self {
53            inputs,
54            outputs,
55            transaction_id: None,
56            nonce: 0,
57            timestamp: chrono::Utc::now().timestamp() as u64,
58        };
59        let signing_buf = transaction.get_input_signing_buf()?;
60
61        for (input, key) in transaction.inputs.iter_mut().zip(signing_keys.iter_mut()) {
62            input.signature = Some(Signature::new_signature(key, &signing_buf));
63        }
64
65        Ok(transaction)
66    }
67
68    /// Mine this transaction (aka. compute POW to allow it to be placed in the network)
69    /// 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
70    /// Difficulty margin can be used to prevent the behavior mentioned above, however it will lead to a longer compute time. The bugger difficulty margin is, the more likely is the transaction to be valid next block.
71    /// Difficulty margin is a multiplier by how much harder does the actual difficulty need to be
72    pub fn compute_pow(
73        &mut self,
74        tx_difficulty: &[u8; 32],
75        difficulty_margin: Option<f64>,
76    ) -> Result<(), EncodeError> {
77        let mut target = BigUint::from_bytes_be(tx_difficulty);
78
79        if let Some(margin) = difficulty_margin {
80            if margin > 0.0 {
81                // Larger margin -> target becomes smaller → more difficult
82                target /= BigUint::from((margin * 1000.0) as u64);
83            }
84        }
85        let mut rng = rand::rng();
86        loop {
87            self.nonce = rng.random();
88            let hashing_buf = self.get_tx_hashing_buf()?;
89            if BigUint::from_bytes_be(&*Hash::new(&hashing_buf)) <= target {
90                self.transaction_id = Some(Hash::new(&hashing_buf));
91                return Ok(());
92            }
93        }
94    }
95
96    /// Get the buffer that needs to be signed by each inputs private key
97    pub fn get_input_signing_buf(&self) -> Result<Vec<u8>, EncodeError> {
98        let mut signature_less_transaction: Transaction = self.clone();
99
100        signature_less_transaction.transaction_id = None;
101        signature_less_transaction.nonce = 0;
102
103        for input in &mut signature_less_transaction.inputs {
104            input.signature = None; // remove all signatures for signing
105        }
106
107        Ok(bincode::encode_to_vec(
108            signature_less_transaction,
109            bincode::config::standard(),
110        )?)
111    }
112
113    /// Get the buffer that needs to be hashed to compute the pow puzzle
114    pub fn get_tx_hashing_buf(&self) -> Result<Vec<u8>, EncodeError> {
115        let mut signature_less_transaction = self.clone();
116
117        signature_less_transaction.transaction_id = None;
118
119        Ok(bincode::encode_to_vec(
120            signature_less_transaction,
121            bincode::config::standard(),
122        )?)
123    }
124}