Skip to main content

commonware_storage/qmdb/current/sync/
mod.rs

1//! Shared synchronization logic for [crate::qmdb::current] databases.
2//!
3//! Contains the implementation of [crate::qmdb::sync::Database] for
4//! [Db](crate::qmdb::current::db::Db), covering every variant (ordered/unordered,
5//! fixed/variable, any snapshot index).
6//!
7//! The canonical root of a `current` database combines the ops root, grafted root, and optional
8//! pending and partial chunk digests into a single hash (see the [Root structure](super) section in
9//! the module documentation). The sync engine operates on the **ops root**, not the canonical root:
10//! it downloads operations and verifies each batch against the ops root using ops-tree range proofs
11//! (identical to `any` sync). Callers that verify current ops proofs directly should use
12//! [crate::qmdb::verify_proof]. [crate::qmdb::current::proof::OpsRootWitness] can be used by
13//! callers that need to authenticate the synced ops root against a trusted canonical root; the sync
14//! engine does not perform this check itself.
15//!
16//! After all operations are synced, the bitmap and grafted tree are reconstructed deterministically
17//! from the operations. The canonical root is then computed from the ops root, the reconstructed
18//! grafted root, and any pending or partial chunk digests.
19//!
20//! The [Database]`::`[root()](crate::qmdb::sync::Database::root) implementation returns the **ops
21//! root** (not the canonical root) because that is what the sync engine verifies against.
22//!
23//! For pruned databases (`range.start > 0`), grafted pinned nodes for the pruned region are read
24//! directly from the ops tree after it is built. This works because of the zero-chunk identity: for
25//! all-zero bitmap chunks (which all pruned chunks are), the grafted leaf equals the ops subtree
26//! root, making the grafted tree structurally identical to the ops tree at and above the grafting
27//! height.
28
29use crate::{
30    Context,
31    index::{Factory as IndexFactory, Unordered as UnorderedIndex},
32    journal::{authenticated, contiguous::Mutable},
33    merkle::{
34        Graftable, Location,
35        full::{self, Merkle},
36    },
37    qmdb::{
38        self,
39        any::{
40            db::Db as AnyDb,
41            operation::{Operation, update::Update},
42        },
43        bitmap::Shared,
44        current::{db, grafting},
45        metrics::Metrics as AnyMetrics,
46        sync::{Database, DatabaseConfig as Config, Request, source},
47    },
48    translator::Translator,
49};
50use commonware_codec::Codec;
51use commonware_cryptography::{DigestOf, Hasher};
52use commonware_parallel::Strategy;
53use commonware_runtime::Spawner;
54use commonware_utils::{bitmap::Prunable as BitMap, range::NonEmptyRange};
55use core::num::NonZeroUsize;
56use std::{num::NonZeroU64, sync::Arc};
57
58#[cfg(test)]
59pub(crate) mod tests;
60
61impl<T: Translator, J: Clone, S: Strategy, B> Config for super::Config<T, J, S, B> {
62    type JournalConfig = J;
63
64    fn journal_config(&self) -> Self::JournalConfig {
65        self.journal_config.clone()
66    }
67}
68
69/// Shared helper to build a `current::db::Db` from sync components.
70///
71/// This follows the same pattern as `any/sync/mod.rs::build_db` but additionally:
72/// * Builds the activity bitmap by replaying the operations log.
73/// * Extracts grafted pinned nodes from the ops tree (zero-chunk identity).
74/// * Builds the grafted tree from the bitmap and ops tree.
75/// * Computes and caches the canonical root.
76#[allow(clippy::too_many_arguments)]
77async fn build_db<F, E, U, I, H, J, const N: usize, S>(
78    context: E,
79    merkle_config: full::Config<S>,
80    log: J,
81    translator: I::Translator,
82    pinned_nodes: Option<Vec<H::Digest>>,
83    range: NonEmptyRange<Location<F>>,
84    apply_batch_size: NonZeroU64,
85    init_concurrency: <I as crate::qmdb::SnapshotBuild<F>>::Concurrency,
86    init_buffer: NonZeroUsize,
87    cache_size: Option<NonZeroUsize>,
88    metadata_partition: String,
89    strategy: S,
90) -> Result<db::Db<F, E, J, I, H, U, N, S>, qmdb::Error<F>>
91where
92    F: Graftable,
93    E: Context + Spawner,
94    U: Update,
95    I: IndexFactory + crate::qmdb::SnapshotBuild<F>,
96    H: Hasher,
97    J: Mutable<Item = Operation<F, U>> + 'static,
98    S: Strategy,
99    Operation<F, U>: Codec,
100{
101    // Build authenticated log.
102    let merkle = Merkle::<F, _, _, S>::init_sync(
103        context.child("merkle"),
104        full::SyncConfig {
105            config: merkle_config,
106            range: range.clone(),
107            pinned_nodes,
108        },
109    )
110    .await?;
111    let index = I::new(context.child("index"), translator);
112    let log = authenticated::Journal::<F, _, _, _, S>::from_components(
113        merkle,
114        log,
115        qmdb::hasher::<H>(),
116        apply_batch_size,
117    )
118    .await?;
119
120    // Initialize bitmap with pruned chunks.
121    //
122    // Floor division is intentional: chunks entirely below range.start are pruned.
123    // If range.start is not chunk-aligned, the partial leading chunk is reconstructed by
124    // init_from_log, which pads the gap between `pruned_chunks * CHUNK_SIZE_BITS` and the
125    // journal's inactivity floor with inactive (false) bits.
126    let pruned_chunks = (*range.start() / BitMap::<N>::CHUNK_SIZE_BITS) as usize;
127    let bitmap = BitMap::<N>::new_with_pruned_chunks(pruned_chunks)
128        .map_err(|_| qmdb::Error::<F>::DataCorrupted("pruned chunks overflow"))?;
129    let bitmap = Arc::new(Shared::<N>::new(bitmap));
130
131    // Build any::Db, handing it the pre-allocated bitmap. `init_from_log` populates the bitmap
132    // during replay.
133    let snapshot_context = context.child("any_snapshot");
134    let any_metrics = AnyMetrics::new(context.child("any"));
135    let any: AnyDb<F, E, J, I, H, U, N, S> = AnyDb::init_from_log(
136        snapshot_context,
137        index,
138        log,
139        Some(bitmap),
140        init_concurrency,
141        init_buffer,
142        cache_size,
143        any_metrics,
144    )
145    .await?;
146
147    // Fetch grafted pinned nodes from the ops tree. For each position the grafted family
148    // needs at its pruning boundary, source the digest from the ops tree via the zero-chunk
149    // identity: when the covered chunks are all zero (which pruned chunks always are), the
150    // ops-family digest at the mapped position equals the grafted digest.
151    //
152    // Requires `range.start <=` target's [`Db::sync_boundary`](db::Db::sync_boundary): that
153    // bound guarantees every required ops-tree node is born at `range.end`.
154    let grafted_pinned_nodes = {
155        let grafted_boundary = Location::<F>::new(pruned_chunks as u64);
156        let grafting_height = grafting::height::<N>();
157        let mut pinned_nodes = Vec::new();
158        for grafted_pos in F::nodes_to_pin(grafted_boundary) {
159            let ops_pos = grafting::grafted_to_ops_pos::<F>(grafted_pos, grafting_height);
160            let digest = any
161                .log
162                .merkle
163                .get_node(ops_pos)
164                .await?
165                .ok_or(qmdb::Error::<F>::DataCorrupted("missing ops pinned node"))?;
166            pinned_nodes.push(digest);
167        }
168        pinned_nodes
169    };
170
171    // Rebuild the grafted tree, graft roots, and canonical root from the synced `any` state.
172    // The canonical root is deterministic because the engine authenticates the ops and the
173    // bitmap is derived from them.
174    let (grafted_tree, graft_roots, root) = db::rebuild_grafted_tree::<F, H, S, N>(
175        any.bitmap.as_ref(),
176        &grafted_pinned_nodes,
177        &any.log.merkle,
178        any.inactivity_floor_loc,
179        any.root(),
180        &strategy,
181    )
182    .await?;
183
184    // Initialize metadata store and construct the Db.
185    let (metadata, _, _) =
186        db::init_metadata::<F, E, DigestOf<H>>(context.child("metadata"), &metadata_partition)
187            .await?;
188
189    let metrics = db::Metrics::new(context);
190    let current_db = db::Db {
191        any,
192        grafted_tree: Arc::new(grafted_tree),
193        graft_roots,
194        metadata,
195        strategy,
196        root,
197        metrics,
198        #[cfg(test)]
199        halt_before_prune_log: false,
200    };
201    current_db.update_metrics();
202
203    // Persist metadata so the db can be reopened with init_fixed/init_variable.
204    let current_db = current_db.sync_metadata().await?;
205
206    Ok(current_db)
207}
208
209impl<F, E, C, I, H, U, const N: usize, S> Database for db::Db<F, E, C, I, H, U, N, S>
210where
211    F: Graftable,
212    E: Context + Spawner,
213    C: Mutable<Item = Operation<F, U>>
214        + crate::qmdb::sync::Journal<F, Context = E, Op = Operation<F, U>>
215        + 'static,
216    I: IndexFactory + crate::qmdb::SnapshotBuild<F> + UnorderedIndex<Value = Location<F>>,
217    H: Hasher,
218    U: Update,
219    S: Strategy,
220    Operation<F, U>: Codec,
221{
222    type Family = F;
223    type Context = E;
224    type Op = Operation<F, U>;
225    type Journal = C;
226    type Hasher = H;
227    type Config = super::Config<
228        I::Translator,
229        <C as crate::qmdb::sync::Journal<F>>::Config,
230        S,
231        <I as crate::qmdb::SnapshotBuild<F>>::Concurrency,
232    >;
233    type Digest = H::Digest;
234
235    async fn from_sync_result(
236        context: Self::Context,
237        config: Self::Config,
238        log: Self::Journal,
239        pinned_nodes: Option<Vec<Self::Digest>>,
240        range: NonEmptyRange<Location<F>>,
241        apply_batch_size: NonZeroU64,
242    ) -> Result<Self, qmdb::Error<F>> {
243        let strategy = config.merkle_config.strategy.clone();
244        build_db::<F, _, U, _, H, _, N, _>(
245            context,
246            config.merkle_config,
247            log,
248            config.translator,
249            pinned_nodes,
250            range,
251            apply_batch_size,
252            config.init_concurrency,
253            config.init_buffer,
254            config.init_cache,
255            config.grafted_metadata_partition,
256            strategy,
257        )
258        .await
259    }
260
261    async fn persist_sync_result(self) -> Result<Self, qmdb::Error<F>> {
262        Ok(self)
263    }
264
265    async fn local_pinned_nodes(
266        context: Self::Context,
267        config: &Self::Config,
268        target: &qmdb::sync::Target<Self::Family, Self::Digest>,
269        journal: &Self::Journal,
270    ) -> Result<Option<Vec<Self::Digest>>, qmdb::Error<F>> {
271        if target.range.start() == Location::new(0)
272            || !qmdb::sync::journal_covers_range(journal.bounds(), &target.range)
273        {
274            return Ok(None);
275        }
276
277        // The inactivity floor is carried by the last commit operation rather than
278        // being the target range's start.
279        let inactivity_floor =
280            qmdb::find_inactivity_floor_at::<F, _>(journal, target.range.end()).await?;
281
282        qmdb::sync::local_pinned_nodes::<F, _, H, S>(
283            context,
284            config.merkle_config.clone(),
285            target,
286            inactivity_floor,
287        )
288        .await
289    }
290
291    /// Returns the ops root (not the canonical root), since the sync engine verifies
292    /// batches against the ops tree.
293    fn root(&self) -> Self::Digest {
294        self.any.root()
295    }
296}
297
298/// A `current` database serves proofs from the `any` database it wraps. The sync engine
299/// operates on the ops root, which is `any`'s root.
300impl<F, E, C, I, H, U, const N: usize, S> crate::qmdb::sync::Source
301    for db::Db<F, E, C, I, H, U, N, S>
302where
303    F: Graftable,
304    E: Context,
305    C: Mutable<Item = Operation<F, U>>,
306    I: UnorderedIndex<Value = Location<F>>,
307    H: Hasher,
308    U: Update,
309    S: Strategy,
310    Operation<F, U>: Codec,
311{
312    type Family = F;
313    type Digest = H::Digest;
314    type Op = Operation<F, U>;
315    type Error = qmdb::Error<F>;
316
317    async fn serve(&self, request: Request<F>) -> source::Result<Self> {
318        self.any.serve(request).await
319    }
320}