1#[cfg(feature = "rocksdb-storage")]
11pub mod rocksdb_store;
12
13#[cfg(feature = "rocksdb-storage")]
14pub use rocksdb_store::{RocksDbBlockStore, RocksDbChainStateStore};
15
16use abtc_domain::primitives::{Block, BlockHash, Txid};
17use abtc_ports::{BlockStore, ChainStateStore, UtxoEntry, UtxoSetInfo};
18use async_trait::async_trait;
19use std::collections::HashMap;
20use std::sync::Arc;
21use tokio::sync::RwLock;
22
23pub struct InMemoryBlockStore {
25 blocks: Arc<RwLock<HashMap<BlockHash, Block>>>,
26 best_block_hash: Arc<RwLock<BlockHash>>,
27 block_heights: Arc<RwLock<HashMap<BlockHash, u32>>>,
28}
29
30impl InMemoryBlockStore {
31 pub fn new() -> Self {
33 InMemoryBlockStore {
34 blocks: Arc::new(RwLock::new(HashMap::new())),
35 best_block_hash: Arc::new(RwLock::new(BlockHash::zero())),
36 block_heights: Arc::new(RwLock::new(HashMap::new())),
37 }
38 }
39
40 pub async fn init_with_genesis(&self, genesis: Block) {
42 let genesis_hash = genesis.block_hash();
43 let mut blocks = self.blocks.write().await;
44 blocks.insert(genesis_hash, genesis);
45
46 let mut best = self.best_block_hash.write().await;
47 *best = genesis_hash;
48
49 let mut heights = self.block_heights.write().await;
50 heights.insert(genesis_hash, 0);
51
52 tracing::debug!(
53 "Initialized block store with genesis block: {}",
54 genesis_hash
55 );
56 }
57}
58
59impl Default for InMemoryBlockStore {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65#[async_trait]
66impl BlockStore for InMemoryBlockStore {
67 async fn store_block(
68 &self,
69 block: &Block,
70 height: u32,
71 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
72 let block_hash = block.block_hash();
73 let mut blocks = self.blocks.write().await;
74 blocks.insert(block_hash, block.clone());
75
76 let mut heights = self.block_heights.write().await;
77 heights.insert(block_hash, height);
78
79 let current_best_height = {
81 let best = self.best_block_hash.read().await;
82 heights.get(&*best).copied().unwrap_or(0)
83 };
84 if height > current_best_height {
85 let mut best = self.best_block_hash.write().await;
86 *best = block_hash;
87 }
88
89 tracing::debug!("Stored block {} at height {}", block_hash, height);
90 Ok(())
91 }
92
93 async fn get_block(
94 &self,
95 hash: &BlockHash,
96 ) -> Result<Option<Block>, Box<dyn std::error::Error + Send + Sync>> {
97 let blocks = self.blocks.read().await;
98 Ok(blocks.get(hash).cloned())
99 }
100
101 async fn get_block_header(
102 &self,
103 hash: &BlockHash,
104 ) -> Result<
105 Option<abtc_domain::primitives::BlockHeader>,
106 Box<dyn std::error::Error + Send + Sync>,
107 > {
108 let blocks = self.blocks.read().await;
109 Ok(blocks.get(hash).map(|b| b.header.clone()))
110 }
111
112 async fn has_block(
113 &self,
114 hash: &BlockHash,
115 ) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
116 let blocks = self.blocks.read().await;
117 Ok(blocks.contains_key(hash))
118 }
119
120 async fn get_best_block_hash(
121 &self,
122 ) -> Result<BlockHash, Box<dyn std::error::Error + Send + Sync>> {
123 let best = self.best_block_hash.read().await;
124 Ok(*best)
125 }
126
127 async fn get_block_height(
128 &self,
129 hash: &BlockHash,
130 ) -> Result<Option<u32>, Box<dyn std::error::Error + Send + Sync>> {
131 let heights = self.block_heights.read().await;
132 Ok(heights.get(hash).copied())
133 }
134}
135
136pub struct InMemoryChainStateStore {
138 utxos: Arc<RwLock<HashMap<(Txid, u32), UtxoEntry>>>,
139 chain_tip: Arc<RwLock<(BlockHash, u32)>>,
140}
141
142impl InMemoryChainStateStore {
143 pub fn new() -> Self {
145 InMemoryChainStateStore {
146 utxos: Arc::new(RwLock::new(HashMap::new())),
147 chain_tip: Arc::new(RwLock::new((BlockHash::zero(), 0))),
148 }
149 }
150
151 pub async fn init_with_genesis(&self, genesis_hash: BlockHash) {
153 let mut tip = self.chain_tip.write().await;
154 *tip = (genesis_hash, 0);
155 tracing::debug!(
156 "Initialized chain state store with genesis tip: {}",
157 genesis_hash
158 );
159 }
160}
161
162impl Default for InMemoryChainStateStore {
163 fn default() -> Self {
164 Self::new()
165 }
166}
167
168#[async_trait]
169impl ChainStateStore for InMemoryChainStateStore {
170 async fn get_utxo(
171 &self,
172 txid: &Txid,
173 vout: u32,
174 ) -> Result<Option<UtxoEntry>, Box<dyn std::error::Error + Send + Sync>> {
175 let utxos = self.utxos.read().await;
176 Ok(utxos.get(&(*txid, vout)).cloned())
177 }
178
179 async fn has_utxo(
180 &self,
181 txid: &Txid,
182 vout: u32,
183 ) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
184 let utxos = self.utxos.read().await;
185 Ok(utxos.contains_key(&(*txid, vout)))
186 }
187
188 async fn write_utxo_set(
189 &self,
190 adds: Vec<(Txid, u32, UtxoEntry)>,
191 removes: Vec<(Txid, u32)>,
192 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
193 let mut utxos = self.utxos.write().await;
194
195 for (txid, vout, entry) in adds {
196 utxos.insert((txid, vout), entry);
197 }
198
199 for (txid, vout) in removes {
200 utxos.remove(&(txid, vout));
201 }
202
203 tracing::debug!("Updated UTXO set");
204 Ok(())
205 }
206
207 async fn get_best_chain_tip(
208 &self,
209 ) -> Result<(BlockHash, u32), Box<dyn std::error::Error + Send + Sync>> {
210 let tip = self.chain_tip.read().await;
211 Ok(*tip)
212 }
213
214 async fn write_chain_tip(
215 &self,
216 hash: BlockHash,
217 height: u32,
218 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
219 let mut tip = self.chain_tip.write().await;
220 *tip = (hash, height);
221 tracing::debug!("Updated chain tip to {} (height {})", hash, height);
222 Ok(())
223 }
224
225 async fn get_utxo_set_info(
226 &self,
227 ) -> Result<UtxoSetInfo, Box<dyn std::error::Error + Send + Sync>> {
228 let utxos = self.utxos.read().await;
229 let tip = self.chain_tip.read().await;
230
231 let txout_count = utxos.len() as u64;
232 let total_sats: i64 = utxos.values().map(|e| e.output.value.as_sat()).sum();
233
234 Ok(UtxoSetInfo {
235 txout_count,
236 total_amount: abtc_domain::primitives::Amount::from_sat(total_sats),
237 best_block: tip.0,
238 height: tip.1,
239 })
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use abtc_domain::primitives::{Amount, Block, BlockHash, BlockHeader, Hash256, TxOut, Txid};
247 use abtc_domain::Script;
248
249 fn make_header(prev: BlockHash, nonce: u32) -> BlockHeader {
250 BlockHeader {
251 version: 1,
252 prev_block_hash: prev,
253 merkle_root: Hash256::from_bytes([nonce as u8; 32]),
254 time: 1231006505 + nonce,
255 bits: 0x1d00ffff,
256 nonce,
257 }
258 }
259
260 fn make_block(prev: BlockHash, nonce: u32) -> Block {
261 Block::new(make_header(prev, nonce), vec![])
262 }
263
264 fn make_utxo_entry(value: i64, height: u32, is_coinbase: bool) -> UtxoEntry {
265 UtxoEntry {
266 output: TxOut::new(Amount::from_sat(value), Script::new()),
267 height,
268 is_coinbase,
269 }
270 }
271
272 #[tokio::test]
275 async fn test_block_store_creation() {
276 let store = InMemoryBlockStore::new();
277 let best = store.get_best_block_hash().await.unwrap();
278 assert_eq!(best, BlockHash::zero());
279 }
280
281 #[tokio::test]
282 async fn test_store_and_retrieve_block() {
283 let store = InMemoryBlockStore::new();
284 let block = make_block(BlockHash::zero(), 1);
285 let hash = block.block_hash();
286
287 store.store_block(&block, 0).await.unwrap();
288
289 let retrieved = store.get_block(&hash).await.unwrap();
290 assert!(retrieved.is_some());
291 assert_eq!(retrieved.unwrap().block_hash(), hash);
292 }
293
294 #[tokio::test]
295 async fn test_get_nonexistent_block() {
296 let store = InMemoryBlockStore::new();
297 let fake = BlockHash::from_hash(Hash256::from_bytes([0xFF; 32]));
298 assert!(store.get_block(&fake).await.unwrap().is_none());
299 }
300
301 #[tokio::test]
302 async fn test_has_block() {
303 let store = InMemoryBlockStore::new();
304 let block = make_block(BlockHash::zero(), 1);
305 let hash = block.block_hash();
306
307 assert!(!store.has_block(&hash).await.unwrap());
308 store.store_block(&block, 0).await.unwrap();
309 assert!(store.has_block(&hash).await.unwrap());
310 }
311
312 #[tokio::test]
313 async fn test_get_block_header() {
314 let store = InMemoryBlockStore::new();
315 let block = make_block(BlockHash::zero(), 42);
316 let hash = block.block_hash();
317
318 store.store_block(&block, 5).await.unwrap();
319
320 let header = store.get_block_header(&hash).await.unwrap();
321 assert!(header.is_some());
322 assert_eq!(header.unwrap().nonce, 42);
323 }
324
325 #[tokio::test]
326 async fn test_get_block_height() {
327 let store = InMemoryBlockStore::new();
328 let block = make_block(BlockHash::zero(), 1);
329 let hash = block.block_hash();
330
331 store.store_block(&block, 100).await.unwrap();
332 assert_eq!(store.get_block_height(&hash).await.unwrap(), Some(100));
333 }
334
335 #[tokio::test]
336 async fn test_store_multiple_blocks_chain() {
337 let store = InMemoryBlockStore::new();
338
339 let b1 = make_block(BlockHash::zero(), 1);
340 let h1 = b1.block_hash();
341 store.store_block(&b1, 0).await.unwrap();
342
343 let b2 = make_block(h1, 2);
344 let h2 = b2.block_hash();
345 store.store_block(&b2, 1).await.unwrap();
346
347 let b3 = make_block(h2, 3);
348 let h3 = b3.block_hash();
349 store.store_block(&b3, 2).await.unwrap();
350
351 assert!(store.has_block(&h1).await.unwrap());
352 assert!(store.has_block(&h2).await.unwrap());
353 assert!(store.has_block(&h3).await.unwrap());
354 assert_eq!(store.get_block_height(&h3).await.unwrap(), Some(2));
355 }
356
357 #[tokio::test]
358 async fn test_init_with_genesis() {
359 let store = InMemoryBlockStore::new();
360 let genesis = make_block(BlockHash::zero(), 0);
361 let genesis_hash = genesis.block_hash();
362
363 store.init_with_genesis(genesis).await;
364
365 assert!(store.has_block(&genesis_hash).await.unwrap());
366 assert_eq!(store.get_best_block_hash().await.unwrap(), genesis_hash);
367 assert_eq!(
368 store.get_block_height(&genesis_hash).await.unwrap(),
369 Some(0)
370 );
371 }
372
373 #[tokio::test]
376 async fn test_chain_state_store_creation() {
377 let store = InMemoryChainStateStore::new();
378 let (tip, height) = store.get_best_chain_tip().await.unwrap();
379 assert_eq!(tip, BlockHash::zero());
380 assert_eq!(height, 0);
381 }
382
383 #[tokio::test]
384 async fn test_write_and_read_utxo() {
385 let store = InMemoryChainStateStore::new();
386 let txid = Txid::from_hash(Hash256::from_bytes([0x01; 32]));
387
388 store
389 .write_utxo_set(vec![(txid, 0, make_utxo_entry(50_000, 10, false))], vec![])
390 .await
391 .unwrap();
392
393 let utxo = store.get_utxo(&txid, 0).await.unwrap();
394 assert!(utxo.is_some());
395 assert_eq!(utxo.unwrap().output.value.as_sat(), 50_000);
396 }
397
398 #[tokio::test]
399 async fn test_has_utxo() {
400 let store = InMemoryChainStateStore::new();
401 let txid = Txid::from_hash(Hash256::from_bytes([0x02; 32]));
402
403 assert!(!store.has_utxo(&txid, 0).await.unwrap());
404
405 store
406 .write_utxo_set(vec![(txid, 0, make_utxo_entry(100, 1, false))], vec![])
407 .await
408 .unwrap();
409
410 assert!(store.has_utxo(&txid, 0).await.unwrap());
411 assert!(!store.has_utxo(&txid, 1).await.unwrap());
412 }
413
414 #[tokio::test]
415 async fn test_atomic_add_and_remove() {
416 let store = InMemoryChainStateStore::new();
417 let txid1 = Txid::from_hash(Hash256::from_bytes([0x01; 32]));
418 let txid2 = Txid::from_hash(Hash256::from_bytes([0x02; 32]));
419
420 store
422 .write_utxo_set(
423 vec![
424 (txid1, 0, make_utxo_entry(100, 1, false)),
425 (txid2, 0, make_utxo_entry(200, 1, false)),
426 ],
427 vec![],
428 )
429 .await
430 .unwrap();
431
432 let txid3 = Txid::from_hash(Hash256::from_bytes([0x03; 32]));
434 store
435 .write_utxo_set(
436 vec![(txid3, 0, make_utxo_entry(300, 2, false))],
437 vec![(txid1, 0)],
438 )
439 .await
440 .unwrap();
441
442 assert!(!store.has_utxo(&txid1, 0).await.unwrap());
443 assert!(store.has_utxo(&txid2, 0).await.unwrap());
444 assert!(store.has_utxo(&txid3, 0).await.unwrap());
445 }
446
447 #[tokio::test]
448 async fn test_write_and_read_chain_tip() {
449 let store = InMemoryChainStateStore::new();
450 let tip_hash = BlockHash::from_hash(Hash256::from_bytes([0x42; 32]));
451
452 store.write_chain_tip(tip_hash, 500).await.unwrap();
453
454 let (hash, height) = store.get_best_chain_tip().await.unwrap();
455 assert_eq!(hash, tip_hash);
456 assert_eq!(height, 500);
457 }
458
459 #[tokio::test]
460 async fn test_utxo_set_info() {
461 let store = InMemoryChainStateStore::new();
462 let txid1 = Txid::from_hash(Hash256::from_bytes([0x01; 32]));
463 let txid2 = Txid::from_hash(Hash256::from_bytes([0x02; 32]));
464 let tip = BlockHash::from_hash(Hash256::from_bytes([0xFF; 32]));
465
466 store
467 .write_utxo_set(
468 vec![
469 (txid1, 0, make_utxo_entry(100_000, 1, false)),
470 (txid2, 0, make_utxo_entry(200_000, 2, false)),
471 ],
472 vec![],
473 )
474 .await
475 .unwrap();
476 store.write_chain_tip(tip, 10).await.unwrap();
477
478 let info = store.get_utxo_set_info().await.unwrap();
479 assert_eq!(info.txout_count, 2);
480 assert_eq!(info.total_amount.as_sat(), 300_000);
481 assert_eq!(info.best_block, tip);
482 assert_eq!(info.height, 10);
483 }
484
485 #[tokio::test]
486 async fn test_multiple_vouts_same_txid() {
487 let store = InMemoryChainStateStore::new();
488 let txid = Txid::from_hash(Hash256::from_bytes([0x10; 32]));
489
490 store
491 .write_utxo_set(
492 vec![
493 (txid, 0, make_utxo_entry(1_000, 5, false)),
494 (txid, 1, make_utxo_entry(2_000, 5, false)),
495 (txid, 2, make_utxo_entry(3_000, 5, false)),
496 ],
497 vec![],
498 )
499 .await
500 .unwrap();
501
502 assert_eq!(
503 store
504 .get_utxo(&txid, 0)
505 .await
506 .unwrap()
507 .unwrap()
508 .output
509 .value
510 .as_sat(),
511 1_000
512 );
513 assert_eq!(
514 store
515 .get_utxo(&txid, 2)
516 .await
517 .unwrap()
518 .unwrap()
519 .output
520 .value
521 .as_sat(),
522 3_000
523 );
524
525 store.write_utxo_set(vec![], vec![(txid, 1)]).await.unwrap();
527 assert!(store.has_utxo(&txid, 0).await.unwrap());
528 assert!(!store.has_utxo(&txid, 1).await.unwrap());
529 assert!(store.has_utxo(&txid, 2).await.unwrap());
530 }
531
532 #[tokio::test]
533 async fn test_coinbase_utxo_entry() {
534 let store = InMemoryChainStateStore::new();
535 let txid = Txid::from_hash(Hash256::from_bytes([0xCB; 32]));
536
537 store
538 .write_utxo_set(
539 vec![(txid, 0, make_utxo_entry(5_000_000_000, 0, true))],
540 vec![],
541 )
542 .await
543 .unwrap();
544
545 let entry = store.get_utxo(&txid, 0).await.unwrap().unwrap();
546 assert!(entry.is_coinbase);
547 assert_eq!(entry.height, 0);
548 assert_eq!(entry.output.value.as_sat(), 5_000_000_000);
549 }
550
551 #[tokio::test]
552 async fn test_init_chain_state_genesis() {
553 let store = InMemoryChainStateStore::new();
554 let genesis_hash = BlockHash::from_hash(Hash256::from_bytes([0xAA; 32]));
555
556 store.init_with_genesis(genesis_hash).await;
557
558 let (tip, height) = store.get_best_chain_tip().await.unwrap();
559 assert_eq!(tip, genesis_hash);
560 assert_eq!(height, 0);
561 }
562
563 #[tokio::test]
574 async fn regression_store_block_updates_best_block_hash() {
575 let store = InMemoryBlockStore::new();
578
579 let b1 = make_block(BlockHash::zero(), 1);
580 let h1 = b1.block_hash();
581 store.store_block(&b1, 1).await.unwrap();
582
583 let best = store.get_best_block_hash().await.unwrap();
584 assert_eq!(best, h1, "best block hash should update after store_block");
585
586 let b2 = make_block(h1, 2);
588 let h2 = b2.block_hash();
589 store.store_block(&b2, 2).await.unwrap();
590
591 let best = store.get_best_block_hash().await.unwrap();
592 assert_eq!(best, h2, "best block hash should follow the highest block");
593 }
594
595 #[tokio::test]
596 async fn regression_store_block_does_not_regress_best_hash() {
597 let store = InMemoryBlockStore::new();
600
601 let b1 = make_block(BlockHash::zero(), 1);
602 let h1 = b1.block_hash();
603 store.store_block(&b1, 10).await.unwrap();
604
605 let b2 = make_block(BlockHash::zero(), 99);
606 store.store_block(&b2, 5).await.unwrap(); let best = store.get_best_block_hash().await.unwrap();
609 assert_eq!(
610 best, h1,
611 "best block hash should not regress to a lower height"
612 );
613 }
614}