Skip to main content

consensus_core/storage/
rocksdb_store.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    collections::{BTreeMap, VecDeque},
6    ops::Bound::{Excluded, Included},
7    time::Duration,
8};
9
10use bytes::Bytes;
11use consensus_config::AuthorityIndex;
12use consensus_types::block::{BlockDigest, BlockRef, Round, TransactionIndex};
13use mysten_common::ZipDebugEqIteratorExt;
14use sui_macros::fail_point;
15#[cfg(not(tidehunter))]
16use typed_store::rocks::{DBMapTableConfigMap, default_db_options};
17use typed_store::{
18    DBMapUtils, Map as _,
19    metrics::SamplingInterval,
20    rocks::{DBMap, MetricConf},
21};
22
23use super::{CommitInfo, Store, WriteBatch};
24use crate::{
25    block::{BlockAPI as _, SignedBlock, VerifiedBlock},
26    commit::{CommitAPI as _, CommitDigest, CommitIndex, CommitRange, CommitRef, TrustedCommit},
27    error::{ConsensusError, ConsensusResult},
28};
29
30/// Persistent storage with RocksDB.
31#[derive(DBMapUtils)]
32#[cfg_attr(tidehunter, tidehunter)]
33pub struct RocksDBStore {
34    /// Stores SignedBlock by refs.
35    blocks: DBMap<(Round, AuthorityIndex, BlockDigest), Bytes>,
36    /// A secondary index that orders refs first by authors.
37    #[rename = "digests"]
38    digests_by_authorities: DBMap<(AuthorityIndex, Round, BlockDigest), ()>,
39    /// Maps commit index to Commit.
40    commits: DBMap<(CommitIndex, CommitDigest), Bytes>,
41    /// Collects votes on commits.
42    /// TODO: batch multiple votes into a single row.
43    commit_votes: DBMap<(CommitIndex, CommitDigest, BlockRef), ()>,
44    /// Stores info related to Commit that helps recovery.
45    commit_info: DBMap<(CommitIndex, CommitDigest), CommitInfo>,
46    /// Maps finalized commits to the transactions rejected in the commit.
47    finalized_commits:
48        DBMap<(CommitIndex, CommitDigest), BTreeMap<BlockRef, Vec<TransactionIndex>>>,
49}
50
51impl RocksDBStore {
52    const BLOCKS_CF: &'static str = "blocks";
53    const DIGESTS_BY_AUTHORITIES_CF: &'static str = "digests";
54    const COMMITS_CF: &'static str = "commits";
55    const COMMIT_VOTES_CF: &'static str = "commit_votes";
56    const COMMIT_INFO_CF: &'static str = "commit_info";
57    const FINALIZED_COMMITS_CF: &'static str = "finalized_commits";
58
59    /// Creates a new instance of RocksDB storage.
60    #[cfg(not(tidehunter))]
61    pub fn new(path: &str) -> Self {
62        // Consensus data has high write throughput (all transactions) and is rarely read
63        // (only during recovery and when helping peers catch up).
64        let db_options =
65            default_db_options().optimize_db_for_write_throughput(2, /* unlimited */ true);
66        let mut metrics_conf = MetricConf::new("consensus");
67        metrics_conf.read_sample_interval = SamplingInterval::new(Duration::from_secs(60), 0);
68        let cf_options = default_db_options().optimize_for_no_deletion();
69        let column_family_options = DBMapTableConfigMap::new(BTreeMap::from([
70            (
71                Self::BLOCKS_CF.to_string(),
72                cf_options
73                    .clone()
74                    // Using larger block is ok since there is not much point reads on the cf.
75                    .set_block_options(512, 128 << 10),
76            ),
77            (
78                Self::DIGESTS_BY_AUTHORITIES_CF.to_string(),
79                cf_options.clone(),
80            ),
81            (Self::COMMITS_CF.to_string(), cf_options.clone()),
82            (Self::COMMIT_VOTES_CF.to_string(), cf_options.clone()),
83            (Self::COMMIT_INFO_CF.to_string(), cf_options.clone()),
84            (Self::FINALIZED_COMMITS_CF.to_string(), cf_options.clone()),
85        ]));
86        Self::open_tables_read_write(
87            path.into(),
88            metrics_conf,
89            Some(db_options.options),
90            Some(column_family_options),
91        )
92    }
93
94    #[cfg(tidehunter)]
95    pub fn new(path: &str) -> Self {
96        tracing::warn!("Consensus store using tidehunter");
97        use typed_store::tidehunter_util::{
98            KeyIndexing, KeySpaceConfig, KeyType, ThConfig, default_mutex_count,
99        };
100        let mutexes = default_mutex_count();
101        let index_digest_key = KeyIndexing::key_reduction(36, 0..12);
102        let index_index_digest_key = KeyIndexing::key_reduction(40, 0..24);
103        let commit_vote_key = KeyIndexing::key_reduction(76, 0..60);
104        let u32_prefix = KeyType::from_prefix_bits(3 * 8);
105        let u64_prefix = KeyType::from_prefix_bits(6 * 8);
106        let configs = vec![
107            (
108                Self::BLOCKS_CF.to_string(),
109                ThConfig::new_with_config_indexing(
110                    index_index_digest_key.clone(),
111                    mutexes,
112                    u32_prefix,
113                    KeySpaceConfig::new(),
114                ),
115            ),
116            (
117                Self::DIGESTS_BY_AUTHORITIES_CF.to_string(),
118                ThConfig::new_with_config_indexing(
119                    index_index_digest_key.clone(),
120                    mutexes,
121                    u64_prefix,
122                    KeySpaceConfig::new(),
123                ),
124            ),
125            (
126                Self::COMMITS_CF.to_string(),
127                ThConfig::new_with_indexing(index_digest_key.clone(), mutexes, u32_prefix),
128            ),
129            (
130                Self::COMMIT_VOTES_CF.to_string(),
131                ThConfig::new_with_config_indexing(
132                    commit_vote_key,
133                    mutexes,
134                    u32_prefix,
135                    KeySpaceConfig::new(),
136                ),
137            ),
138            (
139                Self::COMMIT_INFO_CF.to_string(),
140                ThConfig::new_with_indexing(index_digest_key.clone(), mutexes, u32_prefix),
141            ),
142            (
143                Self::FINALIZED_COMMITS_CF.to_string(),
144                ThConfig::new_with_indexing(index_digest_key.clone(), mutexes, u32_prefix),
145            ),
146        ];
147        Self::open_tables_read_write(
148            path.into(),
149            MetricConf::new("consensus")
150                .with_sampling(SamplingInterval::new(Duration::from_secs(60), 0))
151                .with_th_batch_compression(),
152            configs.into_iter().collect(),
153        )
154    }
155}
156
157impl Store for RocksDBStore {
158    fn write(&self, write_batch: WriteBatch) -> ConsensusResult<()> {
159        fail_point!("consensus-store-before-write");
160
161        let mut batch = self.blocks.batch();
162        for block in write_batch.blocks {
163            let block_ref = block.reference();
164            batch
165                .insert_batch(
166                    &self.blocks,
167                    [(
168                        (block_ref.round, block_ref.author, block_ref.digest),
169                        block.serialized(),
170                    )],
171                )
172                .map_err(ConsensusError::RocksDBFailure)?;
173            batch
174                .insert_batch(
175                    &self.digests_by_authorities,
176                    [((block_ref.author, block_ref.round, block_ref.digest), ())],
177                )
178                .map_err(ConsensusError::RocksDBFailure)?;
179            for vote in block.commit_votes() {
180                batch
181                    .insert_batch(
182                        &self.commit_votes,
183                        [((vote.index, vote.digest, block_ref), ())],
184                    )
185                    .map_err(ConsensusError::RocksDBFailure)?;
186            }
187        }
188
189        for commit in write_batch.commits {
190            batch
191                .insert_batch(
192                    &self.commits,
193                    [((commit.index(), commit.digest()), commit.serialized())],
194                )
195                .map_err(ConsensusError::RocksDBFailure)?;
196        }
197
198        for (commit_ref, commit_info) in write_batch.commit_info {
199            batch
200                .insert_batch(
201                    &self.commit_info,
202                    [((commit_ref.index, commit_ref.digest), commit_info)],
203                )
204                .map_err(ConsensusError::RocksDBFailure)?;
205        }
206
207        for (commit_ref, rejected_transactions) in write_batch.finalized_commits {
208            batch
209                .insert_batch(
210                    &self.finalized_commits,
211                    [((commit_ref.index, commit_ref.digest), rejected_transactions)],
212                )
213                .map_err(ConsensusError::RocksDBFailure)?;
214        }
215
216        batch.write()?;
217        fail_point!("consensus-store-after-write");
218        Ok(())
219    }
220
221    fn read_blocks(&self, refs: &[BlockRef]) -> ConsensusResult<Vec<Option<VerifiedBlock>>> {
222        let keys = refs
223            .iter()
224            .map(|r| (r.round, r.author, r.digest))
225            .collect::<Vec<_>>();
226        let serialized = self.blocks.multi_get(keys)?;
227        let mut blocks = vec![];
228        for (key, serialized) in refs.iter().zip_debug_eq(serialized) {
229            if let Some(serialized) = serialized {
230                let signed_block: SignedBlock =
231                    bcs::from_bytes(&serialized).map_err(ConsensusError::MalformedBlock)?;
232                // Only accepted blocks should have been written to storage.
233                let block = VerifiedBlock::new_verified(signed_block, serialized);
234                // Makes sure block data is not corrupted, by comparing digests.
235                assert_eq!(*key, block.reference());
236                blocks.push(Some(block));
237            } else {
238                blocks.push(None);
239            }
240        }
241        Ok(blocks)
242    }
243
244    fn contains_blocks(&self, refs: &[BlockRef]) -> ConsensusResult<Vec<bool>> {
245        let refs = refs
246            .iter()
247            .map(|r| (r.round, r.author, r.digest))
248            .collect::<Vec<_>>();
249        let exist = self.blocks.multi_contains_keys(refs)?;
250        Ok(exist)
251    }
252
253    fn scan_blocks_by_author(
254        &self,
255        author: AuthorityIndex,
256        start_round: Round,
257    ) -> ConsensusResult<Vec<VerifiedBlock>> {
258        self.scan_blocks_by_author_in_range(author, start_round, Round::MAX, usize::MAX)
259    }
260
261    fn scan_blocks_by_author_in_range(
262        &self,
263        author: AuthorityIndex,
264        start_round: Round,
265        end_round: Round,
266        limit: usize,
267    ) -> ConsensusResult<Vec<VerifiedBlock>> {
268        let mut refs = vec![];
269        for kv in self.digests_by_authorities.safe_range_iter((
270            Included((author, start_round, BlockDigest::MIN)),
271            Excluded((author, end_round, BlockDigest::MIN)),
272        )) {
273            let ((author, round, digest), _) = kv?;
274            refs.push(BlockRef::new(round, author, digest));
275            if refs.len() >= limit {
276                break;
277            }
278        }
279        let results = self.read_blocks(refs.as_slice())?;
280        let mut blocks = Vec::with_capacity(refs.len());
281        for (r, block) in refs.into_iter().zip_debug_eq(results) {
282            blocks.push(
283                block.unwrap_or_else(|| panic!("Storage inconsistency: block {:?} not found!", r)),
284            );
285        }
286        Ok(blocks)
287    }
288
289    // The method returns the last `num_of_rounds` rounds blocks by author in round ascending order.
290    // When a `before_round` is defined then the blocks of round `<=before_round` are returned. If not
291    // then the max value for round will be used as cut off.
292    fn scan_last_blocks_by_author(
293        &self,
294        author: AuthorityIndex,
295        num_of_rounds: u64,
296        before_round: Option<Round>,
297    ) -> ConsensusResult<Vec<VerifiedBlock>> {
298        let before_round = before_round.unwrap_or(Round::MAX);
299        let mut refs = VecDeque::new();
300        for kv in self
301            .digests_by_authorities
302            .reversed_safe_iter_with_bounds(
303                Some((author, Round::MIN, BlockDigest::MIN)),
304                Some((author, before_round, BlockDigest::MAX)),
305            )?
306            .take(num_of_rounds as usize)
307        {
308            let ((author, round, digest), _) = kv?;
309            refs.push_front(BlockRef::new(round, author, digest));
310        }
311        let refs_slice = refs.make_contiguous();
312        let results = self.read_blocks(refs_slice)?;
313        let mut blocks = vec![];
314        for (r, block) in refs.into_iter().zip_debug_eq(results) {
315            blocks.push(
316                block.unwrap_or_else(|| panic!("Storage inconsistency: block {:?} not found!", r)),
317            );
318        }
319        Ok(blocks)
320    }
321
322    fn read_last_commit(&self) -> ConsensusResult<Option<TrustedCommit>> {
323        let Some(result) = self
324            .commits
325            .reversed_safe_iter_with_bounds(None, None)?
326            .next()
327        else {
328            return Ok(None);
329        };
330        let ((_index, digest), serialized) = result?;
331        let commit = TrustedCommit::new_trusted(
332            bcs::from_bytes(&serialized).map_err(ConsensusError::MalformedCommit)?,
333            serialized,
334        );
335        assert_eq!(commit.digest(), digest);
336        Ok(Some(commit))
337    }
338
339    fn scan_commits(&self, range: CommitRange) -> ConsensusResult<Vec<TrustedCommit>> {
340        let mut commits = vec![];
341        for result in self.commits.safe_range_iter((
342            Included((range.start(), CommitDigest::MIN)),
343            Included((range.end(), CommitDigest::MAX)),
344        )) {
345            let ((_index, digest), serialized) = result?;
346            let commit = TrustedCommit::new_trusted(
347                bcs::from_bytes(&serialized).map_err(ConsensusError::MalformedCommit)?,
348                serialized,
349            );
350            assert_eq!(commit.digest(), digest);
351            commits.push(commit);
352        }
353        Ok(commits)
354    }
355
356    fn read_commit_votes(&self, commit_index: CommitIndex) -> ConsensusResult<Vec<BlockRef>> {
357        let mut votes = Vec::new();
358        for vote in self.commit_votes.safe_range_iter((
359            Included((commit_index, CommitDigest::MIN, BlockRef::MIN)),
360            Included((commit_index, CommitDigest::MAX, BlockRef::MAX)),
361        )) {
362            let ((_, _, block_ref), _) = vote?;
363            votes.push(block_ref);
364        }
365        Ok(votes)
366    }
367
368    fn read_last_commit_info(&self) -> ConsensusResult<Option<(CommitRef, CommitInfo)>> {
369        let Some(result) = self
370            .commit_info
371            .reversed_safe_iter_with_bounds(None, None)?
372            .next()
373        else {
374            return Ok(None);
375        };
376        let (key, commit_info) = result.map_err(ConsensusError::RocksDBFailure)?;
377        Ok(Some((CommitRef::new(key.0, key.1), commit_info)))
378    }
379
380    fn read_last_finalized_commit(&self) -> ConsensusResult<Option<CommitRef>> {
381        let Some(result) = self
382            .finalized_commits
383            .reversed_safe_iter_with_bounds(None, None)?
384            .next()
385        else {
386            return Ok(None);
387        };
388        let ((index, digest), _) = result.map_err(ConsensusError::RocksDBFailure)?;
389        Ok(Some(CommitRef::new(index, digest)))
390    }
391
392    fn read_rejected_transactions(
393        &self,
394        commit_ref: CommitRef,
395    ) -> ConsensusResult<Option<BTreeMap<BlockRef, Vec<TransactionIndex>>>> {
396        let result = self
397            .finalized_commits
398            .get(&(commit_ref.index, commit_ref.digest))?;
399        Ok(result)
400    }
401}