Skip to main content

snarkos_node_bft/helpers/
storage.rs

1// Copyright (c) 2019-2026 Provable Inc.
2// This file is part of the snarkOS library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use crate::helpers::{check_timestamp_for_liveness, fmt_id};
17use snarkos_node_bft_ledger_service::LedgerService;
18use snarkos_node_bft_storage_service::StorageService;
19use snarkvm::{
20    ledger::{
21        block::{Block, Transaction},
22        narwhal::{BatchCertificate, BatchHeader, Transmission, TransmissionID},
23    },
24    prelude::{Address, Field, Network, Result, anyhow, bail, ensure},
25    utilities::{cfg_into_iter, cfg_iter, cfg_sorted_by},
26};
27
28use anyhow::Context;
29use indexmap::{IndexMap, IndexSet, map::Entry};
30#[cfg(feature = "locktick")]
31use locktick::parking_lot::RwLock;
32use lru::LruCache;
33#[cfg(not(feature = "locktick"))]
34use parking_lot::RwLock;
35#[cfg(not(feature = "serial"))]
36use rayon::prelude::*;
37use std::{
38    collections::{HashMap, HashSet},
39    num::NonZeroUsize,
40    sync::{
41        Arc,
42        atomic::{AtomicU32, AtomicU64, Ordering},
43    },
44};
45
46#[derive(Clone, Debug)]
47pub struct Storage<N: Network>(Arc<StorageInner<N>>);
48
49impl<N: Network> std::ops::Deref for Storage<N> {
50    type Target = Arc<StorageInner<N>>;
51
52    fn deref(&self) -> &Self::Target {
53        &self.0
54    }
55}
56
57/// The storage for the memory pool.
58///
59/// The storage is used to store the following:
60/// - `current_height` tracker.
61/// - `current_round` tracker.
62/// - `round` to `(certificate ID, batch ID, author)` entries.
63/// - `certificate ID` to `certificate` entries.
64/// - `batch ID` to `round` entries.
65/// - `transmission ID` to `(transmission, certificate IDs)` entries.
66///
67/// The chain of events is as follows:
68/// 1. A `transmission` is received.
69/// 2. After a `batch` is ready to be stored:
70///   - The `certificate` is inserted, triggering updates to the
71///     `rounds`, `certificates`, `batch_ids`, and `transmissions` maps.
72///   - The missing `transmissions` from storage are inserted into the `transmissions` map.
73///   - The certificate ID is inserted into the `transmissions` map.
74/// 3. After a `round` reaches quorum threshold:
75///  - The next round is inserted into the `current_round`.
76#[derive(Debug)]
77pub struct StorageInner<N: Network> {
78    /// The ledger service.
79    ledger: Arc<dyn LedgerService<N>>,
80    /* Once per block */
81    /// The current height.
82    current_height: AtomicU32,
83    /* Once per round */
84    /// The current round.
85    ///
86    /// Invariant: current_round > 0.
87    /// This is established in [`Storage::new`], which sets it to at least 1 via [`Storage::update_current_round`].
88    /// The only callers of [`Storage::update_current_round`] are
89    /// [`Storage::increment_to_next_round`] and [`Storage::sync_round_with_block`],
90    /// both of which set it to at least 1.
91    current_round: AtomicU64,
92    /// The `round` for which garbage collection has occurred **up to** (inclusive).
93    gc_round: AtomicU64,
94    /// The maximum number of rounds to keep in storage.
95    max_gc_rounds: u64,
96    /* Once per batch */
97    /// The map of `round` to a list of `(certificate ID, author)` entries.
98    rounds: RwLock<IndexMap<u64, IndexSet<(Field<N>, Address<N>)>>>,
99    /// A cache of `certificate ID` to unprocessed `certificate`.
100    unprocessed_certificates: RwLock<LruCache<Field<N>, BatchCertificate<N>>>,
101    /// The map of `certificate ID` to `certificate`.
102    certificates: RwLock<IndexMap<Field<N>, BatchCertificate<N>>>,
103    /// The map of `certificate ID` to `round`.
104    batch_ids: RwLock<IndexMap<Field<N>, u64>>,
105    /// The map of `transmission ID` to `(transmission, certificate IDs)` entries.
106    transmissions: Arc<dyn StorageService<N>>,
107}
108
109impl<N: Network> Storage<N> {
110    /// Initializes a new instance of storage.
111    pub fn new(
112        ledger: Arc<dyn LedgerService<N>>,
113        transmissions: Arc<dyn StorageService<N>>,
114        max_gc_rounds: u64,
115    ) -> Result<Self> {
116        // Retrieve the latest committee bonded in the ledger
117        // (genesis committee if the ledger contains only the genesis block).
118        let committee = ledger.current_committee().expect("Ledger is missing a committee.");
119        // Retrieve the round at which that committee was created, or 1 if it is the genesis committee.
120        let current_round = committee.starting_round().max(1);
121        // Set the unprocessed certificates cache size.
122        let unprocessed_cache_size = NonZeroUsize::new((N::LATEST_MAX_CERTIFICATES() * 2) as usize).unwrap();
123
124        // Create the storage.
125        let storage = Self(Arc::new(StorageInner {
126            ledger,
127            current_height: Default::default(),
128            current_round: AtomicU64::new(current_round),
129            gc_round: Default::default(),
130            max_gc_rounds,
131            rounds: Default::default(),
132            unprocessed_certificates: RwLock::new(LruCache::new(unprocessed_cache_size)),
133            certificates: Default::default(),
134            batch_ids: Default::default(),
135            transmissions,
136        }));
137
138        // Perform GC on the current round.
139        // Since there are no certificates yet, this only sets `gc_round`.
140        storage.garbage_collect_certificates(current_round)?;
141
142        // Return the storage.
143        Ok(storage)
144    }
145}
146
147impl<N: Network> Storage<N> {
148    /// Returns the current height.
149    pub fn current_height(&self) -> u32 {
150        // Get the current height.
151        self.current_height.load(Ordering::SeqCst)
152    }
153}
154
155impl<N: Network> Storage<N> {
156    /// Returns the current round.
157    pub fn current_round(&self) -> u64 {
158        // Get the current round.
159        self.current_round.load(Ordering::SeqCst)
160    }
161
162    /// Returns the `round` that garbage collection has occurred **up to** (inclusive).
163    ///
164    /// # Invariants
165    /// The value returned is greater or equal to return values of prior calls to this method.
166    pub fn gc_round(&self) -> u64 {
167        // Get the GC round.
168        self.gc_round.load(Ordering::SeqCst)
169    }
170
171    /// Returns the maximum number of rounds to keep in storage.
172    pub fn max_gc_rounds(&self) -> u64 {
173        self.max_gc_rounds
174    }
175
176    /// Increments storage to the next round, updating the current round.
177    /// Note: This method is only called once per round, upon certification of the primary's batch.
178    pub fn increment_to_next_round(&self, current_round: u64) -> Result<u64> {
179        // Determine the next round.
180        let next_round = current_round + 1;
181
182        // Check if the next round is less than the current round in storage.
183        {
184            // Retrieve the storage round.
185            let storage_round = self.current_round();
186            // If the next round is less than the current round in storage, return early with the storage round.
187            if next_round < storage_round {
188                return Ok(storage_round);
189            }
190
191            trace!("Incrementing storage from round {storage_round} to {next_round}");
192        }
193
194        // Retrieve the current committee.
195        let current_committee = self.ledger.current_committee()?;
196        // Retrieve the current committee's starting round.
197        let starting_round = current_committee.starting_round();
198        // If the primary is behind the current committee's starting round, sync with the latest block.
199        if next_round < starting_round {
200            // Retrieve the latest block round.
201            let latest_block_round = self.ledger.latest_round();
202            // Log the round sync.
203            info!(
204                "Syncing primary round ({next_round}) with the current committee's starting round ({starting_round}). Syncing with the latest block round {latest_block_round}..."
205            );
206            // Sync the round with the latest block.
207            self.sync_round_with_block(latest_block_round);
208            // Return the latest block round.
209            return Ok(latest_block_round);
210        }
211
212        // Update the storage to the next round.
213        self.update_current_round(next_round);
214
215        #[cfg(feature = "metrics")]
216        metrics::gauge(metrics::bft::LAST_STORED_ROUND, next_round as f64);
217
218        // Retrieve the storage round.
219        let storage_round = self.current_round();
220        // Retrieve the GC round.
221        let gc_round = self.gc_round();
222        // Ensure the next round matches in storage.
223        ensure!(next_round == storage_round, "The next round {next_round} does not match in storage ({storage_round})");
224        // Ensure the next round is greater than or equal to the GC round.
225        ensure!(next_round >= gc_round, "The next round {next_round} is behind the GC round {gc_round}");
226
227        // Log the updated round.
228        info!("Starting round {next_round}...");
229        Ok(next_round)
230    }
231
232    /// Updates the storage to the next round.
233    fn update_current_round(&self, next_round: u64) {
234        // Update the current round.
235        self.current_round.store(next_round, Ordering::SeqCst);
236    }
237
238    /// Update the storage by performing garbage collection based on the next round.
239    pub(crate) fn garbage_collect_certificates(&self, next_round: u64) -> Result<()> {
240        // Fetch the current GC round.
241        let current_gc_round = self.gc_round();
242        // Compute the next GC round.
243        let next_gc_round = next_round.saturating_sub(self.max_gc_rounds);
244        // Check if storage needs to be garbage collected.
245        if next_gc_round > current_gc_round {
246            if self
247                .gc_round
248                .compare_exchange(current_gc_round, next_gc_round, Ordering::SeqCst, Ordering::SeqCst)
249                .is_err()
250            {
251                bail!("Concurrent updates to GC round detected.");
252            }
253
254            // Remove the GC round(s) from storage.
255            for gc_round in current_gc_round..=next_gc_round {
256                // Iterate over the certificates for the GC round.
257                for id in self.get_certificate_ids_for_round(gc_round).into_iter() {
258                    trace!(
259                        "Garbage collecting certificate {id} at round {gc_round} (cut-off is round {next_gc_round})"
260                    );
261                    self.remove_certificate(id);
262                }
263            }
264            // Update the GC round.
265            self.gc_round.store(next_gc_round, Ordering::SeqCst);
266        } else if next_gc_round < current_gc_round {
267            bail!("Attempted to decrease GC round from {current_gc_round} to {next_gc_round}");
268        }
269
270        Ok(())
271    }
272}
273
274impl<N: Network> Storage<N> {
275    /// Returns `true` if the storage contains the specified `round`.
276    pub fn contains_certificates_for_round(&self, round: u64) -> bool {
277        // Check if the round exists in storage.
278        self.rounds.read().contains_key(&round)
279    }
280
281    /// Returns `true` if the storage contains the specified `certificate ID`.
282    pub fn contains_certificate(&self, certificate_id: Field<N>) -> bool {
283        // Check if the certificate ID exists in storage.
284        self.certificates.read().contains_key(&certificate_id)
285    }
286
287    /// Returns `true` if the storage contains a certificate from the specified `author` in the given `round`.
288    pub fn contains_certificate_in_round_from(&self, round: u64, author: Address<N>) -> bool {
289        self.rounds.read().get(&round).is_some_and(|set| set.iter().any(|(_, a)| a == &author))
290    }
291
292    /// Returns `true` if the storage contains the specified `certificate ID`.
293    pub fn contains_unprocessed_certificate(&self, certificate_id: Field<N>) -> bool {
294        // Check if the certificate ID exists in storage.
295        self.unprocessed_certificates.read().contains(&certificate_id)
296    }
297
298    /// Returns `true` if the storage contains the specified `batch ID`.
299    pub fn contains_batch(&self, batch_id: Field<N>) -> bool {
300        // Check if the batch ID exists in storage.
301        self.batch_ids.read().contains_key(&batch_id)
302    }
303
304    /// Returns `true` if the storage contains the specified transmission, or it was recorded as aborted.
305    pub fn contains_transmission(&self, transmission_id: impl Into<TransmissionID<N>>) -> bool {
306        self.transmissions.contains_transmission(transmission_id.into())
307    }
308
309    /// Returns the transmission for the given `transmission ID`.
310    /// If the transmission ID does not exist in storage or was aborted, `None` is returned.
311    pub fn get_transmission(&self, transmission_id: impl Into<TransmissionID<N>>) -> Option<Transmission<N>> {
312        self.transmissions.get_transmission(transmission_id.into())
313    }
314
315    /// Returns the round for the given `certificate ID`.
316    /// If the certificate ID does not exist in storage, `None` is returned.
317    pub fn get_round_for_certificate(&self, certificate_id: Field<N>) -> Option<u64> {
318        // Get the round.
319        self.certificates.read().get(&certificate_id).map(|certificate| certificate.round())
320    }
321
322    /// Returns the round for the given `batch ID`.
323    /// If the batch ID does not exist in storage, `None` is returned.
324    pub fn get_round_for_batch(&self, batch_id: Field<N>) -> Option<u64> {
325        // Get the round.
326        self.batch_ids.read().get(&batch_id).copied()
327    }
328
329    /// Returns the certificate round for the given `certificate ID`.
330    /// If the certificate ID does not exist in storage, `None` is returned.
331    pub fn get_certificate_round(&self, certificate_id: Field<N>) -> Option<u64> {
332        // Get the batch certificate and return the round.
333        self.certificates.read().get(&certificate_id).map(|certificate| certificate.round())
334    }
335
336    /// Returns the certificate for the given `certificate ID`.
337    /// If the certificate ID does not exist in storage, `None` is returned.
338    pub fn get_certificate(&self, certificate_id: Field<N>) -> Option<BatchCertificate<N>> {
339        // Get the batch certificate.
340        self.certificates.read().get(&certificate_id).cloned()
341    }
342
343    /// Returns the unprocessed certificate for the given `certificate ID`.
344    /// If the certificate ID does not exist in storage, `None` is returned.
345    pub fn get_unprocessed_certificate(&self, certificate_id: Field<N>) -> Option<BatchCertificate<N>> {
346        // Get the unprocessed certificate.
347        self.unprocessed_certificates.read().peek(&certificate_id).cloned()
348    }
349
350    /// Returns the certificate for the given `round` and `author`.
351    /// If the round does not exist in storage, `None` is returned.
352    /// If the author for the round does not exist in storage, `None` is returned.
353    pub fn get_certificate_for_round_with_author(&self, round: u64, author: Address<N>) -> Option<BatchCertificate<N>> {
354        // Retrieve the certificates.
355        if let Some(entries) = self.rounds.read().get(&round) {
356            let certificates = self.certificates.read();
357            entries.iter().find_map(
358                |(certificate_id, a)| if a == &author { certificates.get(certificate_id).cloned() } else { None },
359            )
360        } else {
361            Default::default()
362        }
363    }
364
365    /// Returns the certificates for the given `round`.
366    /// If the round does not exist in storage, an empty set is returned.
367    pub fn get_certificates_for_round(&self, round: u64) -> IndexSet<BatchCertificate<N>> {
368        // The genesis round does not have batch certificates.
369        if round == 0 {
370            return Default::default();
371        }
372        // Retrieve the certificates.
373        if let Some(entries) = self.rounds.read().get(&round) {
374            let certificates = self.certificates.read();
375            entries.iter().flat_map(|(certificate_id, _)| certificates.get(certificate_id).cloned()).collect()
376        } else {
377            Default::default()
378        }
379    }
380
381    /// Returns the certificate IDs for the given `round`.
382    /// If the round does not exist in storage, an empty set is returned.
383    pub fn get_certificate_ids_for_round(&self, round: u64) -> IndexSet<Field<N>> {
384        // The genesis round does not have batch certificates.
385        if round == 0 {
386            return Default::default();
387        }
388        // Retrieve the certificates.
389        if let Some(entries) = self.rounds.read().get(&round) {
390            entries.iter().map(|(certificate_id, _)| *certificate_id).collect()
391        } else {
392            Default::default()
393        }
394    }
395
396    /// Returns the certificate authors for the given `round`.
397    /// If the round does not exist in storage, an empty set is returned.
398    pub fn get_certificate_authors_for_round(&self, round: u64) -> HashSet<Address<N>> {
399        // The genesis round does not have batch certificates.
400        if round == 0 {
401            return Default::default();
402        }
403        // Retrieve the certificates.
404        if let Some(entries) = self.rounds.read().get(&round) {
405            entries.iter().map(|(_, author)| *author).collect()
406        } else {
407            Default::default()
408        }
409    }
410
411    /// Returns the certificates that have not yet been included in the ledger.
412    /// Note that the order of this set is by round and then insertion.
413    pub(crate) fn get_pending_certificates(&self) -> IndexSet<BatchCertificate<N>> {
414        // Obtain the read locks.
415        let rounds = self.rounds.read();
416        let certificates = self.certificates.read();
417
418        // Iterate over the rounds.
419        cfg_sorted_by!(rounds.clone(), |a, _, b, _| a.cmp(b))
420            .flat_map(|(_, certificates_for_round)| {
421                // Iterate over the certificates for the round.
422                cfg_into_iter!(certificates_for_round).filter_map(|(certificate_id, _)| {
423                    // Skip the certificate if it already exists in the ledger.
424                    if self.ledger.contains_certificate(&certificate_id).unwrap_or(false) {
425                        None
426                    } else {
427                        // Add the certificate to the pending certificates.
428                        certificates.get(&certificate_id).cloned()
429                    }
430                })
431            })
432            .collect()
433    }
434
435    /// Checks the given `batch_header` for validity, returning the missing transmissions from storage.
436    ///
437    /// # Arguments
438    /// - `batch_header`: The batch header to check.
439    /// - `transmissions`: All transmissions referenced by the certificate.
440    /// - `aborted_transmissions`: The set of aborted transmissions in this certificate.
441    ///
442    /// # Invariants
443    /// This method ensures the following invariants:
444    /// - The batch ID does not already exist in storage.
445    /// - The author is a member of the committee for the batch round.
446    /// - The timestamp is within the allowed time range.
447    /// - None of the transmissions are from any past rounds (up to GC).
448    /// - All transmissions declared in the batch header are provided or exist in storage (up to GC).
449    /// - All previous certificates declared in the certificate exist in storage (up to GC).
450    /// - All previous certificates are for the previous round (i.e. round - 1).
451    /// - All previous certificates contain a unique author.
452    /// - The previous certificates reached the quorum threshold (N - f).
453    ///
454    /// # Returns
455    /// - `Ok(Some(txns))` for a valid new batch, where `txns` is the set of missing transactions in the batch
456    ///   that need to be fetched from peers.
457    /// - `Ok(None)` if the batch already exists in storage
458    pub fn check_batch_header(
459        &self,
460        batch_header: &BatchHeader<N>,
461        transmissions: HashMap<TransmissionID<N>, Transmission<N>>,
462        aborted_transmissions: HashSet<TransmissionID<N>>,
463    ) -> Result<Option<HashMap<TransmissionID<N>, Transmission<N>>>> {
464        // Retrieve the round.
465        let round = batch_header.round();
466        // Retrieve the GC round.
467        let gc_round = self.gc_round();
468        // Construct a GC log message.
469        let gc_log = format!("(gc = {gc_round})");
470
471        // Ensure the batch ID does not already exist in storage.
472        if self.contains_batch(batch_header.batch_id()) {
473            debug!("Batch for round {round} already exists in storage {gc_log}");
474            return Ok(None);
475        }
476
477        // Retrieve the committee lookback for the batch round.
478        let Ok(committee_lookback) = self.ledger.get_committee_lookback_for_round(round) else {
479            bail!("Storage failed to retrieve the committee lookback for round {round} {gc_log}")
480        };
481        // Ensure the author is in the committee.
482        if !committee_lookback.is_committee_member(batch_header.author()) {
483            bail!("Author {} is not in the committee for round {round} {gc_log}", batch_header.author())
484        }
485
486        // Check the timestamp for liveness.
487        check_timestamp_for_liveness(batch_header.timestamp())?;
488
489        // Determine which transmissions in this batch are missing from storage.
490        let missing_transmissions = self
491            .transmissions
492            .find_missing_transmissions(batch_header, transmissions, aborted_transmissions)
493            .map_err(|e| anyhow!("{e} for round {round} {gc_log}"))?;
494
495        // Compute the previous round.
496        let previous_round = round.saturating_sub(1);
497        // Check if the previous round is within range of the GC round.
498        if previous_round > gc_round {
499            // Retrieve the committee lookback for the previous round.
500            let Ok(previous_committee_lookback) = self.ledger.get_committee_lookback_for_round(previous_round) else {
501                bail!("Missing committee for the previous round {previous_round} in storage {gc_log}")
502            };
503            // Ensure the previous round certificates exists in storage.
504            if !self.contains_certificates_for_round(previous_round) {
505                bail!("Missing certificates for the previous round {previous_round} in storage {gc_log}")
506            }
507            // Ensure the number of previous certificate IDs is at or below the number of committee members.
508            // As for the `test_network` clause, a more robust approach would be to set the
509            // previous_committee_lookback appropriately when dev-on-prod is activated, or to only skip this
510            // check right at the hotswap height.
511            if !cfg!(feature = "test_network")
512                && batch_header.previous_certificate_ids().len() > previous_committee_lookback.num_members()
513            {
514                bail!("Too many previous certificates for round {round} {gc_log}")
515            }
516            // Initialize a set of the previous authors.
517            let mut previous_authors = HashSet::with_capacity(batch_header.previous_certificate_ids().len());
518            // Ensure storage contains all declared previous certificates (up to GC).
519            for previous_certificate_id in batch_header.previous_certificate_ids() {
520                // Retrieve the previous certificate.
521                let Some(previous_certificate) = self.get_certificate(*previous_certificate_id) else {
522                    bail!(
523                        "Missing previous certificate '{}' for certificate in round {round} {gc_log}",
524                        fmt_id(previous_certificate_id)
525                    )
526                };
527                // Ensure the previous certificate is for the previous round.
528                if previous_certificate.round() != previous_round {
529                    bail!("Round {round} certificate contains a round {previous_round} certificate {gc_log}")
530                }
531                // Ensure the previous author is new.
532                if previous_authors.contains(&previous_certificate.author()) {
533                    bail!("Round {round} certificate contains a duplicate author {gc_log}")
534                }
535                // Insert the author of the previous certificate.
536                previous_authors.insert(previous_certificate.author());
537            }
538            // Ensure the previous certificates have reached the quorum threshold.
539            if !previous_committee_lookback.is_quorum_threshold_reached(&previous_authors) {
540                bail!("Previous certificates for a batch in round {round} did not reach quorum threshold {gc_log}")
541            }
542        }
543
544        Ok(Some(missing_transmissions))
545    }
546
547    /// Check the validity of a certificate coming from another validator.
548    ///
549    /// It suffices to check that the signers (author and endorsers) are members of the applicable committee
550    /// and that they form a quorum in the committee.
551    /// Under the fundamental fault tolerance assumption of at most `f` (stake of) faulty validators,
552    /// the quorum check on signers guarantees that at least one correct validator
553    /// has ensured the validity of the proposal contained in the certificate,
554    /// either by construction (by the author) or by checking (by an endorser):
555    /// given `N > 0` total stake, and `f` the largest integer `< N/3` (where `/` is exact rational division),
556    /// we have `N >= 3f + 1`, which implies `N - f >= 2f + 1`, which is always `> f`;
557    /// `N - f` is the quorum stake.
558    pub fn check_incoming_certificate(&self, certificate: &BatchCertificate<N>) -> Result<()> {
559        // Retrieve the certificate author and round.
560        let certificate_author = certificate.author();
561        let certificate_round = certificate.round();
562
563        // Retrieve the committee lookback.
564        let committee_lookback = self.ledger.get_committee_lookback_for_round(certificate_round)?;
565
566        // Ensure that the signers of the certificate reach the quorum threshold.
567        // Note that certificate.signatures() only returns the endorsing signatures, not the author's signature.
568        let mut signers: HashSet<Address<N>> =
569            certificate.signatures().map(|signature| signature.to_address()).collect();
570        signers.insert(certificate_author);
571        ensure!(
572            committee_lookback.is_quorum_threshold_reached(&signers),
573            "Certificate '{}' for round {certificate_round} does not meet quorum requirements",
574            certificate.id()
575        );
576
577        // Ensure that the signers of the certificate are in the committee.
578        cfg_iter!(&signers).try_for_each(|signer| {
579            ensure!(
580                committee_lookback.is_committee_member(*signer),
581                "Signer '{signer}' of certificate '{}' for round {certificate_round} is not in the committee",
582                certificate.id()
583            );
584            Ok(())
585        })?;
586
587        Ok(())
588    }
589
590    /// Checks the given `certificate` for validity, returning the missing transmissions from storage.
591    ///
592    /// # Arguments
593    /// - `certificate`: The certificate to check.
594    /// - `transmissions`: The transmissions contained in the certificate.
595    /// - `aborted_transmissions`: The aborted transmission contained in the certificate.
596    ///
597    /// # Invariants
598    /// This method ensures the following invariants:
599    /// - The certificate ID does not already exist in storage.
600    /// - The batch ID does not already exist in storage.
601    /// - The author is a member of the committee for the batch round.
602    /// - The author has not already created a certificate for the batch round.
603    /// - The timestamp is within the allowed time range.
604    /// - None of the transmissions are from any past rounds (up to GC).
605    /// - All transmissions declared in the batch header are provided or exist in storage (up to GC).
606    /// - All previous certificates declared in the certificate exist in storage (up to GC).
607    /// - All previous certificates are for the previous round (i.e. round - 1).
608    /// - The previous certificates reached the quorum threshold (N - f).
609    /// - The timestamps from the signers are all within the allowed time range.
610    /// - The signers have reached the quorum threshold (N - f).
611    pub fn check_certificate(
612        &self,
613        certificate: &BatchCertificate<N>,
614        transmissions: HashMap<TransmissionID<N>, Transmission<N>>,
615        aborted_transmissions: HashSet<TransmissionID<N>>,
616    ) -> Result<HashMap<TransmissionID<N>, Transmission<N>>> {
617        // Retrieve the round.
618        let round = certificate.round();
619        // Retrieve the GC round.
620        let gc_round = self.gc_round();
621        // Construct a GC log message.
622        let gc_log = format!("(gc = {gc_round})");
623
624        // Ensure the certificate ID does not already exist in storage.
625        if self.contains_certificate(certificate.id()) {
626            bail!("Certificate for round {round} already exists in storage {gc_log}")
627        }
628
629        // Ensure the storage does not already contain a certificate for this author in this round.
630        if self.contains_certificate_in_round_from(round, certificate.author()) {
631            bail!("Certificate with this author for round {round} already exists in storage {gc_log}")
632        }
633
634        // Ensure the batch header is well-formed.
635        let Some(missing_transmissions) =
636            self.check_batch_header(certificate.batch_header(), transmissions, aborted_transmissions)?
637        else {
638            bail!("Certificate for round {round} already exists in storage {gc_log}")
639        };
640
641        // Check the timestamp for liveness.
642        check_timestamp_for_liveness(certificate.timestamp())?;
643
644        // Retrieve the committee lookback for the batch round.
645        let Ok(committee_lookback) = self.ledger.get_committee_lookback_for_round(round) else {
646            bail!("Storage failed to retrieve the committee for round {round} {gc_log}")
647        };
648
649        // Initialize a set of the signers.
650        let mut signers = HashSet::with_capacity(certificate.signatures().len() + 1);
651        // Append the batch author.
652        signers.insert(certificate.author());
653
654        // Iterate over the signatures.
655        for signature in certificate.signatures() {
656            // Retrieve the signer.
657            let signer = signature.to_address();
658            // Ensure the signer is in the committee.
659            if !committee_lookback.is_committee_member(signer) {
660                bail!("Signer {signer} is not in the committee for round {round} {gc_log}")
661            }
662            // Append the signer.
663            signers.insert(signer);
664        }
665
666        // Ensure the signatures have reached the quorum threshold.
667        if !committee_lookback.is_quorum_threshold_reached(&signers) {
668            bail!("Signatures for a batch in round {round} did not reach quorum threshold {gc_log}")
669        }
670
671        Ok(missing_transmissions)
672    }
673
674    /// Inserts the given `certificate` into storage.
675    ///
676    /// This method triggers updates to the `rounds`, `certificates`, `batch_ids`, and `transmissions` maps.
677    ///
678    /// # Arguments
679    /// - `certificate`: The certificate to insert.
680    /// - `transmissions`: The transmissions contained in the certificate, or the subset of the transmissions that in the certificate that do not yet exist in storage.
681    /// - `aborted_transmissions`: The aborted transmission contained in the certificate.
682    ///
683    /// # Invariants
684    /// This method ensures the following invariants:
685    /// - The certificate ID does not already exist in storage.
686    /// - The batch ID does not already exist in storage.
687    /// - All transmissions declared in the certificate are provided or exist in storage (up to GC).
688    /// - All previous certificates declared in the certificate exist in storage (up to GC).
689    /// - All previous certificates are for the previous round (i.e. round - 1).
690    /// - The previous certificates reached the quorum threshold (N - f).
691    pub fn insert_certificate(
692        &self,
693        certificate: BatchCertificate<N>,
694        transmissions: HashMap<TransmissionID<N>, Transmission<N>>,
695        aborted_transmissions: HashSet<TransmissionID<N>>,
696    ) -> Result<()> {
697        // Ensure the certificate round is above the GC round.
698        ensure!(certificate.round() > self.gc_round(), "Certificate round is at or below the GC round");
699        // Ensure the certificate and its transmissions are valid.
700        let missing_transmissions =
701            self.check_certificate(&certificate, transmissions, aborted_transmissions.clone())?;
702        // Insert the certificate into storage.
703        self.insert_certificate_atomic(certificate, aborted_transmissions, missing_transmissions);
704        Ok(())
705    }
706
707    /// Inserts the given `certificate` into storage.
708    ///
709    /// This method assumes **all missing** transmissions are provided in the `missing_transmissions` map.
710    ///
711    /// This method triggers updates to the `rounds`, `certificates`, `batch_ids`, and `transmissions` maps.
712    fn insert_certificate_atomic(
713        &self,
714        certificate: BatchCertificate<N>,
715        aborted_transmission_ids: HashSet<TransmissionID<N>>,
716        missing_transmissions: HashMap<TransmissionID<N>, Transmission<N>>,
717    ) {
718        // Retrieve the round.
719        let round = certificate.round();
720        // Retrieve the certificate ID.
721        let certificate_id = certificate.id();
722        // Retrieve the author of the batch.
723        let author = certificate.author();
724
725        // Insert the round to certificate ID entry.
726        self.rounds.write().entry(round).or_default().insert((certificate_id, author));
727        // Obtain the certificate's transmission ids.
728        let transmission_ids = certificate.transmission_ids().clone();
729        // Insert the certificate.
730        self.certificates.write().insert(certificate_id, certificate);
731        // Remove the unprocessed certificate.
732        self.unprocessed_certificates.write().pop(&certificate_id);
733        // Insert the batch ID.
734        self.batch_ids.write().insert(certificate_id, round);
735        // Insert the certificate ID for each of the transmissions into storage.
736        self.transmissions.insert_transmissions(
737            certificate_id,
738            transmission_ids,
739            aborted_transmission_ids,
740            missing_transmissions,
741        );
742    }
743
744    /// Inserts the given unprocessed `certificate` into storage.
745    ///
746    /// This is a temporary storage, which is cleared again when calling `insert_certificate_atomic`.
747    pub fn insert_unprocessed_certificate(&self, certificate: BatchCertificate<N>) -> Result<()> {
748        // Ensure the certificate round is above the GC round.
749        ensure!(certificate.round() > self.gc_round(), "Certificate round is at or below the GC round");
750        // Insert the certificate.
751        self.unprocessed_certificates.write().put(certificate.id(), certificate);
752
753        Ok(())
754    }
755
756    /// Removes the given `certificate ID` from storage. This method is used to garbage collect individual certificates once blocks are committed.
757    ///
758    /// This method triggers updates to the `rounds`, `certificates`, `batch_ids`, and `transmissions` maps.
759    ///
760    /// If the certificate was successfully removed, `true` is returned.
761    /// If the certificate did not exist in storage, `false` is returned.
762    fn remove_certificate(&self, certificate_id: Field<N>) -> bool {
763        // Retrieve the certificate.
764        let Some(certificate) = self.get_certificate(certificate_id) else {
765            warn!("Certificate {certificate_id} does not exist in storage");
766            return false;
767        };
768        // Retrieve the round.
769        let round = certificate.round();
770        // Compute the author of the batch.
771        let author = certificate.author();
772
773        // TODO (howardwu): We may want to use `shift_remove` below, in order to align compatibility
774        //  with tests written to for `remove_certificate`. However, this will come with performance hits.
775        //  It will be better to write tests that compare the union of the sets.
776
777        // Update the round.
778        match self.rounds.write().entry(round) {
779            Entry::Occupied(mut entry) => {
780                // Remove the round to certificate ID entry.
781                entry.get_mut().swap_remove(&(certificate_id, author));
782                // If the round is empty, remove it.
783                if entry.get().is_empty() {
784                    entry.swap_remove();
785                }
786            }
787            Entry::Vacant(_) => {}
788        }
789        // Remove the certificate.
790        self.certificates.write().swap_remove(&certificate_id);
791        // Remove the unprocessed certificate.
792        self.unprocessed_certificates.write().pop(&certificate_id);
793        // Remove the batch ID.
794        self.batch_ids.write().swap_remove(&certificate_id);
795        // Remove the transmission entries in the certificate from storage.
796        self.transmissions.remove_transmissions(&certificate_id, certificate.transmission_ids());
797        // Return successfully.
798        true
799    }
800}
801
802impl<N: Network> Storage<N> {
803    /// Syncs the current height with the block.
804    pub(crate) fn sync_height_with_block(&self, next_height: u32) {
805        // If the block height is greater than the current height in storage, sync the height.
806        if next_height > self.current_height() {
807            // Update the current height in storage.
808            self.current_height.store(next_height, Ordering::SeqCst);
809        }
810    }
811
812    /// Syncs the current round with the block.
813    pub(crate) fn sync_round_with_block(&self, next_round: u64) {
814        // Ensure we sync to at least round 1.
815        let next_round = next_round.max(1);
816        // If the round in the block is greater than the current round in storage, sync the round.
817        if next_round > self.current_round() {
818            // Update the current round in storage.
819            self.update_current_round(next_round);
820            // Log the updated round.
821            info!("Synced to round {next_round}...");
822        } else {
823            trace!(
824                "Skipping sync to round {next_round} as it is less than or equal to the current round ({})",
825                self.current_round()
826            );
827        }
828    }
829
830    /// Syncs the batch certificate with the block.
831    pub(crate) fn sync_certificate_with_block(
832        &self,
833        block: &Block<N>,
834        certificate: BatchCertificate<N>,
835        unconfirmed_transactions: &HashMap<N::TransactionID, Transaction<N>>,
836        trusted_ledger_certificate: bool,
837    ) -> Result<()> {
838        // Skip if the certificate round is below the GC round.
839        let gc_round = self.gc_round();
840        if certificate.round() <= gc_round {
841            trace!("Got certificate for round {} below GC round ({gc_round}). Will not store it.", certificate.round());
842            return Ok(());
843        }
844
845        // If the certificate ID already exists in storage, skip it.
846        if self.contains_certificate(certificate.id()) {
847            trace!("Got certificate {} for round {} more than once.", certificate.id(), certificate.round());
848            return Ok(());
849        }
850        // Retrieve the transmissions for the certificate.
851        let mut missing_transmissions = HashMap::new();
852
853        // Retrieve the aborted transmissions for the certificate.
854        let mut aborted_transmissions = HashSet::new();
855
856        // Track the block's aborted solutions and transactions.
857        let aborted_solutions: IndexSet<_> = block.aborted_solution_ids().iter().collect();
858        let aborted_transactions: IndexSet<_> = block.aborted_transaction_ids().iter().collect();
859
860        // Iterate over the transmission IDs.
861        for transmission_id in certificate.transmission_ids() {
862            // If the transmission ID already exists in the map, skip it.
863            if missing_transmissions.contains_key(transmission_id) {
864                continue;
865            }
866            // If the transmission ID exists in storage, skip it.
867            if self.contains_transmission(*transmission_id) {
868                continue;
869            }
870            // Retrieve the transmission.
871            match transmission_id {
872                TransmissionID::Ratification => (),
873                TransmissionID::Solution(solution_id, _) => {
874                    // The solution may exist either in the block itself, or stored in the ledger.
875                    // Note that the ledger does *not* store aborted transmissions, so we need to check if the solution
876                    // was aborted before querying the ledger.
877                    //
878                    // Aborted transmissions only appear in the aborted set of the first block that contains them,
879                    // for subsequent blocks, we can check that `contains_transmission` is true to determine if the transmission was aborted.
880                    if let Some(solution) = block.get_solution(solution_id) {
881                        missing_transmissions.insert(*transmission_id, (*solution).into());
882                    } else if let Ok(Some(solution)) = self.ledger.get_solution(solution_id) {
883                        missing_transmissions.insert(*transmission_id, solution.into());
884                    } else if aborted_solutions.contains(solution_id)
885                        || self.ledger.contains_transmission(transmission_id).unwrap_or(false)
886                    {
887                        aborted_transmissions.insert(*transmission_id);
888                    } else {
889                        bail!("Missing solution {solution_id} for block {}", block.height());
890                    }
891                }
892                TransmissionID::Transaction(transaction_id, _) => {
893                    // The transaction may exists either in the block itself, or stored in the ledger.
894                    // Note that the ledger does *not* store aborted transmissions, so we need to check if the transaction
895                    // was aborted before querying the ledger.
896                    //
897                    // Aborted solutions only appear in the aborted set of the first block that contains them,
898                    // for subsequent blocks, we can check that `contains_transmission` is true to determine if the transaction was aborted.
899                    if let Some(transaction) = unconfirmed_transactions.get(transaction_id) {
900                        missing_transmissions.insert(*transmission_id, transaction.clone().into());
901                    } else if let Ok(Some(transaction)) = self.ledger.get_unconfirmed_transaction(*transaction_id) {
902                        missing_transmissions.insert(*transmission_id, transaction.into());
903                    } else if aborted_transactions.contains(transaction_id)
904                        || self.ledger.contains_transmission(transmission_id).unwrap_or(false)
905                    {
906                        aborted_transmissions.insert(*transmission_id);
907                    } else {
908                        bail!("Missing transaction {transaction_id} for block {}", block.height());
909                    }
910                }
911            }
912        }
913        // Insert the batch certificate into storage.
914        let certificate_id = fmt_id(certificate.id());
915        debug!(
916            "Syncing certificate '{certificate_id}' for round {} with {} transmissions",
917            certificate.round(),
918            certificate.transmission_ids().len()
919        );
920
921        if trusted_ledger_certificate {
922            self.insert_certificate_atomic(certificate, aborted_transmissions, missing_transmissions);
923            Ok(())
924        } else {
925            self.insert_certificate(certificate, missing_transmissions, aborted_transmissions).with_context(|| {
926                format!("Failed to insert certificate '{certificate_id}' from block {}", block.height())
927            })
928        }
929    }
930}
931
932#[cfg(test)]
933impl<N: Network> Storage<N> {
934    /// Returns the ledger service.
935    pub fn ledger(&self) -> &Arc<dyn LedgerService<N>> {
936        &self.ledger
937    }
938
939    /// Returns an iterator over the `(round, (certificate ID, batch ID, author))` entries.
940    pub fn rounds_iter(&self) -> impl Iterator<Item = (u64, IndexSet<(Field<N>, Address<N>)>)> {
941        self.rounds.read().clone().into_iter()
942    }
943
944    /// Returns an iterator over the `(certificate ID, certificate)` entries.
945    pub fn certificates_iter(&self) -> impl Iterator<Item = (Field<N>, BatchCertificate<N>)> {
946        self.certificates.read().clone().into_iter()
947    }
948
949    /// Returns an iterator over the `(batch ID, round)` entries.
950    pub fn batch_ids_iter(&self) -> impl Iterator<Item = (Field<N>, u64)> {
951        self.batch_ids.read().clone().into_iter()
952    }
953
954    /// Returns an iterator over the `(transmission ID, (transmission, certificate IDs))` entries.
955    pub fn transmissions_iter(
956        &self,
957    ) -> impl Iterator<Item = (TransmissionID<N>, (Transmission<N>, IndexSet<Field<N>>))> {
958        self.transmissions.as_hashmap().into_iter()
959    }
960
961    /// Inserts the given `certificate` into storage.
962    ///
963    /// Note: Do NOT use this in production. This is for **testing only**.
964    #[cfg(test)]
965    #[doc(hidden)]
966    pub(crate) fn testing_only_insert_certificate_testing_only(&self, certificate: BatchCertificate<N>) {
967        // Retrieve the round.
968        let round = certificate.round();
969        // Retrieve the certificate ID.
970        let certificate_id = certificate.id();
971        // Retrieve the author of the batch.
972        let author = certificate.author();
973
974        // Insert the round to certificate ID entry.
975        self.rounds.write().entry(round).or_default().insert((certificate_id, author));
976        // Obtain the certificate's transmission ids.
977        let transmission_ids = certificate.transmission_ids().clone();
978        // Insert the certificate.
979        self.certificates.write().insert(certificate_id, certificate);
980        // Insert the batch ID.
981        self.batch_ids.write().insert(certificate_id, round);
982
983        // Construct the dummy missing transmissions (for testing purposes).
984        let missing_transmissions = transmission_ids
985            .iter()
986            .map(|id| (*id, Transmission::Transaction(snarkvm::ledger::narwhal::Data::Buffer(bytes::Bytes::new()))))
987            .collect::<HashMap<_, _>>();
988        // Insert the certificate ID for each of the transmissions into storage.
989        self.transmissions.insert_transmissions(
990            certificate_id,
991            transmission_ids,
992            Default::default(),
993            missing_transmissions,
994        );
995    }
996}
997
998#[cfg(test)]
999pub(crate) mod tests {
1000    use super::*;
1001    use snarkos_node_bft_ledger_service::MockLedgerService;
1002    use snarkos_node_bft_storage_service::BFTMemoryService;
1003    use snarkvm::{
1004        ledger::narwhal::{Data, batch_certificate::test_helpers::sample_batch_certificate_for_round_with_committee},
1005        prelude::{Rng, TestRng},
1006    };
1007
1008    use ::bytes::Bytes;
1009    use indexmap::indexset;
1010
1011    type CurrentNetwork = snarkvm::prelude::MainnetV0;
1012
1013    /// Asserts that the storage matches the expected layout.
1014    pub fn assert_storage<N: Network>(
1015        storage: &Storage<N>,
1016        rounds: &[(u64, IndexSet<(Field<N>, Address<N>)>)],
1017        certificates: &[(Field<N>, BatchCertificate<N>)],
1018        batch_ids: &[(Field<N>, u64)],
1019        transmissions: &HashMap<TransmissionID<N>, (Transmission<N>, IndexSet<Field<N>>)>,
1020    ) {
1021        // Ensure the rounds are well-formed.
1022        assert_eq!(storage.rounds_iter().collect::<Vec<_>>(), *rounds);
1023        // Ensure the certificates are well-formed.
1024        assert_eq!(storage.certificates_iter().collect::<Vec<_>>(), *certificates);
1025        // Ensure the batch IDs are well-formed.
1026        assert_eq!(storage.batch_ids_iter().collect::<Vec<_>>(), *batch_ids);
1027        // Ensure the transmissions are well-formed.
1028        assert_eq!(storage.transmissions_iter().collect::<HashMap<_, _>>(), *transmissions);
1029    }
1030
1031    /// Samples a random transmission.
1032    fn sample_transmission(rng: &mut TestRng) -> Transmission<CurrentNetwork> {
1033        // Sample random fake solution bytes.
1034        let s = |rng: &mut TestRng| Data::Buffer(Bytes::from((0..512).map(|_| rng.random::<u8>()).collect::<Vec<_>>()));
1035        // Sample random fake transaction bytes.
1036        let t =
1037            |rng: &mut TestRng| Data::Buffer(Bytes::from((0..2048).map(|_| rng.random::<u8>()).collect::<Vec<_>>()));
1038        // Sample a random transmission.
1039        match rng.random::<bool>() {
1040            true => Transmission::Solution(s(rng)),
1041            false => Transmission::Transaction(t(rng)),
1042        }
1043    }
1044
1045    /// Samples the random transmissions, returning the missing transmissions and the transmissions.
1046    pub(crate) fn sample_transmissions(
1047        certificate: &BatchCertificate<CurrentNetwork>,
1048        rng: &mut TestRng,
1049    ) -> (
1050        HashMap<TransmissionID<CurrentNetwork>, Transmission<CurrentNetwork>>,
1051        HashMap<TransmissionID<CurrentNetwork>, (Transmission<CurrentNetwork>, IndexSet<Field<CurrentNetwork>>)>,
1052    ) {
1053        // Retrieve the certificate ID.
1054        let certificate_id = certificate.id();
1055
1056        let mut missing_transmissions = HashMap::new();
1057        let mut transmissions = HashMap::<_, (_, IndexSet<Field<CurrentNetwork>>)>::new();
1058        for transmission_id in certificate.transmission_ids() {
1059            // Initialize the transmission.
1060            let transmission = sample_transmission(rng);
1061            // Update the missing transmissions.
1062            missing_transmissions.insert(*transmission_id, transmission.clone());
1063            // Update the transmissions map.
1064            transmissions
1065                .entry(*transmission_id)
1066                .or_insert((transmission, Default::default()))
1067                .1
1068                .insert(certificate_id);
1069        }
1070        (missing_transmissions, transmissions)
1071    }
1072
1073    // TODO (howardwu): Testing with 'max_gc_rounds' set to '0' should ensure everything is cleared after insertion.
1074
1075    #[test]
1076    fn test_certificate_insert_remove() {
1077        let rng = &mut TestRng::default();
1078
1079        // Sample a committee.
1080        let committee = snarkvm::ledger::committee::test_helpers::sample_committee(rng);
1081        // Initialize the ledger.
1082        let ledger = Arc::new(MockLedgerService::new(committee));
1083        // Initialize the storage.
1084        let storage = Storage::<CurrentNetwork>::new(ledger, Arc::new(BFTMemoryService::new()), 1).unwrap();
1085
1086        // Ensure the storage is empty.
1087        assert_storage(&storage, &[], &[], &[], &Default::default());
1088
1089        // Create a new certificate.
1090        let certificate = snarkvm::ledger::narwhal::batch_certificate::test_helpers::sample_batch_certificate(rng);
1091        // Retrieve the certificate ID.
1092        let certificate_id = certificate.id();
1093        // Retrieve the round.
1094        let round = certificate.round();
1095        // Retrieve the author of the batch.
1096        let author = certificate.author();
1097
1098        // Construct the sample 'transmissions'.
1099        let (missing_transmissions, transmissions) = sample_transmissions(&certificate, rng);
1100
1101        // Insert the certificate.
1102        storage.insert_certificate_atomic(certificate.clone(), Default::default(), missing_transmissions);
1103        // Ensure the certificate exists in storage.
1104        assert!(storage.contains_certificate(certificate_id));
1105        // Ensure the certificate is stored in the correct round.
1106        assert_eq!(storage.get_certificates_for_round(round), indexset! { certificate.clone() });
1107        // Ensure the certificate is stored for the correct round and author.
1108        assert_eq!(storage.get_certificate_for_round_with_author(round, author), Some(certificate.clone()));
1109
1110        // Check that the underlying storage representation is correct.
1111        {
1112            // Construct the expected layout for 'rounds'.
1113            let rounds = [(round, indexset! { (certificate_id, author) })];
1114            // Construct the expected layout for 'certificates'.
1115            let certificates = [(certificate_id, certificate.clone())];
1116            // Construct the expected layout for 'batch_ids'.
1117            let batch_ids = [(certificate_id, round)];
1118            // Assert the storage is well-formed.
1119            assert_storage(&storage, &rounds, &certificates, &batch_ids, &transmissions);
1120        }
1121
1122        // Retrieve the certificate.
1123        let candidate_certificate = storage.get_certificate(certificate_id).unwrap();
1124        // Ensure the retrieved certificate is the same as the inserted certificate.
1125        assert_eq!(certificate, candidate_certificate);
1126
1127        // Remove the certificate.
1128        assert!(storage.remove_certificate(certificate_id));
1129        // Ensure the certificate does not exist in storage.
1130        assert!(!storage.contains_certificate(certificate_id));
1131        // Ensure the certificate is no longer stored in the round.
1132        assert!(storage.get_certificates_for_round(round).is_empty());
1133        // Ensure the certificate is no longer stored for the round and author.
1134        assert_eq!(storage.get_certificate_for_round_with_author(round, author), None);
1135        // Ensure the storage is empty.
1136        assert_storage(&storage, &[], &[], &[], &Default::default());
1137    }
1138
1139    #[test]
1140    fn test_certificate_duplicate() {
1141        let rng = &mut TestRng::default();
1142
1143        // Sample a committee.
1144        let committee = snarkvm::ledger::committee::test_helpers::sample_committee(rng);
1145        // Initialize the ledger.
1146        let ledger = Arc::new(MockLedgerService::new(committee));
1147        // Initialize the storage.
1148        let storage = Storage::<CurrentNetwork>::new(ledger, Arc::new(BFTMemoryService::new()), 1).unwrap();
1149
1150        // Ensure the storage is empty.
1151        assert_storage(&storage, &[], &[], &[], &Default::default());
1152
1153        // Create a new certificate.
1154        let certificate = snarkvm::ledger::narwhal::batch_certificate::test_helpers::sample_batch_certificate(rng);
1155        // Retrieve the certificate ID.
1156        let certificate_id = certificate.id();
1157        // Retrieve the round.
1158        let round = certificate.round();
1159        // Retrieve the author of the batch.
1160        let author = certificate.author();
1161
1162        // Construct the expected layout for 'rounds'.
1163        let rounds = [(round, indexset! { (certificate_id, author) })];
1164        // Construct the expected layout for 'certificates'.
1165        let certificates = [(certificate_id, certificate.clone())];
1166        // Construct the expected layout for 'batch_ids'.
1167        let batch_ids = [(certificate_id, round)];
1168        // Construct the sample 'transmissions'.
1169        let (missing_transmissions, transmissions) = sample_transmissions(&certificate, rng);
1170
1171        // Insert the certificate.
1172        storage.insert_certificate_atomic(certificate.clone(), Default::default(), missing_transmissions.clone());
1173        // Ensure the certificate exists in storage.
1174        assert!(storage.contains_certificate(certificate_id));
1175        // Check that the underlying storage representation is correct.
1176        assert_storage(&storage, &rounds, &certificates, &batch_ids, &transmissions);
1177
1178        // Insert the certificate again - without any missing transmissions.
1179        storage.insert_certificate_atomic(certificate.clone(), Default::default(), Default::default());
1180        // Ensure the certificate exists in storage.
1181        assert!(storage.contains_certificate(certificate_id));
1182        // Check that the underlying storage representation remains unchanged.
1183        assert_storage(&storage, &rounds, &certificates, &batch_ids, &transmissions);
1184
1185        // Insert the certificate again - with all of the original missing transmissions.
1186        storage.insert_certificate_atomic(certificate, Default::default(), missing_transmissions);
1187        // Ensure the certificate exists in storage.
1188        assert!(storage.contains_certificate(certificate_id));
1189        // Check that the underlying storage representation remains unchanged.
1190        assert_storage(&storage, &rounds, &certificates, &batch_ids, &transmissions);
1191    }
1192
1193    /// Verify that when inserting a certificate with a mix of provided transmissions and aborted
1194    /// transmission IDs, storage correctly records both: contains_transmission is true for all
1195    /// (including aborted), and aborted IDs are stored so sync can resolve certificate references.
1196    #[test]
1197    fn test_certificate_insert_with_aborted_transmissions() {
1198        use std::collections::HashSet;
1199
1200        let rng = &mut TestRng::default();
1201
1202        let committee = snarkvm::ledger::committee::test_helpers::sample_committee(rng);
1203        let ledger = Arc::new(MockLedgerService::new(committee));
1204        let storage = Storage::<CurrentNetwork>::new(ledger, Arc::new(BFTMemoryService::new()), 1).unwrap();
1205
1206        let certificate = snarkvm::ledger::narwhal::batch_certificate::test_helpers::sample_batch_certificate(rng);
1207        let certificate_id = certificate.id();
1208        let round = certificate.round();
1209        let transmission_ids: Vec<_> = certificate.transmission_ids().iter().copied().collect();
1210
1211        if transmission_ids.len() < 2 {
1212            // Certificate has 0 or 1 transmission; just verify insert without aborted works.
1213            let (missing_transmissions, _) = sample_transmissions(&certificate, rng);
1214            storage.insert_certificate_atomic(certificate.clone(), HashSet::new(), missing_transmissions);
1215            for id in certificate.transmission_ids() {
1216                assert!(storage.contains_transmission(*id));
1217            }
1218            return;
1219        }
1220
1221        let (all_missing, _) = sample_transmissions(&certificate, rng);
1222        let aborted_id = transmission_ids[0];
1223        let aborted_transmission_ids: HashSet<_> = [aborted_id].into_iter().collect();
1224        let mut missing_transmissions = all_missing;
1225        missing_transmissions.remove(&aborted_id);
1226
1227        storage.insert_certificate_atomic(certificate.clone(), aborted_transmission_ids, missing_transmissions);
1228
1229        assert!(storage.contains_certificate(certificate_id));
1230        assert_eq!(storage.get_certificates_for_round(round), indexset! { certificate.clone() });
1231
1232        // Every transmission ID in the certificate (including aborted) should be resolvable.
1233        for id in certificate.transmission_ids() {
1234            assert!(
1235                storage.contains_transmission(*id),
1236                "contains_transmission should be true for all transmission IDs including aborted {id:?}"
1237            );
1238        }
1239
1240        // Aborted transmission has no content in storage; others do.
1241        assert!(
1242            storage.get_transmission(aborted_id).is_none(),
1243            "Aborted transmission should not have content in storage"
1244        );
1245        for id in transmission_ids.iter().skip(1) {
1246            assert!(
1247                storage.get_transmission(*id).is_some(),
1248                "Non-aborted transmission {id:?} should have content in storage"
1249            );
1250        }
1251    }
1252
1253    /// Test that `check_incoming_certificate` does not reject a valid cert.
1254    #[test]
1255    fn test_valid_incoming_certificate() {
1256        let rng = &mut TestRng::default();
1257
1258        // Sample a committee.
1259        let (committee, private_keys) =
1260            snarkvm::ledger::committee::test_helpers::sample_committee_and_keys_for_round(0, 5, rng);
1261        // Initialize the ledger.
1262        let ledger = Arc::new(MockLedgerService::new(committee));
1263        // Initialize the storage.
1264        let storage = Storage::<CurrentNetwork>::new(ledger, Arc::new(BFTMemoryService::new()), 1).unwrap();
1265
1266        // Go through many rounds of valid certificates and ensure they're accepted.
1267        let mut previous_certs = IndexSet::default();
1268
1269        for round in 1..=100 {
1270            let mut new_certs = IndexSet::default();
1271
1272            // Generate one cert per validator
1273            for private_key in private_keys.iter() {
1274                let other_keys: Vec<_> = private_keys.iter().cloned().filter(|k| k != private_key).collect();
1275
1276                let certificate = sample_batch_certificate_for_round_with_committee(
1277                    round,
1278                    previous_certs.clone(),
1279                    private_key,
1280                    &other_keys,
1281                    rng,
1282                );
1283                storage.check_incoming_certificate(&certificate).expect("Valid certificate rejected");
1284                new_certs.insert(certificate.id());
1285
1286                // Construct the sample 'transmissions'.
1287                let (missing_transmissions, _transmissions) = sample_transmissions(&certificate, rng);
1288                storage.insert_certificate_atomic(certificate, Default::default(), missing_transmissions);
1289            }
1290
1291            previous_certs = new_certs;
1292        }
1293    }
1294
1295    /// Make sure that we reject all certificates without sufficient signatures early.
1296    #[test]
1297    fn test_invalid_incoming_certificate_missing_signature() {
1298        let rng = &mut TestRng::default();
1299
1300        // Sample a committee.
1301        let (committee, private_keys) =
1302            snarkvm::ledger::committee::test_helpers::sample_committee_and_keys_for_round(0, 10, rng);
1303        // Initialize the ledger.
1304        let ledger = Arc::new(MockLedgerService::new(committee));
1305        // Initialize the storage.
1306        let storage = Storage::<CurrentNetwork>::new(ledger, Arc::new(BFTMemoryService::new()), 1).unwrap();
1307
1308        // Go through many rounds of valid certificates and ensure they're accepted.
1309        let mut previous_certs = IndexSet::default();
1310
1311        for round in 1..=5 {
1312            let mut new_certs = IndexSet::default();
1313
1314            // Generate one cert per validator
1315            for private_key in private_keys.iter() {
1316                if round < 5 {
1317                    let other_keys: Vec<_> = private_keys.iter().cloned().filter(|k| k != private_key).collect();
1318
1319                    let certificate = sample_batch_certificate_for_round_with_committee(
1320                        round,
1321                        previous_certs.clone(),
1322                        private_key,
1323                        &other_keys,
1324                        rng,
1325                    );
1326                    storage.check_incoming_certificate(&certificate).expect("Valid certificate rejected");
1327                    new_certs.insert(certificate.id());
1328
1329                    // Construct the sample 'transmissions'.
1330                    let (missing_transmissions, _transmissions) = sample_transmissions(&certificate, rng);
1331                    storage.insert_certificate_atomic(certificate, Default::default(), missing_transmissions);
1332                } else {
1333                    // Pick a few signers, but not enough to form a quorum.
1334                    let other_keys: Vec<_> = private_keys[0..=3].iter().cloned().filter(|k| k != private_key).collect();
1335
1336                    let certificate = sample_batch_certificate_for_round_with_committee(
1337                        round,
1338                        previous_certs.clone(),
1339                        private_key,
1340                        &other_keys,
1341                        rng,
1342                    );
1343                    assert!(storage.check_incoming_certificate(&certificate).is_err());
1344                }
1345            }
1346
1347            previous_certs = new_certs;
1348        }
1349    }
1350
1351    /// Verify that `insert_certificate` rejects certs with less edges than required.
1352    #[test]
1353    fn test_invalid_certificate_insufficient_previous_certs() {
1354        let rng = &mut TestRng::default();
1355
1356        // Sample a committee.
1357        let (committee, private_keys) =
1358            snarkvm::ledger::committee::test_helpers::sample_committee_and_keys_for_round(0, 10, rng);
1359        // Initialize the ledger.
1360        let ledger = Arc::new(MockLedgerService::new(committee));
1361        // Initialize the storage.
1362        let storage = Storage::<CurrentNetwork>::new(ledger, Arc::new(BFTMemoryService::new()), 1).unwrap();
1363
1364        // Go through many rounds of valid certificates and ensure they're accepted.
1365        let mut previous_certs = IndexSet::default();
1366
1367        for round in 1..=6 {
1368            let mut new_certs = IndexSet::default();
1369
1370            // Generate one cert per validator
1371            for private_key in private_keys.iter() {
1372                let other_keys: Vec<_> = private_keys.iter().cloned().filter(|k| k != private_key).collect();
1373
1374                let certificate = sample_batch_certificate_for_round_with_committee(
1375                    round,
1376                    previous_certs.clone(),
1377                    private_key,
1378                    &other_keys,
1379                    rng,
1380                );
1381
1382                // Construct the sample 'transmissions'.
1383                let (_missing_transmissions, transmissions) = sample_transmissions(&certificate, rng);
1384                let transmissions = transmissions.into_iter().map(|(k, (t, _))| (k, t)).collect();
1385
1386                if round <= 5 {
1387                    new_certs.insert(certificate.id());
1388                    storage
1389                        .insert_certificate(certificate, transmissions, Default::default())
1390                        .expect("Valid certificate rejected");
1391                } else {
1392                    assert!(storage.insert_certificate(certificate, transmissions, Default::default()).is_err());
1393                }
1394            }
1395
1396            if round < 5 {
1397                previous_certs = new_certs;
1398            } else {
1399                // Remove more than half of the previous certs.
1400                previous_certs = new_certs.into_iter().skip(6).collect();
1401            }
1402        }
1403    }
1404
1405    /// Verify that `insert_certificate` rejects certs that do not increment the round number.
1406    #[test]
1407    fn test_invalid_certificate_wrong_round_number() {
1408        let rng = &mut TestRng::default();
1409
1410        // Sample a committee.
1411        let (committee, private_keys) =
1412            snarkvm::ledger::committee::test_helpers::sample_committee_and_keys_for_round(0, 10, rng);
1413        // Initialize the ledger.
1414        let ledger = Arc::new(MockLedgerService::new(committee));
1415        // Initialize the storage.
1416        let storage = Storage::<CurrentNetwork>::new(ledger, Arc::new(BFTMemoryService::new()), 1).unwrap();
1417
1418        // Go through many rounds of valid certificates and ensure they're accepted.
1419        let mut previous_certs = IndexSet::default();
1420
1421        for round in 1..=6 {
1422            let mut new_certs = IndexSet::default();
1423
1424            // Generate one cert per validator
1425            for private_key in private_keys.iter() {
1426                let cert_round = round.min(5); // In the sixth round, do not increment
1427                let other_keys: Vec<_> = private_keys.iter().cloned().filter(|k| k != private_key).collect();
1428
1429                let certificate = sample_batch_certificate_for_round_with_committee(
1430                    cert_round,
1431                    previous_certs.clone(),
1432                    private_key,
1433                    &other_keys,
1434                    rng,
1435                );
1436
1437                // Construct the sample 'transmissions'.
1438                let (_missing_transmissions, transmissions) = sample_transmissions(&certificate, rng);
1439                let transmissions = transmissions.into_iter().map(|(k, (t, _))| (k, t)).collect();
1440
1441                if round <= 5 {
1442                    new_certs.insert(certificate.id());
1443                    storage
1444                        .insert_certificate(certificate, transmissions, Default::default())
1445                        .expect("Valid certificate rejected");
1446                } else {
1447                    assert!(storage.insert_certificate(certificate, transmissions, Default::default()).is_err());
1448                }
1449            }
1450
1451            if round < 5 {
1452                previous_certs = new_certs;
1453            } else {
1454                // Remove more than half of the previous certs.
1455                previous_certs = new_certs.into_iter().skip(6).collect();
1456            }
1457        }
1458    }
1459}
1460
1461#[cfg(test)]
1462pub mod prop_tests {
1463    use super::*;
1464    use crate::helpers::{now, storage::tests::assert_storage};
1465    use snarkos_node_bft_events::committee_prop_tests::{CommitteeContext, ValidatorSet};
1466    use snarkos_node_bft_ledger_service::MockLedgerService;
1467    use snarkos_node_bft_storage_service::BFTMemoryService;
1468    use snarkvm::{
1469        ledger::{
1470            narwhal::{BatchHeader, Data},
1471            puzzle::SolutionID,
1472        },
1473        prelude::{Signature, Uniform},
1474    };
1475
1476    use ::bytes::Bytes;
1477    use indexmap::indexset;
1478    use proptest::{
1479        collection,
1480        prelude::{Arbitrary, BoxedStrategy, Just, Strategy, any},
1481        prop_oneof,
1482        sample::{Selector, size_range},
1483    };
1484    use rand::{CryptoRng, SeedableRng, TryCryptoRng, TryRng};
1485    use rand_chacha::ChaChaRng;
1486    use std::fmt::Debug;
1487    use test_strategy::proptest;
1488
1489    type CurrentNetwork = snarkvm::prelude::MainnetV0;
1490
1491    impl Arbitrary for Storage<CurrentNetwork> {
1492        type Parameters = CommitteeContext;
1493        type Strategy = BoxedStrategy<Storage<CurrentNetwork>>;
1494
1495        fn arbitrary() -> Self::Strategy {
1496            (any::<CommitteeContext>(), 0..BatchHeader::<CurrentNetwork>::MAX_GC_ROUNDS as u64)
1497                .prop_map(|(CommitteeContext(committee, _), gc_rounds)| {
1498                    let ledger = Arc::new(MockLedgerService::new(committee));
1499                    Storage::<CurrentNetwork>::new(ledger, Arc::new(BFTMemoryService::new()), gc_rounds).unwrap()
1500                })
1501                .boxed()
1502        }
1503
1504        fn arbitrary_with(context: Self::Parameters) -> Self::Strategy {
1505            (Just(context), 0..BatchHeader::<CurrentNetwork>::MAX_GC_ROUNDS as u64)
1506                .prop_map(|(CommitteeContext(committee, _), gc_rounds)| {
1507                    let ledger = Arc::new(MockLedgerService::new(committee));
1508                    Storage::<CurrentNetwork>::new(ledger, Arc::new(BFTMemoryService::new()), gc_rounds).unwrap()
1509                })
1510                .boxed()
1511        }
1512    }
1513
1514    // The `proptest::TestRng` doesn't implement `rand_core::CryptoRng` trait which is required in snarkVM, so we use a wrapper
1515    // We wrap a `ChaChaRng` (rand 0.10 compatible) seeded from the proptest RNG.
1516    #[derive(Debug)]
1517    pub struct CryptoTestRng(ChaChaRng);
1518
1519    impl Arbitrary for CryptoTestRng {
1520        type Parameters = ();
1521        type Strategy = BoxedStrategy<CryptoTestRng>;
1522
1523        fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
1524            use proptest::prelude::RngCore as ProptestRngCore;
1525            Just(0).prop_perturb(|_, mut rng| CryptoTestRng(ChaChaRng::seed_from_u64(rng.next_u64()))).boxed()
1526        }
1527    }
1528
1529    impl TryRng for CryptoTestRng {
1530        type Error = core::convert::Infallible;
1531
1532        fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
1533            TryRng::try_next_u32(&mut self.0)
1534        }
1535
1536        fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
1537            TryRng::try_next_u64(&mut self.0)
1538        }
1539
1540        fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
1541            TryRng::try_fill_bytes(&mut self.0, dest)
1542        }
1543    }
1544
1545    impl TryCryptoRng for CryptoTestRng {}
1546
1547    #[derive(Debug, Clone)]
1548    pub struct AnyTransmission(pub Transmission<CurrentNetwork>);
1549
1550    impl Arbitrary for AnyTransmission {
1551        type Parameters = ();
1552        type Strategy = BoxedStrategy<AnyTransmission>;
1553
1554        fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
1555            any_transmission().prop_map(AnyTransmission).boxed()
1556        }
1557    }
1558
1559    #[derive(Debug, Clone)]
1560    pub struct AnyTransmissionID(pub TransmissionID<CurrentNetwork>);
1561
1562    impl Arbitrary for AnyTransmissionID {
1563        type Parameters = ();
1564        type Strategy = BoxedStrategy<AnyTransmissionID>;
1565
1566        fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
1567            any_transmission_id().prop_map(AnyTransmissionID).boxed()
1568        }
1569    }
1570
1571    fn any_transmission() -> BoxedStrategy<Transmission<CurrentNetwork>> {
1572        prop_oneof![
1573            (collection::vec(any::<u8>(), 512..=512))
1574                .prop_map(|bytes| Transmission::Solution(Data::Buffer(Bytes::from(bytes)))),
1575            (collection::vec(any::<u8>(), 2048..=2048))
1576                .prop_map(|bytes| Transmission::Transaction(Data::Buffer(Bytes::from(bytes)))),
1577        ]
1578        .boxed()
1579    }
1580
1581    pub fn any_solution_id() -> BoxedStrategy<SolutionID<CurrentNetwork>> {
1582        any::<u64>().prop_map(|x| x.into()).boxed()
1583    }
1584
1585    pub fn any_transaction_id() -> BoxedStrategy<<CurrentNetwork as Network>::TransactionID> {
1586        any::<u64>()
1587            .prop_map(|seed| {
1588                let rng = &mut ChaChaRng::seed_from_u64(seed);
1589                <CurrentNetwork as Network>::TransactionID::from(Field::rand(rng))
1590            })
1591            .boxed()
1592    }
1593
1594    pub fn any_transmission_id() -> BoxedStrategy<TransmissionID<CurrentNetwork>> {
1595        prop_oneof![
1596            (any_transaction_id(), any::<<CurrentNetwork as Network>::TransmissionChecksum>())
1597                .prop_map(|(id, cs)| TransmissionID::Transaction(id, cs)),
1598            (any_solution_id(), any::<<CurrentNetwork as Network>::TransmissionChecksum>())
1599                .prop_map(|(id, cs)| TransmissionID::Solution(id, cs)),
1600        ]
1601        .boxed()
1602    }
1603
1604    pub fn sign_batch_header<R: CryptoRng>(
1605        validator_set: &ValidatorSet,
1606        batch_header: &BatchHeader<CurrentNetwork>,
1607        rng: &mut R,
1608    ) -> IndexSet<Signature<CurrentNetwork>> {
1609        let mut signatures = IndexSet::with_capacity(validator_set.0.len());
1610        for validator in validator_set.0.iter() {
1611            let private_key = validator.private_key;
1612            signatures.insert(private_key.sign(&[batch_header.batch_id()], rng).unwrap());
1613        }
1614        signatures
1615    }
1616
1617    #[proptest]
1618    fn test_certificate_duplicate(
1619        context: CommitteeContext,
1620        #[any(size_range(1..16).lift())] transmissions: Vec<(AnyTransmissionID, AnyTransmission)>,
1621        mut rng: CryptoTestRng,
1622        selector: Selector,
1623    ) {
1624        let CommitteeContext(committee, ValidatorSet(validators)) = context;
1625        let committee_id = committee.id();
1626
1627        // Initialize the storage.
1628        let ledger = Arc::new(MockLedgerService::new(committee));
1629        let storage = Storage::<CurrentNetwork>::new(ledger, Arc::new(BFTMemoryService::new()), 1).unwrap();
1630
1631        // Ensure the storage is empty.
1632        assert_storage(&storage, &[], &[], &[], &Default::default());
1633
1634        // Create a new certificate.
1635        let signer = selector.select(&validators);
1636
1637        let mut transmission_map = IndexMap::new();
1638
1639        for (AnyTransmissionID(id), AnyTransmission(t)) in transmissions.iter() {
1640            transmission_map.insert(*id, t.clone());
1641        }
1642
1643        let batch_header = BatchHeader::new(
1644            &signer.private_key,
1645            0,
1646            now(),
1647            committee_id,
1648            transmission_map.keys().cloned().collect(),
1649            Default::default(),
1650            &mut rng,
1651        )
1652        .unwrap();
1653
1654        // Remove the author from the validator set passed to create the batch
1655        // certificate, the author should not sign their own batch.
1656        let mut validators = validators.clone();
1657        validators.remove(signer);
1658
1659        let certificate = BatchCertificate::from(
1660            batch_header.clone(),
1661            sign_batch_header(&ValidatorSet(validators), &batch_header, &mut rng),
1662        )
1663        .unwrap();
1664
1665        // Retrieve the certificate ID.
1666        let certificate_id = certificate.id();
1667        let mut internal_transmissions = HashMap::<_, (_, IndexSet<Field<CurrentNetwork>>)>::new();
1668        for (AnyTransmissionID(id), AnyTransmission(t)) in transmissions.iter().cloned() {
1669            internal_transmissions.entry(id).or_insert((t, Default::default())).1.insert(certificate_id);
1670        }
1671
1672        // Retrieve the round.
1673        let round = certificate.round();
1674        // Retrieve the author of the batch.
1675        let author = certificate.author();
1676
1677        // Construct the expected layout for 'rounds'.
1678        let rounds = [(round, indexset! { (certificate_id, author) })];
1679        // Construct the expected layout for 'certificates'.
1680        let certificates = [(certificate_id, certificate.clone())];
1681        // Construct the expected layout for 'batch_ids'.
1682        let batch_ids = [(certificate_id, round)];
1683
1684        // Insert the certificate.
1685        let missing_transmissions: HashMap<TransmissionID<CurrentNetwork>, Transmission<CurrentNetwork>> =
1686            transmission_map.into_iter().collect();
1687        storage.insert_certificate_atomic(certificate.clone(), Default::default(), missing_transmissions.clone());
1688        // Ensure the certificate exists in storage.
1689        assert!(storage.contains_certificate(certificate_id));
1690        // Check that the underlying storage representation is correct.
1691        assert_storage(&storage, &rounds, &certificates, &batch_ids, &internal_transmissions);
1692
1693        // Insert the certificate again - without any missing transmissions.
1694        storage.insert_certificate_atomic(certificate.clone(), Default::default(), Default::default());
1695        // Ensure the certificate exists in storage.
1696        assert!(storage.contains_certificate(certificate_id));
1697        // Check that the underlying storage representation remains unchanged.
1698        assert_storage(&storage, &rounds, &certificates, &batch_ids, &internal_transmissions);
1699
1700        // Insert the certificate again - with all of the original missing transmissions.
1701        storage.insert_certificate_atomic(certificate, Default::default(), missing_transmissions);
1702        // Ensure the certificate exists in storage.
1703        assert!(storage.contains_certificate(certificate_id));
1704        // Check that the underlying storage representation remains unchanged.
1705        assert_storage(&storage, &rounds, &certificates, &batch_ids, &internal_transmissions);
1706    }
1707}