Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a check for min_gas_price in the health check process #2873

Merged
merged 39 commits into from
Dec 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
597e4ce
Move inline comments in `do_health_check` function to doc comments
seanchen1991 Nov 1, 2022
6d48c10
Add comments on how to proceed
seanchen1991 Nov 2, 2022
a061d81
Bump ibc-proto version
seanchen1991 Nov 15, 2022
60a7195
Update ibc-proto to 0.22 across the board
seanchen1991 Nov 17, 2022
162dae1
Fix merge conflicts
seanchen1991 Nov 17, 2022
cb0e298
Add `query_config_params` function
seanchen1991 Nov 17, 2022
7685023
Implement `TryInto` for `GasPrice`
seanchen1991 Nov 17, 2022
4807244
Saving progress
seanchen1991 Nov 17, 2022
f87e34d
Remove ci/no-std-check/Cargo.toml
seanchen1991 Nov 21, 2022
5c65981
Improve error types
seanchen1991 Nov 21, 2022
b131218
Merge branch 'master' into verify-min-gas-price
seanchen1991 Nov 21, 2022
643faea
Fix clippy error
seanchen1991 Nov 21, 2022
c4c20ba
Saving progress
seanchen1991 Nov 22, 2022
bab00b3
Remove unused error variant
seanchen1991 Nov 22, 2022
326c57a
Implement PartialOrd for GasPrice
seanchen1991 Nov 22, 2022
15369c9
Add GasPriceTooLow error variant
seanchen1991 Nov 22, 2022
727c4c7
Fix gas check comparison logic
seanchen1991 Nov 22, 2022
6266df1
Merge branch 'master' into verify-min-gas-price
seanchen1991 Nov 23, 2022
a409024
Saving progress
seanchen1991 Nov 23, 2022
8d8a24d
Merge branch 'verify-min-gas-price' of https://github.com/informalsys…
seanchen1991 Nov 23, 2022
0db501c
Verify min gas price tests (#2911)
seanchen1991 Nov 29, 2022
803590e
Address PR feedback
seanchen1991 Nov 29, 2022
e23553f
Address PR feedback
seanchen1991 Nov 29, 2022
1195f22
Merge branch 'master' into verify-min-gas-price
seanchen1991 Nov 29, 2022
c749a90
Add method for parsing multiple gas prices as well as unit tests
seanchen1991 Nov 30, 2022
9805f0a
Merge branch 'verify-min-gas-price' of https://github.com/informalsys…
seanchen1991 Nov 30, 2022
13919ea
Improve GasPrice comparison logic in health check function
seanchen1991 Dec 1, 2022
239d33c
Fix clippy issues
seanchen1991 Dec 1, 2022
e25642b
Merge branch 'master' of https://github.com/informalsystems/hermes in…
seanchen1991 Dec 1, 2022
5cab4e8
Add missing imports
seanchen1991 Dec 2, 2022
85eab49
Revert "Add missing imports"
seanchen1991 Dec 2, 2022
230245b
Add missing imports
seanchen1991 Dec 2, 2022
1a041d8
Address PR feedback
seanchen1991 Dec 6, 2022
8bc1df9
Add test that checks malformed price input
seanchen1991 Dec 6, 2022
1757744
Merge branch 'master' into verify-min-gas-price
seanchen1991 Dec 6, 2022
1bd13c5
Add changelog entry
seanchen1991 Dec 7, 2022
cfc3932
Merge branch 'master' into verify-min-gas-price
seanchen1991 Dec 7, 2022
ceb19cc
Merge branch 'master' into verify-min-gas-price
seanchen1991 Dec 7, 2022
70c35eb
Merge branch 'master' into verify-min-gas-price
romac Dec 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- The health check process now compares the configured `gas_price` config setting against the full node's `min_gas_price` setting, ensuring that the former is at least equal to the latter
([#2776](https://github.com/informalsystems/hermes/issues/2776))
2 changes: 1 addition & 1 deletion crates/relayer-cli/src/commands/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ fn register_signals(tx_cmd: Sender<SupervisorCmd>) -> Result<(), io::Error> {
SIGUSR1, // Dump state
];

let mut signals = Signals::new(&sigs)?;
let mut signals = Signals::new(sigs)?;

std::thread::spawn(move || {
for signal in &mut signals {
Expand Down
107 changes: 99 additions & 8 deletions crates/relayer/src/chain/cosmos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use tokio::runtime::Runtime as TokioRuntime;
use tonic::{codegen::http::Uri, metadata::AsciiMetadataValue};
use tracing::{error, instrument, trace, warn};

use ibc_proto::cosmos::base::node::v1beta1::ConfigResponse;
use ibc_proto::cosmos::staking::v1beta1::Params as StakingParams;
use ibc_proto::protobuf::Protobuf;
use ibc_relayer_types::clients::ics07_tendermint::client_state::{
Expand Down Expand Up @@ -77,7 +78,7 @@ use crate::chain::handle::Subscription;
use crate::chain::requests::*;
use crate::chain::tracking::TrackedMsgs;
use crate::client_state::{AnyClientState, IdentifiedAnyClientState};
use crate::config::ChainConfig;
use crate::config::{parse_gas_prices, ChainConfig, GasPrice};
use crate::consensus_state::{AnyConsensusState, AnyConsensusStateWithHeight};
use crate::denom::DenomTrace;
use crate::error::Error;
Expand Down Expand Up @@ -318,6 +319,68 @@ impl CosmosSdkChain {
Ok(params)
}

/// Query the node for its configuration parameters.
///
/// ### Note: This query endpoint was introduced in SDK v0.46.3/v0.45.10. Not available before that.
///
/// Returns:
/// - `Ok(Some(..))` if the query was successful.
/// - `Ok(None) in case the query endpoint is not available.
/// - `Err` for any other error.
pub fn query_config_params(&self) -> Result<Option<ConfigResponse>, Error> {
crate::time!("query_config_params");
crate::telemetry!(query, self.id(), "query_config_params");

// Helper function to diagnose if the node config query is unimplemented
// by matching on the error details.
fn is_unimplemented_node_query(err_status: &tonic::Status) -> bool {
if err_status.code() != tonic::Code::Unimplemented {
return false;
}

err_status
.message()
.contains("unknown service cosmos.base.node.v1beta1.Service")
}

let mut client = self
.block_on(
ibc_proto::cosmos::base::node::v1beta1::service_client::ServiceClient::connect(
self.grpc_addr.clone(),
),
)
.map_err(Error::grpc_transport)?;

let request = tonic::Request::new(ibc_proto::cosmos::base::node::v1beta1::ConfigRequest {});

match self.block_on(client.config(request)) {
Ok(response) => {
let params = response.into_inner();

Ok(Some(params))
}
Err(e) => {
if is_unimplemented_node_query(&e) {
Ok(None)
} else {
Err(Error::grpc_status(e))
}
}
}
}

/// The minimum gas price that this node accepts
pub fn min_gas_price(&self) -> Result<Vec<GasPrice>, Error> {
crate::time!("min_gas_price");

let min_gas_price: Vec<GasPrice> =
self.query_config_params()?.map_or(vec![], |cfg_response| {
parse_gas_prices(cfg_response.minimum_gas_price)
});

Ok(min_gas_price)
}

/// The unbonding period of this chain
pub fn unbonding_period(&self) -> Result<Duration, Error> {
crate::time!("unbonding_period");
Expand Down Expand Up @@ -1873,12 +1936,23 @@ fn client_id_suffix(client_id: &ClientId) -> Option<u64> {
.and_then(|e| e.parse::<u64>().ok())
}

/// Performs a health check on a Cosmos chain.
///
/// This health check checks on the following in this order:
/// 1. Checks on the self-reported health endpoint.
/// 2. Checks that the staking module maintains some historical entries such
/// that local header information is stored in the IBC state and thus
/// client proofs that are part of the connection handshake can be verified.
/// 3. Checks that transaction indexing is enabled.
/// 4. Checks that the chain identifier matches the network name.
/// 5. Checks that the underlying SDK and ibc-go versions are compatible.
/// 6. Checks that the `gas_price` parameter in Hermes is >= the `min_gas_price`
/// advertised by the node Hermes is connected to.
fn do_health_check(chain: &CosmosSdkChain) -> Result<(), Error> {
let chain_id = chain.id();
let grpc_address = chain.grpc_addr.to_string();
let rpc_address = chain.config.rpc_addr.to_string();

// Checkup on the self-reported health endpoint
chain.block_on(chain.rpc_client.health()).map_err(|e| {
Error::health_check_json_rpc(
chain_id.clone(),
Expand All @@ -1888,21 +1962,16 @@ fn do_health_check(chain: &CosmosSdkChain) -> Result<(), Error> {
)
})?;

// Check that the staking module maintains some historical entries, meaning that
// local header information is stored in the IBC state and therefore client
// proofs that are part of the connection handshake messages can be verified.
if chain.historical_entries()? == 0 {
return Err(Error::no_historical_entries(chain_id.clone()));
}

let status = chain.chain_status()?;

// Check that transaction indexing is enabled
if status.node_info.other.tx_index != TxIndexStatus::On {
return Err(Error::tx_indexing_disabled(chain_id.clone()));
}

// Check that the chain identifier matches the network name
if status.node_info.network.as_str() != chain_id.as_str() {
// Log the error, continue optimistically
error!(
Expand All @@ -1912,9 +1981,31 @@ fn do_health_check(chain: &CosmosSdkChain) -> Result<(), Error> {
);
}

let relayer_gas_price = &chain.config.gas_price;
let node_min_gas_prices = chain.min_gas_price()?;
let mut found_matching_denom = false;

for price in node_min_gas_prices {
match relayer_gas_price.partial_cmp(&price) {
Some(Ordering::Less) => return Err(Error::gas_price_too_low(chain_id.clone())),
Some(_) => {
found_matching_denom = true;
break;
}
None => continue,
}
}

if !found_matching_denom {
warn!(
"Chain '{}' has no minimum gas price value configured for denomination '{}'. \
This is usually a sign of misconfiguration, please check your config.toml",
chain_id, relayer_gas_price.denom
);
}

let version_specs = chain.block_on(fetch_version_specs(&chain.config.id, &chain.grpc_addr))?;

// Checkup on the underlying SDK & IBC-go versions
if let Err(diagnostic) = compatibility::run_diagnostic(&version_specs) {
return Err(Error::sdk_module_version(
chain_id.clone(),
Expand Down
108 changes: 107 additions & 1 deletion crates/relayer/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ pub mod types;

use alloc::collections::BTreeMap;
use core::{
cmp::Ordering,
fmt::{Display, Error as FmtError, Formatter},
str::FromStr,
time::Duration,
};
use std::{fs, fs::File, io::Write, path::Path};
Expand All @@ -28,6 +30,7 @@ use crate::error::Error as RelayerError;
use crate::extension_options::ExtensionOptionDynamicFeeTx;
use crate::keyring::Store;

pub use crate::config::Error as ConfigError;
pub use error::Error;

pub use filter::PacketFilter;
Expand All @@ -50,6 +53,56 @@ impl Display for GasPrice {
}
}

impl FromStr for GasPrice {
type Err = ConfigError;

fn from_str(price_in: &str) -> Result<Self, Self::Err> {
// TODO: We split by `char::is_alphabetic` delimiter.
// More robust parsing methods might be needed.
let spos = price_in.find(char::is_alphabetic);

match spos {
Some(position) => {
let (price_str, denom) = price_in.split_at(position);

let price = price_str
.parse::<f64>()
.map_err(|_| Error::invalid_gas_price(price_in.to_string()))?;

Ok(GasPrice {
price,
denom: denom.to_owned(),
})
}

None => Err(Error::invalid_gas_price(price_in.to_string())),
}
}
}

// Note: Only `PartialOrd` is implemented for `GasPrice` because gas
// prices must be of the same denomination in order to be compared.
impl PartialOrd for GasPrice {
seanchen1991 marked this conversation as resolved.
Show resolved Hide resolved
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self.denom == other.denom {
self.price.partial_cmp(&other.price)
} else {
None
}
}
}

/// Attempts to parse 0 or more `GasPrice`s from a String,
/// returning the successfully parsed prices in a Vec. Any
/// single price that fails to be parsed does not affect
/// the parsing of other prices.
pub fn parse_gas_prices(prices: String) -> Vec<GasPrice> {
prices
.split(';')
.filter_map(|gp| GasPrice::from_str(gp).ok())
.collect()
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(
rename_all = "snake_case",
Expand Down Expand Up @@ -482,7 +535,10 @@ pub(crate) fn store_writer(config: &Config, mut writer: impl Write) -> Result<()

#[cfg(test)]
mod tests {
use super::{load, store_writer};
use core::str::FromStr;

use super::{load, parse_gas_prices, store_writer};
use crate::config::GasPrice;
use test_log::test;

#[test]
Expand All @@ -509,4 +565,54 @@ mod tests {
let mut buffer = Vec::new();
store_writer(&config, &mut buffer).unwrap();
}

#[test]
fn gas_price_from_str() {
let gp_original = GasPrice::new(10.0, "atom".to_owned());

let gp_raw = gp_original.to_string();
let gp = GasPrice::from_str(&gp_raw).expect("could not parse String into GasPrice");

assert_eq!(gp, gp_original);
}

#[test]
fn parse_multiple_gas_prices() {
let gas_prices = "0.25token1;0.0001token2";
let parsed = parse_gas_prices(gas_prices.to_string());

let expected = vec![
GasPrice {
price: 0.25,
denom: "token1".to_owned(),
},
GasPrice {
price: 0.0001,
denom: "token2".to_owned(),
},
];

assert_eq!(expected, parsed);
}

#[test]
fn parse_empty_gas_price() {
let empty_price = "";
let parsed = parse_gas_prices(empty_price.to_string());

assert_eq!(parsed, vec![]);
}
seanchen1991 marked this conversation as resolved.
Show resolved Hide resolved

#[test]
fn malformed_gas_prices_do_not_get_parsed() {
let malformed_prices = "token1;.token2;0.25token3";
let parsed = parse_gas_prices(malformed_prices.to_string());

let expected = vec![GasPrice {
price: 0.25,
denom: "token3".to_owned(),
}];

assert_eq!(expected, parsed);
}
}
4 changes: 4 additions & 0 deletions crates/relayer/src/config/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,9 @@ define_error! {
Encode
[ TraceError<toml::ser::Error> ]
|_| { "invalid configuration" },

InvalidGasPrice
{ price: String }
|e| { format!("invalid gas price: {}", e.price) },
}
}
8 changes: 8 additions & 0 deletions crates/relayer/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use ibc_relayer_types::{

use crate::chain::cosmos::version;
use crate::chain::cosmos::BLOCK_MAX_BYTES_MAX_FRACTION;
use crate::config::Error as ConfigError;
use crate::event::monitor;
use crate::keyring::errors::Error as KeyringError;
use crate::sdk_error::SdkError;
Expand All @@ -56,6 +57,10 @@ define_error! {
{ query: AbciQuery }
|e| { format!("ABCI query returned an error: {:?}", e.query) },

Config
[ ConfigError ]
|_| { "Configuration error" },

CheckTx
{
response: TxSyncResponse,
Expand Down Expand Up @@ -523,6 +528,9 @@ define_error! {
)
},

GasPriceTooLow
{ chain_id: ChainId }
|e| { format!("Hermes gas price is lower than the minimum gas price set by node operator'{}'", e.chain_id) },

TxIndexingDisabled
{ chain_id: ChainId }
Expand Down