/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2023 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
use std::{
cmp::Ordering,
collections::{HashMap, HashSet, VecDeque},
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
},
time::UNIX_EPOCH,
};
use async_recursion::async_recursion;
use darkfi_serial::{deserialize_async, serialize_async};
use log::{debug, error, info};
use num_bigint::BigUint;
use smol::{
lock::{Mutex, RwLock},
Executor,
};
use crate::{
net::P2pPtr,
system::{sleep, timeout::timeout, StoppableTask, StoppableTaskPtr, Subscriber, SubscriberPtr},
Error, Result,
};
/// An event graph event
pub mod event;
pub use event::Event;
/// P2P protocol implementation for the Event Graph
pub mod proto;
use proto::{EventRep, EventReq, TipRep, TipReq, REPLY_TIMEOUT};
/// Utility functions
mod util;
use util::{days_since, next_rotation_timestamp, DAY};
#[cfg(test)]
mod tests;
/// Initial genesis timestamp (07 Sep 2023, 00:00:00 UTC)
/// Must always be UTC midnight.
const INITIAL_GENESIS: u64 = 1694044800;
/// Genesis event contents
const GENESIS_CONTENTS: &[u8] = &[0x47, 0x45, 0x4e, 0x45, 0x53, 0x49, 0x53];
/// The number of parents an event is supposed to have.
const N_EVENT_PARENTS: usize = 5;
/// Allowed timestamp drift in seconds
const EVENT_TIME_DRIFT: u64 = 60;
/// Null event ID
pub const NULL_ID: blake3::Hash = blake3::Hash::from_bytes([0x00; blake3::OUT_LEN]);
/// Atomic pointer to an [`EventGraph`] instance.
pub type EventGraphPtr = Arc;
/// An Event Graph instance
pub struct EventGraph {
/// Pointer to the P2P network instance
p2p: P2pPtr,
/// Sled tree containing the DAG
dag: sled::Tree,
/// The set of unreferenced DAG tips
unreferenced_tips: RwLock>,
/// A `HashSet` containg event IDs and their 1-level parents.
/// These come from the events we've sent out using `EventPut`.
/// They are used with `EventReq` to decide if we should reply
/// or not. Additionally it is also used when we broadcast the
/// `TipRep` message telling peers about our unreferenced tips.
broadcasted_ids: RwLock>,
/// Marker telling us if we consider the DAG synced
dag_synced: AtomicBool,
/// DAG Pruning Task
prune_task: Mutex>,
/// Event subscriber, this notifies whenever an event is
/// inserted into the DAG
pub event_sub: SubscriberPtr,
}
impl EventGraph {
/// Create a new [`EventGraph`] instance.
/// * `days_rotation` marks the lifetime of the DAG before it's pruned.
pub async fn new(
p2p: P2pPtr,
sled_db: sled::Db,
dag_tree_name: &str,
days_rotation: u64,
ex: Arc>,
) -> Result {
let dag = sled_db.open_tree(dag_tree_name)?;
let unreferenced_tips = RwLock::new(HashSet::new());
let broadcasted_ids = RwLock::new(HashSet::new());
let event_sub = Subscriber::new();
let self_ = Arc::new(Self {
p2p,
dag: dag.clone(),
unreferenced_tips,
broadcasted_ids,
dag_synced: AtomicBool::new(false),
prune_task: Mutex::new(None),
event_sub,
});
// Create the current genesis event based on the `days_rotation`
let current_genesis = Self::generate_genesis(days_rotation);
// Check if we have it in our DAG.
// If not, we can prune the DAG and insert this new genesis event.
if !dag.contains_key(current_genesis.id().as_bytes())? {
info!(
target: "event_graph::new()",
"[EVENTGRAPH] DAG does not contain current genesis, pruning existing data",
);
dag.clear()?;
self_.dag_insert(current_genesis).await?;
}
// Find the unreferenced tips in the current DAG state.
*self_.unreferenced_tips.write().await = self_.find_unreferenced_tips().await;
// Spawn the DAG pruning task
let self__ = self_.clone();
let prune_task = StoppableTask::new();
*self_.prune_task.lock().await = Some(prune_task.clone());
prune_task.clone().start(
self_.clone().dag_prune(days_rotation),
|_| async move {
self__.clone()._handle_stop(sled_db).await;
},
Error::DetachedTaskStopped,
ex.clone(),
);
Ok(self_)
}
async fn _handle_stop(&self, sled_db: sled::Db) {
info!(target: "event_graph::_handle_stop()", "[EVENTGRAPH] Prune task stopped, flushing sled");
sled_db.flush_async().await.unwrap();
}
/// Generate a deterministic genesis event corresponding to the DAG's configuration.
fn generate_genesis(days_rotation: u64) -> Event {
// Days rotation is u64 except zero
let genesis_days_rotation = if days_rotation == 0 { 1 } else { days_rotation };
// First check how many days passed since initial genesis.
let days_passed = days_since(INITIAL_GENESIS);
// Calculate the number of days_rotation intervals since INITIAL_GENESIS
let rotations_since_genesis = days_passed / genesis_days_rotation;
// Calculate the timestamp of the most recent event
let timestamp =
INITIAL_GENESIS + (rotations_since_genesis * genesis_days_rotation * DAY as u64);
Event { timestamp, content: GENESIS_CONTENTS.to_vec(), parents: [NULL_ID; N_EVENT_PARENTS] }
}
/// Sync the DAG from connected peers
pub async fn dag_sync(&self) -> Result<()> {
// We do an optimistic sync where we ask all our connected peers for
// the DAG tips (unreferenced events) and then we accept the ones we
// see the most times.
// * Compare received tips with local ones, identify which we are missing.
// * Request these from peers
// * Recursively request these backward
//
// Verification:
// * Timestamps should go backwards
// * Cross-check with multiple peers, this means we should request the
// same event from multiple peers and make sure it is the same.
// * Since we should be pruning, if we're not synced after some reasonable
// amount of iterations, these could be faulty peers and we can try again
// from the beginning
// Get references to all our peers.
let channels = self.p2p.channels().await;
let mut communicated_peers = channels.len();
info!(
target: "event_graph::dag_sync()",
"[EVENTGRAPH] Syncing DAG from {} peers...", communicated_peers,
);
// Here we keep track of the tips and how many time we've seen them.
let mut tips: HashMap = HashMap::new();
// Let's first ask all of our peers for their tips and collect them
// in our hashmap above.
for channel in channels.iter() {
let url = channel.address();
let tip_rep_sub = match channel.subscribe_msg::().await {
Ok(v) => v,
Err(e) => {
error!(
target: "event_graph::dag_sync()",
"[EVENTGRAPH] Sync: Couldn't subscribe TipReq for peer {}, skipping ({})",
url, e,
);
communicated_peers -= 1;
continue
}
};
if let Err(e) = channel.send(&TipReq {}).await {
error!(
target: "event_graph::dag_sync()",
"[EVENTGRAPH] Sync: Couldn't contact peer {}, skipping ({})", url, e,
);
communicated_peers -= 1;
continue
};
let peer_tips = match timeout(REPLY_TIMEOUT, tip_rep_sub.receive()).await {
Ok(peer_tips) => peer_tips?,
Err(_) => {
error!(
target: "event_graph::dag_sync()",
"[EVENTGRAPH] Sync: Peer {} didn't reply with tips in time, skipping", url,
);
communicated_peers -= 1;
continue
}
};
let peer_tips = &peer_tips.0;
// Note down the seen tips
for tip in peer_tips {
if let Some(seen_tip) = tips.get_mut(tip) {
*seen_tip += 1;
} else {
tips.insert(*tip, 1);
}
}
}
// After we've communicated all the peers, let's see what happened.
if tips.is_empty() {
error!(
target: "event_graph::dag_sync()",
"[EVENTGRAPH] Sync: Could not find any DAG tips",
);
return Err(Error::DagSyncFailed)
}
// We know the number of peers we've communicated with.
// Arbitrarily, let's not consider events we only got once.
// TODO: This should be more sensible depending on the peer number.
let mut considered_tips = HashSet::new();
for (tip, amount) in tips.iter() {
if amount > &1 {
considered_tips.insert(*tip);
}
}
drop(tips);
// Now begin fetching the events backwards.
let mut missing_parents = vec![];
for tip in considered_tips.iter() {
assert!(tip != &NULL_ID);
if !self.dag.contains_key(tip.as_bytes()).unwrap() {
missing_parents.push(*tip);
}
}
if missing_parents.is_empty() {
info!(target: "event_graph::dag_sync()", "[EVENTGRAPH] DAG synced successfully!");
return Ok(())
}
info!(target: "event_graph::dag_sync()", "[EVENTGRAPH] Fetching events");
let mut received_events = vec![];
while !missing_parents.is_empty() {
for parent_id in missing_parents.clone().iter() {
let mut found_event = false;
for channel in channels.iter() {
let url = channel.address();
debug!(
target: "event_graph::dag_sync()",
"Requesting {} from {}...", parent_id, url,
);
let ev_rep_sub = match channel.subscribe_msg::().await {
Ok(v) => v,
Err(e) => {
error!(
target: "event_graph::dag_sync()",
"[EVENTGRAPH] Sync: Couldn't subscribe EventRep for peer {}, skipping ({})",
url, e,
);
continue
}
};
if let Err(e) = channel.send(&EventReq(*parent_id)).await {
error!(
target: "event_graph::dag_sync()",
"[EVENTGRAPH] Sync: Failed communicating EventReq({}) to {}: {}",
parent_id, url, e,
);
continue
}
let parent = match timeout(REPLY_TIMEOUT, ev_rep_sub.receive()).await {
Ok(parent) => parent,
Err(_) => {
error!(
target: "event_graph::dag_sync()",
"[EVENTGRAPH] Sync: Timeout waiting for parent {} from {}",
parent_id, url,
);
continue
}
};
let parent = match parent {
Ok(v) => v.0.clone(),
Err(e) => {
error!(
target: "event_graph::dag_sync()",
"[EVENTGRAPH] Sync: Failed receiving parent {}: {}",
parent_id, e,
);
continue
}
};
if &parent.id() != parent_id {
error!(
target: "event_graph::dag_sync()",
"[EVENTGRAPH] Sync: Peer {} replied with a wrong event: {}",
url, parent.id(),
);
continue
}
debug!(
target: "event_graph::dag_sync()",
"Got correct parent event {}", parent_id,
);
received_events.push(parent.clone());
let pos = missing_parents.iter().position(|id| id == &parent.id()).unwrap();
missing_parents.remove(pos);
found_event = true;
// See if we have the upper parents
for upper_parent in parent.parents.iter() {
if upper_parent == &NULL_ID {
continue
}
if !self.dag.contains_key(upper_parent.as_bytes()).unwrap() {
debug!(
target: "event_graph::dag_sync()",
"Found upper missing parent event{}", upper_parent,
);
missing_parents.push(*upper_parent);
}
}
break
}
if !found_event {
error!(
target: "event_graph::dag_sync()",
"[EVENTGRAPH] Sync: Failed to get all events",
);
return Err(Error::DagSyncFailed)
}
}
} // <-- while !missing_parents.is_empty
// At this point we should've got all the events.
// We should add them to the DAG.
// TODO: FIXME: Also validate these events.
for event in received_events.iter().rev() {
self.dag_insert(event.clone()).await.unwrap();
}
info!(target: "event_graph::dag_sync()", "[EVENTGRAPH] DAG synced successfully!");
self.dag_synced.store(true, SeqCst);
Ok(())
}
/// Background task periodically pruning the DAG.
async fn dag_prune(self: Arc, days_rotation: u64) -> Result<()> {
// The DAG should periodically be pruned. This can be a configurable
// parameter. By pruning, we should deterministically replace the
// genesis event (can use a deterministic timestamp) and drop everything
// in the DAG, leaving just the new genesis event.
debug!(target: "event_graph::dag_prune()", "Spawned background DAG pruning task");
loop {
if days_rotation == 0 {
return Ok(())
}
// Find the next rotation timestamp:
let next_rotation = next_rotation_timestamp(INITIAL_GENESIS, days_rotation);
// Prepare the new genesis event
let current_genesis = Event {
timestamp: next_rotation,
content: GENESIS_CONTENTS.to_vec(),
parents: [NULL_ID; N_EVENT_PARENTS],
};
// Sleep until it's time to rotate.
let s = UNIX_EPOCH.elapsed().unwrap().as_secs() - next_rotation;
debug!(target: "event_graph::dag_prune()", "Sleeping {}s until next DAG prune", s);
sleep(s).await;
debug!(target: "event_graph::dag_prune()", "Rotation period reached. Pruning DAG");
*self.unreferenced_tips.write().await = HashSet::new();
self.dag.clear()?;
self.dag_insert(current_genesis).await?;
debug!(target: "event_graph::dag_prune()", "DAG pruned successfully");
}
}
/// Insert an event into the DAG.
/// This will append the new event into the unreferenced tips set, and
/// remove the event's parents from it. It will also append the event's
/// level-1 parents to the `broadcasted_ids` set, so the P2P protocol
/// knows that any requests for them are actually legitimate.
/// TODO: The `broadcasted_ids` set should periodically be pruned, when
/// some sensible time has passed after broadcasting the event.
pub async fn dag_insert(&self, event: Event) -> Result {
let event_id = event.id();
debug!(target: "event_graph::dag_insert()", "Inserting event {} into the DAG", event_id);
let s_event = serialize_async(&event).await;
// Update the unreferenced DAG tips set
let mut unreferenced_tips = self.unreferenced_tips.write().await;
let mut bcast_ids = self.broadcasted_ids.write().await;
for parent_id in event.parents.iter() {
if parent_id != &NULL_ID {
unreferenced_tips.remove(parent_id);
bcast_ids.insert(*parent_id);
}
}
unreferenced_tips.insert(event_id);
self.dag.insert(event_id.as_bytes(), s_event).unwrap();
// We hold the write locks until this point because we insert the event
// into the database above, so we don't want anything to read these until
// that insertion is complete.
drop(unreferenced_tips);
drop(bcast_ids);
// Notify about the event on the event subscriber
self.event_sub.notify(event).await;
Ok(event_id)
}
/// Fetch an event from the DAG
pub async fn dag_get(&self, event_id: &blake3::Hash) -> Result> {
let Some(bytes) = self.dag.get(event_id.as_bytes())? else { return Ok(None) };
let event: Event = deserialize_async(&bytes).await?;
Ok(Some(event))
}
/// Find the unreferenced tips in the current DAG state.
async fn find_unreferenced_tips(&self) -> HashSet {
// First get all the event IDs
let mut tips = HashSet::new();
for iter_elem in self.dag.iter() {
let (id, _) = iter_elem.unwrap();
let id = blake3::Hash::from_bytes((&id as &[u8]).try_into().unwrap());
tips.insert(id);
}
for iter_elem in self.dag.iter() {
let (_, event) = iter_elem.unwrap();
let event: Event = deserialize_async(&event).await.unwrap();
for parent in event.parents.iter() {
tips.remove(parent);
}
}
tips
}
/// Get the current set of unreferenced tips in the DAG.
async fn get_unreferenced_tips(&self) -> [blake3::Hash; N_EVENT_PARENTS] {
let unreferenced_tips = self.unreferenced_tips.read().await;
let mut tips = [NULL_ID; N_EVENT_PARENTS];
for (i, tip) in unreferenced_tips.iter().take(N_EVENT_PARENTS).enumerate() {
tips[i] = *tip
}
assert!(tips.iter().any(|x| x != &NULL_ID));
tips
}
/// Internal function used for DAG sorting.
async fn get_unreferenced_tips_sorted(&self) -> [blake3::Hash; N_EVENT_PARENTS] {
let tips = self.get_unreferenced_tips().await;
// Convert the hash to BigUint for sorting
let mut sorted: Vec<_> =
tips.iter().map(|x| BigUint::from_bytes_be(x.as_bytes())).collect();
sorted.sort_unstable();
// Convert back to blake3
let mut tips_sorted = [NULL_ID; N_EVENT_PARENTS];
for (i, id) in sorted.iter().enumerate() {
let mut bytes = id.to_bytes_be();
// Ensure we have 32 bytes
while bytes.len() < blake3::OUT_LEN {
bytes.insert(0, 0);
}
tips_sorted[i] = blake3::Hash::from_bytes(bytes.try_into().unwrap());
}
tips_sorted
}
/// Perform a topological sort of the DAG.
pub async fn order_events(&self) -> Vec {
let mut ordered_events = VecDeque::new();
let mut visited = HashSet::new();
for tip in self.get_unreferenced_tips_sorted().await {
if !visited.contains(&tip) && tip != NULL_ID {
let tip = self.dag.get(tip.as_bytes()).unwrap().unwrap();
let tip = deserialize_async(&tip).await.unwrap();
self.dfs_topological_sort(tip, &mut visited, &mut ordered_events).await;
}
}
ordered_events.make_contiguous().to_vec()
}
/// We do a DFS (), and
/// additionally we consider the timestamps.
#[async_recursion]
async fn dfs_topological_sort(
&self,
event: Event,
visited: &mut HashSet,
ordered_events: &mut VecDeque,
) {
let event_id = event.id();
visited.insert(event_id);
for parent_id in event.parents.iter() {
if !visited.contains(parent_id) && parent_id != &NULL_ID {
let p_event = self.dag.get(parent_id.as_bytes()).unwrap().unwrap();
let p_event = deserialize_async(&p_event).await.unwrap();
self.dfs_topological_sort(p_event, visited, ordered_events).await;
}
}
// Before inserting, check timestamps to determine the correct position.
let mut pos = ordered_events.len();
for (idx, existing_id) in ordered_events.iter().enumerate().rev() {
assert!(existing_id != &NULL_ID);
if self.share_same_parents(&event_id, existing_id).await {
let existing_event = self.dag.get(existing_id.as_bytes()).unwrap().unwrap();
let existing_event: Event = deserialize_async(&existing_event).await.unwrap();
// Sort by timestamp
match event.timestamp.cmp(&existing_event.timestamp) {
Ordering::Less => pos = idx,
Ordering::Equal => {
// In case of a tie-breaker, use the event ID
let a = BigUint::from_bytes_be(event_id.as_bytes());
let b = BigUint::from_bytes_be(existing_id.as_bytes());
if a < b {
pos = idx;
}
}
_ => {}
}
}
}
ordered_events.insert(pos, event_id);
}
/// Check if two events have the same parents
async fn share_same_parents(&self, event_id1: &blake3::Hash, event_id2: &blake3::Hash) -> bool {
let event1 = self.dag.get(event_id1.as_bytes()).unwrap().unwrap();
let event1: Event = deserialize_async(&event1).await.unwrap();
let mut parents1: Vec<_> =
event1.parents.iter().map(|x| BigUint::from_bytes_be(x.as_bytes())).collect();
parents1.sort_unstable();
let event2 = self.dag.get(event_id2.as_bytes()).unwrap().unwrap();
let event2: Event = deserialize_async(&event2).await.unwrap();
let mut parents2: Vec<_> =
event2.parents.iter().map(|x| BigUint::from_bytes_be(x.as_bytes())).collect();
parents2.sort_unstable();
parents1 == parents2
}
}