Ver Fonte

event_graph: Bidirectional sync, persistence, minor improvements

x há 3 meses atrás
pai
commit
b22f0caafb
7 ficheiros alterados com 2047 adições e 3524 exclusões
  1. 12 0
      src/event_graph/deg.rs
  2. 116 188
      src/event_graph/event.rs
  3. 633 992
      src/event_graph/mod.rs
  4. 534 757
      src/event_graph/proto.rs
  5. 298 280
      src/event_graph/rln.rs
  6. 390 1138
      src/event_graph/tests.rs
  7. 64 169
      src/event_graph/util.rs

+ 12 - 0
src/event_graph/deg.rs

@@ -16,17 +16,29 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+//! Debug Event Graph (DEG) types for inspecting protocol-level
+//! message flow. When DEG is enabled on an [`EventGraph`] instance,
+//! every sent/received P2P message produces a [`DegEvent`] that can
+//! be observed through the DEG publisher.
+
 use crate::util::time::NanoTimestamp;
 
+/// Metadata attached to a DEG observation.
 #[derive(Clone, Debug)]
 pub struct MessageInfo {
+    /// Human-readable context lines (addresses, event IDs, etc.)
     pub info: Vec<String>,
+    /// The protocol command that was observed (e.g. "EventPut").
     pub cmd: String,
+    /// Wall-clock time when the message was observed.
     pub time: NanoTimestamp,
 }
 
+/// A debug event emitted by the Event Graph protocol handlers.
 #[derive(Clone, Debug)]
 pub enum DegEvent {
+    /// A message was sent to a peer.
     SendMessage(MessageInfo),
+    /// A message was received from a peer.
     RecvMessage(MessageInfo),
 }

+ 116 - 188
src/event_graph/event.rs

@@ -16,7 +16,9 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::{collections::HashSet, time::UNIX_EPOCH};
+//! Core data types: [`Header`], [`Event`], and [`display_order`].
+
+use std::{cmp::Ordering, collections::HashSet, time::UNIX_EPOCH};
 
 use darkfi_serial::{async_trait, deserialize_async, Encodable, SerialDecodable, SerialEncodable};
 use sled_overlay::{sled, SledTreeOverlay};
@@ -24,139 +26,142 @@ use sled_overlay::{sled, SledTreeOverlay};
 use crate::{event_graph::util::generate_genesis, Result};
 
 use super::{
-    util::next_rotation_timestamp, EventGraph, EVENT_TIME_DRIFT, INITIAL_GENESIS, NULL_ID,
+    util::next_rotation_timestamp, EventGraph, EventGraphConfig, EVENT_TIME_DRIFT, NULL_ID,
     N_EVENT_PARENTS,
 };
 
+/// 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 {
-    /// Event version
-    // pub version: u8,
-    /// Timestamp of the event in milliseconds
+    /// UNIX timestamp of the event in milliseconds.
     pub timestamp: u64,
-    /// Parent nodes in the event DAG
+    /// Parent references. Unused slots are [`NULL_ID`].
     pub parents: [blake3::Hash; N_EVENT_PARENTS],
-    /// DAG layer index of the event
+    /// Monotonically increasing layer index.
     pub layer: u64,
+    /// blake3 hash of the event's content payload
+    pub content_hash: blake3::Hash,
 }
 
 impl Header {
-    // Create a new Header given EventGraph to retrieve the correct layout
-    pub async fn new(event_graph: &EventGraph) -> Self {
-        let current_dag_name = event_graph.current_genesis.read().await.header.timestamp;
-        let (layer, parents) = event_graph.get_next_layer_with_parents(&current_dag_name).await;
-        Self { timestamp: UNIX_EPOCH.elapsed().unwrap().as_millis() as u64, parents, layer }
+    pub async fn new(content: &[u8], eg: &EventGraph) -> Self {
+        let dag_ts = eg.current_genesis.read().await.header.timestamp;
+        let (layer, parents) = eg.get_next_layer_with_parents(&dag_ts).await;
+        Self {
+            timestamp: UNIX_EPOCH.elapsed().unwrap().as_millis() as u64,
+            parents,
+            layer,
+            content_hash: blake3::hash(content),
+        }
     }
 
-    pub async fn new_static(event_graph: &EventGraph) -> Self {
-        let (layer, parents) = event_graph.get_next_layer_with_parents_static().await;
-        Self { timestamp: UNIX_EPOCH.elapsed().unwrap().as_millis() as u64, parents, layer }
+    pub async fn new_static(content: &[u8], eg: &EventGraph) -> Self {
+        let (layer, parents) = eg.get_next_layer_with_parents_static().await;
+        Self {
+            timestamp: UNIX_EPOCH.elapsed().unwrap().as_millis() as u64,
+            parents,
+            layer,
+            content_hash: blake3::hash(content),
+        }
     }
 
-    pub async fn with_timestamp(timestamp: u64, event_graph: &EventGraph) -> Self {
-        let current_dag_name = event_graph.current_genesis.read().await.header.timestamp;
-        let (layer, parents) = event_graph.get_next_layer_with_parents(&current_dag_name).await;
-        Self { timestamp, parents, layer }
+    pub async fn with_timestamp(timestamp: u64, content: &[u8], eg: &EventGraph) -> Self {
+        let dag_ts = eg.current_genesis.read().await.header.timestamp;
+        let (layer, parents) = eg.get_next_layer_with_parents(&dag_ts).await;
+        Self { timestamp, parents, layer, content_hash: blake3::hash(content) }
     }
 
-    /// Hash the [`Header`] to retrieve its ID
+    /// Blake3 hash of `(timestamp, parents, layer, content_hash)`.
     pub fn id(&self) -> blake3::Hash {
-        let mut hasher = blake3::Hasher::new();
-        self.timestamp.encode(&mut hasher).unwrap();
-        self.parents.encode(&mut hasher).unwrap();
-        self.layer.encode(&mut hasher).unwrap();
-        hasher.finalize()
+        let mut h = blake3::Hasher::new();
+        self.timestamp.encode(&mut h).unwrap();
+        self.parents.encode(&mut h).unwrap();
+        self.layer.encode(&mut h).unwrap();
+        h.update(self.content_hash.as_bytes());
+        h.finalize()
     }
 
-    /// Fully validate a header for the correct layout against provided
-    /// DAG [`sled::Tree`] reference and enforce relevant age, assuming
-    /// some possibility for a time drift. Optionally, provide an overlay
-    /// to use that instead of actual referenced DAG.
+    /// Full structural validation against a header DAG.
     pub async fn validate(
         &self,
         header_dag: &sled::Tree,
-        hours_rotation: u64,
+        config: &EventGraphConfig,
         overlay: Option<&SledTreeOverlay>,
     ) -> Result<bool> {
-        // Check if the event is not older than the oldest genesis
-        let genesis_timestamp = generate_genesis(1).header.timestamp;
-        // A day ago genesis same hour
-        let oldest_genesis_ts = genesis_timestamp - 86_400_000u64;
-        if self.timestamp < oldest_genesis_ts - EVENT_TIME_DRIFT {
+        // Lower bound: one day before the most recent hourly genesis.
+        // We build a temporary 1-hour config just to compute the
+        // reference timestamp.
+        let hourly_cfg = EventGraphConfig {
+            initial_genesis: config.initial_genesis,
+            hours_rotation: 1,
+            genesis_contents: config.genesis_contents.clone(),
+            max_dags: config.max_dags,
+        };
+
+        let oldest_allowed = generate_genesis(&hourly_cfg).header.timestamp - 86_400_000;
+
+        if self.timestamp < oldest_allowed - EVENT_TIME_DRIFT {
             return Ok(false)
         }
 
-        // If a rotation has been set, check if the event timestamp
-        // is after the next genesis timestamp
-        if hours_rotation > 0 {
-            let next_genesis_timestamp = next_rotation_timestamp(INITIAL_GENESIS, hours_rotation);
-            if self.timestamp > next_genesis_timestamp + EVENT_TIME_DRIFT {
+        // Upper bound: next rotation boundary + drift
+        if config.hours_rotation > 0 {
+            let next = next_rotation_timestamp(config.initial_genesis, config.hours_rotation);
+            if self.timestamp > next + EVENT_TIME_DRIFT {
                 return Ok(false)
             }
         }
 
-        // Validate the parents. We have to check that at least one parent
-        // is not NULL, that the parents exist, that no two parents are the
-        // same, and that the parent exists in previous layers, to prevent
-        // recursive references(circles).
         let mut seen = HashSet::new();
         let self_id = self.id();
-
-        for parent_id in self.parents.iter() {
-            if parent_id == &NULL_ID {
+        for pid in self.parents.iter() {
+            if pid == &NULL_ID {
                 continue
             }
 
-            if parent_id == &self_id {
-                return Ok(false)
-            }
-
-            if seen.contains(parent_id) {
+            if pid == &self_id || seen.contains(pid) {
                 return Ok(false)
             }
 
-            let parent_bytes = if let Some(overlay) = overlay {
-                overlay.get(parent_id.as_bytes())?
+            let bytes = if let Some(ov) = overlay {
+                ov.get(pid.as_bytes())?
             } else {
-                header_dag.get(parent_id.as_bytes())?
+                header_dag.get(pid.as_bytes())?
             };
-            if parent_bytes.is_none() {
-                return Ok(false)
-            }
 
-            let parent: Header = deserialize_async(&parent_bytes.unwrap()).await?;
+            let Some(bytes) = bytes else { return Ok(false) };
+            let parent: Header = deserialize_async(&bytes).await?;
             if self.layer <= parent.layer {
                 return Ok(false)
             }
-
-            seen.insert(parent_id);
+            seen.insert(pid);
         }
 
         Ok(!seen.is_empty())
     }
 }
 
-/// Representation of an event in the Event Graph
+/// A complete event: [`Header`] + application-defined content.
 #[derive(Debug, Clone, PartialEq, SerialEncodable, SerialDecodable)]
 pub struct Event {
     pub header: Header,
-    /// Content of the event
+    /// Application payload. Must not be empty for non-genesis events.
     pub content: Vec<u8>,
 }
 
 impl Event {
-    /// Create a new event with the given data and an [`EventGraph`] reference.
-    /// The timestamp of the event will be the current time, and the parents
-    /// will be `N_EVENT_PARENTS` from the current event graph unreferenced tips.
-    /// The parents can also include NULL, but this should be handled by the rest
-    /// of the codebase.
-    pub async fn new(data: Vec<u8>, event_graph: &EventGraph) -> Self {
-        let header = Header::new(event_graph).await;
+    pub async fn new(data: Vec<u8>, eg: &EventGraph) -> Self {
+        let header = Header::new(&data, eg).await;
         Self { header, content: data }
     }
 
-    pub async fn new_static(data: Vec<u8>, event_graph: &EventGraph) -> Self {
-        let header = Header::new_static(event_graph).await;
+    pub async fn new_static(data: Vec<u8>, eg: &EventGraph) -> Self {
+        let header = Header::new_static(&data, eg).await;
         Self { header, content: data }
     }
 
@@ -164,152 +169,75 @@ impl Event {
         self.header.id()
     }
 
-    /// Same as `Event::new()` but allows specifying the timestamp explicitly.
-    pub async fn with_timestamp(timestamp: u64, data: Vec<u8>, event_graph: &EventGraph) -> Self {
-        let header = Header::with_timestamp(timestamp, event_graph).await;
+    pub async fn with_timestamp(ts: u64, data: Vec<u8>, eg: &EventGraph) -> Self {
+        let header = Header::with_timestamp(ts, &data, eg).await;
         Self { header, content: data }
     }
 
-    /// Return a reference to the event's content
     pub fn content(&self) -> &[u8] {
         &self.content
     }
 
-    /// Fully validate an event for the correct layout against provided
-    /// [`EventGraph`] reference and enforce relevant age, assuming some
-    /// possibility for a time drift.
-    pub async fn dag_validate(&self, header_dag: &sled::Tree) -> Result<bool> {
+    /// 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.
+    pub async fn dag_validate(
+        &self,
+        hdr_dag: &sled::Tree,
+        config: &EventGraphConfig,
+    ) -> Result<bool> {
         if self.content.is_empty() {
             return Ok(false)
         }
-        // Perform validation
-        self.header.validate(header_dag, 1, None).await
+
+        if !self.content_matches_header() {
+            return Ok(false)
+        }
+
+        self.header.validate(hdr_dag, config, None).await
     }
 
-    /// Validate a new event for the correct layout and enforce relevant age,
-    /// assuming some possibility for a time drift.
-    /// Note: This validation does *NOT* check for recursive references(circles),
-    /// and should be used as a first quick check.
+    /// Quick validation (no DAG lookup).
     pub fn validate_new(&self) -> bool {
-        // Let's not bother with empty events
         if self.content.is_empty() {
             return false
         }
 
-        // Check if the event is too old or too new
+        if !self.content_matches_header() {
+            return false
+        }
+
         let now = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
-        let too_old = self.header.timestamp < now - EVENT_TIME_DRIFT;
-        let too_new = self.header.timestamp > now + EVENT_TIME_DRIFT;
-        if too_old || too_new {
+
+        if self.header.timestamp < now - EVENT_TIME_DRIFT ||
+            self.header.timestamp > now + EVENT_TIME_DRIFT
+        {
             return false
         }
 
-        // Validate the parents. We have to check that at least one parent
-        // is not NULL and that no two parents are the same.
         let mut seen = HashSet::new();
-        let self_id = self.header.id();
-
-        for parent_id in self.header.parents.iter() {
-            if parent_id == &NULL_ID {
+        let sid = self.header.id();
+        for pid in self.header.parents.iter() {
+            if pid == &NULL_ID {
                 continue
             }
-
-            if parent_id == &self_id {
-                return false
-            }
-
-            if seen.contains(parent_id) {
+            if pid == &sid || seen.contains(pid) {
                 return false
             }
-
-            seen.insert(parent_id);
+            seen.insert(pid);
         }
 
         !seen.is_empty()
     }
 }
 
-#[cfg(test)]
-mod tests {
-    use std::sync::Arc;
-
-    use smol::Executor;
-
-    use crate::{
-        event_graph::{EventGraph, EventGraphPtr},
-        net::{P2p, Settings},
-    };
-
-    use super::*;
-
-    async fn make_event_graph() -> Result<EventGraphPtr> {
-        let ex = Arc::new(Executor::new());
-        let p2p = P2p::new(Settings::default(), ex.clone()).await?;
-        let sled_db = sled::Config::new().temporary(true).open().unwrap();
-        EventGraph::new(p2p, sled_db, "/tmp".into(), false, false, 1, ex).await
-    }
-
-    #[test]
-    fn event_is_valid() -> Result<()> {
-        smol::block_on(async {
-            // Generate a dummy event graph
-            let event_graph = make_event_graph().await?;
-
-            let dag_name = event_graph.current_genesis.read().await.header.timestamp.to_string();
-            let hdr_tree_name = format!("headers_{dag_name}");
-            let header_dag = event_graph.dag_store.read().await.get_dag(&hdr_tree_name);
-
-            // Create a new valid event
-            let valid_event = Event::new(vec![1u8], &event_graph).await;
-
-            // Validate our test Event struct
-            assert!(valid_event.dag_validate(&header_dag).await?);
-
-            // Thanks for reading
-            Ok(())
-        })
-    }
-
-    #[test]
-    fn invalid_events() -> Result<()> {
-        smol::block_on(async {
-            // Generate a dummy event graph
-            let event_graph = make_event_graph().await?;
-
-            let dag_name = event_graph.current_genesis.read().await.header.timestamp.to_string();
-            let hdr_tree_name = format!("headers_{dag_name}");
-            let header_dag = event_graph.dag_store.read().await.get_dag(&hdr_tree_name);
-
-            // Create a new valid event
-            let valid_event = Event::new(vec![1u8], &event_graph).await;
-
-            let mut event_empty_content = valid_event.clone();
-            event_empty_content.content = vec![];
-            assert!(!event_empty_content.dag_validate(&header_dag).await?);
-
-            let mut event_timestamp_too_old = valid_event.clone();
-            event_timestamp_too_old.header.timestamp = 1000;
-            assert!(!event_timestamp_too_old.dag_validate(&header_dag).await?);
-
-            let mut event_timestamp_too_new = valid_event.clone();
-            event_timestamp_too_new.header.timestamp = u64::MAX;
-            assert!(!event_timestamp_too_new.dag_validate(&header_dag).await?);
-
-            let mut event_duplicated_parents = valid_event.clone();
-            event_duplicated_parents.header.parents[1] = valid_event.header.parents[0];
-            assert!(!event_duplicated_parents.dag_validate(&header_dag).await?);
-
-            let mut event_null_parents = valid_event.clone();
-            let all_null_parents = [NULL_ID, NULL_ID, NULL_ID, NULL_ID, NULL_ID];
-            event_null_parents.header.parents = all_null_parents;
-            assert!(!event_null_parents.dag_validate(&header_dag).await?);
-
-            let mut event_same_layer_as_parents = valid_event.clone();
-            event_same_layer_as_parents.header.layer = 0;
-            assert!(!event_same_layer_as_parents.dag_validate(&header_dag).await?);
-
-            // Thanks for reading
-            Ok(())
-        })
-    }
+/// 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()))
 }

Diff do ficheiro suprimidas por serem muito extensas
+ 633 - 992
src/event_graph/mod.rs


+ 534 - 757
src/event_graph/proto.rs

@@ -16,6 +16,12 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+//! P2P protocol handlers for the Event Graph.
+//!
+//! Each peer connection spawns a [`ProtocolEventGraph`] instance that
+//! manages message subscriptions and handles incoming events, sync
+//! requests, and bidirectional range queries.
+
 use std::{
     collections::{BTreeMap, HashSet, VecDeque},
     slice,
@@ -26,23 +32,20 @@ use std::{
     },
 };
 
-use darkfi_sdk::{
-    crypto::{poseidon_hash, util::FieldElemAsStr},
-    pasta::pallas,
-};
+use darkfi_sdk::{crypto::poseidon_hash, pasta::pallas};
 use darkfi_serial::{
-    async_trait, deserialize_async_partial, serialize_async, SerialDecodable, SerialEncodable,
+    async_trait, deserialize_async_partial, serialize_async, FutAsyncWriteExt, SerialDecodable,
+    SerialEncodable,
 };
 use smol::Executor;
-use tracing::{debug, error, info, trace, warn};
+use tracing::{error, warn};
 
 use super::{
     event::Header,
-    rln::{closest_epoch, create_slash_proof, hash_event, sss_recover, MessageMetadata, RLNNode},
+    rln::{closest_epoch, create_slash_proof, hash_event, sss_recover, Blob, RLNNode, RlnState},
     Event, EventGraphPtr, LayerUTips, NULL_ID,
 };
 use crate::{
-    event_graph::rln::{read_register_vk, read_signal_vk, read_slash_pk, read_slash_vk, Blob},
     impl_p2p_message,
     net::{
         metering::{MeteringConfiguration, DEFAULT_METERING_CONFIGURATION},
@@ -55,142 +58,217 @@ use crate::{
     Error, Result,
 };
 
-/// Malicious behaviour threshold. If the threshold is reached, we will
-/// drop the peer from our P2P connection.
+/// After this many malicious-looking messages from a single peer, we
+/// drop the connection.
 const MALICIOUS_THRESHOLD: usize = 5;
 
-/// Global limit of messages per window
+/// If a peer sends more than this many unique events within
+/// [`WINDOW_EXPIRY_TIME`], we ban them.
 const WINDOW_MAXSIZE: usize = 200;
-/// Rolling length of the window
+
+/// Rolling window length for the flood-detection counter.
 const WINDOW_EXPIRY_TIME: NanoTimestamp = NanoTimestamp::from_secs(60);
 
-/// Rolling length of the window
+/// Rolling window length for the outbound broadcast rate limiter.
 const RATELIMIT_EXPIRY_TIME: NanoTimestamp = NanoTimestamp::from_secs(10);
-/// Ratelimit kicks in above this count
+/// Rate limiter activates above this many broadcasts in the window.
 const RATELIMIT_MIN_COUNT: usize = 6;
-/// Sample point used to calculate sleep time when ratelimit is active
+/// Reference point for computing sleep time: when count = this value…
 const RATELIMIT_SAMPLE_IDX: usize = 10;
-/// Sleep for this amount of time when `count == RATE_LIMIT_SAMPLE_IDX`.
+/// Sleep this many milliseconds before broadcasting.
 const RATELIMIT_SAMPLE_SLEEP: usize = 1000;
 
+/// Maximum number of recursive round-trips when fetching missing
+/// parent events from a peer during `handle_event_put`.
+///
+/// # Why this limit exists
+///
+/// When we receive a new event whose parents we don't have, we ask
+/// the sender for them. Those parents may themselves reference unknown
+/// grandparents, so we ask again, and so on. A malicious peer can exploit
+/// this by fabricating an arbitrarily deep chain, forcing us into an
+/// unbounded loop of network requests.
+///
+/// # What happens when the limit is hit
+///
+/// The event (and its unresolvable ancestry) is dropped, and the
+/// peer's malicious counter is incremented. This is safe because:
+///
+/// * **Legitimate DAGs** rarely reach this depth. With 5 parents
+///   per event and concurrent users, cross-references keep the
+///   effective depth well below 1000.
+/// * **The header-sync path** (`dag_sync`) is unaffected - it
+///   fetches all headers in bulk by layer, with no recursion.
+///   A node that's 1000+ layers behind should be using `dag_sync`
+///   rather than relying on `EventPut` catch-up.
+/// * **After a full sync**, subsequent `EventPut` events will
+///   typically reference parents that are already known, so the
+///   depth stays near 1.
+const MAX_PARENT_FETCH_DEPTH: usize = 1000;
+
+/// Capacity of the bounded broadcast channel. When the channel is
+/// full, new relay events are dropped rather than blocking the
+/// event processing loop - this provides backpressure and prevents
+/// unbounded memory growth under sustained load.
+const BROADCASTER_CAPACITY: usize = 256;
+
 struct MovingWindow {
     times: VecDeque<NanoTimestamp>,
     expiry_time: NanoTimestamp,
 }
 
 impl MovingWindow {
-    fn new(expiry_time: NanoTimestamp) -> Self {
-        Self { times: VecDeque::new(), expiry_time }
+    fn new(expiry: NanoTimestamp) -> Self {
+        Self { times: VecDeque::new(), expiry_time: expiry }
     }
 
-    /// Clean out expired timestamps from the window.
     fn clean(&mut self) {
         while let Some(ts) = self.times.front() {
-            let Ok(elapsed) = ts.elapsed() else {
-                debug!(target: "event_graph::protocol::MovingWindow::clean", "Timestamp [{ts}] is in future. Removing...");
-                let _ = self.times.pop_front();
-                continue
-            };
-            if elapsed < self.expiry_time {
-                break
+            match ts.elapsed() {
+                Ok(elapsed) if elapsed >= self.expiry_time => {
+                    self.times.pop_front();
+                }
+                Err(_) => {
+                    self.times.pop_front();
+                } // future timestamp — remove
+                _ => break,
             }
-            let _ = self.times.pop_front();
         }
     }
 
-    /// Add new timestamp
     fn ticktock(&mut self) {
         self.clean();
         self.times.push_back(NanoTimestamp::current_time());
     }
 
-    #[inline]
     fn count(&self) -> usize {
         self.times.len()
     }
 }
 
-/// P2P protocol implementation for the Event Graph.
-pub struct ProtocolEventGraph {
-    /// Pointer to the connected peer
-    channel: ChannelPtr,
-    /// Pointer to the Event Graph instance
-    event_graph: EventGraphPtr,
-    /// `MessageSubscriber` for `EventPut`
-    ev_put_sub: MessageSubscription<EventPut>,
-    /// `MessageSubscriber` for `StaticPut`
-    st_put_sub: MessageSubscription<StaticPut>,
-    /// `MessageSubscriber` for `EventReq`
-    ev_req_sub: MessageSubscription<EventReq>,
-    /// `MessageSubscriber` for `EventRep`
-    ev_rep_sub: MessageSubscription<EventRep>,
-    /// `MessageSubscriber` for `HeaderPut`
-    _hdr_put_sub: MessageSubscription<HeaderPut>,
-    /// `MessageSubscriber` for `HeaderReq`
-    hdr_req_sub: MessageSubscription<HeaderReq>,
-    /// `MessageSubscriber` for `HeaderRep`
-    _hdr_rep_sub: MessageSubscription<HeaderRep>,
-    /// `MessageSubscriber` for `TipReq`
-    tip_req_sub: MessageSubscription<TipReq>,
-    /// `MessageSubscriber` for `TipRep`
-    _tip_rep_sub: MessageSubscription<TipRep>,
-    /// Peer malicious message count
-    malicious_count: AtomicUsize,
-    /// P2P jobs manager pointer
-    jobsman: ProtocolJobsManagerPtr,
-    /// To apply the rate-limit, we don't broadcast directly but instead send into the
-    /// sending queue.
-    broadcaster_push: smol::channel::Sender<EventPut>,
-    /// Receive send requests and rate-limit broadcasting them.
-    broadcaster_pull: smol::channel::Receiver<EventPut>,
-}
-
-/// A P2P message representing publishing an event on the network
+/// Broadcast a new event (header + content + optional RLN blob).
 #[derive(Clone, SerialEncodable, SerialDecodable)]
 pub struct EventPut(pub Event, pub Vec<u8>);
 impl_p2p_message!(EventPut, "EventGraph::EventPut", 0, 0, DEFAULT_METERING_CONFIGURATION);
 
-/// A P2P message representing publishing an event of a static graph
-/// (most likely RLN_identities) on the network
+/// Broadcast a static-DAG event (RLN registration / slashing).
 #[derive(Clone, SerialEncodable, SerialDecodable)]
 pub struct StaticPut(pub Event, pub Vec<u8>);
 impl_p2p_message!(StaticPut, "EventGraph::StaticPut", 0, 0, DEFAULT_METERING_CONFIGURATION);
 
-/// A P2P message representing an event request
+/// Request full events by their IDs.
 #[derive(Clone, SerialEncodable, SerialDecodable)]
 pub struct EventReq(pub Vec<blake3::Hash>);
 impl_p2p_message!(EventReq, "EventGraph::EventReq", 0, 0, DEFAULT_METERING_CONFIGURATION);
 
-/// A P2P message representing an event reply
+/// Reply with full events.
 #[derive(Clone, SerialEncodable, SerialDecodable)]
 pub struct EventRep(pub Vec<Event>);
 impl_p2p_message!(EventRep, "EventGraph::EventRep", 0, 0, DEFAULT_METERING_CONFIGURATION);
 
-/// A P2P message representing publishing an event's header on the network
+/// Broadcast a single header (unused in current flow, reserved).
 #[derive(Clone, SerialEncodable, SerialDecodable)]
 pub struct HeaderPut(pub Header);
 impl_p2p_message!(HeaderPut, "EventGraph::HeaderPut", 0, 0, DEFAULT_METERING_CONFIGURATION);
 
-/// A P2P message representing a header request
+/// Request headers that the peer has but we don't, given our tips.
 #[derive(Clone, SerialEncodable, SerialDecodable)]
 pub struct HeaderReq(pub String, pub LayerUTips);
 impl_p2p_message!(HeaderReq, "EventGraph::HeaderReq", 0, 0, DEFAULT_METERING_CONFIGURATION);
 
-/// A P2P message representing a header reply
+/// Reply with headers.
 #[derive(Clone, SerialEncodable, SerialDecodable)]
 pub struct HeaderRep(pub Vec<Header>);
 impl_p2p_message!(HeaderRep, "EventGraph::HeaderRep", 0, 0, DEFAULT_METERING_CONFIGURATION);
 
-/// A P2P message representing a request for a peer's DAG tips
+/// Request a peer's current unreferenced tips for a DAG.
 #[derive(Clone, SerialEncodable, SerialDecodable)]
 pub struct TipReq(pub String);
 impl_p2p_message!(TipReq, "EventGraph::TipReq", 0, 0, DEFAULT_METERING_CONFIGURATION);
 
-/// A P2P message representing a reply for the peer's DAG tips
+/// Reply with unreferenced tips.
 #[derive(Clone, SerialEncodable, SerialDecodable)]
 pub struct TipRep(pub LayerUTips);
 impl_p2p_message!(TipRep, "EventGraph::TipRep", 0, 0, DEFAULT_METERING_CONFIGURATION);
 
+/// Pagination direction for [`RangeReq`].
+#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
+pub enum SyncDirection {
+    /// Ascending timestamps (older -> newer).
+    /// Used for catching up from a known position.
+    Forward,
+    /// Descending timestamps (newer -> older).
+    /// Used for loading the latest messages first and scrolling backward.
+    Backward,
+}
+
+/// Bidirectional content pagination request.
+///
+/// The responder uses its [`TimeIndex`] to serve events around
+/// `cursor_ts` in the requested direction, up to `limit` events.
+/// This is the primary message for lazy content fetching - the
+/// requester already has headers (DAG structure) and wants bodies.
+#[derive(Clone, SerialEncodable, SerialDecodable)]
+pub struct RangeReq {
+    /// Which DAG to query (genesis timestamp as string).
+    pub dag_name: String,
+    /// Timestamp cursor. Use `u64::MAX` for "start from newest"
+    /// or `0` for "start from oldest".
+    pub cursor_ts: u64,
+    /// Which direction to paginate.
+    pub direction: SyncDirection,
+    /// Maximum number of events to return.
+    pub limit: u32,
+}
+impl_p2p_message!(RangeReq, "EventGraph::RangeReq", 0, 0, DEFAULT_METERING_CONFIGURATION);
+
+/// Reply to a [`RangeReq`] with events in the requested order.
+#[derive(Clone, SerialEncodable, SerialDecodable)]
+pub struct RangeRep(pub Vec<Event>);
+impl_p2p_message!(RangeRep, "EventGraph::RangeRep", 0, 0, DEFAULT_METERING_CONFIGURATION);
+
+/// Per-connection protocol handler for the Event Graph.
+///
+/// One instance is created for each peer connection. It subscribes
+/// to all Event Graph P2P message types and spawns async tasks for:
+///
+/// * `handle_event_put` - real-time ingestion of new events,
+///   including RLN proof verification and recursive parent fetching
+///   (bounded by [`MAX_PARENT_FETCH_DEPTH`]).
+/// * `handle_static_put` - RLN registration and slashing events.
+/// * `handle_event_req` - serving event content to peers (only
+///   for IDs we've previously broadcast, to prevent DAG enumeration).
+/// * `handle_header_req` - serving headers the peer is missing.
+/// * `handle_tip_req` - serving our unreferenced tips.
+/// * `handle_range_req` - serving bidirectional paginated content
+///   (the primary mechanism for lazy content fetching).
+/// * `broadcast_rate_limiter` - rate-limiting outbound event
+///   relay through a bounded channel with adaptive sleep.
+///
+/// RLN share metadata is **not** stored on this struct - it lives on
+/// [`EventGraph::rln_state`] so that duplicate/reuse detection works
+/// across all peer connections, not just the one that relayed a
+/// particular event.
+pub struct ProtocolEventGraph {
+    channel: ChannelPtr,
+    event_graph: EventGraphPtr,
+    ev_put_sub: MessageSubscription<EventPut>,
+    st_put_sub: MessageSubscription<StaticPut>,
+    ev_req_sub: MessageSubscription<EventReq>,
+    ev_rep_sub: MessageSubscription<EventRep>,
+    _hdr_put_sub: MessageSubscription<HeaderPut>,
+    hdr_req_sub: MessageSubscription<HeaderReq>,
+    _hdr_rep_sub: MessageSubscription<HeaderRep>,
+    tip_req_sub: MessageSubscription<TipReq>,
+    _tip_rep_sub: MessageSubscription<TipRep>,
+    range_req_sub: MessageSubscription<RangeReq>,
+    _range_rep_sub: MessageSubscription<RangeRep>,
+    malicious_count: AtomicUsize,
+    jobsman: ProtocolJobsManagerPtr,
+    broadcaster_push: smol::channel::Sender<EventPut>,
+    broadcaster_pull: smol::channel::Receiver<EventPut>,
+}
+
 #[async_trait]
 impl ProtocolBase for ProtocolEventGraph {
     async fn start(self: Arc<Self>, ex: Arc<Executor<'_>>) -> Result<()> {
@@ -198,10 +276,9 @@ impl ProtocolBase for ProtocolEventGraph {
         self.jobsman.clone().spawn(self.clone().handle_event_put(), ex.clone()).await;
         self.jobsman.clone().spawn(self.clone().handle_static_put(), ex.clone()).await;
         self.jobsman.clone().spawn(self.clone().handle_event_req(), ex.clone()).await;
-        // self.jobsman.clone().spawn(self.clone().handle_header_put(), ex.clone()).await;
-        // self.jobsman.clone().spawn(self.clone().handle_header_req(), ex.clone()).await;
         self.jobsman.clone().spawn(self.clone().handle_header_req(), ex.clone()).await;
         self.jobsman.clone().spawn(self.clone().handle_tip_req(), ex.clone()).await;
+        self.jobsman.clone().spawn(self.clone().handle_range_req(), ex.clone()).await;
         self.jobsman.clone().spawn(self.clone().broadcast_rate_limiter(), ex.clone()).await;
         Ok(())
     }
@@ -212,7 +289,8 @@ impl ProtocolBase for ProtocolEventGraph {
 }
 
 impl ProtocolEventGraph {
-    pub async fn init(event_graph: EventGraphPtr, channel: ChannelPtr) -> Result<ProtocolBasePtr> {
+    /// Register message dispatchers and subscribe to all channels.
+    pub async fn init(eg: EventGraphPtr, channel: ChannelPtr) -> Result<ProtocolBasePtr> {
         let msg_subsystem = channel.message_subsystem();
         msg_subsystem.add_dispatch::<EventPut>().await;
         msg_subsystem.add_dispatch::<StaticPut>().await;
@@ -223,44 +301,39 @@ impl ProtocolEventGraph {
         msg_subsystem.add_dispatch::<HeaderRep>().await;
         msg_subsystem.add_dispatch::<TipReq>().await;
         msg_subsystem.add_dispatch::<TipRep>().await;
+        msg_subsystem.add_dispatch::<RangeReq>().await;
+        msg_subsystem.add_dispatch::<RangeRep>().await;
 
-        let ev_put_sub = channel.subscribe_msg::<EventPut>().await?;
-        let st_put_sub = channel.subscribe_msg::<StaticPut>().await?;
-        let ev_req_sub = channel.subscribe_msg::<EventReq>().await?;
-        let ev_rep_sub = channel.subscribe_msg::<EventRep>().await?;
-        let _hdr_put_sub = channel.subscribe_msg::<HeaderPut>().await?;
-        let hdr_req_sub = channel.subscribe_msg::<HeaderReq>().await?;
-        let _hdr_rep_sub = channel.subscribe_msg::<HeaderRep>().await?;
-        let tip_req_sub = channel.subscribe_msg::<TipReq>().await?;
-        let _tip_rep_sub = channel.subscribe_msg::<TipRep>().await?;
-
-        let (broadcaster_push, broadcaster_pull) = smol::channel::unbounded();
+        let (push, pull) = smol::channel::bounded(BROADCASTER_CAPACITY);
 
         Ok(Arc::new(Self {
             channel: channel.clone(),
-            event_graph,
-            ev_put_sub,
-            st_put_sub,
-            ev_req_sub,
-            ev_rep_sub,
-            _hdr_put_sub,
-            hdr_req_sub,
-            _hdr_rep_sub,
-            tip_req_sub,
-            _tip_rep_sub,
+            event_graph: eg,
+            ev_put_sub: channel.subscribe_msg().await?,
+            st_put_sub: channel.subscribe_msg().await?,
+            ev_req_sub: channel.subscribe_msg().await?,
+            ev_rep_sub: channel.subscribe_msg().await?,
+            _hdr_put_sub: channel.subscribe_msg().await?,
+            hdr_req_sub: channel.subscribe_msg().await?,
+            _hdr_rep_sub: channel.subscribe_msg().await?,
+            tip_req_sub: channel.subscribe_msg().await?,
+            _tip_rep_sub: channel.subscribe_msg().await?,
+            range_req_sub: channel.subscribe_msg().await?,
+            _range_rep_sub: channel.subscribe_msg().await?,
             malicious_count: AtomicUsize::new(0),
-            jobsman: ProtocolJobsManager::new("ProtocolEventGraph", channel.clone()),
-            broadcaster_push,
-            broadcaster_pull,
+            jobsman: ProtocolJobsManager::new("ProtocolEventGraph", channel),
+            broadcaster_push: push,
+            broadcaster_pull: pull,
         }))
     }
 
-    async fn increase_malicious_count(self: Arc<Self>) -> Result<()> {
-        let malicious_count = self.malicious_count.fetch_add(1, SeqCst);
-        if malicious_count + 1 == MALICIOUS_THRESHOLD {
+    /// Increment the malicious counter; drop peer if threshold reached.
+    async fn strike(self: Arc<Self>) -> Result<()> {
+        let n = self.malicious_count.fetch_add(1, SeqCst);
+        if n + 1 >= MALICIOUS_THRESHOLD {
             error!(
-                target: "event_graph::protocol::handle_event_put",
-                "[EVENTGRAPH] Peer {} reached malicious threshold. Dropping connection.",
+                target: "event_graph::protocol",
+                "[EVENTGRAPH] Peer {} reached malicious threshold",
                 self.channel.display_address(),
             );
             self.channel.stop().await;
@@ -268,814 +341,518 @@ impl ProtocolEventGraph {
         }
 
         warn!(
-            target: "event_graph::protocol::handle_event_put",
-            "[EVENTGRAPH] Peer {} sent us a malicious event", self.channel.display_address(),
+            target: "event_graph::protocol",
+            "[EVENTGRAPH] Peer {} sent malicious data ({}/{})",
+            self.channel.display_address(), n + 1, MALICIOUS_THRESHOLD,
         );
 
         Ok(())
     }
 
-    /// Protocol function handling `EventPut`.
-    /// This is triggered whenever someone broadcasts (or relays) a new
-    /// event on the network.
     async fn handle_event_put(self: Arc<Self>) -> Result<()> {
-        // Rolling window of event timestamps on this channel
         let mut bantimes = MovingWindow::new(WINDOW_EXPIRY_TIME);
-        let mut metadata = MessageMetadata::new();
-        let mut current_epoch = 0;
 
         loop {
             let (event, blob) = match self.ev_put_sub.receive().await {
                 Ok(v) => (v.0.clone(), v.1.clone()),
                 Err(_) => continue,
             };
-            trace!(
-                 target: "event_graph::protocol::handle_event_put",
-                 "Got EventPut: {} [{}]", event.id(), self.channel.display_address(),
-            );
 
-            // Check if node has finished syncing its DAG
-            if !*self.event_graph.synced.read().await {
-                debug!(
-                    target: "event_graph::protocol::handle_event_put",
-                    "DAG is still syncing, skipping..."
-                );
+            if !self.event_graph.is_synced() {
                 continue
             }
 
-            let mut verification_failed = false;
-            #[allow(clippy::never_loop)]
-            loop {
-                if blob.is_empty() {
-                    break
-                }
-                let rcvd_blob: Blob = match deserialize_async_partial(&blob).await {
-                    Ok((v, _)) => v,
-                    Err(e) => {
-                        error!(target: "event_graph::protocol::handle_event_put()","[EVENTGRAPH] Failed deserializing event ephemeral data: {}", e);
-                        break
-                    }
-                };
+            // RLN: verify proof BEFORE recording shares
+            if !blob.is_empty() && self.verify_rln_signal(&event, &blob).await {
+                continue
+            }
 
-                // If the current epoch is different, we reset the stored shares
-                if current_epoch != closest_epoch(event.header.timestamp) {
-                    metadata = MessageMetadata::new()
-                }
+            _ = self.ev_rep_sub.clean().await;
 
-                let rln_app_identifier = pallas::Base::from(1000);
-                current_epoch = closest_epoch(event.header.timestamp);
-                let epoch = pallas::Base::from(current_epoch);
-                let external_nullifier = poseidon_hash([epoch, rln_app_identifier]);
-                let x = hash_event(&event);
-                let identity_root = self.event_graph.rln_identity_tree.read().await.root();
-                let public_inputs = vec![
-                    identity_root,
-                    external_nullifier,
-                    x,
-                    rcvd_blob.y,
-                    rcvd_blob.internal_nullifier,
-                ];
-
-                if metadata.is_duplicate(
-                    &external_nullifier,
-                    &rcvd_blob.internal_nullifier,
-                    &x,
-                    &rcvd_blob.y,
-                ) {
-                    error!(target: "event_graph::protocol::handle_event_put()", "[RLN] Duplicate Message!");
-                    verification_failed = true;
-                    break
-                }
+            // Extract genesis info and immediately release the lock
+            let genesis_ts = self.event_graph.current_genesis.read().await.header.timestamp;
+            let dag_name = genesis_ts.to_string();
+            let eid = event.id();
 
-                if metadata.is_reused(&external_nullifier, &rcvd_blob.internal_nullifier) {
-                    info!(target: "event_graph::protocol::handle_event_put()", "[RLN] Metadata is reused.. slashing..");
-                    let shares =
-                        metadata.get_shares(&external_nullifier, &rcvd_blob.internal_nullifier);
-                    let secret = sss_recover(&shares);
-
-                    // Broadcast slashing event
-                    let slash_pk = read_slash_pk(&self.event_graph.sled_db)?;
-                    // let slash_pk = &self.event_graph.slash_pk;
-                    let mut identity_tree = self.event_graph.rln_identity_tree.write().await;
-
-                    info!("[RLN] Creating slashing proof");
-                    let (proof, identity_root) = match create_slash_proof(
-                        secret,
-                        rcvd_blob.user_msg_limit,
-                        &mut identity_tree,
-                        &slash_pk,
-                    ) {
-                        Ok(v) => v,
-                        Err(e) => {
-                            error!("[RLN] Failed creating RLN slash proof: {}", e);
-                            // Just use an empty "proof"
-                            (Proof::new(vec![]), pallas::Base::from(0))
-                        }
-                    };
-                    drop(identity_tree);
-
-                    let blob =
-                        serialize_async(&(proof, secret, rcvd_blob.user_msg_limit, identity_root))
-                            .await;
-
-                    let evgr = &self.event_graph;
-                    let identity_secret_hash =
-                        poseidon_hash([secret, rcvd_blob.user_msg_limit.into()]);
-                    let identity_commitment = poseidon_hash([identity_secret_hash]);
-                    let rln_commitment = RLNNode::Slashing(identity_commitment);
-                    let st_event =
-                        Event::new_static(serialize_async(&rln_commitment).await, evgr).await;
-                    evgr.static_insert(&st_event).await?;
-                    evgr.static_broadcast(st_event, blob).await?;
-
-                    verification_failed = true;
-                    break
+            // Already known?
+            {
+                let store = self.event_graph.dag_store.read().await;
+                if let Some(slot) = store.get_slot(&genesis_ts) {
+                    if slot.header_tree.contains_key(eid.as_bytes()).unwrap_or(false) {
+                        continue
+                    }
                 }
+            }
 
-                // At this point we can safely add the shares
-                metadata.add_share(
-                    external_nullifier,
-                    rcvd_blob.internal_nullifier,
-                    x,
-                    rcvd_blob.y,
-                )?;
-
-                info!(target: "event_graph::protocol::handle_event_put()", "[RLN] Verifying incoming Event RLN proof");
-                let signal_vk = read_signal_vk(&self.event_graph.sled_db)?;
-                verification_failed = rcvd_blob.proof.verify(&signal_vk, &public_inputs).is_err();
+            // Flood protection
+            bantimes.ticktock();
+            if bantimes.count() > WINDOW_MAXSIZE {
+                self.channel.ban().await;
+                return Err(Error::MaliciousFlood)
+            }
 
-                break
+            // Reject events from before the current rotation period
+            if event.header.timestamp < genesis_ts {
+                continue
             }
 
-            if verification_failed {
-                error!(target: "event_graph::protocol::handle_event_put()", "[RLN] Incoming Event RLN Signaling proof verification failed");
+            // Quick structural validation
+            if !event.validate_new() {
+                self.clone().strike().await?;
                 continue
             }
 
-            // Remove lingering messages from dag_sync event request response
-            _ = self.ev_rep_sub.clean().await;
+            // Fetch missing parents (depth-bounded)
+            // See MAX_PARENT_FETCH_DEPTH doc for why this is bounded.
+            let mut missing = HashSet::new();
+            {
+                let store = self.event_graph.dag_store.read().await;
+                if let Some(slot) = store.get_slot(&genesis_ts) {
+                    for pid in event.header.parents.iter() {
+                        if *pid != NULL_ID &&
+                            !slot.header_tree.contains_key(pid.as_bytes()).unwrap_or(true)
+                        {
+                            missing.insert(*pid);
+                        }
+                    }
+                }
+            }
 
-            // If we have already seen the event, we'll stay quiet.
-            let current_genesis = self.event_graph.current_genesis.read().await;
-            let genesis_timestamp = current_genesis.header.timestamp;
-            let dag_name = genesis_timestamp.to_string();
-            let hdr_tree_name = format!("headers_{dag_name}");
-            let event_id = event.id();
+            if !missing.is_empty() &&
+                !self.clone().fetch_parents(&mut missing, &dag_name, genesis_ts).await
+            {
+                // Depth exceeded or peer misbehaved
+                continue
+            }
+
+            // Insert the event itself
             if self
                 .event_graph
-                .dag_store
-                .read()
+                .header_dag_insert(vec![event.header.clone()], &dag_name)
                 .await
-                .get_dag(&hdr_tree_name)
-                .contains_key(event_id.as_bytes())
-                .unwrap()
+                .is_err()
             {
-                debug!(
-                    target: "event_graph::protocol::handle_event_put",
-                    "Event {event_id} is already known"
-                );
+                self.clone().strike().await?;
                 continue
             }
 
-            // There's a new unique event.
-            // Apply ban logic to stop network floods.
-            bantimes.ticktock();
-            if bantimes.count() > WINDOW_MAXSIZE {
-                self.channel.ban().await;
-                // This error is actually unused. We could return Ok here too.
-                return Err(Error::MaliciousFlood)
+            if self.event_graph.dag_insert(slice::from_ref(&event), &dag_name).await.is_err() {
+                self.clone().strike().await?;
+                continue
             }
 
-            // We received an event. Check if we already have it in our DAG.
-            // Check event is not older that current genesis event timestamp.
-            // Also check if we have the event's parents. In the case we do
-            // not have the parents, we'll request them from the peer that has
-            // sent this event to us. In case they do not reply in time, we drop
-            // the event.
-
-            // Check if the event is older than the genesis event. If so, we should
-            // not include it in our Dag.
-            // The genesis event marks the last time the Dag has been pruned of old
-            // events. The pruning interval is defined by the days_rotation field
-            // of [`EventGraph`].
-            if event.header.timestamp < genesis_timestamp {
-                debug!(
-                    target: "event_graph::protocol::handle_event_put",
-                    "Event {} is older than genesis. Event timestamp: `{}`. Genesis timestamp: `{genesis_timestamp}`",
-                event.id(), event.header.timestamp
+            // Relay to other peers (bounded - drops if channel full)
+            let _ = self.broadcaster_push.try_send(EventPut(event, blob));
+        }
+    }
+
+    /// Recursively fetch missing parent events, up to
+    /// [`MAX_PARENT_FETCH_DEPTH`] rounds.
+    /// Returns `true` on success.
+    async fn fetch_parents(
+        self: Arc<Self>,
+        missing: &mut HashSet<blake3::Hash>,
+        dag_name: &str,
+        dag_ts: u64,
+    ) -> bool {
+        let mut received: BTreeMap<u64, Vec<Event>> = BTreeMap::new();
+        let mut known = HashSet::new();
+        let mut depth = 0usize;
+
+        while !missing.is_empty() {
+            depth += 1;
+            if depth > MAX_PARENT_FETCH_DEPTH {
+                error!(
+                    target: "event_graph::protocol",
+                    "[EVENTGRAPH] Parent fetch depth exceeded ({})",
+                    MAX_PARENT_FETCH_DEPTH,
                 );
+                let _ = self.clone().strike().await;
+                return false
             }
 
-            // Validate the new event first. If we do not consider it valid, we
-            // will just drop it and stay quiet. If the malicious threshold
-            // is reached, we will stop the connection.
-            if !event.validate_new() {
-                self.clone().increase_malicious_count().await?;
-                continue
+            if self.channel.send(&EventReq(missing.iter().cloned().collect())).await.is_err() {
+                return false
             }
 
-            // At this point, this is a new event to us. Let's see if we
-            // have all of its parents.
-            debug!(
-                target: "event_graph::protocol::handle_event_put",
-                "Event {event_id} is new"
-            );
+            let timeout =
+                self.event_graph.p2p.settings().read().await.outbound_connect_timeout_max();
 
-            let mut missing_parents = HashSet::new();
-            for parent_id in event.header.parents.iter() {
-                // `event.validate_new()` should have already made sure that
-                // not all parents are NULL, and that there are no duplicates.
-                if parent_id == &NULL_ID {
-                    continue
-                }
+            let Ok(rep) = self.ev_rep_sub.receive_with_timeout(timeout).await else {
+                self.channel.stop().await;
+                return false
+            };
 
-                if !self
-                    .event_graph
-                    .dag_store
-                    .read()
-                    .await
-                    .get_dag(&hdr_tree_name)
-                    .contains_key(parent_id.as_bytes())
-                    .unwrap()
-                {
-                    missing_parents.insert(*parent_id);
+            for parent in rep.0.clone() {
+                let pid = parent.id();
+                if !missing.contains(&pid) {
+                    // Peer sent an event we didn't ask for
+                    self.channel.stop().await;
+                    return false
+                }
+                received.entry(parent.header.layer).or_default().push(parent.clone());
+                known.insert(pid);
+                missing.remove(&pid);
+
+                // Check for more unknown grandparents
+                let store = self.event_graph.dag_store.read().await;
+                if let Some(slot) = store.get_slot(&dag_ts) {
+                    for gp in parent.header.parents.iter() {
+                        if *gp != NULL_ID &&
+                            !missing.contains(gp) &&
+                            !known.contains(gp) &&
+                            !slot.header_tree.contains_key(gp.as_bytes()).unwrap_or(true)
+                        {
+                            missing.insert(*gp);
+                        }
+                    }
                 }
             }
+        }
 
-            // If we have missing parents, then we have to attempt to
-            // fetch them from this peer. Do this recursively until we
-            // find all of them.
-            if !missing_parents.is_empty() {
-                // We track the received events mapped by their layer.
-                // If/when we get all of them, we need to insert them in order so
-                // the DAG state stays correct and unreferenced tips represent the
-                // actual thing they should. If we insert them out of order, then
-                // we might have wrong unreferenced tips.
-                let mut received_events: BTreeMap<u64, Vec<Event>> = BTreeMap::new();
-                let mut received_events_hashes = HashSet::new();
-
-                debug!(
-                    target: "event_graph::protocol::handle_event_put",
-                    "Event has {} missing parents. Requesting...", missing_parents.len(),
-                );
+        // Insert in layer order. We insert into both header_tree and
+        // main_tree - inserting into header_tree alone would create
+        // an inconsistent state where an event E exists in main_tree
+        // but its parent P does not, even though both have headers.
+        // Any future ancestor walk via main_tree.get() would hit a
+        // None and fail. If the node wants to discard bodies for
+        // space, that should be a separate pruning pass, not a
+        // sync-time partial-insert.
+        let events: Vec<Event> = received.into_values().flatten().collect();
+        let headers: Vec<Header> = events.iter().map(|e| e.header.clone()).collect();
+
+        if self.event_graph.header_dag_insert(headers, dag_name).await.is_err() {
+            return false
+        }
 
-                let current_genesis = self.event_graph.current_genesis.read().await;
-                let dag_name = current_genesis.header.timestamp.to_string();
-                let hdr_tree_name = format!("headers_{dag_name}");
-
-                while !missing_parents.is_empty() {
-                    // for parent_id in missing_parents.clone().iter() {
-                    debug!(
-                        target: "event_graph::protocol::handle_event_put",
-                        "Requesting {missing_parents:?}..."
-                    );
-
-                    self.channel
-                        .send(&EventReq(missing_parents.clone().into_iter().collect()))
-                        .await?;
-
-                    // Node waits for response
-                    let Ok(parents) = self
-                        .ev_rep_sub
-                        .receive_with_timeout(
-                            self.event_graph
-                                .p2p
-                                .settings()
-                                .read()
-                                .await
-                                .outbound_connect_timeout_max(),
-                        )
-                        .await
-                    else {
-                        error!(
-                            target: "event_graph::protocol::handle_event_put",
-                            "[EVENTGRAPH] Timeout while waiting for parents {missing_parents:?} from {}",
-                            self.channel.display_address(),
-                        );
-                        self.channel.stop().await;
-                        return Err(Error::ChannelStopped)
-                    };
+        if self.event_graph.dag_insert(&events, dag_name).await.is_err() {
+            return false
+        }
 
-                    let parents = parents.0.clone();
-
-                    for parent in parents {
-                        let parent_id = parent.id();
-                        if !missing_parents.contains(&parent_id) {
-                            error!(
-                                target: "event_graph::protocol::handle_event_put",
-                                "[EVENTGRAPH] Peer {} replied with a wrong event: {}",
-                                self.channel.display_address(), parent.id(),
-                            );
-                            self.channel.stop().await;
-                            return Err(Error::ChannelStopped)
-                        }
+        true
+    }
 
-                        debug!(
-                            target: "event_graph::protocol::handle_event_put",
-                            "Got correct parent event {}", parent.id(),
-                        );
+    /// Verify an RLN signal proof. Returns `true` if the event
+    /// should be rejected (proof invalid, duplicate, or slashable).
+    async fn verify_rln_signal(&self, event: &Event, blob: &[u8]) -> bool {
+        let rcvd: Blob = match deserialize_async_partial(blob).await {
+            Ok((v, _)) => v,
+            Err(_) => return true, // unparseable blob -> reject
+        };
+
+        let epoch = closest_epoch(event.header.timestamp);
+        let ext_null = poseidon_hash([pallas::Base::from(epoch), pallas::Base::from(1000)]);
+        let x = hash_event(event);
+        let root = self.event_graph.identity_state.read().await.root();
+        let pi = vec![root, ext_null, x, rcvd.y, rcvd.internal_nullifier];
+
+        // Global metadata check
+        {
+            let mut rln = self.event_graph.rln_state.write().await;
+            if rln.current_epoch != epoch {
+                *rln = RlnState::new();
+                rln.current_epoch = epoch;
+            }
 
-                        if let Some(layer_events) = received_events.get_mut(&parent.header.layer) {
-                            layer_events.push(parent.clone());
-                        } else {
-                            let layer_events = vec![parent.clone()];
-                            received_events.insert(parent.header.layer, layer_events);
-                        }
-                        received_events_hashes.insert(parent_id);
-
-                        missing_parents.remove(&parent_id);
-
-                        // See if we have the upper parents
-                        for upper_parent in parent.header.parents.iter() {
-                            if upper_parent == &NULL_ID {
-                                continue
-                            }
-
-                            if !missing_parents.contains(upper_parent) &&
-                                !received_events_hashes.contains(upper_parent) &&
-                                !self
-                                    .event_graph
-                                    .dag_store
-                                    .read()
-                                    .await
-                                    .get_dag(&hdr_tree_name)
-                                    .contains_key(upper_parent.as_bytes())
-                                    .unwrap()
-                            {
-                                debug!(
-                                    target: "event_graph::protocol::handle_event_put",
-                                    "Found upper missing parent event {upper_parent}"
-                                );
-                                missing_parents.insert(*upper_parent);
-                            }
-                        }
-                    }
-                } // <-- while !missing_parents.is_empty()
-
-                // At this point we should've got all the events.
-                // We should add them to the DAG.
-                let mut events = vec![];
-                for (_, tips) in received_events {
-                    for tip in tips {
-                        events.push(tip);
-                    }
-                }
-                let headers = events.iter().map(|x| x.header.clone()).collect();
-                if self.event_graph.header_dag_insert(headers, &dag_name).await.is_err() {
-                    self.clone().increase_malicious_count().await?;
-                    continue
-                }
-                // FIXME
-                if !self.event_graph.fast_mode &&
-                    self.event_graph.dag_insert(&events, &dag_name).await.is_err()
-                {
-                    self.clone().increase_malicious_count().await?;
-                    continue
-                }
-            } // <-- !missing_parents.is_empty()
-
-            // If we're here, we have all the parents, and we can now
-            // perform a full validation and add the actual event to
-            // the DAG.
-            debug!(
-                target: "event_graph::protocol::handle_event_put",
-                "Got all parents necessary for insertion",
-            );
-            if self
-                .event_graph
-                .header_dag_insert(vec![event.header.clone()], &dag_name)
-                .await
-                .is_err()
-            {
-                self.clone().increase_malicious_count().await?;
-                continue
+            if rln.metadata.is_duplicate(&ext_null, &rcvd.internal_nullifier, &x, &rcvd.y) {
+                return true
             }
 
-            if self.event_graph.dag_insert(slice::from_ref(&event), &dag_name).await.is_err() {
-                self.clone().increase_malicious_count().await?;
-                continue
+            if rln.metadata.is_reused(&ext_null, &rcvd.internal_nullifier) {
+                let shares = rln.metadata.get_shares(&ext_null, &rcvd.internal_nullifier);
+                drop(rln);
+                self.slash(shares, rcvd.user_msg_limit).await;
+                return true
             }
+        }
 
-            self.broadcaster_push
-                .send(EventPut(event, blob))
-                .await
-                .expect("push broadcaster closed");
+        // Verify proof using cached VK
+        if rcvd.proof.verify(&self.event_graph.zk_keys.signal_vk, &pi).is_err() {
+            return true
         }
+
+        // Proof valid -> record share
+        let mut rln = self.event_graph.rln_state.write().await;
+        let _ = rln.metadata.add_share(ext_null, rcvd.internal_nullifier, x, rcvd.y);
+        false
+    }
+
+    /// Execute the slashing procedure: recover the secret, load the
+    /// slash proving key from sled, produce a slash proof, and
+    /// broadcast the slashing event.
+    async fn slash(&self, shares: Vec<(pallas::Base, pallas::Base)>, limit: u64) {
+        let secret = match sss_recover(&shares) {
+            Ok(s) => s,
+            Err(e) => {
+                error!(
+                    target: "event_graph::slash",
+                    "[RLN] SSS recovery failed: {e}",
+                );
+                return
+            }
+        };
+
+        // Lazy-load the slash PK from sled
+        let slash_pk = match self.event_graph.zk_keys.load_slash_pk() {
+            Ok(pk) => pk,
+            Err(e) => {
+                error!(
+                    target: "event_graph::slash",
+                    "[RLN] Failed to load slash PK: {e}",
+                );
+                return
+            }
+        };
+
+        let mut id = self.event_graph.identity_state.write().await;
+        let (proof, root) = match create_slash_proof(secret, limit, &mut id, &slash_pk) {
+            Ok(v) => v,
+            Err(e) => {
+                error!(
+                    target: "event_graph::slash",
+                    "[RLN] Slash proof creation failed: {e}",
+                );
+                return
+            }
+        };
+        drop(id);
+
+        let blob = serialize_async(&(proof, secret, limit, root)).await;
+        let commitment = poseidon_hash([poseidon_hash([secret, limit.into()])]);
+        let node = RLNNode::Slashing(commitment);
+        let ev = Event::new_static(serialize_async(&node).await, &self.event_graph).await;
+        let _ = self.event_graph.static_insert(&ev).await;
+        let _ = self.event_graph.static_broadcast(ev, blob).await;
     }
 
     async fn handle_static_put(self: Arc<Self>) -> Result<()> {
-        // Rolling window of event timestamps on this channel
         let mut bantimes = MovingWindow::new(WINDOW_EXPIRY_TIME);
-
         loop {
             let (event, blob) = match self.st_put_sub.receive().await {
                 Ok(v) => (v.0.clone(), v.1.clone()),
                 Err(_) => continue,
             };
-            trace!(
-                 target: "event_graph::protocol::handle_static_put()",
-                 "Got StaticPut: {} [{}]", event.id(), self.channel.address(),
-            );
-
-            // Check if node has finished syncing its DAG
-            if !*self.event_graph.synced.read().await {
-                debug!(
-                    target: "event_graph::protocol::handle_static_put",
-                    "DAG is still syncing, skipping..."
-                );
+            if !self.event_graph.is_synced() {
                 continue
             }
-
-            let event_id = event.id();
-            if self.event_graph.static_dag.contains_key(event_id.as_bytes())? {
-                debug!(
-                    target: "event_graph::protocol::handle_static_put()",
-                    "Event {} is already known", event_id,
-                );
+            let eid = event.id();
+            if self.event_graph.static_dag.contains_key(eid.as_bytes())? {
                 continue
             }
 
-            let rln_account: RLNNode = match deserialize_async_partial(event.content()).await {
+            let rln_node: RLNNode = match deserialize_async_partial(event.content()).await {
                 Ok((v, _)) => v,
-                Err(e) => {
-                    error!(target: "event_graph::protocol::handle_static_put()","[RLN] Failed deserializing event ephemeral data: {}", e);
-                    continue
-                }
+                Err(_) => continue,
             };
-
             if blob.is_empty() {
-                error!(target: "event_graph::protocol::handle_static_put()","[RLN] Failed to register/slash: Not enough data provided");
                 continue
             }
-            match rln_account {
+
+            match rln_node {
                 RLNNode::Registration(commitment) => {
-                    let (proof, user_msg_limit): (Proof, u64) = match deserialize_async_partial(
-                        &blob,
-                    )
-                    .await
+                    let (proof, msg_limit): (Proof, u64) =
+                        match deserialize_async_partial(&blob).await {
+                            Ok((v, _)) => v,
+                            Err(_) => continue,
+                        };
+                    if proof
+                        .verify(
+                            &self.event_graph.zk_keys.register_vk,
+                            &[commitment, msg_limit.into()],
+                        )
+                        .is_err()
                     {
-                        Ok((v, _)) => v,
-                        Err(e) => {
-                            error!(target: "event_graph::protocol::handle_static_put()","[RLN] Failed deserializing event ephemeral data: {}", e);
-                            continue
-                        }
-                    };
-
-                    info!("registering account: {:?}", commitment);
-                    let public_inputs = vec![commitment, user_msg_limit.into()];
-
-                    let register_vk = read_register_vk(&self.event_graph.sled_db)?;
-                    if proof.verify(&register_vk, &public_inputs).is_err() {
-                        error!(target: "event_graph::protocol::handle_static_put()", "[RLN] Incoming Event RLN Registration proof verification failed");
+                        continue
+                    }
+                    // Persist the new identity
+                    if let Err(e) =
+                        self.event_graph.identity_state.write().await.register(commitment)
+                    {
+                        error!("[RLN] Register: {e}");
                         continue
                     }
                 }
                 RLNNode::Slashing(commitment) => {
-                    let (proof, secret, user_msg_limit, identity_root): (
-                        Proof,
-                        pallas::Base,
-                        u64,
-                        pallas::Base,
-                    ) = match deserialize_async_partial(&blob).await {
-                        Ok((v, _)) => v,
-                        Err(e) => {
-                            error!(target: "event_graph::protocol::handle_static_put()","[RLN] Failed deserializing event ephemeral data: {}", e);
-                            continue
-                        }
-                    };
-
-                    let public_inputs =
-                        vec![secret, pallas::Base::from(user_msg_limit), identity_root];
-                    let slash_vk = read_slash_vk(&self.event_graph.sled_db)?;
-                    if proof.verify(&slash_vk, &public_inputs).is_err() {
-                        error!(target: "event_graph::protocol::handle_static_put()", "[RLN] Incoming Event RLN Slashing proof verification failed");
+                    let (proof, secret, msg_limit, root): (Proof, pallas::Base, u64, pallas::Base) =
+                        match deserialize_async_partial(&blob).await {
+                            Ok((v, _)) => v,
+                            Err(_) => continue,
+                        };
+                    if proof
+                        .verify(
+                            &self.event_graph.zk_keys.slash_vk,
+                            &[secret, msg_limit.into(), root],
+                        )
+                        .is_err()
+                    {
+                        continue
+                    }
+                    let rebuilt = poseidon_hash([poseidon_hash([secret, msg_limit.into()])]);
+                    if commitment != rebuilt {
+                        self.clone().strike().await?;
+                        continue
+                    }
+                    if let Err(e) = self.event_graph.identity_state.write().await.slash(rebuilt) {
+                        error!("[RLN] Slash: {e}");
                         continue
                     }
-
-                    let identity_secret_hash = poseidon_hash([secret, user_msg_limit.into()]);
-                    let rebuilt_commitment = poseidon_hash([identity_secret_hash]);
-
-                    assert_eq!(commitment, rebuilt_commitment);
-                    info!("slashing account: {}", rebuilt_commitment.to_string());
-                    let commitment = vec![rebuilt_commitment];
-                    let commitment: Vec<_> = commitment.into_iter().map(|l| (l, l)).collect();
-
-                    let mut rln_id_tree = self.event_graph.rln_identity_tree.write().await;
-                    rln_id_tree.remove_leaves(commitment)?;
                 }
             }
 
-            // Check if event's parents are in the static DAG
-            for parent in event.header.parents.iter() {
-                if *parent == NULL_ID {
-                    continue
-                }
-                if !self.event_graph.static_dag.contains_key(parent.as_bytes())? {
-                    debug!(
-                        target: "event_graph::protocol::handle_static_put()",
-                        "Event {} is orphan", event_id,
-                    );
-                    return Err(Error::EventNotFound("Event is orphan".to_owned()))
+            // Validate parents exist in static DAG
+            for p in event.header.parents.iter() {
+                if *p != NULL_ID && !self.event_graph.static_dag.contains_key(p.as_bytes())? {
+                    return Err(Error::EventNotFound("Orphan static event".into()))
                 }
             }
 
-            // There's a new unique event.
-            // Apply ban logic to stop network floods.
             bantimes.ticktock();
             if bantimes.count() > WINDOW_MAXSIZE {
                 self.channel.ban().await;
-                // This error is actually unused. We could return Ok here too.
                 return Err(Error::MaliciousFlood)
             }
-
-            // Validate the new event first. If we do not consider it valid, we
-            // will just drop it and stay quiet. If the malicious threshold
-            // is reached, we will stop the connection.
             if !event.validate_new() {
-                self.clone().increase_malicious_count().await?;
+                self.clone().strike().await?;
                 continue
             }
 
-            // At this point, this is a new event to us. Let's see if we
-            // have all of its parents.
-            debug!(
-                target: "event_graph::protocol::handle_event_put()",
-                "Event {} is new", event_id,
-            );
-
             self.event_graph.static_insert(&event).await?;
-            self.event_graph.static_broadcast(event, blob).await?
+            self.event_graph.static_broadcast(event, blob).await?;
         }
     }
 
-    /// Protocol function handling `EventReq`.
-    /// This is triggered whenever someone requests an event from us.
     async fn handle_event_req(self: Arc<Self>) -> Result<()> {
         loop {
-            let event_ids = match self.ev_req_sub.receive().await {
+            let ids = match self.ev_req_sub.receive().await {
                 Ok(v) => v.0.clone(),
                 Err(_) => continue,
             };
-            trace!(
-                target: "event_graph::protocol::handle_event_req",
-                "Got EventReq: {event_ids:?} [{}]", self.channel.display_address(),
-            );
-
-            // Check if node has finished syncing its DAG
-            if !*self.event_graph.synced.read().await {
-                debug!(
-                    target: "event_graph::protocol::handle_event_req",
-                    "DAG is still syncing, skipping..."
-                );
+            if !self.event_graph.is_synced() {
                 continue
             }
 
-            // We received an event request from somebody.
-            // If we do have it, we will send it back to them as `EventRep`.
-            // Otherwise, we'll stay quiet. An honest node should always have
-            // something to reply with provided that the request is legitimate,
-            // i.e. we've sent something to them and they did not haveinfo some of
-            // the parents.
-
-            // Check if we expected this request to come around.
-            // I dunno if this is a good idea, but it seems it will help
-            // against malicious event requests where they want us to keep
-            // reading our db and steal our bandwidth.
+            // Only serve IDs we've previously broadcast (prevents
+            // arbitrary DAG enumeration by malicious peers).
+            let bcast = self.event_graph.broadcasted_ids.read().await;
             let mut events = vec![];
-            for event_id in event_ids.iter() {
-                if let Ok(event) = self
-                    .event_graph
-                    .fetch_event_from_dags(event_id)
-                    .await?
-                    .ok_or(Error::EventNotFound("The requested event is not found".to_owned()))
-                {
-                    // At this point we should have it in our DAG.
-                    // This code panics if this is not the case.
-                    debug!(
-                        target: "event_graph::protocol::handle_event_req()",
-                        "Fetching event {:?} from DAG", event_id,
-                    );
-                    events.push(event);
-                } else {
-                    let malicious_count = self.malicious_count.fetch_add(1, SeqCst);
-                    if malicious_count + 1 == MALICIOUS_THRESHOLD {
-                        error!(
-                            target: "event_graph::protocol::handle_event_req",
-                            "[EVENTGRAPH] Peer {} reached malicious threshold. Dropping connection.",
-                            self.channel.display_address(),
-                        );
-                        self.channel.stop().await;
-                        return Err(Error::ChannelStopped)
-                    }
-
-                    warn!(
-                        target: "event_graph::protocol::handle_event_req",
-                        "[EVENTGRAPH] Peer {} requested an unexpected event {event_id:?}",
-                        self.channel.display_address()
-                    );
+            for id in &ids {
+                if !bcast.contains(id) {
+                    self.clone().strike().await?;
                     continue
                 }
-            }
-
-            // Check if the incoming event is older than the genesis event. If so, something
-            // has gone wrong. The event should have been pruned during the last
-            // rotation.
-            let genesis_timestamp = self.event_graph.current_genesis.read().await.header.timestamp;
-            let mut bcast_ids = self.event_graph.broadcasted_ids.write().await;
-
-            for event in events.iter() {
-                if event.header.timestamp < genesis_timestamp {
-                    error!(
-                        target: "event_graph::protocol::handle_event_req",
-                        "Requested event by peer {} is older than previous rotation period. It should have been pruned.
-                    Event timestamp: `{}`. Genesis timestamp: `{genesis_timestamp}`",
-                    event.id(), event.header.timestamp
-                    );
+                if let Some(ev) = self.event_graph.fetch_event_from_dags(id).await? {
+                    events.push(ev);
                 }
-
-                // Now let's get the upper level of event IDs. When we reply, we could
-                // get requests for those IDs as well.
-                for parent_id in event.header.parents.iter() {
-                    if parent_id != &NULL_ID {
-                        bcast_ids.insert(*parent_id);
+            }
+            drop(bcast);
+
+            if !events.is_empty() {
+                let mut b = self.event_graph.broadcasted_ids.write().await;
+                for ev in &events {
+                    for p in ev.header.parents.iter() {
+                        if *p != NULL_ID {
+                            b.insert(*p);
+                        }
                     }
                 }
+                drop(b);
+                self.channel.send(&EventRep(events)).await?;
             }
-            // TODO: We should remove the reply from the bcast IDs for this specific channel.
-            //       We can't remove them for everyone.
-            //bcast_ids.remove(&event_id);
-            drop(bcast_ids);
-
-            // Reply with the event
-            self.channel.send(&EventRep(events)).await?;
         }
     }
 
-    /// Protocol function handling `HeaderReq`.
-    /// This is triggered whenever someone requests syncing headers by
-    /// sending their current headers.
     async fn handle_header_req(self: Arc<Self>) -> Result<()> {
         loop {
             let Ok(v) = self.hdr_req_sub.receive().await else { continue };
-            let (dag_name, tips) = (&v.0, &v.1);
-
-            trace!(
-                target: "event_graph::protocol::handle_tip_req",
-                "Got TipReq [{}]", self.channel.display_address(),
-            );
-
-            // Check if node has finished syncing its DAG
-            if !*self.event_graph.synced.read().await {
-                debug!(
-                    target: "event_graph::protocol::handle_tip_req",
-                    "DAG is still syncing, skipping..."
-                );
+            if !self.event_graph.is_synced() {
                 continue
             }
-
-            // TODO: Rate limit
-
-            // We received header request. Let's find them, add them to
-            // our bcast ids list, and reply with them.
-            let dag_timestamp = u64::from_str(dag_name)?;
-            let store = self.event_graph.dag_store.read().await;
-            if !store.header_dags.contains_key(&dag_timestamp) {
-                continue
+            let (dag_name, tips) = (&v.0, &v.1);
+            let dag_ts = match u64::from_str(dag_name) {
+                Ok(v) => v,
+                Err(_) => continue,
+            };
+            {
+                let s = self.event_graph.dag_store.read().await;
+                if s.get_slot(&dag_ts).is_none() {
+                    continue
+                }
             }
-            let headers = self.event_graph.fetch_headers_with_tips(dag_name, tips).await?;
-            // let mut bcast_ids = self.event_graph.broadcasted_ids.write().await;
-            // for (_, tips) in layers.iter() {
-            //     for tip in tips {
-            //         bcast_ids.insert(*tip);
-            //     }
-            // }
-            // drop(bcast_ids);
-
-            self.channel.send(&HeaderRep(headers)).await?;
+            let hdrs = self.event_graph.fetch_headers_with_tips(dag_name, tips).await?;
+            self.channel.send(&HeaderRep(hdrs)).await?;
         }
-        // Ok(())
     }
 
-    /// Protocol function handling `TipReq`.
-    /// This is triggered when someone requests the current unreferenced
-    /// tips of our DAG.
     async fn handle_tip_req(self: Arc<Self>) -> Result<()> {
         loop {
             let dag_name = match self.tip_req_sub.receive().await {
                 Ok(v) => v.0.clone(),
                 Err(_) => continue,
             };
-            trace!(
-                target: "event_graph::protocol::handle_tip_req",
-                "Got TipReq [{}]", self.channel.display_address(),
-            );
-
-            // Check if node has finished syncing its DAG
-            if !*self.event_graph.synced.read().await {
-                debug!(
-                    target: "event_graph::protocol::handle_tip_req",
-                    "DAG is still syncing, skipping..."
-                );
+            if !self.event_graph.is_synced() {
                 continue
             }
 
-            // TODO: Rate limit
-
-            // We received a tip request. Let's find them, add them to
-            // our bcast ids list, and reply with them.
             let layers = match dag_name.as_str() {
-                "static-dag" => {
-                    let tips = self.event_graph.static_unreferenced_tips().await;
-                    &tips.clone()
-                }
+                "static-dag" => self.event_graph.static_unreferenced_tips().await,
                 _ => {
-                    let dag_timestamp = u64::from_str(&dag_name)?;
+                    let ts = match u64::from_str(&dag_name) {
+                        Ok(v) => v,
+                        Err(_) => continue,
+                    };
                     let store = self.event_graph.dag_store.read().await;
-                    let (_, layers) = match store.header_dags.get(&dag_timestamp) {
-                        Some(v) => v,
+                    match store.get_slot(&ts) {
+                        Some(s) => s.tips.clone(),
                         None => continue,
-                    };
-                    &layers.clone()
+                    }
                 }
             };
-            // let layers = self.event_graph.dag_store.read().await.find_unreferenced_tips(&dag_name).await;
-            let mut bcast_ids = self.event_graph.broadcasted_ids.write().await;
-            for (_, tips) in layers.iter() {
-                for tip in tips {
-                    bcast_ids.insert(*tip);
+
+            let mut b = self.event_graph.broadcasted_ids.write().await;
+            for tips in layers.values() {
+                for t in tips {
+                    b.insert(*t);
                 }
             }
-            drop(bcast_ids);
+            drop(b);
+            self.channel.send(&TipRep(layers)).await?;
+        }
+    }
 
-            self.channel.send(&TipRep(layers.clone())).await?;
+    /// Serve a paginated content request. Uses the local
+    /// [`TimeIndex`] to find events around the cursor, then
+    /// returns their full content.
+    async fn handle_range_req(self: Arc<Self>) -> Result<()> {
+        loop {
+            let req = match self.range_req_sub.receive().await {
+                Ok(v) => v,
+                Err(_) => continue,
+            };
+            if !self.event_graph.is_synced() {
+                continue
+            }
+            let events = self
+                .event_graph
+                .fetch_page(req.cursor_ts, req.direction.clone(), req.limit as usize)
+                .await?;
+            self.channel.send(&RangeRep(events)).await?;
         }
     }
 
-    /// We need to rate limit message propagation so malicious nodes don't get us banned
-    /// for flooding. We do that by aggregating messages here into a queue then apply
-    /// rate limit logic before broadcasting.
-    ///
-    /// The rate limit logic is this:
-    ///
-    /// * If the count is less then RATELIMIT_MIN_COUNT then do nothing.
-    /// * Otherwise sleep for `sleep_time` ms.
-    ///
-    /// To calculate the sleep time, we use the RATELIMIT_SAMPLE_* values.
-    /// For example RATELIMIT_SAMPLE_IDX = 10, RATELIMIT_SAMPLE_SLEEP = 1000
-    /// means that when N = 10, then sleep for 1000 ms.
-    ///
-    /// Let RATELIMIT_MIN_COUNT = 6, then here's a table of sleep times:
-    ///
-    /// | Count | Sleep Time / ms |
-    /// |-------|-----------------|
-    /// | 0     | 0               |
-    /// | 4     | 0               |
-    /// | 6     | 0               |
-    /// | 10    | 1000            |
-    /// | 14    | 2000            |
-    /// | 18    | 3000            |
-    ///
-    /// So we use the sample to calculate a straight line from RATELIMIT_MIN_COUNT.
     async fn broadcast_rate_limiter(self: Arc<Self>) -> Result<()> {
-        let mut ratelimit = MovingWindow::new(RATELIMIT_EXPIRY_TIME);
-
+        let mut rl = MovingWindow::new(RATELIMIT_EXPIRY_TIME);
         loop {
-            let event_put = self.broadcaster_pull.recv().await.expect("pull broadcaster closed");
-
-            ratelimit.ticktock();
-            if ratelimit.count() > RATELIMIT_MIN_COUNT {
-                let sleep_time =
-                    ((ratelimit.count() - RATELIMIT_MIN_COUNT) * RATELIMIT_SAMPLE_SLEEP /
-                        (RATELIMIT_SAMPLE_IDX - RATELIMIT_MIN_COUNT)) as u64;
-                debug!(
-                    target: "event_graph::protocol::broadcast_rate_limiter",
-                    "Activated rate limit: sleeping {sleep_time} ms [count={}]",
-                    ratelimit.count()
-                );
-                // Apply the ratelimit
-                msleep(sleep_time).await;
+            let ep = self.broadcaster_pull.recv().await.expect("broadcaster closed");
+            rl.ticktock();
+            if rl.count() > RATELIMIT_MIN_COUNT {
+                let ms = ((rl.count() - RATELIMIT_MIN_COUNT) * RATELIMIT_SAMPLE_SLEEP /
+                    (RATELIMIT_SAMPLE_IDX - RATELIMIT_MIN_COUNT)) as u64;
+                msleep(ms).await;
             }
-
-            // Relay the event to other peers.
             self.event_graph
                 .p2p
-                .broadcast_with_exclude(&event_put, &[self.channel.address().clone()])
+                .broadcast_with_exclude(&ep, &[self.channel.address().clone()])
                 .await;
         }
     }
 }
-
-#[cfg(test)]
-mod test {
-    use super::*;
-    use std::time::UNIX_EPOCH;
-
-    #[test]
-    fn test_eventgraph_moving_window_clean_future() {
-        let mut window = MovingWindow::new(NanoTimestamp::from_secs(60));
-        let future = UNIX_EPOCH.elapsed().unwrap().as_secs() + 100;
-        window.times.push_back(NanoTimestamp::from_secs(future.into()));
-        window.clean();
-        assert_eq!(window.count(), 0);
-    }
-}

+ 298 - 280
src/event_graph/rln.rs

@@ -1,6 +1,6 @@
 /* This file is part of DarkFi (https://dark.fi)
  *
- * Copyright (C) 2020-2025 Dyne.org foundation
+ * 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
@@ -16,15 +16,25 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::collections::BTreeMap;
-
-use async_trait::async_trait;
-use darkfi_sdk::pasta::pallas;
-
-use std::io::Cursor;
-
-use darkfi_sdk::crypto::{pasta_prelude::FromUniformBytes, poseidon_hash, smt::SmtMemoryFp};
-use darkfi_serial::{FutAsyncWriteExt, SerialDecodable, SerialEncodable};
+//! Rate-Limit Nullifier (RLN) v2 integration for the Event Graph.
+//!
+//! RLN lets anonymous users post to the DAG at a configurable rate.
+//! If a user exceeds their rate limit (by reusing a message slot
+//! within the same epoch), their shares reveal their secret key via
+//! Shamir's Secret Sharing, and anyone can produce a slashing proof
+//! to remove them from the identity tree.
+
+use std::{collections::BTreeMap, io::Cursor};
+
+use darkfi_sdk::{
+    crypto::{
+        pasta_prelude::{FromUniformBytes, PrimeField},
+        poseidon_hash,
+        smt::{MemoryStorageFp, PoseidonFp, SmtMemoryFp, EMPTY_NODES_FP},
+    },
+    pasta::pallas,
+};
+use darkfi_serial::{async_trait, FutAsyncWriteExt, SerialDecodable, SerialEncodable};
 use halo2_proofs::{arithmetic::Field, circuit::Value};
 use rand::rngs::OsRng;
 use sled_overlay::sled;
@@ -41,96 +51,199 @@ pub const RLN2_REGISTER_ZKBIN: &[u8] = include_bytes!("proof/rlnv2-diff-register
 pub const RLN2_SIGNAL_ZKBIN: &[u8] = include_bytes!("proof/rlnv2-diff-signal.zk.bin");
 pub const RLN2_SLASH_ZKBIN: &[u8] = include_bytes!("proof/rlnv2-diff-slash.zk.bin");
 
-/// RLN epoch genesis in millis
+/// RLN epoch genesis in millis.
+/// Used as the time-zero reference for epoch numbering.
 pub const RLN_GENESIS: u64 = 1_738_688_400_000;
-/// RLN epoch length in millis
-pub const RLN_EPOCH_LEN: u64 = 600_000; // 10 min
 
+/// Duration of one RLN epoch in millis (10 minutes).
+pub const RLN_EPOCH_LEN: u64 = 600_000;
+
+/// Ephemeral data attached to an [`EventPut`] when RLN is active.
 #[derive(SerialEncodable, SerialDecodable)]
 pub struct Blob {
+    /// The RLN signal proof.
     pub proof: Proof,
+    /// The `y` share value: `y = a_0 + x * a_1`.
     pub y: pallas::Base,
+    /// Nullifier derived from `(identity, epoch, message_id)`.
     pub internal_nullifier: pallas::Base,
+    /// The user's per-registration message limit.
     pub user_msg_limit: u64,
 }
 
-// pub type SmtAccntFp = SparseMerkleTree<
-//     'static,
-//     SMT_FP_DEPTH,
-//     { SMT_FP_DEPTH + 1 },
-//     pallas::Base,
-//     PoseidonFp,
-//     AccountStorage,
-// >;
-
-// #[derive(Clone)]
-// pub struct AccountStorage {
-//     pub tree: sled::Tree,
-// }
-
-// impl AccountStorage {
-//     pub fn new(sled_db: &sled::Db, name: String) -> Self {
-//         Self { tree: sled_db.open_tree(name).unwrap() }
-//     }
-// }
-
-// impl StorageAdapter for AccountStorage {
-//     type Value = pallas::Base;
-
-//     fn put(&mut self, key: BigUint, value: pallas::Base) -> ContractResult {
-//         self.tree.insert(key.to_bytes_le(), &value.to_repr()).unwrap();
-//         Ok(())
-//     }
-
-//     fn get(&self, key: &BigUint) -> Option<pallas::Base> {
-//         let value = match self.tree.get(&key.to_bytes_le()) {
-//             Ok(v) => v,
-//             Err(e) => {
-//                 error!("SledStorage::get(): Fetching key {:?} from Accounts tree: {}", key, e,);
-//                 return None
-//             }
-//         };
-
-//         let value = value?;
-//         let mut repr = [0; 32];
-//         repr.copy_from_slice(&value);
-
-//         pallas::Base::from_repr(repr).into()
-//     }
-
-//     fn del(&mut self, key: &BigUint) -> ContractResult {
-//         self.tree.remove(key.to_bytes_le()).unwrap();
-//         Ok(())
-//     }
-// }
-
-/// Hash message/event modulo `Fp`
+/// An entry in the static DAG representing an identity event.
+#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
+pub enum RLNNode {
+    /// A new identity commitment being registered.
+    Registration(pallas::Base),
+    /// An identity commitment being slashed (removed).
+    Slashing(pallas::Base),
+}
+
+/// ZK key cache.
+pub struct ZkKeys {
+    /// Verifying key for identity registration proofs.
+    pub register_vk: VerifyingKey,
+    /// Verifying key for signal (rate-limit) proofs.
+    pub signal_vk: VerifyingKey,
+    /// Verifying key for slash proofs.
+    pub slash_vk: VerifyingKey,
+    /// Reference to the sled DB so we can lazy-load the proving keys.
+    sled_db: sled::Db,
+}
+
+impl ZkKeys {
+    /// Ensure all keys exist in sled and load only the verifying
+    /// keys into memory.
+    pub fn build_and_load(sled_db: &sled::Db) -> Result<Self> {
+        ensure_key(sled_db, "rlnv2-diff-register-vk", RLN2_REGISTER_ZKBIN, KeyKind::Vk)?;
+        ensure_key(sled_db, "rlnv2-diff-signal-vk", RLN2_SIGNAL_ZKBIN, KeyKind::Vk)?;
+        ensure_key(sled_db, "rlnv2-diff-slash-pk", RLN2_SLASH_ZKBIN, KeyKind::Pk)?;
+        ensure_key(sled_db, "rlnv2-diff-slash-vk", RLN2_SLASH_ZKBIN, KeyKind::Vk)?;
+
+        Ok(Self {
+            register_vk: read_vk(sled_db, "rlnv2-diff-register-vk", RLN2_REGISTER_ZKBIN)?,
+            signal_vk: read_vk(sled_db, "rlnv2-diff-signal-vk", RLN2_SIGNAL_ZKBIN)?,
+            slash_vk: read_vk(sled_db, "rlnv2-diff-slash-vk", RLN2_SLASH_ZKBIN)?,
+            sled_db: sled_db.clone(),
+        })
+    }
+
+    /// Load the slash proving key from sled.
+    /// This is expensive memory-wise and should only be called when
+    /// a slash proof is about to be created.
+    pub fn load_slash_pk(&self) -> Result<ProvingKey> {
+        read_pk(&self.sled_db, "rlnv2-diff-slash-pk", RLN2_SLASH_ZKBIN)
+    }
+}
+
+/// Mutable RLN state shared across all protocol instances via
+/// `EventGraph::rln_state`. Each peer connection's protocol handler
+/// accesses this through a write lock so that duplicate/reuse
+/// detection works regardless of which peer relayed the event.
+pub struct RlnState {
+    /// Per-nullifier share tracking for the current epoch.
+    pub metadata: MessageMetadata,
+    /// The epoch for which `metadata` is valid. When the epoch
+    /// changes, the metadata is reset.
+    pub current_epoch: u64,
+}
+
+impl RlnState {
+    pub fn new() -> Self {
+        Self { metadata: MessageMetadata::new(), current_epoch: 0 }
+    }
+}
+
+impl Default for RlnState {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+/// The set of currently registered RLN identities, stored as a Sparse
+/// Merkle Tree (SMT).
+///
+/// Persistence model: leaf commitments are stored in a dedicated sled
+/// tree (`rln-identity-leaves`). The in-memory SMT is rebuilt from
+/// these leaves on startup.
+pub struct IdentityState {
+    /// In-memory SMT for fast root computation and membership proofs.
+    smt: SmtMemoryFp,
+    /// Sled tree holding the persisted leaf set.
+    leaves: sled::Tree,
+}
+
+impl IdentityState {
+    /// Create a new identity state, restoring leaves from sled if present.
+    pub fn new(sled_db: &sled::Db) -> Result<Self> {
+        let hasher = PoseidonFp::new();
+        let store = MemoryStorageFp::new();
+        let mut smt = SmtMemoryFp::new(store, hasher, &EMPTY_NODES_FP);
+
+        let leaves = sled_db.open_tree("rln-identity-leaves")?;
+
+        // Rebuild SMT from persisted leaves
+        let mut batch = vec![];
+        for item in leaves.iter() {
+            let (_, val) = item?;
+            let mut repr = [0u8; 32];
+            repr.copy_from_slice(&val);
+            if let Some(c) = pallas::Base::from_repr(repr).into() {
+                batch.push((c, c));
+            }
+        }
+
+        if !batch.is_empty() {
+            info!(
+                target: "event_graph::rln",
+                "[RLN] Restoring {} identities from sled", batch.len(),
+            );
+            smt.insert_batch(batch)?;
+        }
+
+        Ok(Self { smt, leaves })
+    }
+
+    /// Register a new identity. Writes to both the in-memory SMT
+    /// and the sled persistence tree.
+    pub fn register(&mut self, commitment: pallas::Base) -> Result<()> {
+        self.leaves.insert(commitment.to_repr(), commitment.to_repr().as_ref())?;
+        self.smt.insert_batch(vec![(commitment, commitment)])?;
+        Ok(())
+    }
+
+    /// Slash (remove) an identity.
+    pub fn slash(&mut self, commitment: pallas::Base) -> Result<()> {
+        self.leaves.remove(commitment.to_repr())?;
+        self.smt.remove_leaves(vec![(commitment, commitment)])?;
+        Ok(())
+    }
+
+    /// Current Merkle root of the identity tree.
+    pub fn root(&self) -> pallas::Base {
+        self.smt.root()
+    }
+
+    /// Generate a membership proof for `commitment`.
+    pub fn prove_membership(&self, commitment: &pallas::Base) -> darkfi_sdk::crypto::smt::PathFp {
+        self.smt.prove_membership(commitment)
+    }
+}
+
+/// Hash an event's header ID into a field element suitable for use
+/// as the `x` coordinate in the RLN polynomial evaluation.
 pub fn hash_event(event: &Event) -> pallas::Base {
     let mut buf = [0u8; 64];
     buf[..blake3::OUT_LEN].copy_from_slice(event.header.id().as_bytes());
     pallas::Base::from_uniform_bytes(&buf)
 }
 
-/// Find closest epoch to given timestamp
+/// Map a UNIX-millis timestamp to the nearest RLN epoch boundary.
+///
+/// Returns `0` if the timestamp predates [`RLN_GENESIS`], avoiding
+/// underflow panics on malicious timestamps.
 pub fn closest_epoch(timestamp: u64) -> u64 {
-    let time_diff = timestamp - RLN_GENESIS;
-    let epoch_idx = time_diff as f64 / RLN_EPOCH_LEN as f64;
-    let rounded = epoch_idx.round() as i64;
-    RLN_GENESIS + (rounded * RLN_EPOCH_LEN as i64) as u64
+    let Some(diff) = timestamp.checked_sub(RLN_GENESIS) else { return 0 };
+    let idx = (diff as f64 / RLN_EPOCH_LEN as f64).round() as u64;
+    RLN_GENESIS.saturating_add(idx.saturating_mul(RLN_EPOCH_LEN))
 }
 
 #[derive(Debug, Clone)]
 struct ShareData {
-    pub x_shares: Vec<pallas::Base>,
-    pub y_shares: Vec<pallas::Base>,
-}
-
-impl ShareData {
-    fn new() -> Self {
-        Self { x_shares: vec![], y_shares: vec![] }
-    }
+    /// Collected `(x, y)` share pairs for a single internal nullifier.
+    shares: Vec<(pallas::Base, pallas::Base)>,
 }
 
+/// Per-epoch tracking of RLN shares, keyed by nullifier pairs.
+///
+/// Each `(external_nullifier, internal_nullifier)` maps to the set
+/// of `(x, y)` shares seen so far.
+/// This allows detecting:
+/// * **Duplicates** - the exact same `(x, y)` pair arriving twice
+///   (the event is just dropped).
+/// * **Slot reuse** - a different `(x, y)` for the same internal
+///   nullifier (the user reused a message slot, triggering slashing).
 #[derive(Debug, Default)]
 pub struct MessageMetadata {
     data: BTreeMap<pallas::Base, BTreeMap<pallas::Base, ShareData>>,
@@ -138,265 +251,170 @@ pub struct MessageMetadata {
 
 impl MessageMetadata {
     pub fn new() -> Self {
-        Self { data: BTreeMap::new() }
+        Self::default()
     }
 
+    /// Record a new share.
     pub fn add_share(
         &mut self,
-        external_nullifier: pallas::Base,
-        internal_nullifier: pallas::Base,
+        ext_null: pallas::Base,
+        int_null: pallas::Base,
         x: pallas::Base,
         y: pallas::Base,
     ) -> Result<()> {
-        let inner_map = self.data.entry(external_nullifier).or_default();
-        let share_data = inner_map.entry(internal_nullifier).or_insert_with(ShareData::new);
-
-        share_data.x_shares.push(x);
-        share_data.y_shares.push(y);
-
+        self.data
+            .entry(ext_null)
+            .or_default()
+            .entry(int_null)
+            .or_insert_with(|| ShareData { shares: vec![] })
+            .shares
+            .push((x, y));
         Ok(())
     }
 
+    /// Retrieve all shares for a given nullifier pair.
     pub fn get_shares(
         &self,
-        external_nullifier: &pallas::Base,
-        internal_nullifier: &pallas::Base,
+        ext_null: &pallas::Base,
+        int_null: &pallas::Base,
     ) -> Vec<(pallas::Base, pallas::Base)> {
-        if let Some(inner_map) = self.data.get(external_nullifier) {
-            if let Some(share_data) = inner_map.get(internal_nullifier) {
-                return share_data
-                    .x_shares
-                    .iter()
-                    .cloned()
-                    .zip(share_data.y_shares.iter().cloned())
-                    .collect()
-            }
-        }
-
-        vec![]
+        self.data
+            .get(ext_null)
+            .and_then(|m| m.get(int_null))
+            .map(|sd| sd.shares.clone())
+            .unwrap_or_default()
     }
 
-    /// Check if the recieved message and its metadata are duplicated
+    /// Check whether the exact `(x, y)` pair is already recorded.
+    ///
+    /// This compares pairs - not independent coordinates - to avoid
+    /// false positives from cross-matching different shares.
     pub fn is_duplicate(
         &self,
-        external_nullifier: &pallas::Base,
-        internal_nullifier: &pallas::Base,
+        ext_null: &pallas::Base,
+        int_null: &pallas::Base,
         x: &pallas::Base,
         y: &pallas::Base,
     ) -> bool {
-        if let Some(inner_map) = self.data.get(external_nullifier) {
-            if let Some(share_data) = inner_map.get(internal_nullifier) {
-                return share_data.x_shares.contains(x) && share_data.y_shares.contains(y);
-            }
-        }
-
-        false
+        self.data
+            .get(ext_null)
+            .and_then(|m| m.get(int_null))
+            .map(|sd| sd.shares.iter().any(|(sx, sy)| sx == x && sy == y))
+            .unwrap_or(false)
     }
 
-    /// Check if the message has reused the nullifiers
-    pub fn is_reused(
-        &self,
-        external_nullifier: &pallas::Base,
-        internal_nullifier: &pallas::Base,
-    ) -> bool {
-        if let Some(inner_map) = self.data.get(external_nullifier) {
-            return inner_map.get(internal_nullifier).is_some()
-        }
-        false
-    }
-}
-
-#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
-pub enum RLNNode {
-    Registration(pallas::Base),
-    Slashing(pallas::Base),
-}
-
-pub fn process_commitment(node: RLNNode, identity_tree: &mut SmtMemoryFp) -> Result<()> {
-    match node {
-        RLNNode::Registration(commitment) => {
-            // Add to smt
-            let commitment = vec![commitment];
-            let commitment: Vec<_> = commitment.into_iter().map(|l| (l, l)).collect();
-            identity_tree.insert_batch(commitment)?;
-        }
-        RLNNode::Slashing(commitment) => {
-            // Remove from smt
-            let commitment = vec![commitment];
-            let commitment: Vec<_> = commitment.into_iter().map(|l| (l, l)).collect();
-            identity_tree.remove_leaves(commitment)?;
-        }
+    /// Check whether any share has been recorded for this nullifier
+    /// pair in the current epoch.
+    ///
+    /// In RLNv2, each `message_id` produces a unique `internal_nullifier`.
+    /// A repeated `internal_nullifier` means the user reused the same
+    /// message slot, which is a protocol violation that enables secret
+    /// recovery via SSS.
+    pub fn is_reused(&self, ext_null: &pallas::Base, int_null: &pallas::Base) -> bool {
+        self.data.get(ext_null).map(|m| m.contains_key(int_null)).unwrap_or(false)
     }
-
-    Ok(())
 }
 
+/// Create a ZK proof that a user's secret has been recovered (via SSS)
+/// and they should be slashed from the identity tree.
 pub fn create_slash_proof(
     secret: pallas::Base,
     user_msg_limit: u64,
-    identities_tree: &mut SmtMemoryFp,
+    identity_state: &mut IdentityState,
     slash_pk: &ProvingKey,
 ) -> Result<(Proof, pallas::Base)> {
-    let identity_secret_hash = poseidon_hash([secret, user_msg_limit.into()]);
-    let commitment = poseidon_hash([identity_secret_hash]);
-
-    let identity_root = identities_tree.root();
-    let identity_path = identities_tree.prove_membership(&commitment);
-    // TODO: Delete me later
-    assert!(identity_path.verify(&identity_root, &commitment, &commitment));
+    let ish = poseidon_hash([secret, user_msg_limit.into()]);
+    let commitment = poseidon_hash([ish]);
+    let root = identity_state.root();
+    let path = identity_state.prove_membership(&commitment);
 
     let witnesses = vec![
         Witness::Base(Value::known(secret)),
         Witness::Base(Value::known(pallas::Base::from(user_msg_limit))),
-        Witness::SparseMerklePath(Value::known(identity_path.path)),
+        Witness::SparseMerklePath(Value::known(path.path)),
     ];
+    let pi = vec![secret, pallas::Base::from(user_msg_limit), root];
+    let zkbin = ZkBinary::decode(RLN2_SLASH_ZKBIN, false)?;
+    let circuit = ZkCircuit::new(witnesses, &zkbin);
+    let proof = Proof::create(slash_pk, &[circuit], &pi, &mut OsRng)
+        .map_err(|e| Error::Custom(format!("Slash proof creation failed: {e}")))?;
+    Ok((proof, root))
+}
 
-    let public_inputs = vec![secret, pallas::Base::from(user_msg_limit), identity_root];
-
-    let slash_zkbin = ZkBinary::decode(RLN2_SLASH_ZKBIN, false)?;
-    let slash_circuit = ZkCircuit::new(witnesses, &slash_zkbin);
-
-    let proof = Proof::create(slash_pk, &[slash_circuit], &public_inputs, &mut OsRng).unwrap();
+/// Recover the secret from two or more `(x, y)` Shamir shares using
+/// Lagrange interpolation.
+///
+/// Returns an error if fewer than 2 shares are provided or if any two
+/// shares have the same x-coordinate (which would cause a zero
+/// division).
+pub fn sss_recover(shares: &[(pallas::Base, pallas::Base)]) -> Result<pallas::Base> {
+    if shares.len() < 2 {
+        return Err(Error::Custom("Need >1 share for SSS recovery".into()))
+    }
 
-    Ok((proof, identity_root))
-}
+    // Guard against duplicate x-coordinates
+    for i in 0..shares.len() {
+        for j in (i + 1)..shares.len() {
+            if shares[i].0 == shares[j].0 {
+                return Err(Error::Custom("Duplicate x-coordinates in SSS shares".into()))
+            }
+        }
+    }
 
-/// Recover secret using Shamir's secret sharing scheme
-pub fn sss_recover(shares: &[(pallas::Base, pallas::Base)]) -> pallas::Base {
     let mut secret = pallas::Base::zero();
-    for (j, share_j) in shares.iter().enumerate() {
-        let mut prod = pallas::Base::one();
-        for (i, share_i) in shares.iter().enumerate() {
+    for (j, sj) in shares.iter().enumerate() {
+        let mut basis = pallas::Base::one();
+        for (i, si) in shares.iter().enumerate() {
             if i != j {
-                prod *= share_i.0 * (share_i.0 - share_j.0).invert().unwrap();
+                basis *= si.0 * (si.0 - sj.0).invert().unwrap();
             }
         }
-
-        prod *= share_j.1;
-        secret += prod;
-    }
-
-    secret
-}
-
-/// Helper function to read or build register verifying key
-pub(super) fn _build_register_vk(sled_db: &sled::Db) -> Result<()> {
-    // sanity check
-    if sled_db.get("rlnv2-diff-register-vk")?.is_some() {
-        return Ok(())
+        secret += basis * sj.1;
     }
-    let register_zkbin = ZkBinary::decode(RLN2_REGISTER_ZKBIN, false).unwrap();
-    let register_empty_circuit =
-        ZkCircuit::new(empty_witnesses(&register_zkbin).unwrap(), &register_zkbin);
 
-    info!(target: "irc::server", "[RLN] Creating RlnV2_Diff_Register VerifyingKey");
-    let verifyingkey = VerifyingKey::build(register_zkbin.k, &register_empty_circuit);
-    let mut buf = vec![];
-    verifyingkey.write(&mut buf)?;
-    sled_db.insert("rlnv2-diff-register-vk", buf)?;
-    Ok(())
+    Ok(secret)
 }
 
-/// Helper function to read register verifying key
-pub(super) fn read_register_vk(sled_db: &sled::Db) -> Result<VerifyingKey> {
-    if let Some(vk) = sled_db.get("rlnv2-diff-register-vk")? {
-        let register_zkbin = ZkBinary::decode(RLN2_REGISTER_ZKBIN, false).unwrap();
-        let register_empty_circuit =
-            ZkCircuit::new(empty_witnesses(&register_zkbin).unwrap(), &register_zkbin);
-        let mut reader = Cursor::new(vk);
-        Ok(VerifyingKey::read(&mut reader, register_empty_circuit)?)
-    } else {
-        Err(Error::Custom("Error reading register verifying key".to_owned()))
-    }
+enum KeyKind {
+    Pk,
+    Vk,
 }
 
-/// Helper function to build signal verifying key
-pub(super) fn _build_signal_vk(sled_db: &sled::Db) -> Result<()> {
-    // sanity check
-    if sled_db.get("rlnv2-diff-signal-vk")?.is_some() {
+/// Build a key into sled if it doesn't already exist.
+fn ensure_key(sled_db: &sled::Db, key: &str, zkbin_bytes: &[u8], kind: KeyKind) -> Result<()> {
+    if sled_db.get(key)?.is_some() {
         return Ok(())
     }
-    let signal_zkbin = ZkBinary::decode(RLN2_SIGNAL_ZKBIN, false).unwrap();
-    let signal_empty_circuit =
-        ZkCircuit::new(empty_witnesses(&signal_zkbin).unwrap(), &signal_zkbin);
-
-    info!(target: "irc::server", "[RLN] Creating RlnV2_Diff_Signal VerifyingKey");
-    let verifyingkey = VerifyingKey::build(signal_zkbin.k, &signal_empty_circuit);
-    let mut buf = vec![];
-    verifyingkey.write(&mut buf)?;
-    sled_db.insert("rlnv2-diff-signal-vk", buf)?;
-    Ok(())
-}
-
-/// Helper function to read signal verifying key
-pub(super) fn read_signal_vk(sled_db: &sled::Db) -> Result<VerifyingKey> {
-    if let Some(vk) = sled_db.get("rlnv2-diff-signal-vk")? {
-        let signal_zkbin = ZkBinary::decode(RLN2_SIGNAL_ZKBIN, false).unwrap();
-        let signal_empty_circuit =
-            ZkCircuit::new(empty_witnesses(&signal_zkbin).unwrap(), &signal_zkbin);
-        let mut reader = Cursor::new(vk);
-        Ok(VerifyingKey::read(&mut reader, signal_empty_circuit)?)
-    } else {
-        Err(Error::Custom("Error Reading signal verifying key".to_owned()))
-    }
-}
 
-/// Helper function to build slash proving key
-pub(super) fn _build_slash_pk(sled_db: &sled::Db) -> Result<()> {
-    // sanity check
-    if sled_db.get("rlnv2-diff-slash-pk")?.is_some() {
-        return Ok(())
-    }
-    let slash_zkbin = ZkBinary::decode(RLN2_SLASH_ZKBIN, false).unwrap();
-    let slash_empty_circuit = ZkCircuit::new(empty_witnesses(&slash_zkbin).unwrap(), &slash_zkbin);
+    let zkbin = ZkBinary::decode(zkbin_bytes, false)?;
+    let circuit = ZkCircuit::new(empty_witnesses(&zkbin)?, &zkbin);
+    info!(target: "event_graph::rln", "[RLN] Building {key}");
 
-    info!(target: "irc::server", "[RLN] Creating RlnV2_Diff_Slash ProvingKey");
-    let verifyingkey = VerifyingKey::build(slash_zkbin.k, &slash_empty_circuit);
     let mut buf = vec![];
-    verifyingkey.write(&mut buf)?;
-    sled_db.insert("rlnv2-diff-slash-pk", buf)?;
-    Ok(())
-}
-
-/// Helper function to read slash proving key
-pub(super) fn read_slash_pk(sled_db: &sled::Db) -> Result<ProvingKey> {
-    if let Some(vk) = sled_db.get("rlnv2-diff-slash-pk")? {
-        let slash_zkbin = ZkBinary::decode(RLN2_SLASH_ZKBIN, false).unwrap();
-        let slash_empty_circuit =
-            ZkCircuit::new(empty_witnesses(&slash_zkbin).unwrap(), &slash_zkbin);
-        let mut reader = Cursor::new(vk);
-        Ok(ProvingKey::read(&mut reader, slash_empty_circuit)?)
-    } else {
-        Err(Error::Custom("Error Reading slash proving key".to_owned()))
+    match kind {
+        KeyKind::Pk => {
+            let pk = ProvingKey::build(zkbin.k, &circuit);
+            pk.write(&mut buf)?;
+        }
+        KeyKind::Vk => {
+            let vk = VerifyingKey::build(zkbin.k, &circuit);
+            vk.write(&mut buf)?;
+        }
     }
+    sled_db.insert(key, buf)?;
+    Ok(())
 }
 
-/// Helper function to build slash verifying key
-pub(super) fn _build_slash_vk(sled_db: &sled::Db) -> Result<()> {
-    // sanity check
-    if sled_db.get("rlnv2-diff-slash-vk")?.is_some() {
-        return Ok(())
-    }
-    let slash_zkbin = ZkBinary::decode(RLN2_SLASH_ZKBIN, false).unwrap();
-    let slash_empty_circuit = ZkCircuit::new(empty_witnesses(&slash_zkbin).unwrap(), &slash_zkbin);
-
-    info!(target: "irc::server", "[RLN] Creating RlnV2_Diff_Slash VerifyingKey");
-    let verifyingkey = VerifyingKey::build(slash_zkbin.k, &slash_empty_circuit);
-    let mut buf = vec![];
-    verifyingkey.write(&mut buf)?;
-    sled_db.insert("rlnv2-diff-slash-vk", buf)?;
-    Ok(())
+fn read_vk(sled_db: &sled::Db, key: &str, zkbin_bytes: &[u8]) -> Result<VerifyingKey> {
+    let bytes = sled_db.get(key)?.ok_or_else(|| Error::Custom(format!("{key} not found")))?;
+    let zkbin = ZkBinary::decode(zkbin_bytes, false)?;
+    let circuit = ZkCircuit::new(empty_witnesses(&zkbin)?, &zkbin);
+    Ok(VerifyingKey::read(&mut Cursor::new(bytes), circuit)?)
 }
 
-/// Helper function to read slash proving key
-pub(super) fn read_slash_vk(sled_db: &sled::Db) -> Result<VerifyingKey> {
-    if let Some(vk) = sled_db.get("rlnv2-diff-slash-pk")? {
-        let slash_zkbin = ZkBinary::decode(RLN2_SLASH_ZKBIN, false).unwrap();
-        let slash_empty_circuit =
-            ZkCircuit::new(empty_witnesses(&slash_zkbin).unwrap(), &slash_zkbin);
-        let mut reader = Cursor::new(vk);
-        Ok(VerifyingKey::read(&mut reader, slash_empty_circuit)?)
-    } else {
-        Err(Error::Custom("Error Reading slash verifying key".to_owned()))
-    }
+fn read_pk(sled_db: &sled::Db, key: &str, zkbin_bytes: &[u8]) -> Result<ProvingKey> {
+    let bytes = sled_db.get(key)?.ok_or_else(|| Error::Custom(format!("{key} not found")))?;
+    let zkbin = ZkBinary::decode(zkbin_bytes, false)?;
+    let circuit = ZkCircuit::new(empty_witnesses(&zkbin)?, &zkbin);
+    Ok(ProvingKey::read(&mut Cursor::new(bytes), circuit)?)
 }

+ 390 - 1138
src/event_graph/tests.rs

@@ -16,45 +16,54 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-// cargo test --release --features=event-graph --lib eventgraph_propagation -- --include-ignored
-
 use std::{
-    collections::{BTreeMap, HashMap, HashSet},
+    collections::{HashMap, HashSet},
     slice,
-    sync::Arc,
+    sync::{atomic::Ordering, Arc},
     time::{Duration, UNIX_EPOCH},
 };
 
-use darkfi_serial::{deserialize_async, serialize_async};
+use darkfi_serial::serialize_async;
 use rand::{prelude::SliceRandom, rngs::ThreadRng};
 use sled_overlay::sled;
 use smol::{channel, future, Executor};
-use tracing::{info, warn};
 use url::Url;
 
 use crate::{
     error::Result,
     event_graph::{
+        compute_unreferenced_tips,
         event::Header,
-        proto::{EventPut, ProtocolEventGraph},
-        util::next_rotation_timestamp,
-        DAGStore, Event, EventGraph, EventGraphPtr, DAGS_MAX_NUMBER, GENESIS_CONTENTS,
-        INITIAL_GENESIS, NULL_ID, N_EVENT_PARENTS,
+        proto::{EventPut, ProtocolEventGraph, SyncDirection},
+        util::next_hour_timestamp,
+        DagStore, Event, EventGraph, EventGraphConfig, EventGraphPtr, TimeIndex, NULL_ID,
+        NULL_PARENTS, N_EVENT_PARENTS,
     },
     net::{session::SESSION_DEFAULT, settings::NetworkProfile, P2p, Settings},
-    system::{msleep, sleep, timeout::timeout},
+    system::{sleep, timeout::timeout},
     util::logger::{setup_test_logger, Level},
-    Error,
 };
 
-// Number of nodes to spawn and number of peers each node connects to
 const N_NODES: usize = 5;
 const N_CONNS: usize = 2;
-//const N_NODES: usize = 50;
-//const N_CONNS: usize = N_NODES / 3;
+
+/// Test config: 15 Apr 2026 UTC, hourly rotation, 24-DAG window.
+fn test_config() -> EventGraphConfig {
+    EventGraphConfig {
+        initial_genesis: 1_776_211_200_000,
+        hours_rotation: 1,
+        genesis_contents: b"test-graph-v1".to_vec(),
+        max_dags: Some(24),
+    }
+}
+
+/// Archive-mode variant of the test config.
+fn archive_config() -> EventGraphConfig {
+    EventGraphConfig { max_dags: None, ..test_config() }
+}
 
 fn init_logger() {
-    let ignored_targets = [
+    let ignored = [
         "sled",
         "net::protocol_ping",
         "net::channel::subscribe_stop()",
@@ -70,24 +79,11 @@ fn init_logger() {
         "net::channel::main_receive_loop()",
         "net::tcp",
     ];
-    // We check this error so we can execute same file tests in parallel,
-    // otherwise second one fails to init logger here.
-    if setup_test_logger(
-        &ignored_targets,
-        false,
-        Level::Info,
-        //Level::Verbose,
-        //Level::Debug,
-        //Level::Tracing,
-    )
-    .is_err()
-    {
-        warn!(target: "test_harness", "Logger already initialized");
-    }
+    let _ = setup_test_logger(&ignored, false, Level::Info);
 }
 
 async fn spawn_node(
-    inbound_addrs: Vec<Url>,
+    inbound: Vec<Url>,
     peers: Vec<Url>,
     ex: Arc<Executor<'static>>,
 ) -> Arc<EventGraph> {
@@ -98,7 +94,7 @@ async fn spawn_node(
     );
     let settings = Settings {
         localnet: true,
-        inbound_addrs,
+        inbound_addrs: inbound,
         outbound_connections: 0,
         inbound_connections: usize::MAX,
         peers,
@@ -109,1231 +105,487 @@ async fn spawn_node(
 
     let p2p = P2p::new(settings, ex.clone()).await.unwrap();
     let sled_db = sled::Config::new().temporary(true).open().unwrap();
-    let event_graph =
-        EventGraph::new(p2p.clone(), sled_db, "/tmp".into(), false, false, 1, ex.clone())
-            .await
-            .unwrap();
-    *event_graph.synced.write().await = true;
-    let event_graph_ = event_graph.clone();
-
-    // Register the P2P protocols
-    let registry = p2p.protocol_registry();
-    registry
-        .register(SESSION_DEFAULT, move |channel, _| {
-            let event_graph_ = event_graph_.clone();
-            async move { ProtocolEventGraph::init(event_graph_, channel).await.unwrap() }
-        })
-        .await;
-
-    event_graph
-}
+    let eg = EventGraph::new(p2p.clone(), sled_db, "/tmp".into(), false, test_config(), ex.clone())
+        .await
+        .unwrap();
 
-async fn bootstrap_nodes(
-    peer_indexes: &[usize],
-    starting_port: usize,
-    rng: &mut ThreadRng,
-    ex: Arc<Executor<'static>>,
-) -> Vec<Arc<EventGraph>> {
-    let mut eg_instances = vec![];
+    // Mark as synced so protocol handlers accept events during tests
+    eg.synced.store(true, Ordering::Release);
 
-    // Initialize the nodes
-    for i in 0..N_NODES {
-        // Everyone will connect to N_CONNS random peers.
-        let mut peer_indexes_copy = peer_indexes.to_owned();
-        peer_indexes_copy.remove(i);
-        let peer_indexes_to_connect: Vec<_> =
-            peer_indexes_copy.choose_multiple(rng, N_CONNS).collect();
-
-        let mut peers = vec![];
-        for peer_index in peer_indexes_to_connect {
-            let port = starting_port + peer_index;
-            peers.push(Url::parse(&format!("tcp://127.0.0.1:{port}")).unwrap());
-        }
-
-        let event_graph = spawn_node(
-            vec![Url::parse(&format!("tcp://127.0.0.1:{}", starting_port + i)).unwrap()],
-            peers,
-            ex.clone(),
-        )
+    let eg_ = eg.clone();
+    p2p.protocol_registry()
+        .register(SESSION_DEFAULT, move |channel, _| {
+            let eg_ = eg_.clone();
+            async move { ProtocolEventGraph::init(eg_, channel).await.unwrap() }
+        })
         .await;
-
-        eg_instances.push(event_graph);
-    }
-
-    // Start the P2P network
-    for eg in eg_instances.iter() {
-        eg.p2p.clone().start().await.unwrap();
-    }
-
-    info!("Waiting 5s until all peers connect");
-    sleep(5).await;
-
-    eg_instances
-}
-
-async fn assert_dags(eg_instances: &[Arc<EventGraph>], expected_len: usize, rng: &mut ThreadRng) {
-    let random_node = eg_instances.choose(rng).unwrap();
-    let random_node_genesis = random_node.current_genesis.read().await.header.timestamp;
-    let store = random_node.dag_store.read().await;
-    let (_, unreferenced_tips) = store.main_dags.get(&random_node_genesis).unwrap();
-    let last_layer_tips = unreferenced_tips.last_key_value().unwrap().1.clone();
-    for (i, eg) in eg_instances.iter().enumerate() {
-        let current_genesis = eg.current_genesis.read().await;
-        let dag_name = current_genesis.header.timestamp.to_string();
-        let dag = eg.dag_store.read().await.get_dag(&dag_name);
-        let unreferenced_tips = eg.dag_store.read().await.find_unreferenced_tips(&dag).await;
-        let node_last_layer_tips = unreferenced_tips.last_key_value().unwrap().1.clone();
-        assert!(
-            dag.len() == expected_len,
-            "Node {i}, expected {expected_len} events, have {}",
-            dag.len()
-        );
-        assert_eq!(
-            node_last_layer_tips, last_layer_tips,
-            "Node {i} contains malformed unreferenced tips"
-        );
-    }
-}
-
-macro_rules! test_body {
-    ($real_call:ident) => {
-        init_logger();
-
-        let ex = Arc::new(Executor::new());
-        let ex_ = ex.clone();
-        let (signal, shutdown) = channel::unbounded::<()>();
-
-        // Run a thread for each node.
-        easy_parallel::Parallel::new()
-            .each(0..N_NODES, |_| future::block_on(ex.run(shutdown.recv())))
-            .finish(|| {
-                future::block_on(async {
-                    $real_call(ex_).await;
-                    drop(signal);
-                })
-            });
-    };
+    eg
 }
 
 #[test]
-fn eventgraph_propagation() {
-    test_body!(eventgraph_propagation_real);
-}
-
-async fn eventgraph_propagation_real(ex: Arc<Executor<'static>>) {
-    let mut rng = rand::thread_rng();
-    let peer_indexes: Vec<usize> = (0..N_NODES).collect();
-
-    // Bootstrap nodes
-    let mut eg_instances = bootstrap_nodes(&peer_indexes, 13200, &mut rng, ex.clone()).await;
-
-    // Grab genesis event
-    let random_node = eg_instances.choose(&mut rng).unwrap();
-    let current_genesis = random_node.current_genesis.read().await;
-    let dag_name = current_genesis.header.timestamp.to_string();
-    let (id, _) = random_node.dag_store.read().await.get_dag(&dag_name).last().unwrap().unwrap();
-    let genesis_event_id = blake3::Hash::from_bytes((&id as &[u8]).try_into().unwrap());
-
-    drop(current_genesis);
-
-    // =========================================
-    // 1. Assert that everyone's DAG is the same
-    // =========================================
-    assert_dags(&eg_instances, 1, &mut rng).await;
-
-    // ==========================================
-    // 2. Create an event in one node and publish
-    // ==========================================
-    let random_node = eg_instances.choose(&mut rng).unwrap();
-    let current_genesis = random_node.current_genesis.read().await;
-    let dag_name = current_genesis.header.timestamp.to_string();
-    let event = Event::new(vec![1, 2, 3, 4], random_node).await;
-    assert!(event.header.parents.contains(&genesis_event_id));
-    // The node adds it to their DAG, on layer 1.
-    random_node.header_dag_insert(vec![event.header.clone()], &dag_name).await.unwrap();
-    let event_id = random_node.dag_insert(slice::from_ref(&event), &dag_name).await.unwrap()[0];
-
-    let store = random_node.dag_store.read().await;
-    let (_, tips_layers) = store.header_dags.get(&current_genesis.header.timestamp).unwrap();
-
-    // Since genesis was referenced, its layer (0) have been removed
-    assert_eq!(tips_layers.len(), 1);
-    assert!(tips_layers.last_key_value().unwrap().1.get(&event_id).is_some());
-    drop(store);
-    drop(current_genesis);
-    info!("Broadcasting event {event_id}");
-    random_node.p2p.broadcast(&EventPut(event, vec![])).await;
-    info!("Waiting 5s for event propagation");
-    sleep(5).await;
-
-    // ====================================================
-    // 3. Assert that everyone has the new event in the DAG
-    // ====================================================
-    assert_dags(&eg_instances, 2, &mut rng).await;
-
-    // ==============================================================
-    // 4. Create multiple events on a node and broadcast the last one
-    //    The `EventPut` logic should manage to fetch all of them,
-    //    provided that the last one references the earlier ones.
-    // ==============================================================
-    let random_node = eg_instances.choose(&mut rng).unwrap();
-    let event0 = Event::new(vec![1, 2, 3, 4, 0], random_node).await;
-    random_node.header_dag_insert(vec![event0.header.clone()], &dag_name).await.unwrap();
-    let event0_id = random_node.dag_insert(slice::from_ref(&event0), &dag_name).await.unwrap()[0];
-    let event1 = Event::new(vec![1, 2, 3, 4, 1], random_node).await;
-    random_node.header_dag_insert(vec![event1.header.clone()], &dag_name).await.unwrap();
-    let event1_id = random_node.dag_insert(slice::from_ref(&event1), &dag_name).await.unwrap()[0];
-    let event2 = Event::new(vec![1, 2, 3, 4, 2], random_node).await;
-    random_node.header_dag_insert(vec![event2.header.clone()], &dag_name).await.unwrap();
-    let event2_id = random_node.dag_insert(slice::from_ref(&event2), &dag_name).await.unwrap()[0];
-    // Genesis event + event from 2. + upper 3 events (layer 4)
-    let current_genesis = random_node.current_genesis.read().await;
-    let dag_name = current_genesis.header.timestamp.to_string();
-    assert_eq!(random_node.dag_store.read().await.get_dag(&dag_name).len(), 5);
-    let random_node_genesis = random_node.current_genesis.read().await.header.timestamp;
-    let store = random_node.dag_store.read().await;
-    let (_, tips_layers) = store.header_dags.get(&random_node_genesis).unwrap();
-    assert_eq!(tips_layers.len(), 1);
-    assert!(tips_layers.get(&4).unwrap().get(&event2_id).is_some());
-    drop(current_genesis);
-    drop(store);
-
-    let event_chain = vec![
-        (event0_id, event0.header.parents),
-        (event1_id, event1.header.parents),
-        (event2_id, event2.header.parents),
-    ];
-
-    info!("Broadcasting event {event2_id}");
-    info!("Event chain: {event_chain:#?}");
-    random_node.p2p.broadcast(&EventPut(event2, vec![])).await;
-    info!("Waiting 5s for event propagation");
-    sleep(5).await;
-
-    // ==========================================
-    // 5. Assert that everyone has all the events
-    // ==========================================
-    assert_dags(&eg_instances, 5, &mut rng).await;
-
-    // ===========================================
-    // 6. Create multiple events on multiple nodes
-    // ===========================================
-    // node 1
-    // =======
-    let node1 = eg_instances.choose(&mut rng).unwrap();
-    let event0_1 = Event::new(vec![1, 2, 3, 4, 3], node1).await;
-    node1.header_dag_insert(vec![event0_1.header.clone()], &dag_name).await.unwrap();
-    node1.dag_insert(slice::from_ref(&event0_1), &dag_name).await.unwrap();
-    node1.p2p.broadcast(&EventPut(event0_1, vec![])).await;
-    msleep(300).await;
-
-    let event1_1 = Event::new(vec![1, 2, 3, 4, 4], node1).await;
-    node1.header_dag_insert(vec![event1_1.header.clone()], &dag_name).await.unwrap();
-    node1.dag_insert(slice::from_ref(&event1_1), &dag_name).await.unwrap();
-    node1.p2p.broadcast(&EventPut(event1_1, vec![])).await;
-    msleep(300).await;
-
-    let event2_1 = Event::new(vec![1, 2, 3, 4, 5], node1).await;
-    node1.header_dag_insert(vec![event2_1.header.clone()], &dag_name).await.unwrap();
-    node1.dag_insert(slice::from_ref(&event2_1), &dag_name).await.unwrap();
-    node1.p2p.broadcast(&EventPut(event2_1, vec![])).await;
-    msleep(300).await;
-
-    // =======
-    // node 2
-    // =======
-    let node2 = eg_instances.choose(&mut rng).unwrap();
-    let event0_2 = Event::new(vec![1, 2, 3, 4, 6], node2).await;
-    node2.header_dag_insert(vec![event0_2.header.clone()], &dag_name).await.unwrap();
-    node2.dag_insert(slice::from_ref(&event0_2), &dag_name).await.unwrap();
-    node2.p2p.broadcast(&EventPut(event0_2, vec![])).await;
-    msleep(300).await;
-
-    let event1_2 = Event::new(vec![1, 2, 3, 4, 7], node2).await;
-    node2.header_dag_insert(vec![event1_2.header.clone()], &dag_name).await.unwrap();
-    node2.dag_insert(slice::from_ref(&event1_2), &dag_name).await.unwrap();
-    node2.p2p.broadcast(&EventPut(event1_2, vec![])).await;
-    msleep(300).await;
-
-    let event2_2 = Event::new(vec![1, 2, 3, 4, 8], node2).await;
-    node2.header_dag_insert(vec![event2_2.header.clone()], &dag_name).await.unwrap();
-    node2.dag_insert(slice::from_ref(&event2_2), &dag_name).await.unwrap();
-    node2.p2p.broadcast(&EventPut(event2_2, vec![])).await;
-    msleep(300).await;
-
-    // =======
-    // node 3
-    // =======
-    let node3 = eg_instances.choose(&mut rng).unwrap();
-    let event0_3 = Event::new(vec![1, 2, 3, 4, 9], node3).await;
-    node3.header_dag_insert(vec![event0_3.header.clone()], &dag_name).await.unwrap();
-    node3.dag_insert(slice::from_ref(&event0_3), &dag_name).await.unwrap();
-    node3.p2p.broadcast(&EventPut(event0_3, vec![])).await;
-    msleep(300).await;
-
-    let event1_3 = Event::new(vec![1, 2, 3, 4, 10], node3).await;
-    node3.header_dag_insert(vec![event1_3.header.clone()], &dag_name).await.unwrap();
-    node3.dag_insert(slice::from_ref(&event1_3), &dag_name).await.unwrap();
-    node3.p2p.broadcast(&EventPut(event1_3, vec![])).await;
-    msleep(300).await;
-
-    let event2_3 = Event::new(vec![1, 2, 3, 4, 11], node3).await;
-    node3.header_dag_insert(vec![event2_3.header.clone()], &dag_name).await.unwrap();
-    node3.dag_insert(slice::from_ref(&event2_3), &dag_name).await.unwrap();
-    node3.p2p.broadcast(&EventPut(event2_3, vec![])).await;
-    msleep(300).await;
-
-    // /////
-    // //
-    // let node4 = eg_instances.choose(&mut rng).unwrap();
-    // let event0_4 = Event::new(vec![1, 2, 3, 4, 12], node4).await;
-    // node4.dag_insert(&[event0_4.clone()]).await.unwrap();
-    // node4.p2p.broadcast(&EventPut(event0_4)).await;
-    // sleep(1).await;
-
-    // let event1_4 = Event::new(vec![1, 2, 3, 4, 13], node4).await;
-    // node4.dag_insert(&[event1_4.clone()]).await.unwrap();
-    // node4.p2p.broadcast(&EventPut(event1_4)).await;
-    // sleep(1).await;
-
-    // let event2_4 = Event::new(vec![1, 2, 3, 4, 14], node4).await;
-    // node4.dag_insert(&[event2_4.clone()]).await.unwrap();
-    // node4.p2p.broadcast(&EventPut(event2_4)).await;
-    // // sleep(1).await;
-
-    // ==========================================
-    // 7. Assert that everyone has all the events
-    // ==========================================
-    // 5 events from 2. and 4. + 9 events from 6. = 14
-    assert_dags(&eg_instances, 14, &mut rng).await;
-
-    // ============================================================
-    // 8. Start a new node and try to sync the DAG from other peers
-    // ============================================================
-    {
-        // Connect to N_CONNS random peers.
-        let peer_indexes_to_connect: Vec<_> =
-            peer_indexes.choose_multiple(&mut rng, N_CONNS).collect();
-
-        let mut peers = vec![];
-        for peer_index in peer_indexes_to_connect {
-            let port = 13200 + peer_index;
-            peers.push(Url::parse(&format!("tcp://127.0.0.1:{port}")).unwrap());
-        }
-
-        let event_graph = spawn_node(
-            vec![Url::parse(&format!("tcp://127.0.0.1:{}", 13200 + N_NODES + 1)).unwrap()],
-            peers,
-            ex.clone(),
-        )
-        .await;
-
-        eg_instances.push(event_graph.clone());
-
-        event_graph.p2p.clone().start().await.unwrap();
-
-        info!("Waiting 5s for new node connection");
-        sleep(5).await;
-
-        event_graph.sync_selected(1, false).await.unwrap();
+fn evgr_time_index_bidirectional_queries() {
+    let mut idx = TimeIndex::new();
+    for ts in [100_u64, 200, 200, 300, 400, 500] {
+        let id = blake3::hash(&ts.to_be_bytes());
+        idx.insert(ts, id);
     }
 
-    // ============================================================
-    // 9. Assert the new synced DAG has the same contents as others
-    // ============================================================
-    // 5 events from 2. and 4. + 9 events from 6. = 14
-    assert_dags(&eg_instances, 14, &mut rng).await;
-
-    // Stop the P2P network
-    for eg in eg_instances.iter() {
-        eg.p2p.clone().stop().await;
-    }
+    assert_eq!(idx.len(), 6);
+    assert_eq!(idx.newest(3).len(), 3);
+    assert_eq!(idx.oldest(2).len(), 2);
+    // Before 300 -> events at 200 (x2) and 100
+    assert_eq!(idx.before(300, 10).len(), 3);
+    // After 200 -> events at 300, 400, 500
+    assert_eq!(idx.after(200, 10).len(), 3);
 }
 
 #[test]
-#[ignore]
-fn eventgraph_chaotic_propagation() {
-    test_body!(eventgraph_chaotic_propagation_real);
+fn evgr_time_index_saturating_cursor() {
+    let mut idx = TimeIndex::new();
+    idx.insert(100, blake3::hash(b"x"));
+
+    // before(0) should not underflow
+    assert_eq!(idx.before(0, 10).len(), 0);
+    // after(u64::MAX) should not overflow
+    assert_eq!(idx.after(u64::MAX, 10).len(), 0);
 }
 
-async fn eventgraph_chaotic_propagation_real(ex: Arc<Executor<'static>>) {
-    let mut rng = rand::thread_rng();
-    let peer_indexes: Vec<usize> = (0..N_NODES).collect();
-    let n_events: usize = 100000;
-
-    // Bootstrap nodes
-    let mut eg_instances = bootstrap_nodes(&peer_indexes, 14200, &mut rng, ex.clone()).await;
-
-    // =========================================
-    // 1. Assert that everyone's DAG is the same
-    // =========================================
-    assert_dags(&eg_instances, 1, &mut rng).await;
-
-    // ===========================================
-    // 2. Create multiple events on multiple nodes
-    for i in 0..n_events {
-        let random_node = eg_instances.choose(&mut rng).unwrap();
-        let event = Event::new(i.to_be_bytes().to_vec(), random_node).await;
-        let current_genesis = random_node.current_genesis.read().await;
-        let dag_name = current_genesis.header.timestamp.to_string();
-        random_node.header_dag_insert(vec![event.header.clone()], &dag_name).await.unwrap();
-        random_node.dag_insert(slice::from_ref(&event), &dag_name).await.unwrap();
-        random_node.p2p.broadcast(&EventPut(event, vec![])).await;
-    }
-    info!("Waiting 5s for events propagation");
-    sleep(5).await;
-
-    // ==========================================
-    // 3. Assert that everyone has all the events
-    // ==========================================
-    assert_dags(&eg_instances, n_events + 1, &mut rng).await;
-
-    // ============================================================
-    // 4. Start a new node and try to sync the DAG from other peers
-    // ============================================================
-    {
-        // Connect to N_CONNS random peers.
-        let peer_indexes_to_connect: Vec<_> =
-            peer_indexes.choose_multiple(&mut rng, N_CONNS).collect();
-
-        let mut peers = vec![];
-        for peer_index in peer_indexes_to_connect {
-            let port = 14200 + peer_index;
-            peers.push(Url::parse(&format!("tcp://127.0.0.1:{port}")).unwrap());
-        }
-
-        let event_graph = spawn_node(
-            vec![Url::parse(&format!("tcp://127.0.0.1:{}", 14200 + N_NODES + 1)).unwrap()],
-            peers,
-            ex.clone(),
-        )
-        .await;
-
-        eg_instances.push(event_graph.clone());
-
-        event_graph.p2p.clone().start().await.unwrap();
-
-        info!("Waiting 5s for new node connection");
-        sleep(5).await;
-
-        event_graph.sync_selected(2, false).await.unwrap()
-    }
-
-    // ============================================================
-    // 5. Assert the new synced DAG has the same contents as others
-    // ============================================================
-    assert_dags(&eg_instances, n_events + 1, &mut rng).await;
-
-    // Stop the P2P network
-    for eg in eg_instances.iter() {
-        eg.p2p.clone().stop().await;
-    }
-}
-
-// DAGStore tests
-async fn make_dag_store() -> Result<DAGStore> {
+async fn make_dag_store() -> Result<DagStore> {
     let sled_db = sled::Config::new().temporary(true).open()?;
-    let hours_rotation = 1;
-
-    let dag_store = DAGStore {
-        db: sled_db.clone(),
-        header_dags: BTreeMap::default(),
-        main_dags: BTreeMap::default(),
-    }
-    .new(sled_db.clone(), hours_rotation)
-    .await;
-
-    Ok(dag_store)
+    Ok(DagStore::new(sled_db, &test_config()).await)
 }
+
 #[test]
-fn header_dags_and_main_dags_length_equals_dags_max_number() -> Result<()> {
+fn evgr_dag_store_creates_rolling_window() -> Result<()> {
     smol::block_on(async {
-        let dag_store = make_dag_store().await?;
-        assert_eq!(dag_store.header_dags.len() as i8, DAGS_MAX_NUMBER);
-        assert_eq!(dag_store.main_dags.len() as i8, DAGS_MAX_NUMBER);
-
+        let store = make_dag_store().await?;
+        assert_eq!(store.dag_timestamps().len(), 24);
         Ok(())
     })
 }
 
 #[test]
-fn all_dag_trees_are_created_on_sled_after_dag_store_creation() -> Result<()> {
+fn evgr_dag_store_all_slots_have_genesis() -> Result<()> {
     smol::block_on(async {
-        let dag_store = make_dag_store().await?;
-        let dag_trees: Vec<String> =
-            dag_store.db.tree_names().iter().map(|n| String::from_utf8_lossy(n).into()).collect();
-
-        // Should have 2 * DAGS_MAX_NUMBER trees + 1 (the default tree)
-        assert_eq!(dag_trees.len() as i8, DAGS_MAX_NUMBER * 2 + 1);
-
-        for (dag_timestamp, _) in dag_store.header_dags {
-            assert!(dag_trees.contains(&format!("headers_{dag_timestamp}")));
+        let store = make_dag_store().await?;
+        for ts in store.dag_timestamps() {
+            let slot = store.get_slot(&ts).unwrap();
+            assert!(!slot.header_tree.is_empty());
+            assert!(!slot.main_tree.is_empty());
+            assert!(!slot.tips.is_empty());
+            assert!(!slot.time_index.is_empty());
         }
-
-        for (dag_timestamp, _) in dag_store.main_dags {
-            assert!(dag_trees.contains(&dag_timestamp.to_string()));
-        }
-
         Ok(())
     })
 }
 
 #[test]
-fn genesis_events_or_headers_are_added_to_all_trees_and_utips() -> Result<()> {
+fn evgr_dag_store_add_drops_oldest_in_bounded_mode() -> Result<()> {
     smol::block_on(async {
-        let dag_store = make_dag_store().await?;
-
-        for (_, (tree, layer_utips)) in dag_store.header_dags {
-            let genesis_header = tree.first()?;
-            // A Genesis Header is found in sled tree
-            assert!(genesis_header.is_some());
-            let (genesis_hash, genesis_header) = genesis_header.unwrap();
-            let genesis_header: Header = deserialize_async(&genesis_header).await?;
-            let genesis_hash: blake3::Hash = deserialize_async(&genesis_hash).await?;
-            assert_eq!(genesis_header.layer, 0);
-            assert!(genesis_header.parents.iter().all(|p| *p == NULL_ID));
-            // The Genesis Header hash is stored as Unreferenced tip
-            assert!(layer_utips.contains_key(&0));
-            assert!(layer_utips.get(&0).unwrap().contains(&genesis_hash));
-        }
-
-        for (_, (tree, layer_utips)) in dag_store.main_dags {
-            let genesis_event = tree.first()?;
-            // A Genesis Event is found in sled tree
-            assert!(genesis_event.is_some());
-            let (genesis_hash, genesis_event) = genesis_event.unwrap();
-            let genesis_event: Event = deserialize_async(&genesis_event).await?;
-            let genesis_hash: blake3::Hash = deserialize_async(&genesis_hash).await?;
-            assert_eq!(genesis_event.header.layer, 0);
-            assert!(genesis_event.header.parents.iter().all(|p| *p == NULL_ID));
-            assert_eq!(genesis_event.content, GENESIS_CONTENTS);
-            // The Genesis Header hash is stored as Unreferenced tip
-            assert!(layer_utips.contains_key(&0));
-            assert!(layer_utips.get(&0).unwrap().contains(&genesis_hash));
-        }
+        let mut store = make_dag_store().await?;
+        let oldest_ts = store.dag_timestamps()[0];
+        let new_ts = next_hour_timestamp(1);
+        let hdr = Header {
+            timestamp: new_ts,
+            parents: NULL_PARENTS,
+            layer: 0,
+            content_hash: blake3::hash(b"test-graph-v1"),
+        };
+        let genesis = Event { header: hdr, content: b"test-graph-v1".to_vec() };
+        store.add_dag(&genesis, Some(24)).await;
 
+        assert_eq!(store.dag_timestamps().len(), 24);
+        assert!(store.get_slot(&new_ts).is_some());
+        assert!(store.get_slot(&oldest_ts).is_none());
         Ok(())
     })
 }
 
 #[test]
-fn adding_new_dag_removes_oldest_dag_tree() -> Result<()> {
+fn evgr_dag_store_archive_mode_never_drops() -> Result<()> {
     smol::block_on(async {
-        let mut dag_store = make_dag_store().await?;
-        let oldest_dag_timestamp = dag_store.main_dags.first_key_value().unwrap().0.to_owned();
-        // Next dag to add
-        let next_rotation = next_rotation_timestamp(INITIAL_GENESIS, 1);
-        let header =
-            Header { timestamp: next_rotation, parents: [NULL_ID; N_EVENT_PARENTS], layer: 0 };
-        let next_genesis = Event { header, content: GENESIS_CONTENTS.to_vec() };
-
-        dag_store.add_dag(&next_genesis.header.timestamp.to_string(), &next_genesis).await;
-
-        // The length of the dags should stay the same after adding
-        assert_eq!(dag_store.main_dags.len() as i8, DAGS_MAX_NUMBER);
-        assert_eq!(dag_store.header_dags.len() as i8, DAGS_MAX_NUMBER);
-        // We should have an entry with the new dag timestamp
-        assert!(dag_store.main_dags.contains_key(&next_rotation));
-        assert!(dag_store.header_dags.contains_key(&next_rotation));
-        // The oldest dag entry should have been removed
-        assert!(!dag_store.main_dags.contains_key(&oldest_dag_timestamp));
-        assert!(!dag_store.header_dags.contains_key(&oldest_dag_timestamp));
-
-        let dag_trees: Vec<String> =
-            dag_store.db.tree_names().iter().map(|n| String::from_utf8_lossy(n).into()).collect();
-
-        // The number of dag trees should stay the same after adding
-        assert_eq!(dag_trees.len() as i8, 2 * DAGS_MAX_NUMBER + 1);
-        // We should have a tree with the new dag timestamp value
-        assert!(dag_trees.contains(&next_rotation.to_string()));
-        assert!(dag_trees.contains(&format!("headers_{next_rotation}")));
-        // The oldest dag sled tree should have been removed
-        assert!(!dag_trees.contains(&oldest_dag_timestamp.to_string()));
-        assert!(!dag_trees.contains(&format!("headers_{oldest_dag_timestamp}")));
+        let sled_db = sled::Config::new().temporary(true).open()?;
+        let mut store = DagStore::new(sled_db, &archive_config()).await;
+        let initial = store.dag_timestamps().len();
+
+        // Add DAGs well beyond the normal 24-window
+        for i in 1..=30i64 {
+            let ts = next_hour_timestamp(i);
+            let hdr = Header {
+                timestamp: ts,
+                parents: NULL_PARENTS,
+                layer: 0,
+                content_hash: blake3::hash(b"test-graph-v1"),
+            };
+            let genesis = Event { header: hdr, content: b"test-graph-v1".to_vec() };
+            store.add_dag(&genesis, None).await;
+        }
 
+        // Nothing should have been dropped
+        assert_eq!(store.dag_timestamps().len(), initial + 30);
         Ok(())
     })
 }
 
 #[test]
-fn sort_moves_current_dag_to_front() -> Result<()> {
+fn evgr_dag_store_archive_mode_discovers_existing_trees() -> Result<()> {
     smol::block_on(async {
-        let dag_store = make_dag_store().await?;
-
-        let trees = dag_store.sort_dags().await;
-        let first_tree_name: String =
-            String::from_utf8_lossy(&trees.first().unwrap().name()).into();
-        assert_eq!(
-            first_tree_name,
-            dag_store.main_dags.last_key_value().unwrap().0.to_owned().to_string()
-        );
+        let sled_db = sled::Config::new().temporary(true).open()?;
+
+        // First run: create archive store and add some historical DAGs
+        let historical_ts = next_hour_timestamp(-100);
+        {
+            let mut store = DagStore::new(sled_db.clone(), &archive_config()).await;
+            let hdr = Header {
+                timestamp: historical_ts,
+                parents: NULL_PARENTS,
+                layer: 0,
+                content_hash: blake3::hash(b"test-graph-v1"),
+            };
+            let genesis = Event { header: hdr, content: b"test-graph-v1".to_vec() };
+            store.add_dag(&genesis, None).await;
+            drop(store);
+        }
 
+        // Second run: reopen and verify the historical DAG is discovered
+        let store = DagStore::new(sled_db, &archive_config()).await;
+        assert!(
+            store.get_slot(&historical_ts).is_some(),
+            "Archive mode should discover historical DAGs on restart"
+        );
         Ok(())
     })
 }
 
 #[test]
-fn unreferenced_tips_are_found() -> Result<()> {
+fn evgr_compute_unreferenced_tips_single_pass() -> Result<()> {
     smol::block_on(async {
-        let dag_store = make_dag_store().await?;
-
-        let current_dag_tree = dag_store.main_dags.last_key_value().unwrap().1 .0.clone();
-        let current_dag_genesis_hash = *dag_store
-            .main_dags
-            .last_key_value()
-            .unwrap()
-            .1
-             .1
-            .get(&0)
-            .unwrap()
-            .iter()
-            .next()
-            .unwrap();
-
-        let mut parents = [NULL_ID; N_EVENT_PARENTS];
-        parents[0] = current_dag_genesis_hash;
-        let event2 = Event {
+        let store = make_dag_store().await?;
+        let ts = *store.dag_timestamps().last().unwrap();
+        let slot = store.get_slot(&ts).unwrap();
+        let genesis_hash = *slot.tips.get(&0).unwrap().iter().next().unwrap();
+
+        // Build a small DAG manually:
+        //      genesis
+        //      /     \
+        //     e2      e4  (both at layer 1)
+        //      |
+        //     e3        (layer 2)
+        //
+        // Header IDs include content_hash, so events with identical
+        // (timestamp, parents, layer) but different content get
+        // distinct IDs.
+        let now = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
+
+        let mut p = [NULL_ID; N_EVENT_PARENTS];
+        p[0] = genesis_hash;
+        let e2 = Event {
             header: Header {
-                timestamp: UNIX_EPOCH.elapsed().unwrap().as_millis() as u64,
-                parents,
+                timestamp: now,
+                parents: p,
                 layer: 1,
+                content_hash: blake3::hash(b"e2"),
             },
-            content: "event2".as_bytes().to_vec(),
+            content: b"e2".to_vec(),
         };
-        let event2_hash = event2.id();
-        current_dag_tree.insert(event2_hash.as_bytes(), serialize_async(&event2).await)?;
+        slot.main_tree.insert(e2.id().as_bytes(), serialize_async(&e2).await)?;
 
-        let mut parents = [NULL_ID; N_EVENT_PARENTS];
-        parents[0] = event2_hash;
-        let event3 = Event {
+        let mut p = [NULL_ID; N_EVENT_PARENTS];
+        p[0] = e2.id();
+        let e3 = Event {
             header: Header {
-                timestamp: UNIX_EPOCH.elapsed().unwrap().as_millis() as u64,
-                parents,
+                timestamp: now,
+                parents: p,
                 layer: 2,
+                content_hash: blake3::hash(b"e3"),
             },
-            content: "event3".as_bytes().to_vec(),
+            content: b"e3".to_vec(),
         };
-        let event3_hash = event3.id();
-        current_dag_tree.insert(event3_hash.as_bytes(), serialize_async(&event3).await)?;
+        slot.main_tree.insert(e3.id().as_bytes(), serialize_async(&e3).await)?;
 
-        let mut parents = [NULL_ID; N_EVENT_PARENTS];
-        parents[0] = current_dag_genesis_hash;
-        let event4 = Event {
+        let mut p = [NULL_ID; N_EVENT_PARENTS];
+        p[0] = genesis_hash;
+        let e4 = Event {
             header: Header {
-                timestamp: UNIX_EPOCH.elapsed().unwrap().as_millis() as u64,
-                parents,
-                layer: 2,
+                timestamp: now,
+                parents: p,
+                layer: 1,
+                content_hash: blake3::hash(b"e4"),
             },
-            content: "event4".as_bytes().to_vec(),
+            content: b"e4".to_vec(),
         };
-        let event4_hash = event4.id();
-        current_dag_tree.insert(event4_hash.as_bytes(), serialize_async(&event4).await)?;
-
-        let layer_utips = dag_store.find_unreferenced_tips(&current_dag_tree).await;
-        // We have unreferenced tips only on the 2nd layer
-        assert_eq!(layer_utips.len(), 1);
-        // We have two unreferenced tips
-        let tip_hashes = layer_utips.get(&2).unwrap();
-        assert_eq!(tip_hashes.len(), 2);
-        // Event3 and Event4 are the only unreferenced tips
-        assert!(tip_hashes.contains(&event3_hash));
-        assert!(tip_hashes.contains(&event4_hash));
+        slot.main_tree.insert(e4.id().as_bytes(), serialize_async(&e4).await)?;
+
+        assert_ne!(e2.id(), e4.id(), "e2 and e4 must have distinct IDs");
+
+        let tips = compute_unreferenced_tips(&slot.main_tree).await;
 
+        // e3 (layer 2) and e4 (layer 1) are unreferenced;
+        // e2 is a parent of e3, so it's not a tip.
+        assert!(tips.get(&2).unwrap().contains(&e3.id()));
+        assert!(tips.get(&1).unwrap().contains(&e4.id()));
+        assert!(!tips.values().any(|set| set.contains(&e2.id())));
         Ok(())
     })
 }
 
-// EventGraph tests
 async fn make_event_graph() -> Result<EventGraphPtr> {
     let ex = Arc::new(Executor::new());
     let p2p = P2p::new(Settings::default(), ex.clone()).await?;
     let sled_db = sled::Config::new().temporary(true).open()?;
-    EventGraph::new(p2p, sled_db, "/tmp".into(), false, false, 1, ex).await
+    EventGraph::new(p2p, sled_db, "/tmp".into(), false, test_config(), ex).await
 }
 
 #[test]
-fn dag_insert_on_invalid_dag_name() -> Result<()> {
+fn evgr_dag_insert_valid_event() -> Result<()> {
     smol::block_on(async {
-        let event_graph = make_event_graph().await?;
-
-        let new_event = Event::new("new_event".as_bytes().to_vec(), &event_graph).await;
-        // Using a dag name that is not a u64 timestamp gives an error
-        let res = event_graph.dag_insert(&[new_event], "non_timestamp_dag_name").await;
-        assert!(res.is_err());
-        let err = res.unwrap_err();
-        match err {
-            Error::ParseIntError(_) => {}
-            _ => panic!("expected parse error"),
-        }
-
+        let eg = make_event_graph().await?;
+        let dag_ts = eg.current_genesis.read().await.header.timestamp;
+        let dag_name = dag_ts.to_string();
+        let sub = eg.event_pub.clone().subscribe().await;
+
+        let event = Event::new(b"hello".to_vec(), &eg).await;
+        eg.header_dag_insert(vec![event.header.clone()], &dag_name).await?;
+        let ids = eg.dag_insert(slice::from_ref(&event), &dag_name).await?;
+        assert_eq!(ids.len(), 1);
+
+        // Tips updated to include the new event
+        let store = eg.dag_store.read().await;
+        let slot = store.get_slot(&dag_ts).unwrap();
+        assert!(slot.tips.get(&1).unwrap().contains(&event.id()));
+        drop(store);
+
+        // Publisher notified
+        let Ok(notified) = timeout(Duration::from_secs(1), sub.receive()).await else {
+            panic!("Event notification not received");
+        };
+        assert_eq!(notified.id(), event.id());
         Ok(())
     })
 }
 
 #[test]
-fn invalid_header_dag_insert() -> Result<()> {
+fn evgr_dag_insert_duplicate_skipped() -> Result<()> {
     smol::block_on(async {
-        let event_graph = make_event_graph().await?;
-        let dag_name = event_graph
-            .dag_store
-            .read()
-            .await
-            .main_dags
-            .last_key_value()
-            .unwrap()
-            .0
-            .clone()
-            .to_string();
-
-        let new_event = Event::new("new_event".as_bytes().to_vec(), &event_graph).await;
-        // Inserting an invalid event gives an error
-        let mut event_timestamp_too_old = new_event.clone();
-        event_timestamp_too_old.header.timestamp = 1000;
-
-        let res =
-            event_graph.header_dag_insert(vec![event_timestamp_too_old.header], &dag_name).await;
-        assert!(res.is_err());
-
-        let err = res.unwrap_err();
-        match err {
-            Error::HeaderIsInvalid => {}
-            _ => panic!("expected invalid header error"),
-        }
+        let eg = make_event_graph().await?;
+        let dag_name = eg.current_genesis.read().await.header.timestamp.to_string();
+        let event = Event::new(b"dup".to_vec(), &eg).await;
+        eg.header_dag_insert(vec![event.header.clone()], &dag_name).await?;
 
+        assert_eq!(eg.dag_insert(slice::from_ref(&event), &dag_name).await?.len(), 1);
+        assert!(eg.dag_insert(slice::from_ref(&event), &dag_name).await?.is_empty());
         Ok(())
     })
 }
 
 #[test]
-fn dag_insert_without_inserting_header() -> Result<()> {
+fn evgr_dag_insert_without_header_skipped() -> Result<()> {
     smol::block_on(async {
-        let event_graph = make_event_graph().await?;
-        let dag_name = event_graph
-            .dag_store
-            .read()
-            .await
-            .main_dags
-            .last_key_value()
-            .unwrap()
-            .0
-            .clone()
-            .to_string();
-
-        let new_event = Event::new("new_event".as_bytes().to_vec(), &event_graph).await;
-        let res = event_graph.dag_insert(slice::from_ref(&new_event), &dag_name).await;
-        // Inserting event without inserting its header first gets skipped
-        assert!(res.is_ok() && res.unwrap().is_empty());
+        let eg = make_event_graph().await?;
+        let dag_name = eg.current_genesis.read().await.header.timestamp.to_string();
+        let event = Event::new(b"orphan".to_vec(), &eg).await;
+
+        // No header_dag_insert call -> event shouldn't be inserted
+        let ids = eg.dag_insert(slice::from_ref(&event), &dag_name).await?;
+        assert!(ids.is_empty());
         Ok(())
     })
 }
 
 #[test]
-fn dag_insert_duplicate_event() -> Result<()> {
+fn evgr_fetch_page_both_directions() -> Result<()> {
     smol::block_on(async {
-        let event_graph = make_event_graph().await?;
-        let dag_name = event_graph
-            .dag_store
-            .read()
-            .await
-            .main_dags
-            .last_key_value()
-            .unwrap()
-            .0
-            .clone()
-            .to_string();
-
-        let new_event = Event::new("new_event".as_bytes().to_vec(), &event_graph).await;
-        event_graph.header_dag_insert(vec![new_event.header.clone()], &dag_name).await?;
-        let res = event_graph.dag_insert(slice::from_ref(&new_event), &dag_name).await;
-        // Proper insertion
-        assert!(res.is_ok() && res.unwrap().len() == 1);
-        // Inserting duplicate event gets skipped
-        let res = event_graph.dag_insert(&[new_event], &dag_name).await;
-        assert!(res.is_ok() && res.unwrap().is_empty());
+        let eg = make_event_graph().await?;
+        let dag_name = eg.current_genesis.read().await.header.timestamp.to_string();
+
+        // Insert 10 events with strictly-increasing timestamps
+        let base = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
+        let mut inserted = vec![];
+        for i in 0..10u64 {
+            let ev = Event::with_timestamp(base + i, vec![i as u8], &eg).await;
+            eg.header_dag_insert(vec![ev.header.clone()], &dag_name).await?;
+            eg.dag_insert(slice::from_ref(&ev), &dag_name).await?;
+            inserted.push(ev);
+        }
 
-        Ok(())
-    })
-}
+        // Backward from u64::MAX -> should get newest events first
+        let page = eg.fetch_page(u64::MAX, SyncDirection::Backward, 5).await?;
+        assert_eq!(page.len(), 5);
+        // Ensure descending timestamps
+        for w in page.windows(2) {
+            assert!(w[0].header.timestamp >= w[1].header.timestamp);
+        }
 
-#[test]
-fn dag_insert_valid_event() -> Result<()> {
-    smol::block_on(async {
-        let event_graph = make_event_graph().await?;
-        let dag_name = *event_graph.dag_store.read().await.main_dags.last_key_value().unwrap().0;
-        let new_event_sub = event_graph.event_pub.clone().subscribe().await;
-
-        let new_event = Event::new("new_event".as_bytes().to_vec(), &event_graph).await;
-        event_graph
-            .header_dag_insert(vec![new_event.header.clone()], &dag_name.to_string())
-            .await?;
-        let res = event_graph.dag_insert(slice::from_ref(&new_event), &dag_name.to_string()).await;
-        assert!(res.is_ok() && res.unwrap().len() == 1);
-        // Unreferenced tips is updated
-        let layer_utips =
-            event_graph.dag_store.read().await.main_dags.get(&dag_name).unwrap().1.clone();
-        assert!(layer_utips.get(&1).unwrap().contains(&new_event.id()));
-        // The new event notification is sent to subscriber
-        let dur = Duration::from_secs(1);
-        let Ok(res) = timeout(dur, new_event_sub.receive()).await else {
-            panic!("Event is not sent to subscriber")
-        };
-        assert_eq!(res.id(), new_event.id());
+        // Forward from 0 -> oldest first
+        let page = eg.fetch_page(0, SyncDirection::Forward, 5).await?;
+        assert!(!page.is_empty());
+        for w in page.windows(2) {
+            assert!(w[0].header.timestamp <= w[1].header.timestamp);
+        }
         Ok(())
     })
 }
 
-/*
-   This function builds the following graph
-
-   Layer    3           2                    1                    0
-        [Event3A]-----[Event2A]-------|
-                                      |-----[Event1A]-----|
-                                               |          |
-                                      ---------|          |
-        [Event3B]-----[Event2B]-------|                   |
-                                      |                   |
-                                      |-----[Event1B]-----|-----[GENESIS]
-                                                          |
-        [Event3C]-----[Event2C]----|                      |
-                                   |  |-----[Event1C]-----|
-                                   ---|                   |
-                                   |  |                   |
-        [Event3D]-----[Event2D]----|  |------[Event1D]----|
-*/
-async fn build_graph() -> Result<(EventGraphPtr, Vec<Event>)> {
-    let event_graph = make_event_graph().await?;
-    let mut events = vec![];
-    let dag_name = event_graph
-        .dag_store
-        .read()
-        .await
-        .main_dags
-        .last_key_value()
-        .unwrap()
-        .0
-        .clone()
-        .to_string();
-
-    let current_dag_genesis_hash = event_graph.current_genesis.read().await.id();
-
-    // first layer
-    let mut parents = [NULL_ID; N_EVENT_PARENTS];
-    parents[0] = current_dag_genesis_hash;
-    let event1a = Event {
-        header: Header {
-            timestamp: UNIX_EPOCH.elapsed().unwrap().as_millis() as u64 + 1,
-            layer: 1,
-            parents,
-        },
-        content: "Event1A".as_bytes().to_vec(),
-    };
-    events.push(event1a.clone());
-
-    let event1b = Event {
-        header: Header {
-            timestamp: UNIX_EPOCH.elapsed().unwrap().as_millis() as u64 + 2,
-            layer: 1,
-            parents,
-        },
-        content: "Event1B".as_bytes().to_vec(),
-    };
-    events.push(event1b.clone());
-
-    let event1c = Event {
-        header: Header {
-            timestamp: UNIX_EPOCH.elapsed().unwrap().as_millis() as u64 + 3,
-            layer: 1,
-            parents,
-        },
-        content: "Event1C".as_bytes().to_vec(),
-    };
-    events.push(event1c.clone());
+async fn build_graph() -> Result<(EventGraphPtr, HashMap<&'static str, Event>)> {
+    let eg = make_event_graph().await?;
+    let dag_name = eg.current_genesis.read().await.header.timestamp.to_string();
+    let genesis_hash = eg.current_genesis.read().await.id();
+    let base = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
 
-    let event1d = Event {
+    let make = |off: u64, layer: u64, parents: [blake3::Hash; N_EVENT_PARENTS], name: &str| Event {
         header: Header {
-            timestamp: UNIX_EPOCH.elapsed().unwrap().as_millis() as u64 + 4,
-            layer: 1,
+            timestamp: base + off,
+            layer,
             parents,
+            content_hash: blake3::hash(name.as_bytes()),
         },
-        content: "Event1D".as_bytes().to_vec(),
+        content: name.as_bytes().to_vec(),
     };
-    events.push(event1d.clone());
 
-    // second layer
-    parents[0] = event1a.id();
-    let event2a = Event {
-        header: Header {
-            timestamp: UNIX_EPOCH.elapsed().unwrap().as_millis() as u64 + 5,
-            layer: 2,
-            parents,
-        },
-        content: "Event2A".as_bytes().to_vec(),
-    };
-    events.push(event2a.clone());
-
-    parents[1] = event1b.id();
-    let event2b = Event {
-        header: Header {
-            timestamp: UNIX_EPOCH.elapsed().unwrap().as_millis() as u64 + 6,
-            layer: 2,
-            parents,
-        },
-        content: "Event2B".as_bytes().to_vec(),
-    };
-    events.push(event2b.clone());
-
-    parents[0] = event1c.id();
-    parents[1] = event1d.id();
-    let event2c = Event {
-        header: Header {
-            timestamp: UNIX_EPOCH.elapsed().unwrap().as_millis() as u64 + 7,
-            layer: 2,
-            parents,
-        },
-        content: "Event2C".as_bytes().to_vec(),
-    };
-    events.push(event2c.clone());
-
-    let event2d = Event {
-        header: Header {
-            timestamp: UNIX_EPOCH.elapsed().unwrap().as_millis() as u64 + 8,
-            layer: 2,
-            parents,
-        },
-        content: "Event2D".as_bytes().to_vec(),
-    };
-    events.push(event2d.clone());
-
-    // third layer
-    let mut parents = [NULL_ID; N_EVENT_PARENTS];
-    parents[0] = event2a.id();
-    let event3a = Event {
-        header: Header {
-            timestamp: UNIX_EPOCH.elapsed().unwrap().as_millis() as u64 + 9,
-            layer: 3,
-            parents,
-        },
-        content: "Event3A".as_bytes().to_vec(),
-    };
-    events.push(event3a.clone());
-
-    parents[0] = event2b.id();
-    let event3b = Event {
-        header: Header {
-            timestamp: UNIX_EPOCH.elapsed().unwrap().as_millis() as u64 + 10,
-            layer: 3,
-            parents,
-        },
-        content: "Event3B".as_bytes().to_vec(),
-    };
-    events.push(event3b.clone());
-
-    parents[0] = event2c.id();
-    let event3c = Event {
-        header: Header {
-            timestamp: UNIX_EPOCH.elapsed().unwrap().as_millis() as u64 + 11,
-            layer: 3,
-            parents,
-        },
-        content: "Event3C".as_bytes().to_vec(),
-    };
-    events.push(event3c.clone());
-
-    parents[0] = event2d.id();
-    let event3d = Event {
-        header: Header {
-            timestamp: UNIX_EPOCH.elapsed().unwrap().as_millis() as u64 + 12,
-            layer: 3,
-            parents,
-        },
-        content: "Event3D".as_bytes().to_vec(),
-    };
-    events.push(event3d.clone());
-
-    // Insert events 1a to 1d
-    event_graph
-        .header_dag_insert(
-            vec![
-                event1a.header.clone(),
-                event1b.header.clone(),
-                event1c.header.clone(),
-                event1d.header.clone(),
-            ],
-            &dag_name,
-        )
-        .await?;
-    event_graph.dag_insert(&[event1a, event1b, event1c, event1d], &dag_name).await?;
-    // Insert events 2a to 2d
-    event_graph
-        .header_dag_insert(
-            vec![
-                event2a.header.clone(),
-                event2b.header.clone(),
-                event2c.header.clone(),
-                event2d.header.clone(),
-            ],
-            &dag_name,
-        )
-        .await?;
-    event_graph.dag_insert(&[event2a, event2b, event2c, event2d], &dag_name).await?;
-    // Insert events 3a to 3d
-    event_graph
-        .header_dag_insert(
-            vec![
-                event3a.header.clone(),
-                event3b.header.clone(),
-                event3c.header.clone(),
-                event3d.header.clone(),
-            ],
-            &dag_name,
-        )
-        .await?;
-    event_graph.dag_insert(&[event3a, event3b, event3c, event3d], &dag_name).await?;
-
-    //panic!("REACHED HERE");
-
-    Ok((event_graph, events))
+    //           genesis
+    //          / | | \
+    //       e1a e1b e1c e1d       (layer 1)
+    //        |   |   |   |
+    //       e2a e2b e2c e2d        (layer 2)
+    let mut p = [NULL_ID; N_EVENT_PARENTS];
+    p[0] = genesis_hash;
+    let e1a = make(1, 1, p, "e1a");
+    let e1b = make(2, 1, p, "e1b");
+    let e1c = make(3, 1, p, "e1c");
+    let e1d = make(4, 1, p, "e1d");
+
+    let mut p = [NULL_ID; N_EVENT_PARENTS];
+    p[0] = e1a.id();
+    let e2a = make(5, 2, p, "e2a");
+    p[0] = e1b.id();
+    let e2b = make(6, 2, p, "e2b");
+    p[0] = e1c.id();
+    let e2c = make(7, 2, p, "e2c");
+    p[0] = e1d.id();
+    let e2d = make(8, 2, p, "e2d");
+
+    let l1 = vec![e1a.clone(), e1b.clone(), e1c.clone(), e1d.clone()];
+    let l2 = vec![e2a.clone(), e2b.clone(), e2c.clone(), e2d.clone()];
+
+    eg.header_dag_insert(l1.iter().map(|e| e.header.clone()).collect(), &dag_name).await?;
+    eg.dag_insert(&l1, &dag_name).await?;
+    eg.header_dag_insert(l2.iter().map(|e| e.header.clone()).collect(), &dag_name).await?;
+    eg.dag_insert(&l2, &dag_name).await?;
+
+    let mut map = HashMap::new();
+    map.insert("e1a", e1a);
+    map.insert("e1b", e1b);
+    map.insert("e1c", e1c);
+    map.insert("e1d", e1d);
+    map.insert("e2a", e2a);
+    map.insert("e2b", e2b);
+    map.insert("e2c", e2c);
+    map.insert("e2d", e2d);
+    Ok((eg, map))
 }
 
 #[test]
-fn find_ancestors_of_an_event() -> Result<()> {
+fn evgr_ancestor_walk_via_header_tree() -> Result<()> {
     smol::block_on(async {
-        let (event_graph, events) = build_graph().await?;
-
-        let dag_name = event_graph
-            .dag_store
-            .read()
-            .await
-            .main_dags
-            .last_key_value()
-            .unwrap()
-            .0
-            .clone()
-            .to_string();
-
-        let tree = event_graph.dag_store.read().await.get_dag(&format!("headers_{dag_name}"));
-
-        let events_map: HashMap<String, Event> =
-            events.into_iter().map(|e| (String::from_utf8_lossy(&e.content).into(), e)).collect();
-        let genesis_header = event_graph.current_genesis.read().await.header.clone();
-        let genesis_hash = genesis_header.id();
-        // Genesis layer
-        let mut genesis_ancestors = HashSet::new();
-        event_graph.get_ancestors(&mut genesis_ancestors, genesis_header, &tree).await?;
-        assert!(genesis_ancestors.is_empty());
-
-        // 1st layer
-        let mut event1a_ancestors = HashSet::new();
-        event_graph
-            .get_ancestors(
-                &mut event1a_ancestors,
-                events_map.get("Event1A").unwrap().header.clone(),
-                &tree,
-            )
-            .await?;
-        let mut event1b_ancestors = HashSet::new();
-        event_graph
-            .get_ancestors(
-                &mut event1b_ancestors,
-                events_map.get("Event1B").unwrap().header.clone(),
-                &tree,
-            )
-            .await?;
-        let mut event1c_ancestors = HashSet::new();
-        event_graph
-            .get_ancestors(
-                &mut event1c_ancestors,
-                events_map.get("Event1C").unwrap().header.clone(),
-                &tree,
-            )
-            .await?;
-        let mut event1d_ancestors = HashSet::new();
-        event_graph
-            .get_ancestors(
-                &mut event1d_ancestors,
-                events_map.get("Event1D").unwrap().header.clone(),
-                &tree,
-            )
-            .await?;
-
-        // Only genesis is the ancestor
-        assert!(event1a_ancestors.len() == 1 && event1a_ancestors.contains(&genesis_hash));
-        assert!(event1b_ancestors.len() == 1 && event1b_ancestors.contains(&genesis_hash));
-        assert!(event1c_ancestors.len() == 1 && event1c_ancestors.contains(&genesis_hash));
-        assert!(event1d_ancestors.len() == 1 && event1d_ancestors.contains(&genesis_hash));
-
-        // 2nd layer
-        let event2a_expected_ancestors =
-            HashSet::from([genesis_hash, events_map.get("Event1A").unwrap().id()]);
-        let event2b_expected_ancestors = HashSet::from([
-            genesis_hash,
-            events_map.get("Event1B").unwrap().id(),
-            events_map.get("Event1A").unwrap().id(),
-        ]);
-        let event2cd_expected_ancestors = HashSet::from([
-            genesis_hash,
-            events_map.get("Event1C").unwrap().id(),
-            events_map.get("Event1D").unwrap().id(),
-        ]);
-
-        let mut event2a_ancestors = HashSet::new();
-        event_graph
-            .get_ancestors(
-                &mut event2a_ancestors,
-                events_map.get("Event2A").unwrap().header.clone(),
-                &tree,
-            )
-            .await?;
-        let mut event2b_ancestors = HashSet::new();
-        event_graph
-            .get_ancestors(
-                &mut event2b_ancestors,
-                events_map.get("Event2B").unwrap().header.clone(),
-                &tree,
-            )
-            .await?;
-        let mut event2c_ancestors = HashSet::new();
-        event_graph
-            .get_ancestors(
-                &mut event2c_ancestors,
-                events_map.get("Event2C").unwrap().header.clone(),
-                &tree,
-            )
-            .await?;
-        let mut event2d_ancestors = HashSet::new();
-        event_graph
-            .get_ancestors(
-                &mut event2d_ancestors,
-                events_map.get("Event2D").unwrap().header.clone(),
-                &tree,
-            )
-            .await?;
-
-        assert_eq!(event2a_ancestors, event2a_expected_ancestors);
-        assert_eq!(event2b_ancestors, event2b_expected_ancestors);
-        assert_eq!(event2c_ancestors, event2cd_expected_ancestors);
-        assert_eq!(event2d_ancestors, event2cd_expected_ancestors);
-
-        // 3rd layer
-        let mut event3a_expected_ancestors = event2a_expected_ancestors.clone();
-        event3a_expected_ancestors.insert(events_map.get("Event2A").unwrap().header.clone().id());
-        let mut event3b_expected_ancestors = event2b_expected_ancestors.clone();
-        event3b_expected_ancestors.insert(events_map.get("Event2B").unwrap().header.clone().id());
-        let mut event3c_expected_ancestors = event2cd_expected_ancestors.clone();
-        event3c_expected_ancestors.insert(events_map.get("Event2C").unwrap().header.clone().id());
-        let mut event3d_expected_ancestors = event2cd_expected_ancestors.clone();
-        event3d_expected_ancestors.insert(events_map.get("Event2D").unwrap().header.clone().id());
-
-        let mut event3a_ancestors = HashSet::new();
-        event_graph
-            .get_ancestors(
-                &mut event3a_ancestors,
-                events_map.get("Event3A").unwrap().header.clone(),
-                &tree,
-            )
-            .await?;
-        let mut event3b_ancestors = HashSet::new();
-        event_graph
-            .get_ancestors(
-                &mut event3b_ancestors,
-                events_map.get("Event3B").unwrap().header.clone(),
-                &tree,
-            )
-            .await?;
-        let mut event3c_ancestors = HashSet::new();
-        event_graph
-            .get_ancestors(
-                &mut event3c_ancestors,
-                events_map.get("Event3C").unwrap().header.clone(),
-                &tree,
-            )
-            .await?;
-        let mut event3d_ancestors = HashSet::new();
-        event_graph
-            .get_ancestors(
-                &mut event3d_ancestors,
-                events_map.get("Event3D").unwrap().header.clone(),
-                &tree,
-            )
-            .await?;
-
-        assert_eq!(event3a_ancestors, event3a_expected_ancestors);
-        assert_eq!(event3b_ancestors, event3b_expected_ancestors);
-        assert_eq!(event3c_ancestors, event3c_expected_ancestors);
-        assert_eq!(event3d_ancestors, event3d_expected_ancestors);
+        let (eg, evs) = build_graph().await?;
+        let dag_ts = eg.current_genesis.read().await.header.timestamp;
+        let store = eg.dag_store.read().await;
+        let slot = store.get_slot(&dag_ts).unwrap();
+        let genesis_hash = eg.current_genesis.read().await.id();
+
+        // Layer-1 events should have only genesis as ancestor
+        for name in ["e1a", "e1b", "e1c", "e1d"] {
+            let mut ancestors = HashSet::new();
+            eg.get_ancestors(&mut ancestors, evs[name].header.clone(), &slot.header_tree).await?;
+            assert_eq!(ancestors, HashSet::from([genesis_hash]));
+        }
 
+        // e2a's ancestors = {genesis, e1a}
+        let mut ancestors = HashSet::new();
+        eg.get_ancestors(&mut ancestors, evs["e2a"].header.clone(), &slot.header_tree).await?;
+        assert_eq!(ancestors, HashSet::from([genesis_hash, evs["e1a"].id()]));
         Ok(())
     })
 }
 
 #[test]
-fn fetches_headers_with_tips() -> Result<()> {
+fn evgr_order_events_is_chronological() -> Result<()> {
     smol::block_on(async {
-        let (event_graph, events) = build_graph().await?;
-
-        let dag_name = event_graph
-            .dag_store
-            .read()
-            .await
-            .main_dags
-            .last_key_value()
-            .unwrap()
-            .0
-            .clone()
-            .to_string();
-
-        let map: HashMap<blake3::Hash, String> = events
-            .into_iter()
-            .map(|e| (e.id(), String::from_utf8_lossy(&e.content).into()))
-            .collect();
-        let name_map: HashMap<String, blake3::Hash> =
-            map.iter().map(|(hash, content)| (content.clone(), *hash)).collect();
-
-        let genesis_hash = event_graph.current_genesis.read().await.id();
-        let patha = ["Event3A", "Event2A", "Event1A"];
-        let pathb = ["Event3B", "Event2B", "Event1B", "Event1A"];
-        let pathc = ["Event3C", "Event2C", "Event1C", "Event1D"];
-        let pathd = ["Event3D", "Event2D", "Event1C", "Event1D"];
-
-        let patha_tip = BTreeMap::from([(3, HashSet::from([*name_map.get("Event3A").unwrap()]))]);
-        // Should be only headers that are not ancestors of Event3A
-        let headers = event_graph.fetch_headers_with_tips(&dag_name, &patha_tip).await?;
-        assert!(headers.iter().all(
-            |h| h.id() != genesis_hash && !patha.contains(&map.get(&h.id()).unwrap().as_str())
-        ));
-
-        let pathb_tip = BTreeMap::from([(3, HashSet::from([*name_map.get("Event3B").unwrap()]))]);
-        // Should be only headers that are not ancestors of Event3B
-        let headers = event_graph.fetch_headers_with_tips(&dag_name, &pathb_tip).await?;
-        assert!(headers.iter().all(
-            |h| h.id() != genesis_hash && !pathb.contains(&map.get(&h.id()).unwrap().as_str())
-        ));
-
-        let pathc_tip = BTreeMap::from([(3, HashSet::from([*name_map.get("Event3C").unwrap()]))]);
-        // Should be only headers that are not ancestors of Event3C
-        let headers = event_graph.fetch_headers_with_tips(&dag_name, &pathc_tip).await?;
-        assert!(headers.iter().all(
-            |h| h.id() != genesis_hash && !pathc.contains(&map.get(&h.id()).unwrap().as_str())
-        ));
-
-        let pathd_tip = BTreeMap::from([(3, HashSet::from([*name_map.get("Event3D").unwrap()]))]);
-        // Should be only headers that are not ancestors of Event3D
-        let headers = event_graph.fetch_headers_with_tips(&dag_name, &pathd_tip).await?;
-        assert!(headers.iter().all(
-            |h| h.id() != genesis_hash && !pathd.contains(&map.get(&h.id()).unwrap().as_str())
-        ));
-
-        // Two tips Event3A and Event3D
-        let mut comb_tip = BTreeMap::new();
-        comb_tip.extend(patha_tip);
-        comb_tip.get_mut(&3).unwrap().extend(pathd_tip.get(&3).unwrap());
-
-        // Should be only headers that are not ancestors of Event3A and Event3D
-        let headers = event_graph.fetch_headers_with_tips(&dag_name, &comb_tip).await?;
-        assert!(headers.iter().all(|h| h.id() != genesis_hash &&
-            !patha.contains(&map.get(&h.id()).unwrap().as_str()) &&
-            !pathd.contains(&map.get(&h.id()).unwrap().as_str())));
-
+        let (eg, _) = build_graph().await?;
+        let ordered = eg.order_events().await;
+        for w in ordered.windows(2) {
+            assert!(w[0].header.timestamp <= w[1].header.timestamp);
+        }
         Ok(())
     })
 }
+
+macro_rules! test_body {
+    ($real_call:ident) => {
+        init_logger();
+        let ex = Arc::new(Executor::new());
+        let ex_ = ex.clone();
+        let (signal, shutdown) = channel::unbounded::<()>();
+        easy_parallel::Parallel::new()
+            .each(0..N_NODES, |_| future::block_on(ex.run(shutdown.recv())))
+            .finish(|| {
+                future::block_on(async {
+                    $real_call(ex_).await;
+                    drop(signal);
+                })
+            });
+    };
+}
+
+#[test]
+fn evgr_eventgraph_propagation() {
+    test_body!(eventgraph_propagation_real);
+}
+
+async fn eventgraph_propagation_real(ex: Arc<Executor<'static>>) {
+    let mut rng: ThreadRng = rand::thread_rng();
+    let idxs: Vec<usize> = (0..N_NODES).collect();
+
+    // Bootstrap a small network
+    let mut nodes = vec![];
+    for i in 0..N_NODES {
+        let mut pi = idxs.clone();
+        pi.remove(i);
+        let conns: Vec<_> = pi.choose_multiple(&mut rng, N_CONNS).collect();
+        let peers: Vec<_> = conns
+            .iter()
+            .map(|p| Url::parse(&format!("tcp://127.0.0.1:{}", 13200 + *p)).unwrap())
+            .collect();
+        let inbound = vec![Url::parse(&format!("tcp://127.0.0.1:{}", 13200 + i)).unwrap()];
+        nodes.push(spawn_node(inbound, peers, ex.clone()).await);
+    }
+    for eg in &nodes {
+        eg.p2p.clone().start().await.unwrap();
+    }
+    sleep(5).await;
+
+    // Broadcast an event from a random node
+    let dag_name = nodes[0].current_genesis.read().await.header.timestamp.to_string();
+    let node = nodes.choose(&mut rng).unwrap();
+    let ev = Event::new(vec![1, 2, 3, 4], node).await;
+    node.header_dag_insert(vec![ev.header.clone()], &dag_name).await.unwrap();
+    node.dag_insert(slice::from_ref(&ev), &dag_name).await.unwrap();
+    node.p2p.broadcast(&EventPut(ev.clone(), vec![])).await;
+    sleep(5).await;
+
+    // Every node should now have at least genesis + the new event
+    for (i, eg) in nodes.iter().enumerate() {
+        let ts = eg.current_genesis.read().await.header.timestamp;
+        let store = eg.dag_store.read().await;
+        let slot = store.get_slot(&ts).unwrap();
+        assert!(
+            slot.main_tree.len() >= 2,
+            "Node {i} has only {} events in main_tree",
+            slot.main_tree.len()
+        );
+    }
+
+    for eg in &nodes {
+        eg.p2p.clone().stop().await;
+    }
+}

+ 64 - 169
src/event_graph/util.rs

@@ -16,6 +16,8 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+//! Timestamp arithmetic, genesis generation, and replay logging.
+
 use std::{
     collections::HashMap,
     fs::{self, File, OpenOptions},
@@ -27,10 +29,9 @@ use std::{
 use darkfi_serial::{deserialize, deserialize_async, serialize};
 use sled_overlay::sled;
 use tinyjson::JsonValue;
-use tracing::error;
 
 use crate::{
-    event_graph::{Event, GENESIS_CONTENTS, INITIAL_GENESIS, NULL_ID, N_EVENT_PARENTS},
+    event_graph::{Event, EventGraphConfig, NULL_ID, N_EVENT_PARENTS},
     util::{encoding::base64, file::load_file},
     Result,
 };
@@ -43,112 +44,79 @@ use crate::rpc::{
 
 use super::event::Header;
 
-/// MilliSeconds in an hour
+/// Milliseconds in one hour.
 pub(super) const HOUR: i64 = 3_600_000;
 
-/// Calculate the next hour timestamp given a number of hours.
-/// If `hours` is 0, calculate the timestamp of this hour.
+/// Timestamp (millis) for the start of the hour `hours` offsets from now.
 pub(super) fn next_hour_timestamp(hours: i64) -> u64 {
-    // Get current time
     let now = UNIX_EPOCH.elapsed().unwrap().as_millis() as i64;
-
-    // Find the timestamp for the next hour
-    let next_hour = (now / HOUR) * HOUR;
-
-    // Adjust for hours_from_now
-    (next_hour + (HOUR * hours)) as u64
+    ((now / HOUR) * HOUR + HOUR * hours) as u64
 }
 
-/// Calculate the number of hours since a given timestamp.
-pub(super) fn hours_since(next_hour_ts: u64) -> u64 {
-    // Get current time
+/// Whole hours elapsed since `ts`.
+pub(super) fn hours_since(ts: u64) -> u64 {
     let now = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
-
-    // Calculate the difference between the current timestamp
-    // and the given midnight timestamp
-    let elapsed_seconds = now - next_hour_ts;
-
-    // Convert the elapsed seconds into hours
-    elapsed_seconds / HOUR as u64
+    (now - ts) / HOUR as u64
 }
 
-/// Calculate the timestamp of the next DAG rotation.
+/// 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 {
-    // Prevent division by 0
     if rotation_period == 0 {
         panic!("Rotation period cannot be 0");
     }
-    // Calculate the number of hours since the given starting point
-    let hours_passed = hours_since(starting_timestamp);
-
-    // Find out how many rotation periods have occurred since
-    // the starting point.
-    // Note: when rotation_period = 1, rotations_since_start = hours_passed
-    let rotations_since_start = hours_passed.div_ceil(rotation_period);
-
-    // Find out the number of hours until the next rotation. Panic if result is beyond the range
-    // of i64.
-    let hours_until_next_rotation: i64 =
-        (rotations_since_start * rotation_period - hours_passed).try_into().unwrap();
-
-    // Get the timestamp for the next rotation
-    if hours_until_next_rotation == 0 {
-        // If there are 0 hours until the next rotation, we want
-        // to rotate next hour. This is a special case.
-        return next_hour_timestamp(1)
+    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)
     }
-    next_hour_timestamp(hours_until_next_rotation)
 }
 
-/// Calculate the time in milliseconds until the next_rotation, given
-/// as a timestamp.
-/// `next_rotation` here represents a timestamp in UNIX epoch format.
+/// Milliseconds remaining until `next_rotation`.
+///
+/// # Panics
+///
+/// Panics if `next_rotation` is in the past.
 pub fn millis_until_next_rotation(next_rotation: u64) -> u64 {
-    // Store `now` in a variable in order to avoid a TOCTOU error.
-    // There may be a drift of one second between this panic check and
-    // the return value if we get unlucky.
     let now = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
-    if next_rotation < now {
-        panic!("Next rotation timestamp is in the past");
-    }
+    assert!(next_rotation >= now, "Next rotation is in the past");
     next_rotation - now
 }
 
-/// Generate a deterministic genesis event corresponding to the DAG's configuration.
-pub fn generate_genesis(hours_rotation: u64) -> Event {
-    let parents = [NULL_ID; N_EVENT_PARENTS];
-    let layer = 0;
-    let content = GENESIS_CONTENTS.to_vec();
-
-    // Hours rotation is u64 except zero
-    if hours_rotation == 0 {
-        return Event { header: Header { timestamp: INITIAL_GENESIS, parents, layer }, content }
-    }
-
-    // First check how many hours passed since initial genesis.
-    let hours_passed = hours_since(INITIAL_GENESIS);
-
-    // Calculate the number of hours_rotation intervals since INITIAL_GENESIS
-    let rotations_since_genesis = hours_passed / hours_rotation;
-
-    // Calculate the timestamp of the most recent event
-    let timestamp = INITIAL_GENESIS + (rotations_since_genesis * hours_rotation * HOUR as u64);
-
-    Event { header: Header { timestamp, parents, layer }, content }
+/// 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<u8>) -> Result<()> {
     fs::create_dir_all(datastore)?;
-    let datastore = datastore.join("replayer.log");
-    if !datastore.exists() {
-        File::create(&datastore)?;
-    };
-
-    let mut file = OpenOptions::new().append(true).open(&datastore)?;
-    let v = base64::encode(&value);
-    let f = format!("{cmd} {v}");
-    writeln!(file, "{f}")?;
-
+    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(())
 }
 
@@ -156,103 +124,30 @@ pub(super) fn replayer_log(datastore: &Path, cmd: String, value: Vec<u8>) -> Res
 pub async fn recreate_from_replayer_log(datastore: &Path) -> JsonResult {
     let log_path = datastore.join("replayer.log");
     if !log_path.exists() {
-        error!("Error loading replayed log");
         return JsonResult::Error(JsonError::new(
             ErrorCode::ParseError,
-            Some("Error loading replayed log".to_string()),
+            Some("Log not found".into()),
             1,
         ))
-    };
-
+    }
     let reader = load_file(&log_path).unwrap();
-
-    let db_datastore = datastore.join("replayed_db");
-
-    let sled_db = sled::open(db_datastore).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 line = line.split(' ').collect::<Vec<&str>>();
-        if line[0] == "insert" {
-            let v = base64::decode(line[1]).unwrap();
-            let v: Event = deserialize(&v).unwrap();
-            let v_se = serialize(&v);
-            dag.insert(v.header.id().as_bytes(), v_se).unwrap();
+        let parts = line.split(' ').collect::<Vec<&str>>();
+        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 iter_elem in dag.iter() {
-        let (id, val) = iter_elem.unwrap();
+    for item in dag.iter() {
+        let (id, val) = item.unwrap();
         let id = blake3::Hash::from_bytes((&id as &[u8]).try_into().unwrap());
-        let val: Event = deserialize_async(&val).await.unwrap();
-        graph.insert(id, val);
+        graph.insert(id, deserialize_async::<Event>(&val).await.unwrap());
     }
-
-    let json_graph = graph
-        .into_iter()
-        .map(|(k, v)| {
-            let key = k.to_string();
-            let value = JsonValue::from(v);
-            (key, value)
-        })
-        .collect();
+    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))]);
-    let result = JsonValue::Object(HashMap::from([("eventgraph_info".to_string(), values)]));
-
-    JsonResponse::new(result, 1).into()
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_hours_since() {
-        let five_hours_ago = next_hour_timestamp(-5);
-        assert_eq!(hours_since(five_hours_ago), 5);
-
-        let this_hour = next_hour_timestamp(0);
-        assert_eq!(hours_since(this_hour), 0);
-    }
-
-    #[test]
-    fn test_next_rotation_timestamp() {
-        let starting_point = next_hour_timestamp(-10);
-        let rotation_period = 7;
-
-        // The first rotation since the starting point would be 3 hours ago.
-        // So the next rotation should be 4 hours from now.
-        let expected = next_hour_timestamp(4);
-        assert_eq!(next_rotation_timestamp(starting_point, rotation_period), expected);
-
-        // When starting from current hour with a rotation period of 1 (hour),
-        // we should get next hours's timestamp.
-        // This is a special case.
-        let this_hour: u64 = next_hour_timestamp(0);
-        let next_hour = this_hour + 3_600_000u64; // add an hour
-        assert_eq!(next_hour, next_rotation_timestamp(this_hour, 1));
-    }
-
-    #[test]
-    #[should_panic]
-    fn test_next_rotation_timestamp_panics_on_overflow() {
-        next_rotation_timestamp(0, u64::MAX);
-    }
-
-    #[test]
-    #[should_panic]
-    fn test_next_rotation_timestamp_panics_on_division_by_zero() {
-        next_rotation_timestamp(0, 0);
-    }
-
-    #[test]
-    fn test_millis_until_next_rotation_is_within_rotation_interval() {
-        let hours_rotation = 1u64;
-        // The amount of time in seconds between rotations.
-        let rotation_interval = hours_rotation * 3_600_000u64;
-        let next_rotation_timestamp = next_rotation_timestamp(INITIAL_GENESIS, hours_rotation);
-        let s = millis_until_next_rotation(next_rotation_timestamp);
-        assert!(s < rotation_interval);
-    }
+    JsonResponse::new(JsonValue::Object(HashMap::from([("eventgraph_info".into(), values)])), 1)
+        .into()
 }

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff