Skip to main content

hotmint_consensus/
store.rs

1use std::collections::{BTreeMap, HashMap};
2use std::sync::Arc;
3
4use parking_lot::RwLock;
5
6use hotmint_types::{Block, BlockHash, Height, QuorumCertificate};
7
8pub trait BlockStore: Send + Sync {
9    fn put_block(&mut self, block: Block);
10    fn get_block(&self, hash: &BlockHash) -> Option<Block>;
11    fn get_block_by_height(&self, h: Height) -> Option<Block>;
12
13    /// Store the QC that committed a block at the given height.
14    fn put_commit_qc(&mut self, _height: Height, _qc: QuorumCertificate) {}
15    /// Retrieve the commit QC for a block at the given height.
16    fn get_commit_qc(&self, _height: Height) -> Option<QuorumCertificate> {
17        None
18    }
19
20    /// Flush pending writes to durable storage.
21    fn flush(&self) {}
22
23    /// Get blocks in [from, to] inclusive. Default iterates one-by-one.
24    fn get_blocks_in_range(&self, from: Height, to: Height) -> Vec<Block> {
25        let mut blocks = Vec::new();
26        let mut h = from.as_u64();
27        while h <= to.as_u64() {
28            if let Some(block) = self.get_block_by_height(Height(h)) {
29                blocks.push(block);
30            }
31            h += 1;
32        }
33        blocks
34    }
35
36    /// Return the highest stored block height.
37    fn tip_height(&self) -> Height {
38        Height::GENESIS
39    }
40
41    /// Store a tx hash → (height, index) mapping.
42    fn put_tx_index(&mut self, _tx_hash: [u8; 32], _height: Height, _index: u32) {}
43
44    /// Look up a tx hash → (height, index_in_block).
45    fn get_tx_location(&self, _tx_hash: &[u8; 32]) -> Option<(Height, u32)> {
46        None
47    }
48
49    /// Store the EndBlockResponse for a given height.
50    fn put_block_results(&mut self, _height: Height, _results: hotmint_types::EndBlockResponse) {}
51
52    /// Retrieve the EndBlockResponse for a given height.
53    fn get_block_results(&self, _height: Height) -> Option<hotmint_types::EndBlockResponse> {
54        None
55    }
56}
57
58/// Adapter that implements `BlockStore` over a shared `Arc<RwLock<Box<dyn BlockStore>>>`,
59/// acquiring and releasing the lock for each individual operation.
60///
61/// Uses `read` / `write` on `parking_lot::RwLock` (R-1) for fast, non-poisoning
62/// synchronous locking. The lock hold times are microsecond-level HashMap lookups.
63pub struct SharedStoreAdapter(pub Arc<RwLock<Box<dyn BlockStore>>>);
64
65impl BlockStore for SharedStoreAdapter {
66    fn put_block(&mut self, block: Block) {
67        self.0.write().put_block(block);
68    }
69    fn get_block(&self, hash: &BlockHash) -> Option<Block> {
70        self.0.read().get_block(hash)
71    }
72    fn get_block_by_height(&self, h: Height) -> Option<Block> {
73        self.0.read().get_block_by_height(h)
74    }
75    fn get_blocks_in_range(&self, from: Height, to: Height) -> Vec<Block> {
76        self.0.read().get_blocks_in_range(from, to)
77    }
78    fn tip_height(&self) -> Height {
79        self.0.read().tip_height()
80    }
81    fn put_commit_qc(&mut self, height: Height, qc: QuorumCertificate) {
82        self.0.write().put_commit_qc(height, qc);
83    }
84    fn get_commit_qc(&self, height: Height) -> Option<QuorumCertificate> {
85        self.0.read().get_commit_qc(height)
86    }
87    fn flush(&self) {
88        self.0.read().flush();
89    }
90    fn put_tx_index(&mut self, tx_hash: [u8; 32], height: Height, index: u32) {
91        self.0.write().put_tx_index(tx_hash, height, index);
92    }
93    fn get_tx_location(&self, tx_hash: &[u8; 32]) -> Option<(Height, u32)> {
94        self.0.read().get_tx_location(tx_hash)
95    }
96    fn put_block_results(&mut self, height: Height, results: hotmint_types::EndBlockResponse) {
97        self.0.write().put_block_results(height, results);
98    }
99    fn get_block_results(&self, height: Height) -> Option<hotmint_types::EndBlockResponse> {
100        self.0.read().get_block_results(height)
101    }
102}
103
104/// In-memory block store stub
105pub struct MemoryBlockStore {
106    by_hash: HashMap<BlockHash, Block>,
107    by_height: BTreeMap<u64, BlockHash>,
108    commit_qcs: HashMap<u64, QuorumCertificate>,
109}
110
111impl Default for MemoryBlockStore {
112    fn default() -> Self {
113        Self::new()
114    }
115}
116
117impl MemoryBlockStore {
118    pub fn new() -> Self {
119        let mut store = Self {
120            by_hash: HashMap::new(),
121            by_height: BTreeMap::new(),
122            commit_qcs: HashMap::new(),
123        };
124        let genesis = Block::genesis();
125        store.put_block(genesis);
126        store
127    }
128
129    /// Create a new in-memory block store wrapped in `Arc<parking_lot::RwLock<Box<dyn BlockStore>>>`,
130    /// ready for use with `ConsensusEngine`.
131    pub fn new_shared() -> crate::engine::SharedBlockStore {
132        Arc::new(RwLock::new(Box::new(Self::new())))
133    }
134}
135
136impl BlockStore for MemoryBlockStore {
137    fn put_block(&mut self, block: Block) {
138        let hash = block.hash;
139        self.by_height.insert(block.height.as_u64(), hash);
140        self.by_hash.insert(hash, block);
141    }
142
143    fn get_block(&self, hash: &BlockHash) -> Option<Block> {
144        self.by_hash.get(hash).cloned()
145    }
146
147    fn get_block_by_height(&self, h: Height) -> Option<Block> {
148        self.by_height
149            .get(&h.as_u64())
150            .and_then(|hash| self.by_hash.get(hash))
151            .cloned()
152    }
153
154    fn get_blocks_in_range(&self, from: Height, to: Height) -> Vec<Block> {
155        self.by_height
156            .range(from.as_u64()..=to.as_u64())
157            .filter_map(|(_, hash)| self.by_hash.get(hash).cloned())
158            .collect()
159    }
160
161    fn tip_height(&self) -> Height {
162        self.by_height
163            .keys()
164            .next_back()
165            .map(|h| Height(*h))
166            .unwrap_or(Height::GENESIS)
167    }
168
169    fn put_commit_qc(&mut self, height: Height, qc: QuorumCertificate) {
170        self.commit_qcs.insert(height.as_u64(), qc);
171    }
172
173    fn get_commit_qc(&self, height: Height) -> Option<QuorumCertificate> {
174        self.commit_qcs.get(&height.as_u64()).cloned()
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use hotmint_types::{ValidatorId, ViewNumber};
182
183    fn make_block(height: u64, parent: BlockHash) -> Block {
184        let hash = BlockHash([height as u8; 32]);
185        Block {
186            height: Height(height),
187            parent_hash: parent,
188            view: ViewNumber(height),
189            proposer: ValidatorId(0),
190            timestamp: 0,
191            payload: vec![],
192            app_hash: BlockHash::GENESIS,
193            evidence: Vec::new(),
194            hash,
195        }
196    }
197
198    #[test]
199    fn test_genesis_present() {
200        let store = MemoryBlockStore::new();
201        let genesis = store.get_block(&BlockHash::GENESIS);
202        assert!(genesis.is_some());
203        assert_eq!(genesis.unwrap().height, Height::GENESIS);
204    }
205
206    #[test]
207    fn test_put_and_get_by_hash() {
208        let mut store = MemoryBlockStore::new();
209        let block = make_block(1, BlockHash::GENESIS);
210        let hash = block.hash;
211        store.put_block(block);
212        let retrieved = store.get_block(&hash).unwrap();
213        assert_eq!(retrieved.height, Height(1));
214    }
215
216    #[test]
217    fn test_get_by_height() {
218        let mut store = MemoryBlockStore::new();
219        let b1 = make_block(1, BlockHash::GENESIS);
220        let b2 = make_block(2, b1.hash);
221        store.put_block(b1);
222        store.put_block(b2);
223
224        assert!(store.get_block_by_height(Height(1)).is_some());
225        assert!(store.get_block_by_height(Height(2)).is_some());
226        assert!(store.get_block_by_height(Height(99)).is_none());
227    }
228
229    #[test]
230    fn test_get_nonexistent() {
231        let store = MemoryBlockStore::new();
232        assert!(store.get_block(&BlockHash([99u8; 32])).is_none());
233        assert!(store.get_block_by_height(Height(999)).is_none());
234    }
235
236    #[test]
237    fn test_get_blocks_in_range() {
238        let mut store = MemoryBlockStore::new();
239        let b1 = make_block(1, BlockHash::GENESIS);
240        let b2 = make_block(2, b1.hash);
241        let b3 = make_block(3, b2.hash);
242        store.put_block(b1);
243        store.put_block(b2);
244        store.put_block(b3);
245
246        let blocks = store.get_blocks_in_range(Height(1), Height(3));
247        assert_eq!(blocks.len(), 3);
248        assert_eq!(blocks[0].height, Height(1));
249        assert_eq!(blocks[2].height, Height(3));
250
251        // Partial range
252        let blocks = store.get_blocks_in_range(Height(2), Height(3));
253        assert_eq!(blocks.len(), 2);
254
255        // Out of range
256        let blocks = store.get_blocks_in_range(Height(10), Height(20));
257        assert!(blocks.is_empty());
258    }
259
260    #[test]
261    fn test_tip_height() {
262        let store = MemoryBlockStore::new();
263        assert_eq!(store.tip_height(), Height::GENESIS);
264
265        let mut store = MemoryBlockStore::new();
266        let b1 = make_block(1, BlockHash::GENESIS);
267        let b2 = make_block(2, b1.hash);
268        store.put_block(b1);
269        store.put_block(b2);
270        assert_eq!(store.tip_height(), Height(2));
271    }
272
273    #[test]
274    fn test_overwrite_same_height() {
275        let mut store = MemoryBlockStore::new();
276        let b1 = make_block(1, BlockHash::GENESIS);
277        store.put_block(b1);
278        // Different block at same height (different hash)
279        let mut b2 = make_block(1, BlockHash::GENESIS);
280        b2.hash = BlockHash([42u8; 32]);
281        b2.payload = vec![1, 2, 3];
282        store.put_block(b2);
283        // Height now points to new block
284        let retrieved = store.get_block_by_height(Height(1)).unwrap();
285        assert_eq!(retrieved.hash, BlockHash([42u8; 32]));
286    }
287}