Skip to main content

hotmint_storage/
block_store.rs

1use hotmint_consensus::store::BlockStore;
2use hotmint_types::{Block, BlockHash, Height, QuorumCertificate};
3use ruc::*;
4use std::path::Path;
5use tracing::debug;
6use vsdb::MapxOrd;
7
8/// File name for the persisted instance IDs of the block store collections.
9const META_FILE: &str = "block_store.meta";
10
11/// Persistent block store backed by vsdb
12pub struct VsdbBlockStore {
13    by_hash: MapxOrd<[u8; 32], Block>,
14    by_height: MapxOrd<u64, [u8; 32]>,
15    commit_qcs: MapxOrd<u64, QuorumCertificate>,
16}
17
18impl VsdbBlockStore {
19    /// Opens an existing block store or creates a fresh one.
20    ///
21    /// Must be called after [`vsdb::vsdb_set_base_dir`].
22    /// The instance IDs of the three internal collections are stored in
23    /// `data_dir/block_store.meta` (24 bytes: three little-endian u64s).
24    /// On first run the file is created; on subsequent runs the collections
25    /// are recovered from their saved IDs via [`MapxOrd::from_meta`].
26    pub fn open(data_dir: &Path) -> Result<Self> {
27        let meta_path = data_dir.join(META_FILE);
28        if meta_path.exists() {
29            let bytes = std::fs::read(&meta_path).c(d!("read block_store.meta"))?;
30            if bytes.len() != 24 {
31                return Err(eg!(
32                    "corrupt block_store.meta: expected 24 bytes, got {}",
33                    bytes.len()
34                ));
35            }
36            let by_hash_id = u64::from_le_bytes(bytes[0..8].try_into().unwrap());
37            let by_height_id = u64::from_le_bytes(bytes[8..16].try_into().unwrap());
38            let commit_qcs_id = u64::from_le_bytes(bytes[16..24].try_into().unwrap());
39            Ok(Self {
40                by_hash: MapxOrd::from_meta(by_hash_id).c(d!("restore by_hash"))?,
41                by_height: MapxOrd::from_meta(by_height_id).c(d!("restore by_height"))?,
42                commit_qcs: MapxOrd::from_meta(commit_qcs_id).c(d!("restore commit_qcs"))?,
43            })
44        } else {
45            let by_hash: MapxOrd<[u8; 32], Block> = MapxOrd::new();
46            let by_height: MapxOrd<u64, [u8; 32]> = MapxOrd::new();
47            let commit_qcs: MapxOrd<u64, QuorumCertificate> = MapxOrd::new();
48
49            let by_hash_id = by_hash.save_meta().c(d!())?;
50            let by_height_id = by_height.save_meta().c(d!())?;
51            let commit_qcs_id = commit_qcs.save_meta().c(d!())?;
52
53            let mut meta = [0u8; 24];
54            meta[0..8].copy_from_slice(&by_hash_id.to_le_bytes());
55            meta[8..16].copy_from_slice(&by_height_id.to_le_bytes());
56            meta[16..24].copy_from_slice(&commit_qcs_id.to_le_bytes());
57            std::fs::write(&meta_path, meta).c(d!("write block_store.meta"))?;
58
59            let mut store = Self {
60                by_hash,
61                by_height,
62                commit_qcs,
63            };
64            store.put_block(Block::genesis());
65            Ok(store)
66        }
67    }
68
69    /// Creates a new in-memory block store without any persistent meta file.
70    /// Intended for unit tests only; use [`Self::open`] in production.
71    pub fn new() -> Self {
72        let mut store = Self {
73            by_hash: MapxOrd::new(),
74            by_height: MapxOrd::new(),
75            commit_qcs: MapxOrd::new(),
76        };
77        store.put_block(Block::genesis());
78        store
79    }
80
81    pub fn contains(&self, hash: &BlockHash) -> bool {
82        self.by_hash.contains_key(&hash.0)
83    }
84
85    pub fn flush(&self) {
86        vsdb::vsdb_flush();
87    }
88}
89
90impl Default for VsdbBlockStore {
91    fn default() -> Self {
92        Self::new()
93    }
94}
95
96impl BlockStore for VsdbBlockStore {
97    fn put_block(&mut self, block: Block) {
98        debug!(height = block.height.as_u64(), hash = %block.hash, "storing block to vsdb");
99        self.by_height.insert(&block.height.as_u64(), &block.hash.0);
100        self.by_hash.insert(&block.hash.0, &block);
101    }
102
103    fn get_block(&self, hash: &BlockHash) -> Option<Block> {
104        self.by_hash.get(&hash.0)
105    }
106
107    fn get_block_by_height(&self, h: Height) -> Option<Block> {
108        self.by_height
109            .get(&h.as_u64())
110            .and_then(|hash_bytes| self.by_hash.get(&hash_bytes))
111    }
112
113    fn get_blocks_in_range(&self, from: Height, to: Height) -> Vec<Block> {
114        self.by_height
115            .range(from.as_u64()..=to.as_u64())
116            .filter_map(|(_, hash_bytes)| self.by_hash.get(&hash_bytes))
117            .collect()
118    }
119
120    fn tip_height(&self) -> Height {
121        self.by_height
122            .last()
123            .map(|(h, _)| Height(h))
124            .unwrap_or(Height::GENESIS)
125    }
126
127    fn put_commit_qc(&mut self, height: Height, qc: QuorumCertificate) {
128        self.commit_qcs.insert(&height.as_u64(), &qc);
129    }
130
131    fn get_commit_qc(&self, height: Height) -> Option<QuorumCertificate> {
132        self.commit_qcs.get(&height.as_u64())
133    }
134
135    fn flush(&self) {
136        vsdb::vsdb_flush();
137    }
138}