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#[derive(Encode, Decode, Debug, Clone)]
101struct BlockchainData {
102 difficulty_state: DifficultyState,
103 block_store: BlockStore,
104}
105
106#[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 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 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 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 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 if self.block_store.get_last_block_hash() != new_block.meta.previous_block {
194 return Err(BlockchainError::InvalidPreviousBlockHash);
195 }
196
197 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 self.utxos.validate_transaction(
210 transaction,
211 &BigUint::from_bytes_be(&self.get_transaction_difficulty()),
212 )?;
213
214 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 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 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 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 self.utxos.recall_block_utxos(&utxo_diffs)?;
296
297 self.block_store().pop_block()?;
298
299 *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 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
346pub 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
361pub 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
373pub 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}