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 }