Skip to main content

snap_coin/core/
blockchain.rs

1use std::{
2    collections::HashSet,
3    fs::{self, File},
4    path::Path,
5};
6
7use bincode::{Decode, Encode};
8use num_bigint::BigUint;
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12use crate::{
13    core::{
14        block::{Block, BlockError, MAX_TRANSACTIONS_PER_BLOCK},
15        block_store::{BlockStore, BlockStoreError},
16        difficulty::DifficultyState,
17        transaction::{Transaction, TransactionError, TransactionId},
18        utxo::{UTXODiff, UTXOs},
19    },
20    economics::{DEV_WALLET, EXPIRATION_TIME, calculate_dev_fee, get_block_reward},
21};
22
23#[derive(Error, Debug, Serialize, Deserialize, Clone, Encode, Decode)]
24pub enum BlockchainError {
25    #[error("IO error: {0}")]
26    Io(String),
27
28    #[error("Bincode decode error: {0}")]
29    BincodeDecode(String),
30
31    #[error("Bincode encode error: {0}")]
32    BincodeEncode(String),
33
34    #[error("Block does not have a hash attached")]
35    IncompleteBlock,
36
37    #[error("Transaction is invalid: {0}")]
38    InvalidTransaction(String),
39
40    #[error("Double spend detected in transaction input")]
41    DoubleSpend,
42
43    #[error("Too many reward transaction in block")]
44    RewardOverspend,
45
46    #[error("Reward transaction is invalid")]
47    InvalidRewardTransaction,
48
49    #[error("Reward transaction's outputs do not sum up to block reward amount")]
50    InvalidRewardTransactionAmount,
51
52    #[error("Reward transaction's id is missing")]
53    RewardTransactionIdMissing,
54
55    #[error("Reward transaction's outputs do not include a dev fee transaction")]
56    NoDevFee,
57
58    #[error("No blocks to to pop")]
59    NoBlocksToPop,
60
61    #[error("Block to pop was not found")]
62    BlockNotFound,
63
64    #[error("Block or transaction timestamp is invalid or expired")]
65    InvalidTimestamp,
66
67    #[error("Previous block hash is invalid")]
68    InvalidPreviousBlockHash,
69
70    #[error("Block has too many transactions")]
71    TooManyTransactions,
72
73    #[error("Block error: {0}")]
74    Block(#[from] BlockError),
75
76    #[error("Block store error: {0}")]
77    BlockStore(#[from] BlockStoreError),
78
79    #[error("UTXOs error: {0}")]
80    UTXOs(String),
81
82    #[error("Live transaction difficulty not beat")]
83    LiveTransactionDifficulty,
84}
85
86impl From<TransactionError> for BlockchainError {
87    fn from(err: TransactionError) -> Self {
88        BlockchainError::InvalidTransaction(err.to_string())
89    }
90}
91
92/// BlockchainData is an object used for storing and loading current blockchain state.
93#[derive(Encode, Decode, Debug, Clone)]
94struct BlockchainData {
95    difficulty_state: DifficultyState,
96    block_store: BlockStore,
97}
98
99/// The blockchain, handles everything. Core to this crypto coin.
100#[derive(Debug)]
101pub struct Blockchain {
102    blockchain_path: String,
103    block_store: BlockStore,
104    utxos: UTXOs,
105    difficulty_state: DifficultyState,
106}
107
108impl Blockchain {
109    /// Create a new blockchain or load one if exists at blockchain_path
110    pub fn new(blockchain_path: &str) -> Self {
111        let mut blockchain_path = blockchain_path.to_string();
112        if !blockchain_path.ends_with('/') {
113            blockchain_path.push('/');
114        }
115        blockchain_path.push_str("blockchain/");
116
117        if !Path::new(&blockchain_path).exists() {
118            fs::create_dir_all(format!("{}blocks/", &blockchain_path)).unwrap();
119        }
120
121        match Self::load_blockchain_data(&blockchain_path) {
122            Ok(blockchain_data) => {
123                return Blockchain {
124                    block_store: blockchain_data.block_store,
125                    utxos: UTXOs::new(blockchain_path.clone()),
126                    difficulty_state: blockchain_data.difficulty_state,
127                    blockchain_path,
128                };
129            }
130            Err(_) => {
131                return Blockchain {
132                    utxos: UTXOs::new(blockchain_path.clone()),
133                    difficulty_state: DifficultyState::new_default(),
134                    block_store: BlockStore::new_empty(&format!("{}blocks/", blockchain_path)),
135                    blockchain_path,
136                };
137            }
138        }
139    }
140
141    /// Load the blockchain data
142    fn load_blockchain_data(blockchain_path: &str) -> Result<BlockchainData, BlockchainError> {
143        let mut file = File::open(format!("{}blockchain.dat", blockchain_path))
144            .map_err(|e| BlockchainError::Io(e.to_string()))?;
145        Ok(
146            bincode::decode_from_std_read(&mut file, bincode::config::standard())
147                .map_err(|e| BlockchainError::BincodeDecode(e.to_string()))?,
148        )
149    }
150
151    /// Save the blockchain data
152    fn save_blockchain_data(&self) -> Result<(), BlockchainError> {
153        let mut file = File::create(format!("{}blockchain.dat", self.blockchain_path))
154            .map_err(|e| BlockchainError::Io(e.to_string()))?;
155        let blockchain_data = BlockchainData {
156            difficulty_state: self.difficulty_state.clone(),
157            block_store: self.block_store().clone(),
158        };
159        file.sync_all()
160            .map_err(|e| BlockchainError::Io(e.to_string()))?;
161
162        bincode::encode_into_std_write(blockchain_data, &mut file, bincode::config::standard())
163            .map_err(|e| BlockchainError::BincodeEncode(e.to_string()))?;
164        Ok(())
165    }
166
167    /// Add a block to the blockchain, and then save the state of it
168    /// Will return a blockchain error if the block or any of its included transactions are invalid
169    pub fn add_block(&self, new_block: Block) -> Result<(), BlockchainError> {
170        new_block.check_meta()?;
171
172        new_block.validate_difficulties(
173            &self.get_block_difficulty(),
174            &self.get_transaction_difficulty(),
175        )?;
176
177        // Validate previous hash
178        if self.block_store.get_last_block_hash() != new_block.meta.previous_block {
179            return Err(BlockchainError::InvalidPreviousBlockHash);
180        }
181
182        // Check if we don't have too many TXs
183        if new_block.transactions.len() > MAX_TRANSACTIONS_PER_BLOCK {
184            return Err(BlockchainError::TooManyTransactions);
185        }
186
187        let mut used_inputs: HashSet<(TransactionId, usize)> = HashSet::new();
188
189        let mut seen_reward_transaction = false;
190        for transaction in &new_block.transactions {
191            if !transaction.inputs.is_empty() {
192                // Normal tx
193                // Validate transaction in context of UTXOs
194                self.utxos.validate_transaction(
195                    transaction,
196                    &BigUint::from_bytes_be(&self.get_transaction_difficulty()),
197                )?;
198
199                // Check for double spending
200                for input in &transaction.inputs {
201                    let key = (input.transaction_id, input.output_index);
202                    if !used_inputs.insert(key.clone()) {
203                        return Err(BlockchainError::DoubleSpend);
204                    }
205                }
206
207                validate_transaction_timestamp_in_block(&transaction, &new_block)?;
208            } else {
209                // Reward tx
210                if seen_reward_transaction {
211                    return Err(BlockchainError::RewardOverspend);
212                }
213                seen_reward_transaction = true;
214
215                if transaction.outputs.len() < 2 {
216                    return Err(BlockchainError::InvalidRewardTransaction);
217                }
218
219                if transaction.transaction_id.is_none() {
220                    return Err(BlockchainError::RewardTransactionIdMissing);
221                }
222
223                if transaction
224                    .outputs
225                    .iter()
226                    .fold(0, |acc, output| acc + output.amount)
227                    != get_block_reward(self.block_store().get_height())
228                {
229                    return Err(BlockchainError::InvalidRewardTransactionAmount);
230                }
231
232                let mut has_dev_fee = false;
233                for output in &transaction.outputs {
234                    if output.receiver == DEV_WALLET
235                        && output.amount
236                            == calculate_dev_fee(get_block_reward(self.block_store().get_height()))
237                    {
238                        has_dev_fee = true;
239                        break;
240                    }
241                }
242
243                if !has_dev_fee {
244                    return Err(BlockchainError::NoDevFee);
245                }
246            }
247        }
248
249        // Calculate and execute all utxo diffs
250        let mut utxo_diffs = UTXODiff::new_empty();
251
252        for transaction in &new_block.transactions {
253            utxo_diffs.extend(&mut self.utxos.execute_transaction(transaction)?);
254        }
255
256        self.difficulty_state.update_difficulty(&new_block);
257
258        self.block_store().add_block(new_block, utxo_diffs)?;
259        self.save_blockchain_data()?;
260
261        Ok(())
262    }
263
264    /// Remove the last block added to the blockchain, and update the states of utxos and difficulty manager to return the blockchain to the state it was before the last block was added
265    pub fn pop_block(&self) -> Result<(), BlockchainError> {
266        if self.block_store().get_height() == 0 {
267            return Err(BlockchainError::NoBlocksToPop);
268        }
269
270        let recalled_block = self
271            .block_store()
272            .get_last_block()
273            .ok_or(BlockchainError::BlockNotFound)?;
274        let utxo_diffs = self
275            .block_store()
276            .get_last_utxo_diffs()
277            .ok_or(BlockchainError::BlockNotFound)?;
278
279        // Rollback UTXOs
280        self.utxos.recall_block_utxos(&utxo_diffs)?;
281
282        self.block_store().pop_block()?;
283
284        // Update difficulty manager
285        *self.difficulty_state.block_difficulty.write().unwrap() =
286            recalled_block.meta.block_pow_difficulty;
287        *self
288            .difficulty_state
289            .transaction_difficulty
290            .write()
291            .unwrap() = recalled_block.meta.tx_pow_difficulty;
292        if self.block_store().get_height() > 0
293            && let Some(last_block) = self.block_store().get_last_block()
294        {
295            *self.difficulty_state.last_timestamp.write().unwrap() = last_block.timestamp;
296        } else {
297            *self.difficulty_state.last_timestamp.write().unwrap() = recalled_block.timestamp;
298        }
299
300        // Save blockchain data
301        self.save_blockchain_data()?;
302
303        Ok(())
304    }
305
306    pub fn get_utxos(&self) -> &UTXOs {
307        &self.utxos
308    }
309
310    pub fn get_difficulty_manager(&self) -> &DifficultyState {
311        &self.difficulty_state
312    }
313
314    pub fn get_transaction_difficulty(&self) -> [u8; 32] {
315        *self.difficulty_state.transaction_difficulty.read().unwrap()
316    }
317
318    pub fn get_block_difficulty(&self) -> [u8; 32] {
319        *self.difficulty_state.block_difficulty.read().unwrap()
320    }
321
322    pub fn block_store(&self) -> &BlockStore {
323        &self.block_store
324    }
325}
326
327/// Returns true if transaction timestamp is valid in the context of a block
328pub fn validate_transaction_timestamp_in_block(
329    transaction: &Transaction,
330    owning_block: &Block,
331) -> Result<(), BlockchainError> {
332    if transaction.timestamp > owning_block.timestamp {
333        return Err(BlockchainError::InvalidTimestamp);
334    }
335    if transaction.timestamp + EXPIRATION_TIME < owning_block.timestamp {
336        return Err(BlockchainError::InvalidTimestamp);
337    }
338
339    Ok(())
340}
341
342/// Returns true if transaction timestamp is valid in the context of current time
343pub fn validate_transaction_timestamp(transaction: &Transaction) -> Result<(), BlockchainError> {
344    if transaction.timestamp - EXPIRATION_TIME > chrono::Utc::now().timestamp() as u64 {
345        return Err(BlockchainError::InvalidTimestamp);
346    }
347    if transaction.timestamp + EXPIRATION_TIME < chrono::Utc::now().timestamp() as u64 {
348        return Err(BlockchainError::InvalidTimestamp);
349    }
350
351    Ok(())
352}
353
354/// Returns false if block timestamp is valid
355pub fn validate_block_timestamp(block: &Block) -> Result<(), BlockchainError> {
356    if block.timestamp - EXPIRATION_TIME > chrono::Utc::now().timestamp() as u64 {
357        return Err(BlockchainError::InvalidTimestamp);
358    }
359    if block.timestamp + EXPIRATION_TIME < chrono::Utc::now().timestamp() as u64 {
360        return Err(BlockchainError::InvalidTimestamp);
361    }
362
363    Ok(())
364}