Skip to main content

hotmint_consensus/
store.rs

1use std::collections::{BTreeMap, HashMap};
2
3use hotmint_types::{Block, BlockHash, Height, QuorumCertificate};
4
5pub trait BlockStore: Send + Sync {
6    fn put_block(&mut self, block: Block);
7    fn get_block(&self, hash: &BlockHash) -> Option<Block>;
8    fn get_block_by_height(&self, h: Height) -> Option<Block>;
9
10    /// Store the QC that committed a block at the given height.
11    fn put_commit_qc(&mut self, _height: Height, _qc: QuorumCertificate) {}
12    /// Retrieve the commit QC for a block at the given height.
13    fn get_commit_qc(&self, _height: Height) -> Option<QuorumCertificate> {
14        None
15    }
16
17    /// Flush pending writes to durable storage.
18    fn flush(&self) {}
19
20    /// Get blocks in [from, to] inclusive. Default iterates one-by-one.
21    fn get_blocks_in_range(&self, from: Height, to: Height) -> Vec<Block> {
22        let mut blocks = Vec::new();
23        let mut h = from.as_u64();
24        while h <= to.as_u64() {
25            if let Some(block) = self.get_block_by_height(Height(h)) {
26                blocks.push(block);
27            }
28            h += 1;
29        }
30        blocks
31    }
32
33    /// Return the highest stored block height.
34    fn tip_height(&self) -> Height {
35        Height::GENESIS
36    }
37}
38
39/// In-memory block store stub
40pub struct MemoryBlockStore {
41    by_hash: HashMap<BlockHash, Block>,
42    by_height: BTreeMap<u64, BlockHash>,
43    commit_qcs: HashMap<u64, QuorumCertificate>,
44}
45
46impl Default for MemoryBlockStore {
47    fn default() -> Self {
48        Self::new()
49    }
50}
51
52impl MemoryBlockStore {
53    pub fn new() -> Self {
54        let mut store = Self {
55            by_hash: HashMap::new(),
56            by_height: BTreeMap::new(),
57            commit_qcs: HashMap::new(),
58        };
59        let genesis = Block::genesis();
60        store.put_block(genesis);
61        store
62    }
63}
64
65impl BlockStore for MemoryBlockStore {
66    fn put_block(&mut self, block: Block) {
67        let hash = block.hash;
68        self.by_height.insert(block.height.as_u64(), hash);
69        self.by_hash.insert(hash, block);
70    }
71
72    fn get_block(&self, hash: &BlockHash) -> Option<Block> {
73        self.by_hash.get(hash).cloned()
74    }
75
76    fn get_block_by_height(&self, h: Height) -> Option<Block> {
77        self.by_height
78            .get(&h.as_u64())
79            .and_then(|hash| self.by_hash.get(hash))
80            .cloned()
81    }
82
83    fn get_blocks_in_range(&self, from: Height, to: Height) -> Vec<Block> {
84        self.by_height
85            .range(from.as_u64()..=to.as_u64())
86            .filter_map(|(_, hash)| self.by_hash.get(hash).cloned())
87            .collect()
88    }
89
90    fn tip_height(&self) -> Height {
91        self.by_height
92            .keys()
93            .next_back()
94            .map(|h| Height(*h))
95            .unwrap_or(Height::GENESIS)
96    }
97
98    fn put_commit_qc(&mut self, height: Height, qc: QuorumCertificate) {
99        self.commit_qcs.insert(height.as_u64(), qc);
100    }
101
102    fn get_commit_qc(&self, height: Height) -> Option<QuorumCertificate> {
103        self.commit_qcs.get(&height.as_u64()).cloned()
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use hotmint_types::{ValidatorId, ViewNumber};
111
112    fn make_block(height: u64, parent: BlockHash) -> Block {
113        let hash = BlockHash([height as u8; 32]);
114        Block {
115            height: Height(height),
116            parent_hash: parent,
117            view: ViewNumber(height),
118            proposer: ValidatorId(0),
119            payload: vec![],
120            hash,
121        }
122    }
123
124    #[test]
125    fn test_genesis_present() {
126        let store = MemoryBlockStore::new();
127        let genesis = store.get_block(&BlockHash::GENESIS);
128        assert!(genesis.is_some());
129        assert_eq!(genesis.unwrap().height, Height::GENESIS);
130    }
131
132    #[test]
133    fn test_put_and_get_by_hash() {
134        let mut store = MemoryBlockStore::new();
135        let block = make_block(1, BlockHash::GENESIS);
136        let hash = block.hash;
137        store.put_block(block);
138        let retrieved = store.get_block(&hash).unwrap();
139        assert_eq!(retrieved.height, Height(1));
140    }
141
142    #[test]
143    fn test_get_by_height() {
144        let mut store = MemoryBlockStore::new();
145        let b1 = make_block(1, BlockHash::GENESIS);
146        let b2 = make_block(2, b1.hash);
147        store.put_block(b1);
148        store.put_block(b2);
149
150        assert!(store.get_block_by_height(Height(1)).is_some());
151        assert!(store.get_block_by_height(Height(2)).is_some());
152        assert!(store.get_block_by_height(Height(99)).is_none());
153    }
154
155    #[test]
156    fn test_get_nonexistent() {
157        let store = MemoryBlockStore::new();
158        assert!(store.get_block(&BlockHash([99u8; 32])).is_none());
159        assert!(store.get_block_by_height(Height(999)).is_none());
160    }
161
162    #[test]
163    fn test_get_blocks_in_range() {
164        let mut store = MemoryBlockStore::new();
165        let b1 = make_block(1, BlockHash::GENESIS);
166        let b2 = make_block(2, b1.hash);
167        let b3 = make_block(3, b2.hash);
168        store.put_block(b1);
169        store.put_block(b2);
170        store.put_block(b3);
171
172        let blocks = store.get_blocks_in_range(Height(1), Height(3));
173        assert_eq!(blocks.len(), 3);
174        assert_eq!(blocks[0].height, Height(1));
175        assert_eq!(blocks[2].height, Height(3));
176
177        // Partial range
178        let blocks = store.get_blocks_in_range(Height(2), Height(3));
179        assert_eq!(blocks.len(), 2);
180
181        // Out of range
182        let blocks = store.get_blocks_in_range(Height(10), Height(20));
183        assert!(blocks.is_empty());
184    }
185
186    #[test]
187    fn test_tip_height() {
188        let store = MemoryBlockStore::new();
189        assert_eq!(store.tip_height(), Height::GENESIS);
190
191        let mut store = MemoryBlockStore::new();
192        let b1 = make_block(1, BlockHash::GENESIS);
193        let b2 = make_block(2, b1.hash);
194        store.put_block(b1);
195        store.put_block(b2);
196        assert_eq!(store.tip_height(), Height(2));
197    }
198
199    #[test]
200    fn test_overwrite_same_height() {
201        let mut store = MemoryBlockStore::new();
202        let b1 = make_block(1, BlockHash::GENESIS);
203        store.put_block(b1);
204        // Different block at same height (different hash)
205        let mut b2 = make_block(1, BlockHash::GENESIS);
206        b2.hash = BlockHash([42u8; 32]);
207        b2.payload = vec![1, 2, 3];
208        store.put_block(b2);
209        // Height now points to new block
210        let retrieved = store.get_block_by_height(Height(1)).unwrap();
211        assert_eq!(retrieved.hash, BlockHash([42u8; 32]));
212    }
213}