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