urma_node/daemon/
inputs.rs1use crate::{Error, model::ArchiveConfig, platform::read_admin_file};
2use libp2p::identity::{KeyType, Keypair};
3use std::{fmt::Display, io, path::Path};
4use urma_chain::observation::Chain;
5use urma_runtime::p2p::{HeaderBootstrap, context_headers};
6
7struct Limits;
8
9impl Limits {
10 const CONFIG: usize = 64 * 1024;
11 const BOOTSTRAP: usize = 1024 * 1024;
12 const TOKEN: usize = 128;
13 const PEER_KEY: usize = 4096;
14}
15
16pub(super) fn invalid(error: impl Display) -> Error {
17 Error::Invalid(error.to_string())
18}
19
20#[derive(serde::Deserialize)]
21#[serde(deny_unknown_fields)]
22struct HeaderBootstrapFile {
23 version: u32,
24 chain: Chain,
25 height: u64,
26 header: String,
27 ancestors: Vec<String>,
28}
29
30pub fn read_archive_config(path: &Path) -> Result<ArchiveConfig, Error> {
31 Ok(serde_json::from_slice(&read_admin_file(
32 path,
33 Limits::CONFIG,
34 )?)?)
35}
36
37pub fn read_token(path: &Path) -> Result<String, Error> {
38 let bytes = match urma_io::read_shared(path, Limits::TOKEN) {
39 Ok(bytes) => bytes,
40 Err(urma_io::Error::PrivatePermissions) => {
41 return Err(Error::Invalid(
42 "service token must not be writable by group or open to others".into(),
43 ));
44 }
45 Err(error) => return Err(invalid(format!("invalid service token file: {error}"))),
46 };
47 Ok(std::str::from_utf8(&bytes)
48 .map_err(invalid)?
49 .trim()
50 .to_string())
51}
52
53pub fn read_peer_key(path: &Path) -> Result<Keypair, Error> {
54 if !std::fs::symlink_metadata(path)?.is_file() {
55 return Err(Error::Invalid(
56 "peer key must be a regular private file".into(),
57 ));
58 }
59 let bytes = urma_io::read_private(path, Limits::PEER_KEY).map_err(invalid)?;
60 let key = Keypair::from_protobuf_encoding(&bytes).map_err(invalid)?;
61 if key.key_type() != KeyType::Ed25519 {
62 return Err(Error::Invalid("peer identity must be Ed25519".into()));
63 }
64 Ok(key)
65}
66
67pub fn read_header_bootstrap(
68 path: &Path,
69 chain: Chain,
70 application_height: u64,
71) -> Result<HeaderBootstrap, Error> {
72 let bytes = match urma_io::read_bounded(path, Limits::BOOTSTRAP) {
73 Ok(bytes) => bytes,
74 Err(urma_io::Error::TooLarge { .. }) => {
75 return Err(Error::Invalid("header bootstrap exceeds 1 MiB".into()));
76 }
77 Err(error) => return Err(io::Error::other(error).into()),
78 };
79 let input: HeaderBootstrapFile = serde_json::from_slice(&bytes).map_err(invalid)?;
80 if input.version != 1
81 || input.chain.genesis().map_err(invalid)? != chain.genesis().map_err(invalid)?
82 {
83 return Err(Error::Invalid(
84 "header bootstrap version or chain mismatch".into(),
85 ));
86 }
87 if input.height > application_height {
88 return Err(Error::Invalid(
89 "application start precedes the supplied header anchor".into(),
90 ));
91 }
92 let context = u64::try_from(context_headers(chain)).map_err(invalid)?;
93 let count = usize::try_from(input.height.min(context)).map_err(invalid)?;
94 if input.ancestors.len() != count {
95 return Err(Error::Invalid(format!(
96 "header bootstrap needs exactly {count} ancestors"
97 )));
98 }
99 let anchor = header(&input.header)?;
100 let ancestors = input
101 .ancestors
102 .iter()
103 .map(|raw| header(raw))
104 .collect::<Result<Vec<_>, _>>()?;
105 HeaderBootstrap::new(chain, input.height, anchor, ancestors).map_err(invalid)
106}
107
108fn header(raw: &str) -> Result<bitcoin::block::Header, Error> {
109 if raw.len() != 160 {
110 return Err(Error::Invalid(
111 "bootstrap header must be exactly 80 bytes encoded as hex".into(),
112 ));
113 }
114 bitcoin::consensus::deserialize(&hex::decode(raw).map_err(invalid)?).map_err(invalid)
115}
116
117pub(super) fn private_directory(path: &Path) -> Result<(), Error> {
118 match urma_io::create_private_directory(path) {
119 Ok(()) => Ok(()),
120 Err(error)
121 if error.kind() == io::ErrorKind::AlreadyExists
122 && std::fs::symlink_metadata(path)?.is_dir() =>
123 {
124 Ok(())
125 }
126 Err(error) => Err(error.into()),
127 }
128}