tendermint_light_client/components/
io.rs

1//! Provides an interface and a default implementation of the `Io` component
2
3use 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
18/// Type for selecting either a specific height or the latest one
19pub enum AtHeight {
20    /// A specific height
21    At(Height),
22    /// The latest height
23    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    /// Whether this error means that a timeout occurred when querying a node.
106    pub fn is_timeout(&self) -> Option<Duration> {
107        match self {
108            Self::Timeout(e) => Some(e.duration),
109            _ => None,
110        }
111    }
112}
113
114/// Interface for fetching light blocks from a full node, typically via the RPC client.
115pub trait Io: Send + Sync {
116    /// Fetch a light block at the given height from a peer
117    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    /// Production implementation of the Io component, which fetches
144    /// light blocks from full nodes via RPC.
145    #[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        /// Constructs a new ProdIo component.
174        ///
175        /// A peer map which maps peer IDS to their network address must be supplied.
176        pub fn new(
177            peer_id: PeerId,
178            rpc_client: rpc::HttpClient, /* TODO(thane): Generalize over client transport
179                                          * (instead of using HttpClient directly) */
180            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}