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