Răsfoiți Sursa

event_graph: Make rotation arithmetic total

Invalid rotation config or edge timestamps could panic, wrap,
or mutate/open DAG sled trees before failing.
x 1 lună în urmă
părinte
comite
75b2bd0d87
4 a modificat fișierele cu 144 adăugiri și 47 ștergeri
  1. 4 4
      src/event_graph/event.rs
  2. 50 9
      src/event_graph/mod.rs
  3. 48 8
      src/event_graph/tests.rs
  4. 42 26
      src/event_graph/util.rs

+ 4 - 4
src/event_graph/event.rs

@@ -21,7 +21,9 @@ use std::{cmp::Ordering, collections::HashSet, time::UNIX_EPOCH};
 use darkfi_serial::{async_trait, deserialize_async, Encodable, SerialDecodable, SerialEncodable};
 use sled_overlay::{sled, SledTreeOverlay};
 
-use super::{util::HOUR, EventGraph, EventGraphConfig, EVENT_TIME_DRIFT, NULL_ID, N_EVENT_PARENTS};
+use super::{
+    util::HOUR_MS, EventGraph, EventGraphConfig, EVENT_TIME_DRIFT, NULL_ID, N_EVENT_PARENTS,
+};
 use crate::Result;
 
 /// The fixed-size structural metadata of an event.
@@ -136,9 +138,7 @@ impl Header {
             return self.timestamp <= now.saturating_add(EVENT_TIME_DRIFT)
         }
 
-        let Some(rotation_ms) = config.hours_rotation.checked_mul(HOUR as u64) else {
-            return false
-        };
+        let Some(rotation_ms) = config.hours_rotation.checked_mul(HOUR_MS) else { return false };
         let Some(next_slot) = dag_genesis.checked_add(rotation_ms) else { return false };
         let Some(upper_bound) = next_slot.checked_add(EVENT_TIME_DRIFT) else { return false };
 

+ 50 - 9
src/event_graph/mod.rs

@@ -127,6 +127,37 @@ pub struct EventGraphConfig {
     pub max_dags: Option<usize>,
 }
 
+impl EventGraphConfig {
+    /// Validate consensus-critical event graph configuration.
+    pub fn validate(&self) -> Result<()> {
+        if self.max_dags == Some(0) {
+            return Err(Error::Custom("event graph max_dags must be greater than 0".into()))
+        }
+
+        self.rotation_period_millis()?;
+        Ok(())
+    }
+
+    /// Rotation period in milliseconds, or `None` for non-rotating graphs.
+    pub(crate) fn rotation_period_millis(&self) -> Result<Option<u64>> {
+        if self.hours_rotation == 0 {
+            return Ok(None)
+        }
+
+        let rotation_ms = self.hours_rotation.checked_mul(util::HOUR_MS).ok_or_else(|| {
+            Error::Custom("event graph rotation period overflows milliseconds".into())
+        })?;
+
+        if self.initial_genesis.checked_add(rotation_ms).is_none() {
+            return Err(Error::Custom(
+                "event graph initial genesis plus one rotation overflows".into(),
+            ))
+        }
+
+        Ok(Some(rotation_ms))
+    }
+}
+
 pub type EventGraphPtr = Arc<EventGraph>;
 /// Unreferenced tips grouped by layer.
 pub type LayerUTips = BTreeMap<u64, HashSet<blake3::Hash>>;
@@ -325,13 +356,14 @@ impl DagStore {
     /// * **Archive mode** (`max_dags = None`): discover *all*
     ///   existing DAG trees in sled and load them, plus ensure the
     ///   recent window exists. Nothing is ever dropped.
-    pub async fn new(sled_db: sled::Db, config: &EventGraphConfig) -> Self {
+    pub async fn new(sled_db: sled::Db, config: &EventGraphConfig) -> Result<Self> {
+        config.validate()?;
         let mut dags = BTreeMap::new();
 
         if config.hours_rotation == 0 {
             let genesis = generate_genesis(config);
             dags.insert(genesis.header.timestamp, Self::create_slot(&sled_db, &genesis).await);
-            return Self { db: sled_db, dags }
+            return Ok(Self { db: sled_db, dags })
         }
 
         // Determine how many recent DAGs to create/ensure exist.
@@ -380,7 +412,7 @@ impl DagStore {
             dags.insert(ts, Self::create_slot(&sled_db, &genesis).await);
         }
 
-        Self { db: sled_db, dags }
+        Ok(Self { db: sled_db, dags })
     }
 
     async fn create_slot(db: &sled::Db, genesis: &Event) -> DagSlot {
@@ -408,16 +440,23 @@ impl DagStore {
 
     /// Add a new DAG on rotation. In bounded mode, drops the oldest DAG
     /// when the limit is reached. In archive mode, never drops.
-    pub async fn add_dag(&mut self, genesis: &Event, max_dags: Option<usize>) {
+    pub async fn add_dag(&mut self, genesis: &Event, max_dags: Option<usize>) -> Result<()> {
         if let Some(limit) = max_dags {
+            if limit == 0 {
+                return Err(Error::Custom("event graph max_dags must be greater than 0".into()))
+            }
+
             if self.dags.len() >= limit {
-                let (_, old) = self.dags.pop_first().unwrap();
+                let Some((_, old)) = self.dags.pop_first() else {
+                    return Err(Error::Custom("event graph DAG store is empty".into()))
+                };
                 self.db.drop_tree(old.header_tree.name()).unwrap();
                 self.db.drop_tree(old.main_tree.name()).unwrap();
             }
         }
         let slot = Self::create_slot(&self.db, genesis).await;
         self.dags.insert(genesis.header.timestamp, slot);
+        Ok(())
     }
 
     pub fn get_slot(&self, ts: &u64) -> Option<&DagSlot> {
@@ -612,6 +651,7 @@ impl EventGraph {
         config: EventGraphConfig,
         ex: Arc<Executor<'_>>,
     ) -> Result<EventGraphPtr> {
+        config.validate()?;
         let zk_keys = Arc::new(ZkKeys::build_and_load(&sled_db)?);
         Self::with_zk_keys(p2p, sled_db, datastore, replay_mode, config, zk_keys, ex).await
     }
@@ -632,12 +672,13 @@ impl EventGraph {
         zk_keys: Arc<ZkKeys>,
         ex: Arc<Executor<'_>>,
     ) -> Result<EventGraphPtr> {
+        config.validate()?;
         let identity_state = IdentityState::new(&sled_db)?;
         let rln_app_id = rln::RlnAppId::from_genesis(&config.genesis_contents);
         let current_genesis = generate_genesis(&config);
         let (pregenerated_identity_commitments, pregenerated_identity_commitment_reprs) =
             validate_pregenerated_identity_commitments(&config)?;
-        let dag_store = DagStore::new(sled_db.clone(), &config).await;
+        let dag_store = DagStore::new(sled_db.clone(), &config).await?;
         let static_dag = Self::static_new(&sled_db, &config).await?;
         let static_dag_blobs = sled_db.open_tree("static-dag-blobs")?;
         let dag_blobs = sled_db.open_tree("dag-blobs")?;
@@ -1436,7 +1477,7 @@ impl EventGraph {
             }
         }
 
-        self.dag_store.write().await.add_dag(&genesis, self.config.max_dags).await;
+        self.dag_store.write().await.add_dag(&genesis, self.config.max_dags).await?;
         *cur = genesis;
         *bcast = HashSet::new();
         Ok(())
@@ -1445,7 +1486,7 @@ impl EventGraph {
     async fn dag_prune_task(self: Arc<Self>) -> Result<()> {
         loop {
             let next =
-                next_rotation_timestamp(self.config.initial_genesis, self.config.hours_rotation);
+                next_rotation_timestamp(self.config.initial_genesis, self.config.hours_rotation)?;
             let hdr = Header {
                 timestamp: next,
                 parents: NULL_PARENTS,
@@ -1453,7 +1494,7 @@ impl EventGraph {
                 content_hash: blake3::hash(&self.config.genesis_contents),
             };
             let genesis = Event { header: hdr, content: self.config.genesis_contents.clone() };
-            msleep(millis_until_next_rotation(next)).await;
+            msleep(millis_until_next_rotation(next)?).await;
             self.dag_prune(genesis).await?;
         }
     }

+ 48 - 8
src/event_graph/tests.rs

@@ -38,7 +38,7 @@ use crate::{
             archive_config, bounded_dag_store_config, init_logger, make_eg, make_eg_with_config,
             make_network, run_multi_node_test, shutdown_network, test_config, TestIdentity,
         },
-        util::next_hour_timestamp,
+        util::{millis_until_next_rotation, next_hour_timestamp, next_rotation_timestamp},
         DagStore, Event, EventGraphConfig, EventGraphPtr, LayerUTips, TimeIndex, NULL_ID,
         NULL_PARENTS, N_EVENT_PARENTS,
     },
@@ -165,6 +165,46 @@ fn evgr_parent_selection_does_not_wrap_saturated_layer() {
     assert_eq!(parents[0], tip);
 }
 
+#[test]
+fn evgr_config_rejects_invalid_rotation_settings() {
+    let zero_dags = EventGraphConfig { max_dags: Some(0), ..test_config() };
+    assert!(zero_dags.validate().is_err());
+
+    let overflowing_rotation = EventGraphConfig { hours_rotation: u64::MAX, ..test_config() };
+    assert!(overflowing_rotation.validate().is_err());
+
+    let overflowing_genesis =
+        EventGraphConfig { initial_genesis: u64::MAX, hours_rotation: 1, ..test_config() };
+    assert!(overflowing_genesis.validate().is_err());
+}
+
+#[test]
+fn evgr_invalid_config_does_not_open_dag_trees() {
+    smol::block_on(async {
+        let sled_db = sled::Config::new().temporary(true).open().unwrap();
+        let before = sled_db.tree_names();
+        let config = EventGraphConfig { hours_rotation: 1, max_dags: Some(0), ..test_config() };
+
+        let result = DagStore::new(sled_db.clone(), &config).await;
+
+        assert!(matches!(result, Err(crate::Error::Custom(_))));
+        assert_eq!(sled_db.tree_names(), before);
+    })
+}
+
+#[test]
+fn evgr_rotation_helpers_are_total() {
+    const HOUR_MS: u64 = 3_600_000;
+
+    assert!(next_rotation_timestamp(0, 0).is_err());
+
+    let now = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
+    let future_start = now + HOUR_MS;
+    assert_eq!(next_rotation_timestamp(future_start, 1).unwrap(), future_start);
+    assert!(millis_until_next_rotation(now.saturating_sub(1)).is_err());
+    assert_eq!(super::util::hours_since(future_start), 0);
+}
+
 #[test]
 fn evgr_time_index_queries_and_saturating_cursor() {
     // Forward, backward, newest, oldest queries plus the saturating
@@ -190,7 +230,7 @@ fn evgr_time_index_queries_and_saturating_cursor() {
 
 async fn make_dag_store() -> Result<DagStore> {
     let sled_db = sled::Config::new().temporary(true).open().unwrap();
-    Ok(DagStore::new(sled_db, &bounded_dag_store_config()).await)
+    DagStore::new(sled_db, &bounded_dag_store_config()).await
 }
 
 #[test]
@@ -210,14 +250,14 @@ fn evgr_dag_store_eviction_policy() {
             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;
+        store.add_dag(&genesis, Some(24)).await.unwrap();
         assert_eq!(store.dag_timestamps().len(), 24);
         assert!(store.get_slot(&new_ts).is_some());
         assert!(store.get_slot(&oldest_ts).is_none());
 
         // (b) archive
         let sled_db = sled::Config::new().temporary(true).open().unwrap();
-        let mut archive = DagStore::new(sled_db, &archive_config()).await;
+        let mut archive = DagStore::new(sled_db, &archive_config()).await.unwrap();
         let initial = archive.dag_timestamps().len();
         for i in 1..=30i64 {
             let ts = next_hour_timestamp(i);
@@ -228,7 +268,7 @@ fn evgr_dag_store_eviction_policy() {
                 content_hash: blake3::hash(b"test-graph-v1"),
             };
             let genesis = Event { header: hdr, content: b"test-graph-v1".to_vec() };
-            archive.add_dag(&genesis, None).await;
+            archive.add_dag(&genesis, None).await.unwrap();
         }
         assert_eq!(archive.dag_timestamps().len(), initial + 30);
     })
@@ -240,7 +280,7 @@ fn evgr_dag_store_archive_mode_discovers_existing_trees() {
         let sled_db = sled::Config::new().temporary(true).open().unwrap();
         let historical_ts = next_hour_timestamp(-100);
         {
-            let mut store = DagStore::new(sled_db.clone(), &archive_config()).await;
+            let mut store = DagStore::new(sled_db.clone(), &archive_config()).await.unwrap();
             let hdr = Header {
                 timestamp: historical_ts,
                 parents: NULL_PARENTS,
@@ -248,11 +288,11 @@ fn evgr_dag_store_archive_mode_discovers_existing_trees() {
                 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;
+            store.add_dag(&genesis, None).await.unwrap();
             drop(store);
         }
 
-        let store = DagStore::new(sled_db, &archive_config()).await;
+        let store = DagStore::new(sled_db, &archive_config()).await.unwrap();
         assert!(
             store.get_slot(&historical_ts).is_some(),
             "Archive mode should discover historical DAGs on restart"

+ 42 - 26
src/event_graph/util.rs

@@ -36,7 +36,7 @@ use super::{
 };
 use crate::{
     util::{encoding::base64, file::load_file},
-    Result,
+    Error, Result,
 };
 
 #[cfg(feature = "rpc")]
@@ -46,48 +46,62 @@ use crate::rpc::{
 };
 
 /// Milliseconds in one hour.
-pub(super) const HOUR: i64 = 3_600_000;
+pub(super) const HOUR_MS: u64 = 3_600_000;
 
 /// Timestamp (millis) for the start of the hour `hours` offsets from now.
 pub(super) fn next_hour_timestamp(hours: i64) -> u64 {
-    let now = UNIX_EPOCH.elapsed().unwrap().as_millis() as i64;
-    ((now / HOUR) * HOUR + HOUR * hours) as u64
+    let now = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
+    let base = (now / HOUR_MS) * HOUR_MS;
+    let offset = hours.unsigned_abs().saturating_mul(HOUR_MS);
+
+    if hours.is_negative() {
+        base.saturating_sub(offset)
+    } else {
+        base.saturating_add(offset)
+    }
 }
 
 /// Whole hours elapsed since `ts`.
 pub(super) fn hours_since(ts: u64) -> u64 {
     let now = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
-    (now - ts) / HOUR as u64
+    now.saturating_sub(ts) / HOUR_MS
 }
 
 /// 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 {
+pub fn next_rotation_timestamp(starting_timestamp: u64, rotation_period: u64) -> Result<u64> {
     if rotation_period == 0 {
-        panic!("Rotation period cannot be 0");
+        return Err(Error::Custom("event graph rotation period cannot be 0".into()))
     }
-    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)
+
+    let rotation_ms = rotation_period.checked_mul(HOUR_MS).ok_or_else(|| {
+        Error::Custom("event graph rotation period overflows milliseconds".into())
+    })?;
+    let now = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
+
+    if now < starting_timestamp {
+        return Ok(starting_timestamp)
     }
+
+    let elapsed = now.saturating_sub(starting_timestamp);
+    let periods = elapsed
+        .checked_div(rotation_ms)
+        .and_then(|p| p.checked_add(1))
+        .ok_or_else(|| Error::Custom("event graph rotation calculation overflowed".into()))?;
+    let offset = periods
+        .checked_mul(rotation_ms)
+        .ok_or_else(|| Error::Custom("event graph rotation offset overflowed".into()))?;
+
+    starting_timestamp
+        .checked_add(offset)
+        .ok_or_else(|| Error::Custom("event graph next rotation timestamp overflowed".into()))
 }
 
 /// Milliseconds remaining until `next_rotation`.
-///
-/// # Panics
-///
-/// Panics if `next_rotation` is in the past.
-pub fn millis_until_next_rotation(next_rotation: u64) -> u64 {
+pub fn millis_until_next_rotation(next_rotation: u64) -> Result<u64> {
     let now = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
-    assert!(next_rotation >= now, "Next rotation is in the past");
-    next_rotation - now
+    next_rotation
+        .checked_sub(now)
+        .ok_or_else(|| Error::Custom("event graph next rotation is in the past".into()))
 }
 
 /// Generate the deterministic genesis event for the current rotation
@@ -102,7 +116,9 @@ pub fn generate_genesis(config: &EventGraphConfig) -> Event {
     } 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 offset_hours = rotations.saturating_mul(config.hours_rotation);
+        let offset_ms = offset_hours.saturating_mul(HOUR_MS);
+        config.initial_genesis.saturating_add(offset_ms)
     };
     let content_hash = blake3::hash(&config.genesis_contents);
     let header = Header { timestamp, parents: [NULL_ID; N_EVENT_PARENTS], layer: 0, content_hash };