penumbra_sdk_ibc/component/rpc/
utils.rs1use 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#[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#[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}