diff --git a/.changelog/unreleased/features/842-impl-grpc-services.md b/.changelog/unreleased/features/842-impl-grpc-services.md new file mode 100644 index 000000000..081992300 --- /dev/null +++ b/.changelog/unreleased/features/842-impl-grpc-services.md @@ -0,0 +1,2 @@ +- Blanket implementation of core gRPC services + ([\#686](https://github.com/cosmos/ibc-rs/issues/686)) \ No newline at end of file diff --git a/crates/ibc/Cargo.toml b/crates/ibc/Cargo.toml index 12291754b..f5267c1d1 100644 --- a/crates/ibc/Cargo.toml +++ b/crates/ibc/Cargo.toml @@ -37,6 +37,8 @@ std = [ ] parity-scale-codec = ["dep:parity-scale-codec", "dep:scale-info", "ibc-proto/parity-scale-codec"] borsh = ["dep:borsh", "ibc-proto/borsh"] +# includes gRPC services for IBC core +grpc = ["dep:tonic", "ibc-proto/server"] # This feature is required for token transfer (ICS-20) serde = ["dep:serde", "dep:serde_derive", "serde_json", "ics23/serde"] @@ -67,6 +69,7 @@ num-traits = { version = "0.2.15", default-features = false } derive_more = { version = "0.99.17", default-features = false, features = ["from", "into", "display", "try_into"] } uint = { version = "0.9", default-features = false } primitive-types = { version = "0.12.0", default-features = false, features = ["serde_no_std"] } +tonic = { version = "0.9", optional = true } ## for codec encode or decode parity-scale-codec = { version = "3.0.0", default-features = false, features = ["full"], optional = true } diff --git a/crates/ibc/src/core/context.rs b/crates/ibc/src/core/context.rs index b74437af1..d886a2aa9 100644 --- a/crates/ibc/src/core/context.rs +++ b/crates/ibc/src/core/context.rs @@ -21,8 +21,7 @@ use crate::core::ics04_channel::error::ChannelError; use crate::core::ics04_channel::error::PacketError; use crate::core::ics04_channel::packet::{Receipt, Sequence}; use crate::core::ics23_commitment::commitment::CommitmentPrefix; -use crate::core::ics24_host::identifier::ClientId; -use crate::core::ics24_host::identifier::ConnectionId; +use crate::core::ics24_host::identifier::{ClientId, ConnectionId}; use crate::core::ics24_host::path::{ AckPath, ChannelEndPath, ClientConnectionPath, ClientConsensusStatePath, CommitmentPath, ConnectionPath, ReceiptPath, SeqAckPath, SeqRecvPath, SeqSendPath, diff --git a/crates/ibc/src/core/ics02_client/handler/create_client.rs b/crates/ibc/src/core/ics02_client/handler/create_client.rs index e3ee280ab..bae748f7c 100644 --- a/crates/ibc/src/core/ics02_client/handler/create_client.rs +++ b/crates/ibc/src/core/ics02_client/handler/create_client.rs @@ -69,7 +69,7 @@ where let client_id = ClientId::new(client_type.clone(), id_counter).map_err(|e| { ContextError::from(ClientError::ClientIdentifierConstructor { - client_type: client_state.client_type(), + client_type: client_type.clone(), counter: id_counter, validation_error: e, }) diff --git a/crates/ibc/src/core/ics04_channel/error.rs b/crates/ibc/src/core/ics04_channel/error.rs index d8a98a2a7..478c1575f 100644 --- a/crates/ibc/src/core/ics04_channel/error.rs +++ b/crates/ibc/src/core/ics04_channel/error.rs @@ -171,6 +171,8 @@ pub enum PacketError { }, /// Cannot encode sequence `{sequence}` CannotEncodeSequence { sequence: Sequence }, + /// other error: `{description}` + Other { description: String }, } impl From for ChannelError { diff --git a/crates/ibc/src/lib.rs b/crates/ibc/src/lib.rs index 68004e69f..6575a4d24 100644 --- a/crates/ibc/src/lib.rs +++ b/crates/ibc/src/lib.rs @@ -53,6 +53,9 @@ pub mod clients; pub mod core; pub mod hosts; +#[cfg(feature = "grpc")] +pub mod services; + #[cfg(any(test, feature = "mocks"))] pub mod mock; #[cfg(any(test, feature = "mocks"))] diff --git a/crates/ibc/src/services/core/channel.rs b/crates/ibc/src/services/core/channel.rs new file mode 100644 index 000000000..3442f569d --- /dev/null +++ b/crates/ibc/src/services/core/channel.rs @@ -0,0 +1,559 @@ +//! [`ChannelQueryService`](ChannelQueryService) takes a generic `I` to store `ibc_context` that implements [`QueryContext`](QueryContext). +//! `I` must be a type where writes from one thread are readable from another. +//! This means using `Arc>` or `Arc>` in most cases. + +use alloc::str::FromStr; +use std::boxed::Box; + +use ibc_proto::google::protobuf::Any; +use ibc_proto::ibc::core::channel::v1::query_server::Query as ChannelQuery; +use ibc_proto::ibc::core::channel::v1::{ + PacketState, QueryChannelClientStateRequest, QueryChannelClientStateResponse, + QueryChannelConsensusStateRequest, QueryChannelConsensusStateResponse, QueryChannelRequest, + QueryChannelResponse, QueryChannelsRequest, QueryChannelsResponse, + QueryConnectionChannelsRequest, QueryConnectionChannelsResponse, + QueryNextSequenceReceiveRequest, QueryNextSequenceReceiveResponse, + QueryNextSequenceSendRequest, QueryNextSequenceSendResponse, QueryPacketAcknowledgementRequest, + QueryPacketAcknowledgementResponse, QueryPacketAcknowledgementsRequest, + QueryPacketAcknowledgementsResponse, QueryPacketCommitmentRequest, + QueryPacketCommitmentResponse, QueryPacketCommitmentsRequest, QueryPacketCommitmentsResponse, + QueryPacketReceiptRequest, QueryPacketReceiptResponse, QueryUnreceivedAcksRequest, + QueryUnreceivedAcksResponse, QueryUnreceivedPacketsRequest, QueryUnreceivedPacketsResponse, +}; +use ibc_proto::ibc::core::client::v1::IdentifiedClientState; +use tonic::{Request, Response, Status}; + +use crate::core::ics04_channel::packet::Sequence; +use crate::core::ics24_host::identifier::{ChannelId, ConnectionId, PortId}; +use crate::core::ics24_host::path::{ + AckPath, ChannelEndPath, ClientConsensusStatePath, ClientStatePath, CommitmentPath, Path, + ReceiptPath, SeqRecvPath, SeqSendPath, +}; +use crate::core::ValidationContext; +use crate::prelude::*; +use crate::services::core::context::QueryContext; +use crate::Height; + +// TODO(rano): currently the services don't support pagination, so we return all the results. + +/// The generic `I` must be a type where writes from one thread are readable from another. +/// This means using `Arc>` or `Arc>` in most cases. +pub struct ChannelQueryService +where + I: QueryContext + Send + Sync + 'static, + ::AnyClientState: Into, + ::AnyConsensusState: Into, +{ + ibc_context: I, +} + +impl ChannelQueryService +where + I: QueryContext + Send + Sync + 'static, + ::AnyClientState: Into, + ::AnyConsensusState: Into, +{ + /// The parameter `ibc_context` must be a type where writes from one thread are readable from another. + /// This means using `Arc>` or `Arc>` in most cases. + pub fn new(ibc_context: I) -> Self { + Self { ibc_context } + } +} + +#[tonic::async_trait] +impl ChannelQuery for ChannelQueryService +where + I: QueryContext + Send + Sync + 'static, + ::AnyClientState: Into, + ::AnyConsensusState: Into, +{ + async fn channel( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let channel_id = ChannelId::from_str(request_ref.channel_id.as_str())?; + + let port_id = PortId::from_str(request_ref.port_id.as_str())?; + + let channel_end_path = ChannelEndPath::new(&port_id, &channel_id); + + let channel_end = self.ibc_context.channel_end(&channel_end_path)?; + + let current_height = self.ibc_context.host_height()?; + + let proof = self + .ibc_context + .get_proof(current_height, &Path::ChannelEnd(channel_end_path)) + .ok_or_else(|| { + Status::not_found(format!( + "Channel end proof not found for channel {}", + channel_id + )) + })?; + + Ok(Response::new(QueryChannelResponse { + channel: Some(channel_end.into()), + proof, + proof_height: Some(current_height.into()), + })) + } + + async fn channels( + &self, + _request: Request, + ) -> Result, Status> { + let channel_ends = self.ibc_context.channel_ends()?; + + Ok(Response::new(QueryChannelsResponse { + channels: channel_ends.into_iter().map(Into::into).collect(), + height: Some(self.ibc_context.host_height()?.into()), + // no support for pagination yet + pagination: None, + })) + } + + async fn connection_channels( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let connection_id = ConnectionId::from_str(request_ref.connection.as_str())?; + + let all_channel_ends = self.ibc_context.channel_ends()?; + + let connection_channel_ends = all_channel_ends + .into_iter() + .filter(|channel_end| { + channel_end + .channel_end + .connection_hops() + .iter() + .any(|connection_hop| connection_hop == &connection_id) + }) + .map(Into::into) + .collect(); + + Ok(Response::new(QueryConnectionChannelsResponse { + channels: connection_channel_ends, + height: Some(self.ibc_context.host_height()?.into()), + // no support for pagination yet + pagination: None, + })) + } + + async fn channel_client_state( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let channel_id = ChannelId::from_str(request_ref.channel_id.as_str())?; + + let port_id = PortId::from_str(request_ref.port_id.as_str())?; + + let channel_end_path = ChannelEndPath::new(&port_id, &channel_id); + + let channel_end = self.ibc_context.channel_end(&channel_end_path)?; + + let connection_end = channel_end + .connection_hops() + .first() + .map(|connection_id| self.ibc_context.connection_end(connection_id)) + .ok_or_else(|| { + Status::not_found(format!("Channel {} has no connection hops", channel_id)) + })??; + + let client_state = self.ibc_context.client_state(connection_end.client_id())?; + + let current_height = self.ibc_context.host_height()?; + + let proof = self + .ibc_context + .get_proof( + current_height, + &Path::ClientState(ClientStatePath::new(connection_end.client_id())), + ) + .ok_or_else(|| { + Status::not_found(format!( + "Client state proof not found for client {}", + connection_end.client_id() + )) + })?; + + Ok(Response::new(QueryChannelClientStateResponse { + identified_client_state: Some(IdentifiedClientState { + client_id: connection_end.client_id().as_str().into(), + client_state: Some(client_state.into()), + }), + proof, + proof_height: Some(current_height.into()), + })) + } + + async fn channel_consensus_state( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let channel_id = ChannelId::from_str(request_ref.channel_id.as_str())?; + + let port_id = PortId::from_str(request_ref.port_id.as_str())?; + + let height = Height::new(request_ref.revision_number, request_ref.revision_height) + .map_err(|e| Status::invalid_argument(e.to_string()))?; + + let channel_end_path = ChannelEndPath::new(&port_id, &channel_id); + + let channel_end = self.ibc_context.channel_end(&channel_end_path)?; + + let connection_end = channel_end + .connection_hops() + .first() + .map(|connection_id| self.ibc_context.connection_end(connection_id)) + .ok_or_else(|| { + Status::not_found(format!("Channel {} has no connection hops", channel_id)) + })??; + + let consensus_path = ClientConsensusStatePath::new(connection_end.client_id(), &height); + + let consensus_state = self.ibc_context.consensus_state(&consensus_path)?; + + let current_height = self.ibc_context.host_height()?; + + let proof = self + .ibc_context + .get_proof(current_height, &Path::ClientConsensusState(consensus_path)) + .ok_or_else(|| { + Status::not_found(format!( + "Consensus state proof not found for client {}", + connection_end.client_id() + )) + })?; + + Ok(Response::new(QueryChannelConsensusStateResponse { + client_id: connection_end.client_id().as_str().into(), + consensus_state: Some(consensus_state.into()), + proof, + proof_height: Some(current_height.into()), + })) + } + + async fn packet_commitment( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let channel_id = ChannelId::from_str(request_ref.channel_id.as_str())?; + + let port_id = PortId::from_str(request_ref.port_id.as_str())?; + + let sequence = Sequence::from(request_ref.sequence); + + let commitment_path = CommitmentPath::new(&port_id, &channel_id, sequence); + + let packet_commitment_data = self.ibc_context.get_packet_commitment(&commitment_path)?; + + let current_height = self.ibc_context.host_height()?; + + let proof = self + .ibc_context + .get_proof(current_height, &Path::Commitment(commitment_path)) + .ok_or_else(|| { + Status::not_found(format!( + "Packet commitment proof not found for channel {}", + channel_id + )) + })?; + + Ok(Response::new(QueryPacketCommitmentResponse { + commitment: packet_commitment_data.into_vec(), + proof, + proof_height: Some(current_height.into()), + })) + } + + async fn packet_commitments( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let channel_id = ChannelId::from_str(request_ref.channel_id.as_str())?; + + let port_id = PortId::from_str(request_ref.port_id.as_str())?; + + let channel_end_path = ChannelEndPath::new(&port_id, &channel_id); + + let commitments = self + .ibc_context + .packet_commitments(&channel_end_path)? + .into_iter() + .map(|path| { + self.ibc_context + .get_packet_commitment(&path) + .map(|commitment| PacketState { + port_id: path.port_id.as_str().into(), + channel_id: path.channel_id.as_str().into(), + sequence: path.sequence.into(), + data: commitment.into_vec(), + }) + }) + .collect::>()?; + + Ok(Response::new(QueryPacketCommitmentsResponse { + commitments, + height: Some(self.ibc_context.host_height()?.into()), + // no support for pagination yet + pagination: None, + })) + } + + async fn packet_receipt( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let channel_id = ChannelId::from_str(request_ref.channel_id.as_str())?; + + let port_id = PortId::from_str(request_ref.port_id.as_str())?; + + let sequence = Sequence::from(request_ref.sequence); + + let receipt_path = ReceiptPath::new(&port_id, &channel_id, sequence); + + // Receipt only has one enum + // Unreceived packets are not stored + let packet_receipt_data = self.ibc_context.get_packet_receipt(&receipt_path); + + let current_height = self.ibc_context.host_height()?; + + let proof = self + .ibc_context + .get_proof(current_height, &Path::Receipt(receipt_path)) + .ok_or_else(|| { + Status::not_found(format!( + "Packet receipt proof not found for channel {}", + channel_id + )) + })?; + + Ok(Response::new(QueryPacketReceiptResponse { + received: packet_receipt_data.is_ok(), + proof, + proof_height: Some(current_height.into()), + })) + } + + async fn packet_acknowledgement( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let channel_id = ChannelId::from_str(request_ref.channel_id.as_str())?; + + let port_id = PortId::from_str(request_ref.port_id.as_str())?; + + let sequence = Sequence::from(request_ref.sequence); + + let acknowledgement_path = AckPath::new(&port_id, &channel_id, sequence); + + let packet_acknowledgement_data = self + .ibc_context + .get_packet_acknowledgement(&acknowledgement_path)?; + + let current_height = self.ibc_context.host_height()?; + + let proof = self + .ibc_context + .get_proof(current_height, &Path::Ack(acknowledgement_path)) + .ok_or_else(|| { + Status::not_found(format!( + "Packet acknowledgement proof not found for channel {}", + channel_id + )) + })?; + + Ok(Response::new(QueryPacketAcknowledgementResponse { + acknowledgement: packet_acknowledgement_data.into_vec(), + proof, + proof_height: Some(current_height.into()), + })) + } + + /// Returns all the acknowledgements if sequences is omitted. + async fn packet_acknowledgements( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let channel_id = ChannelId::from_str(request_ref.channel_id.as_str())?; + + let port_id = PortId::from_str(request_ref.port_id.as_str())?; + + let commitment_sequences = request_ref + .packet_commitment_sequences + .iter() + .copied() + .map(Sequence::from); + + let channel_end_path = ChannelEndPath::new(&port_id, &channel_id); + + let acknowledgements = self + .ibc_context + .packet_acknowledgements(&channel_end_path, commitment_sequences)? + .into_iter() + .map(|path| { + self.ibc_context + .get_packet_acknowledgement(&path) + .map(|acknowledgement| PacketState { + port_id: path.port_id.as_str().into(), + channel_id: path.channel_id.as_str().into(), + sequence: path.sequence.into(), + data: acknowledgement.into_vec(), + }) + }) + .collect::>()?; + + Ok(Response::new(QueryPacketAcknowledgementsResponse { + acknowledgements, + height: Some(self.ibc_context.host_height()?.into()), + // no support for pagination yet + pagination: None, + })) + } + + async fn unreceived_packets( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let channel_id = ChannelId::from_str(request_ref.channel_id.as_str())?; + + let port_id = PortId::from_str(request_ref.port_id.as_str())?; + + let sequences = request_ref + .packet_commitment_sequences + .iter() + .copied() + .map(Sequence::from); + + let channel_end_path = ChannelEndPath::new(&port_id, &channel_id); + + let unreceived_packets = self + .ibc_context + .unreceived_packets(&channel_end_path, sequences)?; + + Ok(Response::new(QueryUnreceivedPacketsResponse { + sequences: unreceived_packets.into_iter().map(Into::into).collect(), + height: Some(self.ibc_context.host_height()?.into()), + })) + } + + /// Returns all the unreceived acknowledgements if sequences is omitted. + async fn unreceived_acks( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let channel_id = ChannelId::from_str(request_ref.channel_id.as_str())?; + + let port_id = PortId::from_str(request_ref.port_id.as_str())?; + + let sequences = request_ref + .packet_ack_sequences + .iter() + .copied() + .map(Sequence::from); + + let channel_end_path = ChannelEndPath::new(&port_id, &channel_id); + + let unreceived_acks = self + .ibc_context + .unreceived_acks(&channel_end_path, sequences)?; + + Ok(Response::new(QueryUnreceivedAcksResponse { + sequences: unreceived_acks.into_iter().map(Into::into).collect(), + height: Some(self.ibc_context.host_height()?.into()), + })) + } + + async fn next_sequence_receive( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let channel_id = ChannelId::from_str(request_ref.channel_id.as_str())?; + + let port_id = PortId::from_str(request_ref.port_id.as_str())?; + + let next_seq_recv_path = SeqRecvPath::new(&port_id, &channel_id); + + let next_sequence_recv = self + .ibc_context + .get_next_sequence_recv(&next_seq_recv_path)?; + + let current_height = self.ibc_context.host_height()?; + + let proof = self + .ibc_context + .get_proof(current_height, &Path::SeqRecv(next_seq_recv_path)) + .ok_or_else(|| { + Status::not_found(format!( + "Next sequence receive proof not found for channel {}", + channel_id + )) + })?; + + Ok(Response::new(QueryNextSequenceReceiveResponse { + next_sequence_receive: next_sequence_recv.into(), + proof, + proof_height: Some(current_height.into()), + })) + } + + async fn next_sequence_send( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let channel_id = ChannelId::from_str(request_ref.channel_id.as_str())?; + + let port_id = PortId::from_str(request_ref.port_id.as_str())?; + + let next_seq_send_path = SeqSendPath::new(&port_id, &channel_id); + + let next_sequence_send = self + .ibc_context + .get_next_sequence_send(&next_seq_send_path)?; + + let current_height = self.ibc_context.host_height()?; + + let proof = self + .ibc_context + .get_proof(current_height, &Path::SeqSend(next_seq_send_path)) + .ok_or_else(|| { + Status::not_found(format!( + "Next sequence send proof not found for channel {}", + channel_id + )) + })?; + + Ok(Response::new(QueryNextSequenceSendResponse { + next_sequence_send: next_sequence_send.into(), + proof, + proof_height: Some(current_height.into()), + })) + } +} diff --git a/crates/ibc/src/services/core/client.rs b/crates/ibc/src/services/core/client.rs new file mode 100644 index 000000000..11a7ce548 --- /dev/null +++ b/crates/ibc/src/services/core/client.rs @@ -0,0 +1,298 @@ +//! [`ClientQueryService`](ClientQueryService) takes generics `I` and `U` to store `ibc_context` and `upgrade_context` that implement [`QueryContext`](QueryContext) and [`UpgradeValidationContext`](UpgradeValidationContext) respectively. +//! `I` must be a type where writes from one thread are readable from another. +//! This means using `Arc>` or `Arc>` in most cases. + +use core::str::FromStr; +use std::boxed::Box; + +use ibc_proto::google::protobuf::Any; +use ibc_proto::ibc::core::client::v1::query_server::Query as ClientQuery; +use ibc_proto::ibc::core::client::v1::{ + ConsensusStateWithHeight, IdentifiedClientState, QueryClientParamsRequest, + QueryClientParamsResponse, QueryClientStateRequest, QueryClientStateResponse, + QueryClientStatesRequest, QueryClientStatesResponse, QueryClientStatusRequest, + QueryClientStatusResponse, QueryConsensusStateHeightsRequest, + QueryConsensusStateHeightsResponse, QueryConsensusStateRequest, QueryConsensusStateResponse, + QueryConsensusStatesRequest, QueryConsensusStatesResponse, QueryUpgradedClientStateRequest, + QueryUpgradedClientStateResponse, QueryUpgradedConsensusStateRequest, + QueryUpgradedConsensusStateResponse, +}; +use tonic::{Request, Response, Status}; + +use crate::core::ics02_client::client_state::ClientStateValidation; +use crate::core::ics02_client::error::ClientError; +use crate::core::ics24_host::identifier::ClientId; +use crate::core::ics24_host::path::{ + ClientConsensusStatePath, ClientStatePath, Path, UpgradeClientPath, +}; +use crate::core::{ContextError, ValidationContext}; +use crate::hosts::tendermint::upgrade_proposal::UpgradeValidationContext; +use crate::prelude::*; +use crate::services::core::context::QueryContext; +use crate::Height; + +// TODO(rano): currently the services don't support pagination, so we return all the results. + +/// Generics `I` and `U` must be a type where writes from one thread are readable from another. +/// This means using `Arc>` or `Arc>` in most cases. +pub struct ClientQueryService +where + I: QueryContext + Send + Sync + 'static, + U: UpgradeValidationContext + Send + Sync + 'static, + ::AnyClientState: Into, + ::AnyConsensusState: Into, + ::AnyClientState: Into, + ::AnyConsensusState: Into, +{ + ibc_context: I, + upgrade_context: U, +} + +impl ClientQueryService +where + I: QueryContext + Send + Sync + 'static, + U: UpgradeValidationContext + Send + Sync + 'static, + ::AnyClientState: Into, + ::AnyConsensusState: Into, + ::AnyClientState: Into, + ::AnyConsensusState: Into, +{ + /// Parameters `ibc_context` and `upgrade_context` must be a type where writes from one thread are readable from another. + /// This means using `Arc>` or `Arc>` in most cases. + pub fn new(ibc_context: I, upgrade_context: U) -> Self { + Self { + ibc_context, + upgrade_context, + } + } +} + +#[tonic::async_trait] +impl ClientQuery for ClientQueryService +where + I: QueryContext + Send + Sync + 'static, + U: UpgradeValidationContext + Send + Sync + 'static, + ::AnyClientState: Into, + ::AnyConsensusState: Into, + ::AnyClientState: Into, + ::AnyConsensusState: Into, +{ + async fn client_state( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let client_id = ClientId::from_str(request_ref.client_id.as_str())?; + let client_state = self.ibc_context.client_state(&client_id)?; + + let current_height = self.ibc_context.host_height()?; + + let proof = self + .ibc_context + .get_proof( + current_height, + &Path::ClientState(ClientStatePath::new(&client_id)), + ) + .ok_or_else(|| { + Status::not_found(format!( + "Proof unavailable for client {} at height {}", + client_id, current_height + )) + })?; + + Ok(Response::new(QueryClientStateResponse { + client_state: Some(client_state.into()), + proof, + proof_height: Some(current_height.into()), + })) + } + + async fn client_states( + &self, + _request: Request, + ) -> Result, Status> { + let client_states = self.ibc_context.client_states()?; + + Ok(Response::new(QueryClientStatesResponse { + client_states: client_states + .into_iter() + .map(|(id, state)| IdentifiedClientState { + client_id: id.into(), + client_state: Some(state.into()), + }) + .collect(), + // no support for pagination yet + pagination: None, + })) + } + + async fn consensus_state( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let client_id = ClientId::from_str(request_ref.client_id.as_str())?; + + let (height, consensus_state) = if request_ref.latest_height { + self.ibc_context + .consensus_states(&client_id)? + .into_iter() + .max_by_key(|(h, _)| *h) + .ok_or_else(|| { + Status::not_found(format!( + "Consensus state not found for client {}", + client_id + )) + })? + } else { + let height = Height::new(request_ref.revision_number, request_ref.revision_height) + .map_err(|e| Status::invalid_argument(e.to_string()))?; + let consensus_state = self + .ibc_context + .consensus_state(&ClientConsensusStatePath::new(&client_id, &height))?; + + (height, consensus_state) + }; + + let current_height = self.ibc_context.host_height()?; + + let proof = self + .ibc_context + .get_proof( + current_height, + &Path::ClientConsensusState(ClientConsensusStatePath::new(&client_id, &height)), + ) + .ok_or_else(|| { + Status::not_found(format!( + "Consensus state not found for client {} at height {}", + client_id, height + )) + })?; + + Ok(Response::new(QueryConsensusStateResponse { + consensus_state: Some(consensus_state.into()), + proof, + proof_height: Some(current_height.into()), + })) + } + + async fn consensus_states( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let client_id = ClientId::from_str(request_ref.client_id.as_str())?; + + let consensus_states = self.ibc_context.consensus_states(&client_id)?; + + Ok(Response::new(QueryConsensusStatesResponse { + consensus_states: consensus_states + .into_iter() + .map(|(height, state)| ConsensusStateWithHeight { + height: Some(height.into()), + consensus_state: Some(state.into()), + }) + .collect(), + // no support for pagination yet + pagination: None, + })) + } + + async fn consensus_state_heights( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let client_id = ClientId::from_str(request_ref.client_id.as_str())?; + + let consensus_state_heights = self.ibc_context.consensus_state_heights(&client_id)?; + + Ok(Response::new(QueryConsensusStateHeightsResponse { + consensus_state_heights: consensus_state_heights + .into_iter() + .map(|height| height.into()) + .collect(), + // no support for pagination yet + pagination: None, + })) + } + + async fn client_status( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let client_id = ClientId::from_str(request_ref.client_id.as_str())?; + + let client_state = self.ibc_context.client_state(&client_id)?; + let client_validation_ctx = self.ibc_context.get_client_validation_context(); + let client_status = client_state + .status(client_validation_ctx, &client_id) + .map_err(ContextError::from)?; + + Ok(Response::new(QueryClientStatusResponse { + status: format!("{}", client_status), + })) + } + + async fn client_params( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "Querying ClientParams is not supported yet", + )) + } + + async fn upgraded_client_state( + &self, + _request: Request, + ) -> Result, Status> { + let plan = self + .upgrade_context + .upgrade_plan() + .map_err(ClientError::from) + .map_err(ContextError::from)?; + + let upgraded_client_state_path = UpgradeClientPath::UpgradedClientState(plan.height); + + let upgraded_client_state = self + .upgrade_context + .upgraded_client_state(&upgraded_client_state_path) + .map_err(ClientError::from) + .map_err(ContextError::from)?; + + Ok(Response::new(QueryUpgradedClientStateResponse { + upgraded_client_state: Some(upgraded_client_state.into()), + })) + } + + async fn upgraded_consensus_state( + &self, + _request: Request, + ) -> Result, Status> { + let plan = self + .upgrade_context + .upgrade_plan() + .map_err(ClientError::from) + .map_err(ContextError::from)?; + + let upgraded_consensus_state_path = + UpgradeClientPath::UpgradedClientConsensusState(plan.height); + + let upgraded_consensus_state = self + .upgrade_context + .upgraded_consensus_state(&upgraded_consensus_state_path) + .map_err(ClientError::from) + .map_err(ContextError::from)?; + + Ok(Response::new(QueryUpgradedConsensusStateResponse { + upgraded_consensus_state: Some(upgraded_consensus_state.into()), + })) + } +} diff --git a/crates/ibc/src/services/core/connection.rs b/crates/ibc/src/services/core/connection.rs new file mode 100644 index 000000000..eaf77ca65 --- /dev/null +++ b/crates/ibc/src/services/core/connection.rs @@ -0,0 +1,227 @@ +//! [`ConnectionQueryService`](ConnectionQueryService) takes a generic `I` to store `ibc_context` that implements [`QueryContext`](QueryContext). +//! `I` must be a type where writes from one thread are readable from another. +//! This means using `Arc>` or `Arc>` in most cases. + +use alloc::str::FromStr; +use std::boxed::Box; + +use ibc_proto::google::protobuf::Any; +use ibc_proto::ibc::core::client::v1::IdentifiedClientState; +use ibc_proto::ibc::core::connection::v1::query_server::Query as ConnectionQuery; +use ibc_proto::ibc::core::connection::v1::{ + Params as ConnectionParams, QueryClientConnectionsRequest, QueryClientConnectionsResponse, + QueryConnectionClientStateRequest, QueryConnectionClientStateResponse, + QueryConnectionConsensusStateRequest, QueryConnectionConsensusStateResponse, + QueryConnectionParamsRequest, QueryConnectionParamsResponse, QueryConnectionRequest, + QueryConnectionResponse, QueryConnectionsRequest, QueryConnectionsResponse, +}; +use tonic::{Request, Response, Status}; + +use crate::core::ics24_host::identifier::{ClientId, ConnectionId}; +use crate::core::ics24_host::path::{ + ClientConnectionPath, ClientConsensusStatePath, ClientStatePath, ConnectionPath, Path, +}; +use crate::core::ValidationContext; +use crate::prelude::*; +use crate::services::core::context::QueryContext; +use crate::Height; + +// TODO(rano): currently the services don't support pagination, so we return all the results. + +/// The generic `I` must be a type where writes from one thread are readable from another. +/// This means using `Arc>` or `Arc>` in most cases. +pub struct ConnectionQueryService +where + I: QueryContext + Send + Sync + 'static, + ::AnyClientState: Into, + ::AnyConsensusState: Into, +{ + ibc_context: I, +} + +impl ConnectionQueryService +where + I: QueryContext + Send + Sync + 'static, + ::AnyClientState: Into, + ::AnyConsensusState: Into, +{ + /// The parameter `ibc_context` must be a type where writes from one thread are readable from another. + /// This means using `Arc>` or `Arc>` in most cases. + pub fn new(ibc_context: I) -> Self { + Self { ibc_context } + } +} + +#[tonic::async_trait] +impl ConnectionQuery for ConnectionQueryService +where + I: QueryContext + Send + Sync + 'static, + ::AnyClientState: Into, + ::AnyConsensusState: Into, +{ + async fn connection( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let connection_id = ConnectionId::from_str(request_ref.connection_id.as_str())?; + + let connection_end = self.ibc_context.connection_end(&connection_id)?; + + let current_height = self.ibc_context.host_height()?; + + let proof = self + .ibc_context + .get_proof( + current_height, + &Path::Connection(ConnectionPath::new(&connection_id)), + ) + .ok_or_else(|| { + Status::not_found(format!( + "Proof not found for connection path {}", + connection_id.as_str() + )) + })?; + + Ok(Response::new(QueryConnectionResponse { + connection: Some(connection_end.into()), + proof, + proof_height: Some(current_height.into()), + })) + } + + async fn connections( + &self, + _request: Request, + ) -> Result, Status> { + let connections = self.ibc_context.connection_ends()?; + + Ok(Response::new(QueryConnectionsResponse { + connections: connections.into_iter().map(Into::into).collect(), + height: Some(self.ibc_context.host_height()?.into()), + // no support for pagination yet + pagination: None, + })) + } + + async fn client_connections( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let client_id = ClientId::from_str(request_ref.client_id.as_str())?; + + let connections = self.ibc_context.client_connection_ends(&client_id)?; + + let current_height = self.ibc_context.host_height()?; + + let proof: Vec = self + .ibc_context + .get_proof( + current_height, + &Path::ClientConnection(ClientConnectionPath::new(&client_id)), + ) + .ok_or_else(|| { + Status::not_found(format!( + "Proof not found for client connection path {}", + client_id.as_str() + )) + })?; + + Ok(Response::new(QueryClientConnectionsResponse { + connection_paths: connections.into_iter().map(|x| x.as_str().into()).collect(), + proof, + proof_height: Some(current_height.into()), + })) + } + + async fn connection_client_state( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let connection_id = ConnectionId::from_str(request_ref.connection_id.as_str())?; + + let connection_end = self.ibc_context.connection_end(&connection_id)?; + + let client_state = self.ibc_context.client_state(connection_end.client_id())?; + + let current_height = self.ibc_context.host_height()?; + + let proof = self + .ibc_context + .get_proof( + current_height, + &Path::ClientState(ClientStatePath::new(connection_end.client_id())), + ) + .ok_or_else(|| { + Status::not_found(format!( + "Proof not found for client state path {}", + connection_end.client_id().as_str() + )) + })?; + + Ok(Response::new(QueryConnectionClientStateResponse { + identified_client_state: Some(IdentifiedClientState { + client_id: connection_end.client_id().as_str().into(), + client_state: Some(client_state.into()), + }), + proof, + proof_height: Some(current_height.into()), + })) + } + + async fn connection_consensus_state( + &self, + request: Request, + ) -> Result, Status> { + let request_ref = request.get_ref(); + + let connection_id = ConnectionId::from_str(request_ref.connection_id.as_str())?; + + let connection_end = self.ibc_context.connection_end(&connection_id)?; + + let height = Height::new(request_ref.revision_number, request_ref.revision_height) + .map_err(|e| Status::invalid_argument(e.to_string()))?; + + let consensus_path = ClientConsensusStatePath::new(connection_end.client_id(), &height); + + let consensus_state = self.ibc_context.consensus_state(&consensus_path)?; + + let current_height = self.ibc_context.host_height()?; + + let proof = self + .ibc_context + .get_proof(current_height, &Path::ClientConsensusState(consensus_path)) + .ok_or_else(|| { + Status::not_found(format!( + "Proof not found for consensus state path {}", + connection_end.client_id().as_str() + )) + })?; + + Ok(Response::new(QueryConnectionConsensusStateResponse { + consensus_state: Some(consensus_state.into()), + client_id: connection_end.client_id().as_str().into(), + proof, + proof_height: Some(current_height.into()), + })) + } + + async fn connection_params( + &self, + _request: Request, + ) -> Result, Status> { + Ok(Response::new(QueryConnectionParamsResponse { + params: Some(ConnectionParams { + max_expected_time_per_block: self + .ibc_context + .max_expected_time_per_block() + .as_secs(), + }), + })) + } +} diff --git a/crates/ibc/src/services/core/context.rs b/crates/ibc/src/services/core/context.rs new file mode 100644 index 000000000..14632f25f --- /dev/null +++ b/crates/ibc/src/services/core/context.rs @@ -0,0 +1,87 @@ +//! Required traits for blanket implementations of [`gRPC query services`](crate::services::core). + +use crate::core::ics03_connection::connection::IdentifiedConnectionEnd; +use crate::core::ics04_channel::channel::IdentifiedChannelEnd; +use crate::core::ics04_channel::packet::Sequence; +use crate::core::ics24_host::identifier::{ClientId, ConnectionId}; +use crate::core::ics24_host::path::{AckPath, ChannelEndPath, CommitmentPath, Path}; +use crate::core::{ContextError, ValidationContext}; +use crate::prelude::*; +use crate::Height; + +/// Context to be implemented by the host to provide proofs in gRPC query responses +/// +/// Trait used for the [`gRPC query services`](crate::services). +pub trait ProvableContext { + /// Returns the proof for the given path at the given height. + /// As this is in the context of IBC, the path is expected to be an [`IbcPath`](Path). + fn get_proof(&self, height: Height, path: &Path) -> Option>; +} + +/// Context to be implemented by the host that provides gRPC query services. +/// +/// Trait used for the [`gRPC query services`](crate::services). +pub trait QueryContext: ProvableContext + ValidationContext { + // Client queries + + /// Returns the list of all clients. + fn client_states( + &self, + ) -> Result::AnyClientState)>, ContextError>; + + /// Returns the list of all consensus states for the given client. + fn consensus_states( + &self, + client_id: &ClientId, + ) -> Result::AnyConsensusState)>, ContextError>; + + /// Returns the list of all heights at which consensus states for the given client are. + fn consensus_state_heights(&self, client_id: &ClientId) -> Result, ContextError>; + + // Connection queries + + /// Returns the list of all connection ends. + fn connection_ends(&self) -> Result, ContextError>; + + /// Returns the list of all connection ids of the given client. + fn client_connection_ends( + &self, + client_id: &ClientId, + ) -> Result, ContextError>; + + // Channel queries + + /// Returns the list of all channel ends. + fn channel_ends(&self) -> Result, ContextError>; + + // Packet queries + + /// Returns the list of all packet commitments for the given channel end. + fn packet_commitments( + &self, + channel_end_path: &ChannelEndPath, + ) -> Result, ContextError>; + + /// Filters the list of packet sequences for the given channel end that are acknowledged. + /// Returns all the packet acknowledgements if `sequences` is empty. + fn packet_acknowledgements( + &self, + channel_end_path: &ChannelEndPath, + sequences: impl ExactSizeIterator, + ) -> Result, ContextError>; + + /// Filters the packet sequences for the given channel end that are not received. + fn unreceived_packets( + &self, + channel_end_path: &ChannelEndPath, + sequences: impl ExactSizeIterator, + ) -> Result, ContextError>; + + /// Filters the list of packet sequences for the given channel end whose acknowledgement is not received. + /// Returns all the unreceived acknowledgements if `sequences` is empty. + fn unreceived_acks( + &self, + channel_end_path: &ChannelEndPath, + sequences: impl ExactSizeIterator, + ) -> Result, ContextError>; +} diff --git a/crates/ibc/src/services/core/mod.rs b/crates/ibc/src/services/core/mod.rs new file mode 100644 index 000000000..4404ef3e9 --- /dev/null +++ b/crates/ibc/src/services/core/mod.rs @@ -0,0 +1,4 @@ +pub mod channel; +pub mod client; +pub mod connection; +pub mod context; diff --git a/crates/ibc/src/services/error.rs b/crates/ibc/src/services/error.rs new file mode 100644 index 000000000..433a5e321 --- /dev/null +++ b/crates/ibc/src/services/error.rs @@ -0,0 +1,18 @@ +use alloc::string::ToString; + +use tonic::Status; + +use crate::core::ics24_host::identifier::IdentifierError; +use crate::core::ContextError; + +impl From for Status { + fn from(err: IdentifierError) -> Self { + Status::invalid_argument(err.to_string()) + } +} + +impl From for Status { + fn from(err: ContextError) -> Self { + Status::not_found(err.to_string()) + } +} diff --git a/crates/ibc/src/services/mod.rs b/crates/ibc/src/services/mod.rs new file mode 100644 index 000000000..d90ad3055 --- /dev/null +++ b/crates/ibc/src/services/mod.rs @@ -0,0 +1,48 @@ +//! Implementation of the gRPC services of core IBC components. +//! +//! The provided structs includes blanket implementation of their corresponding gRPC service traits, +//! if the host implements the following _context_ traits. +//! - [`ValidationContext`](crate::core::ValidationContext) +//! - [`ProvableContext`](crate::services::core::context::ProvableContext) +//! - [`QueryContext`](crate::services::core::context::QueryContext) +//! - [`UpgradeValidationContext`](crate::hosts::tendermint::upgrade_proposal::UpgradeValidationContext) +//! - Only for [`ClientQuery::upgraded_client_state`](ibc_proto::ibc::core::client::v1::query_server::Query::upgraded_client_state) and [`ClientQuery::upgraded_client_state`](ibc_proto::ibc::core::client::v1::query_server::Query::upgraded_consensus_state) +//! +//! Example +//! ```rust,ignore +//! use ibc_proto::ibc::core::{ +//! channel::v1::query_server::QueryServer as ChannelQueryServer +//! client::v1::query_server::QueryServer as ClientQueryServer, +//! connection::v1::query_server::QueryServer as ConnectionQueryServer, +//! } +//! use ibc::core::ValidationContext; +//! use ibc::hosts::tendermint::upgrade_proposal::UpgradeValidationContext; +//! use ibc::services::core::{ProvableContext, QueryContext}; +//! use ibc::services::{ChannelQueryService, ClientQueryService, ConnectionQueryService}; +//! +//! struct Ibc; +//! impl ValidationContext for Ibc { } +//! impl ProvableContext for Ibc { } +//! impl QueryContext for Ibc { } +//! +//! struct Upgrade; +//! impl UpgradeValidationContext for Upgrade { } +//! +//! let ibc = Ibc::new(); +//! let upgrade = Upgrade::new(); +//! +//! // `ibc` and `upgrade` must be thread-safe +//! +//! let client_service = ClientQueryServer::new(ClientQueryService::new(ibc.clone(), upgrade)) +//! let connection_service = ConnectionQueryServer::new(ConnectionQueryService::new(ibc.clone())) +//! let channel_service = ChannelQueryServer::new(ChannelQueryService::new(ibc)) +//! +//! let grpc_server = tonic::transport::Server::builder() +//! .add_service(client_service) +//! .add_service(connection_service) +//! .add_service(channel_service) +//! .serve(addr); +//! ``` + +pub mod core; +pub mod error;