Skip to main content

snarkos_utilities/
node_data.rs

1// Copyright (c) 2019-2026 Provable Inc.
2// This file is part of the snarkOS library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use std::path::{Path, PathBuf};
17
18/// The filename of the gateway peer cache.
19pub const GATEWAY_PEER_CACHE_FILE: &str = "gateway-peer-cache";
20/// The old filename of the gateway peer cache.
21pub const LEGACY_GATEWAY_PEER_CACHE_FILE: &str = "cached_gateway_peers";
22
23/// The filename of the router peer cache.
24pub const ROUTER_PEER_CACHE_FILE: &str = "router-peer-cache";
25/// The old filename of the router peer cache.
26pub const LEGACY_ROUTER_PEER_CACHE_FILE: &str = "cached_router_peers";
27
28/// The filename of the proposal cache.
29pub const CURRENT_PROPOSAL_CACHE_FILE: &str = "current-proposal-cache";
30
31/// The filename used to persist the hotswapped dev committee's starting round.
32#[cfg(feature = "test_network")]
33pub const DEV_COMMITTEE_STATE_FILE: &str = "dev-committee-state";
34
35/// The filename of the JWT secret for a given address.
36pub fn jwt_secret_file<D: std::fmt::Display>(address: &D) -> PathBuf {
37    PathBuf::from(format!("jwt_secret_{address}.txt"))
38}
39
40/// The old filename of the current proposal cache.
41pub fn legacy_current_proposal_cache_file(network: u16, dev: Option<u16>) -> PathBuf {
42    if let Some(dev) = dev {
43        PathBuf::from(format!(".current-proposal-cache-{network}-{dev}"))
44    } else {
45        PathBuf::from(format!("current-proposal-cache-{network}"))
46    }
47}
48
49/// Tracks information about where the node-specfic configuration files are stored.
50#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct NodeDataDir {
52    path: PathBuf,
53}
54
55impl NodeDataDir {
56    /// Initializes the node data directory the given path.
57    pub fn new(path: PathBuf) -> Self {
58        Self { path }
59    }
60
61    /// Initializes the node data directory to a location suitable for unit/integration tests.
62    pub fn new_test(dev: Option<u16>) -> Self {
63        if let Some(dev) = dev {
64            Self { path: PathBuf::from(format!(".node-data-test-{dev}")) }
65        } else {
66            Self { path: PathBuf::from(".node-data-test") }
67        }
68    }
69
70    /// Initializes the node data directory path to the development path for the specified network and node index.
71    pub fn new_development(network: u16, dev: u16) -> Self {
72        // Use the current directory as the base path, and fall back to the
73        // cargo manifest directory if the current directory is not available.
74        let path = std::env::current_dir()
75            .unwrap_or(PathBuf::from(env!("CARGO_MANIFEST_DIR")))
76            .join(format!(".node-data-{network}-{dev}"));
77
78        Self::new(path)
79    }
80
81    pub fn path(&self) -> &Path {
82        &self.path
83    }
84
85    /// The location to store the previous peer cache.
86    pub fn router_peer_cache_path(&self) -> PathBuf {
87        self.path.join(ROUTER_PEER_CACHE_FILE)
88    }
89
90    pub fn gateway_peer_cache_path(&self) -> PathBuf {
91        self.path.join(GATEWAY_PEER_CACHE_FILE)
92    }
93
94    /// The location to store the current proposal cache.
95    pub fn current_proposal_cache_path(&self) -> PathBuf {
96        self.path.join(CURRENT_PROPOSAL_CACHE_FILE)
97    }
98
99    /// The location used to persist the hotswapped dev committee's starting round.
100    #[cfg(feature = "test_network")]
101    pub fn dev_committee_state_path(&self) -> PathBuf {
102        self.path.join(DEV_COMMITTEE_STATE_FILE)
103    }
104
105    /// The location to store the JWT secret for a given address.
106    pub fn jwt_secret_path<D: std::fmt::Display>(&self, address: &D) -> PathBuf {
107        self.path.join(jwt_secret_file(address))
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn the_cache_filenames_are_pinned() {
117        // These are on-disk names. Renaming one does not fail anything at compile time; it just
118        // orphans the file a running node already wrote, so the change should be deliberate.
119        assert_eq!(ROUTER_PEER_CACHE_FILE, "router-peer-cache");
120        assert_eq!(GATEWAY_PEER_CACHE_FILE, "gateway-peer-cache");
121        assert_eq!(CURRENT_PROPOSAL_CACHE_FILE, "current-proposal-cache");
122        assert_eq!(LEGACY_ROUTER_PEER_CACHE_FILE, "cached_router_peers");
123        assert_eq!(LEGACY_GATEWAY_PEER_CACHE_FILE, "cached_gateway_peers");
124    }
125
126    #[test]
127    fn the_legacy_filenames_are_distinct_from_the_current_ones() {
128        assert_ne!(ROUTER_PEER_CACHE_FILE, LEGACY_ROUTER_PEER_CACHE_FILE);
129        assert_ne!(GATEWAY_PEER_CACHE_FILE, LEGACY_GATEWAY_PEER_CACHE_FILE);
130        assert_ne!(ROUTER_PEER_CACHE_FILE, GATEWAY_PEER_CACHE_FILE);
131    }
132
133    #[test]
134    fn every_path_is_rooted_at_the_data_dir() {
135        let dir = NodeDataDir::new(PathBuf::from("/var/lib/snarkos"));
136
137        for path in [dir.router_peer_cache_path(), dir.gateway_peer_cache_path(), dir.current_proposal_cache_path()] {
138            assert!(path.starts_with(dir.path()), "{path:?} escaped the data dir");
139            assert_eq!(path.parent().unwrap(), dir.path());
140        }
141    }
142
143    #[test]
144    fn each_cache_path_is_a_distinct_file() {
145        let dir = NodeDataDir::new(PathBuf::from("/var/lib/snarkos"));
146
147        // The router and gateway caches hold different peer sets; sharing a filename would have
148        // one silently overwrite the other.
149        assert_ne!(dir.router_peer_cache_path(), dir.gateway_peer_cache_path());
150        assert_ne!(dir.router_peer_cache_path(), dir.current_proposal_cache_path());
151        assert_ne!(dir.gateway_peer_cache_path(), dir.current_proposal_cache_path());
152    }
153
154    #[test]
155    fn the_jwt_secret_path_agrees_with_the_bare_filename_helper() {
156        let dir = NodeDataDir::new(PathBuf::from("/var/lib/snarkos"));
157        let address = "aleo1example";
158
159        // `cli::commands::start` builds this path itself out of `path()` and `jwt_secret_file`
160        // rather than calling `jwt_secret_path`, so the two constructions have to stay in step or
161        // the node writes its JWT secret where the reader will not look.
162        assert_eq!(dir.jwt_secret_path(&address), dir.path().join(jwt_secret_file(&address)));
163    }
164
165    #[test]
166    fn the_jwt_secret_filename_is_scoped_to_the_address() {
167        assert_eq!(jwt_secret_file(&"aleo1abc"), PathBuf::from("jwt_secret_aleo1abc.txt"));
168        assert_ne!(jwt_secret_file(&"aleo1abc"), jwt_secret_file(&"aleo1def"));
169    }
170
171    #[test]
172    fn the_legacy_proposal_cache_filename_switches_on_dev() {
173        // note: the dev form is a hidden file and the non-dev form is not.
174        assert_eq!(legacy_current_proposal_cache_file(1, Some(3)), PathBuf::from(".current-proposal-cache-1-3"));
175        assert_eq!(legacy_current_proposal_cache_file(1, None), PathBuf::from("current-proposal-cache-1"));
176
177        // Distinct dev indices must not collide.
178        assert_ne!(legacy_current_proposal_cache_file(1, Some(0)), legacy_current_proposal_cache_file(1, Some(1)));
179        // Neither must distinct networks.
180        assert_ne!(legacy_current_proposal_cache_file(0, None), legacy_current_proposal_cache_file(1, None));
181    }
182
183    #[test]
184    fn test_data_dirs_are_separated_by_dev_index() {
185        assert_eq!(NodeDataDir::new_test(None).path(), Path::new(".node-data-test"));
186        assert_eq!(NodeDataDir::new_test(Some(2)).path(), Path::new(".node-data-test-2"));
187
188        // Two dev nodes running side by side must not share a data dir.
189        assert_ne!(NodeDataDir::new_test(Some(0)), NodeDataDir::new_test(Some(1)));
190        assert_ne!(NodeDataDir::new_test(None), NodeDataDir::new_test(Some(0)));
191    }
192
193    #[test]
194    fn development_data_dirs_are_absolute_and_separated_by_network_and_index() {
195        let first = NodeDataDir::new_development(1, 0);
196        let second = NodeDataDir::new_development(1, 1);
197        let other_network = NodeDataDir::new_development(2, 0);
198
199        // The path is anchored at the current directory, so it must not be a bare relative name.
200        assert!(first.path().is_absolute());
201        assert_eq!(first.path().file_name().unwrap(), ".node-data-1-0");
202
203        assert_ne!(first, second);
204        assert_ne!(first, other_network);
205    }
206}