Browse Source

event_graph: Validate header layers exactly

x 1 month ago
parent
commit
33e62c9a83
3 changed files with 102 additions and 6 deletions
  1. 7 4
      src/event_graph/event.rs
  2. 7 1
      src/event_graph/mod.rs
  3. 88 1
      src/event_graph/tests.rs

+ 7 - 4
src/event_graph/event.rs

@@ -116,6 +116,7 @@ impl Header {
         }
 
         let mut seen = HashSet::new();
+        let mut max_parent_layer = None;
         let self_id = self.id();
         for pid in self.parents.iter() {
             if pid == &NULL_ID {
@@ -134,13 +135,15 @@ impl Header {
 
             let Some(bytes) = bytes else { return Ok(false) };
             let parent: Header = deserialize_async(&bytes).await?;
-            if self.layer <= parent.layer {
-                return Ok(false)
-            }
+            max_parent_layer =
+                Some(max_parent_layer.map_or(parent.layer, |m: u64| m.max(parent.layer)));
             seen.insert(pid);
         }
 
-        Ok(!seen.is_empty())
+        let Some(max_parent_layer) = max_parent_layer else { return Ok(false) };
+        let Some(expected_layer) = max_parent_layer.checked_add(1) else { return Ok(false) };
+
+        Ok(self.layer == expected_layer)
     }
 }
 

+ 7 - 1
src/event_graph/mod.rs

@@ -287,6 +287,10 @@ pub(crate) async fn compute_unreferenced_tips(dag: &sled::Tree) -> LayerUTips {
 }
 
 /// Pick up to N_EVENT_PARENTS tips from the highest layers.
+///
+/// If the highest local tip is already at `u64::MAX`, no valid child
+/// layer exists. Return `u64::MAX` rather than wrapping; header
+/// validation rejects any attempted child of that saturated layer.
 fn select_parents_from_tips(tips: &LayerUTips) -> (u64, [blake3::Hash; N_EVENT_PARENTS]) {
     let mut parents = [NULL_ID; N_EVENT_PARENTS];
     let mut i = 0;
@@ -300,7 +304,9 @@ fn select_parents_from_tips(tips: &LayerUTips) -> (u64, [blake3::Hash; N_EVENT_P
         }
     }
 
-    (tips.last_key_value().unwrap().0 + 1, parents)
+    let layer =
+        tips.last_key_value().and_then(|(layer, _)| layer.checked_add(1)).unwrap_or(u64::MAX);
+    (layer, parents)
 }
 
 /// Storage layer for all rotating DAGs.

+ 88 - 1
src/event_graph/tests.rs

@@ -36,7 +36,7 @@ use crate::{
         proto::{cap_layer_tips, count_layer_tips, EventPut, SyncDirection, MAX_RANGE_PAGE_SIZE},
         test_helpers::{
             archive_config, bounded_dag_store_config, init_logger, make_eg, make_network,
-            run_multi_node_test, shutdown_network, TestIdentity,
+            run_multi_node_test, shutdown_network, test_config, TestIdentity,
         },
         util::next_hour_timestamp,
         DagStore, Event, EventGraphPtr, LayerUTips, TimeIndex, NULL_ID, NULL_PARENTS,
@@ -154,6 +154,17 @@ fn evgr_layer_tip_cap_is_bounded() {
     assert!(capped.get(&0).is_some_and(|layer| layer.len() == 1));
 }
 
+#[test]
+fn evgr_parent_selection_does_not_wrap_saturated_layer() {
+    let tip = blake3::hash(b"saturated-tip");
+    let tips = LayerUTips::from([(u64::MAX, HashSet::from([tip]))]);
+
+    let (layer, parents) = super::select_parents_from_tips(&tips);
+
+    assert_eq!(layer, u64::MAX);
+    assert_eq!(parents[0], tip);
+}
+
 #[test]
 fn evgr_time_index_queries_and_saturating_cursor() {
     // Forward, backward, newest, oldest queries plus the saturating
@@ -350,6 +361,82 @@ fn evgr_dag_insert_without_header_skipped() {
     })
 }
 
+#[test]
+fn evgr_header_insert_rejects_layer_jump() {
+    smol::block_on(async {
+        let eg = make_eg().await;
+        let genesis = eg.current_genesis.read().await.clone();
+        let dag_name = genesis.header.timestamp.to_string();
+        let mut parents = [NULL_ID; N_EVENT_PARENTS];
+        parents[0] = genesis.id();
+
+        let event = Event {
+            header: Header {
+                timestamp: UNIX_EPOCH.elapsed().unwrap().as_millis() as u64,
+                parents,
+                layer: 2,
+                content_hash: blake3::hash(b"layer-jump"),
+            },
+            content: b"layer-jump".to_vec(),
+        };
+
+        let err = eg.header_dag_insert(vec![event.header], &dag_name).await.unwrap_err();
+        assert!(matches!(err, crate::Error::HeaderIsInvalid));
+    })
+}
+
+#[test]
+fn evgr_header_insert_rejects_duplicate_parents() {
+    smol::block_on(async {
+        let eg = make_eg().await;
+        let genesis = eg.current_genesis.read().await.clone();
+        let dag_name = genesis.header.timestamp.to_string();
+        let mut parents = [NULL_ID; N_EVENT_PARENTS];
+        parents[0] = genesis.id();
+        parents[1] = genesis.id();
+
+        let event = Event {
+            header: Header {
+                timestamp: UNIX_EPOCH.elapsed().unwrap().as_millis() as u64,
+                parents,
+                layer: 1,
+                content_hash: blake3::hash(b"duplicate-parents"),
+            },
+            content: b"duplicate-parents".to_vec(),
+        };
+
+        let err = eg.header_dag_insert(vec![event.header], &dag_name).await.unwrap_err();
+        assert!(matches!(err, crate::Error::HeaderIsInvalid));
+    })
+}
+
+#[test]
+fn evgr_header_validate_rejects_layer_overflow_parent() {
+    smol::block_on(async {
+        let db = sled::Config::new().temporary(true).open().unwrap();
+        let tree = db.open_tree("headers").unwrap();
+        let timestamp = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
+        let parent = Header {
+            timestamp,
+            parents: NULL_PARENTS,
+            layer: u64::MAX,
+            content_hash: blake3::hash(b"overflow-parent"),
+        };
+        tree.insert(parent.id().as_bytes(), serialize_async(&parent).await).unwrap();
+
+        let mut parents = [NULL_ID; N_EVENT_PARENTS];
+        parents[0] = parent.id();
+        let child = Header {
+            timestamp,
+            parents,
+            layer: u64::MAX,
+            content_hash: blake3::hash(b"overflow-child"),
+        };
+
+        assert!(!child.validate(&tree, &test_config(), None).await.unwrap());
+    })
+}
+
 #[test]
 fn evgr_fetch_page_both_directions() {
     smol::block_on(async {