Skip to main content

hotmint_storage/
block_store.rs

1use hotmint_consensus::store::BlockStore;
2use hotmint_types::{Block, BlockHash, EndBlockResponse, 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    /// tx_hash → (height, tx_index_in_block)
17    tx_index: MapxOrd<[u8; 32], (u64, u32)>,
18    /// height → EndBlockResponse (block execution results)
19    block_results: MapxOrd<u64, EndBlockResponse>,
20}
21
22impl VsdbBlockStore {
23    /// Opens an existing block store or creates a fresh one.
24    ///
25    /// Must be called after [`vsdb::vsdb_set_base_dir`].
26    /// The instance IDs of the internal collections are stored in
27    /// `data_dir/block_store.meta`. On first run the file is created;
28    /// on subsequent runs the collections are recovered from saved IDs.
29    ///
30    /// Backward-compatible: 24-byte meta (v1, 3 collections) is auto-migrated
31    /// to 40 bytes (v2, 5 collections) on first open.
32    pub fn open(data_dir: &Path) -> Result<Self> {
33        let meta_path = data_dir.join(META_FILE);
34        if meta_path.exists() {
35            let bytes = std::fs::read(&meta_path).c(d!("read block_store.meta"))?;
36            if bytes.len() == 24 {
37                // v1 meta: migrate by creating two new collections.
38                let by_hash_id = u64::from_le_bytes(bytes[0..8].try_into().unwrap());
39                let by_height_id = u64::from_le_bytes(bytes[8..16].try_into().unwrap());
40                let commit_qcs_id = u64::from_le_bytes(bytes[16..24].try_into().unwrap());
41                let tx_index: MapxOrd<[u8; 32], (u64, u32)> = MapxOrd::new();
42                let block_results: MapxOrd<u64, EndBlockResponse> = MapxOrd::new();
43                let tx_index_id = tx_index.save_meta().c(d!())?;
44                let block_results_id = block_results.save_meta().c(d!())?;
45                let mut meta = [0u8; 40];
46                meta[0..8].copy_from_slice(&by_hash_id.to_le_bytes());
47                meta[8..16].copy_from_slice(&by_height_id.to_le_bytes());
48                meta[16..24].copy_from_slice(&commit_qcs_id.to_le_bytes());
49                meta[24..32].copy_from_slice(&tx_index_id.to_le_bytes());
50                meta[32..40].copy_from_slice(&block_results_id.to_le_bytes());
51                std::fs::write(&meta_path, meta).c(d!("write block_store.meta v2"))?;
52                Ok(Self {
53                    by_hash: MapxOrd::from_meta(by_hash_id).c(d!("restore by_hash"))?,
54                    by_height: MapxOrd::from_meta(by_height_id).c(d!("restore by_height"))?,
55                    commit_qcs: MapxOrd::from_meta(commit_qcs_id).c(d!("restore commit_qcs"))?,
56                    tx_index,
57                    block_results,
58                })
59            } else if bytes.len() == 40 {
60                let by_hash_id = u64::from_le_bytes(bytes[0..8].try_into().unwrap());
61                let by_height_id = u64::from_le_bytes(bytes[8..16].try_into().unwrap());
62                let commit_qcs_id = u64::from_le_bytes(bytes[16..24].try_into().unwrap());
63                let tx_index_id = u64::from_le_bytes(bytes[24..32].try_into().unwrap());
64                let block_results_id = u64::from_le_bytes(bytes[32..40].try_into().unwrap());
65                Ok(Self {
66                    by_hash: MapxOrd::from_meta(by_hash_id).c(d!("restore by_hash"))?,
67                    by_height: MapxOrd::from_meta(by_height_id).c(d!("restore by_height"))?,
68                    commit_qcs: MapxOrd::from_meta(commit_qcs_id).c(d!("restore commit_qcs"))?,
69                    tx_index: MapxOrd::from_meta(tx_index_id).c(d!("restore tx_index"))?,
70                    block_results: MapxOrd::from_meta(block_results_id)
71                        .c(d!("restore block_results"))?,
72                })
73            } else {
74                Err(eg!(
75                    "corrupt block_store.meta: expected 24 or 40 bytes, got {}",
76                    bytes.len()
77                ))
78            }
79        } else {
80            let by_hash: MapxOrd<[u8; 32], Block> = MapxOrd::new();
81            let by_height: MapxOrd<u64, [u8; 32]> = MapxOrd::new();
82            let commit_qcs: MapxOrd<u64, QuorumCertificate> = MapxOrd::new();
83            let tx_index: MapxOrd<[u8; 32], (u64, u32)> = MapxOrd::new();
84            let block_results: MapxOrd<u64, EndBlockResponse> = MapxOrd::new();
85
86            let by_hash_id = by_hash.save_meta().c(d!())?;
87            let by_height_id = by_height.save_meta().c(d!())?;
88            let commit_qcs_id = commit_qcs.save_meta().c(d!())?;
89            let tx_index_id = tx_index.save_meta().c(d!())?;
90            let block_results_id = block_results.save_meta().c(d!())?;
91
92            let mut meta = [0u8; 40];
93            meta[0..8].copy_from_slice(&by_hash_id.to_le_bytes());
94            meta[8..16].copy_from_slice(&by_height_id.to_le_bytes());
95            meta[16..24].copy_from_slice(&commit_qcs_id.to_le_bytes());
96            meta[24..32].copy_from_slice(&tx_index_id.to_le_bytes());
97            meta[32..40].copy_from_slice(&block_results_id.to_le_bytes());
98            std::fs::write(&meta_path, meta).c(d!("write block_store.meta"))?;
99
100            let mut store = Self {
101                by_hash,
102                by_height,
103                commit_qcs,
104                tx_index,
105                block_results,
106            };
107            store.put_block(Block::genesis());
108            Ok(store)
109        }
110    }
111
112    /// Creates a new in-memory block store without any persistent meta file.
113    /// Intended for unit tests only; use [`Self::open`] in production.
114    pub fn new() -> Self {
115        let mut store = Self {
116            by_hash: MapxOrd::new(),
117            by_height: MapxOrd::new(),
118            commit_qcs: MapxOrd::new(),
119            tx_index: MapxOrd::new(),
120            block_results: MapxOrd::new(),
121        };
122        store.put_block(Block::genesis());
123        store
124    }
125
126    pub fn contains(&self, hash: &BlockHash) -> bool {
127        self.by_hash.contains_key(&hash.0)
128    }
129
130    pub fn flush(&self) {
131        vsdb::vsdb_flush();
132    }
133}
134
135impl Default for VsdbBlockStore {
136    fn default() -> Self {
137        Self::new()
138    }
139}
140
141impl BlockStore for VsdbBlockStore {
142    fn put_block(&mut self, block: Block) {
143        debug!(height = block.height.as_u64(), hash = %block.hash, "storing block to vsdb");
144        self.by_height.insert(&block.height.as_u64(), &block.hash.0);
145        self.by_hash.insert(&block.hash.0, &block);
146    }
147
148    fn get_block(&self, hash: &BlockHash) -> Option<Block> {
149        self.by_hash.get(&hash.0)
150    }
151
152    fn get_block_by_height(&self, h: Height) -> Option<Block> {
153        self.by_height
154            .get(&h.as_u64())
155            .and_then(|hash_bytes| self.by_hash.get(&hash_bytes))
156    }
157
158    fn get_blocks_in_range(&self, from: Height, to: Height) -> Vec<Block> {
159        self.by_height
160            .range(from.as_u64()..=to.as_u64())
161            .filter_map(|(_, hash_bytes)| self.by_hash.get(&hash_bytes))
162            .collect()
163    }
164
165    fn tip_height(&self) -> Height {
166        self.by_height
167            .last()
168            .map(|(h, _)| Height(h))
169            .unwrap_or(Height::GENESIS)
170    }
171
172    fn put_commit_qc(&mut self, height: Height, qc: QuorumCertificate) {
173        self.commit_qcs.insert(&height.as_u64(), &qc);
174    }
175
176    fn get_commit_qc(&self, height: Height) -> Option<QuorumCertificate> {
177        self.commit_qcs.get(&height.as_u64())
178    }
179
180    fn flush(&self) {
181        vsdb::vsdb_flush();
182    }
183
184    fn put_tx_index(&mut self, tx_hash: [u8; 32], height: Height, index: u32) {
185        self.tx_index.insert(&tx_hash, &(height.as_u64(), index));
186    }
187
188    fn get_tx_location(&self, tx_hash: &[u8; 32]) -> Option<(Height, u32)> {
189        self.tx_index.get(tx_hash).map(|(h, idx)| (Height(h), idx))
190    }
191
192    fn put_block_results(&mut self, height: Height, results: EndBlockResponse) {
193        self.block_results.insert(&height.as_u64(), &results);
194    }
195
196    fn get_block_results(&self, height: Height) -> Option<EndBlockResponse> {
197        self.block_results.get(&height.as_u64())
198    }
199}