/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2026 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 .
*/
//! Timestamp arithmetic, genesis generation, and replay logging.
use std::{
collections::HashMap,
fs::{self, File, OpenOptions},
io::Write,
path::Path,
time::UNIX_EPOCH,
};
use darkfi_serial::{deserialize, deserialize_async, serialize};
use sled_overlay::sled;
use tinyjson::JsonValue;
use super::{
event::{Event, Header},
EventGraphConfig, NULL_ID, N_EVENT_PARENTS,
};
use crate::{
util::{encoding::base64, file::load_file},
Result,
};
#[cfg(feature = "rpc")]
use crate::rpc::{
jsonrpc::{ErrorCode, JsonError, JsonResponse, JsonResult},
util::json_map,
};
/// Milliseconds in one hour.
pub(super) const HOUR: i64 = 3_600_000;
/// Timestamp (millis) for the start of the hour `hours` offsets from now.
pub(super) fn next_hour_timestamp(hours: i64) -> u64 {
let now = UNIX_EPOCH.elapsed().unwrap().as_millis() as i64;
((now / HOUR) * HOUR + HOUR * hours) as u64
}
/// Whole hours elapsed since `ts`.
pub(super) fn hours_since(ts: u64) -> u64 {
let now = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
(now - ts) / HOUR as u64
}
/// Timestamp of the next DAG rotation.
///
/// # Panics
///
/// Panics if `rotation_period` is zero.
pub fn next_rotation_timestamp(starting_timestamp: u64, rotation_period: u64) -> u64 {
if rotation_period == 0 {
panic!("Rotation period cannot be 0");
}
let passed = hours_since(starting_timestamp);
let rotations = passed.div_ceil(rotation_period);
let until: i64 = (rotations * rotation_period - passed).try_into().unwrap();
if until == 0 {
next_hour_timestamp(1)
} else {
next_hour_timestamp(until)
}
}
/// Milliseconds remaining until `next_rotation`.
///
/// # Panics
///
/// Panics if `next_rotation` is in the past.
pub fn millis_until_next_rotation(next_rotation: u64) -> u64 {
let now = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
assert!(next_rotation >= now, "Next rotation is in the past");
next_rotation - now
}
/// Generate the deterministic genesis event for the current rotation
/// period, using the caller-provided [`EventGraphConfig`].
///
/// * `hours_rotation == 0` -> timestamp is `initial_genesis`.
/// * `hours_rotation > 0` -> timestamp is the most recent
/// multiple-of-N boundary since `initial_genesis`.
pub fn generate_genesis(config: &EventGraphConfig) -> Event {
let timestamp = if config.hours_rotation == 0 {
config.initial_genesis
} else {
let passed = hours_since(config.initial_genesis);
let rotations = passed / config.hours_rotation;
config.initial_genesis + (rotations * config.hours_rotation * HOUR as u64)
};
let content_hash = blake3::hash(&config.genesis_contents);
let header = Header { timestamp, parents: [NULL_ID; N_EVENT_PARENTS], layer: 0, content_hash };
Event { header, content: config.genesis_contents.clone() }
}
/// Append a replayer log entry for DAG state recreation.
pub(super) fn replayer_log(datastore: &Path, cmd: String, value: Vec) -> Result<()> {
fs::create_dir_all(datastore)?;
let p = datastore.join("replayer.log");
if !p.exists() {
File::create(&p)?;
}
let mut f = OpenOptions::new().append(true).open(&p)?;
writeln!(f, "{cmd} {}", base64::encode(&value))?;
Ok(())
}
#[cfg(feature = "rpc")]
pub async fn recreate_from_replayer_log(datastore: &Path) -> JsonResult {
let log_path = datastore.join("replayer.log");
if !log_path.exists() {
return JsonResult::Error(JsonError::new(
ErrorCode::ParseError,
Some("Log not found".into()),
1,
))
}
let reader = load_file(&log_path).unwrap();
let sled_db = sled::open(datastore.join("replayed_db")).unwrap();
let dag = sled_db.open_tree("replayer").unwrap();
for line in reader.lines() {
let parts = line.split(' ').collect::>();
if parts[0] == "insert" {
let v: Event = deserialize(&base64::decode(parts[1]).unwrap()).unwrap();
dag.insert(v.header.id().as_bytes(), serialize(&v)).unwrap();
}
}
let mut graph = HashMap::new();
for item in dag.iter() {
let (id, val) = item.unwrap();
let id = blake3::Hash::from_bytes((&id as &[u8]).try_into().unwrap());
graph.insert(id, deserialize_async::(&val).await.unwrap());
}
let json_graph = graph.into_iter().map(|(k, v)| (k.to_string(), JsonValue::from(v))).collect();
let values = json_map([("dag", JsonValue::Object(json_graph))]);
JsonResponse::new(JsonValue::Object(HashMap::from([("eventgraph_info".into(), values)])), 1)
.into()
}
/// Which DAG an event came from. Used by [`event_to_gource`] to
/// pick the appropriate path prefix when formatting visualization
/// output.
#[derive(Copy, Clone, Debug)]
pub enum DagKind {
/// A rotating-DAG event (regular IRC traffic, etc.)
Rotating,
/// A static-DAG event (RLN registration or slash)
Static,
}
impl DagKind {
fn path_prefix(self) -> &'static str {
match self {
DagKind::Rotating => "rotating",
DagKind::Static => "static",
}
}
}
/// Format an [`Event`] as a single Gource custom-log line.
///
/// Output format (Gource custom log, pipe-delimited):
///
/// ```text
/// ||A|///
/// ```
pub fn event_to_gource(ev: &Event, kind: DagKind) -> String {
let unix_secs = ev.header.timestamp / 1_000;
// First non-NULL parent -> 8-char hex prefix; otherwise "genesis".
let username = ev
.header
.parents
.iter()
.find(|p| **p != NULL_ID)
.map(|p| {
let hex = p.to_hex();
hex[..8.min(hex.len())].to_string()
})
.unwrap_or_else(|| "genesis".to_string());
let id_hex = ev.id().to_hex();
let id_prefix = &id_hex[..16.min(id_hex.len())];
format!(
"{}|{}|A|/{}/{:06}/{}",
unix_secs,
username,
kind.path_prefix(),
ev.header.layer,
id_prefix,
)
}