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