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