/* 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 .
*/
use std::{cmp::Ordering, collections::HashSet};
use darkfi_serial::{async_trait, deserialize_async, Encodable, SerialDecodable, SerialEncodable};
use kvdb_overlay::{Tree, TreeOverlay};
use super::{
util::{unix_timestamp_millis, HOUR_MS},
EventGraph, EventGraphConfig, EVENT_TIME_DRIFT, NULL_ID, N_EVENT_PARENTS,
};
use crate::Result;
/// The fixed-size structural metadata of an event.
///
/// Headers are lightweight and encode the full DAG topology without
/// carrying the variable-length content. The content is committed
/// to via `content_hash`, so peers can verify the integrity of an
/// event body against the header that announced it.
#[derive(Debug, Clone, PartialEq, SerialEncodable, SerialDecodable)]
pub struct Header {
/// UNIX timestamp of the event in milliseconds.
pub timestamp: u64,
/// Parent references. Unused slots are [`NULL_ID`].
pub parents: [blake3::Hash; N_EVENT_PARENTS],
/// Monotonically increasing layer index.
pub layer: u64,
/// blake3 hash of the event's content payload
pub content_hash: blake3::Hash,
}
impl Header {
pub async fn new(content: &[u8], eg: &EventGraph) -> Result {
let dag_ts = eg.current_genesis.read().await.header.timestamp;
let (layer, parents) = eg.get_next_layer_with_parents(&dag_ts).await?;
Ok(Self {
timestamp: unix_timestamp_millis()?,
parents,
layer,
content_hash: blake3::hash(content),
})
}
pub async fn new_static(content: &[u8], eg: &EventGraph) -> Result {
let (layer, parents) = eg.get_next_layer_with_parents_static().await?;
Ok(Self {
timestamp: unix_timestamp_millis()?,
parents,
layer,
content_hash: blake3::hash(content),
})
}
pub async fn with_timestamp(timestamp: u64, content: &[u8], eg: &EventGraph) -> Result {
let dag_ts = eg.current_genesis.read().await.header.timestamp;
let (layer, parents) = eg.get_next_layer_with_parents(&dag_ts).await?;
Ok(Self { timestamp, parents, layer, content_hash: blake3::hash(content) })
}
/// Blake3 hash of `(timestamp, parents, layer, content_hash)`.
pub fn id(&self) -> blake3::Hash {
let mut h = blake3::Hasher::new();
let _ = self.timestamp.encode(&mut h);
let _ = self.parents.encode(&mut h);
let _ = self.layer.encode(&mut h);
h.update(self.content_hash.as_bytes());
h.finalize()
}
/// Full structural validation against a header DAG.
///
/// `dag_genesis` is the timestamp/name of the target rotating DAG slot.
pub async fn validate(
&self,
header_dag: &Tree,
config: &EventGraphConfig,
dag_genesis: u64,
overlay: Option<&TreeOverlay>,
) -> Result {
if !self.timestamp_fits_slot(config, dag_genesis) {
return Ok(false)
}
let mut seen = HashSet::new();
let mut max_parent_layer = None;
let self_id = self.id();
for pid in self.parents.iter() {
if pid == &NULL_ID {
continue
}
if pid == &self_id || seen.contains(pid) {
return Ok(false)
}
let bytes = if let Some(ov) = overlay {
ov.get(pid.as_bytes())?
} else {
header_dag.get(pid.as_bytes())?
};
let Some(bytes) = bytes else { return Ok(false) };
let parent: Header = deserialize_async(&bytes).await?;
max_parent_layer =
Some(max_parent_layer.map_or(parent.layer, |m: u64| m.max(parent.layer)));
seen.insert(pid);
}
let Some(max_parent_layer) = max_parent_layer else { return Ok(false) };
let Some(expected_layer) = max_parent_layer.checked_add(1) else { return Ok(false) };
Ok(self.layer == expected_layer)
}
/// Check whether this header timestamp belongs to the target DAG slot.
fn timestamp_fits_slot(&self, config: &EventGraphConfig, dag_genesis: u64) -> bool {
if self.timestamp < dag_genesis.saturating_sub(EVENT_TIME_DRIFT) {
return false
}
if config.hours_rotation == 0 {
let Ok(now) = unix_timestamp_millis() else { return false };
return self.timestamp <= now.saturating_add(EVENT_TIME_DRIFT)
}
let Some(rotation_ms) = config.hours_rotation.checked_mul(HOUR_MS) else { return false };
let Some(next_slot) = dag_genesis.checked_add(rotation_ms) else { return false };
let Some(upper_bound) = next_slot.checked_add(EVENT_TIME_DRIFT) else { return false };
self.timestamp < upper_bound
}
}
/// A complete event: [`Header`] + application-defined content.
#[derive(Debug, Clone, PartialEq, SerialEncodable, SerialDecodable)]
pub struct Event {
pub header: Header,
/// Application payload. Must not be empty for non-genesis events.
pub content: Vec,
}
impl Event {
pub async fn new(data: Vec, eg: &EventGraph) -> Result {
let header = Header::new(&data, eg).await?;
Ok(Self { header, content: data })
}
pub async fn new_static(data: Vec, eg: &EventGraph) -> Result {
let header = Header::new_static(&data, eg).await?;
Ok(Self { header, content: data })
}
pub fn id(&self) -> blake3::Hash {
self.header.id()
}
pub async fn with_timestamp(ts: u64, data: Vec, eg: &EventGraph) -> Result {
let header = Header::with_timestamp(ts, &data, eg).await?;
Ok(Self { header, content: data })
}
pub fn content(&self) -> &[u8] {
&self.content
}
/// Check that the content matches the hash committed to in the header.
pub fn content_matches_header(&self) -> bool {
blake3::hash(&self.content) == self.header.content_hash
}
/// Validate for insertion into a DAG.
///
/// `dag_genesis` is the timestamp/name of the target rotating DAG slot.
pub async fn dag_validate(
&self,
hdr_dag: &Tree,
config: &EventGraphConfig,
dag_genesis: u64,
) -> Result {
if self.content.is_empty() {
return Ok(false)
}
if !self.content_matches_header() {
return Ok(false)
}
self.header.validate(hdr_dag, config, dag_genesis, None).await
}
/// Quick validation (no DAG lookup).
pub fn validate_new(&self) -> bool {
if !self.validate_new_common() {
return false
}
let Ok(now) = unix_timestamp_millis() else { return false };
if self.header.timestamp < now.saturating_sub(EVENT_TIME_DRIFT) ||
self.header.timestamp > now.saturating_add(EVENT_TIME_DRIFT)
{
return false
}
true
}
/// Quick validation for static-DAG events.
///
/// Static-DAG events (RLN registrations and slashes) are
/// persistent across rotation windows by design - they form
/// the consensus identity tree and a node syncing for the
/// first time may legitimately receive registrations that are
/// hours, days, or longer old. Rejecting them on a 60-second
/// time-drift window (as `validate_new` does for rotating
/// events, where freshness IS part of the threat model)
/// would prevent any late-joining node from ever syncing
/// historical RLN state.
///
/// This method runs the same structural checks as
/// `validate_new` (non-empty content, content matches header,
/// well-formed parent set) but omits the drift-window check.
/// RLN proof verification of the static event itself happens
/// separately in `EventGraph::rln_verify_static_event`.
pub fn validate_new_static(&self) -> bool {
self.validate_new_common()
}
/// Shared validation between `validate_new` and
/// `validate_new_static`. Returns false if the event is
/// structurally malformed in any time-independent way.
fn validate_new_common(&self) -> bool {
if self.content.is_empty() {
return false
}
if !self.content_matches_header() {
return false
}
let mut seen = HashSet::new();
let sid = self.header.id();
for pid in self.header.parents.iter() {
if pid == &NULL_ID {
continue
}
if pid == &sid || seen.contains(pid) {
return false
}
seen.insert(pid);
}
!seen.is_empty()
}
}
/// Chronological comparator with deterministic hash tie-breaking.
pub fn display_order(a: &Event, b: &Event) -> Ordering {
a.header
.timestamp
.cmp(&b.header.timestamp)
.then_with(|| a.id().as_bytes().cmp(b.id().as_bytes()))
}