diff --git a/rust/Dockerfile b/rust/Dockerfile index 81186c125f..a32c45c737 100644 --- a/rust/Dockerfile +++ b/rust/Dockerfile @@ -18,6 +18,7 @@ COPY rust/main/applications rust/main/applications COPY rust/main/chains rust/main/chains COPY rust/main/hyperlane-base rust/main/hyperlane-base COPY rust/main/hyperlane-core rust/main/hyperlane-core +COPY rust/main/hyperlane-metric rust/main/hyperlane-metric COPY rust/main/hyperlane-test rust/main/hyperlane-test COPY rust/main/ethers-prometheus rust/main/ethers-prometheus COPY rust/main/utils rust/main/utils diff --git a/rust/main/Cargo.lock b/rust/main/Cargo.lock index 72f0271959..2433ccfc39 100644 --- a/rust/main/Cargo.lock +++ b/rust/main/Cargo.lock @@ -3153,6 +3153,7 @@ dependencies = [ "ethers-core", "futures", "hyperlane-core", + "hyperlane-metric", "log", "maplit", "parking_lot 0.12.3", @@ -4587,6 +4588,7 @@ dependencies = [ "hyperlane-cosmos", "hyperlane-ethereum", "hyperlane-fuel", + "hyperlane-metric", "hyperlane-operation-verifier", "hyperlane-sealevel", "hyperlane-test", @@ -4744,6 +4746,7 @@ dependencies = [ "futures-util", "hex 0.4.3", "hyperlane-core", + "hyperlane-metric", "hyperlane-operation-verifier", "hyperlane-warp-route", "itertools 0.12.1", @@ -4776,6 +4779,20 @@ dependencies = [ "url", ] +[[package]] +name = "hyperlane-metric" +version = "0.1.0" +dependencies = [ + "async-trait", + "derive-new", + "derive_builder", + "maplit", + "prometheus", + "serde", + "serde_json", + "url", +] + [[package]] name = "hyperlane-operation-verifier" version = "0.1.0" @@ -4798,6 +4815,7 @@ dependencies = [ "borsh 0.9.3", "derive-new", "hyperlane-core", + "hyperlane-metric", "hyperlane-operation-verifier", "hyperlane-sealevel-igp", "hyperlane-sealevel-interchain-security-module-interface", @@ -4808,6 +4826,7 @@ dependencies = [ "hyperlane-warp-route", "jsonrpc-core", "lazy_static", + "maplit", "multisig-ism", "num-traits", "reqwest", @@ -8633,7 +8652,7 @@ dependencies = [ [[package]] name = "solana-account-decoder" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "Inflector", "base64 0.13.1", @@ -8657,7 +8676,7 @@ dependencies = [ [[package]] name = "solana-address-lookup-table-program" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "bincode", "bytemuck", @@ -8677,7 +8696,7 @@ dependencies = [ [[package]] name = "solana-clap-utils" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "chrono", "clap 2.34.0", @@ -8694,7 +8713,7 @@ dependencies = [ [[package]] name = "solana-cli-config" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "dirs-next", "lazy_static", @@ -8709,7 +8728,7 @@ dependencies = [ [[package]] name = "solana-client" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "async-mutex", "async-trait", @@ -8762,7 +8781,7 @@ dependencies = [ [[package]] name = "solana-config-program" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "bincode", "chrono", @@ -8775,7 +8794,7 @@ dependencies = [ [[package]] name = "solana-faucet" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "bincode", "byteorder", @@ -8798,7 +8817,7 @@ dependencies = [ [[package]] name = "solana-frozen-abi" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "ahash 0.7.8", "blake3", @@ -8831,7 +8850,7 @@ dependencies = [ [[package]] name = "solana-frozen-abi-macro" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "proc-macro2 1.0.93", "quote 1.0.37", @@ -8842,7 +8861,7 @@ dependencies = [ [[package]] name = "solana-logger" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "env_logger", "lazy_static", @@ -8852,7 +8871,7 @@ dependencies = [ [[package]] name = "solana-measure" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "log", "solana-sdk", @@ -8861,7 +8880,7 @@ dependencies = [ [[package]] name = "solana-metrics" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "crossbeam-channel", "gethostname", @@ -8874,7 +8893,7 @@ dependencies = [ [[package]] name = "solana-net-utils" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "bincode", "clap 3.2.25", @@ -8895,7 +8914,7 @@ dependencies = [ [[package]] name = "solana-perf" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "ahash 0.7.8", "bincode", @@ -8921,7 +8940,7 @@ dependencies = [ [[package]] name = "solana-program" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "base64 0.13.1", "bincode", @@ -8969,7 +8988,7 @@ dependencies = [ [[package]] name = "solana-program-runtime" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "base64 0.13.1", "bincode", @@ -8995,7 +9014,7 @@ dependencies = [ [[package]] name = "solana-rayon-threadlimit" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "lazy_static", "num_cpus", @@ -9004,7 +9023,7 @@ dependencies = [ [[package]] name = "solana-remote-wallet" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "console", "dialoguer", @@ -9022,7 +9041,7 @@ dependencies = [ [[package]] name = "solana-sdk" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "assert_matches", "base64 0.13.1", @@ -9072,7 +9091,7 @@ dependencies = [ [[package]] name = "solana-sdk-macro" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "bs58 0.4.0", "proc-macro2 1.0.93", @@ -9084,7 +9103,7 @@ dependencies = [ [[package]] name = "solana-streamer" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "crossbeam-channel", "futures-util", @@ -9112,7 +9131,7 @@ dependencies = [ [[package]] name = "solana-transaction-status" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "Inflector", "base64 0.13.1", @@ -9140,7 +9159,7 @@ dependencies = [ [[package]] name = "solana-version" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "log", "rustc_version", @@ -9155,7 +9174,7 @@ dependencies = [ [[package]] name = "solana-vote-program" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "bincode", "log", @@ -9175,7 +9194,7 @@ dependencies = [ [[package]] name = "solana-zk-token-sdk" version = "1.14.13" -source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2024-11-20#913db71a07f967f4c5c7e7f747cb48517cdbf09e" +source = "git+https://github.com/hyperlane-xyz/solana.git?tag=hyperlane-1.14.13-2025-02-14#b94add305fc3232797f518ec7d9919c8528b381c" dependencies = [ "aes-gcm-siv", "arrayref", diff --git a/rust/main/Cargo.toml b/rust/main/Cargo.toml index 5881b2d957..33788b60eb 100644 --- a/rust/main/Cargo.toml +++ b/rust/main/Cargo.toml @@ -242,42 +242,42 @@ version = "=0.5.2" [patch.crates-io.solana-account-decoder] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2024-11-20" +tag = "hyperlane-1.14.13-2025-02-14" version = "=1.14.13" [patch.crates-io.solana-clap-utils] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2024-11-20" +tag = "hyperlane-1.14.13-2025-02-14" version = "=1.14.13" [patch.crates-io.solana-cli-config] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2024-11-20" +tag = "hyperlane-1.14.13-2025-02-14" version = "=1.14.13" [patch.crates-io.solana-client] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2024-11-20" +tag = "hyperlane-1.14.13-2025-02-14" version = "=1.14.13" [patch.crates-io.solana-program] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2024-11-20" +tag = "hyperlane-1.14.13-2025-02-14" version = "=1.14.13" [patch.crates-io.solana-sdk] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2024-11-20" +tag = "hyperlane-1.14.13-2025-02-14" version = "=1.14.13" [patch.crates-io.solana-transaction-status] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2024-11-20" +tag = "hyperlane-1.14.13-2025-02-14" version = "=1.14.13" [patch.crates-io.solana-zk-token-sdk] git = "https://github.com/hyperlane-xyz/solana.git" -tag = "hyperlane-1.14.13-2024-11-20" +tag = "hyperlane-1.14.13-2025-02-14" version = "=1.14.13" [patch.crates-io.spl-associated-token-account] diff --git a/rust/main/chains/hyperlane-ethereum/Cargo.toml b/rust/main/chains/hyperlane-ethereum/Cargo.toml index 194dea2fa6..a5dccaa965 100644 --- a/rust/main/chains/hyperlane-ethereum/Cargo.toml +++ b/rust/main/chains/hyperlane-ethereum/Cargo.toml @@ -32,6 +32,7 @@ url.workspace = true ethers-prometheus = { path = "../../ethers-prometheus", features = ["serde"] } hyperlane-core = { path = "../../hyperlane-core", features = ["async"] } +hyperlane-metric = { path = "../../hyperlane-metric" } hyperlane-operation-verifier = { path = "../../applications/hyperlane-operation-verifier" } hyperlane-warp-route = { path = "../../applications/hyperlane-warp-route" } diff --git a/rust/main/chains/hyperlane-ethereum/src/rpc_clients/fallback.rs b/rust/main/chains/hyperlane-ethereum/src/rpc_clients/fallback.rs index 9c52a6bb3b..e3a77bf932 100644 --- a/rust/main/chains/hyperlane-ethereum/src/rpc_clients/fallback.rs +++ b/rust/main/chains/hyperlane-ethereum/src/rpc_clients/fallback.rs @@ -12,7 +12,8 @@ use serde_json::Value; use tokio::time::sleep; use tracing::{instrument, warn_span}; -use ethers_prometheus::json_rpc_client::{JsonRpcBlockGetter, PrometheusJsonRpcClientConfigExt}; +use ethers_prometheus::json_rpc_client::JsonRpcBlockGetter; +use hyperlane_metric::prometheus_metric::PrometheusConfigExt; use crate::rpc_clients::{categorize_client_response, CategorizedResponse}; @@ -30,7 +31,7 @@ impl Deref for EthereumFallbackProvider { impl Debug for EthereumFallbackProvider where - C: JsonRpcClient + PrometheusJsonRpcClientConfigExt, + C: JsonRpcClient + PrometheusConfigExt, { #[allow(clippy::get_first)] // TODO: `rustc` 1.80.1 clippy issue fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { @@ -78,7 +79,7 @@ impl JsonRpcClient for EthereumFallbackProvider> where C: JsonRpcClient + Into> - + PrometheusJsonRpcClientConfigExt + + PrometheusConfigExt + Clone, JsonRpcBlockGetter: BlockNumberGetter, { @@ -181,7 +182,7 @@ mod tests { } } - impl PrometheusJsonRpcClientConfigExt for EthereumProviderMock { + impl PrometheusConfigExt for EthereumProviderMock { fn node_host(&self) -> &str { todo!() } @@ -194,7 +195,7 @@ mod tests { impl EthereumFallbackProvider> where C: JsonRpcClient - + PrometheusJsonRpcClientConfigExt + + PrometheusConfigExt + Into> + Clone, JsonRpcBlockGetter: BlockNumberGetter, diff --git a/rust/main/chains/hyperlane-ethereum/src/rpc_clients/retrying.rs b/rust/main/chains/hyperlane-ethereum/src/rpc_clients/retrying.rs index 59baeec762..cf8b59cba6 100644 --- a/rust/main/chains/hyperlane-ethereum/src/rpc_clients/retrying.rs +++ b/rust/main/chains/hyperlane-ethereum/src/rpc_clients/retrying.rs @@ -3,9 +3,8 @@ use std::{fmt::Debug, str::FromStr, time::Duration}; use crate::rpc_clients::{categorize_client_response, CategorizedResponse}; use async_trait::async_trait; use ethers::providers::{Http, JsonRpcClient, ProviderError}; -use ethers_prometheus::json_rpc_client::{ - PrometheusJsonRpcClient, PrometheusJsonRpcClientConfigExt, -}; +use ethers_prometheus::json_rpc_client::PrometheusJsonRpcClient; +use hyperlane_metric::prometheus_metric::PrometheusConfigExt; use serde::{de::DeserializeOwned, Serialize}; use serde_json::Value; use thiserror::Error; diff --git a/rust/main/chains/hyperlane-ethereum/src/rpc_clients/trait_builder.rs b/rust/main/chains/hyperlane-ethereum/src/rpc_clients/trait_builder.rs index 3bf234fe75..edc093017d 100644 --- a/rust/main/chains/hyperlane-ethereum/src/rpc_clients/trait_builder.rs +++ b/rust/main/chains/hyperlane-ethereum/src/rpc_clients/trait_builder.rs @@ -1,4 +1,4 @@ -use std::fmt::{Debug, Write}; +use std::fmt::Debug; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; @@ -15,18 +15,19 @@ use ethers::prelude::{ use ethers::types::Address; use ethers_signers::Signer; use hyperlane_core::rpc_clients::FallbackProvider; +use hyperlane_metric::utils::url_to_host_info; use reqwest::header::{HeaderName, HeaderValue}; use reqwest::{Client, Url}; use thiserror::Error; -use ethers_prometheus::json_rpc_client::{ - JsonRpcBlockGetter, JsonRpcClientMetrics, JsonRpcClientMetricsBuilder, NodeInfo, - PrometheusJsonRpcClient, PrometheusJsonRpcClientConfig, -}; +use ethers_prometheus::json_rpc_client::{JsonRpcBlockGetter, PrometheusJsonRpcClient}; use ethers_prometheus::middleware::{MiddlewareMetrics, PrometheusMiddlewareConf}; use hyperlane_core::{ ChainCommunicationError, ChainResult, ContractLocator, HyperlaneDomain, KnownHyperlaneDomain, }; +use hyperlane_metric::prometheus_metric::{ + NodeInfo, PrometheusClientMetrics, PrometheusClientMetricsBuilder, PrometheusConfig, +}; use tracing::instrument; use crate::signer::Signers; @@ -69,7 +70,7 @@ pub trait BuildableWithProvider { conn: &ConnectionConf, locator: &ContractLocator, signer: Option, - rpc_metrics: Option, + client_metrics: Option, middleware_metrics: Option<(MiddlewareMetrics, PrometheusMiddlewareConf)>, ) -> ChainResult { Ok(match &conn.rpc_connection { @@ -89,7 +90,7 @@ pub trait BuildableWithProvider { let metrics_provider = self.wrap_rpc_with_metrics( http_provider, url.clone(), - &rpc_metrics, + &client_metrics, &middleware_metrics, ); let retrying_provider = @@ -107,7 +108,7 @@ pub trait BuildableWithProvider { let metrics_provider = self.wrap_rpc_with_metrics( http_provider, url.clone(), - &rpc_metrics, + &client_metrics, &middleware_metrics, ); builder = builder.add_provider(metrics_provider); @@ -125,7 +126,7 @@ pub trait BuildableWithProvider { let metrics_provider = self.wrap_rpc_with_metrics( http_provider, url.clone(), - &rpc_metrics, + &client_metrics, &middleware_metrics, ); let retrying_http_provider = RetryingProvider::new(metrics_provider, None, None); @@ -146,28 +147,17 @@ pub trait BuildableWithProvider { &self, client: C, url: Url, - rpc_metrics: &Option, + client_metrics: &Option, middleware_metrics: &Option<(MiddlewareMetrics, PrometheusMiddlewareConf)>, ) -> PrometheusJsonRpcClient { PrometheusJsonRpcClient::new( client, - rpc_metrics + client_metrics .clone() - .unwrap_or_else(|| JsonRpcClientMetricsBuilder::default().build().unwrap()), - PrometheusJsonRpcClientConfig { + .unwrap_or_else(|| PrometheusClientMetricsBuilder::default().build().unwrap()), + PrometheusConfig { node: Some(NodeInfo { - host: { - let mut s = String::new(); - if let Some(host) = url.host_str() { - s.push_str(host); - if let Some(port) = url.port() { - write!(&mut s, ":{port}").unwrap(); - } - Some(s) - } else { - None - } - }, + host: url_to_host_info(&url), }), // steal the chain info from the middleware conf chain: middleware_metrics diff --git a/rust/main/chains/hyperlane-sealevel/Cargo.toml b/rust/main/chains/hyperlane-sealevel/Cargo.toml index 9b180d775f..392014a41c 100644 --- a/rust/main/chains/hyperlane-sealevel/Cargo.toml +++ b/rust/main/chains/hyperlane-sealevel/Cargo.toml @@ -12,6 +12,7 @@ borsh.workspace = true derive-new.workspace = true jsonrpc-core.workspace = true lazy_static.workspace = true +maplit.workspace = true num-traits.workspace = true reqwest.workspace = true serde.workspace = true @@ -32,6 +33,7 @@ hyperlane-core = { path = "../../hyperlane-core", features = [ "solana", "async", ] } +hyperlane-metric = { path = "../../hyperlane-metric" } hyperlane-operation-verifier = { path = "../../applications/hyperlane-operation-verifier" } hyperlane-sealevel-interchain-security-module-interface = { path = "../../../sealevel/libraries/interchain-security-module-interface" } hyperlane-sealevel-mailbox = { path = "../../../sealevel/programs/mailbox", features = [ diff --git a/rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs b/rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs index 95cd97eb35..55fbeaf892 100644 --- a/rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs +++ b/rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs @@ -1,4 +1,4 @@ -use std::ops::RangeInclusive; +use std::{ops::RangeInclusive, sync::Arc}; use async_trait::async_trait; use derive_new::new; @@ -37,10 +37,11 @@ pub struct SealevelInterchainGasPaymaster { impl SealevelInterchainGasPaymaster { /// Create a new Sealevel IGP. pub async fn new( + rpc_client: Arc, conf: &ConnectionConf, igp_account_locator: &ContractLocator<'_>, ) -> ChainResult { - let provider = SealevelProvider::new(igp_account_locator.domain.clone(), conf); + let provider = SealevelProvider::new(rpc_client, igp_account_locator.domain.clone(), conf); let program_id = Self::determine_igp_program_id(provider.rpc(), &igp_account_locator.address).await?; let (data_pda_pubkey, _) = @@ -89,7 +90,7 @@ impl InterchainGasPaymaster for SealevelInterchainGasPaymaster {} /// Struct that retrieves event data for a Sealevel IGP contract #[derive(Debug)] pub struct SealevelInterchainGasPaymasterIndexer { - rpc_client: SealevelRpcClient, + rpc_client: Arc, igp: SealevelInterchainGasPaymaster, log_meta_composer: LogMetaComposer, advanced_log_meta: bool, @@ -106,14 +107,14 @@ pub struct SealevelGasPayment { impl SealevelInterchainGasPaymasterIndexer { /// Create a new Sealevel IGP indexer. pub async fn new( + rpc_client: Arc, conf: &ConnectionConf, igp_account_locator: ContractLocator<'_>, advanced_log_meta: bool, ) -> ChainResult { - // Set the `processed` commitment at rpc level - let rpc_client = SealevelRpcClient::new(conf.url.to_string()); - - let igp = SealevelInterchainGasPaymaster::new(conf, &igp_account_locator).await?; + let igp = + SealevelInterchainGasPaymaster::new(rpc_client.clone(), conf, &igp_account_locator) + .await?; let log_meta_composer = LogMetaComposer::new( igp.program_id, diff --git a/rust/main/chains/hyperlane-sealevel/src/interchain_security_module.rs b/rust/main/chains/hyperlane-sealevel/src/interchain_security_module.rs index 89023032dd..9ac7c0c2ac 100644 --- a/rust/main/chains/hyperlane-sealevel/src/interchain_security_module.rs +++ b/rust/main/chains/hyperlane-sealevel/src/interchain_security_module.rs @@ -10,7 +10,7 @@ use hyperlane_core::{ use hyperlane_sealevel_interchain_security_module_interface::InterchainSecurityModuleInstruction; use serializable_account_meta::SimulationReturnData; -use crate::{ConnectionConf, SealevelKeypair, SealevelProvider, SealevelRpcClient}; +use crate::{SealevelKeypair, SealevelProvider, SealevelRpcClient}; /// A reference to an InterchainSecurityModule contract on some Sealevel chain #[derive(Debug)] @@ -23,11 +23,10 @@ pub struct SealevelInterchainSecurityModule { impl SealevelInterchainSecurityModule { /// Create a new sealevel InterchainSecurityModule pub fn new( - conf: &ConnectionConf, + provider: SealevelProvider, locator: ContractLocator, payer: Option, ) -> Self { - let provider = SealevelProvider::new(locator.domain.clone(), conf); let program_id = Pubkey::from(<[u8; 32]>::from(locator.address)); Self { payer, diff --git a/rust/main/chains/hyperlane-sealevel/src/lib.rs b/rust/main/chains/hyperlane-sealevel/src/lib.rs index 5a91ce0b0e..520088643f 100644 --- a/rust/main/chains/hyperlane-sealevel/src/lib.rs +++ b/rust/main/chains/hyperlane-sealevel/src/lib.rs @@ -11,9 +11,10 @@ pub use keypair::*; pub use mailbox::*; pub use merkle_tree_hook::*; pub use provider::*; -pub(crate) use rpc::SealevelRpcClient; +pub use rpc::*; pub use solana_sdk::signer::keypair::Keypair; pub use trait_builder::*; +pub use tx_submitter::TransactionSubmitter; pub use validator_announce::*; mod account; @@ -26,6 +27,7 @@ mod keypair; mod log_meta_composer; mod mailbox; mod merkle_tree_hook; +mod metric; mod multisig_ism; mod priority_fee; mod provider; diff --git a/rust/main/chains/hyperlane-sealevel/src/mailbox.rs b/rust/main/chains/hyperlane-sealevel/src/mailbox.rs index 673806008f..09e0ac66f6 100644 --- a/rust/main/chains/hyperlane-sealevel/src/mailbox.rs +++ b/rust/main/chains/hyperlane-sealevel/src/mailbox.rs @@ -90,11 +90,12 @@ pub struct SealevelMailbox { impl SealevelMailbox { /// Create a new sealevel mailbox pub fn new( + provider: SealevelProvider, + tx_submitter: Box, conf: &ConnectionConf, - locator: ContractLocator, + locator: &ContractLocator, payer: Option, ) -> ChainResult { - let provider = SealevelProvider::new(locator.domain.clone(), conf); let program_id = Pubkey::from(<[u8; 32]>::from(locator.address)); let domain = locator.domain.id(); let inbox = Pubkey::find_program_address(mailbox_inbox_pda_seeds!(), &program_id); @@ -111,9 +112,7 @@ impl SealevelMailbox { outbox, payer, priority_fee_oracle: conf.priority_fee_oracle.create_oracle(), - tx_submitter: conf - .transaction_submitter - .create_submitter(provider.rpc().url()), + tx_submitter, provider, }) } @@ -576,12 +575,15 @@ pub struct SealevelMailboxIndexer { impl SealevelMailboxIndexer { /// Create a new SealevelMailboxIndexer pub fn new( + provider: SealevelProvider, + tx_submitter: Box, + locator: &ContractLocator, conf: &ConnectionConf, - locator: ContractLocator, advanced_log_meta: bool, ) -> ChainResult { + let mailbox = SealevelMailbox::new(provider, tx_submitter, conf, locator, None)?; + let program_id = Pubkey::from(<[u8; 32]>::from(locator.address)); - let mailbox = SealevelMailbox::new(conf, locator, None)?; let dispatch_message_log_meta_composer = LogMetaComposer::new( mailbox.program_id, diff --git a/rust/main/chains/hyperlane-sealevel/src/metric/mod.rs b/rust/main/chains/hyperlane-sealevel/src/metric/mod.rs new file mode 100644 index 0000000000..80ec402b27 --- /dev/null +++ b/rust/main/chains/hyperlane-sealevel/src/metric/mod.rs @@ -0,0 +1 @@ +pub mod prometheus_sender; diff --git a/rust/main/chains/hyperlane-sealevel/src/metric/prometheus_sender.rs b/rust/main/chains/hyperlane-sealevel/src/metric/prometheus_sender.rs new file mode 100644 index 0000000000..c66e333e8e --- /dev/null +++ b/rust/main/chains/hyperlane-sealevel/src/metric/prometheus_sender.rs @@ -0,0 +1,55 @@ +use std::time::Instant; + +use hyperlane_metric::prometheus_metric::{PrometheusClientMetrics, PrometheusConfig}; +use solana_client::{ + client_error::ClientError, + http_sender::HttpSender, + rpc_request::RpcRequest, + rpc_sender::{RpcSender, RpcTransportStats}, +}; +use url::Url; + +/// Sealevel RPC with prometheus metrics +/// Wraps around HttpSender +/// https://github.com/anza-xyz/agave/blob/master/rpc-client/src/http_sender.rs#L137 +pub struct PrometheusSealevelRpcSender { + pub inner: HttpSender, + pub metrics: PrometheusClientMetrics, + pub config: PrometheusConfig, +} + +impl PrometheusSealevelRpcSender { + pub fn new(url: Url, metrics: PrometheusClientMetrics, config: PrometheusConfig) -> Self { + Self { + inner: HttpSender::new(url), + metrics, + config, + } + } +} + +/// Implement this trait so it can be used with Solana RPC Client +#[async_trait::async_trait] +impl RpcSender for PrometheusSealevelRpcSender { + fn get_transport_stats(&self) -> RpcTransportStats { + self.inner.get_transport_stats() + } + + async fn send( + &self, + request: RpcRequest, + params: serde_json::Value, + ) -> Result { + let start = Instant::now(); + let method = format!("{}", request); + + let res = self.inner.send(request, params).await; + + self.metrics + .increment_metrics(&self.config, &method, start, res.is_ok()); + res + } + fn url(&self) -> String { + self.inner.url() + } +} diff --git a/rust/main/chains/hyperlane-sealevel/src/multisig_ism.rs b/rust/main/chains/hyperlane-sealevel/src/multisig_ism.rs index 1396248fc6..a714b5eaad 100644 --- a/rust/main/chains/hyperlane-sealevel/src/multisig_ism.rs +++ b/rust/main/chains/hyperlane-sealevel/src/multisig_ism.rs @@ -10,7 +10,7 @@ use solana_sdk::{ pubkey::Pubkey, }; -use crate::{ConnectionConf, SealevelKeypair, SealevelProvider, SealevelRpcClient}; +use crate::{SealevelKeypair, SealevelProvider, SealevelRpcClient}; use multisig_ism::interface::{ MultisigIsmInstruction, VALIDATORS_AND_THRESHOLD_ACCOUNT_METAS_PDA_SEEDS, @@ -28,11 +28,10 @@ pub struct SealevelMultisigIsm { impl SealevelMultisigIsm { /// Create a new Sealevel MultisigIsm. pub fn new( - conf: &ConnectionConf, + provider: SealevelProvider, locator: ContractLocator, payer: Option, ) -> Self { - let provider = SealevelProvider::new(locator.domain.clone(), conf); let program_id = Pubkey::from(<[u8; 32]>::from(locator.address)); Self { diff --git a/rust/main/chains/hyperlane-sealevel/src/provider.rs b/rust/main/chains/hyperlane-sealevel/src/provider.rs index ea970fef67..779ddb1b61 100644 --- a/rust/main/chains/hyperlane-sealevel/src/provider.rs +++ b/rust/main/chains/hyperlane-sealevel/src/provider.rs @@ -37,21 +37,22 @@ lazy_static! { /// A wrapper around a Sealevel provider to get generic blockchain information. #[derive(Debug)] pub struct SealevelProvider { - domain: HyperlaneDomain, rpc_client: Arc, + domain: HyperlaneDomain, native_token: NativeToken, } impl SealevelProvider { - /// Create a new Sealevel provider. - pub fn new(domain: HyperlaneDomain, conf: &ConnectionConf) -> Self { - // Set the `processed` commitment at rpc level - let rpc_client = Arc::new(SealevelRpcClient::new(conf.url.to_string())); + /// constructor + pub fn new( + rpc_client: Arc, + domain: HyperlaneDomain, + conf: &ConnectionConf, + ) -> Self { let native_token = conf.native_token.clone(); - Self { - domain, rpc_client, + domain, native_token, } } diff --git a/rust/main/chains/hyperlane-sealevel/src/rpc.rs b/rust/main/chains/hyperlane-sealevel/src/rpc.rs deleted file mode 100644 index 1c82b77c09..0000000000 --- a/rust/main/chains/hyperlane-sealevel/src/rpc.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub use client::SealevelRpcClient; - -mod client; diff --git a/rust/main/chains/hyperlane-sealevel/src/rpc/client.rs b/rust/main/chains/hyperlane-sealevel/src/rpc/client.rs index 4af5ee8f7d..60ec093e75 100644 --- a/rust/main/chains/hyperlane-sealevel/src/rpc/client.rs +++ b/rust/main/chains/hyperlane-sealevel/src/rpc/client.rs @@ -45,12 +45,14 @@ pub struct SealevelTxCostEstimate { compute_unit_price_micro_lamports: u64, } +/// Wrapper struct around Solana's RpcClient pub struct SealevelRpcClient(RpcClient); impl SealevelRpcClient { /// The max amount of compute units for a transaction. const MAX_COMPUTE_UNITS: u32 = 1_400_000; + /// constructor pub fn new(rpc_endpoint: String) -> Self { Self(RpcClient::new_with_commitment( rpc_endpoint, @@ -58,6 +60,12 @@ impl SealevelRpcClient { )) } + /// constructor with an rpc client + pub fn from_rpc_client(rpc_client: RpcClient) -> Self { + Self(rpc_client) + } + + /// confirm transaction with given commitment pub async fn confirm_transaction_with_commitment( &self, signature: &Signature, @@ -96,6 +104,7 @@ impl SealevelRpcClient { Ok(account_metas) } + /// get account with finalized commitment pub async fn get_account_with_finalized_commitment( &self, pubkey: &Pubkey, @@ -105,6 +114,7 @@ impl SealevelRpcClient { .ok_or_else(|| ChainCommunicationError::from_other_str("Could not find account data")) } + /// get account option with finalized commitment pub async fn get_account_option_with_finalized_commitment( &self, pubkey: &Pubkey, @@ -118,6 +128,7 @@ impl SealevelRpcClient { Ok(account) } + /// get balance pub async fn get_balance(&self, pubkey: &Pubkey) -> ChainResult { let balance = self .0 @@ -129,6 +140,7 @@ impl SealevelRpcClient { Ok(balance.into()) } + /// get block pub async fn get_block(&self, slot: u64) -> ChainResult { let config = RpcBlockConfig { commitment: Some(CommitmentConfig::finalized()), @@ -142,6 +154,7 @@ impl SealevelRpcClient { .map_err(Into::into) } + /// get minimum balance for rent exemption pub async fn get_minimum_balance_for_rent_exemption(&self, len: usize) -> ChainResult { self.0 .get_minimum_balance_for_rent_exemption(len) @@ -149,6 +162,7 @@ impl SealevelRpcClient { .map_err(ChainCommunicationError::from_other) } + /// get multiple accounts with finalized commitment pub async fn get_multiple_accounts_with_finalized_commitment( &self, pubkeys: &[Pubkey], @@ -163,6 +177,7 @@ impl SealevelRpcClient { Ok(accounts) } + /// get latest block hash with commitment pub async fn get_latest_blockhash_with_commitment( &self, commitment: CommitmentConfig, @@ -174,6 +189,7 @@ impl SealevelRpcClient { .map(|(blockhash, _)| blockhash) } + /// get program accounts with config pub async fn get_program_accounts_with_config( &self, pubkey: &Pubkey, @@ -185,6 +201,7 @@ impl SealevelRpcClient { .map_err(ChainCommunicationError::from_other) } + /// get statuses based on signatures pub async fn get_signature_statuses( &self, signatures: &[Signature], @@ -195,6 +212,7 @@ impl SealevelRpcClient { .map_err(ChainCommunicationError::from_other) } + /// get slot pub async fn get_slot(&self) -> ChainResult { let slot = self .get_slot_raw() @@ -205,6 +223,7 @@ impl SealevelRpcClient { Ok(slot) } + /// get slot pub async fn get_slot_raw(&self) -> ChainResult { self.0 .get_slot_with_commitment(CommitmentConfig::finalized()) @@ -212,6 +231,7 @@ impl SealevelRpcClient { .map_err(ChainCommunicationError::from_other) } + /// get transaction pub async fn get_transaction( &self, signature: &Signature, @@ -228,6 +248,7 @@ impl SealevelRpcClient { .map_err(Into::into) } + /// check if block hash is valid pub async fn is_blockhash_valid(&self, hash: &Hash) -> ChainResult { self.0 .is_blockhash_valid(hash, CommitmentConfig::processed()) @@ -235,6 +256,7 @@ impl SealevelRpcClient { .map_err(ChainCommunicationError::from_other) } + /// send transaction pub async fn send_transaction( &self, transaction: &Transaction, @@ -339,6 +361,7 @@ impl SealevelRpcClient { Ok(None) } + /// simulate a transaction pub async fn simulate_transaction( &self, transaction: &Transaction, @@ -530,6 +553,7 @@ impl SealevelRpcClient { Ok(tx) } + /// Get Url pub fn url(&self) -> String { self.0.url() } diff --git a/rust/main/chains/hyperlane-sealevel/src/rpc/client/tests.rs b/rust/main/chains/hyperlane-sealevel/src/rpc/client/tests.rs index 81859559c2..5cd2142ce2 100644 --- a/rust/main/chains/hyperlane-sealevel/src/rpc/client/tests.rs +++ b/rust/main/chains/hyperlane-sealevel/src/rpc/client/tests.rs @@ -1,9 +1,12 @@ +use solana_client::nonblocking::rpc_client::RpcClient; + use crate::SealevelRpcClient; //#[tokio::test] async fn _test_get_block() { + let rpc_client = RpcClient::new("".to_string()); // given - let client = SealevelRpcClient::new("".to_string()); + let client = SealevelRpcClient::from_rpc_client(rpc_client); // when let slot = 301337842; // block which requires latest version of solana-client diff --git a/rust/main/chains/hyperlane-sealevel/src/rpc/client_builder.rs b/rust/main/chains/hyperlane-sealevel/src/rpc/client_builder.rs new file mode 100644 index 0000000000..10afc21b24 --- /dev/null +++ b/rust/main/chains/hyperlane-sealevel/src/rpc/client_builder.rs @@ -0,0 +1,48 @@ +use hyperlane_metric::prometheus_metric::{ChainInfo, PrometheusClientMetrics, PrometheusConfig}; +use solana_client::{nonblocking::rpc_client::RpcClient, rpc_client::RpcClientConfig}; +use solana_sdk::commitment_config::CommitmentConfig; +use url::Url; + +use crate::metric::prometheus_sender::PrometheusSealevelRpcSender; + +use super::SealevelRpcClient; + +#[derive(Clone)] +/// SealevelRpcClient builder +pub struct SealevelRpcClientBuilder { + rpc_url: Url, + prometheus_config: Option<(PrometheusClientMetrics, PrometheusConfig)>, +} + +impl SealevelRpcClientBuilder { + /// Instantiate builder + pub fn new(rpc_url: Url) -> Self { + Self { + rpc_url, + prometheus_config: None, + } + } + + /// add prometheus metrics to builder + pub fn with_prometheus_metrics( + mut self, + metrics: PrometheusClientMetrics, + chain: Option, + ) -> Self { + let metrics_config = PrometheusConfig::from_url(&self.rpc_url, chain); + self.prometheus_config = Some((metrics, metrics_config)); + self + } + + /// build SealevelRpcClient + pub fn build(self) -> SealevelRpcClient { + let (metrics, metrics_config) = self.prometheus_config.unwrap_or_default(); + + let sender = PrometheusSealevelRpcSender::new(self.rpc_url, metrics, metrics_config); + let rpc_client = RpcClient::new_sender( + sender, + RpcClientConfig::with_commitment(CommitmentConfig::processed()), + ); + SealevelRpcClient::from_rpc_client(rpc_client) + } +} diff --git a/rust/main/chains/hyperlane-sealevel/src/rpc/mod.rs b/rust/main/chains/hyperlane-sealevel/src/rpc/mod.rs new file mode 100644 index 0000000000..d03a8c4cd9 --- /dev/null +++ b/rust/main/chains/hyperlane-sealevel/src/rpc/mod.rs @@ -0,0 +1,5 @@ +pub use client::SealevelRpcClient; + +mod client; +/// SealevelRpcClientBuilder +pub mod client_builder; diff --git a/rust/main/chains/hyperlane-sealevel/src/trait_builder.rs b/rust/main/chains/hyperlane-sealevel/src/trait_builder.rs index 13cc0b90a4..7320a721f7 100644 --- a/rust/main/chains/hyperlane-sealevel/src/trait_builder.rs +++ b/rust/main/chains/hyperlane-sealevel/src/trait_builder.rs @@ -1,8 +1,10 @@ use hyperlane_core::{config::OperationBatchConfig, ChainCommunicationError, NativeToken}; +use hyperlane_metric::prometheus_metric::{ChainInfo, PrometheusClientMetrics}; use serde::Serialize; use url::Url; use crate::{ + client_builder::SealevelRpcClientBuilder, priority_fee::{ConstantPriorityFeeOracle, HeliusPriorityFeeOracle, PriorityFeeOracle}, tx_submitter::{JitoTransactionSubmitter, RpcTransactionSubmitter, TransactionSubmitter}, }; @@ -115,19 +117,38 @@ impl Default for TransactionSubmitterConfig { impl TransactionSubmitterConfig { /// Create a new transaction submitter from the configuration - pub fn create_submitter(&self, default_rpc_url: String) -> Box { + pub fn create_submitter( + &self, + default_rpc_url: String, + metrics: PrometheusClientMetrics, + chain: Option, + ) -> Box { match self { - TransactionSubmitterConfig::Rpc { url } => Box::new(RpcTransactionSubmitter::new( - url.clone().unwrap_or(default_rpc_url), - )), + TransactionSubmitterConfig::Rpc { url } => { + let rpc_url = url.clone().unwrap_or(default_rpc_url); + let rpc_url = Url::parse(&rpc_url).unwrap(); + // now that we know what the RPC URL is, we + // can create a metrics config that has the correct + // node info + let rpc_client = SealevelRpcClientBuilder::new(rpc_url) + .with_prometheus_metrics(metrics, chain) + .build(); + Box::new(RpcTransactionSubmitter::new(rpc_client)) + } TransactionSubmitterConfig::Jito { url } => { // Default to a bundle-only URL (i.e. revert protected) - Box::new(JitoTransactionSubmitter::new(url.clone().unwrap_or_else( - || { - "https://mainnet.block-engine.jito.wtf/api/v1/transactions?bundleOnly=true" - .to_string() - }, - ))) + let rpc_url = url.clone().unwrap_or_else(|| { + "https://mainnet.block-engine.jito.wtf/api/v1/transactions?bundleOnly=true" + .to_string() + }); + let rpc_url = Url::parse(&rpc_url).unwrap(); + // now that we know what the RPC URL is, we + // can create a metrics config that has the correct + // node info + let rpc_client = SealevelRpcClientBuilder::new(rpc_url) + .with_prometheus_metrics(metrics, chain) + .build(); + Box::new(JitoTransactionSubmitter::new(rpc_client)) } } } diff --git a/rust/main/chains/hyperlane-sealevel/src/tx_submitter.rs b/rust/main/chains/hyperlane-sealevel/src/tx_submitter.rs index 8db9a502e7..6d40a73842 100644 --- a/rust/main/chains/hyperlane-sealevel/src/tx_submitter.rs +++ b/rust/main/chains/hyperlane-sealevel/src/tx_submitter.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use derive_new::new; use hyperlane_core::ChainResult; use solana_sdk::{ compute_budget::ComputeBudgetInstruction, instruction::Instruction, pubkey::Pubkey, @@ -25,25 +26,18 @@ pub trait TransactionSubmitter: Send + Sync { skip_preflight: bool, ) -> ChainResult; + /// Get the RPC client fn rpc_client(&self) -> Option<&SealevelRpcClient> { None } } /// A transaction submitter that uses the vanilla RPC to submit transactions. -#[derive(Debug)] +#[derive(Debug, new)] pub struct RpcTransactionSubmitter { rpc_client: SealevelRpcClient, } -impl RpcTransactionSubmitter { - pub fn new(url: String) -> Self { - Self { - rpc_client: SealevelRpcClient::new(url), - } - } -} - #[async_trait] impl TransactionSubmitter for RpcTransactionSubmitter { fn get_priority_fee_instruction( @@ -71,7 +65,7 @@ impl TransactionSubmitter for RpcTransactionSubmitter { } /// A transaction submitter that uses the Jito API to submit transactions. -#[derive(Debug)] +#[derive(Debug, new)] pub struct JitoTransactionSubmitter { rpc_client: SealevelRpcClient, } @@ -80,12 +74,6 @@ impl JitoTransactionSubmitter { /// The minimum tip to include in a transaction. /// From https://docs.jito.wtf/lowlatencytxnsend/#sendtransaction const MINIMUM_TIP_LAMPORTS: u64 = 1000; - - pub fn new(url: String) -> Self { - Self { - rpc_client: SealevelRpcClient::new(url), - } - } } #[async_trait] diff --git a/rust/main/chains/hyperlane-sealevel/src/validator_announce.rs b/rust/main/chains/hyperlane-sealevel/src/validator_announce.rs index 3edfa0d064..a7d4933c1d 100644 --- a/rust/main/chains/hyperlane-sealevel/src/validator_announce.rs +++ b/rust/main/chains/hyperlane-sealevel/src/validator_announce.rs @@ -9,7 +9,7 @@ use hyperlane_sealevel_validator_announce::{ use solana_sdk::pubkey::Pubkey; use tracing::{info, instrument, warn}; -use crate::{ConnectionConf, SealevelProvider, SealevelRpcClient}; +use crate::{SealevelProvider, SealevelRpcClient}; /// A reference to a ValidatorAnnounce contract on some Sealevel chain #[derive(Debug)] @@ -21,8 +21,7 @@ pub struct SealevelValidatorAnnounce { impl SealevelValidatorAnnounce { /// Create a new Sealevel ValidatorAnnounce - pub fn new(conf: &ConnectionConf, locator: ContractLocator) -> Self { - let provider = SealevelProvider::new(locator.domain.clone(), conf); + pub fn new(provider: SealevelProvider, locator: &ContractLocator) -> Self { let program_id = Pubkey::from(<[u8; 32]>::from(locator.address)); Self { program_id, diff --git a/rust/main/ethers-prometheus/Cargo.toml b/rust/main/ethers-prometheus/Cargo.toml index 3d2e74d6de..cd489c1a20 100644 --- a/rust/main/ethers-prometheus/Cargo.toml +++ b/rust/main/ethers-prometheus/Cargo.toml @@ -26,6 +26,7 @@ tokio = { workspace = true, features = ["time", "sync", "parking_lot"] } # enable feature for this crate that is imported by ethers-rs primitive-types = { workspace = true, features = ["fp-conversion"] } hyperlane-core = { path = "../hyperlane-core", features = ["agent", "float"] } +hyperlane-metric = { path = "../hyperlane-metric" } [build-dependencies] abigen = { path = "../utils/abigen", features = ["ethers"] } diff --git a/rust/main/ethers-prometheus/src/json_rpc_client.rs b/rust/main/ethers-prometheus/src/json_rpc_client.rs index 2cc8defe9b..4c4fa8a3d0 100644 --- a/rust/main/ethers-prometheus/src/json_rpc_client.rs +++ b/rust/main/ethers-prometheus/src/json_rpc_client.rs @@ -5,104 +5,16 @@ use std::fmt::{Debug, Formatter}; use std::time::Instant; use async_trait::async_trait; -use derive_builder::Builder; use derive_new::new; use ethers::prelude::JsonRpcClient; use ethers_core::types::U64; use hyperlane_core::rpc_clients::BlockNumberGetter; use hyperlane_core::ChainCommunicationError; -use maplit::hashmap; -use prometheus::{CounterVec, IntCounterVec}; +use hyperlane_metric::prometheus_metric::{ + PrometheusClientMetrics, PrometheusConfig, PrometheusConfigExt, +}; use serde::{de::DeserializeOwned, Serialize}; -pub use crate::ChainInfo; - -/// Some basic information about a node. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "camelCase"))] -pub struct NodeInfo { - /// The host of the node, e.g. `alchemy.com`, `quicknode.pro`, or - /// `localhost:8545`. - pub host: Option, -} - -/// Container for all the relevant rpc client metrics. -#[derive(Clone, Builder)] -pub struct JsonRpcClientMetrics { - /// Total number of requests made to this client. - /// - `provider_node`: node this is connecting to, e.g. `alchemy.com`, - /// `quicknode.pro`, or `localhost:8545`. - /// - `chain`: chain name (or chain id if the name is unknown) of the chain - /// the request was made on. - /// - `method`: request method string. - /// - `status`: `success` or `failure` depending on the response. A `success` - /// might still be an "error" but not one with the transport layer. - #[builder(setter(into, strip_option), default)] - request_count: Option, - - /// Total number of seconds spent making requests. - /// - `provider_node`: node this is connecting to, e.g. `alchemy.com`, - /// `quicknode.pro`, or `localhost:8545`. - /// - `chain`: chain name (or chain id if the name is unknown) of the chain - /// the request was made on. - /// - `method`: request method string. - /// - `status`: `success` or `failure` depending on the response. A `success` - /// might still be an "error" but not one with the transport layer. - #[builder(setter(into, strip_option), default)] - request_duration_seconds: Option, -} - -/// Expected label names for the metric. -pub const REQUEST_COUNT_LABELS: &[&str] = &["provider_node", "chain", "method", "status"]; -/// Help string for the metric. -pub const REQUEST_COUNT_HELP: &str = "Total number of requests made to this client"; - -/// Expected label names for the metric. -pub const REQUEST_DURATION_SECONDS_LABELS: &[&str] = - &["provider_node", "chain", "method", "status"]; -/// Help string for the metric. -pub const REQUEST_DURATION_SECONDS_HELP: &str = "Total number of seconds spent making requests"; - -/// Configuration for the prometheus JsonRpcClioent. This can be loaded via -/// serde. -#[derive(Default, Clone, Debug)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "camelCase"))] -pub struct PrometheusJsonRpcClientConfig { - /// Information about what node this client is connecting to. - pub node: Option, - - /// Information about the chain this client is for. - pub chain: Option, -} - -/// Helper functions for displaying node and chain information -pub trait PrometheusJsonRpcClientConfigExt { - /// The "host" part of the URL this node is connecting to. E.g. - /// `avalanche.api.onfinality.io`. - fn node_host(&self) -> &str; - /// Chain name this RPC client is connected to. - fn chain_name(&self) -> &str; -} - -impl PrometheusJsonRpcClientConfigExt for PrometheusJsonRpcClientConfig { - fn node_host(&self) -> &str { - self.node - .as_ref() - .and_then(|n| n.host.as_ref()) - .map(|h| h.as_str()) - .unwrap_or("unknown") - } - fn chain_name(&self) -> &str { - self.chain - .as_ref() - .and_then(|c| c.name.as_ref()) - .map(|n| n.as_str()) - .unwrap_or("unknown") - } -} - /// An ethers-rs JsonRpcClient wrapper that instruments requests with prometheus /// metrics. To make this as flexible as possible, the metric vecs need to be /// created and named externally, they should follow the naming convention here @@ -110,8 +22,8 @@ impl PrometheusJsonRpcClientConfigExt for PrometheusJsonRpcClientConfig { #[derive(new)] pub struct PrometheusJsonRpcClient { inner: C, - metrics: JsonRpcClientMetrics, - config: PrometheusJsonRpcClientConfig, + metrics: PrometheusClientMetrics, + config: PrometheusConfig, } impl Clone for PrometheusJsonRpcClient { @@ -140,7 +52,7 @@ impl PrometheusJsonRpcClient { } } -impl PrometheusJsonRpcClientConfigExt for PrometheusJsonRpcClient { +impl PrometheusConfigExt for PrometheusJsonRpcClient { /// The "host" part of the URL this node is connecting to. E.g. /// `avalanche.api.onfinality.io`. fn node_host(&self) -> &str { @@ -168,20 +80,8 @@ where { let start = Instant::now(); let res = self.inner.request(method, params).await; - let labels = hashmap! { - "provider_node" => self.config.node_host(), - "chain" => self.config.chain_name(), - "method" => method, - "status" => if res.is_ok() { "success" } else { "failure" } - }; - if let Some(counter) = &self.metrics.request_count { - counter.with(&labels).inc() - } - if let Some(counter) = &self.metrics.request_duration_seconds { - counter - .with(&labels) - .inc_by((Instant::now() - start).as_secs_f64()) - }; + self.metrics + .increment_metrics(&self.config, method, start, res.is_ok()); res } } diff --git a/rust/main/ethers-prometheus/src/lib.rs b/rust/main/ethers-prometheus/src/lib.rs index 8cf57329f0..21cd8e87f3 100644 --- a/rust/main/ethers-prometheus/src/lib.rs +++ b/rust/main/ethers-prometheus/src/lib.rs @@ -7,13 +7,3 @@ mod contracts; pub mod json_rpc_client; pub mod middleware; - -/// Some basic information about a chain. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "camelCase"))] -pub struct ChainInfo { - /// A human-friendly name for the chain. This should be a short string like - /// "kovan". - pub name: Option, -} diff --git a/rust/main/ethers-prometheus/src/middleware/mod.rs b/rust/main/ethers-prometheus/src/middleware/mod.rs index 5b82912cbf..5ea70097e7 100644 --- a/rust/main/ethers-prometheus/src/middleware/mod.rs +++ b/rust/main/ethers-prometheus/src/middleware/mod.rs @@ -13,6 +13,7 @@ use ethers::abi::AbiEncode; use ethers::prelude::*; use ethers::types::transaction::eip2718::TypedTransaction; use ethers::utils::hex::ToHex; +use hyperlane_metric::prometheus_metric::ChainInfo; use maplit::hashmap; use prometheus::{CounterVec, IntCounterVec}; use static_assertions::assert_impl_all; @@ -20,8 +21,6 @@ use tokio::sync::RwLock; pub use error::PrometheusMiddlewareError; -pub use crate::ChainInfo; - mod error; /// Some basic information about a wallet. diff --git a/rust/main/hyperlane-base/Cargo.toml b/rust/main/hyperlane-base/Cargo.toml index 136f1f6cb7..55d7b685a1 100644 --- a/rust/main/hyperlane-base/Cargo.toml +++ b/rust/main/hyperlane-base/Cargo.toml @@ -47,8 +47,8 @@ backtrace = { workspace = true, optional = true } backtrace-oneline = { path = "../utils/backtrace-oneline", optional = true } ethers-prometheus = { path = "../ethers-prometheus", features = ["serde"] } - hyperlane-core = { path = "../hyperlane-core", features = ["agent", "float"] } +hyperlane-metric = { path = "../hyperlane-metric" } hyperlane-operation-verifier = { path = "../applications/hyperlane-operation-verifier" } hyperlane-test = { path = "../hyperlane-test" } diff --git a/rust/main/hyperlane-base/src/metrics/core.rs b/rust/main/hyperlane-base/src/metrics/core.rs index 44fd6d4b09..11b05d7c19 100644 --- a/rust/main/hyperlane-base/src/metrics/core.rs +++ b/rust/main/hyperlane-base/src/metrics/core.rs @@ -13,7 +13,8 @@ use prometheus::{ }; use tokio::sync::RwLock; -use ethers_prometheus::{json_rpc_client::JsonRpcClientMetrics, middleware::MiddlewareMetrics}; +use ethers_prometheus::middleware::MiddlewareMetrics; +use hyperlane_metric::prometheus_metric::PrometheusClientMetrics; use crate::metrics::{ json_rpc_client::create_json_rpc_client_metrics, provider::create_provider_metrics, @@ -48,7 +49,7 @@ pub struct CoreMetrics { /// Set of metrics that tightly wrap the JsonRpcClient for use with the /// quorum provider. - json_rpc_client_metrics: OnceLock, + client_metrics: OnceLock, /// Set of provider-specific metrics. These only need to get created once. provider_metrics: OnceLock, @@ -197,7 +198,7 @@ impl CoreMetrics { latest_checkpoint, - json_rpc_client_metrics: OnceLock::new(), + client_metrics: OnceLock::new(), provider_metrics: OnceLock::new(), validator_metrics: ValidatorObservabilityMetricManager::new( @@ -217,8 +218,8 @@ impl CoreMetrics { /// Create the json rpc provider metrics attached to this core metrics /// instance. - pub fn json_rpc_client_metrics(&self) -> JsonRpcClientMetrics { - self.json_rpc_client_metrics + pub fn client_metrics(&self) -> PrometheusClientMetrics { + self.client_metrics .get_or_init(|| { create_json_rpc_client_metrics(self).expect("Failed to create rpc client metrics!") }) diff --git a/rust/main/hyperlane-base/src/metrics/json_rpc_client.rs b/rust/main/hyperlane-base/src/metrics/json_rpc_client.rs index 2b58b3b47e..312888ec7f 100644 --- a/rust/main/hyperlane-base/src/metrics/json_rpc_client.rs +++ b/rust/main/hyperlane-base/src/metrics/json_rpc_client.rs @@ -1,12 +1,15 @@ -use ethers_prometheus::json_rpc_client::*; use eyre::Result; +use hyperlane_metric::prometheus_metric::{ + PrometheusClientMetrics, PrometheusClientMetricsBuilder, REQUEST_COUNT_HELP, + REQUEST_COUNT_LABELS, REQUEST_DURATION_SECONDS_HELP, REQUEST_DURATION_SECONDS_LABELS, +}; use crate::CoreMetrics; pub(crate) fn create_json_rpc_client_metrics( metrics: &CoreMetrics, -) -> Result { - Ok(JsonRpcClientMetricsBuilder::default() +) -> Result { + Ok(PrometheusClientMetricsBuilder::default() .request_count(metrics.new_int_counter( "request_count", REQUEST_COUNT_HELP, diff --git a/rust/main/hyperlane-base/src/settings/chains.rs b/rust/main/hyperlane-base/src/settings/chains.rs index 1147ab9601..2122078926 100644 --- a/rust/main/hyperlane-base/src/settings/chains.rs +++ b/rust/main/hyperlane-base/src/settings/chains.rs @@ -4,8 +4,7 @@ use axum::async_trait; use ethers::prelude::Selector; use eyre::{eyre, Context, Report, Result}; -use ethers_prometheus::middleware::{ChainInfo, ContractInfo, PrometheusMiddlewareConf}; - +use ethers_prometheus::middleware::{ContractInfo, PrometheusMiddlewareConf}; use hyperlane_core::{ config::OperationBatchConfig, AggregationIsm, CcipReadIsm, ContractLocator, HyperlaneAbi, HyperlaneDomain, HyperlaneDomainProtocol, HyperlaneMessage, HyperlaneProvider, IndexMode, @@ -21,7 +20,11 @@ use hyperlane_ethereum::{ EthereumReorgPeriod, EthereumValidatorAnnounceAbi, }; use hyperlane_fuel as h_fuel; -use hyperlane_sealevel as h_sealevel; +use hyperlane_metric::prometheus_metric::ChainInfo; +use hyperlane_sealevel::{ + self as h_sealevel, client_builder::SealevelRpcClientBuilder, SealevelProvider, + SealevelRpcClient, TransactionSubmitter, +}; use crate::{ metrics::AgentMetricsConf, @@ -196,7 +199,7 @@ impl ChainConf { /// Try to convert the chain settings into an ApplicationOperationVerifier. pub async fn build_application_operation_verifier( &self, - _metrics: &CoreMetrics, + metrics: &CoreMetrics, ) -> Result> { let ctx = "Building application operation verifier"; let locator = self.locator(H256::zero()); @@ -207,7 +210,10 @@ impl ChainConf { as Box), ChainConnectionConf::Fuel(_) => todo!(), ChainConnectionConf::Sealevel(conf) => { - let provider = h_sealevel::SealevelProvider::new(locator.domain.clone(), conf); + let rpc_client = Arc::new(build_sealevel_rpc_client(self, conf, metrics)); + + let provider = + h_sealevel::SealevelProvider::new(rpc_client, locator.domain.clone(), conf); let verifier = h_sealevel::application::SealevelApplicationOperationVerifier::new(provider); Ok(Box::new(verifier) as Box) @@ -234,10 +240,11 @@ impl ChainConf { .await } ChainConnectionConf::Fuel(_) => todo!(), - ChainConnectionConf::Sealevel(conf) => Ok(Box::new(h_sealevel::SealevelProvider::new( - locator.domain.clone(), - conf, - )) as Box), + ChainConnectionConf::Sealevel(conf) => { + let rpc_client = Arc::new(build_sealevel_rpc_client(self, conf, metrics)); + let provider = build_sealevel_provider(rpc_client, &locator, conf); + Ok(Box::new(provider) as Box) + } ChainConnectionConf::Cosmos(conf) => { let provider = h_cosmos::CosmosProvider::new( locator.domain.clone(), @@ -270,9 +277,16 @@ impl ChainConf { } ChainConnectionConf::Sealevel(conf) => { let keypair = self.sealevel_signer().await.context(ctx)?; + + let rpc_client = Arc::new(build_sealevel_rpc_client(self, conf, metrics)); + let provider = build_sealevel_provider(rpc_client, &locator, conf); + let tx_submitter = build_tx_submitter(self, conf, metrics); + h_sealevel::SealevelMailbox::new( + provider, + tx_submitter, conf, - locator, + &locator, keypair.map(h_sealevel::SealevelKeypair::new), ) .map(|m| Box::new(m) as Box) @@ -305,7 +319,11 @@ impl ChainConf { todo!("Fuel does not support merkle tree hooks yet") } ChainConnectionConf::Sealevel(conf) => { - h_sealevel::SealevelMailbox::new(conf, locator, None) + let rpc_client = Arc::new(build_sealevel_rpc_client(self, conf, metrics)); + let provider = build_sealevel_provider(rpc_client, &locator, conf); + let tx_submitter = build_tx_submitter(self, conf, metrics); + + h_sealevel::SealevelMailbox::new(provider, tx_submitter, conf, &locator, None) .map(|m| Box::new(m) as Box) .map_err(Into::into) } @@ -343,9 +361,14 @@ impl ChainConf { } ChainConnectionConf::Fuel(_) => todo!(), ChainConnectionConf::Sealevel(conf) => { + let rpc_client = Arc::new(build_sealevel_rpc_client(self, conf, metrics)); + let provider = build_sealevel_provider(rpc_client, &locator, conf); + let tx_submitter = build_tx_submitter(self, conf, metrics); let indexer = Box::new(h_sealevel::SealevelMailboxIndexer::new( + provider, + tx_submitter, + &locator, conf, - locator, advanced_log_meta, )?); Ok(indexer as Box>) @@ -388,9 +411,14 @@ impl ChainConf { } ChainConnectionConf::Fuel(_) => todo!(), ChainConnectionConf::Sealevel(conf) => { + let rpc_client = Arc::new(build_sealevel_rpc_client(self, conf, metrics)); + let provider = build_sealevel_provider(rpc_client, &locator, conf); + let tx_submitter = build_tx_submitter(self, conf, metrics); let indexer = Box::new(h_sealevel::SealevelMailboxIndexer::new( + provider, + tx_submitter, + &locator, conf, - locator, advanced_log_meta, )?); Ok(indexer as Box>) @@ -431,8 +459,10 @@ impl ChainConf { } ChainConnectionConf::Fuel(_) => todo!(), ChainConnectionConf::Sealevel(conf) => { + let rpc_client = Arc::new(build_sealevel_rpc_client(self, conf, metrics)); let paymaster = Box::new( - h_sealevel::SealevelInterchainGasPaymaster::new(conf, &locator).await?, + h_sealevel::SealevelInterchainGasPaymaster::new(rpc_client, conf, &locator) + .await?, ); Ok(paymaster as Box) } @@ -475,8 +505,11 @@ impl ChainConf { } ChainConnectionConf::Fuel(_) => todo!(), ChainConnectionConf::Sealevel(conf) => { + let rpc_client = Arc::new(build_sealevel_rpc_client(self, conf, metrics)); + let indexer = Box::new( h_sealevel::SealevelInterchainGasPaymasterIndexer::new( + rpc_client, conf, locator, advanced_log_meta, @@ -521,9 +554,15 @@ impl ChainConf { } ChainConnectionConf::Fuel(_) => todo!(), ChainConnectionConf::Sealevel(conf) => { + let rpc_client = Arc::new(build_sealevel_rpc_client(self, conf, metrics)); + let provider = build_sealevel_provider(rpc_client, &locator, conf); + let tx_submitter = build_tx_submitter(self, conf, metrics); + let mailbox_indexer = Box::new(h_sealevel::SealevelMailboxIndexer::new( + provider, + tx_submitter, + &locator, conf, - locator, advanced_log_meta, )?); let indexer = Box::new(h_sealevel::SealevelMerkleTreeHookIndexer::new( @@ -561,7 +600,11 @@ impl ChainConf { } ChainConnectionConf::Fuel(_) => todo!(), ChainConnectionConf::Sealevel(conf) => { - let va = Box::new(h_sealevel::SealevelValidatorAnnounce::new(conf, locator)); + let rpc_client = Arc::new(build_sealevel_rpc_client(self, conf, metrics)); + let provider = build_sealevel_provider(rpc_client, &locator, conf); + let va = Box::new(h_sealevel::SealevelValidatorAnnounce::new( + provider, &locator, + )); Ok(va as Box) } ChainConnectionConf::Cosmos(conf) => { @@ -601,8 +644,10 @@ impl ChainConf { ChainConnectionConf::Fuel(_) => todo!(), ChainConnectionConf::Sealevel(conf) => { let keypair = self.sealevel_signer().await.context(ctx)?; + let rpc_client = Arc::new(build_sealevel_rpc_client(self, conf, metrics)); + let provider = build_sealevel_provider(rpc_client, &locator, conf); let ism = Box::new(h_sealevel::SealevelInterchainSecurityModule::new( - conf, + provider, locator, keypair.map(h_sealevel::SealevelKeypair::new), )); @@ -637,8 +682,10 @@ impl ChainConf { ChainConnectionConf::Fuel(_) => todo!(), ChainConnectionConf::Sealevel(conf) => { let keypair = self.sealevel_signer().await.context(ctx)?; + let rpc_client = Arc::new(build_sealevel_rpc_client(self, conf, metrics)); + let provider = build_sealevel_provider(rpc_client, &locator, conf); let ism = Box::new(h_sealevel::SealevelMultisigIsm::new( - conf, + provider, locator, keypair.map(h_sealevel::SealevelKeypair::new), )); @@ -878,7 +925,7 @@ impl ChainConf { signer = self.ethereum_signer().await?; } let metrics_conf = self.metrics_conf(); - let rpc_metrics = Some(metrics.json_rpc_client_metrics()); + let rpc_metrics = Some(metrics.client_metrics()); let middleware_metrics = Some((metrics.provider_metrics(), metrics_conf)); let res = builder .build_with_connection_conf(conf, locator, signer, rpc_metrics, middleware_metrics) @@ -886,3 +933,41 @@ impl ChainConf { Ok(res?) } } + +/// Helper to build a sealevel rpc client with metrics +fn build_sealevel_rpc_client( + chain_conf: &ChainConf, + connection_conf: &h_sealevel::ConnectionConf, + metrics: &CoreMetrics, +) -> SealevelRpcClient { + let middleware_metrics = chain_conf.metrics_conf(); + let rpc_client_url = connection_conf.url.clone(); + let client_metrics = metrics.client_metrics(); + SealevelRpcClientBuilder::new(rpc_client_url) + .with_prometheus_metrics(client_metrics.clone(), middleware_metrics.chain.clone()) + .build() +} + +/// Helper to build a sealevel provider +fn build_sealevel_provider( + rpc_client: Arc, + locator: &ContractLocator, + conf: &h_sealevel::ConnectionConf, +) -> SealevelProvider { + SealevelProvider::new(rpc_client, locator.domain.clone(), conf) +} + +fn build_tx_submitter( + chain_conf: &ChainConf, + connection_conf: &h_sealevel::ConnectionConf, + metrics: &CoreMetrics, +) -> Box { + let middleware_metrics = chain_conf.metrics_conf(); + let rpc_client_url = connection_conf.url.clone(); + let client_metrics = metrics.client_metrics(); + connection_conf.transaction_submitter.create_submitter( + rpc_client_url.to_string(), + client_metrics, + middleware_metrics.chain.clone(), + ) +} diff --git a/rust/main/hyperlane-metric/Cargo.toml b/rust/main/hyperlane-metric/Cargo.toml new file mode 100644 index 0000000000..c6ac11f588 --- /dev/null +++ b/rust/main/hyperlane-metric/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "hyperlane-metric" +documentation.workspace = true +edition.workspace = true +homepage.workspace = true +license-file.workspace = true +publish.workspace = true +version.workspace = true + +[dependencies] +async-trait.workspace = true +derive-new.workspace = true +derive_builder.workspace = true +maplit.workspace = true +prometheus.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +url.workspace = true diff --git a/rust/main/hyperlane-metric/src/lib.rs b/rust/main/hyperlane-metric/src/lib.rs new file mode 100644 index 0000000000..ea76471861 --- /dev/null +++ b/rust/main/hyperlane-metric/src/lib.rs @@ -0,0 +1,2 @@ +pub mod prometheus_metric; +pub mod utils; diff --git a/rust/main/hyperlane-metric/src/prometheus_metric.rs b/rust/main/hyperlane-metric/src/prometheus_metric.rs new file mode 100644 index 0000000000..3e86faeac0 --- /dev/null +++ b/rust/main/hyperlane-metric/src/prometheus_metric.rs @@ -0,0 +1,141 @@ +//! A wrapper around a JsonRpcClient to give insight at the request level. This +//! was designed specifically for use with the quorum provider. +use std::{fmt::Debug, time::Instant}; + +use derive_builder::Builder; +use maplit::hashmap; +use prometheus::{CounterVec, IntCounterVec}; +use serde::Deserialize; +use url::Url; + +use crate::utils::url_to_host_info; + +/// Expected label names for the metric. +pub const REQUEST_COUNT_LABELS: &[&str] = &["provider_node", "chain", "method", "status"]; +/// Help string for the metric. +pub const REQUEST_COUNT_HELP: &str = "Total number of requests made to this client"; + +/// Expected label names for the metric. +pub const REQUEST_DURATION_SECONDS_LABELS: &[&str] = + &["provider_node", "chain", "method", "status"]; +/// Help string for the metric. +pub const REQUEST_DURATION_SECONDS_HELP: &str = "Total number of seconds spent making requests"; + +/// Container for all the relevant rpc client metrics. +#[derive(Clone, Builder, Default)] +pub struct PrometheusClientMetrics { + /// Total number of requests made to this client. + /// - `provider_node`: node this is connecting to, e.g. `alchemy.com`, + /// `quicknode.pro`, or `localhost:8545`. + /// - `chain`: chain name (or chain id if the name is unknown) of the chain + /// the request was made on. + /// - `method`: request method string. + /// - `status`: `success` or `failure` depending on the response. A `success` + /// might still be an "error" but not one with the transport layer. + #[builder(setter(into, strip_option), default)] + pub request_count: Option, + + /// Total number of seconds spent making requests. + /// - `provider_node`: node this is connecting to, e.g. `alchemy.com`, + /// `quicknode.pro`, or `localhost:8545`. + /// - `chain`: chain name (or chain id if the name is unknown) of the chain + /// the request was made on. + /// - `method`: request method string. + /// - `status`: `success` or `failure` depending on the response. A `success` + /// might still be an "error" but not one with the transport layer. + #[builder(setter(into, strip_option), default)] + pub request_duration_seconds: Option, +} + +impl PrometheusClientMetrics { + /// Update prometheus metrics + pub fn increment_metrics( + &self, + config: &PrometheusConfig, + method: &str, + start: Instant, + success: bool, + ) { + let labels = hashmap! { + "provider_node" => config.node_host(), + "chain" => config.chain_name(), + "method" => method, + "status" => if success { "success" } else { "failure" }, + }; + if let Some(counter) = &self.request_count { + counter.with(&labels).inc() + } + if let Some(counter) = &self.request_duration_seconds { + counter + .with(&labels) + .inc_by((Instant::now() - start).as_secs_f64()) + }; + } +} + +/// Some basic information about a chain. +#[derive(Clone, Debug, Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub struct ChainInfo { + /// A human-friendly name for the chain. This should be a short string like + /// "kovan". + pub name: Option, +} + +/// Some basic information about a node. +#[derive(Clone, Debug, Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub struct NodeInfo { + /// The host of the node, e.g. `alchemy.com`, `quicknode.pro`, or + /// `localhost:8545`. + pub host: Option, +} + +/// Configuration for the prometheus JsonRpcClioent. This can be loaded via +/// serde. +#[derive(Default, Clone, Debug, Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub struct PrometheusConfig { + /// Information about what node this client is connecting to. + pub node: Option, + + /// Information about the chain this client is for. + pub chain: Option, +} + +impl PrometheusConfig { + pub fn from_url(url: &Url, chain: Option) -> Self { + Self { + node: Some(NodeInfo { + host: url_to_host_info(url), + }), + chain, + } + } +} + +/// Helper functions for displaying node and chain information +pub trait PrometheusConfigExt { + /// The "host" part of the URL this node is connecting to. E.g. + /// `avalanche.api.onfinality.io`. + fn node_host(&self) -> &str; + /// Chain name this RPC client is connected to. + fn chain_name(&self) -> &str; +} + +impl PrometheusConfigExt for PrometheusConfig { + fn node_host(&self) -> &str { + self.node + .as_ref() + .and_then(|n| n.host.as_ref()) + .map(|h| h.as_str()) + .unwrap_or("unknown") + } + fn chain_name(&self) -> &str { + self.chain + .as_ref() + .and_then(|c| c.name.as_ref()) + .map(|n| n.as_str()) + .unwrap_or("unknown") + } +} diff --git a/rust/main/hyperlane-metric/src/utils.rs b/rust/main/hyperlane-metric/src/utils.rs new file mode 100644 index 0000000000..ab75a209e5 --- /dev/null +++ b/rust/main/hyperlane-metric/src/utils.rs @@ -0,0 +1,17 @@ +use std::fmt::Write; + +use url::Url; + +/// converts url into a host:port string +pub fn url_to_host_info(url: &Url) -> Option { + let mut s = String::new(); + if let Some(host) = url.host_str() { + s.push_str(host); + if let Some(port) = url.port() { + write!(&mut s, ":{port}").unwrap(); + } + Some(s) + } else { + None + } +} diff --git a/rust/main/utils/run-locally/src/invariants/termination_invariants.rs b/rust/main/utils/run-locally/src/invariants/termination_invariants.rs index 9f44bde2af..f2a7d27c3a 100644 --- a/rust/main/utils/run-locally/src/invariants/termination_invariants.rs +++ b/rust/main/utils/run-locally/src/invariants/termination_invariants.rs @@ -215,6 +215,7 @@ pub fn relayer_termination_invariants_met( if !relayer_balance_check(starting_relayer_balance)? { return Ok(false); } + Ok(true) } diff --git a/rust/main/utils/run-locally/src/sealevel/termination_invariants.rs b/rust/main/utils/run-locally/src/sealevel/termination_invariants.rs index e7d29a1b68..e07e17634e 100644 --- a/rust/main/utils/run-locally/src/sealevel/termination_invariants.rs +++ b/rust/main/utils/run-locally/src/sealevel/termination_invariants.rs @@ -1,5 +1,7 @@ use std::path::Path; +use maplit::hashmap; + use crate::{ config::Config, invariants::{ @@ -9,6 +11,7 @@ use crate::{ logging::log, sealevel::{solana::*, SOL_MESSAGES_EXPECTED, SOL_MESSAGES_WITH_NON_MATCHING_IGP}, server::{fetch_relayer_gas_payment_event_count, fetch_relayer_message_processed_count}, + {fetch_metric, RELAYER_METRICS_PORT}, }; /// Use the metrics to check if the relayer queues are empty and the expected @@ -60,6 +63,23 @@ pub fn termination_invariants_met( return Ok(false); } + if !request_metric_invariant_met(total_messages_expected)? { + return Ok(false); + } + log!("Termination invariants have been meet"); Ok(true) } + +pub fn request_metric_invariant_met(expected_request_count: u32) -> eyre::Result { + let request_count = fetch_metric( + RELAYER_METRICS_PORT, + "hyperlane_request_count", + &hashmap! {"chain" => "sealeveltest1", "status" => "success"}, + )? + .iter() + .sum::(); + + assert!(request_count > expected_request_count); + Ok(true) +}