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) -> Result<(), BlockchainError> {
170 new_block.check_meta()?;
171
172 new_block.validate_difficulties(
173 &self.get_block_difficulty(),
174 &self.get_transaction_difficulty(),
175 )?;
176
177 if self.block_store.get_last_block_hash() != new_block.meta.previous_block {
179 return Err(BlockchainError::InvalidPreviousBlockHash);
180 }
181
182 if new_block.transactions.len() > MAX_TRANSACTIONS_PER_BLOCK {
184 return Err(BlockchainError::TooManyTransactions);
185 }
186
187 let mut used_inputs: HashSet<(TransactionId, usize)> = HashSet::new();
188
189 let mut seen_reward_transaction = false;
190 for transaction in &new_block.transactions {
191 if !transaction.inputs.is_empty() {
192 self.utxos.validate_transaction(
195 transaction,
196 &BigUint::from_bytes_be(&self.get_transaction_difficulty()),
197 )?;
198
199 for input in &transaction.inputs {
201 let key = (input.transaction_id, input.output_index);
202 if !used_inputs.insert(key.clone()) {
203 return Err(BlockchainError::DoubleSpend);
204 }
205 }
206
207 validate_transaction_timestamp_in_block(&transaction, &new_block)?;
208 } else {
209 if seen_reward_transaction {
211 return Err(BlockchainError::RewardOverspend);
212 }
213 seen_reward_transaction = true;
214
215 if transaction.outputs.len() < 2 {
216 return Err(BlockchainError::InvalidRewardTransaction);
217 }
218
219 if transaction.transaction_id.is_none() {
220 return Err(BlockchainError::RewardTransactionIdMissing);
221 }
222
223 if transaction
224 .outputs
225 .iter()
226 .fold(0, |acc, output| acc + output.amount)
227 != get_block_reward(self.block_store().get_height())
228 {
229 return Err(BlockchainError::InvalidRewardTransactionAmount);
230 }
231
232 let mut has_dev_fee = false;
233 for output in &transaction.outputs {
234 if output.receiver == DEV_WALLET
235 && output.amount
236 == calculate_dev_fee(get_block_reward(self.block_store().get_height()))
237 {
238 has_dev_fee = true;
239 break;
240 }
241 }
242
243 if !has_dev_fee {
244 return Err(BlockchainError::NoDevFee);
245 }
246 }
247 }
248
249 let mut utxo_diffs = UTXODiff::new_empty();
251
252 for transaction in &new_block.transactions {
253 utxo_diffs.extend(&mut self.utxos.execute_transaction(transaction)?);
254 }
255
256 self.difficulty_state.update_difficulty(&new_block);
257
258 self.block_store().add_block(new_block, utxo_diffs)?;
259 self.save_blockchain_data()?;
260
261 Ok(())
262 }
263
264 pub fn pop_block(&self) -> Result<(), BlockchainError> {
266 if self.block_store().get_height() == 0 {
267 return Err(BlockchainError::NoBlocksToPop);
268 }
269
270 let recalled_block = self
271 .block_store()
272 .get_last_block()
273 .ok_or(BlockchainError::BlockNotFound)?;
274 let utxo_diffs = self
275 .block_store()
276 .get_last_utxo_diffs()
277 .ok_or(BlockchainError::BlockNotFound)?;
278
279 self.utxos.recall_block_utxos(&utxo_diffs)?;
281
282 self.block_store().pop_block()?;
283
284 *self.difficulty_state.block_difficulty.write().unwrap() =
286 recalled_block.meta.block_pow_difficulty;
287 *self
288 .difficulty_state
289 .transaction_difficulty
290 .write()
291 .unwrap() = recalled_block.meta.tx_pow_difficulty;
292 if self.block_store().get_height() > 0
293 && let Some(last_block) = self.block_store().get_last_block()
294 {
295 *self.difficulty_state.last_timestamp.write().unwrap() = last_block.timestamp;
296 } else {
297 *self.difficulty_state.last_timestamp.write().unwrap() = recalled_block.timestamp;
298 }
299
300 self.save_blockchain_data()?;
302
303 Ok(())
304 }
305
306 pub fn get_utxos(&self) -> &UTXOs {
307 &self.utxos
308 }
309
310 pub fn get_difficulty_manager(&self) -> &DifficultyState {
311 &self.difficulty_state
312 }
313
314 pub fn get_transaction_difficulty(&self) -> [u8; 32] {
315 *self.difficulty_state.transaction_difficulty.read().unwrap()
316 }
317
318 pub fn get_block_difficulty(&self) -> [u8; 32] {
319 *self.difficulty_state.block_difficulty.read().unwrap()
320 }
321
322 pub fn block_store(&self) -> &BlockStore {
323 &self.block_store
324 }
325}
326
327pub fn validate_transaction_timestamp_in_block(
329 transaction: &Transaction,
330 owning_block: &Block,
331) -> Result<(), BlockchainError> {
332 if transaction.timestamp > owning_block.timestamp {
333 return Err(BlockchainError::InvalidTimestamp);
334 }
335 if transaction.timestamp + EXPIRATION_TIME < owning_block.timestamp {
336 return Err(BlockchainError::InvalidTimestamp);
337 }
338
339 Ok(())
340}
341
342pub fn validate_transaction_timestamp(transaction: &Transaction) -> Result<(), BlockchainError> {
344 if transaction.timestamp - EXPIRATION_TIME > chrono::Utc::now().timestamp() as u64 {
345 return Err(BlockchainError::InvalidTimestamp);
346 }
347 if transaction.timestamp + EXPIRATION_TIME < chrono::Utc::now().timestamp() as u64 {
348 return Err(BlockchainError::InvalidTimestamp);
349 }
350
351 Ok(())
352}
353
354pub fn validate_block_timestamp(block: &Block) -> Result<(), BlockchainError> {
356 if block.timestamp - EXPIRATION_TIME > chrono::Utc::now().timestamp() as u64 {
357 return Err(BlockchainError::InvalidTimestamp);
358 }
359 if block.timestamp + EXPIRATION_TIME < chrono::Utc::now().timestamp() as u64 {
360 return Err(BlockchainError::InvalidTimestamp);
361 }
362
363 Ok(())
364}