diff --git a/CHANGELOG.md b/CHANGELOG.md index e96f8a3bab..6bb09a98b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,9 +14,7 @@ - [ibc-relayer] - Support for relayer restart ([#561]) - Add support for ordered channels ([#599]) - - Consistent identifier handling across ICS 02, 03 and 04 ([#622]) - -- [ibc-relayer] + - Misbehaviour detection and evidence submission ([#632]) - Use a stateless light client without a runtime ([#673]) - [ibc-relayer-cli] @@ -35,7 +33,7 @@ - MBT: use modelator crate ([#761]) - [ibc-relayer] - - [nothing yet] + - Consistent identifier handling across ICS 02, 03 and 04 ([#622]) - [ibc-relayer-cli] - Clarified success path for updating a client that is already up-to-date ([#734]) @@ -80,6 +78,7 @@ [#550]: https://github.com/informalsystems/ibc-rs/issues/550 [#599]: https://github.com/informalsystems/ibc-rs/issues/599 [#630]: https://github.com/informalsystems/ibc-rs/issues/630 +[#632]: https://github.com/informalsystems/ibc-rs/issues/632 [#672]: https://github.com/informalsystems/ibc-rs/issues/672 [#673]: https://github.com/informalsystems/ibc-rs/issues/673 [#675]: https://github.com/informalsystems/ibc-rs/issues/675 diff --git a/e2e/e2e/client.py b/e2e/e2e/client.py index c70d66739d..d636c17b48 100644 --- a/e2e/e2e/client.py +++ b/e2e/e2e/client.py @@ -40,14 +40,13 @@ class ClientUpdated: @cmd("tx raw update-client") class TxUpdateClient(Cmd[ClientUpdated]): dst_chain_id: ChainId - src_chain_id: ChainId dst_client_id: ClientId def args(self) -> List[str]: - return [self.dst_chain_id, self.src_chain_id, self.dst_client_id] + return [self.dst_chain_id, self.dst_client_id] def process(self, result: Any) -> ClientUpdated: - return from_dict(ClientUpdated, result['UpdateClient']) + return from_dict(ClientUpdated, result['UpdateClient']['common']) # ----------------------------------------------------------------------------- @@ -102,8 +101,8 @@ def create_client(c: Config, dst: ChainId, src: ChainId) -> ClientCreated: return client -def update_client(c: Config, dst: ChainId, src: ChainId, client_id: ClientId) -> ClientUpdated: - cmd = TxUpdateClient(dst_chain_id=dst, src_chain_id=src, +def update_client(c: Config, dst: ChainId, client_id: ClientId) -> ClientUpdated: + cmd = TxUpdateClient(dst_chain_id=dst, dst_client_id=client_id) res = cmd.run(c).success() l.info(f'Updated client to: {res.consensus_height}') @@ -122,7 +121,7 @@ def create_update_query_client(c: Config, dst: ChainId, src: ChainId) -> ClientI split() query_client_state(c, dst, client.client_id) split() - update_client(c, dst, src, client.client_id) + update_client(c, dst, client.client_id) split() query_client_state(c, dst, client.client_id) split() diff --git a/e2e/e2e/common.py b/e2e/e2e/common.py index 97ae5c47f9..10b96b7f51 100644 --- a/e2e/e2e/common.py +++ b/e2e/e2e/common.py @@ -39,7 +39,6 @@ class Ordering(Enum): ClientType = NewType('ClientType', str) BlockHeight = NewType('BlockHeight', str) - def split(): sleep(0.5) print() diff --git a/modules/src/events.rs b/modules/src/events.rs index d0e41aed51..2d1001e9dd 100644 --- a/modules/src/events.rs +++ b/modules/src/events.rs @@ -13,6 +13,7 @@ use crate::Height; #[derive(Debug, Clone, Deserialize, Serialize)] pub enum IbcEventType { CreateClient, + UpdateClient, SendPacket, WriteAck, } @@ -21,6 +22,7 @@ impl IbcEventType { pub fn as_str(&self) -> &'static str { match *self { IbcEventType::CreateClient => "create_client", + IbcEventType::UpdateClient => "update_client", IbcEventType::SendPacket => "send_packet", IbcEventType::WriteAck => "write_acknowledgement", } @@ -35,7 +37,7 @@ pub enum IbcEvent { CreateClient(ClientEvents::CreateClient), UpdateClient(ClientEvents::UpdateClient), UpgradeClient(ClientEvents::UpgradeClient), - ClientMisbehavior(ClientEvents::ClientMisbehavior), + ClientMisbehaviour(ClientEvents::ClientMisbehaviour), OpenInitConnection(ConnectionEvents::OpenInit), OpenTryConnection(ConnectionEvents::OpenTry), @@ -87,7 +89,7 @@ impl IbcEvent { IbcEvent::NewBlock(bl) => bl.height(), IbcEvent::CreateClient(ev) => ev.height(), IbcEvent::UpdateClient(ev) => ev.height(), - IbcEvent::ClientMisbehavior(ev) => ev.height(), + IbcEvent::ClientMisbehaviour(ev) => ev.height(), IbcEvent::OpenInitConnection(ev) => ev.height(), IbcEvent::OpenTryConnection(ev) => ev.height(), IbcEvent::OpenAckConnection(ev) => ev.height(), @@ -113,7 +115,7 @@ impl IbcEvent { IbcEvent::CreateClient(ev) => ev.set_height(height), IbcEvent::UpdateClient(ev) => ev.set_height(height), IbcEvent::UpgradeClient(ev) => ev.set_height(height), - IbcEvent::ClientMisbehavior(ev) => ev.set_height(height), + IbcEvent::ClientMisbehaviour(ev) => ev.set_height(height), IbcEvent::OpenInitConnection(ev) => ev.set_height(height), IbcEvent::OpenTryConnection(ev) => ev.set_height(height), IbcEvent::OpenAckConnection(ev) => ev.set_height(height), diff --git a/modules/src/ics02_client/client_consensus.rs b/modules/src/ics02_client/client_consensus.rs index 5951cd5c64..722f58c3c8 100644 --- a/modules/src/ics02_client/client_consensus.rs +++ b/modules/src/ics02_client/client_consensus.rs @@ -1,15 +1,20 @@ use core::marker::{Send, Sync}; -use std::convert::TryFrom; +use std::convert::{TryFrom, TryInto}; use chrono::{DateTime, Utc}; use prost_types::Any; use serde::Serialize; use tendermint_proto::Protobuf; +use ibc_proto::ibc::core::client::v1::ConsensusStateWithHeight; + +use crate::events::IbcEventType; use crate::ics02_client::client_type::ClientType; use crate::ics02_client::error::{Error, Kind}; +use crate::ics02_client::height::Height; use crate::ics07_tendermint::consensus_state; use crate::ics23_commitment::commitment::CommitmentRoot; +use crate::ics24_host::identifier::ClientId; #[cfg(any(test, feature = "mocks"))] use crate::mock::client_state::MockConsensusState; @@ -108,6 +113,41 @@ impl From for Any { } } +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +pub struct AnyConsensusStateWithHeight { + pub height: Height, + pub consensus_state: AnyConsensusState, +} + +impl Protobuf for AnyConsensusStateWithHeight {} + +impl TryFrom for AnyConsensusStateWithHeight { + type Error = Error; + + fn try_from(value: ConsensusStateWithHeight) -> Result { + let state = value + .consensus_state + .map(AnyConsensusState::try_from) + .transpose() + .map_err(|e| Kind::InvalidRawConsensusState.context(e))? + .ok_or(Kind::EmptyConsensusStateResponse)?; + + Ok(AnyConsensusStateWithHeight { + height: value.height.ok_or(Kind::InvalidHeightResult)?.try_into()?, + consensus_state: state, + }) + } +} + +impl From for ConsensusStateWithHeight { + fn from(value: AnyConsensusStateWithHeight) -> Self { + ConsensusStateWithHeight { + height: Some(value.height.into()), + consensus_state: Some(value.consensus_state.into()), + } + } +} + impl ConsensusState for AnyConsensusState { fn client_type(&self) -> ClientType { self.client_type() @@ -125,3 +165,11 @@ impl ConsensusState for AnyConsensusState { self } } + +#[derive(Clone, Debug)] +pub struct QueryClientEventRequest { + pub height: crate::Height, + pub event_id: IbcEventType, + pub client_id: ClientId, + pub consensus_height: crate::Height, +} diff --git a/modules/src/ics02_client/client_state.rs b/modules/src/ics02_client/client_state.rs index 105ab95198..cb1f35fc15 100644 --- a/modules/src/ics02_client/client_state.rs +++ b/modules/src/ics02_client/client_state.rs @@ -7,6 +7,7 @@ use tendermint_proto::Protobuf; use crate::ics02_client::client_type::ClientType; use crate::ics02_client::error::{Error, Kind}; + use crate::ics07_tendermint::client_state; use crate::ics24_host::identifier::ChainId; #[cfg(any(test, feature = "mocks"))] @@ -53,6 +54,7 @@ impl AnyClientState { Self::Mock(mock_state) => mock_state.latest_height(), } } + pub fn client_type(&self) -> ClientType { match self { Self::Tendermint(state) => state.client_type(), diff --git a/modules/src/ics02_client/error.rs b/modules/src/ics02_client/error.rs index 7fca139325..672a024b93 100644 --- a/modules/src/ics02_client/error.rs +++ b/modules/src/ics02_client/error.rs @@ -53,6 +53,9 @@ pub enum Kind { #[error("unknown header type: {0}")] UnknownHeaderType(String), + #[error("unknown misbehaviour type: {0}")] + UnknownMisbehaviourType(String), + #[error("invalid raw client state")] InvalidRawClientState, @@ -65,6 +68,9 @@ pub enum Kind { #[error("invalid raw header")] InvalidRawHeader, + #[error("invalid raw misbehaviour")] + InvalidRawMisbehaviour, + #[error("invalid height result")] InvalidHeightResult, diff --git a/modules/src/ics02_client/events.rs b/modules/src/ics02_client/events.rs index 9fede49bf4..4c07c115e5 100644 --- a/modules/src/ics02_client/events.rs +++ b/modules/src/ics02_client/events.rs @@ -1,17 +1,22 @@ //! Types for the IBC events emitted from Tendermint Websocket by the client module. +use std::convert::{TryFrom, TryInto}; + +use anomaly::BoxError; +use serde_derive::{Deserialize, Serialize}; +use subtle_encoding::hex; +use tendermint_proto::Protobuf; + use crate::attribute; use crate::events::{IbcEvent, RawObject}; use crate::ics02_client::client_type::ClientType; -use crate::ics24_host::identifier::ClientId; -use anomaly::BoxError; - +use crate::ics02_client::header::AnyHeader; use crate::ics02_client::height::Height; -use serde_derive::{Deserialize, Serialize}; -use std::convert::{TryFrom, TryInto}; +use crate::ics24_host::identifier::ClientId; /// The content of the `type` field for the event that a chain produces upon executing the create client transaction. const CREATE_EVENT_TYPE: &str = "create_client"; const UPDATE_EVENT_TYPE: &str = "update_client"; +const MISBEHAVIOUR_EVENT_TYPE: &str = "client_misbehaviour"; const UPGRADE_EVENT_TYPE: &str = "upgrade_client"; /// The content of the `key` field for the attribute containing the client identifier. @@ -23,12 +28,19 @@ const CLIENT_TYPE_ATTRIBUTE_KEY: &str = "client_type"; /// The content of the `key` field for the attribute containing the height. const CONSENSUS_HEIGHT_ATTRIBUTE_KEY: &str = "consensus_height"; +/// The content of the `key` field for the header in update client event. +const HEADER: &str = "header"; + pub fn try_from_tx(event: &tendermint::abci::Event) -> Option { match event.type_str.as_ref() { CREATE_EVENT_TYPE => Some(IbcEvent::CreateClient(CreateClient( extract_attributes_from_tx(event), ))), - UPDATE_EVENT_TYPE => Some(IbcEvent::UpdateClient(UpdateClient( + UPDATE_EVENT_TYPE => Some(IbcEvent::UpdateClient(UpdateClient { + common: extract_attributes_from_tx(event), + header: extract_header_from_tx(event), + })), + MISBEHAVIOUR_EVENT_TYPE => Some(IbcEvent::ClientMisbehaviour(ClientMisbehaviour( extract_attributes_from_tx(event), ))), UPGRADE_EVENT_TYPE => Some(IbcEvent::UpgradeClient(UpgradeClient( @@ -56,6 +68,19 @@ fn extract_attributes_from_tx(event: &tendermint::abci::Event) -> Attributes { attr } +pub fn extract_header_from_tx(event: &tendermint::abci::Event) -> Option { + for tag in &event.attributes { + let key = tag.key.as_ref(); + let value = tag.value.as_ref(); + if let HEADER = key { + let header_bytes = hex::decode(value).unwrap(); + let header: AnyHeader = Protobuf::decode(header_bytes.as_ref()).unwrap(); + return Some(header); + } + } + None +} + /// NewBlock event signals the committing & execution of a new block. // TODO - find a better place for NewBlock #[derive(Debug, Deserialize, Serialize, Clone)] @@ -143,37 +168,57 @@ impl From for IbcEvent { /// UpdateClient event signals a recent update of an on-chain client (IBC Client). #[derive(Debug, Deserialize, Serialize, Clone)] -pub struct UpdateClient(Attributes); +pub struct UpdateClient { + pub common: Attributes, + pub header: Option, +} impl UpdateClient { pub fn client_id(&self) -> &ClientId { - &self.0.client_id + &self.common.client_id + } + pub fn client_type(&self) -> ClientType { + self.common.client_type } pub fn height(&self) -> Height { - self.0.height + self.common.height } + pub fn set_height(&mut self, height: Height) { - self.0.height = height; + self.common.height = height; + } + + pub fn consensus_height(&self) -> Height { + self.common.consensus_height } } impl From for UpdateClient { fn from(attrs: Attributes) -> Self { - UpdateClient(attrs) + UpdateClient { + common: attrs, + header: None, + } } } impl TryFrom for UpdateClient { type Error = BoxError; fn try_from(obj: RawObject) -> Result { + let header_str: String = attribute!(obj, "update_client.header"); + let header_bytes = hex::decode(header_str).unwrap(); + let header: AnyHeader = Protobuf::decode(header_bytes.as_ref()).unwrap(); let consensus_height_str: String = attribute!(obj, "update_client.consensus_height"); - Ok(UpdateClient(Attributes { - height: obj.height, - client_id: attribute!(obj, "update_client.client_id"), - client_type: attribute!(obj, "update_client.client_type"), - consensus_height: consensus_height_str.as_str().try_into()?, - })) + Ok(UpdateClient { + common: Attributes { + height: obj.height, + client_id: attribute!(obj, "update_client.client_id"), + client_type: attribute!(obj, "update_client.client_type"), + consensus_height: consensus_height_str.as_str().try_into()?, + }, + header: Some(header), + }) } } @@ -183,12 +228,12 @@ impl From for IbcEvent { } } -/// ClientMisbehavior event signals the update of an on-chain client (IBC Client) with evidence of -/// misbehavior. +/// ClientMisbehaviour event signals the update of an on-chain client (IBC Client) with evidence of +/// misbehaviour. #[derive(Debug, Deserialize, Serialize, Clone)] -pub struct ClientMisbehavior(Attributes); +pub struct ClientMisbehaviour(Attributes); -impl ClientMisbehavior { +impl ClientMisbehaviour { pub fn client_id(&self) -> &ClientId { &self.0.client_id } @@ -200,11 +245,11 @@ impl ClientMisbehavior { } } -impl TryFrom for ClientMisbehavior { +impl TryFrom for ClientMisbehaviour { type Error = BoxError; fn try_from(obj: RawObject) -> Result { let consensus_height_str: String = attribute!(obj, "client_misbehaviour.consensus_height"); - Ok(ClientMisbehavior(Attributes { + Ok(ClientMisbehaviour(Attributes { height: obj.height, client_id: attribute!(obj, "client_misbehaviour.client_id"), client_type: attribute!(obj, "client_misbehaviour.client_type"), @@ -213,9 +258,9 @@ impl TryFrom for ClientMisbehavior { } } -impl From for IbcEvent { - fn from(v: ClientMisbehavior) -> Self { - IbcEvent::ClientMisbehavior(v) +impl From for IbcEvent { + fn from(v: ClientMisbehaviour) -> Self { + IbcEvent::ClientMisbehaviour(v) } } diff --git a/modules/src/ics02_client/handler.rs b/modules/src/ics02_client/handler.rs index 481d621985..d2f05b7161 100644 --- a/modules/src/ics02_client/handler.rs +++ b/modules/src/ics02_client/handler.rs @@ -25,5 +25,8 @@ where ClientMsg::CreateClient(msg) => create_client::process(ctx, msg), ClientMsg::UpdateClient(msg) => update_client::process(ctx, msg), ClientMsg::UpgradeClient(msg) => upgrade_client::process(ctx, msg), + _ => { + unimplemented!() + } } } diff --git a/modules/src/ics02_client/header.rs b/modules/src/ics02_client/header.rs index cf5d0297d7..0b642fc95e 100644 --- a/modules/src/ics02_client/header.rs +++ b/modules/src/ics02_client/header.rs @@ -1,6 +1,7 @@ use std::convert::TryFrom; use prost_types::Any; +use serde_derive::{Deserialize, Serialize}; use tendermint_proto::Protobuf; use crate::ics02_client::client_type::ClientType; @@ -26,7 +27,7 @@ pub trait Header: Clone + std::fmt::Debug + Send + Sync { fn wrap_any(self) -> AnyHeader; } -#[derive(Clone, Debug, PartialEq)] // TODO: Add Eq bound once possible +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] // TODO: Add Eq bound once possible #[allow(clippy::large_enum_variant)] pub enum AnyHeader { Tendermint(TendermintHeader), diff --git a/modules/src/ics02_client/height.rs b/modules/src/ics02_client/height.rs index 0c5222aa6f..43e5df4718 100644 --- a/modules/src/ics02_client/height.rs +++ b/modules/src/ics02_client/height.rs @@ -2,11 +2,12 @@ use std::cmp::Ordering; use std::convert::TryFrom; use std::str::FromStr; +use serde_derive::{Deserialize, Serialize}; use tendermint_proto::Protobuf; -use crate::ics02_client::error::{Error, Kind}; use ibc_proto::ibc::core::client::v1::Height as RawHeight; -use serde_derive::{Deserialize, Serialize}; + +use crate::ics02_client::error::{Error, Kind}; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Height { @@ -144,6 +145,12 @@ impl TryFrom<&str> for Height { } } +impl From for String { + fn from(height: Height) -> Self { + format!("{}-{}", height.revision_number, height.revision_number) + } +} + impl FromStr for Height { type Err = Error; diff --git a/modules/src/ics02_client/misbehaviour.rs b/modules/src/ics02_client/misbehaviour.rs new file mode 100644 index 0000000000..760e21e00b --- /dev/null +++ b/modules/src/ics02_client/misbehaviour.rs @@ -0,0 +1,112 @@ +use std::convert::TryFrom; + +use prost_types::Any; +use tendermint_proto::Protobuf; + +use crate::ics02_client::error::{Error, Kind}; +use crate::ics07_tendermint::misbehaviour::Misbehaviour as TmMisbehaviour; + +#[cfg(any(test, feature = "mocks"))] +use crate::mock::misbehaviour::Misbehaviour as MockMisbehaviour; + +use crate::ics24_host::identifier::ClientId; +use crate::Height; + +pub const TENDERMINT_MISBEHAVIOR_TYPE_URL: &str = "/ibc.lightclients.tendermint.v1.Misbehaviour"; + +#[cfg(any(test, feature = "mocks"))] +pub const MOCK_MISBEHAVIOUR_TYPE_URL: &str = "/ibc.mock.Misbehavior"; + +#[dyn_clonable::clonable] +pub trait Misbehaviour: Clone + std::fmt::Debug + Send + Sync { + /// The type of client (eg. Tendermint) + fn client_id(&self) -> &ClientId; + + /// The height of the consensus state + fn height(&self) -> Height; + + fn wrap_any(self) -> AnyMisbehaviour; +} + +#[derive(Clone, Debug, PartialEq)] // TODO: Add Eq bound once possible +#[allow(clippy::large_enum_variant)] +pub enum AnyMisbehaviour { + Tendermint(TmMisbehaviour), + + #[cfg(any(test, feature = "mocks"))] + Mock(MockMisbehaviour), +} + +impl Misbehaviour for AnyMisbehaviour { + fn client_id(&self) -> &ClientId { + match self { + Self::Tendermint(misbehaviour) => misbehaviour.client_id(), + + #[cfg(any(test, feature = "mocks"))] + Self::Mock(misbehaviour) => misbehaviour.client_id(), + } + } + + fn height(&self) -> Height { + match self { + Self::Tendermint(misbehaviour) => misbehaviour.height(), + + #[cfg(any(test, feature = "mocks"))] + Self::Mock(misbehaviour) => misbehaviour.height(), + } + } + + fn wrap_any(self) -> AnyMisbehaviour { + self + } +} + +impl Protobuf for AnyMisbehaviour {} + +impl TryFrom for AnyMisbehaviour { + type Error = Error; + + fn try_from(raw: Any) -> Result { + match raw.type_url.as_str() { + TENDERMINT_MISBEHAVIOR_TYPE_URL => Ok(AnyMisbehaviour::Tendermint( + TmMisbehaviour::decode_vec(&raw.value) + .map_err(|e| Kind::InvalidRawMisbehaviour.context(e))?, + )), + + #[cfg(any(test, feature = "mocks"))] + MOCK_MISBEHAVIOUR_TYPE_URL => Ok(AnyMisbehaviour::Mock( + MockMisbehaviour::decode_vec(&raw.value) + .map_err(|e| Kind::InvalidRawMisbehaviour.context(e))?, + )), + _ => Err(Kind::UnknownMisbehaviourType(raw.type_url).into()), + } + } +} + +impl From for Any { + fn from(value: AnyMisbehaviour) -> Self { + match value { + AnyMisbehaviour::Tendermint(misbehaviour) => Any { + type_url: TENDERMINT_MISBEHAVIOR_TYPE_URL.to_string(), + value: misbehaviour.encode_vec().unwrap(), + }, + + #[cfg(any(test, feature = "mocks"))] + AnyMisbehaviour::Mock(misbehaviour) => Any { + type_url: MOCK_MISBEHAVIOUR_TYPE_URL.to_string(), + value: misbehaviour.encode_vec().unwrap(), + }, + } + } +} + +impl std::fmt::Display for AnyMisbehaviour { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + match self { + AnyMisbehaviour::Tendermint(tm) => write!(f, "{}", tm), + + #[cfg(any(test, feature = "mocks"))] + AnyMisbehaviour::Mock(mock) => write!(f, "{:?}", mock), + } + } +} diff --git a/modules/src/ics02_client/mod.rs b/modules/src/ics02_client/mod.rs index cef7efc570..f0ecc6e946 100644 --- a/modules/src/ics02_client/mod.rs +++ b/modules/src/ics02_client/mod.rs @@ -10,4 +10,5 @@ pub mod events; pub mod handler; pub mod header; pub mod height; +pub mod misbehaviour; pub mod msgs; diff --git a/modules/src/ics02_client/msgs.rs b/modules/src/ics02_client/msgs.rs index 9d27533507..85ee98b1d4 100644 --- a/modules/src/ics02_client/msgs.rs +++ b/modules/src/ics02_client/msgs.rs @@ -5,10 +5,12 @@ //! https://github.com/cosmos/ics/tree/master/spec/ics-002-client-semantics#create. use crate::ics02_client::msgs::create_client::MsgCreateAnyClient; +use crate::ics02_client::msgs::misbehavior::MsgSubmitAnyMisbehaviour; use crate::ics02_client::msgs::update_client::MsgUpdateAnyClient; use crate::ics02_client::msgs::upgrade_client::MsgUpgradeAnyClient; pub mod create_client; +pub mod misbehavior; pub mod update_client; pub mod upgrade_client; @@ -17,5 +19,6 @@ pub mod upgrade_client; pub enum ClientMsg { CreateClient(MsgCreateAnyClient), UpdateClient(MsgUpdateAnyClient), + Misbehaviour(MsgSubmitAnyMisbehaviour), UpgradeClient(MsgUpgradeAnyClient), } diff --git a/modules/src/ics02_client/msgs/create_client.rs b/modules/src/ics02_client/msgs/create_client.rs index 9baac4f763..296055b2df 100644 --- a/modules/src/ics02_client/msgs/create_client.rs +++ b/modules/src/ics02_client/msgs/create_client.rs @@ -107,7 +107,6 @@ mod tests { use crate::ics02_client::client_consensus::AnyConsensusState; use crate::ics02_client::msgs::MsgCreateAnyClient; - use crate::ics07_tendermint::client_state::test_util::get_dummy_tendermint_client_state; use crate::ics07_tendermint::header::test_util::get_dummy_tendermint_header; use crate::test_utils::get_dummy_account_id; diff --git a/modules/src/ics02_client/msgs/misbehavior.rs b/modules/src/ics02_client/msgs/misbehavior.rs new file mode 100644 index 0000000000..a414152c69 --- /dev/null +++ b/modules/src/ics02_client/msgs/misbehavior.rs @@ -0,0 +1,63 @@ +use std::convert::TryFrom; + +use tendermint_proto::Protobuf; + +use ibc_proto::ibc::core::client::v1::MsgSubmitMisbehaviour as RawMsgSubmitMisbehaviour; + +use crate::ics02_client::error::{Error, Kind}; +use crate::ics02_client::misbehaviour::AnyMisbehaviour; +use crate::ics24_host::identifier::ClientId; +use crate::signer::Signer; +use crate::tx_msg::Msg; + +pub const TYPE_URL: &str = "/ibc.core.client.v1.MsgSubmitMisbehaviour"; + +/// A type of message that submits client misbehaviour proof. +#[derive(Clone, Debug, PartialEq)] +pub struct MsgSubmitAnyMisbehaviour { + /// client unique identifier + pub client_id: ClientId, + /// misbehaviour used for freezing the light client + pub misbehaviour: AnyMisbehaviour, + /// signer address + pub signer: Signer, +} + +impl Msg for MsgSubmitAnyMisbehaviour { + type ValidationError = crate::ics24_host::error::ValidationError; + type Raw = RawMsgSubmitMisbehaviour; + + fn route(&self) -> String { + crate::keys::ROUTER_KEY.to_string() + } + + fn type_url(&self) -> String { + TYPE_URL.to_string() + } +} + +impl Protobuf for MsgSubmitAnyMisbehaviour {} + +impl TryFrom for MsgSubmitAnyMisbehaviour { + type Error = Error; + + fn try_from(raw: RawMsgSubmitMisbehaviour) -> Result { + let raw_misbehaviour = raw.misbehaviour.ok_or(Kind::InvalidRawMisbehaviour)?; + + Ok(MsgSubmitAnyMisbehaviour { + client_id: raw.client_id.parse().unwrap(), + misbehaviour: AnyMisbehaviour::try_from(raw_misbehaviour).unwrap(), + signer: raw.signer.into(), + }) + } +} + +impl From for RawMsgSubmitMisbehaviour { + fn from(ics_msg: MsgSubmitAnyMisbehaviour) -> Self { + RawMsgSubmitMisbehaviour { + client_id: ics_msg.client_id.to_string(), + misbehaviour: Some(ics_msg.misbehaviour.into()), + signer: ics_msg.signer.to_string(), + } + } +} diff --git a/modules/src/ics03_connection/handler/verify.rs b/modules/src/ics03_connection/handler/verify.rs index b8e4cd3b69..217f6239c0 100644 --- a/modules/src/ics03_connection/handler/verify.rs +++ b/modules/src/ics03_connection/handler/verify.rs @@ -1,8 +1,7 @@ //! ICS3 verification functions, common across all four handlers of ICS3. use crate::ics02_client::client_consensus::ConsensusState; -use crate::ics02_client::client_state::AnyClientState; -use crate::ics02_client::client_state::ClientState; +use crate::ics02_client::client_state::{AnyClientState, ClientState}; use crate::ics02_client::{client_def::AnyClient, client_def::ClientDef}; use crate::ics03_connection::connection::ConnectionEnd; use crate::ics03_connection::context::ConnectionReader; diff --git a/modules/src/ics07_tendermint/client_def.rs b/modules/src/ics07_tendermint/client_def.rs index 0c88ab4ecf..5e99d62a8a 100644 --- a/modules/src/ics07_tendermint/client_def.rs +++ b/modules/src/ics07_tendermint/client_def.rs @@ -1,7 +1,6 @@ use crate::ics02_client::client_consensus::AnyConsensusState; use crate::ics02_client::client_def::ClientDef; use crate::ics02_client::client_state::AnyClientState; -use crate::ics02_client::header::Header as ICS2Header; use crate::ics03_connection::connection::ConnectionEnd; use crate::ics04_channel::channel::ChannelEnd; use crate::ics04_channel::packet::Sequence; diff --git a/modules/src/ics07_tendermint/client_state.rs b/modules/src/ics07_tendermint/client_state.rs index 6a3058f3fd..f4cb801980 100644 --- a/modules/src/ics07_tendermint/client_state.rs +++ b/modules/src/ics07_tendermint/client_state.rs @@ -16,6 +16,7 @@ use crate::ics07_tendermint::header::Header; use crate::ics23_commitment::merkle::cosmos_specs; use crate::ics24_host::identifier::ChainId; use crate::Height; +use std::str::FromStr; #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct ClientState { @@ -154,14 +155,9 @@ impl TryFrom for ClientState { .clone() .ok_or_else(|| Kind::InvalidRawClientState.context("missing trusting period"))?; - let chain_id = raw - .chain_id - .clone() - .try_into() - .map_err(|e| Kind::InvalidChainId(raw.chain_id.clone(), e))?; - Ok(Self { - chain_id, + chain_id: ChainId::from_str(raw.chain_id.as_str()) + .map_err(|_| Kind::InvalidRawClientState.context("Invalid chain identifier"))?, trust_level: TrustThreshold { numerator: trust_level.numerator, denominator: trust_level.denominator, @@ -358,7 +354,6 @@ mod tests { #[cfg(any(test, feature = "mocks"))] pub mod test_util { - use std::convert::TryInto; use std::time::Duration; use tendermint::block::Header; @@ -369,11 +364,9 @@ pub mod test_util { use crate::ics24_host::identifier::ChainId; pub fn get_dummy_tendermint_client_state(tm_header: Header) -> AnyClientState { - let chain_id: ChainId = tm_header.chain_id.clone().try_into().unwrap(); - AnyClientState::Tendermint( ClientState::new( - chain_id, + ChainId::from(tm_header.chain_id.clone()), Default::default(), Duration::from_secs(64000), Duration::from_secs(128000), diff --git a/modules/src/ics07_tendermint/error.rs b/modules/src/ics07_tendermint/error.rs index 63620dd96b..482c4f4ae2 100644 --- a/modules/src/ics07_tendermint/error.rs +++ b/modules/src/ics07_tendermint/error.rs @@ -36,6 +36,9 @@ pub enum Kind { #[error("invalid raw header")] InvalidRawHeader, + + #[error("invalid raw misbehaviour")] + InvalidRawMisbehaviour, } impl Kind { diff --git a/modules/src/ics07_tendermint/header.rs b/modules/src/ics07_tendermint/header.rs index 04cda093c6..eb0eac2654 100644 --- a/modules/src/ics07_tendermint/header.rs +++ b/modules/src/ics07_tendermint/header.rs @@ -1,7 +1,9 @@ use std::convert::{TryFrom, TryInto}; +use serde_derive::{Deserialize, Serialize}; use tendermint::block::signed_header::SignedHeader; use tendermint::validator::Set as ValidatorSet; +use tendermint::Time; use tendermint_proto::Protobuf; use ibc_proto::ibc::lightclients::tendermint::v1::Header as RawHeader; @@ -11,9 +13,10 @@ use crate::ics02_client::header::AnyHeader; use crate::ics07_tendermint::error::{Error, Kind}; use crate::ics24_host::identifier::ChainId; use crate::Height; +use std::cmp::Ordering; /// Tendermint consensus header -#[derive(Clone, Debug, PartialEq)] // TODO: Add Eq bound once present in tendermint-rs +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] // TODO: Add Eq bound once present in tendermint-rs pub struct Header { pub signed_header: SignedHeader, // contains the commitment root pub validator_set: ValidatorSet, // the validator set that signed Header @@ -21,16 +24,45 @@ pub struct Header { pub trusted_validator_set: ValidatorSet, // the last trusted validator set at trusted height } +impl Header { + pub fn height(&self) -> Height { + Height::new( + ChainId::chain_version(self.signed_header.header.chain_id.as_str()), + u64::from(self.signed_header.header.height), + ) + } + pub fn time(&self) -> Time { + self.signed_header.header.time + } + + pub fn compatible_with(&self, other_header: &Header) -> bool { + let ibc_client_height = other_header.signed_header.header.height; + let self_header_height = self.signed_header.header.height; + + match self_header_height.cmp(&&ibc_client_height) { + Ordering::Equal => { + // 1 - fork + self.signed_header.commit.block_id == other_header.signed_header.commit.block_id + } + Ordering::Greater => { + // 2 - BFT time violation + self.signed_header.header.time > other_header.signed_header.header.time + } + Ordering::Less => { + // 3 - BFT time violation + self.signed_header.header.time < other_header.signed_header.header.time + } + } + } +} + impl crate::ics02_client::header::Header for Header { fn client_type(&self) -> ClientType { ClientType::Tendermint } fn height(&self) -> Height { - Height::new( - ChainId::chain_version(self.signed_header.header.chain_id.as_str()), - u64::from(self.signed_header.header.height), - ) + self.height() } fn wrap_any(self) -> AnyHeader { diff --git a/modules/src/ics07_tendermint/misbehaviour.rs b/modules/src/ics07_tendermint/misbehaviour.rs new file mode 100644 index 0000000000..8061c082df --- /dev/null +++ b/modules/src/ics07_tendermint/misbehaviour.rs @@ -0,0 +1,76 @@ +use std::convert::{TryFrom, TryInto}; + +use tendermint_proto::Protobuf; + +use ibc_proto::ibc::lightclients::tendermint::v1::Misbehaviour as RawMisbehaviour; + +use crate::ics02_client::misbehaviour::AnyMisbehaviour; +use crate::ics07_tendermint::error::{Error, Kind}; +use crate::ics07_tendermint::header::Header; +use crate::ics24_host::identifier::ClientId; +use crate::Height; + +#[derive(Clone, Debug, PartialEq)] +pub struct Misbehaviour { + pub client_id: ClientId, + pub header1: Header, + pub header2: Header, +} + +impl crate::ics02_client::misbehaviour::Misbehaviour for Misbehaviour { + fn client_id(&self) -> &ClientId { + &self.client_id + } + + fn height(&self) -> Height { + self.header1.height() + } + + fn wrap_any(self) -> AnyMisbehaviour { + AnyMisbehaviour::Tendermint(self) + } +} + +impl Protobuf for Misbehaviour {} + +impl TryFrom for Misbehaviour { + type Error = Error; + + fn try_from(raw: RawMisbehaviour) -> Result { + Ok(Self { + client_id: Default::default(), + header1: raw + .header_1 + .ok_or_else(|| Kind::InvalidRawMisbehaviour.context("missing header1"))? + .try_into()?, + header2: raw + .header_2 + .ok_or_else(|| Kind::InvalidRawMisbehaviour.context("missing header2"))? + .try_into()?, + }) + } +} + +impl From for RawMisbehaviour { + fn from(value: Misbehaviour) -> Self { + RawMisbehaviour { + client_id: value.client_id.to_string(), + header_1: Some(value.header1.into()), + header_2: Some(value.header2.into()), + } + } +} + +impl std::fmt::Display for Misbehaviour { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!( + f, + "{:?} h1: {:?}-{:?} h2: {:?}-{:?}", + self.client_id, + self.header1.height(), + self.header1.trusted_height, + self.header2.height(), + self.header2.trusted_height, + ) + } +} diff --git a/modules/src/ics07_tendermint/mod.rs b/modules/src/ics07_tendermint/mod.rs index 0c5a0a311c..0c4263997f 100644 --- a/modules/src/ics07_tendermint/mod.rs +++ b/modules/src/ics07_tendermint/mod.rs @@ -5,3 +5,4 @@ pub mod client_state; pub mod consensus_state; pub mod error; pub mod header; +pub mod misbehaviour; diff --git a/modules/src/ics18_relayer/context.rs b/modules/src/ics18_relayer/context.rs index fd552145f2..38571922b6 100644 --- a/modules/src/ics18_relayer/context.rs +++ b/modules/src/ics18_relayer/context.rs @@ -1,11 +1,13 @@ use prost_types::Any; +use crate::events::IbcEvent; use crate::ics02_client::client_state::AnyClientState; use crate::ics02_client::header::AnyHeader; + use crate::ics18_relayer::error::Error; use crate::ics24_host::identifier::ClientId; +use crate::signer::Signer; use crate::Height; -use crate::{events::IbcEvent, signer::Signer}; /// Trait capturing all dependencies (i.e., the context) which algorithms in ICS18 require to /// relay packets between chains. This trait comprises the dependencies towards a single chain. diff --git a/modules/src/lib.rs b/modules/src/lib.rs index 4c5072defb..da2b838fa5 100644 --- a/modules/src/lib.rs +++ b/modules/src/lib.rs @@ -1,5 +1,6 @@ #![forbid(unsafe_code)] #![deny(clippy::all)] +#![allow(clippy::large_enum_variant)] #![deny( warnings, // missing_docs, @@ -30,6 +31,7 @@ pub mod handler; pub mod keys; pub mod macros; pub mod proofs; +pub mod query; pub mod signer; pub mod tx_msg; diff --git a/modules/src/mock/context.rs b/modules/src/mock/context.rs index 71c74052b3..a75cc09126 100644 --- a/modules/src/mock/context.rs +++ b/modules/src/mock/context.rs @@ -9,7 +9,7 @@ use sha2::Digest; use crate::application::ics20_fungible_token_transfer::context::Ics20Context; use crate::events::IbcEvent; -use crate::ics02_client::client_consensus::AnyConsensusState; +use crate::ics02_client::client_consensus::{AnyConsensusState, AnyConsensusStateWithHeight}; use crate::ics02_client::client_state::AnyClientState; use crate::ics02_client::client_type::ClientType; use crate::ics02_client::context::{ClientKeeper, ClientReader}; @@ -418,6 +418,17 @@ impl MockContext { pub fn add_port(&mut self, port_id: PortId) { self.port_capabilities.insert(port_id, Capability::new()); } + + pub fn consensus_states(&self, client_id: &ClientId) -> Vec { + self.clients[client_id] + .consensus_states + .iter() + .map(|(k, v)| AnyConsensusStateWithHeight { + height: *k, + consensus_state: v.clone(), + }) + .collect() + } } impl Ics26Context for MockContext {} diff --git a/modules/src/mock/header.rs b/modules/src/mock/header.rs index 6b48b33a6d..e06ed4c4ec 100644 --- a/modules/src/mock/header.rs +++ b/modules/src/mock/header.rs @@ -1,6 +1,6 @@ use std::convert::{TryFrom, TryInto}; -use serde::Serialize; +use serde_derive::{Deserialize, Serialize}; use tendermint_proto::Protobuf; use ibc_proto::ibc::mock::Header as RawMockHeader; @@ -13,7 +13,7 @@ use crate::ics02_client::header::Header; use crate::mock::client_state::MockConsensusState; use crate::Height; -#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)] +#[derive(Copy, Clone, Default, Debug, Deserialize, PartialEq, Eq, Serialize)] pub struct MockHeader { pub height: Height, pub timestamp: u64, diff --git a/modules/src/mock/misbehaviour.rs b/modules/src/mock/misbehaviour.rs new file mode 100644 index 0000000000..edc8e5f6e2 --- /dev/null +++ b/modules/src/mock/misbehaviour.rs @@ -0,0 +1,62 @@ +use std::convert::{TryFrom, TryInto}; + +use tendermint_proto::Protobuf; + +use ibc_proto::ibc::mock::Misbehaviour as RawMisbehaviour; + +use crate::ics02_client::error::{self, Error}; +use crate::ics02_client::misbehaviour::AnyMisbehaviour; +use crate::ics24_host::identifier::ClientId; +use crate::mock::header::MockHeader; +use crate::Height; + +#[derive(Clone, Debug, PartialEq)] +pub struct Misbehaviour { + pub client_id: ClientId, + pub header1: MockHeader, + pub header2: MockHeader, +} + +impl crate::ics02_client::misbehaviour::Misbehaviour for Misbehaviour { + fn client_id(&self) -> &ClientId { + &self.client_id + } + + fn height(&self) -> Height { + self.header1.height() + } + + fn wrap_any(self) -> AnyMisbehaviour { + AnyMisbehaviour::Mock(self) + } +} + +impl Protobuf for Misbehaviour {} + +impl TryFrom for Misbehaviour { + type Error = Error; + + fn try_from(raw: RawMisbehaviour) -> Result { + Ok(Self { + client_id: Default::default(), + header1: raw + .header1 + .ok_or_else(|| error::Kind::InvalidRawMisbehaviour.context("missing header1"))? + .try_into()?, + header2: raw + .header2 + .ok_or_else(|| error::Kind::InvalidRawMisbehaviour.context("missing header2"))? + .try_into()?, + }) + } +} + +impl From for RawMisbehaviour { + fn from(value: Misbehaviour) -> Self { + RawMisbehaviour { + client_id: value.client_id.to_string(), + header1: Some(value.header1.into()), + header2: Some(value.header2.into()), + } + } +} diff --git a/modules/src/mock/mod.rs b/modules/src/mock/mod.rs index 8c139015bc..54aa60cde3 100644 --- a/modules/src/mock/mod.rs +++ b/modules/src/mock/mod.rs @@ -5,3 +5,4 @@ pub mod client_state; pub mod context; pub mod header; pub mod host; +pub mod misbehaviour; diff --git a/modules/src/query.rs b/modules/src/query.rs new file mode 100644 index 0000000000..b2923c98bf --- /dev/null +++ b/modules/src/query.rs @@ -0,0 +1,9 @@ +use crate::ics02_client::client_consensus::QueryClientEventRequest; +use crate::ics04_channel::channel::QueryPacketEventDataRequest; + +/// Used for queries and not yet standardized in channel's query.proto +#[derive(Clone, Debug)] +pub enum QueryTxRequest { + Packet(QueryPacketEventDataRequest), + Client(QueryClientEventRequest), +} diff --git a/proto/definitions/mock/ibc.mock.proto b/proto/definitions/mock/ibc.mock.proto index 70f50ca15e..d8a2fe5946 100644 --- a/proto/definitions/mock/ibc.mock.proto +++ b/proto/definitions/mock/ibc.mock.proto @@ -15,3 +15,9 @@ message ClientState { message ConsensusState { Header header = 1; } + +message Misbehaviour { + string client_id = 1; + Header header1 = 2; + Header header2 = 3; +} diff --git a/proto/src/lib.rs b/proto/src/lib.rs index 43a8f37742..4178b8844d 100644 --- a/proto/src/lib.rs +++ b/proto/src/lib.rs @@ -38,6 +38,17 @@ pub mod cosmos { pub mod v1beta1 { include!("prost/cosmos.base.query.v1beta1.rs"); } + + pub mod pagination { + use super::v1beta1::PageRequest; + + pub fn all() -> Option { + Some(PageRequest { + limit: u64::MAX, + ..Default::default() + }) + } + } } pub mod reflection { pub mod v1beta1 { diff --git a/proto/src/prost/ibc.mock.rs b/proto/src/prost/ibc.mock.rs index b3ae08f3d9..28da0c903b 100644 --- a/proto/src/prost/ibc.mock.rs +++ b/proto/src/prost/ibc.mock.rs @@ -15,3 +15,12 @@ pub struct ConsensusState { #[prost(message, optional, tag="1")] pub header: ::core::option::Option
, } +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Misbehaviour { + #[prost(string, tag="1")] + pub client_id: ::prost::alloc::string::String, + #[prost(message, optional, tag="2")] + pub header1: ::core::option::Option
, + #[prost(message, optional, tag="3")] + pub header2: ::core::option::Option
, +} diff --git a/relayer-cli/src/commands.rs b/relayer-cli/src/commands.rs index d9f495633a..dc74e66508 100644 --- a/relayer-cli/src/commands.rs +++ b/relayer-cli/src/commands.rs @@ -15,11 +15,13 @@ use self::{ create::CreateCmds, keys::KeysCmd, listen::ListenCmd, query::QueryCmd, start::StartCmd, start_multi::StartMultiCmd, tx::TxCmd, update::UpdateCmds, version::VersionCmd, }; +use crate::commands::misbehaviour::MisbehaviourCmd; mod config; mod create; mod keys; mod listen; +mod misbehaviour; mod query; mod start; mod start_multi; @@ -79,6 +81,10 @@ pub enum CliCmd { #[options(help = "Listen to and display IBC events emitted by a chain")] Listen(ListenCmd), + /// The `misbehaviour` subcommand + #[options(help = "Listen to client update IBC events and handles misbehaviour")] + Misbehaviour(MisbehaviourCmd), + /// The `version` subcommand #[options(help = "Display version information")] Version(VersionCmd), diff --git a/relayer-cli/src/commands/misbehaviour.rs b/relayer-cli/src/commands/misbehaviour.rs new file mode 100644 index 0000000000..f31d79102e --- /dev/null +++ b/relayer-cli/src/commands/misbehaviour.rs @@ -0,0 +1,129 @@ +use abscissa_core::{config, error::BoxError, Command, Options, Runnable}; +use ibc::events::IbcEvent; +use ibc::ics02_client::events::UpdateClient; +use ibc::ics02_client::height::Height; +use ibc::ics24_host::identifier::{ChainId, ClientId}; +use ibc_relayer::chain::handle::ChainHandle; +use ibc_relayer::foreign_client::ForeignClient; + +use crate::application::CliApp; +use crate::cli_utils::spawn_chain_runtime; +use crate::conclude::Output; +use crate::prelude::*; +use ibc::ics02_client::client_state::ClientState; + +#[derive(Clone, Command, Debug, Options)] +pub struct MisbehaviourCmd { + #[options( + free, + required, + help = "identifier of the chain where client updates are monitored for misbehaviour" + )] + chain_id: ChainId, + + #[options( + free, + required, + help = "identifier of the client to be monitored for misbehaviour" + )] + client_id: ClientId, +} + +impl Runnable for MisbehaviourCmd { + fn run(&self) { + let config = app_config(); + + let res = monitor_misbehaviour(&self.chain_id, &self.client_id, &config); + match res { + Ok(()) => Output::success(()).exit(), + Err(e) => Output::error(format!("{}", e)).exit(), + } + } +} + +pub fn monitor_misbehaviour( + chain_id: &ChainId, + client_id: &ClientId, + config: &config::Reader, +) -> Result<(), BoxError> { + let chain = spawn_chain_runtime(&config, chain_id) + .map_err(|e| format!("could not spawn the chain runtime for {}", chain_id))?; + + let subscription = chain.subscribe()?; + + // check previous updates that may have been missed + misbehaviour_handling(chain.clone(), config, client_id, None)?; + + // process update client events + while let Ok(event_batch) = subscription.recv() { + for event in event_batch.events.iter() { + match event { + IbcEvent::UpdateClient(update) => { + debug!("{:?}", update); + misbehaviour_handling( + chain.clone(), + config, + update.client_id(), + Some(update.clone()), + )?; + } + + IbcEvent::CreateClient(create) => { + // TODO - get header from full node, consensus state from chain, compare + } + + IbcEvent::ClientMisbehaviour(misbehaviour) => { + // TODO - submit misbehaviour to the witnesses (our full node) + } + + _ => {} + } + } + } + + Ok(()) +} + +fn misbehaviour_handling( + chain: Box, + config: &config::Reader, + client_id: &ClientId, + update: Option, +) -> Result<(), BoxError> { + let client_state = chain + .query_client_state(client_id, Height::zero()) + .map_err(|e| format!("could not query client state for {}", client_id))?; + + if client_state.is_frozen() { + // nothing to do + return Ok(()); + } + let counterparty_chain = + spawn_chain_runtime(&config, &client_state.chain_id()).map_err(|e| { + format!( + "could not spawn the chain runtime for {}", + client_state.chain_id() + ) + })?; + + let client = + ForeignClient::restore_client(chain.clone(), counterparty_chain.clone(), client_id); + + let misbehaviour_detection_result = client + .detect_misbehaviour_and_send_evidence(update) + .map_err(|e| { + format!( + "could not run misbehaviour detection for {}: {}", + client_id, e + ) + })?; + + if !misbehaviour_detection_result.is_empty() { + info!( + "evidence submission result {:?}", + misbehaviour_detection_result + ); + } + + Ok(()) +} diff --git a/relayer-cli/src/commands/query.rs b/relayer-cli/src/commands/query.rs index 85f56006bf..8df32359ea 100644 --- a/relayer-cli/src/commands/query.rs +++ b/relayer-cli/src/commands/query.rs @@ -1,8 +1,9 @@ //! `query` subcommand -use crate::commands::query::channels::QueryChannelsCmd; use abscissa_core::{Command, Options, Runnable}; +use crate::commands::query::channels::QueryChannelsCmd; + mod channel; mod channels; mod client; @@ -52,6 +53,10 @@ pub enum QueryClientCmds { #[options(help = "Query client consensus state")] Consensus(client::QueryClientConsensusCmd), + /// The `query client header` subcommand + #[options(help = "Query client header")] + Header(client::QueryClientHeaderCmd), + /// The `query client connections` subcommand #[options(help = "Query client connections")] Connections(client::QueryClientConnectionsCmd), diff --git a/relayer-cli/src/commands/query/client.rs b/relayer-cli/src/commands/query/client.rs index be6deb6da2..4344390b26 100644 --- a/relayer-cli/src/commands/query/client.rs +++ b/relayer-cli/src/commands/query/client.rs @@ -4,8 +4,14 @@ use abscissa_core::{Command, Options, Runnable}; use tokio::runtime::Runtime as TokioRuntime; use tracing::info; +use ibc::events::IbcEventType; +use ibc::ics02_client::client_consensus::QueryClientEventRequest; +use ibc::ics02_client::client_state::ClientState; use ibc::ics24_host::identifier::ChainId; use ibc::ics24_host::identifier::ClientId; +use ibc::query::QueryTxRequest; +use ibc::Height; +use ibc_proto::ibc::core::client::v1::QueryConsensusStatesRequest; use ibc_proto::ibc::core::connection::v1::QueryClientConnectionsRequest; use ibc_relayer::chain::Chain; use ibc_relayer::chain::CosmosSdkChain; @@ -22,7 +28,7 @@ pub struct QueryClientStateCmd { #[options(free, required, help = "identifier of the client to query")] client_id: ClientId, - #[options(help = "the chain height which this query should reflect", short = "h")] + #[options(help = "the chain height context for the query", short = "h")] height: Option, } @@ -63,26 +69,21 @@ pub struct QueryClientConsensusCmd { #[options(free, required, help = "identifier of the client to query")] client_id: ClientId, - #[options( - free, - required, - help = "revision number of the client's consensus state to query" - )] - consensus_rev_number: u64, + #[options(help = "height of the client's consensus state to query", short = "c")] + consensus_height: Option, + + #[options(help = "show only consensus heights", short = "s")] + heights_only: bool, #[options( - free, - required, - help = "height (revision height) of the client's consensus state to query" + help = "the chain height context to be used, applicable only to a specific height", + short = "h" )] - consensus_rev_height: u64, - - #[options(help = "the chain height which this query should reflect", short = "h")] height: Option, } /// Implementation of the query for a client's consensus state at a certain height. -/// hermes query client consensus ibc-0 07-tendermint-0 22 +/// hermes query client consensus ibc-0 07-tendermint-0 -c 22 impl Runnable for QueryClientConsensusCmd { fn run(&self) { let config = app_config(); @@ -102,12 +103,112 @@ impl Runnable for QueryClientConsensusCmd { let rt = Arc::new(TokioRuntime::new().unwrap()); let chain = CosmosSdkChain::bootstrap(chain_config.clone(), rt).unwrap(); + + let counterparty_chain = match chain.query_client_state(&self.client_id, Height::zero()) { + Ok(cs) => cs.chain_id(), + Err(e) => { + return Output::error(format!( + "Failed while querying client '{}' on chain '{}' with error: {}", + self.client_id, self.chain_id, e + )) + .exit() + } + }; + + match self.consensus_height { + Some(cs_height) => { + let consensus_height = ibc::Height::new(counterparty_chain.version(), cs_height); + let height = ibc::Height::new(chain.id().version(), self.height.unwrap_or(0_u64)); + let res = chain.proven_client_consensus(&self.client_id, consensus_height, height); + match res { + Ok((cs, _)) => Output::success(cs).exit(), + Err(e) => Output::error(format!("{}", e)).exit(), + } + } + None => { + let res = chain.query_consensus_states(QueryConsensusStatesRequest { + client_id: self.client_id.to_string(), + pagination: ibc_proto::cosmos::base::query::pagination::all(), + }); + + match res { + Ok(states) => { + if self.heights_only { + let heights: Vec = states.iter().map(|cs| cs.height).collect(); + Output::success(heights).exit() + } else { + Output::success(states).exit() + } + } + Err(e) => Output::error(format!("{}", e)).exit(), + } + } + } + } +} + +/// Query client header command +#[derive(Clone, Command, Debug, Options)] +pub struct QueryClientHeaderCmd { + #[options(free, required, help = "identifier of the chain to query")] + chain_id: ChainId, + + #[options(free, required, help = "identifier of the client to query")] + client_id: ClientId, + + #[options(free, required, help = "height of header to query")] + consensus_height: u64, + + #[options(help = "the chain height context for the query", short = "h")] + height: Option, +} + +/// Implementation of the query for the header used in a client update at a certain height. +/// hermes query client header ibc-0 07-tendermint-0 22 +impl Runnable for QueryClientHeaderCmd { + fn run(&self) { + let config = app_config(); + + let chain_config = match config.find_chain(&self.chain_id) { + None => { + return Output::error(format!( + "chain '{}' not found in configuration file", + self.chain_id + )) + .exit() + } + Some(chain_config) => chain_config, + }; + + info!("Options {:?}", self); + + let rt = Arc::new(TokioRuntime::new().unwrap()); + let chain = CosmosSdkChain::bootstrap(chain_config.clone(), rt).unwrap(); + + let counterparty_chain = match chain.query_client_state(&self.client_id, Height::zero()) { + Ok(cs) => cs.chain_id(), + Err(e) => { + return Output::error(format!( + "Failed while querying client '{}' on chain '{}' with error: {}", + self.client_id, self.chain_id, e + )) + .exit() + } + }; + let consensus_height = - ibc::Height::new(self.consensus_rev_number, self.consensus_rev_height); + ibc::Height::new(counterparty_chain.version(), self.consensus_height); let height = ibc::Height::new(chain.id().version(), self.height.unwrap_or(0_u64)); - match chain.proven_client_consensus(&self.client_id, consensus_height, height) { - Ok((cs, _)) => Output::success(cs).exit(), + let res = chain.query_txs(QueryTxRequest::Client(QueryClientEventRequest { + height, + event_id: IbcEventType::UpdateClient, + client_id: self.client_id.clone(), + consensus_height, + })); + + match res { + Ok(header) => Output::success(header).exit(), Err(e) => Output::error(format!("{}", e)).exit(), } } diff --git a/relayer-cli/src/commands/query/clients.rs b/relayer-cli/src/commands/query/clients.rs index 9af7a1f29f..39aaf949d9 100644 --- a/relayer-cli/src/commands/query/clients.rs +++ b/relayer-cli/src/commands/query/clients.rs @@ -40,7 +40,9 @@ impl Runnable for QueryAllClientsCmd { let rt = Arc::new(TokioRuntime::new().unwrap()); let chain = CosmosSdkChain::bootstrap(chain_config.clone(), rt).unwrap(); - let req = QueryClientStatesRequest { pagination: None }; + let req = QueryClientStatesRequest { + pagination: ibc_proto::cosmos::base::query::pagination::all(), + }; let res: Result<_, Error> = chain .query_clients(req) diff --git a/relayer-cli/src/commands/query/connection.rs b/relayer-cli/src/commands/query/connection.rs index 8099265f20..74905e4975 100644 --- a/relayer-cli/src/commands/query/connection.rs +++ b/relayer-cli/src/commands/query/connection.rs @@ -93,7 +93,7 @@ impl Runnable for QueryConnectionChannelsCmd { let req = QueryConnectionChannelsRequest { connection: self.connection_id.to_string(), - pagination: None, + pagination: ibc_proto::cosmos::base::query::pagination::all(), }; let res: Result<_, Error> = chain diff --git a/relayer-cli/src/commands/query/connections.rs b/relayer-cli/src/commands/query/connections.rs index f4ebd54936..e9a8e71e8a 100644 --- a/relayer-cli/src/commands/query/connections.rs +++ b/relayer-cli/src/commands/query/connections.rs @@ -36,7 +36,9 @@ impl Runnable for QueryConnectionsCmd { let rt = Arc::new(TokioRuntime::new().unwrap()); let chain = CosmosSdkChain::bootstrap(chain_config.clone(), rt).unwrap(); - let req = QueryConnectionsRequest { pagination: None }; + let req = QueryConnectionsRequest { + pagination: ibc_proto::cosmos::base::query::pagination::all(), + }; let res = chain.query_connections(req); diff --git a/relayer-cli/src/commands/query/packet.rs b/relayer-cli/src/commands/query/packet.rs index 31014f5aad..2342e1d538 100644 --- a/relayer-cli/src/commands/query/packet.rs +++ b/relayer-cli/src/commands/query/packet.rs @@ -68,7 +68,7 @@ impl Runnable for QueryPacketCommitmentsCmd { let grpc_request = QueryPacketCommitmentsRequest { port_id: opts.port_id.to_string(), channel_id: opts.channel_id.to_string(), - pagination: None, + pagination: ibc_proto::cosmos::base::query::pagination::all(), }; let res: Result<(Vec, Height), Error> = chain @@ -266,7 +266,7 @@ impl Runnable for QueryUnreceivedPacketsCmd { let commitments_request = QueryPacketCommitmentsRequest { port_id: opts.port_id.to_string(), channel_id: opts.channel_id.to_string(), - pagination: None, + pagination: ibc_proto::cosmos::base::query::pagination::all(), }; let seq_res = src_chain @@ -363,7 +363,7 @@ impl Runnable for QueryPacketAcknowledgementsCmd { let grpc_request = QueryPacketAcknowledgementsRequest { port_id: opts.port_id.to_string(), channel_id: opts.channel_id.to_string(), - pagination: None, + pagination: ibc_proto::cosmos::base::query::pagination::all(), }; let res: Result<(Vec, Height), Error> = chain @@ -561,7 +561,7 @@ impl Runnable for QueryUnreceivedAcknowledgementCmd { let acks_request = QueryPacketAcknowledgementsRequest { port_id: opts.port_id.to_string(), channel_id: opts.channel_id.to_string(), - pagination: None, + pagination: ibc_proto::cosmos::base::query::pagination::all(), }; let seq_res = src_chain diff --git a/relayer-cli/src/commands/tx/client.rs b/relayer-cli/src/commands/tx/client.rs index 8f6701a978..c3de8d402f 100644 --- a/relayer-cli/src/commands/tx/client.rs +++ b/relayer-cli/src/commands/tx/client.rs @@ -2,11 +2,12 @@ use abscissa_core::{Command, Options, Runnable}; use tracing::info; use ibc::events::IbcEvent; +use ibc::ics02_client::client_state::ClientState; use ibc::ics24_host::identifier::{ChainId, ClientId}; use ibc_relayer::foreign_client::ForeignClient; use crate::application::app_config; -use crate::cli_utils::ChainHandlePair; +use crate::cli_utils::{spawn_chain_runtime, ChainHandlePair}; use crate::conclude::{exit_with_unrecoverable_error, Output}; use crate::error::{Error, Kind}; @@ -24,7 +25,6 @@ pub struct TxCreateClientCmd { impl Runnable for TxCreateClientCmd { fn run(&self) { let config = app_config(); - let chains = match ChainHandlePair::spawn(&config, &self.src_chain_id, &self.dst_chain_id) { Ok(chains) => chains, Err(e) => return Output::error(format!("{}", e)).exit(), @@ -53,34 +53,64 @@ pub struct TxUpdateClientCmd { #[options(free, required, help = "identifier of the destination chain")] dst_chain_id: ChainId, - #[options(free, required, help = "identifier of the source chain")] - src_chain_id: ChainId, - #[options( free, required, help = "identifier of the client to be updated on destination chain" )] dst_client_id: ClientId, + + #[options(help = "the target height of the client update", short = "h")] + target_height: Option, + + #[options(help = "the trusted height of the client update", short = "t")] + trusted_height: Option, } impl Runnable for TxUpdateClientCmd { fn run(&self) { let config = app_config(); - let chains = match ChainHandlePair::spawn(&config, &self.src_chain_id, &self.dst_chain_id) { - Ok(chains) => chains, + let dst_chain = match spawn_chain_runtime(&config, &self.dst_chain_id) { + Ok(handle) => handle, Err(e) => return Output::error(format!("{}", e)).exit(), }; + let src_chain_id = + match dst_chain.query_client_state(&self.dst_client_id, ibc::Height::zero()) { + Ok(cs) => cs.chain_id(), + Err(e) => { + return Output::error(format!( + "Query of client '{}' on chain '{}' failed with error: {}", + self.dst_client_id, self.dst_chain_id, e + )) + .exit() + } + }; + + let src_chain = match spawn_chain_runtime(&config, &src_chain_id) { + Ok(handle) => handle, + Err(e) => return Output::error(format!("{}", e)).exit(), + }; + + let height = match self.target_height { + Some(height) => ibc::Height::new(src_chain.id().version(), height), + None => ibc::Height::zero(), + }; + + let trusted_height = match self.trusted_height { + Some(height) => ibc::Height::new(src_chain.id().version(), height), + None => ibc::Height::zero(), + }; + let client = ForeignClient { - dst_chain: chains.dst, - src_chain: chains.src, + dst_chain, + src_chain, id: self.dst_client_id.clone(), }; let res: Result = client - .build_update_client_and_send() + .build_update_client_and_send(height, trusted_height) .map_err(|e| Kind::Tx.context(e).into()); match res { diff --git a/relayer/src/chain.rs b/relayer/src/chain.rs index 32595ef543..74d67ba0a1 100644 --- a/relayer/src/chain.rs +++ b/relayer/src/chain.rs @@ -7,16 +7,17 @@ use tokio::runtime::Runtime as TokioRuntime; pub use cosmos::CosmosSdkChain; use ibc::events::IbcEvent; -use ibc::ics02_client::client_consensus::ConsensusState; +use ibc::ics02_client::client_consensus::{AnyConsensusStateWithHeight, ConsensusState}; use ibc::ics02_client::client_state::ClientState; use ibc::ics02_client::header::Header; use ibc::ics03_connection::connection::{ConnectionEnd, State}; use ibc::ics03_connection::version::{get_compatible_versions, Version}; -use ibc::ics04_channel::channel::{ChannelEnd, QueryPacketEventDataRequest}; +use ibc::ics04_channel::channel::ChannelEnd; use ibc::ics04_channel::packet::{PacketMsgType, Sequence}; use ibc::ics23_commitment::commitment::{CommitmentPrefix, CommitmentProofBytes}; use ibc::ics24_host::identifier::{ChainId, ChannelId, ClientId, ConnectionId, PortId}; use ibc::proofs::{ConsensusProof, Proofs}; +use ibc::query::QueryTxRequest; use ibc::signer::Signer; use ibc::Height as ICSHeight; use ibc_proto::ibc::core::channel::v1::{ @@ -24,7 +25,7 @@ use ibc_proto::ibc::core::channel::v1::{ QueryNextSequenceReceiveRequest, QueryPacketAcknowledgementsRequest, QueryPacketCommitmentsRequest, QueryUnreceivedAcksRequest, QueryUnreceivedPacketsRequest, }; -use ibc_proto::ibc::core::client::v1::QueryClientStatesRequest; +use ibc_proto::ibc::core::client::v1::{QueryClientStatesRequest, QueryConsensusStatesRequest}; use ibc_proto::ibc::core::commitment::v1::MerkleProof; use ibc_proto::ibc::core::connection::v1::{ QueryClientConnectionsRequest, QueryConnectionsRequest, @@ -128,6 +129,11 @@ pub trait Chain: Sized { height: ICSHeight, ) -> Result; + fn query_consensus_states( + &self, + request: QueryConsensusStatesRequest, + ) -> Result, Error>; + fn query_upgraded_client_state( &self, height: ICSHeight, @@ -207,7 +213,7 @@ pub trait Chain: Sized { request: QueryNextSequenceReceiveRequest, ) -> Result; - fn query_txs(&self, request: QueryPacketEventDataRequest) -> Result, Error>; + fn query_txs(&self, request: QueryTxRequest) -> Result, Error>; // Provable queries fn proven_client_state( diff --git a/relayer/src/chain/cosmos.rs b/relayer/src/chain/cosmos.rs index 8c50a11598..287e269981 100644 --- a/relayer/src/chain/cosmos.rs +++ b/relayer/src/chain/cosmos.rs @@ -9,9 +9,6 @@ use bitcoin::hashes::hex::ToHex; use crossbeam_channel as channel; use prost::Message; use prost_types::Any; -use tokio::runtime::Runtime as TokioRuntime; -use tonic::codegen::http::Uri; - use tendermint::abci::Path as TendermintABCIPath; use tendermint::account::Id as AccountId; use tendermint::block::Height; @@ -20,22 +17,30 @@ use tendermint_light_client::types::LightBlock as TMLightBlock; use tendermint_proto::Protobuf; use tendermint_rpc::query::Query; use tendermint_rpc::{endpoint::broadcast::tx_commit::Response, Client, HttpClient, Order}; +use tokio::runtime::Runtime as TokioRuntime; +use tonic::codegen::http::Uri; use ibc::downcast; use ibc::events::{from_tx_response_event, IbcEvent}; +use ibc::ics02_client::client_consensus::{ + AnyConsensusState, AnyConsensusStateWithHeight, QueryClientEventRequest, +}; +use ibc::ics02_client::client_state::AnyClientState; +use ibc::ics02_client::events as ClientEvents; use ibc::ics03_connection::connection::ConnectionEnd; use ibc::ics04_channel::channel::{ChannelEnd, QueryPacketEventDataRequest}; use ibc::ics04_channel::events as ChannelEvents; use ibc::ics04_channel::packet::{PacketMsgType, Sequence}; use ibc::ics07_tendermint::client_state::ClientState; use ibc::ics07_tendermint::consensus_state::ConsensusState as TMConsensusState; -use ibc::ics07_tendermint::header::Header as TMHeader; +use ibc::ics07_tendermint::header::Header as TmHeader; use ibc::ics23_commitment::commitment::CommitmentPrefix; use ibc::ics23_commitment::merkle::convert_tm_to_ics_merkle_proof; use ibc::ics24_host::identifier::{ChainId, ChannelId, ClientId, ConnectionId, PortId}; use ibc::ics24_host::Path::ClientConsensusState as ClientConsensusPath; use ibc::ics24_host::Path::ClientState as ClientStatePath; use ibc::ics24_host::{ClientUpgradePath, Path, IBC_QUERY_PATH, SDK_UPGRADE_QUERY_PATH}; +use ibc::query::QueryTxRequest; use ibc::signer::Signer; use ibc::Height as ICSHeight; // Support for GRPC @@ -51,7 +56,7 @@ use ibc_proto::ibc::core::channel::v1::{ QueryNextSequenceReceiveRequest, QueryPacketAcknowledgementsRequest, QueryPacketCommitmentsRequest, QueryUnreceivedAcksRequest, QueryUnreceivedPacketsRequest, }; -use ibc_proto::ibc::core::client::v1::QueryClientStatesRequest; +use ibc_proto::ibc::core::client::v1::{QueryClientStatesRequest, QueryConsensusStatesRequest}; use ibc_proto::ibc::core::commitment::v1::MerkleProof; use ibc_proto::ibc::core::connection::v1::{ QueryClientConnectionsRequest, QueryConnectionsRequest, @@ -66,8 +71,6 @@ use crate::light_client::tendermint::LightClient as TmLightClient; use crate::light_client::LightClient; use super::Chain; -use ibc::ics02_client::client_consensus::AnyConsensusState; -use ibc::ics02_client::client_state::AnyClientState; // TODO size this properly const DEFAULT_MAX_GAS: u64 = 300000; @@ -311,7 +314,7 @@ impl CosmosSdkChain { impl Chain for CosmosSdkChain { type LightBlock = TMLightBlock; - type Header = TMHeader; + type Header = TmHeader; type ConsensusState = TMConsensusState; type ClientState = ClientState; @@ -616,6 +619,37 @@ impl Chain for CosmosSdkChain { Ok((tm_consensus_state, proof)) } + /// Performs a query to retrieve the identifiers of all connections. + fn query_consensus_states( + &self, + request: QueryConsensusStatesRequest, + ) -> Result, Error> { + crate::time!("query_chain_clients"); + + let mut client = self + .block_on( + ibc_proto::ibc::core::client::v1::query_client::QueryClient::connect( + self.grpc_addr.clone(), + ), + ) + .map_err(|e| Kind::Grpc.context(e))?; + + let request = tonic::Request::new(request); + let response = self + .block_on(client.consensus_states(request)) + .map_err(|e| Kind::Grpc.context(e))? + .into_inner(); + + let mut consensus_states: Vec = response + .consensus_states + .into_iter() + .filter_map(|cs| TryFrom::try_from(cs).ok()) + .collect(); + consensus_states.sort_by(|a, b| a.height.cmp(&b.height)); + consensus_states.reverse(); + Ok(consensus_states) + } + /// Performs a query to retrieve the identifiers of all connections. fn query_client_connections( &self, @@ -920,28 +954,56 @@ impl Chain for CosmosSdkChain { /// string attributes (sequence is emmitted as a string). /// Therefore, here we perform one tx_search for each query. Alternatively, a single query /// for all packets could be performed but it would return all packets ever sent. - fn query_txs(&self, request: QueryPacketEventDataRequest) -> Result, Error> { + fn query_txs(&self, request: QueryTxRequest) -> Result, Error> { crate::time!("query_txs"); - let mut result: Vec = vec![]; - - for seq in request.sequences.iter() { - // query all Tx-es that include events related to packet with given port, channel and sequence - let response = self - .block_on(self.rpc_client.tx_search( - packet_query(&request, seq), - false, - 1, - 1, - Order::Ascending, - )) - .unwrap(); // todo - - let mut events = packet_from_tx_search_response(self.id(), &request, *seq, response) - .map_or(vec![], |v| vec![v]); - result.append(&mut events); + match request { + QueryTxRequest::Packet(request) => { + crate::time!("query_txs"); + + let mut result: Vec = vec![]; + + for seq in &request.sequences { + // query all Tx-es that include events related to packet with given port, channel and sequence + let response = self + .block_on(self.rpc_client.tx_search( + packet_query(&request, *seq), + false, + 1, + 1, + Order::Ascending, + )) + .unwrap(); // todo + + let mut events = + packet_from_tx_search_response(self.id(), &request, *seq, response) + .map_or(vec![], |v| vec![v]); + + result.append(&mut events); + } + Ok(result) + } + + QueryTxRequest::Client(request) => { + let response = self + .block_on(self.rpc_client.tx_search( + header_query(&request), + false, + 1, + 1, + Order::Ascending, + )) + .unwrap(); // todo + + if response.txs.is_empty() { + return Ok(vec![]); + } + + let events = update_client_from_tx_search_response(self.id(), &request, response); + + Ok(events) + } } - Ok(result) } fn proven_client_state( @@ -1131,7 +1193,7 @@ impl Chain for CosmosSdkChain { ) -> Result { crate::time!("build_header"); - Ok(TMHeader { + Ok(TmHeader { trusted_height, signed_header: target_light_block.signed_header.clone(), validator_set: target_light_block.validators, @@ -1140,7 +1202,7 @@ impl Chain for CosmosSdkChain { } } -fn packet_query(request: &QueryPacketEventDataRequest, seq: &Sequence) -> Query { +fn packet_query(request: &QueryPacketEventDataRequest, seq: Sequence) -> Query { tendermint_rpc::query::Query::eq( format!("{}.packet_src_channel", request.event_id.as_str()), request.source_channel_id.to_string(), @@ -1163,6 +1225,20 @@ fn packet_query(request: &QueryPacketEventDataRequest, seq: &Sequence) -> Query ) } +fn header_query(request: &QueryClientEventRequest) -> Query { + tendermint_rpc::query::Query::eq( + format!("{}.client_id", request.event_id.as_str()), + request.client_id.to_string(), + ) + .and_eq( + format!("{}.consensus_height", request.event_id.as_str()), + format!( + "{}-{}", + request.consensus_height.revision_number, request.consensus_height.revision_height + ), + ) +} + // Extract the packet events from the query_txs RPC response. For any given // packet query, there is at most one Tx matching such query. Moreover, a Tx may // contain several events, but a single one must match the packet query. @@ -1231,6 +1307,54 @@ fn packet_from_tx_search_response( } } +// Extract all update client events for the requested client and height from the query_txs RPC response. +fn update_client_from_tx_search_response( + chain_id: &ChainId, + request: &QueryClientEventRequest, + response: tendermint_rpc::endpoint::tx_search::Response, +) -> Vec { + crate::time!("update_client_from_tx_search_response"); + + let mut matching = Vec::new(); + + for r in response.txs { + let height = ICSHeight::new(chain_id.version(), u64::from(r.height)); + if request.height != ICSHeight::zero() && height > request.height { + return vec![]; + } + + for e in r.tx_result.events { + if e.type_str != request.event_id.as_str() { + continue; + } + + let res = ClientEvents::try_from_tx(&e); + if res.is_none() { + continue; + } + let event = res.unwrap(); + let update = match &event { + IbcEvent::UpdateClient(update) => Some(update), + _ => None, + }; + + if update.is_none() { + continue; + } + + let update = update.unwrap(); + if update.common.client_id != request.client_id + || update.common.consensus_height != request.consensus_height + { + continue; + } + + matching.push(event); + } + } + matching +} + /// Perform a generic `abci_query`, and return the corresponding deserialized response data. async fn abci_query( chain: &CosmosSdkChain, diff --git a/relayer/src/chain/handle.rs b/relayer/src/chain/handle.rs index 0fba1f3fbd..3043c2441a 100644 --- a/relayer/src/chain/handle.rs +++ b/relayer/src/chain/handle.rs @@ -5,12 +5,16 @@ use crossbeam_channel as channel; use dyn_clone::DynClone; use serde::{Serialize, Serializer}; +use ibc::ics02_client::client_consensus::{AnyConsensusState, AnyConsensusStateWithHeight}; +use ibc::ics02_client::client_state::AnyClientState; +use ibc::ics02_client::events::UpdateClient; +use ibc::ics02_client::misbehaviour::AnyMisbehaviour; use ibc::{ events::IbcEvent, ics02_client::header::AnyHeader, ics03_connection::{connection::ConnectionEnd, version::Version}, ics04_channel::{ - channel::{ChannelEnd, QueryPacketEventDataRequest}, + channel::ChannelEnd, packet::{PacketMsgType, Sequence}, }, ics23_commitment::commitment::CommitmentPrefix, @@ -19,22 +23,21 @@ use ibc::{ signer::Signer, Height, }; - use ibc_proto::ibc::core::channel::v1::{ PacketState, QueryNextSequenceReceiveRequest, QueryPacketAcknowledgementsRequest, QueryPacketCommitmentsRequest, QueryUnreceivedAcksRequest, QueryUnreceivedPacketsRequest, }; - +use ibc_proto::ibc::core::client::v1::QueryClientStatesRequest; +use ibc_proto::ibc::core::client::v1::QueryConsensusStatesRequest; use ibc_proto::ibc::core::commitment::v1::MerkleProof; +pub use prod::ProdChainHandle; use crate::connection::ConnectionMsgType; use crate::keyring::store::KeyEntry; use crate::{error::Error, event::monitor::EventBatch}; +use ibc::query::QueryTxRequest; mod prod; -use ibc::ics02_client::client_consensus::AnyConsensusState; -use ibc::ics02_client::client_state::AnyClientState; -pub use prod::ProdChainHandle; pub type Subscription = channel::Receiver>; @@ -47,6 +50,7 @@ pub fn reply_channel() -> (ReplyTo, Reply) { /// Requests that a `ChainHandle` may send to a `ChainRuntime`. #[derive(Clone, Debug)] +#[allow(clippy::large_enum_variant)] pub enum ChainRequest { Terminate { reply_to: ReplyTo<()>, @@ -97,6 +101,12 @@ pub enum ChainRequest { reply_to: ReplyTo, }, + BuildMisbehaviour { + client_state: AnyClientState, + update_event: UpdateClient, + reply_to: ReplyTo>, + }, + BuildConnectionProofsAndClientState { message_type: ConnectionMsgType, connection_id: ConnectionId, @@ -105,12 +115,22 @@ pub enum ChainRequest { reply_to: ReplyTo<(Option, Proofs)>, }, + QueryClients { + request: QueryClientStatesRequest, + reply_to: ReplyTo>, + }, + QueryClientState { client_id: ClientId, height: Height, reply_to: ReplyTo, }, + QueryConsensusStates { + request: QueryConsensusStatesRequest, + reply_to: ReplyTo>, + }, + QueryUpgradedClientState { height: Height, reply_to: ReplyTo<(AnyClientState, MerkleProof)>, @@ -203,7 +223,7 @@ pub enum ChainRequest { }, QueryPacketEventData { - request: QueryPacketEventDataRequest, + request: QueryTxRequest, reply_to: ReplyTo>, }, } @@ -227,12 +247,19 @@ pub trait ChainHandle: DynClone + Send + Sync + Debug { fn query_latest_height(&self) -> Result; + fn query_clients(&self, request: QueryClientStatesRequest) -> Result, Error>; + fn query_client_state( &self, client_id: &ClientId, height: Height, ) -> Result; + fn query_consensus_states( + &self, + request: QueryConsensusStatesRequest, + ) -> Result, Error>; + fn query_upgraded_client_state( &self, height: Height, @@ -302,6 +329,12 @@ pub trait ChainHandle: DynClone + Send + Sync + Debug { client_state: AnyClientState, ) -> Result; + fn check_misbehaviour( + &self, + update: UpdateClient, + client_state: AnyClientState, + ) -> Result, Error>; + fn build_connection_proofs_and_client_state( &self, message_type: ConnectionMsgType, @@ -346,7 +379,7 @@ pub trait ChainHandle: DynClone + Send + Sync + Debug { request: QueryUnreceivedAcksRequest, ) -> Result, Error>; - fn query_txs(&self, request: QueryPacketEventDataRequest) -> Result, Error>; + fn query_txs(&self, request: QueryTxRequest) -> Result, Error>; } impl Serialize for dyn ChainHandle { diff --git a/relayer/src/chain/handle/prod.rs b/relayer/src/chain/handle/prod.rs index 881de4ff67..92c5e857e2 100644 --- a/relayer/src/chain/handle/prod.rs +++ b/relayer/src/chain/handle/prod.rs @@ -2,13 +2,18 @@ use std::fmt::Debug; use crossbeam_channel as channel; +use ibc::ics02_client::client_consensus::{AnyConsensusState, AnyConsensusStateWithHeight}; +use ibc::ics02_client::client_state::AnyClientState; +use ibc::ics02_client::events::UpdateClient; +use ibc::ics02_client::misbehaviour::AnyMisbehaviour; use ibc::ics04_channel::packet::{PacketMsgType, Sequence}; +use ibc::query::QueryTxRequest; use ibc::{ events::IbcEvent, ics02_client::header::AnyHeader, ics03_connection::connection::ConnectionEnd, ics03_connection::version::Version, - ics04_channel::channel::{ChannelEnd, QueryPacketEventDataRequest}, + ics04_channel::channel::ChannelEnd, ics23_commitment::commitment::CommitmentPrefix, ics24_host::identifier::ChainId, ics24_host::identifier::ChannelId, @@ -21,6 +26,7 @@ use ibc_proto::ibc::core::channel::v1::{ PacketState, QueryNextSequenceReceiveRequest, QueryPacketAcknowledgementsRequest, QueryPacketCommitmentsRequest, QueryUnreceivedAcksRequest, QueryUnreceivedPacketsRequest, }; +use ibc_proto::ibc::core::client::v1::{QueryClientStatesRequest, QueryConsensusStatesRequest}; use ibc_proto::ibc::core::commitment::v1::MerkleProof; use crate::{ @@ -30,8 +36,6 @@ use crate::{ }; use super::{reply_channel, ChainHandle, ChainRequest, ReplyTo, Subscription}; -use ibc::ics02_client::client_consensus::AnyConsensusState; -use ibc::ics02_client::client_state::AnyClientState; #[derive(Debug, Clone)] pub struct ProdChainHandle { @@ -101,6 +105,10 @@ impl ChainHandle for ProdChainHandle { self.send(|reply_to| ChainRequest::QueryLatestHeight { reply_to }) } + fn query_clients(&self, request: QueryClientStatesRequest) -> Result, Error> { + self.send(|reply_to| ChainRequest::QueryClients { request, reply_to }) + } + fn query_client_state( &self, client_id: &ClientId, @@ -113,6 +121,13 @@ impl ChainHandle for ProdChainHandle { }) } + fn query_consensus_states( + &self, + request: QueryConsensusStatesRequest, + ) -> Result, Error> { + self.send(|reply_to| ChainRequest::QueryConsensusStates { request, reply_to }) + } + fn query_upgraded_client_state( &self, height: Height, @@ -238,6 +253,18 @@ impl ChainHandle for ProdChainHandle { }) } + fn check_misbehaviour( + &self, + update_event: UpdateClient, + client_state: AnyClientState, + ) -> Result, Error> { + self.send(|reply_to| ChainRequest::BuildMisbehaviour { + client_state, + update_event, + reply_to, + }) + } + fn build_connection_proofs_and_client_state( &self, message_type: ConnectionMsgType, @@ -316,7 +343,7 @@ impl ChainHandle for ProdChainHandle { self.send(|reply_to| ChainRequest::QueryUnreceivedAcknowledgement { request, reply_to }) } - fn query_txs(&self, request: QueryPacketEventDataRequest) -> Result, Error> { + fn query_txs(&self, request: QueryTxRequest) -> Result, Error> { self.send(|reply_to| ChainRequest::QueryPacketEventData { request, reply_to }) } } diff --git a/relayer/src/chain/mock.rs b/relayer/src/chain/mock.rs index 7653ee290d..1f23fbcaf3 100644 --- a/relayer/src/chain/mock.rs +++ b/relayer/src/chain/mock.rs @@ -10,9 +10,10 @@ use tokio::runtime::Runtime; use ibc::downcast; use ibc::events::IbcEvent; +use ibc::ics02_client::client_consensus::AnyConsensusStateWithHeight; use ibc::ics02_client::client_state::AnyClientState; use ibc::ics03_connection::connection::ConnectionEnd; -use ibc::ics04_channel::channel::{ChannelEnd, QueryPacketEventDataRequest}; +use ibc::ics04_channel::channel::ChannelEnd; use ibc::ics04_channel::packet::{PacketMsgType, Sequence}; use ibc::ics07_tendermint::client_state::ClientState as TendermintClientState; use ibc::ics07_tendermint::consensus_state::ConsensusState as TendermintConsensusState; @@ -22,14 +23,16 @@ use ibc::ics23_commitment::commitment::CommitmentPrefix; use ibc::ics24_host::identifier::{ChainId, ChannelId, ClientId, ConnectionId, PortId}; use ibc::mock::context::MockContext; use ibc::mock::host::HostType; +use ibc::query::QueryTxRequest; use ibc::signer::Signer; +use ibc::test_utils::get_dummy_account_id; use ibc::Height; use ibc_proto::ibc::core::channel::v1::{ PacketState, QueryChannelsRequest, QueryConnectionChannelsRequest, QueryNextSequenceReceiveRequest, QueryPacketAcknowledgementsRequest, QueryPacketCommitmentsRequest, QueryUnreceivedAcksRequest, QueryUnreceivedPacketsRequest, }; -use ibc_proto::ibc::core::client::v1::QueryClientStatesRequest; +use ibc_proto::ibc::core::client::v1::{QueryClientStatesRequest, QueryConsensusStatesRequest}; use ibc_proto::ibc::core::commitment::v1::MerkleProof; use ibc_proto::ibc::core::connection::v1::{ QueryClientConnectionsRequest, QueryConnectionsRequest, @@ -41,7 +44,6 @@ use crate::error::{Error, Kind}; use crate::event::monitor::EventBatch; use crate::keyring::store::{KeyEntry, KeyRing}; use crate::light_client::{mock::LightClient as MockLightClient, LightClient}; -use ibc::test_utils::get_dummy_account_id; /// The representation of a mocked chain as the relayer sees it. /// The relayer runtime and the light client will engage with the MockChain to query/send tx; the @@ -226,7 +228,7 @@ impl Chain for MockChain { unimplemented!() } - fn query_txs(&self, _request: QueryPacketEventDataRequest) -> Result, Error> { + fn query_txs(&self, _request: QueryTxRequest) -> Result, Error> { unimplemented!() } @@ -314,6 +316,15 @@ impl Chain for MockChain { }) } + fn query_consensus_states( + &self, + request: QueryConsensusStatesRequest, + ) -> Result, Error> { + Ok(self + .context + .consensus_states(&request.client_id.parse().unwrap())) + } + fn query_upgraded_consensus_state( &self, _height: Height, diff --git a/relayer/src/chain/runtime.rs b/relayer/src/chain/runtime.rs index fe221c0018..75edcc7dd2 100644 --- a/relayer/src/chain/runtime.rs +++ b/relayer/src/chain/runtime.rs @@ -3,23 +3,28 @@ use std::{sync::Arc, thread}; use crossbeam_channel as channel; use tokio::runtime::Runtime as TokioRuntime; +use ibc::ics02_client::client_consensus::AnyConsensusStateWithHeight; +use ibc::ics02_client::events::UpdateClient; +use ibc::ics02_client::misbehaviour::AnyMisbehaviour; use ibc::{ events::IbcEvent, ics02_client::{ - client_consensus::ConsensusState, client_state::ClientState, header::AnyHeader, - header::Header, - }, - ics03_connection::{connection::ConnectionEnd, version::Version}, - ics04_channel::{ - channel::{ChannelEnd, QueryPacketEventDataRequest}, - packet::{PacketMsgType, Sequence}, + client_consensus::{AnyConsensusState, ConsensusState}, + client_state::{AnyClientState, ClientState}, + header::{AnyHeader, Header}, }, + ics03_connection::connection::ConnectionEnd, + ics03_connection::version::Version, + ics04_channel::channel::ChannelEnd, + ics04_channel::packet::{PacketMsgType, Sequence}, ics23_commitment::commitment::CommitmentPrefix, ics24_host::identifier::{ChannelId, ClientId, ConnectionId, PortId}, proofs::Proofs, + query::QueryTxRequest, signer::Signer, Height, }; +use ibc_proto::ibc::core::client::v1::{QueryClientStatesRequest, QueryConsensusStatesRequest}; use ibc_proto::ibc::core::{ channel::v1::{ PacketState, QueryNextSequenceReceiveRequest, QueryPacketAcknowledgementsRequest, @@ -41,8 +46,6 @@ use super::{ handle::{ChainHandle, ChainRequest, ProdChainHandle, ReplyTo, Subscription}, Chain, }; -use ibc::ics02_client::client_consensus::AnyConsensusState; -use ibc::ics02_client::client_state::AnyClientState; pub struct Threads { pub chain_runtime: thread::JoinHandle<()>, @@ -195,6 +198,10 @@ impl ChainRuntime { self.build_consensus_state(trusted, target, client_state, reply_to)? } + Ok(ChainRequest::BuildMisbehaviour { client_state, update_event, reply_to }) => { + self.check_misbehaviour(update_event, client_state, reply_to)? + } + Ok(ChainRequest::BuildConnectionProofsAndClientState { message_type, connection_id, client_id, height, reply_to }) => { self.build_connection_proofs_and_client_state(message_type, connection_id, client_id, height, reply_to)? }, @@ -207,10 +214,18 @@ impl ChainRuntime { self.query_latest_height(reply_to)? } + Ok(ChainRequest::QueryClients { request, reply_to }) => { + self.query_clients(request, reply_to)? + }, + Ok(ChainRequest::QueryClientState { client_id, height, reply_to }) => { self.query_client_state(client_id, height, reply_to)? }, + Ok(ChainRequest::QueryConsensusStates { request, reply_to }) => { + self.query_consensus_states(request, reply_to)? + }, + Ok(ChainRequest::QueryUpgradedClientState { height, reply_to }) => { self.query_upgraded_client_state(height, reply_to)? } @@ -423,6 +438,24 @@ impl ChainRuntime { Ok(()) } + /// Constructs AnyMisbehaviour for the update event + fn check_misbehaviour( + &mut self, + update_event: UpdateClient, + client_state: AnyClientState, + reply_to: ReplyTo>, + ) -> Result<(), Error> { + let misbehaviour = self + .light_client + .check_misbehaviour(update_event, &client_state)?; + + reply_to + .send(Ok(misbehaviour)) + .map_err(|e| Kind::Channel.context(e))?; + + Ok(()) + } + fn build_connection_proofs_and_client_state( &self, message_type: ConnectionMsgType, @@ -466,6 +499,20 @@ impl ChainRuntime { Ok(()) } + fn query_clients( + &self, + request: QueryClientStatesRequest, + reply_to: ReplyTo>, + ) -> Result<(), Error> { + let clients = self.chain.query_clients(request); + + reply_to + .send(clients) + .map_err(|e| Kind::Channel.context(e))?; + + Ok(()) + } + fn query_upgraded_client_state( &self, height: Height, @@ -483,6 +530,20 @@ impl ChainRuntime { Ok(()) } + fn query_consensus_states( + &self, + request: QueryConsensusStatesRequest, + reply_to: ReplyTo>, + ) -> Result<(), Error> { + let consensus_states = self.chain.query_consensus_states(request); + + reply_to + .send(consensus_states) + .map_err(|e| Kind::Channel.context(e))?; + + Ok(()) + } + fn query_upgraded_consensus_state( &self, height: Height, @@ -713,7 +774,7 @@ impl ChainRuntime { fn query_txs( &self, - request: QueryPacketEventDataRequest, + request: QueryTxRequest, reply_to: ReplyTo>, ) -> Result<(), Error> { let result = self.chain.query_txs(request); diff --git a/relayer/src/error.rs b/relayer/src/error.rs index 680344c91b..70f8eb3228 100644 --- a/relayer/src/error.rs +++ b/relayer/src/error.rs @@ -163,6 +163,9 @@ pub enum Kind { #[error("the input header is not recognized as a header for this chain")] InvalidInputHeader, + #[error("error raised while submitting the misbehaviour evidence: {0}")] + Misbehaviour(String), + #[error("invalid key address: {0}")] InvalidKeyAddress(String), diff --git a/relayer/src/event/rpc.rs b/relayer/src/event/rpc.rs index 72d031989a..6f50ba5f7c 100644 --- a/relayer/src/event/rpc.rs +++ b/relayer/src/event/rpc.rs @@ -68,6 +68,9 @@ pub fn build_event(mut object: RawObject) -> Result { "update_client" => Ok(IbcEvent::from(ClientEvents::UpdateClient::try_from( object, )?)), + "submit_misbehaviour" => Ok(IbcEvent::from(ClientEvents::ClientMisbehaviour::try_from( + object, + )?)), // Connection events "connection_open_init" => Ok(IbcEvent::from(ConnectionEvents::OpenInit::try_from( diff --git a/relayer/src/foreign_client.rs b/relayer/src/foreign_client.rs index 5b86202e96..88d4685246 100644 --- a/relayer/src/foreign_client.rs +++ b/relayer/src/foreign_client.rs @@ -4,18 +4,27 @@ use prost_types::Any; use thiserror::Error; use tracing::{debug, error, info, warn}; -use ibc::events::IbcEvent; -use ibc::ics02_client::client_consensus::ConsensusState; +use ibc::downcast; +use ibc::events::{IbcEvent, IbcEventType}; +use ibc::ics02_client::client_consensus::{ + AnyConsensusStateWithHeight, ConsensusState, QueryClientEventRequest, +}; use ibc::ics02_client::client_state::ClientState; +use ibc::ics02_client::events::UpdateClient; use ibc::ics02_client::header::Header; +use ibc::ics02_client::misbehaviour::AnyMisbehaviour; use ibc::ics02_client::msgs::create_client::MsgCreateAnyClient; +use ibc::ics02_client::msgs::misbehavior::MsgSubmitAnyMisbehaviour; use ibc::ics02_client::msgs::update_client::MsgUpdateAnyClient; use ibc::ics02_client::msgs::upgrade_client::MsgUpgradeAnyClient; use ibc::ics24_host::identifier::{ChainId, ClientId}; +use ibc::query::QueryTxRequest; use ibc::tx_msg::Msg; use ibc::Height; +use ibc_proto::ibc::core::client::v1::QueryConsensusStatesRequest; use crate::chain::handle::ChainHandle; +use crate::relay::MAX_ITER; #[derive(Debug, Error)] pub enum ForeignClientError { @@ -31,6 +40,9 @@ pub enum ForeignClientError { #[error("failed while finding client {0}: expected chain_id in client state: {1}; actual chain_id: {2}")] ClientFind(ClientId, ChainId, ChainId), + #[error("error raised while submitting the misbehaviour evidence: {0}")] + Misbehaviour(String), + #[error("failed while trying to upgrade client id {0} with error: {1}")] ClientUpgrade(ClientId, String), } @@ -76,6 +88,18 @@ impl ForeignClient { Ok(client) } + pub fn restore_client( + dst_chain: Box, + src_chain: Box, + client_id: &ClientId, + ) -> ForeignClient { + ForeignClient { + id: client_id.clone(), + dst_chain: dst_chain.clone(), + src_chain: src_chain.clone(), + } + } + /// Queries `host_chain` to verify that a client with identifier `client_id` exists. /// If the client does not exist, returns an error. If the client exists, cross-checks that the /// identifier for the target chain of this client (i.e., the chain whose headers this client is @@ -295,11 +319,20 @@ impl ForeignClient { Ok(()) } + /// Wrapper for build_update_client_with_trusted. + pub fn build_update_client( + &self, + target_height: Height, + ) -> Result, ForeignClientError> { + self.build_update_client_with_trusted(target_height, Height::zero()) + } + /// Returns a vector with a message for updating the client to height `target_height`. /// If the client already stores consensus states for this height, returns an empty vector. - pub fn build_update_client( + pub fn build_update_client_with_trusted( &self, target_height: Height, + trusted_height: Height, ) -> Result, ForeignClientError> { // Wait for source chain to reach `target_height` while self.src_chain().query_latest_height().map_err(|e| { @@ -323,7 +356,33 @@ impl ForeignClient { )) })?; - let trusted_height = client_state.latest_height(); + // If not specified, set trusted state to the highest height smaller than target height. + // Otherwise ensure that a consensus state at trusted height exists on-chain. + let cs_heights = self.consensus_state_heights()?; + let trusted_height = if trusted_height == Height::zero() { + // Get highest height smaller than target height + cs_heights + .into_iter() + .find(|h| h < &target_height) + .ok_or_else(|| { + ForeignClientError::ClientUpdate(format!( + "chain {} is missing trusted state smaller than target height {}", + self.dst_chain().id(), + target_height + )) + })? + } else { + cs_heights + .into_iter() + .find(|h| h == &trusted_height) + .ok_or_else(|| { + ForeignClientError::ClientUpdate(format!( + "chain {} is missing trusted state at height {}", + self.dst_chain().id(), + trusted_height + )) + })? + }; if trusted_height >= target_height { warn!( @@ -360,16 +419,28 @@ impl ForeignClient { Ok(vec![new_msg.to_any()]) } - pub fn build_update_client_and_send(&self) -> Result { - let h = self.src_chain.query_latest_height().map_err(|e| { - ForeignClientError::ClientUpdate(format!( - "failed while querying src chain ({}) for latest height: {}", - self.src_chain.id(), - e - )) - })?; + pub fn build_latest_update_client_and_send(&self) -> Result { + self.build_update_client_and_send(Height::zero(), Height::zero()) + } + + pub fn build_update_client_and_send( + &self, + height: Height, + trusted_height: Height, + ) -> Result { + let h = if height == Height::zero() { + self.src_chain.query_latest_height().map_err(|e| { + ForeignClientError::ClientUpdate(format!( + "failed while querying src chain ({}) for latest height: {}", + self.src_chain.id(), + e + )) + })? + } else { + height + }; - let new_msgs = self.build_update_client(h)?; + let new_msgs = self.build_update_client_with_trusted(h, trusted_height)?; if new_msgs.is_empty() { return Err(ForeignClientError::ClientUpdate(format!( "Client {} is already up-to-date with chain {}@{}", @@ -393,7 +464,7 @@ impl ForeignClient { /// Attempts to update a client using header from the latest height of its source chain. pub fn update(&self) -> Result<(), ForeignClientError> { - let res = self.build_update_client_and_send().map_err(|e| { + let res = self.build_latest_update_client_and_send().map_err(|e| { ForeignClientError::ClientUpdate(format!("build_create_client_and_send {:?}", e)) })?; @@ -406,6 +477,250 @@ impl ForeignClient { Ok(()) } + + /// Retrieves the client update event that was emitted when a consensus state at the + /// specified height was created on chain. + /// It is possible that the event cannot be retrieved if the information is not yet available + /// on the full node. To handle this the query is retried a few times. + pub fn update_client_event( + &self, + consensus_height: Height, + ) -> Result, ForeignClientError> { + let request = QueryClientEventRequest { + height: Height::zero(), + event_id: IbcEventType::UpdateClient, + client_id: self.id.clone(), + consensus_height, + }; + + let mut events = vec![]; + for i in 0..MAX_ITER { + thread::sleep(Duration::from_millis(100)); + let result = self + .dst_chain + .query_txs(QueryTxRequest::Client(request.clone())) + .map_err(|e| { + ForeignClientError::Misbehaviour(format!( + "failed to query Tx-es for update client event {}", + e + )) + }); + match result { + Err(e) => { + error!("query_tx with error {}, retry {}/{}", e, i + 1, MAX_ITER); + continue; + } + Ok(result) => { + events = result; + } + } + } + + if events.is_empty() { + return Ok(None); + } + + // It is possible in theory that `query_txs` returns multiple client update events for the + // same consensus height. This could happen when multiple client updates with same header + // were submitted to chain. However this is not what it's observed during testing. + // Regardless, just take the event from the first update. + let event = events[0].clone(); + let update = downcast!(event.clone() => IbcEvent::UpdateClient).ok_or_else(|| { + ForeignClientError::Misbehaviour(format!( + "query Tx-es returned unexpected event {}", + event.to_json() + )) + })?; + Ok(Some(update)) + } + + /// Retrieves all consensus states for this client and sorts them in descending height + /// order. If consensus states are not pruned on chain, then last consensus state is the one + /// installed by the `CreateClient` operation. + fn consensus_states(&self) -> Result, ForeignClientError> { + let mut consensus_states = self + .dst_chain + .query_consensus_states(QueryConsensusStatesRequest { + client_id: self.id.to_string(), + pagination: ibc_proto::cosmos::base::query::pagination::all(), + }) + .map_err(|e| { + ForeignClientError::ClientQuery( + self.id().clone(), + self.src_chain.id(), + format!("{}", e), + ) + })?; + consensus_states.sort_by_key(|a| std::cmp::Reverse(a.height)); + Ok(consensus_states) + } + + /// Retrieves all consensus heights for this client sorted in descending + /// order. + fn consensus_state_heights(&self) -> Result, ForeignClientError> { + let consensus_state_heights: Vec = self + .consensus_states()? + .iter() + .map(|cs| cs.height) + .collect(); + + Ok(consensus_state_heights) + } + + /// Checks for misbehaviour and submits evidence. + /// The check starts with and `update_event` emitted by chain B (`dst_chain`) for a client update + /// with a header from chain A (`src_chain`). The algorithm goes backwards through the headers + /// until it gets to the first misbehaviour. + /// + /// The following cases are covered: + /// 1 - fork: + /// Assumes at least one consensus state before the fork point exists. + /// Let existing consensus states on chain B be: [Sn,.., Sf, Sf-1, S0] with `Sf-1` being + /// the most recent state before fork. + /// Chain A is queried for a header `Hf'` at `Sf.height` and if it is different than the `Hf` + /// in the event for the client update (the one that has generated `Sf` on chain), then the two + /// headers are included in the evidence and submitted. + /// Note that in this case the headers are different but have the same height. + /// + /// 2 - BFT time violation for unavailable header (a.k.a. Future Lunatic Attack or FLA): + /// Some header with a height that is higher than the latest + /// height on A has been accepted and a consensus state was created on B. Note that this implies + /// that the timestamp of this header must be within the `clock_drift` of the client. + /// Assume the client on B has been updated with `h2`(not present on/ produced by chain A) + /// and it has a timestamp of `t2` that is at most `clock_drift` in the future. + /// Then the latest header from A is fetched, let it be `h1`, with a timestamp of `t1`. + /// If `t1 >= t2` then evidence of misbehavior is submitted to A. + /// + /// 3 - BFT time violation for existing headers (TODO): + /// Ensure that consensus state times are monotonically increasing with height. + /// + /// Other notes: + /// - the algorithm builds misbehavior at each consensus height, starting with the + /// highest height assuming the previous one is trusted. It submits the first constructed + /// evidence (the one with the highest height) + /// - a lot of the logic here is derived from the behavior of the only implemented client + /// (ics07-tendermint) and might not be general enough. + /// + pub fn handle_misbehaviour( + &self, + mut update: Option, + ) -> Result, ForeignClientError> { + thread::sleep(Duration::from_millis(100)); + + // Get the latest client state on destination. + let client_state = self + .dst_chain() + .query_client_state(&self.id, Height::zero()) + .map_err(|e| { + ForeignClientError::Misbehaviour(format!( + "failed querying client state on dst chain {} with error: {}", + self.id, e + )) + })?; + + // Get the list of consensus state heights in descending order. + // Note: If chain does not prune consensus states then the last consensus state is + // the one installed by the `CreateClient` which does not include a header. + // For chains that do support pruning, it is possible that the last consensus state + // was installed by an `UpdateClient` and an event and header will be found. + let consensus_state_heights = self.consensus_state_heights()?; + info!( + "checking misbehaviour for consensus state heights {:?}", + consensus_state_heights + ); + + for target_height in consensus_state_heights { + // Start with specified update event or the one for latest consensus height + let update_event = if let Some(ref event) = update { + // we are here only on the first iteration when called with `Some` update event + event.clone() + } else if let Some(event) = self.update_client_event(target_height)? { + // we are here either on the first iteration with `None` initial update event or + // subsequent iterations + event + } else { + // we are here if the consensus state was installed on-chain when client was + // created, therefore there will be no update client event + break; + }; + + // Skip over heights higher than the update event one. + // This can happen if a client update happened with a lower height than latest. + if target_height > update_event.consensus_height() { + continue; + } + + // Ensure consensus height of the event is same as target height. This should be the + // case as we either + // - got the `update_event` from the `target_height` above, or + // - an `update_event` was specified and we should eventually find a consensus state + // at that height + // We break here in case we got a bogus event. + if target_height < update_event.consensus_height() { + break; + } + + // Check for misbehaviour according to the specific source chain type. + // In case of Tendermint client, this will also check the BFT time violation if + // a header for the event height cannot be retrieved from the witness. + let misbehavior = self + .src_chain + .check_misbehaviour(update_event, client_state.clone()) + .map_err(|e| { + ForeignClientError::Misbehaviour(format!("failed to build misbehaviour {}", e)) + })?; + + if misbehavior.is_some() { + // TODO - add updateClient messages if light blocks are returned from + // `src_chain.check_misbehaviour` call above i.e. supporting headers are required + return Ok(misbehavior); + } + + // Clear the update + update = None; + } + + Ok(None) + } + + pub fn detect_misbehaviour_and_send_evidence( + &self, + update: Option, + ) -> Result, ForeignClientError> { + match self.handle_misbehaviour(update)? { + None => Ok(vec![]), + Some(misbehaviour) => { + error!("MISBEHAVIOUR DETECTED {}, sending evidence", misbehaviour); + + let signer = self.dst_chain().get_signer().map_err(|e| { + ForeignClientError::Misbehaviour(format!( + "failed getting signer for destination chain ({}), error: {}", + self.dst_chain.id(), + e + )) + })?; + + let msg = MsgSubmitAnyMisbehaviour { + client_id: self.id.clone(), + misbehaviour, + signer, + }; + + let events = self + .dst_chain() + .send_msgs(vec![msg.to_any()]) + .map_err(|e| { + ForeignClientError::Misbehaviour(format!( + "failed sending evidence to destination chain ({}), error: {}", + self.dst_chain.id(), + e + )) + })?; + + Ok(events) + } + } + } } pub fn extract_client_id(event: &IbcEvent) -> Result<&ClientId, ForeignClientError> { @@ -498,7 +813,7 @@ mod test { }; // This action should fail because no client exists (yet) - let res = a_client.build_update_client_and_send(); + let res = a_client.build_latest_update_client_and_send(); assert!( res.is_err(), "build_update_client_and_send was supposed to fail (no client existed)" @@ -522,7 +837,7 @@ mod test { // This should fail because the client on chain a already has the latest headers. Chain b, // the source chain for the client on a, is at the same height where it was when the client // was created, so an update should fail here. - let res = a_client.build_update_client_and_send(); + let res = a_client.build_latest_update_client_and_send(); assert!( res.is_err(), "build_update_client_and_send was supposed to fail", @@ -551,7 +866,7 @@ mod test { // Now we can update both clients -- a ping pong, similar to ICS18 `client_update_ping_pong` for _i in 1..num_iterations { - let res = a_client.build_update_client_and_send(); + let res = a_client.build_latest_update_client_and_send(); assert!( res.is_ok(), "build_update_client_and_send failed (chain a) with error: {:?}", @@ -567,7 +882,7 @@ mod test { ); // And also update the client on chain b. - let res = b_client.build_update_client_and_send(); + let res = b_client.build_latest_update_client_and_send(); assert!( res.is_ok(), "build_update_client_and_send failed (chain b) with error: {:?}", diff --git a/relayer/src/lib.rs b/relayer/src/lib.rs index e14841c1fb..8bd1d09c4b 100644 --- a/relayer/src/lib.rs +++ b/relayer/src/lib.rs @@ -8,7 +8,6 @@ unused_qualifications, rust_2018_idioms )] -// #![allow(dead_code, unreachable_code, unused_imports, unused_variables)] //! IBC Relayer implementation diff --git a/relayer/src/light_client.rs b/relayer/src/light_client.rs index 2191734899..0d858d139e 100644 --- a/relayer/src/light_client.rs +++ b/relayer/src/light_client.rs @@ -2,6 +2,8 @@ use ibc::ics02_client::client_state::AnyClientState; use crate::chain::Chain; use crate::error; +use ibc::ics02_client::events::UpdateClient; +use ibc::ics02_client::misbehaviour::AnyMisbehaviour; pub mod tendermint; @@ -23,6 +25,11 @@ pub trait LightClient: Send + Sync { client_state: &AnyClientState, ) -> Result; + fn check_misbehaviour( + &mut self, + update: UpdateClient, + client_state: &AnyClientState, + ) -> Result, error::Error>; /// Fetch a header from the chain at the given height, without verifying it fn fetch(&mut self, height: ibc::Height) -> Result; } diff --git a/relayer/src/light_client/mock.rs b/relayer/src/light_client/mock.rs index 8ab73bc342..48dfe0c278 100644 --- a/relayer/src/light_client/mock.rs +++ b/relayer/src/light_client/mock.rs @@ -1,6 +1,8 @@ use tendermint_testgen::light_block::TmLightBlock; use ibc::ics02_client::client_state::AnyClientState; +use ibc::ics02_client::events::UpdateClient; +use ibc::ics02_client::misbehaviour::AnyMisbehaviour; use ibc::ics24_host::identifier::ChainId; use ibc::mock::host::HostBlock; use ibc::Height; @@ -40,4 +42,12 @@ impl super::LightClient for LightClient { fn fetch(&mut self, height: Height) -> Result { Ok(self.light_block(height)) } + + fn check_misbehaviour( + &mut self, + _update: UpdateClient, + _client_state: &AnyClientState, + ) -> Result, Error> { + unimplemented!() + } } diff --git a/relayer/src/light_client/tendermint.rs b/relayer/src/light_client/tendermint.rs index f5648b4c6b..bbb89e33b8 100644 --- a/relayer/src/light_client/tendermint.rs +++ b/relayer/src/light_client/tendermint.rs @@ -1,7 +1,5 @@ use std::convert::TryFrom; -use tendermint_rpc as rpc; - use tendermint_light_client::{ components::{self, io::AtHeight}, light_client::{LightClient as TmLightClient, Options as TmOptions}, @@ -11,13 +9,22 @@ use tendermint_light_client::{ types::Height as TMHeight, types::{LightBlock, PeerId, Status}, }; +use tendermint_rpc as rpc; use ibc::{ downcast, - ics02_client::{client_state::AnyClientState, client_type::ClientType}, + ics02_client::{ + client_state::AnyClientState, + client_type::ClientType, + events::UpdateClient, + header::AnyHeader, + misbehaviour::{AnyMisbehaviour, Misbehaviour}, + }, + ics07_tendermint::{header::Header as TmHeader, misbehaviour::Misbehaviour as TmMisbehaviour}, ics24_host::identifier::ChainId, }; +use crate::error::Kind; use crate::{ chain::CosmosSdkChain, config::ChainConfig, @@ -56,6 +63,79 @@ impl super::LightClient for LightClient { self.fetch_light_block(AtHeight::At(height)) } + + /// Given a client update event that includes the header used in a client update. + /// it looks for misbehaviour by fetching a header at same or latest height. + /// TODO - return also intermediate headers. + fn check_misbehaviour( + &mut self, + update: UpdateClient, + client_state: &AnyClientState, + ) -> Result, Error> { + crate::time!("light client check_misbehaviour"); + + let update_header = update.header.clone().ok_or_else(|| { + Kind::Misbehaviour(format!( + "missing header in update client event {}", + self.chain_id + )) + })?; + + let tm_ibc_client_header = + downcast!(update_header => AnyHeader::Tendermint).ok_or_else(|| { + Kind::Misbehaviour(format!( + "header type incompatible for chain {}", + self.chain_id + )) + })?; + + let latest_chain_block = self.fetch_light_block(AtHeight::Highest)?; + let latest_chain_height = + ibc::Height::new(self.chain_id.version(), latest_chain_block.height().into()); + + // set the target height to the minimum between the update height and latest chain height + let target_height = std::cmp::min(update.consensus_height(), latest_chain_height); + let trusted_height = tm_ibc_client_header.trusted_height; + + // TODO - check that a consensus state at trusted_height still exists on-chain, + // currently we don't have access to Cosmos chain query from here + + if trusted_height >= latest_chain_height { + // Can happen with multiple FLA attacks, we return no evidence and hope to catch this in + // the next iteration. e.g: + // existing consensus states: 1000, 900, 300, 200 (only known by the caller) + // latest_chain_height = 300 + // target_height = 1000 + // trusted_height = 900 + return Ok(None); + } + + let tm_witness_node_header = { + let trusted_light_block = self.fetch(trusted_height.increment())?; + let target_light_block = self.verify(trusted_height, target_height, &client_state)?; + TmHeader { + trusted_height, + signed_header: target_light_block.signed_header, + validator_set: target_light_block.validators, + trusted_validator_set: trusted_light_block.validators, + } + }; + + let misbehaviour = if !tm_witness_node_header.compatible_with(&tm_ibc_client_header) { + Some( + AnyMisbehaviour::Tendermint(TmMisbehaviour { + client_id: update.client_id().clone(), + header1: tm_ibc_client_header, + header2: tm_witness_node_header, + }) + .wrap_any(), + ) + } else { + None + }; + + Ok(misbehaviour) + } } impl LightClient { diff --git a/relayer/src/link.rs b/relayer/src/link.rs index 7d5236a2c7..83a81eb258 100644 --- a/relayer/src/link.rs +++ b/relayer/src/link.rs @@ -5,6 +5,7 @@ use prost_types::Any; use thiserror::Error; use tracing::{error, info}; +use ibc::query::QueryTxRequest; use ibc::{ downcast, events::{IbcEvent, IbcEventType}, @@ -23,7 +24,6 @@ use ibc::{ tx_msg::Msg, Height, }; - use ibc_proto::ibc::core::channel::v1::{ QueryNextSequenceReceiveRequest, QueryPacketAcknowledgementsRequest, QueryPacketCommitmentsRequest, QueryUnreceivedAcksRequest, QueryUnreceivedPacketsRequest, @@ -545,7 +545,7 @@ impl RelayPath { let pc_request = QueryPacketCommitmentsRequest { port_id: self.src_port_id().to_string(), channel_id: self.src_channel_id().to_string(), - pagination: None, + pagination: ibc_proto::cosmos::base::query::pagination::all(), }; let (packet_commitments, query_height) = self.src_chain.query_packet_commitments(pc_request)?; @@ -587,7 +587,7 @@ impl RelayPath { return Ok(()); } - self.all_events = self.src_chain.query_txs(QueryPacketEventDataRequest { + let query = QueryTxRequest::Packet(QueryPacketEventDataRequest { event_id: IbcEventType::SendPacket, source_port_id: self.src_port_id().clone(), source_channel_id: self.src_channel_id().clone(), @@ -595,7 +595,9 @@ impl RelayPath { destination_channel_id: self.dst_channel_id().clone(), sequences, height: self.src_height, - })?; + }); + + self.all_events = self.src_chain.query_txs(query)?; let mut packet_sequences = vec![]; for event in self.all_events.iter() { @@ -613,7 +615,7 @@ impl RelayPath { let pc_request = QueryPacketAcknowledgementsRequest { port_id: self.src_port_id().to_string(), channel_id: self.src_channel_id().to_string(), - pagination: None, + pagination: ibc_proto::cosmos::base::query::pagination::all(), }; let (acks_on_source, query_height) = self .src_chain @@ -661,7 +663,7 @@ impl RelayPath { self.all_events = self .src_chain - .query_txs(QueryPacketEventDataRequest { + .query_txs(QueryTxRequest::Packet(QueryPacketEventDataRequest { event_id: IbcEventType::WriteAck, source_port_id: self.dst_port_id().clone(), source_channel_id: self.dst_channel_id().clone(), @@ -669,7 +671,7 @@ impl RelayPath { destination_channel_id: self.src_channel_id().clone(), sequences, height: self.src_height, - }) + })) .map_err(|e| LinkError::QueryError(self.src_chain.id(), e))?; let mut packet_sequences = vec![];