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    /// WARNING: This can be randomly async unsafe, so if you have problems, wrap it in a spawn_blocking()
170    pub fn add_block(&self, new_block: Block, is_ibd: bool) -> Result<(), BlockchainError> {
171        new_block.check_meta()?;
172
173        new_block.validate_difficulties(
174            &self.get_block_difficulty(),
175            &self.get_transaction_difficulty(),
176        )?;
177
178        // Validate previous hash
179        if self.block_store.get_last_block_hash() != new_block.meta.previous_block {
180            return Err(BlockchainError::InvalidPreviousBlockHash);
181        }
182
183        // Check if we don't have too many TXs
184        if new_block.transactions.len() > MAX_TRANSACTIONS_PER_BLOCK {
185            return Err(BlockchainError::TooManyTransactions);
186        }
187
188        let mut used_inputs: HashSet<(TransactionId, usize)> = HashSet::new();
189
190        let mut seen_reward_transaction = false;
191        for transaction in &new_block.transactions {
192            if !transaction.inputs.is_empty() {
193                // Normal tx
194                // Validate transaction in context of UTXOs
195                self.utxos.validate_transaction(
196                    transaction,
197                    &BigUint::from_bytes_be(&self.get_transaction_difficulty()),
198                    is_ibd,
199                )?;
200
201                // Check for double spending
202                for input in &transaction.inputs {
203                    let key = (input.transaction_id, input.output_index);
204                    if !used_inputs.insert(key.clone()) {
205                        return Err(BlockchainError::DoubleSpend);
206                    }
207                }
208
209                validate_transaction_timestamp_in_block(&transaction, &new_block)?;
210            } else {
211                // Reward tx
212                if seen_reward_transaction {
213                    return Err(BlockchainError::RewardOverspend);
214                }
215                seen_reward_transaction = true;
216
217                if transaction.outputs.len() < 2 {
218                    return Err(BlockchainError::InvalidRewardTransaction);
219                }
220
221                if transaction.transaction_id.is_none() {
222                    return Err(BlockchainError::RewardTransactionIdMissing);
223                }
224
225                if transaction
226                    .outputs
227                    .iter()
228                    .fold(0, |acc, output| acc + output.amount)
229                    != get_block_reward(self.block_store().get_height())
230                {
231                    return Err(BlockchainError::InvalidRewardTransactionAmount);
232                }
233
234                let mut has_dev_fee = false;
235                for output in &transaction.outputs {
236                    if output.receiver == DEV_WALLET
237                        && output.amount
238                            == calculate_dev_fee(get_block_reward(self.block_store().get_height()))
239                    {
240                        has_dev_fee = true;
241                        break;
242                    }
243                }
244
245                if !has_dev_fee {
246                    return Err(BlockchainError::NoDevFee);
247                }
248            }
249        }
250
251        // Calculate and execute all utxo diffs
252        let mut utxo_diffs = UTXODiff::new_empty();
253
254        for transaction in &new_block.transactions {
255            utxo_diffs.extend(&mut self.utxos.execute_transaction(transaction)?);
256        }
257
258        self.difficulty_state.update_difficulty(&new_block);
259
260        self.block_store().add_block(new_block, utxo_diffs)?;
261        self.save_blockchain_data()?;
262
263        Ok(())
264    }
265
266    /// 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
267    pub fn pop_block(&self) -> Result<(), BlockchainError> {
268        if self.block_store().get_height() == 0 {
269            return Err(BlockchainError::NoBlocksToPop);
270        }
271
272        let recalled_block = self
273            .block_store()
274            .get_last_block()
275            .ok_or(BlockchainError::BlockNotFound)?;
276        let utxo_diffs = self
277            .block_store()
278            .get_last_utxo_diffs()
279            .ok_or(BlockchainError::BlockNotFound)?;
280
281        // Rollback UTXOs
282        self.utxos.recall_block_utxos(&utxo_diffs)?;
283
284        self.block_store().pop_block()?;
285
286        // Update difficulty manager
287        *self.difficulty_state.block_difficulty.write().unwrap() =
288            recalled_block.meta.block_pow_difficulty;
289        *self
290            .difficulty_state
291            .transaction_difficulty
292            .write()
293            .unwrap() = recalled_block.meta.tx_pow_difficulty;
294        if self.block_store().get_height() > 0
295            && let Some(last_block) = self.block_store().get_last_block()
296        {
297            *self.difficulty_state.last_timestamp.write().unwrap() = last_block.timestamp;
298        } else {
299            *self.difficulty_state.last_timestamp.write().unwrap() = recalled_block.timestamp;
300        }
301
302        // Save blockchain data
303        self.save_blockchain_data()?;
304
305        Ok(())
306    }
307
308    pub fn get_utxos(&self) -> &UTXOs {
309        &self.utxos
310    }
311
312    pub fn get_difficulty_manager(&self) -> &DifficultyState {
313        &self.difficulty_state
314    }
315
316    pub fn get_transaction_difficulty(&self) -> [u8; 32] {
317        *self.difficulty_state.transaction_difficulty.read().unwrap()
318    }
319
320    pub fn get_block_difficulty(&self) -> [u8; 32] {
321        *self.difficulty_state.block_difficulty.read().unwrap()
322    }
323
324    pub fn block_store(&self) -> &BlockStore {
325        &self.block_store
326    }
327}
328
329/// Returns true if transaction timestamp is valid in the context of a block
330pub fn validate_transaction_timestamp_in_block(
331    transaction: &Transaction,
332    owning_block: &Block,
333) -> Result<(), BlockchainError> {
334    if transaction.timestamp > owning_block.timestamp {
335        return Err(BlockchainError::InvalidTimestamp);
336    }
337    if transaction.timestamp + EXPIRATION_TIME < owning_block.timestamp {
338        return Err(BlockchainError::InvalidTimestamp);
339    }
340
341    Ok(())
342}
343
344/// Returns true if transaction timestamp is valid in the context of current time
345pub fn validate_transaction_timestamp(transaction: &Transaction) -> Result<(), BlockchainError> {
346    if transaction.timestamp - EXPIRATION_TIME > chrono::Utc::now().timestamp() as u64 {
347        return Err(BlockchainError::InvalidTimestamp);
348    }
349    if transaction.timestamp + EXPIRATION_TIME < chrono::Utc::now().timestamp() as u64 {
350        return Err(BlockchainError::InvalidTimestamp);
351    }
352
353    Ok(())
354}
355
356/// Returns false if block timestamp is valid
357pub fn validate_block_timestamp(block: &Block) -> Result<(), BlockchainError> {
358    if block.timestamp - EXPIRATION_TIME > chrono::Utc::now().timestamp() as u64 {
359        return Err(BlockchainError::InvalidTimestamp);
360    }
361    if block.timestamp + EXPIRATION_TIME < chrono::Utc::now().timestamp() as u64 {
362        return Err(BlockchainError::InvalidTimestamp);
363    }
364
365    Ok(())
366}