use snarkvm::{
console::types::{Address, Field},
ledger::narwhal::BatchCertificate,
prelude::Network,
};
use indexmap::IndexSet;
use std::collections::{BTreeMap, HashMap};
#[derive(Debug)]
pub struct DAG<N: Network> {
graph: BTreeMap<u64, HashMap<Address<N>, BatchCertificate<N>>>,
recent_committed_ids: BTreeMap<u64, IndexSet<Field<N>>>,
last_committed_round: u64,
}
impl<N: Network> Default for DAG<N> {
fn default() -> Self {
Self::new()
}
}
impl<N: Network> DAG<N> {
pub fn new() -> Self {
Self { graph: Default::default(), recent_committed_ids: Default::default(), last_committed_round: 0 }
}
pub const fn graph(&self) -> &BTreeMap<u64, HashMap<Address<N>, BatchCertificate<N>>> {
&self.graph
}
pub const fn last_committed_round(&self) -> u64 {
self.last_committed_round
}
pub fn is_recently_committed(&self, round: u64, certificate_id: Field<N>) -> bool {
self.recent_committed_ids.get(&round).is_some_and(|ids| ids.contains(&certificate_id))
}
pub fn contains_certificate_in_round(&self, round: u64, certificate_id: Field<N>) -> bool {
self.graph.get(&round).is_some_and(|map| map.values().any(|certificate| certificate.id() == certificate_id))
}
pub fn get_certificate_for_round_with_author(&self, round: u64, author: Address<N>) -> Option<BatchCertificate<N>> {
self.graph.get(&round).and_then(|certificates| certificates.get(&author)).cloned()
}
pub fn get_certificate_for_round_with_id(
&self,
round: u64,
certificate_id: Field<N>,
) -> Option<BatchCertificate<N>> {
self.graph
.get(&round)
.and_then(|map| map.values().find(|certificate| certificate.id() == certificate_id))
.cloned()
}
pub fn get_certificates_for_round(&self, round: u64) -> Option<HashMap<Address<N>, BatchCertificate<N>>> {
self.graph.get(&round).cloned()
}
pub fn insert(&mut self, certificate: BatchCertificate<N>) {
let round = certificate.round();
let author = certificate.author();
if !self.is_recently_committed(round, certificate.id()) {
let previous = self.graph.entry(round).or_default().insert(author, certificate);
if previous.is_none() {
trace!("Added new certificate for round {round} by author {author} to the DAG");
} else {
#[cfg(debug_assertions)]
error!("A certificate for round {round} by author {author} already existed in the DAG");
}
}
}
pub fn commit(&mut self, certificate: &BatchCertificate<N>, max_gc_rounds: u64) {
let certificate_id = certificate.id();
let certificate_round = certificate.round();
let author = certificate.author();
let is_new = self.recent_committed_ids.entry(certificate_round).or_default().insert(certificate_id);
if !is_new {
trace!("Certificate {certificate_id} was already committed for round {certificate_round}");
}
if self.last_committed_round < certificate_round {
trace!(
"Last committed round updated from {last_committed_round} to {certificate_round}",
last_committed_round = self.last_committed_round
);
self.last_committed_round = certificate_round;
} else {
}
self.recent_committed_ids.retain(|round, _| round + max_gc_rounds > self.last_committed_round);
self.graph.retain(|round, _| round + max_gc_rounds > self.last_committed_round);
self.graph.retain(|round, map| match *round > certificate_round {
true => true,
false => {
map.remove(&author);
!map.is_empty()
}
});
}
}
#[cfg(test)]
pub(crate) mod test_helpers {
use super::*;
pub(crate) fn mock_dag_with_modified_last_committed_round<N: Network>(last_committed_round: u64) -> DAG<N> {
let mut dag = DAG::<N>::new();
dag.last_committed_round = last_committed_round;
dag
}
}
#[cfg(test)]
mod tests {
use super::*;
use snarkvm::{
prelude::{MainnetV0, narwhal::batch_certificate::test_helpers::sample_batch_certificate_for_round},
utilities::TestRng,
};
#[test]
fn test_dag_empty() {
let dag = DAG::<MainnetV0>::new();
assert_eq!(dag.get_certificates_for_round(0), None);
assert_eq!(dag.last_committed_round(), 0);
}
#[test]
fn test_dag_insert() {
let rng = &mut TestRng::default();
let mut dag = DAG::<MainnetV0>::new();
const ROUND: u64 = 2;
let certificate = sample_batch_certificate_for_round(ROUND, rng);
dag.insert(certificate.clone());
assert!(dag.contains_certificate_in_round(ROUND, certificate.id()));
assert_eq!(dag.get_certificate_for_round_with_author(ROUND, certificate.author()), Some(certificate.clone()));
assert_eq!(dag.get_certificate_for_round_with_id(ROUND, certificate.id()), Some(certificate.clone()));
assert_eq!(
dag.get_certificates_for_round(ROUND),
Some(vec![(certificate.author(), certificate)].into_iter().collect())
);
assert_eq!(dag.last_committed_round(), 0);
}
#[test]
fn test_dag_commit() {
let rng = &mut TestRng::default();
let mut dag = DAG::<MainnetV0>::new();
let certificate_2 = sample_batch_certificate_for_round(2, &mut TestRng::fixed(123456789));
let certificate_3 = sample_batch_certificate_for_round(3, &mut TestRng::fixed(123456789));
dag.insert(certificate_2.clone());
assert!(dag.contains_certificate_in_round(2, certificate_2.id()));
assert_eq!(dag.get_certificate_for_round_with_author(2, certificate_2.author()), Some(certificate_2.clone()));
assert_eq!(dag.get_certificate_for_round_with_id(2, certificate_2.id()), Some(certificate_2.clone()));
assert_eq!(
dag.get_certificates_for_round(2),
Some(vec![(certificate_2.author(), certificate_2.clone())].into_iter().collect())
);
assert_eq!(dag.last_committed_round(), 0);
dag.insert(certificate_3.clone());
assert!(dag.contains_certificate_in_round(3, certificate_3.id()));
assert_eq!(dag.get_certificate_for_round_with_author(3, certificate_3.author()), Some(certificate_3.clone()));
assert_eq!(dag.get_certificate_for_round_with_id(3, certificate_3.id()), Some(certificate_3.clone()));
assert_eq!(
dag.get_certificates_for_round(3),
Some(vec![(certificate_3.author(), certificate_3.clone())].into_iter().collect())
);
assert_eq!(dag.last_committed_round(), 0);
let lower = sample_batch_certificate_for_round(2, rng);
dag.insert(lower.clone());
let higher = sample_batch_certificate_for_round(4, rng);
dag.insert(higher.clone());
dag.commit(&certificate_3, 10);
assert!(!dag.contains_certificate_in_round(2, certificate_2.id()));
assert!(!dag.contains_certificate_in_round(3, certificate_3.id()));
assert!(dag.contains_certificate_in_round(2, lower.id()));
assert!(dag.contains_certificate_in_round(4, higher.id()));
assert_eq!(dag.last_committed_round(), 3);
}
#[test]
fn test_is_recently_committed() {
let mut dag = DAG::<MainnetV0>::new();
let certificate_2 = sample_batch_certificate_for_round(2, &mut TestRng::fixed(123456789));
let certificate_3 = sample_batch_certificate_for_round(3, &mut TestRng::fixed(123456789));
let certificate_4 = sample_batch_certificate_for_round(4, &mut TestRng::fixed(123456789));
dag.insert(certificate_2.clone());
assert!(!dag.is_recently_committed(2, certificate_2.id()));
assert!(!dag.is_recently_committed(3, certificate_3.id()));
dag.commit(&certificate_2, 2);
assert!(dag.is_recently_committed(2, certificate_2.id()));
assert!(!dag.is_recently_committed(3, certificate_3.id()));
dag.insert(certificate_3.clone());
assert!(dag.is_recently_committed(2, certificate_2.id()));
assert!(!dag.is_recently_committed(3, certificate_3.id()));
dag.commit(&certificate_3, 2);
assert!(dag.is_recently_committed(2, certificate_2.id()));
assert!(dag.is_recently_committed(3, certificate_3.id()));
dag.insert(certificate_4.clone());
assert!(dag.is_recently_committed(2, certificate_2.id()));
assert!(dag.is_recently_committed(3, certificate_3.id()));
assert!(!dag.is_recently_committed(4, certificate_4.id()));
dag.commit(&certificate_4, 2);
assert!(!dag.is_recently_committed(2, certificate_2.id()));
assert!(dag.is_recently_committed(3, certificate_3.id()));
assert!(dag.is_recently_committed(4, certificate_4.id()));
}
}