snap_coin/core/
blockchain.rs

1use bincode::{Decode, Encode, config};
2use num_bigint::BigUint;
3use serde::{Deserialize, Serialize};
4use std::collections::{HashMap, HashSet};
5use std::fs::{self, File};
6use std::path::Path;
7use thiserror::Error;
8
9use crate::blockchain_data_provider::BlockchainDataProvider;
10use crate::core::block::Block;
11use crate::core::difficulty::{DifficultyManager, calculate_block_difficulty};
12use crate::core::transaction::{Transaction, TransactionId};
13use crate::core::utxo::{TransactionError, UTXODiff, UTXOs};
14use crate::crypto::Hash;
15use crate::economics::{
16    DEV_WALLET, EXPIRATION_TIME, GENESIS_PREVIOUS_BLOCK_HASH, calculate_dev_fee, get_block_reward,
17};
18
19#[derive(Error, Debug, Serialize, Deserialize)]
20pub enum BlockchainError {
21    #[error("IO error: {0}")]
22    Io(String),
23
24    #[error("Bincode decode error: {0}")]
25    BincodeDecode(String),
26
27    #[error("Bincode encode error: {0}")]
28    BincodeEncode(String),
29
30    #[error("Block hash mismatch. Expected {expected}, got {actual}")]
31    HashMismatch { expected: String, actual: String },
32
33    #[error("Block timestamp is in the future: {0}")]
34    FutureTimestamp(u64),
35
36    #[error("Invalid block / transaction difficulty")]
37    InvalidDifficulty,
38
39    #[error("Block does not have a hash attached")]
40    MissingHash,
41
42    #[error("Transaction is invalid: {0}")]
43    InvalidTransaction(String),
44
45    #[error("Double spend detected in transaction input")]
46    DoubleSpend,
47
48    #[error("Too many reward transaction in block")]
49    RewardOverspend,
50
51    #[error("Reward transaction is invalid")]
52    InvalidRewardTransaction,
53
54    #[error("Reward transaction's outputs do not sum up to block reward amount")]
55    InvalidRewardTransactionAmount,
56
57    #[error("Reward transaction's id is missing")]
58    RewardTransactionIdMissing,
59
60    #[error("Reward transaction's outputs do not include a dev fee transaction")]
61    NoDevFee,
62
63    #[error("No blocks to to pop")]
64    NoBlocksToPop,
65
66    #[error("Block or transaction timestamp is invalid or expired")]
67    InvalidTimestamp,
68
69    #[error("Previous block hash is invalid")]
70    InvalidPreviousBlockHash,
71}
72
73impl From<TransactionError> for BlockchainError {
74    fn from(err: TransactionError) -> Self {
75        BlockchainError::InvalidTransaction(err.to_string())
76    }
77}
78
79/// BlockchainData is an object used for storing and loading current blockchain state.
80#[derive(Encode, Decode, Debug, Clone)]
81struct BlockchainData {
82    difficulty_manager: DifficultyManager,
83    height: usize,
84    block_lookup: HashMap<Hash, usize>,
85    utxos: UTXOs,
86}
87
88/// The blockchain, handles everything. Core to this crypto coin.
89#[derive(Debug)]
90pub struct Blockchain {
91    blockchain_path: String,
92    height: usize,
93    block_lookup: HashMap<Hash, usize>,
94    utxos: UTXOs,
95    difficulty_manager: DifficultyManager,
96}
97
98impl Blockchain {
99    /// Create a new blockchain or load one if exists at blockchain_path
100    pub fn new(blockchain_path: &str) -> Self {
101        let mut blockchain_path = blockchain_path.to_string();
102        if !blockchain_path.ends_with('/') {
103            blockchain_path.push('/');
104        }
105        blockchain_path.push_str("blockchain/");
106
107        if !Path::new(&blockchain_path).exists() {
108            fs::create_dir_all(format!("{}blocks/", &blockchain_path)).unwrap();
109        }
110
111        match Self::load_cache(&blockchain_path) {
112            Ok(cache) => {
113                return Blockchain {
114                    blockchain_path,
115                    height: cache.height,
116                    block_lookup: cache.block_lookup,
117                    utxos: cache.utxos,
118                    difficulty_manager: cache.difficulty_manager,
119                };
120            }
121            Err(_) => {
122                return Blockchain {
123                    blockchain_path,
124                    height: 0,
125                    block_lookup: HashMap::new(),
126                    utxos: UTXOs::new(),
127                    difficulty_manager: DifficultyManager::new(
128                        chrono::Utc::now().timestamp() as u64
129                    ),
130                };
131            }
132        }
133    }
134
135    /// Load the blockchain cache
136    fn load_cache(blockchain_path: &str) -> Result<BlockchainData, BlockchainError> {
137        let mut file = File::open(format!("{}blockchain.dat", blockchain_path))
138            .map_err(|e| BlockchainError::Io(e.to_string()))?;
139        Ok(bincode::decode_from_std_read(&mut file, config::standard())
140            .map_err(|e| BlockchainError::BincodeDecode(e.to_string()))?)
141    }
142
143    /// Save the blockchain cache
144    fn save_cache(&self) -> Result<(), BlockchainError> {
145        let mut file = File::create(format!("{}blockchain.dat", self.blockchain_path))
146            .map_err(|e| BlockchainError::Io(e.to_string()))?;
147        let cache = BlockchainData {
148            difficulty_manager: self.difficulty_manager,
149            height: self.height,
150            block_lookup: self.block_lookup.clone(),
151            utxos: self.utxos.clone(),
152        };
153        file.sync_all()
154            .map_err(|e| BlockchainError::Io(e.to_string()))?;
155
156        bincode::encode_into_std_write(cache, &mut file, config::standard())
157            .map_err(|e| BlockchainError::BincodeEncode(e.to_string()))?;
158        Ok(())
159    }
160
161    fn blocks_dir(&self) -> String {
162        format!("{}blocks/", &self.blockchain_path)
163    }
164    fn block_path_by_height(&self, height: usize) -> String {
165        format!("{}{}.dat", self.blocks_dir(), height)
166    }
167    fn utxo_diffs_path_by_height(&self, height: usize) -> String {
168        format!("{}utxo-diffs-{}.dat", self.blocks_dir(), height)
169    }
170    fn block_path_by_hash(&self, hash: &Hash) -> String {
171        format!("{}{}.dat", self.blocks_dir(), self.block_lookup[hash])
172    }
173
174    /// Add a block to the blockchain, and then save the state of it
175    /// Will return a blockchain error if the block or any of its included transactions are invalid
176    pub fn add_block(&mut self, new_block: Block) -> Result<(), BlockchainError> {
177        let block_hash = new_block.hash.ok_or(BlockchainError::MissingHash)?;
178
179        if !block_hash.compare_with_data(
180            &new_block
181                .get_hashing_buf()
182                .map_err(|e| BlockchainError::BincodeEncode(e.to_string()))?,
183        ) {
184            return Err(BlockchainError::HashMismatch {
185                expected: Hash::new(
186                    &new_block
187                        .get_hashing_buf()
188                        .map_err(|e| BlockchainError::BincodeEncode(e.to_string()))?,
189                )
190                .dump_base36(),
191                actual: block_hash.dump_base36(),
192            });
193        }
194
195        if new_block.timestamp > chrono::Utc::now().timestamp() as u64 {
196            return Err(BlockchainError::FutureTimestamp(new_block.timestamp));
197        }
198
199        if new_block.block_pow_difficulty != self.difficulty_manager.block_difficulty
200            || new_block.tx_pow_difficulty != self.difficulty_manager.transaction_difficulty
201        {
202            return Err(BlockchainError::InvalidDifficulty);
203        }
204
205        if self.get_height() == 0 && new_block.previous_block != GENESIS_PREVIOUS_BLOCK_HASH {
206            return Err(BlockchainError::InvalidPreviousBlockHash);
207        } else if self.get_height() != 0 && *self.get_block_hash_by_height(self.get_height() - 1).unwrap() != new_block.previous_block {
208            return Err(BlockchainError::InvalidPreviousBlockHash);
209        }
210
211        if BigUint::from_bytes_be(&*block_hash)
212            > BigUint::from_bytes_be(&calculate_block_difficulty(
213                &self.difficulty_manager.block_difficulty,
214                new_block.transactions.len(),
215            ))
216        {
217            return Err(BlockchainError::InvalidDifficulty);
218        }
219
220        let mut used_inputs: HashSet<(TransactionId, usize)> = HashSet::new();
221
222        let mut seen_reward_transaction = false;
223        for transaction in &new_block.transactions {
224            if !transaction.inputs.is_empty() {
225                self.utxos.validate_transaction(
226                    transaction,
227                    &BigUint::from_bytes_be(&self.difficulty_manager.transaction_difficulty),
228                )?;
229
230                for input in &transaction.inputs {
231                    let key = (input.transaction_id.clone(), input.output_index);
232                    if !used_inputs.insert(key.clone()) {
233                        return Err(BlockchainError::DoubleSpend);
234                    }
235                }
236
237                validate_transaction_timestamp_in_block(&transaction, &new_block)?;
238            } else {
239                if seen_reward_transaction {
240                    return Err(BlockchainError::RewardOverspend);
241                }
242                seen_reward_transaction = true;
243
244                if transaction.outputs.len() < 2 {
245                    return Err(BlockchainError::InvalidRewardTransaction);
246                }
247
248                if transaction.transaction_id.is_none() {
249                    return Err(BlockchainError::RewardTransactionIdMissing);
250                }
251
252                if transaction
253                    .outputs
254                    .iter()
255                    .fold(0, |acc, output| acc + output.amount)
256                    != get_block_reward(self.height)
257                {
258                    return Err(BlockchainError::InvalidRewardTransactionAmount);
259                }
260
261                let mut has_dev_fee = false;
262                for output in &transaction.outputs {
263                    if output.receiver == DEV_WALLET
264                        && output.amount == calculate_dev_fee(get_block_reward(self.height))
265                    {
266                        has_dev_fee = true;
267                        break;
268                    }
269                }
270
271                if !has_dev_fee {
272                    return Err(BlockchainError::NoDevFee);
273                }
274            }
275        }
276
277        let mut utxo_diffs = UTXODiff::new_empty();
278
279        for transaction in &new_block.transactions {
280            utxo_diffs.extend(&mut self.utxos.execute_transaction(transaction));
281        }
282
283        self.difficulty_manager.update_difficulty(&new_block);
284        self.block_lookup.insert(block_hash, self.height);
285
286        // Encode and save block
287        let mut file = File::create(self.block_path_by_height(self.height))
288            .map_err(|e| BlockchainError::Io(e.to_string()))?;
289        bincode::encode_into_std_write(new_block, &mut file, config::standard())
290            .map_err(|e| BlockchainError::Io(e.to_string()))?;
291        file.sync_all()
292            .map_err(|e| BlockchainError::Io(e.to_string()))?;
293
294        // Encode and save utxo diffs
295        let mut file = File::create(self.utxo_diffs_path_by_height(self.height))
296            .map_err(|e| BlockchainError::Io(e.to_string()))?;
297        bincode::encode_into_std_write(utxo_diffs, &mut file, config::standard())
298            .map_err(|e| BlockchainError::Io(e.to_string()))?;
299        file.sync_all()
300            .map_err(|e| BlockchainError::Io(e.to_string()))?;
301
302        self.height += 1;
303        self.save_cache()?;
304
305        Ok(())
306    }
307
308    /// 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
309    pub fn pop_block(&mut self) -> Result<(), BlockchainError> {
310        if self.height == 0 {
311            return Err(BlockchainError::NoBlocksToPop);
312        }
313
314        let recalled_block = self
315            .get_block_by_height(self.height - 1)
316            .ok_or(BlockchainError::MissingHash)?;
317        let utxo_diffs = self
318            .get_utxo_diffs_by_height(self.height - 1)
319            .ok_or(BlockchainError::MissingHash)?;
320
321        // Rollback UTXOs
322        self.utxos.recall_block_utxos(utxo_diffs);
323
324        // Remove the block file and the utxo diffs file
325        fs::remove_file(self.block_path_by_height(self.height - 1))
326            .map_err(|e| BlockchainError::Io(e.to_string()))?;
327        fs::remove_file(self.utxo_diffs_path_by_height(self.height - 1))
328            .map_err(|e| BlockchainError::Io(e.to_string()))?;
329
330        // Decrement height
331        self.height -= 1;
332
333        // Update difficulty manager
334        self.difficulty_manager.block_difficulty = recalled_block.block_pow_difficulty;
335        self.difficulty_manager.transaction_difficulty = recalled_block.tx_pow_difficulty;
336        if self.height > 0
337            && let Some(last_block) = self.get_block_by_height(self.height - 1)
338        {
339            self.difficulty_manager.last_timestamp = last_block.timestamp;
340        } else {
341            self.difficulty_manager.last_timestamp = recalled_block.timestamp;
342        }
343
344        // Save cache
345        self.block_lookup.remove(&recalled_block.hash.unwrap());
346        self.save_cache()?;
347
348        Ok(())
349    }
350
351    pub fn get_block_by_height(&self, height: usize) -> Option<Block> {
352        if height >= self.height {
353            return None;
354        }
355
356        let block_path = self.block_path_by_height(height);
357        let mut file = File::open(&block_path).ok()?;
358        bincode::decode_from_std_read(&mut file, config::standard()).ok()
359    }
360
361    pub fn get_utxo_diffs_by_height(&self, height: usize) -> Option<UTXODiff> {
362        if height >= self.height {
363            return None;
364        }
365
366        let diffs_path = self.utxo_diffs_path_by_height(height);
367        let mut file = File::open(&diffs_path).ok()?;
368        bincode::decode_from_std_read(&mut file, config::standard()).ok()
369    }
370
371    pub fn get_block_by_hash(&self, hash: &Hash) -> Option<Block> {
372        let block_path = self.block_path_by_hash(hash);
373        let mut file = File::open(&block_path).ok()?;
374        bincode::decode_from_std_read(&mut file, config::standard()).ok()
375    }
376
377    pub fn get_height_by_hash(&self, hash: &Hash) -> Option<usize> {
378        self.block_lookup.get(hash).copied()
379    }
380
381    pub fn get_block_hash_by_height(&self, height: usize) -> Option<&Hash> {
382        match self.block_lookup.iter().find(|x| x.1 == &height) {
383            Some(blt) => Some(blt.0),
384            None => None,
385        }
386    }
387
388    pub fn get_height(&self) -> usize {
389        self.height
390    }
391
392    pub fn get_utxos(&self) -> &UTXOs {
393        &self.utxos
394    }
395
396    pub fn get_difficulty_manager(&self) -> &DifficultyManager {
397        &self.difficulty_manager
398    }
399
400    pub fn get_transaction_difficulty(&self) -> [u8; 32] {
401        self.difficulty_manager.transaction_difficulty
402    }
403
404    pub fn get_block_difficulty(&self) -> [u8; 32] {
405        self.difficulty_manager.block_difficulty
406    }
407
408    pub fn get_all_blocks(&self) -> Vec<&Hash> {
409        self.block_lookup.keys().collect()
410    }
411}
412
413#[async_trait::async_trait]
414impl BlockchainDataProvider for Blockchain {
415    async fn get_height(
416        &self,
417    ) -> Result<usize, crate::blockchain_data_provider::BlockchainDataProviderError> {
418        Ok(self.get_height())
419    }
420
421    async fn get_reward(
422        &self,
423    ) -> Result<u64, crate::blockchain_data_provider::BlockchainDataProviderError> {
424        Ok(get_block_reward(self.get_height()))
425    }
426
427    async fn get_block_by_height(
428        &self,
429        height: usize,
430    ) -> Result<Option<Block>, crate::blockchain_data_provider::BlockchainDataProviderError> {
431        Ok(self.get_block_by_height(height))
432    }
433
434    async fn get_block_by_hash(
435        &self,
436        hash: &Hash,
437    ) -> Result<Option<Block>, crate::blockchain_data_provider::BlockchainDataProviderError> {
438        Ok(self.get_block_by_hash(hash))
439    }
440
441    async fn get_height_by_hash(
442        &self,
443        hash: &Hash,
444    ) -> Result<Option<usize>, crate::blockchain_data_provider::BlockchainDataProviderError> {
445        Ok(self.get_height_by_hash(hash))
446    }
447
448    async fn get_block_hash_by_height(
449        &self,
450        height: usize,
451    ) -> Result<Option<Hash>, crate::blockchain_data_provider::BlockchainDataProviderError> {
452        Ok(self.get_block_hash_by_height(height).copied())
453    }
454
455    async fn get_transaction_difficulty(
456        &self,
457    ) -> Result<[u8; 32], crate::blockchain_data_provider::BlockchainDataProviderError> {
458        Ok(self.get_transaction_difficulty())
459    }
460
461    async fn get_block_difficulty(
462        &self,
463    ) -> Result<[u8; 32], crate::blockchain_data_provider::BlockchainDataProviderError> {
464        Ok(self.get_block_difficulty())
465    }
466
467    async fn get_available_transaction_outputs(
468        &self,
469        address: crate::crypto::keys::Public,
470    ) -> Result<
471        Vec<(TransactionId, super::transaction::TransactionOutput, usize)>,
472        crate::blockchain_data_provider::BlockchainDataProviderError,
473    > {
474        let mut available_outputs = vec![];
475
476        for (transaction_id, outputs) in self.utxos.utxos.iter() {
477            for (output_index, output) in outputs.iter().enumerate() {
478                if let Some(output) = output
479                    && output.receiver == address
480                {
481                    available_outputs.push((*transaction_id, output.clone(), output_index));
482                }
483            }
484        }
485
486        Ok(available_outputs)
487    }
488}
489
490/// Returns true if transaction timestamp is valid in the context of a block
491pub fn validate_transaction_timestamp_in_block(
492    transaction: &Transaction,
493    owning_block: &Block,
494) -> Result<(), BlockchainError> {
495    if transaction.timestamp > owning_block.timestamp {
496        return Err(BlockchainError::InvalidTimestamp);
497    }
498    if transaction.timestamp + EXPIRATION_TIME < owning_block.timestamp {
499        return Err(BlockchainError::InvalidTimestamp);
500    }
501
502    Ok(())
503}
504
505/// Returns true if transaction timestamp is valid in the context of current time
506pub fn validate_transaction_timestamp(transaction: &Transaction) -> Result<(), BlockchainError> {
507    if transaction.timestamp > chrono::Utc::now().timestamp() as u64 {
508        return Err(BlockchainError::InvalidTimestamp);
509    }
510    if transaction.timestamp + EXPIRATION_TIME < chrono::Utc::now().timestamp() as u64 {
511        return Err(BlockchainError::InvalidTimestamp);
512    }
513
514    Ok(())
515}
516
517/// Returns false if block timestamp is valid
518pub fn validate_block_timestamp(block: &Block) -> Result<(), BlockchainError> {
519    if block.timestamp > chrono::Utc::now().timestamp() as u64 {
520        return Err(BlockchainError::InvalidTimestamp);
521    }
522    if block.timestamp + EXPIRATION_TIME < chrono::Utc::now().timestamp() as u64 {
523        return Err(BlockchainError::InvalidTimestamp);
524    }
525
526    Ok(())
527}