1mod gc;
5pub use gc::*;
6
7use super::{
8 BLOCK_BLOOM_LEN, EthBlockBloomStore, EthMappingsStore, PersistentStore, SettingsStore,
9 decode_block_bloom, encode_block_bloom,
10};
11use crate::blocks::TipsetKey;
12use crate::db::{DBStatistics, parity_db_config::ParityDbConfig};
13use crate::libp2p_bitswap::{BitswapStoreRead, BitswapStoreReadWrite};
14use crate::prelude::*;
15use crate::rpc::eth::types::EthHash;
16use crate::shim::clock::ChainEpoch;
17use crate::utils::{broadcast::has_subscribers, multihash::prelude::*};
18use bytes::Bytes;
19use fvm_ipld_encoding::DAG_CBOR;
20use parity_db::{CompressionType, Db, Operation, Options};
21use parking_lot::RwLock;
22use std::path::PathBuf;
23use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
24use tracing::warn;
25
26#[derive(Copy, Clone, Debug, Display, PartialEq, FromRepr, EnumIter)]
29#[repr(u8)]
30pub enum DbColumn {
31 GraphDagCborBlake2b256,
34 GraphFull,
39 Settings,
41 EthMappings,
43 PersistentGraph,
47 EthBlockBloom,
49}
50
51impl DbColumn {
52 fn create_column_options(compression: CompressionType) -> Vec<parity_db::ColumnOptions> {
53 DbColumn::iter()
54 .map(|col| {
55 match col {
56 DbColumn::GraphDagCborBlake2b256 | DbColumn::PersistentGraph => {
57 parity_db::ColumnOptions {
58 preimage: true,
59 compression,
60 ..Default::default()
61 }
62 }
63 DbColumn::GraphFull => parity_db::ColumnOptions {
64 preimage: true,
65 btree_index: true,
67 compression,
68 ..Default::default()
69 },
70 DbColumn::Settings => parity_db::ColumnOptions {
71 preimage: false,
74 btree_index: true,
76 compression,
77 ..Default::default()
78 },
79 DbColumn::EthMappings => parity_db::ColumnOptions {
80 preimage: false,
81 btree_index: false,
82 compression,
83 ..Default::default()
84 },
85 DbColumn::EthBlockBloom => parity_db::ColumnOptions {
86 preimage: false,
87 btree_index: true,
88 compression,
89 ..Default::default()
90 },
91 }
92 })
93 .collect()
94 }
95}
96
97type WriteOpsBroadcastTxSender = tokio::sync::broadcast::Sender<Vec<(Cid, Bytes)>>;
98
99pub struct ParityDb {
100 pub db: parity_db::Db,
101 statistics_enabled: bool,
102 disable_persistent_fallback: bool,
104 write_ops_broadcast_tx: RwLock<Option<WriteOpsBroadcastTxSender>>,
105}
106
107impl ParityDb {
108 pub fn to_options(path: impl Into<PathBuf>, config: &ParityDbConfig) -> Options {
109 Options {
110 path: path.into(),
111 sync_wal: true,
112 sync_data: true,
113 stats: config.enable_statistics,
114 salt: None,
115 columns: DbColumn::create_column_options(CompressionType::Lz4),
116 compression_threshold: [(0, 128)].into_iter().collect(),
117 }
118 }
119
120 pub fn open(path: impl Into<PathBuf>, config: &ParityDbConfig) -> anyhow::Result<Self> {
121 let opts = Self::to_options(path.into(), config);
122 Self::open_with_options(&opts)
123 }
124
125 pub fn open_with_options(options: &Options) -> anyhow::Result<Self> {
126 Ok(Self {
127 db: Db::open_or_create(options)?,
128 statistics_enabled: options.stats,
129 disable_persistent_fallback: false,
130 write_ops_broadcast_tx: RwLock::new(None),
131 })
132 }
133
134 fn choose_column(cid: &Cid) -> DbColumn {
137 match cid.codec() {
138 DAG_CBOR if cid.hash().code() == u64::from(MultihashCode::Blake2b256) => {
139 DbColumn::GraphDagCborBlake2b256
140 }
141 _ => DbColumn::GraphFull,
142 }
143 }
144
145 fn read_from_column<K>(&self, key: K, column: DbColumn) -> anyhow::Result<Option<Vec<u8>>>
146 where
147 K: AsRef<[u8]>,
148 {
149 self.db
150 .get(column as u8, key.as_ref())
151 .with_context(|| format!("error from column {column}"))
152 }
153
154 fn write_to_column<K, V>(&self, key: K, value: V, column: DbColumn) -> anyhow::Result<()>
155 where
156 K: AsRef<[u8]>,
157 V: AsRef<[u8]>,
158 {
159 let tx = [(column as u8, key.as_ref(), Some(value.as_ref().to_vec()))];
160 self.db
161 .commit(tx)
162 .with_context(|| format!("error writing to column {column}"))
163 }
164}
165
166impl SettingsStore for ParityDb {
167 fn read_bin(&self, key: &str) -> anyhow::Result<Option<Vec<u8>>> {
168 self.read_from_column(key.as_bytes(), DbColumn::Settings)
169 }
170
171 fn write_bin(&self, key: &str, value: &[u8]) -> anyhow::Result<()> {
172 self.write_to_column(key.as_bytes(), value, DbColumn::Settings)
173 }
174
175 fn exists(&self, key: &str) -> anyhow::Result<bool> {
176 self.db
177 .get_size(DbColumn::Settings as u8, key.as_bytes())
178 .map(|size| size.is_some())
179 .context("error checking if key exists")
180 }
181
182 fn setting_keys(&self) -> anyhow::Result<Vec<String>> {
183 let mut iter = self.db.iter(DbColumn::Settings as u8)?;
184 let mut keys = vec![];
185 while let Some((key, _)) = iter.next()? {
186 keys.push(String::from_utf8(key)?);
187 }
188 Ok(keys)
189 }
190}
191
192impl super::HeaviestTipsetKeyProvider for ParityDb {
193 fn heaviest_tipset_key(&self) -> anyhow::Result<Option<TipsetKey>> {
194 super::SettingsStoreExt::read_obj::<TipsetKey>(self, super::setting_keys::HEAD_KEY)
195 }
196
197 fn set_heaviest_tipset_key(&self, tsk: &TipsetKey) -> anyhow::Result<()> {
198 super::SettingsStoreExt::write_obj(self, super::setting_keys::HEAD_KEY, tsk)
199 }
200}
201
202impl EthMappingsStore for ParityDb {
203 fn read_bin(&self, key: &EthHash) -> anyhow::Result<Option<Vec<u8>>> {
204 self.read_from_column(key.0.as_bytes(), DbColumn::EthMappings)
205 }
206
207 fn write_bin(&self, key: &EthHash, value: &[u8]) -> anyhow::Result<()> {
208 self.write_to_column(key.0.as_bytes(), value, DbColumn::EthMappings)
209 }
210
211 fn exists(&self, key: &EthHash) -> anyhow::Result<bool> {
212 self.db
213 .get_size(DbColumn::EthMappings as u8, key.0.as_bytes())
214 .map(|size| size.is_some())
215 .context("error checking if key exists")
216 }
217
218 fn get_message_cids(&self) -> anyhow::Result<Vec<(Cid, u64)>> {
219 let mut cids = Vec::new();
220
221 self.db
222 .iter_column_while(DbColumn::EthMappings as u8, |val| {
223 if let Ok(value) = fvm_ipld_encoding::from_slice::<(Cid, u64)>(&val.value) {
224 cids.push(value);
225 }
226 true
227 })?;
228
229 Ok(cids)
230 }
231
232 fn delete(&self, keys: Vec<EthHash>) -> anyhow::Result<()> {
233 Ok(self.db.commit_changes(keys.into_iter().map(|key| {
234 let bytes = key.0.as_bytes().to_vec();
235 (DbColumn::EthMappings as u8, Operation::Dereference(bytes))
236 }))?)
237 }
238
239 fn tipset_key_by_epoch(&self, epoch: ChainEpoch) -> anyhow::Result<Option<TipsetKey>> {
240 let key = epoch.to_le_bytes();
241 if let Some(bytes) = self.read_from_column(key, DbColumn::EthMappings)? {
242 Ok(Some(fvm_ipld_encoding::from_slice(&bytes)?))
243 } else {
244 Ok(None)
245 }
246 }
247
248 fn delete_tipset_key_at_epoch(&self, epoch: ChainEpoch) -> anyhow::Result<()> {
249 Ok(self.db.commit_changes(std::iter::once(epoch).map(|epoch| {
250 (
251 DbColumn::EthMappings as u8,
252 Operation::Dereference(epoch.to_le_bytes().to_vec()),
253 )
254 }))?)
255 }
256
257 fn set_tipset_key_at_epoch_raw(
258 &self,
259 epoch: ChainEpoch,
260 tsk: &TipsetKey,
261 ) -> anyhow::Result<()> {
262 let key = epoch.to_le_bytes();
263 let bytes = fvm_ipld_encoding::to_vec(tsk)?;
264 self.write_to_column(key, bytes, DbColumn::EthMappings)
265 }
266}
267
268impl EthBlockBloomStore for ParityDb {
269 fn read_bloom(&self, key: &Cid) -> anyhow::Result<Option<[u8; BLOCK_BLOOM_LEN]>> {
270 Ok(self
271 .read_from_column(key.to_bytes(), DbColumn::EthBlockBloom)?
272 .and_then(|entry| decode_block_bloom(&entry).map(|(_, bloom)| *bloom)))
273 }
274
275 fn write_bloom(
276 &self,
277 key: &Cid,
278 height: ChainEpoch,
279 bloom: &[u8; BLOCK_BLOOM_LEN],
280 ) -> anyhow::Result<()> {
281 self.write_to_column(
282 key.to_bytes(),
283 encode_block_bloom(height, bloom),
284 DbColumn::EthBlockBloom,
285 )
286 }
287
288 fn delete_blooms_before_height(&self, height: ChainEpoch) -> anyhow::Result<()> {
289 let mut stale = Vec::new();
290 let mut iter = self.db.iter(DbColumn::EthBlockBloom as u8)?;
291 while let Some((key, entry)) = iter.next()? {
292 if decode_block_bloom(&entry).is_none_or(|(h, _)| h < height) {
294 stale.push(key);
295 }
296 }
297 Ok(self.db.commit_changes(
298 stale
299 .into_iter()
300 .map(|key| (DbColumn::EthBlockBloom as u8, Operation::Dereference(key))),
301 )?)
302 }
303}
304
305impl Blockstore for ParityDb {
306 fn get(&self, k: &Cid) -> anyhow::Result<Option<Vec<u8>>> {
307 let column = Self::choose_column(k);
308 let res = self.read_from_column(k.to_bytes(), column)?;
309 if res.is_some() {
310 return Ok(res);
311 }
312 self.get_persistent(k)
313 }
314
315 fn put_keyed(&self, k: &Cid, block: &[u8]) -> anyhow::Result<()> {
316 let column = Self::choose_column(k);
317 self.write_to_column(k.to_bytes(), block, column)?;
319 match &*self.write_ops_broadcast_tx.read() {
320 Some(tx) if has_subscribers(tx) => {
321 let _ = tx.send(vec![(*k, Bytes::copy_from_slice(block))]);
322 }
323 _ => {}
324 }
325
326 Ok(())
327 }
328
329 fn put_many_keyed<D, I>(&self, blocks: I) -> anyhow::Result<()>
330 where
331 Self: Sized,
332 D: AsRef<[u8]>,
333 I: IntoIterator<Item = (Cid, D)>,
334 {
335 let tx_opt = &*self.write_ops_broadcast_tx.read();
336 let has_subscribers = tx_opt.as_ref().map(has_subscribers).unwrap_or_default();
337 let mut values_for_subscriber = vec![];
338 let values = blocks.into_iter().map(|(k, v)| {
339 let column = Self::choose_column(&k);
340 let v = v.as_ref().to_vec();
341 if has_subscribers {
342 values_for_subscriber.push((k, Bytes::copy_from_slice(&v)));
343 }
344 (column, k.to_bytes(), v)
345 });
346 let tx = values
347 .into_iter()
348 .map(|(col, k, v)| (col as u8, Operation::Set(k, v)));
349 self.db.commit_changes(tx).context("error bulk writing")?;
350 if let Some(tx) = tx_opt {
351 let _ = tx.send(values_for_subscriber);
352 }
353 Ok(())
354 }
355}
356
357impl PersistentStore for ParityDb {
358 fn put_keyed_persistent(&self, k: &Cid, block: &[u8]) -> anyhow::Result<()> {
359 self.write_to_column(k.to_bytes(), block, DbColumn::PersistentGraph)
360 }
361}
362
363impl BitswapStoreRead for ParityDb {
364 fn contains(&self, cid: &Cid) -> anyhow::Result<bool> {
365 for column in [DbColumn::GraphDagCborBlake2b256, DbColumn::GraphFull] {
371 if self
372 .db
373 .get_size(column as u8, &cid.to_bytes())
374 .context("error checking if key exists")?
375 .is_some()
376 {
377 return Ok(true);
378 }
379 }
380 Ok(false)
381 }
382
383 fn get(&self, cid: &Cid) -> anyhow::Result<Option<Vec<u8>>> {
384 Blockstore::get(self, cid)
385 }
386}
387
388impl BitswapStoreReadWrite for ParityDb {
389 type Hashes = MultihashCode;
390
391 fn insert(&self, block: &crate::libp2p_bitswap::Block64<Self::Hashes>) -> anyhow::Result<()> {
392 self.put_keyed(block.cid(), block.data())
393 }
394}
395
396impl DBStatistics for ParityDb {
397 fn get_statistics(&self) -> Option<String> {
398 if !self.statistics_enabled {
399 return None;
400 }
401
402 let mut buf = Vec::new();
403 if let Err(err) = self.db.write_stats_text(&mut buf, None) {
404 warn!("Unable to write database statistics: {err}");
405 return None;
406 }
407
408 match String::from_utf8(buf) {
409 Ok(stats) => Some(stats),
410 Err(e) => {
411 warn!("Malformed statistics: {e}");
412 None
413 }
414 }
415 }
416}
417
418type Op = (u8, Operation<Vec<u8>, Vec<u8>>);
419
420impl ParityDb {
421 #[allow(dead_code)]
426 pub fn dereference_operation(key: &Cid) -> Op {
427 let column = Self::choose_column(key);
428 (column as u8, Operation::Dereference(key.to_bytes()))
429 }
430
431 pub fn set_operation(column: u8, key: Vec<u8>, value: Vec<u8>) -> Op {
438 (column, Operation::Set(key, value))
439 }
440
441 fn get_persistent(&self, k: &Cid) -> anyhow::Result<Option<Vec<u8>>> {
443 if self.disable_persistent_fallback {
444 return Ok(None);
445 }
446 self.read_from_column(k.to_bytes(), DbColumn::PersistentGraph)
447 }
448}
449
450impl super::BlockstoreWriteOpsSubscribable for ParityDb {
451 fn subscribe_write_ops(
452 &self,
453 ) -> anyhow::Result<tokio::sync::broadcast::Receiver<Vec<(Cid, bytes::Bytes)>>> {
454 if let Some(tx) = self.write_ops_broadcast_tx.read().as_ref() {
456 return Ok(tx.subscribe());
457 }
458 let mut tx_lock = self.write_ops_broadcast_tx.write();
459 if let Some(tx) = tx_lock.as_ref() {
460 return Ok(tx.subscribe());
461 }
462 let (tx, rx) = tokio::sync::broadcast::channel(65536);
463 *tx_lock = Some(tx);
464 Ok(rx)
465 }
466
467 fn unsubscribe_write_ops(&self) {
468 self.write_ops_broadcast_tx.write().take();
469 }
470}
471
472#[cfg(test)]
473mod test {
474 use super::*;
475 use crate::db::{BlockstoreWriteOpsSubscribable, tests::db_utils::parity::TempParityDB};
476 use fvm_ipld_encoding::IPLD_RAW;
477 use nom::AsBytes;
478 use std::ops::Deref;
479
480 #[test]
481 fn write_read_different_columns_test() {
482 let db = TempParityDB::new();
483 let data = [
484 b"h'nglui mglw'nafh".to_vec(),
485 b"Cthulhu".to_vec(),
486 b"R'lyeh wgah'nagl fhtagn!!".to_vec(),
487 ];
488 let cids = [
489 Cid::new_v1(DAG_CBOR, MultihashCode::Blake2b256.digest(&data[0])),
490 Cid::new_v1(DAG_CBOR, MultihashCode::Sha2_256.digest(&data[1])),
491 Cid::new_v1(IPLD_RAW, MultihashCode::Blake2b256.digest(&data[1])),
492 ];
493
494 let cases = [
495 (DbColumn::GraphDagCborBlake2b256, cids[0], &data[0]),
496 (DbColumn::GraphFull, cids[1], &data[1]),
497 (DbColumn::GraphFull, cids[2], &data[2]),
498 ];
499
500 for (_, cid, data) in cases {
501 db.put_keyed(&cid, data).unwrap();
502 }
503
504 for (column, cid, data) in cases {
505 let actual = db
506 .read_from_column(cid.to_bytes(), column)
507 .unwrap()
508 .expect("data not found");
509 assert_eq!(data, actual.as_bytes());
510
511 let other_column = match column {
513 DbColumn::GraphDagCborBlake2b256 => DbColumn::GraphFull,
514 DbColumn::GraphFull => DbColumn::GraphDagCborBlake2b256,
515 DbColumn::Settings => panic!("invalid column for IPLD data"),
516 DbColumn::EthMappings => panic!("invalid column for IPLD data"),
517 DbColumn::EthBlockBloom => panic!("invalid column for IPLD data"),
518 DbColumn::PersistentGraph => panic!("invalid column for GC enabled IPLD data"),
519 };
520 let actual = db.read_from_column(cid.to_bytes(), other_column).unwrap();
521 assert!(actual.is_none());
522
523 let actual = fvm_ipld_blockstore::Blockstore::get(db.as_ref(), &cid)
525 .unwrap()
526 .expect("data not found");
527 assert_eq!(data, actual.as_slice());
528 }
529
530 db.write_to_column(b"dagon", b"bloop", DbColumn::Settings)
532 .unwrap();
533 let actual = db
534 .read_from_column(b"dagon", DbColumn::Settings)
535 .unwrap()
536 .expect("data not found");
537 assert_eq!(b"bloop", actual.as_bytes());
538 }
539
540 #[test]
541 fn choose_column_test() {
542 let data = [0u8; 32];
543 let cases = [
544 (
545 Cid::new_v1(DAG_CBOR, MultihashCode::Blake2b256.digest(&data)),
546 DbColumn::GraphDagCborBlake2b256,
547 ),
548 (
549 Cid::new_v1(
550 fvm_ipld_encoding::CBOR,
551 MultihashCode::Blake2b256.digest(&data),
552 ),
553 DbColumn::GraphFull,
554 ),
555 (
556 Cid::new_v1(DAG_CBOR, MultihashCode::Sha2_256.digest(&data)),
557 DbColumn::GraphFull,
558 ),
559 ];
560
561 for (cid, expected) in cases {
562 let actual = ParityDb::choose_column(&cid);
563 assert_eq!(expected, actual);
564 }
565 }
566
567 #[test]
568 fn persistent_tests() {
569 let db = TempParityDB::new();
570 let data = [
571 b"h'nglui mglw'nafh".to_vec(),
572 b"Cthulhu".to_vec(),
573 b"R'lyeh wgah'nagl fhtagn!!".to_vec(),
574 ];
575
576 let persistent_data = data
577 .clone()
578 .into_iter()
579 .map(|mut entry| {
580 entry.push(255);
581 entry
582 })
583 .collect_vec();
584
585 let cids = [
586 Cid::new_v1(DAG_CBOR, MultihashCode::Blake2b256.digest(&data[0])),
587 Cid::new_v1(DAG_CBOR, MultihashCode::Sha2_256.digest(&data[1])),
588 Cid::new_v1(IPLD_RAW, MultihashCode::Blake2b256.digest(&data[1])),
589 ];
590
591 for idx in 0..3 {
592 let cid = &cids[idx];
593 let persistent_entry = &persistent_data[idx];
594 let data_entry = &data[idx];
595 db.put_keyed_persistent(cid, persistent_entry).unwrap();
596 assert_eq!(
599 Blockstore::get(db.deref(), cid).unwrap(),
600 Some(persistent_entry.clone())
601 );
602 assert!(
603 db.read_from_column(cid.to_bytes(), DbColumn::PersistentGraph)
604 .unwrap()
605 .is_some()
606 );
607 db.put_keyed(cid, data_entry).unwrap();
608 assert_eq!(
609 Blockstore::get(db.deref(), cid).unwrap(),
610 Some(data_entry.clone())
611 );
612 }
613 }
614
615 #[test]
616 fn subscription_tests() {
617 let db = TempParityDB::new();
618 assert!(db.write_ops_broadcast_tx.read().is_none());
619 let data = [
620 b"h'nglui mglw'nafh".to_vec(),
621 b"Cthulhu".to_vec(),
622 b"R'lyeh wgah'nagl fhtagn!!".to_vec(),
623 ];
624
625 let cids = [
626 Cid::new_v1(DAG_CBOR, MultihashCode::Blake2b256.digest(&data[0])),
627 Cid::new_v1(DAG_CBOR, MultihashCode::Sha2_256.digest(&data[1])),
628 Cid::new_v1(IPLD_RAW, MultihashCode::Blake2b256.digest(&data[1])),
629 ];
630
631 let mut rx1 = db.subscribe_write_ops().unwrap();
632 let mut rx2 = db.subscribe_write_ops().unwrap();
633
634 assert!(has_subscribers(
635 db.write_ops_broadcast_tx.read().as_ref().unwrap()
636 ));
637
638 for (idx, cid) in cids.iter().enumerate() {
639 let data_entry = &data[idx];
640 db.put_keyed(cid, data_entry).unwrap();
641 let expected = vec![(*cid, Bytes::copy_from_slice(data_entry))];
642 assert_eq!(rx1.blocking_recv().unwrap(), expected);
643 assert_eq!(rx2.blocking_recv().unwrap(), expected);
644 }
645
646 drop(rx1);
647 drop(rx2);
648
649 assert!(!has_subscribers(
650 db.write_ops_broadcast_tx.read().as_ref().unwrap()
651 ));
652
653 db.unsubscribe_write_ops();
654
655 assert!(db.write_ops_broadcast_tx.read().is_none());
656 }
657}