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#[derive(Encode, Decode, Debug, Clone)]
94struct BlockchainData {
95 difficulty_state: DifficultyState,
96 block_store: BlockStore,
97}
98
99#[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 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 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 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 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 if self.block_store.get_last_block_hash() != new_block.meta.previous_block {
180 return Err(BlockchainError::InvalidPreviousBlockHash);
181 }
182
183 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 self.utxos.validate_transaction(
196 transaction,
197 &BigUint::from_bytes_be(&self.get_transaction_difficulty()),
198 is_ibd,
199 )?;
200
201 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 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 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 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 self.utxos.recall_block_utxos(&utxo_diffs)?;
283
284 self.block_store().pop_block()?;
285
286 *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 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
329pub 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
344pub 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
356pub 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}