saito_core/core/consensus/
blockring.rs

1use log::{debug, trace};
2
3use crate::core::consensus::block::Block;
4use crate::core::consensus::ringitem::RingItem;
5use crate::core::defs::{BlockId, PrintForLog, SaitoHash};
6
7//
8// TODO -- shift to a RingBuffer ? or Slice-VecDeque so that we can have
9// contiguous entries for rapid lookups, inserts and updates? we want to
10// have fast access to elements in random positions in the data structure
11//
12#[derive(Debug)]
13pub struct BlockRing {
14    //
15    // include Slice-VecDeque and have a slice that points to
16    // contiguous entries for rapid lookups, inserts and updates?
17    //
18    pub ring: Vec<RingItem>,
19    pub lc_pos: Option<usize>,
20    pub empty: bool,
21    pub genesis_period: BlockId,
22}
23
24impl BlockRing {
25    /// Create new `BlockRing`
26    #[allow(clippy::new_without_default)]
27    pub fn new(genesis_period: BlockId) -> Self {
28        let mut init_ring: Vec<RingItem> = vec![];
29        for _i in 0..(genesis_period * 2) {
30            init_ring.push(RingItem::default());
31        }
32
33        BlockRing {
34            ring: init_ring,
35            lc_pos: None,
36            empty: true,
37            genesis_period,
38        }
39    }
40    pub fn get_ring_buffer_size(&self) -> BlockId {
41        self.genesis_period * 2
42    }
43    pub fn add_block(&mut self, block: &Block) {
44        let insert_pos = block.id % self.get_ring_buffer_size();
45        trace!(
46            "blockring.add_block : {:?} at pos = {:?}",
47            block.hash.to_hex(),
48            insert_pos
49        );
50        self.ring[insert_pos as usize].add_block(block.id, block.hash);
51    }
52
53    pub fn contains_block_hash_at_block_id(&self, block_id: u64, block_hash: SaitoHash) -> bool {
54        let insert_pos = block_id % self.get_ring_buffer_size();
55        self.ring[insert_pos as usize].contains_block_hash(block_hash)
56    }
57
58    pub fn get_latest_block_hash(&self) -> SaitoHash {
59        match self.lc_pos {
60            Some(lc_pos_block_ring) => match self.ring[lc_pos_block_ring].lc_pos {
61                Some(lc_pos_block_item) => {
62                    self.ring[lc_pos_block_ring].block_hashes[lc_pos_block_item]
63                }
64                None => [0; 32],
65            },
66            None => [0; 32],
67        }
68    }
69
70    pub fn get_latest_block_id(&self) -> BlockId {
71        match self.lc_pos {
72            Some(lc_pos_block_ring) => match self.ring[lc_pos_block_ring].lc_pos {
73                Some(lc_pos_block_item) => {
74                    self.ring[lc_pos_block_ring].block_ids[lc_pos_block_item]
75                }
76                None => 0,
77            },
78            None => 0,
79        }
80    }
81
82    pub fn get_longest_chain_block_hash_at_block_id(&self, id: u64) -> Option<SaitoHash> {
83        let insert_pos = (id % self.get_ring_buffer_size()) as usize;
84        trace!(
85            "blockring -> insert_pos : {:?}, id : {:?}, ring_length : {:?}",
86            insert_pos,
87            id,
88            self.get_ring_buffer_size()
89        );
90        match self.ring[insert_pos].lc_pos {
91            Some(lc_pos) => {
92                trace!(
93                    "lc_pos : {:?}, ring_size : {:?} hash_count : {:?}",
94                    lc_pos,
95                    self.ring.len(),
96                    self.ring[insert_pos].block_hashes.len()
97                );
98                let ring_item = self.ring.get(insert_pos)?;
99                let item_id = ring_item.block_ids.get(lc_pos)?;
100                if item_id != &id {
101                    trace!(
102                        "get_longest_chain_block_hash_by_block_id : {:?} insert_pos = {:?} is not set",
103                        id,
104                        insert_pos
105                    );
106                    return None;
107                }
108                let hash = ring_item.block_hashes.get(lc_pos)?;
109                Some(*hash)
110            }
111            None => {
112                trace!(
113                    "get_longest_chain_block_hash_by_block_id : {:?} insert_pos = {:?} is not set",
114                    id,
115                    insert_pos
116                );
117                None
118            }
119        }
120    }
121
122    pub fn is_block_hash_at_block_id(&self, block_id: u64, block_hash: SaitoHash) -> bool {
123        let insert_pos = block_id % self.get_ring_buffer_size();
124        for i in 0..self.ring[insert_pos as usize].block_hashes.len() {
125            if self.ring[insert_pos as usize].block_hashes[i] == block_hash {
126                return true;
127            }
128        }
129        false
130    }
131
132    pub fn is_empty(&self) -> bool {
133        self.empty
134    }
135
136    pub fn delete_block(&mut self, block_id: u64, block_hash: SaitoHash) {
137        debug!(
138            "blockring.delete_block : block_id = {:?}, block_hash = {:?}",
139            block_id,
140            block_hash.to_hex()
141        );
142        let insert_pos = block_id % self.get_ring_buffer_size();
143        self.ring[insert_pos as usize].delete_block(block_id, block_hash);
144    }
145
146    pub fn get_block_hashes_at_block_id(&self, block_id: u64) -> Vec<SaitoHash> {
147        let insert_pos = block_id % self.get_ring_buffer_size();
148        let mut v: Vec<SaitoHash> = vec![];
149        for i in 0..self.ring[insert_pos as usize].block_hashes.len() {
150            if self.ring[insert_pos as usize].block_ids[i] == block_id {
151                v.push(self.ring[insert_pos as usize].block_hashes[i]);
152            }
153        }
154        v
155    }
156
157    pub fn on_chain_reorganization(&mut self, block_id: u64, hash: SaitoHash, lc: bool) -> bool {
158        let insert_pos = block_id % self.get_ring_buffer_size();
159        self.ring[insert_pos as usize].on_chain_reorganization(hash, lc);
160        trace!(
161            "blockring.on_chain_reorg : block_id = {:?}, hash = {:?}, insert_pos = {:?}, lc_pos = {:?}",
162            block_id,
163            hash.to_hex(),
164            insert_pos,
165             self.ring[insert_pos as usize].lc_pos
166        );
167        if lc {
168            self.lc_pos = Some(insert_pos as usize);
169        } else {
170            //
171            // if we are unsetting the longest-chain, we automatically
172            // roll backwards and set the longest-chain to the previous
173            // position if available. this adds some complexity to unwinding
174            // the chain but should ensure that in most situations there is
175            // always a known longest-chain position. this is not guaranteed
176            // behavior, so the blockring should not be treated as something
177            // that guarantees correctness of lc_pos in situations like this.
178            //
179            if let Some(lc_pos) = self.lc_pos {
180                if lc_pos == insert_pos as usize {
181                    let previous_block_index: usize = if lc_pos > 0 {
182                        lc_pos - 1
183                    } else {
184                        self.get_ring_buffer_size() as usize - 1
185                    };
186
187                    // reset to lc_pos to unknown
188                    self.lc_pos = None;
189
190                    // but try to find it
191                    // let previous_block_index_lc_pos = self.ring[previous_block_index as usize].lc_pos;
192                    if let Some(previous_block_index_lc_pos) =
193                        self.ring[previous_block_index].lc_pos
194                    {
195                        if self.ring[previous_block_index].block_ids.len()
196                            > previous_block_index_lc_pos
197                        {
198                            if self.ring[previous_block_index].block_ids
199                                [previous_block_index_lc_pos]
200                                == block_id - 1
201                            {
202                                self.lc_pos = Some(previous_block_index);
203                            }
204                        }
205                    }
206                }
207            }
208        }
209        true
210    }
211
212    pub fn print_lc(&self) {
213        for i in 0..self.genesis_period {
214            if !self.ring[i as usize].block_hashes.is_empty() {
215                trace!(
216                    "Block {:?}: {:?}",
217                    i,
218                    self.get_longest_chain_block_hash_at_block_id(i)
219                );
220            }
221        }
222    }
223
224    pub fn get_block_hash_by_block_id(&self, block_id: u64) -> Option<SaitoHash> {
225        let insert_pos = (block_id % self.get_ring_buffer_size()) as usize;
226        self.ring[insert_pos]
227            .block_ids
228            .iter()
229            .position(|&id| id == block_id)
230            .map(|pos| self.ring[insert_pos].block_hashes[pos])
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use crate::core::consensus::block::Block;
237    use crate::core::consensus::blockring::BlockRing;
238
239    #[test]
240    fn blockring_new_test() {
241        let blockring = BlockRing::new(1_000);
242        assert_eq!(blockring.ring.len() as u64, 2_000);
243        assert_eq!(blockring.lc_pos, None);
244    }
245
246    #[test]
247    fn blockring_add_block_test() {
248        let mut blockring = BlockRing::new(1_000);
249        let mut block = Block::new();
250        block.id = 1;
251        block.generate_hash();
252        let block_hash = block.hash;
253        let block_id = block.id;
254
255        // everything is empty to start
256        assert_eq!(blockring.is_empty(), true);
257        assert_eq!(blockring.get_latest_block_hash(), [0; 32]);
258        assert_eq!(blockring.get_latest_block_id(), 0);
259        assert_eq!(blockring.get_longest_chain_block_hash_at_block_id(0), None);
260        assert_eq!(
261            blockring.contains_block_hash_at_block_id(block.id, block.hash),
262            false
263        );
264
265        blockring.add_block(&block);
266        blockring.on_chain_reorganization(block.id, block.hash, true);
267
268        // assert_eq!(blockring.is_empty(), false);
269        assert_eq!(blockring.get_latest_block_hash(), block_hash);
270        assert_eq!(blockring.get_latest_block_id(), block_id);
271        assert_eq!(
272            blockring
273                .get_longest_chain_block_hash_at_block_id(block_id)
274                .unwrap(),
275            block_hash
276        );
277        assert_eq!(
278            blockring.contains_block_hash_at_block_id(block.id, block.hash),
279            true
280        );
281    }
282
283    #[test]
284    fn blockring_delete_block_test() {
285        let mut blockring = BlockRing::new(1_000);
286        let mut block = Block::new();
287        block.generate_hash();
288        let block_hash = block.hash;
289        let block_id = block.id;
290
291        // everything is empty to start
292        assert_eq!(blockring.is_empty(), true);
293        assert_eq!(blockring.get_latest_block_hash(), [0; 32]);
294        assert_eq!(blockring.get_latest_block_id(), 0);
295        assert_eq!(blockring.get_longest_chain_block_hash_at_block_id(0), None);
296        assert_eq!(
297            blockring.contains_block_hash_at_block_id(block.id, block.hash),
298            false
299        );
300
301        blockring.add_block(&block);
302        blockring.on_chain_reorganization(block.id, block.hash, true);
303
304        // assert_eq!(blockring.is_empty(), false);
305        assert_eq!(blockring.get_latest_block_hash(), block_hash);
306        assert_eq!(blockring.get_latest_block_id(), block_id);
307        assert_eq!(
308            blockring
309                .get_longest_chain_block_hash_at_block_id(block_id)
310                .unwrap(),
311            block_hash
312        );
313        assert_eq!(
314            blockring.contains_block_hash_at_block_id(block.id, block.hash),
315            true
316        );
317
318        blockring.delete_block(block.id, block.hash);
319        assert_eq!(
320            blockring.contains_block_hash_at_block_id(block.id, block.hash),
321            false
322        );
323    }
324
325    #[tokio::test]
326    #[serial_test::serial]
327    //
328    // does reorg update blockring view of longest-chain
329    //
330    async fn blockring_manual_reorganization_test() {
331        let mut block1 = Block::new();
332        let mut block2 = Block::new();
333        let mut block3 = Block::new();
334        let mut block4 = Block::new();
335        let mut block5 = Block::new();
336
337        block1.id = 1;
338        block2.id = 2;
339        block3.id = 3;
340        block4.id = 4;
341        block5.id = 5;
342
343        block1.generate().unwrap();
344        block2.generate().unwrap();
345        block3.generate().unwrap();
346        block4.generate().unwrap();
347        block5.generate().unwrap();
348
349        let mut blockring = BlockRing::new(1_000);
350
351        blockring.add_block(&block1);
352        blockring.add_block(&block2);
353        blockring.add_block(&block3);
354        blockring.add_block(&block4);
355        blockring.add_block(&block5);
356
357        // do we contain these block hashes?
358        assert_eq!(
359            blockring.contains_block_hash_at_block_id(1, block1.hash),
360            true
361        );
362        assert_eq!(
363            blockring.contains_block_hash_at_block_id(2, block2.hash),
364            true
365        );
366        assert_eq!(
367            blockring.contains_block_hash_at_block_id(3, block3.hash),
368            true
369        );
370        assert_eq!(
371            blockring.contains_block_hash_at_block_id(4, block4.hash),
372            true
373        );
374        assert_eq!(
375            blockring.contains_block_hash_at_block_id(5, block5.hash),
376            true
377        );
378        assert_eq!(
379            blockring.contains_block_hash_at_block_id(2, block4.hash),
380            false
381        );
382
383        // reorganize longest chain
384        blockring.on_chain_reorganization(1, block1.hash, true);
385        assert_eq!(blockring.get_latest_block_id(), 1);
386        blockring.on_chain_reorganization(2, block2.hash, true);
387        assert_eq!(blockring.get_latest_block_id(), 2);
388        blockring.on_chain_reorganization(3, block3.hash, true);
389        assert_eq!(blockring.get_latest_block_id(), 3);
390        blockring.on_chain_reorganization(4, block4.hash, true);
391        assert_eq!(blockring.get_latest_block_id(), 4);
392        blockring.on_chain_reorganization(5, block5.hash, true);
393        assert_eq!(blockring.get_latest_block_id(), 5);
394        blockring.on_chain_reorganization(5, block5.hash, false);
395        assert_eq!(blockring.get_latest_block_id(), 4);
396        blockring.on_chain_reorganization(4, block4.hash, false);
397        assert_eq!(blockring.get_latest_block_id(), 3);
398        blockring.on_chain_reorganization(3, block3.hash, false);
399        assert_eq!(blockring.get_latest_block_id(), 2);
400
401        // reorg in the wrong block_id location, should not change
402        blockring.on_chain_reorganization(532, block5.hash, false);
403        assert_eq!(blockring.get_latest_block_id(), 2);
404
405        // double reorg in correct and should be fine still
406        blockring.on_chain_reorganization(2, block2.hash, true);
407        blockring.on_chain_reorganization(2, block2.hash, true);
408        assert_eq!(blockring.get_latest_block_id(), 2);
409    }
410}