cometbft_light_client/light_client.rs
1//! Light client implementation as per the [Core Verification specification][1].
2//!
3//! [1]: https://github.com/cometbft/cometbft-rs/blob/main/docs/spec/lightclient/verification/verification.md
4
5use core::fmt;
6
7use contracts::*;
8
9// Re-export for backward compatibility
10pub use crate::verifier::options::Options;
11use crate::{
12 components::{clock::Clock, io::*, scheduler::*},
13 contracts::*,
14 errors::Error,
15 state::State,
16 verifier::{
17 types::{Height, LightBlock, PeerId, Status},
18 Verdict, Verifier,
19 },
20};
21
22/// The light client implements a read operation of a header from the blockchain,
23/// by communicating with full nodes. As full nodes may be faulty, it cannot trust
24/// the received information, but the light client has to check whether the header
25/// it receives coincides with the one generated by Tendermint consensus.
26///
27/// In the CometBFT blockchain, the validator set may change with every new block.
28/// The staking and unbonding mechanism induces a security model: starting at time
29/// of the header, more than two-thirds of the next validators of a new block are
30/// correct for the duration of the trusted period. The fault-tolerant read operation
31/// is designed for this security model.
32pub struct LightClient {
33 /// The peer id of the peer this client is connected to
34 pub peer: PeerId,
35 /// Options for this light client
36 pub options: Options,
37
38 clock: Box<dyn Clock>,
39 scheduler: Box<dyn Scheduler>,
40 verifier: Box<dyn Verifier>,
41 io: Box<dyn Io>,
42}
43
44impl fmt::Debug for LightClient {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 f.debug_struct("LightClient")
47 .field("peer", &self.peer)
48 .field("options", &self.options)
49 .finish()
50 }
51}
52
53impl LightClient {
54 /// Constructs a new light client
55 pub fn new(
56 peer: PeerId,
57 options: Options,
58 clock: impl Clock + 'static,
59 scheduler: impl Scheduler + 'static,
60 verifier: impl Verifier + 'static,
61 io: impl Io + 'static,
62 ) -> Self {
63 Self {
64 peer,
65 options,
66 clock: Box::new(clock),
67 scheduler: Box::new(scheduler),
68 verifier: Box::new(verifier),
69 io: Box::new(io),
70 }
71 }
72
73 /// Constructs a new light client from boxed components
74 pub fn from_boxed(
75 peer: PeerId,
76 options: Options,
77 clock: Box<dyn Clock>,
78 scheduler: Box<dyn Scheduler>,
79 verifier: Box<dyn Verifier>,
80 io: Box<dyn Io>,
81 ) -> Self {
82 Self {
83 peer,
84 options,
85 clock,
86 scheduler,
87 verifier,
88 io,
89 }
90 }
91
92 /// Attempt to update the light client to the highest block of the primary node.
93 ///
94 /// Note: This function delegates the actual work to `verify_to_target`.
95 pub fn verify_to_highest(&mut self, state: &mut State) -> Result<LightBlock, Error> {
96 let target_block = self
97 .io
98 .fetch_light_block(AtHeight::Highest)
99 .map_err(Error::io)?;
100
101 self.verify_to_target(target_block.height(), state)
102 }
103
104 /// Update the light client to a block of the primary node at the given height.
105 ///
106 /// This is the main function and uses the following components:
107 ///
108 /// - The I/O component is called to fetch the next light block. It is the only component that
109 /// communicates with other nodes.
110 /// - The Verifier component checks whether a header is valid and checks if a new light block
111 /// should be trusted based on a previously verified light block.
112 /// - When doing _forward_ verification, the Scheduler component decides which height to try to
113 /// verify next, in case the current block pass verification but cannot be trusted yet.
114 /// - When doing _backward_ verification, the Hasher component is used to determine whether the
115 /// `last_block_id` hash of a block matches the hash of the block right below it.
116 ///
117 /// ## Implements
118 /// - [LCV-DIST-SAFE.1]
119 /// - [LCV-DIST-LIFE.1]
120 /// - [LCV-PRE-TP.1]
121 /// - [LCV-POST-LS.1]
122 /// - [LCV-INV-TP.1]
123 ///
124 /// ## Postcondition
125 /// - The light store contains a light block that corresponds to a block of the blockchain of
126 /// height `target_height` [LCV-POST-LS.1]
127 ///
128 /// ## Error conditions
129 /// - The light store does not contains a trusted light block within the trusting period
130 /// [LCV-PRE-TP.1]
131 /// - If the core verification loop invariant is violated [LCV-INV-TP.1]
132 /// - If verification of a light block fails
133 /// - If the fetching a light block from the primary node fails
134 #[allow(clippy::nonminimal_bool)]
135 #[ensures(
136 ret.is_ok() -> trusted_store_contains_block_at_target_height(
137 state.light_store.as_ref(),
138 target_height,
139 )
140 )]
141 pub fn verify_to_target(
142 &self,
143 target_height: Height,
144 state: &mut State,
145 ) -> Result<LightBlock, Error> {
146 // Let's first look in the store to see whether
147 // we have already successfully verified this block.
148 if let Some(light_block) = state.light_store.get_trusted_or_verified(target_height) {
149 return Ok(light_block);
150 }
151
152 // Get the highest trusted state
153 let highest = state
154 .light_store
155 .highest_trusted_or_verified_before(target_height)
156 .or_else(|| state.light_store.lowest_trusted_or_verified())
157 .ok_or_else(Error::no_initial_trusted_state)?;
158
159 if target_height >= highest.height() {
160 // Perform forward verification with bisection
161 self.verify_forward(target_height, state)
162 } else {
163 // Perform sequential backward verification
164 self.verify_backward(target_height, state)
165 }
166 }
167
168 /// Perform forward verification with bisection.
169 fn verify_forward(
170 &self,
171 target_height: Height,
172 state: &mut State,
173 ) -> Result<LightBlock, Error> {
174 let mut current_height = target_height;
175
176 loop {
177 let now = self.clock.now();
178
179 // Get the latest trusted state
180 let trusted_block = state
181 .light_store
182 .highest_trusted_or_verified_before(target_height)
183 .ok_or_else(Error::no_initial_trusted_state)?;
184
185 if target_height < trusted_block.height() {
186 return Err(Error::target_lower_than_trusted_state(
187 target_height,
188 trusted_block.height(),
189 ));
190 }
191
192 // Check invariant [LCV-INV-TP.1]
193 if !is_within_trust_period(&trusted_block, self.options.trusting_period, now) {
194 return Err(Error::trusted_state_outside_trusting_period(
195 Box::new(trusted_block),
196 self.options,
197 ));
198 }
199
200 // Log the current height as a dependency of the block at the target height
201 state.trace_block(target_height, current_height);
202
203 // If the trusted state is now at a height equal to the target height, we are done.
204 // [LCV-DIST-LIFE.1]
205 if target_height == trusted_block.height() {
206 return Ok(trusted_block);
207 }
208
209 // Fetch the block at the current height from the light store if already present,
210 // or from the primary peer otherwise.
211 let (current_block, status) = self.get_or_fetch_block(current_height, state)?;
212
213 // Validate and verify the current block
214 let verdict = self.verifier.verify_update_header(
215 current_block.as_untrusted_state(),
216 trusted_block.as_trusted_state(),
217 &self.options,
218 now,
219 );
220
221 match verdict {
222 Verdict::Success => {
223 // Verification succeeded, add the block to the light store with
224 // the `Verified` status or higher if already trusted.
225 let new_status = Status::most_trusted(Status::Verified, status);
226 state.light_store.update(¤t_block, new_status);
227
228 // Log the trusted height as a dependency of the block at the current height
229 state.trace_block(current_height, trusted_block.height());
230 },
231 Verdict::Invalid(e) => {
232 // Verification failed, add the block to the light store with `Failed` status,
233 // and abort.
234 state.light_store.update(¤t_block, Status::Failed);
235
236 return Err(Error::invalid_light_block(e));
237 },
238 Verdict::NotEnoughTrust(_) => {
239 // The current block cannot be trusted because of a missing overlap in the
240 // validator sets. Add the block to the light store with
241 // the `Unverified` status. This will engage bisection in an
242 // attempt to raise the height of the highest trusted state
243 // until there is enough overlap.
244 state.light_store.update(¤t_block, Status::Unverified);
245 },
246 }
247
248 // Compute the next height to fetch and verify
249 current_height =
250 self.scheduler
251 .schedule(state.light_store.as_ref(), current_height, target_height);
252 }
253 }
254
255 /// Stub for when "unstable" feature is disabled.
256 #[doc(hidden)]
257 #[cfg(not(feature = "unstable"))]
258 fn verify_backward(
259 &self,
260 target_height: Height,
261 state: &mut State,
262 ) -> Result<LightBlock, Error> {
263 let trusted_state = state
264 .light_store
265 .highest_trusted_or_verified_before(target_height)
266 .or_else(|| state.light_store.lowest_trusted_or_verified())
267 .ok_or_else(Error::no_initial_trusted_state)?;
268
269 Err(Error::target_lower_than_trusted_state(
270 target_height,
271 trusted_state.height(),
272 ))
273 }
274
275 /// Perform sequential backward verification.
276 ///
277 /// Backward verification is implemented by taking a sliding window
278 /// of length two between the trusted state and the target block and
279 /// checking whether the last_block_id hash of the higher block
280 /// matches the computed hash of the lower block.
281 ///
282 /// ## Performance
283 /// The algorithm implemented is very inefficient in case the target
284 /// block is much lower than the highest trusted state.
285 /// For a trusted state at height `T`, and a target block at height `H`,
286 /// it will fetch and check hashes of `T - H` blocks.
287 ///
288 /// ## Stability
289 /// This feature is only available if the `unstable` flag of is enabled.
290 /// If the flag is disabled, then any attempt to verify a block whose
291 /// height is lower than the highest trusted state will result in a
292 /// `TargetLowerThanTrustedState` error.
293 #[cfg(feature = "unstable")]
294 fn verify_backward(
295 &self,
296 target_height: Height,
297 state: &mut State,
298 ) -> Result<LightBlock, Error> {
299 use cometbft::crypto::default::Sha256;
300
301 let root = state
302 .light_store
303 .highest_trusted_or_verified_before(target_height)
304 .or_else(|| state.light_store.lowest_trusted_or_verified())
305 .ok_or_else(Error::no_initial_trusted_state)?;
306
307 assert!(root.height() >= target_height);
308
309 // Check invariant [LCV-INV-TP.1]
310 if !is_within_trust_period(&root, self.options.trusting_period, self.clock.now()) {
311 return Err(Error::trusted_state_outside_trusting_period(
312 Box::new(root),
313 self.options,
314 ));
315 }
316
317 // Compute a range of `Height`s from `trusted_height - 1` to `target_height`, inclusive.
318 let range = (target_height.value()..root.height().value()).rev();
319 let heights = range.map(|h| Height::try_from(h).unwrap());
320
321 let mut latest = root;
322
323 for height in heights {
324 let (current, _status) = self.get_or_fetch_block(height, state)?;
325
326 let latest_last_block_id = latest
327 .signed_header
328 .header
329 .last_block_id
330 .ok_or_else(|| Error::missing_last_block_id(latest.height()))?;
331
332 let current_hash = current.signed_header.header.hash_with::<Sha256>();
333
334 if current_hash != latest_last_block_id.hash {
335 return Err(Error::invalid_adjacent_headers(
336 current_hash,
337 latest_last_block_id.hash,
338 ));
339 }
340
341 // `latest` and `current` are linked together by `last_block_id`,
342 // therefore it is not relevant which we verified first.
343 // For consistency, we say that `latest` was verifed using
344 // `current` so that the trace is always pointing down the chain.
345 state.light_store.insert(current.clone(), Status::Trusted);
346 state.light_store.insert(latest.clone(), Status::Trusted);
347 state.trace_block(latest.height(), current.height());
348
349 latest = current;
350 }
351
352 // We reached the target height.
353 assert_eq!(latest.height(), target_height);
354
355 Ok(latest)
356 }
357
358 /// Look in the light store for a block from the given peer at the given height,
359 /// which has not previously failed verification (ie. its status is not `Failed`).
360 ///
361 /// If one cannot be found, fetch the block from the given peer and store
362 /// it in the light store with `Unverified` status.
363 ///
364 /// ## Postcondition
365 /// - The provider of block that is returned matches the given peer.
366 #[ensures(ret.as_ref().map(|(lb, _)| lb.provider == self.peer).unwrap_or(true))]
367 pub fn get_or_fetch_block(
368 &self,
369 height: Height,
370 state: &mut State,
371 ) -> Result<(LightBlock, Status), Error> {
372 let block = state.light_store.get_non_failed(height);
373
374 if let Some(block) = block {
375 return Ok(block);
376 }
377
378 let block = self
379 .io
380 .fetch_light_block(AtHeight::At(height))
381 .map_err(Error::io)?;
382
383 state.light_store.insert(block.clone(), Status::Unverified);
384
385 Ok((block, Status::Unverified))
386 }
387
388 /// Get the block at the given height or the latest block from the chain if the given height is
389 /// lower than the latest height.
390 pub fn get_target_block_or_latest(
391 &mut self,
392 height: Height,
393 state: &mut State,
394 ) -> Result<TargetOrLatest, Error> {
395 let block = state.light_store.get_non_failed(height);
396
397 if let Some((block, _)) = block {
398 return Ok(TargetOrLatest::Target(block));
399 }
400
401 let block = self.io.fetch_light_block(AtHeight::At(height));
402
403 if let Ok(block) = block {
404 return Ok(TargetOrLatest::Target(block));
405 }
406
407 let latest = self
408 .io
409 .fetch_light_block(AtHeight::Highest)
410 .map_err(Error::io)?;
411
412 if latest.height() == height {
413 Ok(TargetOrLatest::Target(latest))
414 } else {
415 Ok(TargetOrLatest::Latest(latest))
416 }
417 }
418}
419
420pub enum TargetOrLatest {
421 Latest(LightBlock),
422 Target(LightBlock),
423}