penumbra_sdk_ibc/component/rpc/
utils.rs

1use std::str::FromStr;
2
3use anyhow::bail;
4use anyhow::Context as _;
5use cnidarium::Snapshot;
6use cnidarium::Storage;
7use ibc_proto::ibc::core::client::v1::Height;
8use tracing::debug;
9use tracing::instrument;
10
11type Type = tonic::metadata::MetadataMap;
12
13/// Determines which state snapshot to open given the height header in a [`MetadataMap`].
14///
15/// Returns the latest snapshot if the height header is 0, 0-0, or absent.
16#[instrument(skip_all, level = "debug")]
17pub(in crate::component::rpc) fn determine_snapshot_from_metadata(
18    storage: Storage,
19    metadata: &Type,
20) -> anyhow::Result<Snapshot> {
21    let height = determine_height_from_metadata(metadata)
22        .context("failed to determine height from metadata")?;
23
24    debug!(?height, "determining snapshot from height header");
25
26    if height.revision_height == 0 {
27        Ok(storage.latest_snapshot())
28    } else {
29        storage
30            .snapshot(height.revision_height)
31            .context("failed to create state snapshot from IBC height header")
32    }
33}
34
35#[instrument(skip_all, level = "debug")]
36fn determine_height_from_metadata(
37    metadata: &tonic::metadata::MetadataMap,
38) -> anyhow::Result<Height> {
39    match metadata.get("height") {
40        None => {
41            debug!("height header was missing; assuming a height of 0");
42            Ok(TheHeight::zero().into_inner())
43        }
44        Some(entry) => entry
45            .to_str()
46            .context("height header was present but its entry was not ASCII")
47            .and_then(parse_as_ibc_height)
48            .context("failed to parse height header as IBC height"),
49    }
50}
51
52/// Newtype wrapper around [`Height`] to implement [`FromStr`].
53#[derive(Debug)]
54struct TheHeight(Height);
55
56impl TheHeight {
57    fn zero() -> Self {
58        Self(Height {
59            revision_number: 0,
60            revision_height: 0,
61        })
62    }
63    fn into_inner(self) -> Height {
64        self.0
65    }
66}
67
68impl FromStr for TheHeight {
69    type Err = anyhow::Error;
70
71    fn from_str(input: &str) -> Result<Self, Self::Err> {
72        const FORM: &str = "input was not of the form '0' or '<number>-<height>'";
73
74        let mut parts = input.split('-');
75
76        let revision_number = parts
77            .next()
78            .context(FORM)?
79            .parse::<u64>()
80            .context("failed to parse revision number as u64")?;
81        let revision_height = match parts.next() {
82            None if revision_number == 0 => return Ok(Self::zero()),
83            None => bail!(FORM),
84            Some(rev_height) => rev_height
85                .parse::<u64>()
86                .context("failed to parse revision height as u64")?,
87        };
88
89        Ok(TheHeight(Height {
90            revision_number,
91            revision_height,
92        }))
93    }
94}
95
96fn parse_as_ibc_height(input: &str) -> anyhow::Result<Height> {
97    let height = input
98        .trim()
99        .parse::<TheHeight>()
100        .context("failed to parse as IBC height")?
101        .into_inner();
102
103    Ok(height)
104}
105
106#[cfg(test)]
107mod tests {
108    use ibc_proto::ibc::core::client::v1::Height;
109    use tonic::metadata::MetadataMap;
110
111    use crate::component::rpc::utils::determine_height_from_metadata;
112
113    use super::TheHeight;
114
115    fn zero() -> Height {
116        Height {
117            revision_number: 0,
118            revision_height: 0,
119        }
120    }
121
122    fn height(revision_number: u64, revision_height: u64) -> Height {
123        Height {
124            revision_number,
125            revision_height,
126        }
127    }
128
129    #[track_caller]
130    fn assert_ibc_height_is_parsed_correctly(input: &str, expected: Height) {
131        let actual = input.parse::<TheHeight>().unwrap().into_inner();
132        assert_eq!(expected, actual);
133    }
134
135    #[test]
136    fn parse_ibc_height() {
137        assert_ibc_height_is_parsed_correctly("0", zero());
138        assert_ibc_height_is_parsed_correctly("0-0", zero());
139        assert_ibc_height_is_parsed_correctly("0-1", height(0, 1));
140        assert_ibc_height_is_parsed_correctly("1-0", height(1, 0));
141        assert_ibc_height_is_parsed_correctly("1-1", height(1, 1));
142    }
143
144    #[track_caller]
145    fn assert_ibc_height_is_determined_correctly(input: Option<&str>, expected: Height) {
146        let mut metadata = MetadataMap::new();
147        if let Some(input) = input {
148            metadata.insert("height", input.parse().unwrap());
149        }
150        let actual = determine_height_from_metadata(&metadata).unwrap();
151        assert_eq!(expected, actual);
152    }
153
154    #[test]
155    fn determine_ibc_height_from_metadata() {
156        assert_ibc_height_is_determined_correctly(None, zero());
157        assert_ibc_height_is_determined_correctly(Some("0"), zero());
158        assert_ibc_height_is_determined_correctly(Some("0-0"), zero());
159        assert_ibc_height_is_determined_correctly(Some("0-1"), height(0, 1));
160        assert_ibc_height_is_determined_correctly(Some("1-0"), height(1, 0));
161        assert_ibc_height_is_determined_correctly(Some("1-1"), height(1, 1));
162    }
163}