tendermint_light_client/components/
io.rs1use std::time::Duration;
4
5use flex_error::{define_error, TraceError};
6use tendermint_rpc as rpc;
7#[cfg(feature = "rpc-client")]
8use tendermint_rpc::Client;
9
10use crate::verifier::types::{Height, LightBlock};
11
12#[cfg(feature = "tokio")]
13type TimeoutError = flex_error::DisplayOnly<tokio::time::error::Elapsed>;
14
15#[cfg(not(feature = "tokio"))]
16type TimeoutError = flex_error::NoSource;
17
18pub enum AtHeight {
20 At(Height),
22 Highest,
24}
25
26impl From<Height> for AtHeight {
27 fn from(height: Height) -> Self {
28 if height.value() == 0 {
29 Self::Highest
30 } else {
31 Self::At(height)
32 }
33 }
34}
35
36define_error! {
37 #[derive(Debug)]
38 IoError {
39 Rpc
40 [ rpc::Error ]
41 | _ | { "rpc error" },
42
43 InvalidHeight
44 | _ | {
45 "invalid height: given height must be greater than 0"
46 },
47
48 HeightTooHigh
49 {
50 height: Height,
51 latest_height: Height,
52 }
53 |e| {
54 format_args!("height ({0}) is higher than latest height ({1})",
55 e.height, e.latest_height)
56 },
57
58 InvalidValidatorSet
59 [ tendermint::Error ]
60 | _ | { "fetched validator set is invalid" },
61
62 Timeout
63 { duration: Duration }
64 [ TimeoutError ]
65 | e | {
66 format_args!("task timed out after {} ms",
67 e.duration.as_millis())
68 },
69
70 Runtime
71 [ TraceError<std::io::Error> ]
72 | _ | { "failed to initialize runtime" },
73
74 }
75}
76
77impl IoError {
78 pub fn from_rpc(err: rpc::Error) -> Self {
79 Self::from_height_too_high(&err).unwrap_or_else(|| Self::rpc(err))
80 }
81
82 pub fn from_height_too_high(err: &rpc::Error) -> Option<Self> {
83 use regex::Regex;
84
85 let err_str = err.to_string();
86
87 if err_str.contains("must be less than or equal to") {
88 let re = Regex::new(
89 r"height (\d+) must be less than or equal to the current blockchain height (\d+)",
90 )
91 .ok()?;
92
93 let captures = re.captures(&err_str)?;
94 let height = Height::try_from(captures[1].parse::<i64>().ok()?).ok()?;
95 let latest_height = Height::try_from(captures[2].parse::<i64>().ok()?).ok()?;
96
97 Some(Self::height_too_high(height, latest_height))
98 } else {
99 None
100 }
101 }
102}
103
104impl IoErrorDetail {
105 pub fn is_timeout(&self) -> Option<Duration> {
107 match self {
108 Self::Timeout(e) => Some(e.duration),
109 _ => None,
110 }
111 }
112}
113
114pub trait Io: Send + Sync {
116 fn fetch_light_block(&self, height: AtHeight) -> Result<LightBlock, IoError>;
118}
119
120impl<F: Send + Sync> Io for F
121where
122 F: Fn(AtHeight) -> Result<LightBlock, IoError>,
123{
124 fn fetch_light_block(&self, height: AtHeight) -> Result<LightBlock, IoError> {
125 self(height)
126 }
127}
128
129#[cfg(feature = "rpc-client")]
130pub use self::prod::ProdIo;
131
132#[cfg(feature = "rpc-client")]
133mod prod {
134 use tendermint::{
135 account::Id as TMAccountId, block::signed_header::SignedHeader as TMSignedHeader,
136 validator::Set as TMValidatorSet,
137 };
138 use tendermint_rpc::Paging;
139
140 use super::*;
141 use crate::{utils::block_on, verifier::types::PeerId};
142
143 #[derive(Clone, Debug)]
146 pub struct ProdIo {
147 peer_id: PeerId,
148 rpc_client: rpc::HttpClient,
149 timeout: Option<Duration>,
150 }
151
152 impl Io for ProdIo {
153 fn fetch_light_block(&self, height: AtHeight) -> Result<LightBlock, IoError> {
154 let signed_header = self.fetch_signed_header(height)?;
155 let height = signed_header.header.height;
156 let proposer_address = signed_header.header.proposer_address;
157
158 let validator_set = self.fetch_validator_set(height.into(), Some(proposer_address))?;
159 let next_validator_set = self.fetch_validator_set(height.increment().into(), None)?;
160
161 let light_block = LightBlock::new(
162 signed_header,
163 validator_set,
164 next_validator_set,
165 self.peer_id,
166 );
167
168 Ok(light_block)
169 }
170 }
171
172 impl ProdIo {
173 pub fn new(
177 peer_id: PeerId,
178 rpc_client: rpc::HttpClient, timeout: Option<Duration>,
181 ) -> Self {
182 Self {
183 peer_id,
184 rpc_client,
185 timeout,
186 }
187 }
188
189 pub fn peer_id(&self) -> PeerId {
190 self.peer_id
191 }
192
193 pub fn rpc_client(&self) -> &rpc::HttpClient {
194 &self.rpc_client
195 }
196
197 pub fn timeout(&self) -> Option<Duration> {
198 self.timeout
199 }
200
201 pub fn fetch_signed_header(&self, height: AtHeight) -> Result<TMSignedHeader, IoError> {
202 let client = self.rpc_client.clone();
203 let res = block_on(self.timeout, async move {
204 match height {
205 AtHeight::Highest => client.latest_commit().await,
206 AtHeight::At(height) => client.commit(height).await,
207 }
208 })?;
209
210 match res {
211 Ok(response) => Ok(response.signed_header),
212 Err(err) => Err(IoError::from_rpc(err)),
213 }
214 }
215
216 pub fn fetch_validator_set(
217 &self,
218 height: AtHeight,
219 proposer_address: Option<TMAccountId>,
220 ) -> Result<TMValidatorSet, IoError> {
221 let height = match height {
222 AtHeight::Highest => {
223 return Err(IoError::invalid_height());
224 },
225 AtHeight::At(height) => height,
226 };
227
228 let client = self.rpc_client.clone();
229 let response = block_on(self.timeout, async move {
230 client.validators(height, Paging::All).await
231 })?
232 .map_err(IoError::rpc)?;
233
234 let validator_set = match proposer_address {
235 Some(proposer_address) => {
236 TMValidatorSet::with_proposer(response.validators, proposer_address)
237 .map_err(IoError::invalid_validator_set)?
238 },
239 None => TMValidatorSet::without_proposer(response.validators),
240 };
241
242 Ok(validator_set)
243 }
244 }
245}