Skip to main content

commonware_storage/qmdb/current/sync/
mod.rs

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