1use super::{
5 BLOCK_BLOOM_LEN, EthBlockBloomStore, EthMappingsStore, SettingsStore, SettingsStoreExt,
6 decode_block_bloom, encode_block_bloom,
7};
8use crate::blocks::TipsetKey;
9use crate::db::PersistentStore;
10use crate::libp2p_bitswap::{BitswapStoreRead, BitswapStoreReadWrite};
11use crate::prelude::*;
12use crate::rpc::eth::types::EthHash;
13use crate::shim::clock::ChainEpoch;
14use crate::utils::db::car_stream::CarBlock;
15use crate::utils::multihash::prelude::*;
16use ahash::HashMap;
17use indexmap::IndexMap;
18use nunny::Vec as NonEmpty;
19use parking_lot::RwLock;
20
21#[derive(Debug, Default)]
22pub struct MemoryDB {
23 blockchain_db: RwLock<HashMap<Cid, Vec<u8>>>,
24 blockchain_persistent_db: RwLock<HashMap<Cid, Vec<u8>>>,
25 settings_db: RwLock<HashMap<String, Vec<u8>>>,
26 pub eth_mappings_db: RwLock<HashMap<EthHash, Vec<u8>>>,
27 pub ts_lookup_db: RwLock<HashMap<ChainEpoch, TipsetKey>>,
28 pub eth_block_bloom_db: RwLock<HashMap<Cid, Vec<u8>>>,
29}
30
31impl MemoryDB {
32 pub fn blockstore_len(&self) -> usize {
33 self.blockchain_db.read().len() + self.blockchain_persistent_db.read().len()
34 }
35
36 pub fn blockstore_size_bytes(&self) -> usize {
37 self.blockchain_db
38 .read()
39 .iter()
40 .chain(self.blockchain_persistent_db.read().iter())
41 .map(|(k, v)| k.to_bytes().len() + v.len())
42 .sum()
43 }
44
45 pub async fn export_forest_car<W: tokio::io::AsyncWrite + Unpin>(
46 &self,
47 writer: &mut W,
48 ) -> anyhow::Result<()> {
49 let roots =
50 SettingsStoreExt::read_obj::<TipsetKey>(self, crate::db::setting_keys::HEAD_KEY)?
51 .context("chain head is not tracked and cannot be exported")?
52 .into_cids();
53 self.export_forest_car_with_roots(roots, writer).await
54 }
55
56 pub async fn export_forest_car_with_roots<W: tokio::io::AsyncWrite + Unpin>(
57 &self,
58 roots: NonEmpty<Cid>,
59 writer: &mut W,
60 ) -> anyhow::Result<()> {
61 let blocks = {
62 let blockchain_db = self.blockchain_db.read();
63 let blockchain_persistent_db = self.blockchain_persistent_db.read();
64 blockchain_db
65 .iter()
66 .chain(blockchain_persistent_db.iter())
67 .sorted_by_key(|&(&cid, _)| cid)
69 .map(|(&cid, data)| {
70 anyhow::Ok(CarBlock {
71 cid,
72 data: data.clone().into(),
73 })
74 })
75 .collect_vec()
76 };
77 let frames =
78 crate::db::car::forest::Encoder::compress_stream_default(futures::stream::iter(blocks));
79 crate::db::car::forest::Encoder::write(writer, roots, frames).await
80 }
81}
82
83impl SettingsStore for MemoryDB {
84 fn read_bin(&self, key: &str) -> anyhow::Result<Option<Vec<u8>>> {
85 Ok(self.settings_db.read().get(key).cloned())
86 }
87
88 fn write_bin(&self, key: &str, value: &[u8]) -> anyhow::Result<()> {
89 self.settings_db
90 .write()
91 .insert(key.to_owned(), value.to_vec());
92 Ok(())
93 }
94
95 fn exists(&self, key: &str) -> anyhow::Result<bool> {
96 Ok(self.settings_db.read().contains_key(key))
97 }
98
99 fn setting_keys(&self) -> anyhow::Result<Vec<String>> {
100 Ok(self.settings_db.read().keys().cloned().collect_vec())
101 }
102}
103
104impl EthMappingsStore for MemoryDB {
105 fn read_bin(&self, key: &EthHash) -> anyhow::Result<Option<Vec<u8>>> {
106 Ok(self.eth_mappings_db.read().get(key).cloned())
107 }
108
109 fn write_bin(&self, key: &EthHash, value: &[u8]) -> anyhow::Result<()> {
110 self.eth_mappings_db
111 .write()
112 .insert(key.to_owned(), value.to_vec());
113 Ok(())
114 }
115
116 fn exists(&self, key: &EthHash) -> anyhow::Result<bool> {
117 Ok(self.eth_mappings_db.read().contains_key(key))
118 }
119
120 fn get_message_cids(&self) -> anyhow::Result<Vec<(Cid, u64)>> {
121 let cids = self
122 .eth_mappings_db
123 .read()
124 .values()
125 .filter_map(|value| fvm_ipld_encoding::from_slice::<(Cid, u64)>(value).ok())
126 .collect();
127
128 Ok(cids)
129 }
130
131 fn delete(&self, keys: Vec<EthHash>) -> anyhow::Result<()> {
132 let mut lock = self.eth_mappings_db.write();
133 for hash in keys.iter() {
134 lock.remove(hash);
135 }
136 Ok(())
137 }
138
139 fn tipset_key_by_epoch(&self, epoch: ChainEpoch) -> anyhow::Result<Option<TipsetKey>> {
140 Ok(self.ts_lookup_db.read().get(&epoch).cloned())
141 }
142
143 fn delete_tipset_key_at_epoch(&self, epoch: ChainEpoch) -> anyhow::Result<()> {
144 self.ts_lookup_db.write().remove(&epoch);
145 Ok(())
146 }
147
148 fn set_tipset_key_at_epoch_raw(
149 &self,
150 epoch: ChainEpoch,
151 tsk: &TipsetKey,
152 ) -> anyhow::Result<()> {
153 self.ts_lookup_db.write().insert(epoch, tsk.clone());
154 Ok(())
155 }
156}
157
158impl EthBlockBloomStore for MemoryDB {
159 fn read_bloom(&self, key: &Cid) -> anyhow::Result<Option<[u8; BLOCK_BLOOM_LEN]>> {
160 Ok(self
161 .eth_block_bloom_db
162 .read()
163 .get(key)
164 .and_then(|entry| decode_block_bloom(entry).map(|(_, bloom)| *bloom)))
165 }
166
167 fn write_bloom(
168 &self,
169 key: &Cid,
170 height: ChainEpoch,
171 bloom: &[u8; BLOCK_BLOOM_LEN],
172 ) -> anyhow::Result<()> {
173 self.eth_block_bloom_db
174 .write()
175 .insert(*key, encode_block_bloom(height, bloom));
176 Ok(())
177 }
178
179 fn delete_blooms_before_height(&self, height: ChainEpoch) -> anyhow::Result<()> {
180 self.eth_block_bloom_db
181 .write()
182 .retain(|_, entry| decode_block_bloom(entry).is_some_and(|(h, _)| h >= height));
183 Ok(())
184 }
185}
186
187impl Blockstore for MemoryDB {
188 fn get(&self, k: &Cid) -> anyhow::Result<Option<Vec<u8>>> {
189 Ok(self.blockchain_db.read().get(k).cloned().or(self
190 .blockchain_persistent_db
191 .read()
192 .get(k)
193 .cloned()))
194 }
195
196 fn put_keyed(&self, k: &Cid, block: &[u8]) -> anyhow::Result<()> {
197 self.blockchain_db.write().insert(*k, block.to_vec());
198 Ok(())
199 }
200}
201
202impl PersistentStore for MemoryDB {
203 fn put_keyed_persistent(&self, k: &Cid, block: &[u8]) -> anyhow::Result<()> {
204 self.blockchain_persistent_db
205 .write()
206 .insert(*k, block.to_vec());
207 Ok(())
208 }
209}
210
211impl BitswapStoreRead for MemoryDB {
212 fn contains(&self, cid: &Cid) -> anyhow::Result<bool> {
213 Ok(self.blockchain_db.read().contains_key(cid))
214 }
215
216 fn get(&self, cid: &Cid) -> anyhow::Result<Option<Vec<u8>>> {
217 Blockstore::get(self, cid)
218 }
219}
220
221impl BitswapStoreReadWrite for MemoryDB {
222 type Hashes = MultihashCode;
223
224 fn insert(&self, block: &crate::libp2p_bitswap::Block64<Self::Hashes>) -> anyhow::Result<()> {
225 self.put_keyed(block.cid(), block.data())
226 }
227}
228
229impl super::HeaviestTipsetKeyProvider for MemoryDB {
230 fn heaviest_tipset_key(&self) -> anyhow::Result<Option<TipsetKey>> {
231 SettingsStoreExt::read_obj::<TipsetKey>(self, crate::db::setting_keys::HEAD_KEY)
232 }
233
234 fn set_heaviest_tipset_key(&self, tsk: &TipsetKey) -> anyhow::Result<()> {
235 SettingsStoreExt::write_obj(self, crate::db::setting_keys::HEAD_KEY, tsk)
236 }
237}
238
239#[derive(Debug, Default, derive_more::Deref)]
240pub struct IndexMapBlockstore {
242 inner: RwLock<IndexMap<Cid, Vec<u8>>>,
243}
244
245impl IndexMapBlockstore {
246 #[allow(dead_code)]
247 pub async fn export_forest_car<W: tokio::io::AsyncWrite + Unpin>(
248 &self,
249 roots: NonEmpty<Cid>,
250 writer: &mut W,
251 ) -> anyhow::Result<()> {
252 let blocks = {
253 let inner = self.inner.read();
254 let invalid_roots = roots
255 .iter()
256 .filter(|&c| !inner.contains_key(c))
257 .collect_vec();
258 anyhow::ensure!(
259 invalid_roots.is_empty(),
260 "All roots should present in the blockstore, invalid roots: {invalid_roots:?}"
261 );
262 inner
263 .iter()
264 .map(|(&cid, data)| {
265 anyhow::Ok(CarBlock {
266 cid,
267 data: data.clone().into(),
268 })
269 })
270 .collect_vec()
271 };
272 let frames =
273 crate::db::car::forest::Encoder::compress_stream_default(futures::stream::iter(blocks));
274 crate::db::car::forest::Encoder::write(writer, roots, frames).await
275 }
276}
277
278impl Blockstore for IndexMapBlockstore {
279 fn get(&self, k: &Cid) -> anyhow::Result<Option<Vec<u8>>> {
280 Ok(self.read().get(k).cloned())
281 }
282
283 fn put_keyed(&self, k: &Cid, block: &[u8]) -> anyhow::Result<()> {
284 self.write().insert(*k, block.to_vec());
285 Ok(())
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use crate::{
293 db::{car::ForestCar, setting_keys::HEAD_KEY},
294 utils::cid::CidCborExt as _,
295 };
296 use fil_actors_shared::fvm_ipld_hamt::Hamt;
297 use fvm_ipld_encoding::DAG_CBOR;
298 use multihash_codetable::Code::Blake2b256;
299 use nunny::vec as nonempty;
300
301 #[tokio::test]
302 async fn test_export_forest_car() {
303 let db = MemoryDB::default();
304 let record1 = b"non-persistent";
305 let key1 = Cid::new_v1(DAG_CBOR, Blake2b256.digest(record1.as_slice()));
306 db.put_keyed(&key1, record1.as_slice()).unwrap();
307
308 let record2 = b"persistent";
309 let key2 = Cid::new_v1(DAG_CBOR, Blake2b256.digest(record2.as_slice()));
310 db.put_keyed_persistent(&key2, record2.as_slice()).unwrap();
311
312 let mut car_db_bytes = vec![];
313 assert!(
314 db.export_forest_car(&mut car_db_bytes)
315 .await
316 .unwrap_err()
317 .to_string()
318 .contains("chain head is not tracked and cannot be exported")
319 );
320
321 db.write_obj(HEAD_KEY, &TipsetKey::from(nonempty![key1]))
322 .unwrap();
323
324 car_db_bytes.clear();
325 db.export_forest_car(&mut car_db_bytes).await.unwrap();
326
327 let car = ForestCar::new(car_db_bytes).unwrap();
328 assert_eq!(car.head_tipset_key(), &nonempty![key1]);
329 assert!(car.has(&key1).unwrap());
330 assert!(car.has(&key2).unwrap());
331 }
332
333 #[test]
334 fn block_bloom_encode_decode() {
335 let bloom = [0xab; 256];
336 let entry = encode_block_bloom(42, &bloom);
337 let (height, decoded) = decode_block_bloom(&entry).unwrap();
338 assert_eq!(height, 42);
339 assert_eq!(decoded, &bloom);
340 assert!(decode_block_bloom(&[0, 1, 2]).is_none());
341 }
342
343 #[tokio::test]
344 async fn test_index_map_blockstore() {
345 const BIT_WIDTH: u32 = 5;
346
347 let db = IndexMapBlockstore::default();
348 let mut hamt: Hamt<_, TipsetKey, ChainEpoch> = Hamt::new_with_bit_width(&db, BIT_WIDTH);
350 let checkpoints = [
351 (
352 0,
353 TipsetKey::from(nunny::vec![Cid::from_cbor_blake2b256(&"1").unwrap()]),
354 ),
355 (
356 5,
357 TipsetKey::from(nunny::vec![Cid::from_cbor_blake2b256(&"5").unwrap()]),
358 ),
359 (
360 10,
361 TipsetKey::from(nunny::vec![Cid::from_cbor_blake2b256(&"10").unwrap()]),
362 ),
363 ];
364 for (epoch, tsk) in checkpoints.iter().cloned() {
365 hamt.set(epoch, tsk).unwrap();
366 }
367 let hamt_root = hamt.flush().unwrap();
368 assert!(db.has(&hamt_root).unwrap(), "hamt root should present");
369
370 let mut car = vec![];
372 db.export_forest_car(
373 nunny::vec![Cid::from_cbor_blake2b256(&"invalid").unwrap()],
374 &mut car,
375 )
376 .await
377 .unwrap_err();
378
379 let mut car_bytes = vec![];
381 let car_roots = nunny::vec![hamt_root];
382 db.export_forest_car(car_roots.clone(), &mut car_bytes)
383 .await
384 .unwrap();
385
386 let car: ForestCar<Vec<u8>> = ForestCar::new(car_bytes).unwrap();
387 let hamt_from_car: Hamt<_, TipsetKey, ChainEpoch> =
388 Hamt::load_with_bit_width(&hamt_root, &car, BIT_WIDTH).unwrap();
389
390 let checkpoints_from_memdb_hamt = {
391 let mut v = vec![];
392 hamt.for_each_cacheless(|epoch, tsk| {
393 v.push((*epoch, tsk.clone()));
394 anyhow::Ok(())
395 })
396 .unwrap();
397 v
398 };
399 let checkpoints_from_car_hamt = {
400 let mut v = vec![];
401 hamt_from_car
402 .for_each_cacheless(|epoch, tsk| {
403 v.push((*epoch, tsk.clone()));
404 anyhow::Ok(())
405 })
406 .unwrap();
407 v
408 };
409 assert_eq!(checkpoints_from_memdb_hamt, checkpoints_from_car_hamt);
411 assert_eq!(
413 HashMap::from_iter(checkpoints),
414 HashMap::from_iter(checkpoints_from_car_hamt)
415 );
416 }
417}