1use 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
39pub 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#[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#[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 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 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 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 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 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 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 fetches
378 .buffered(uris.len())
379 .collect::<Vec<_>>()
380 .await
381 .into_iter()
382 .collect()
383 }
384
385 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 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
451fn 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
485fn 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
499fn 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 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}