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(&current_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(&current_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(&current_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}