massa_pos_exports/
pos_final_state.rs

1use crate::{
2    CycleInfo, CycleInfoDeserializer, CycleInfoSerializer, DeferredCreditDeserializer,
3    DeferredCreditSerializer, DeferredCredits, PoSChanges, PoSConfig, PosError, PosResult,
4    ProductionStats, SelectorController,
5};
6use bitvec::vec::BitVec;
7use massa_db_exports::{
8    DBBatch, MassaDirection, MassaIteratorMode, ShareableMassaDBController,
9    CYCLE_HISTORY_DESER_ERROR, CYCLE_HISTORY_PREFIX, CYCLE_HISTORY_SER_ERROR,
10    DEFERRED_CREDITS_DESER_ERROR, DEFERRED_CREDITS_PREFIX, DEFERRED_CREDITS_SER_ERROR, STATE_CF,
11};
12use massa_hash::{Hash, HashXof, HASH_XOF_SIZE_BYTES};
13use massa_ledger_exports::LedgerController;
14use massa_models::{
15    address::Address, amount::Amount, config::LEDGER_ENTRY_BASE_COST, prehash::PreHashMap,
16    slot::Slot,
17};
18use massa_serialization::{
19    buf_to_array_ctr, DeserializeError, Deserializer, Serializer, U64VarIntSerializer,
20};
21use nom::AsBytes;
22use std::{
23    collections::{BTreeMap, VecDeque},
24    ops::{
25        Bound::{Excluded, Included, Unbounded},
26        RangeBounds,
27    },
28    path::PathBuf,
29};
30use tracing::{debug, warn};
31
32// General cycle info idents
33const COMPLETE_IDENT: u8 = 0u8;
34const RNG_SEED_IDENT: u8 = 1u8;
35const FINAL_STATE_HASH_SNAPSHOT_IDENT: u8 = 2u8;
36const ROLL_COUNT_IDENT: u8 = 3u8;
37const PROD_STATS_IDENT: u8 = 4u8;
38
39// Production stats idents
40const PROD_STATS_FAIL_IDENT: u8 = 0u8;
41const PROD_STATS_SUCCESS_IDENT: u8 = 1u8;
42
43/// Complete key formatting macro
44#[macro_export]
45macro_rules! complete_key {
46    ($cycle_prefix:expr) => {
47        [&$cycle_prefix[..], &[COMPLETE_IDENT]].concat()
48    };
49}
50
51/// Rng seed key formatting macro
52#[macro_export]
53macro_rules! rng_seed_key {
54    ($cycle_prefix:expr) => {
55        [&$cycle_prefix[..], &[RNG_SEED_IDENT]].concat()
56    };
57}
58
59/// Final state hash snapshot key formatting macro
60#[macro_export]
61macro_rules! final_state_hash_snapshot_key {
62    ($cycle_prefix:expr) => {
63        [&$cycle_prefix[..], &[FINAL_STATE_HASH_SNAPSHOT_IDENT]].concat()
64    };
65}
66
67/// Roll count key prefix macro
68#[macro_export]
69macro_rules! roll_count_prefix {
70    ($cycle_prefix:expr) => {
71        [&$cycle_prefix[..], &[ROLL_COUNT_IDENT]].concat()
72    };
73}
74
75/// Roll count key formatting macro
76#[macro_export]
77macro_rules! roll_count_key {
78    ($cycle_prefix:expr, $addr:expr) => {
79        [
80            &$cycle_prefix[..],
81            &[ROLL_COUNT_IDENT],
82            &$addr.to_prefixed_bytes()[..],
83        ]
84        .concat()
85    };
86}
87
88/// Production stats prefix macro
89#[macro_export]
90macro_rules! prod_stats_prefix {
91    ($cycle_prefix:expr) => {
92        [&$cycle_prefix[..], &[PROD_STATS_IDENT]].concat()
93    };
94}
95
96/// Production stats fail key formatting macro
97#[macro_export]
98macro_rules! prod_stats_fail_key {
99    ($cycle_prefix:expr, $addr:expr) => {
100        [
101            &$cycle_prefix[..],
102            &[PROD_STATS_IDENT],
103            &$addr.to_prefixed_bytes()[..],
104            &[PROD_STATS_FAIL_IDENT],
105        ]
106        .concat()
107    };
108}
109
110/// Production stats success key formatting macro
111#[macro_export]
112macro_rules! prod_stats_success_key {
113    ($cycle_prefix:expr, $addr:expr) => {
114        [
115            &$cycle_prefix[..],
116            &[PROD_STATS_IDENT],
117            &$addr.to_prefixed_bytes()[..],
118            &[PROD_STATS_SUCCESS_IDENT],
119        ]
120        .concat()
121    };
122}
123
124/// Deferred credits key formatting macro
125#[macro_export]
126macro_rules! deferred_credits_key {
127    ($id:expr) => {
128        [&DEFERRED_CREDITS_PREFIX.as_bytes(), &$id[..]].concat()
129    };
130}
131
132#[derive(Clone)]
133/// Final state of PoS
134pub struct PoSFinalState {
135    /// proof-of-stake configuration
136    pub config: PoSConfig,
137    /// Access to the RocksDB database
138    pub db: ShareableMassaDBController,
139    /// contiguous cycle history, back = newest
140    pub cycle_history_cache: VecDeque<(u64, bool)>,
141    /// rng_seed cache to get rng_seed for the current cycle
142    pub rng_seed_cache: Option<(u64, BitVec<u8>)>,
143    /// selector controller
144    pub selector: Box<dyn SelectorController>,
145    /// initial rolls, used for negative cycle look back
146    pub initial_rolls: BTreeMap<Address, u64>,
147    /// initial seeds, used for negative cycle look back (cycles -2, -1 in that order)
148    pub initial_seeds: Vec<Hash>,
149    /// inner deferred credits serializer (one slot's credits)
150    pub credits_serializer: DeferredCreditSerializer,
151    /// inner deferred credits deserializer (one slot's credits)
152    pub credits_deserializer: DeferredCreditDeserializer,
153    /// cycle info serializer
154    pub cycle_info_serializer: CycleInfoSerializer,
155    /// cycle info deserializer
156    pub cycle_info_deserializer: CycleInfoDeserializer,
157}
158
159impl PoSFinalState {
160    /// Number of slots (and thus RNG seed bits) in a full cycle.
161    fn slots_per_cycle(&self) -> usize {
162        self.config
163            .periods_per_cycle
164            .saturating_mul(self.config.thread_count as u64)
165            .try_into()
166            .expect("slots_per_cycle overflow")
167    }
168
169    /// create a new `PoSFinalState`
170    pub fn new(
171        config: PoSConfig,
172        initial_seed_string: &str,
173        initial_rolls_path: &PathBuf,
174        selector: Box<dyn SelectorController>,
175        db: ShareableMassaDBController,
176    ) -> Result<Self, PosError> {
177        // load get initial rolls from file
178        let initial_rolls = serde_json::from_str::<BTreeMap<Address, u64>>(
179            &std::fs::read_to_string(initial_rolls_path).map_err(|err| {
180                PosError::RollsFileLoadingError(format!("error while deserializing: {}", err))
181            })?,
182        )
183        .map_err(|err| PosError::RollsFileLoadingError(format!("error opening file: {}", err)))?;
184
185        // Seeds used as the initial seeds for negative cycles (-2 and -1 respectively)
186        let init_seed = Hash::compute_from(initial_seed_string.as_bytes());
187        let initial_seeds = vec![Hash::compute_from(init_seed.to_bytes()), init_seed];
188
189        let cycle_info_deserializer =
190            CycleInfoDeserializer::new(config.max_rolls_length, config.max_production_stats_length);
191
192        let pos_state = Self {
193            config: config.clone(),
194            db,
195            cycle_history_cache: Default::default(),
196            rng_seed_cache: None,
197            selector,
198            initial_rolls,
199            initial_seeds,
200            credits_serializer: DeferredCreditSerializer::new(),
201            credits_deserializer: DeferredCreditDeserializer::new(config.max_credit_length),
202            cycle_info_serializer: CycleInfoSerializer::new(),
203            cycle_info_deserializer,
204        };
205
206        Ok(pos_state)
207    }
208
209    /// Try load initial deferred credits from file
210    ///
211    /// Credits that would fail at execution time (same rules as `transfer_coins` when
212    /// crediting with `from_addr = None`) are skipped:
213    /// * non-existing SC address
214    /// * non-existing user address with `amount` below the ledger entry base cost
215    pub fn load_initial_deferred_credits(
216        &mut self,
217        batch: &mut DBBatch,
218        ledger: &dyn LedgerController,
219    ) -> Result<(), PosError> {
220        let Some(initial_deferred_credits_path) = &self.config.initial_deferred_credits_path else {
221            return Ok(());
222        };
223
224        use serde::Deserialize;
225        #[derive(Deserialize)]
226        struct AddressInitialDeferredCredits {
227            slot: Slot,
228            amount: Amount,
229        }
230
231        let initial_deferred_credits =
232            serde_json::from_str::<PreHashMap<Address, Vec<AddressInitialDeferredCredits>>>(
233                &std::fs::read_to_string(initial_deferred_credits_path).map_err(|err| {
234                    PosError::DeferredCreditsFileLoadingError(format!(
235                        "error while deserializing initial deferred credits file {}: {}",
236                        initial_deferred_credits_path.display(),
237                        err
238                    ))
239                })?,
240            )
241            .map_err(|err| {
242                PosError::DeferredCreditsFileLoadingError(format!(
243                    "error loading initial deferred credits file {}: {}",
244                    initial_deferred_credits_path.display(),
245                    err
246                ))
247            })?;
248
249        for (address, deferred_credits) in initial_deferred_credits {
250            let address_is_sc = matches!(address, Address::SC(..));
251            let address_exists = ledger.entry_exists(&address);
252            for AddressInitialDeferredCredits { slot, amount } in deferred_credits {
253                if address_is_sc {
254                    warn!(
255                        "deferred credits for address {} with amount {} will not execute because it is a SC address",
256                        address, amount
257                    );
258                } else if !address_exists && amount < LEDGER_ENTRY_BASE_COST {
259                    warn!(
260                        "deferred credits for address {} with amount {} will likely not execute because the address does not exist in the ledger and the amount is below the ledger entry base cost",
261                        address, amount
262                    );
263                }
264
265                // Note that we still put the deferred credits in the database even if they are likely to fail at execution time.
266                // It's for instance possible the address is created before the execution slot of the deferred credits.
267                self.put_deferred_credits_entry(&slot, &address, &amount, batch);
268            }
269        }
270
271        Ok(())
272    }
273
274    /// After bootstrap or load from disk, recompute the caches
275    pub fn recompute_pos_state_caches(&mut self) {
276        self.cycle_history_cache = self.get_cycle_history_cycles().into();
277
278        if let Some((cycle, _)) = self.cycle_history_cache.back() {
279            self.rng_seed_cache = self
280                .get_cycle_history_rng_seed(*cycle)
281                .map(|rng_seed| (*cycle, rng_seed));
282        } else {
283            self.rng_seed_cache = None;
284        }
285    }
286
287    /// Ensures every complete cycle has the persisted fields required for selector feeding.
288    ///
289    /// A cycle marked complete must have an RNG seed and a non-empty
290    /// `final_state_hash_snapshot`. Used after load/bootstrap and during DB validation so
291    /// malformed history fails fast instead of panicking later in `feed_selector`.
292    pub fn validate_selector_history(&self) -> PosResult<()> {
293        for (cycle, complete) in self.get_cycle_history_cycles() {
294            if !complete {
295                continue;
296            }
297            if self.get_cycle_history_rng_seed(cycle).is_none() {
298                return Err(PosError::ContainerInconsistency(format!(
299                    "complete cycle {} is missing RNG seed required for selector feeding",
300                    cycle
301                )));
302            }
303            if self
304                .get_cycle_history_final_state_hash_snapshot(cycle)
305                .is_none()
306            {
307                return Err(PosError::ContainerInconsistency(format!(
308                    "complete cycle {} is missing final state hash snapshot required for selector feeding",
309                    cycle
310                )));
311            }
312        }
313        Ok(())
314    }
315
316    /// Reset the state of the PoS final state
317    ///
318    /// USED ONLY FOR BOOTSTRAP
319    pub fn reset(&mut self) {
320        let mut db = self.db.write();
321        db.delete_prefix(CYCLE_HISTORY_PREFIX, STATE_CF, None);
322        db.delete_prefix(DEFERRED_CREDITS_PREFIX, STATE_CF, None);
323        self.cycle_history_cache = Default::default();
324        self.rng_seed_cache = None;
325    }
326
327    /// Create the initial cycle based off the initial rolls.
328    ///
329    /// This should be called only if bootstrap did not happen.
330    pub fn create_initial_cycle(&mut self, batch: &mut DBBatch) {
331        let mut rng_seed = BitVec::with_capacity(self.slots_per_cycle());
332        rng_seed.extend(vec![false; self.config.thread_count as usize]);
333
334        self.put_new_cycle_info(
335            &CycleInfo::new(
336                0,
337                false,
338                self.initial_rolls.clone(),
339                rng_seed,
340                PreHashMap::default(),
341            ),
342            batch,
343        );
344    }
345
346    /// Create a cycle based off of another cycle_info.
347    ///
348    /// Used for downtime interpolation, when restarting from a snapshot.
349    /// Roll counts are always carried over from `last_cycle_info`.
350    ///
351    /// `rng_seed` and `production_stats` are kept only when continuing the same
352    /// incomplete cycle (`last_cycle_info.cycle` matches the target cycle and
353    /// `first_slot` is not the first slot of that cycle). Otherwise the seed
354    /// starts fresh and `production_stats` are empty, so interpolated future
355    /// cycles do not inherit stale production results from the snapshot.
356    ///
357    /// In all cases, `rng_seed` is then extended with `false` bits for every
358    /// interpolated slot in `[first_slot, last_slot]`.
359    pub fn create_new_cycle_from_last(
360        &mut self,
361        last_cycle_info: &CycleInfo,
362        first_slot: Slot,
363        last_slot: Slot,
364        batch: &mut DBBatch,
365    ) -> Result<(), PosError> {
366        let cycle = last_slot.get_cycle(self.config.periods_per_cycle);
367
368        // Keep seed/stats only when filling the gap in the same incomplete cycle.
369        // Require both same cycle id and a non-first first_slot (defense in depth:
370        // callers may reuse a snapshot CycleInfo across several target cycles).
371        let continuing_same_cycle = last_cycle_info.cycle == cycle
372            && !first_slot.is_first_of_cycle(self.config.periods_per_cycle);
373
374        let (mut rng_seed, production_stats) = if continuing_same_cycle {
375            (
376                last_cycle_info.rng_seed.clone(),
377                last_cycle_info.production_stats.clone(),
378            )
379        } else {
380            (
381                BitVec::with_capacity(self.slots_per_cycle()),
382                PreHashMap::default(),
383            )
384        };
385
386        let num_slots = match last_slot.slots_since(&first_slot, self.config.thread_count) {
387            Ok(slots_since) => slots_since.saturating_add(1),
388            // if first_slot is greater than last_slot, we have nothing to do
389            Err(_) => return Ok(()),
390        };
391
392        let slots_per_cycle = self.slots_per_cycle();
393        // Existing seed (e.g. genesis thread_count padding) may already cover some of
394        // the interpolated range. Cap so we never exceed a full cycle.
395        if rng_seed.len() > slots_per_cycle {
396            return Err(PosError::ContainerInconsistency(format!(
397                "RNG seed for cycle {} exceeds slots_per_cycle: {} > {}",
398                cycle,
399                rng_seed.len(),
400                slots_per_cycle
401            )));
402        }
403        // Pad downtime slots; not the same as leaving the seed empty.
404        let to_add = (num_slots as usize).min(slots_per_cycle.saturating_sub(rng_seed.len()));
405        rng_seed.extend(vec![false; to_add]);
406
407        let complete =
408            last_slot.is_last_of_cycle(self.config.periods_per_cycle, self.config.thread_count);
409        if rng_seed.len() > slots_per_cycle || (complete && rng_seed.len() != slots_per_cycle) {
410            return Err(PosError::ContainerInconsistency(format!(
411                "invalid RNG seed length {} for cycle {} (slots_per_cycle {}, complete={})",
412                rng_seed.len(),
413                cycle,
414                slots_per_cycle,
415                complete
416            )));
417        }
418
419        self.put_new_cycle_info(
420            &CycleInfo::new(
421                cycle,
422                complete,
423                last_cycle_info.roll_counts.clone(),
424                rng_seed,
425                production_stats,
426            ),
427            batch,
428        );
429
430        Ok(())
431    }
432
433    /// Deletes a given cycle from RocksDB
434    pub fn delete_cycle_info(&mut self, cycle: u64, batch: &mut DBBatch) {
435        let db = self.db.read();
436
437        let prefix = self.cycle_history_cycle_prefix(cycle);
438
439        for (serialized_key, _) in db.prefix_iterator_cf(STATE_CF, &prefix) {
440            if !serialized_key.starts_with(prefix.as_bytes()) {
441                break;
442            }
443            db.delete_key(batch, serialized_key.to_vec());
444        }
445    }
446
447    /// Sends the current draw inputs (initial or bootstrapped) to the selector.
448    /// Waits for the initial draws to be performed.
449    pub fn compute_initial_draws(&mut self) -> PosResult<()> {
450        // Reject incomplete selector inputs before recomputing draws from history.
451        self.validate_selector_history()?;
452
453        // if cycle_history starts at a cycle that is strictly higher than 0, do not feed cycles 0, 1 to selector
454        let history_starts_late = self
455            .cycle_history_cache
456            .front()
457            .map(|c_info| c_info.0 > 0)
458            .unwrap_or(false);
459
460        let mut max_cycle = None;
461
462        // feed cycles 0, 1 to selector if necessary
463        if !history_starts_late {
464            for draw_cycle in 0u64..=1 {
465                self.feed_selector(draw_cycle)?;
466                max_cycle = Some(draw_cycle);
467            }
468        }
469
470        // feed cycles available from history
471        for (idx, hist_item) in self.cycle_history_cache.iter().enumerate() {
472            if !hist_item.1 {
473                break;
474            }
475            if history_starts_late && idx == 0 {
476                // If the history starts late, the first RNG seed cannot be used to draw
477                // because the roll distribution which should be provided by the previous element is absent.
478                continue;
479            }
480            let draw_cycle = hist_item.0.checked_add(2).ok_or_else(|| {
481                PosError::OverflowError("cycle overflow in give_selector_controller".into())
482            })?;
483            self.feed_selector(draw_cycle)?;
484            max_cycle = Some(draw_cycle);
485        }
486
487        // wait for all fed cycles to be drawn
488        if let Some(wait_cycle) = max_cycle {
489            self.selector.as_mut().wait_for_draws(wait_cycle)?;
490        }
491        Ok(())
492    }
493
494    /// Technical specification of `apply_changes_to_batch`:
495    ///
496    /// set `self.last_final_slot` = C
497    /// if cycle C is absent from `self.cycle_history_cache`:
498    ///     `push` a new empty `CycleInfo` on disk and reflect in `self.cycle_history_cache` and set its cycle = C
499    ///     `pop_front` from `cycle_history_cache` until front() represents cycle C-4 or later (not C-3 because we might need older endorsement draws on the limit between 2 cycles)
500    ///     delete the removed cycles from disk
501    /// for the cycle C entry in the db:
502    ///     extend `seed_bits` with `changes.seed_bits`
503    ///     extend `roll_counts` with `changes.roll_changes`
504    ///         delete all entries from `roll_counts` for which the roll count is zero
505    ///     add each element of `changes.production_stats` to the cycle's `production_stats`
506    /// for each `changes.deferred_credits` targeting cycle Ct:
507    ///     overwrite `self.deferred_credits` entries of cycle Ct in `cycle_history` with the ones from change
508    ///         remove entries for which Amount = 0
509    /// if slot S was the last of cycle C:
510    ///     set complete=true for cycle C in the history
511    ///     compute the seed hash and notifies the `PoSDrawer` for cycle `C+3`
512    ///
513    pub fn apply_changes_to_batch(
514        &mut self,
515        changes: PoSChanges,
516        slot: Slot,
517        feed_selector: bool,
518        batch: &mut DBBatch,
519    ) -> PosResult<()> {
520        let slots_per_cycle = self.slots_per_cycle();
521
522        // compute the current cycle from the given slot
523        let cycle = slot.get_cycle(self.config.periods_per_cycle);
524
525        // if cycle C is absent from self.cycle_history:
526        // push a new empty CycleInfo at the back of self.cycle_history and set its cycle = C
527        // pop_front from cycle_history until front() represents cycle C-4 or later
528        // (not C-3 because we might need older endorsement draws on the limit between 2 cycles)
529        if let Some(info) = self.cycle_history_cache.back() {
530            if cycle == info.0 && !info.1 {
531                // extend the last incomplete cycle
532            } else if info.0.checked_add(1) == Some(cycle) && info.1 {
533                // the previous cycle is complete, push a new incomplete/empty one to extend
534                // By now `_finalize` should already have written its final_state_hash_snapshot.
535                // Catch a missing snapshot here (first slot of the new cycle) instead of waiting
536                // until feed_selector needs it as a lookback (~1 cycle later).
537                if self
538                    .get_cycle_history_final_state_hash_snapshot(info.0)
539                    .is_none()
540                {
541                    return Err(PosError::ContainerInconsistency(format!(
542                        "complete cycle {} is missing final state hash snapshot before starting cycle {}",
543                        info.0, cycle
544                    )));
545                }
546
547                let roll_counts = self.get_all_roll_counts(info.0);
548                self.put_new_cycle_info(
549                    &CycleInfo::new(
550                        cycle,
551                        false,
552                        roll_counts,
553                        BitVec::with_capacity(slots_per_cycle),
554                        PreHashMap::default(),
555                    ),
556                    batch,
557                );
558                while self.cycle_history_cache.len() > self.config.cycle_history_length {
559                    if let Some((old_cycle, _)) = self.cycle_history_cache.pop_front() {
560                        self.delete_cycle_info(old_cycle, batch);
561                    }
562                }
563            } else {
564                return Err(PosError::OverflowError(
565                    "invalid cycle sequence in PoS final state".into(),
566                ));
567            }
568        } else {
569            return Err(PosError::ContainerInconsistency(
570                "PoS history should never be empty here".into(),
571            ));
572        }
573
574        let complete: bool =
575            slot.is_last_of_cycle(self.config.periods_per_cycle, self.config.thread_count);
576
577        // OPTIM: we could avoid reading the previous seed bits with a cache or with an update function
578        // Validate selector inputs before marking the cycle complete so a malformed seed cannot
579        // leave `complete=true` in the in-memory cache when we return an error.
580        let mut rng_seed = self.get_cycle_history_rng_seed(cycle).ok_or_else(|| {
581            PosError::ContainerInconsistency(format!("missing RNG seed for cycle {}", cycle))
582        })?;
583        // Reject already-corrupt or overflowing seed state before mutating caches/batch.
584        if rng_seed.len() > slots_per_cycle {
585            return Err(PosError::ContainerInconsistency(format!(
586                "RNG seed for cycle {} exceeds slots_per_cycle: {} > {}",
587                cycle,
588                rng_seed.len(),
589                slots_per_cycle
590            )));
591        }
592        rng_seed.extend(changes.seed_bits);
593        if rng_seed.len() > slots_per_cycle || (complete && rng_seed.len() != slots_per_cycle) {
594            return Err(PosError::ContainerInconsistency(format!(
595                "cycle {} RNG seed length {} invalid (slots_per_cycle {}, complete={})",
596                cycle,
597                rng_seed.len(),
598                slots_per_cycle,
599                complete
600            )));
601        }
602
603        self.put_cycle_history_complete(cycle, complete, batch);
604        self.put_cycle_history_rng_seed(cycle, rng_seed, batch);
605
606        // extend roll counts
607        for (addr, roll_count) in changes.roll_changes {
608            self.put_cycle_history_address_entry(cycle, &addr, Some(&roll_count), None, batch);
609        }
610
611        // extend production stats
612        for (addr, stats) in changes.production_stats {
613            if let Some(prev_production_stats) = self.get_production_stats_for_address(cycle, &addr)
614            {
615                let mut new_production_stats = prev_production_stats;
616                new_production_stats.extend(&stats);
617                self.put_cycle_history_address_entry(
618                    cycle,
619                    &addr,
620                    None,
621                    Some(&new_production_stats),
622                    batch,
623                );
624            } else {
625                self.put_cycle_history_address_entry(cycle, &addr, None, Some(&stats), batch);
626            }
627        }
628
629        // extend deferred_credits with changes.deferred_credits and remove zeros
630        for (slot, credits) in changes.deferred_credits.credits.iter() {
631            for (address, amount) in credits.iter() {
632                self.put_deferred_credits_entry(slot, address, amount, batch);
633            }
634        }
635
636        // feed the cycle if it is complete
637        // notify the PoSDrawer about the newly ready draw data
638        // to draw cycle + 2, we use the rng data from cycle - 1 and the seed from cycle
639        debug!(
640            "After slot {} PoS cycle list is {:?}",
641            slot, self.cycle_history_cache
642        );
643        if complete && feed_selector {
644            self.feed_selector(cycle.checked_add(2).ok_or_else(|| {
645                PosError::OverflowError("cycle overflow when feeding selector".into())
646            })?)
647        } else {
648            Ok(())
649        }
650    }
651
652    /// Feeds the selector targeting a given draw cycle
653    pub fn feed_selector(&self, draw_cycle: u64) -> PosResult<()> {
654        // get roll lookback
655
656        let (lookback_rolls, lookback_state_hash) = match draw_cycle.checked_sub(3) {
657            // looking back in history
658            Some(c) => {
659                let index = self
660                    .get_cycle_index(c)
661                    .ok_or(PosError::CycleUnavailable(c))?;
662                let cycle_info = &self.cycle_history_cache[index];
663                if !cycle_info.1 {
664                    return Err(PosError::CycleUnfinished(c));
665                }
666                // take the final_state_hash_snapshot at cycle - 3
667                // it will later be combined with rng_seed from cycle - 2 to determine the selection seed
668                // do this here to avoid a potential attacker manipulating the selections
669                let state_hash = self
670                    .get_cycle_history_final_state_hash_snapshot(cycle_info.0)
671                    .ok_or_else(|| {
672                        PosError::ContainerInconsistency(format!(
673                            "complete cycle {} is missing final state hash snapshot required for selector feeding",
674                            c
675                        ))
676                    })?;
677                (self.get_all_roll_counts(cycle_info.0), Some(state_hash))
678            }
679            // looking back to negative cycles
680            None => (self.initial_rolls.clone(), None),
681        };
682
683        // get seed lookback
684        let lookback_seed = match draw_cycle.checked_sub(2) {
685            // looking back in history
686            Some(c) => {
687                let index = self
688                    .get_cycle_index(c)
689                    .ok_or(PosError::CycleUnavailable(c))?;
690                let cycle_info = &self.cycle_history_cache[index];
691                if !cycle_info.1 {
692                    return Err(PosError::CycleUnfinished(c));
693                }
694                let u64_ser = U64VarIntSerializer::new();
695                let mut seed = Vec::new();
696                u64_ser.serialize(&c, &mut seed).unwrap();
697                let rng_seed = self
698                    .get_cycle_history_rng_seed(cycle_info.0)
699                    .ok_or_else(|| {
700                        PosError::ContainerInconsistency(format!(
701                            "complete cycle {} is missing RNG seed required for selector feeding",
702                            c
703                        ))
704                    })?;
705                seed.extend(rng_seed.into_vec());
706                if let Some(lookback_state_hash) = lookback_state_hash {
707                    seed.extend(lookback_state_hash.to_bytes());
708                }
709                Hash::compute_from(&seed)
710            }
711            // looking back to negative cycles
712            None => self.initial_seeds[draw_cycle as usize],
713        };
714
715        // feed selector
716        self.selector
717            .as_ref()
718            .feed_cycle(draw_cycle, lookback_rolls, lookback_seed)
719    }
720
721    /// Feeds the selector targeting a given draw cycle
722    pub fn feed_cycle_state_hash(
723        &self,
724        cycle: u64,
725        final_state_hash: HashXof<HASH_XOF_SIZE_BYTES>,
726    ) {
727        if self.get_cycle_index(cycle).is_some() {
728            let mut batch = DBBatch::new();
729            self.put_cycle_history_final_state_hash_snapshot(
730                cycle,
731                Some(final_state_hash),
732                &mut batch,
733            );
734
735            self.db.write().write_batch(batch, Default::default(), None);
736        } else {
737            panic!("cycle {} should be contained here", cycle);
738        }
739    }
740}
741
742// RocksDB getters
743impl PoSFinalState {
744    /// Retrieves the amount of rolls a given address has at the latest cycle
745    pub fn get_rolls_for(&self, addr: &Address) -> u64 {
746        self.cycle_history_cache
747            .back()
748            .and_then(|info| {
749                let cycle = info.0;
750                let db = self.db.read();
751
752                let key = roll_count_key!(self.cycle_history_cycle_prefix(cycle), addr);
753
754                if let Some(serialized_value) =
755                    db.get_cf(STATE_CF, key).expect(CYCLE_HISTORY_DESER_ERROR)
756                {
757                    let (_, amount) = self
758                        .cycle_info_deserializer
759                        .rolls_deser
760                        .u64_deserializer
761                        .deserialize::<DeserializeError>(&serialized_value)
762                        .expect(CYCLE_HISTORY_DESER_ERROR);
763
764                    Some(amount)
765                } else {
766                    None
767                }
768            })
769            .unwrap_or_default()
770    }
771
772    /// Retrieves the amount of rolls a given address has at a given cycle - 3
773    /// if cycle - 3 does not exist, values from initial rolls are returned
774    pub fn get_address_active_rolls(&self, addr: &Address, cycle: u64) -> Option<u64> {
775        match cycle.checked_sub(3) {
776            Some(lookback_cycle) => {
777                let key = roll_count_key!(self.cycle_history_cycle_prefix(lookback_cycle), addr);
778                let db = self.db.read();
779                if let Some(serialized_value) =
780                    db.get_cf(STATE_CF, key).expect(CYCLE_HISTORY_DESER_ERROR)
781                {
782                    let (_, amount) = self
783                        .cycle_info_deserializer
784                        .rolls_deser
785                        .u64_deserializer
786                        .deserialize::<DeserializeError>(&serialized_value)
787                        .expect(CYCLE_HISTORY_DESER_ERROR);
788
789                    Some(amount)
790                } else {
791                    None
792                }
793            }
794            None => self.initial_rolls.get(addr).cloned(),
795        }
796    }
797
798    /// Gets all active rolls for a given cycle - 3, use self.initial_rolls if cycle - 3 does not exist
799    pub fn get_all_active_rolls(&self, cycle: u64) -> BTreeMap<Address, u64> {
800        match cycle.checked_sub(3) {
801            Some(lookback_cycle) => {
802                // get rolls
803                self.get_all_roll_counts(lookback_cycle)
804            }
805            None => self.initial_rolls.clone(),
806        }
807    }
808
809    /// Retrieves every deferred credit in a slot range
810    /// Warning: this can be quite slow if there is tons of Deferred credits (e.g. just after genesis) to fetch
811    ///          so there is an option to filter by an address to improve the speed (cf json rpc api get_addresses)
812    pub fn get_deferred_credits_range<R>(
813        &self,
814        range: R,
815        addr_filter: Option<&Address>,
816    ) -> DeferredCredits
817    where
818        R: RangeBounds<Slot>,
819    {
820        let db = self.db.read();
821
822        let mut deferred_credits = DeferredCredits::new();
823
824        let mut start_key_buffer = Vec::new();
825        start_key_buffer.extend_from_slice(DEFERRED_CREDITS_PREFIX.as_bytes());
826
827        match range.start_bound() {
828            Included(slot) => {
829                start_key_buffer.extend_from_slice(&slot.to_bytes_key());
830            }
831            Excluded(slot) => {
832                start_key_buffer.extend_from_slice(
833                    &slot
834                        .get_next_slot(self.config.thread_count)
835                        .expect(DEFERRED_CREDITS_SER_ERROR)
836                        .to_bytes_key(),
837                );
838            }
839            Unbounded => {}
840        };
841
842        let iterator_cf = db
843            .iterator_cf(
844                STATE_CF,
845                MassaIteratorMode::From(&start_key_buffer, MassaDirection::Forward),
846            )
847            .take_while(|(k, _)| k.starts_with(DEFERRED_CREDITS_PREFIX.as_bytes()));
848
849        for (serialized_key, serialized_value) in iterator_cf {
850            // deserialize the slot
851            let rest_key = &serialized_key[DEFERRED_CREDITS_PREFIX.len()..];
852            let (rest_key, slot) = buf_to_array_ctr(rest_key, Slot::from_bytes_key)
853                .expect(DEFERRED_CREDITS_DESER_ERROR);
854
855            if !range.contains(&slot) {
856                break;
857            }
858
859            let (_, address) = self
860                .credits_deserializer
861                .address_deserializer
862                .deserialize::<DeserializeError>(rest_key)
863                .expect(DEFERRED_CREDITS_DESER_ERROR);
864
865            if let Some(addr_filter_value) = &addr_filter {
866                if &&address != addr_filter_value {
867                    continue;
868                }
869            }
870
871            let (_, amount) = self
872                .credits_deserializer
873                .amount_deserializer
874                .deserialize::<DeserializeError>(&serialized_value)
875                .expect(DEFERRED_CREDITS_DESER_ERROR);
876
877            deferred_credits.insert(slot, address, amount);
878        }
879
880        deferred_credits
881    }
882
883    /// Gets the index of a cycle in history
884    pub fn get_cycle_index(&self, cycle: u64) -> Option<usize> {
885        let first_cycle = match self.cycle_history_cache.front() {
886            Some(c) => c.0,
887            None => return None, // history empty
888        };
889        if cycle < first_cycle {
890            return None; // in the past
891        }
892        let index: usize = match (cycle - first_cycle).try_into() {
893            Ok(v) => v,
894            Err(_) => return None, // usize overflow
895        };
896        if index >= self.cycle_history_cache.len() {
897            return None; // in the future
898        }
899        Some(index)
900    }
901
902    /// Get all the roll counts for a given cycle
903    pub fn get_all_roll_counts(&self, cycle: u64) -> BTreeMap<Address, u64> {
904        let db = self.db.read();
905
906        if self.get_cycle_index(cycle).is_none() {
907            panic!("Cycle {} not in history", cycle)
908        }
909
910        let mut roll_counts: BTreeMap<Address, u64> = BTreeMap::new();
911        let prefix = roll_count_prefix!(self.cycle_history_cycle_prefix(cycle));
912        for (serialized_key, serialized_value) in db.prefix_iterator_cf(STATE_CF, &prefix) {
913            if !serialized_key.starts_with(prefix.as_bytes()) {
914                break;
915            }
916
917            // deserialize the cycle
918            let rest_key = &serialized_key[CYCLE_HISTORY_PREFIX.len()..];
919            let (rest_key, _cycle) = buf_to_array_ctr(rest_key, |v| u64::from_be_bytes(*v))
920                .expect(CYCLE_HISTORY_DESER_ERROR);
921
922            let (_, address) = self
923                .cycle_info_deserializer
924                .rolls_deser
925                .address_deserializer
926                .deserialize::<DeserializeError>(&rest_key[1..])
927                .expect(CYCLE_HISTORY_DESER_ERROR);
928
929            let (_, amount) = self
930                .cycle_info_deserializer
931                .rolls_deser
932                .u64_deserializer
933                .deserialize::<DeserializeError>(&serialized_value)
934                .expect(CYCLE_HISTORY_DESER_ERROR);
935
936            roll_counts.insert(address, amount);
937        }
938
939        roll_counts
940    }
941
942    /// Retrieves the productions statistics for all addresses on a given cycle
943    pub fn get_all_production_stats(
944        &self,
945        cycle: u64,
946    ) -> Option<PreHashMap<Address, ProductionStats>> {
947        self.get_cycle_index(cycle)
948            .map(|idx| self.get_all_production_stats_private(self.cycle_history_cache[idx].0))
949    }
950
951    /// Retrieves the productions statistics for all addresses on a given cycle
952    fn get_all_production_stats_private(&self, cycle: u64) -> PreHashMap<Address, ProductionStats> {
953        let db = self.db.read();
954
955        let mut production_stats: PreHashMap<Address, ProductionStats> = PreHashMap::default();
956        let mut cur_production_stat = ProductionStats::default();
957        let mut cur_address = None;
958
959        let prefix = prod_stats_prefix!(self.cycle_history_cycle_prefix(cycle));
960        for (serialized_key, serialized_value) in db.prefix_iterator_cf(STATE_CF, &prefix) {
961            if !serialized_key.starts_with(prefix.as_bytes()) {
962                break;
963            }
964
965            // deserialize the cycle
966            let rest_key = &serialized_key[CYCLE_HISTORY_PREFIX.len()..];
967            let (rest_key, _cycle) = buf_to_array_ctr(rest_key, |v| u64::from_be_bytes(*v))
968                .expect(CYCLE_HISTORY_DESER_ERROR);
969
970            let (rest_key, address) = self
971                .cycle_info_deserializer
972                .production_stats_deser
973                .address_deserializer
974                .deserialize::<DeserializeError>(&rest_key[1..])
975                .expect(CYCLE_HISTORY_DESER_ERROR);
976
977            if cur_address != Some(address) {
978                cur_address = Some(address);
979                cur_production_stat = ProductionStats::default();
980            }
981
982            let (_, value) = self
983                .cycle_info_deserializer
984                .production_stats_deser
985                .u64_deserializer
986                .deserialize::<DeserializeError>(&serialized_value)
987                .expect(CYCLE_HISTORY_DESER_ERROR);
988
989            if rest_key.len() == 1 && rest_key[0] == PROD_STATS_FAIL_IDENT {
990                cur_production_stat.block_failure_count = value;
991            } else if rest_key.len() == 1 && rest_key[0] == PROD_STATS_SUCCESS_IDENT {
992                cur_production_stat.block_success_count = value;
993            } else {
994                panic!("{}", CYCLE_HISTORY_DESER_ERROR);
995            }
996
997            production_stats.insert(address, cur_production_stat);
998        }
999
1000        production_stats
1001    }
1002
1003    /// Getter for the rng_seed of a given cycle, prioritizing the cache and querying the database as fallback.
1004    fn get_cycle_history_rng_seed(&self, cycle: u64) -> Option<BitVec<u8>> {
1005        if let Some((cached_cycle, rng_seed)) = &self.rng_seed_cache {
1006            if *cached_cycle == cycle {
1007                return Some(rng_seed.clone());
1008            }
1009        }
1010
1011        let serialized_rng_seed = self
1012            .db
1013            .read()
1014            .get_cf(
1015                STATE_CF,
1016                rng_seed_key!(self.cycle_history_cycle_prefix(cycle)),
1017            )
1018            .expect(CYCLE_HISTORY_DESER_ERROR);
1019        let serialized_rng_seed = serialized_rng_seed?;
1020
1021        let (_, rng_seed) = self
1022            .cycle_info_deserializer
1023            .bitvec_deser
1024            .deserialize::<DeserializeError>(&serialized_rng_seed)
1025            .expect(CYCLE_HISTORY_DESER_ERROR);
1026
1027        Some(rng_seed)
1028    }
1029
1030    /// Getter for the final_state_hash_snapshot of a given cycle.
1031    ///
1032    /// Returns `None` if the RocksDB key is absent or if the stored value is `None`.
1033    fn get_cycle_history_final_state_hash_snapshot(
1034        &self,
1035        cycle: u64,
1036    ) -> Option<HashXof<HASH_XOF_SIZE_BYTES>> {
1037        let db = self.db.read();
1038
1039        let serialized_state_hash = db
1040            .get_cf(
1041                STATE_CF,
1042                final_state_hash_snapshot_key!(self.cycle_history_cycle_prefix(cycle)),
1043            )
1044            .expect(CYCLE_HISTORY_DESER_ERROR)?;
1045        let (_, state_hash) = self
1046            .cycle_info_deserializer
1047            .opt_hash_deser
1048            .deserialize::<DeserializeError>(&serialized_state_hash)
1049            .expect(CYCLE_HISTORY_DESER_ERROR);
1050        state_hash
1051    }
1052
1053    /// Used to recompute the cycle cache from the disk.
1054    ///
1055    fn get_cycle_history_cycles(&self) -> Vec<(u64, bool)> {
1056        let mut found_cycles: Vec<u64> = Vec::new();
1057
1058        {
1059            let db = self.db.read();
1060
1061            while let Some((serialized_key, _)) = match found_cycles.last() {
1062                Some(prev_cycle) => db
1063                    .iterator_cf(
1064                        STATE_CF,
1065                        MassaIteratorMode::From(
1066                            &self.cycle_history_cycle_prefix(prev_cycle.saturating_add(1)),
1067                            MassaDirection::Forward,
1068                        ),
1069                    )
1070                    .next(),
1071                None => db
1072                    .iterator_cf(
1073                        STATE_CF,
1074                        MassaIteratorMode::From(
1075                            CYCLE_HISTORY_PREFIX.as_bytes(),
1076                            MassaDirection::Forward,
1077                        ),
1078                    )
1079                    .next(),
1080            } {
1081                if !serialized_key.starts_with(CYCLE_HISTORY_PREFIX.as_bytes()) {
1082                    break;
1083                }
1084
1085                // deserialize the cycle
1086                let rest_key = &serialized_key[CYCLE_HISTORY_PREFIX.len()..];
1087                let (_rest_key, cycle) = buf_to_array_ctr(rest_key, |v| u64::from_be_bytes(*v))
1088                    .expect(CYCLE_HISTORY_DESER_ERROR);
1089
1090                found_cycles.push(cycle);
1091            }
1092        }
1093
1094        found_cycles
1095            .into_iter()
1096            .map(|cycle| (cycle, self.is_cycle_complete(cycle).unwrap_or(false)))
1097            .collect()
1098    }
1099
1100    /// Queries a given cycle info in the database
1101    pub fn get_cycle_info(&self, cycle: u64) -> Option<CycleInfo> {
1102        // TODO improve performance by not taking a lock and re-searching the key at every element
1103
1104        let complete = self.is_cycle_complete(cycle)?;
1105        let rng_seed = self.get_cycle_history_rng_seed(cycle)?;
1106        let final_state_hash_snapshot = self.get_cycle_history_final_state_hash_snapshot(cycle);
1107
1108        let roll_counts = self.get_all_roll_counts(cycle);
1109
1110        let production_stats = self.get_all_production_stats(cycle).unwrap_or_default();
1111
1112        let mut cycle_info =
1113            CycleInfo::new(cycle, complete, roll_counts, rng_seed, production_stats);
1114        cycle_info.final_state_hash_snapshot = final_state_hash_snapshot;
1115        Some(cycle_info)
1116    }
1117
1118    /// Gets the deferred credits for a given address that will be credited at a given slot
1119    pub fn get_address_credits_for_slot(&self, addr: &Address, slot: &Slot) -> Option<Amount> {
1120        let db = self.db.read();
1121
1122        let mut serialized_key = Vec::new();
1123        serialized_key.extend_from_slice(&slot.to_bytes_key());
1124        self.credits_serializer
1125            .address_ser
1126            .serialize(addr, &mut serialized_key)
1127            .expect(DEFERRED_CREDITS_SER_ERROR);
1128
1129        match db.get_cf(STATE_CF, deferred_credits_key!(serialized_key)) {
1130            Ok(Some(serialized_amount)) => {
1131                let (_, amount) = self
1132                    .credits_deserializer
1133                    .amount_deserializer
1134                    .deserialize::<DeserializeError>(&serialized_amount)
1135                    .expect(DEFERRED_CREDITS_DESER_ERROR);
1136                Some(amount)
1137            }
1138            _ => None,
1139        }
1140    }
1141
1142    /// Gets the production stats for a given address
1143    pub fn get_production_stats_for_address(
1144        &self,
1145        cycle: u64,
1146        address: &Address,
1147    ) -> Option<ProductionStats> {
1148        let db = self.db.read();
1149
1150        let prefix = self.cycle_history_cycle_prefix(cycle);
1151
1152        let query = vec![
1153            (STATE_CF, prod_stats_fail_key!(prefix, *address)),
1154            (STATE_CF, prod_stats_success_key!(prefix, *address)),
1155        ];
1156
1157        let results = db.multi_get_cf(query);
1158
1159        match (results.first(), results.get(1)) {
1160            (Some(Ok(Some(serialized_fail))), Some(Ok(Some(serialized_success)))) => {
1161                let (_, fail) = self
1162                    .cycle_info_deserializer
1163                    .production_stats_deser
1164                    .u64_deserializer
1165                    .deserialize::<DeserializeError>(serialized_fail)
1166                    .expect(CYCLE_HISTORY_DESER_ERROR);
1167                let (_, success) = self
1168                    .cycle_info_deserializer
1169                    .production_stats_deser
1170                    .u64_deserializer
1171                    .deserialize::<DeserializeError>(serialized_success)
1172                    .expect(CYCLE_HISTORY_DESER_ERROR);
1173
1174                Some(ProductionStats {
1175                    block_success_count: success,
1176                    block_failure_count: fail,
1177                })
1178            }
1179            _ => None,
1180        }
1181    }
1182
1183    /// Check if a cycle is complete (all slots finalized)
1184    pub fn is_cycle_complete(&self, cycle: u64) -> Option<bool> {
1185        let key = complete_key!(self.cycle_history_cycle_prefix(cycle));
1186        let res = self.db.read().get_cf(STATE_CF, key);
1187        match res {
1188            Ok(Some(complete_value)) => Some(complete_value.first() == Some(&1)),
1189            Ok(None) => None,
1190            Err(err) => {
1191                panic!(
1192                    "Error while checking if cycle {} is complete: {}",
1193                    cycle, err
1194                );
1195            }
1196        }
1197    }
1198}
1199
1200// RocksDB setters
1201impl PoSFinalState {
1202    /// Helper function to put a new CycleInfo to RocksDB, and update the cycle_history cache
1203    pub fn put_new_cycle_info(&mut self, cycle_info: &CycleInfo, batch: &mut DBBatch) {
1204        self.put_cycle_history_complete(cycle_info.cycle, cycle_info.complete, batch);
1205        self.put_cycle_history_rng_seed(cycle_info.cycle, cycle_info.rng_seed.clone(), batch);
1206        self.put_cycle_history_final_state_hash_snapshot(
1207            cycle_info.cycle,
1208            cycle_info.final_state_hash_snapshot,
1209            batch,
1210        );
1211        for (address, roll) in cycle_info.roll_counts.iter() {
1212            self.put_cycle_history_address_entry(
1213                cycle_info.cycle,
1214                address,
1215                Some(roll),
1216                None,
1217                batch,
1218            );
1219        }
1220        for (address, prod_stats) in cycle_info.production_stats.iter() {
1221            self.put_cycle_history_address_entry(
1222                cycle_info.cycle,
1223                address,
1224                None,
1225                Some(prod_stats),
1226                batch,
1227            );
1228        }
1229        self.cycle_history_cache
1230            .push_back((cycle_info.cycle, cycle_info.complete));
1231    }
1232
1233    /// Helper function to put a the complete flag for a given cycle
1234    fn put_cycle_history_complete(&mut self, cycle: u64, value: bool, batch: &mut DBBatch) {
1235        let db = self.db.read();
1236
1237        let prefix = self.cycle_history_cycle_prefix(cycle);
1238
1239        let serialized_value = if value { &[1] } else { &[0] };
1240
1241        db.put_or_update_entry_value(batch, complete_key!(prefix), serialized_value);
1242
1243        if let Some(index) = self.get_cycle_index(cycle) {
1244            self.cycle_history_cache[index].1 = value;
1245        }
1246    }
1247
1248    /// Helper function to put a the final_state_hash_snapshot for a given cycle
1249    fn put_cycle_history_final_state_hash_snapshot(
1250        &self,
1251        cycle: u64,
1252        value: Option<HashXof<HASH_XOF_SIZE_BYTES>>,
1253        batch: &mut DBBatch,
1254    ) {
1255        let db = self.db.read();
1256
1257        let prefix = self.cycle_history_cycle_prefix(cycle);
1258
1259        let mut serialized_value = Vec::new();
1260        self.cycle_info_serializer
1261            .opt_hash_ser
1262            .serialize(&value, &mut serialized_value)
1263            .expect(CYCLE_HISTORY_SER_ERROR);
1264
1265        db.put_or_update_entry_value(
1266            batch,
1267            final_state_hash_snapshot_key!(prefix),
1268            &serialized_value,
1269        );
1270    }
1271
1272    /// Helper function to put a the rng_seed for a given cycle
1273    fn put_cycle_history_rng_seed(&mut self, cycle: u64, value: BitVec<u8>, batch: &mut DBBatch) {
1274        let db = self.db.read();
1275
1276        let prefix = self.cycle_history_cycle_prefix(cycle);
1277
1278        let mut serialized_value = Vec::new();
1279        self.cycle_info_serializer
1280            .bitvec_ser
1281            .serialize(&value, &mut serialized_value)
1282            .expect(CYCLE_HISTORY_SER_ERROR);
1283
1284        self.rng_seed_cache = Some((cycle, value.clone()));
1285
1286        db.put_or_update_entry_value(batch, rng_seed_key!(prefix), &serialized_value);
1287    }
1288
1289    /// Internal function to put an entry for a given address in the cycle history
1290    fn put_cycle_history_address_entry(
1291        &self,
1292        cycle: u64,
1293        address: &Address,
1294        roll_count: Option<&u64>,
1295        production_stats: Option<&ProductionStats>,
1296        batch: &mut DBBatch,
1297    ) {
1298        let db = self.db.read();
1299
1300        let prefix = self.cycle_history_cycle_prefix(cycle);
1301
1302        // Roll count
1303        if let Some(0) = roll_count {
1304            db.delete_key(batch, roll_count_key!(prefix, address));
1305        } else if let Some(roll_count) = roll_count {
1306            let mut serialized_roll_count = Vec::new();
1307            self.cycle_info_serializer
1308                .u64_ser
1309                .serialize(roll_count, &mut serialized_roll_count)
1310                .expect(CYCLE_HISTORY_SER_ERROR);
1311            db.put_or_update_entry_value(
1312                batch,
1313                roll_count_key!(prefix, address),
1314                &serialized_roll_count,
1315            );
1316        }
1317
1318        // Production stats
1319        if let Some(production_stats) = production_stats {
1320            let mut serialized_prod_stats_fail = Vec::new();
1321            self.cycle_info_serializer
1322                .u64_ser
1323                .serialize(
1324                    &production_stats.block_failure_count,
1325                    &mut serialized_prod_stats_fail,
1326                )
1327                .expect(CYCLE_HISTORY_SER_ERROR);
1328            db.put_or_update_entry_value(
1329                batch,
1330                prod_stats_fail_key!(prefix, address),
1331                &serialized_prod_stats_fail,
1332            );
1333
1334            // Production stats success
1335            let mut serialized_prod_stats_success = Vec::new();
1336            self.cycle_info_serializer
1337                .u64_ser
1338                .serialize(
1339                    &production_stats.block_success_count,
1340                    &mut serialized_prod_stats_success,
1341                )
1342                .expect(CYCLE_HISTORY_SER_ERROR);
1343            db.put_or_update_entry_value(
1344                batch,
1345                prod_stats_success_key!(prefix, address),
1346                &serialized_prod_stats_success,
1347            );
1348        }
1349    }
1350
1351    /// Internal function to put an entry
1352    pub fn put_deferred_credits_entry(
1353        &self,
1354        slot: &Slot,
1355        address: &Address,
1356        amount: &Amount,
1357        batch: &mut DBBatch,
1358    ) {
1359        let db = self.db.read();
1360
1361        let mut serialized_key = Vec::new();
1362        serialized_key.extend_from_slice(&slot.to_bytes_key());
1363        self.credits_serializer
1364            .address_ser
1365            .serialize(address, &mut serialized_key)
1366            .expect(DEFERRED_CREDITS_SER_ERROR);
1367
1368        if amount.is_zero() {
1369            db.delete_key(batch, deferred_credits_key!(serialized_key));
1370        } else {
1371            let mut serialized_amount = Vec::new();
1372            self.credits_serializer
1373                .amount_ser
1374                .serialize(amount, &mut serialized_amount)
1375                .expect(DEFERRED_CREDITS_SER_ERROR);
1376
1377            db.put_or_update_entry_value(
1378                batch,
1379                deferred_credits_key!(serialized_key),
1380                &serialized_amount,
1381            );
1382        }
1383    }
1384}
1385
1386/// Helpers for key and value management
1387impl PoSFinalState {
1388    /// Helper function to construct the key prefix associated with a given cycle
1389    fn cycle_history_cycle_prefix(&self, cycle: u64) -> Vec<u8> {
1390        let mut serialized_key = Vec::new();
1391        serialized_key.extend_from_slice(CYCLE_HISTORY_PREFIX.as_bytes());
1392        serialized_key.extend_from_slice(&cycle.to_be_bytes());
1393        serialized_key
1394    }
1395
1396    /// Deserializes the key and value, useful after bootstrap
1397    pub fn is_cycle_history_key_value_valid(
1398        &self,
1399        serialized_key: &[u8],
1400        serialized_value: &[u8],
1401    ) -> bool {
1402        if !serialized_key.starts_with(CYCLE_HISTORY_PREFIX.as_bytes()) {
1403            return false;
1404        }
1405
1406        // deserialize the cycle
1407        let rest_key = &serialized_key[CYCLE_HISTORY_PREFIX.len()..];
1408        let Some((rest_key, cycle)) = buf_to_array_ctr(rest_key, |v| u64::from_be_bytes(*v)) else {
1409            return false;
1410        };
1411
1412        if rest_key.is_empty() {
1413            return false;
1414        }
1415
1416        let slots_per_cycle = self.slots_per_cycle();
1417
1418        match rest_key[0] {
1419            COMPLETE_IDENT => {
1420                if rest_key.len() != 1 {
1421                    return false;
1422                }
1423                if serialized_value.len() != 1 {
1424                    return false;
1425                }
1426                if serialized_value[0] > 1 {
1427                    return false;
1428                }
1429                // A completed cycle must have an RNG seed of exactly slots_per_cycle bits.
1430                if serialized_value[0] == 1 {
1431                    let prefix = self.cycle_history_cycle_prefix(cycle);
1432                    let Ok(Some(serialized_rng_seed)) =
1433                        self.db.read().get_cf(STATE_CF, rng_seed_key!(prefix))
1434                    else {
1435                        return false;
1436                    };
1437                    let Ok((rest, rng_seed)) =
1438                        self.cycle_info_deserializer
1439                            .bitvec_deser
1440                            .deserialize::<DeserializeError>(&serialized_rng_seed)
1441                    else {
1442                        return false;
1443                    };
1444                    if !rest.is_empty() || rng_seed.len() != slots_per_cycle {
1445                        return false;
1446                    }
1447                }
1448            }
1449            RNG_SEED_IDENT => {
1450                if rest_key.len() != 1 {
1451                    return false;
1452                }
1453                let Ok((rest_key, rng_seed)) = self
1454                    .cycle_info_deserializer
1455                    .bitvec_deser
1456                    .deserialize::<DeserializeError>(serialized_value)
1457                else {
1458                    return false;
1459                };
1460                if !rest_key.is_empty() {
1461                    return false;
1462                }
1463                // Seed length must never exceed a full cycle; exact length for complete
1464                // cycles is enforced when validating the COMPLETE flag above.
1465                if rng_seed.len() > slots_per_cycle {
1466                    return false;
1467                }
1468            }
1469            FINAL_STATE_HASH_SNAPSHOT_IDENT => {
1470                if rest_key.len() != 1 {
1471                    return false;
1472                }
1473                let Ok((rest_key, _final_state_hash)) = self
1474                    .cycle_info_deserializer
1475                    .opt_hash_deser
1476                    .deserialize::<DeserializeError>(serialized_value)
1477                else {
1478                    return false;
1479                };
1480                if !rest_key.is_empty() {
1481                    return false;
1482                }
1483            }
1484            ROLL_COUNT_IDENT => {
1485                let Ok((rest_key, _addr)): std::result::Result<
1486                    (&[u8], Address),
1487                    nom::Err<massa_serialization::DeserializeError<'_>>,
1488                > = self
1489                    .cycle_info_deserializer
1490                    .rolls_deser
1491                    .address_deserializer
1492                    .deserialize::<DeserializeError>(&rest_key[1..])
1493                else {
1494                    return false;
1495                };
1496                if !rest_key.is_empty() {
1497                    return false;
1498                }
1499                let Ok((rest_key, _addr)) = self
1500                    .cycle_info_deserializer
1501                    .rolls_deser
1502                    .u64_deserializer
1503                    .deserialize::<DeserializeError>(serialized_value)
1504                else {
1505                    return false;
1506                };
1507                if !rest_key.is_empty() {
1508                    return false;
1509                }
1510            }
1511            PROD_STATS_IDENT => {
1512                let Ok((rest_key, _addr)): std::result::Result<
1513                    (&[u8], Address),
1514                    nom::Err<massa_serialization::DeserializeError<'_>>,
1515                > = self
1516                    .cycle_info_deserializer
1517                    .rolls_deser
1518                    .address_deserializer
1519                    .deserialize::<DeserializeError>(&rest_key[1..])
1520                else {
1521                    return false;
1522                };
1523                if rest_key.len() != 1 {
1524                    return false;
1525                }
1526
1527                match rest_key[0] {
1528                    PROD_STATS_FAIL_IDENT => {
1529                        let Ok((rest_key, _fail)) = self
1530                            .cycle_info_deserializer
1531                            .production_stats_deser
1532                            .u64_deserializer
1533                            .deserialize::<DeserializeError>(serialized_value)
1534                        else {
1535                            return false;
1536                        };
1537                        if !rest_key.is_empty() {
1538                            return false;
1539                        }
1540                    }
1541                    PROD_STATS_SUCCESS_IDENT => {
1542                        let Ok((rest_key, _success)) = self
1543                            .cycle_info_deserializer
1544                            .production_stats_deser
1545                            .u64_deserializer
1546                            .deserialize::<DeserializeError>(serialized_value)
1547                        else {
1548                            return false;
1549                        };
1550                        if !rest_key.is_empty() {
1551                            return false;
1552                        }
1553                    }
1554                    _ => {
1555                        return false;
1556                    }
1557                }
1558            }
1559            _ => {
1560                return false;
1561            }
1562        }
1563
1564        true
1565    }
1566
1567    /// Deserializes the key and value, useful after bootstrap
1568    pub fn is_deferred_credits_key_value_valid(
1569        &self,
1570        serialized_key: &[u8],
1571        serialized_value: &[u8],
1572    ) -> bool {
1573        if !serialized_key.starts_with(DEFERRED_CREDITS_PREFIX.as_bytes()) {
1574            return false;
1575        }
1576
1577        // deserialize the slot
1578        let rest_key = &serialized_key[DEFERRED_CREDITS_PREFIX.len()..];
1579        let Some((rest_key, _slot)) = buf_to_array_ctr(rest_key, Slot::from_bytes_key) else {
1580            return false;
1581        };
1582
1583        let Ok((rest_key, _addr)): std::result::Result<
1584            (&[u8], Address),
1585            nom::Err<massa_serialization::DeserializeError<'_>>,
1586        > = self
1587            .credits_deserializer
1588            .address_deserializer
1589            .deserialize::<DeserializeError>(rest_key)
1590        else {
1591            return false;
1592        };
1593        if !rest_key.is_empty() {
1594            return false;
1595        }
1596
1597        let Ok((rest_key, _amount)) = self
1598            .credits_deserializer
1599            .amount_deserializer
1600            .deserialize::<DeserializeError>(serialized_value)
1601        else {
1602            return false;
1603        };
1604        if !rest_key.is_empty() {
1605            return false;
1606        }
1607
1608        true
1609    }
1610}
1611
1612/// Helpers for testing
1613#[cfg(feature = "test-exports")]
1614impl PoSFinalState {
1615    /// Queries all the deferred credits in the database
1616    pub fn get_deferred_credits(&self) -> DeferredCredits {
1617        let db = self.db.read();
1618
1619        let mut deferred_credits = DeferredCredits::new();
1620
1621        for (serialized_key, serialized_value) in
1622            db.prefix_iterator_cf(STATE_CF, DEFERRED_CREDITS_PREFIX.as_bytes())
1623        {
1624            if !serialized_key.starts_with(DEFERRED_CREDITS_PREFIX.as_bytes()) {
1625                break;
1626            }
1627
1628            // deserialize the slot
1629            let rest_key = &serialized_key[DEFERRED_CREDITS_PREFIX.len()..];
1630            let (rest_key, slot) = buf_to_array_ctr(rest_key, Slot::from_bytes_key)
1631                .expect(DEFERRED_CREDITS_DESER_ERROR);
1632
1633            let (_, address) = self
1634                .credits_deserializer
1635                .address_deserializer
1636                .deserialize::<DeserializeError>(rest_key)
1637                .expect(DEFERRED_CREDITS_DESER_ERROR);
1638
1639            let (_, amount) = self
1640                .credits_deserializer
1641                .amount_deserializer
1642                .deserialize::<DeserializeError>(&serialized_value)
1643                .expect(DEFERRED_CREDITS_DESER_ERROR);
1644
1645            deferred_credits.insert(slot, address, amount);
1646        }
1647        deferred_credits
1648    }
1649}
1650
1651#[cfg(test)]
1652mod tests {
1653    use super::*;
1654
1655    use std::collections::HashMap;
1656    use std::str::FromStr;
1657    use std::sync::Arc;
1658
1659    use assert_matches::assert_matches;
1660    use bitvec::prelude::*;
1661    use parking_lot::RwLock;
1662    use tempfile::TempDir;
1663
1664    use crate::MockSelectorController;
1665
1666    use massa_db_exports::{MassaDBConfig, MassaDBController};
1667    use massa_db_worker::MassaDB;
1668    use massa_models::config::constants::{
1669        MAX_DEFERRED_CREDITS_LENGTH, MAX_PRODUCTION_STATS_LENGTH, MAX_ROLLS_COUNT_LENGTH,
1670        POS_SAVED_CYCLES,
1671    };
1672    use massa_signature::KeyPair;
1673
1674    // This test checks that the initial deferred credits are loaded correctly
1675    #[test]
1676    fn test_initial_deferred_credits_loading() {
1677        let initial_deferred_credits_file = tempfile::NamedTempFile::new()
1678            .expect("could not create temporary initial deferred credits file");
1679        // write down some deferred credits
1680        let deferred_credits_file_contents = "{
1681            \"AU12pAcVUzsgUBJHaYSAtDKVTYnUT9NorBDjoDovMfAFTLFa16MNa\": [
1682                {
1683                    \"slot\": {\"period\": 3, \"thread\": 0},
1684                    \"amount\": \"5.01\"
1685                },
1686                {
1687                    \"slot\": {\"period\": 4, \"thread\": 1},
1688                    \"amount\": \"6.0\"
1689                },
1690                {
1691                    \"slot\": {\"period\": 255, \"thread\": 0},
1692                    \"amount\": \"5.01\"
1693                },
1694                {
1695                    \"slot\": {\"period\": 256, \"thread\": 0},
1696                    \"amount\": \"6.0\"
1697                }
1698            ],
1699            \"AU1wN8rn4SkwYSTDF3dHFY4U28KtsqKL1NnEjDZhHnHEy6cEQm53\": [
1700                {
1701                    \"slot\": {\"period\": 3, \"thread\": 0},
1702                    \"amount\": \"2.01\"
1703                }
1704            ]
1705        }";
1706        std::fs::write(
1707            initial_deferred_credits_file.path(),
1708            deferred_credits_file_contents.as_bytes(),
1709        )
1710        .expect("failed writing initial deferred credits file");
1711        let pos_config = PoSConfig {
1712            periods_per_cycle: 2,
1713            thread_count: 2,
1714            cycle_history_length: POS_SAVED_CYCLES,
1715            max_rolls_length: MAX_ROLLS_COUNT_LENGTH,
1716            max_production_stats_length: MAX_PRODUCTION_STATS_LENGTH,
1717            max_credit_length: MAX_DEFERRED_CREDITS_LENGTH,
1718            initial_deferred_credits_path: Some(initial_deferred_credits_file.path().to_path_buf()),
1719        };
1720        // initialize the database and pos_state
1721        let tempdir = tempfile::TempDir::new().expect("cannot create temp directory");
1722        let db_config = MassaDBConfig {
1723            path: tempdir.path().to_path_buf(),
1724            max_history_length: 10,
1725            max_final_state_elements_size: 100_000,
1726            max_versioning_elements_size: 100_000,
1727            thread_count: 2,
1728            max_ledger_backups: 10,
1729            enable_metrics: false,
1730        };
1731        let db = Arc::new(RwLock::new(
1732            Box::new(MassaDB::new(db_config)) as Box<dyn MassaDBController + 'static>
1733        ));
1734        let selector_controller = Box::new(MockSelectorController::new());
1735        let init_seed = Hash::compute_from(b"");
1736        let initial_seeds = vec![Hash::compute_from(init_seed.to_bytes()), init_seed];
1737        let cycle_info_deserializer = CycleInfoDeserializer::new(
1738            pos_config.max_rolls_length,
1739            pos_config.max_production_stats_length,
1740        );
1741        let mut pos_state = PoSFinalState {
1742            config: pos_config.clone(),
1743            db: db.clone(),
1744            cycle_history_cache: Default::default(),
1745            rng_seed_cache: None,
1746            selector: selector_controller,
1747            initial_rolls: Default::default(),
1748            initial_seeds,
1749            credits_serializer: DeferredCreditSerializer::new(),
1750            credits_deserializer: DeferredCreditDeserializer::new(pos_config.max_credit_length),
1751            cycle_info_serializer: CycleInfoSerializer::new(),
1752            cycle_info_deserializer,
1753        };
1754        let mut batch = DBBatch::new();
1755        // load initial deferred credits
1756        let mut ledger = massa_ledger_exports::MockLedgerController::new();
1757        ledger.expect_entry_exists().returning(|_| false);
1758        pos_state
1759            .load_initial_deferred_credits(&mut batch, &ledger)
1760            .expect("error while loading initial deferred credits");
1761        db.write().write_batch(batch, DBBatch::new(), None);
1762        let deferred_credits = pos_state.get_deferred_credits().credits;
1763        let addr1 =
1764            Address::from_str("AU12pAcVUzsgUBJHaYSAtDKVTYnUT9NorBDjoDovMfAFTLFa16MNa").unwrap();
1765        let a_a1_s3 = Amount::from_str("5.01").unwrap();
1766        let addr2 =
1767            Address::from_str("AU1wN8rn4SkwYSTDF3dHFY4U28KtsqKL1NnEjDZhHnHEy6cEQm53").unwrap();
1768        let a_a2_s3 = Amount::from_str("2.01").unwrap();
1769        let expected_credits = vec![
1770            (
1771                Slot::new(3, 0),
1772                vec![(addr1, a_a1_s3), (addr2, a_a2_s3)]
1773                    .into_iter()
1774                    .collect(),
1775            ),
1776            (
1777                Slot::new(4, 1),
1778                vec![(addr1, Amount::from_str("6.0").unwrap())]
1779                    .into_iter()
1780                    .collect(),
1781            ),
1782            (
1783                Slot::new(255, 0),
1784                vec![(addr1, Amount::from_str("5.01").unwrap())]
1785                    .into_iter()
1786                    .collect(),
1787            ),
1788            (
1789                Slot::new(256, 0),
1790                vec![(addr1, Amount::from_str("6.0").unwrap())]
1791                    .into_iter()
1792                    .collect(),
1793            ),
1794        ]
1795        .into_iter()
1796        .collect();
1797        assert_eq!(
1798            deferred_credits, expected_credits,
1799            "deferred credits not loaded correctly"
1800        );
1801        let credits_range_1 =
1802            pos_state.get_deferred_credits_range(Slot::new(4, 0)..Slot::new(4, 1), None);
1803        assert!(credits_range_1.is_empty());
1804        let credits_range_2 =
1805            pos_state.get_deferred_credits_range(Slot::new(2, 0)..Slot::new(3, 1), None);
1806        let expected_credits_range_2 = vec![(
1807            Slot::new(3, 0),
1808            vec![(addr1, a_a1_s3), (addr2, a_a2_s3)]
1809                .into_iter()
1810                .collect(),
1811        )]
1812        .into_iter()
1813        .collect();
1814        assert_eq!(credits_range_2.credits, expected_credits_range_2);
1815        let credits_range_3 =
1816            pos_state.get_deferred_credits_range(Slot::new(7, 0)..Slot::new(9, 5), None);
1817        assert!(credits_range_3.is_empty());
1818        let credits_range_4 =
1819            pos_state.get_deferred_credits_range(Slot::new(7, 0)..Slot::new(255, 1), None);
1820
1821        let a_a1_s255 = Amount::from_str("5.01").unwrap();
1822        let expected_credits_range_4 = vec![(
1823            Slot::new(255, 0),
1824            vec![(addr1, a_a1_s255)].into_iter().collect(),
1825        )]
1826        .into_iter()
1827        .collect();
1828        assert_eq!(credits_range_4.credits, expected_credits_range_4);
1829    }
1830
1831    // This test checks that the initial rolls are loaded correctly
1832    #[test]
1833    fn test_initial_rolls_loading() {
1834        let initial_deferred_credits_file =
1835            tempfile::NamedTempFile::new().expect("could not create temporary initial rolls file");
1836        let initial_rolls_file_0 =
1837            tempfile::NamedTempFile::new().expect("could not create temporary initial rolls file");
1838
1839        // write down some rolls info
1840        let rolls_file_contents_0 = "{}";
1841        std::fs::write(
1842            initial_rolls_file_0.path(),
1843            rolls_file_contents_0.as_bytes(),
1844        )
1845        .expect("failed writing initial rolls file 0");
1846        // write down some deferred credits
1847        let deferred_credits_file_contents = "{}";
1848        std::fs::write(
1849            initial_deferred_credits_file.path(),
1850            deferred_credits_file_contents.as_bytes(),
1851        )
1852        .expect("failed writing initial deferred credits file");
1853
1854        // initialize the database
1855        let tempdir = tempfile::TempDir::new().expect("cannot create temp directory");
1856        let db_config = MassaDBConfig {
1857            path: tempdir.path().to_path_buf(),
1858            max_history_length: 10,
1859            max_final_state_elements_size: 100,
1860            max_versioning_elements_size: 100,
1861            thread_count: 2,
1862            max_ledger_backups: 10,
1863            enable_metrics: false,
1864        };
1865        let db = Arc::new(RwLock::new(
1866            Box::new(MassaDB::new(db_config)) as Box<dyn MassaDBController + 'static>
1867        ));
1868        let selector_controller = Box::new(MockSelectorController::new());
1869
1870        let pos_config = PoSConfig {
1871            periods_per_cycle: 2,
1872            thread_count: 2,
1873            cycle_history_length: POS_SAVED_CYCLES,
1874            max_rolls_length: MAX_ROLLS_COUNT_LENGTH,
1875            max_production_stats_length: MAX_PRODUCTION_STATS_LENGTH,
1876            max_credit_length: MAX_DEFERRED_CREDITS_LENGTH,
1877            initial_deferred_credits_path: Some(initial_deferred_credits_file.path().to_path_buf()),
1878        };
1879
1880        let init_seed = "";
1881        let pos_state_0 = PoSFinalState::new(
1882            pos_config.clone(),
1883            init_seed,
1884            &initial_rolls_file_0.path().to_path_buf(),
1885            selector_controller,
1886            db.clone(),
1887        );
1888
1889        // Check ok with empty roll file
1890        assert!(pos_state_0.is_ok());
1891
1892        // Init a invalid roll file (invalid content)
1893        let initial_rolls_file_1 =
1894            tempfile::NamedTempFile::new().expect("could not create temporary initial rolls file");
1895        let data_1 = HashMap::from([("foo", "bar")]);
1896        let rolls_file_contents_1 = serde_json::to_string(&data_1).unwrap();
1897        // println!("rolls_file_contents_1: {}", rolls_file_contents_1);
1898        std::fs::write(
1899            initial_rolls_file_1.path(),
1900            rolls_file_contents_1.as_bytes(),
1901        )
1902        .expect("failed writing initial rolls file 1");
1903        let selector_controller = Box::new(MockSelectorController::new());
1904
1905        let pos_state_1 = PoSFinalState::new(
1906            pos_config.clone(),
1907            init_seed,
1908            &initial_rolls_file_1.path().to_path_buf(),
1909            selector_controller,
1910            db.clone(),
1911        );
1912
1913        // Check ko
1914        assert!(pos_state_1.is_err());
1915
1916        // Now check with valid data
1917
1918        let addr1_ = "AU12pAcVUzsgUBJHaYSAtDKVTYnUT9NorBDjoDovMfAFTLFa16MNa";
1919        let addr1 = Address::from_str(addr1_).unwrap();
1920        let roll1: u64 = 5;
1921        let addr2_ = "AU1wN8rn4SkwYSTDF3dHFY4U28KtsqKL1NnEjDZhHnHEy6cEQm53";
1922        let addr2 = Address::from_str(addr2_).unwrap();
1923        let roll2: u64 = 65529;
1924
1925        let initial_rolls_file_2 =
1926            tempfile::NamedTempFile::new().expect("could not create temporary initial rolls file");
1927        let data_2 = HashMap::from([(addr1_, roll1), (addr2_, roll2)]);
1928        let rolls_file_contents_2 = serde_json::to_string(&data_2).unwrap();
1929        std::fs::write(
1930            initial_rolls_file_2.path(),
1931            rolls_file_contents_2.as_bytes(),
1932        )
1933        .expect("failed writing initial rolls file 2");
1934        let selector_controller = Box::new(MockSelectorController::new());
1935
1936        let mut pos_state_2 = PoSFinalState::new(
1937            pos_config,
1938            init_seed,
1939            &initial_rolls_file_2.path().to_path_buf(),
1940            selector_controller,
1941            db.clone(),
1942        )
1943        .unwrap();
1944
1945        assert_eq!(pos_state_2.initial_rolls.get(&addr1), Some(&roll1));
1946        assert_eq!(pos_state_2.initial_rolls.get(&addr2), Some(&roll2));
1947        // Note: get_address_active_rolls (if not cycle -3) uses initial_rolls
1948        assert_eq!(pos_state_2.get_address_active_rolls(&addr1, 0), Some(roll1));
1949        // Note: get_all_active_rolls (if not cycle -3) uses initial_rolls
1950        assert_eq!(
1951            pos_state_2.get_all_active_rolls(0).get(&addr1),
1952            Some(&roll1)
1953        );
1954        assert_eq!(
1955            pos_state_2.get_all_active_rolls(0).get(&addr2),
1956            Some(&roll2)
1957        );
1958
1959        // Simulate some cycle with address 1 + decrease rolls and address 2 + increase rolls
1960        let roll_counts_c0 = BTreeMap::from([(addr1, roll1), (addr2, roll2)]);
1961        let roll_a1_c1 = roll1.checked_sub(1).unwrap();
1962        let roll_counts_c1 =
1963            BTreeMap::from([(addr1, roll_a1_c1), (addr2, roll2.checked_add(1).unwrap())]);
1964        let roll_counts_c2 = BTreeMap::from([
1965            (addr1, roll1.checked_sub(2).unwrap()),
1966            (addr2, roll2.checked_add(2).unwrap()),
1967        ]);
1968        let roll_a1_c3 = roll1.checked_sub(3).unwrap();
1969        let roll_a2_c3 = roll2.checked_add(3).unwrap();
1970        let roll_counts_c3 = BTreeMap::from([(addr1, roll_a1_c3), (addr2, roll_a2_c3)]);
1971        let roll_a1_c4 = roll1.checked_sub(4).unwrap();
1972        let roll_a2_c4 = roll2.checked_add(4).unwrap();
1973        let roll_counts_c4 = BTreeMap::from([(addr1, roll_a1_c4), (addr2, roll_a2_c4)]);
1974        let cycle_info_0 = CycleInfo::new(
1975            0,
1976            Default::default(),
1977            roll_counts_c0,
1978            Default::default(),
1979            Default::default(),
1980        );
1981        let cycle_info_1 = CycleInfo {
1982            cycle: 1,
1983            roll_counts: roll_counts_c1.clone(),
1984            ..cycle_info_0.clone()
1985        };
1986        let cycle_info_2 = CycleInfo {
1987            cycle: 2,
1988            roll_counts: roll_counts_c2,
1989            ..cycle_info_0.clone()
1990        };
1991        let cycle_info_3 = CycleInfo {
1992            cycle: 3,
1993            roll_counts: roll_counts_c3,
1994            ..cycle_info_0.clone()
1995        };
1996        let cycle_info_4 = CycleInfo {
1997            cycle: 4,
1998            roll_counts: roll_counts_c4,
1999            ..cycle_info_0.clone()
2000        };
2001
2002        let mut batch = DBBatch::new();
2003        pos_state_2.put_new_cycle_info(&cycle_info_0, &mut batch);
2004        pos_state_2.put_new_cycle_info(&cycle_info_1, &mut batch);
2005        pos_state_2.put_new_cycle_info(&cycle_info_2, &mut batch);
2006        pos_state_2.put_new_cycle_info(&cycle_info_3, &mut batch);
2007        pos_state_2.put_new_cycle_info(&cycle_info_4, &mut batch);
2008        pos_state_2
2009            .db
2010            .write()
2011            .write_batch(batch, DBBatch::new(), None);
2012
2013        assert_eq!(
2014            pos_state_2.get_address_active_rolls(&addr1, 4),
2015            Some(roll_a1_c1)
2016        );
2017        assert_eq!(
2018            pos_state_2.get_address_active_rolls(&addr1, 4 + 3 + 1),
2019            None
2020        );
2021        assert_eq!(pos_state_2.get_address_active_rolls(&addr1, 19), None);
2022
2023        let rolls_1 = pos_state_2.get_rolls_for(&addr1);
2024        assert_eq!(rolls_1, roll_a1_c4);
2025        let rolls_2 = pos_state_2.get_rolls_for(&addr2);
2026        assert_eq!(rolls_2, roll_a2_c4);
2027
2028        // Will fetch at cycle - 3
2029        let active_rolls = pos_state_2.get_all_active_rolls(4);
2030        assert_eq!(active_rolls, roll_counts_c1);
2031    }
2032
2033    // This test checks that the recompute_pos_cache function recovers every cycle and does return correctly.
2034    // The test example is chosen to have the cycles overlap a power of 2, to check that the order of cycles in the DB
2035    // (lexicographical over bytes) is the same as the order of cycles in the cache (numerical).
2036    // If this is not handled properly, the node hangs as explained here: https://github.com/massalabs/massa/issues/4101
2037    #[test]
2038    fn test_pos_cache_recomputation() {
2039        let pos_config = PoSConfig {
2040            periods_per_cycle: 2,
2041            thread_count: 2,
2042            cycle_history_length: POS_SAVED_CYCLES,
2043            max_rolls_length: MAX_ROLLS_COUNT_LENGTH,
2044            max_production_stats_length: MAX_PRODUCTION_STATS_LENGTH,
2045            max_credit_length: MAX_DEFERRED_CREDITS_LENGTH,
2046            initial_deferred_credits_path: None,
2047        };
2048
2049        // initialize the database and pos_state
2050        let tempdir = TempDir::new().expect("cannot create temp directory");
2051        let db_config = MassaDBConfig {
2052            path: tempdir.path().to_path_buf(),
2053            max_history_length: 10,
2054            max_final_state_elements_size: 100_000,
2055            max_versioning_elements_size: 100_000,
2056            thread_count: 2,
2057            max_ledger_backups: 10,
2058            enable_metrics: false,
2059        };
2060        let db = Arc::new(RwLock::new(
2061            Box::new(MassaDB::new(db_config)) as Box<dyn MassaDBController + 'static>
2062        ));
2063        let selector_controller = Box::new(MockSelectorController::new());
2064        let init_seed = Hash::compute_from(b"");
2065        let initial_seeds = vec![Hash::compute_from(init_seed.to_bytes()), init_seed];
2066
2067        let cycle_info_deserializer = CycleInfoDeserializer::new(
2068            pos_config.max_rolls_length,
2069            pos_config.max_production_stats_length,
2070        );
2071
2072        let mut pos_state = PoSFinalState {
2073            config: pos_config.clone(),
2074            db: db.clone(),
2075            cycle_history_cache: Default::default(),
2076            rng_seed_cache: None,
2077            selector: selector_controller,
2078            initial_rolls: Default::default(),
2079            initial_seeds,
2080            credits_serializer: DeferredCreditSerializer::new(),
2081            credits_deserializer: DeferredCreditDeserializer::new(pos_config.max_credit_length),
2082            cycle_info_serializer: CycleInfoSerializer::new(),
2083            cycle_info_deserializer,
2084        };
2085
2086        // Populate the disk with some cycle infos
2087        let mut cycle_infos = Vec::new();
2088        for cycle in 509..516 {
2089            cycle_infos.push(CycleInfo::new(
2090                cycle,
2091                Default::default(),
2092                Default::default(),
2093                Default::default(),
2094                Default::default(),
2095            ));
2096        }
2097
2098        let mut batch = DBBatch::new();
2099        pos_state.put_new_cycle_info(&cycle_infos[0], &mut batch);
2100        pos_state.put_new_cycle_info(&cycle_infos[1], &mut batch);
2101        pos_state.put_new_cycle_info(&cycle_infos[2], &mut batch);
2102        pos_state.put_new_cycle_info(&cycle_infos[3], &mut batch);
2103        pos_state.put_new_cycle_info(&cycle_infos[4], &mut batch);
2104        pos_state.put_new_cycle_info(&cycle_infos[5], &mut batch);
2105        pos_state.put_new_cycle_info(&cycle_infos[6], &mut batch);
2106
2107        pos_state
2108            .db
2109            .write()
2110            .write_batch(batch, DBBatch::new(), None);
2111
2112        // Recompute the cache, and assert we do not miss any data
2113        // We .clear() the cache explicitly, even though we do not need to, to make sure the recomputation works
2114        pos_state.cycle_history_cache.clear();
2115        pos_state.recompute_pos_state_caches();
2116
2117        // Assert that the cache contains the expected cycles
2118        assert_eq!(
2119            pos_state.cycle_history_cache.len(),
2120            cycle_infos.len(),
2121            "Cycle history len does not match"
2122        );
2123
2124        for ((cycle_from_history, _), cycle) in
2125            pos_state.cycle_history_cache.iter().zip(cycle_infos.iter())
2126        {
2127            assert_eq!(
2128                *cycle_from_history, cycle.cycle,
2129                "Cycle number does not match"
2130            );
2131        }
2132    }
2133
2134    // This test aims to check that the basic workflow of apply changes to the PoS state works.
2135    #[test]
2136    fn test_pos_final_state_hash_computation() {
2137        let pos_config = PoSConfig {
2138            // slots_per_cycle = 4*2 = 8 so the seed bits accumulated below stay within bounds
2139            periods_per_cycle: 4,
2140            thread_count: 2,
2141            cycle_history_length: POS_SAVED_CYCLES,
2142            max_rolls_length: MAX_ROLLS_COUNT_LENGTH,
2143            max_production_stats_length: MAX_PRODUCTION_STATS_LENGTH,
2144            max_credit_length: MAX_DEFERRED_CREDITS_LENGTH,
2145            initial_deferred_credits_path: None,
2146        };
2147
2148        // initialize the database and pos_state
2149        let tempdir = TempDir::new().expect("cannot create temp directory");
2150        let db_config = MassaDBConfig {
2151            path: tempdir.path().to_path_buf(),
2152            max_history_length: 10,
2153            max_final_state_elements_size: 100_000,
2154            max_versioning_elements_size: 100_000,
2155            thread_count: 2,
2156            max_ledger_backups: 10,
2157            enable_metrics: false,
2158        };
2159        let db = Arc::new(RwLock::new(
2160            Box::new(MassaDB::new(db_config)) as Box<dyn MassaDBController + 'static>
2161        ));
2162        let selector_controller = Box::new(MockSelectorController::new());
2163        let init_seed = Hash::compute_from(b"");
2164        let initial_seeds = vec![Hash::compute_from(init_seed.to_bytes()), init_seed];
2165
2166        let cycle_info_deserializer = CycleInfoDeserializer::new(
2167            pos_config.max_rolls_length,
2168            pos_config.max_production_stats_length,
2169        );
2170
2171        let mut pos_state = PoSFinalState {
2172            config: pos_config.clone(),
2173            db: db.clone(),
2174            cycle_history_cache: Default::default(),
2175            rng_seed_cache: None,
2176            selector: selector_controller,
2177            initial_rolls: Default::default(),
2178            initial_seeds,
2179            credits_serializer: DeferredCreditSerializer::new(),
2180            credits_deserializer: DeferredCreditDeserializer::new(pos_config.max_credit_length),
2181            cycle_info_serializer: CycleInfoSerializer::new(),
2182            cycle_info_deserializer,
2183        };
2184
2185        pos_state.recompute_pos_state_caches();
2186
2187        let mut batch = DBBatch::new();
2188        pos_state.create_initial_cycle(&mut batch);
2189        db.write()
2190            .write_batch(batch, Default::default(), Some(Slot::new(0, 0)));
2191
2192        // add changes
2193        let addr = Address::from_public_key(&KeyPair::generate(0).unwrap().get_public_key());
2194        let mut roll_changes = PreHashMap::default();
2195        roll_changes.insert(addr, 10);
2196        let mut production_stats = PreHashMap::default();
2197        production_stats.insert(
2198            addr,
2199            ProductionStats {
2200                block_success_count: 4,
2201                block_failure_count: 0,
2202            },
2203        );
2204        let changes = PoSChanges {
2205            seed_bits: bitvec![u8, Lsb0; 0, 1],
2206            roll_changes: roll_changes.clone(),
2207            production_stats: production_stats.clone(),
2208            deferred_credits: DeferredCredits::new(),
2209        };
2210
2211        let mut batch = DBBatch::new();
2212        pos_state
2213            .apply_changes_to_batch(changes, Slot::new(0, 0), false, &mut batch)
2214            .unwrap();
2215        db.write()
2216            .write_batch(batch, Default::default(), Some(Slot::new(0, 0)));
2217
2218        // update changes once
2219        roll_changes.clear();
2220        roll_changes.insert(addr, 20);
2221        production_stats.clear();
2222        production_stats.insert(
2223            addr,
2224            ProductionStats {
2225                block_success_count: 4,
2226                block_failure_count: 6,
2227            },
2228        );
2229        let changes = PoSChanges {
2230            seed_bits: bitvec![u8, Lsb0; 1, 0],
2231            roll_changes: roll_changes.clone(),
2232            production_stats: production_stats.clone(),
2233            deferred_credits: DeferredCredits::new(),
2234        };
2235
2236        let mut batch = DBBatch::new();
2237        pos_state
2238            .apply_changes_to_batch(changes, Slot::new(0, 1), false, &mut batch)
2239            .unwrap();
2240        db.write()
2241            .write_batch(batch, Default::default(), Some(Slot::new(0, 1)));
2242
2243        // update changes twice
2244        roll_changes.clear();
2245        roll_changes.insert(addr, 0);
2246        production_stats.clear();
2247        production_stats.insert(
2248            addr,
2249            ProductionStats {
2250                block_success_count: 4,
2251                block_failure_count: 12,
2252            },
2253        );
2254
2255        let changes = PoSChanges {
2256            seed_bits: bitvec![u8, Lsb0; 0, 1],
2257            roll_changes,
2258            production_stats,
2259            deferred_credits: DeferredCredits::new(),
2260        };
2261
2262        let mut batch = DBBatch::new();
2263        pos_state
2264            .apply_changes_to_batch(changes, Slot::new(1, 0), false, &mut batch)
2265            .unwrap();
2266        db.write()
2267            .write_batch(batch, Default::default(), Some(Slot::new(1, 0)));
2268
2269        let cycles = pos_state.get_cycle_history_cycles();
2270        assert_eq!(cycles.len(), 1, "wrong number of cycles");
2271        assert_eq!(cycles[0].0, 0, "cycle should be the 1st one");
2272        assert!(!cycles[0].1, "cycle should not be complete yet");
2273
2274        let cycle_info_a = pos_state.get_cycle_info(0).unwrap();
2275
2276        let mut prod_stats = HashMap::default();
2277        prod_stats.insert(
2278            addr,
2279            ProductionStats {
2280                block_success_count: 12,
2281                block_failure_count: 18,
2282            },
2283        );
2284
2285        let cycle_info_b = CycleInfo::new(
2286            0,
2287            false,
2288            BTreeMap::default(),
2289            bitvec![u8, Lsb0; 0, 0, 0, 1, 1, 0, 0, 1],
2290            prod_stats,
2291        );
2292
2293        assert_eq!(cycle_info_a, cycle_info_b, "cycle_info mismatch");
2294    }
2295
2296    #[test]
2297    fn test_feed_selector() {
2298        let initial_deferred_credits_file =
2299            tempfile::NamedTempFile::new().expect("could not create temporary initial rolls file");
2300
2301        let initial_rolls_file_0 =
2302            tempfile::NamedTempFile::new().expect("could not create temporary initial rolls file");
2303
2304        // write down some rolls info
2305        let rolls_file_contents_0 = "{}";
2306        std::fs::write(
2307            initial_rolls_file_0.path(),
2308            rolls_file_contents_0.as_bytes(),
2309        )
2310        .expect("failed writing initial rolls file 0");
2311
2312        // write down some deferred credits
2313        let deferred_credits_file_contents = "{}";
2314        std::fs::write(
2315            initial_deferred_credits_file.path(),
2316            deferred_credits_file_contents.as_bytes(),
2317        )
2318        .expect("failed writing initial deferred credits file");
2319
2320        // initialize the database
2321        let tempdir = tempfile::TempDir::new().expect("cannot create temp directory");
2322        let db_config = MassaDBConfig {
2323            path: tempdir.path().to_path_buf(),
2324            max_history_length: 10,
2325            max_final_state_elements_size: 100,
2326            max_versioning_elements_size: 100,
2327            thread_count: 2,
2328            max_ledger_backups: 10,
2329            enable_metrics: false,
2330        };
2331        let db = Arc::new(RwLock::new(
2332            Box::new(MassaDB::new(db_config)) as Box<dyn MassaDBController + 'static>
2333        ));
2334        let selector_controller = Box::new(MockSelectorController::new());
2335
2336        let pos_config = PoSConfig {
2337            periods_per_cycle: 2,
2338            thread_count: 2,
2339            cycle_history_length: POS_SAVED_CYCLES,
2340            max_rolls_length: MAX_ROLLS_COUNT_LENGTH,
2341            max_production_stats_length: MAX_PRODUCTION_STATS_LENGTH,
2342            max_credit_length: MAX_DEFERRED_CREDITS_LENGTH,
2343            initial_deferred_credits_path: Some(initial_deferred_credits_file.path().to_path_buf()),
2344        };
2345
2346        let init_seed = "";
2347        let mut pos_state_0 = PoSFinalState::new(
2348            pos_config,
2349            init_seed,
2350            &initial_rolls_file_0.path().to_path_buf(),
2351            selector_controller,
2352            db.clone(),
2353        )
2354        .unwrap();
2355
2356        let cycle_info_0 = CycleInfo::new(
2357            0,
2358            true,
2359            Default::default(),
2360            Default::default(),
2361            Default::default(),
2362        );
2363        let cycle_info_1 = CycleInfo {
2364            cycle: 1,
2365            ..cycle_info_0.clone()
2366        };
2367        let cycle_info_2 = CycleInfo {
2368            cycle: 2,
2369            ..cycle_info_0.clone()
2370        };
2371        let cycle_info_3 = CycleInfo {
2372            cycle: 3,
2373            ..cycle_info_0.clone()
2374        };
2375        let cycle_info_4 = CycleInfo {
2376            cycle: 4,
2377            complete: false,
2378            ..cycle_info_0.clone()
2379        };
2380
2381        let mut batch = DBBatch::new();
2382        pos_state_0.put_new_cycle_info(&cycle_info_0, &mut batch);
2383        pos_state_0.put_new_cycle_info(&cycle_info_1, &mut batch);
2384        pos_state_0.put_new_cycle_info(&cycle_info_2, &mut batch);
2385        pos_state_0.put_new_cycle_info(&cycle_info_3, &mut batch);
2386        pos_state_0.put_new_cycle_info(&cycle_info_4, &mut batch);
2387        pos_state_0
2388            .db
2389            .write()
2390            .write_batch(batch, DBBatch::new(), None);
2391
2392        // Test feed selector with unfinished cycle
2393        assert_matches!(
2394            pos_state_0.feed_selector(4 + 3),
2395            Err(PosError::CycleUnfinished(4))
2396        );
2397
2398        // F100: complete cycles without a final state hash snapshot must error, not panic
2399        assert_matches!(
2400            pos_state_0.feed_selector(4),
2401            Err(PosError::ContainerInconsistency(_))
2402        );
2403        assert_matches!(
2404            pos_state_0.validate_selector_history(),
2405            Err(PosError::ContainerInconsistency(_))
2406        );
2407    }
2408
2409    #[test]
2410    fn test_feed_selector_2() {
2411        let initial_deferred_credits_file =
2412            tempfile::NamedTempFile::new().expect("could not create temporary initial rolls file");
2413
2414        let initial_rolls_file_0 =
2415            tempfile::NamedTempFile::new().expect("could not create temporary initial rolls file");
2416
2417        // write down some rolls info
2418        let rolls_file_contents_0 = "{}";
2419        std::fs::write(
2420            initial_rolls_file_0.path(),
2421            rolls_file_contents_0.as_bytes(),
2422        )
2423        .expect("failed writing initial rolls file 0");
2424
2425        // write down some deferred credits
2426        let deferred_credits_file_contents = "{}";
2427        std::fs::write(
2428            initial_deferred_credits_file.path(),
2429            deferred_credits_file_contents.as_bytes(),
2430        )
2431        .expect("failed writing initial deferred credits file");
2432
2433        // initialize the database
2434        let tempdir = tempfile::TempDir::new().expect("cannot create temp directory");
2435        let db_config = MassaDBConfig {
2436            path: tempdir.path().to_path_buf(),
2437            max_history_length: 10,
2438            thread_count: 2,
2439            max_final_state_elements_size: 100,
2440            max_versioning_elements_size: 100,
2441            max_ledger_backups: 10,
2442            enable_metrics: false,
2443        };
2444        let db = Arc::new(RwLock::new(
2445            Box::new(MassaDB::new(db_config)) as Box<dyn MassaDBController + 'static>
2446        ));
2447        let mut selector_controller = Box::new(MockSelectorController::new());
2448        selector_controller
2449            .expect_feed_cycle()
2450            .times(1)
2451            .returning(|_, _, _| Ok(()));
2452
2453        let pos_config = PoSConfig {
2454            periods_per_cycle: 2,
2455            thread_count: 2,
2456            cycle_history_length: POS_SAVED_CYCLES,
2457            max_rolls_length: MAX_ROLLS_COUNT_LENGTH,
2458            max_production_stats_length: MAX_PRODUCTION_STATS_LENGTH,
2459            max_credit_length: MAX_DEFERRED_CREDITS_LENGTH,
2460            initial_deferred_credits_path: Some(initial_deferred_credits_file.path().to_path_buf()),
2461        };
2462
2463        let init_seed = "";
2464        let mut pos_state_0 = PoSFinalState::new(
2465            pos_config,
2466            init_seed,
2467            &initial_rolls_file_0.path().to_path_buf(),
2468            selector_controller,
2469            db.clone(),
2470        )
2471        .unwrap();
2472
2473        let cycle_info_0 = CycleInfo::new(
2474            0,
2475            true,
2476            Default::default(),
2477            Default::default(),
2478            Default::default(),
2479        );
2480        let cycle_info_1 = CycleInfo {
2481            cycle: 1,
2482            ..cycle_info_0.clone()
2483        };
2484        let cycle_info_2 = CycleInfo {
2485            cycle: 2,
2486            ..cycle_info_0.clone()
2487        };
2488        let cycle_info_3 = CycleInfo {
2489            cycle: 3,
2490            ..cycle_info_0.clone()
2491        };
2492        let cycle_info_4 = CycleInfo {
2493            cycle: 4,
2494            complete: false,
2495            ..cycle_info_0.clone()
2496        };
2497
2498        let mut batch = DBBatch::new();
2499        pos_state_0.put_new_cycle_info(&cycle_info_0, &mut batch);
2500        pos_state_0.put_new_cycle_info(&cycle_info_1, &mut batch);
2501        pos_state_0.put_new_cycle_info(&cycle_info_2, &mut batch);
2502        pos_state_0.put_new_cycle_info(&cycle_info_3, &mut batch);
2503        pos_state_0.put_new_cycle_info(&cycle_info_4, &mut batch);
2504        pos_state_0
2505            .db
2506            .write()
2507            .write_batch(batch, DBBatch::new(), None);
2508
2509        // Note: by using cycle 2, feed_selector will use initial_rolls & initial_seeds
2510        let _ = pos_state_0.feed_selector(2);
2511    }
2512
2513    /// Oversized RNG seeds must be rejected by DB key/value validation (bootstrap/DoS hardening).
2514    #[test]
2515    fn test_rng_seed_validation_rejects_oversized_seed() {
2516        let pos_config = PoSConfig {
2517            periods_per_cycle: 2,
2518            thread_count: 2, // slots_per_cycle = 4
2519            cycle_history_length: POS_SAVED_CYCLES,
2520            max_rolls_length: MAX_ROLLS_COUNT_LENGTH,
2521            max_production_stats_length: MAX_PRODUCTION_STATS_LENGTH,
2522            max_credit_length: MAX_DEFERRED_CREDITS_LENGTH,
2523            initial_deferred_credits_path: None,
2524        };
2525
2526        let tempdir = TempDir::new().expect("cannot create temp directory");
2527        let db_config = MassaDBConfig {
2528            path: tempdir.path().to_path_buf(),
2529            max_history_length: 10,
2530            max_final_state_elements_size: 100_000,
2531            max_versioning_elements_size: 100_000,
2532            thread_count: 2,
2533            max_ledger_backups: 10,
2534            enable_metrics: false,
2535        };
2536        let db = Arc::new(RwLock::new(
2537            Box::new(MassaDB::new(db_config)) as Box<dyn MassaDBController + 'static>
2538        ));
2539        let selector_controller = Box::new(MockSelectorController::new());
2540        let init_seed = Hash::compute_from(b"");
2541        let initial_seeds = vec![Hash::compute_from(init_seed.to_bytes()), init_seed];
2542
2543        let cycle_info_deserializer = CycleInfoDeserializer::new(
2544            pos_config.max_rolls_length,
2545            pos_config.max_production_stats_length,
2546        );
2547
2548        let mut pos_state = PoSFinalState {
2549            config: pos_config.clone(),
2550            db: db.clone(),
2551            cycle_history_cache: Default::default(),
2552            rng_seed_cache: None,
2553            selector: selector_controller,
2554            initial_rolls: Default::default(),
2555            initial_seeds,
2556            credits_serializer: DeferredCreditSerializer::new(),
2557            credits_deserializer: DeferredCreditDeserializer::new(pos_config.max_credit_length),
2558            cycle_info_serializer: CycleInfoSerializer::new(),
2559            cycle_info_deserializer,
2560        };
2561
2562        let oversized = bitvec![u8, Lsb0; 0, 1, 0, 1, 0]; // 5 > 4
2563        let cycle_info = CycleInfo::new(
2564            0,
2565            false,
2566            BTreeMap::default(),
2567            oversized.clone(),
2568            PreHashMap::default(),
2569        );
2570        let mut batch = DBBatch::new();
2571        pos_state.put_new_cycle_info(&cycle_info, &mut batch);
2572        db.write().write_batch(batch, DBBatch::new(), None);
2573
2574        let prefix = pos_state.cycle_history_cycle_prefix(0);
2575        let key = rng_seed_key!(prefix);
2576        let mut serialized_value = Vec::new();
2577        pos_state
2578            .cycle_info_serializer
2579            .bitvec_ser
2580            .serialize(&oversized, &mut serialized_value)
2581            .unwrap();
2582
2583        assert!(
2584            !pos_state.is_cycle_history_key_value_valid(&key, &serialized_value),
2585            "oversized RNG seed must be rejected"
2586        );
2587
2588        // Completing a cycle with a short seed must also be rejected
2589        let short_seed = bitvec![u8, Lsb0; 0, 1];
2590        let mut batch = DBBatch::new();
2591        pos_state.put_cycle_history_rng_seed(0, short_seed, &mut batch);
2592        pos_state.put_cycle_history_complete(0, true, &mut batch);
2593        db.write().write_batch(batch, DBBatch::new(), None);
2594
2595        let complete_key = complete_key!(prefix);
2596        assert!(
2597            !pos_state.is_cycle_history_key_value_valid(&complete_key, &[1]),
2598            "complete cycle with wrong RNG seed length must be rejected"
2599        );
2600    }
2601
2602    /// Cycle completion with a wrong seed length returns an error instead of panicking.
2603    #[test]
2604    fn test_apply_changes_returns_error_on_seed_length_mismatch() {
2605        let pos_config = PoSConfig {
2606            periods_per_cycle: 2,
2607            thread_count: 2, // slots_per_cycle = 4
2608            cycle_history_length: POS_SAVED_CYCLES,
2609            max_rolls_length: MAX_ROLLS_COUNT_LENGTH,
2610            max_production_stats_length: MAX_PRODUCTION_STATS_LENGTH,
2611            max_credit_length: MAX_DEFERRED_CREDITS_LENGTH,
2612            initial_deferred_credits_path: None,
2613        };
2614
2615        let tempdir = TempDir::new().expect("cannot create temp directory");
2616        let db_config = MassaDBConfig {
2617            path: tempdir.path().to_path_buf(),
2618            max_history_length: 10,
2619            max_final_state_elements_size: 100_000,
2620            max_versioning_elements_size: 100_000,
2621            thread_count: 2,
2622            max_ledger_backups: 10,
2623            enable_metrics: false,
2624        };
2625        let db = Arc::new(RwLock::new(
2626            Box::new(MassaDB::new(db_config)) as Box<dyn MassaDBController + 'static>
2627        ));
2628        let selector_controller = Box::new(MockSelectorController::new());
2629        let init_seed = Hash::compute_from(b"");
2630        let initial_seeds = vec![Hash::compute_from(init_seed.to_bytes()), init_seed];
2631
2632        let cycle_info_deserializer = CycleInfoDeserializer::new(
2633            pos_config.max_rolls_length,
2634            pos_config.max_production_stats_length,
2635        );
2636
2637        let mut pos_state = PoSFinalState {
2638            config: pos_config.clone(),
2639            db: db.clone(),
2640            cycle_history_cache: Default::default(),
2641            rng_seed_cache: None,
2642            selector: selector_controller,
2643            initial_rolls: Default::default(),
2644            initial_seeds,
2645            credits_serializer: DeferredCreditSerializer::new(),
2646            credits_deserializer: DeferredCreditDeserializer::new(pos_config.max_credit_length),
2647            cycle_info_serializer: CycleInfoSerializer::new(),
2648            cycle_info_deserializer,
2649        };
2650
2651        // Incomplete cycle with a short seed; completing it with one more bit yields length 3 ≠ 4.
2652        let mut batch = DBBatch::new();
2653        pos_state.put_new_cycle_info(
2654            &CycleInfo::new(
2655                0,
2656                false,
2657                BTreeMap::default(),
2658                bitvec![u8, Lsb0; 0, 1],
2659                PreHashMap::default(),
2660            ),
2661            &mut batch,
2662        );
2663        db.write()
2664            .write_batch(batch, Default::default(), Some(Slot::new(1, 0)));
2665        pos_state.recompute_pos_state_caches();
2666
2667        // Last slot of cycle 0 with periods_per_cycle=2, thread_count=2 is (1, 1).
2668        let changes = PoSChanges {
2669            seed_bits: bitvec![u8, Lsb0; 1],
2670            roll_changes: PreHashMap::default(),
2671            production_stats: PreHashMap::default(),
2672            deferred_credits: DeferredCredits::new(),
2673        };
2674        let mut batch = DBBatch::new();
2675        let err = pos_state
2676            .apply_changes_to_batch(changes, Slot::new(1, 1), false, &mut batch)
2677            .expect_err("seed length mismatch on cycle completion must be an error");
2678        assert_matches!(err, PosError::ContainerInconsistency(_));
2679    }
2680
2681    /// Completing a cycle during downtime interpolation must cap seed bits at slots_per_cycle
2682    /// when the existing seed already includes genesis padding.
2683    #[test]
2684    fn test_create_new_cycle_from_last_caps_seed_on_complete() {
2685        let pos_config = PoSConfig {
2686            periods_per_cycle: 2,
2687            thread_count: 2, // slots_per_cycle = 4
2688            cycle_history_length: POS_SAVED_CYCLES,
2689            max_rolls_length: MAX_ROLLS_COUNT_LENGTH,
2690            max_production_stats_length: MAX_PRODUCTION_STATS_LENGTH,
2691            max_credit_length: MAX_DEFERRED_CREDITS_LENGTH,
2692            initial_deferred_credits_path: None,
2693        };
2694
2695        let tempdir = TempDir::new().expect("cannot create temp directory");
2696        let db_config = MassaDBConfig {
2697            path: tempdir.path().to_path_buf(),
2698            max_history_length: 10,
2699            max_final_state_elements_size: 100_000,
2700            max_versioning_elements_size: 100_000,
2701            thread_count: 2,
2702            max_ledger_backups: 10,
2703            enable_metrics: false,
2704        };
2705        let db = Arc::new(RwLock::new(
2706            Box::new(MassaDB::new(db_config)) as Box<dyn MassaDBController + 'static>
2707        ));
2708        let selector_controller = Box::new(MockSelectorController::new());
2709        let init_seed = Hash::compute_from(b"");
2710        let initial_seeds = vec![Hash::compute_from(init_seed.to_bytes()), init_seed];
2711
2712        let cycle_info_deserializer = CycleInfoDeserializer::new(
2713            pos_config.max_rolls_length,
2714            pos_config.max_production_stats_length,
2715        );
2716
2717        let mut pos_state = PoSFinalState {
2718            config: pos_config.clone(),
2719            db: db.clone(),
2720            cycle_history_cache: Default::default(),
2721            rng_seed_cache: None,
2722            selector: selector_controller,
2723            initial_rolls: Default::default(),
2724            initial_seeds,
2725            credits_serializer: DeferredCreditSerializer::new(),
2726            credits_deserializer: DeferredCreditDeserializer::new(pos_config.max_credit_length),
2727            cycle_info_serializer: CycleInfoSerializer::new(),
2728            cycle_info_deserializer,
2729        };
2730
2731        // Same pattern as create_initial_cycle: thread_count padding bits already present.
2732        let last_cycle = CycleInfo::new(
2733            0,
2734            false,
2735            BTreeMap::default(),
2736            bitvec![u8, Lsb0; 0, 0],
2737            PreHashMap::default(),
2738        );
2739        let mut batch = DBBatch::new();
2740        pos_state
2741            .create_new_cycle_from_last(
2742                &last_cycle,
2743                Slot::new(0, 1),
2744                Slot::new(1, 1), // last slot of cycle 0
2745                &mut batch,
2746            )
2747            .expect("interpolation should cap seed length instead of overflowing");
2748        db.write()
2749            .write_batch(batch, Default::default(), Some(Slot::new(1, 1)));
2750
2751        let cycle_info = pos_state.get_cycle_info(0).expect("cycle 0 should exist");
2752        assert!(cycle_info.complete);
2753        assert_eq!(cycle_info.rng_seed.len(), 4);
2754    }
2755
2756    #[test]
2757    fn test_create_new_cycle_from_last_production_stats() {
2758        let pos_config = PoSConfig {
2759            periods_per_cycle: 2,
2760            thread_count: 2, // slots_per_cycle = 4
2761            cycle_history_length: POS_SAVED_CYCLES,
2762            max_rolls_length: MAX_ROLLS_COUNT_LENGTH,
2763            max_production_stats_length: MAX_PRODUCTION_STATS_LENGTH,
2764            max_credit_length: MAX_DEFERRED_CREDITS_LENGTH,
2765            initial_deferred_credits_path: None,
2766        };
2767
2768        let tempdir = TempDir::new().expect("cannot create temp directory");
2769        let db_config = MassaDBConfig {
2770            path: tempdir.path().to_path_buf(),
2771            max_history_length: 10,
2772            max_final_state_elements_size: 100_000,
2773            max_versioning_elements_size: 100_000,
2774            thread_count: 2,
2775            max_ledger_backups: 10,
2776            enable_metrics: false,
2777        };
2778        let db = Arc::new(RwLock::new(
2779            Box::new(MassaDB::new(db_config)) as Box<dyn MassaDBController + 'static>
2780        ));
2781        let selector_controller = Box::new(MockSelectorController::new());
2782        let init_seed = Hash::compute_from(b"");
2783        let initial_seeds = vec![Hash::compute_from(init_seed.to_bytes()), init_seed];
2784
2785        let cycle_info_deserializer = CycleInfoDeserializer::new(
2786            pos_config.max_rolls_length,
2787            pos_config.max_production_stats_length,
2788        );
2789
2790        let mut pos_state = PoSFinalState {
2791            config: pos_config.clone(),
2792            db: db.clone(),
2793            cycle_history_cache: Default::default(),
2794            rng_seed_cache: None,
2795            selector: selector_controller,
2796            initial_rolls: Default::default(),
2797            initial_seeds,
2798            credits_serializer: DeferredCreditSerializer::new(),
2799            credits_deserializer: DeferredCreditDeserializer::new(pos_config.max_credit_length),
2800            cycle_info_serializer: CycleInfoSerializer::new(),
2801            cycle_info_deserializer,
2802        };
2803
2804        // Snapshot cycle 0 with non-empty production stats and a partial seed.
2805        let stats_addr =
2806            Address::from_str("AU12pAcVUzsgUBJHaYSAtDKVTYnUT9NorBDjoDovMfAFTLFa16MNa").unwrap();
2807        let mut snapshot_stats = PreHashMap::default();
2808        snapshot_stats.insert(
2809            stats_addr,
2810            ProductionStats {
2811                block_success_count: 3,
2812                block_failure_count: 1,
2813            },
2814        );
2815        let snapshot = CycleInfo::new(
2816            0,
2817            false,
2818            BTreeMap::default(),
2819            bitvec![u8, Lsb0; 0, 0],
2820            snapshot_stats,
2821        );
2822
2823        // Filling the gap in the SAME incomplete cycle keeps the production stats.
2824        let mut batch = DBBatch::new();
2825        pos_state
2826            .create_new_cycle_from_last(
2827                &snapshot,
2828                Slot::new(0, 1), // internal slot of cycle 0 (not first)
2829                Slot::new(0, 1),
2830                &mut batch,
2831            )
2832            .expect("same-cycle interpolation should succeed");
2833        db.write()
2834            .write_batch(batch, Default::default(), Some(Slot::new(0, 1)));
2835
2836        let cycle_0 = pos_state.get_cycle_info(0).expect("cycle 0 should exist");
2837        let kept = cycle_0
2838            .production_stats
2839            .get(&stats_addr)
2840            .expect("continuing the same incomplete cycle must keep production stats");
2841        assert_eq!(kept.block_success_count, 3);
2842        assert_eq!(kept.block_failure_count, 1);
2843
2844        // F65: interpolating a new cycle must NOT inherit the snapshot's production stats.
2845        let mut batch = DBBatch::new();
2846        pos_state
2847            .create_new_cycle_from_last(
2848                &snapshot,
2849                Slot::new(2, 0), // first slot of cycle 1
2850                Slot::new(3, 1), // last slot of cycle 1
2851                &mut batch,
2852            )
2853            .expect("new cycle interpolation should succeed");
2854        db.write()
2855            .write_batch(batch, Default::default(), Some(Slot::new(3, 1)));
2856
2857        let cycle_1 = pos_state.get_cycle_info(1).expect("cycle 1 should exist");
2858        assert!(
2859            cycle_1.production_stats.is_empty(),
2860            "interpolated new cycle must have empty production stats, got {:?}",
2861            cycle_1.production_stats
2862        );
2863    }
2864}