/ src / event_graph / util.rs
util.rs
  1  /* This file is part of DarkFi (https://dark.fi)
  2   *
  3   * Copyright (C) 2020-2025 Dyne.org foundation
  4   *
  5   * This program is free software: you can redistribute it and/or modify
  6   * it under the terms of the GNU Affero General Public License as
  7   * published by the Free Software Foundation, either version 3 of the
  8   * License, or (at your option) any later version.
  9   *
 10   * This program is distributed in the hope that it will be useful,
 11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
 12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13   * GNU Affero General Public License for more details.
 14   *
 15   * You should have received a copy of the GNU Affero General Public License
 16   * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17   */
 18  
 19  use std::{
 20      collections::HashMap,
 21      fs::{self, File, OpenOptions},
 22      io::Write,
 23      path::Path,
 24      time::UNIX_EPOCH,
 25  };
 26  
 27  use darkfi_serial::{deserialize, deserialize_async, serialize};
 28  use sled_overlay::sled;
 29  use tinyjson::JsonValue;
 30  use tracing::error;
 31  
 32  use crate::{
 33      event_graph::{Event, GENESIS_CONTENTS, INITIAL_GENESIS, NULL_ID, N_EVENT_PARENTS},
 34      rpc::{
 35          jsonrpc::{ErrorCode, JsonError, JsonResponse, JsonResult},
 36          util::json_map,
 37      },
 38      util::{encoding::base64, file::load_file},
 39      Result,
 40  };
 41  
 42  /// MilliSeconds in a day
 43  pub(super) const DAY: i64 = 86_400_000;
 44  
 45  /// Calculate the midnight timestamp given a number of days.
 46  /// If `days` is 0, calculate the midnight timestamp of today.
 47  pub(super) fn midnight_timestamp(days: i64) -> u64 {
 48      // Get current time
 49      let now = UNIX_EPOCH.elapsed().unwrap().as_millis() as i64;
 50  
 51      // Find the timestamp for the midnight of the current day
 52      let cur_midnight = (now / DAY) * DAY;
 53  
 54      // Adjust for days_from_now
 55      (cur_midnight + (DAY * days)) as u64
 56  }
 57  
 58  /// Calculate the number of days since a given midnight timestamp.
 59  pub(super) fn days_since(midnight_ts: u64) -> u64 {
 60      // Get current time
 61      let now = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
 62  
 63      // Calculate the difference between the current timestamp
 64      // and the given midnight timestamp
 65      let elapsed_seconds = now - midnight_ts;
 66  
 67      // Convert the elapsed seconds into days
 68      elapsed_seconds / DAY as u64
 69  }
 70  
 71  /// Calculate the timestamp of the next DAG rotation.
 72  pub fn next_rotation_timestamp(starting_timestamp: u64, rotation_period: u64) -> u64 {
 73      // Prevent division by 0
 74      if rotation_period == 0 {
 75          panic!("Rotation period cannot be 0");
 76      }
 77      // Calculate the number of days since the given starting point
 78      let days_passed = days_since(starting_timestamp);
 79  
 80      // Find out how many rotation periods have occurred since
 81      // the starting point.
 82      // Note: when rotation_period = 1, rotations_since_start = days_passed
 83      let rotations_since_start = days_passed.div_ceil(rotation_period);
 84  
 85      // Find out the number of days until the next rotation. Panic if result is beyond the range
 86      // of i64.
 87      let days_until_next_rotation: i64 =
 88          (rotations_since_start * rotation_period - days_passed).try_into().unwrap();
 89  
 90      // Get the timestamp for the next rotation
 91      if days_until_next_rotation == 0 {
 92          // If there are 0 days until the next rotation, we want
 93          // to rotate tomorrow, at midnight. This is a special case.
 94          return midnight_timestamp(1)
 95      }
 96      midnight_timestamp(days_until_next_rotation)
 97  }
 98  
 99  /// Calculate the time in milliseconds until the next_rotation, given
100  /// as a timestamp.
101  /// `next_rotation` here represents a timestamp in UNIX epoch format.
102  pub fn millis_until_next_rotation(next_rotation: u64) -> u64 {
103      // Store `now` in a variable in order to avoid a TOCTOU error.
104      // There may be a drift of one second between this panic check and
105      // the return value if we get unlucky.
106      let now = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
107      if next_rotation < now {
108          panic!("Next rotation timestamp is in the past");
109      }
110      next_rotation - now
111  }
112  
113  /// Generate a deterministic genesis event corresponding to the DAG's configuration.
114  pub fn generate_genesis(days_rotation: u64) -> Event {
115      // Days rotation is u64 except zero
116      let timestamp = if days_rotation == 0 {
117          INITIAL_GENESIS
118      } else {
119          // First check how many days passed since initial genesis.
120          let days_passed = days_since(INITIAL_GENESIS);
121  
122          // Calculate the number of days_rotation intervals since INITIAL_GENESIS
123          let rotations_since_genesis = days_passed / days_rotation;
124  
125          // Calculate the timestamp of the most recent event
126          INITIAL_GENESIS + (rotations_since_genesis * days_rotation * DAY as u64)
127      };
128      Event {
129          timestamp,
130          content: GENESIS_CONTENTS.to_vec(),
131          parents: [NULL_ID; N_EVENT_PARENTS],
132          layer: 0,
133      }
134  }
135  
136  pub(super) fn replayer_log(datastore: &Path, cmd: String, value: Vec<u8>) -> Result<()> {
137      fs::create_dir_all(datastore)?;
138      let datastore = datastore.join("replayer.log");
139      if !datastore.exists() {
140          File::create(&datastore)?;
141      };
142  
143      let mut file = OpenOptions::new().append(true).open(&datastore)?;
144      let v = base64::encode(&value);
145      let f = format!("{cmd} {v}");
146      writeln!(file, "{f}")?;
147  
148      Ok(())
149  }
150  
151  pub async fn recreate_from_replayer_log(datastore: &Path) -> JsonResult {
152      let log_path = datastore.join("replayer.log");
153      if !log_path.exists() {
154          error!("Error loading replayed log");
155          return JsonResult::Error(JsonError::new(
156              ErrorCode::ParseError,
157              Some("Error loading replayed log".to_string()),
158              1,
159          ))
160      };
161  
162      let reader = load_file(&log_path).unwrap();
163  
164      let db_datastore = datastore.join("replayed_db");
165  
166      let sled_db = sled::open(db_datastore).unwrap();
167      let dag = sled_db.open_tree("replayer").unwrap();
168  
169      for line in reader.lines() {
170          let line = line.split(' ').collect::<Vec<&str>>();
171          if line[0] == "insert" {
172              let v = base64::decode(line[1]).unwrap();
173              let v: Event = deserialize(&v).unwrap();
174              let v_se = serialize(&v);
175              dag.insert(v.id().as_bytes(), v_se).unwrap();
176          }
177      }
178  
179      let mut graph = HashMap::new();
180      for iter_elem in dag.iter() {
181          let (id, val) = iter_elem.unwrap();
182          let id = blake3::Hash::from_bytes((&id as &[u8]).try_into().unwrap());
183          let val: Event = deserialize_async(&val).await.unwrap();
184          graph.insert(id, val);
185      }
186  
187      let json_graph = graph
188          .into_iter()
189          .map(|(k, v)| {
190              let key = k.to_string();
191              let value = JsonValue::from(v);
192              (key, value)
193          })
194          .collect();
195      let values = json_map([("dag", JsonValue::Object(json_graph))]);
196      let result = JsonValue::Object(HashMap::from([("eventgraph_info".to_string(), values)]));
197  
198      JsonResponse::new(result, 1).into()
199  }
200  
201  #[cfg(test)]
202  mod tests {
203      use super::*;
204  
205      #[test]
206      fn test_days_since() {
207          let five_days_ago = midnight_timestamp(-5);
208          assert_eq!(days_since(five_days_ago), 5);
209  
210          let today = midnight_timestamp(0);
211          assert_eq!(days_since(today), 0);
212      }
213  
214      #[test]
215      fn test_next_rotation_timestamp() {
216          let starting_point = midnight_timestamp(-10);
217          let rotation_period = 7;
218  
219          // The first rotation since the starting point would be 3 days ago.
220          // So the next rotation should be 4 days from now.
221          let expected = midnight_timestamp(4);
222          assert_eq!(next_rotation_timestamp(starting_point, rotation_period), expected);
223  
224          // When starting from today with a rotation period of 1 (day),
225          // we should get tomorrow's timestamp.
226          // This is a special case.
227          let midnight_today: u64 = midnight_timestamp(0);
228          let midnight_tomorrow = midnight_today + 86_400_000u64; // add a day
229          assert_eq!(midnight_tomorrow, next_rotation_timestamp(midnight_today, 1));
230      }
231  
232      #[test]
233      #[should_panic]
234      fn test_next_rotation_timestamp_panics_on_overflow() {
235          next_rotation_timestamp(0, u64::MAX);
236      }
237  
238      #[test]
239      #[should_panic]
240      fn test_next_rotation_timestamp_panics_on_division_by_zero() {
241          next_rotation_timestamp(0, 0);
242      }
243  
244      #[test]
245      fn test_millis_until_next_rotation_is_within_rotation_interval() {
246          let days_rotation = 1u64;
247          // The amount of time in seconds between rotations.
248          let rotation_interval = days_rotation * 86_400_000u64;
249          let next_rotation_timestamp = next_rotation_timestamp(INITIAL_GENESIS, days_rotation);
250          let s = millis_until_next_rotation(next_rotation_timestamp);
251          assert!(s < rotation_interval);
252      }
253  }