use std::{fs, io, path::Path, sync::Arc};
use log::*;
use serde::{Serialize, de::DeserializeOwned};
use tari_common::{
configuration::bootstrap::prompt,
exit_codes::{ExitCode, ExitError},
};
use tari_comms::{
NodeIdentity,
multiaddr::{Multiaddr, Protocol},
peer_manager::PeerFeatures,
tor::TorIdentity,
};
use tari_p2p::TransportType;
use tari_utilities::hex::Hex;
pub const LOG_TARGET: &str = "minotari_application";
const REQUIRED_IDENTITY_PERMS: u32 = 0o100600;
pub fn setup_node_identity<P: AsRef<Path>>(
identity_file: P,
public_addresses: Vec<Multiaddr>,
create_id: bool,
peer_features: PeerFeatures,
transport_type: TransportType,
) -> Result<Arc<NodeIdentity>, ExitError> {
match load_node_identity(&identity_file, transport_type) {
Ok(mut id) => {
id.set_peer_features(peer_features);
let filtered_addresses = filter_addresses_for_transport(public_addresses, transport_type);
for public_address in filtered_addresses {
id.add_public_address(public_address.clone());
debug!(
target: LOG_TARGET,
"Added address: {}",
public_address
);
}
Ok(Arc::new(id))
},
Err(IdentityError::InvalidPermissions) => Err(ExitError::new(
ExitCode::ConfigError,
format!(
"{path} has incorrect permissions. You can update the identity file with the correct permissions \
using 'chmod 600 {path}', or delete the identity file and a new one will be created on next start",
path = identity_file.as_ref().to_string_lossy()
),
)),
Err(e) => {
warn!(target: LOG_TARGET, "Failed to load node identity: {e}");
if !create_id {
let prompt = prompt("Node identity does not exist.\nWould you like to create one (Y/n)?");
if !prompt {
error!(
target: LOG_TARGET,
"Node identity not found. {e}. You can update the configuration file to point to a valid node \
identity file, or re-run the node and create a new one."
);
return Err(ExitError::new(
ExitCode::ConfigError,
format!(
"Node identity information not found. {e}. You can update the configuration file to point \
to a valid node identity file, or re-run the node to create a new one"
),
));
};
}
debug!(target: LOG_TARGET, "Existing node id not found. {e}. Creating new ID");
let filtered_addresses = filter_addresses_for_transport(public_addresses, transport_type);
match create_new_node_identity(&identity_file, filtered_addresses, peer_features) {
Ok(id) => {
info!(
target: LOG_TARGET,
"New node identity [{}] with public key {} has been created at {}.",
id.node_id(),
id.public_key(),
identity_file.as_ref().to_str().unwrap_or("?"),
);
Ok(Arc::new(id))
},
Err(e) => {
error!(target: LOG_TARGET, "Could not create new node id. {e}.");
Err(ExitError::new(
ExitCode::ConfigError,
format!("Could not create new node id. {e}."),
))
},
}
},
}
}
fn load_node_identity<P: AsRef<Path>>(path: P, transport_type: TransportType) -> Result<NodeIdentity, IdentityError> {
check_identity_file(&path)?;
let id_str = fs::read_to_string(path.as_ref())?;
let id = json5::from_str::<NodeIdentity>(&id_str)?;
let id = if transport_type == TransportType::Tcp {
let current_addresses = id.public_addresses();
debug!(
target: LOG_TARGET,
"Filtering addresses for TCP transport. Current addresses: {:?}",
current_addresses
);
let filtered_addresses: Vec<Multiaddr> = current_addresses
.into_iter()
.filter(|addr| !addr.iter().any(|p| matches!(p, Protocol::Onion3(_))))
.collect();
debug!(
target: LOG_TARGET,
"After filtering for TCP transport, {} addresses remain: {:?}",
filtered_addresses.len(),
filtered_addresses
);
id.set_public_addresses(filtered_addresses);
id
} else {
id
};
if !id.is_signed() {
id.sign();
}
debug!(
target: LOG_TARGET,
"Node ID loaded with public key {} and Node id {}",
id.public_key().to_hex(),
id.node_id().to_hex()
);
save_as_json(&path, &id)?;
Ok(id)
}
fn filter_addresses_for_transport(addresses: Vec<Multiaddr>, transport_type: TransportType) -> Vec<Multiaddr> {
if transport_type == TransportType::Tcp {
let filtered: Vec<Multiaddr> = addresses
.into_iter()
.filter(|addr| !addr.iter().any(|p| matches!(p, Protocol::Onion3(_))))
.collect();
debug!(
target: LOG_TARGET,
"Filtered addresses for TCP transport: {:?}",
filtered
);
filtered
} else {
debug!(
target: LOG_TARGET,
"No filtering for {:?} transport, keeping all {} addresses",
transport_type,
addresses.len()
);
addresses
}
}
fn create_new_node_identity<P: AsRef<Path>>(
path: P,
public_addresses: Vec<Multiaddr>,
features: PeerFeatures,
) -> Result<NodeIdentity, IdentityError> {
let node_identity = NodeIdentity::random_multiple_addresses(&mut rand::rng(), public_addresses, features);
save_as_json(&path, &node_identity)?;
Ok(node_identity)
}
pub fn load_from_json<P: AsRef<Path>, T: DeserializeOwned>(path: P) -> Result<Option<T>, IdentityError> {
if !path.as_ref().exists() {
return Ok(None);
}
let contents = fs::read_to_string(path)?;
let object = json5::from_str(&contents)?;
Ok(Some(object))
}
pub fn load_tor_identity<P: AsRef<Path>>(path: P) -> Result<Option<TorIdentity>, IdentityError> {
check_identity_file(&path)?;
let identity = load_from_json(path)?;
Ok(identity)
}
pub fn save_as_json<P: AsRef<Path>, T: Serialize>(path: P, object: &T) -> Result<(), IdentityError> {
let json = json5::to_string(object)?;
if let Some(p) = path.as_ref().parent() &&
!p.exists()
{
fs::create_dir_all(p)?;
}
let json_with_comment =
format!("// This file is generated by the Minotari base node. Any changes will be overwritten.\n{json}");
fs::write(path.as_ref(), json_with_comment.as_bytes())?;
set_permissions(path, REQUIRED_IDENTITY_PERMS)?;
Ok(())
}
fn check_identity_file<P: AsRef<Path>>(path: P) -> Result<(), IdentityError> {
if !path.as_ref().exists() {
return Err(IdentityError::NotFound);
}
if !path.as_ref().metadata()?.is_file() {
return Err(IdentityError::NotFile);
}
if !has_permissions(&path, REQUIRED_IDENTITY_PERMS)? {
return Err(IdentityError::InvalidPermissions);
}
Ok(())
}
#[cfg(target_family = "unix")]
fn set_permissions<P: AsRef<Path>>(path: P, new_perms: u32) -> io::Result<()> {
use std::os::unix::fs::PermissionsExt;
let metadata = fs::metadata(&path)?;
let mut perms = metadata.permissions();
perms.set_mode(new_perms);
fs::set_permissions(path, perms)?;
Ok(())
}
#[cfg(target_family = "windows")]
fn set_permissions<P: AsRef<Path>>(_: P, _: u32) -> io::Result<()> {
Ok(())
}
#[cfg(target_family = "unix")]
fn has_permissions<P: AsRef<Path>>(path: P, perms: u32) -> io::Result<bool> {
use std::os::unix::fs::PermissionsExt;
let metadata = fs::metadata(path)?;
Ok(metadata.permissions().mode() == perms)
}
#[cfg(target_family = "windows")]
fn has_permissions<P: AsRef<Path>>(_: P, _: u32) -> io::Result<bool> {
Ok(true)
}
#[derive(Debug, thiserror::Error)]
pub enum IdentityError {
#[error("Identity file has invalid permissions")]
InvalidPermissions,
#[error("Identity file was not found")]
NotFound,
#[error("Path is not a file")]
NotFile,
#[error("Malformed identity file: {0}")]
JsonError(#[from] json5::Error),
#[error(transparent)]
Io(#[from] io::Error),
}