Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

ibc: handle height in gRPC request metadata if specified #4905

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/core/component/ibc/src/component/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use super::HostInterface;
mod client_query;
mod connection_query;
mod consensus_query;
mod utils;

use std::marker::PhantomData;

Expand Down
17 changes: 15 additions & 2 deletions crates/core/component/ibc/src/component/rpc/client_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -34,7 +35,13 @@ impl<HI: HostInterface + Send + Sync + 'static> ClientQuery for IbcQuery<HI> {
&self,
request: tonic::Request<QueryClientStateRequest>,
) -> std::result::Result<Response<QueryClientStateResponse>, 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}")))?;

Expand Down Expand Up @@ -111,7 +118,13 @@ impl<HI: HostInterface + Send + Sync + 'static> ClientQuery for IbcQuery<HI> {
&self,
request: tonic::Request<QueryConsensusStateRequest>,
) -> std::result::Result<tonic::Response<QueryConsensusStateResponse>, 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 {
Expand Down
10 changes: 9 additions & 1 deletion crates/core/component/ibc/src/component/rpc/connection_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,7 +34,14 @@ impl<HI: HostInterface + Send + Sync + 'static> ConnectionQuery for IbcQuery<HI>
request: tonic::Request<QueryConnectionRequest>,
) -> std::result::Result<tonic::Response<QueryConnectionResponse>, 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}")))?;

Expand Down
23 changes: 20 additions & 3 deletions crates/core/component/ibc/src/component/rpc/consensus_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -364,7 +365,12 @@ impl<HI: HostInterface + Send + Sync + 'static> ConsensusQuery for IbcQuery<HI>
&self,
request: tonic::Request<QueryPacketCommitmentRequest>,
) -> std::result::Result<tonic::Response<QueryPacketCommitmentResponse>, 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}")))?;
Expand Down Expand Up @@ -473,7 +479,12 @@ impl<HI: HostInterface + Send + Sync + 'static> ConsensusQuery for IbcQuery<HI>
&self,
request: tonic::Request<QueryPacketReceiptRequest>,
) -> std::result::Result<tonic::Response<QueryPacketReceiptResponse>, 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}")))?;
Expand Down Expand Up @@ -513,7 +524,13 @@ impl<HI: HostInterface + Send + Sync + 'static> ConsensusQuery for IbcQuery<HI>
request: tonic::Request<QueryPacketAcknowledgementRequest>,
) -> std::result::Result<tonic::Response<QueryPacketAcknowledgementResponse>, 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())
Expand Down
160 changes: 160 additions & 0 deletions crates/core/component/ibc/src/component/rpc/utils.rs
Original file line number Diff line number Diff line change
@@ -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<Snapshot> {
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<Height> {
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<Self, Self::Err> {
const FORM: &str = "input was not of the form '0' or '<number>-<height>'";

let mut parts = input.split('-');

let revision_number = parts
.next()
.context(FORM)?
.parse::<u64>()
.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::<u64>()
.context("failed to parse revision height as u64")?,
};

Ok(TheHeight(Height {
revision_number,
revision_height,
}))
}
}

fn parse_as_ibc_height(input: &str) -> anyhow::Result<Height> {
let height = input
.trim()
.parse::<TheHeight>()
.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::<TheHeight>().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));
}
}
Loading