decdn_common/data_dir.rs
1//! Which files live in a `data_dir`, and whose they are.
2//!
3//! A `decdn-node` daemon and a standalone client can be pointed at the same
4//! directory, and they keep *different* stores there. The buyer pools are the
5//! pair that matters: the daemon's [`NODE_BUYER_DB_FILE`] and the client's
6//! [`CLIENT_BUYER_DB_FILE`] share one table format and nothing else — different
7//! pools, different owners, and `redb` holds a process-exclusive lock on the
8//! daemon's for its lifetime, so no other process can open it.
9//!
10//! Naming them here rather than in each crate is what lets an operator tool
11//! tell the two apart before it writes to the wrong one (#2078).
12
13use std::io;
14use std::path::Path;
15
16/// The daemon's buyer-pool store: buyer pool state and its owner index.
17pub const NODE_BUYER_DB_FILE: &str = "buyer.redb";
18
19/// The daemon's seller-side per-lane voucher store.
20pub const NODE_LANES_DB_FILE: &str = "lanes.redb";
21
22/// The daemon's pending-settle store (seller and buyer sets).
23pub const NODE_SETTLE_DB_FILE: &str = "settle.redb";
24
25/// The daemon's settlement-watcher checkpoint store.
26pub const NODE_CHECKPOINT_DB_FILE: &str = "checkpoint.redb";
27
28/// The standalone client's buyer-only pool store (`decdn get`, `decdn pool`).
29pub const CLIENT_BUYER_DB_FILE: &str = "buyer-pools.redb";
30
31/// Every store file a `decdn-node` daemon owns in its data dir.
32///
33/// The daemon's store opens all of them at bring-up, so any one of them present
34/// means a daemon has run against this directory. Nothing a client runs creates
35/// any of them.
36pub const DAEMON_STORE_FILES: &[&str] = &[
37 NODE_BUYER_DB_FILE,
38 NODE_LANES_DB_FILE,
39 NODE_SETTLE_DB_FILE,
40 NODE_CHECKPOINT_DB_FILE,
41];
42
43/// The daemon-owned file that marks `data_dir` as a node's, if any.
44///
45/// Checks the whole [`DAEMON_STORE_FILES`] set rather than
46/// [`NODE_BUYER_DB_FILE`] alone, because the buyer store is exactly the file an
47/// operator recovering from a reset deletes or loses. A directory whose
48/// `buyer.redb` is gone but whose `lanes.redb` remains is still a node's, and
49/// treating it as a client's is how a second deposit gets escrowed into a store
50/// the daemon never reads.
51///
52/// Each file is checked with `symlink_metadata`, not [`Path::exists`], which
53/// collapses every stat error to `false`. Only `NotFound` reads as "absent". Any
54/// other stat error (a permission denial, a loop, a transient I/O fault) leaves
55/// the owner unknown, and this returns it: the callers gate refusals that move
56/// USDC on this answer, so an unknown owner must refuse rather than pass as a
57/// client's directory (#2086).
58///
59/// # Errors
60///
61/// The first stat error other than `NotFound`, with the path it concerns.
62pub fn daemon_marker(data_dir: &Path) -> io::Result<Option<&'static str>> {
63 for name in DAEMON_STORE_FILES.iter().copied() {
64 let path = data_dir.join(name);
65 match std::fs::symlink_metadata(&path) {
66 Ok(_) => return Ok(Some(name)),
67 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
68 Err(e) => {
69 return Err(io::Error::new(
70 e.kind(),
71 format!("failed to stat {}: {e}", path.display()),
72 ));
73 }
74 }
75 }
76 Ok(None)
77}
78
79#[cfg(test)]
80#[allow(clippy::unwrap_used, clippy::expect_used)]
81mod tests;