use crate::{avalanche::jsonrpc::info::*, errors::*};
pub use avalanche_types::key::bls::{private_key::Key as BlsPrivateKey, ProofOfPossession};
use avalanche_types::{
ids::node::Id as NodeId,
jsonrpc::info::{GetNodeVersionResult, UptimeResult, VmVersions},
};
use rcgen::{Certificate, CertificateParams, DistinguishedName, DnType, PKCS_RSA_SHA256};
use rustls_pemfile::certs;
use serde::{Deserialize, Serialize};
use std::net::{IpAddr, Ipv4Addr};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AvalancheNode {
pub id: NodeId,
pub signer: Option<ProofOfPossession>,
pub network: String,
pub http_host: String,
pub http_port: u16,
pub https_enabled: bool,
pub public_ip: IpAddr,
pub staking_port: u16,
pub versions: AvalancheNodeVersions,
pub uptime: AvalancheNodeUptime,
}
impl Default for AvalancheNode {
fn default() -> Self {
Self {
id: NodeId::default(),
signer: None,
network: String::from("local"),
http_host: String::from("127.0.0.1"),
http_port: 9650,
https_enabled: false,
public_ip: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
staking_port: 9651,
versions: AvalancheNodeVersions::default(),
uptime: AvalancheNodeUptime::default(),
}
}
}
impl AvalancheNode {
pub fn get_http_endpoint(&self) -> String {
format!(
"{api_scheme}://{http_host}:{http_port}",
api_scheme = if self.https_enabled { "https" } else { "http" },
http_host = self.http_host,
http_port = self.http_port
)
}
pub fn update_info(&mut self) -> Result<(), AshError> {
let http_endpoint = self.get_http_endpoint();
let api_path = format!("{}/{}", http_endpoint, AVAX_INFO_API_ENDPOINT);
(self.id, self.signer) = get_node_id(&api_path).map_err(|e| RpcError::GetFailure {
data_type: "ID".to_string(),
target_type: "node".to_string(),
target_value: http_endpoint.clone(),
msg: e.to_string(),
})?;
let node_ip = get_node_ip(&api_path).map_err(|e| RpcError::GetFailure {
data_type: "node IP".to_string(),
target_type: "node".to_string(),
target_value: http_endpoint.clone(),
msg: e.to_string(),
})?;
self.public_ip = node_ip.ip();
self.staking_port = node_ip.port();
self.versions = get_node_version(&api_path).map_err(|e| RpcError::GetFailure {
data_type: "version".to_string(),
target_type: "node".to_string(),
target_value: http_endpoint.clone(),
msg: e.to_string(),
})?;
self.network = get_network_name(&api_path).map_err(|e| RpcError::GetFailure {
data_type: "network".to_string(),
target_type: "node".to_string(),
target_value: http_endpoint.clone(),
msg: e.to_string(),
})?;
let uptime = get_node_uptime(&api_path);
match uptime {
Ok(uptime) => self.uptime = uptime,
Err(e) => match e {
RpcError::ResponseError {
code,
message,
data,
} => {
if code == -32000 && message.contains("node is not a validator") {
self.uptime = AvalancheNodeUptime::default();
} else {
return Err(AshError::RpcError(RpcError::GetFailure {
data_type: "uptime".to_string(),
target_type: "node".to_string(),
target_value: http_endpoint,
msg: format!(
"{:?}",
RpcError::ResponseError {
code,
message,
data,
}
),
}));
}
}
_ => {
return Err(AshError::RpcError(RpcError::GetFailure {
data_type: "uptime".to_string(),
target_type: "node".to_string(),
target_value: http_endpoint,
msg: e.to_string(),
}));
}
},
}
Ok(())
}
pub fn check_chain_bootstrapping(&self, chain: &str) -> Result<bool, AshError> {
let http_endpoint = self.get_http_endpoint();
let api_path = format!("{}/{}", http_endpoint, AVAX_INFO_API_ENDPOINT);
let is_bootstrapped =
is_bootstrapped(&api_path, chain).map_err(|e| RpcError::GetFailure {
data_type: format!("{} chain bootstrapping", chain),
target_type: "node".to_string(),
target_value: http_endpoint.clone(),
msg: e.to_string(),
})?;
Ok(is_bootstrapped)
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AvalancheNodeVersions {
pub avalanchego_version: String,
pub database_version: String,
pub git_commit: String,
pub vm_versions: VmVersions,
pub rpc_protocol_version: String,
}
impl From<GetNodeVersionResult> for AvalancheNodeVersions {
fn from(node_version: GetNodeVersionResult) -> Self {
Self {
avalanchego_version: node_version.version,
database_version: node_version.database_version,
git_commit: node_version.git_commit,
vm_versions: node_version.vm_versions,
rpc_protocol_version: node_version.rpc_protocol_version,
}
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AvalancheNodeUptime {
pub rewarding_stake_percentage: f64,
pub weighted_average_percentage: f64,
}
impl From<UptimeResult> for AvalancheNodeUptime {
fn from(node_uptime: UptimeResult) -> Self {
Self {
rewarding_stake_percentage: node_uptime.rewarding_stake_percentage,
weighted_average_percentage: node_uptime.weighted_average_percentage,
}
}
}
pub fn node_id_from_cert_der(cert_bytes: &[u8]) -> Result<NodeId, AshError> {
let node_id = NodeId::from_cert_der_bytes(cert_bytes)
.map_err(|e| AvalancheNodeError::InvalidCertificate(e.to_string()))?;
Ok(node_id)
}
pub fn node_id_from_cert_pem(cert_str: &str) -> Result<NodeId, AshError> {
let cert_der = certs(&mut cert_str.as_bytes())
.map_err(|e| AvalancheNodeError::InvalidCertificate(e.to_string()))?
.remove(0);
let node_id = node_id_from_cert_der(&cert_der)?;
Ok(node_id)
}
pub fn generate_node_id(san: impl Into<Vec<String>>) -> Result<(NodeId, String, String), AshError> {
let mut cert_params = CertificateParams::new(san);
if cfg!(target_arch = "aarch64") && cfg!(target_os = "macos") {
cert_params.alg = &PKCS_RSA_SHA256
}
let mut distinguished_name = DistinguishedName::new();
distinguished_name.push(DnType::CountryName, "Avalanche");
distinguished_name.push(DnType::OrganizationName, "E36 Knots");
distinguished_name.push(DnType::OrganizationalUnitName, "Ash");
distinguished_name.push(DnType::CommonName, "Ash CLI self signed cert");
cert_params.distinguished_name = distinguished_name;
let cert = Certificate::from_params(cert_params).unwrap();
let cert_pem = cert.serialize_pem().unwrap();
let key_pem = cert.serialize_private_key_pem();
let node_id = node_id_from_cert_pem(&cert_pem)?;
Ok((node_id, cert_pem, key_pem))
}
pub fn generate_node_bls_key() -> Result<(BlsPrivateKey, ProofOfPossession), AshError> {
let key = BlsPrivateKey::generate().map_err(|e| {
AvalancheNodeError::BlsError(format!("failed to generate private key: {}", e))
})?;
let pop = key.to_proof_of_possession();
Ok((key, pop))
}
#[cfg(test)]
mod tests {
use super::*;
use std::{fs, str::FromStr};
const ASH_TEST_HTTP_PORT: u16 = 9650;
const ASH_TEST_HTTP_HOST: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
const ASH_TEST_STACKING_PORT: u16 = 9651;
const ASH_TEST_NODE_ID: &str = "NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg";
const ASH_TEST_NETWORK_NAME: &str = "network-1337";
#[test]
#[ignore]
fn test_avalanche_node_update_info() {
let mut node = AvalancheNode {
http_host: ASH_TEST_HTTP_HOST.to_string(),
http_port: ASH_TEST_HTTP_PORT,
..Default::default()
};
assert_eq!(node.http_host, ASH_TEST_HTTP_HOST.to_string());
assert_eq!(node.http_port, ASH_TEST_HTTP_PORT);
node.update_info().unwrap();
assert_eq!(node.id, NodeId::from_str(ASH_TEST_NODE_ID).unwrap());
assert_eq!(node.network, ASH_TEST_NETWORK_NAME);
assert_eq!(node.public_ip, ASH_TEST_HTTP_HOST);
assert_eq!(node.staking_port, ASH_TEST_STACKING_PORT);
assert!(!node.versions.avalanchego_version.is_empty());
assert!(!node.versions.database_version.is_empty());
assert!(!node.versions.git_commit.is_empty());
assert!(node.versions.vm_versions != VmVersions::default());
assert!(!node.versions.rpc_protocol_version.is_empty());
assert_ne!(node.uptime.rewarding_stake_percentage, 0.0);
assert_ne!(node.uptime.weighted_average_percentage, 0.0);
}
#[test]
#[ignore]
fn test_avalanche_node_chain_bootstrapping() {
let node = AvalancheNode {
http_host: ASH_TEST_HTTP_HOST.to_string(),
http_port: ASH_TEST_HTTP_PORT,
..Default::default()
};
let is_bootstrapped_p = node.check_chain_bootstrapping("P").unwrap();
let is_bootstrapped_x = node.check_chain_bootstrapping("X").unwrap();
let is_bootstrapped_c = node.check_chain_bootstrapping("C").unwrap();
assert!(is_bootstrapped_p);
assert!(is_bootstrapped_x);
assert!(is_bootstrapped_c);
}
#[test]
fn test_node_id_from_cert_der() {
let cert_pem = fs::read_to_string("tests/certs/validator01.crt").unwrap();
let cert_der = certs(&mut cert_pem.as_bytes()).unwrap().remove(0);
let node_id = node_id_from_cert_der(&cert_der).unwrap();
assert_eq!(
node_id,
NodeId::from_str("NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg").unwrap()
);
}
#[test]
fn test_node_id_from_cert_pem() {
let cert_pem = fs::read_to_string("tests/certs/validator01.crt").unwrap();
let node_id = node_id_from_cert_pem(&cert_pem).unwrap();
assert_eq!(
node_id,
NodeId::from_str("NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg").unwrap()
);
}
}