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