Skip to main content

iota_graphql_rpc/types/
checkpoint.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{
6    collections::{BTreeMap, BTreeSet, HashMap},
7    ops::RangeInclusive,
8};
9
10use async_graphql::{
11    connection::{Connection, CursorType, Edge},
12    dataloader::Loader,
13    *,
14};
15use diesel::{ExpressionMethods, OptionalExtension, QueryDsl};
16use fastcrypto::encoding::{Base58, Encoding};
17use iota_indexer::{
18    models::checkpoints::StoredCheckpoint, pruning::CommitterTables, schema::checkpoints,
19};
20use iota_sdk_types::{CheckpointDigest, CheckpointSummary};
21use serde::{Deserialize, Serialize};
22
23use crate::{
24    config::DEFAULT_PAGE_SIZE,
25    connection::ScanConnection,
26    consistency::Checkpointed,
27    data::{Conn, DataLoader, Db, DbConnection, QueryExecutor},
28    error::Error,
29    types::{
30        base64::Base64,
31        cursor::{self, Page, ScanLimited, Target},
32        date_time::DateTime,
33        digest::Digest,
34        epoch::Epoch,
35        gas::GasCostSummary,
36        transaction_block::{self, TransactionBlock, TransactionBlockFilter},
37        uint53::UInt53,
38    },
39};
40
41/// Filter either by the digest, or the sequence number, or neither, to get the
42/// latest checkpoint.
43#[derive(Default, InputObject)]
44pub(crate) struct CheckpointId {
45    pub digest: Option<Digest>,
46    pub sequence_number: Option<UInt53>,
47}
48
49/// `DataLoader` key for fetching a `Checkpoint` by its sequence number,
50/// constrained by a consistency cursor.
51#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)]
52struct SeqNumKey {
53    pub sequence_number: u64,
54    /// The digest is not used for fetching, but is used as an additional
55    /// filter, to correctly implement a request that sets both a sequence
56    /// number and a digest.
57    pub digest: Option<Digest>,
58    pub checkpoint_viewed_at: u64,
59}
60
61/// DataLoader key for fetching a `Checkpoint` by its digest, optionally
62/// constrained by a consistency cursor.
63#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)]
64struct DigestKey {
65    pub digest: Digest,
66    pub checkpoint_viewed_at: u64,
67}
68
69#[derive(Clone)]
70pub(crate) struct Checkpoint {
71    /// Representation of transaction data in the Indexer's Store. The indexer
72    /// stores the transaction data and its effects together, in one table.
73    pub stored: StoredCheckpoint,
74    /// The checkpoint_sequence_number at which this was viewed at.
75    pub checkpoint_viewed_at: u64,
76}
77
78pub(crate) type Cursor = cursor::JsonCursor<CheckpointCursor>;
79
80/// The cursor returned for each `Checkpoint` in a connection's page of results.
81/// The `checkpoint_viewed_at` will set the consistent upper bound for
82/// subsequent queries made on this cursor.
83#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
84pub(crate) struct CheckpointCursor {
85    /// The checkpoint sequence number this was viewed at.
86    #[serde(rename = "c")]
87    pub checkpoint_viewed_at: UInt53,
88    #[serde(rename = "s")]
89    pub sequence_number: UInt53,
90}
91
92/// Checkpoints contain finalized transactions and are used for node
93/// synchronization and global transaction ordering.
94#[Object]
95impl Checkpoint {
96    /// A 32-byte hash that uniquely identifies the checkpoint contents, encoded
97    /// in Base58. This hash can be used to verify checkpoint contents by
98    /// checking signatures against the committee, Hashing contents to match
99    /// digest, and checking that the previous checkpoint digest matches.
100    #[graphql(complexity = 0)]
101    async fn digest(&self) -> Result<String> {
102        Ok(self.digest_impl().extend()?.to_base58())
103    }
104
105    /// The Base64-encoded BCS serialization of this checkpoint's
106    /// `CheckpointSummary`.
107    ///
108    /// # Note
109    /// For older checkpoints, where this data was not recorded, `null` is
110    /// returned.
111    #[graphql(complexity = 0)]
112    async fn bcs(&self) -> Result<Option<Base64>> {
113        if self.stored.content_digest.is_none() || self.stored.version_specific_data.is_none() {
114            return Ok(None);
115        }
116        let summary = CheckpointSummary::try_from(self.stored.clone())
117            .map_err(|e| Error::Internal(format!("Failed to rebuild checkpoint summary: {e}")))
118            .extend()?;
119        let bytes = bcs::to_bytes(&summary)
120            .map_err(|e| Error::Internal(format!("Failed to serialize checkpoint summary: {e}")))
121            .extend()?;
122        Ok(Some(Base64::from(bytes)))
123    }
124
125    /// This checkpoint's position in the total order of finalized checkpoints,
126    /// agreed upon by consensus.
127    #[graphql(complexity = 0)]
128    async fn sequence_number(&self) -> Result<UInt53> {
129        UInt53::try_from(self.sequence_number_impl()).extend()
130    }
131
132    /// The timestamp at which the checkpoint is agreed to have happened
133    /// according to consensus. Transactions that access time in this
134    /// checkpoint will observe this timestamp.
135    #[graphql(complexity = 0)]
136    async fn timestamp(&self) -> Result<DateTime> {
137        DateTime::from_ms(self.stored.timestamp_ms).extend()
138    }
139
140    /// This is an aggregation of signatures from a quorum of validators for the
141    /// checkpoint proposal.
142    #[graphql(complexity = 0)]
143    async fn validator_signatures(&self) -> Base64 {
144        Base64::from(&self.stored.validator_signature)
145    }
146
147    /// The digest of the checkpoint at the previous sequence number.
148    #[graphql(complexity = 0)]
149    async fn previous_checkpoint_digest(&self) -> Option<String> {
150        self.stored
151            .previous_checkpoint_digest
152            .as_ref()
153            .map(Base58::encode)
154    }
155
156    /// The total number of transaction blocks in the network by the end of this
157    /// checkpoint.
158    #[graphql(complexity = 0)]
159    async fn network_total_transactions(&self) -> Result<Option<UInt53>> {
160        Ok(Some(
161            UInt53::try_from(self.network_total_transactions_impl()).extend()?,
162        ))
163    }
164
165    /// The computation cost, storage cost, storage rebate, and non-refundable
166    /// storage fee accumulated during this epoch, up to and including this
167    /// checkpoint. These values increase monotonically across checkpoints
168    /// in the same epoch, and reset on epoch boundaries.
169    #[graphql(complexity = 0)]
170    async fn rolling_gas_summary(&self) -> Option<GasCostSummary> {
171        Some(GasCostSummary {
172            computation_cost: self.stored.computation_cost as u64,
173            computation_cost_burned: self.stored.computation_cost_burned(),
174            storage_cost: self.stored.storage_cost as u64,
175            storage_rebate: self.stored.storage_rebate as u64,
176            non_refundable_storage_fee: self.stored.non_refundable_storage_fee as u64,
177        })
178    }
179
180    /// The epoch this checkpoint is part of.
181    async fn epoch(&self, ctx: &Context<'_>) -> Result<Option<Epoch>> {
182        Epoch::query(
183            ctx,
184            Some(self.stored.epoch as u64),
185            self.checkpoint_viewed_at,
186        )
187        .await
188        .extend()
189    }
190
191    /// Transactions in this checkpoint.
192    ///
193    /// `scanLimit` restricts the number of candidate transactions scanned when
194    /// gathering a page of results. It is required for queries that apply two
195    /// or more complex filters (on function, affected address, recipient, input
196    /// object, changed object, or wrapped or deleted object), and can be at
197    /// most `serviceConfig.maxScanLimit`. A `kind` filter cannot be
198    /// combined with any of them.
199    ///
200    /// When the scan limit is reached the page will be returned even if it has
201    /// fewer than `first` results when paginating forward (`last` when
202    /// paginating backwards). If there are more transactions to scan,
203    /// `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to
204    /// `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set
205    /// to the last transaction that was scanned as opposed to the last (or
206    /// first) transaction in the page.
207    ///
208    /// Requesting the next (or previous) page after this cursor will resume the
209    /// search, scanning the next `scanLimit` many transactions in the
210    /// direction of pagination, and so on until all transactions in the
211    /// scanning range have been visited.
212    ///
213    /// By default, the scanning range consists of all transactions in this
214    /// checkpoint.
215    ///
216    /// DEPRECATION NOTICE: Support for the combination of two or more complex
217    /// filters as discussed above will stop with the v1.38 release. `scanLimit`
218    /// will thus become obsolete and will be removed as well.
219    #[graphql(
220        complexity = "first.or(last).unwrap_or(DEFAULT_PAGE_SIZE as u64) as usize * child_complexity"
221    )]
222    async fn transaction_blocks(
223        &self,
224        ctx: &Context<'_>,
225        first: Option<u64>,
226        after: Option<transaction_block::Cursor>,
227        last: Option<u64>,
228        before: Option<transaction_block::Cursor>,
229        filter: Option<TransactionBlockFilter>,
230        #[graphql(
231            deprecation = "`scanLimit` will be removed with v1.38, along with the support for combining complex filters."
232        )]
233        scan_limit: Option<u64>,
234    ) -> Result<ScanConnection<String, TransactionBlock>> {
235        let page = Page::from_params(ctx.data_unchecked(), first, after, last, before)?;
236
237        let Some(filter) = filter
238            .unwrap_or_default()
239            .intersect(TransactionBlockFilter {
240                at_checkpoint: Some(UInt53::try_from(self.stored.sequence_number as u64).extend()?),
241                ..Default::default()
242            })
243        else {
244            return Ok(ScanConnection::new(false, false));
245        };
246
247        TransactionBlock::paginate(ctx, page, filter, self.checkpoint_viewed_at, scan_limit)
248            .await
249            .extend()
250    }
251}
252
253impl CheckpointId {
254    pub(crate) fn by_seq_num(seq_num: u64) -> Result<Self, Error> {
255        Ok(CheckpointId {
256            sequence_number: Some(seq_num.try_into()?),
257            digest: None,
258        })
259    }
260}
261
262impl Checkpoint {
263    pub(crate) fn sequence_number_impl(&self) -> u64 {
264        self.stored.sequence_number as u64
265    }
266
267    pub(crate) fn network_total_transactions_impl(&self) -> u64 {
268        self.stored.network_total_transactions as u64
269    }
270
271    pub(crate) fn digest_impl(&self) -> Result<CheckpointDigest, Error> {
272        CheckpointDigest::from_bytes(self.stored.checkpoint_digest.clone())
273            .map_err(|e| Error::Internal(format!("Failed to deserialize checkpoint digest: {e}")))
274    }
275
276    /// Look up a `Checkpoint` in the database, filtered by either sequence
277    /// number or digest. If both filters are supplied they will both be
278    /// applied. If none are supplied, the latest checkpoint is fetched.
279    pub(crate) async fn query(
280        ctx: &Context<'_>,
281        filter: CheckpointId,
282        checkpoint_viewed_at: u64,
283    ) -> Result<Option<Self>, Error> {
284        match filter {
285            CheckpointId {
286                sequence_number: Some(sequence_number),
287                digest,
288            } => {
289                let DataLoader(dl) = ctx.data_unchecked();
290                dl.load_one(SeqNumKey {
291                    sequence_number: sequence_number.into(),
292                    digest,
293                    checkpoint_viewed_at,
294                })
295                .await
296            }
297
298            CheckpointId {
299                sequence_number: None,
300                digest: Some(digest),
301            } => {
302                let DataLoader(dl) = ctx.data_unchecked();
303                dl.load_one(DigestKey {
304                    digest,
305                    checkpoint_viewed_at,
306                })
307                .await
308            }
309
310            CheckpointId {
311                sequence_number: None,
312                digest: None,
313            } => Checkpoint::query_latest_at(ctx.data_unchecked(), checkpoint_viewed_at).await,
314        }
315    }
316
317    /// Look up the latest `Checkpoint` from the database, optionally filtered
318    /// by a consistency cursor (querying for a consistency cursor in the
319    /// past looks for the latest checkpoint as of that cursor).
320    async fn query_latest_at(db: &Db, checkpoint_viewed_at: u64) -> Result<Option<Self>, Error> {
321        use checkpoints::dsl;
322
323        let stored: Option<StoredCheckpoint> = db
324            .execute(move |conn| {
325                conn.first(move || {
326                    dsl::checkpoints
327                        .filter(dsl::sequence_number.le(checkpoint_viewed_at as i64))
328                        .order_by(dsl::sequence_number.desc())
329                })
330                .optional()
331            })
332            .await
333            .map_err(|e| Error::Internal(format!("Failed to fetch checkpoint: {e}")))?;
334
335        Ok(stored.map(|stored| Checkpoint {
336            stored,
337            checkpoint_viewed_at,
338        }))
339    }
340
341    /// Look up a `Checkpoint` in the database and retrieve its `timestamp_ms`
342    /// field. This method takes a connection, so that it can be used within
343    /// a transaction.
344    pub(crate) fn query_timestamp(
345        conn: &mut Conn<'_>,
346        seq_num: u64,
347    ) -> Result<u64, diesel::result::Error> {
348        use checkpoints::dsl;
349
350        let stored: i64 = conn.first(|| {
351            dsl::checkpoints
352                .select(dsl::timestamp_ms)
353                .filter(dsl::sequence_number.eq(seq_num as i64))
354        })?;
355
356        Ok(stored as u64)
357    }
358
359    /// Returns the inclusive `[lo, hi]` checkpoint sequence-number range to
360    /// paginate over, given the `checkpoint_viewed_at` and optional `epoch`
361    /// filter. Returns `None` when `filter` targets an epoch that does not
362    /// exist.
363    ///
364    /// The `epochs` table is never pruned but an in-progress epoch's
365    /// `last_checkpoint_id` is NULL - in that case the upper bound is
366    /// capped at `checkpoint_viewed_at`.
367    async fn pagination_range(
368        db: &Db,
369        filter: Option<u64>,
370        checkpoint_viewed_at: u64,
371    ) -> Result<Option<(u64, u64)>, Error> {
372        let Some(epoch) = filter else {
373            return Ok(Some((0, checkpoint_viewed_at)));
374        };
375
376        let row: Option<(i64, Option<i64>)> = db
377            .execute(move |conn| {
378                use iota_indexer::schema::epochs::dsl as e;
379                conn.first(move || {
380                    e::epochs
381                        .select((e::first_checkpoint_id, e::last_checkpoint_id))
382                        .filter(e::epoch.eq(epoch as i64))
383                })
384                .optional()
385            })
386            .await
387            .map_err(|err| Error::Internal(format!("Failed to fetch epoch range: {err}")))?;
388
389        Ok(row.map(|(first, last)| {
390            let hi = last
391                .map(|l| std::cmp::min(l as u64, checkpoint_viewed_at))
392                .unwrap_or(checkpoint_viewed_at);
393            (first as u64, hi)
394        }))
395    }
396
397    /// Query the database for a `page` of checkpoints. The Page uses the
398    /// checkpoint sequence number of the stored checkpoint and the
399    /// checkpoint at which this was viewed at as the cursor, and
400    /// can optionally be further `filter`-ed by an epoch number (to only return
401    /// checkpoints within that epoch).
402    ///
403    /// The `checkpoint_viewed_at` parameter represents the checkpoint sequence
404    /// number at which this page was queried for. Each entity returned in
405    /// the connection will inherit this checkpoint, so that when viewing
406    /// that entity's state, it will be from the reference of this
407    /// checkpoint_viewed_at parameter.
408    ///
409    /// If the `Page<Cursor>` is set, then this function will defer to the
410    /// `checkpoint_viewed_at` in the cursor if they are consistent.
411    ///
412    /// A cursor or epoch in the pruned range is served from the fallback KV
413    /// store when configured, otherwise the request errors.
414    pub(crate) async fn paginate(
415        db: &Db,
416        page: Page<Cursor>,
417        filter: Option<u64>,
418        checkpoint_viewed_at: u64,
419    ) -> Result<Connection<String, Checkpoint>, Error> {
420        let cursor_viewed_at = page.validate_cursor_consistency()?;
421        let checkpoint_viewed_at = cursor_viewed_at.unwrap_or(checkpoint_viewed_at);
422
423        if page.limit() == 0 {
424            return Ok(Connection::new(false, false));
425        }
426
427        let Some((mut absolute_lo_incl, absolute_hi_incl)) =
428            Self::pagination_range(db, filter, checkpoint_viewed_at).await?
429        else {
430            return Ok(Connection::new(false, false));
431        };
432
433        // Without a fallback, anything below the pruning watermark is unreachable
434        if !db.inner.is_fallback_enabled() {
435            if let Some(lowest_unpruned_cp) = db
436                .inner
437                .watermark_cache()
438                .get_lowest_available_cp_for_tables(&[CommitterTables::Checkpoints])
439                .map(|w| w as u64)
440            {
441                absolute_lo_incl = absolute_lo_incl.max(lowest_unpruned_cp);
442            }
443        }
444
445        let available_range = absolute_lo_incl..=absolute_hi_incl;
446
447        if available_range.is_empty() {
448            return Err(Error::DataPruned(
449                "all checkpoints in the requested range have been pruned".into(),
450            ));
451        }
452
453        let Some(page_range) = page.narrow_to_available_range(&available_range)? else {
454            return Ok(Connection::new(false, false));
455        };
456
457        // Take `limit` sequence numbers from the appropriate end of the page range.
458        let limit = page.limit();
459        let picked_seqs: Vec<u64> = if page.is_from_front() {
460            page_range.take(limit).collect()
461        } else {
462            page_range.rev().take(limit).collect()
463        };
464        let mut all_rows: Vec<StoredCheckpoint> = db
465            .inner
466            .get_stored_checkpoints_by_seqs_with_fallback(picked_seqs.clone())
467            .await
468            .map_err(|err| Error::Internal(format!("Failed to fetch checkpoints: {err}")))?
469            .into_iter()
470            .flatten()
471            .collect();
472        all_rows.sort_by_key(|s| s.sequence_number);
473
474        // We validated the available range earlier, unpruned range should be present in
475        // the DB, rest should be present in fallback KV if configured. In such case we
476        // expect all checkpoints to be returned.
477        if all_rows.len() < picked_seqs.len() {
478            let picked: BTreeSet<u64> = picked_seqs.iter().copied().collect();
479            let returned: BTreeSet<u64> =
480                all_rows.iter().map(|r| r.sequence_number as u64).collect();
481            let misses: Vec<u64> = picked.difference(&returned).copied().collect();
482            return Err(Error::Internal(format!(
483                "checkpoints {misses:?} expected to be available but not found"
484            )));
485        }
486
487        let fetched_lo = all_rows.first().expect("checked non-empty").sequence_number as u64;
488        let fetched_hi = all_rows.last().expect("checked non-empty").sequence_number as u64;
489        let has_prev = fetched_lo > absolute_lo_incl;
490        let has_next = fetched_hi < absolute_hi_incl;
491
492        let mut conn = Connection::new(has_prev, has_next);
493        for stored in all_rows {
494            let cursor = stored.cursor(checkpoint_viewed_at).encode_cursor();
495            conn.edges.push(Edge::new(
496                cursor,
497                Checkpoint {
498                    stored,
499                    checkpoint_viewed_at,
500                },
501            ));
502        }
503
504        Ok(conn)
505    }
506}
507
508impl Target<Cursor> for StoredCheckpoint {
509    fn cursor(&self, checkpoint_viewed_at: u64) -> Cursor {
510        Cursor::new(CheckpointCursor {
511            checkpoint_viewed_at: UInt53::new_unchecked(checkpoint_viewed_at),
512            sequence_number: UInt53::new_unchecked(self.sequence_number as u64),
513        })
514    }
515}
516
517impl Checkpointed for Cursor {
518    fn checkpoint_viewed_at(&self) -> u64 {
519        self.checkpoint_viewed_at.into()
520    }
521}
522
523impl ScanLimited for Cursor {}
524
525impl Page<Cursor> {
526    /// Narrow page range to the `available` range and return the inclusive
527    /// range this page should cover, or `None` if the range is empty.
528    ///
529    /// A cursor below `available.start()` returns `Error::DataPruned` (the
530    /// data was pruned since the cursor was issued). A cursor above
531    /// `available.end()` returns `Error::Client` (the cursor is malformed).
532    /// When `after > before` the range is empty and no error is returned.
533    fn narrow_to_available_range(
534        &self,
535        available: &RangeInclusive<u64>,
536    ) -> Result<Option<RangeInclusive<u64>>, Error> {
537        let lo = *available.start();
538        let hi = *available.end();
539
540        let after = self
541            .after()
542            .map(|c| ("after", u64::from(c.sequence_number)));
543        let before = self
544            .before()
545            .map(|c| ("before", u64::from(c.sequence_number)));
546
547        // If `after > before`, the range is empty; skip cursor validation.
548        if let (Some((_, after_seq)), Some((_, before_seq))) = (after, before) {
549            if after_seq > before_seq {
550                return Ok(None);
551            }
552        }
553
554        // Since `after <= before`, it's enough to check if the smaller cursor is below
555        // `lo`. Analogously for `hi`.
556        if let Some((name, seq)) = after.or(before) {
557            if seq < lo {
558                return Err(Error::DataPruned(format!(
559                    "`{name}` cursor (seq {seq}) is below the available range {available:?}"
560                )));
561            }
562        }
563        if let Some((name, seq)) = before.or(after) {
564            if seq > hi {
565                return Err(Error::Client(format!(
566                    "`{name}` cursor (seq {seq}) is above the available range {available:?}"
567                )));
568            }
569        }
570
571        // Cursors are exclusive.
572        let page_lo = after.map(|(_, seq)| seq.saturating_add(1)).unwrap_or(lo);
573        let page_hi = before.map(|(_, seq)| seq.saturating_sub(1)).unwrap_or(hi);
574        let range = page_lo..=page_hi;
575        if range.is_empty() {
576            Ok(None)
577        } else {
578            Ok(Some(range))
579        }
580    }
581}
582
583impl Loader<SeqNumKey> for Db {
584    type Value = Checkpoint;
585    type Error = Error;
586
587    async fn load(&self, keys: &[SeqNumKey]) -> Result<HashMap<SeqNumKey, Checkpoint>, Error> {
588        // Drop keys querying for a checkpoint after their own consistency cursor.
589        let seqs: Vec<u64> = keys
590            .iter()
591            .filter(|key| key.checkpoint_viewed_at >= key.sequence_number)
592            .map(|key| key.sequence_number)
593            .collect();
594
595        let rows = self
596            .inner
597            .get_stored_checkpoints_by_seqs_with_fallback(seqs.clone())
598            .await
599            .map_err(|e| Error::Internal(format!("Failed to fetch checkpoints: {e}")))?;
600
601        let checkpoint_id_to_stored: BTreeMap<u64, StoredCheckpoint> = seqs
602            .into_iter()
603            .zip(rows)
604            .filter_map(|(seq, row)| row.map(|stored| (seq, stored)))
605            .collect();
606
607        Ok(keys
608            .iter()
609            .filter_map(|key| {
610                let stored = checkpoint_id_to_stored.get(&key.sequence_number).cloned()?;
611                let checkpoint = Checkpoint {
612                    stored,
613                    checkpoint_viewed_at: key.checkpoint_viewed_at,
614                };
615
616                let digest = &checkpoint.stored.checkpoint_digest;
617                if matches!(key.digest, Some(d) if d.as_slice() != digest) {
618                    None
619                } else {
620                    Some((*key, checkpoint))
621                }
622            })
623            .collect())
624    }
625}
626
627impl Loader<DigestKey> for Db {
628    type Value = Checkpoint;
629    type Error = Error;
630
631    async fn load(&self, keys: &[DigestKey]) -> Result<HashMap<DigestKey, Checkpoint>, Error> {
632        let digests: Vec<CheckpointDigest> = keys.iter().map(|key| key.digest.into()).collect();
633
634        let rows = self
635            .inner
636            .get_stored_checkpoints_by_digests_with_fallback(digests.clone())
637            .await
638            .map_err(|e| Error::Internal(format!("Failed to fetch checkpoints: {e}")))?;
639
640        let checkpoint_id_to_stored: BTreeMap<Vec<u8>, StoredCheckpoint> = digests
641            .into_iter()
642            .zip(rows)
643            .filter_map(|(digest, row)| row.map(|stored| (digest.bytes().to_vec(), stored)))
644            .collect();
645
646        Ok(keys
647            .iter()
648            .filter_map(|key| {
649                let DigestKey {
650                    digest,
651                    checkpoint_viewed_at,
652                } = *key;
653
654                let stored = checkpoint_id_to_stored.get(digest.as_slice()).cloned()?;
655
656                let checkpoint = Checkpoint {
657                    stored,
658                    checkpoint_viewed_at,
659                };
660
661                // Filter by key's checkpoint viewed at here. Doing this in memory because it
662                // should be quite rare that this query actually filters
663                // something, but encoding it in SQL is complicated.
664                let seq_num = checkpoint.stored.sequence_number as u64;
665                (checkpoint_viewed_at >= seq_num).then_some((*key, checkpoint))
666            })
667            .collect())
668    }
669}