diff --git a/crates/core/component/ibc/src/component/rpc.rs b/crates/core/component/ibc/src/component/rpc.rs index 4d125b1350..4b54f52105 100644 --- a/crates/core/component/ibc/src/component/rpc.rs +++ b/crates/core/component/ibc/src/component/rpc.rs @@ -6,6 +6,7 @@ use super::HostInterface; mod client_query; mod connection_query; mod consensus_query; +mod utils; use std::marker::PhantomData; diff --git a/crates/core/component/ibc/src/component/rpc/client_query.rs b/crates/core/component/ibc/src/component/rpc/client_query.rs index 9b52f35097..6865b23b76 100644 --- a/crates/core/component/ibc/src/component/rpc/client_query.rs +++ b/crates/core/component/ibc/src/component/rpc/client_query.rs @@ -26,6 +26,7 @@ use crate::component::{ClientStateReadExt, HostInterface}; use crate::prefix::MerklePrefixExt; use crate::IBC_COMMITMENT_PREFIX; +use super::utils::determine_snapshot_from_metadata; use super::IbcQuery; #[async_trait] @@ -34,7 +35,13 @@ impl ClientQuery for IbcQuery { &self, request: tonic::Request, ) -> std::result::Result, Status> { - let snapshot = self.storage.latest_snapshot(); + let snapshot = match determine_snapshot_from_metadata(self.storage.clone(), request.metadata()) { + Err(err) => return Err(tonic::Status::aborted( + format!("could not determine the correct snapshot to open given the `\"height\"` header of the request: {err:#}") + )), + Ok(snapshot) => snapshot, + }; + let client_id = ClientId::from_str(&request.get_ref().client_id) .map_err(|e| tonic::Status::invalid_argument(format!("invalid client id: {e}")))?; let height = Height { @@ -109,7 +116,13 @@ impl ClientQuery for IbcQuery { &self, request: tonic::Request, ) -> std::result::Result, tonic::Status> { - let snapshot = self.storage.latest_snapshot(); + let snapshot = match determine_snapshot_from_metadata(self.storage.clone(), request.metadata()) { + Err(err) => return Err(tonic::Status::aborted( + format!("could not determine the correct snapshot to open given the `\"height\"` header of the request: {err:#}") + )), + Ok(snapshot) => snapshot, + }; + let client_id = ClientId::from_str(&request.get_ref().client_id) .map_err(|e| tonic::Status::invalid_argument(format!("invalid client id: {e}")))?; let height = if request.get_ref().latest_height { diff --git a/crates/core/component/ibc/src/component/rpc/connection_query.rs b/crates/core/component/ibc/src/component/rpc/connection_query.rs index ae98d9f381..2046b8be98 100644 --- a/crates/core/component/ibc/src/component/rpc/connection_query.rs +++ b/crates/core/component/ibc/src/component/rpc/connection_query.rs @@ -19,6 +19,7 @@ use ibc_types::DomainType; use prost::Message; use std::str::FromStr; +use crate::component::rpc::utils::determine_snapshot_from_metadata; use crate::component::{ConnectionStateReadExt, HostInterface}; use crate::prefix::MerklePrefixExt; use crate::IBC_COMMITMENT_PREFIX; @@ -33,7 +34,14 @@ impl ConnectionQuery for IbcQuery request: tonic::Request, ) -> std::result::Result, tonic::Status> { tracing::debug!("querying connection {:?}", request); - let snapshot = self.storage.latest_snapshot(); + + let snapshot = match determine_snapshot_from_metadata(self.storage.clone(), request.metadata()) { + Err(err) => return Err(tonic::Status::aborted( + format!("could not determine the correct snapshot to open given the `\"height\"` header of the request: {err:#}") + )), + Ok(snapshot) => snapshot, + }; + let connection_id = &ConnectionId::from_str(&request.get_ref().connection_id) .map_err(|e| tonic::Status::aborted(format!("invalid connection id: {e}")))?; diff --git a/crates/core/component/ibc/src/component/rpc/consensus_query.rs b/crates/core/component/ibc/src/component/rpc/consensus_query.rs index 39221e2384..f8d58dc681 100644 --- a/crates/core/component/ibc/src/component/rpc/consensus_query.rs +++ b/crates/core/component/ibc/src/component/rpc/consensus_query.rs @@ -31,6 +31,7 @@ use std::str::FromStr; use crate::component::{ChannelStateReadExt, ConnectionStateReadExt, HostInterface}; +use super::utils::determine_snapshot_from_metadata; use super::IbcQuery; #[async_trait] @@ -349,7 +350,12 @@ impl ConsensusQuery for IbcQuery &self, request: tonic::Request, ) -> std::result::Result, tonic::Status> { - let snapshot = self.storage.latest_snapshot(); + let snapshot = match determine_snapshot_from_metadata(self.storage.clone(), request.metadata()) { + Err(err) => return Err(tonic::Status::aborted( + format!("could not determine the correct snapshot to open given the `\"height\"` header of the request: {err:#}") + )), + Ok(snapshot) => snapshot, + }; let port_id = PortId::from_str(&request.get_ref().port_id) .map_err(|e| tonic::Status::aborted(format!("invalid port id: {e}")))?; @@ -453,7 +459,12 @@ impl ConsensusQuery for IbcQuery &self, request: tonic::Request, ) -> std::result::Result, tonic::Status> { - let snapshot = self.storage.latest_snapshot(); + let snapshot = match determine_snapshot_from_metadata(self.storage.clone(), request.metadata()) { + Err(err) => return Err(tonic::Status::aborted( + format!("could not determine the correct snapshot to open given the `\"height\"` header of the request: {err:#}") + )), + Ok(snapshot) => snapshot, + }; let port_id = PortId::from_str(&request.get_ref().port_id) .map_err(|e| tonic::Status::aborted(format!("invalid port id: {e}")))?; @@ -488,7 +499,13 @@ impl ConsensusQuery for IbcQuery request: tonic::Request, ) -> std::result::Result, tonic::Status> { - let snapshot = self.storage.latest_snapshot(); + let snapshot = match determine_snapshot_from_metadata(self.storage.clone(), request.metadata()) { + Err(err) => return Err(tonic::Status::aborted( + format!("could not determine the correct snapshot to open given the `\"height\"` header of the request: {err:#}") + )), + Ok(snapshot) => snapshot, + }; + let channel_id = ChannelId::from_str(request.get_ref().channel_id.as_str()) .map_err(|e| tonic::Status::aborted(format!("invalid channel id: {e}")))?; let port_id = PortId::from_str(request.get_ref().port_id.as_str()) diff --git a/crates/core/component/ibc/src/component/rpc/utils.rs b/crates/core/component/ibc/src/component/rpc/utils.rs new file mode 100644 index 0000000000..642d4bcff2 --- /dev/null +++ b/crates/core/component/ibc/src/component/rpc/utils.rs @@ -0,0 +1,160 @@ +use std::str::FromStr; + +use anyhow::bail; +use anyhow::Context as _; +use cnidarium::Snapshot; +use cnidarium::Storage; +use ibc_proto::ibc::core::client::v1::Height; +use tracing::debug; +use tracing::instrument; + +type Type = tonic::metadata::MetadataMap; + +/// Determines which state snapshot to open given the height header in a [`MetadataMap`]. +/// +/// Returns the latest snapshot if the height header is 0, 0-0, or absent. +#[instrument(skip_all, level = "debug")] +pub(in crate::component::rpc) fn determine_snapshot_from_metadata( + storage: Storage, + metadata: &Type, +) -> anyhow::Result { + let height = determine_height_from_metadata(metadata) + .context("failed to determine height from metadata")?; + if height.revision_height == 0 { + Ok(storage.latest_snapshot()) + } else { + storage + .snapshot(height.revision_height) + .context("failed to create state snapshot from IBC height in height header") + } +} + +#[instrument(skip_all, level = "debug")] +fn determine_height_from_metadata( + metadata: &tonic::metadata::MetadataMap, +) -> anyhow::Result { + match metadata.get("height") { + None => { + debug!("height header was missing; assuming a height of 0"); + Ok(TheHeight::zero().into_inner()) + } + Some(entry) => entry + .to_str() + .context("height header was present but its entry was not ASCII") + .and_then(parse_as_ibc_height) + .context("failed to parse height header as IBC height"), + } +} + +/// Newtype wrapper around [`Height`] to implement [`FromStr`]. +#[derive(Debug)] +struct TheHeight(Height); + +impl TheHeight { + fn zero() -> Self { + Self(Height { + revision_number: 0, + revision_height: 0, + }) + } + fn into_inner(self) -> Height { + self.0 + } +} + +impl FromStr for TheHeight { + type Err = anyhow::Error; + + fn from_str(input: &str) -> Result { + const FORM: &str = "input was not of the form '0' or '-'"; + + let mut parts = input.split('-'); + + let revision_number = parts + .next() + .context(FORM)? + .parse::() + .context("failed to parse revision number as u64")?; + let revision_height = match parts.next() { + None if revision_number == 0 => return Ok(Self::zero()), + None => bail!(FORM), + Some(rev_height) => rev_height + .parse::() + .context("failed to parse revision height as u64")?, + }; + + Ok(TheHeight(Height { + revision_number, + revision_height, + })) + } +} + +fn parse_as_ibc_height(input: &str) -> anyhow::Result { + let height = input + .trim() + .parse::() + .context("failed to parse as IBC height")? + .into_inner(); + + Ok(height) +} + +#[cfg(test)] +mod tests { + use ibc_proto::ibc::core::client::v1::Height; + use tonic::metadata::MetadataMap; + + use crate::component::rpc::utils::determine_height_from_metadata; + + use super::TheHeight; + + fn zero() -> Height { + Height { + revision_number: 0, + revision_height: 0, + } + } + + fn height(revision_number: u64, revision_height: u64) -> Height { + Height { + revision_number, + revision_height, + } + } + + #[track_caller] + fn assert_ibc_height_is_parsed_correctly(input: &str, expected: Height) { + let actual = input.parse::().unwrap().into_inner(); + assert_eq!(expected, actual); + } + + #[test] + fn parse_ibc_height() { + assert_ibc_height_is_parsed_correctly("0", zero()); + assert_ibc_height_is_parsed_correctly("0-0", zero()); + assert_ibc_height_is_parsed_correctly("0-1", height(0, 1)); + assert_ibc_height_is_parsed_correctly("1-0", height(1, 0)); + assert_ibc_height_is_parsed_correctly("1-1", height(1, 1)); + } + + #[track_caller] + fn assert_ibc_height_is_determined_correctly(input: Option<&str>, expected: Height) { + let mut metadata = MetadataMap::new(); + if let Some(input) = input { + metadata.insert("height", input.parse().unwrap()); + } + let actual = determine_height_from_metadata(&metadata).unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn determine_ibc_height_from_metadata() { + assert_ibc_height_is_determined_correctly(None, zero()); + assert_ibc_height_is_determined_correctly(Some("0"), zero()); + assert_ibc_height_is_determined_correctly(Some("0-0"), zero()); + assert_ibc_height_is_determined_correctly(Some("0-1"), height(0, 1)); + assert_ibc_height_is_determined_correctly(Some("1-0"), height(1, 0)); + assert_ibc_height_is_determined_correctly(Some("1-1"), height(1, 1)); + } +}