diff --git a/core/Cargo.toml b/core/Cargo.toml index 1d1743df..95d73ad5 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "oracle-core" version = "2.0.0-beta9" -authors = ["Robert Kornacki <11645932+robkorn@users.noreply.github.com>", "@greenhat", "@kettlebell", "@SethDusek"] +authors = [ + "Robert Kornacki <11645932+robkorn@users.noreply.github.com>", + "@greenhat", + "@kettlebell", + "@SethDusek", +] edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -29,19 +34,20 @@ ergo-lib = { workspace = true } ergo-node-interface = { git = "https://github.com/ergoplatform/ergo-node-interface-rust", rev = "143c2a3dc8fb772d1af37f1f1e1924067c6aad14" } # ergo-node-interface = { version = "0.4" } derive_more = "0.99" -clap = {version = "4.2.4", features = ["derive"]} +clap = { version = "4.2.4", features = ["derive"] } exitcode = "1.1.2" lazy_static = "1.4.0" once_cell = "1.15.0" futures = "0.3" +prometheus = "0.13" [dev-dependencies] -ergo-lib = { workspace = true, features = ["arbitrary"]} -proptest = {version = "1.0.0"} -proptest-derive = {version = "0.3.0"} -sigma-test-util = {version = "0.3.0"} -ergo-chain-sim = {version = "0.1.0", path="../ergo-chain-sim"} -env_logger = {version = "0.10.0"} -tokio-test = {version = "0.4"} -pretty_assertions = {workspace = true} +ergo-lib = { workspace = true, features = ["arbitrary"] } +proptest = { version = "1.0.0" } +proptest-derive = { version = "0.3.0" } +sigma-test-util = { version = "0.3.0" } +ergo-chain-sim = { version = "0.1.0", path = "../ergo-chain-sim" } +env_logger = { version = "0.10.0" } +tokio-test = { version = "0.4" } +pretty_assertions = { workspace = true } expect-test = "1.0.1" diff --git a/core/src/address_util.rs b/core/src/address_util.rs index 685ce37f..1c4d2ca1 100644 --- a/core/src/address_util.rs +++ b/core/src/address_util.rs @@ -1,9 +1,10 @@ -use ergo_lib::ergotree_ir::{ - chain::address::{Address, AddressEncoder, AddressEncoderError}, - mir::constant::{Constant, Literal}, - serialization::{SigmaParsingError, SigmaSerializable, SigmaSerializationError}, - sigma_protocol::sigma_boolean::ProveDlog, -}; +use ergo_lib::ergo_chain_types::EcPoint; +use ergo_lib::ergotree_ir::chain::address::Address; +use ergo_lib::ergotree_ir::chain::address::AddressEncoderError; +use ergo_lib::ergotree_ir::chain::address::NetworkAddress; +use ergo_lib::ergotree_ir::chain::address::NetworkPrefix; +use ergo_lib::ergotree_ir::serialization::SigmaParsingError; +use ergo_lib::ergotree_ir::serialization::SigmaSerializationError; use thiserror::Error; #[derive(Error, Debug)] @@ -22,126 +23,11 @@ pub enum AddressUtilError { Base16DecodeError(#[from] base16::DecodeError), } -/// Given a P2S Ergo address, extract the hex-encoded serialized ErgoTree (script) -pub fn address_to_tree(address: &str) -> Result { - let address_parsed = AddressEncoder::unchecked_parse_network_address_from_str(address)?; - let script = address_parsed.address().script()?; - Ok(base16::encode_lower(&script.sigma_serialize_bytes()?)) -} - -/// Given a P2S Ergo address, convert it to a hex-encoded Sigma byte array constant -pub fn address_to_bytes(address: &str) -> Result { - let address_parsed = AddressEncoder::unchecked_parse_network_address_from_str(address)?; - let script = address_parsed.address().script()?; - Ok(base16::encode_lower( - &Constant::from(script.sigma_serialize_bytes()?).sigma_serialize_bytes()?, - )) -} - -/// Given an Ergo P2PK Address, convert it to a raw hex-encoded EC point -/// and prepend the type bytes so it is encoded and ready -/// to be used in a register. -pub fn address_to_raw_for_register(address: &str) -> Result { - let address_parsed = AddressEncoder::unchecked_parse_network_address_from_str(address)?; - match address_parsed.address() { - Address::P2Pk(ProveDlog { h }) => Ok(base16::encode_lower( - &Constant::from(*h).sigma_serialize_bytes()?, - )), - Address::P2SH(_) | Address::P2S(_) => Err(AddressUtilError::ExpectedP2PK), - } -} - -/// Given an Ergo P2PK Address, convert it to a raw hex-encoded EC point -pub fn address_to_raw(address: &str) -> Result { - let address_parsed = AddressEncoder::unchecked_parse_network_address_from_str(address)?; - match address_parsed.address() { - Address::P2Pk(_) => Ok(base16::encode_lower( - &address_parsed.address().content_bytes(), - )), - Address::P2SH(_) | Address::P2S(_) => Err(AddressUtilError::ExpectedP2PK), - } -} - -/// Given a raw hex-encoded EC point, convert it to a P2PK address -pub fn raw_to_address(raw: &str) -> Result { - let bytes = base16::decode(raw)?; - Address::p2pk_from_pk_bytes(&bytes).map_err(Into::into) -} - -/// Given a raw hex-encoded EC point from a register (thus with type encoded characters in front), -/// convert it to a P2PK address -pub fn raw_from_register_to_address(raw: &str) -> Result { - let bytes = base16::decode(raw)?; - let constant = Constant::sigma_parse_bytes(&bytes)?; - if let Literal::GroupElement(h) = constant.v { - Ok(Address::P2Pk(ProveDlog { h })) - } else { - Err(AddressUtilError::ExpectedP2PK) - } -} - -#[cfg(test)] -mod test { - use ergo_lib::ergotree_ir::chain::address::{AddressEncoder, NetworkPrefix}; - - use crate::address_util::{ - address_to_bytes, address_to_raw, address_to_raw_for_register, address_to_tree, - raw_from_register_to_address, raw_to_address, - }; - - // Test serialization for default address argument of /utils/addressToRaw - #[test] - fn test_address_to_raw_for_register() { - assert_eq!( - "07028333f9f7454f8d5ff73dbac9833767ed6fc3a86cf0a73df946b32ea9927d9197", - address_to_raw_for_register("3WwbzW6u8hKWBcL1W7kNVMr25s2UHfSBnYtwSHvrRQt7DdPuoXrt") - .unwrap() - ); - assert_eq!( - "028333f9f7454f8d5ff73dbac9833767ed6fc3a86cf0a73df946b32ea9927d9197", - address_to_raw("3WwbzW6u8hKWBcL1W7kNVMr25s2UHfSBnYtwSHvrRQt7DdPuoXrt").unwrap() - ); - } - #[test] - fn test_address_raw_roundtrip() { - let address = AddressEncoder::new(NetworkPrefix::Testnet) - .parse_address_from_str("3WwbzW6u8hKWBcL1W7kNVMr25s2UHfSBnYtwSHvrRQt7DdPuoXrt") - .unwrap(); - assert_eq!( - address, - raw_to_address( - &address_to_raw("3WwbzW6u8hKWBcL1W7kNVMr25s2UHfSBnYtwSHvrRQt7DdPuoXrt").unwrap() - ) - .unwrap() - ); - } - #[test] - fn test_address_raw_register_roundtrip() { - let address = AddressEncoder::new(NetworkPrefix::Testnet) - .parse_address_from_str("3WwbzW6u8hKWBcL1W7kNVMr25s2UHfSBnYtwSHvrRQt7DdPuoXrt") - .unwrap(); - assert_eq!( - address, - raw_from_register_to_address( - &address_to_raw_for_register( - "3WwbzW6u8hKWBcL1W7kNVMr25s2UHfSBnYtwSHvrRQt7DdPuoXrt" - ) - .unwrap() - ) - .unwrap() - ); - } - - // test serialization of "sigmaProp(true)" script - #[test] - fn test_address_to_tree() { - assert_eq!( - "10010101d17300", - address_to_tree("Ms7smJwLGbUAjuWQ").unwrap() - ); - assert_eq!( - "0e0710010101d17300", - address_to_bytes("Ms7smJwLGbUAjuWQ").unwrap() - ); - } +pub fn pks_to_network_addresses( + pks: Vec, + network_prefix: NetworkPrefix, +) -> Vec { + pks.into_iter() + .map(|pk| NetworkAddress::new(network_prefix, &Address::P2Pk(pk.into()))) + .collect() } diff --git a/core/src/api.rs b/core/src/api.rs index 9bf3876b..9c330bb3 100644 --- a/core/src/api.rs +++ b/core/src/api.rs @@ -2,9 +2,10 @@ use std::convert::From; use std::net::SocketAddr; use std::sync::Arc; -use crate::box_kind::{OracleBoxWrapper, PoolBox}; -use crate::node_interface::node_api::NodeApi; -use crate::oracle_config::{get_core_api_port, ORACLE_CONFIG}; +use crate::box_kind::PoolBox; +use crate::monitor::{check_oracle_health, check_pool_health, PoolHealth}; +use crate::node_interface::node_api::{NodeApi, NodeApiError}; +use crate::oracle_config::ORACLE_CONFIG; use crate::oracle_state::{DataSourceError, LocalDatapointState, OraclePool}; use crate::pool_config::POOL_CONFIG; use axum::http::StatusCode; @@ -135,26 +136,8 @@ fn pool_status_sync(oracle_pool: Arc) -> Result= pool_box_height) - .count(); - - let collected_boxes = oracle_pool - .get_collected_datapoint_boxes_source() - .get_collected_datapoint_boxes()?; - let collected_count_previous_epoch = collected_boxes - .into_iter() - .filter(|b| b.get_box().creation_height == pool_box_height) - .count(); - - let active_oracle_count = collected_count_previous_epoch + posted_count_current_epoch; let pool_health = pool_health_sync(oracle_pool)?; - + let active_oracle_count = pool_health.details.active_oracles.len(); let json = Json(json!({ "latest_pool_datapoint": pool_box.rate(), "latest_pool_box_height": pool_box_height, @@ -202,72 +185,43 @@ fn oracle_health_sync(oracle_pool: Arc) -> Result match b { - OracleBoxWrapper::Posted(posted_box) => { - let creation_height = posted_box.get_box().creation_height; - check_details["posted_box_height"] = json!(creation_height); - creation_height > pool_box_height - } - OracleBoxWrapper::Collected(collected_box) => { - let creation_height = collected_box.get_box().creation_height; - check_details["collected_box_height"] = json!(creation_height); - creation_height == pool_box_height - } - }, - None => false, - }; - let json = json!({ - "status": if is_healthy { "OK" } else { "DOWN" }, - "details": check_details, - }); - Ok(json) + .creation_height + .into(); + let oracle_health = check_oracle_health(oracle_pool, pool_box_height)?; + Ok(serde_json::to_value(oracle_health).unwrap()) } async fn pool_health(oracle_pool: Arc) -> Result, ApiError> { - let json = task::spawn_blocking(|| pool_health_sync(oracle_pool)) + let json = task::spawn_blocking(|| pool_health_sync_json(oracle_pool)) .await .unwrap()?; Ok(Json(json)) } -fn pool_health_sync(oracle_pool: Arc) -> Result { - let pool_conf = &POOL_CONFIG; + +fn pool_health_sync(oracle_pool: Arc) -> Result { let node_api = NodeApi::new(ORACLE_CONFIG.node_api_key.clone(), &ORACLE_CONFIG.node_url); - let current_height = node_api.node.current_block_height()? as u32; + let current_height = (node_api.node.current_block_height()? as u32).into(); let pool_box_height = oracle_pool .get_pool_box_source() .get_pool_box()? .get_box() - .creation_height; - let epoch_length = pool_conf - .refresh_box_wrapper_inputs - .contract_inputs - .contract_parameters() - .epoch_length() - .0 as u32; - let check_details = json!({ - "pool_box_height": pool_box_height, - "current_block_height": current_height, - "epoch_length": epoch_length, - }); - let is_healthy = pool_box_height >= current_height - epoch_length; - let json = json!({ - "status": if is_healthy { "OK" } else { "DOWN" }, - "details": check_details, - }); - Ok(json) + .creation_height + .into(); + let network_prefix = node_api.get_change_address()?.network(); + let pool_health = + check_pool_health(current_height, pool_box_height, oracle_pool, network_prefix)?; + Ok(pool_health) +} + +fn pool_health_sync_json(oracle_pool: Arc) -> Result { + let pool_health = pool_health_sync(oracle_pool)?; + Ok(serde_json::to_value(pool_health).unwrap()) } pub async fn start_rest_server( repost_receiver: Receiver, oracle_pool: Arc, + api_port: u16, ) -> Result<(), anyhow::Error> { let op_clone = oracle_pool.clone(); let op_clone2 = oracle_pool.clone(); @@ -290,7 +244,8 @@ pub async fn start_rest_server( .allow_origin(tower_http::cors::Any) .allow_methods([axum::http::Method::GET]), ); - let addr = SocketAddr::from(([0, 0, 0, 0], get_core_api_port().parse().unwrap())); + let addr = SocketAddr::from(([0, 0, 0, 0], api_port)); + log::info!("Starting REST server on {}", addr); axum::Server::try_bind(&addr)? .serve(app.into_make_service()) .await?; @@ -322,3 +277,9 @@ impl From for ApiError { ApiError(format!("Error: {:?}", err)) } } + +impl From for ApiError { + fn from(err: NodeApiError) -> Self { + ApiError(format!("NodeApiError: {:?}", err)) + } +} diff --git a/core/src/box_kind/oracle_box.rs b/core/src/box_kind/oracle_box.rs index cc6d7843..730046f0 100644 --- a/core/src/box_kind/oracle_box.rs +++ b/core/src/box_kind/oracle_box.rs @@ -175,11 +175,10 @@ impl OracleBox for OracleBoxWrapper { } fn public_key(&self) -> EcPoint { - self.get_box() - .get_register(NonMandatoryRegisterId::R4.into()) - .unwrap() - .try_extract_into::() - .unwrap() + match self { + OracleBoxWrapper::Posted(p) => p.public_key().clone(), + OracleBoxWrapper::Collected(c) => c.public_key().clone(), + } } fn get_box(&self) -> &ErgoBox { @@ -276,6 +275,14 @@ impl CollectedOracleBox { pub fn get_box(&self) -> &ErgoBox { &self.ergo_box } + + pub fn public_key(&self) -> EcPoint { + self.ergo_box + .get_register(NonMandatoryRegisterId::R4.into()) + .unwrap() + .try_extract_into::() + .unwrap() + } } #[derive(Clone, Debug)] diff --git a/core/src/box_kind/pool_box.rs b/core/src/box_kind/pool_box.rs index 155a609a..a60cdc16 100644 --- a/core/src/box_kind/pool_box.rs +++ b/core/src/box_kind/pool_box.rs @@ -13,6 +13,7 @@ use crate::contracts::pool::PoolContractInputs; use crate::contracts::pool::PoolContractParameters; use crate::oracle_types::BlockHeight; use crate::oracle_types::EpochCounter; +use crate::oracle_types::Rate; use crate::spec_token::PoolTokenId; use crate::spec_token::RefreshTokenId; use crate::spec_token::RewardTokenId; @@ -25,7 +26,7 @@ pub trait PoolBox { fn pool_nft_token(&self) -> SpecToken; fn reward_token(&self) -> SpecToken; fn epoch_counter(&self) -> EpochCounter; - fn rate(&self) -> i64; + fn rate(&self) -> Rate; fn get_box(&self) -> &ErgoBox; } @@ -124,12 +125,13 @@ impl PoolBox for PoolBoxWrapper { ) } - fn rate(&self) -> i64 { + fn rate(&self) -> Rate { self.ergo_box .get_register(NonMandatoryRegisterId::R4.into()) .unwrap() .try_extract_into::() .unwrap() + .into() } fn reward_token(&self) -> SpecToken { @@ -225,7 +227,7 @@ pub fn make_pool_box_candidate( /// Make a pool box without type-checking reward token. Mainly used when updating the pool pub fn make_pool_box_candidate_unchecked( contract: &PoolContract, - datapoint: i64, + datapoint: Rate, epoch_counter: EpochCounter, pool_nft_token: SpecToken, reward_token: SpecToken, @@ -233,6 +235,7 @@ pub fn make_pool_box_candidate_unchecked( creation_height: BlockHeight, ) -> Result { let mut builder = ErgoBoxCandidateBuilder::new(value, contract.ergo_tree(), creation_height.0); + let datapoint: i64 = datapoint.into(); builder.set_register_value(NonMandatoryRegisterId::R4, datapoint.into()); builder.set_register_value(NonMandatoryRegisterId::R5, (epoch_counter.0 as i32).into()); builder.add_token(pool_nft_token.into()); diff --git a/core/src/main.rs b/core/src/main.rs index 0baca4c8..67b94e59 100644 --- a/core/src/main.rs +++ b/core/src/main.rs @@ -29,7 +29,9 @@ mod datapoint_source; mod default_parameters; mod explorer_api; mod logging; +mod metrics; mod migrate; +mod monitor; mod node_interface; mod oracle_config; mod oracle_state; @@ -55,13 +57,13 @@ use clap::{Parser, Subcommand}; use crossbeam::channel::bounded; use datapoint_source::RuntimeDataPointSource; use ergo_lib::ergo_chain_types::Digest32; -use ergo_lib::ergotree_ir::chain::address::Address; -use ergo_lib::ergotree_ir::chain::address::NetworkAddress; use ergo_lib::ergotree_ir::chain::address::NetworkPrefix; use ergo_lib::ergotree_ir::chain::token::TokenAmount; use ergo_lib::ergotree_ir::chain::token::TokenId; use log::error; use log::LevelFilter; +use metrics::start_metrics_server; +use metrics::update_metrics; use node_interface::assert_wallet_unlocked; use node_interface::node_api::NodeApi; use oracle_config::ORACLE_CONFIG; @@ -91,6 +93,7 @@ use std::thread; use std::time::Duration; use crate::actions::execute_action; +use crate::address_util::pks_to_network_addresses; use crate::api::start_rest_server; use crate::default_parameters::print_contract_hashes; use crate::migrate::check_migration_to_split_config; @@ -332,15 +335,26 @@ fn main() { if enable_rest_api { let op_clone = oracle_pool.clone(); tokio_runtime.spawn(async { - if let Err(e) = start_rest_server(repost_receiver, op_clone).await { + if let Err(e) = + start_rest_server(repost_receiver, op_clone, ORACLE_CONFIG.core_api_port) + .await + { error!("An error occurred while starting the REST server: {}", e); std::process::exit(exitcode::SOFTWARE); } }); } + if let Some(metrics_port) = ORACLE_CONFIG.metrics_port { + tokio_runtime.spawn(async move { + if let Err(e) = start_metrics_server(metrics_port).await { + error!("An error occurred while starting the metrics server: {}", e); + std::process::exit(exitcode::SOFTWARE); + } + }); + } loop { if let Err(e) = main_loop_iteration( - &oracle_pool, + oracle_pool.clone(), read_only, &datapoint_source, &node_api, @@ -474,7 +488,7 @@ fn handle_pool_command(command: Command, node_api: &NodeApi) { } fn main_loop_iteration( - oracle_pool: &OraclePool, + oracle_pool: Arc, read_only: bool, datapoint_source: &RuntimeDataPointSource, node_api: &NodeApi, @@ -506,7 +520,7 @@ fn main_loop_iteration( log::debug!("Height {height}. Building action for command: {:?}", cmd); let build_action_tuple_res = build_action( cmd, - oracle_pool, + &oracle_pool, node_api, height, network_change_address.address(), @@ -521,6 +535,7 @@ fn main_loop_iteration( } }; } + update_metrics(oracle_pool)?; Ok(()) } @@ -535,13 +550,12 @@ fn log_and_continue_if_non_fatal( found_public_keys, found_num, })) => { - let found_oracle_addresses: String = found_public_keys - .into_iter() - .map(|pk| { - NetworkAddress::new(network_prefix, &Address::P2Pk(pk.into())).to_base58() - }) - .collect::>() - .join(", "); + let found_oracle_addresses: String = + pks_to_network_addresses(found_public_keys, network_prefix) + .into_iter() + .map(|net_addr| net_addr.to_base58()) + .collect::>() + .join(", "); log::error!("Refresh failed, not enough datapoints. The minimum number of datapoints within the deviation range: required minumum {expected}, found {found_num} from addresses {found_oracle_addresses},"); Ok(None) } diff --git a/core/src/metrics.rs b/core/src/metrics.rs new file mode 100644 index 00000000..37c77a58 --- /dev/null +++ b/core/src/metrics.rs @@ -0,0 +1,332 @@ +use std::convert::From; +use std::net::SocketAddr; +use std::sync::Arc; + +use axum::response::IntoResponse; +use axum::response::Response; +use axum::routing::get; +use axum::Router; +use ergo_node_interface::scanning::NodeError; +use once_cell::sync::Lazy; +use prometheus::Encoder; +use prometheus::IntGauge; +use prometheus::IntGaugeVec; +use prometheus::Opts; +use prometheus::TextEncoder; +use reqwest::StatusCode; +use tower_http::cors::CorsLayer; + +use crate::box_kind::PoolBox; +use crate::monitor::check_oracle_health; +use crate::monitor::check_pool_health; +use crate::monitor::OracleHealth; +use crate::monitor::PoolHealth; +use crate::node_interface::node_api::NodeApi; +use crate::oracle_config::ORACLE_CONFIG; +use crate::oracle_state::OraclePool; + +static POOL_BOX_HEIGHT: Lazy = Lazy::new(|| { + let m = IntGauge::with_opts( + Opts::new("pool_box_height", "The height of the pool box") + .namespace("ergo") + .subsystem("oracle"), + ) + .unwrap(); + prometheus::register(Box::new(m.clone())).expect("Failed to register"); + m +}); + +static POOL_BOX_RATE: Lazy = Lazy::new(|| { + let m = IntGauge::with_opts( + Opts::new("pool_box_rate", "exchange rate from the pool box") + .namespace("ergo") + .subsystem("oracle"), + ) + .unwrap(); + prometheus::register(Box::new(m.clone())).expect("Failed to register"); + m +}); + +static POOL_BOX_REWARD_TOKEN_AMOUNT: Lazy = Lazy::new(|| { + let m = IntGauge::with_opts( + Opts::new( + "pool_box_reward_token_amount", + "The amount of reward token in the pool box", + ) + .namespace("ergo") + .subsystem("oracle"), + ) + .unwrap(); + prometheus::register(Box::new(m.clone())).expect("Failed to register"); + m +}); + +static CURRENT_HEIGHT: Lazy = Lazy::new(|| { + let m = IntGauge::with_opts( + Opts::new("current_height", "The current height") + .namespace("ergo") + .subsystem("oracle"), + ) + .unwrap(); + prometheus::register(Box::new(m.clone())).expect("Failed to register"); + m +}); + +static EPOCH_LENGTH: Lazy = Lazy::new(|| { + let m = IntGauge::with_opts( + Opts::new("epoch_length", "The epoch length") + .namespace("ergo") + .subsystem("oracle"), + ) + .unwrap(); + prometheus::register(Box::new(m.clone())).expect("Failed to register"); + m +}); + +static POOL_IS_HEALTHY: Lazy = Lazy::new(|| { + let m = IntGauge::with_opts( + Opts::new( + "pool_is_healthy", + "The health status of the pool, 1 for Ok and 0 for Down", + ) + .namespace("ergo") + .subsystem("oracle"), + ) + .unwrap(); + prometheus::register(Box::new(m.clone())).expect("Failed to register"); + m +}); + +static ORACLE_IS_HEALTHY: Lazy = Lazy::new(|| { + let m = IntGauge::with_opts( + Opts::new( + "oracle_is_healthy", + "The health status of the oracle, 1 for Ok and 0 for Down", + ) + .namespace("ergo") + .subsystem("oracle"), + ) + .unwrap(); + prometheus::register(Box::new(m.clone())).expect("Failed to register"); + m +}); + +static MY_ORACLE_BOX_HEIGHT: Lazy = Lazy::new(|| { + let m = IntGaugeVec::new( + Opts::new( + "oracle_box_height", + "The height of the posted/collected oracle box for this oracle", + ) + .namespace("ergo") + .subsystem("oracle"), + &["box_type"], + ) + .unwrap(); + prometheus::register(Box::new(m.clone())).expect("Failed to register"); + m +}); + +static ALL_ORACLE_BOX_HEIGHT: Lazy = Lazy::new(|| { + let m = IntGaugeVec::new( + Opts::new( + "all_oracle_box_height", + "The height of the posted/collected oracle box for all oracles", + ) + .namespace("ergo") + .subsystem("oracle"), + &["box_type", "oracle_address"], + ) + .unwrap(); + prometheus::register(Box::new(m.clone())).expect("Failed to register"); + m +}); + +static ACTIVE_ORACLE_BOX_HEIGHT: Lazy = Lazy::new(|| { + let m = IntGaugeVec::new( + Opts::new( + "active_oracle_box_height", + "The height of the posted/collected oracle boxes of active oracles", + ) + .namespace("ergo") + .subsystem("oracle"), + &["box_type", "oracle_address"], + ) + .unwrap(); + prometheus::register(Box::new(m.clone())).expect("Failed to register"); + m +}); + +static ACTIVE_ORACLE_COUNT: Lazy = Lazy::new(|| { + let m = IntGauge::with_opts( + Opts::new("active_oracle_count", "The number of active oracles") + .namespace("ergo") + .subsystem("oracle"), + ) + .unwrap(); + prometheus::register(Box::new(m.clone())).expect("Failed to register"); + m +}); + +static REQUIRED_ORACLE_COUNT: Lazy = Lazy::new(|| { + let m = IntGauge::with_opts( + Opts::new( + "required_oracle_count", + "The minimum number of active oracles", + ) + .namespace("ergo") + .subsystem("oracle"), + ) + .unwrap(); + prometheus::register(Box::new(m.clone())).expect("Failed to register"); + m +}); + +static ORACLE_NODE_WALLET_BALANCE: Lazy = Lazy::new(|| { + let m = IntGauge::with_opts( + Opts::new( + "oracle_node_wallet_nano_erg", + "Coins in the oracle's node wallet", + ) + .namespace("ergo") + .subsystem("oracle"), + ) + .unwrap(); + prometheus::register(Box::new(m.clone())).expect("Failed to register"); + m +}); + +static REWARD_TOKENS_IN_BUYBACK_BOX: Lazy = Lazy::new(|| { + let m = IntGauge::with_opts( + Opts::new( + "reward_tokens_in_buyback_box", + "The amount of reward tokens in the buyback box", + ) + .namespace("ergo") + .subsystem("oracle"), + ) + .unwrap(); + prometheus::register(Box::new(m.clone())).expect("Failed to register"); + m +}); + +fn update_pool_health(pool_health: &PoolHealth) { + POOL_BOX_HEIGHT.set(pool_health.details.pool_box_height.into()); + CURRENT_HEIGHT.set(pool_health.details.current_height.into()); + EPOCH_LENGTH.set(pool_health.details.epoch_length.into()); + POOL_IS_HEALTHY.set(pool_health.status as i64); + for oracle in &pool_health.details.all_oracles { + let box_type = oracle.box_height.label_name(); + let box_height = oracle.box_height.oracle_box_height().into(); + ALL_ORACLE_BOX_HEIGHT + .with_label_values(&[box_type, &oracle.address.to_base58()]) + .set(box_height); + } + for oracle in &pool_health.details.active_oracles { + let box_type = oracle.box_height.label_name(); + let box_height = oracle.box_height.oracle_box_height().into(); + ACTIVE_ORACLE_BOX_HEIGHT + .with_label_values(&[box_type, &oracle.address.to_base58()]) + .set(box_height); + } + ACTIVE_ORACLE_COUNT.set(pool_health.details.active_oracles.len() as i64); + REQUIRED_ORACLE_COUNT.set(pool_health.details.min_data_points.into()); +} + +fn update_oracle_health(oracle_health: &OracleHealth) { + let box_type = oracle_health.details.box_details.label_name(); + MY_ORACLE_BOX_HEIGHT + .with_label_values(&[box_type]) + .set(oracle_health.details.box_details.oracle_box_height().into()); + ORACLE_IS_HEALTHY.set(oracle_health.status as i64); +} + +fn update_reward_tokens_in_buyback_box(oracle_pool: Arc) { + if let Some(buyback_box) = oracle_pool + .get_buyback_box_source() + .map(|s| s.get_buyback_box()) + .transpose() + .ok() + .flatten() + .flatten() + { + let reward_token_amount: i64 = buyback_box + .reward_token() + .map(|t| t.amount.into()) + .unwrap_or(0); + REWARD_TOKENS_IN_BUYBACK_BOX.set(reward_token_amount); + } +} + +pub fn update_metrics(oracle_pool: Arc) -> Result<(), anyhow::Error> { + let node_api = NodeApi::new(ORACLE_CONFIG.node_api_key.clone(), &ORACLE_CONFIG.node_url); + let current_height = (node_api.node.current_block_height()? as u32).into(); + let network_prefix = node_api.get_change_address()?.network(); + let pool_box = &oracle_pool.get_pool_box_source().get_pool_box()?; + { + let rate = pool_box.rate(); + POOL_BOX_RATE.set(rate.into()); + }; + let pool_box_height = pool_box.get_box().creation_height.into(); + let pool_health = check_pool_health( + current_height, + pool_box_height, + oracle_pool.clone(), + network_prefix, + )?; + update_pool_health(&pool_health); + let oracle_health = check_oracle_health(oracle_pool.clone(), pool_box_height)?; + update_oracle_health(&oracle_health); + let wallet_balance: i64 = node_api.node.wallet_nano_ergs_balance()? as i64; + ORACLE_NODE_WALLET_BALANCE.set(wallet_balance); + POOL_BOX_REWARD_TOKEN_AMOUNT.set(pool_box.reward_token().amount.into()); + update_reward_tokens_in_buyback_box(oracle_pool); + Ok(()) +} + +async fn serve_metrics() -> impl IntoResponse { + let registry = prometheus::default_registry(); + let metric_families = registry.gather(); + let mut buffer = vec![]; + let encoder = TextEncoder::new(); + encoder.encode(&metric_families, &mut buffer).unwrap(); + + let metrics = String::from_utf8(buffer).unwrap(); + axum::response::Response::builder() + .header("Content-Type", encoder.format_type()) + .body(metrics) + .unwrap() +} + +pub async fn start_metrics_server(port_num: u16) -> Result<(), anyhow::Error> { + let app = Router::new().route("/metrics", get(serve_metrics)).layer( + CorsLayer::new() + .allow_origin(tower_http::cors::Any) + .allow_methods([axum::http::Method::GET]), + ); + let addr = SocketAddr::from(([0, 0, 0, 0], port_num)); + log::info!("Starting metrics server on {}", addr); + axum::Server::try_bind(&addr)? + .serve(app.into_make_service()) + .await?; + Ok(()) +} + +struct MetricsError(String); + +impl From for MetricsError { + fn from(err: NodeError) -> Self { + MetricsError(format!("NodeError: {}", err)) + } +} + +impl IntoResponse for MetricsError { + fn into_response(self) -> Response { + (StatusCode::INTERNAL_SERVER_ERROR, self.0).into_response() + } +} + +impl From for MetricsError { + fn from(err: anyhow::Error) -> Self { + MetricsError(format!("Error: {:?}", err)) + } +} diff --git a/core/src/monitor.rs b/core/src/monitor.rs new file mode 100644 index 00000000..58954fca --- /dev/null +++ b/core/src/monitor.rs @@ -0,0 +1,226 @@ +use std::sync::Arc; + +use ergo_lib::ergotree_ir::chain::address::Address; +use ergo_lib::ergotree_ir::chain::address::NetworkAddress; +use ergo_lib::ergotree_ir::chain::address::NetworkPrefix; + +use crate::box_kind::CollectedOracleBox; +use crate::box_kind::OracleBoxWrapper; +use crate::box_kind::PostedOracleBox; +use crate::oracle_state::DataSourceError; +use crate::oracle_state::OraclePool; +use crate::oracle_types::BlockHeight; +use crate::oracle_types::EpochLength; +use crate::oracle_types::MinDatapoints; +use crate::pool_config::POOL_CONFIG; + +#[derive(Debug, serde::Serialize, Copy, Clone)] +pub enum HealthStatus { + Ok = 1, + Down = 0, +} + +impl HealthStatus { + pub fn get_integer_value(&self) -> i32 { + *self as i32 + } +} + +#[derive(Debug, serde::Serialize)] +pub struct PoolHealthDetails { + pub pool_box_height: BlockHeight, + pub current_height: BlockHeight, + pub epoch_length: EpochLength, + pub all_oracles: Vec, + pub active_oracles: Vec, + pub min_data_points: MinDatapoints, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct OracleDetails { + pub address: NetworkAddress, + pub box_height: OracleBoxDetails, +} + +#[derive(Debug, serde::Serialize)] +pub struct PoolHealth { + pub status: HealthStatus, + pub details: PoolHealthDetails, +} + +pub fn check_pool_health( + current_height: BlockHeight, + pool_box_height: BlockHeight, + oracle_pool: Arc, + network_prefix: NetworkPrefix, +) -> Result { + let pool_conf = &POOL_CONFIG; + let epoch_length = pool_conf + .refresh_box_wrapper_inputs + .contract_inputs + .contract_parameters() + .epoch_length() + .0 + .into(); + let acceptable_pool_box_delay_blocks = 3; + let is_healthy = + pool_box_height >= current_height - epoch_length - acceptable_pool_box_delay_blocks; + let all_oracles = get_all_oracle_boxes(oracle_pool, network_prefix)?; + let active_oracles = get_active_oracle_boxes(&all_oracles, pool_box_height); + Ok(PoolHealth { + status: if is_healthy { + HealthStatus::Ok + } else { + HealthStatus::Down + }, + details: PoolHealthDetails { + pool_box_height, + current_height, + epoch_length, + all_oracles, + active_oracles, + min_data_points: pool_conf + .refresh_box_wrapper_inputs + .contract_inputs + .contract_parameters() + .min_data_points(), + }, + }) +} + +pub fn get_all_oracle_boxes( + oracle_pool: Arc, + network_prefix: NetworkPrefix, +) -> Result, DataSourceError> { + let mut oracle_details = vec![]; + let posted_boxes = oracle_pool + .get_posted_datapoint_boxes_source() + .get_posted_datapoint_boxes()?; + let collected_boxes = oracle_pool + .get_collected_datapoint_boxes_source() + .get_collected_datapoint_boxes()?; + for b in posted_boxes { + let detail = OracleDetails { + address: NetworkAddress::new(network_prefix, &Address::P2Pk(b.public_key().into())), + box_height: b.into(), + }; + oracle_details.push(detail); + } + for b in collected_boxes { + let detail = OracleDetails { + address: NetworkAddress::new(network_prefix, &Address::P2Pk(b.public_key().into())), + box_height: b.into(), + }; + oracle_details.push(detail); + } + Ok(oracle_details) +} + +pub fn get_active_oracle_boxes( + all_oracle_boxes: &Vec, + pool_box_height: BlockHeight, +) -> Vec { + let mut active_oracles: Vec = vec![]; + for oracle_box in all_oracle_boxes { + match oracle_box.box_height { + OracleBoxDetails::PostedBox(posted_box_height) => { + if posted_box_height >= pool_box_height { + active_oracles.push(oracle_box.clone()); + } + } + OracleBoxDetails::CollectedBox(collected_box_height) => { + if collected_box_height == pool_box_height { + active_oracles.push(oracle_box.clone()); + } + } + } + } + active_oracles +} + +#[derive(Debug, serde::Serialize)] +pub struct OracleHealth { + pub status: HealthStatus, + pub details: OracleHealthDetails, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub enum OracleBoxDetails { + PostedBox(BlockHeight), + CollectedBox(BlockHeight), +} + +impl OracleBoxDetails { + pub fn oracle_box_height(&self) -> BlockHeight { + match self { + OracleBoxDetails::PostedBox(height) => *height, + OracleBoxDetails::CollectedBox(height) => *height, + } + } + + pub fn label_name(&self) -> &'static str { + match self { + OracleBoxDetails::PostedBox(_) => "posted", + OracleBoxDetails::CollectedBox(_) => "collected", + } + } +} + +impl From for OracleBoxDetails { + fn from(box_wrapper: PostedOracleBox) -> Self { + OracleBoxDetails::PostedBox(box_wrapper.get_box().creation_height.into()) + } +} + +impl From for OracleBoxDetails { + fn from(box_wrapper: CollectedOracleBox) -> Self { + OracleBoxDetails::CollectedBox(box_wrapper.get_box().creation_height.into()) + } +} + +#[derive(Debug, serde::Serialize)] +pub struct OracleHealthDetails { + pub pool_box_height: BlockHeight, + pub box_details: OracleBoxDetails, +} + +pub fn check_oracle_health( + oracle_pool: Arc, + pool_box_height: BlockHeight, +) -> Result { + let health = match oracle_pool + .get_local_datapoint_box_source() + .get_local_oracle_datapoint_box()? + .ok_or_else(|| anyhow::anyhow!("Oracle box not found"))? + { + OracleBoxWrapper::Posted(posted_box) => { + let posted_box_height = posted_box.get_box().creation_height.into(); + OracleHealth { + status: if posted_box_height > pool_box_height { + HealthStatus::Ok + } else { + HealthStatus::Down + }, + details: OracleHealthDetails { + pool_box_height, + box_details: OracleBoxDetails::PostedBox(posted_box_height), + }, + } + } + OracleBoxWrapper::Collected(collected_box) => { + let creation_height = collected_box.get_box().creation_height.into(); + OracleHealth { + status: if creation_height == pool_box_height { + HealthStatus::Ok + } else { + HealthStatus::Down + }, + details: OracleHealthDetails { + pool_box_height, + box_details: OracleBoxDetails::CollectedBox(creation_height), + }, + } + } + }; + Ok(health) +} diff --git a/core/src/oracle_config.rs b/core/src/oracle_config.rs index 12200608..dcb1e5f1 100644 --- a/core/src/oracle_config.rs +++ b/core/src/oracle_config.rs @@ -35,6 +35,7 @@ pub struct OracleConfig { pub oracle_address: NetworkAddress, pub data_point_source_custom_script: Option, pub explorer_url: Option, + pub metrics_port: Option, } impl OracleConfig { @@ -102,6 +103,7 @@ impl Default for OracleConfig { log_level: LevelFilter::Info.into(), node_url: Url::parse("http://127.0.0.1:9053").unwrap(), explorer_url: Some(default_explorer_api_url(address.network())), + metrics_port: None, } } } @@ -116,8 +118,3 @@ lazy_static! { .map(|c| BoxValue::try_from(c.base_fee).unwrap()) .unwrap_or_else(|_| SUGGESTED_TX_FEE()); } - -/// Returns "core_api_port" from the config file -pub fn get_core_api_port() -> String { - ORACLE_CONFIG.core_api_port.to_string() -} diff --git a/core/src/oracle_state.rs b/core/src/oracle_state.rs index 1f35021e..38a37ded 100644 --- a/core/src/oracle_state.rs +++ b/core/src/oracle_state.rs @@ -7,7 +7,7 @@ use crate::box_kind::{ }; use crate::datapoint_source::DataPointSourceError; use crate::oracle_config::ORACLE_CONFIG; -use crate::oracle_types::{BlockHeight, EpochCounter}; +use crate::oracle_types::{BlockHeight, EpochCounter, Rate}; use crate::pool_config::POOL_CONFIG; use crate::scans::{GenericTokenScan, NodeScanRegistry, ScanError, ScanGetBoxes}; use crate::spec_token::{ @@ -154,7 +154,7 @@ pub struct BuybackBoxScan { pub struct LiveEpochState { pub pool_box_epoch_id: EpochCounter, pub local_datapoint_box_state: Option, - pub latest_pool_datapoint: u64, + pub latest_pool_datapoint: Rate, pub latest_pool_box_height: BlockHeight, } @@ -261,7 +261,7 @@ impl OraclePool { }, }); - let latest_pool_datapoint = pool_box.rate() as u64; + let latest_pool_datapoint = pool_box.rate(); let epoch_state = LiveEpochState { pool_box_epoch_id: epoch_id, diff --git a/core/src/oracle_types.rs b/core/src/oracle_types.rs index a1e2847a..a1a43026 100644 --- a/core/src/oracle_types.rs +++ b/core/src/oracle_types.rs @@ -10,7 +10,7 @@ use derive_more::Sub; use serde::Deserialize; use serde::Serialize; -#[derive(PartialEq, PartialOrd, Eq, Ord, Debug, Serialize, Deserialize, Copy, Clone)] +#[derive(PartialEq, PartialOrd, Eq, Ord, Debug, Serialize, Deserialize, Copy, Clone, From)] #[serde(transparent)] pub struct BlockHeight(pub u32); @@ -21,6 +21,21 @@ impl std::ops::Sub for BlockHeight { } } +impl std::ops::Add for BlockHeight { + type Output = BlockHeight; + fn add(self, other: EpochLength) -> BlockHeight { + BlockHeight(self.0 + other.0 as u32) + } +} + +impl std::ops::Add for BlockHeight { + type Output = BlockHeight; + fn add(self, other: u32) -> BlockHeight { + // Unwrap here to panic on overflow instead of wrapping around + BlockHeight(self.0.checked_add(other).unwrap()) + } +} + impl std::ops::Sub for BlockHeight { type Output = BlockHeight; fn sub(self, other: u32) -> BlockHeight { @@ -29,24 +44,42 @@ impl std::ops::Sub for BlockHeight { } } +impl From for i64 { + fn from(block_height: BlockHeight) -> Self { + block_height.0 as i64 + } +} + impl std::fmt::Display for BlockHeight { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } -#[derive(PartialEq, PartialOrd, Eq, Ord, Debug, Serialize, Deserialize, Copy, Clone)] +#[derive(PartialEq, PartialOrd, Eq, Ord, Debug, Serialize, Deserialize, Copy, Clone, From)] #[serde(transparent)] pub struct EpochLength(pub i32); -#[derive(PartialEq, PartialOrd, Eq, Ord, Debug, Serialize, Deserialize, Copy, Clone)] +impl From for i64 { + fn from(epoch_length: EpochLength) -> Self { + epoch_length.0 as i64 + } +} + +#[derive(PartialEq, PartialOrd, Eq, Ord, Debug, Serialize, Deserialize, Copy, Clone, From)] #[serde(transparent)] pub struct EpochCounter(pub u32); -#[derive(PartialEq, PartialOrd, Eq, Ord, Debug, Serialize, Deserialize, Copy, Clone)] +#[derive(PartialEq, PartialOrd, Eq, Ord, Debug, Serialize, Deserialize, Copy, Clone, From)] #[serde(transparent)] pub struct MinDatapoints(pub i32); +impl From for i64 { + fn from(min_datapoints: MinDatapoints) -> Self { + min_datapoints.0 as i64 + } +} + #[derive( PartialEq, PartialOrd, diff --git a/core/src/scans.rs b/core/src/scans.rs index b438f3f1..94e30ba9 100644 --- a/core/src/scans.rs +++ b/core/src/scans.rs @@ -1,4 +1,3 @@ -use crate::address_util::AddressUtilError; use crate::contracts::pool::PoolContractError; use crate::contracts::refresh::RefreshContractError; use crate::node_interface::node_api::{NodeApi, NodeApiError}; @@ -34,8 +33,6 @@ pub enum ScanError { RefreshContract(#[from] RefreshContractError), #[error("pool contract error: {0}")] PoolContract(#[from] PoolContractError), - #[error("address util error: {0}")] - AddressUtilError(#[from] AddressUtilError), } pub trait NodeScanId { diff --git a/scripts/grafana_dashboard.json b/scripts/grafana_dashboard.json new file mode 100644 index 00000000..be01558a --- /dev/null +++ b/scripts/grafana_dashboard.json @@ -0,0 +1,900 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 6, + "links": [], + "panels": [ + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 24, + "panels": [], + "title": "Pool", + "type": "row" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "from": "", + "id": 1, + "text": "OK", + "to": "", + "type": 1, + "value": "1" + }, + { + "from": "", + "id": 2, + "text": "DOWN", + "to": "", + "type": 1, + "value": "0" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 2, + "x": 0, + "y": 1 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "7.5.11", + "targets": [ + { + "exemplar": true, + "expr": "ergo_oracle_pool_is_healthy", + "format": "time_series", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Pool health", + "transformations": [], + "type": "stat" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 3, + "x": 2, + "y": 1 + }, + "id": 32, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "7.5.11", + "targets": [ + { + "exemplar": true, + "expr": "ergo_oracle_active_oracle_count", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Active oracle count", + "type": "stat" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 3, + "x": 5, + "y": 1 + }, + "id": 34, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "7.5.11", + "targets": [ + { + "exemplar": true, + "expr": "ergo_oracle_required_oracle_count", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Min required oracle count", + "type": "stat" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 3, + "x": 8, + "y": 1 + }, + "id": 18, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "7.5.11", + "targets": [ + { + "exemplar": true, + "expr": "ergo_oracle_pool_box_height", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Pool box height", + "type": "stat" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 4, + "x": 11, + "y": 1 + }, + "id": 16, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "7.5.11", + "targets": [ + { + "exemplar": true, + "expr": "ergo_oracle_pool_box_reward_token_amount", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Reward token amount in the pool box", + "type": "stat" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 4, + "x": 15, + "y": 1 + }, + "id": 12, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "7.5.11", + "targets": [ + { + "exemplar": true, + "expr": "ergo_oracle_pool_box_rate", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Rate in pool box", + "type": "stat" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 4, + "x": 19, + "y": 1 + }, + "id": 26, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "7.5.11", + "targets": [ + { + "exemplar": true, + "expr": "ergo_oracle_reward_tokens_in_buyback_box", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Reward tokens in buyback box", + "type": "stat" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 17, + "w": 23, + "x": 0, + "y": 6 + }, + "hiddenSeries": false, + "id": 30, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.11", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "ergo_oracle_active_oracle_box_height", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Active oracle boxes heights", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:1025", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:1026", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "id": 22, + "panels": [], + "title": "Oracle", + "type": "row" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "from": "", + "id": 1, + "text": "OK", + "to": "", + "type": 1, + "value": "1" + }, + { + "from": "", + "id": 2, + "text": "Down", + "to": "", + "type": 1, + "value": "0" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 5, + "x": 0, + "y": 24 + }, + "id": 10, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "7.5.11", + "targets": [ + { + "exemplar": true, + "expr": "ergo_oracle_oracle_is_healthy", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Oracle health", + "type": "stat" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 5, + "y": 24 + }, + "id": 20, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "7.5.11", + "targets": [ + { + "exemplar": true, + "expr": "ergo_oracle_current_height", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Node height", + "type": "stat" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fieldConfig": { + "defaults": { + "unit": "short" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 15, + "w": 11, + "x": 11, + "y": 24 + }, + "hiddenSeries": false, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.11", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "ergo_oracle_pool_box_height", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "", + "refId": "A" + }, + { + "exemplar": true, + "expr": "ergo_oracle_current_height", + "hide": false, + "interval": "", + "legendFormat": "", + "refId": "B" + }, + { + "exemplar": true, + "expr": "ergo_oracle_oracle_box_height", + "hide": false, + "interval": "", + "legendFormat": "", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Pool box, oracle box, node heights", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:221", + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:222", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 11, + "x": 0, + "y": 31 + }, + "id": 14, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "7.5.11", + "targets": [ + { + "exemplar": true, + "expr": "ergo_oracle_oracle_node_wallet_nano_erg", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Nano ERGs in node's wallet", + "type": "stat" + } + ], + "refresh": "30s", + "schemaVersion": 27, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Ergo oracle", + "uid": "ORWMfHl4k", + "version": 33 +} \ No newline at end of file