use core::time::Duration;
use flex_error::{define_error, DisplayOnly, TraceError};
use http::uri::InvalidUri;
use humantime::format_duration;
use ibc_proto::protobuf::Error as TendermintProtoError;
use prost::{DecodeError, EncodeError};
use regex::Regex;
use tendermint::abci;
use tendermint::Error as TendermintError;
use tendermint_light_client::components::io::IoError as LightClientIoError;
use tendermint_light_client::errors::{
Error as LightClientError, ErrorDetail as LightClientErrorDetail,
};
use tendermint_rpc::endpoint::abci_query::AbciQuery;
use tendermint_rpc::endpoint::broadcast::tx_sync::Response as TxSyncResponse;
use tendermint_rpc::Error as TendermintRpcError;
use tonic::{
metadata::errors::InvalidMetadataValue, transport::Error as TransportError,
Status as GrpcStatus,
};
use ibc_relayer_types::{
applications::ics29_fee::error::Error as FeeError,
clients::ics07_tendermint::error as tendermint_error,
core::{
ics02_client::{client_type::ClientType, error as client_error},
ics03_connection::error as connection_error,
ics23_commitment::error as commitment_error,
ics24_host::identifier::{ChainId, ChannelId, ConnectionId},
},
proofs::ProofError,
relayer::ics18_relayer::error as relayer_error,
};
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, KeyType};
use crate::sdk_error::SdkError;
define_error! {
Error {
Io
[ TraceError<std::io::Error> ]
|_| { "I/O error" },
Rpc
{ url: tendermint_rpc::Url }
[ TendermintRpcError ]
|e| { format!("RPC error to endpoint {}", e.url) },
AbciQuery
{ query: AbciQuery }
|e| { format!("ABCI query returned an error: {:?}", e.query) },
Config
[ ConfigError ]
|_| { "Configuration error" },
CheckTx
{
response: TxSyncResponse,
}
| e | { format!("CheckTx returned an error: {:?}", e.response) },
DeliverTx
{
detail: SdkError,
tx: abci::response::DeliverTx,
}
|e| { format!("DeliverTx Commit returns error: {0}. RawResult: {1:?}", e.detail, e.tx) },
SendTx
{
detail: String
}
|e| { format_args!("send_tx resulted in chain error event: {}", e.detail) },
WebSocket
{ url: tendermint_rpc::Url }
|e| { format!("Websocket error to endpoint {}", e.url) },
EventMonitor
[ monitor::Error ]
|_| { "event monitor error" },
Grpc
|_| { "gRPC error" },
GrpcStatus
{ status: GrpcStatus }
|e| { format!("gRPC call failed with status: {0}", e.status) },
GrpcTransport
[ TraceError<TransportError> ]
|_| { "error in underlying transport when making gRPC call" },
GrpcResponseParam
{ param: String }
|e| { format!("missing parameter in GRPC response: {}", e.param) },
Decode
[ TendermintProtoError ]
|_| { "error decoding protobuf" },
LightClientVerification
{ chain_id: String }
[ LightClientError ]
|e| { format!("light client verification error for chain id {0}", e.chain_id) },
LightClientState
[ client_error::Error ]
|_| { "light client encountered error due to client state".to_string() },
LightClientIo
{ address: String }
[ LightClientIoError ]
|e| { format!("light client error for RPC address {0}", e.address) },
ChainNotCaughtUp
{
address: String,
chain_id: ChainId,
}
|e| { format!("node at {} running chain {} not caught up", e.address, e.chain_id) },
PrivateStore
|_| { "requested proof for a path in the private store" },
Event
|_| { "bad notification" },
ConversionFromAny
[ TendermintProtoError ]
|_| { "conversion from a protobuf `Any` into a domain type failed" },
EmptyUpgradedClientState
|_| { "found no upgraded client state" },
ConsensusStateTypeMismatch
{
expected: ClientType,
got: ClientType,
}
|e| { format!("consensus state type mismatch; hint: expected client type '{0}', got '{1}'", e.expected, e.got) },
EmptyResponseValue
|_| { "empty response value" },
EmptyResponseProof
|_| { "empty response proof" },
RpcResponse
{ detail: String }
| e | { format!("RPC client returns error response: {}", e.detail) },
MalformedProof
[ ProofError ]
|_| { "malformed proof" },
InvalidHeight
[ TendermintError ]
|_| { "invalid height" },
InvalidHeightNoSource
|_| { "invalid height" },
InvalidMetadata
[ TraceError<InvalidMetadataValue> ]
|_| { "invalid metadata" },
BuildClientStateFailure
|_| { "failed to create client state" },
CreateClient
{ client_id: String }
|e| { format!("failed to create client {0}", e.client_id) },
ClientStateType
{ client_state_type: String }
|e| { format!("unexpected client state type {0}", e.client_state_type) },
ConnectionNotFound
{ connection_id: ConnectionId }
|e| { format!("connection not found: {0}", e.connection_id) },
BadConnectionState
|_| { "bad connection state" },
ConnOpen
{ connection_id: ConnectionId, reason: String }
|e| {
format!("failed to build conn open message {0}: {1}", e.connection_id, e.reason)
},
ConnOpenInit
{ reason: String }
|e| { format!("failed to build conn open init: {0}", e.reason) },
ConnOpenTry
{ reason: String }
|e| { format!("failed to build conn open try: {0}", e.reason) },
ChanOpenAck
{ channel_id: ChannelId, reason: String }
|e| {
format!("failed to build channel open ack {0}: {1}", e.channel_id, e.reason)
},
ChanOpenConfirm
{ channel_id: ChannelId, reason: String }
|e| {
format!("failed to build channel open confirm {0}: {1}", e.channel_id, e.reason)
},
ConsensusProof
[ ProofError ]
|_| { "failed to build consensus proof" },
Packet
{ channel_id: ChannelId, reason: String }
|e| {
format!("failed to build packet {0}: {1}", e.channel_id, e.reason)
},
RecvPacket
{ channel_id: ChannelId, reason: String }
|e| {
format!("failed to build recv packet {0}: {1}", e.channel_id, e.reason)
},
AckPacket
{ channel_id: ChannelId, reason: String }
|e| {
format!("failed to build acknowledge packet {0}: {1}", e.channel_id, e.reason)
},
TimeoutPacket
{ channel_id: ChannelId, reason: String }
|e| {
format!("failed to build timeout packet {0}: {1}", e.channel_id, e.reason)
},
MessageTransaction
{ reason: String }
|e| { format!("message transaction failure: {0}", e.reason) },
Query
{ query: String }
|e| { format!("query error occurred (failed to query for {0})", e.query) },
KeyBase
[ KeyringError ]
|_| { "keyring error" },
KeyNotFound
{ key_name: String }
[ KeyringError ]
|e| { format!("signature key not found: {}", e.key_name) },
Ics02
[ client_error::Error ]
|e| { format!("ICS 02 error: {}", e.source) },
Ics03
[ connection_error::Error ]
|_| { "ICS 03 error" },
Ics07
[ tendermint_error::Error ]
|_| { "ICS 07 error" },
Ics18
[ relayer_error::Error ]
|_| { "ICS 18 error" },
Ics23
[ commitment_error::Error ]
|_| { "ICS 23 error" },
Ics29
[ FeeError ]
| _ | { "ICS 29 error" },
InvalidUri
{ uri: String }
[ TraceError<InvalidUri> ]
|e| { format!("error parsing URI {}", e.uri) },
ChainIdentifier
{ chain_id: String }
|e| { format!("invalid chain identifier format: {0}", e.chain_id) },
NonProvableData
|_| { "requested proof for data in the privateStore" },
ChannelSend
|_| { "internal message-passing failure while sending inter-thread request/response" },
ChannelReceive
[ TraceError<crossbeam_channel::RecvError> ]
|_| { "internal message-passing failure while receiving inter-thread request/response" },
ChannelReceiveTimeout
[ TraceError<crossbeam_channel::RecvTimeoutError> ]
|_| { "timeout when waiting for reponse over inter-thread channel" },
InvalidInputHeader
|_| { "the input header is not recognized as a header for this chain" },
TxNoConfirmation
|_| { "failed tx: no confirmation" },
Misbehaviour
{ reason: String }
|e| { format!("error raised while submitting the misbehaviour evidence: {0}", e.reason) },
InvalidKeyAddress
{ address: String }
[ TendermintError ]
|e| { format!("invalid key address: {0}", e.address) },
Bech32Encoding
[ TraceError<bech32::Error> ]
|_| { "bech32 encoding failed" },
ClientTypeMismatch
{
expected: ClientType,
got: ClientType,
}
|e| {
format!("client type mismatch: expected '{}', got '{}'",
e.expected, e.got)
},
ProtobufDecode
{ payload_type: String }
[ TraceError<DecodeError> ]
|e| { format!("error decoding protocol buffer for {}", e.payload_type) },
ProtobufEncode
{ payload_type: String }
[ TraceError<EncodeError> ]
|e| { format!("error encoding protocol buffer for {}", e.payload_type) },
TxSimulateGasEstimateExceeded
{
chain_id: ChainId,
estimated_gas: u64,
max_gas: u64,
}
|e| {
format!("{} gas estimate {} from simulated Tx exceeds the maximum configured {}",
e.chain_id, e.estimated_gas, e.max_gas)
},
HealthCheckJsonRpc
{
chain_id: ChainId,
address: String,
endpoint: String,
}
[ DisplayOnly<tendermint_rpc::error::Error> ]
|e| {
format!("health check failed for endpoint {0} on the JSON-RPC interface of chain {1}:{2}",
e.endpoint, e.chain_id, e.address)
},
FetchVersionParsing
{
chain_id: ChainId,
address: String,
}
[ version::Error ]
|e| {
format!("failed while parsing version info for chain {0}:{1}; caused by: {2}",
e.chain_id, e.address, e.source)
},
FetchVersionGrpcTransport
{
chain_id: ChainId,
address: String,
endpoint: String,
}
[ DisplayOnly<tonic::transport::Error> ]
|e| {
format!("failed while fetching version info from endpoint {0} on the gRPC interface of chain {1}:{2}",
e.endpoint, e.chain_id, e.address)
},
FetchVersionGrpcStatus
{
chain_id: ChainId,
address: String,
endpoint: String,
status: tonic::Status
}
|e| {
format!("failed while fetching version info from endpoint {0} on the gRPC interface of chain {1}:{2}; caused by: {3}",
e.endpoint, e.chain_id, e.address, e.status)
},
FetchVersionInvalidVersionResponse
{
chain_id: ChainId,
address: String,
endpoint: String,
}
|e| {
format!("failed while fetching version info from endpoint {0} on the gRPC interface of chain {1}:{2}; the gRPC response contains no application version information",
e.endpoint, e.chain_id, e.address)
},
ConfigValidationJsonRpc
{
chain_id: ChainId,
address: String,
endpoint: String,
}
[ DisplayOnly<tendermint_rpc::error::Error> ]
|e| {
format!("semantic config validation: failed to reach endpoint {0} on the JSON-RPC interface of chain {1}:{2}",
e.endpoint, e.chain_id, e.address)
},
ConfigValidationTxSizeOutOfBounds
{
chain_id: ChainId,
configured_bound: usize,
genesis_bound: u64,
}
|e| {
format!("semantic config validation failed for option `max_tx_size` for chain '{}', reason: `max_tx_size` = {} is greater than {}% of the consensus parameter `max_size` = {}",
e.chain_id, e.configured_bound, BLOCK_MAX_BYTES_MAX_FRACTION * 100.0, e.genesis_bound)
},
ConfigValidationMaxGasTooHigh
{
chain_id: ChainId,
configured_max_gas: u64,
consensus_max_gas: i64,
}
|e| {
format!("semantic config validation failed for option `max_gas` for chain '{}', reason: `max_gas` = {} is greater than the consensus parameter `max_gas` = {}",
e.chain_id, e.configured_max_gas, e.consensus_max_gas)
},
ConfigValidationTrustingPeriodSmallerThanZero
{
chain_id: ChainId,
trusting_period: Duration,
}
|e| {
format!("semantic config validation failed for option `trusting_period` of chain '{}', reason: trusting period ({}) must be greater than zero",
e.chain_id, format_duration(e.trusting_period))
},
ConfigValidationTrustingPeriodGreaterThanUnbondingPeriod
{
chain_id: ChainId,
trusting_period: Duration,
unbonding_period: Duration,
}
|e| {
format!("semantic config validation failed for option `trusting_period` of chain '{}', reason: trusting period ({}) must be smaller than the unbonding period ({})",
e.chain_id, format_duration(e.trusting_period), format_duration(e.unbonding_period))
},
ConfigValidationDefaultGasTooHigh
{
chain_id: ChainId,
default_gas: u64,
max_gas: u64,
}
|e| {
format!("semantic config validation failed for option `default_gas` of chain '{}', reason: default gas ({}) must be smaller than the max gas ({})",
e.chain_id, e.default_gas, e.max_gas)
},
ConfigValidationGasMultiplierLow
{
chain_id: ChainId,
gas_multiplier: f64,
}
|e| {
format!("semantic config validation failed for option `gas_multiplier` of chain '{}', reason: gas multiplier ({}) is smaller than `1.1`, which could trigger gas fee errors in production", e.chain_id, e.gas_multiplier)
},
SdkModuleVersion
{
chain_id: ChainId,
address: String,
cause: String
}
|e| {
format!("Hermes health check failed while verifying the application compatibility for chain {0}:{1}; caused by: {2}",
e.chain_id, e.address, e.cause)
},
UnknownAccountType
{
type_url: String
}
|e| {
format!("failed to deserialize account of an unknown protobuf type: {0}", e.type_url)
},
EmptyBaseAccount
|_| { "empty BaseAccount within EthAccount" },
EmptyQueryAccount
{ address: String }
|e| { format!("Query/Account RPC returned an empty account for address: {}", e.address) },
NoHistoricalEntries
{ chain_id: ChainId }
|e| {
format_args!(
"staking module for chain '{}' does not maintain any historical entries \
(`historical_entries` staking params is set to 0)",
e.chain_id
)
},
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 }
|e| {
format_args!(
"transaction indexing for chain '{}' is disabled (`node_info.other.tx_index` is off)",
e.chain_id
)
},
EmptyDenomTrace
{ hash: String }
|e| {
format_args!(
"Query/DenomTrace RPC returned an empty denom trace for trace hash: {}", e.hash)
},
MessageTooBigForTx
{ len: usize }
|e| {
format_args!("message with length {} is too large for a transaction", e.len)
},
InvalidKeyType
{ key_type: KeyType }
|e| {
format!("Invalid key type {} for the current chain", e.key_type)
}
}
}
impl Error {
pub fn send<T>(_: crossbeam_channel::SendError<T>) -> Error {
Error::channel_send()
}
pub fn is_trusted_state_outside_trusting_period_error(&self) -> bool {
match self.detail() {
ErrorDetail::LightClientVerification(e) => matches!(
e.source,
LightClientErrorDetail::TrustedStateOutsideTrustingPeriod(_)
),
_ => false,
}
}
}
impl GrpcStatusSubdetail {
pub fn is_client_state_height_too_low(&self) -> bool {
let msg = self.status.message();
msg.contains("verification failed") && msg.contains("client state height < proof height")
}
pub fn is_account_sequence_mismatch_that_requires_refresh(&self) -> bool {
self.status.message().contains("account sequence mismatch")
}
pub fn is_out_of_order_packet_sequence_error(&self) -> bool {
self.status
.message()
.contains("packet sequence is out of order")
}
pub fn is_account_sequence_mismatch_that_can_be_ignored(&self) -> bool {
match parse_sequences_in_mismatch_error_message(self.status.message()) {
None => false,
Some((expected, got)) => expected < got,
}
}
}
fn parse_sequences_in_mismatch_error_message(message: &str) -> Option<(u64, u64)> {
let re =
Regex::new(r#"account sequence mismatch, expected (?P<expected>\d+), got (?P<got>\d+)"#)
.unwrap();
match re.captures(message) {
None => None,
Some(captures) => match (captures["expected"].parse(), captures["got"].parse()) {
(Ok(e), Ok(g)) => Some((e, g)),
_ => None,
},
}
}
pub const QUERY_PROOF_EXPECT_MSG: &str =
"Internal error. Requested proof with query but no proof was returned.";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_sequences_in_mismatch_error_message() {
struct Test<'a> {
name: &'a str,
message: &'a str,
result: Option<(u64, u64)>,
}
let tests: Vec<Test<'_>> = vec![
Test {
name: "good mismatch error, expected < got",
message:
"account sequence mismatch, expected 100, got 200: incorrect account sequence",
result: Some((100, 200)),
},
Test {
name: "good mismatch error, expected > got",
message:
"account sequence mismatch, expected 200, got 100: incorrect account sequence",
result: Some((200, 100)),
},
Test {
name: "good changed mismatch error, expected < got",
message: "account sequence mismatch, expected 100, got 200: this part has changed",
result: Some((100, 200)),
},
Test {
name: "good changed mismatch error, expected > got",
message:
"account sequence mismatch, expected 200, got 100 --> this part has changed",
result: Some((200, 100)),
},
Test {
name: "good changed mismatch error, expected > got",
message:
"codespace sdk code 32: incorrect account sequence: account sequence mismatch, expected 200, got 100",
result: Some((200, 100)),
},
Test {
name: "bad mismatch error, bad expected",
message:
"account sequence mismatch, expected 2a5, got 100: incorrect account sequence",
result: None,
},
Test {
name: "bad mismatch error, bad got",
message:
"account sequence mismatch, expected 25, got -29: incorrect account sequence",
result: None,
},
Test {
name: "not a mismatch error",
message: "some other error message",
result: None,
},
];
for test in tests {
assert_eq!(
test.result,
parse_sequences_in_mismatch_error_message(test.message),
"{}",
test.name
)
}
}
}