Skip to main content

iota_storage/
http_key_value_store.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{str::FromStr, sync::Arc, time::Duration};
6
7use anyhow;
8use async_trait::async_trait;
9use bytes::Bytes;
10use futures::stream::{self, StreamExt};
11use iota_sdk_types::{
12    Address, CheckpointContents, CheckpointDigest, ObjectId, TransactionDigest, TransactionEffects,
13    TransactionEvents, Version,
14};
15use iota_types::{
16    effects::TransactionEffectsAPI,
17    error::{IotaError, IotaResult},
18    messages_checkpoint::{CertifiedCheckpointSummary, CheckpointSequenceNumber},
19    object::Object,
20    storage::ObjectKey,
21    transaction::TransactionEnvelope,
22};
23use moka::sync::{Cache as MokaCache, CacheBuilder as MokaCacheBuilder};
24use reqwest::{
25    Client, StatusCode, Url,
26    header::{CONTENT_LENGTH, HeaderValue},
27};
28use serde::{Deserialize, Serialize};
29use tap::TapFallible;
30use tracing::{error, info, instrument, trace, warn};
31
32use crate::{
33    key_value_store::{
34        KVStoreTransactionData, TransactionKeyValueStore, TransactionKeyValueStoreTrait,
35    },
36    key_value_store_metrics::KeyValueStoreMetrics,
37};
38
39/// Reads a trusted HTTP key-value store. Its checks catch data stored under the
40/// wrong key, not forged data: signatures are not verified.
41pub struct HttpKVStore {
42    base_url: Url,
43    client: Client,
44    cache: MokaCache<Url, Bytes>,
45    metrics: Arc<KeyValueStoreMetrics>,
46}
47
48pub fn encode_digest<T: AsRef<[u8]>>(digest: &T) -> String {
49    base64_url::encode(digest)
50}
51
52// for non-digest keys, we need a tag to make sure we don't have collisions
53#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
54pub enum TaggedKey {
55    CheckpointSequenceNumber(CheckpointSequenceNumber),
56}
57
58pub fn encoded_tagged_key(key: &TaggedKey) -> String {
59    let bytes = bcs::to_bytes(key).expect("failed to serialize key");
60    base64_url::encode(&bytes)
61}
62
63pub fn encode_object_key(object_key: &ObjectKey) -> String {
64    let bytes = bcs::to_bytes(object_key).expect("failed to serialize object key");
65    base64_url::encode(&bytes)
66}
67
68trait IntoIotaResult<T> {
69    fn into_iota_result(self) -> IotaResult<T>;
70}
71
72impl<T, E> IntoIotaResult<T> for Result<T, E>
73where
74    E: std::error::Error,
75{
76    fn into_iota_result(self) -> IotaResult<T> {
77        self.map_err(|e| IotaError::Storage(e.to_string()))
78    }
79}
80
81/// Represents the supported items the REST API accepts when fetching the data
82/// based on Digest or Sequence number.
83#[derive(
84    Clone,
85    Copy,
86    Debug,
87    PartialEq,
88    Eq,
89    PartialOrd,
90    Ord,
91    Hash,
92    Deserialize,
93    strum::EnumString,
94    strum::Display,
95)]
96pub enum ItemType {
97    #[strum(serialize = "tx")]
98    #[serde(rename = "tx")]
99    Transaction,
100    #[strum(serialize = "fx")]
101    #[serde(rename = "fx")]
102    TransactionEffects,
103    #[strum(serialize = "cc")]
104    #[serde(rename = "cc")]
105    CheckpointContents,
106    #[strum(serialize = "cs")]
107    #[serde(rename = "cs")]
108    CheckpointSummary,
109    #[strum(serialize = "tx2c")]
110    #[serde(rename = "tx2c")]
111    TransactionToCheckpoint,
112    #[strum(serialize = "ob")]
113    #[serde(rename = "ob")]
114    Object,
115    #[strum(serialize = "evtx")]
116    #[serde(rename = "evtx")]
117    EventTransactionDigest,
118    #[strum(serialize = "txa")]
119    #[serde(rename = "txa")]
120    TransactionDigestsByAddress,
121}
122
123#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
124pub enum Key {
125    Transaction(TransactionDigest),
126    TransactionEffects(TransactionDigest),
127    CheckpointContents(CheckpointSequenceNumber),
128    CheckpointSummary(CheckpointSequenceNumber),
129    CheckpointSummaryByDigest(CheckpointDigest),
130    TransactionToCheckpoint(TransactionDigest),
131    ObjectKey(ObjectKey),
132    EventsByTransactionDigest(TransactionDigest),
133    TransactionDigestsByAddress(Address),
134}
135
136impl Key {
137    // Create a [`Key`] instance based on the provided item type and
138    /// [`base64_url`] encoded string.
139    ///
140    /// # Example
141    ///
142    /// ```rust
143    /// use std::str::FromStr;
144    ///
145    /// use iota_sdk_types::TransactionDigest;
146    /// use iota_storage::http_key_value_store::Key;
147    ///
148    /// let key = Key::new("tx", "7jb54RvJduLj9HdV9L41UJqZ5KWdzYY2rl1eL8AVl9o").unwrap();
149    /// assert_eq!(
150    ///     key,
151    ///     Key::Transaction(
152    ///         TransactionDigest::from_str("H2tetNL3CfroDF3iJNA7wFo6oRQiJedGTeykZi6HAGqP").unwrap()
153    ///     )
154    /// );
155    /// ```
156    pub fn new(item_type: &str, encoded_key: &str) -> anyhow::Result<Self> {
157        let item_type =
158            ItemType::from_str(item_type).map_err(|e| anyhow::anyhow!("invalid item type: {e}"))?;
159        let decoded_key = base64_url::decode(encoded_key)
160            .map_err(|err| anyhow::anyhow!("invalid base64 url string: {err}"))?;
161
162        match item_type {
163            ItemType::Transaction => Ok(Key::Transaction(TransactionDigest::from_bytes(
164                decoded_key.as_slice(),
165            )?)),
166            ItemType::TransactionEffects => Ok(Key::TransactionEffects(
167                TransactionDigest::from_bytes(decoded_key.as_slice())?,
168            )),
169            ItemType::CheckpointContents => {
170                let tagged_key = bcs::from_bytes(&decoded_key).map_err(|err| {
171                    anyhow::anyhow!("failed to deserialize checkpoint sequence number: {err}")
172                })?;
173                match tagged_key {
174                    TaggedKey::CheckpointSequenceNumber(seq) => Ok(Key::CheckpointContents(seq)),
175                }
176            }
177            ItemType::CheckpointSummary => {
178                // first try to decode as digest, otherwise try to decode as tagged key
179                match CheckpointDigest::from_bytes(decoded_key.clone()) {
180                    Err(_) => {
181                        let tagged_key = bcs::from_bytes(&decoded_key).map_err(|err| {
182                            anyhow::anyhow!(
183                                "failed to deserialize checkpoint sequence number: {err}"
184                            )
185                        })?;
186                        match tagged_key {
187                            TaggedKey::CheckpointSequenceNumber(seq) => {
188                                Ok(Key::CheckpointSummary(seq))
189                            }
190                        }
191                    }
192                    Ok(cs_digest) => Ok(Key::CheckpointSummaryByDigest(cs_digest)),
193                }
194            }
195            ItemType::TransactionToCheckpoint => Ok(Key::TransactionToCheckpoint(
196                TransactionDigest::from_bytes(decoded_key.as_slice())?,
197            )),
198            ItemType::Object => {
199                let object_key: ObjectKey = bcs::from_bytes(&decoded_key)
200                    .map_err(|err| anyhow::anyhow!("failed to deserialize object key: {err}"))?;
201
202                Ok(Key::ObjectKey(ObjectKey(object_key.0, object_key.1)))
203            }
204            ItemType::EventTransactionDigest => Ok(Key::EventsByTransactionDigest(
205                TransactionDigest::from_bytes(decoded_key.as_slice())?,
206            )),
207            ItemType::TransactionDigestsByAddress => Ok(Key::TransactionDigestsByAddress(
208                Address::from_bytes(decoded_key.as_slice())?,
209            )),
210        }
211    }
212
213    /// Get the REST API resource type.
214    ///
215    /// This method returns the corresponding resource type string
216    /// for a given `Key` variant.
217    ///
218    /// This is used to construct the REST API route,
219    /// typically in the format `/{item_type}/{digest}`.
220    ///
221    /// # Example
222    /// ```rust
223    /// use iota_sdk_types::TransactionDigest;
224    /// use iota_storage::http_key_value_store::{ItemType, Key};
225    ///
226    /// let item_type = Key::CheckpointContents(1).item_type();
227    /// assert_eq!(item_type, ItemType::CheckpointContents);
228    /// let item_type = Key::Transaction(TransactionDigest::random()).item_type();
229    /// assert_eq!(item_type, ItemType::Transaction);
230    /// ```
231    pub fn item_type(&self) -> ItemType {
232        match self {
233            Key::Transaction(_) => ItemType::Transaction,
234            Key::TransactionEffects(_) => ItemType::TransactionEffects,
235            Key::CheckpointContents(_) => ItemType::CheckpointContents,
236            Key::CheckpointSummary(_) | Key::CheckpointSummaryByDigest(_) => {
237                ItemType::CheckpointSummary
238            }
239            Key::TransactionToCheckpoint(_) => ItemType::TransactionToCheckpoint,
240            Key::ObjectKey(_) => ItemType::Object,
241            Key::EventsByTransactionDigest(_) => ItemType::EventTransactionDigest,
242            Key::TransactionDigestsByAddress(_) => ItemType::TransactionDigestsByAddress,
243        }
244    }
245
246    /// Returns a tuple containing the resource type and the encoded key.
247    ///
248    /// This is used to construct the REST API route, typically in the format
249    /// `/:item_type/:digest`.
250    ///
251    /// # Examples
252    ///
253    /// ```rust
254    /// use iota_sdk_types::TransactionDigest;
255    /// use iota_storage::http_key_value_store::{
256    ///     ItemType, Key, TaggedKey, encode_digest, encode_object_key, encoded_tagged_key,
257    /// };
258    ///
259    /// let tx_digest = TransactionDigest::random();
260    /// // encode the tx_digest as base64 url
261    /// let expected_encoded_digest = encode_digest(&tx_digest);
262    /// let key = Key::Transaction(tx_digest);
263    /// let (resource_type, encoded_key_digest) = key.to_path_elements();
264    /// assert_eq!(resource_type, ItemType::Transaction);
265    /// assert_eq!(encoded_key_digest, expected_encoded_digest);
266    ///
267    /// let chk_seq_num = 123;
268    /// let key = Key::CheckpointSummary(chk_seq_num);
269    /// // encode the checkpoint sequence number as base64 url
270    /// let expected_encoded_seq_num =
271    ///     encoded_tagged_key(&TaggedKey::CheckpointSequenceNumber(chk_seq_num));
272    /// let (resource_type, encoded_key_digest) = key.to_path_elements();
273    /// assert_eq!(resource_type, ItemType::CheckpointSummary);
274    /// assert_eq!(encoded_key_digest, expected_encoded_seq_num);
275    /// ```
276    pub fn to_path_elements(&self) -> (ItemType, String) {
277        let encoded_key_digest = match self {
278            Key::Transaction(digest) => encode_digest(digest),
279            Key::TransactionEffects(digest) => encode_digest(digest),
280            Key::CheckpointContents(seq) => {
281                encoded_tagged_key(&TaggedKey::CheckpointSequenceNumber(*seq))
282            }
283            Key::CheckpointSummary(seq) => {
284                encoded_tagged_key(&TaggedKey::CheckpointSequenceNumber(*seq))
285            }
286            Key::CheckpointSummaryByDigest(digest) => encode_digest(digest),
287            Key::TransactionToCheckpoint(digest) => encode_digest(digest),
288            Key::ObjectKey(object_key) => encode_object_key(object_key),
289            Key::EventsByTransactionDigest(digest) => encode_digest(digest),
290            // TODO: `encode_digest` could be renamed to `encode` to fit more use cases.
291            // tracking issue: https://github.com/iotaledger/iota/issues/11754
292            Key::TransactionDigestsByAddress(address) => encode_digest(address),
293        };
294
295        (self.item_type(), encoded_key_digest)
296    }
297}
298
299#[derive(Clone, Debug)]
300enum Value {
301    Tx(Box<TransactionEnvelope>),
302    Fx(Box<TransactionEffects>),
303    Events(Box<TransactionEvents>),
304    CheckpointContents(Box<CheckpointContents>),
305    CheckpointSummary(Box<CertifiedCheckpointSummary>),
306    TxToCheckpoint(CheckpointSequenceNumber),
307}
308
309impl HttpKVStore {
310    pub fn new_kv(
311        base_url: &str,
312        cache_size: u64,
313        metrics: Arc<KeyValueStoreMetrics>,
314    ) -> IotaResult<TransactionKeyValueStore> {
315        let inner = Arc::new(Self::new(base_url, cache_size, metrics.clone())?);
316        Ok(TransactionKeyValueStore::new("http", metrics, inner))
317    }
318
319    pub fn new(
320        base_url: &str,
321        cache_size: u64,
322        metrics: Arc<KeyValueStoreMetrics>,
323    ) -> IotaResult<Self> {
324        info!("creating HttpKVStore with base_url: {}", base_url);
325
326        let _ = rustls::crypto::ring::default_provider().install_default();
327        let client = Client::builder().http2_prior_knowledge().build().unwrap();
328
329        let base_url = if base_url.ends_with('/') {
330            base_url.to_string()
331        } else {
332            format!("{base_url}/")
333        };
334
335        let base_url = Url::parse(&base_url).into_iota_result()?;
336
337        let cache = MokaCacheBuilder::new(cache_size)
338            .time_to_idle(Duration::from_secs(600))
339            .build();
340
341        Ok(Self {
342            base_url,
343            client,
344            cache,
345            metrics,
346        })
347    }
348
349    fn get_url(&self, key: &Key) -> IotaResult<Url> {
350        let (item_type, digest) = key.to_path_elements();
351        let joined = self
352            .base_url
353            .join(&format!("{item_type}/{digest}"))
354            .into_iota_result()?;
355        Url::from_str(joined.as_str()).into_iota_result()
356    }
357
358    fn evict(&self, keys: impl IntoIterator<Item = Key>) {
359        for key in keys {
360            if let Ok(url) = self.get_url(&key) {
361                self.cache.invalidate(&url);
362            }
363        }
364    }
365
366    /// Fetches every key: one entry per key, in key order, with `None` for a
367    /// key the store does not have.
368    ///
369    /// # Errors
370    ///
371    /// Fails if any fetch fails.
372    async fn multi_fetch(&self, uris: Vec<Key>) -> IotaResult<Vec<Option<Bytes>>> {
373        let uris_vec = uris.to_vec();
374        let fetches = stream::iter(uris_vec.into_iter().map(|url| self.fetch(url)));
375        // Waits for every fetch, also after one fails, so that the others still
376        // cache what they read for a retry.
377        fetches
378            .buffered(uris.len())
379            .collect::<Vec<_>>()
380            .await
381            .into_iter()
382            .collect()
383    }
384
385    /// Fetches one key, with `None` if the store does not have it.
386    ///
387    /// # Errors
388    ///
389    /// Fails if the URL cannot be built, if the request or reading the body
390    /// fails, and on an HTTP status other than success, `404 Not Found` and
391    /// `400 Bad Request`. The error text reaches clients, so it leaves out the
392    /// store's URL.
393    async fn fetch(&self, key: Key) -> IotaResult<Option<Bytes>> {
394        let url = self.get_url(&key)?;
395
396        trace!("fetching url: {}", url);
397
398        if let Some(res) = self.cache.get(&url) {
399            trace!("found cached data for url: {}, len: {:?}", url, res.len());
400            self.metrics
401                .key_value_store_num_fetches_success
402                .with_label_values(&["http_cache", "url"])
403                .inc();
404            return Ok(Some(res));
405        }
406
407        self.metrics
408            .key_value_store_num_fetches_not_found
409            .with_label_values(&["http_cache", "url"])
410            .inc();
411
412        let resp = self
413            .client
414            .get(url.clone())
415            .send()
416            .await
417            .map_err(|err| request_failed(&url, err))
418            .into_iota_result()?;
419        trace!(
420            "got response {} for url: {}, len: {:?}",
421            url,
422            resp.status(),
423            resp.headers()
424                .get(CONTENT_LENGTH)
425                .unwrap_or(&HeaderValue::from_static("0"))
426        );
427        match resp.status() {
428            status if status.is_success() => {
429                let bytes = resp
430                    .bytes()
431                    .await
432                    .map_err(|err| request_failed(&url, err))
433                    .into_iota_result()?;
434                self.cache.insert(url, bytes.clone());
435
436                Ok(Some(bytes))
437            }
438            // The store answers 400 for a key it cannot parse, which a retry
439            // does not change.
440            StatusCode::NOT_FOUND | StatusCode::BAD_REQUEST => Ok(None),
441            status => {
442                warn!("key-value store returned {status} for url: {url}");
443                Err(IotaError::Storage(format!(
444                    "key-value store returned {status} for {key:?}"
445                )))
446            }
447        }
448    }
449}
450
451/// Logs a failed request and drops the URL from the error, whose text reaches
452/// clients.
453fn request_failed(url: &Url, err: reqwest::Error) -> reqwest::Error {
454    warn!("key-value store request failed for url: {url}: {err:?}");
455    err.without_url()
456}
457
458fn deser<K, T>(key: &K, bytes: &[u8]) -> Option<T>
459where
460    K: std::fmt::Debug,
461    T: for<'de> Deserialize<'de>,
462{
463    bcs::from_bytes(bytes)
464        .tap_err(|e| warn!("Error deserializing data for key {:?}: {:?}", key, e))
465        .ok()
466}
467
468fn map_fetch<'a, K>((fetch, key): (&'a Option<Bytes>, &'a K)) -> Option<(&'a Bytes, &'a K)> {
469    fetch.as_ref().map(|bytes| (bytes, key))
470}
471
472fn multi_split_slice<'a, T>(slice: &'a [T], lengths: &'a [usize]) -> Vec<&'a [T]> {
473    let mut start = 0;
474    lengths
475        .iter()
476        .map(|length| {
477            let end = start + length;
478            let result = &slice[start..end];
479            start = end;
480            result
481        })
482        .collect()
483}
484
485/// The keys whose fetch returned data that was then rejected.
486fn rejected_keys<'a, K: Copy, T>(
487    fetches: &'a [Option<Bytes>],
488    keys: &'a [K],
489    values: &'a [Option<T>],
490) -> impl Iterator<Item = K> + 'a {
491    fetches
492        .iter()
493        .zip(keys)
494        .zip(values)
495        .filter(|((fetch, _), value)| fetch.is_some() && value.is_none())
496        .map(|((_, key), _)| *key)
497}
498
499/// Decodes `bytes` and keeps the value only if `key_of` gives back `key`.
500fn deser_check_key<T, K>(key: &K, bytes: &Bytes, key_of: impl FnOnce(&T) -> K) -> Option<T>
501where
502    K: std::fmt::Debug + PartialEq,
503    T: for<'de> Deserialize<'de>,
504{
505    deser(key, bytes).and_then(|o: T| {
506        let actual = key_of(&o);
507        if actual == *key {
508            Some(o)
509        } else {
510            error!("Key mismatch - expected: {key:?}, got: {actual:?}");
511            None
512        }
513    })
514}
515
516#[async_trait]
517impl TransactionKeyValueStoreTrait for HttpKVStore {
518    #[instrument(level = "trace", skip_all)]
519    async fn multi_get(
520        &self,
521        transaction_keys: &[TransactionDigest],
522        effects_keys: &[TransactionDigest],
523    ) -> IotaResult<KVStoreTransactionData> {
524        let num_txns = transaction_keys.len();
525        let num_effects = effects_keys.len();
526
527        let keys = transaction_keys
528            .iter()
529            .map(|tx| Key::Transaction(*tx))
530            .chain(effects_keys.iter().map(|fx| Key::TransactionEffects(*fx)))
531            .collect::<Vec<_>>();
532
533        let fetches = self.multi_fetch(keys).await?;
534        let txn_slice = fetches[..num_txns].to_vec();
535        let fx_slice = fetches[num_txns..num_txns + num_effects].to_vec();
536
537        let txn_results = txn_slice
538            .iter()
539            .take(num_txns)
540            .zip(transaction_keys.iter())
541            .map(map_fetch)
542            .map(|maybe_bytes| {
543                maybe_bytes.and_then(|(bytes, digest)| {
544                    deser_check_key(digest, bytes, |tx: &TransactionEnvelope| *tx.digest())
545                })
546            })
547            .collect::<Vec<_>>();
548
549        let fx_results = fx_slice
550            .iter()
551            .take(num_effects)
552            .zip(effects_keys.iter())
553            .map(map_fetch)
554            .map(|maybe_bytes| {
555                maybe_bytes.and_then(|(bytes, digest)| {
556                    deser_check_key(digest, bytes, |fx: &TransactionEffects| {
557                        *fx.transaction_digest()
558                    })
559                })
560            })
561            .collect::<Vec<_>>();
562
563        Ok((txn_results, fx_results))
564    }
565
566    #[instrument(level = "trace", skip_all)]
567    async fn multi_get_checkpoints(
568        &self,
569        checkpoint_summaries: &[CheckpointSequenceNumber],
570        checkpoint_contents: &[CheckpointSequenceNumber],
571        checkpoint_summaries_by_digest: &[CheckpointDigest],
572    ) -> IotaResult<(
573        Vec<Option<CertifiedCheckpointSummary>>,
574        Vec<Option<CheckpointContents>>,
575        Vec<Option<CertifiedCheckpointSummary>>,
576    )> {
577        let keys = checkpoint_summaries
578            .iter()
579            .map(|cp| Key::CheckpointSummary(*cp))
580            .chain(
581                checkpoint_contents
582                    .iter()
583                    .map(|cp| Key::CheckpointContents(*cp)),
584            )
585            .chain(
586                checkpoint_summaries_by_digest
587                    .iter()
588                    .map(|cp| Key::CheckpointSummaryByDigest(*cp)),
589            )
590            .collect::<Vec<_>>();
591
592        let summaries_len = checkpoint_summaries.len();
593        let contents_len = checkpoint_contents.len();
594        let summaries_by_digest_len = checkpoint_summaries_by_digest.len();
595
596        let fetches = self.multi_fetch(keys).await?;
597
598        let input_slices = [summaries_len, contents_len, summaries_by_digest_len];
599
600        let result_slices = multi_split_slice(&fetches, &input_slices);
601
602        let summaries_results = result_slices[0]
603            .iter()
604            .zip(checkpoint_summaries.iter())
605            .map(map_fetch)
606            .map(|maybe_bytes| {
607                maybe_bytes.and_then(|(bytes, seq)| {
608                    deser_check_key(seq, bytes, |s: &CertifiedCheckpointSummary| {
609                        s.data().sequence_number
610                    })
611                })
612            })
613            .collect::<Vec<_>>();
614        let contents_results = result_slices[1]
615            .iter()
616            .zip(checkpoint_contents.iter())
617            .map(map_fetch)
618            .map(|maybe_bytes| {
619                maybe_bytes.and_then(|(bytes, seq)| deser::<_, CheckpointContents>(seq, bytes))
620            })
621            .collect::<Vec<_>>();
622
623        let summaries_by_digest_results = result_slices[2]
624            .iter()
625            .zip(checkpoint_summaries_by_digest.iter())
626            .map(map_fetch)
627            .map(|maybe_bytes| {
628                maybe_bytes.and_then(|(bytes, digest)| {
629                    deser_check_key(digest, bytes, |s: &CertifiedCheckpointSummary| *s.digest())
630                })
631            })
632            .collect::<Vec<_>>();
633
634        // A rejected entry would otherwise stay cached and keep being rejected.
635        self.evict(
636            rejected_keys(result_slices[0], checkpoint_summaries, &summaries_results)
637                .map(Key::CheckpointSummary)
638                .chain(
639                    rejected_keys(result_slices[1], checkpoint_contents, &contents_results)
640                        .map(Key::CheckpointContents),
641                )
642                .chain(
643                    rejected_keys(
644                        result_slices[2],
645                        checkpoint_summaries_by_digest,
646                        &summaries_by_digest_results,
647                    )
648                    .map(Key::CheckpointSummaryByDigest),
649                ),
650        );
651
652        Ok((
653            summaries_results,
654            contents_results,
655            summaries_by_digest_results,
656        ))
657    }
658
659    #[instrument(level = "trace", skip_all)]
660    async fn get_transaction_perpetual_checkpoint(
661        &self,
662        digest: TransactionDigest,
663    ) -> IotaResult<Option<CheckpointSequenceNumber>> {
664        let key = Key::TransactionToCheckpoint(digest);
665        self.fetch(key).await.map(|maybe| {
666            maybe.and_then(|bytes| deser::<_, CheckpointSequenceNumber>(&key, bytes.as_ref()))
667        })
668    }
669
670    #[instrument(level = "trace", skip_all)]
671    async fn get_object(
672        &self,
673        object_id: ObjectId,
674        version: Version,
675    ) -> IotaResult<Option<Object>> {
676        let key = Key::ObjectKey(ObjectKey(object_id, version));
677        self.fetch(key)
678            .await
679            .map(|maybe| maybe.and_then(|bytes| deser::<_, Object>(&key, bytes.as_ref())))
680    }
681
682    #[instrument(level = "trace", skip_all)]
683    async fn multi_get_objects(
684        &self,
685        object_keys: &[ObjectKey],
686    ) -> IotaResult<Vec<Option<Object>>> {
687        let keys = object_keys
688            .iter()
689            .map(|key| Key::ObjectKey(*key))
690            .collect::<Vec<_>>();
691
692        let fetches = self.multi_fetch(keys).await?;
693
694        let results = fetches
695            .iter()
696            .zip(object_keys.iter())
697            .map(map_fetch)
698            .map(|maybe_bytes| maybe_bytes.and_then(|(bytes, key)| deser::<_, Object>(&key, bytes)))
699            .collect::<Vec<_>>();
700
701        Ok(results)
702    }
703
704    #[instrument(level = "trace", skip_all)]
705    async fn multi_get_transactions_perpetual_checkpoints(
706        &self,
707        digests: &[TransactionDigest],
708    ) -> IotaResult<Vec<Option<CheckpointSequenceNumber>>> {
709        let keys = digests
710            .iter()
711            .map(|digest| Key::TransactionToCheckpoint(*digest))
712            .collect::<Vec<_>>();
713
714        let fetches = self.multi_fetch(keys).await?;
715
716        let results = fetches
717            .iter()
718            .zip(digests.iter())
719            .map(map_fetch)
720            .map(|maybe_bytes| {
721                maybe_bytes
722                    .and_then(|(bytes, key)| deser::<_, CheckpointSequenceNumber>(&key, bytes))
723            })
724            .collect::<Vec<_>>();
725
726        Ok(results)
727    }
728
729    #[instrument(level = "trace", skip_all)]
730    async fn multi_get_events_by_tx_digests(
731        &self,
732        digests: &[TransactionDigest],
733    ) -> IotaResult<Vec<Option<TransactionEvents>>> {
734        let keys = digests
735            .iter()
736            .map(|digest| Key::EventsByTransactionDigest(*digest))
737            .collect::<Vec<_>>();
738        Ok(self
739            .multi_fetch(keys)
740            .await?
741            .iter()
742            .zip(digests.iter())
743            .map(map_fetch)
744            .map(|maybe_bytes| {
745                maybe_bytes.and_then(|(bytes, key)| deser::<_, TransactionEvents>(&key, bytes))
746            })
747            .collect::<Vec<_>>())
748    }
749
750    async fn evict_objects(&self, object_keys: &[ObjectKey]) {
751        self.evict(object_keys.iter().map(|key| Key::ObjectKey(*key)));
752    }
753
754    async fn evict_events_by_tx_digests(&self, digests: &[TransactionDigest]) {
755        self.evict(
756            digests
757                .iter()
758                .map(|digest| Key::EventsByTransactionDigest(*digest)),
759        );
760    }
761}