darkfi/event_graph/
util.rs

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