Skip to main content

rain_metadata/meta/
cache.rs

1// SPDX-License-Identifier: LicenseRef-DCL-1.0
2// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
3//! The meta cache, and the one way into it.
4//!
5//! A hash is a claim about the bytes it keys. Caching bytes that do not hash to
6//! their key stores a lie the rest of the crate reads back as truth, and
7//! `cas.md` puts the check at exactly this point - "before the content is
8//! stored under the hash" - so everything downstream can stop asking.
9//!
10//! Keeping that as a convention did not hold. The map was a bare `HashMap`
11//! field on `Store`, so any method could reach past the check, and several did:
12//! `update` shipped without it, `search_deployer`, `set_deployer` and
13//! `set_deployer_from_query_response` each wrote to the cache directly. Each
14//! was found separately, after the fact.
15//!
16//! So the map lives here with a private field and no unguarded insert. Every
17//! write goes through [MetaCache::insert_verified] because the type system
18//! offers nothing else, including from code written long after this.
19
20use std::collections::BTreeMap;
21
22use alloy::primitives::{hex, keccak256};
23use serde::{Deserialize, Deserializer};
24
25use crate::error::Error;
26
27/// Meta bytes keyed by their own keccak256 hash.
28///
29/// The key is not a name for the bytes, it is a digest of them, and this type
30/// exists to make that true by construction rather than by discipline.
31/// The map is a [BTreeMap] so serializing twice gives the same bytes.
32/// [std::collections::HashMap] iterates in an order randomized per process,
33/// which would make a serialized cache unreproducible for no gain - every
34/// access here is by key.
35#[derive(Clone, Debug, Default, PartialEq, serde::Serialize)]
36pub struct MetaCache {
37    inner: BTreeMap<Vec<u8>, Vec<u8>>,
38}
39
40/// Deserializing is a way into the cache, so it goes through the same gate.
41///
42/// A derived impl would build `inner` directly, which is how the invariant
43/// leaked the first time this type was written: entries refused by
44/// [MetaCache::insert_verified] were accepted wholesale off the wire. A cache
45/// is only as good as the worst entry in it, so one bad pair rejects the whole
46/// map rather than being dropped quietly - unlike a responder's single answer,
47/// a serialized cache is something this process wrote and should not be able
48/// to get wrong.
49impl<'de> Deserialize<'de> for MetaCache {
50    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
51        #[derive(Deserialize)]
52        struct Wire {
53            inner: BTreeMap<Vec<u8>, Vec<u8>>,
54        }
55
56        let wire = Wire::deserialize(deserializer)?;
57        let mut cache = MetaCache::default();
58        for (hash, bytes) in wire.inner {
59            cache
60                .insert_verified(&hash, bytes)
61                .map_err(serde::de::Error::custom)?;
62        }
63        Ok(cache)
64    }
65}
66
67impl MetaCache {
68    /// Caches `bytes` under `hash`, and only if they hash to it.
69    ///
70    /// A mismatch is [Error::CorruptRecord] rather than a miss: the responder
71    /// answered a question about one hash with bytes that are another, which
72    /// is not the same fact as the hash being absent.
73    /// rainlanguage/rain.metadata#234 and #213 settled that distinction for the
74    /// query layer; this is the same distinction at the cache.
75    pub fn insert_verified(&mut self, hash: &[u8], bytes: Vec<u8>) -> Result<&Vec<u8>, Error> {
76        if keccak256(&bytes).0 != hash {
77            return Err(Error::CorruptRecord(format!(
78                "bytes do not hash to the requested {}",
79                hex::encode_prefixed(hash)
80            )));
81        }
82        self.inner.insert(hash.to_vec(), bytes);
83        self.inner.get(hash).ok_or(Error::NoRecordFound)
84    }
85
86    /// The bytes cached under `hash`, if any.
87    pub fn get(&self, hash: &[u8]) -> Option<&Vec<u8>> {
88        self.inner.get(hash)
89    }
90
91    /// Whether anything is cached under `hash`.
92    pub fn contains_key(&self, hash: &[u8]) -> bool {
93        self.inner.contains_key(hash)
94    }
95
96    /// Drops whatever is cached under `hash`. Removing cannot break the
97    /// invariant, so it needs no check.
98    pub fn remove(&mut self, hash: &[u8]) {
99        self.inner.remove(hash);
100    }
101
102    /// Every cached pair. Entries are verified by construction, so copying one
103    /// into another [MetaCache] cannot introduce an unverified entry.
104    pub fn iter(&self) -> impl Iterator<Item = (&Vec<u8>, &Vec<u8>)> {
105        self.inner.iter()
106    }
107
108    /// Whether anything is cached at all.
109    pub fn is_empty(&self) -> bool {
110        self.inner.is_empty()
111    }
112
113    /// How many metas are cached.
114    pub fn len(&self) -> usize {
115        self.inner.len()
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    fn hashed(bytes: &[u8]) -> Vec<u8> {
124        keccak256(bytes).0.to_vec()
125    }
126
127    /// Bytes that hash to their key are cached and readable back.
128    #[test]
129    fn test_insert_verified_accepts_matching_bytes() {
130        let bytes = b"content".to_vec();
131        let hash = hashed(&bytes);
132        let mut cache = MetaCache::default();
133
134        assert_eq!(cache.insert_verified(&hash, bytes.clone()).unwrap(), &bytes);
135        assert_eq!(cache.get(&hash), Some(&bytes));
136        assert!(cache.contains_key(&hash));
137        assert_eq!(cache.len(), 1);
138    }
139
140    /// Bytes that do not hash to their key are refused, and refused as corrupt
141    /// rather than as a miss, with the requested hash named.
142    #[test]
143    fn test_insert_verified_rejects_mismatched_bytes_as_corrupt() {
144        let wrong_hash = vec![0x99u8; 32];
145        let mut cache = MetaCache::default();
146
147        match cache
148            .insert_verified(&wrong_hash, b"content".to_vec())
149            .unwrap_err()
150        {
151            Error::CorruptRecord(message) => assert!(
152                message.contains(&hex::encode_prefixed(&wrong_hash)),
153                "{}",
154                message
155            ),
156            other => panic!("expected CorruptRecord, got {:?}", other),
157        }
158
159        // and nothing was cached on the way out
160        assert!(cache.is_empty());
161        assert!(!cache.contains_key(&wrong_hash));
162    }
163
164    /// Deserializing is a way in, so it is gated too. A derived impl would
165    /// build the map directly and accept off the wire exactly what
166    /// insert_verified refuses in process.
167    #[test]
168    fn test_deserialize_rejects_an_unverified_entry() {
169        #[derive(serde::Serialize)]
170        struct Wire {
171            inner: std::collections::BTreeMap<Vec<u8>, Vec<u8>>,
172        }
173        let planted = Wire {
174            inner: std::collections::BTreeMap::from([(
175                vec![0x99u8; 32],
176                b"not the preimage".to_vec(),
177            )]),
178        };
179
180        let wire = serde_cbor::to_vec(&planted).unwrap();
181        let round: Result<MetaCache, _> = serde_cbor::from_slice(&wire);
182        assert!(round.is_err(), "an unverified entry round tripped in");
183    }
184
185    /// A verified entry survives the round trip, so the gate rejects lies
186    /// rather than everything.
187    #[test]
188    fn test_deserialize_keeps_a_verified_entry() {
189        let bytes = b"content".to_vec();
190        let hash = hashed(&bytes);
191        let mut cache = MetaCache::default();
192        cache.insert_verified(&hash, bytes.clone()).unwrap();
193
194        let wire = serde_cbor::to_vec(&cache).unwrap();
195        let round: MetaCache = serde_cbor::from_slice(&wire).unwrap();
196        assert_eq!(round.get(&hash), Some(&bytes));
197    }
198
199    /// Serializing the same cache twice gives the same bytes, which a
200    /// HashMap would not guarantee across processes.
201    #[test]
202    fn test_serialization_is_deterministic() {
203        let mut cache = MetaCache::default();
204        for content in [b"one".to_vec(), b"two".to_vec(), b"three".to_vec()] {
205            let hash = hashed(&content);
206            cache.insert_verified(&hash, content).unwrap();
207        }
208        let a = serde_cbor::to_vec(&cache).unwrap();
209        let b = serde_cbor::to_vec(&cache.clone()).unwrap();
210        assert_eq!(a, b);
211    }
212}