Skip to main content

abtc_adapters/storage/
mod.rs

1//! Storage implementations
2//!
3//! Provides both in-memory (HashMap-based) and persistent (RocksDB-based)
4//! implementations of BlockStore and ChainStateStore traits.
5//!
6//! - `InMemoryBlockStore` / `InMemoryChainStateStore` — suitable for testing
7//! - `RocksDbBlockStore` / `RocksDbChainStateStore` — persistent, crash-safe
8//!   (requires the `rocksdb-storage` feature)
9
10#[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
23/// In-memory implementation of BlockStore using HashMap and RwLock
24pub 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    /// Create a new in-memory block store
32    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    /// Initialize with genesis block
41    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        // Update best block hash if this block extends the chain
80        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
136/// In-memory implementation of ChainStateStore using HashMap and RwLock
137pub struct InMemoryChainStateStore {
138    utxos: Arc<RwLock<HashMap<(Txid, u32), UtxoEntry>>>,
139    chain_tip: Arc<RwLock<(BlockHash, u32)>>,
140}
141
142impl InMemoryChainStateStore {
143    /// Create a new in-memory chain state store
144    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    /// Initialize with genesis block tip
152    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    // ── InMemoryBlockStore tests ────────────────────────────
273
274    #[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    // ── InMemoryChainStateStore tests ───────────────────────
374
375    #[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        // Add two UTXOs
421        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        // Add one, remove one
433        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        // Remove middle vout only
526        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    // ═══════════════════════════════════════════════════════════════════
564    // Regression tests — Session 14, Part 4 (code review fixes)
565    //
566    // These tests were written specifically for this implementation to
567    // guard against bugs found during a code review. They are NOT ports
568    // of Bitcoin Core test vectors. Each test name begins with
569    // `regression_` so it can be distinguished from the implementation
570    // unit tests above, which exercise baseline functionality.
571    // ═══════════════════════════════════════════════════════════════════
572
573    #[tokio::test]
574    async fn regression_store_block_updates_best_block_hash() {
575        // Review finding #8: store_block never updated best_block_hash, so
576        // get_best_block_hash always returned BlockHash::zero().
577        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        // Storing a higher block should update again.
587        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        // Safety net: storing a block at a lower height must NOT demote
598        // the best block hash.
599        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(); // lower height
607
608        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}